feat Make skill portable, tested, and STE100-clean

Rework the FRACAS skill so it captures, tracks, and closes requests end to end, runs on any dev setup, and passes the skill validator with STE100 wording. Tools: - Discover the project root with locate-dominating-file, so no hardcoded home path remains. Root falls back from the tool file directory to default-directory. - Make the MCP framework a soft dependency with plain function stubs, so the handlers load and run under bare batch Emacs. - Fix the JSON sentinels: nil for null and :json-false for false. The earlier :null and :false became the strings "null" and "false", and the latter was truthy to every consumer. - Add optional model and notes fields to set-result and complete, to match the result-line convention already in the stream. - Restructure the tag vocabulary into a four-axis alist (kind, scope, impact, misc) and rename trivial to minor. Docs: - Restructure SKILL.md to the required Overview, Usage, and Core Concepts sections, add references/tools.md for progressive disclosure, and rewrite all prose to ASD-STE100. Tests: - Add fracas-tests.el (24 ERT tests) and run-tests.sh. The suite needs no MCP framework and uses a temporary stream file per test. Validator: 34 checks pass, no errors or warnings. Tests: 24 pass.

Commit
a55b61481bcf26c2572a02e762e7341b19256df6
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
.kiro/skills/fracas/SKILL.md
index 5744f00d..afb4cd02 100644..100644
@@ -5,246 +5,346 @@
5 5
6 6 # FRACAS — Feature Requests As a Constant Agent-monitored Stream
7 7
8 Removed: Use this skill when the user asks to capture feature requests or to
9 Removed: track a feature in `REQUESTS.org`.
8 Added: ## Overview
10 9
11 Removed: `REQUESTS.org` is a flat stream. Each request is a top-level heading.
12 Removed: The newest request comes first. There are no container or topical
13 Removed: headings. Scope, impact, and kind are expressed as tags on each
14 Removed: heading, not by position.
10 Added: FRACAS keeps every feature request in one Org file, `REQUESTS.org`.
11 Added: Dedicated Emacs tools write that file. The work follows three steps:
15 12
16 Removed: Keep the session open across prompts. Treat each prompt as a separate
17 Removed: feature request. Do not merge requests. Do not build, test, commit, or
18 Removed: start an agent.
13 Added: 1. Capture the request at the top of the stream.
14 Added: 2. Track its status while the work runs.
15 Added: 3. Close it with commit evidence and test evidence.
19 16
17 Added: ## Usage
18 Added:
19 Added: Use this skill when the user does one of these things:
20 Added:
21 Added: - The user states a feature request, a defect, or a change to make.
22 Added: - The user asks you to record or capture a request.
23 Added: - The user asks for the status of a request, or asks to change it.
24 Added: - The user asks you to close, cancel, or block a request.
25 Added: - The user asks what requests are open.
26 Added:
27 Added: Treat each prompt as one separate feature request. Do not merge two
28 Added: requests. Do not build, test, commit, or start an agent for a capture.
29 Added:
20 30 The current prompt is the feature request:
21 31
22 32 $ARGUMENTS
23 33
24 Removed: If the prompt is empty, ask for one line that describes the feature
34 Added: If the prompt is empty, ask the user for one line that describes the
25 35 request. Then stop. Capture the next prompt as the next request.
26 36
27 Removed: ## Use the dedicated FRACAS MCP tools
37 Added: ## Core Concepts
28 38
29 Removed: This skill provides dedicated Emacs Model Context Protocol (MCP)
30 Removed: tools. Use them for every interaction with `REQUESTS.org`. Do not edit
31 Removed: the file as raw text. Do not use shell or file tools to capture,
32 Removed: change status, log, or verify an entry.
39 Added: ### The stream
33 40
34 Removed: The dedicated tools avoid a security prompt. The generic `eval-elisp`
35 Removed: tool passes its code through the Emacs security form walker. The
36 Removed: walker prompts for each file function, such as `find-file-noselect`
37 Removed: and `save-buffer`. The FRACAS tools register as normal MCP tools. They
38 Removed: run through the tool dispatch path, which does not use the walker. The
39 Removed: read-only tools also carry a `readOnlyHint` annotation. The MCP client
40 Removed: can approve a read-only tool without a prompt.
41 Added: `REQUESTS.org` is a flat stream. Each request is a top-level heading.
42 Added: The newest request comes first. The file holds no container heading and
43 Added: no topical heading. Tags on the heading give the kind, the scope, and
44 Added: the impact. The position of a heading gives only its age.
41 45
42 Removed: Each FRACAS tool takes a `root` argument. Set `root` to the absolute
43 Removed: path of the project directory that contains `REQUESTS.org`. The tools
44 Removed: operate only on that one file.
46 Added: ### The Org ID is the handle
45 47
46 Removed: ### Install the tools once per session
48 Added: Every request carries an Org ID. Each tool that changes an entry needs
49 Added: that ID. Never select an entry by its title text, because two titles
50 Added: can match. Run `fracas-list` to get the ID of each entry.
47 51
48 Removed: Load the tool file one time in the running Emacs before you use the
49 Removed: tools. Run this with the Emacs MCP `eval-elisp` tool:
52 Added: ### The file owns the status sequence
50 53
51 Removed: ```elisp
52 Removed: (load "/home/blendux/git/hito/.kiro/skills/fracas/fracas-tools.el" nil t)
53 Removed: ```
54 Added: The `#+TODO:` line in the file preamble declares the keyword sequence.
55 Added: The tools read that line. The tools never declare their own keywords.
56 Added: To change the keywords, edit the `#+TODO:` line.
54 57
55 Removed: Confirm the tools are present. This expression returns the thirteen
56 Removed: FRACAS tool names:
58 Added: ### Evidence, not silence
57 59
58 Removed: ```elisp
59 Removed: (seq-filter (lambda (name) (string-prefix-p "fracas-" name))
60 Removed: (mapcar (lambda (tool) (alist-get 'name tool))
61 Removed: (mcp-server-tools-list)))
62 Removed: ```
60 Added: Three transitions demand a written reason or written evidence:
63 61
64 Removed: If the load fails, stop and report the problem. Do not fall back to
65 Removed: raw file edits.
62 Added: - `BLOCKED` needs a reason.
63 Added: - `CANCELLED` needs a reason.
64 Added: - `DONE` needs a commit hash and a test recap.
66 65
67 Removed: ### The FRACAS tools
66 Added: The tools refuse these transitions without that text. A stalled request
67 Added: or a dropped request therefore always states why.
68 68
69 Removed: - `fracas-inspect` — return the file's TODO keyword sequence and the
70 Removed: closed tag vocabulary. Read-only.
71 Removed: - `fracas-list` — list the top-level request entries, newest first,
72 Removed: with each entry's Org ID, title, state, `SCHEDULED` time, and tags.
73 Removed: Takes an optional `state` or `tag` filter. Read-only.
74 Removed: - `fracas-capture` — create a TODO request as a top-level heading at
75 Removed: the top of the stream. Applies tags, records the capture time as an
76 Removed: inactive `SCHEDULED` timestamp, wraps the body to 72 columns, and
77 Removed: assigns an Org ID.
78 Removed: - `fracas-set-status` — set the TODO keyword of an entry. Rejects
79 Removed: `DONE`.
80 Removed: - `fracas-cancel` — set an entry to `CANCELLED` with a required
81 Removed: reason.
82 Removed: - `fracas-block` — set an entry to `BLOCKED` with a required reason.
83 Removed: - `fracas-check` — manage a request's checkbox list to split a complex
84 Removed: task into sub-tasks. Add an item, toggle one, or list them. A `[/]`
85 Removed: cookie tracks progress.
86 Removed: - `fracas-log` — append a timestamped progress note to an entry's
87 Removed: `:LOGBOOK:` drawer. Append-only.
88 Removed: - `fracas-retag` — replace an entry's tags with a validated set from
89 Removed: the closed vocabulary.
90 Removed: - `fracas-set-result` — write the structured `- result ::` line
91 Removed: (commit hash and test recap) for an entry.
92 Removed: - `fracas-complete` — set an entry to `DONE`. Records the result
93 Removed: evidence, then confirms Org inserted the `CLOSED` timestamp. Never
94 Removed: writes the timestamp itself.
95 Removed: - `fracas-show` — return the full content of an entry: heading, state,
96 Removed: tags, timestamps, body, logbook, checklist, and result. Read-only.
97 Removed: - `fracas-verify` — return the heading, TODO state, `CLOSED`
98 Removed: timestamp, and tags of an entry. Read-only.
69 Added: ### Which tool sets which state
99 70
100 Removed: Use `fracas-show` to read an entry's full content and `fracas-retag`
101 Removed: to re-tag it. These are the sanctioned tools for those actions,
102 Removed: because the Emacs MCP `org-*` tools cannot operate on this file.
71 Added: Pick the tool from the target state:
103 72
104 Removed: ## The tag vocabulary
73 Added: - Target `IN-PROGRESS`, `TESTING`, or `TESTED` → `fracas-set-status`
74 Added: - Target `BLOCKED` → `fracas-block`, with a reason
75 Added: - Target `CANCELLED` → `fracas-cancel`, with a reason
76 Added: - Target `DONE` → `fracas-complete`, with commit and test evidence
105 77
106 Removed: Every request carries one kind tag and one scope tag. An impact tag is
107 Removed: optional and marks an exception. Add the `doctrine` flag when the
108 Removed: request touches or deviates from Heavy Duty doctrine. The vocabulary is
109 Removed: closed; `fracas-capture` rejects any other tag. The tags are short and
110 Removed: bare, with no axis prefix. The three axes use distinct words, so a
111 Removed: bare tag stays unambiguous.
78 Added: ### The tag vocabulary
112 79
113 Removed: Kind — the change type. Choose one:
80 Added: Every request carries one kind tag and one scope tag. The impact axis
81 Added: and the misc axis are optional. The vocabulary is closed, so
82 Added: `fracas-capture` rejects any other tag. The tools hold the vocabulary as
83 Added: an alist keyed by axis. `fracas-inspect` returns the same grouping. The
84 Added: tags are short and bare, with no axis prefix. The four axes use distinct
85 Added: words, so a bare tag stays unambiguous.
114 86
87 Added: Kind — the type of change. Choose one:
88 Added:
115 89 - `feat` — a new capability.
116 90 - `fix` — a defect repair.
117 Removed: - `refactor` — a change that keeps behaviour.
91 Added: - `refactor` — a change that keeps the behavior.
118 92 - `chore` — maintenance or tooling.
119 93 - `docs` — documentation only.
120 94
121 Removed: Scope — the layer the change touches. Choose one:
95 Added: Scope — the layer that the change touches. Choose one:
122 96
123 97 - `core` — pure domain (`hito.core`).
124 Removed: - `app` — service, ports, adapters (`hito.app`).
98 Added: - `app` — service, ports, and adapters (`hito.app`).
125 99 - `web` — Dream and dream-html (`hito.web`).
126 Removed: - `ui` — visual style, layout, interaction.
100 Added: - `ui` — visual style, layout, and interaction.
127 101
128 Removed: Impact — an optional exception flag. Add one only when it applies:
102 Added: Impact — optional. The two tags are opposite ends of one scale, so
103 Added: choose at most one:
129 104
130 Removed: - `trivial` — a trivial change, such as a cosmetic or one-line fix.
131 Removed: - `major` — a potential major rework.
105 Added: - `minor` — a small change, such as a cosmetic fix or a one-line fix.
106 Added: - `major` — a possible major rework.
132 107
133 Removed: Leave the impact tag off for an ordinary request.
108 Added: Leave the impact axis empty for an ordinary request.
134 109
135 Removed: Infer the tags from the request text at capture time. You may re-tag
136 Removed: an entry later, with `fracas-retag`, as the feature shape changes.
110 Added: Misc — optional flags. Add one only when it applies:
137 111
138 Removed: ## Step 1 — Capture at the top of the stream
112 Added: - `doctrine` — the request touches Heavy Duty doctrine, or deviates
113 Added: from it.
139 114
140 Removed: Run `fracas-inspect` first. Read the TODO keyword sequence and the tag
141 Removed: vocabulary.
115 Added: Infer the tags from the request text when you capture it. You can
116 Added: re-tag an entry later with `fracas-retag`, as the shape of the feature
117 Added: changes.
142 118
119 Added: ## Install the tools once per session
120 Added:
121 Added: The tools live in `fracas-tools.el`, beside this file. Load that file
122 Added: one time in the running Emacs. Use the generic `eval-elisp` tool for
123 Added: the load only.
124 Added:
125 Added: Pass the project directory of your own session in `project`. Do not
126 Added: write an absolute path from a home directory into the form, because that
127 Added: path differs on every machine.
128 Added:
129 Added: ```elisp
130 Added: (let* ((project "/the/project/directory/you/work/in")
131 Added: (tools (expand-file-name ".kiro/skills/fracas/fracas-tools.el"
132 Added: project)))
133 Added: (unless (file-readable-p tools)
134 Added: (error "No FRACAS tools at %s" tools))
135 Added: (load tools nil t))
136 Added: ```
137 Added:
138 Added: The tool file then finds the project root itself. It searches upward
139 Added: from its own directory for `REQUESTS.org`. The search assumes no
140 Added: directory layout, so the same file works on every machine. The result
141 Added: becomes `fracas-project-root`, and every tool uses it by default.
142 Added:
143 Added: Do not depend on the Emacs `default-directory`. That directory often
144 Added: sits outside the project, and the search then finds no stream file.
145 Added:
146 Added: Then verify that the tools are present. This expression returns the
147 Added: thirteen tool names:
148 Added:
149 Added: ```elisp
150 Added: (seq-filter (lambda (name) (string-prefix-p "fracas-" name))
151 Added: (mcp-server-tools-list-names))
152 Added: ```
153 Added:
154 Added: If the load fails, stop and report the problem. Do not edit
155 Added: `REQUESTS.org` as raw text instead.
156 Added:
157 Added: ### If the tools are not callable
158 Added:
159 Added: The tools register inside Emacs. The MCP client lists its tools when it
160 Added: connects, so a mid-session load can leave the tools absent from your own
161 Added: tool list. In that case, call each tool through the dispatch path:
162 Added:
163 Added: ```elisp
164 Added: (mcp-server-tools-call "fracas-list" '((state . "TODO")))
165 Added: ```
166 Added:
167 Added: This path runs the same handler as a direct tool call. To make the tools
168 Added: callable directly, load `fracas-tools.el` from the Emacs init file.
169 Added:
170 Added: ### Why dedicated tools
171 Added:
172 Added: The generic `eval-elisp` tool sends its code through the Emacs security
173 Added: form walker. The walker prompts for each file function, such as
174 Added: `find-file-noselect` and `save-buffer`. The FRACAS tools register as
175 Added: normal MCP tools, and tool dispatch does not use the walker. The
176 Added: read-only tools also carry a `readOnlyHint` annotation, so the client
177 Added: can approve them without a prompt.
178 Added:
179 Added: You **MUST** use these tools for every change to `REQUESTS.org`.
180 Added:
181 Added: - You **MUST NOT** edit the file as raw text, because raw edits break
182 Added: the timestamps, the IDs, and the tag alignment.
183 Added: - You **MUST NOT** use a shell tool or a file tool for an entry, because
184 Added: those tools apply no validation.
185 Added:
186 Added: ## The FRACAS tools
187 Added:
188 Added: Each tool accepts an optional `root` argument. `root` names the project
189 Added: directory. When you omit it, the tools use `fracas-project-root`. Set
190 Added: `root` only for a different project.
191 Added:
192 Added: | Tool | Purpose | Read-only |
193 Added: |---|---|---|
194 Added: | `fracas-inspect` | Report the keyword sequence and the tag axes. | Yes |
195 Added: | `fracas-list` | List entries, newest first, with ID, title, state, and tags. Accepts a `state` or `tag` filter. | Yes |
196 Added: | `fracas-show` | Report one entry in full: body, logbook, checklist, and result. | Yes |
197 Added: | `fracas-verify` | Report the heading, state, `CLOSED` time, and tags of one entry. | Yes |
198 Added: | `fracas-capture` | Create a `TODO` request at the top of the stream. | No |
199 Added: | `fracas-set-status` | Set the keyword of an entry. Refuses `DONE`. | No |
200 Added: | `fracas-block` | Set `BLOCKED` and record a required reason. | No |
201 Added: | `fracas-cancel` | Set `CANCELLED` and record a required reason. | No |
202 Added: | `fracas-check` | Add, toggle, or list the checklist items of one request. | No |
203 Added: | `fracas-log` | Append a timestamped note to the `:LOGBOOK:` drawer. | No |
204 Added: | `fracas-retag` | Replace the tags of an entry with a validated set. | No |
205 Added: | `fracas-set-result` | Write the `- result ::` line. | No |
206 Added: | `fracas-complete` | Set `DONE` after it records the result evidence. | No |
207 Added:
208 Added: Use `fracas-show` to read one entry, and `fracas-retag` to re-tag it.
209 Added: These are the correct tools for those two actions, because the generic
210 Added: Emacs `org-*` tools cannot operate on this file.
211 Added:
212 Added: For the exact arguments and the result shape of each tool, read
213 Added: `references/tools.md`.
214 Added:
215 Added: ## Step 1 — Capture the request
216 Added:
143 217 Run `fracas-capture` with these arguments:
144 218
145 Removed: - `root` — the project directory.
146 219 - `title` — a short imperative title under 60 characters.
147 220 - `body` — the full request text.
148 Removed: - `tags` — the inferred tags: one kind, one scope, an optional impact
149 Removed: (`trivial` or `major`), plus optional `doctrine`.
221 Added: - `tags` — one kind tag, one scope tag, and any optional tag.
150 222
151 Removed: The tool inserts the entry as a top-level heading at the top of the
152 Removed: file, so the newest request is first. It records the capture time as
153 Removed: an inactive `SCHEDULED` timestamp, applies the tags, wraps the body to
154 Removed: 72 columns, and returns the new entry's Org ID. Report the Org ID.
223 Added: The tool inserts the entry at the top of the file. It records the
224 Added: capture time as an inactive `SCHEDULED` timestamp. It applies the tags,
225 Added: wraps the body to 72 columns, and assigns an Org ID. Report that ID to
226 Added: the user.
155 227
156 228 Keep the request text unchanged. If the text holds separate ideas,
157 Removed: format the body as a list. Pass the body as plain text; do not
158 Removed: pre-wrap it.
229 Added: write the body as a list. Pass the body as plain text, because the tool
230 Added: wraps it.
159 231
160 Removed: ## Step 2 — Track the status
232 Added: If `fracas-capture` rejects a tag, run `fracas-inspect` to read the
233 Added: vocabulary. Then fix the tag and re-run the capture.
161 234
162 Removed: Find the entry first. Run `fracas-list` to get each entry's Org ID,
163 Removed: title, state, and tags. Pass a `state` or `tag` filter to narrow the
164 Removed: list, for example `IN-PROGRESS`. Use the Org ID for every later call.
165 Removed: Do not match an entry by its title text.
235 Added: Then proceed to Step 2 when work starts on the request.
166 236
167 Removed: Read the current status with `fracas-verify` before you change a
168 Removed: keyword. Use one keyword from the sequence that `fracas-inspect`
169 Removed: returned.
237 Added: ## Step 2 — Track the status
170 238
171 Removed: Run `fracas-set-status` for a status change to any open keyword:
239 Added: First find the entry. Run `fracas-list` to read the ID, the title, the
240 Added: state, and the tags of each entry. Pass a `state` filter or a `tag`
241 Added: filter to narrow the list. Use the ID for every later call.
172 242
173 Removed: - `TODO` — Capture the request. Do not start work.
174 Removed: - `IN-PROGRESS` — Start work.
175 Removed: - `TESTING` — Run tests. The tests do not pass.
176 Removed: - `TESTED` — Tests pass. No commit exists.
177 Removed: - `BLOCKED` — Work cannot continue. Use `fracas-block` with a reason.
178 Removed: - `CANCELLED` — Stop work on purpose. Use `fracas-cancel` with a
179 Removed: reason.
243 Added: Read the current state with `fracas-verify` before you change a
244 Added: keyword. Then run `fracas-set-status` with one open keyword:
180 245
181 Removed: Change the keyword only after you observe the current status. Run
182 Removed: `fracas-verify` after the change to confirm the keyword.
246 Added: - `TODO` — the request is captured. Work has not started.
247 Added: - `IN-PROGRESS` — work has started.
248 Added: - `TESTING` — the tests run. The tests do not pass.
249 Added: - `TESTED` — the tests pass. No commit exists.
183 250
184 Removed: #### Cancel or block with a reason
251 Added: Run `fracas-verify` again after the change to confirm the keyword.
185 252
186 Removed: Record a required reason when you cancel or block a request, so the
187 Removed: decision is never silent. Use `fracas-cancel` for the `CANCELLED`
188 Removed: transition and `fracas-block` for the `BLOCKED` transition. Do not use
189 Removed: `fracas-set-status` for these two transitions. Each tool needs a
190 Removed: `reason` and writes it to the entry.
253 Added: ### Block or cancel with a reason
191 254
192 Removed: The file owns the TODO sequence. The `#+TODO:` line in the file
193 Removed: preamble declares it. The tools read that sequence; they never define
194 Removed: their own. To change the keywords, edit the `#+TODO:` line.
255 Added: Run `fracas-block` when work cannot continue. Run `fracas-cancel` when
256 Added: you stop work on purpose. Each tool needs a `reason`, and writes it to
257 Added: the entry.
195 258
196 Removed: ### Split a complex task with a checklist
259 Added: You **MUST NOT** use `fracas-set-status` here, because it records no reason.
197 260
198 Removed: Run `fracas-check` to break one request into sub-tasks that share the
199 Removed: request. Use it when a request is large enough to track in parts, but
200 Removed: not large enough to become its own stream entries.
261 Added: ### Split a large request with a checklist
201 262
263 Added: Run `fracas-check` when one request is large enough to track in parts:
264 Added:
202 265 - `action` `add` — append an unchecked item. Needs `item`.
203 Removed: - `action` `toggle` — flip a matching item done or undone. Needs
204 Removed: `item`.
205 Removed: - `action` `list` — return the items and their state.
266 Added: - `action` `toggle` — flip one matching item. Needs `item`.
267 Added: - `action` `list` — report the items and their state.
206 268
207 Removed: The tool keeps the items in a `Checklist [/]:` block in the request
208 Removed: body. The `[/]` cookie tracks completed items against the total. The
209 Removed: items stay inside the one request; they are not separate stream
210 Removed: entries and they have no independent TODO keyword. For genuinely
211 Removed: independent work, capture a new request instead.
269 Added: The tool keeps the items in a `Checklist [/]:` block in the body. The
270 Added: `[/]` cookie counts the completed items against the total. The items
271 Added: stay inside the one request. They are not separate entries, and they
272 Added: carry no keyword of their own. For independent work, capture a new
273 Added: request instead.
212 274
213 275 ### Record progress in the logbook
214 276
215 Removed: Run `fracas-log` to add a progress note during an agentic session:
277 Added: Run `fracas-log` with a `note` to record progress during a session. The
278 Added: tool prepends a timestamped item to the `:LOGBOOK:` drawer. The logbook
279 Added: is append-only. The tool never rewrites an earlier note, and never
280 Added: rewrites the body.
216 281
217 Removed: - `root` — the project directory.
218 Removed: - `id` — the Org ID of the entry.
219 Removed: - `note` — the progress note.
282 Added: Then proceed to Step 3 when every closing criterion is met.
220 283
221 Removed: The tool appends a timestamped item to the entry's `:LOGBOOK:`
222 Removed: drawer. The logbook is append-only. It never rewrites an earlier note
223 Removed: or the request body.
284 Added: ## Step 3 — Close the request
224 285
225 Removed: ### Complete with result evidence
286 Added: Check each criterion before you close a request. Copy this list and mark
287 Added: each item:
226 288
227 Removed: For `DONE`, use `fracas-complete`, not `fracas-set-status`. Set `DONE`
228 Removed: only when all of these are true:
289 Added: - [ ] The build passes.
290 Added: - [ ] The tests pass.
291 Added: - [ ] The files have the required format.
292 Added: - [ ] A commit exists.
229 293
230 Removed: - The build passes.
231 Removed: - Tests pass.
232 Removed: - The files have the required format.
233 Removed: - A commit exists.
294 Added: If one criterion fails, do not close the request. Set the state with
295 Added: `fracas-set-status` instead. Then fix the failure and re-run this
296 Added: check.
234 297
235 Removed: Run `fracas-complete` with these arguments:
298 Added: When every item is marked, run `fracas-complete` with these arguments:
236 299
237 Removed: - `root` — the project directory.
238 300 - `id` — the Org ID of the entry.
239 Removed: - `commit` — the commit hash for the completed work.
301 Added: - `commit` — the commit hash of the completed work.
240 302 - `tests` — a short test recap, for example `215 pass`.
303 Added: - `model` — optional. The agent that did the work.
304 Added: - `notes` — optional. A short tail, such as a root cause.
241 305
242 Removed: The tool writes a `- result :: commit=… tests=…` line, sets `DONE`,
243 Removed: lets Org's configured logging add the `CLOSED` timestamp, and confirms
244 Removed: the timestamp. Do not add or edit the timestamp yourself. The entry
245 Removed: stays where it is in the stream; there is no refile step. Use
246 Removed: `fracas-set-result` on its own to record the result line before
247 Removed: completion.
306 Added: The tool writes the result line, sets `DONE`, then confirms that Org
307 Added: recorded the `CLOSED` timestamp. Org writes that timestamp, because the
308 Added: file sets `org-log-done` to `time`.
248 309
249 Removed: Record each prompt as its own request. If you cannot complete a
250 Removed: request, record the request and the failure.
310 Added: You **MUST NOT** write or edit that timestamp, because Org owns it.
311 Added:
312 Added: The result line takes this form. It stays on one line, because the
313 Added: reader reads one line:
314 Added:
315 Added: ```org
316 Added: - result :: model=example-agent commit=3051af0 tests=243 pass; short root cause
317 Added: ```
318 Added:
319 Added: The entry stays in place in the stream. There is no refile step.
320 Added:
321 Added: Run `fracas-verify` last. Verify that the state is `DONE` and that the
322 Added: `CLOSED` timestamp is present. To write the result line before you close
323 Added: the request, run `fracas-set-result` on its own.
324 Added:
325 Added: ## Test the tools
326 Added:
327 Added: The tools carry a test suite. Run it after any change to
328 Added: `fracas-tools.el`:
329 Added:
330 Added: ```sh
331 Added: .kiro/skills/fracas/run-tests.sh
332 Added: ```
333 Added:
334 Added: The suite needs no MCP framework and no configuration. Each test uses a
335 Added: temporary stream file, so the project stream file stays unchanged.
336 Added:
337 Added: ## Common mistakes
338 Added:
339 Added: - **Editing the file as raw text.** The tools keep the timestamps, the
340 Added: IDs, and the tag alignment correct. Raw edits do not.
341 Added: - **Selecting an entry by title.** Two titles can match. Use the Org ID
342 Added: from `fracas-list`.
343 Added: - **Using `fracas-set-status` for `DONE`.** That call fails by design.
344 Added: Use `fracas-complete`, which demands the evidence.
345 Added: - **Writing a `CLOSED` timestamp.** Org writes it. The tool only
346 Added: confirms it.
347 Added: - **Merging two requests.** Record each prompt as its own request.
348 Added:
349 Added: If you cannot complete a request, record the request and record the
350 Added: failure.
.kiro/skills/fracas/fracas-tests.el
index 00000000..7639cf2c 000000..100644
@@ -0,0 +1,446 @@
1 Added: ;;; fracas-tests.el --- End-to-end tests for the FRACAS tools -*- lexical-binding: t; -*-
2 Added:
3 Added: ;;; Commentary:
4 Added:
5 Added: ;; These tests exercise the FRACAS handler functions directly. The MCP
6 Added: ;; framework calls each handler with one alist of arguments, so a direct call
7 Added: ;; follows the same path as a tool call. Each test runs against a temporary
8 Added: ;; stream file, so no test touches the project stream file.
9 Added: ;;
10 Added: ;; Run the tests with the runner script in this directory:
11 Added: ;;
12 Added: ;; ./run-tests.sh
13 Added:
14 Added: ;;; Code:
15 Added:
16 Added: (require 'ert)
17 Added: (require 'json)
18 Added:
19 Added: ;;; Fixtures
20 Added:
21 Added: (defvar fracas-tests--preamble
22 Added: (concat "#+TITLE: Test stream\n"
23 Added: "#+TODO: TODO IN-PROGRESS TESTING TESTED BLOCKED | CANCELLED DONE\n"
24 Added: "\n"
25 Added: "# Local Variables:\n"
26 Added: "# org-log-done: time\n"
27 Added: "# End:\n")
28 Added: "Preamble of the temporary stream file.
29 Added: The `#+TODO:' line gives the keyword sequence. The local variable
30 Added: `org-log-done' makes Org write a CLOSED timestamp.")
31 Added:
32 Added: (defun fracas-tests--decode (json-string)
33 Added: "Return JSON-STRING decoded into Lisp with alists for objects.
34 Added: Decode JSON null to nil and JSON false to `:json-false', which are the
35 Added: same sentinels that the handlers encode from. A round trip therefore
36 Added: gives back the value that the handler started with."
37 Added: (json-parse-string json-string
38 Added: :object-type 'alist
39 Added: :null-object nil
40 Added: :false-object :json-false))
41 Added:
42 Added: (defun fracas-tests--call (handler args)
43 Added: "Call HANDLER with ARGS and return the decoded result.
44 Added: Signal an error when the handler reports one, so a failure is visible."
45 Added: (let ((result (fracas-tests--decode (funcall handler args))))
46 Added: (when (alist-get 'error result)
47 Added: (error "Handler failed: %s" (alist-get 'error result)))
48 Added: result))
49 Added:
50 Added: (defmacro fracas-tests--with-stream (root &rest body)
51 Added: "Create a temporary project directory, bind ROOT to it, then run BODY.
52 Added: Delete the directory and its buffers after BODY."
53 Added: (declare (indent 1))
54 Added: `(let* ((,root (file-name-as-directory (make-temp-file "fracas-test" t)))
55 Added: (file (expand-file-name fracas-requests-file-name ,root))
56 Added: (enable-local-variables :all)
57 Added: (org-id-track-globally nil)
58 Added: (create-lockfiles nil))
59 Added: (unwind-protect
60 Added: (progn
61 Added: (with-temp-file file (insert fracas-tests--preamble))
62 Added: ,@body)
63 Added: (dolist (buf (buffer-list))
64 Added: (when (and (buffer-file-name buf)
65 Added: (string-prefix-p ,root (buffer-file-name buf)))
66 Added: (with-current-buffer buf (set-buffer-modified-p nil))
67 Added: (kill-buffer buf)))
68 Added: (delete-directory ,root t))))
69 Added:
70 Added: (defun fracas-tests--capture (root title tags &optional body)
71 Added: "Capture a request in ROOT with TITLE, TAGS, and optional BODY.
72 Added: Return the new entry's Org ID."
73 Added: (alist-get 'id (fracas-tests--call
74 Added: #'fracas-tools--capture-handler
75 Added: (list (cons 'root root)
76 Added: (cons 'title title)
77 Added: (cons 'tags tags)
78 Added: (cons 'body (or body "Request body."))))))
79 Added:
80 Added: (defun fracas-tests--file-text (root)
81 Added: "Return the text of the stream file in ROOT."
82 Added: (with-temp-buffer
83 Added: (insert-file-contents (expand-file-name fracas-requests-file-name root))
84 Added: (buffer-string)))
85 Added:
86 Added: ;;; Root discovery
87 Added:
88 Added: (ert-deftest fracas-test-locate-root-finds-the-stream-file ()
89 Added: "`fracas-locate-root' finds the root from a nested directory."
90 Added: (fracas-tests--with-stream root
91 Added: (let ((nested (expand-file-name "a/b/c/" root)))
92 Added: (make-directory nested t)
93 Added: (should (equal (fracas-locate-root nested) root)))))
94 Added:
95 Added: (ert-deftest fracas-test-locate-root-returns-nil-without-a-stream-file ()
96 Added: "`fracas-locate-root' returns nil when no ancestor holds the file."
97 Added: (let ((empty (file-name-as-directory (make-temp-file "fracas-empty" t))))
98 Added: (unwind-protect
99 Added: (should (null (fracas-locate-root empty)))
100 Added: (delete-directory empty t))))
101 Added:
102 Added: (ert-deftest fracas-test-root-argument-overrides-the-default ()
103 Added: "An explicit `root' argument selects the file, not `fracas-project-root'."
104 Added: (fracas-tests--with-stream root
105 Added: (should (equal (fracas-tools--file (list (cons 'root root)))
106 Added: (expand-file-name fracas-requests-file-name root)))))
107 Added:
108 Added: (ert-deftest fracas-test-absent-root-uses-the-default ()
109 Added: "An absent `root' argument falls back to `fracas-project-root'."
110 Added: (fracas-tests--with-stream root
111 Added: (let ((fracas-project-root root))
112 Added: (should (equal (fracas-tools--file nil)
113 Added: (expand-file-name fracas-requests-file-name root))))))
114 Added:
115 Added: (ert-deftest fracas-test-a-missing-directory-signals-an-error ()
116 Added: "A `root' that is not a directory signals an error."
117 Added: (should-error (fracas-tools--file
118 Added: (list (cons 'root "/fracas/no/such/directory")))))
119 Added:
120 Added: ;;; Capture
121 Added:
122 Added: (ert-deftest fracas-test-capture-creates-an-addressable-todo ()
123 Added: "Capture writes a TODO entry with an ID, a SCHEDULED time, and tags."
124 Added: (fracas-tests--with-stream root
125 Added: (let* ((id (fracas-tests--capture root "Add a widget" ["feat" "web"]))
126 Added: (entry (fracas-tests--call #'fracas-tools--show-handler
127 Added: (list (cons 'root root) (cons 'id id)))))
128 Added: (should (stringp id))
129 Added: (should (equal (alist-get 'state entry) "TODO"))
130 Added: (should (equal (alist-get 'heading entry) "Add a widget"))
131 Added: (should (equal (append (alist-get 'tags entry) nil) '("feat" "web")))
132 Added: (should (stringp (alist-get 'scheduled entry)))
133 Added: (should (null (alist-get 'closed entry))))))
134 Added:
135 Added: (ert-deftest fracas-test-capture-puts-the-newest-request-first ()
136 Added: "Capture inserts each new request above the previous request."
137 Added: (fracas-tests--with-stream root
138 Added: (fracas-tests--capture root "First request" ["feat" "core"])
139 Added: (fracas-tests--capture root "Second request" ["fix" "app"])
140 Added: (let ((rows (fracas-tests--decode
141 Added: (fracas-tools--list-handler (list (cons 'root root))))))
142 Added: (should (equal (length rows) 2))
143 Added: (should (equal (alist-get 'title (aref rows 0)) "Second request"))
144 Added: (should (equal (alist-get 'title (aref rows 1)) "First request")))))
145 Added:
146 Added: (ert-deftest fracas-test-capture-rejects-a-tag-outside-the-vocabulary ()
147 Added: "Capture rejects any tag that the closed vocabulary does not hold."
148 Added: (fracas-tests--with-stream root
149 Added: (let ((result (fracas-tests--decode
150 Added: (fracas-tools--capture-handler
151 Added: (list (cons 'root root)
152 Added: (cons 'title "Bad tags")
153 Added: (cons 'tags ["feat" "trivial"]))))))
154 Added: (should (string-match-p "Unknown tag" (alist-get 'error result))))))
155 Added:
156 Added: ;;; Status
157 Added:
158 Added: (ert-deftest fracas-test-set-status-moves-through-the-open-keywords ()
159 Added: "Set-status accepts each open keyword from the file's own sequence."
160 Added: (fracas-tests--with-stream root
161 Added: (let ((id (fracas-tests--capture root "Track status" ["feat" "core"])))
162 Added: (dolist (state '("IN-PROGRESS" "TESTING" "TESTED"))
163 Added: (let ((result (fracas-tests--call
164 Added: #'fracas-tools--set-status-handler
165 Added: (list (cons 'root root) (cons 'id id)
166 Added: (cons 'state state)))))
167 Added: (should (equal (alist-get 'state result) state))))
168 Added: (should (equal (alist-get 'state (fracas-tests--call
169 Added: #'fracas-tools--verify-handler
170 Added: (list (cons 'root root) (cons 'id id))))
171 Added: "TESTED")))))
172 Added:
173 Added: (ert-deftest fracas-test-set-status-refuses-done ()
174 Added: "Set-status refuses DONE, because completion needs result evidence."
175 Added: (fracas-tests--with-stream root
176 Added: (let* ((id (fracas-tests--capture root "Refuse done" ["feat" "core"]))
177 Added: (result (fracas-tests--decode
178 Added: (fracas-tools--set-status-handler
179 Added: (list (cons 'root root) (cons 'id id)
180 Added: (cons 'state "DONE"))))))
181 Added: (should (string-match-p "fracas-complete" (alist-get 'error result))))))
182 Added:
183 Added: (ert-deftest fracas-test-block-and-cancel-record-a-reason ()
184 Added: "Block and cancel write the reason, so the decision is never silent."
185 Added: (fracas-tests--with-stream root
186 Added: (let ((blocked (fracas-tests--capture root "Blocked work" ["feat" "app"]))
187 Added: (dropped (fracas-tests--capture root "Dropped work" ["feat" "ui"])))
188 Added: (fracas-tests--call #'fracas-tools--block-handler
189 Added: (list (cons 'root root) (cons 'id blocked)
190 Added: (cons 'reason "The route does not exist")))
191 Added: (fracas-tests--call #'fracas-tools--cancel-handler
192 Added: (list (cons 'root root) (cons 'id dropped)
193 Added: (cons 'reason "The user withdrew the request")))
194 Added: (let ((text (fracas-tests--file-text root)))
195 Added: (should (string-match-p "- blocked :: The route does not exist" text))
196 Added: (should (string-match-p "- cancelled :: The user withdrew" text)))
197 Added: (should (equal "BLOCKED"
198 Added: (alist-get 'state (fracas-tests--call
199 Added: #'fracas-tools--verify-handler
200 Added: (list (cons 'root root)
201 Added: (cons 'id blocked))))))
202 Added: (should (equal "CANCELLED"
203 Added: (alist-get 'state (fracas-tests--call
204 Added: #'fracas-tools--verify-handler
205 Added: (list (cons 'root root)
206 Added: (cons 'id dropped)))))))))
207 Added:
208 Added: (ert-deftest fracas-test-block-requires-a-reason ()
209 Added: "Block refuses a blank reason."
210 Added: (fracas-tests--with-stream root
211 Added: (let* ((id (fracas-tests--capture root "Needs a reason" ["feat" "app"]))
212 Added: (result (fracas-tests--decode
213 Added: (fracas-tools--block-handler
214 Added: (list (cons 'root root) (cons 'id id)
215 Added: (cons 'reason " "))))))
216 Added: (should (string-match-p "reason" (alist-get 'error result))))))
217 Added:
218 Added: ;;; Checklist, logbook, and tags
219 Added:
220 Added: (ert-deftest fracas-test-checklist-adds-and-toggles-items ()
221 Added: "The checklist adds an item, then toggles it, and reports booleans."
222 Added: (fracas-tests--with-stream root
223 Added: (let ((id (fracas-tests--capture root "Split the task" ["feat" "core"])))
224 Added: (let ((added (fracas-tests--call
225 Added: #'fracas-tools--check-handler
226 Added: (list (cons 'root root) (cons 'id id)
227 Added: (cons 'action "add") (cons 'item "step one")))))
228 Added: (should (equal (alist-get 'done (aref (alist-get 'items added) 0))
229 Added: :json-false)))
230 Added: (let ((toggled (fracas-tests--call
231 Added: #'fracas-tools--check-handler
232 Added: (list (cons 'root root) (cons 'id id)
233 Added: (cons 'action "toggle") (cons 'item "step one")))))
234 Added: (should (eq (alist-get 'done (aref (alist-get 'items toggled) 0)) t)))
235 Added: (should (string-match-p "Checklist \\[1/1\\]" (fracas-tests--file-text root))))))
236 Added:
237 Added: (ert-deftest fracas-test-log-appends-and-keeps-earlier-notes ()
238 Added: "The logbook keeps every note, newest first."
239 Added: (fracas-tests--with-stream root
240 Added: (let ((id (fracas-tests--capture root "Log progress" ["feat" "core"])))
241 Added: (fracas-tests--call #'fracas-tools--log-handler
242 Added: (list (cons 'root root) (cons 'id id)
243 Added: (cons 'note "First note")))
244 Added: (fracas-tests--call #'fracas-tools--log-handler
245 Added: (list (cons 'root root) (cons 'id id)
246 Added: (cons 'note "Second note")))
247 Added: (let* ((entry (fracas-tests--call #'fracas-tools--show-handler
248 Added: (list (cons 'root root) (cons 'id id))))
249 Added: (notes (append (alist-get 'logbook entry) nil)))
250 Added: (should (equal (length notes) 2))
251 Added: (should (string-match-p "Second note" (nth 0 notes)))
252 Added: (should (string-match-p "First note" (nth 1 notes)))))))
253 Added:
254 Added: (ert-deftest fracas-test-retag-validates-against-the-vocabulary ()
255 Added: "Retag replaces the tags, and refuses a tag outside the vocabulary."
256 Added: (fracas-tests--with-stream root
257 Added: (let ((id (fracas-tests--capture root "Retag me" ["feat" "core"])))
258 Added: (let ((result (fracas-tests--call
259 Added: #'fracas-tools--retag-handler
260 Added: (list (cons 'root root) (cons 'id id)
261 Added: (cons 'tags ["fix" "web" "minor"])))))
262 Added: (should (equal (append (alist-get 'tags result) nil)
263 Added: '("fix" "web" "minor"))))
264 Added: (let ((result (fracas-tests--decode
265 Added: (fracas-tools--retag-handler
266 Added: (list (cons 'root root) (cons 'id id)
267 Added: (cons 'tags ["nonsense"]))))))
268 Added: (should (string-match-p "Unknown tag" (alist-get 'error result)))))))
269 Added:
270 Added: ;;; Result and completion
271 Added:
272 Added: (ert-deftest fracas-test-set-result-writes-model-and-notes ()
273 Added: "The result line holds the model, the commit, the tests, and the notes."
274 Added: (fracas-tests--with-stream root
275 Added: (let* ((id (fracas-tests--capture root "Record a result" ["fix" "core"]))
276 Added: (result (fracas-tests--call
277 Added: #'fracas-tools--set-result-handler
278 Added: (list (cons 'root root) (cons 'id id)
279 Added: (cons 'commit "abc1234") (cons 'tests "246 pass")
280 Added: (cons 'model "test-agent")
281 Added: (cons 'notes "the sentinel was wrong")))))
282 Added: (should (equal (alist-get 'result result)
283 Added: "model=test-agent commit=abc1234 tests=246 pass; the sentinel was wrong")))))
284 Added:
285 Added: (ert-deftest fracas-test-set-result-omits-absent-optional-fields ()
286 Added: "The result line holds only the commit and the tests when nothing else is given."
287 Added: (fracas-tests--with-stream root
288 Added: (let* ((id (fracas-tests--capture root "Plain result" ["fix" "core"]))
289 Added: (result (fracas-tests--call
290 Added: #'fracas-tools--set-result-handler
291 Added: (list (cons 'root root) (cons 'id id)
292 Added: (cons 'commit "abc1234") (cons 'tests "246 pass")))))
293 Added: (should (equal (alist-get 'result result)
294 Added: "commit=abc1234 tests=246 pass")))))
295 Added:
296 Added: (ert-deftest fracas-test-set-result-replaces-an-earlier-line ()
297 Added: "A second result call replaces the earlier result line."
298 Added: (fracas-tests--with-stream root
299 Added: (let ((id (fracas-tests--capture root "Replace result" ["fix" "core"])))
300 Added: (dolist (commit '("aaa1111" "bbb2222"))
301 Added: (fracas-tests--call #'fracas-tools--set-result-handler
302 Added: (list (cons 'root root) (cons 'id id)
303 Added: (cons 'commit commit) (cons 'tests "1 pass"))))
304 Added: (let ((text (fracas-tests--file-text root)))
305 Added: (should-not (string-match-p "aaa1111" text))
306 Added: (should (string-match-p "bbb2222" text))))))
307 Added:
308 Added: (ert-deftest fracas-test-complete-sets-done-with-result-and-closed ()
309 Added: "Completion writes the result, sets DONE, and confirms the CLOSED time."
310 Added: (fracas-tests--with-stream root
311 Added: (let* ((id (fracas-tests--capture root "Finish the work" ["feat" "core"]))
312 Added: (result (fracas-tests--call
313 Added: #'fracas-tools--complete-handler
314 Added: (list (cons 'root root) (cons 'id id)
315 Added: (cons 'commit "def5678") (cons 'tests "247 pass")
316 Added: (cons 'model "test-agent")))))
317 Added: (should (equal (alist-get 'state result) "DONE"))
318 Added: (should (string-match-p "commit=def5678" (alist-get 'result result)))
319 Added: (should (string-match-p "\\[.*\\]" (alist-get 'closed result)))
320 Added: (let ((entry (fracas-tests--call #'fracas-tools--verify-handler
321 Added: (list (cons 'root root) (cons 'id id)))))
322 Added: (should (equal (alist-get 'state entry) "DONE"))
323 Added: (should (stringp (alist-get 'closed entry)))))))
324 Added:
325 Added: (ert-deftest fracas-test-complete-refuses-a-file-without-closed-logging ()
326 Added: "Completion refuses to run when the file does not log a CLOSED time.
327 Added: Org must write the CLOSED timestamp, so the tool never writes it."
328 Added: (let* ((root (file-name-as-directory (make-temp-file "fracas-nolog" t)))
329 Added: (file (expand-file-name fracas-requests-file-name root))
330 Added: (enable-local-variables :all)
331 Added: (org-id-track-globally nil)
332 Added: (create-lockfiles nil)
333 Added: (org-log-done nil))
334 Added: (unwind-protect
335 Added: (progn
336 Added: ;; This preamble holds no `org-log-done' local variable.
337 Added: (with-temp-file file
338 Added: (insert "#+TITLE: No logging\n"
339 Added: "#+TODO: TODO IN-PROGRESS | CANCELLED DONE\n"))
340 Added: (let ((id (fracas-tests--capture root "No logging" ["feat" "core"])))
341 Added: ;; Close the buffer, so the next open reads the local variables
342 Added: ;; of the file as it now stands.
343 Added: (dolist (buf (buffer-list))
344 Added: (when (equal (buffer-file-name buf) file)
345 Added: (with-current-buffer buf (set-buffer-modified-p nil))
346 Added: (kill-buffer buf)))
347 Added: (let ((result (fracas-tests--decode
348 Added: (fracas-tools--complete-handler
349 Added: (list (cons 'root root) (cons 'id id)
350 Added: (cons 'commit "def5678")
351 Added: (cons 'tests "1 pass"))))))
352 Added: (should (stringp (alist-get 'error result)))
353 Added: (should (string-match-p "org-log-done"
354 Added: (alist-get 'error result))))))
355 Added: (dolist (buf (buffer-list))
356 Added: (when (and (buffer-file-name buf)
357 Added: (string-prefix-p root (buffer-file-name buf)))
358 Added: (with-current-buffer buf (set-buffer-modified-p nil))
359 Added: (kill-buffer buf)))
360 Added: (delete-directory root t))))
361 Added:
362 Added: ;;; Reading
363 Added:
364 Added: (ert-deftest fracas-test-list-encodes-an-absent-value-as-json-null ()
365 Added: "A heading without an ID reports JSON null, not the text \"null\"."
366 Added: (fracas-tests--with-stream root
367 Added: (let ((file (expand-file-name fracas-requests-file-name root)))
368 Added: (with-current-buffer (find-file-noselect file)
369 Added: (goto-char (point-min))
370 Added: (insert "* TODO Entry without an identifier :feat:core:\n")
371 Added: (save-buffer))
372 Added: (let* ((json (fracas-tools--list-handler (list (cons 'root root))))
373 Added: (rows (fracas-tests--decode json)))
374 Added: (should (null (alist-get 'id (aref rows 0))))
375 Added: (should-not (string-match-p "\"null\"" json))))))
376 Added:
377 Added: (ert-deftest fracas-test-list-filters-by-state-and-tag ()
378 Added: "The list filters narrow the result by TODO keyword and by tag."
379 Added: (fracas-tests--with-stream root
380 Added: (let ((first (fracas-tests--capture root "Filter one" ["feat" "core"])))
381 Added: (fracas-tests--capture root "Filter two" ["fix" "web"])
382 Added: (fracas-tests--call #'fracas-tools--set-status-handler
383 Added: (list (cons 'root root) (cons 'id first)
384 Added: (cons 'state "IN-PROGRESS")))
385 Added: (let ((by-state (fracas-tests--decode
386 Added: (fracas-tools--list-handler
387 Added: (list (cons 'root root) (cons 'state "IN-PROGRESS")))))
388 Added: (by-tag (fracas-tests--decode
389 Added: (fracas-tools--list-handler
390 Added: (list (cons 'root root) (cons 'tag "web"))))))
391 Added: (should (equal (length by-state) 1))
392 Added: (should (equal (alist-get 'title (aref by-state 0)) "Filter one"))
393 Added: (should (equal (length by-tag) 1))
394 Added: (should (equal (alist-get 'title (aref by-tag 0)) "Filter two"))))))
395 Added:
396 Added: (ert-deftest fracas-test-inspect-reports-the-sequence-and-the-axes ()
397 Added: "Inspect reports the file's keyword sequence and the tag axes."
398 Added: (fracas-tests--with-stream root
399 Added: (let* ((result (fracas-tests--call #'fracas-tools--inspect-handler
400 Added: (list (cons 'root root))))
401 Added: (keywords (append (alist-get 'todo_keywords result) nil))
402 Added: (axes (alist-get 'tags result)))
403 Added: (should (member "IN-PROGRESS" keywords))
404 Added: (should (member "DONE" keywords))
405 Added: (should (equal (mapcar #'car axes) '(kind scope impact misc)))
406 Added: (should (equal (append (alist-get 'impact axes) nil) '("minor" "major"))))))
407 Added:
408 Added: ;;; The full path
409 Added:
410 Added: (ert-deftest fracas-test-the-full-request-path-runs-end-to-end ()
411 Added: "One request moves from capture to DONE through every step."
412 Added: (fracas-tests--with-stream root
413 Added: (let ((id (fracas-tests--capture root "Ship the feature" ["feat" "web"]
414 Added: "Add a widget to the page.")))
415 Added: (fracas-tests--call #'fracas-tools--check-handler
416 Added: (list (cons 'root root) (cons 'id id)
417 Added: (cons 'action "add") (cons 'item "write the code")))
418 Added: (fracas-tests--call #'fracas-tools--set-status-handler
419 Added: (list (cons 'root root) (cons 'id id)
420 Added: (cons 'state "IN-PROGRESS")))
421 Added: (fracas-tests--call #'fracas-tools--log-handler
422 Added: (list (cons 'root root) (cons 'id id)
423 Added: (cons 'note "Started the work")))
424 Added: (fracas-tests--call #'fracas-tools--check-handler
425 Added: (list (cons 'root root) (cons 'id id)
426 Added: (cons 'action "toggle")
427 Added: (cons 'item "write the code")))
428 Added: (fracas-tests--call #'fracas-tools--set-status-handler
429 Added: (list (cons 'root root) (cons 'id id)
430 Added: (cons 'state "TESTED")))
431 Added: (fracas-tests--call #'fracas-tools--complete-handler
432 Added: (list (cons 'root root) (cons 'id id)
433 Added: (cons 'commit "0badc0de") (cons 'tests "3 pass")
434 Added: (cons 'model "test-agent")))
435 Added: (let ((entry (fracas-tests--call #'fracas-tools--show-handler
436 Added: (list (cons 'root root) (cons 'id id)))))
437 Added: (should (equal (alist-get 'state entry) "DONE"))
438 Added: (should (stringp (alist-get 'closed entry)))
439 Added: (should (string-match-p "commit=0badc0de" (alist-get 'result entry)))
440 Added: (should (equal (length (alist-get 'logbook entry)) 1))
441 Added: (should (eq (alist-get 'done (aref (alist-get 'checklist entry) 0)) t))
442 Added: (should (string-match-p "Add a widget" (alist-get 'body entry)))))))
443 Added:
444 Added: (provide 'fracas-tests)
445 Added:
446 Added: ;;; fracas-tests.el ends here
.kiro/skills/fracas/fracas-tools.el
index 80e62846..4c139e1d 100644..100644
@@ -12,8 +12,9 @@
12 12 ;; agent does not run raw `eval-elisp' snippets for each capture, status
13 13 ;; change, log, result, or verification. Each operation becomes a named tool.
14 14 ;;
15 Removed: ;; REQUESTS.org is a flat stream: each request is a top-level heading, newest
16 Removed: ;; first, tagged by kind, scope, and impact. There are no container headings.
15 Added: ;; REQUESTS.org is a flat stream. Each request is a top-level heading, newest
16 Added: ;; first, tagged by kind, scope, and impact. The file holds no container
17 Added: ;; heading.
17 18 ;;
18 19 ;; Why dedicated tools instead of `eval-elisp'?
19 20 ;;
@@ -29,36 +30,91 @@
29 30 ;; tool also carries MCP `annotations' so the MCP client can auto-approve the
30 31 ;; read-only tools.
31 32 ;;
32 Removed: ;; The tools operate only on the file "REQUESTS.org" under a caller-
33 Removed: ;; supplied project ROOT. They never touch any other file.
33 Added: ;; The tools operate only on the file "REQUESTS.org". They find that file
34 Added: ;; under `fracas-project-root', or under a caller-supplied project ROOT that
35 Added: ;; overrides it. They never touch any other file.
34 36 ;;
35 Removed: ;; Install (run once per Emacs session):
37 Added: ;; `fracas-project-root' comes from an upward search for the stream file. The
38 Added: ;; search starts at this file's own directory, then at `default-directory'. It
39 Added: ;; assumes no directory layout, so this file needs no absolute path and it
40 Added: ;; works on every machine.
36 41 ;;
37 Removed: ;; (load "/home/blendux/git/hito/.kiro/skills/fracas/fracas-tools.el")
42 Added: ;; Install the tools once per Emacs session. Load this file, and the tools
43 Added: ;; register themselves. With `mcp-server-emacs-tools-enabled' set to `all',
44 Added: ;; which is the default, they appear in the MCP tool list at once.
38 45 ;;
39 Removed: ;; The tools self-register on load. With `mcp-server-emacs-tools-enabled' set
40 Removed: ;; to `all' (the default), they appear in the MCP tool list immediately.
46 Added: ;; The MCP framework is a soft dependency. When the framework is absent, for
47 Added: ;; example in a batch test run, this file still loads and every handler stays
48 Added: ;; callable. Run the test suite with the run-tests.sh script beside this file.
41 49
42 50 ;;; Code:
43 51
44 Removed: (require 'mcp-server-tools)
52 Added: (require 'cl-lib)
45 53 (require 'org)
46 54 (require 'org-id)
47 55 (require 'json)
56 Added: (require 'subr-x)
48 57
58 Added: ;; Load the MCP tool framework when it is available. When it is absent, for
59 Added: ;; example in a batch test run, define the two symbols the registrations below
60 Added: ;; need and discard each registration. The handler functions stay callable, so
61 Added: ;; the test suite runs on any machine without the framework.
62 Added: (defconst fracas-mcp-available (require 'mcp-server-tools nil t)
63 Added: "Non-nil when the Emacs MCP tool framework is available.")
64 Added:
65 Added: (unless fracas-mcp-available
66 Added: ;; Define plain functions, never a struct. A stub struct would clobber the
67 Added: ;; real slot layout if the framework loads later in the same session.
68 Added: (defun make-mcp-server-tool (&rest _args)
69 Added: "Return nil. The MCP framework is absent."
70 Added: nil)
71 Added: (defun mcp-server-register-tool (_tool)
72 Added: "Discard _TOOL. The MCP framework is absent."
73 Added: nil))
74 Added:
75 Added: (defvar fracas-requests-file-name "REQUESTS.org"
76 Added: "Name of the Org file that FRACAS manages.")
77 Added:
78 Added: (defvar fracas-tools-path
79 Added: (let ((file (or load-file-name buffer-file-name)))
80 Added: (and file (expand-file-name file)))
81 Added: "Absolute path of this file, or nil when the path is unknown.")
82 Added:
83 Added: (defun fracas-locate-root (start)
84 Added: "Return the closest directory at or above START that holds the stream file.
85 Added: The stream file is `fracas-requests-file-name'. Return nil when no
86 Added: ancestor directory holds that file."
87 Added: (let ((dir (and start (locate-dominating-file
88 Added: (file-name-as-directory (expand-file-name start))
89 Added: fracas-requests-file-name))))
90 Added: (and dir (expand-file-name (file-name-as-directory dir)))))
91 Added:
92 Added: (defvar fracas-project-root
93 Added: (or (fracas-locate-root (and fracas-tools-path
94 Added: (file-name-directory fracas-tools-path)))
95 Added: (fracas-locate-root default-directory)
96 Added: (expand-file-name default-directory))
97 Added: "Default project directory that holds the FRACAS stream file.
98 Added: The value comes from an upward search for `fracas-requests-file-name',
99 Added: first from this file's own directory, then from `default-directory'.
100 Added: The search makes no assumption about the depth of this file in the
101 Added: project. Set this variable to override the search, or pass a `root'
102 Added: argument to any tool.")
103 Added:
49 104 ;;; Helpers
50 105
51 106 (defun fracas-tools--file (args)
52 Removed: "Return the absolute REQUESTS.org path for ARGS.
53 Removed: ARGS must contain a `root' string naming the project directory.
54 Removed: Signal an error when `root' is missing or is not a directory."
55 Removed: (let ((root (alist-get 'root args)))
107 Added: "Return the absolute path of the stream file for ARGS.
108 Added: ARGS may hold a `root' string that names the project directory. When
109 Added: `root' is absent, use `fracas-project-root'. Signal an error when the
110 Added: selected root is not a directory."
111 Added: (let ((root (or (alist-get 'root args) fracas-project-root)))
56 112 (unless (and (stringp root) (> (length root) 0))
57 Removed: (error "Missing required `root' (project directory)"))
113 Added: (error "No project root: pass `root' or set `fracas-project-root'"))
58 114 (let ((dir (expand-file-name root)))
59 115 (unless (file-directory-p dir)
60 116 (error "Not a directory: %s" dir))
61 Removed: (expand-file-name "REQUESTS.org" dir))))
117 Added: (expand-file-name fracas-requests-file-name dir))))
62 118
63 119 (defun fracas-tools--buffer (file)
64 120 "Return an org-mode buffer visiting FILE, creating it as needed."
@@ -97,14 +153,28 @@
97 153 (forward-line 1))
98 154 (set-marker end nil))))
99 155
100 Removed: (defun fracas-tools--set-result (commit tests)
156 Added: (defun fracas-tools--nonblank (value)
157 Added: "Return VALUE trimmed when it is a non-blank string, else nil."
158 Added: (and (stringp value)
159 Added: (let ((trimmed (string-trim value)))
160 Added: (and (> (length trimmed) 0) trimmed))))
161 Added:
162 Added: (defun fracas-tools--set-result (commit tests &optional model notes)
101 163 "Write the structured result line for the entry at point.
102 164 Replace an existing `- result ::' line, or append one at the end of
103 Removed: the entry body. COMMIT is a commit hash; TESTS is a short recap
104 Removed: such as \"215 pass\". Point must be on the entry heading."
165 Added: the entry body. COMMIT is a commit hash. TESTS is a short recap
166 Added: such as \"215 pass\". MODEL names the agent that did the work, and
167 Added: NOTES adds a free-text tail after a semicolon. Both are optional.
168 Added: Keep the line unwrapped, because the reader captures a single line.
169 Added: Point must be on the entry heading."
105 170 (org-back-to-heading t)
106 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
107 Removed: (line (format "- result :: commit=%s tests=%s" commit tests)))
171 Added: (let* ((model (fracas-tools--nonblank model))
172 Added: (notes (fracas-tools--nonblank notes))
173 Added: (subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
174 Added: (line (concat "- result :: "
175 Added: (if model (format "model=%s " model) "")
176 Added: (format "commit=%s tests=%s" commit tests)
177 Added: (if notes (format "; %s" notes) ""))))
108 178 (org-back-to-heading t)
109 179 (if (re-search-forward "^[ \t]*- result ::.*$" subtree-end t)
110 180 (replace-match line t t)
@@ -117,7 +187,7 @@
117 187 "Append NOTE as a timestamped item to the entry's `:LOGBOOK:' drawer.
118 188 Create the drawer directly after the metadata when it is absent.
119 189 Insert the newest item first and wrap it to 72 columns. This is
120 Removed: append-only; it never edits an existing item or the body. Point
190 Added: append-only. It never edits an existing item or the body. Point
121 191 must be on the entry heading."
122 192 (let ((fill-column 72))
123 193 (org-back-to-heading t)
@@ -155,25 +225,31 @@
155 225
156 226 ;;; Tag vocabulary
157 227
158 Removed: (defconst fracas-tools--tags
159 Removed: '("feat" "fix" "refactor" "chore" "docs"
160 Removed: "core" "app" "web" "ui"
161 Removed: "trivial" "major"
162 Removed: "doctrine")
163 Removed: "Closed tag vocabulary for FRACAS request headings.
164 Removed: A request carries one kind tag (feat, fix, refactor, chore, docs) and
165 Removed: one scope tag (core, app, web, ui). An impact tag is optional and
166 Removed: marks an exception: `trivial' for a trivial change, `major' for a
167 Removed: potential major rework. The `doctrine' flag is optional.
168 Removed: The three axes use distinct words, so a bare tag stays unambiguous.")
228 Added: (defconst fracas-tools--tag-axes
229 Added: '((kind . ("feat" "fix" "refactor" "chore" "docs"))
230 Added: (scope . ("core" "app" "web" "ui"))
231 Added: (impact . ("minor" "major"))
232 Added: (misc . ("doctrine")))
233 Added: "Closed tag vocabulary for FRACAS request headings, grouped by axis.
234 Added: A request carries one `kind' tag (feat, fix, refactor, chore, docs) and
235 Added: one `scope' tag (core, app, web, ui). Two axes are optional: `impact'
236 Added: takes at most one of `minor' or `major', which are opposite ends of one
237 Added: scale, and `misc' holds free flags, currently `doctrine' for a request
238 Added: that touches Heavy Duty doctrine.
239 Added: The axes use distinct words, so a bare tag stays unambiguous.")
169 240
241 Added: (defun fracas-tools--all-tags ()
242 Added: "Return every tag in the vocabulary as one flat list of strings."
243 Added: (apply #'append (mapcar #'cdr fracas-tools--tag-axes)))
244 Added:
170 245 (defun fracas-tools--check-tags (tags)
171 246 "Signal an error when TAGS holds a value outside the vocabulary.
172 247 TAGS is a list of strings. Return TAGS unchanged when valid."
173 Removed: (dolist (tag tags)
174 Removed: (unless (member tag fracas-tools--tags)
175 Removed: (error "Unknown tag `%s'; allowed: %s"
176 Removed: tag (string-join fracas-tools--tags ", "))))
248 Added: (let ((allowed (fracas-tools--all-tags)))
249 Added: (dolist (tag tags)
250 Added: (unless (member tag allowed)
251 Added: (error "Unknown tag `%s'; allowed: %s"
252 Added: tag (string-join allowed ", ")))))
177 253 tags)
178 254
179 255 (defun fracas-tools--goto-stream-top ()
@@ -196,17 +272,19 @@
196 272 (org-with-wide-buffer
197 273 `((file . ,file)
198 274 (todo_keywords . ,(vconcat org-todo-keywords-1))
199 Removed: (tags . ,(vconcat fracas-tools--tags))))))))
275 Added: (tags . ,(mapcar (lambda (axis)
276 Added: (cons (car axis) (vconcat (cdr axis))))
277 Added: fracas-tools--tag-axes))))))))
200 278
201 279 (mcp-server-register-tool
202 280 (make-mcp-server-tool
203 281 :name "fracas-inspect"
204 282 :title "FRACAS Inspect"
205 Removed: :description "Inspect REQUESTS.org: return its TODO keyword sequence and the closed tag vocabulary for request headings. Read-only."
283 Added: :description "Inspect REQUESTS.org: return its TODO keyword sequence and the closed tag vocabulary for request headings, grouped by axis (kind, scope, impact, misc). Read-only."
206 284 :input-schema '((type . "object")
207 285 (properties . ((root . ((type . "string")
208 286 (description . "Absolute path to the project directory containing REQUESTS.org")))))
209 Removed: (required . ["root"]))
287 Added: (required . []))
210 288 :function #'fracas-tools--inspect-handler
211 289 :annotations '((readOnlyHint . t)
212 290 (destructiveHint . :false)
@@ -234,11 +312,10 @@
234 312 (tags (org-get-tags nil t)))
235 313 (when (and (or (null state) (equal state todo))
236 314 (or (null tag) (member tag tags)))
237 Removed: (push `((id . ,(or (org-id-get) :null))
315 Added: (push `((id . ,(org-id-get))
238 316 (title . ,(org-get-heading t t t t))
239 Removed: (state . ,(or todo :null))
240 Removed: (scheduled . ,(or (org-entry-get nil "SCHEDULED")
241 Removed: :null))
317 Added: (state . ,todo)
318 Added: (scheduled . ,(org-entry-get nil "SCHEDULED"))
242 319 (tags . ,(vconcat tags)))
243 320 rows))))
244 321 ;; The file is newest-first, so reverse to keep that order.
@@ -256,7 +333,7 @@
256 333 (description . "Optional TODO keyword filter, for example TODO or IN-PROGRESS")))
257 334 (tag . ((type . "string")
258 335 (description . "Optional tag filter, for example web or major")))))
259 Removed: (required . ["root"]))
336 Added: (required . []))
260 337 :function #'fracas-tools--list-handler
261 338 :annotations '((readOnlyHint . t)
262 339 (destructiveHint . :false)
@@ -312,8 +389,8 @@
312 389 (description . "Full request text, verbatim")))
313 390 (tags . ((type . "array")
314 391 (items . ((type . "string")))
315 Removed: (description . "Tags from the closed vocabulary: one kind (feat|fix|refactor|chore|docs), one scope (core|app|web|ui), an optional impact (trivial|major), plus optional doctrine.")))))
316 Removed: (required . ["root" "title"]))
392 Added: (description . "Tags from the closed vocabulary: one kind (feat|fix|refactor|chore|docs), one scope (core|app|web|ui), an optional impact (minor|major), plus optional misc flags (doctrine).")))))
393 Added: (required . ["title"]))
317 394 :function #'fracas-tools--capture-handler
318 395 :annotations '((readOnlyHint . :false)
319 396 (destructiveHint . :false)
@@ -350,7 +427,7 @@
350 427 (description . "Org ID of the entry")))
351 428 (state . ((type . "string")
352 429 (description . "TODO keyword from the file's #+TODO sequence")))))
353 Removed: (required . ["root" "id" "state"]))
430 Added: (required . ["id" "state"]))
354 431 :function #'fracas-tools--set-status-handler
355 432 :annotations '((readOnlyHint . :false)
356 433 (destructiveHint . :false)
@@ -378,14 +455,14 @@
378 455 (make-mcp-server-tool
379 456 :name "fracas-log"
380 457 :title "FRACAS Log"
381 Removed: :description "Append a timestamped progress note to a FRACAS entry's `:LOGBOOK:' drawer. Append-only feedback from an agentic session; it never rewrites earlier notes or the request body. Newest note first, wrapped to 72 columns."
458 Added: :description "Append a timestamped progress note to a FRACAS entry's `:LOGBOOK:' drawer. The drawer is append-only. The tool never rewrites an earlier note, and never rewrites the request body. Newest note first, wrapped to 72 columns."
382 459 :input-schema '((type . "object")
383 460 (properties . ((root . ((type . "string")))
384 461 (id . ((type . "string")
385 462 (description . "Org ID of the entry")))
386 463 (note . ((type . "string")
387 464 (description . "Progress note to append")))))
388 Removed: (required . ["root" "id" "note"]))
465 Added: (required . ["id" "note"]))
389 466 :function #'fracas-tools--log-handler
390 467 :annotations '((readOnlyHint . :false)
391 468 (destructiveHint . :false)
@@ -396,27 +473,30 @@
396 473
397 474 (defun fracas-tools--set-result-handler (args)
398 475 "Write the structured result line for an entry.
399 Removed: ARGS keys: `root', `id', `commit', `tests'. COMMIT is a commit hash;
400 Removed: TESTS is a short recap such as \"215 pass\". Replace an existing result
401 Removed: line or append one at the end of the body."
476 Added: ARGS keys: `root', `id', `commit', `tests', `model' (optional),
477 Added: `notes' (optional). COMMIT is a commit hash. TESTS is a short recap
478 Added: such as \"215 pass\". Replace an existing result line or append one at
479 Added: the end of the body."
402 480 (fracas-tools--json
403 481 (let ((file (fracas-tools--file args))
404 482 (id (or (alist-get 'id args) (error "Missing `id'")))
405 483 (commit (or (alist-get 'commit args) (error "Missing `commit'")))
406 Removed: (tests (or (alist-get 'tests args) (error "Missing `tests'"))))
484 Added: (tests (or (alist-get 'tests args) (error "Missing `tests'")))
485 Added: (model (alist-get 'model args))
486 Added: (notes (alist-get 'notes args)))
407 487 (with-current-buffer (fracas-tools--buffer file)
408 488 (fracas-tools--goto-id id)
409 Removed: (fracas-tools--set-result commit tests)
489 Added: (fracas-tools--set-result commit tests model notes)
410 490 (when (buffer-modified-p) (save-buffer))
491 Added: (fracas-tools--goto-id id)
411 492 `((id . ,id)
412 Removed: (commit . ,commit)
413 Removed: (tests . ,tests))))))
493 Added: (result . ,(fracas-tools--result-text)))))))
414 494
415 495 (mcp-server-register-tool
416 496 (make-mcp-server-tool
417 497 :name "fracas-set-result"
418 498 :title "FRACAS Set Result"
419 Removed: :description "Write the structured `- result ::' line for a FRACAS entry, recording the commit hash and a short test recap. Replaces an existing result line or appends one. fracas-complete calls this when you pass commit and tests."
499 Added: :description "Write the structured `- result ::' line for a FRACAS entry, recording the commit hash, a short test recap, and optionally the model that did the work and a free-text note. Replaces an existing result line or appends one. fracas-complete calls this when you pass commit and tests."
420 500 :input-schema '((type . "object")
421 501 (properties . ((root . ((type . "string")))
422 502 (id . ((type . "string")
@@ -424,8 +504,12 @@
424 504 (commit . ((type . "string")
425 505 (description . "Commit hash")))
426 506 (tests . ((type . "string")
427 Removed: (description . "Short test recap, for example \"215 pass\"")))))
428 Removed: (required . ["root" "id" "commit" "tests"]))
507 Added: (description . "Short test recap, for example \"215 pass\"")))
508 Added: (model . ((type . "string")
509 Added: (description . "Optional model or agent that did the work, for example kiro-cli")))
510 Added: (notes . ((type . "string")
511 Added: (description . "Optional free-text tail appended after a semicolon, for example a root cause")))))
512 Added: (required . ["id" "commit" "tests"]))
429 513 :function #'fracas-tools--set-result-handler
430 514 :annotations '((readOnlyHint . :false)
431 515 (destructiveHint . :false)
@@ -489,7 +573,7 @@
489 573 (items '()))
490 574 (org-back-to-heading t)
491 575 (while (re-search-forward "^- \\[\\(.\\)\\] \\(.*\\)$" subtree-end t)
492 Removed: (push `((done . ,(if (string-equal (match-string 1) " ") :false t))
576 Added: (push `((done . ,(if (string-equal (match-string 1) " ") :json-false t))
493 577 (text . ,(string-trim (match-string-no-properties 2))))
494 578 items))
495 579 (vconcat (nreverse items))))
@@ -497,7 +581,7 @@
497 581 (defun fracas-tools--check-handler (args)
498 582 "Manage the checklist of a request, for splitting a complex task.
499 583 ARGS keys: `root', `id', `action' (add|toggle|list), `item'.
500 Removed: `add' appends an unchecked item; `toggle' flips a matching item;
584 Added: `add' appends an unchecked item. `toggle' flips a matching item.
501 585 `list' returns the items. A `[/]' cookie tracks progress."
502 586 (fracas-tools--json
503 587 (let ((file (fracas-tools--file args))
@@ -524,7 +608,7 @@
524 608 (make-mcp-server-tool
525 609 :name "fracas-check"
526 610 :title "FRACAS Checklist"
527 Removed: :description "Manage a request's checkbox list to split a complex task into sub-tasks with their own done state. Actions: add an unchecked item, toggle a matching item, or list items. A [/] cookie on the checklist header tracks progress. Items stay inside the one request; they are not separate stream entries."
611 Added: :description "Manage a request's checkbox list to split a complex task into sub-tasks with their own done state. Actions: add an unchecked item, toggle a matching item, or list items. A [/] cookie on the checklist header tracks progress. The items stay inside the one request. They are not separate stream entries."
528 612 :input-schema '((type . "object")
529 613 (properties . ((root . ((type . "string")))
530 614 (id . ((type . "string")
@@ -533,7 +617,7 @@
533 617 (description . "add, toggle, or list")))
534 618 (item . ((type . "string")
535 619 (description . "Item text for add or toggle")))))
536 Removed: (required . ["root" "id" "action"]))
620 Added: (required . ["id" "action"]))
537 621 :function #'fracas-tools--check-handler
538 622 :annotations '((readOnlyHint . :false)
539 623 (destructiveHint . :false)
@@ -580,15 +664,14 @@
580 664
581 665 (defun fracas-tools--result-text ()
582 666 "Return the text after `- result ::' for the entry at point.
583 Removed: Return :null when there is no result line. Point must be on the
584 Removed: heading."
667 Added: Return nil when there is no result line, so it encodes as JSON null.
668 Added: Point must be on the heading."
585 669 (org-back-to-heading t)
586 670 (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point))))
587 671 (org-back-to-heading t)
588 Removed: (if (re-search-forward "^[ \t]*- result ::[ \t]*\\(.*\\)$"
589 Removed: subtree-end t)
590 Removed: (string-trim (match-string-no-properties 1))
591 Removed: :null)))
672 Added: (when (re-search-forward "^[ \t]*- result ::[ \t]*\\(.*\\)$"
673 Added: subtree-end t)
674 Added: (string-trim (match-string-no-properties 1)))))
592 675
593 676 (defun fracas-tools--show-handler (args)
594 677 "Return the full content of an entry.
@@ -603,10 +686,10 @@
603 686 (fracas-tools--goto-id id)
604 687 `((id . ,id)
605 688 (heading . ,(org-get-heading t t t t))
606 Removed: (state . ,(or (org-get-todo-state) :null))
689 Added: (state . ,(org-get-todo-state))
607 690 (tags . ,(vconcat (org-get-tags nil t)))
608 Removed: (scheduled . ,(or (org-entry-get nil "SCHEDULED") :null))
609 Removed: (closed . ,(or (org-entry-get nil "CLOSED") :null))
691 Added: (scheduled . ,(org-entry-get nil "SCHEDULED"))
692 Added: (closed . ,(org-entry-get nil "CLOSED"))
610 693 (body . ,(fracas-tools--body-text))
611 694 (logbook . ,(fracas-tools--logbook-items))
612 695 (checklist . ,(fracas-tools--checklist-items))
@@ -621,7 +704,7 @@
621 704 (properties . ((root . ((type . "string")))
622 705 (id . ((type . "string")
623 706 (description . "Org ID of the entry")))))
624 Removed: (required . ["root" "id"]))
707 Added: (required . ["id"]))
625 708 :function #'fracas-tools--show-handler
626 709 :annotations '((readOnlyHint . t)
627 710 (destructiveHint . :false)
@@ -651,7 +734,7 @@
651 734 (make-mcp-server-tool
652 735 :name "fracas-retag"
653 736 :title "FRACAS Retag"
654 Removed: :description "Replace the tags on a FRACAS entry with a validated set from the closed vocabulary. Use this to re-tag as a feature's shape changes; it keeps the file's default tag alignment."
737 Added: :description "Replace the tags on a FRACAS entry with a validated set from the closed vocabulary. Use this tool to re-tag an entry as the shape of the feature changes. The tool keeps the file's default tag alignment."
655 738 :input-schema '((type . "object")
656 739 (properties . ((root . ((type . "string")))
657 740 (id . ((type . "string")
@@ -659,7 +742,7 @@
659 742 (tags . ((type . "array")
660 743 (items . ((type . "string")))
661 744 (description . "Tags from the closed vocabulary")))))
662 Removed: (required . ["root" "id" "tags"]))
745 Added: (required . ["id" "tags"]))
663 746 :function #'fracas-tools--retag-handler
664 747 :annotations '((readOnlyHint . :false)
665 748 (destructiveHint . :false)
@@ -723,7 +806,7 @@
723 806 (description . "Org ID of the entry")))
724 807 (reason . ((type . "string")
725 808 (description . "Reason for cancelling the entry")))))
726 Removed: (required . ["root" "id" "reason"]))
809 Added: (required . ["id" "reason"]))
727 810 :function #'fracas-tools--cancel-handler
728 811 :annotations '((readOnlyHint . :false)
729 812 (destructiveHint . :false)
@@ -755,7 +838,7 @@
755 838 (description . "Org ID of the entry")))
756 839 (reason . ((type . "string")
757 840 (description . "Reason for blocking the entry")))))
758 Removed: (required . ["root" "id" "reason"]))
841 Added: (required . ["id" "reason"]))
759 842 :function #'fracas-tools--block-handler
760 843 :annotations '((readOnlyHint . :false)
761 844 (destructiveHint . :false)
@@ -766,21 +849,23 @@
766 849
767 850 (defun fracas-tools--complete-handler (args)
768 851 "Set an entry to DONE with its result evidence and confirm CLOSED.
769 Removed: ARGS keys: `root', `id', `commit', `tests'. Write the structured result
770 Removed: line (commit hash and test recap), then transition to DONE. Requires
771 Removed: `org-log-done' to be `time' so the normal Org transition inserts CLOSED;
772 Removed: this handler never writes CLOSED."
852 Added: ARGS keys: `root', `id', `commit', `tests', `model' (optional),
853 Added: `notes' (optional). Write the structured result line, then transition
854 Added: to DONE. Requires `org-log-done' to be `time' so the normal Org
855 Added: transition inserts CLOSED; this handler never writes CLOSED."
773 856 (fracas-tools--json
774 857 (let ((file (fracas-tools--file args))
775 858 (id (or (alist-get 'id args) (error "Missing `id'")))
776 859 (commit (or (alist-get 'commit args) (error "Missing `commit'")))
777 Removed: (tests (or (alist-get 'tests args) (error "Missing `tests'"))))
860 Added: (tests (or (alist-get 'tests args) (error "Missing `tests'")))
861 Added: (model (alist-get 'model args))
862 Added: (notes (alist-get 'notes args)))
778 863 (with-current-buffer (fracas-tools--buffer file)
779 864 (hack-local-variables)
780 865 (unless (eq org-log-done 'time)
781 866 (error "org-log-done is not set to time"))
782 867 (fracas-tools--goto-id id)
783 Removed: (fracas-tools--set-result commit tests)
868 Added: (fracas-tools--set-result commit tests model notes)
784 869 (fracas-tools--goto-id id)
785 870 (org-todo "DONE")
786 871 (when (buffer-modified-p) (save-buffer))
@@ -789,23 +874,26 @@
789 874 (error "Org did not record a CLOSED timestamp"))
790 875 `((id . ,id)
791 876 (state . ,(org-get-todo-state))
792 Removed: (commit . ,commit)
793 Removed: (tests . ,tests)
877 Added: (result . ,(fracas-tools--result-text))
794 878 (closed . ,closed)))))))
795 879
796 880 (mcp-server-register-tool
797 881 (make-mcp-server-tool
798 882 :name "fracas-complete"
799 883 :title "FRACAS Complete"
800 Removed: :description "Set a FRACAS entry to DONE. Records the result evidence (commit hash and a short test recap) as a `- result ::' line, then confirms Org inserted a CLOSED timestamp. Requires org-log-done set to time. Never writes the timestamp itself. The entry stays in place; there is no refile step."
884 Added: :description "Set a FRACAS entry to DONE. The tool records the result evidence as a `- result ::' line. The evidence holds the commit hash, a short test recap, and optionally the agent that did the work and a free-text note. The tool then confirms that Org inserted a CLOSED timestamp. The file must set org-log-done to time. The tool never writes the timestamp itself. The entry stays in place, and there is no refile step."
801 885 :input-schema '((type . "object")
802 886 (properties . ((root . ((type . "string")))
803 887 (id . ((type . "string")))
804 888 (commit . ((type . "string")
805 889 (description . "Commit hash for the completed work")))
806 890 (tests . ((type . "string")
807 Removed: (description . "Short test recap, for example \"215 pass\"")))))
808 Removed: (required . ["root" "id" "commit" "tests"]))
891 Added: (description . "Short test recap, for example \"215 pass\"")))
892 Added: (model . ((type . "string")
893 Added: (description . "Optional model or agent that did the work, for example kiro-cli")))
894 Added: (notes . ((type . "string")
895 Added: (description . "Optional free-text tail appended after a semicolon, for example a root cause")))))
896 Added: (required . ["id" "commit" "tests"]))
809 897 :function #'fracas-tools--complete-handler
810 898 :annotations '((readOnlyHint . :false)
811 899 (destructiveHint . :false)
@@ -826,7 +914,7 @@
826 914 `((id . ,id)
827 915 (heading . ,(org-get-heading t t t t))
828 916 (state . ,(org-get-todo-state))
829 Removed: (closed . ,(or (org-entry-get nil "CLOSED") :null))
917 Added: (closed . ,(org-entry-get nil "CLOSED"))
830 918 (tags . ,(vconcat (org-get-tags nil t)))))))))
831 919
832 920 (mcp-server-register-tool
@@ -837,7 +925,7 @@
837 925 :input-schema '((type . "object")
838 926 (properties . ((root . ((type . "string")))
839 927 (id . ((type . "string")))))
840 Removed: (required . ["root" "id"]))
928 Added: (required . ["id"]))
841 929 :function #'fracas-tools--verify-handler
842 930 :annotations '((readOnlyHint . t)
843 931 (destructiveHint . :false)
.kiro/skills/fracas/references/tools.md
index 00000000..d058daaa 000000..100644
@@ -0,0 +1,168 @@
1 Added: # FRACAS tool reference
2 Added:
3 Added: Read this file when you need the exact arguments of one tool, or the
4 Added: shape of its result. The main skill file gives the workflow.
5 Added:
6 Added: Every tool accepts an optional `root` argument. `root` names the project
7 Added: directory that holds the stream file. When you omit `root`, the tools use
8 Added: `fracas-project-root`. That variable comes from an upward search for
9 Added: `REQUESTS.org`, so it needs no configuration.
10 Added:
11 Added: Every tool returns one JSON object. A failed call returns an object with
12 Added: one `error` field. An absent value returns JSON `null`.
13 Added:
14 Added: ## Read-only tools
15 Added:
16 Added: ### fracas-inspect
17 Added:
18 Added: Arguments: none beyond `root`.
19 Added:
20 Added: Returns `file`, `todo_keywords`, and `tags`. The `tags` field groups the
21 Added: vocabulary by axis: `kind`, `scope`, `impact`, and `misc`.
22 Added:
23 Added: Run this tool when a tag call fails, or when you need the keyword
24 Added: sequence of the file.
25 Added:
26 Added: ### fracas-list
27 Added:
28 Added: Arguments:
29 Added:
30 Added: - `state` — optional. Keep only entries with this TODO keyword.
31 Added: - `tag` — optional. Keep only entries that carry this tag.
32 Added:
33 Added: Returns an array of objects, newest first. Each object holds `id`,
34 Added: `title`, `state`, `scheduled`, and `tags`. The `id` field is `null` for
35 Added: an entry that carries no Org ID.
36 Added:
37 Added: Pass a filter whenever you can, because the stream grows without bound.
38 Added:
39 Added: ### fracas-show
40 Added:
41 Added: Arguments:
42 Added:
43 Added: - `id` — required. The Org ID of the entry.
44 Added:
45 Added: Returns `id`, `heading`, `state`, `tags`, `scheduled`, `closed`, `body`,
46 Added: `logbook`, `checklist`, and `result`. The `checklist` field holds objects
47 Added: with a boolean `done` field and a `text` field.
48 Added:
49 Added: ### fracas-verify
50 Added:
51 Added: Arguments:
52 Added:
53 Added: - `id` — required. The Org ID of the entry.
54 Added:
55 Added: Returns `id`, `heading`, `state`, `closed`, and `tags`. Use this tool
56 Added: before a status change and after a status change.
57 Added:
58 Added: ## Writing tools
59 Added:
60 Added: ### fracas-capture
61 Added:
62 Added: Arguments:
63 Added:
64 Added: - `title` — required. A short imperative title under 60 characters.
65 Added: - `body` — optional. The full request text, as plain text.
66 Added: - `tags` — optional. An array of tags from the closed vocabulary.
67 Added:
68 Added: Returns `id`, `file`, `heading`, and `tags`.
69 Added:
70 Added: The tool inserts the entry as a top-level heading above every other
71 Added: entry. It records the capture time as an inactive `SCHEDULED` timestamp.
72 Added: It wraps the body to 72 columns, and it assigns an Org ID.
73 Added:
74 Added: ### fracas-set-status
75 Added:
76 Added: Arguments:
77 Added:
78 Added: - `id` — required. The Org ID of the entry.
79 Added: - `state` — required. One keyword from the file's own `#+TODO:` line.
80 Added:
81 Added: Returns `id` and `state`.
82 Added:
83 Added: The tool refuses `DONE`. Use `fracas-complete` for `DONE`.
84 Added:
85 Added: ### fracas-block and fracas-cancel
86 Added:
87 Added: Arguments:
88 Added:
89 Added: - `id` — required. The Org ID of the entry.
90 Added: - `reason` — required. The reason for the transition.
91 Added:
92 Added: Returns `id` and `state`.
93 Added:
94 Added: `fracas-block` writes a `- blocked ::` line and sets `BLOCKED`.
95 Added: `fracas-cancel` writes a `- cancelled ::` line and sets `CANCELLED`.
96 Added: Each tool refuses a blank reason.
97 Added:
98 Added: ### fracas-check
99 Added:
100 Added: Arguments:
101 Added:
102 Added: - `id` — required. The Org ID of the entry.
103 Added: - `action` — required. One of `add`, `toggle`, or `list`.
104 Added: - `item` — required for `add` and for `toggle`. The item text.
105 Added:
106 Added: Returns `id` and `items`.
107 Added:
108 Added: The tool keeps the items in a `Checklist [/]:` block in the body. The
109 Added: `[/]` cookie counts the completed items. `toggle` matches the item text
110 Added: from the start, and it fails when no item matches.
111 Added:
112 Added: ### fracas-log
113 Added:
114 Added: Arguments:
115 Added:
116 Added: - `id` — required. The Org ID of the entry.
117 Added: - `note` — required. The progress note.
118 Added:
119 Added: Returns `id` and `state`.
120 Added:
121 Added: The tool prepends a timestamped item to the `:LOGBOOK:` drawer, and
122 Added: creates the drawer when it is absent. The drawer is append-only.
123 Added:
124 Added: ### fracas-retag
125 Added:
126 Added: Arguments:
127 Added:
128 Added: - `id` — required. The Org ID of the entry.
129 Added: - `tags` — required. An array of tags from the closed vocabulary.
130 Added:
131 Added: Returns `id` and `tags`.
132 Added:
133 Added: The tool replaces every tag on the entry. It validates each tag first,
134 Added: so one invalid tag leaves the entry unchanged.
135 Added:
136 Added: ### fracas-set-result
137 Added:
138 Added: Arguments:
139 Added:
140 Added: - `id` — required. The Org ID of the entry.
141 Added: - `commit` — required. The commit hash.
142 Added: - `tests` — required. A short test recap.
143 Added: - `model` — optional. The agent that did the work.
144 Added: - `notes` — optional. A short tail after a semicolon.
145 Added:
146 Added: Returns `id` and `result`.
147 Added:
148 Added: The tool replaces an existing result line, or appends one at the end of
149 Added: the body. The line stays on one line, because the reader reads one line.
150 Added:
151 Added: ### fracas-complete
152 Added:
153 Added: Arguments: the same arguments as `fracas-set-result`.
154 Added:
155 Added: Returns `id`, `state`, `result`, and `closed`.
156 Added:
157 Added: The tool writes the result line, sets `DONE`, then reads back the
158 Added: `CLOSED` timestamp. It fails when the file does not set `org-log-done`
159 Added: to `time`, because Org must write that timestamp.
160 Added:
161 Added: ## Result line format
162 Added:
163 Added: ```org
164 Added: - result :: model=example-agent commit=3051af0 tests=243 pass; short root cause
165 Added: ```
166 Added:
167 Added: The `model` field and the tail after the semicolon are optional. The
168 Added: `commit` field and the `tests` field are always present.
.kiro/skills/fracas/run-tests.sh
index 00000000..03fe9397 000000..100755
@@ -0,0 +1,35 @@
1 Added: #!/bin/sh
2 Added: # Run the FRACAS test suite in a batch Emacs.
3 Added: #
4 Added: # Required dependency: emacs, with Org mode. Org mode ships with Emacs.
5 Added: # The suite needs no MCP framework and no configuration. It runs on any
6 Added: # machine. Every test uses a temporary stream file, so the project stream
7 Added: # file stays unchanged.
8 Added: #
9 Added: # Exit status: 0 when every test passes, 1 otherwise.
10 Added:
11 Added: set -eu
12 Added:
13 Added: dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
14 Added:
15 Added: if ! command -v emacs >/dev/null 2>&1; then
16 Added: echo "Error: emacs is not on PATH. Install Emacs, then run this script again." >&2
17 Added: exit 1
18 Added: fi
19 Added:
20 Added: for file in fracas-tools.el fracas-tests.el; do
21 Added: if [ ! -r "$dir/$file" ]; then
22 Added: echo "Error: cannot read $dir/$file. The skill directory is incomplete." >&2
23 Added: exit 1
24 Added: fi
25 Added: done
26 Added:
27 Added: if emacs --batch -Q \
28 Added: -l "$dir/fracas-tools.el" \
29 Added: -l "$dir/fracas-tests.el" \
30 Added: -f ert-run-tests-batch-and-exit; then
31 Added: echo "FRACAS tests passed."
32 Added: else
33 Added: echo "Error: FRACAS tests failed. Read the ERT output above for the failing test." >&2
34 Added: exit 1
35 Added: fi