refactor Migrate hito from FRACAS to the reusable RAIL skill

Generalize the project-specific FRACAS skill into RAIL, a Rolling Action Item List manager useful for any project, then move hito onto it. Skill: - Rename the skill and its tools from fracas to rail, with the /rail command. Keep every mechanical behaviour: the flat stream, Org IDs, SCHEDULED and CLOSED timestamps, the result line, the checklist, the append-only logbook, the reason-gated block and cancel, and the evidence-gated complete. - Read the tag vocabulary from the stream file's own #+TAGS: group tags, instead of a hardcoded list. The tools now carry no project vocabulary. A file with no #+TAGS: line accepts any tag. - Generalize all wording from "feature request" to "action item", and remove every hito and Heavy Duty specific. Data: - Rename REQUESTS.org to RAIL.org and add hito's tag axes to the preamble (Kind, Scope, Impact, Misc). The 74 entries are unchanged. Remove the old .kiro/skills/fracas skill, which RAIL supersedes. Validator: 34 checks pass. Tests: 25 pass, including a check that a file without a vocabulary accepts any tag.

Commit
b65dabb668769e29cfbcda77e5697696f8404432
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 afb4cd02..00000000 100644..000000
@@ -1,350 +0,0 @@
1 Removed: ---
2 Removed: name: fracas
3 Removed: description: Manage feature requests with Emacs MCP tools.
4 Removed: ---
5 Removed:
6 Removed: # FRACAS — Feature Requests As a Constant Agent-monitored Stream
7 Removed:
8 Removed: ## Overview
9 Removed:
10 Removed: FRACAS keeps every feature request in one Org file, `REQUESTS.org`.
11 Removed: Dedicated Emacs tools write that file. The work follows three steps:
12 Removed:
13 Removed: 1. Capture the request at the top of the stream.
14 Removed: 2. Track its status while the work runs.
15 Removed: 3. Close it with commit evidence and test evidence.
16 Removed:
17 Removed: ## Usage
18 Removed:
19 Removed: Use this skill when the user does one of these things:
20 Removed:
21 Removed: - The user states a feature request, a defect, or a change to make.
22 Removed: - The user asks you to record or capture a request.
23 Removed: - The user asks for the status of a request, or asks to change it.
24 Removed: - The user asks you to close, cancel, or block a request.
25 Removed: - The user asks what requests are open.
26 Removed:
27 Removed: Treat each prompt as one separate feature request. Do not merge two
28 Removed: requests. Do not build, test, commit, or start an agent for a capture.
29 Removed:
30 Removed: The current prompt is the feature request:
31 Removed:
32 Removed: $ARGUMENTS
33 Removed:
34 Removed: If the prompt is empty, ask the user for one line that describes the
35 Removed: request. Then stop. Capture the next prompt as the next request.
36 Removed:
37 Removed: ## Core Concepts
38 Removed:
39 Removed: ### The stream
40 Removed:
41 Removed: `REQUESTS.org` is a flat stream. Each request is a top-level heading.
42 Removed: The newest request comes first. The file holds no container heading and
43 Removed: no topical heading. Tags on the heading give the kind, the scope, and
44 Removed: the impact. The position of a heading gives only its age.
45 Removed:
46 Removed: ### The Org ID is the handle
47 Removed:
48 Removed: Every request carries an Org ID. Each tool that changes an entry needs
49 Removed: that ID. Never select an entry by its title text, because two titles
50 Removed: can match. Run `fracas-list` to get the ID of each entry.
51 Removed:
52 Removed: ### The file owns the status sequence
53 Removed:
54 Removed: The `#+TODO:` line in the file preamble declares the keyword sequence.
55 Removed: The tools read that line. The tools never declare their own keywords.
56 Removed: To change the keywords, edit the `#+TODO:` line.
57 Removed:
58 Removed: ### Evidence, not silence
59 Removed:
60 Removed: Three transitions demand a written reason or written evidence:
61 Removed:
62 Removed: - `BLOCKED` needs a reason.
63 Removed: - `CANCELLED` needs a reason.
64 Removed: - `DONE` needs a commit hash and a test recap.
65 Removed:
66 Removed: The tools refuse these transitions without that text. A stalled request
67 Removed: or a dropped request therefore always states why.
68 Removed:
69 Removed: ### Which tool sets which state
70 Removed:
71 Removed: Pick the tool from the target state:
72 Removed:
73 Removed: - Target `IN-PROGRESS`, `TESTING`, or `TESTED` → `fracas-set-status`
74 Removed: - Target `BLOCKED` → `fracas-block`, with a reason
75 Removed: - Target `CANCELLED` → `fracas-cancel`, with a reason
76 Removed: - Target `DONE` → `fracas-complete`, with commit and test evidence
77 Removed:
78 Removed: ### The tag vocabulary
79 Removed:
80 Removed: Every request carries one kind tag and one scope tag. The impact axis
81 Removed: and the misc axis are optional. The vocabulary is closed, so
82 Removed: `fracas-capture` rejects any other tag. The tools hold the vocabulary as
83 Removed: an alist keyed by axis. `fracas-inspect` returns the same grouping. The
84 Removed: tags are short and bare, with no axis prefix. The four axes use distinct
85 Removed: words, so a bare tag stays unambiguous.
86 Removed:
87 Removed: Kind — the type of change. Choose one:
88 Removed:
89 Removed: - `feat` — a new capability.
90 Removed: - `fix` — a defect repair.
91 Removed: - `refactor` — a change that keeps the behavior.
92 Removed: - `chore` — maintenance or tooling.
93 Removed: - `docs` — documentation only.
94 Removed:
95 Removed: Scope — the layer that the change touches. Choose one:
96 Removed:
97 Removed: - `core` — pure domain (`hito.core`).
98 Removed: - `app` — service, ports, and adapters (`hito.app`).
99 Removed: - `web` — Dream and dream-html (`hito.web`).
100 Removed: - `ui` — visual style, layout, and interaction.
101 Removed:
102 Removed: Impact — optional. The two tags are opposite ends of one scale, so
103 Removed: choose at most one:
104 Removed:
105 Removed: - `minor` — a small change, such as a cosmetic fix or a one-line fix.
106 Removed: - `major` — a possible major rework.
107 Removed:
108 Removed: Leave the impact axis empty for an ordinary request.
109 Removed:
110 Removed: Misc — optional flags. Add one only when it applies:
111 Removed:
112 Removed: - `doctrine` — the request touches Heavy Duty doctrine, or deviates
113 Removed: from it.
114 Removed:
115 Removed: Infer the tags from the request text when you capture it. You can
116 Removed: re-tag an entry later with `fracas-retag`, as the shape of the feature
117 Removed: changes.
118 Removed:
119 Removed: ## Install the tools once per session
120 Removed:
121 Removed: The tools live in `fracas-tools.el`, beside this file. Load that file
122 Removed: one time in the running Emacs. Use the generic `eval-elisp` tool for
123 Removed: the load only.
124 Removed:
125 Removed: Pass the project directory of your own session in `project`. Do not
126 Removed: write an absolute path from a home directory into the form, because that
127 Removed: path differs on every machine.
128 Removed:
129 Removed: ```elisp
130 Removed: (let* ((project "/the/project/directory/you/work/in")
131 Removed: (tools (expand-file-name ".kiro/skills/fracas/fracas-tools.el"
132 Removed: project)))
133 Removed: (unless (file-readable-p tools)
134 Removed: (error "No FRACAS tools at %s" tools))
135 Removed: (load tools nil t))
136 Removed: ```
137 Removed:
138 Removed: The tool file then finds the project root itself. It searches upward
139 Removed: from its own directory for `REQUESTS.org`. The search assumes no
140 Removed: directory layout, so the same file works on every machine. The result
141 Removed: becomes `fracas-project-root`, and every tool uses it by default.
142 Removed:
143 Removed: Do not depend on the Emacs `default-directory`. That directory often
144 Removed: sits outside the project, and the search then finds no stream file.
145 Removed:
146 Removed: Then verify that the tools are present. This expression returns the
147 Removed: thirteen tool names:
148 Removed:
149 Removed: ```elisp
150 Removed: (seq-filter (lambda (name) (string-prefix-p "fracas-" name))
151 Removed: (mcp-server-tools-list-names))
152 Removed: ```
153 Removed:
154 Removed: If the load fails, stop and report the problem. Do not edit
155 Removed: `REQUESTS.org` as raw text instead.
156 Removed:
157 Removed: ### If the tools are not callable
158 Removed:
159 Removed: The tools register inside Emacs. The MCP client lists its tools when it
160 Removed: connects, so a mid-session load can leave the tools absent from your own
161 Removed: tool list. In that case, call each tool through the dispatch path:
162 Removed:
163 Removed: ```elisp
164 Removed: (mcp-server-tools-call "fracas-list" '((state . "TODO")))
165 Removed: ```
166 Removed:
167 Removed: This path runs the same handler as a direct tool call. To make the tools
168 Removed: callable directly, load `fracas-tools.el` from the Emacs init file.
169 Removed:
170 Removed: ### Why dedicated tools
171 Removed:
172 Removed: The generic `eval-elisp` tool sends its code through the Emacs security
173 Removed: form walker. The walker prompts for each file function, such as
174 Removed: `find-file-noselect` and `save-buffer`. The FRACAS tools register as
175 Removed: normal MCP tools, and tool dispatch does not use the walker. The
176 Removed: read-only tools also carry a `readOnlyHint` annotation, so the client
177 Removed: can approve them without a prompt.
178 Removed:
179 Removed: You **MUST** use these tools for every change to `REQUESTS.org`.
180 Removed:
181 Removed: - You **MUST NOT** edit the file as raw text, because raw edits break
182 Removed: the timestamps, the IDs, and the tag alignment.
183 Removed: - You **MUST NOT** use a shell tool or a file tool for an entry, because
184 Removed: those tools apply no validation.
185 Removed:
186 Removed: ## The FRACAS tools
187 Removed:
188 Removed: Each tool accepts an optional `root` argument. `root` names the project
189 Removed: directory. When you omit it, the tools use `fracas-project-root`. Set
190 Removed: `root` only for a different project.
191 Removed:
192 Removed: | Tool | Purpose | Read-only |
193 Removed: |---|---|---|
194 Removed: | `fracas-inspect` | Report the keyword sequence and the tag axes. | Yes |
195 Removed: | `fracas-list` | List entries, newest first, with ID, title, state, and tags. Accepts a `state` or `tag` filter. | Yes |
196 Removed: | `fracas-show` | Report one entry in full: body, logbook, checklist, and result. | Yes |
197 Removed: | `fracas-verify` | Report the heading, state, `CLOSED` time, and tags of one entry. | Yes |
198 Removed: | `fracas-capture` | Create a `TODO` request at the top of the stream. | No |
199 Removed: | `fracas-set-status` | Set the keyword of an entry. Refuses `DONE`. | No |
200 Removed: | `fracas-block` | Set `BLOCKED` and record a required reason. | No |
201 Removed: | `fracas-cancel` | Set `CANCELLED` and record a required reason. | No |
202 Removed: | `fracas-check` | Add, toggle, or list the checklist items of one request. | No |
203 Removed: | `fracas-log` | Append a timestamped note to the `:LOGBOOK:` drawer. | No |
204 Removed: | `fracas-retag` | Replace the tags of an entry with a validated set. | No |
205 Removed: | `fracas-set-result` | Write the `- result ::` line. | No |
206 Removed: | `fracas-complete` | Set `DONE` after it records the result evidence. | No |
207 Removed:
208 Removed: Use `fracas-show` to read one entry, and `fracas-retag` to re-tag it.
209 Removed: These are the correct tools for those two actions, because the generic
210 Removed: Emacs `org-*` tools cannot operate on this file.
211 Removed:
212 Removed: For the exact arguments and the result shape of each tool, read
213 Removed: `references/tools.md`.
214 Removed:
215 Removed: ## Step 1 — Capture the request
216 Removed:
217 Removed: Run `fracas-capture` with these arguments:
218 Removed:
219 Removed: - `title` — a short imperative title under 60 characters.
220 Removed: - `body` — the full request text.
221 Removed: - `tags` — one kind tag, one scope tag, and any optional tag.
222 Removed:
223 Removed: The tool inserts the entry at the top of the file. It records the
224 Removed: capture time as an inactive `SCHEDULED` timestamp. It applies the tags,
225 Removed: wraps the body to 72 columns, and assigns an Org ID. Report that ID to
226 Removed: the user.
227 Removed:
228 Removed: Keep the request text unchanged. If the text holds separate ideas,
229 Removed: write the body as a list. Pass the body as plain text, because the tool
230 Removed: wraps it.
231 Removed:
232 Removed: If `fracas-capture` rejects a tag, run `fracas-inspect` to read the
233 Removed: vocabulary. Then fix the tag and re-run the capture.
234 Removed:
235 Removed: Then proceed to Step 2 when work starts on the request.
236 Removed:
237 Removed: ## Step 2 — Track the status
238 Removed:
239 Removed: First find the entry. Run `fracas-list` to read the ID, the title, the
240 Removed: state, and the tags of each entry. Pass a `state` filter or a `tag`
241 Removed: filter to narrow the list. Use the ID for every later call.
242 Removed:
243 Removed: Read the current state with `fracas-verify` before you change a
244 Removed: keyword. Then run `fracas-set-status` with one open keyword:
245 Removed:
246 Removed: - `TODO` — the request is captured. Work has not started.
247 Removed: - `IN-PROGRESS` — work has started.
248 Removed: - `TESTING` — the tests run. The tests do not pass.
249 Removed: - `TESTED` — the tests pass. No commit exists.
250 Removed:
251 Removed: Run `fracas-verify` again after the change to confirm the keyword.
252 Removed:
253 Removed: ### Block or cancel with a reason
254 Removed:
255 Removed: Run `fracas-block` when work cannot continue. Run `fracas-cancel` when
256 Removed: you stop work on purpose. Each tool needs a `reason`, and writes it to
257 Removed: the entry.
258 Removed:
259 Removed: You **MUST NOT** use `fracas-set-status` here, because it records no reason.
260 Removed:
261 Removed: ### Split a large request with a checklist
262 Removed:
263 Removed: Run `fracas-check` when one request is large enough to track in parts:
264 Removed:
265 Removed: - `action` `add` — append an unchecked item. Needs `item`.
266 Removed: - `action` `toggle` — flip one matching item. Needs `item`.
267 Removed: - `action` `list` — report the items and their state.
268 Removed:
269 Removed: The tool keeps the items in a `Checklist [/]:` block in the body. The
270 Removed: `[/]` cookie counts the completed items against the total. The items
271 Removed: stay inside the one request. They are not separate entries, and they
272 Removed: carry no keyword of their own. For independent work, capture a new
273 Removed: request instead.
274 Removed:
275 Removed: ### Record progress in the logbook
276 Removed:
277 Removed: Run `fracas-log` with a `note` to record progress during a session. The
278 Removed: tool prepends a timestamped item to the `:LOGBOOK:` drawer. The logbook
279 Removed: is append-only. The tool never rewrites an earlier note, and never
280 Removed: rewrites the body.
281 Removed:
282 Removed: Then proceed to Step 3 when every closing criterion is met.
283 Removed:
284 Removed: ## Step 3 — Close the request
285 Removed:
286 Removed: Check each criterion before you close a request. Copy this list and mark
287 Removed: each item:
288 Removed:
289 Removed: - [ ] The build passes.
290 Removed: - [ ] The tests pass.
291 Removed: - [ ] The files have the required format.
292 Removed: - [ ] A commit exists.
293 Removed:
294 Removed: If one criterion fails, do not close the request. Set the state with
295 Removed: `fracas-set-status` instead. Then fix the failure and re-run this
296 Removed: check.
297 Removed:
298 Removed: When every item is marked, run `fracas-complete` with these arguments:
299 Removed:
300 Removed: - `id` — the Org ID of the entry.
301 Removed: - `commit` — the commit hash of the completed work.
302 Removed: - `tests` — a short test recap, for example `215 pass`.
303 Removed: - `model` — optional. The agent that did the work.
304 Removed: - `notes` — optional. A short tail, such as a root cause.
305 Removed:
306 Removed: The tool writes the result line, sets `DONE`, then confirms that Org
307 Removed: recorded the `CLOSED` timestamp. Org writes that timestamp, because the
308 Removed: file sets `org-log-done` to `time`.
309 Removed:
310 Removed: You **MUST NOT** write or edit that timestamp, because Org owns it.
311 Removed:
312 Removed: The result line takes this form. It stays on one line, because the
313 Removed: reader reads one line:
314 Removed:
315 Removed: ```org
316 Removed: - result :: model=example-agent commit=3051af0 tests=243 pass; short root cause
317 Removed: ```
318 Removed:
319 Removed: The entry stays in place in the stream. There is no refile step.
320 Removed:
321 Removed: Run `fracas-verify` last. Verify that the state is `DONE` and that the
322 Removed: `CLOSED` timestamp is present. To write the result line before you close
323 Removed: the request, run `fracas-set-result` on its own.
324 Removed:
325 Removed: ## Test the tools
326 Removed:
327 Removed: The tools carry a test suite. Run it after any change to
328 Removed: `fracas-tools.el`:
329 Removed:
330 Removed: ```sh
331 Removed: .kiro/skills/fracas/run-tests.sh
332 Removed: ```
333 Removed:
334 Removed: The suite needs no MCP framework and no configuration. Each test uses a
335 Removed: temporary stream file, so the project stream file stays unchanged.
336 Removed:
337 Removed: ## Common mistakes
338 Removed:
339 Removed: - **Editing the file as raw text.** The tools keep the timestamps, the
340 Removed: IDs, and the tag alignment correct. Raw edits do not.
341 Removed: - **Selecting an entry by title.** Two titles can match. Use the Org ID
342 Removed: from `fracas-list`.
343 Removed: - **Using `fracas-set-status` for `DONE`.** That call fails by design.
344 Removed: Use `fracas-complete`, which demands the evidence.
345 Removed: - **Writing a `CLOSED` timestamp.** Org writes it. The tool only
346 Removed: confirms it.
347 Removed: - **Merging two requests.** Record each prompt as its own request.
348 Removed:
349 Removed: If you cannot complete a request, record the request and record the
350 Removed: failure.
.kiro/skills/fracas/fracas-tests.el
index 7639cf2c..00000000 100644..000000
@@ -1,446 +0,0 @@
1 Removed: ;;; fracas-tests.el --- End-to-end tests for the FRACAS tools -*- lexical-binding: t; -*-
2 Removed:
3 Removed: ;;; Commentary:
4 Removed:
5 Removed: ;; These tests exercise the FRACAS handler functions directly. The MCP
6 Removed: ;; framework calls each handler with one alist of arguments, so a direct call
7 Removed: ;; follows the same path as a tool call. Each test runs against a temporary
8 Removed: ;; stream file, so no test touches the project stream file.
9 Removed: ;;
10 Removed: ;; Run the tests with the runner script in this directory:
11 Removed: ;;
12 Removed: ;; ./run-tests.sh
13 Removed:
14 Removed: ;;; Code:
15 Removed:
16 Removed: (require 'ert)
17 Removed: (require 'json)
18 Removed:
19 Removed: ;;; Fixtures
20 Removed:
21 Removed: (defvar fracas-tests--preamble
22 Removed: (concat "#+TITLE: Test stream\n"
23 Removed: "#+TODO: TODO IN-PROGRESS TESTING TESTED BLOCKED | CANCELLED DONE\n"
24 Removed: "\n"
25 Removed: "# Local Variables:\n"
26 Removed: "# org-log-done: time\n"
27 Removed: "# End:\n")
28 Removed: "Preamble of the temporary stream file.
29 Removed: The `#+TODO:' line gives the keyword sequence. The local variable
30 Removed: `org-log-done' makes Org write a CLOSED timestamp.")
31 Removed:
32 Removed: (defun fracas-tests--decode (json-string)
33 Removed: "Return JSON-STRING decoded into Lisp with alists for objects.
34 Removed: Decode JSON null to nil and JSON false to `:json-false', which are the
35 Removed: same sentinels that the handlers encode from. A round trip therefore
36 Removed: gives back the value that the handler started with."
37 Removed: (json-parse-string json-string
38 Removed: :object-type 'alist
39 Removed: :null-object nil
40 Removed: :false-object :json-false))
41 Removed:
42 Removed: (defun fracas-tests--call (handler args)
43 Removed: "Call HANDLER with ARGS and return the decoded result.
44 Removed: Signal an error when the handler reports one, so a failure is visible."
45 Removed: (let ((result (fracas-tests--decode (funcall handler args))))
46 Removed: (when (alist-get 'error result)
47 Removed: (error "Handler failed: %s" (alist-get 'error result)))
48 Removed: result))
49 Removed:
50 Removed: (defmacro fracas-tests--with-stream (root &rest body)
51 Removed: "Create a temporary project directory, bind ROOT to it, then run BODY.
52 Removed: Delete the directory and its buffers after BODY."
53 Removed: (declare (indent 1))
54 Removed: `(let* ((,root (file-name-as-directory (make-temp-file "fracas-test" t)))
55 Removed: (file (expand-file-name fracas-requests-file-name ,root))
56 Removed: (enable-local-variables :all)
57 Removed: (org-id-track-globally nil)
58 Removed: (create-lockfiles nil))
59 Removed: (unwind-protect
60 Removed: (progn
61 Removed: (with-temp-file file (insert fracas-tests--preamble))
62 Removed: ,@body)
63 Removed: (dolist (buf (buffer-list))
64 Removed: (when (and (buffer-file-name buf)
65 Removed: (string-prefix-p ,root (buffer-file-name buf)))
66 Removed: (with-current-buffer buf (set-buffer-modified-p nil))
67 Removed: (kill-buffer buf)))
68 Removed: (delete-directory ,root t))))
69 Removed:
70 Removed: (defun fracas-tests--capture (root title tags &optional body)
71 Removed: "Capture a request in ROOT with TITLE, TAGS, and optional BODY.
72 Removed: Return the new entry's Org ID."
73 Removed: (alist-get 'id (fracas-tests--call
74 Removed: #'fracas-tools--capture-handler
75 Removed: (list (cons 'root root)
76 Removed: (cons 'title title)
77 Removed: (cons 'tags tags)
78 Removed: (cons 'body (or body "Request body."))))))
79 Removed:
80 Removed: (defun fracas-tests--file-text (root)
81 Removed: "Return the text of the stream file in ROOT."
82 Removed: (with-temp-buffer
83 Removed: (insert-file-contents (expand-file-name fracas-requests-file-name root))
84 Removed: (buffer-string)))
85 Removed:
86 Removed: ;;; Root discovery
87 Removed:
88 Removed: (ert-deftest fracas-test-locate-root-finds-the-stream-file ()
89 Removed: "`fracas-locate-root' finds the root from a nested directory."
90 Removed: (fracas-tests--with-stream root
91 Removed: (let ((nested (expand-file-name "a/b/c/" root)))
92 Removed: (make-directory nested t)
93 Removed: (should (equal (fracas-locate-root nested) root)))))
94 Removed:
95 Removed: (ert-deftest fracas-test-locate-root-returns-nil-without-a-stream-file ()
96 Removed: "`fracas-locate-root' returns nil when no ancestor holds the file."
97 Removed: (let ((empty (file-name-as-directory (make-temp-file "fracas-empty" t))))
98 Removed: (unwind-protect
99 Removed: (should (null (fracas-locate-root empty)))
100 Removed: (delete-directory empty t))))
101 Removed:
102 Removed: (ert-deftest fracas-test-root-argument-overrides-the-default ()
103 Removed: "An explicit `root' argument selects the file, not `fracas-project-root'."
104 Removed: (fracas-tests--with-stream root
105 Removed: (should (equal (fracas-tools--file (list (cons 'root root)))
106 Removed: (expand-file-name fracas-requests-file-name root)))))
107 Removed:
108 Removed: (ert-deftest fracas-test-absent-root-uses-the-default ()
109 Removed: "An absent `root' argument falls back to `fracas-project-root'."
110 Removed: (fracas-tests--with-stream root
111 Removed: (let ((fracas-project-root root))
112 Removed: (should (equal (fracas-tools--file nil)
113 Removed: (expand-file-name fracas-requests-file-name root))))))
114 Removed:
115 Removed: (ert-deftest fracas-test-a-missing-directory-signals-an-error ()
116 Removed: "A `root' that is not a directory signals an error."
117 Removed: (should-error (fracas-tools--file
118 Removed: (list (cons 'root "/fracas/no/such/directory")))))
119 Removed:
120 Removed: ;;; Capture
121 Removed:
122 Removed: (ert-deftest fracas-test-capture-creates-an-addressable-todo ()
123 Removed: "Capture writes a TODO entry with an ID, a SCHEDULED time, and tags."
124 Removed: (fracas-tests--with-stream root
125 Removed: (let* ((id (fracas-tests--capture root "Add a widget" ["feat" "web"]))
126 Removed: (entry (fracas-tests--call #'fracas-tools--show-handler
127 Removed: (list (cons 'root root) (cons 'id id)))))
128 Removed: (should (stringp id))
129 Removed: (should (equal (alist-get 'state entry) "TODO"))
130 Removed: (should (equal (alist-get 'heading entry) "Add a widget"))
131 Removed: (should (equal (append (alist-get 'tags entry) nil) '("feat" "web")))
132 Removed: (should (stringp (alist-get 'scheduled entry)))
133 Removed: (should (null (alist-get 'closed entry))))))
134 Removed:
135 Removed: (ert-deftest fracas-test-capture-puts-the-newest-request-first ()
136 Removed: "Capture inserts each new request above the previous request."
137 Removed: (fracas-tests--with-stream root
138 Removed: (fracas-tests--capture root "First request" ["feat" "core"])
139 Removed: (fracas-tests--capture root "Second request" ["fix" "app"])
140 Removed: (let ((rows (fracas-tests--decode
141 Removed: (fracas-tools--list-handler (list (cons 'root root))))))
142 Removed: (should (equal (length rows) 2))
143 Removed: (should (equal (alist-get 'title (aref rows 0)) "Second request"))
144 Removed: (should (equal (alist-get 'title (aref rows 1)) "First request")))))
145 Removed:
146 Removed: (ert-deftest fracas-test-capture-rejects-a-tag-outside-the-vocabulary ()
147 Removed: "Capture rejects any tag that the closed vocabulary does not hold."
148 Removed: (fracas-tests--with-stream root
149 Removed: (let ((result (fracas-tests--decode
150 Removed: (fracas-tools--capture-handler
151 Removed: (list (cons 'root root)
152 Removed: (cons 'title "Bad tags")
153 Removed: (cons 'tags ["feat" "trivial"]))))))
154 Removed: (should (string-match-p "Unknown tag" (alist-get 'error result))))))
155 Removed:
156 Removed: ;;; Status
157 Removed:
158 Removed: (ert-deftest fracas-test-set-status-moves-through-the-open-keywords ()
159 Removed: "Set-status accepts each open keyword from the file's own sequence."
160 Removed: (fracas-tests--with-stream root
161 Removed: (let ((id (fracas-tests--capture root "Track status" ["feat" "core"])))
162 Removed: (dolist (state '("IN-PROGRESS" "TESTING" "TESTED"))
163 Removed: (let ((result (fracas-tests--call
164 Removed: #'fracas-tools--set-status-handler
165 Removed: (list (cons 'root root) (cons 'id id)
166 Removed: (cons 'state state)))))
167 Removed: (should (equal (alist-get 'state result) state))))
168 Removed: (should (equal (alist-get 'state (fracas-tests--call
169 Removed: #'fracas-tools--verify-handler
170 Removed: (list (cons 'root root) (cons 'id id))))
171 Removed: "TESTED")))))
172 Removed:
173 Removed: (ert-deftest fracas-test-set-status-refuses-done ()
174 Removed: "Set-status refuses DONE, because completion needs result evidence."
175 Removed: (fracas-tests--with-stream root
176 Removed: (let* ((id (fracas-tests--capture root "Refuse done" ["feat" "core"]))
177 Removed: (result (fracas-tests--decode
178 Removed: (fracas-tools--set-status-handler
179 Removed: (list (cons 'root root) (cons 'id id)
180 Removed: (cons 'state "DONE"))))))
181 Removed: (should (string-match-p "fracas-complete" (alist-get 'error result))))))
182 Removed:
183 Removed: (ert-deftest fracas-test-block-and-cancel-record-a-reason ()
184 Removed: "Block and cancel write the reason, so the decision is never silent."
185 Removed: (fracas-tests--with-stream root
186 Removed: (let ((blocked (fracas-tests--capture root "Blocked work" ["feat" "app"]))
187 Removed: (dropped (fracas-tests--capture root "Dropped work" ["feat" "ui"])))
188 Removed: (fracas-tests--call #'fracas-tools--block-handler
189 Removed: (list (cons 'root root) (cons 'id blocked)
190 Removed: (cons 'reason "The route does not exist")))
191 Removed: (fracas-tests--call #'fracas-tools--cancel-handler
192 Removed: (list (cons 'root root) (cons 'id dropped)
193 Removed: (cons 'reason "The user withdrew the request")))
194 Removed: (let ((text (fracas-tests--file-text root)))
195 Removed: (should (string-match-p "- blocked :: The route does not exist" text))
196 Removed: (should (string-match-p "- cancelled :: The user withdrew" text)))
197 Removed: (should (equal "BLOCKED"
198 Removed: (alist-get 'state (fracas-tests--call
199 Removed: #'fracas-tools--verify-handler
200 Removed: (list (cons 'root root)
201 Removed: (cons 'id blocked))))))
202 Removed: (should (equal "CANCELLED"
203 Removed: (alist-get 'state (fracas-tests--call
204 Removed: #'fracas-tools--verify-handler
205 Removed: (list (cons 'root root)
206 Removed: (cons 'id dropped)))))))))
207 Removed:
208 Removed: (ert-deftest fracas-test-block-requires-a-reason ()
209 Removed: "Block refuses a blank reason."
210 Removed: (fracas-tests--with-stream root
211 Removed: (let* ((id (fracas-tests--capture root "Needs a reason" ["feat" "app"]))
212 Removed: (result (fracas-tests--decode
213 Removed: (fracas-tools--block-handler
214 Removed: (list (cons 'root root) (cons 'id id)
215 Removed: (cons 'reason " "))))))
216 Removed: (should (string-match-p "reason" (alist-get 'error result))))))
217 Removed:
218 Removed: ;;; Checklist, logbook, and tags
219 Removed:
220 Removed: (ert-deftest fracas-test-checklist-adds-and-toggles-items ()
221 Removed: "The checklist adds an item, then toggles it, and reports booleans."
222 Removed: (fracas-tests--with-stream root
223 Removed: (let ((id (fracas-tests--capture root "Split the task" ["feat" "core"])))
224 Removed: (let ((added (fracas-tests--call
225 Removed: #'fracas-tools--check-handler
226 Removed: (list (cons 'root root) (cons 'id id)
227 Removed: (cons 'action "add") (cons 'item "step one")))))
228 Removed: (should (equal (alist-get 'done (aref (alist-get 'items added) 0))
229 Removed: :json-false)))
230 Removed: (let ((toggled (fracas-tests--call
231 Removed: #'fracas-tools--check-handler
232 Removed: (list (cons 'root root) (cons 'id id)
233 Removed: (cons 'action "toggle") (cons 'item "step one")))))
234 Removed: (should (eq (alist-get 'done (aref (alist-get 'items toggled) 0)) t)))
235 Removed: (should (string-match-p "Checklist \\[1/1\\]" (fracas-tests--file-text root))))))
236 Removed:
237 Removed: (ert-deftest fracas-test-log-appends-and-keeps-earlier-notes ()
238 Removed: "The logbook keeps every note, newest first."
239 Removed: (fracas-tests--with-stream root
240 Removed: (let ((id (fracas-tests--capture root "Log progress" ["feat" "core"])))
241 Removed: (fracas-tests--call #'fracas-tools--log-handler
242 Removed: (list (cons 'root root) (cons 'id id)
243 Removed: (cons 'note "First note")))
244 Removed: (fracas-tests--call #'fracas-tools--log-handler
245 Removed: (list (cons 'root root) (cons 'id id)
246 Removed: (cons 'note "Second note")))
247 Removed: (let* ((entry (fracas-tests--call #'fracas-tools--show-handler
248 Removed: (list (cons 'root root) (cons 'id id))))
249 Removed: (notes (append (alist-get 'logbook entry) nil)))
250 Removed: (should (equal (length notes) 2))
251 Removed: (should (string-match-p "Second note" (nth 0 notes)))
252 Removed: (should (string-match-p "First note" (nth 1 notes)))))))
253 Removed:
254 Removed: (ert-deftest fracas-test-retag-validates-against-the-vocabulary ()
255 Removed: "Retag replaces the tags, and refuses a tag outside the vocabulary."
256 Removed: (fracas-tests--with-stream root
257 Removed: (let ((id (fracas-tests--capture root "Retag me" ["feat" "core"])))
258 Removed: (let ((result (fracas-tests--call
259 Removed: #'fracas-tools--retag-handler
260 Removed: (list (cons 'root root) (cons 'id id)
261 Removed: (cons 'tags ["fix" "web" "minor"])))))
262 Removed: (should (equal (append (alist-get 'tags result) nil)
263 Removed: '("fix" "web" "minor"))))
264 Removed: (let ((result (fracas-tests--decode
265 Removed: (fracas-tools--retag-handler
266 Removed: (list (cons 'root root) (cons 'id id)
267 Removed: (cons 'tags ["nonsense"]))))))
268 Removed: (should (string-match-p "Unknown tag" (alist-get 'error result)))))))
269 Removed:
270 Removed: ;;; Result and completion
271 Removed:
272 Removed: (ert-deftest fracas-test-set-result-writes-model-and-notes ()
273 Removed: "The result line holds the model, the commit, the tests, and the notes."
274 Removed: (fracas-tests--with-stream root
275 Removed: (let* ((id (fracas-tests--capture root "Record a result" ["fix" "core"]))
276 Removed: (result (fracas-tests--call
277 Removed: #'fracas-tools--set-result-handler
278 Removed: (list (cons 'root root) (cons 'id id)
279 Removed: (cons 'commit "abc1234") (cons 'tests "246 pass")
280 Removed: (cons 'model "test-agent")
281 Removed: (cons 'notes "the sentinel was wrong")))))
282 Removed: (should (equal (alist-get 'result result)
283 Removed: "model=test-agent commit=abc1234 tests=246 pass; the sentinel was wrong")))))
284 Removed:
285 Removed: (ert-deftest fracas-test-set-result-omits-absent-optional-fields ()
286 Removed: "The result line holds only the commit and the tests when nothing else is given."
287 Removed: (fracas-tests--with-stream root
288 Removed: (let* ((id (fracas-tests--capture root "Plain result" ["fix" "core"]))
289 Removed: (result (fracas-tests--call
290 Removed: #'fracas-tools--set-result-handler
291 Removed: (list (cons 'root root) (cons 'id id)
292 Removed: (cons 'commit "abc1234") (cons 'tests "246 pass")))))
293 Removed: (should (equal (alist-get 'result result)
294 Removed: "commit=abc1234 tests=246 pass")))))
295 Removed:
296 Removed: (ert-deftest fracas-test-set-result-replaces-an-earlier-line ()
297 Removed: "A second result call replaces the earlier result line."
298 Removed: (fracas-tests--with-stream root
299 Removed: (let ((id (fracas-tests--capture root "Replace result" ["fix" "core"])))
300 Removed: (dolist (commit '("aaa1111" "bbb2222"))
301 Removed: (fracas-tests--call #'fracas-tools--set-result-handler
302 Removed: (list (cons 'root root) (cons 'id id)
303 Removed: (cons 'commit commit) (cons 'tests "1 pass"))))
304 Removed: (let ((text (fracas-tests--file-text root)))
305 Removed: (should-not (string-match-p "aaa1111" text))
306 Removed: (should (string-match-p "bbb2222" text))))))
307 Removed:
308 Removed: (ert-deftest fracas-test-complete-sets-done-with-result-and-closed ()
309 Removed: "Completion writes the result, sets DONE, and confirms the CLOSED time."
310 Removed: (fracas-tests--with-stream root
311 Removed: (let* ((id (fracas-tests--capture root "Finish the work" ["feat" "core"]))
312 Removed: (result (fracas-tests--call
313 Removed: #'fracas-tools--complete-handler
314 Removed: (list (cons 'root root) (cons 'id id)
315 Removed: (cons 'commit "def5678") (cons 'tests "247 pass")
316 Removed: (cons 'model "test-agent")))))
317 Removed: (should (equal (alist-get 'state result) "DONE"))
318 Removed: (should (string-match-p "commit=def5678" (alist-get 'result result)))
319 Removed: (should (string-match-p "\\[.*\\]" (alist-get 'closed result)))
320 Removed: (let ((entry (fracas-tests--call #'fracas-tools--verify-handler
321 Removed: (list (cons 'root root) (cons 'id id)))))
322 Removed: (should (equal (alist-get 'state entry) "DONE"))
323 Removed: (should (stringp (alist-get 'closed entry)))))))
324 Removed:
325 Removed: (ert-deftest fracas-test-complete-refuses-a-file-without-closed-logging ()
326 Removed: "Completion refuses to run when the file does not log a CLOSED time.
327 Removed: Org must write the CLOSED timestamp, so the tool never writes it."
328 Removed: (let* ((root (file-name-as-directory (make-temp-file "fracas-nolog" t)))
329 Removed: (file (expand-file-name fracas-requests-file-name root))
330 Removed: (enable-local-variables :all)
331 Removed: (org-id-track-globally nil)
332 Removed: (create-lockfiles nil)
333 Removed: (org-log-done nil))
334 Removed: (unwind-protect
335 Removed: (progn
336 Removed: ;; This preamble holds no `org-log-done' local variable.
337 Removed: (with-temp-file file
338 Removed: (insert "#+TITLE: No logging\n"
339 Removed: "#+TODO: TODO IN-PROGRESS | CANCELLED DONE\n"))
340 Removed: (let ((id (fracas-tests--capture root "No logging" ["feat" "core"])))
341 Removed: ;; Close the buffer, so the next open reads the local variables
342 Removed: ;; of the file as it now stands.
343 Removed: (dolist (buf (buffer-list))
344 Removed: (when (equal (buffer-file-name buf) file)
345 Removed: (with-current-buffer buf (set-buffer-modified-p nil))
346 Removed: (kill-buffer buf)))
347 Removed: (let ((result (fracas-tests--decode
348 Removed: (fracas-tools--complete-handler
349 Removed: (list (cons 'root root) (cons 'id id)
350 Removed: (cons 'commit "def5678")
351 Removed: (cons 'tests "1 pass"))))))
352 Removed: (should (stringp (alist-get 'error result)))
353 Removed: (should (string-match-p "org-log-done"
354 Removed: (alist-get 'error result))))))
355 Removed: (dolist (buf (buffer-list))
356 Removed: (when (and (buffer-file-name buf)
357 Removed: (string-prefix-p root (buffer-file-name buf)))
358 Removed: (with-current-buffer buf (set-buffer-modified-p nil))
359 Removed: (kill-buffer buf)))
360 Removed: (delete-directory root t))))
361 Removed:
362 Removed: ;;; Reading
363 Removed:
364 Removed: (ert-deftest fracas-test-list-encodes-an-absent-value-as-json-null ()
365 Removed: "A heading without an ID reports JSON null, not the text \"null\"."
366 Removed: (fracas-tests--with-stream root
367 Removed: (let ((file (expand-file-name fracas-requests-file-name root)))
368 Removed: (with-current-buffer (find-file-noselect file)
369 Removed: (goto-char (point-min))
370 Removed: (insert "* TODO Entry without an identifier :feat:core:\n")
371 Removed: (save-buffer))
372 Removed: (let* ((json (fracas-tools--list-handler (list (cons 'root root))))
373 Removed: (rows (fracas-tests--decode json)))
374 Removed: (should (null (alist-get 'id (aref rows 0))))
375 Removed: (should-not (string-match-p "\"null\"" json))))))
376 Removed:
377 Removed: (ert-deftest fracas-test-list-filters-by-state-and-tag ()
378 Removed: "The list filters narrow the result by TODO keyword and by tag."
379 Removed: (fracas-tests--with-stream root
380 Removed: (let ((first (fracas-tests--capture root "Filter one" ["feat" "core"])))
381 Removed: (fracas-tests--capture root "Filter two" ["fix" "web"])
382 Removed: (fracas-tests--call #'fracas-tools--set-status-handler
383 Removed: (list (cons 'root root) (cons 'id first)
384 Removed: (cons 'state "IN-PROGRESS")))
385 Removed: (let ((by-state (fracas-tests--decode
386 Removed: (fracas-tools--list-handler
387 Removed: (list (cons 'root root) (cons 'state "IN-PROGRESS")))))
388 Removed: (by-tag (fracas-tests--decode
389 Removed: (fracas-tools--list-handler
390 Removed: (list (cons 'root root) (cons 'tag "web"))))))
391 Removed: (should (equal (length by-state) 1))
392 Removed: (should (equal (alist-get 'title (aref by-state 0)) "Filter one"))
393 Removed: (should (equal (length by-tag) 1))
394 Removed: (should (equal (alist-get 'title (aref by-tag 0)) "Filter two"))))))
395 Removed:
396 Removed: (ert-deftest fracas-test-inspect-reports-the-sequence-and-the-axes ()
397 Removed: "Inspect reports the file's keyword sequence and the tag axes."
398 Removed: (fracas-tests--with-stream root
399 Removed: (let* ((result (fracas-tests--call #'fracas-tools--inspect-handler
400 Removed: (list (cons 'root root))))
401 Removed: (keywords (append (alist-get 'todo_keywords result) nil))
402 Removed: (axes (alist-get 'tags result)))
403 Removed: (should (member "IN-PROGRESS" keywords))
404 Removed: (should (member "DONE" keywords))
405 Removed: (should (equal (mapcar #'car axes) '(kind scope impact misc)))
406 Removed: (should (equal (append (alist-get 'impact axes) nil) '("minor" "major"))))))
407 Removed:
408 Removed: ;;; The full path
409 Removed:
410 Removed: (ert-deftest fracas-test-the-full-request-path-runs-end-to-end ()
411 Removed: "One request moves from capture to DONE through every step."
412 Removed: (fracas-tests--with-stream root
413 Removed: (let ((id (fracas-tests--capture root "Ship the feature" ["feat" "web"]
414 Removed: "Add a widget to the page.")))
415 Removed: (fracas-tests--call #'fracas-tools--check-handler
416 Removed: (list (cons 'root root) (cons 'id id)
417 Removed: (cons 'action "add") (cons 'item "write the code")))
418 Removed: (fracas-tests--call #'fracas-tools--set-status-handler
419 Removed: (list (cons 'root root) (cons 'id id)
420 Removed: (cons 'state "IN-PROGRESS")))
421 Removed: (fracas-tests--call #'fracas-tools--log-handler
422 Removed: (list (cons 'root root) (cons 'id id)
423 Removed: (cons 'note "Started the work")))
424 Removed: (fracas-tests--call #'fracas-tools--check-handler
425 Removed: (list (cons 'root root) (cons 'id id)
426 Removed: (cons 'action "toggle")
427 Removed: (cons 'item "write the code")))
428 Removed: (fracas-tests--call #'fracas-tools--set-status-handler
429 Removed: (list (cons 'root root) (cons 'id id)
430 Removed: (cons 'state "TESTED")))
431 Removed: (fracas-tests--call #'fracas-tools--complete-handler
432 Removed: (list (cons 'root root) (cons 'id id)
433 Removed: (cons 'commit "0badc0de") (cons 'tests "3 pass")
434 Removed: (cons 'model "test-agent")))
435 Removed: (let ((entry (fracas-tests--call #'fracas-tools--show-handler
436 Removed: (list (cons 'root root) (cons 'id id)))))
437 Removed: (should (equal (alist-get 'state entry) "DONE"))
438 Removed: (should (stringp (alist-get 'closed entry)))
439 Removed: (should (string-match-p "commit=0badc0de" (alist-get 'result entry)))
440 Removed: (should (equal (length (alist-get 'logbook entry)) 1))
441 Removed: (should (eq (alist-get 'done (aref (alist-get 'checklist entry) 0)) t))
442 Removed: (should (string-match-p "Add a widget" (alist-get 'body entry)))))))
443 Removed:
444 Removed: (provide 'fracas-tests)
445 Removed:
446 Removed: ;;; fracas-tests.el ends here
.kiro/skills/fracas/fracas-tools.el
index 4c139e1d..00000000 100644..000000
@@ -1,937 +0,0 @@
1 Removed: ;;; fracas-tools.el --- FRACAS MCP tools for REQUESTS.org -*- lexical-binding: t; -*-
2 Removed:
3 Removed: ;; Copyright (C) 2025
4 Removed:
5 Removed: ;; This file is NOT part of GNU Emacs.
6 Removed:
7 Removed: ;;; Commentary:
8 Removed:
9 Removed: ;; FRACAS means "Feature Requests As a Constant Agent-monitored Stream".
10 Removed: ;;
11 Removed: ;; This file registers dedicated Emacs MCP tools for the FRACAS skill so the
12 Removed: ;; agent does not run raw `eval-elisp' snippets for each capture, status
13 Removed: ;; change, log, result, or verification. Each operation becomes a named tool.
14 Removed: ;;
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. The file holds no container
17 Removed: ;; heading.
18 Removed: ;;
19 Removed: ;; Why dedicated tools instead of `eval-elisp'?
20 Removed: ;;
21 Removed: ;; The `eval-elisp' tool routes its argument through
22 Removed: ;; `mcp-server-security-safe-eval', whose form walker blocks or prompts for
23 Removed: ;; "dangerous" functions such as `find-file-noselect', `write-file', and
24 Removed: ;; `save-buffer'. With `mcp-server-security-prompt-for-permissions' set to t,
25 Removed: ;; every FRACAS snippet triggers a minibuffer prompt.
26 Removed: ;;
27 Removed: ;; A registered MCP tool runs through `mcp-server-tools-call', which calls the
28 Removed: ;; handler function directly and does NOT pass through the form walker. The
29 Removed: ;; handlers below therefore run without the repeated security prompt. Each
30 Removed: ;; tool also carries MCP `annotations' so the MCP client can auto-approve the
31 Removed: ;; read-only tools.
32 Removed: ;;
33 Removed: ;; The tools operate only on the file "REQUESTS.org". They find that file
34 Removed: ;; under `fracas-project-root', or under a caller-supplied project ROOT that
35 Removed: ;; overrides it. They never touch any other file.
36 Removed: ;;
37 Removed: ;; `fracas-project-root' comes from an upward search for the stream file. The
38 Removed: ;; search starts at this file's own directory, then at `default-directory'. It
39 Removed: ;; assumes no directory layout, so this file needs no absolute path and it
40 Removed: ;; works on every machine.
41 Removed: ;;
42 Removed: ;; Install the tools once per Emacs session. Load this file, and the tools
43 Removed: ;; register themselves. With `mcp-server-emacs-tools-enabled' set to `all',
44 Removed: ;; which is the default, they appear in the MCP tool list at once.
45 Removed: ;;
46 Removed: ;; The MCP framework is a soft dependency. When the framework is absent, for
47 Removed: ;; example in a batch test run, this file still loads and every handler stays
48 Removed: ;; callable. Run the test suite with the run-tests.sh script beside this file.
49 Removed:
50 Removed: ;;; Code:
51 Removed:
52 Removed: (require 'cl-lib)
53 Removed: (require 'org)
54 Removed: (require 'org-id)
55 Removed: (require 'json)
56 Removed: (require 'subr-x)
57 Removed:
58 Removed: ;; Load the MCP tool framework when it is available. When it is absent, for
59 Removed: ;; example in a batch test run, define the two symbols the registrations below
60 Removed: ;; need and discard each registration. The handler functions stay callable, so
61 Removed: ;; the test suite runs on any machine without the framework.
62 Removed: (defconst fracas-mcp-available (require 'mcp-server-tools nil t)
63 Removed: "Non-nil when the Emacs MCP tool framework is available.")
64 Removed:
65 Removed: (unless fracas-mcp-available
66 Removed: ;; Define plain functions, never a struct. A stub struct would clobber the
67 Removed: ;; real slot layout if the framework loads later in the same session.
68 Removed: (defun make-mcp-server-tool (&rest _args)
69 Removed: "Return nil. The MCP framework is absent."
70 Removed: nil)
71 Removed: (defun mcp-server-register-tool (_tool)
72 Removed: "Discard _TOOL. The MCP framework is absent."
73 Removed: nil))
74 Removed:
75 Removed: (defvar fracas-requests-file-name "REQUESTS.org"
76 Removed: "Name of the Org file that FRACAS manages.")
77 Removed:
78 Removed: (defvar fracas-tools-path
79 Removed: (let ((file (or load-file-name buffer-file-name)))
80 Removed: (and file (expand-file-name file)))
81 Removed: "Absolute path of this file, or nil when the path is unknown.")
82 Removed:
83 Removed: (defun fracas-locate-root (start)
84 Removed: "Return the closest directory at or above START that holds the stream file.
85 Removed: The stream file is `fracas-requests-file-name'. Return nil when no
86 Removed: ancestor directory holds that file."
87 Removed: (let ((dir (and start (locate-dominating-file
88 Removed: (file-name-as-directory (expand-file-name start))
89 Removed: fracas-requests-file-name))))
90 Removed: (and dir (expand-file-name (file-name-as-directory dir)))))
91 Removed:
92 Removed: (defvar fracas-project-root
93 Removed: (or (fracas-locate-root (and fracas-tools-path
94 Removed: (file-name-directory fracas-tools-path)))
95 Removed: (fracas-locate-root default-directory)
96 Removed: (expand-file-name default-directory))
97 Removed: "Default project directory that holds the FRACAS stream file.
98 Removed: The value comes from an upward search for `fracas-requests-file-name',
99 Removed: first from this file's own directory, then from `default-directory'.
100 Removed: The search makes no assumption about the depth of this file in the
101 Removed: project. Set this variable to override the search, or pass a `root'
102 Removed: argument to any tool.")
103 Removed:
104 Removed: ;;; Helpers
105 Removed:
106 Removed: (defun fracas-tools--file (args)
107 Removed: "Return the absolute path of the stream file for ARGS.
108 Removed: ARGS may hold a `root' string that names the project directory. When
109 Removed: `root' is absent, use `fracas-project-root'. Signal an error when the
110 Removed: selected root is not a directory."
111 Removed: (let ((root (or (alist-get 'root args) fracas-project-root)))
112 Removed: (unless (and (stringp root) (> (length root) 0))
113 Removed: (error "No project root: pass `root' or set `fracas-project-root'"))
114 Removed: (let ((dir (expand-file-name root)))
115 Removed: (unless (file-directory-p dir)
116 Removed: (error "Not a directory: %s" dir))
117 Removed: (expand-file-name fracas-requests-file-name dir))))
118 Removed:
119 Removed: (defun fracas-tools--buffer (file)
120 Removed: "Return an org-mode buffer visiting FILE, creating it as needed."
121 Removed: (let ((buf (find-file-noselect file)))
122 Removed: (with-current-buffer buf
123 Removed: (unless (derived-mode-p 'org-mode)
124 Removed: (org-mode)))
125 Removed: buf))
126 Removed:
127 Removed: (defun fracas-tools--goto-id (id)
128 Removed: "Move point to the heading with Org ID in the current buffer.
129 Removed: Signal an error when ID is not found."
130 Removed: (let ((marker (org-id-find id 'marker)))
131 Removed: (unless marker
132 Removed: (error "Org ID not found: %s" id))
133 Removed: (goto-char marker)))
134 Removed:
135 Removed: (defun fracas-tools--fill-body ()
136 Removed: "Wrap the body of the entry at point to 72 columns.
137 Removed: Fill every paragraph after the metadata (SCHEDULED line, property
138 Removed: drawer) up to the next heading. Use `org-fill-paragraph' so Org
139 Removed: list items and other structure fill correctly. Point must be on
140 Removed: the entry heading."
141 Removed: (let ((fill-column 72))
142 Removed: (org-back-to-heading t)
143 Removed: (let ((end (save-excursion (org-end-of-subtree t t) (point-marker))))
144 Removed: ;; Move past the heading and all metadata (planning line,
145 Removed: ;; property drawer, logbook) to the first line of body text.
146 Removed: (org-end-of-meta-data t)
147 Removed: ;; Fill each body line. `org-fill-paragraph' fills the whole
148 Removed: ;; element and is idempotent, so stepping one line at a time is
149 Removed: ;; safe and does not overshoot a trailing paragraph.
150 Removed: (while (< (point) end)
151 Removed: (unless (looking-at-p "^[ \t]*$")
152 Removed: (org-fill-paragraph))
153 Removed: (forward-line 1))
154 Removed: (set-marker end nil))))
155 Removed:
156 Removed: (defun fracas-tools--nonblank (value)
157 Removed: "Return VALUE trimmed when it is a non-blank string, else nil."
158 Removed: (and (stringp value)
159 Removed: (let ((trimmed (string-trim value)))
160 Removed: (and (> (length trimmed) 0) trimmed))))
161 Removed:
162 Removed: (defun fracas-tools--set-result (commit tests &optional model notes)
163 Removed: "Write the structured result line for the entry at point.
164 Removed: Replace an existing `- result ::' line, or append one at the end of
165 Removed: the entry body. COMMIT is a commit hash. TESTS is a short recap
166 Removed: such as \"215 pass\". MODEL names the agent that did the work, and
167 Removed: NOTES adds a free-text tail after a semicolon. Both are optional.
168 Removed: Keep the line unwrapped, because the reader captures a single line.
169 Removed: Point must be on the entry heading."
170 Removed: (org-back-to-heading t)
171 Removed: (let* ((model (fracas-tools--nonblank model))
172 Removed: (notes (fracas-tools--nonblank notes))
173 Removed: (subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
174 Removed: (line (concat "- result :: "
175 Removed: (if model (format "model=%s " model) "")
176 Removed: (format "commit=%s tests=%s" commit tests)
177 Removed: (if notes (format "; %s" notes) ""))))
178 Removed: (org-back-to-heading t)
179 Removed: (if (re-search-forward "^[ \t]*- result ::.*$" subtree-end t)
180 Removed: (replace-match line t t)
181 Removed: (goto-char subtree-end)
182 Removed: (skip-chars-backward "\n")
183 Removed: (insert "\n\n" line))
184 Removed: (set-marker subtree-end nil)))
185 Removed:
186 Removed: (defun fracas-tools--append-log (note)
187 Removed: "Append NOTE as a timestamped item to the entry's `:LOGBOOK:' drawer.
188 Removed: Create the drawer directly after the metadata when it is absent.
189 Removed: Insert the newest item first and wrap it to 72 columns. This is
190 Removed: append-only. It never edits an existing item or the body. Point
191 Removed: must be on the entry heading."
192 Removed: (let ((fill-column 72))
193 Removed: (org-back-to-heading t)
194 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
195 Removed: (ts (format-time-string "[%Y-%m-%d %a %H:%M]")))
196 Removed: (org-back-to-heading t)
197 Removed: (let ((drawer-start
198 Removed: (save-excursion
199 Removed: (when (re-search-forward "^[ \t]*:LOGBOOK:[ \t]*$" subtree-end t)
200 Removed: (line-beginning-position)))))
201 Removed: (unless drawer-start
202 Removed: (org-end-of-meta-data t)
203 Removed: (insert ":LOGBOOK:\n:END:\n")
204 Removed: (setq drawer-start
205 Removed: (save-excursion
206 Removed: (org-back-to-heading t)
207 Removed: (re-search-forward "^[ \t]*:LOGBOOK:[ \t]*$" subtree-end t)
208 Removed: (line-beginning-position))))
209 Removed: (goto-char drawer-start)
210 Removed: (forward-line 1)
211 Removed: (let ((item-start (point)))
212 Removed: (insert (format "- %s %s\n" ts note))
213 Removed: (save-excursion
214 Removed: (goto-char item-start)
215 Removed: (org-fill-paragraph))))
216 Removed: (set-marker subtree-end nil))))
217 Removed:
218 Removed: (defmacro fracas-tools--json (&rest body)
219 Removed: "Evaluate BODY and return its value as a JSON string.
220 Removed: Catch any error and return a JSON object with an `error' field."
221 Removed: (declare (indent 0))
222 Removed: `(condition-case err
223 Removed: (json-encode (progn ,@body))
224 Removed: (error (json-encode `((error . ,(error-message-string err)))))))
225 Removed:
226 Removed: ;;; Tag vocabulary
227 Removed:
228 Removed: (defconst fracas-tools--tag-axes
229 Removed: '((kind . ("feat" "fix" "refactor" "chore" "docs"))
230 Removed: (scope . ("core" "app" "web" "ui"))
231 Removed: (impact . ("minor" "major"))
232 Removed: (misc . ("doctrine")))
233 Removed: "Closed tag vocabulary for FRACAS request headings, grouped by axis.
234 Removed: A request carries one `kind' tag (feat, fix, refactor, chore, docs) and
235 Removed: one `scope' tag (core, app, web, ui). Two axes are optional: `impact'
236 Removed: takes at most one of `minor' or `major', which are opposite ends of one
237 Removed: scale, and `misc' holds free flags, currently `doctrine' for a request
238 Removed: that touches Heavy Duty doctrine.
239 Removed: The axes use distinct words, so a bare tag stays unambiguous.")
240 Removed:
241 Removed: (defun fracas-tools--all-tags ()
242 Removed: "Return every tag in the vocabulary as one flat list of strings."
243 Removed: (apply #'append (mapcar #'cdr fracas-tools--tag-axes)))
244 Removed:
245 Removed: (defun fracas-tools--check-tags (tags)
246 Removed: "Signal an error when TAGS holds a value outside the vocabulary.
247 Removed: TAGS is a list of strings. Return TAGS unchanged when valid."
248 Removed: (let ((allowed (fracas-tools--all-tags)))
249 Removed: (dolist (tag tags)
250 Removed: (unless (member tag allowed)
251 Removed: (error "Unknown tag `%s'; allowed: %s"
252 Removed: tag (string-join allowed ", ")))))
253 Removed: tags)
254 Removed:
255 Removed: (defun fracas-tools--goto-stream-top ()
256 Removed: "Move point to the insertion place for a new request.
257 Removed: That place is the start of the first top-level heading, after the
258 Removed: file preamble. When no heading exists, move to the end of the
259 Removed: preamble."
260 Removed: (goto-char (point-min))
261 Removed: (if (re-search-forward "^\\* " nil t)
262 Removed: (goto-char (line-beginning-position))
263 Removed: (goto-char (point-max))))
264 Removed:
265 Removed: ;;; inspect (read-only)
266 Removed:
267 Removed: (defun fracas-tools--inspect-handler (args)
268 Removed: "Report the TODO sequence and the tag vocabulary for REQUESTS.org."
269 Removed: (fracas-tools--json
270 Removed: (let ((file (fracas-tools--file args)))
271 Removed: (with-current-buffer (fracas-tools--buffer file)
272 Removed: (org-with-wide-buffer
273 Removed: `((file . ,file)
274 Removed: (todo_keywords . ,(vconcat org-todo-keywords-1))
275 Removed: (tags . ,(mapcar (lambda (axis)
276 Removed: (cons (car axis) (vconcat (cdr axis))))
277 Removed: fracas-tools--tag-axes))))))))
278 Removed:
279 Removed: (mcp-server-register-tool
280 Removed: (make-mcp-server-tool
281 Removed: :name "fracas-inspect"
282 Removed: :title "FRACAS Inspect"
283 Removed: :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."
284 Removed: :input-schema '((type . "object")
285 Removed: (properties . ((root . ((type . "string")
286 Removed: (description . "Absolute path to the project directory containing REQUESTS.org")))))
287 Removed: (required . []))
288 Removed: :function #'fracas-tools--inspect-handler
289 Removed: :annotations '((readOnlyHint . t)
290 Removed: (destructiveHint . :false)
291 Removed: (idempotentHint . t)
292 Removed: (openWorldHint . :false))))
293 Removed:
294 Removed: ;;; list (read-only)
295 Removed:
296 Removed: (defun fracas-tools--list-handler (args)
297 Removed: "List the top-level request entries in REQUESTS.org, newest first.
298 Removed: ARGS keys: `root', `state' (optional), `tag' (optional). When STATE is
299 Removed: given, return only entries with that TODO keyword. When TAG is given,
300 Removed: return only entries that carry that tag. Each row has `id', `title',
301 Removed: `state', `scheduled', and `tags'."
302 Removed: (fracas-tools--json
303 Removed: (let ((file (fracas-tools--file args))
304 Removed: (state (alist-get 'state args))
305 Removed: (tag (alist-get 'tag args)))
306 Removed: (with-current-buffer (fracas-tools--buffer file)
307 Removed: (org-with-wide-buffer
308 Removed: (goto-char (point-min))
309 Removed: (let ((rows '()))
310 Removed: (while (re-search-forward "^\\* " nil t)
311 Removed: (let ((todo (org-get-todo-state))
312 Removed: (tags (org-get-tags nil t)))
313 Removed: (when (and (or (null state) (equal state todo))
314 Removed: (or (null tag) (member tag tags)))
315 Removed: (push `((id . ,(org-id-get))
316 Removed: (title . ,(org-get-heading t t t t))
317 Removed: (state . ,todo)
318 Removed: (scheduled . ,(org-entry-get nil "SCHEDULED"))
319 Removed: (tags . ,(vconcat tags)))
320 Removed: rows))))
321 Removed: ;; The file is newest-first, so reverse to keep that order.
322 Removed: (vconcat (nreverse rows))))))))
323 Removed:
324 Removed: (mcp-server-register-tool
325 Removed: (make-mcp-server-tool
326 Removed: :name "fracas-list"
327 Removed: :title "FRACAS List"
328 Removed: :description "List the top-level feature-request entries in REQUESTS.org, newest first, with each entry's Org ID, title, TODO state, SCHEDULED time, and tags. Pass an optional `state' or `tag' to filter. Read-only."
329 Removed: :input-schema '((type . "object")
330 Removed: (properties . ((root . ((type . "string")
331 Removed: (description . "Absolute path to the project directory")))
332 Removed: (state . ((type . "string")
333 Removed: (description . "Optional TODO keyword filter, for example TODO or IN-PROGRESS")))
334 Removed: (tag . ((type . "string")
335 Removed: (description . "Optional tag filter, for example web or major")))))
336 Removed: (required . []))
337 Removed: :function #'fracas-tools--list-handler
338 Removed: :annotations '((readOnlyHint . t)
339 Removed: (destructiveHint . :false)
340 Removed: (idempotentHint . t)
341 Removed: (openWorldHint . :false))))
342 Removed:
343 Removed: ;;; capture
344 Removed:
345 Removed: (defun fracas-tools--capture-handler (args)
346 Removed: "Capture a TODO entry at the top of the REQUESTS.org stream.
347 Removed: ARGS keys: `root', `title', `body' (optional), `tags' (optional array).
348 Removed: Insert the entry as a top-level heading directly below the file
349 Removed: preamble, so the newest request is first. Record the capture time as
350 Removed: an inactive SCHEDULED timestamp, apply TAGS from the closed
351 Removed: vocabulary, assign an Org ID, and wrap the body to 72 columns."
352 Removed: (fracas-tools--json
353 Removed: (let* ((file (fracas-tools--file args))
354 Removed: (title (or (alist-get 'title args) (error "Missing `title'")))
355 Removed: (body (or (alist-get 'body args) ""))
356 Removed: (tags (fracas-tools--check-tags
357 Removed: (append (alist-get 'tags args) nil)))
358 Removed: (captured-at (format-time-string "[%Y-%m-%d %a %H:%M]")))
359 Removed: (with-current-buffer (fracas-tools--buffer file)
360 Removed: (org-with-wide-buffer
361 Removed: (fracas-tools--goto-stream-top)
362 Removed: (let ((start (point)))
363 Removed: (insert (format "* TODO %s\nSCHEDULED: %s\n" title captured-at))
364 Removed: (unless (string-empty-p body)
365 Removed: (insert body "\n"))
366 Removed: (goto-char start)
367 Removed: (when tags
368 Removed: (org-set-tags tags))
369 Removed: (let ((id (org-id-get-create)))
370 Removed: (fracas-tools--fill-body)
371 Removed: (when (buffer-modified-p) (save-buffer))
372 Removed: (goto-char (org-id-find id 'marker))
373 Removed: `((id . ,id)
374 Removed: (file . ,file)
375 Removed: (heading . ,(org-get-heading t t t t))
376 Removed: (tags . ,(vconcat (org-get-tags nil t)))))))))))
377 Removed:
378 Removed: (mcp-server-register-tool
379 Removed: (make-mcp-server-tool
380 Removed: :name "fracas-capture"
381 Removed: :title "FRACAS Capture"
382 Removed: :description "Capture a TODO feature request as a top-level heading at the top of REQUESTS.org, so the newest request comes first. Applies scope, impact, and kind tags from the closed vocabulary, records the capture time as an inactive SCHEDULED timestamp, wraps the body to 72 columns, and assigns an Org ID."
383 Removed: :input-schema '((type . "object")
384 Removed: (properties . ((root . ((type . "string")
385 Removed: (description . "Absolute path to the project directory")))
386 Removed: (title . ((type . "string")
387 Removed: (description . "Imperative title under 60 chars")))
388 Removed: (body . ((type . "string")
389 Removed: (description . "Full request text, verbatim")))
390 Removed: (tags . ((type . "array")
391 Removed: (items . ((type . "string")))
392 Removed: (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 Removed: (required . ["title"]))
394 Removed: :function #'fracas-tools--capture-handler
395 Removed: :annotations '((readOnlyHint . :false)
396 Removed: (destructiveHint . :false)
397 Removed: (idempotentHint . :false)
398 Removed: (openWorldHint . :false))))
399 Removed:
400 Removed: ;;; set-status
401 Removed:
402 Removed: (defun fracas-tools--set-status-handler (args)
403 Removed: "Change the TODO keyword of an entry.
404 Removed: ARGS keys: `root', `id', `state'. STATE must be one keyword from the
405 Removed: file's own #+TODO sequence, and must not be DONE (use fracas-complete)."
406 Removed: (fracas-tools--json
407 Removed: (let ((file (fracas-tools--file args))
408 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
409 Removed: (state (or (alist-get 'state args) (error "Missing `state'"))))
410 Removed: (when (string-equal state "DONE")
411 Removed: (error "Use fracas-complete for DONE, not fracas-set-status"))
412 Removed: (with-current-buffer (fracas-tools--buffer file)
413 Removed: (fracas-tools--goto-id id)
414 Removed: (org-todo state)
415 Removed: (when (buffer-modified-p) (save-buffer))
416 Removed: `((id . ,id)
417 Removed: (state . ,(org-get-todo-state)))))))
418 Removed:
419 Removed: (mcp-server-register-tool
420 Removed: (make-mcp-server-tool
421 Removed: :name "fracas-set-status"
422 Removed: :title "FRACAS Set Status"
423 Removed: :description "Set the TODO keyword of a FRACAS entry (for example IN-PROGRESS, TESTING, TESTED, BLOCKED, CANCELLED). Use one keyword from the file's own #+TODO sequence. Does not accept DONE; use fracas-complete for that."
424 Removed: :input-schema '((type . "object")
425 Removed: (properties . ((root . ((type . "string")))
426 Removed: (id . ((type . "string")
427 Removed: (description . "Org ID of the entry")))
428 Removed: (state . ((type . "string")
429 Removed: (description . "TODO keyword from the file's #+TODO sequence")))))
430 Removed: (required . ["id" "state"]))
431 Removed: :function #'fracas-tools--set-status-handler
432 Removed: :annotations '((readOnlyHint . :false)
433 Removed: (destructiveHint . :false)
434 Removed: (idempotentHint . t)
435 Removed: (openWorldHint . :false))))
436 Removed:
437 Removed: ;;; log
438 Removed:
439 Removed: (defun fracas-tools--log-handler (args)
440 Removed: "Append a timestamped note to an entry's `:LOGBOOK:' drawer.
441 Removed: ARGS keys: `root', `id', `note'. Append-only progress feedback from an
442 Removed: agentic session. Never edits an existing note or the request body."
443 Removed: (fracas-tools--json
444 Removed: (let ((file (fracas-tools--file args))
445 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
446 Removed: (note (or (alist-get 'note args) (error "Missing `note'"))))
447 Removed: (with-current-buffer (fracas-tools--buffer file)
448 Removed: (fracas-tools--goto-id id)
449 Removed: (fracas-tools--append-log note)
450 Removed: (when (buffer-modified-p) (save-buffer))
451 Removed: `((id . ,id)
452 Removed: (state . ,(org-get-todo-state)))))))
453 Removed:
454 Removed: (mcp-server-register-tool
455 Removed: (make-mcp-server-tool
456 Removed: :name "fracas-log"
457 Removed: :title "FRACAS Log"
458 Removed: :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."
459 Removed: :input-schema '((type . "object")
460 Removed: (properties . ((root . ((type . "string")))
461 Removed: (id . ((type . "string")
462 Removed: (description . "Org ID of the entry")))
463 Removed: (note . ((type . "string")
464 Removed: (description . "Progress note to append")))))
465 Removed: (required . ["id" "note"]))
466 Removed: :function #'fracas-tools--log-handler
467 Removed: :annotations '((readOnlyHint . :false)
468 Removed: (destructiveHint . :false)
469 Removed: (idempotentHint . :false)
470 Removed: (openWorldHint . :false))))
471 Removed:
472 Removed: ;;; set-result
473 Removed:
474 Removed: (defun fracas-tools--set-result-handler (args)
475 Removed: "Write the structured result line for an entry.
476 Removed: ARGS keys: `root', `id', `commit', `tests', `model' (optional),
477 Removed: `notes' (optional). COMMIT is a commit hash. TESTS is a short recap
478 Removed: such as \"215 pass\". Replace an existing result line or append one at
479 Removed: the end of the body."
480 Removed: (fracas-tools--json
481 Removed: (let ((file (fracas-tools--file args))
482 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
483 Removed: (commit (or (alist-get 'commit args) (error "Missing `commit'")))
484 Removed: (tests (or (alist-get 'tests args) (error "Missing `tests'")))
485 Removed: (model (alist-get 'model args))
486 Removed: (notes (alist-get 'notes args)))
487 Removed: (with-current-buffer (fracas-tools--buffer file)
488 Removed: (fracas-tools--goto-id id)
489 Removed: (fracas-tools--set-result commit tests model notes)
490 Removed: (when (buffer-modified-p) (save-buffer))
491 Removed: (fracas-tools--goto-id id)
492 Removed: `((id . ,id)
493 Removed: (result . ,(fracas-tools--result-text)))))))
494 Removed:
495 Removed: (mcp-server-register-tool
496 Removed: (make-mcp-server-tool
497 Removed: :name "fracas-set-result"
498 Removed: :title "FRACAS Set Result"
499 Removed: :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."
500 Removed: :input-schema '((type . "object")
501 Removed: (properties . ((root . ((type . "string")))
502 Removed: (id . ((type . "string")
503 Removed: (description . "Org ID of the entry")))
504 Removed: (commit . ((type . "string")
505 Removed: (description . "Commit hash")))
506 Removed: (tests . ((type . "string")
507 Removed: (description . "Short test recap, for example \"215 pass\"")))
508 Removed: (model . ((type . "string")
509 Removed: (description . "Optional model or agent that did the work, for example kiro-cli")))
510 Removed: (notes . ((type . "string")
511 Removed: (description . "Optional free-text tail appended after a semicolon, for example a root cause")))))
512 Removed: (required . ["id" "commit" "tests"]))
513 Removed: :function #'fracas-tools--set-result-handler
514 Removed: :annotations '((readOnlyHint . :false)
515 Removed: (destructiveHint . :false)
516 Removed: (idempotentHint . t)
517 Removed: (openWorldHint . :false))))
518 Removed:
519 Removed: ;;; check (checklist for sub-tasks)
520 Removed:
521 Removed: (defconst fracas-tools--checklist-header "Checklist [/]:"
522 Removed: "Header line that introduces a request's checkbox list.
523 Removed: The `[/]' cookie tracks completed items against the total.")
524 Removed:
525 Removed: (defun fracas-tools--checklist-add (item)
526 Removed: "Add ITEM as an unchecked checkbox to the entry at point.
527 Removed: Create the checklist block when it does not exist. Wrap ITEM to 72
528 Removed: columns and refresh the `[/]' cookie. Point must be on the heading."
529 Removed: (let ((fill-column 72))
530 Removed: (org-back-to-heading t)
531 Removed: (let* ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
532 Removed: (line (format "- [ ] %s\n" item))
533 Removed: insert-at)
534 Removed: (org-back-to-heading t)
535 Removed: (if (re-search-forward "^Checklist \\[[0-9]*/[0-9]*\\]:[ \t]*$"
536 Removed: subtree-end t)
537 Removed: ;; Existing block: step past the trailing checkbox items.
538 Removed: (progn (forward-line 1)
539 Removed: (while (looking-at-p "^- \\[.\\] \\|^ ") (forward-line 1))
540 Removed: (setq insert-at (point))
541 Removed: (insert line))
542 Removed: ;; No block: append one at the end of the body.
543 Removed: (goto-char subtree-end)
544 Removed: (skip-chars-backward "\n")
545 Removed: (insert "\n\n" fracas-tools--checklist-header "\n")
546 Removed: (setq insert-at (point))
547 Removed: (insert line))
548 Removed: (save-excursion (goto-char insert-at) (org-fill-paragraph))
549 Removed: (org-update-checkbox-count)
550 Removed: (set-marker subtree-end nil))))
551 Removed:
552 Removed: (defun fracas-tools--checklist-toggle (item)
553 Removed: "Toggle the checkbox whose text matches ITEM in the entry at point.
554 Removed: Signal an error when no item matches. Refresh the `[/]' cookie.
555 Removed: Point must be on the heading."
556 Removed: (org-back-to-heading t)
557 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker))))
558 Removed: (org-back-to-heading t)
559 Removed: (if (re-search-forward (concat "^- \\[.\\] " (regexp-quote item))
560 Removed: subtree-end t)
561 Removed: (progn (beginning-of-line) (org-toggle-checkbox)
562 Removed: (org-update-checkbox-count))
563 Removed: (set-marker subtree-end nil)
564 Removed: (error "No checklist item matches: %s" item))
565 Removed: (set-marker subtree-end nil)))
566 Removed:
567 Removed: (defun fracas-tools--checklist-items ()
568 Removed: "Return the checklist items of the entry at point.
569 Removed: Each item is an alist with `done' and `text'. Point must be on the
570 Removed: heading."
571 Removed: (org-back-to-heading t)
572 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point)))
573 Removed: (items '()))
574 Removed: (org-back-to-heading t)
575 Removed: (while (re-search-forward "^- \\[\\(.\\)\\] \\(.*\\)$" subtree-end t)
576 Removed: (push `((done . ,(if (string-equal (match-string 1) " ") :json-false t))
577 Removed: (text . ,(string-trim (match-string-no-properties 2))))
578 Removed: items))
579 Removed: (vconcat (nreverse items))))
580 Removed:
581 Removed: (defun fracas-tools--check-handler (args)
582 Removed: "Manage the checklist of a request, for splitting a complex task.
583 Removed: ARGS keys: `root', `id', `action' (add|toggle|list), `item'.
584 Removed: `add' appends an unchecked item. `toggle' flips a matching item.
585 Removed: `list' returns the items. A `[/]' cookie tracks progress."
586 Removed: (fracas-tools--json
587 Removed: (let ((file (fracas-tools--file args))
588 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
589 Removed: (action (or (alist-get 'action args) (error "Missing `action'")))
590 Removed: (item (alist-get 'item args)))
591 Removed: (with-current-buffer (fracas-tools--buffer file)
592 Removed: (fracas-tools--goto-id id)
593 Removed: (cond
594 Removed: ((string-equal action "add")
595 Removed: (unless item (error "`add' needs an `item'"))
596 Removed: (fracas-tools--checklist-add item))
597 Removed: ((string-equal action "toggle")
598 Removed: (unless item (error "`toggle' needs an `item'"))
599 Removed: (fracas-tools--checklist-toggle item))
600 Removed: ((string-equal action "list") nil)
601 Removed: (t (error "Unknown action `%s'; use add, toggle, or list" action)))
602 Removed: (when (buffer-modified-p) (save-buffer))
603 Removed: (fracas-tools--goto-id id)
604 Removed: `((id . ,id)
605 Removed: (items . ,(fracas-tools--checklist-items)))))))
606 Removed:
607 Removed: (mcp-server-register-tool
608 Removed: (make-mcp-server-tool
609 Removed: :name "fracas-check"
610 Removed: :title "FRACAS Checklist"
611 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. The items stay inside the one request. They are not separate stream entries."
612 Removed: :input-schema '((type . "object")
613 Removed: (properties . ((root . ((type . "string")))
614 Removed: (id . ((type . "string")
615 Removed: (description . "Org ID of the request")))
616 Removed: (action . ((type . "string")
617 Removed: (description . "add, toggle, or list")))
618 Removed: (item . ((type . "string")
619 Removed: (description . "Item text for add or toggle")))))
620 Removed: (required . ["id" "action"]))
621 Removed: :function #'fracas-tools--check-handler
622 Removed: :annotations '((readOnlyHint . :false)
623 Removed: (destructiveHint . :false)
624 Removed: (idempotentHint . :false)
625 Removed: (openWorldHint . :false))))
626 Removed:
627 Removed: ;;; show (read-only)
628 Removed:
629 Removed: (defun fracas-tools--body-text ()
630 Removed: "Return the plain body text of the entry at point.
631 Removed: Read from the first line after the metadata up to the first of: a
632 Removed: `Checklist [' line, a `- result ::' line, or the end of the subtree.
633 Removed: Return the trimmed string. Point must be on the entry heading."
634 Removed: (org-back-to-heading t)
635 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point))))
636 Removed: (org-back-to-heading t)
637 Removed: (org-end-of-meta-data t)
638 Removed: (let ((body-start (point))
639 Removed: (body-end subtree-end))
640 Removed: (save-excursion
641 Removed: (goto-char body-start)
642 Removed: (when (re-search-forward "^\\(Checklist \\[\\|[ \t]*- result ::\\)"
643 Removed: subtree-end t)
644 Removed: (setq body-end (line-beginning-position))))
645 Removed: (string-trim
646 Removed: (buffer-substring-no-properties body-start body-end)))))
647 Removed:
648 Removed: (defun fracas-tools--logbook-items ()
649 Removed: "Return the `:LOGBOOK:' drawer item lines of the entry at point.
650 Removed: Each item is a string, in the order stored (newest first). Return an
651 Removed: empty vector when there is no drawer. Point must be on the heading."
652 Removed: (org-back-to-heading t)
653 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point)))
654 Removed: (items '()))
655 Removed: (org-back-to-heading t)
656 Removed: (when (re-search-forward "^[ \t]*:LOGBOOK:[ \t]*$" subtree-end t)
657 Removed: (forward-line 1)
658 Removed: (while (and (< (point) subtree-end)
659 Removed: (not (looking-at-p "^[ \t]*:END:[ \t]*$")))
660 Removed: (when (looking-at "^[ \t]*- \\(.*\\)$")
661 Removed: (push (string-trim (match-string-no-properties 1)) items))
662 Removed: (forward-line 1)))
663 Removed: (vconcat (nreverse items))))
664 Removed:
665 Removed: (defun fracas-tools--result-text ()
666 Removed: "Return the text after `- result ::' for the entry at point.
667 Removed: Return nil when there is no result line, so it encodes as JSON null.
668 Removed: Point must be on the heading."
669 Removed: (org-back-to-heading t)
670 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point))))
671 Removed: (org-back-to-heading t)
672 Removed: (when (re-search-forward "^[ \t]*- result ::[ \t]*\\(.*\\)$"
673 Removed: subtree-end t)
674 Removed: (string-trim (match-string-no-properties 1)))))
675 Removed:
676 Removed: (defun fracas-tools--show-handler (args)
677 Removed: "Return the full content of an entry.
678 Removed: ARGS keys: `root', `id'. Report the heading, state, tags, scheduled
679 Removed: and closed timestamps, body text, logbook notes, checklist items, and
680 Removed: result line. Read-only."
681 Removed: (fracas-tools--json
682 Removed: (let ((file (fracas-tools--file args))
683 Removed: (id (or (alist-get 'id args) (error "Missing `id'"))))
684 Removed: (with-current-buffer (fracas-tools--buffer file)
685 Removed: (org-with-wide-buffer
686 Removed: (fracas-tools--goto-id id)
687 Removed: `((id . ,id)
688 Removed: (heading . ,(org-get-heading t t t t))
689 Removed: (state . ,(org-get-todo-state))
690 Removed: (tags . ,(vconcat (org-get-tags nil t)))
691 Removed: (scheduled . ,(org-entry-get nil "SCHEDULED"))
692 Removed: (closed . ,(org-entry-get nil "CLOSED"))
693 Removed: (body . ,(fracas-tools--body-text))
694 Removed: (logbook . ,(fracas-tools--logbook-items))
695 Removed: (checklist . ,(fracas-tools--checklist-items))
696 Removed: (result . ,(fracas-tools--result-text))))))))
697 Removed:
698 Removed: (mcp-server-register-tool
699 Removed: (make-mcp-server-tool
700 Removed: :name "fracas-show"
701 Removed: :title "FRACAS Show"
702 Removed: :description "Return the full content of a FRACAS entry: heading, state, tags, scheduled and closed timestamps, body text, logbook notes, checklist items, and result line. Read-only."
703 Removed: :input-schema '((type . "object")
704 Removed: (properties . ((root . ((type . "string")))
705 Removed: (id . ((type . "string")
706 Removed: (description . "Org ID of the entry")))))
707 Removed: (required . ["id"]))
708 Removed: :function #'fracas-tools--show-handler
709 Removed: :annotations '((readOnlyHint . t)
710 Removed: (destructiveHint . :false)
711 Removed: (idempotentHint . t)
712 Removed: (openWorldHint . :false))))
713 Removed:
714 Removed: ;;; retag
715 Removed:
716 Removed: (defun fracas-tools--retag-handler (args)
717 Removed: "Replace the tags on an entry with a validated set.
718 Removed: ARGS keys: `root', `id', `tags' (array). Validate TAGS against the
719 Removed: closed vocabulary, then set them, keeping the file's default tag
720 Removed: alignment."
721 Removed: (fracas-tools--json
722 Removed: (let ((file (fracas-tools--file args))
723 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
724 Removed: (tags (fracas-tools--check-tags
725 Removed: (append (alist-get 'tags args) nil))))
726 Removed: (with-current-buffer (fracas-tools--buffer file)
727 Removed: (fracas-tools--goto-id id)
728 Removed: (org-set-tags tags)
729 Removed: (when (buffer-modified-p) (save-buffer))
730 Removed: `((id . ,id)
731 Removed: (tags . ,(vconcat (org-get-tags nil t))))))))
732 Removed:
733 Removed: (mcp-server-register-tool
734 Removed: (make-mcp-server-tool
735 Removed: :name "fracas-retag"
736 Removed: :title "FRACAS Retag"
737 Removed: :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."
738 Removed: :input-schema '((type . "object")
739 Removed: (properties . ((root . ((type . "string")))
740 Removed: (id . ((type . "string")
741 Removed: (description . "Org ID of the entry")))
742 Removed: (tags . ((type . "array")
743 Removed: (items . ((type . "string")))
744 Removed: (description . "Tags from the closed vocabulary")))))
745 Removed: (required . ["id" "tags"]))
746 Removed: :function #'fracas-tools--retag-handler
747 Removed: :annotations '((readOnlyHint . :false)
748 Removed: (destructiveHint . :false)
749 Removed: (idempotentHint . :false)
750 Removed: (openWorldHint . :false))))
751 Removed:
752 Removed: ;;; cancel and block
753 Removed:
754 Removed: (defun fracas-tools--transition-with-reason (id keyword marker-label reason)
755 Removed: "Transition entry ID to KEYWORD and record REASON.
756 Removed: Signal an error when REASON is missing or blank. Write a wrapped body
757 Removed: line `- MARKER-LABEL :: REASON', replacing an existing line of that
758 Removed: form or appending one at the end of the body. Fill the line to 72
759 Removed: columns, then set the TODO keyword and save."
760 Removed: (when (or (null reason) (string-empty-p (string-trim reason)))
761 Removed: (error "A reason is required"))
762 Removed: (fracas-tools--goto-id id)
763 Removed: (let ((fill-column 72))
764 Removed: (org-back-to-heading t)
765 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
766 Removed: (line (format "- %s :: %s" marker-label reason))
767 Removed: insert-at)
768 Removed: (org-back-to-heading t)
769 Removed: (if (re-search-forward
770 Removed: (format "^[ \t]*- %s ::.*$" (regexp-quote marker-label))
771 Removed: subtree-end t)
772 Removed: (progn (replace-match line t t)
773 Removed: (setq insert-at (line-beginning-position)))
774 Removed: (goto-char subtree-end)
775 Removed: (skip-chars-backward "\n")
776 Removed: (insert "\n\n" line)
777 Removed: (setq insert-at (line-beginning-position)))
778 Removed: (save-excursion (goto-char insert-at) (org-fill-paragraph))
779 Removed: (set-marker subtree-end nil)))
780 Removed: (fracas-tools--goto-id id)
781 Removed: (org-todo keyword)
782 Removed: (when (buffer-modified-p) (save-buffer)))
783 Removed:
784 Removed: (defun fracas-tools--cancel-handler (args)
785 Removed: "Set an entry to CANCELLED with a required reason.
786 Removed: ARGS keys: `root', `id', `reason'. Record REASON as a `- cancelled ::'
787 Removed: line so the decision is never silent."
788 Removed: (fracas-tools--json
789 Removed: (let ((file (fracas-tools--file args))
790 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
791 Removed: (reason (alist-get 'reason args)))
792 Removed: (with-current-buffer (fracas-tools--buffer file)
793 Removed: (fracas-tools--transition-with-reason id "CANCELLED" "cancelled" reason)
794 Removed: (fracas-tools--goto-id id)
795 Removed: `((id . ,id)
796 Removed: (state . ,(org-get-todo-state)))))))
797 Removed:
798 Removed: (mcp-server-register-tool
799 Removed: (make-mcp-server-tool
800 Removed: :name "fracas-cancel"
801 Removed: :title "FRACAS Cancel"
802 Removed: :description "Set a FRACAS entry to CANCELLED and record a required reason as a `- cancelled ::' line, so the decision is never silent."
803 Removed: :input-schema '((type . "object")
804 Removed: (properties . ((root . ((type . "string")))
805 Removed: (id . ((type . "string")
806 Removed: (description . "Org ID of the entry")))
807 Removed: (reason . ((type . "string")
808 Removed: (description . "Reason for cancelling the entry")))))
809 Removed: (required . ["id" "reason"]))
810 Removed: :function #'fracas-tools--cancel-handler
811 Removed: :annotations '((readOnlyHint . :false)
812 Removed: (destructiveHint . :false)
813 Removed: (idempotentHint . :false)
814 Removed: (openWorldHint . :false))))
815 Removed:
816 Removed: (defun fracas-tools--block-handler (args)
817 Removed: "Set an entry to BLOCKED with a required reason.
818 Removed: ARGS keys: `root', `id', `reason'. Record REASON as a `- blocked ::'
819 Removed: line so the blocker is never silent."
820 Removed: (fracas-tools--json
821 Removed: (let ((file (fracas-tools--file args))
822 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
823 Removed: (reason (alist-get 'reason args)))
824 Removed: (with-current-buffer (fracas-tools--buffer file)
825 Removed: (fracas-tools--transition-with-reason id "BLOCKED" "blocked" reason)
826 Removed: (fracas-tools--goto-id id)
827 Removed: `((id . ,id)
828 Removed: (state . ,(org-get-todo-state)))))))
829 Removed:
830 Removed: (mcp-server-register-tool
831 Removed: (make-mcp-server-tool
832 Removed: :name "fracas-block"
833 Removed: :title "FRACAS Block"
834 Removed: :description "Set a FRACAS entry to BLOCKED and record a required reason as a `- blocked ::' line, so the blocker is never silent."
835 Removed: :input-schema '((type . "object")
836 Removed: (properties . ((root . ((type . "string")))
837 Removed: (id . ((type . "string")
838 Removed: (description . "Org ID of the entry")))
839 Removed: (reason . ((type . "string")
840 Removed: (description . "Reason for blocking the entry")))))
841 Removed: (required . ["id" "reason"]))
842 Removed: :function #'fracas-tools--block-handler
843 Removed: :annotations '((readOnlyHint . :false)
844 Removed: (destructiveHint . :false)
845 Removed: (idempotentHint . :false)
846 Removed: (openWorldHint . :false))))
847 Removed:
848 Removed: ;;; complete
849 Removed:
850 Removed: (defun fracas-tools--complete-handler (args)
851 Removed: "Set an entry to DONE with its result evidence and confirm CLOSED.
852 Removed: ARGS keys: `root', `id', `commit', `tests', `model' (optional),
853 Removed: `notes' (optional). Write the structured result line, then transition
854 Removed: to DONE. Requires `org-log-done' to be `time' so the normal Org
855 Removed: transition inserts CLOSED; this handler never writes CLOSED."
856 Removed: (fracas-tools--json
857 Removed: (let ((file (fracas-tools--file args))
858 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
859 Removed: (commit (or (alist-get 'commit args) (error "Missing `commit'")))
860 Removed: (tests (or (alist-get 'tests args) (error "Missing `tests'")))
861 Removed: (model (alist-get 'model args))
862 Removed: (notes (alist-get 'notes args)))
863 Removed: (with-current-buffer (fracas-tools--buffer file)
864 Removed: (hack-local-variables)
865 Removed: (unless (eq org-log-done 'time)
866 Removed: (error "org-log-done is not set to time"))
867 Removed: (fracas-tools--goto-id id)
868 Removed: (fracas-tools--set-result commit tests model notes)
869 Removed: (fracas-tools--goto-id id)
870 Removed: (org-todo "DONE")
871 Removed: (when (buffer-modified-p) (save-buffer))
872 Removed: (let ((closed (org-entry-get nil "CLOSED")))
873 Removed: (unless closed
874 Removed: (error "Org did not record a CLOSED timestamp"))
875 Removed: `((id . ,id)
876 Removed: (state . ,(org-get-todo-state))
877 Removed: (result . ,(fracas-tools--result-text))
878 Removed: (closed . ,closed)))))))
879 Removed:
880 Removed: (mcp-server-register-tool
881 Removed: (make-mcp-server-tool
882 Removed: :name "fracas-complete"
883 Removed: :title "FRACAS Complete"
884 Removed: :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."
885 Removed: :input-schema '((type . "object")
886 Removed: (properties . ((root . ((type . "string")))
887 Removed: (id . ((type . "string")))
888 Removed: (commit . ((type . "string")
889 Removed: (description . "Commit hash for the completed work")))
890 Removed: (tests . ((type . "string")
891 Removed: (description . "Short test recap, for example \"215 pass\"")))
892 Removed: (model . ((type . "string")
893 Removed: (description . "Optional model or agent that did the work, for example kiro-cli")))
894 Removed: (notes . ((type . "string")
895 Removed: (description . "Optional free-text tail appended after a semicolon, for example a root cause")))))
896 Removed: (required . ["id" "commit" "tests"]))
897 Removed: :function #'fracas-tools--complete-handler
898 Removed: :annotations '((readOnlyHint . :false)
899 Removed: (destructiveHint . :false)
900 Removed: (idempotentHint . t)
901 Removed: (openWorldHint . :false))))
902 Removed:
903 Removed: ;;; verify (read-only)
904 Removed:
905 Removed: (defun fracas-tools--verify-handler (args)
906 Removed: "Return the current state of the entry with `id'.
907 Removed: ARGS keys: `root', `id'. Read-only."
908 Removed: (fracas-tools--json
909 Removed: (let ((file (fracas-tools--file args))
910 Removed: (id (or (alist-get 'id args) (error "Missing `id'"))))
911 Removed: (with-current-buffer (fracas-tools--buffer file)
912 Removed: (org-with-wide-buffer
913 Removed: (fracas-tools--goto-id id)
914 Removed: `((id . ,id)
915 Removed: (heading . ,(org-get-heading t t t t))
916 Removed: (state . ,(org-get-todo-state))
917 Removed: (closed . ,(org-entry-get nil "CLOSED"))
918 Removed: (tags . ,(vconcat (org-get-tags nil t)))))))))
919 Removed:
920 Removed: (mcp-server-register-tool
921 Removed: (make-mcp-server-tool
922 Removed: :name "fracas-verify"
923 Removed: :title "FRACAS Verify"
924 Removed: :description "Return the heading, TODO state, CLOSED timestamp, and tags of a FRACAS entry. Read-only."
925 Removed: :input-schema '((type . "object")
926 Removed: (properties . ((root . ((type . "string")))
927 Removed: (id . ((type . "string")))))
928 Removed: (required . ["id"]))
929 Removed: :function #'fracas-tools--verify-handler
930 Removed: :annotations '((readOnlyHint . t)
931 Removed: (destructiveHint . :false)
932 Removed: (idempotentHint . t)
933 Removed: (openWorldHint . :false))))
934 Removed:
935 Removed: (provide 'fracas-tools)
936 Removed:
937 Removed: ;;; fracas-tools.el ends here
.kiro/skills/fracas/references/tools.md
index d058daaa..00000000 100644..000000
@@ -1,168 +0,0 @@
1 Removed: # FRACAS tool reference
2 Removed:
3 Removed: Read this file when you need the exact arguments of one tool, or the
4 Removed: shape of its result. The main skill file gives the workflow.
5 Removed:
6 Removed: Every tool accepts an optional `root` argument. `root` names the project
7 Removed: directory that holds the stream file. When you omit `root`, the tools use
8 Removed: `fracas-project-root`. That variable comes from an upward search for
9 Removed: `REQUESTS.org`, so it needs no configuration.
10 Removed:
11 Removed: Every tool returns one JSON object. A failed call returns an object with
12 Removed: one `error` field. An absent value returns JSON `null`.
13 Removed:
14 Removed: ## Read-only tools
15 Removed:
16 Removed: ### fracas-inspect
17 Removed:
18 Removed: Arguments: none beyond `root`.
19 Removed:
20 Removed: Returns `file`, `todo_keywords`, and `tags`. The `tags` field groups the
21 Removed: vocabulary by axis: `kind`, `scope`, `impact`, and `misc`.
22 Removed:
23 Removed: Run this tool when a tag call fails, or when you need the keyword
24 Removed: sequence of the file.
25 Removed:
26 Removed: ### fracas-list
27 Removed:
28 Removed: Arguments:
29 Removed:
30 Removed: - `state` — optional. Keep only entries with this TODO keyword.
31 Removed: - `tag` — optional. Keep only entries that carry this tag.
32 Removed:
33 Removed: Returns an array of objects, newest first. Each object holds `id`,
34 Removed: `title`, `state`, `scheduled`, and `tags`. The `id` field is `null` for
35 Removed: an entry that carries no Org ID.
36 Removed:
37 Removed: Pass a filter whenever you can, because the stream grows without bound.
38 Removed:
39 Removed: ### fracas-show
40 Removed:
41 Removed: Arguments:
42 Removed:
43 Removed: - `id` — required. The Org ID of the entry.
44 Removed:
45 Removed: Returns `id`, `heading`, `state`, `tags`, `scheduled`, `closed`, `body`,
46 Removed: `logbook`, `checklist`, and `result`. The `checklist` field holds objects
47 Removed: with a boolean `done` field and a `text` field.
48 Removed:
49 Removed: ### fracas-verify
50 Removed:
51 Removed: Arguments:
52 Removed:
53 Removed: - `id` — required. The Org ID of the entry.
54 Removed:
55 Removed: Returns `id`, `heading`, `state`, `closed`, and `tags`. Use this tool
56 Removed: before a status change and after a status change.
57 Removed:
58 Removed: ## Writing tools
59 Removed:
60 Removed: ### fracas-capture
61 Removed:
62 Removed: Arguments:
63 Removed:
64 Removed: - `title` — required. A short imperative title under 60 characters.
65 Removed: - `body` — optional. The full request text, as plain text.
66 Removed: - `tags` — optional. An array of tags from the closed vocabulary.
67 Removed:
68 Removed: Returns `id`, `file`, `heading`, and `tags`.
69 Removed:
70 Removed: The tool inserts the entry as a top-level heading above every other
71 Removed: entry. It records the capture time as an inactive `SCHEDULED` timestamp.
72 Removed: It wraps the body to 72 columns, and it assigns an Org ID.
73 Removed:
74 Removed: ### fracas-set-status
75 Removed:
76 Removed: Arguments:
77 Removed:
78 Removed: - `id` — required. The Org ID of the entry.
79 Removed: - `state` — required. One keyword from the file's own `#+TODO:` line.
80 Removed:
81 Removed: Returns `id` and `state`.
82 Removed:
83 Removed: The tool refuses `DONE`. Use `fracas-complete` for `DONE`.
84 Removed:
85 Removed: ### fracas-block and fracas-cancel
86 Removed:
87 Removed: Arguments:
88 Removed:
89 Removed: - `id` — required. The Org ID of the entry.
90 Removed: - `reason` — required. The reason for the transition.
91 Removed:
92 Removed: Returns `id` and `state`.
93 Removed:
94 Removed: `fracas-block` writes a `- blocked ::` line and sets `BLOCKED`.
95 Removed: `fracas-cancel` writes a `- cancelled ::` line and sets `CANCELLED`.
96 Removed: Each tool refuses a blank reason.
97 Removed:
98 Removed: ### fracas-check
99 Removed:
100 Removed: Arguments:
101 Removed:
102 Removed: - `id` — required. The Org ID of the entry.
103 Removed: - `action` — required. One of `add`, `toggle`, or `list`.
104 Removed: - `item` — required for `add` and for `toggle`. The item text.
105 Removed:
106 Removed: Returns `id` and `items`.
107 Removed:
108 Removed: The tool keeps the items in a `Checklist [/]:` block in the body. The
109 Removed: `[/]` cookie counts the completed items. `toggle` matches the item text
110 Removed: from the start, and it fails when no item matches.
111 Removed:
112 Removed: ### fracas-log
113 Removed:
114 Removed: Arguments:
115 Removed:
116 Removed: - `id` — required. The Org ID of the entry.
117 Removed: - `note` — required. The progress note.
118 Removed:
119 Removed: Returns `id` and `state`.
120 Removed:
121 Removed: The tool prepends a timestamped item to the `:LOGBOOK:` drawer, and
122 Removed: creates the drawer when it is absent. The drawer is append-only.
123 Removed:
124 Removed: ### fracas-retag
125 Removed:
126 Removed: Arguments:
127 Removed:
128 Removed: - `id` — required. The Org ID of the entry.
129 Removed: - `tags` — required. An array of tags from the closed vocabulary.
130 Removed:
131 Removed: Returns `id` and `tags`.
132 Removed:
133 Removed: The tool replaces every tag on the entry. It validates each tag first,
134 Removed: so one invalid tag leaves the entry unchanged.
135 Removed:
136 Removed: ### fracas-set-result
137 Removed:
138 Removed: Arguments:
139 Removed:
140 Removed: - `id` — required. The Org ID of the entry.
141 Removed: - `commit` — required. The commit hash.
142 Removed: - `tests` — required. A short test recap.
143 Removed: - `model` — optional. The agent that did the work.
144 Removed: - `notes` — optional. A short tail after a semicolon.
145 Removed:
146 Removed: Returns `id` and `result`.
147 Removed:
148 Removed: The tool replaces an existing result line, or appends one at the end of
149 Removed: the body. The line stays on one line, because the reader reads one line.
150 Removed:
151 Removed: ### fracas-complete
152 Removed:
153 Removed: Arguments: the same arguments as `fracas-set-result`.
154 Removed:
155 Removed: Returns `id`, `state`, `result`, and `closed`.
156 Removed:
157 Removed: The tool writes the result line, sets `DONE`, then reads back the
158 Removed: `CLOSED` timestamp. It fails when the file does not set `org-log-done`
159 Removed: to `time`, because Org must write that timestamp.
160 Removed:
161 Removed: ## Result line format
162 Removed:
163 Removed: ```org
164 Removed: - result :: model=example-agent commit=3051af0 tests=243 pass; short root cause
165 Removed: ```
166 Removed:
167 Removed: The `model` field and the tail after the semicolon are optional. The
168 Removed: `commit` field and the `tests` field are always present.
.kiro/skills/fracas/run-tests.sh
index 03fe9397..00000000 100755..000000
@@ -1,35 +0,0 @@
1 Removed: #!/bin/sh
2 Removed: # Run the FRACAS test suite in a batch Emacs.
3 Removed: #
4 Removed: # Required dependency: emacs, with Org mode. Org mode ships with Emacs.
5 Removed: # The suite needs no MCP framework and no configuration. It runs on any
6 Removed: # machine. Every test uses a temporary stream file, so the project stream
7 Removed: # file stays unchanged.
8 Removed: #
9 Removed: # Exit status: 0 when every test passes, 1 otherwise.
10 Removed:
11 Removed: set -eu
12 Removed:
13 Removed: dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
14 Removed:
15 Removed: if ! command -v emacs >/dev/null 2>&1; then
16 Removed: echo "Error: emacs is not on PATH. Install Emacs, then run this script again." >&2
17 Removed: exit 1
18 Removed: fi
19 Removed:
20 Removed: for file in fracas-tools.el fracas-tests.el; do
21 Removed: if [ ! -r "$dir/$file" ]; then
22 Removed: echo "Error: cannot read $dir/$file. The skill directory is incomplete." >&2
23 Removed: exit 1
24 Removed: fi
25 Removed: done
26 Removed:
27 Removed: if emacs --batch -Q \
28 Removed: -l "$dir/fracas-tools.el" \
29 Removed: -l "$dir/fracas-tests.el" \
30 Removed: -f ert-run-tests-batch-and-exit; then
31 Removed: echo "FRACAS tests passed."
32 Removed: else
33 Removed: echo "Error: FRACAS tests failed. Read the ERT output above for the failing test." >&2
34 Removed: exit 1
35 Removed: fi
.kiro/skills/rail/SKILL.md
index 00000000..0db0b75a 000000..100644
@@ -0,0 +1,339 @@
1 Added: ---
2 Added: name: rail
3 Added: description: Manage a rolling action item list in a RAIL.org stream with dedicated Emacs MCP tools.
4 Added: ---
5 Added:
6 Added: # RAIL — Rolling Action Item List
7 Added:
8 Added: ## Overview
9 Added:
10 Added: RAIL keeps every action item in one Org file, `RAIL.org`, at the root of
11 Added: a project. Dedicated Emacs tools write that file. The work follows three
12 Added: steps:
13 Added:
14 Added: 1. Capture the item at the top of the stream.
15 Added: 2. Track its status while the work runs.
16 Added: 3. Close it with commit evidence and test evidence.
17 Added:
18 Added: The command for this skill is `/rail`.
19 Added:
20 Added: ## Usage
21 Added:
22 Added: Use this skill when the user does one of these things:
23 Added:
24 Added: - The user runs `/rail`.
25 Added: - The user states an action item, a feature request, a defect, or a
26 Added: change to make.
27 Added: - The user asks you to record or capture an item.
28 Added: - The user asks for the status of an item, or asks to change it.
29 Added: - The user asks you to close, cancel, or block an item.
30 Added: - The user asks what items are open.
31 Added:
32 Added: Treat each prompt as one separate action item. Do not merge two items.
33 Added: Do not build, test, commit, or start an agent for a capture.
34 Added:
35 Added: The current prompt is the action item:
36 Added:
37 Added: $ARGUMENTS
38 Added:
39 Added: If the prompt is empty, ask the user for one line that describes the
40 Added: item. Then stop. Capture the next prompt as the next item.
41 Added:
42 Added: ## Core Concepts
43 Added:
44 Added: ### The stream
45 Added:
46 Added: `RAIL.org` is a flat stream. Each action item is a top-level heading.
47 Added: The newest item comes first. The file holds no container heading and no
48 Added: topical heading. Tags on the heading give the axes of the item. The
49 Added: position of a heading gives only its age.
50 Added:
51 Added: ### The file owns the vocabulary and the workflow
52 Added:
53 Added: The tools carry no project vocabulary and no project workflow. The
54 Added: stream file owns both:
55 Added:
56 Added: - The `#+TODO:` line declares the status keywords.
57 Added: - The `#+TAGS:` lines declare the tag vocabulary, grouped into axes.
58 Added:
59 Added: The tools read both from the file. One tool file therefore serves every
60 Added: project. To change the states or the tags, edit the preamble of
61 Added: `RAIL.org`. Run `rail-inspect` to read the current vocabulary.
62 Added:
63 Added: ### The Org ID is the handle
64 Added:
65 Added: Every item carries an Org ID. Each tool that changes an item needs that
66 Added: ID. Never select an item by its title text, because two titles can
67 Added: match. Run `rail-list` to get the ID of each item.
68 Added:
69 Added: ### Evidence, not silence
70 Added:
71 Added: Three transitions demand a written reason or written evidence:
72 Added:
73 Added: - `BLOCKED` needs a reason.
74 Added: - `CANCELLED` needs a reason.
75 Added: - `DONE` needs a commit hash and a test recap.
76 Added:
77 Added: The tools refuse these transitions without that text. A stalled item or
78 Added: a dropped item therefore always states why.
79 Added:
80 Added: ### The tag vocabulary is grouped by axis
81 Added:
82 Added: The `#+TAGS:` lines group the tags into named axes. A common shape holds
83 Added: a `Kind` axis and a `Scope` axis, plus optional axes. Each axis holds a
84 Added: short set of bare tags. Distinct words across axes keep a bare tag
85 Added: unambiguous. This is a convention, not a tool rule. The tool accepts any
86 Added: tag that a `#+TAGS:` line declares, in any axis the project chooses.
87 Added:
88 Added: Infer the tags from the item text when you capture it. You can re-tag an
89 Added: item later with `rail-retag`, as its shape changes.
90 Added:
91 Added: ## Set up the stream file
92 Added:
93 Added: A project needs one `RAIL.org` file at its root before the tools run.
94 Added: Create it with this preamble, then edit the axes to fit the project:
95 Added:
96 Added: ```org
97 Added: #+TITLE: Project action items
98 Added: #+TODO: TODO IN-PROGRESS TESTING TESTED BLOCKED | CANCELLED DONE
99 Added: #+TAGS: [ Kind : feat fix refactor chore docs ]
100 Added: #+TAGS: [ Scope : core app web ui ]
101 Added: #+TAGS: [ Impact : minor major ]
102 Added:
103 Added: # Local Variables:
104 Added: # org-log-done: time
105 Added: # End:
106 Added: ```
107 Added:
108 Added: The `#+TODO:` keywords before the bar are open states. The keywords
109 Added: after the bar are closed states. The `org-log-done` local variable makes
110 Added: Org write a `CLOSED` timestamp on the transition to `DONE`, which
111 Added: `rail-complete` requires. A file with no `#+TAGS:` line accepts any tag.
112 Added:
113 Added: ## Install the tools once per session
114 Added:
115 Added: The tools live in `rail-tools.el`, beside this file. Load that file one
116 Added: time in the running Emacs. Use the generic `eval-elisp` tool for the load
117 Added: only.
118 Added:
119 Added: Pass the project directory of your own session in `project`. Do not write
120 Added: an absolute path from a home directory into the form, because that path
121 Added: differs on every machine.
122 Added:
123 Added: ```elisp
124 Added: (let* ((project "/the/project/directory/you/work/in")
125 Added: (tools (expand-file-name ".kiro/skills/rail/rail-tools.el"
126 Added: project)))
127 Added: (unless (file-readable-p tools)
128 Added: (error "No RAIL tools at %s" tools))
129 Added: (load tools nil t))
130 Added: ```
131 Added:
132 Added: The tool file then finds the project root itself. It searches upward from
133 Added: its own directory for `RAIL.org`. The search assumes no directory layout,
134 Added: so the same file works on every machine. The result becomes
135 Added: `rail-project-root`, and every tool uses it by default.
136 Added:
137 Added: Do not depend on the Emacs `default-directory`. That directory often sits
138 Added: outside the project, and the search then finds no stream file.
139 Added:
140 Added: Then verify that the tools are present. This expression returns the
141 Added: thirteen tool names:
142 Added:
143 Added: ```elisp
144 Added: (seq-filter (lambda (name) (string-prefix-p "rail-" name))
145 Added: (mcp-server-tools-list-names))
146 Added: ```
147 Added:
148 Added: If the load fails, stop and report the problem. Do not edit `RAIL.org` as
149 Added: raw text instead.
150 Added:
151 Added: ### If the tools are not callable
152 Added:
153 Added: The tools register inside Emacs. The MCP client lists its tools when it
154 Added: connects, so a mid-session load can leave the tools absent from your own
155 Added: tool list. In that case, call each tool through the dispatch path:
156 Added:
157 Added: ```elisp
158 Added: (mcp-server-tools-call "rail-list" '((state . "TODO")))
159 Added: ```
160 Added:
161 Added: This path runs the same handler as a direct tool call. To make the tools
162 Added: callable directly, load `rail-tools.el` from the Emacs init file.
163 Added:
164 Added: ### Why dedicated tools
165 Added:
166 Added: The generic `eval-elisp` tool sends its code through the Emacs security
167 Added: form walker. The walker prompts for each file function, such as
168 Added: `find-file-noselect` and `save-buffer`. The RAIL tools register as normal
169 Added: MCP tools, and tool dispatch does not use the walker. The read-only tools
170 Added: also carry a `readOnlyHint` annotation, so the client can approve them
171 Added: without a prompt.
172 Added:
173 Added: You **MUST** use these tools for every change to `RAIL.org`.
174 Added:
175 Added: - You **MUST NOT** edit the file as raw text, because raw edits break the
176 Added: timestamps, the IDs, and the tag alignment.
177 Added: - You **MUST NOT** use a shell tool or a file tool for an item, because
178 Added: those tools apply no validation.
179 Added:
180 Added: ## The RAIL tools
181 Added:
182 Added: Each tool accepts an optional `root` argument. `root` names the project
183 Added: directory that holds `RAIL.org`. When you omit `root`, the tools use
184 Added: `rail-project-root`. Set `root` only for a different project.
185 Added:
186 Added: | Tool | Purpose | Read-only |
187 Added: |---|---|---|
188 Added: | `rail-inspect` | Report the keyword sequence and the tag axes. | Yes |
189 Added: | `rail-list` | List items, newest first, with ID, title, state, and tags. Accepts a `state` or `tag` filter. | Yes |
190 Added: | `rail-show` | Report one item in full: body, logbook, checklist, and result. | Yes |
191 Added: | `rail-verify` | Report the heading, state, `CLOSED` time, and tags of one item. | Yes |
192 Added: | `rail-capture` | Create a `TODO` item at the top of the stream. | No |
193 Added: | `rail-set-status` | Set the keyword of an item. Refuses `DONE`. | No |
194 Added: | `rail-block` | Set `BLOCKED` and record a required reason. | No |
195 Added: | `rail-cancel` | Set `CANCELLED` and record a required reason. | No |
196 Added: | `rail-check` | Add, toggle, or list the checklist items of one item. | No |
197 Added: | `rail-log` | Append a timestamped note to the `:LOGBOOK:` drawer. | No |
198 Added: | `rail-retag` | Replace the tags of an item with a validated set. | No |
199 Added: | `rail-set-result` | Write the `- result ::` line. | No |
200 Added: | `rail-complete` | Set `DONE` after it records the result evidence. | No |
201 Added:
202 Added: Use `rail-show` to read one item, and `rail-retag` to re-tag it. These
203 Added: are the correct tools for those two actions, because the generic Emacs
204 Added: `org-*` tools cannot operate on this file.
205 Added:
206 Added: For the exact arguments and the result shape of each tool, read
207 Added: `references/tools.md`.
208 Added:
209 Added: ## Step 1 — Capture the item
210 Added:
211 Added: Run `rail-capture` with these arguments:
212 Added:
213 Added: - `title` — a short imperative title under 60 characters.
214 Added: - `body` — the full item text.
215 Added: - `tags` — the tags that fit the item, from the file vocabulary.
216 Added:
217 Added: The tool inserts the item at the top of the file. It records the capture
218 Added: time as an inactive `SCHEDULED` timestamp. It applies the tags, wraps the
219 Added: body to 72 columns, and assigns an Org ID. Report that ID to the user.
220 Added:
221 Added: Keep the item text unchanged. If the text holds separate ideas, write the
222 Added: body as a list. Pass the body as plain text, because the tool wraps it.
223 Added:
224 Added: If `rail-capture` rejects a tag, run `rail-inspect` to read the
225 Added: vocabulary. Then fix the tag and re-run the capture.
226 Added:
227 Added: Then proceed to Step 2 when work starts on the item.
228 Added:
229 Added: ## Step 2 — Track the status
230 Added:
231 Added: First find the item. Run `rail-list` to read the ID, the title, the
232 Added: state, and the tags of each item. Pass a `state` filter or a `tag` filter
233 Added: to narrow the list. Use the ID for every later call.
234 Added:
235 Added: Read the current state with `rail-verify` before you change a keyword.
236 Added: Then pick the tool from the target state:
237 Added:
238 Added: - Target an open keyword, such as `IN-PROGRESS` → `rail-set-status`
239 Added: - Target `BLOCKED` → `rail-block`, with a reason
240 Added: - Target `CANCELLED` → `rail-cancel`, with a reason
241 Added: - Target `DONE` → `rail-complete`, with commit and test evidence
242 Added:
243 Added: Run `rail-verify` again after the change to confirm the keyword.
244 Added:
245 Added: ### Block or cancel with a reason
246 Added:
247 Added: Run `rail-block` when work cannot continue. Run `rail-cancel` when you
248 Added: stop work on purpose. Each tool needs a `reason`, and writes it to the
249 Added: item.
250 Added:
251 Added: You **MUST NOT** use `rail-set-status` here, because it records no reason.
252 Added:
253 Added: ### Split a large item with a checklist
254 Added:
255 Added: Run `rail-check` when one item is large enough to track in parts:
256 Added:
257 Added: - `action` `add` — append an unchecked item. Needs `item`.
258 Added: - `action` `toggle` — flip one matching item. Needs `item`.
259 Added: - `action` `list` — report the items and their state.
260 Added:
261 Added: The tool keeps the items in a `Checklist [/]:` block in the body. The
262 Added: `[/]` cookie counts the completed items against the total. The items stay
263 Added: inside the one action item. They are not separate stream entries, and
264 Added: they carry no keyword of their own. For independent work, capture a new
265 Added: action item instead.
266 Added:
267 Added: ### Record progress in the logbook
268 Added:
269 Added: Run `rail-log` with a `note` to record progress during a session. The
270 Added: tool prepends a timestamped item to the `:LOGBOOK:` drawer. The logbook
271 Added: is append-only. The tool never rewrites an earlier note, and never
272 Added: rewrites the body.
273 Added:
274 Added: Then proceed to Step 3 when every closing criterion is met.
275 Added:
276 Added: ## Step 3 — Close the item
277 Added:
278 Added: Check each criterion before you close an item. Copy this list and mark
279 Added: each item:
280 Added:
281 Added: - [ ] The build passes.
282 Added: - [ ] The tests pass.
283 Added: - [ ] The files have the required format.
284 Added: - [ ] A commit exists.
285 Added:
286 Added: If one criterion fails, do not close the item. Set the state with
287 Added: `rail-set-status` instead. Then fix the failure and re-run this check.
288 Added:
289 Added: When every item is marked, run `rail-complete` with these arguments:
290 Added:
291 Added: - `id` — the Org ID of the item.
292 Added: - `commit` — the commit hash of the completed work.
293 Added: - `tests` — a short test recap, for example `215 pass`.
294 Added: - `model` — optional. The agent that did the work.
295 Added: - `notes` — optional. A short tail, such as a root cause.
296 Added:
297 Added: The tool writes the result line, sets `DONE`, then confirms that Org
298 Added: recorded the `CLOSED` timestamp. Org writes that timestamp, because the
299 Added: file sets `org-log-done` to `time`.
300 Added:
301 Added: You **MUST NOT** write or edit that timestamp, because Org owns it.
302 Added:
303 Added: The result line takes this form. It stays on one line, because the reader
304 Added: reads one line:
305 Added:
306 Added: ```org
307 Added: - result :: model=example-agent commit=3051af0 tests=243 pass; short root cause
308 Added: ```
309 Added:
310 Added: The item stays in place in the stream. There is no refile step.
311 Added:
312 Added: Run `rail-verify` last. Verify that the state is `DONE` and that the
313 Added: `CLOSED` timestamp is present. To write the result line before you close
314 Added: the item, run `rail-set-result` on its own.
315 Added:
316 Added: ## Test the tools
317 Added:
318 Added: The tools carry a test suite. Run it after any change to `rail-tools.el`:
319 Added:
320 Added: ```sh
321 Added: .kiro/skills/rail/run-tests.sh
322 Added: ```
323 Added:
324 Added: The suite needs no MCP framework and no configuration. Each test uses a
325 Added: temporary stream file, so the project stream file stays unchanged.
326 Added:
327 Added: ## Common mistakes
328 Added:
329 Added: - **Editing the file as raw text.** The tools keep the timestamps, the
330 Added: IDs, and the tag alignment correct. Raw edits do not.
331 Added: - **Selecting an item by title.** Two titles can match. Use the Org ID
332 Added: from `rail-list`.
333 Added: - **Using `rail-set-status` for `DONE`.** That call fails by design. Use
334 Added: `rail-complete`, which demands the evidence.
335 Added: - **Writing a `CLOSED` timestamp.** Org writes it. The tool only confirms
336 Added: it.
337 Added: - **Merging two items.** Record each prompt as its own action item.
338 Added:
339 Added: If you cannot complete an item, record the item and record the failure.
.kiro/skills/rail/rail-tests.el
index 00000000..0c207f72 000000..100644
@@ -0,0 +1,481 @@
1 Added: ;;; rail-tests.el --- End-to-end tests for the RAIL tools -*- lexical-binding: t; -*-
2 Added:
3 Added: ;;; Commentary:
4 Added:
5 Added: ;; These tests exercise the RAIL 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 rail-tests--preamble
22 Added: (concat "#+TITLE: Test stream\n"
23 Added: "#+TODO: TODO IN-PROGRESS TESTING TESTED BLOCKED | CANCELLED DONE\n"
24 Added: "#+TAGS: [ Kind : feat fix refactor chore docs ]\n"
25 Added: "#+TAGS: [ Scope : core app web ui ]\n"
26 Added: "#+TAGS: [ Impact : minor major ]\n"
27 Added: "\n"
28 Added: "# Local Variables:\n"
29 Added: "# org-log-done: time\n"
30 Added: "# End:\n")
31 Added: "Preamble of the temporary stream file.
32 Added: The `#+TODO:' line gives the keyword sequence. The `#+TAGS:' lines give
33 Added: the tag vocabulary, grouped into axes. The local variable
34 Added: `org-log-done' makes Org write a CLOSED timestamp.")
35 Added:
36 Added: (defun rail-tests--decode (json-string)
37 Added: "Return JSON-STRING decoded into Lisp with alists for objects.
38 Added: Decode JSON null to nil and JSON false to `:json-false', which are the
39 Added: same sentinels that the handlers encode from. A round trip therefore
40 Added: gives back the value that the handler started with."
41 Added: (json-parse-string json-string
42 Added: :object-type 'alist
43 Added: :null-object nil
44 Added: :false-object :json-false))
45 Added:
46 Added: (defun rail-tests--call (handler args)
47 Added: "Call HANDLER with ARGS and return the decoded result.
48 Added: Signal an error when the handler reports one, so a failure is visible."
49 Added: (let ((result (rail-tests--decode (funcall handler args))))
50 Added: (when (alist-get 'error result)
51 Added: (error "Handler failed: %s" (alist-get 'error result)))
52 Added: result))
53 Added:
54 Added: (defmacro rail-tests--with-stream (root &rest body)
55 Added: "Create a temporary project directory, bind ROOT to it, then run BODY.
56 Added: Delete the directory and its buffers after BODY."
57 Added: (declare (indent 1))
58 Added: `(let* ((,root (file-name-as-directory (make-temp-file "rail-test" t)))
59 Added: (file (expand-file-name rail-stream-file-name ,root))
60 Added: (enable-local-variables :all)
61 Added: (org-id-track-globally nil)
62 Added: (create-lockfiles nil))
63 Added: (unwind-protect
64 Added: (progn
65 Added: (with-temp-file file (insert rail-tests--preamble))
66 Added: ,@body)
67 Added: (dolist (buf (buffer-list))
68 Added: (when (and (buffer-file-name buf)
69 Added: (string-prefix-p ,root (buffer-file-name buf)))
70 Added: (with-current-buffer buf (set-buffer-modified-p nil))
71 Added: (kill-buffer buf)))
72 Added: (delete-directory ,root t))))
73 Added:
74 Added: (defun rail-tests--capture (root title tags &optional body)
75 Added: "Capture a request in ROOT with TITLE, TAGS, and optional BODY.
76 Added: Return the new entry's Org ID."
77 Added: (alist-get 'id (rail-tests--call
78 Added: #'rail-tools--capture-handler
79 Added: (list (cons 'root root)
80 Added: (cons 'title title)
81 Added: (cons 'tags tags)
82 Added: (cons 'body (or body "Request body."))))))
83 Added:
84 Added: (defun rail-tests--file-text (root)
85 Added: "Return the text of the stream file in ROOT."
86 Added: (with-temp-buffer
87 Added: (insert-file-contents (expand-file-name rail-stream-file-name root))
88 Added: (buffer-string)))
89 Added:
90 Added: ;;; Root discovery
91 Added:
92 Added: (ert-deftest rail-test-locate-root-finds-the-stream-file ()
93 Added: "`rail-locate-root' finds the root from a nested directory."
94 Added: (rail-tests--with-stream root
95 Added: (let ((nested (expand-file-name "a/b/c/" root)))
96 Added: (make-directory nested t)
97 Added: (should (equal (rail-locate-root nested) root)))))
98 Added:
99 Added: (ert-deftest rail-test-locate-root-returns-nil-without-a-stream-file ()
100 Added: "`rail-locate-root' returns nil when no ancestor holds the file."
101 Added: (let ((empty (file-name-as-directory (make-temp-file "rail-empty" t))))
102 Added: (unwind-protect
103 Added: (should (null (rail-locate-root empty)))
104 Added: (delete-directory empty t))))
105 Added:
106 Added: (ert-deftest rail-test-root-argument-overrides-the-default ()
107 Added: "An explicit `root' argument selects the file, not `rail-project-root'."
108 Added: (rail-tests--with-stream root
109 Added: (should (equal (rail-tools--file (list (cons 'root root)))
110 Added: (expand-file-name rail-stream-file-name root)))))
111 Added:
112 Added: (ert-deftest rail-test-absent-root-uses-the-default ()
113 Added: "An absent `root' argument falls back to `rail-project-root'."
114 Added: (rail-tests--with-stream root
115 Added: (let ((rail-project-root root))
116 Added: (should (equal (rail-tools--file nil)
117 Added: (expand-file-name rail-stream-file-name root))))))
118 Added:
119 Added: (ert-deftest rail-test-a-missing-directory-signals-an-error ()
120 Added: "A `root' that is not a directory signals an error."
121 Added: (should-error (rail-tools--file
122 Added: (list (cons 'root "/rail/no/such/directory")))))
123 Added:
124 Added: ;;; Capture
125 Added:
126 Added: (ert-deftest rail-test-capture-creates-an-addressable-todo ()
127 Added: "Capture writes a TODO entry with an ID, a SCHEDULED time, and tags."
128 Added: (rail-tests--with-stream root
129 Added: (let* ((id (rail-tests--capture root "Add a widget" ["feat" "web"]))
130 Added: (entry (rail-tests--call #'rail-tools--show-handler
131 Added: (list (cons 'root root) (cons 'id id)))))
132 Added: (should (stringp id))
133 Added: (should (equal (alist-get 'state entry) "TODO"))
134 Added: (should (equal (alist-get 'heading entry) "Add a widget"))
135 Added: (should (equal (append (alist-get 'tags entry) nil) '("feat" "web")))
136 Added: (should (stringp (alist-get 'scheduled entry)))
137 Added: (should (null (alist-get 'closed entry))))))
138 Added:
139 Added: (ert-deftest rail-test-capture-puts-the-newest-request-first ()
140 Added: "Capture inserts each new request above the previous request."
141 Added: (rail-tests--with-stream root
142 Added: (rail-tests--capture root "First request" ["feat" "core"])
143 Added: (rail-tests--capture root "Second request" ["fix" "app"])
144 Added: (let ((rows (rail-tests--decode
145 Added: (rail-tools--list-handler (list (cons 'root root))))))
146 Added: (should (equal (length rows) 2))
147 Added: (should (equal (alist-get 'title (aref rows 0)) "Second request"))
148 Added: (should (equal (alist-get 'title (aref rows 1)) "First request")))))
149 Added:
150 Added: (ert-deftest rail-test-capture-rejects-a-tag-outside-the-vocabulary ()
151 Added: "Capture rejects any tag that the closed vocabulary does not hold."
152 Added: (rail-tests--with-stream root
153 Added: (let ((result (rail-tests--decode
154 Added: (rail-tools--capture-handler
155 Added: (list (cons 'root root)
156 Added: (cons 'title "Bad tags")
157 Added: (cons 'tags ["feat" "trivial"]))))))
158 Added: (should (string-match-p "Unknown tag" (alist-get 'error result))))))
159 Added:
160 Added: ;;; Status
161 Added:
162 Added: (ert-deftest rail-test-set-status-moves-through-the-open-keywords ()
163 Added: "Set-status accepts each open keyword from the file's own sequence."
164 Added: (rail-tests--with-stream root
165 Added: (let ((id (rail-tests--capture root "Track status" ["feat" "core"])))
166 Added: (dolist (state '("IN-PROGRESS" "TESTING" "TESTED"))
167 Added: (let ((result (rail-tests--call
168 Added: #'rail-tools--set-status-handler
169 Added: (list (cons 'root root) (cons 'id id)
170 Added: (cons 'state state)))))
171 Added: (should (equal (alist-get 'state result) state))))
172 Added: (should (equal (alist-get 'state (rail-tests--call
173 Added: #'rail-tools--verify-handler
174 Added: (list (cons 'root root) (cons 'id id))))
175 Added: "TESTED")))))
176 Added:
177 Added: (ert-deftest rail-test-set-status-refuses-done ()
178 Added: "Set-status refuses DONE, because completion needs result evidence."
179 Added: (rail-tests--with-stream root
180 Added: (let* ((id (rail-tests--capture root "Refuse done" ["feat" "core"]))
181 Added: (result (rail-tests--decode
182 Added: (rail-tools--set-status-handler
183 Added: (list (cons 'root root) (cons 'id id)
184 Added: (cons 'state "DONE"))))))
185 Added: (should (string-match-p "rail-complete" (alist-get 'error result))))))
186 Added:
187 Added: (ert-deftest rail-test-block-and-cancel-record-a-reason ()
188 Added: "Block and cancel write the reason, so the decision is never silent."
189 Added: (rail-tests--with-stream root
190 Added: (let ((blocked (rail-tests--capture root "Blocked work" ["feat" "app"]))
191 Added: (dropped (rail-tests--capture root "Dropped work" ["feat" "ui"])))
192 Added: (rail-tests--call #'rail-tools--block-handler
193 Added: (list (cons 'root root) (cons 'id blocked)
194 Added: (cons 'reason "The route does not exist")))
195 Added: (rail-tests--call #'rail-tools--cancel-handler
196 Added: (list (cons 'root root) (cons 'id dropped)
197 Added: (cons 'reason "The user withdrew the request")))
198 Added: (let ((text (rail-tests--file-text root)))
199 Added: (should (string-match-p "- blocked :: The route does not exist" text))
200 Added: (should (string-match-p "- cancelled :: The user withdrew" text)))
201 Added: (should (equal "BLOCKED"
202 Added: (alist-get 'state (rail-tests--call
203 Added: #'rail-tools--verify-handler
204 Added: (list (cons 'root root)
205 Added: (cons 'id blocked))))))
206 Added: (should (equal "CANCELLED"
207 Added: (alist-get 'state (rail-tests--call
208 Added: #'rail-tools--verify-handler
209 Added: (list (cons 'root root)
210 Added: (cons 'id dropped)))))))))
211 Added:
212 Added: (ert-deftest rail-test-block-requires-a-reason ()
213 Added: "Block refuses a blank reason."
214 Added: (rail-tests--with-stream root
215 Added: (let* ((id (rail-tests--capture root "Needs a reason" ["feat" "app"]))
216 Added: (result (rail-tests--decode
217 Added: (rail-tools--block-handler
218 Added: (list (cons 'root root) (cons 'id id)
219 Added: (cons 'reason " "))))))
220 Added: (should (string-match-p "reason" (alist-get 'error result))))))
221 Added:
222 Added: ;;; Checklist, logbook, and tags
223 Added:
224 Added: (ert-deftest rail-test-checklist-adds-and-toggles-items ()
225 Added: "The checklist adds an item, then toggles it, and reports booleans."
226 Added: (rail-tests--with-stream root
227 Added: (let ((id (rail-tests--capture root "Split the task" ["feat" "core"])))
228 Added: (let ((added (rail-tests--call
229 Added: #'rail-tools--check-handler
230 Added: (list (cons 'root root) (cons 'id id)
231 Added: (cons 'action "add") (cons 'item "step one")))))
232 Added: (should (equal (alist-get 'done (aref (alist-get 'items added) 0))
233 Added: :json-false)))
234 Added: (let ((toggled (rail-tests--call
235 Added: #'rail-tools--check-handler
236 Added: (list (cons 'root root) (cons 'id id)
237 Added: (cons 'action "toggle") (cons 'item "step one")))))
238 Added: (should (eq (alist-get 'done (aref (alist-get 'items toggled) 0)) t)))
239 Added: (should (string-match-p "Checklist \\[1/1\\]" (rail-tests--file-text root))))))
240 Added:
241 Added: (ert-deftest rail-test-log-appends-and-keeps-earlier-notes ()
242 Added: "The logbook keeps every note, newest first."
243 Added: (rail-tests--with-stream root
244 Added: (let ((id (rail-tests--capture root "Log progress" ["feat" "core"])))
245 Added: (rail-tests--call #'rail-tools--log-handler
246 Added: (list (cons 'root root) (cons 'id id)
247 Added: (cons 'note "First note")))
248 Added: (rail-tests--call #'rail-tools--log-handler
249 Added: (list (cons 'root root) (cons 'id id)
250 Added: (cons 'note "Second note")))
251 Added: (let* ((entry (rail-tests--call #'rail-tools--show-handler
252 Added: (list (cons 'root root) (cons 'id id))))
253 Added: (notes (append (alist-get 'logbook entry) nil)))
254 Added: (should (equal (length notes) 2))
255 Added: (should (string-match-p "Second note" (nth 0 notes)))
256 Added: (should (string-match-p "First note" (nth 1 notes)))))))
257 Added:
258 Added: (ert-deftest rail-test-retag-validates-against-the-vocabulary ()
259 Added: "Retag replaces the tags, and refuses a tag outside the vocabulary."
260 Added: (rail-tests--with-stream root
261 Added: (let ((id (rail-tests--capture root "Retag me" ["feat" "core"])))
262 Added: (let ((result (rail-tests--call
263 Added: #'rail-tools--retag-handler
264 Added: (list (cons 'root root) (cons 'id id)
265 Added: (cons 'tags ["fix" "web" "minor"])))))
266 Added: (should (equal (append (alist-get 'tags result) nil)
267 Added: '("fix" "web" "minor"))))
268 Added: (let ((result (rail-tests--decode
269 Added: (rail-tools--retag-handler
270 Added: (list (cons 'root root) (cons 'id id)
271 Added: (cons 'tags ["nonsense"]))))))
272 Added: (should (string-match-p "Unknown tag" (alist-get 'error result)))))))
273 Added:
274 Added: ;;; Result and completion
275 Added:
276 Added: (ert-deftest rail-test-set-result-writes-model-and-notes ()
277 Added: "The result line holds the model, the commit, the tests, and the notes."
278 Added: (rail-tests--with-stream root
279 Added: (let* ((id (rail-tests--capture root "Record a result" ["fix" "core"]))
280 Added: (result (rail-tests--call
281 Added: #'rail-tools--set-result-handler
282 Added: (list (cons 'root root) (cons 'id id)
283 Added: (cons 'commit "abc1234") (cons 'tests "246 pass")
284 Added: (cons 'model "test-agent")
285 Added: (cons 'notes "the sentinel was wrong")))))
286 Added: (should (equal (alist-get 'result result)
287 Added: "model=test-agent commit=abc1234 tests=246 pass; the sentinel was wrong")))))
288 Added:
289 Added: (ert-deftest rail-test-set-result-omits-absent-optional-fields ()
290 Added: "The result line holds only the commit and the tests when nothing else is given."
291 Added: (rail-tests--with-stream root
292 Added: (let* ((id (rail-tests--capture root "Plain result" ["fix" "core"]))
293 Added: (result (rail-tests--call
294 Added: #'rail-tools--set-result-handler
295 Added: (list (cons 'root root) (cons 'id id)
296 Added: (cons 'commit "abc1234") (cons 'tests "246 pass")))))
297 Added: (should (equal (alist-get 'result result)
298 Added: "commit=abc1234 tests=246 pass")))))
299 Added:
300 Added: (ert-deftest rail-test-set-result-replaces-an-earlier-line ()
301 Added: "A second result call replaces the earlier result line."
302 Added: (rail-tests--with-stream root
303 Added: (let ((id (rail-tests--capture root "Replace result" ["fix" "core"])))
304 Added: (dolist (commit '("aaa1111" "bbb2222"))
305 Added: (rail-tests--call #'rail-tools--set-result-handler
306 Added: (list (cons 'root root) (cons 'id id)
307 Added: (cons 'commit commit) (cons 'tests "1 pass"))))
308 Added: (let ((text (rail-tests--file-text root)))
309 Added: (should-not (string-match-p "aaa1111" text))
310 Added: (should (string-match-p "bbb2222" text))))))
311 Added:
312 Added: (ert-deftest rail-test-complete-sets-done-with-result-and-closed ()
313 Added: "Completion writes the result, sets DONE, and confirms the CLOSED time."
314 Added: (rail-tests--with-stream root
315 Added: (let* ((id (rail-tests--capture root "Finish the work" ["feat" "core"]))
316 Added: (result (rail-tests--call
317 Added: #'rail-tools--complete-handler
318 Added: (list (cons 'root root) (cons 'id id)
319 Added: (cons 'commit "def5678") (cons 'tests "247 pass")
320 Added: (cons 'model "test-agent")))))
321 Added: (should (equal (alist-get 'state result) "DONE"))
322 Added: (should (string-match-p "commit=def5678" (alist-get 'result result)))
323 Added: (should (string-match-p "\\[.*\\]" (alist-get 'closed result)))
324 Added: (let ((entry (rail-tests--call #'rail-tools--verify-handler
325 Added: (list (cons 'root root) (cons 'id id)))))
326 Added: (should (equal (alist-get 'state entry) "DONE"))
327 Added: (should (stringp (alist-get 'closed entry)))))))
328 Added:
329 Added: (ert-deftest rail-test-complete-refuses-a-file-without-closed-logging ()
330 Added: "Completion refuses to run when the file does not log a CLOSED time.
331 Added: Org must write the CLOSED timestamp, so the tool never writes it."
332 Added: (let* ((root (file-name-as-directory (make-temp-file "rail-nolog" t)))
333 Added: (file (expand-file-name rail-stream-file-name root))
334 Added: (enable-local-variables :all)
335 Added: (org-id-track-globally nil)
336 Added: (create-lockfiles nil)
337 Added: (org-log-done nil))
338 Added: (unwind-protect
339 Added: (progn
340 Added: ;; This preamble holds no `org-log-done' local variable.
341 Added: (with-temp-file file
342 Added: (insert "#+TITLE: No logging\n"
343 Added: "#+TODO: TODO IN-PROGRESS | CANCELLED DONE\n"))
344 Added: (let ((id (rail-tests--capture root "No logging" ["feat" "core"])))
345 Added: ;; Close the buffer, so the next open reads the local variables
346 Added: ;; of the file as it now stands.
347 Added: (dolist (buf (buffer-list))
348 Added: (when (equal (buffer-file-name buf) file)
349 Added: (with-current-buffer buf (set-buffer-modified-p nil))
350 Added: (kill-buffer buf)))
351 Added: (let ((result (rail-tests--decode
352 Added: (rail-tools--complete-handler
353 Added: (list (cons 'root root) (cons 'id id)
354 Added: (cons 'commit "def5678")
355 Added: (cons 'tests "1 pass"))))))
356 Added: (should (stringp (alist-get 'error result)))
357 Added: (should (string-match-p "org-log-done"
358 Added: (alist-get 'error result))))))
359 Added: (dolist (buf (buffer-list))
360 Added: (when (and (buffer-file-name buf)
361 Added: (string-prefix-p root (buffer-file-name buf)))
362 Added: (with-current-buffer buf (set-buffer-modified-p nil))
363 Added: (kill-buffer buf)))
364 Added: (delete-directory root t))))
365 Added:
366 Added: ;;; Reading
367 Added:
368 Added: (ert-deftest rail-test-list-encodes-an-absent-value-as-json-null ()
369 Added: "A heading without an ID reports JSON null, not the text \"null\"."
370 Added: (rail-tests--with-stream root
371 Added: (let ((file (expand-file-name rail-stream-file-name root)))
372 Added: (with-current-buffer (find-file-noselect file)
373 Added: (goto-char (point-min))
374 Added: (insert "* TODO Entry without an identifier :feat:core:\n")
375 Added: (save-buffer))
376 Added: (let* ((json (rail-tools--list-handler (list (cons 'root root))))
377 Added: (rows (rail-tests--decode json)))
378 Added: (should (null (alist-get 'id (aref rows 0))))
379 Added: (should-not (string-match-p "\"null\"" json))))))
380 Added:
381 Added: (ert-deftest rail-test-list-filters-by-state-and-tag ()
382 Added: "The list filters narrow the result by TODO keyword and by tag."
383 Added: (rail-tests--with-stream root
384 Added: (let ((first (rail-tests--capture root "Filter one" ["feat" "core"])))
385 Added: (rail-tests--capture root "Filter two" ["fix" "web"])
386 Added: (rail-tests--call #'rail-tools--set-status-handler
387 Added: (list (cons 'root root) (cons 'id first)
388 Added: (cons 'state "IN-PROGRESS")))
389 Added: (let ((by-state (rail-tests--decode
390 Added: (rail-tools--list-handler
391 Added: (list (cons 'root root) (cons 'state "IN-PROGRESS")))))
392 Added: (by-tag (rail-tests--decode
393 Added: (rail-tools--list-handler
394 Added: (list (cons 'root root) (cons 'tag "web"))))))
395 Added: (should (equal (length by-state) 1))
396 Added: (should (equal (alist-get 'title (aref by-state 0)) "Filter one"))
397 Added: (should (equal (length by-tag) 1))
398 Added: (should (equal (alist-get 'title (aref by-tag 0)) "Filter two"))))))
399 Added:
400 Added: (ert-deftest rail-test-inspect-reports-the-sequence-and-the-axes ()
401 Added: "Inspect reports the file's keyword sequence and the file's tag axes."
402 Added: (rail-tests--with-stream root
403 Added: (let* ((result (rail-tests--call #'rail-tools--inspect-handler
404 Added: (list (cons 'root root))))
405 Added: (keywords (append (alist-get 'todo_keywords result) nil))
406 Added: (axes (alist-get 'tags result)))
407 Added: (should (member "IN-PROGRESS" keywords))
408 Added: (should (member "DONE" keywords))
409 Added: ;; The axes come from the file's own #+TAGS: lines, not from the code.
410 Added: (should (equal (mapcar #'car axes) '(kind scope impact)))
411 Added: (should (equal (append (alist-get 'impact axes) nil) '("minor" "major")))
412 Added: (should (equal (append (alist-get 'scope axes) nil) '("core" "app" "web" "ui"))))))
413 Added:
414 Added: (ert-deftest rail-test-a-file-without-tags-accepts-any-tag ()
415 Added: "A file that declares no #+TAGS: vocabulary accepts any tag.
416 Added: The tool carries no vocabulary of its own, so an undeclared file places
417 Added: no restriction on the tags."
418 Added: (let* ((root (file-name-as-directory (make-temp-file "rail-notags" t)))
419 Added: (file (expand-file-name rail-stream-file-name root))
420 Added: (org-id-track-globally nil)
421 Added: (create-lockfiles nil))
422 Added: (unwind-protect
423 Added: (progn
424 Added: (with-temp-file file
425 Added: (insert "#+TITLE: No vocabulary\n"
426 Added: "#+TODO: TODO | DONE\n"))
427 Added: (let* ((cap (rail-tests--call
428 Added: #'rail-tools--capture-handler
429 Added: (list (cons 'root root)
430 Added: (cons 'title "Any tag is fine")
431 Added: (cons 'tags ["anything" "at" "all"]))))
432 Added: (id (alist-get 'id cap)))
433 Added: (should (stringp id))
434 Added: (should (equal (append (alist-get 'tags cap) nil)
435 Added: '("anything" "at" "all")))))
436 Added: (dolist (buf (buffer-list))
437 Added: (when (and (buffer-file-name buf)
438 Added: (string-prefix-p root (buffer-file-name buf)))
439 Added: (with-current-buffer buf (set-buffer-modified-p nil))
440 Added: (kill-buffer buf)))
441 Added: (delete-directory root t))))
442 Added:
443 Added: ;;; The full path
444 Added:
445 Added: (ert-deftest rail-test-the-full-request-path-runs-end-to-end ()
446 Added: "One request moves from capture to DONE through every step."
447 Added: (rail-tests--with-stream root
448 Added: (let ((id (rail-tests--capture root "Ship the feature" ["feat" "web"]
449 Added: "Add a widget to the page.")))
450 Added: (rail-tests--call #'rail-tools--check-handler
451 Added: (list (cons 'root root) (cons 'id id)
452 Added: (cons 'action "add") (cons 'item "write the code")))
453 Added: (rail-tests--call #'rail-tools--set-status-handler
454 Added: (list (cons 'root root) (cons 'id id)
455 Added: (cons 'state "IN-PROGRESS")))
456 Added: (rail-tests--call #'rail-tools--log-handler
457 Added: (list (cons 'root root) (cons 'id id)
458 Added: (cons 'note "Started the work")))
459 Added: (rail-tests--call #'rail-tools--check-handler
460 Added: (list (cons 'root root) (cons 'id id)
461 Added: (cons 'action "toggle")
462 Added: (cons 'item "write the code")))
463 Added: (rail-tests--call #'rail-tools--set-status-handler
464 Added: (list (cons 'root root) (cons 'id id)
465 Added: (cons 'state "TESTED")))
466 Added: (rail-tests--call #'rail-tools--complete-handler
467 Added: (list (cons 'root root) (cons 'id id)
468 Added: (cons 'commit "0badc0de") (cons 'tests "3 pass")
469 Added: (cons 'model "test-agent")))
470 Added: (let ((entry (rail-tests--call #'rail-tools--show-handler
471 Added: (list (cons 'root root) (cons 'id id)))))
472 Added: (should (equal (alist-get 'state entry) "DONE"))
473 Added: (should (stringp (alist-get 'closed entry)))
474 Added: (should (string-match-p "commit=0badc0de" (alist-get 'result entry)))
475 Added: (should (equal (length (alist-get 'logbook entry)) 1))
476 Added: (should (eq (alist-get 'done (aref (alist-get 'checklist entry) 0)) t))
477 Added: (should (string-match-p "Add a widget" (alist-get 'body entry)))))))
478 Added:
479 Added: (provide 'rail-tests)
480 Added:
481 Added: ;;; rail-tests.el ends here
.kiro/skills/rail/rail-tools.el
index 00000000..d7bb0f98 000000..100644
@@ -0,0 +1,968 @@
1 Added: ;;; rail-tools.el --- RAIL MCP tools for RAIL.org -*- lexical-binding: t; -*-
2 Added:
3 Added: ;; Copyright (C) 2025
4 Added:
5 Added: ;; This file is NOT part of GNU Emacs.
6 Added:
7 Added: ;;; Commentary:
8 Added:
9 Added: ;; RAIL means "Rolling Action Item List".
10 Added: ;;
11 Added: ;; This file registers dedicated Emacs MCP tools for the RAIL skill so the
12 Added: ;; agent does not run raw `eval-elisp' snippets for each capture, status
13 Added: ;; change, log, result, or verification. Each operation becomes a named tool.
14 Added: ;;
15 Added: ;; RAIL.org is a flat stream. Each action item is a top-level heading, newest
16 Added: ;; first, tagged from a vocabulary that the file itself declares. The file
17 Added: ;; holds no container heading.
18 Added: ;;
19 Added: ;; The tools carry no project vocabulary and no project workflow. The stream
20 Added: ;; file owns both. The `#+TODO:' line declares the status keywords. The
21 Added: ;; `#+TAGS:' lines declare the tag vocabulary, grouped into axes. The tools
22 Added: ;; read both from the file, so one tool file serves every project.
23 Added: ;;
24 Added: ;; Why dedicated tools instead of `eval-elisp'?
25 Added: ;;
26 Added: ;; The `eval-elisp' tool routes its argument through
27 Added: ;; `mcp-server-security-safe-eval', whose form walker blocks or prompts for
28 Added: ;; "dangerous" functions such as `find-file-noselect', `write-file', and
29 Added: ;; `save-buffer'. With `mcp-server-security-prompt-for-permissions' set to t,
30 Added: ;; every RAIL snippet triggers a minibuffer prompt.
31 Added: ;;
32 Added: ;; A registered MCP tool runs through `mcp-server-tools-call', which calls the
33 Added: ;; handler function directly and does NOT pass through the form walker. The
34 Added: ;; handlers below therefore run without the repeated security prompt. Each
35 Added: ;; tool also carries MCP `annotations' so the MCP client can auto-approve the
36 Added: ;; read-only tools.
37 Added: ;;
38 Added: ;; The tools operate only on the file "RAIL.org". They find that file
39 Added: ;; under `rail-project-root', or under a caller-supplied project ROOT that
40 Added: ;; overrides it. They never touch any other file.
41 Added: ;;
42 Added: ;; `rail-project-root' comes from an upward search for the stream file. The
43 Added: ;; search starts at this file's own directory, then at `default-directory'. It
44 Added: ;; assumes no directory layout, so this file needs no absolute path and it
45 Added: ;; works on every machine.
46 Added: ;;
47 Added: ;; Install the tools once per Emacs session. Load this file, and the tools
48 Added: ;; register themselves. With `mcp-server-emacs-tools-enabled' set to `all',
49 Added: ;; which is the default, they appear in the MCP tool list at once.
50 Added: ;;
51 Added: ;; The MCP framework is a soft dependency. When the framework is absent, for
52 Added: ;; example in a batch test run, this file still loads and every handler stays
53 Added: ;; callable. Run the test suite with the run-tests.sh script beside this file.
54 Added:
55 Added: ;;; Code:
56 Added:
57 Added: (require 'cl-lib)
58 Added: (require 'org)
59 Added: (require 'org-id)
60 Added: (require 'json)
61 Added: (require 'subr-x)
62 Added:
63 Added: ;; Load the MCP tool framework when it is available. When it is absent, for
64 Added: ;; example in a batch test run, define the two symbols the registrations below
65 Added: ;; need and discard each registration. The handler functions stay callable, so
66 Added: ;; the test suite runs on any machine without the framework.
67 Added: (defconst rail-mcp-available (require 'mcp-server-tools nil t)
68 Added: "Non-nil when the Emacs MCP tool framework is available.")
69 Added:
70 Added: (unless rail-mcp-available
71 Added: ;; Define plain functions, never a struct. A stub struct would clobber the
72 Added: ;; real slot layout if the framework loads later in the same session.
73 Added: (defun make-mcp-server-tool (&rest _args)
74 Added: "Return nil. The MCP framework is absent."
75 Added: nil)
76 Added: (defun mcp-server-register-tool (_tool)
77 Added: "Discard _TOOL. The MCP framework is absent."
78 Added: nil))
79 Added:
80 Added: (defvar rail-stream-file-name "RAIL.org"
81 Added: "Name of the Org file that RAIL manages.")
82 Added:
83 Added: (defvar rail-tools-path
84 Added: (let ((file (or load-file-name buffer-file-name)))
85 Added: (and file (expand-file-name file)))
86 Added: "Absolute path of this file, or nil when the path is unknown.")
87 Added:
88 Added: (defun rail-locate-root (start)
89 Added: "Return the closest directory at or above START that holds the stream file.
90 Added: The stream file is `rail-stream-file-name'. Return nil when no
91 Added: ancestor directory holds that file."
92 Added: (let ((dir (and start (locate-dominating-file
93 Added: (file-name-as-directory (expand-file-name start))
94 Added: rail-stream-file-name))))
95 Added: (and dir (expand-file-name (file-name-as-directory dir)))))
96 Added:
97 Added: (defvar rail-project-root
98 Added: (or (rail-locate-root (and rail-tools-path
99 Added: (file-name-directory rail-tools-path)))
100 Added: (rail-locate-root default-directory)
101 Added: (expand-file-name default-directory))
102 Added: "Default project directory that holds the RAIL stream file.
103 Added: The value comes from an upward search for `rail-stream-file-name',
104 Added: first from this file's own directory, then from `default-directory'.
105 Added: The search makes no assumption about the depth of this file in the
106 Added: project. Set this variable to override the search, or pass a `root'
107 Added: argument to any tool.")
108 Added:
109 Added: ;;; Helpers
110 Added:
111 Added: (defun rail-tools--file (args)
112 Added: "Return the absolute path of the stream file for ARGS.
113 Added: ARGS may hold a `root' string that names the project directory. When
114 Added: `root' is absent, use `rail-project-root'. Signal an error when the
115 Added: selected root is not a directory."
116 Added: (let ((root (or (alist-get 'root args) rail-project-root)))
117 Added: (unless (and (stringp root) (> (length root) 0))
118 Added: (error "No project root: pass `root' or set `rail-project-root'"))
119 Added: (let ((dir (expand-file-name root)))
120 Added: (unless (file-directory-p dir)
121 Added: (error "Not a directory: %s" dir))
122 Added: (expand-file-name rail-stream-file-name dir))))
123 Added:
124 Added: (defun rail-tools--buffer (file)
125 Added: "Return an org-mode buffer visiting FILE, creating it as needed."
126 Added: (let ((buf (find-file-noselect file)))
127 Added: (with-current-buffer buf
128 Added: (unless (derived-mode-p 'org-mode)
129 Added: (org-mode)))
130 Added: buf))
131 Added:
132 Added: (defun rail-tools--goto-id (id)
133 Added: "Move point to the heading with Org ID in the current buffer.
134 Added: Signal an error when ID is not found."
135 Added: (let ((marker (org-id-find id 'marker)))
136 Added: (unless marker
137 Added: (error "Org ID not found: %s" id))
138 Added: (goto-char marker)))
139 Added:
140 Added: (defun rail-tools--fill-body ()
141 Added: "Wrap the body of the entry at point to 72 columns.
142 Added: Fill every paragraph after the metadata (SCHEDULED line, property
143 Added: drawer) up to the next heading. Use `org-fill-paragraph' so Org
144 Added: list items and other structure fill correctly. Point must be on
145 Added: the entry heading."
146 Added: (let ((fill-column 72))
147 Added: (org-back-to-heading t)
148 Added: (let ((end (save-excursion (org-end-of-subtree t t) (point-marker))))
149 Added: ;; Move past the heading and all metadata (planning line,
150 Added: ;; property drawer, logbook) to the first line of body text.
151 Added: (org-end-of-meta-data t)
152 Added: ;; Fill each body line. `org-fill-paragraph' fills the whole
153 Added: ;; element and is idempotent, so stepping one line at a time is
154 Added: ;; safe and does not overshoot a trailing paragraph.
155 Added: (while (< (point) end)
156 Added: (unless (looking-at-p "^[ \t]*$")
157 Added: (org-fill-paragraph))
158 Added: (forward-line 1))
159 Added: (set-marker end nil))))
160 Added:
161 Added: (defun rail-tools--nonblank (value)
162 Added: "Return VALUE trimmed when it is a non-blank string, else nil."
163 Added: (and (stringp value)
164 Added: (let ((trimmed (string-trim value)))
165 Added: (and (> (length trimmed) 0) trimmed))))
166 Added:
167 Added: (defun rail-tools--set-result (commit tests &optional model notes)
168 Added: "Write the structured result line for the entry at point.
169 Added: Replace an existing `- result ::' line, or append one at the end of
170 Added: the entry body. COMMIT is a commit hash. TESTS is a short recap
171 Added: such as \"215 pass\". MODEL names the agent that did the work, and
172 Added: NOTES adds a free-text tail after a semicolon. Both are optional.
173 Added: Keep the line unwrapped, because the reader captures a single line.
174 Added: Point must be on the entry heading."
175 Added: (org-back-to-heading t)
176 Added: (let* ((model (rail-tools--nonblank model))
177 Added: (notes (rail-tools--nonblank notes))
178 Added: (subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
179 Added: (line (concat "- result :: "
180 Added: (if model (format "model=%s " model) "")
181 Added: (format "commit=%s tests=%s" commit tests)
182 Added: (if notes (format "; %s" notes) ""))))
183 Added: (org-back-to-heading t)
184 Added: (if (re-search-forward "^[ \t]*- result ::.*$" subtree-end t)
185 Added: (replace-match line t t)
186 Added: (goto-char subtree-end)
187 Added: (skip-chars-backward "\n")
188 Added: (insert "\n\n" line))
189 Added: (set-marker subtree-end nil)))
190 Added:
191 Added: (defun rail-tools--append-log (note)
192 Added: "Append NOTE as a timestamped item to the entry's `:LOGBOOK:' drawer.
193 Added: Create the drawer directly after the metadata when it is absent.
194 Added: Insert the newest item first and wrap it to 72 columns. This is
195 Added: append-only. It never edits an existing item or the body. Point
196 Added: must be on the entry heading."
197 Added: (let ((fill-column 72))
198 Added: (org-back-to-heading t)
199 Added: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
200 Added: (ts (format-time-string "[%Y-%m-%d %a %H:%M]")))
201 Added: (org-back-to-heading t)
202 Added: (let ((drawer-start
203 Added: (save-excursion
204 Added: (when (re-search-forward "^[ \t]*:LOGBOOK:[ \t]*$" subtree-end t)
205 Added: (line-beginning-position)))))
206 Added: (unless drawer-start
207 Added: (org-end-of-meta-data t)
208 Added: (insert ":LOGBOOK:\n:END:\n")
209 Added: (setq drawer-start
210 Added: (save-excursion
211 Added: (org-back-to-heading t)
212 Added: (re-search-forward "^[ \t]*:LOGBOOK:[ \t]*$" subtree-end t)
213 Added: (line-beginning-position))))
214 Added: (goto-char drawer-start)
215 Added: (forward-line 1)
216 Added: (let ((item-start (point)))
217 Added: (insert (format "- %s %s\n" ts note))
218 Added: (save-excursion
219 Added: (goto-char item-start)
220 Added: (org-fill-paragraph))))
221 Added: (set-marker subtree-end nil))))
222 Added:
223 Added: (defmacro rail-tools--json (&rest body)
224 Added: "Evaluate BODY and return its value as a JSON string.
225 Added: Catch any error and return a JSON object with an `error' field."
226 Added: (declare (indent 0))
227 Added: `(condition-case err
228 Added: (json-encode (progn ,@body))
229 Added: (error (json-encode `((error . ,(error-message-string err)))))))
230 Added:
231 Added: ;;; Tag vocabulary
232 Added:
233 Added: ;; The vocabulary is not hardcoded. Each stream file declares it with
234 Added: ;; `#+TAGS:' group-tag lines, for example:
235 Added: ;;
236 Added: ;; #+TAGS: [ Kind : feat fix chore ]
237 Added: ;; #+TAGS: [ Scope : core web ]
238 Added: ;;
239 Added: ;; Org parses those lines into `org-current-tag-alist'. The functions below
240 Added: ;; read that alist, so the vocabulary follows the file, not this code. A file
241 Added: ;; with no `#+TAGS:' line accepts any tag.
242 Added:
243 Added: (defun rail-tools--tag-axes ()
244 Added: "Return the tag vocabulary of the current buffer, grouped by axis.
245 Added: Read the group tags that Org parsed from the `#+TAGS:' lines into
246 Added: `org-current-tag-alist'. Return an alist that maps each axis symbol to
247 Added: its list of tag strings. Return nil when the file declares no axis, and
248 Added: then the file accepts any tag."
249 Added: (let ((axes '()) (current nil))
250 Added: (dolist (entry org-current-tag-alist)
251 Added: (pcase entry
252 Added: (`(:startgrouptag) (setq current nil))
253 Added: (`(:endgrouptag)
254 Added: (when current
255 Added: (push (cons (intern (downcase (car current)))
256 Added: (nreverse (cdr current)))
257 Added: axes))
258 Added: (setq current nil))
259 Added: (`(:grouptags))
260 Added: (`(,(and tag (pred stringp)) . ,_)
261 Added: (if current
262 Added: (setcdr current (cons tag (cdr current)))
263 Added: ;; The first tag in a group is the axis name.
264 Added: (setq current (cons tag '()))))))
265 Added: (nreverse axes)))
266 Added:
267 Added: (defun rail-tools--all-tags ()
268 Added: "Return every tag the current buffer declares, as one flat list.
269 Added: Return nil when the file declares no vocabulary."
270 Added: (apply #'append (mapcar #'cdr (rail-tools--tag-axes))))
271 Added:
272 Added: (defun rail-tools--check-tags (tags)
273 Added: "Signal an error when TAGS holds a tag outside the file vocabulary.
274 Added: TAGS is a list of strings. When the file declares no vocabulary, accept
275 Added: any tag. Return TAGS unchanged when valid."
276 Added: (let ((allowed (rail-tools--all-tags)))
277 Added: (when allowed
278 Added: (dolist (tag tags)
279 Added: (unless (member tag allowed)
280 Added: (error "Unknown tag `%s'; allowed: %s"
281 Added: tag (string-join allowed ", "))))))
282 Added: tags)
283 Added:
284 Added: (defun rail-tools--goto-stream-top ()
285 Added: "Move point to the insertion place for a new item.
286 Added: That place is the start of the first top-level heading, after the
287 Added: file preamble. When no heading exists, move to the end of the
288 Added: preamble."
289 Added: (goto-char (point-min))
290 Added: (if (re-search-forward "^\\* " nil t)
291 Added: (goto-char (line-beginning-position))
292 Added: (goto-char (point-max))))
293 Added:
294 Added: ;;; inspect (read-only)
295 Added:
296 Added: (defun rail-tools--inspect-handler (args)
297 Added: "Report the TODO sequence and the tag vocabulary for RAIL.org.
298 Added: Read both from the stream file, so the report mirrors the file."
299 Added: (rail-tools--json
300 Added: (let ((file (rail-tools--file args)))
301 Added: (with-current-buffer (rail-tools--buffer file)
302 Added: (org-with-wide-buffer
303 Added: `((file . ,file)
304 Added: (todo_keywords . ,(vconcat org-todo-keywords-1))
305 Added: (tags . ,(mapcar (lambda (axis)
306 Added: (cons (car axis) (vconcat (cdr axis))))
307 Added: (rail-tools--tag-axes)))))))))
308 Added:
309 Added: (mcp-server-register-tool
310 Added: (make-mcp-server-tool
311 Added: :name "rail-inspect"
312 Added: :title "RAIL Inspect"
313 Added: :description "Inspect RAIL.org: return its TODO keyword sequence and the tag vocabulary that the file declares, grouped by axis. Read-only."
314 Added: :input-schema '((type . "object")
315 Added: (properties . ((root . ((type . "string")
316 Added: (description . "Absolute path to the project directory containing RAIL.org")))))
317 Added: (required . []))
318 Added: :function #'rail-tools--inspect-handler
319 Added: :annotations '((readOnlyHint . t)
320 Added: (destructiveHint . :false)
321 Added: (idempotentHint . t)
322 Added: (openWorldHint . :false))))
323 Added:
324 Added: ;;; list (read-only)
325 Added:
326 Added: (defun rail-tools--list-handler (args)
327 Added: "List the top-level action items in RAIL.org, newest first.
328 Added: ARGS keys: `root', `state' (optional), `tag' (optional). When STATE is
329 Added: given, return only items with that TODO keyword. When TAG is given,
330 Added: return only items that carry that tag. Each row has `id', `title',
331 Added: `state', `scheduled', and `tags'."
332 Added: (rail-tools--json
333 Added: (let ((file (rail-tools--file args))
334 Added: (state (alist-get 'state args))
335 Added: (tag (alist-get 'tag args)))
336 Added: (with-current-buffer (rail-tools--buffer file)
337 Added: (org-with-wide-buffer
338 Added: (goto-char (point-min))
339 Added: (let ((rows '()))
340 Added: (while (re-search-forward "^\\* " nil t)
341 Added: (let ((todo (org-get-todo-state))
342 Added: (tags (org-get-tags nil t)))
343 Added: (when (and (or (null state) (equal state todo))
344 Added: (or (null tag) (member tag tags)))
345 Added: (push `((id . ,(org-id-get))
346 Added: (title . ,(org-get-heading t t t t))
347 Added: (state . ,todo)
348 Added: (scheduled . ,(org-entry-get nil "SCHEDULED"))
349 Added: (tags . ,(vconcat tags)))
350 Added: rows))))
351 Added: ;; The file is newest-first, so reverse to keep that order.
352 Added: (vconcat (nreverse rows))))))))
353 Added:
354 Added: (mcp-server-register-tool
355 Added: (make-mcp-server-tool
356 Added: :name "rail-list"
357 Added: :title "RAIL List"
358 Added: :description "List the top-level action items in RAIL.org, newest first, with each entry's Org ID, title, TODO state, SCHEDULED time, and tags. Pass an optional `state' or `tag' to filter. Read-only."
359 Added: :input-schema '((type . "object")
360 Added: (properties . ((root . ((type . "string")
361 Added: (description . "Absolute path to the project directory")))
362 Added: (state . ((type . "string")
363 Added: (description . "Optional TODO keyword filter, for example TODO or IN-PROGRESS")))
364 Added: (tag . ((type . "string")
365 Added: (description . "Optional tag filter, for example web or major")))))
366 Added: (required . []))
367 Added: :function #'rail-tools--list-handler
368 Added: :annotations '((readOnlyHint . t)
369 Added: (destructiveHint . :false)
370 Added: (idempotentHint . t)
371 Added: (openWorldHint . :false))))
372 Added:
373 Added: ;;; capture
374 Added:
375 Added: (defun rail-tools--capture-handler (args)
376 Added: "Capture a TODO entry at the top of the RAIL.org stream.
377 Added: ARGS keys: `root', `title', `body' (optional), `tags' (optional array).
378 Added: Insert the entry as a top-level heading directly below the file
379 Added: preamble, so the newest item is first. Record the capture time as an
380 Added: inactive SCHEDULED timestamp, apply TAGS from the file vocabulary,
381 Added: assign an Org ID, and wrap the body to 72 columns."
382 Added: (rail-tools--json
383 Added: (let* ((file (rail-tools--file args))
384 Added: (title (or (alist-get 'title args) (error "Missing `title'")))
385 Added: (body (or (alist-get 'body args) ""))
386 Added: (raw-tags (append (alist-get 'tags args) nil))
387 Added: (captured-at (format-time-string "[%Y-%m-%d %a %H:%M]")))
388 Added: (with-current-buffer (rail-tools--buffer file)
389 Added: (org-with-wide-buffer
390 Added: ;; Validate inside the buffer, because the vocabulary lives here.
391 Added: (let ((tags (rail-tools--check-tags raw-tags)))
392 Added: (rail-tools--goto-stream-top)
393 Added: (let ((start (point)))
394 Added: (insert (format "* TODO %s\nSCHEDULED: %s\n" title captured-at))
395 Added: (unless (string-empty-p body)
396 Added: (insert body "\n"))
397 Added: (goto-char start)
398 Added: (when tags
399 Added: (org-set-tags tags))
400 Added: (let ((id (org-id-get-create)))
401 Added: (rail-tools--fill-body)
402 Added: (when (buffer-modified-p) (save-buffer))
403 Added: (goto-char (org-id-find id 'marker))
404 Added: `((id . ,id)
405 Added: (file . ,file)
406 Added: (heading . ,(org-get-heading t t t t))
407 Added: (tags . ,(vconcat (org-get-tags nil t))))))))))))
408 Added:
409 Added: (mcp-server-register-tool
410 Added: (make-mcp-server-tool
411 Added: :name "rail-capture"
412 Added: :title "RAIL Capture"
413 Added: :description "Capture a TODO action item as a top-level heading at the top of RAIL.org, so the newest item comes first. Applies tags that the file vocabulary permits, records the capture time as an inactive SCHEDULED timestamp, wraps the body to 72 columns, and assigns an Org ID."
414 Added: :input-schema '((type . "object")
415 Added: (properties . ((root . ((type . "string")
416 Added: (description . "Absolute path to the project directory")))
417 Added: (title . ((type . "string")
418 Added: (description . "Imperative title under 60 chars")))
419 Added: (body . ((type . "string")
420 Added: (description . "Full request text, verbatim")))
421 Added: (tags . ((type . "array")
422 Added: (items . ((type . "string")))
423 Added: (description . "Tags from the vocabulary that the file declares in its #+TAGS: lines. Run rail-inspect to read the axes and their allowed tags.")))))
424 Added: (required . ["title"]))
425 Added: :function #'rail-tools--capture-handler
426 Added: :annotations '((readOnlyHint . :false)
427 Added: (destructiveHint . :false)
428 Added: (idempotentHint . :false)
429 Added: (openWorldHint . :false))))
430 Added:
431 Added: ;;; set-status
432 Added:
433 Added: (defun rail-tools--set-status-handler (args)
434 Added: "Change the TODO keyword of an entry.
435 Added: ARGS keys: `root', `id', `state'. STATE must be one keyword from the
436 Added: file's own #+TODO sequence, and must not be DONE (use rail-complete)."
437 Added: (rail-tools--json
438 Added: (let ((file (rail-tools--file args))
439 Added: (id (or (alist-get 'id args) (error "Missing `id'")))
440 Added: (state (or (alist-get 'state args) (error "Missing `state'"))))
441 Added: (when (string-equal state "DONE")
442 Added: (error "Use rail-complete for DONE, not rail-set-status"))
443 Added: (with-current-buffer (rail-tools--buffer file)
444 Added: (rail-tools--goto-id id)
445 Added: (org-todo state)
446 Added: (when (buffer-modified-p) (save-buffer))
447 Added: `((id . ,id)
448 Added: (state . ,(org-get-todo-state)))))))
449 Added:
450 Added: (mcp-server-register-tool
451 Added: (make-mcp-server-tool
452 Added: :name "rail-set-status"
453 Added: :title "RAIL Set Status"
454 Added: :description "Set the TODO keyword of a RAIL entry to any open keyword from the file's own #+TODO sequence. Does not accept DONE; use rail-complete for that."
455 Added: :input-schema '((type . "object")
456 Added: (properties . ((root . ((type . "string")))
457 Added: (id . ((type . "string")
458 Added: (description . "Org ID of the entry")))
459 Added: (state . ((type . "string")
460 Added: (description . "TODO keyword from the file's #+TODO sequence")))))
461 Added: (required . ["id" "state"]))
462 Added: :function #'rail-tools--set-status-handler
463 Added: :annotations '((readOnlyHint . :false)
464 Added: (destructiveHint . :false)
465 Added: (idempotentHint . t)
466 Added: (openWorldHint . :false))))
467 Added:
468 Added: ;;; log
469 Added:
470 Added: (defun rail-tools--log-handler (args)
471 Added: "Append a timestamped note to an entry's `:LOGBOOK:' drawer.
472 Added: ARGS keys: `root', `id', `note'. Append-only progress feedback from an
473 Added: agentic session. Never edits an existing note or the item body."
474 Added: (rail-tools--json
475 Added: (let ((file (rail-tools--file args))
476 Added: (id (or (alist-get 'id args) (error "Missing `id'")))
477 Added: (note (or (alist-get 'note args) (error "Missing `note'"))))
478 Added: (with-current-buffer (rail-tools--buffer file)
479 Added: (rail-tools--goto-id id)
480 Added: (rail-tools--append-log note)
481 Added: (when (buffer-modified-p) (save-buffer))
482 Added: `((id . ,id)
483 Added: (state . ,(org-get-todo-state)))))))
484 Added:
485 Added: (mcp-server-register-tool
486 Added: (make-mcp-server-tool
487 Added: :name "rail-log"
488 Added: :title "RAIL Log"
489 Added: :description "Append a timestamped progress note to a RAIL entry's `:LOGBOOK:' drawer. The drawer is append-only. The tool never rewrites an earlier note, and never rewrites the item body. Newest note first, wrapped to 72 columns."
490 Added: :input-schema '((type . "object")
491 Added: (properties . ((root . ((type . "string")))
492 Added: (id . ((type . "string")
493 Added: (description . "Org ID of the entry")))
494 Added: (note . ((type . "string")
495 Added: (description . "Progress note to append")))))
496 Added: (required . ["id" "note"]))
497 Added: :function #'rail-tools--log-handler
498 Added: :annotations '((readOnlyHint . :false)
499 Added: (destructiveHint . :false)
500 Added: (idempotentHint . :false)
501 Added: (openWorldHint . :false))))
502 Added:
503 Added: ;;; set-result
504 Added:
505 Added: (defun rail-tools--set-result-handler (args)
506 Added: "Write the structured result line for an entry.
507 Added: ARGS keys: `root', `id', `commit', `tests', `model' (optional),
508 Added: `notes' (optional). COMMIT is a commit hash. TESTS is a short recap
509 Added: such as \"215 pass\". Replace an existing result line or append one at
510 Added: the end of the body."
511 Added: (rail-tools--json
512 Added: (let ((file (rail-tools--file args))
513 Added: (id (or (alist-get 'id args) (error "Missing `id'")))
514 Added: (commit (or (alist-get 'commit args) (error "Missing `commit'")))
515 Added: (tests (or (alist-get 'tests args) (error "Missing `tests'")))
516 Added: (model (alist-get 'model args))
517 Added: (notes (alist-get 'notes args)))
518 Added: (with-current-buffer (rail-tools--buffer file)
519 Added: (rail-tools--goto-id id)
520 Added: (rail-tools--set-result commit tests model notes)
521 Added: (when (buffer-modified-p) (save-buffer))
522 Added: (rail-tools--goto-id id)
523 Added: `((id . ,id)
524 Added: (result . ,(rail-tools--result-text)))))))
525 Added:
526 Added: (mcp-server-register-tool
527 Added: (make-mcp-server-tool
528 Added: :name "rail-set-result"
529 Added: :title "RAIL Set Result"
530 Added: :description "Write the structured `- result ::' line for a RAIL 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. rail-complete calls this when you pass commit and tests."
531 Added: :input-schema '((type . "object")
532 Added: (properties . ((root . ((type . "string")))
533 Added: (id . ((type . "string")
534 Added: (description . "Org ID of the entry")))
535 Added: (commit . ((type . "string")
536 Added: (description . "Commit hash")))
537 Added: (tests . ((type . "string")
538 Added: (description . "Short test recap, for example \"215 pass\"")))
539 Added: (model . ((type . "string")
540 Added: (description . "Optional model or agent that did the work, for example the agent name")))
541 Added: (notes . ((type . "string")
542 Added: (description . "Optional free-text tail appended after a semicolon, for example a root cause")))))
543 Added: (required . ["id" "commit" "tests"]))
544 Added: :function #'rail-tools--set-result-handler
545 Added: :annotations '((readOnlyHint . :false)
546 Added: (destructiveHint . :false)
547 Added: (idempotentHint . t)
548 Added: (openWorldHint . :false))))
549 Added:
550 Added: ;;; check (checklist for sub-tasks)
551 Added:
552 Added: (defconst rail-tools--checklist-header "Checklist [/]:"
553 Added: "Header line that introduces an item's checkbox list.
554 Added: The `[/]' cookie tracks completed items against the total.")
555 Added:
556 Added: (defun rail-tools--checklist-add (item)
557 Added: "Add ITEM as an unchecked checkbox to the entry at point.
558 Added: Create the checklist block when it does not exist. Wrap ITEM to 72
559 Added: columns and refresh the `[/]' cookie. Point must be on the heading."
560 Added: (let ((fill-column 72))
561 Added: (org-back-to-heading t)
562 Added: (let* ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
563 Added: (line (format "- [ ] %s\n" item))
564 Added: insert-at)
565 Added: (org-back-to-heading t)
566 Added: (if (re-search-forward "^Checklist \\[[0-9]*/[0-9]*\\]:[ \t]*$"
567 Added: subtree-end t)
568 Added: ;; Existing block: step past the trailing checkbox items.
569 Added: (progn (forward-line 1)
570 Added: (while (looking-at-p "^- \\[.\\] \\|^ ") (forward-line 1))
571 Added: (setq insert-at (point))
572 Added: (insert line))
573 Added: ;; No block: append one at the end of the body.
574 Added: (goto-char subtree-end)
575 Added: (skip-chars-backward "\n")
576 Added: (insert "\n\n" rail-tools--checklist-header "\n")
577 Added: (setq insert-at (point))
578 Added: (insert line))
579 Added: (save-excursion (goto-char insert-at) (org-fill-paragraph))
580 Added: (org-update-checkbox-count)
581 Added: (set-marker subtree-end nil))))
582 Added:
583 Added: (defun rail-tools--checklist-toggle (item)
584 Added: "Toggle the checkbox whose text matches ITEM in the entry at point.
585 Added: Signal an error when no item matches. Refresh the `[/]' cookie.
586 Added: Point must be on the heading."
587 Added: (org-back-to-heading t)
588 Added: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker))))
589 Added: (org-back-to-heading t)
590 Added: (if (re-search-forward (concat "^- \\[.\\] " (regexp-quote item))
591 Added: subtree-end t)
592 Added: (progn (beginning-of-line) (org-toggle-checkbox)
593 Added: (org-update-checkbox-count))
594 Added: (set-marker subtree-end nil)
595 Added: (error "No checklist item matches: %s" item))
596 Added: (set-marker subtree-end nil)))
597 Added:
598 Added: (defun rail-tools--checklist-items ()
599 Added: "Return the checklist items of the entry at point.
600 Added: Each item is an alist with `done' and `text'. Point must be on the
601 Added: heading."
602 Added: (org-back-to-heading t)
603 Added: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point)))
604 Added: (items '()))
605 Added: (org-back-to-heading t)
606 Added: (while (re-search-forward "^- \\[\\(.\\)\\] \\(.*\\)$" subtree-end t)
607 Added: (push `((done . ,(if (string-equal (match-string 1) " ") :json-false t))
608 Added: (text . ,(string-trim (match-string-no-properties 2))))
609 Added: items))
610 Added: (vconcat (nreverse items))))
611 Added:
612 Added: (defun rail-tools--check-handler (args)
613 Added: "Manage the checklist of an item, for splitting a complex task.
614 Added: ARGS keys: `root', `id', `action' (add|toggle|list), `item'.
615 Added: `add' appends an unchecked item. `toggle' flips a matching item.
616 Added: `list' returns the items. A `[/]' cookie tracks progress."
617 Added: (rail-tools--json
618 Added: (let ((file (rail-tools--file args))
619 Added: (id (or (alist-get 'id args) (error "Missing `id'")))
620 Added: (action (or (alist-get 'action args) (error "Missing `action'")))
621 Added: (item (alist-get 'item args)))
622 Added: (with-current-buffer (rail-tools--buffer file)
623 Added: (rail-tools--goto-id id)
624 Added: (cond
625 Added: ((string-equal action "add")
626 Added: (unless item (error "`add' needs an `item'"))
627 Added: (rail-tools--checklist-add item))
628 Added: ((string-equal action "toggle")
629 Added: (unless item (error "`toggle' needs an `item'"))
630 Added: (rail-tools--checklist-toggle item))
631 Added: ((string-equal action "list") nil)
632 Added: (t (error "Unknown action `%s'; use add, toggle, or list" action)))
633 Added: (when (buffer-modified-p) (save-buffer))
634 Added: (rail-tools--goto-id id)
635 Added: `((id . ,id)
636 Added: (items . ,(rail-tools--checklist-items)))))))
637 Added:
638 Added: (mcp-server-register-tool
639 Added: (make-mcp-server-tool
640 Added: :name "rail-check"
641 Added: :title "RAIL Checklist"
642 Added: :description "Manage an item'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."
643 Added: :input-schema '((type . "object")
644 Added: (properties . ((root . ((type . "string")))
645 Added: (id . ((type . "string")
646 Added: (description . "Org ID of the item")))
647 Added: (action . ((type . "string")
648 Added: (description . "add, toggle, or list")))
649 Added: (item . ((type . "string")
650 Added: (description . "Item text for add or toggle")))))
651 Added: (required . ["id" "action"]))
652 Added: :function #'rail-tools--check-handler
653 Added: :annotations '((readOnlyHint . :false)
654 Added: (destructiveHint . :false)
655 Added: (idempotentHint . :false)
656 Added: (openWorldHint . :false))))
657 Added:
658 Added: ;;; show (read-only)
659 Added:
660 Added: (defun rail-tools--body-text ()
661 Added: "Return the plain body text of the entry at point.
662 Added: Read from the first line after the metadata up to the first of: a
663 Added: `Checklist [' line, a `- result ::' line, or the end of the subtree.
664 Added: Return the trimmed string. Point must be on the entry heading."
665 Added: (org-back-to-heading t)
666 Added: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point))))
667 Added: (org-back-to-heading t)
668 Added: (org-end-of-meta-data t)
669 Added: (let ((body-start (point))
670 Added: (body-end subtree-end))
671 Added: (save-excursion
672 Added: (goto-char body-start)
673 Added: (when (re-search-forward "^\\(Checklist \\[\\|[ \t]*- result ::\\)"
674 Added: subtree-end t)
675 Added: (setq body-end (line-beginning-position))))
676 Added: (string-trim
677 Added: (buffer-substring-no-properties body-start body-end)))))
678 Added:
679 Added: (defun rail-tools--logbook-items ()
680 Added: "Return the `:LOGBOOK:' drawer item lines of the entry at point.
681 Added: Each item is a string, in the order stored (newest first). Return an
682 Added: empty vector when there is no drawer. Point must be on the heading."
683 Added: (org-back-to-heading t)
684 Added: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point)))
685 Added: (items '()))
686 Added: (org-back-to-heading t)
687 Added: (when (re-search-forward "^[ \t]*:LOGBOOK:[ \t]*$" subtree-end t)
688 Added: (forward-line 1)
689 Added: (while (and (< (point) subtree-end)
690 Added: (not (looking-at-p "^[ \t]*:END:[ \t]*$")))
691 Added: (when (looking-at "^[ \t]*- \\(.*\\)$")
692 Added: (push (string-trim (match-string-no-properties 1)) items))
693 Added: (forward-line 1)))
694 Added: (vconcat (nreverse items))))
695 Added:
696 Added: (defun rail-tools--result-text ()
697 Added: "Return the text after `- result ::' for the entry at point.
698 Added: Return nil when there is no result line, so it encodes as JSON null.
699 Added: Point must be on the heading."
700 Added: (org-back-to-heading t)
701 Added: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point))))
702 Added: (org-back-to-heading t)
703 Added: (when (re-search-forward "^[ \t]*- result ::[ \t]*\\(.*\\)$"
704 Added: subtree-end t)
705 Added: (string-trim (match-string-no-properties 1)))))
706 Added:
707 Added: (defun rail-tools--show-handler (args)
708 Added: "Return the full content of an entry.
709 Added: ARGS keys: `root', `id'. Report the heading, state, tags, scheduled
710 Added: and closed timestamps, body text, logbook notes, checklist items, and
711 Added: result line. Read-only."
712 Added: (rail-tools--json
713 Added: (let ((file (rail-tools--file args))
714 Added: (id (or (alist-get 'id args) (error "Missing `id'"))))
715 Added: (with-current-buffer (rail-tools--buffer file)
716 Added: (org-with-wide-buffer
717 Added: (rail-tools--goto-id id)
718 Added: `((id . ,id)
719 Added: (heading . ,(org-get-heading t t t t))
720 Added: (state . ,(org-get-todo-state))
721 Added: (tags . ,(vconcat (org-get-tags nil t)))
722 Added: (scheduled . ,(org-entry-get nil "SCHEDULED"))
723 Added: (closed . ,(org-entry-get nil "CLOSED"))
724 Added: (body . ,(rail-tools--body-text))
725 Added: (logbook . ,(rail-tools--logbook-items))
726 Added: (checklist . ,(rail-tools--checklist-items))
727 Added: (result . ,(rail-tools--result-text))))))))
728 Added:
729 Added: (mcp-server-register-tool
730 Added: (make-mcp-server-tool
731 Added: :name "rail-show"
732 Added: :title "RAIL Show"
733 Added: :description "Return the full content of a RAIL entry: heading, state, tags, scheduled and closed timestamps, body text, logbook notes, checklist items, and result line. Read-only."
734 Added: :input-schema '((type . "object")
735 Added: (properties . ((root . ((type . "string")))
736 Added: (id . ((type . "string")
737 Added: (description . "Org ID of the entry")))))
738 Added: (required . ["id"]))
739 Added: :function #'rail-tools--show-handler
740 Added: :annotations '((readOnlyHint . t)
741 Added: (destructiveHint . :false)
742 Added: (idempotentHint . t)
743 Added: (openWorldHint . :false))))
744 Added:
745 Added: ;;; retag
746 Added:
747 Added: (defun rail-tools--retag-handler (args)
748 Added: "Replace the tags on an entry with a validated set.
749 Added: ARGS keys: `root', `id', `tags' (array). Validate TAGS against the
750 Added: file vocabulary, then set them, keeping the file's default tag
751 Added: alignment."
752 Added: (rail-tools--json
753 Added: (let ((file (rail-tools--file args))
754 Added: (id (or (alist-get 'id args) (error "Missing `id'")))
755 Added: (raw-tags (append (alist-get 'tags args) nil)))
756 Added: (with-current-buffer (rail-tools--buffer file)
757 Added: (let ((tags (rail-tools--check-tags raw-tags)))
758 Added: (rail-tools--goto-id id)
759 Added: (org-set-tags tags)
760 Added: (when (buffer-modified-p) (save-buffer))
761 Added: `((id . ,id)
762 Added: (tags . ,(vconcat (org-get-tags nil t)))))))))
763 Added:
764 Added: (mcp-server-register-tool
765 Added: (make-mcp-server-tool
766 Added: :name "rail-retag"
767 Added: :title "RAIL Retag"
768 Added: :description "Replace the tags on a RAIL entry with a validated set from the file vocabulary. Use this tool to re-tag an entry as its shape changes. The tool keeps the file's default tag alignment."
769 Added: :input-schema '((type . "object")
770 Added: (properties . ((root . ((type . "string")))
771 Added: (id . ((type . "string")
772 Added: (description . "Org ID of the entry")))
773 Added: (tags . ((type . "array")
774 Added: (items . ((type . "string")))
775 Added: (description . "Tags from the file vocabulary")))))
776 Added: (required . ["id" "tags"]))
777 Added: :function #'rail-tools--retag-handler
778 Added: :annotations '((readOnlyHint . :false)
779 Added: (destructiveHint . :false)
780 Added: (idempotentHint . :false)
781 Added: (openWorldHint . :false))))
782 Added:
783 Added: ;;; cancel and block
784 Added:
785 Added: (defun rail-tools--transition-with-reason (id keyword marker-label reason)
786 Added: "Transition entry ID to KEYWORD and record REASON.
787 Added: Signal an error when REASON is missing or blank. Write a wrapped body
788 Added: line `- MARKER-LABEL :: REASON', replacing an existing line of that
789 Added: form or appending one at the end of the body. Fill the line to 72
790 Added: columns, then set the TODO keyword and save."
791 Added: (when (or (null reason) (string-empty-p (string-trim reason)))
792 Added: (error "A reason is required"))
793 Added: (rail-tools--goto-id id)
794 Added: (let ((fill-column 72))
795 Added: (org-back-to-heading t)
796 Added: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
797 Added: (line (format "- %s :: %s" marker-label reason))
798 Added: insert-at)
799 Added: (org-back-to-heading t)
800 Added: (if (re-search-forward
801 Added: (format "^[ \t]*- %s ::.*$" (regexp-quote marker-label))
802 Added: subtree-end t)
803 Added: (progn (replace-match line t t)
804 Added: (setq insert-at (line-beginning-position)))
805 Added: (goto-char subtree-end)
806 Added: (skip-chars-backward "\n")
807 Added: (insert "\n\n" line)
808 Added: (setq insert-at (line-beginning-position)))
809 Added: (save-excursion (goto-char insert-at) (org-fill-paragraph))
810 Added: (set-marker subtree-end nil)))
811 Added: (rail-tools--goto-id id)
812 Added: (org-todo keyword)
813 Added: (when (buffer-modified-p) (save-buffer)))
814 Added:
815 Added: (defun rail-tools--cancel-handler (args)
816 Added: "Set an entry to CANCELLED with a required reason.
817 Added: ARGS keys: `root', `id', `reason'. Record REASON as a `- cancelled ::'
818 Added: line so the decision is never silent."
819 Added: (rail-tools--json
820 Added: (let ((file (rail-tools--file args))
821 Added: (id (or (alist-get 'id args) (error "Missing `id'")))
822 Added: (reason (alist-get 'reason args)))
823 Added: (with-current-buffer (rail-tools--buffer file)
824 Added: (rail-tools--transition-with-reason id "CANCELLED" "cancelled" reason)
825 Added: (rail-tools--goto-id id)
826 Added: `((id . ,id)
827 Added: (state . ,(org-get-todo-state)))))))
828 Added:
829 Added: (mcp-server-register-tool
830 Added: (make-mcp-server-tool
831 Added: :name "rail-cancel"
832 Added: :title "RAIL Cancel"
833 Added: :description "Set a RAIL entry to CANCELLED and record a required reason as a `- cancelled ::' line, so the decision is never silent."
834 Added: :input-schema '((type . "object")
835 Added: (properties . ((root . ((type . "string")))
836 Added: (id . ((type . "string")
837 Added: (description . "Org ID of the entry")))
838 Added: (reason . ((type . "string")
839 Added: (description . "Reason for cancelling the entry")))))
840 Added: (required . ["id" "reason"]))
841 Added: :function #'rail-tools--cancel-handler
842 Added: :annotations '((readOnlyHint . :false)
843 Added: (destructiveHint . :false)
844 Added: (idempotentHint . :false)
845 Added: (openWorldHint . :false))))
846 Added:
847 Added: (defun rail-tools--block-handler (args)
848 Added: "Set an entry to BLOCKED with a required reason.
849 Added: ARGS keys: `root', `id', `reason'. Record REASON as a `- blocked ::'
850 Added: line so the blocker is never silent."
851 Added: (rail-tools--json
852 Added: (let ((file (rail-tools--file args))
853 Added: (id (or (alist-get 'id args) (error "Missing `id'")))
854 Added: (reason (alist-get 'reason args)))
855 Added: (with-current-buffer (rail-tools--buffer file)
856 Added: (rail-tools--transition-with-reason id "BLOCKED" "blocked" reason)
857 Added: (rail-tools--goto-id id)
858 Added: `((id . ,id)
859 Added: (state . ,(org-get-todo-state)))))))
860 Added:
861 Added: (mcp-server-register-tool
862 Added: (make-mcp-server-tool
863 Added: :name "rail-block"
864 Added: :title "RAIL Block"
865 Added: :description "Set a RAIL entry to BLOCKED and record a required reason as a `- blocked ::' line, so the blocker is never silent."
866 Added: :input-schema '((type . "object")
867 Added: (properties . ((root . ((type . "string")))
868 Added: (id . ((type . "string")
869 Added: (description . "Org ID of the entry")))
870 Added: (reason . ((type . "string")
871 Added: (description . "Reason for blocking the entry")))))
872 Added: (required . ["id" "reason"]))
873 Added: :function #'rail-tools--block-handler
874 Added: :annotations '((readOnlyHint . :false)
875 Added: (destructiveHint . :false)
876 Added: (idempotentHint . :false)
877 Added: (openWorldHint . :false))))
878 Added:
879 Added: ;;; complete
880 Added:
881 Added: (defun rail-tools--complete-handler (args)
882 Added: "Set an entry to DONE with its result evidence and confirm CLOSED.
883 Added: ARGS keys: `root', `id', `commit', `tests', `model' (optional),
884 Added: `notes' (optional). Write the structured result line, then transition
885 Added: to DONE. Requires `org-log-done' to be `time' so the normal Org
886 Added: transition inserts CLOSED; this handler never writes CLOSED."
887 Added: (rail-tools--json
888 Added: (let ((file (rail-tools--file args))
889 Added: (id (or (alist-get 'id args) (error "Missing `id'")))
890 Added: (commit (or (alist-get 'commit args) (error "Missing `commit'")))
891 Added: (tests (or (alist-get 'tests args) (error "Missing `tests'")))
892 Added: (model (alist-get 'model args))
893 Added: (notes (alist-get 'notes args)))
894 Added: (with-current-buffer (rail-tools--buffer file)
895 Added: (hack-local-variables)
896 Added: (unless (eq org-log-done 'time)
897 Added: (error "org-log-done is not set to time"))
898 Added: (rail-tools--goto-id id)
899 Added: (rail-tools--set-result commit tests model notes)
900 Added: (rail-tools--goto-id id)
901 Added: (org-todo "DONE")
902 Added: (when (buffer-modified-p) (save-buffer))
903 Added: (let ((closed (org-entry-get nil "CLOSED")))
904 Added: (unless closed
905 Added: (error "Org did not record a CLOSED timestamp"))
906 Added: `((id . ,id)
907 Added: (state . ,(org-get-todo-state))
908 Added: (result . ,(rail-tools--result-text))
909 Added: (closed . ,closed)))))))
910 Added:
911 Added: (mcp-server-register-tool
912 Added: (make-mcp-server-tool
913 Added: :name "rail-complete"
914 Added: :title "RAIL Complete"
915 Added: :description "Set a RAIL 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."
916 Added: :input-schema '((type . "object")
917 Added: (properties . ((root . ((type . "string")))
918 Added: (id . ((type . "string")))
919 Added: (commit . ((type . "string")
920 Added: (description . "Commit hash for the completed work")))
921 Added: (tests . ((type . "string")
922 Added: (description . "Short test recap, for example \"215 pass\"")))
923 Added: (model . ((type . "string")
924 Added: (description . "Optional model or agent that did the work, for example the agent name")))
925 Added: (notes . ((type . "string")
926 Added: (description . "Optional free-text tail appended after a semicolon, for example a root cause")))))
927 Added: (required . ["id" "commit" "tests"]))
928 Added: :function #'rail-tools--complete-handler
929 Added: :annotations '((readOnlyHint . :false)
930 Added: (destructiveHint . :false)
931 Added: (idempotentHint . t)
932 Added: (openWorldHint . :false))))
933 Added:
934 Added: ;;; verify (read-only)
935 Added:
936 Added: (defun rail-tools--verify-handler (args)
937 Added: "Return the current state of the entry with `id'.
938 Added: ARGS keys: `root', `id'. Read-only."
939 Added: (rail-tools--json
940 Added: (let ((file (rail-tools--file args))
941 Added: (id (or (alist-get 'id args) (error "Missing `id'"))))
942 Added: (with-current-buffer (rail-tools--buffer file)
943 Added: (org-with-wide-buffer
944 Added: (rail-tools--goto-id id)
945 Added: `((id . ,id)
946 Added: (heading . ,(org-get-heading t t t t))
947 Added: (state . ,(org-get-todo-state))
948 Added: (closed . ,(org-entry-get nil "CLOSED"))
949 Added: (tags . ,(vconcat (org-get-tags nil t)))))))))
950 Added:
951 Added: (mcp-server-register-tool
952 Added: (make-mcp-server-tool
953 Added: :name "rail-verify"
954 Added: :title "RAIL Verify"
955 Added: :description "Return the heading, TODO state, CLOSED timestamp, and tags of a RAIL entry. Read-only."
956 Added: :input-schema '((type . "object")
957 Added: (properties . ((root . ((type . "string")))
958 Added: (id . ((type . "string")))))
959 Added: (required . ["id"]))
960 Added: :function #'rail-tools--verify-handler
961 Added: :annotations '((readOnlyHint . t)
962 Added: (destructiveHint . :false)
963 Added: (idempotentHint . t)
964 Added: (openWorldHint . :false))))
965 Added:
966 Added: (provide 'rail-tools)
967 Added:
968 Added: ;;; rail-tools.el ends here
.kiro/skills/rail/references/tools.md
index 00000000..3fdd594d 000000..100644
@@ -0,0 +1,168 @@
1 Added: # RAIL 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: `rail-project-root`. That variable comes from an upward search for
9 Added: `RAIL.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: ### rail-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: ### rail-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: ### rail-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: ### rail-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: ### rail-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 item text, as plain text.
66 Added: - `tags` — optional. An array of tags from the file vocabulary (the #+TAGS: lines).
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: ### rail-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 `rail-complete` for `DONE`.
84 Added:
85 Added: ### rail-block and rail-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: `rail-block` writes a `- blocked ::` line and sets `BLOCKED`.
95 Added: `rail-cancel` writes a `- cancelled ::` line and sets `CANCELLED`.
96 Added: Each tool refuses a blank reason.
97 Added:
98 Added: ### rail-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: ### rail-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: ### rail-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 file vocabulary (the #+TAGS: lines).
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: ### rail-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: ### rail-complete
152 Added:
153 Added: Arguments: the same arguments as `rail-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/rail/run-tests.sh
index 00000000..3e35738e 000000..100755
@@ -0,0 +1,35 @@
1 Added: #!/bin/sh
2 Added: # Run the RAIL 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 rail-tools.el rail-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/rail-tools.el" \
29 Added: -l "$dir/rail-tests.el" \
30 Added: -f ert-run-tests-batch-and-exit; then
31 Added: echo "RAIL tests passed."
32 Added: else
33 Added: echo "Error: RAIL tests failed. Read the ERT output above for the failing test." >&2
34 Added: exit 1
35 Added: fi
RAIL.org
index 00000000..5c3f46a4 000000..100644
@@ -0,0 +1,667 @@
1 Added: #+TITLE: Hito action items
2 Added: #+AUTHOR: Marius Peter
3 Added: #+STARTUP: content
4 Added: #+TODO: TODO IN-PROGRESS TESTING TESTED BLOCKED | CANCELLED DONE
5 Added: #+TAGS: [ Kind : feat fix refactor chore docs ]
6 Added: #+TAGS: [ Scope : core app web ui ]
7 Added: #+TAGS: [ Impact : minor major ]
8 Added: #+TAGS: [ Misc : doctrine ]
9 Added:
10 Added: * DONE Add a time graph of feedback elements to the logbook :feat:ui:
11 Added: CLOSED: [2026-09-06 Sun 14:23] SCHEDULED: [2026-09-06 Sun 13:43]
12 Added: :PROPERTIES:
13 Added: :ID: b7813f9d-931a-4993-ae08-ff1c661b9775
14 Added: :END:
15 Added: The logbook page should present a time graph of various feedback elements over time.
16 Added:
17 Added: - result :: model=kiro-cli commit=71580da tests=246 pass
18 Added: * DONE Fix elapsed duration timer on workout logging page :fix:ui:
19 Added: CLOSED: [2026-09-06 Sun 13:58] SCHEDULED: [2026-09-06 Sun 13:42]
20 Added: :PROPERTIES:
21 Added: :ID: 8fcbf31c-92d4-4b9b-ab6f-b5e723f70312
22 Added: :END:
23 Added: Fix the elapsed duration timer in the current workout logging page.
24 Added:
25 Added: - result :: model=kiro-cli commit=dbe2dda tests=241 pass; root cause js_of_ocaml 31-bit int overflow on epoch values, fixed with float arithmetic
26 Added: * DONE Move username and password editing into a modal :refactor:ui:
27 Added: CLOSED: [2026-09-06 Sun 14:30] SCHEDULED: [2026-09-06 Sun 13:39]
28 Added: :PROPERTIES:
29 Added: :ID: a68fba78-f707-4f57-b757-2ba1214d9876
30 Added: :END:
31 Added: On the profile page, username and password should not have dedicated input fields. We should only see username, then the value for the user's username, then the change username button affordance, not the input field. Editing username and password should pop open a modal menu.
32 Added:
33 Added: - result :: model=kiro-cli commit=d38fd43 tests=247 pass
34 Added: * DONE Remove the black line after the masthead :refactor:ui:minor:
35 Added: CLOSED: [2026-09-06 Sun 13:52] SCHEDULED: [2026-09-06 Sun 13:32]
36 Added: :PROPERTIES:
37 Added: :ID: fc45fee8-a8c9-4d8b-8403-5d3219926c63
38 Added: :END:
39 Added:
40 Added: Remove the black line after the masthead
41 Added:
42 Added: - result :: model=kiro-cli commit=d6b3b97 tests=241 pass
43 Added: * BLOCKED Add profile dropdown actions :feat:app:
44 Added: SCHEDULED: [2026-09-06 Sun 13:09]
45 Added: :PROPERTIES:
46 Added: :ID: db8901b3-f932-4c88-8d0d-7ddadfb97f32
47 Added: :END:
48 Added: Source: application feedback (VPS, trainee t1, sequence 5, submitted 2026-09-05T22:18:25Z)
49 Added:
50 Added: Make the profile control a drop-down with Profile, Settings, and Sign out actions.
51 Added:
52 Added: - blocked :: No Settings page or /settings route exists; /profile already holds account settings (username, password). Awaiting decision: (1) point both Profile and Settings at /profile, (2) drop Settings for Profile + Sign out only, or (3) create a new /settings page (scope TBD).
53 Added:
54 Added: * DONE Define five-point subjective feedback scores :feat:core:
55 Added: CLOSED: [2026-09-06 Sun 14:07] SCHEDULED: [2026-09-06 Sun 13:09]
56 Added: :PROPERTIES:
57 Added: :ID: 2b46d3fb-9cfc-43f3-958c-b348d1b7d58a
58 Added: :END:
59 Added: Source: application feedback (VPS, trainee t1, sequence 1, submitted 2026-09-05T22:13:05Z)
60 Added:
61 Added: Use exactly the scores 1 through 5 for subjective feedback. Score 1 means very poor. Score 5 means very good. Update the core domain as a consequence.
62 Added:
63 Added: - result :: model=kiro-cli commit=3051af0 tests=243 pass; level now Very_poor..Very_good (1..5); migration 6 drops legacy feedback
64 Added:
65 Added: * DONE Preserve partial subjective feedback :feat:core:
66 Added: CLOSED: [2026-09-06 Sun 14:19] SCHEDULED: [2026-09-06 Sun 13:09]
67 Added: :PROPERTIES:
68 Added: :ID: 50a8e1a2-d6a4-4aa9-8852-65afc15517ce
69 Added: :END:
70 Added: Source: application feedback (VPS, trainee t1, sequence 1, submitted 2026-09-05T22:13:05Z)
71 Added:
72 Added: Make Back and Skip preserve partial answers. Make Close cancel the feedback flow. Make Skip ignore the feedback event. Allow subjective feedback to be logged as a partial list.
73 Added:
74 Added: - result :: model=kiro-cli commit=6fde451 tests=245 pass
75 Added:
76 Added: * DONE Format page titles with an em dash :feat:ui:minor:
77 Added: CLOSED: [2026-09-06 Sun 13:51] SCHEDULED: [2026-09-06 Sun 13:09]
78 Added: :PROPERTIES:
79 Added: :ID: 9800d7b7-552e-4ed1-9e96-587400ee9f7b
80 Added: :END:
81 Added: Source: application feedback (VPS, trainee t1, sequence 4, submitted 2026-09-05T22:16:53Z)
82 Added:
83 Added: Use a literal em dash in page titles, such as Hito — Home.
84 Added:
85 Added: - result :: model=kiro-cli commit=5e4236c tests=241 pass
86 Added:
87 Added: * DONE Standardize the global corner radius :refactor:ui:minor:
88 Added: CLOSED: [2026-09-06 Sun 13:53] SCHEDULED: [2026-09-06 Sun 13:09]
89 Added: :PROPERTIES:
90 Added: :ID: 357455c5-a6a3-4dec-ad6b-5be098a11013
91 Added: :END:
92 Added: Source: application feedback (VPS, trainee t1, sequence 3, submitted 2026-09-05T22:15:29Z)
93 Added:
94 Added: Use one global border radius in relative units so that it scales across components.
95 Added:
96 Added: - result :: model=kiro-cli commit=a45027e tests=241 pass
97 Added:
98 Added: * DONE Animate modals with reduced-motion support :feat:ui:
99 Added: CLOSED: [2026-09-06 Sun 13:54] SCHEDULED: [2026-09-06 Sun 13:09]
100 Added: :PROPERTIES:
101 Added: :ID: 30266d78-2731-4306-a748-02e479d97867
102 Added: :END:
103 Added: Source: application feedback (VPS, trainee t1, sequence 6, submitted 2026-09-06T08:37:36Z)
104 Added:
105 Added: Apply a short, smooth modal animation to every modal. Disable modal animation when the user prefers reduced motion.
106 Added:
107 Added: - result :: model=kiro-cli commit=4b8df35 tests=241 pass
108 Added: * DONE Verify POST redirect GET for forms :feat:app:
109 Added: CLOSED: [2026-09-06 Sun 14:25] SCHEDULED: [2026-09-06 Sun 13:09]
110 Added: :PROPERTIES:
111 Added: :ID: 106b5917-48d9-423e-9ce9-11dc4c230603
112 Added: :END:
113 Added: Source: application feedback (VPS, trainee t1, sequence 2, submitted 2026-09-05T22:14:58Z)
114 Added:
115 Added: Redirect each form submission to the page that originated the form flow when practical. Decide whether a result type should persist success and error messages across the redirect.
116 Added:
117 Added: - result :: model=kiro-cli commit=0637e57 tests=247 pass; audit found all state-changing POSTs already redirect; decision: flash/query for success, inline 400 re-render for field validation
118 Added:
119 Added: * DONE Give each prescribed exercise its own routine sub-row :feat:web:
120 Added: CLOSED: [2026-09-06 Sun 14:01] SCHEDULED: [2026-09-05 Sat 23:10]
121 Added: :PROPERTIES:
122 Added: :ID: be3140eb-47d5-4391-9f05-7b8c3cd294f1
123 Added: :END:
124 Added: - Each prescribed exercise should exist in a separate sub-row.
125 Added: - Stimuli count column should not exist.
126 Added: - Each prescribed exercise should have specific sub-columns for exercise name, rep count.
127 Added:
128 Added: - result :: model=kiro-cli commit=28caa63 tests=241 pass
129 Added:
130 Added: * DONE Capture each prompt as its own request :chore:web:
131 Added: CLOSED: [2026-09-05 Sat 22:01] SCHEDULED: [2026-09-05 Sat 21:31]
132 Added: :PROPERTIES:
133 Added: :ID: cfdca5b4-80d3-448f-bf54-a365dd36a578
134 Added: :END:
135 Added:
136 Added: - result :: model=kiro-cli commit=d5553a2 tests=pass
137 Added: Edit the stream-of-thought SKILL so each individual prompt in a session becomes its own feature request, not the whole session as one request.
138 Added: * DONE Use POST-redirect-GET for all forms :feat:app:
139 Added: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
140 Added: :PROPERTIES:
141 Added: :ID: 3A2316B3-F9C1-41F2-B0FC-E1F6958BC9D5
142 Added: :END:
143 Added:
144 Added: Ensure proper POST redirect GET patter for all forms
145 Added:
146 Added: Clarification:
147 Added: - Apply this to feedback, application feedback, and workout forms.
148 Added: - Preserve success and error messages for toast display on the redirected page.
149 Added:
150 Added: - result :: commit=41a44ff tests=215 pass
151 Added:
152 Added: * DONE Standardize shared CRUD patterns :refactor:app:
153 Added: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
154 Added: :PROPERTIES:
155 Added: :ID: D214DE90-88B9-4ADD-AB2F-4AF7B7E87EF7
156 Added: :END:
157 Added:
158 Added: Rationalize shared CRUD pattern for domain objects, user data, and app feedback
159 Added:
160 Added: - result :: commit=ea594ba tests=215 pass
161 Added:
162 Added: * DONE Use worse-same-better subjective feedback :feat:core:
163 Added: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
164 Added: :PROPERTIES:
165 Added: :ID: 484FE4E8-73F1-4789-AF8D-FC07087DEC5E
166 Added: :END:
167 Added:
168 Added: Subjective Feedback evidence should be collected against the following pattern: Worse, Same, Better [than usual]
169 Added:
170 Added: Clarification:
171 Added: - Ask, "How are you feeling?"
172 Added: - Collect Worse, Same, and Better through inline button groups.
173 Added: - Apply this to sleep, appetite, readiness, motivation, and perceived difficulty.
174 Added: - This probably has an impact on core data types as well.
175 Added:
176 Added: - result :: commit=f09fd5d tests=215 pass
177 Added:
178 Added: * DONE Make Write feedback the primary action :feat:app:
179 Added: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
180 Added: :PROPERTIES:
181 Added: :ID: 84E85E7A-A1DF-4F2E-913C-9A1C6F441DDB
182 Added: :END:
183 Added:
184 Added: "Write feedback" should be the single Primary action on the app-feedback page. Secondary actions are CRUD operations on previous feedback items.
185 Added:
186 Added: Clarification:
187 Added: - Secondary CRUD actions are editing and removing the user’s feedback items.
188 Added: - Show the primary action once.
189 Added:
190 Added: - result :: commit=43da59d tests=215 pass
191 Added:
192 Added: * BLOCKED Import Hevy CSV from profile :feat:app:
193 Added: SCHEDULED: [2026-09-05 Sat 21:25]
194 Added: :PROPERTIES:
195 Added: :ID: 2559dd31-18e1-4f62-8d16-92f96d782203
196 Added: :END:
197 Added:
198 Added: - note :: Deferred by the request clarification. Left open under Dispatch; not implemented this session.
199 Added: On profile page, user should be able to import Hevy CSV
200 Added:
201 Added: Clarification:
202 Added: - Defer this feedback feature.
203 Added:
204 Added: * CANCELLED Fix workout cancellation flow :fix:app:
205 Added: CLOSED: [2026-09-05 Sat 21:58] SCHEDULED: [2026-09-05 Sat 21:25]
206 Added: :PROPERTIES:
207 Added: :ID: 2B914C9E-DF02-420C-8D5A-D0503552C88F
208 Added: :END:
209 Added:
210 Added: workout cancellation flow still has bug
211 Added:
212 Added: - note :: No remaining defect. Prior commits 7d9ee9e, 5aaa11f, and test ea1f211 fixed cancellation: CSRF-guarded discard, no stale record, 303 redirect to Home. test_web.ml cancellation cases pass (203 tests green).
213 Added: * CANCELLED Emphasize the secondary color :feat:ui:
214 Added: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
215 Added: :PROPERTIES:
216 Added: :ID: C1CCF7A7-FA07-43C5-82BA-54E9B8C3B54B
217 Added: :END:
218 Added:
219 Added: oxblood color is still too dominant; secondary color needs to be more emphasized
220 Added:
221 Added: Clarification:
222 Added: - Use brass for secondary buttons, such as cancellation.
223 Added:
224 Added: - note :: Already implemented by the existing brass secondary-button styling; no new work was required.
225 Added:
226 Added: * CANCELLED Use progressive subjective feedback buttons :feat:ui:
227 Added: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
228 Added: :PROPERTIES:
229 Added: :ID: F59C1B0A-C238-4059-91D1-42DB5CC17CBF
230 Added: :END:
231 Added:
232 Added: subjective feedback affordances should be more progressive. not dropdowns, but button groups containing three buttons each for below, usual, and above.
233 Added:
234 Added: - note :: Duplicate of the completed Worse/Same/Better inline controls proposal (commit f09fd5d).
235 Added:
236 Added: * DONE Store database timestamps as ISO 8601 :feat:core:
237 Added: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
238 Added: :PROPERTIES:
239 Added: :ID: 950BD105-C680-4D58-914F-0FAFA0ECBA63
240 Added: :END:
241 Added:
242 Added: database timestamps should be stored as ISO8601 strings
243 Added:
244 Added: Clarification:
245 Added: - Store timestamps in UTC Zulu time.
246 Added: - Use the ISO 8601 format.
247 Added: - Do not migrate existing data.
248 Added: - Existing data can be dropped safely.
249 Added:
250 Added: - result :: commit=79bbbd7 tests=215 pass; migration 5 drops affected legacy evidence
251 Added: * DONE Remove spurious Page updated mention :fix:ui:minor:
252 Added: CLOSED: [2026-09-05 Sat 21:57] SCHEDULED: [2026-09-05 Sat 21:25]
253 Added: :PROPERTIES:
254 Added: :ID: C8FD4798-94A4-42C6-8D86-03A4A58DF703
255 Added: :END:
256 Added:
257 Added: Spurious "Page updated" mention in workout logging screen
258 Added:
259 Added: - result :: model=kiro-cli commit=dab53ca tests=pass
260 Added: * DONE Confirm workout override with a modal :feat:ui:
261 Added: CLOSED: [2026-09-05 Sat 22:23] SCHEDULED: [2026-09-05 Sat 21:25]
262 Added: :PROPERTIES:
263 Added: :ID: b9d05a7f-c8f9-4fb6-878f-a9d1f89ed45d
264 Added: :END:
265 Added:
266 Added: - result :: model=kiro-cli commit=7c9f701 tests=pass
267 Added: Implement a modal confirmation when beginning a workout under override
268 Added:
269 Added: Clarification:
270 Added: - The override condition is training before full rest has completed.
271 Added: - Show a confirmation modal menu before starting the workout.
272 Added:
273 Added: * DONE Show early workout notice as a toast :feat:ui:doctrine:
274 Added: CLOSED: [2026-09-05 Sat 22:26] SCHEDULED: [2026-09-05 Sat 21:25]
275 Added: :PROPERTIES:
276 Added: :ID: 8052f6e1-7281-4a57-a615-504476ca6c16
277 Added: :END:
278 Added:
279 Added: - result :: model=kiro-cli commit=9702a02 tests=pass
280 Added: "begin before recovery finished" should be a toast message rather than a component in the main content.
281 Added:
282 Added: Clarification:
283 Added: - The toast is informational only because the modal already confirms the override.
284 Added: - Display it for 3 seconds, or for the duration recommended by WCAG.
285 Added:
286 Added: * DONE Add custom error pages :feat:app:
287 Added: CLOSED: [2026-09-05 Sat 22:20] SCHEDULED: [2026-09-05 Sat 21:25]
288 Added: :PROPERTIES:
289 Added: :ID: 965d5284-a593-43ca-b8e4-202b093e6502
290 Added: :END:
291 Added:
292 Added: - result :: model=kiro-cli commit=34ab578 tests=pass
293 Added: Need custom 404 etc. pages
294 Added:
295 Added: Clarification:
296 Added: - Add the most common 400 and 500 error pages.
297 Added: - Do not expose specific information from the server.
298 Added: - Keep the desktop top navigation affordance.
299 Added: - Keep the mobile bottom navigation affordance.
300 Added:
301 Added: * DONE Show feedback author and contribution count :feat:web:
302 Added: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:24]
303 Added: :PROPERTIES:
304 Added: :ID: 428E433E-5F01-44CF-B642-FC1DD80BF087
305 Added: :END:
306 Added:
307 Added: feedback should display username that submitted it, alongside their total number of feedback contributions.
308 Added:
309 Added: Clarification:
310 Added: - Users should see real usernames.
311 Added: - Rank the feedback list by upvotes.
312 Added: - Let users upvote feedback submitted by other users they think is valuable.
313 Added:
314 Added: - result :: commit=98e5fb5 tests=215 pass
315 Added:
316 Added: * CANCELLED Keep the routine as a table :feat:ui:
317 Added: CLOSED: [2026-09-05 Sat 22:02] SCHEDULED: [2026-09-05 Sat 21:24]
318 Added: :PROPERTIES:
319 Added: :ID: 6b126d4c-6058-4b7a-8c67-640ce6160ba7
320 Added: :END:
321 Added:
322 Added: - note :: Duplicate. The routine already renders as an HTML table (commit 4e4acac, "Replace routine accordions with an HTML table"). No new work.
323 Added: routine must absolutely be a table.
324 Added:
325 Added: Clarification:
326 Added: - This is a duplicate request.
327 Added:
328 Added: * DONE Fix workout log exercise selection :fix:ui:
329 Added: CLOSED: [2026-09-05 Sat 22:02] SCHEDULED: [2026-09-05 Sat 21:24]
330 Added: :PROPERTIES:
331 Added: :ID: 45453231-7ac9-444c-96c2-af387a34bb0c
332 Added: :END:
333 Added:
334 Added: - result :: model=kiro-cli commit=61bf45c tests=pass
335 Added: Broken workout log editing: exercise selection button.
336 Added:
337 Added: Clarification:
338 Added: - This concerns editing a previously logged workout on mobile.
339 Added: - The user must be able to select and switch the exercise correctly.
340 Added:
341 Added: * DONE Use one desktop-to-mobile breakpoint :feat:ui:
342 Added: :PROPERTIES:
343 Added: :ID: 948EABAC-9D59-4101-8901-E37DE0D9DFF1
344 Added: :END:
345 Added:
346 Added: Desktop has top navigation. Mobile has an identical bottom navigation.
347 Added:
348 Added: * DONE Use three always-available navigation actions :feat:ui:
349 Added: :PROPERTIES:
350 Added: :ID: FE8559B5-190D-4C5C-9A1E-CE5FA826C094
351 Added: :END:
352 Added:
353 Added: Use `Home`, `Routine` when the user is not logging (changing to `Current Workout` while logging), and `<username>` for the workout log. Keep all three actions available in both modes.
354 Added:
355 Added: * DONE Simplify the top bar brand :refactor:ui:
356 Added: :PROPERTIES:
357 Added: :ID: DB0BC418-C81C-4AB9-87BC-1B475747A04D
358 Added: :END:
359 Added:
360 Added: Replace the top bar brand with `hito` in Work Sans, without a subtitle.
361 Added:
362 Added: * DONE Remove the mobile hamburger menu :refactor:ui:minor:
363 Added: :PROPERTIES:
364 Added: :ID: C32BAF01-548E-4CBD-BBD0-00ECEDEE97CB
365 Added: :END:
366 Added:
367 Added: Do not show a hamburger menu on mobile.
368 Added:
369 Added: * DONE Increase the masthead brand size :feat:ui:minor:
370 Added: :PROPERTIES:
371 Added: :ID: 8D6BC6B9-F248-4714-BA93-9C27532DF490
372 Added: :END:
373 Added:
374 Added: Make the masthead brand name appear much larger.
375 Added:
376 Added: * DONE Hide sign-out on mobile :feat:ui:minor:
377 Added: :PROPERTIES:
378 Added: :ID: 2265C4FA-071E-4BFA-A038-3CD3CB3656EA
379 Added: :END:
380 Added:
381 Added: Do not show the sign-out button on mobile.
382 Added:
383 Added: * DONE Remove thick black bars from mobile bottom navigation :refactor:ui:minor:
384 Added: :PROPERTIES:
385 Added: :ID: F2D41A95-58A6-461A-A5DF-AC862FCD877A
386 Added: :END:
387 Added:
388 Added: Remove the thick black bars visible in the mobile bottom navigation.
389 Added:
390 Added: * DONE Fix active bottom-navigation state for current workout and logbook :fix:ui:
391 Added: :PROPERTIES:
392 Added: :ID: 35E2CC6C-B489-4CBC-B214-DC8D1626E559
393 Added: :END:
394 Added:
395 Added: When a current workout is open, selecting the logbook tab marks both Current Workout and Logbook as active in the bottom navigation. Ensure only the selected page is highlighted.
396 Added:
397 Added: * DONE Increase bottom-navigation button text size :feat:ui:minor:
398 Added: :PROPERTIES:
399 Added: :ID: 06F564D7-A177-4F3D-9F17-80B35C1A4C25
400 Added: :END:
401 Added:
402 Added: Increase the bottom-navigation button text font size to better support WCAG-aligned readability.
403 Added:
404 Added: * DONE Animate page transitions :feat:ui:
405 Added: :PROPERTIES:
406 Added: :ID: 41C03F68-9DD2-4B79-B1A7-8EE450599B9B
407 Added: :END:
408 Added:
409 Added: Animate page transitions between Home, Current Workout, and Logbook.
410 Added:
411 Added: * DONE Standardize Logbook navigation and add a mobile profile button :refactor:ui:
412 Added: :PROPERTIES:
413 Added: :ID: D52C18B2-2F97-4A70-8F0D-6CBD261F20F2
414 Added: :END:
415 Added:
416 Added: Always label the logbook tab `Logbook`. Add a dedicated profile button for the user name in the mobile top navigation bar.
417 Added:
418 Added: * DONE Return the middle navigation tab to Routine after cancellation :feat:ui:
419 Added: :PROPERTIES:
420 Added: :ID: 31F886F8-D3A3-464D-8785-4548B9B50C11
421 Added: :END:
422 Added:
423 Added: After cancelling workout logging, change the middle navigation tab from Current Workout back to Routine.
424 Added: - result :: model=kiro-cli commit=ea1f211 tests=pass
425 Added:
426 Added: * DONE Add placeholders to load and reps fields :feat:ui:minor:
427 Added: :PROPERTIES:
428 Added: :ID: 61A936E9-A4F6-4FA1-8282-27231E82ED61
429 Added: :END:
430 Added:
431 Added: Add placeholder values to the load and reps fields on the workout logging page.
432 Added:
433 Added: * DONE Remove the redundant fieldset legend :refactor:ui:minor:
434 Added: :PROPERTIES:
435 Added: :ID: 7BC3EF05-5208-4CD9-9BD3-113F063E8D07
436 Added: :END:
437 Added:
438 Added: Remove the redundant fieldset legend from workout logging.
439 Added:
440 Added: * DONE Show an in-progress workout card on Home :feat:ui:
441 Added: :PROPERTIES:
442 Added: :ID: 9C1865CB-70F2-41FA-917E-369C5CAD7031
443 Added: :END:
444 Added:
445 Added: When the user is logging a current workout, show a Home card that indicates the workout is in progress and links to the Current Workout tab.
446 Added:
447 Added: * DONE Make workout cancellation unconditional :feat:app:
448 Added: :PROPERTIES:
449 Added: :ID: E86AC9DF-E909-42F4-9D52-8DEEFCC70B5F
450 Added: :END:
451 Added:
452 Added: When cancelling a workout, always leave the current workout view without showing an invalid-form response. Save the workout log as-is for later editing, then redirect the user to Home.
453 Added:
454 Added: * DONE Add a sticky workout timer bar :feat:ui:
455 Added: :PROPERTIES:
456 Added: :ID: C6B85644-E352-4442-A49B-C3582BD89670
457 Added: :END:
458 Added:
459 Added: When logging a workout, show a sticky bar at the top of the screen with a workout timer.
460 Added:
461 Added: * DONE Replace the exercise button group with a dropdown :refactor:ui:
462 Added: :PROPERTIES:
463 Added: :ID: EAF4A9D5-1EC3-48B0-975C-58939C5A86ED
464 Added: :END:
465 Added:
466 Added: Use a dropdown menu to select the exercise instead of the exercise button group.
467 Added:
468 Added: * DONE Remove the boulder top border from exercise logging cards :refactor:ui:minor:
469 Added: :PROPERTIES:
470 Added: :ID: 40B2A19E-16FC-481E-BC3A-B0D9C4D41AF0
471 Added: :END:
472 Added:
473 Added: Do not show a boulder top fieldset border on the exercise logging fieldset card.
474 Added:
475 Added: * DONE Confirm workout cancellation in a modal :feat:ui:
476 Added: :PROPERTIES:
477 Added: :ID: ABD736DF-FC08-42C2-832A-783A12C8CEF8
478 Added: :END:
479 Added:
480 Added: Show a modal menu that confirms workout logging cancellation before cancelling.
481 Added:
482 Added: * DONE Make workout logging fields compact on mobile :feat:ui:
483 Added: :PROPERTIES:
484 Added: :ID: 9C5050C3-4016-4580-9019-09F2C929E7FD
485 Added: :END:
486 Added:
487 Added: Use a grid layout so the exercise name, load input, and reps input appear on one row on mobile.
488 Added:
489 Added: * DONE Preserve and navigate after modal workout cancellation :feat:ui:
490 Added: :PROPERTIES:
491 Added: :ID: 8B43EF3E-ECEA-4309-B45E-E887704E3683
492 Added: :END:
493 Added:
494 Added: When cancellation is confirmed, return the Current Workout tab to the Routine tab, save the workout as a partial logbook entry, and keep it editable later.
495 Added:
496 Added: * DONE Use shared load and reps column headings :feat:web:
497 Added: :PROPERTIES:
498 Added: :ID: EA7B46E1-9E11-46B2-9986-061CFCA047B1
499 Added: :END:
500 Added:
501 Added: In exercise logging fieldsets, show Load and Reps as column headings instead of repeating them above every input in a multi-exercise group.
502 Added:
503 Added: * DONE Hide exercise-selector submit buttons in SPA mode :feat:ui:minor:
504 Added: :PROPERTIES:
505 Added: :ID: 5EB7AFE6-A3F3-459A-AC43-509C8E335C7D
506 Added: :END:
507 Added:
508 Added: When JavaScript progressive enhancement is active, do not show the submit button for exercise selection.
509 Added:
510 Added: * DONE Fix workout cancellation persistence :fix:app:
511 Added: :PROPERTIES:
512 Added: :ID: 148045D9-31FD-4324-893E-BD5A18126EB7
513 Added: :END:
514 Added:
515 Added: Cancelling workout logging currently redirects without actually cancelling the workout. Make the cancellation operation take effect.
516 Added: - result :: model=kiro-cli commit=5aaa11f tests=pass
517 Added:
518 Added: * DONE Make the entire logging fieldset a responsive grid :feat:ui:
519 Added: :PROPERTIES:
520 Added: :ID: B8E23595-5400-4B1E-A263-4EA0AE928D48
521 Added: :END:
522 Added:
523 Added: Use one responsive grid for the entire workout logging fieldset instead of separate exercise-field layouts.
524 Added: - result :: model=kiro-cli commit=6b62f25 tests=pass
525 Added:
526 Added: * DONE Update the workout elapsed timer live :feat:ui:
527 Added: :PROPERTIES:
528 Added: :ID: DD858602-CC01-433B-BE33-E59CABE79060
529 Added: :END:
530 Added:
531 Added: Ensure the elapsed duration timer in the Current Workout view updates continuously while logging.
532 Added: - result :: model=kiro-cli commit=f9cfb69 tests=pass
533 Added:
534 Added: * DONE Fix the workout cancel button :fix:app:
535 Added: :PROPERTIES:
536 Added: :ID: 4A3D0AD4-57FE-49D4-A47D-E8D57CA07EFB
537 Added: :END:
538 Added:
539 Added: [2026-09-05 Sat 17:23]
540 Added: Fix the workout cancel button bug. We are cancelling a workout does not properly cancel the workout and redirect to the home page
541 Added:
542 Added: * DONE Hide the exercise choice label :feat:ui:minor:
543 Added: :PROPERTIES:
544 Added: :ID: 3474D6D1-9D7A-4BB0-ABC3-CA9BDFF994D8
545 Added: :END:
546 Added:
547 Added: [2026-09-05 Sat 17:26]
548 Added: Make the exercise choice label invisible in the current workout logging page
549 Added:
550 Added: * DONE Replace history with a logbook endpoint :refactor:app:
551 Added: :PROPERTIES:
552 Added: :ID: 34A2E9B5-79E6-4026-93CC-A13E099F0FFA
553 Added: :END:
554 Added:
555 Added: Replace the history endpoint with a logbook endpoint that shows all recorded evidence for the user.
556 Added:
557 Added: * DONE Allow spontaneous logbook feedback :feat:web:
558 Added: :PROPERTIES:
559 Added: :ID: E6C5DEA7-F9F6-47AA-9BE0-0337A2C3FCAE
560 Added: :END:
561 Added:
562 Added: Allow users to submit subjective feedback at any time from the Logbook. Suggest this feedback flow at the end of each workout, while keeping it available independently.
563 Added:
564 Added: * DONE Gate logbook feedback form behind a button :feat:app:
565 Added: :PROPERTIES:
566 Added: :ID: 7E5113F3-B4CC-4258-9CA8-B4D4F8E5695A
567 Added: :END:
568 Added:
569 Added: The feedback form under the logbook tab should be accessed via a dedicated button not displayed outright in the logbook page
570 Added: - result :: model=kiro-cli commit=6cc9062 tests=pass
571 Added:
572 Added: * DONE Add authenticated app feedback :feat:app:
573 Added: :PROPERTIES:
574 Added: :ID: D69A809A-C005-4FAE-A83A-D31EC73FDC11
575 Added: :END:
576 Added:
577 Added: [2026-09-05 Sat 17:45]
578 Added: Next to the profile button in the top header, add an app feedback button which will insert app-specific feedback written by the user to an appropriate SQLite table on the server. On the user app feedback screen, the user should be able to type freeform feedback on the application in one tab and in a second tab view the list of feedback submitted
579 Added:
580 Added: * DONE Add animations to routine accordions :feat:ui:
581 Added: :PROPERTIES:
582 Added: :ID: 1159365E-8A37-46BF-9471-081C36951FFE
583 Added: :END:
584 Added:
585 Added: Add animations to the routine accordions.
586 Added:
587 Added: * DONE Style workout accordions with CSS :feat:ui:
588 Added: :PROPERTIES:
589 Added: :ID: AA62E903-0B38-492A-A7F3-1D33B1341F8B
590 Added: :END:
591 Added:
592 Added: Style the workout accordions with CSS.
593 Added:
594 Added: * DONE Replace routine accordions with an HTML table :refactor:ui:
595 Added: :PROPERTIES:
596 Added: :ID: 1FB93D06-7F39-48DF-9446-30262FEB9EDD
597 Added: :END:
598 Added:
599 Added: Replace the routine accordion interface with a basic HTML table.
600 Added: - result :: model=kiro-cli commit=4e4acac tests=pass
601 Added:
602 Added: * DONE Remove all doctrine-note elements :refactor:web:minor:doctrine:
603 Added: :PROPERTIES:
604 Added: :ID: 5C899949-BF43-4DFC-A2ED-ABB610F33E3E
605 Added: :END:
606 Added:
607 Added: * DONE Use brass for secondary buttons :feat:ui:
608 Added: :PROPERTIES:
609 Added: :ID: 58F4069E-C63B-4F88-B61E-9D32A2C7E931
610 Added: :END:
611 Added:
612 Added: Use a secondary brass color for secondary buttons.
613 Added:
614 Added: * DONE Style exercise names as headings :feat:ui:
615 Added: :PROPERTIES:
616 Added: :ID: D1D1A24B-5C5A-4087-AFCE-FA2CA5E91D13
617 Added: :END:
618 Added:
619 Added: In the exercise fieldset, style the exercise name as a black Work Sans heading.
620 Added:
621 Added: * DONE Limit Courier Prime Mono usage :feat:web:
622 Added: :PROPERTIES:
623 Added: :ID: F16278C5-7676-4674-91E5-1BDBB55F3050
624 Added: :END:
625 Added:
626 Added: Use Courier Prime Mono only for numerical inputs and outputs, and eventually for tabular data.
627 Added:
628 Added: * DONE Harmonize UI corner radii :refactor:ui:
629 Added: :PROPERTIES:
630 Added: :ID: 2F91D541-E1FD-4970-9928-8699EFE6F9A2
631 Added: :END:
632 Added:
633 Added: Use uniformly rounded corners for all buttons, cards, and other UI elements.
634 Added:
635 Added: * DONE Harmonize keyboard-focus halo styling :refactor:ui:
636 Added: :PROPERTIES:
637 Added: :ID: 26B135E8-3496-4445-A882-20BAEF45670C
638 Added: :END:
639 Added:
640 Added: Change the WCAG keyboard-navigation focus halo color so it follows the general UI theme and is less visually obtrusive.
641 Added:
642 Added: * DONE Style the cancellation modal to match the UI :feat:ui:
643 Added: :PROPERTIES:
644 Added: :ID: 71E0C5B7-0181-4F98-B9F1-868635E2FB02
645 Added: :END:
646 Added:
647 Added: Style the modal cancellation menu so it matches the overall UI aesthetic.
648 Added:
649 Added: * DONE Style form dropdowns to match the UI :feat:ui:
650 Added: :PROPERTIES:
651 Added: :ID: FA54EA36-BDDE-427F-B236-11BA6DB4C531
652 Added: :END:
653 Added:
654 Added: Style form dropdown menus so they match the overall UI aesthetic.
655 Added:
656 Added: * DONE Add a dedicated profile page :feat:ui:
657 Added: :PROPERTIES:
658 Added: :ID: 280116A7-6B34-496B-B19B-77D41FB6CBC5
659 Added: :END:
660 Added:
661 Added: Make the top-bar profile button open a dedicated profile page instead of the Logbook. Allow users to change their username and update their password.
662 Added: - result :: model=kiro-cli commit=430a6ea tests=pass
663 Added:
664 Added:
665 Added: # Local Variables:
666 Added: # org-log-done: time
667 Added: # End:
REQUESTS.org
index e78b0719..00000000 100644..000000
@@ -1,663 +0,0 @@
1 Removed: #+TITLE: Hito Requests log
2 Removed: #+AUTHOR: Marius Peter
3 Removed: #+STARTUP: content
4 Removed: #+TODO: TODO IN-PROGRESS TESTING TESTED BLOCKED | CANCELLED DONE
5 Removed:
6 Removed: * DONE Add a time graph of feedback elements to the logbook :feat:ui:
7 Removed: CLOSED: [2026-09-06 Sun 14:23] SCHEDULED: [2026-09-06 Sun 13:43]
8 Removed: :PROPERTIES:
9 Removed: :ID: b7813f9d-931a-4993-ae08-ff1c661b9775
10 Removed: :END:
11 Removed: The logbook page should present a time graph of various feedback elements over time.
12 Removed:
13 Removed: - result :: model=kiro-cli commit=71580da tests=246 pass
14 Removed: * DONE Fix elapsed duration timer on workout logging page :fix:ui:
15 Removed: CLOSED: [2026-09-06 Sun 13:58] SCHEDULED: [2026-09-06 Sun 13:42]
16 Removed: :PROPERTIES:
17 Removed: :ID: 8fcbf31c-92d4-4b9b-ab6f-b5e723f70312
18 Removed: :END:
19 Removed: Fix the elapsed duration timer in the current workout logging page.
20 Removed:
21 Removed: - result :: model=kiro-cli commit=dbe2dda tests=241 pass; root cause js_of_ocaml 31-bit int overflow on epoch values, fixed with float arithmetic
22 Removed: * DONE Move username and password editing into a modal :refactor:ui:
23 Removed: CLOSED: [2026-09-06 Sun 14:30] SCHEDULED: [2026-09-06 Sun 13:39]
24 Removed: :PROPERTIES:
25 Removed: :ID: a68fba78-f707-4f57-b757-2ba1214d9876
26 Removed: :END:
27 Removed: On the profile page, username and password should not have dedicated input fields. We should only see username, then the value for the user's username, then the change username button affordance, not the input field. Editing username and password should pop open a modal menu.
28 Removed:
29 Removed: - result :: model=kiro-cli commit=d38fd43 tests=247 pass
30 Removed: * DONE Remove the black line after the masthead :refactor:ui:minor:
31 Removed: CLOSED: [2026-09-06 Sun 13:52] SCHEDULED: [2026-09-06 Sun 13:32]
32 Removed: :PROPERTIES:
33 Removed: :ID: fc45fee8-a8c9-4d8b-8403-5d3219926c63
34 Removed: :END:
35 Removed:
36 Removed: Remove the black line after the masthead
37 Removed:
38 Removed: - result :: model=kiro-cli commit=d6b3b97 tests=241 pass
39 Removed: * BLOCKED Add profile dropdown actions :feat:app:
40 Removed: SCHEDULED: [2026-09-06 Sun 13:09]
41 Removed: :PROPERTIES:
42 Removed: :ID: db8901b3-f932-4c88-8d0d-7ddadfb97f32
43 Removed: :END:
44 Removed: Source: application feedback (VPS, trainee t1, sequence 5, submitted 2026-09-05T22:18:25Z)
45 Removed:
46 Removed: Make the profile control a drop-down with Profile, Settings, and Sign out actions.
47 Removed:
48 Removed: - blocked :: No Settings page or /settings route exists; /profile already holds account settings (username, password). Awaiting decision: (1) point both Profile and Settings at /profile, (2) drop Settings for Profile + Sign out only, or (3) create a new /settings page (scope TBD).
49 Removed:
50 Removed: * DONE Define five-point subjective feedback scores :feat:core:
51 Removed: CLOSED: [2026-09-06 Sun 14:07] SCHEDULED: [2026-09-06 Sun 13:09]
52 Removed: :PROPERTIES:
53 Removed: :ID: 2b46d3fb-9cfc-43f3-958c-b348d1b7d58a
54 Removed: :END:
55 Removed: Source: application feedback (VPS, trainee t1, sequence 1, submitted 2026-09-05T22:13:05Z)
56 Removed:
57 Removed: Use exactly the scores 1 through 5 for subjective feedback. Score 1 means very poor. Score 5 means very good. Update the core domain as a consequence.
58 Removed:
59 Removed: - result :: model=kiro-cli commit=3051af0 tests=243 pass; level now Very_poor..Very_good (1..5); migration 6 drops legacy feedback
60 Removed:
61 Removed: * DONE Preserve partial subjective feedback :feat:core:
62 Removed: CLOSED: [2026-09-06 Sun 14:19] SCHEDULED: [2026-09-06 Sun 13:09]
63 Removed: :PROPERTIES:
64 Removed: :ID: 50a8e1a2-d6a4-4aa9-8852-65afc15517ce
65 Removed: :END:
66 Removed: Source: application feedback (VPS, trainee t1, sequence 1, submitted 2026-09-05T22:13:05Z)
67 Removed:
68 Removed: Make Back and Skip preserve partial answers. Make Close cancel the feedback flow. Make Skip ignore the feedback event. Allow subjective feedback to be logged as a partial list.
69 Removed:
70 Removed: - result :: model=kiro-cli commit=6fde451 tests=245 pass
71 Removed:
72 Removed: * DONE Format page titles with an em dash :feat:ui:minor:
73 Removed: CLOSED: [2026-09-06 Sun 13:51] SCHEDULED: [2026-09-06 Sun 13:09]
74 Removed: :PROPERTIES:
75 Removed: :ID: 9800d7b7-552e-4ed1-9e96-587400ee9f7b
76 Removed: :END:
77 Removed: Source: application feedback (VPS, trainee t1, sequence 4, submitted 2026-09-05T22:16:53Z)
78 Removed:
79 Removed: Use a literal em dash in page titles, such as Hito — Home.
80 Removed:
81 Removed: - result :: model=kiro-cli commit=5e4236c tests=241 pass
82 Removed:
83 Removed: * DONE Standardize the global corner radius :refactor:ui:minor:
84 Removed: CLOSED: [2026-09-06 Sun 13:53] SCHEDULED: [2026-09-06 Sun 13:09]
85 Removed: :PROPERTIES:
86 Removed: :ID: 357455c5-a6a3-4dec-ad6b-5be098a11013
87 Removed: :END:
88 Removed: Source: application feedback (VPS, trainee t1, sequence 3, submitted 2026-09-05T22:15:29Z)
89 Removed:
90 Removed: Use one global border radius in relative units so that it scales across components.
91 Removed:
92 Removed: - result :: model=kiro-cli commit=a45027e tests=241 pass
93 Removed:
94 Removed: * DONE Animate modals with reduced-motion support :feat:ui:
95 Removed: CLOSED: [2026-09-06 Sun 13:54] SCHEDULED: [2026-09-06 Sun 13:09]
96 Removed: :PROPERTIES:
97 Removed: :ID: 30266d78-2731-4306-a748-02e479d97867
98 Removed: :END:
99 Removed: Source: application feedback (VPS, trainee t1, sequence 6, submitted 2026-09-06T08:37:36Z)
100 Removed:
101 Removed: Apply a short, smooth modal animation to every modal. Disable modal animation when the user prefers reduced motion.
102 Removed:
103 Removed: - result :: model=kiro-cli commit=4b8df35 tests=241 pass
104 Removed: * DONE Verify POST redirect GET for forms :feat:app:
105 Removed: CLOSED: [2026-09-06 Sun 14:25] SCHEDULED: [2026-09-06 Sun 13:09]
106 Removed: :PROPERTIES:
107 Removed: :ID: 106b5917-48d9-423e-9ce9-11dc4c230603
108 Removed: :END:
109 Removed: Source: application feedback (VPS, trainee t1, sequence 2, submitted 2026-09-05T22:14:58Z)
110 Removed:
111 Removed: Redirect each form submission to the page that originated the form flow when practical. Decide whether a result type should persist success and error messages across the redirect.
112 Removed:
113 Removed: - result :: model=kiro-cli commit=0637e57 tests=247 pass; audit found all state-changing POSTs already redirect; decision: flash/query for success, inline 400 re-render for field validation
114 Removed:
115 Removed: * DONE Give each prescribed exercise its own routine sub-row :feat:web:
116 Removed: CLOSED: [2026-09-06 Sun 14:01] SCHEDULED: [2026-09-05 Sat 23:10]
117 Removed: :PROPERTIES:
118 Removed: :ID: be3140eb-47d5-4391-9f05-7b8c3cd294f1
119 Removed: :END:
120 Removed: - Each prescribed exercise should exist in a separate sub-row.
121 Removed: - Stimuli count column should not exist.
122 Removed: - Each prescribed exercise should have specific sub-columns for exercise name, rep count.
123 Removed:
124 Removed: - result :: model=kiro-cli commit=28caa63 tests=241 pass
125 Removed:
126 Removed: * DONE Capture each prompt as its own request :chore:web:
127 Removed: CLOSED: [2026-09-05 Sat 22:01] SCHEDULED: [2026-09-05 Sat 21:31]
128 Removed: :PROPERTIES:
129 Removed: :ID: cfdca5b4-80d3-448f-bf54-a365dd36a578
130 Removed: :END:
131 Removed:
132 Removed: - result :: model=kiro-cli commit=d5553a2 tests=pass
133 Removed: Edit the stream-of-thought SKILL so each individual prompt in a session becomes its own feature request, not the whole session as one request.
134 Removed: * DONE Use POST-redirect-GET for all forms :feat:app:
135 Removed: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
136 Removed: :PROPERTIES:
137 Removed: :ID: 3A2316B3-F9C1-41F2-B0FC-E1F6958BC9D5
138 Removed: :END:
139 Removed:
140 Removed: Ensure proper POST redirect GET patter for all forms
141 Removed:
142 Removed: Clarification:
143 Removed: - Apply this to feedback, application feedback, and workout forms.
144 Removed: - Preserve success and error messages for toast display on the redirected page.
145 Removed:
146 Removed: - result :: commit=41a44ff tests=215 pass
147 Removed:
148 Removed: * DONE Standardize shared CRUD patterns :refactor:app:
149 Removed: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
150 Removed: :PROPERTIES:
151 Removed: :ID: D214DE90-88B9-4ADD-AB2F-4AF7B7E87EF7
152 Removed: :END:
153 Removed:
154 Removed: Rationalize shared CRUD pattern for domain objects, user data, and app feedback
155 Removed:
156 Removed: - result :: commit=ea594ba tests=215 pass
157 Removed:
158 Removed: * DONE Use worse-same-better subjective feedback :feat:core:
159 Removed: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
160 Removed: :PROPERTIES:
161 Removed: :ID: 484FE4E8-73F1-4789-AF8D-FC07087DEC5E
162 Removed: :END:
163 Removed:
164 Removed: Subjective Feedback evidence should be collected against the following pattern: Worse, Same, Better [than usual]
165 Removed:
166 Removed: Clarification:
167 Removed: - Ask, "How are you feeling?"
168 Removed: - Collect Worse, Same, and Better through inline button groups.
169 Removed: - Apply this to sleep, appetite, readiness, motivation, and perceived difficulty.
170 Removed: - This probably has an impact on core data types as well.
171 Removed:
172 Removed: - result :: commit=f09fd5d tests=215 pass
173 Removed:
174 Removed: * DONE Make Write feedback the primary action :feat:app:
175 Removed: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
176 Removed: :PROPERTIES:
177 Removed: :ID: 84E85E7A-A1DF-4F2E-913C-9A1C6F441DDB
178 Removed: :END:
179 Removed:
180 Removed: "Write feedback" should be the single Primary action on the app-feedback page. Secondary actions are CRUD operations on previous feedback items.
181 Removed:
182 Removed: Clarification:
183 Removed: - Secondary CRUD actions are editing and removing the user’s feedback items.
184 Removed: - Show the primary action once.
185 Removed:
186 Removed: - result :: commit=43da59d tests=215 pass
187 Removed:
188 Removed: * BLOCKED Import Hevy CSV from profile :feat:app:
189 Removed: SCHEDULED: [2026-09-05 Sat 21:25]
190 Removed: :PROPERTIES:
191 Removed: :ID: 2559dd31-18e1-4f62-8d16-92f96d782203
192 Removed: :END:
193 Removed:
194 Removed: - note :: Deferred by the request clarification. Left open under Dispatch; not implemented this session.
195 Removed: On profile page, user should be able to import Hevy CSV
196 Removed:
197 Removed: Clarification:
198 Removed: - Defer this feedback feature.
199 Removed:
200 Removed: * CANCELLED Fix workout cancellation flow :fix:app:
201 Removed: CLOSED: [2026-09-05 Sat 21:58] SCHEDULED: [2026-09-05 Sat 21:25]
202 Removed: :PROPERTIES:
203 Removed: :ID: 2B914C9E-DF02-420C-8D5A-D0503552C88F
204 Removed: :END:
205 Removed:
206 Removed: workout cancellation flow still has bug
207 Removed:
208 Removed: - note :: No remaining defect. Prior commits 7d9ee9e, 5aaa11f, and test ea1f211 fixed cancellation: CSRF-guarded discard, no stale record, 303 redirect to Home. test_web.ml cancellation cases pass (203 tests green).
209 Removed: * CANCELLED Emphasize the secondary color :feat:ui:
210 Removed: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
211 Removed: :PROPERTIES:
212 Removed: :ID: C1CCF7A7-FA07-43C5-82BA-54E9B8C3B54B
213 Removed: :END:
214 Removed:
215 Removed: oxblood color is still too dominant; secondary color needs to be more emphasized
216 Removed:
217 Removed: Clarification:
218 Removed: - Use brass for secondary buttons, such as cancellation.
219 Removed:
220 Removed: - note :: Already implemented by the existing brass secondary-button styling; no new work was required.
221 Removed:
222 Removed: * CANCELLED Use progressive subjective feedback buttons :feat:ui:
223 Removed: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
224 Removed: :PROPERTIES:
225 Removed: :ID: F59C1B0A-C238-4059-91D1-42DB5CC17CBF
226 Removed: :END:
227 Removed:
228 Removed: subjective feedback affordances should be more progressive. not dropdowns, but button groups containing three buttons each for below, usual, and above.
229 Removed:
230 Removed: - note :: Duplicate of the completed Worse/Same/Better inline controls proposal (commit f09fd5d).
231 Removed:
232 Removed: * DONE Store database timestamps as ISO 8601 :feat:core:
233 Removed: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:25]
234 Removed: :PROPERTIES:
235 Removed: :ID: 950BD105-C680-4D58-914F-0FAFA0ECBA63
236 Removed: :END:
237 Removed:
238 Removed: database timestamps should be stored as ISO8601 strings
239 Removed:
240 Removed: Clarification:
241 Removed: - Store timestamps in UTC Zulu time.
242 Removed: - Use the ISO 8601 format.
243 Removed: - Do not migrate existing data.
244 Removed: - Existing data can be dropped safely.
245 Removed:
246 Removed: - result :: commit=79bbbd7 tests=215 pass; migration 5 drops affected legacy evidence
247 Removed: * DONE Remove spurious Page updated mention :fix:ui:minor:
248 Removed: CLOSED: [2026-09-05 Sat 21:57] SCHEDULED: [2026-09-05 Sat 21:25]
249 Removed: :PROPERTIES:
250 Removed: :ID: C8FD4798-94A4-42C6-8D86-03A4A58DF703
251 Removed: :END:
252 Removed:
253 Removed: Spurious "Page updated" mention in workout logging screen
254 Removed:
255 Removed: - result :: model=kiro-cli commit=dab53ca tests=pass
256 Removed: * DONE Confirm workout override with a modal :feat:ui:
257 Removed: CLOSED: [2026-09-05 Sat 22:23] SCHEDULED: [2026-09-05 Sat 21:25]
258 Removed: :PROPERTIES:
259 Removed: :ID: b9d05a7f-c8f9-4fb6-878f-a9d1f89ed45d
260 Removed: :END:
261 Removed:
262 Removed: - result :: model=kiro-cli commit=7c9f701 tests=pass
263 Removed: Implement a modal confirmation when beginning a workout under override
264 Removed:
265 Removed: Clarification:
266 Removed: - The override condition is training before full rest has completed.
267 Removed: - Show a confirmation modal menu before starting the workout.
268 Removed:
269 Removed: * DONE Show early workout notice as a toast :feat:ui:doctrine:
270 Removed: CLOSED: [2026-09-05 Sat 22:26] SCHEDULED: [2026-09-05 Sat 21:25]
271 Removed: :PROPERTIES:
272 Removed: :ID: 8052f6e1-7281-4a57-a615-504476ca6c16
273 Removed: :END:
274 Removed:
275 Removed: - result :: model=kiro-cli commit=9702a02 tests=pass
276 Removed: "begin before recovery finished" should be a toast message rather than a component in the main content.
277 Removed:
278 Removed: Clarification:
279 Removed: - The toast is informational only because the modal already confirms the override.
280 Removed: - Display it for 3 seconds, or for the duration recommended by WCAG.
281 Removed:
282 Removed: * DONE Add custom error pages :feat:app:
283 Removed: CLOSED: [2026-09-05 Sat 22:20] SCHEDULED: [2026-09-05 Sat 21:25]
284 Removed: :PROPERTIES:
285 Removed: :ID: 965d5284-a593-43ca-b8e4-202b093e6502
286 Removed: :END:
287 Removed:
288 Removed: - result :: model=kiro-cli commit=34ab578 tests=pass
289 Removed: Need custom 404 etc. pages
290 Removed:
291 Removed: Clarification:
292 Removed: - Add the most common 400 and 500 error pages.
293 Removed: - Do not expose specific information from the server.
294 Removed: - Keep the desktop top navigation affordance.
295 Removed: - Keep the mobile bottom navigation affordance.
296 Removed:
297 Removed: * DONE Show feedback author and contribution count :feat:web:
298 Removed: CLOSED: [2026-09-05 Sat 22:54] SCHEDULED: [2026-09-05 Sat 21:24]
299 Removed: :PROPERTIES:
300 Removed: :ID: 428E433E-5F01-44CF-B642-FC1DD80BF087
301 Removed: :END:
302 Removed:
303 Removed: feedback should display username that submitted it, alongside their total number of feedback contributions.
304 Removed:
305 Removed: Clarification:
306 Removed: - Users should see real usernames.
307 Removed: - Rank the feedback list by upvotes.
308 Removed: - Let users upvote feedback submitted by other users they think is valuable.
309 Removed:
310 Removed: - result :: commit=98e5fb5 tests=215 pass
311 Removed:
312 Removed: * CANCELLED Keep the routine as a table :feat:ui:
313 Removed: CLOSED: [2026-09-05 Sat 22:02] SCHEDULED: [2026-09-05 Sat 21:24]
314 Removed: :PROPERTIES:
315 Removed: :ID: 6b126d4c-6058-4b7a-8c67-640ce6160ba7
316 Removed: :END:
317 Removed:
318 Removed: - note :: Duplicate. The routine already renders as an HTML table (commit 4e4acac, "Replace routine accordions with an HTML table"). No new work.
319 Removed: routine must absolutely be a table.
320 Removed:
321 Removed: Clarification:
322 Removed: - This is a duplicate request.
323 Removed:
324 Removed: * DONE Fix workout log exercise selection :fix:ui:
325 Removed: CLOSED: [2026-09-05 Sat 22:02] SCHEDULED: [2026-09-05 Sat 21:24]
326 Removed: :PROPERTIES:
327 Removed: :ID: 45453231-7ac9-444c-96c2-af387a34bb0c
328 Removed: :END:
329 Removed:
330 Removed: - result :: model=kiro-cli commit=61bf45c tests=pass
331 Removed: Broken workout log editing: exercise selection button.
332 Removed:
333 Removed: Clarification:
334 Removed: - This concerns editing a previously logged workout on mobile.
335 Removed: - The user must be able to select and switch the exercise correctly.
336 Removed:
337 Removed: * DONE Use one desktop-to-mobile breakpoint :feat:ui:
338 Removed: :PROPERTIES:
339 Removed: :ID: 948EABAC-9D59-4101-8901-E37DE0D9DFF1
340 Removed: :END:
341 Removed:
342 Removed: Desktop has top navigation. Mobile has an identical bottom navigation.
343 Removed:
344 Removed: * DONE Use three always-available navigation actions :feat:ui:
345 Removed: :PROPERTIES:
346 Removed: :ID: FE8559B5-190D-4C5C-9A1E-CE5FA826C094
347 Removed: :END:
348 Removed:
349 Removed: Use `Home`, `Routine` when the user is not logging (changing to `Current Workout` while logging), and `<username>` for the workout log. Keep all three actions available in both modes.
350 Removed:
351 Removed: * DONE Simplify the top bar brand :refactor:ui:
352 Removed: :PROPERTIES:
353 Removed: :ID: DB0BC418-C81C-4AB9-87BC-1B475747A04D
354 Removed: :END:
355 Removed:
356 Removed: Replace the top bar brand with `hito` in Work Sans, without a subtitle.
357 Removed:
358 Removed: * DONE Remove the mobile hamburger menu :refactor:ui:minor:
359 Removed: :PROPERTIES:
360 Removed: :ID: C32BAF01-548E-4CBD-BBD0-00ECEDEE97CB
361 Removed: :END:
362 Removed:
363 Removed: Do not show a hamburger menu on mobile.
364 Removed:
365 Removed: * DONE Increase the masthead brand size :feat:ui:minor:
366 Removed: :PROPERTIES:
367 Removed: :ID: 8D6BC6B9-F248-4714-BA93-9C27532DF490
368 Removed: :END:
369 Removed:
370 Removed: Make the masthead brand name appear much larger.
371 Removed:
372 Removed: * DONE Hide sign-out on mobile :feat:ui:minor:
373 Removed: :PROPERTIES:
374 Removed: :ID: 2265C4FA-071E-4BFA-A038-3CD3CB3656EA
375 Removed: :END:
376 Removed:
377 Removed: Do not show the sign-out button on mobile.
378 Removed:
379 Removed: * DONE Remove thick black bars from mobile bottom navigation :refactor:ui:minor:
380 Removed: :PROPERTIES:
381 Removed: :ID: F2D41A95-58A6-461A-A5DF-AC862FCD877A
382 Removed: :END:
383 Removed:
384 Removed: Remove the thick black bars visible in the mobile bottom navigation.
385 Removed:
386 Removed: * DONE Fix active bottom-navigation state for current workout and logbook :fix:ui:
387 Removed: :PROPERTIES:
388 Removed: :ID: 35E2CC6C-B489-4CBC-B214-DC8D1626E559
389 Removed: :END:
390 Removed:
391 Removed: When a current workout is open, selecting the logbook tab marks both Current Workout and Logbook as active in the bottom navigation. Ensure only the selected page is highlighted.
392 Removed:
393 Removed: * DONE Increase bottom-navigation button text size :feat:ui:minor:
394 Removed: :PROPERTIES:
395 Removed: :ID: 06F564D7-A177-4F3D-9F17-80B35C1A4C25
396 Removed: :END:
397 Removed:
398 Removed: Increase the bottom-navigation button text font size to better support WCAG-aligned readability.
399 Removed:
400 Removed: * DONE Animate page transitions :feat:ui:
401 Removed: :PROPERTIES:
402 Removed: :ID: 41C03F68-9DD2-4B79-B1A7-8EE450599B9B
403 Removed: :END:
404 Removed:
405 Removed: Animate page transitions between Home, Current Workout, and Logbook.
406 Removed:
407 Removed: * DONE Standardize Logbook navigation and add a mobile profile button :refactor:ui:
408 Removed: :PROPERTIES:
409 Removed: :ID: D52C18B2-2F97-4A70-8F0D-6CBD261F20F2
410 Removed: :END:
411 Removed:
412 Removed: Always label the logbook tab `Logbook`. Add a dedicated profile button for the user name in the mobile top navigation bar.
413 Removed:
414 Removed: * DONE Return the middle navigation tab to Routine after cancellation :feat:ui:
415 Removed: :PROPERTIES:
416 Removed: :ID: 31F886F8-D3A3-464D-8785-4548B9B50C11
417 Removed: :END:
418 Removed:
419 Removed: After cancelling workout logging, change the middle navigation tab from Current Workout back to Routine.
420 Removed: - result :: model=kiro-cli commit=ea1f211 tests=pass
421 Removed:
422 Removed: * DONE Add placeholders to load and reps fields :feat:ui:minor:
423 Removed: :PROPERTIES:
424 Removed: :ID: 61A936E9-A4F6-4FA1-8282-27231E82ED61
425 Removed: :END:
426 Removed:
427 Removed: Add placeholder values to the load and reps fields on the workout logging page.
428 Removed:
429 Removed: * DONE Remove the redundant fieldset legend :refactor:ui:minor:
430 Removed: :PROPERTIES:
431 Removed: :ID: 7BC3EF05-5208-4CD9-9BD3-113F063E8D07
432 Removed: :END:
433 Removed:
434 Removed: Remove the redundant fieldset legend from workout logging.
435 Removed:
436 Removed: * DONE Show an in-progress workout card on Home :feat:ui:
437 Removed: :PROPERTIES:
438 Removed: :ID: 9C1865CB-70F2-41FA-917E-369C5CAD7031
439 Removed: :END:
440 Removed:
441 Removed: When the user is logging a current workout, show a Home card that indicates the workout is in progress and links to the Current Workout tab.
442 Removed:
443 Removed: * DONE Make workout cancellation unconditional :feat:app:
444 Removed: :PROPERTIES:
445 Removed: :ID: E86AC9DF-E909-42F4-9D52-8DEEFCC70B5F
446 Removed: :END:
447 Removed:
448 Removed: When cancelling a workout, always leave the current workout view without showing an invalid-form response. Save the workout log as-is for later editing, then redirect the user to Home.
449 Removed:
450 Removed: * DONE Add a sticky workout timer bar :feat:ui:
451 Removed: :PROPERTIES:
452 Removed: :ID: C6B85644-E352-4442-A49B-C3582BD89670
453 Removed: :END:
454 Removed:
455 Removed: When logging a workout, show a sticky bar at the top of the screen with a workout timer.
456 Removed:
457 Removed: * DONE Replace the exercise button group with a dropdown :refactor:ui:
458 Removed: :PROPERTIES:
459 Removed: :ID: EAF4A9D5-1EC3-48B0-975C-58939C5A86ED
460 Removed: :END:
461 Removed:
462 Removed: Use a dropdown menu to select the exercise instead of the exercise button group.
463 Removed:
464 Removed: * DONE Remove the boulder top border from exercise logging cards :refactor:ui:minor:
465 Removed: :PROPERTIES:
466 Removed: :ID: 40B2A19E-16FC-481E-BC3A-B0D9C4D41AF0
467 Removed: :END:
468 Removed:
469 Removed: Do not show a boulder top fieldset border on the exercise logging fieldset card.
470 Removed:
471 Removed: * DONE Confirm workout cancellation in a modal :feat:ui:
472 Removed: :PROPERTIES:
473 Removed: :ID: ABD736DF-FC08-42C2-832A-783A12C8CEF8
474 Removed: :END:
475 Removed:
476 Removed: Show a modal menu that confirms workout logging cancellation before cancelling.
477 Removed:
478 Removed: * DONE Make workout logging fields compact on mobile :feat:ui:
479 Removed: :PROPERTIES:
480 Removed: :ID: 9C5050C3-4016-4580-9019-09F2C929E7FD
481 Removed: :END:
482 Removed:
483 Removed: Use a grid layout so the exercise name, load input, and reps input appear on one row on mobile.
484 Removed:
485 Removed: * DONE Preserve and navigate after modal workout cancellation :feat:ui:
486 Removed: :PROPERTIES:
487 Removed: :ID: 8B43EF3E-ECEA-4309-B45E-E887704E3683
488 Removed: :END:
489 Removed:
490 Removed: When cancellation is confirmed, return the Current Workout tab to the Routine tab, save the workout as a partial logbook entry, and keep it editable later.
491 Removed:
492 Removed: * DONE Use shared load and reps column headings :feat:web:
493 Removed: :PROPERTIES:
494 Removed: :ID: EA7B46E1-9E11-46B2-9986-061CFCA047B1
495 Removed: :END:
496 Removed:
497 Removed: In exercise logging fieldsets, show Load and Reps as column headings instead of repeating them above every input in a multi-exercise group.
498 Removed:
499 Removed: * DONE Hide exercise-selector submit buttons in SPA mode :feat:ui:minor:
500 Removed: :PROPERTIES:
501 Removed: :ID: 5EB7AFE6-A3F3-459A-AC43-509C8E335C7D
502 Removed: :END:
503 Removed:
504 Removed: When JavaScript progressive enhancement is active, do not show the submit button for exercise selection.
505 Removed:
506 Removed: * DONE Fix workout cancellation persistence :fix:app:
507 Removed: :PROPERTIES:
508 Removed: :ID: 148045D9-31FD-4324-893E-BD5A18126EB7
509 Removed: :END:
510 Removed:
511 Removed: Cancelling workout logging currently redirects without actually cancelling the workout. Make the cancellation operation take effect.
512 Removed: - result :: model=kiro-cli commit=5aaa11f tests=pass
513 Removed:
514 Removed: * DONE Make the entire logging fieldset a responsive grid :feat:ui:
515 Removed: :PROPERTIES:
516 Removed: :ID: B8E23595-5400-4B1E-A263-4EA0AE928D48
517 Removed: :END:
518 Removed:
519 Removed: Use one responsive grid for the entire workout logging fieldset instead of separate exercise-field layouts.
520 Removed: - result :: model=kiro-cli commit=6b62f25 tests=pass
521 Removed:
522 Removed: * DONE Update the workout elapsed timer live :feat:ui:
523 Removed: :PROPERTIES:
524 Removed: :ID: DD858602-CC01-433B-BE33-E59CABE79060
525 Removed: :END:
526 Removed:
527 Removed: Ensure the elapsed duration timer in the Current Workout view updates continuously while logging.
528 Removed: - result :: model=kiro-cli commit=f9cfb69 tests=pass
529 Removed:
530 Removed: * DONE Fix the workout cancel button :fix:app:
531 Removed: :PROPERTIES:
532 Removed: :ID: 4A3D0AD4-57FE-49D4-A47D-E8D57CA07EFB
533 Removed: :END:
534 Removed:
535 Removed: [2026-09-05 Sat 17:23]
536 Removed: Fix the workout cancel button bug. We are cancelling a workout does not properly cancel the workout and redirect to the home page
537 Removed:
538 Removed: * DONE Hide the exercise choice label :feat:ui:minor:
539 Removed: :PROPERTIES:
540 Removed: :ID: 3474D6D1-9D7A-4BB0-ABC3-CA9BDFF994D8
541 Removed: :END:
542 Removed:
543 Removed: [2026-09-05 Sat 17:26]
544 Removed: Make the exercise choice label invisible in the current workout logging page
545 Removed:
546 Removed: * DONE Replace history with a logbook endpoint :refactor:app:
547 Removed: :PROPERTIES:
548 Removed: :ID: 34A2E9B5-79E6-4026-93CC-A13E099F0FFA
549 Removed: :END:
550 Removed:
551 Removed: Replace the history endpoint with a logbook endpoint that shows all recorded evidence for the user.
552 Removed:
553 Removed: * DONE Allow spontaneous logbook feedback :feat:web:
554 Removed: :PROPERTIES:
555 Removed: :ID: E6C5DEA7-F9F6-47AA-9BE0-0337A2C3FCAE
556 Removed: :END:
557 Removed:
558 Removed: Allow users to submit subjective feedback at any time from the Logbook. Suggest this feedback flow at the end of each workout, while keeping it available independently.
559 Removed:
560 Removed: * DONE Gate logbook feedback form behind a button :feat:app:
561 Removed: :PROPERTIES:
562 Removed: :ID: 7E5113F3-B4CC-4258-9CA8-B4D4F8E5695A
563 Removed: :END:
564 Removed:
565 Removed: The feedback form under the logbook tab should be accessed via a dedicated button not displayed outright in the logbook page
566 Removed: - result :: model=kiro-cli commit=6cc9062 tests=pass
567 Removed:
568 Removed: * DONE Add authenticated app feedback :feat:app:
569 Removed: :PROPERTIES:
570 Removed: :ID: D69A809A-C005-4FAE-A83A-D31EC73FDC11
571 Removed: :END:
572 Removed:
573 Removed: [2026-09-05 Sat 17:45]
574 Removed: Next to the profile button in the top header, add an app feedback button which will insert app-specific feedback written by the user to an appropriate SQLite table on the server. On the user app feedback screen, the user should be able to type freeform feedback on the application in one tab and in a second tab view the list of feedback submitted
575 Removed:
576 Removed: * DONE Add animations to routine accordions :feat:ui:
577 Removed: :PROPERTIES:
578 Removed: :ID: 1159365E-8A37-46BF-9471-081C36951FFE
579 Removed: :END:
580 Removed:
581 Removed: Add animations to the routine accordions.
582 Removed:
583 Removed: * DONE Style workout accordions with CSS :feat:ui:
584 Removed: :PROPERTIES:
585 Removed: :ID: AA62E903-0B38-492A-A7F3-1D33B1341F8B
586 Removed: :END:
587 Removed:
588 Removed: Style the workout accordions with CSS.
589 Removed:
590 Removed: * DONE Replace routine accordions with an HTML table :refactor:ui:
591 Removed: :PROPERTIES:
592 Removed: :ID: 1FB93D06-7F39-48DF-9446-30262FEB9EDD
593 Removed: :END:
594 Removed:
595 Removed: Replace the routine accordion interface with a basic HTML table.
596 Removed: - result :: model=kiro-cli commit=4e4acac tests=pass
597 Removed:
598 Removed: * DONE Remove all doctrine-note elements :refactor:web:minor:doctrine:
599 Removed: :PROPERTIES:
600 Removed: :ID: 5C899949-BF43-4DFC-A2ED-ABB610F33E3E
601 Removed: :END:
602 Removed:
603 Removed: * DONE Use brass for secondary buttons :feat:ui:
604 Removed: :PROPERTIES:
605 Removed: :ID: 58F4069E-C63B-4F88-B61E-9D32A2C7E931
606 Removed: :END:
607 Removed:
608 Removed: Use a secondary brass color for secondary buttons.
609 Removed:
610 Removed: * DONE Style exercise names as headings :feat:ui:
611 Removed: :PROPERTIES:
612 Removed: :ID: D1D1A24B-5C5A-4087-AFCE-FA2CA5E91D13
613 Removed: :END:
614 Removed:
615 Removed: In the exercise fieldset, style the exercise name as a black Work Sans heading.
616 Removed:
617 Removed: * DONE Limit Courier Prime Mono usage :feat:web:
618 Removed: :PROPERTIES:
619 Removed: :ID: F16278C5-7676-4674-91E5-1BDBB55F3050
620 Removed: :END:
621 Removed:
622 Removed: Use Courier Prime Mono only for numerical inputs and outputs, and eventually for tabular data.
623 Removed:
624 Removed: * DONE Harmonize UI corner radii :refactor:ui:
625 Removed: :PROPERTIES:
626 Removed: :ID: 2F91D541-E1FD-4970-9928-8699EFE6F9A2
627 Removed: :END:
628 Removed:
629 Removed: Use uniformly rounded corners for all buttons, cards, and other UI elements.
630 Removed:
631 Removed: * DONE Harmonize keyboard-focus halo styling :refactor:ui:
632 Removed: :PROPERTIES:
633 Removed: :ID: 26B135E8-3496-4445-A882-20BAEF45670C
634 Removed: :END:
635 Removed:
636 Removed: Change the WCAG keyboard-navigation focus halo color so it follows the general UI theme and is less visually obtrusive.
637 Removed:
638 Removed: * DONE Style the cancellation modal to match the UI :feat:ui:
639 Removed: :PROPERTIES:
640 Removed: :ID: 71E0C5B7-0181-4F98-B9F1-868635E2FB02
641 Removed: :END:
642 Removed:
643 Removed: Style the modal cancellation menu so it matches the overall UI aesthetic.
644 Removed:
645 Removed: * DONE Style form dropdowns to match the UI :feat:ui:
646 Removed: :PROPERTIES:
647 Removed: :ID: FA54EA36-BDDE-427F-B236-11BA6DB4C531
648 Removed: :END:
649 Removed:
650 Removed: Style form dropdown menus so they match the overall UI aesthetic.
651 Removed:
652 Removed: * DONE Add a dedicated profile page :feat:ui:
653 Removed: :PROPERTIES:
654 Removed: :ID: 280116A7-6B34-496B-B19B-77D41FB6CBC5
655 Removed: :END:
656 Removed:
657 Removed: Make the top-bar profile button open a dedicated profile page instead of the Logbook. Allow users to change their username and update their password.
658 Removed: - result :: model=kiro-cli commit=430a6ea tests=pass
659 Removed:
660 Removed:
661 Removed: # Local Variables:
662 Removed: # org-log-done: time
663 Removed: # End: