chore Remove local RAIL skill copy for the global install

Drop hito's own copy of the RAIL skill. The skill now lives in the shared agent-skills repo and installs globally by symlink at ~/.kiro/skills/rail, so every project shares one version. hito keeps its own RAIL.org stream and its tag vocabulary. Only the skill code moves out.

Commit
95e9b61bbd7c6903c274ebcadd86288f27e91b04
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
.kiro/skills/rail/SKILL.md
index c13dc0c2..00000000 100644..000000
@@ -1,354 +0,0 @@
1 Removed: ---
2 Removed: name: rail
3 Removed: description: Manage a rolling action item list in a RAIL.org stream with dedicated Emacs MCP tools.
4 Removed: ---
5 Removed:
6 Removed: # RAIL — Rolling Action Item List
7 Removed:
8 Removed: ## Overview
9 Removed:
10 Removed: RAIL keeps every action item in one Org file, `RAIL.org`, at the root of
11 Removed: a project. Dedicated Emacs tools write that file. The work follows three
12 Removed: steps:
13 Removed:
14 Removed: 1. Capture the item at the top of the stream.
15 Removed: 2. Track its status while the work runs.
16 Removed: 3. Close it with commit evidence and test evidence.
17 Removed:
18 Removed: The command for this skill is `/rail`.
19 Removed:
20 Removed: ## Usage
21 Removed:
22 Removed: Use this skill when the user does one of these things:
23 Removed:
24 Removed: - The user runs `/rail`.
25 Removed: - The user states an action item, a feature request, a defect, or a
26 Removed: change to make.
27 Removed: - The user asks you to record or capture an item.
28 Removed: - The user asks for the status of an item, or asks to change it.
29 Removed: - The user asks you to close, cancel, or block an item.
30 Removed: - The user asks what items are open.
31 Removed:
32 Removed: Treat each prompt as one separate action item. Do not merge two items.
33 Removed: Do not build, test, commit, or start an agent for a capture.
34 Removed:
35 Removed: The current prompt is the action item:
36 Removed:
37 Removed: $ARGUMENTS
38 Removed:
39 Removed: If the prompt is empty, ask the user for one line that describes the
40 Removed: item. Then stop. Capture the next prompt as the next item.
41 Removed:
42 Removed: ## Core Concepts
43 Removed:
44 Removed: ### The stream
45 Removed:
46 Removed: `RAIL.org` is a flat stream. Each action item is a top-level heading.
47 Removed: The newest item comes first. The file holds no container heading and no
48 Removed: topical heading. Tags on the heading give the axes of the item. The
49 Removed: position of a heading gives only its age.
50 Removed:
51 Removed: ### The file owns the vocabulary and the workflow
52 Removed:
53 Removed: The tools carry no project vocabulary and no project workflow. The
54 Removed: stream file owns both:
55 Removed:
56 Removed: - The `#+TODO:` line declares the status keywords.
57 Removed: - The `#+TAGS:` lines declare the tag vocabulary, grouped into axes.
58 Removed:
59 Removed: The tools read both from the file. One tool file therefore serves every
60 Removed: project. To change the states or the tags, edit the preamble of
61 Removed: `RAIL.org`. Run `rail-inspect` to read the current vocabulary.
62 Removed:
63 Removed: ### The Org ID is the handle
64 Removed:
65 Removed: Every item carries an Org ID. Each tool that changes an item needs that
66 Removed: ID. Never select an item by its title text, because two titles can
67 Removed: match. Run `rail-list` to get the ID of each item.
68 Removed:
69 Removed: ### Evidence, not silence
70 Removed:
71 Removed: Three transitions demand a written reason or written evidence:
72 Removed:
73 Removed: - `BLOCKED` needs a reason.
74 Removed: - `CANCELLED` needs a reason.
75 Removed: - `DONE` needs a commit hash and a test recap.
76 Removed:
77 Removed: The tools refuse these transitions without that text. A stalled item or
78 Removed: a dropped item therefore always states why.
79 Removed:
80 Removed: ### The tag vocabulary is grouped by axis
81 Removed:
82 Removed: The `#+TAGS:` lines group the tags into named axes. A common shape holds
83 Removed: a `Kind` axis and a `Scope` axis, plus optional axes. Each axis holds a
84 Removed: short set of bare tags. Distinct words across axes keep a bare tag
85 Removed: unambiguous. This is a convention, not a tool rule. The tool accepts any
86 Removed: tag that a `#+TAGS:` line declares, in any axis the project chooses.
87 Removed:
88 Removed: Infer the tags from the item text when you capture it. You can re-tag an
89 Removed: item later with `rail-retag`, as its shape changes.
90 Removed:
91 Removed: ## Set up the stream file
92 Removed:
93 Removed: A project needs one `RAIL.org` file at its root before the tools run.
94 Removed: Create it with this preamble, then edit the axes to fit the project:
95 Removed:
96 Removed: ```org
97 Removed: #+TITLE: Project action items
98 Removed: #+TODO: TODO IN-PROGRESS TESTING TESTED BLOCKED | CANCELLED DONE
99 Removed: #+TAGS: [ Kind : feat fix refactor chore docs ]
100 Removed: #+TAGS: [ Scope : core app web ui ]
101 Removed: #+TAGS: [ Impact : minor major ]
102 Removed:
103 Removed: # Local Variables:
104 Removed: # org-log-done: time
105 Removed: # End:
106 Removed: ```
107 Removed:
108 Removed: The `#+TODO:` keywords before the bar are open states. The keywords
109 Removed: after the bar are closed states. The `org-log-done` local variable makes
110 Removed: Org write a `CLOSED` timestamp on the transition to `DONE`, which
111 Removed: `rail-complete` requires. A file with no `#+TAGS:` line accepts any tag.
112 Removed:
113 Removed: ## Install the tools once per session
114 Removed:
115 Removed: The tools live in `rail-tools.el`, beside this `SKILL.md`. The skill can
116 Removed: sit in one project, or in a shared global directory that serves every
117 Removed: project. Load the tool file one time in the running Emacs. Use the
118 Removed: generic `eval-elisp` tool for the load only.
119 Removed:
120 Removed: Load the tool file from the directory of this `SKILL.md`, and pass the
121 Removed: directory of the project you work in as the project root. These are two
122 Removed: separate places. The tool file has one fixed home. The project root
123 Removed: changes with each project.
124 Removed:
125 Removed: ```elisp
126 Removed: (let ((tools "/absolute/path/to/this/skill/rail-tools.el")
127 Removed: (project "/the/project/directory/you/work/in"))
128 Removed: (unless (file-readable-p tools)
129 Removed: (error "No RAIL tools at %s" tools))
130 Removed: (load tools nil t)
131 Removed: (setq rail-project-root (file-name-as-directory project)))
132 Removed: ```
133 Removed:
134 Removed: Set `rail-project-root` to the project you work in. Every tool then uses
135 Removed: that project by default, wherever the tool file itself lives. Do not
136 Removed: write an absolute path from a home directory as a fixed constant in a
137 Removed: committed file, because that path differs on every machine.
138 Removed:
139 Removed: The tool file also runs an upward search for `RAIL.org` at load time, from
140 Removed: its own directory and then from `default-directory`. That search finds the
141 Removed: project only when the tool file sits inside the project. For a shared
142 Removed: install, or when `default-directory` sits outside the project, set
143 Removed: `rail-project-root` as shown, or pass `root` to each tool call.
144 Removed:
145 Removed: Then verify that the tools are present. This expression returns the
146 Removed: thirteen tool names:
147 Removed:
148 Removed: ```elisp
149 Removed: (seq-filter (lambda (name) (string-prefix-p "rail-" name))
150 Removed: (mcp-server-tools-list-names))
151 Removed: ```
152 Removed:
153 Removed: Confirm that the tools resolve the right project. This expression returns
154 Removed: the stream file path:
155 Removed:
156 Removed: ```elisp
157 Removed: (alist-get 'file (json-parse-string
158 Removed: (alist-get 'text (aref (mcp-server-tools-call
159 Removed: "rail-inspect" nil) 0))
160 Removed: :object-type 'alist))
161 Removed: ```
162 Removed:
163 Removed: If the load fails, stop and report the problem. Do not edit `RAIL.org` as
164 Removed: raw text instead.
165 Removed:
166 Removed: ### If the tools are not callable
167 Removed:
168 Removed: The tools register inside Emacs. The MCP client lists its tools when it
169 Removed: connects, so a mid-session load can leave the tools absent from your own
170 Removed: tool list. In that case, call each tool through the dispatch path:
171 Removed:
172 Removed: ```elisp
173 Removed: (mcp-server-tools-call "rail-list" '((state . "TODO")))
174 Removed: ```
175 Removed:
176 Removed: This path runs the same handler as a direct tool call. To make the tools
177 Removed: callable directly, load `rail-tools.el` from the Emacs init file.
178 Removed:
179 Removed: ### Why dedicated tools
180 Removed:
181 Removed: The generic `eval-elisp` tool sends its code through the Emacs security
182 Removed: form walker. The walker prompts for each file function, such as
183 Removed: `find-file-noselect` and `save-buffer`. The RAIL tools register as normal
184 Removed: MCP tools, and tool dispatch does not use the walker. The read-only tools
185 Removed: also carry a `readOnlyHint` annotation, so the client can approve them
186 Removed: without a prompt.
187 Removed:
188 Removed: You **MUST** use these tools for every change to `RAIL.org`.
189 Removed:
190 Removed: - You **MUST NOT** edit the file as raw text, because raw edits break the
191 Removed: timestamps, the IDs, and the tag alignment.
192 Removed: - You **MUST NOT** use a shell tool or a file tool for an item, because
193 Removed: those tools apply no validation.
194 Removed:
195 Removed: ## The RAIL tools
196 Removed:
197 Removed: Each tool accepts an optional `root` argument. `root` names the project
198 Removed: directory that holds `RAIL.org`. When you omit `root`, the tools use
199 Removed: `rail-project-root`. Set `root` only for a different project.
200 Removed:
201 Removed: | Tool | Purpose | Read-only |
202 Removed: |---|---|---|
203 Removed: | `rail-inspect` | Report the keyword sequence and the tag axes. | Yes |
204 Removed: | `rail-list` | List items, newest first, with ID, title, state, and tags. Accepts a `state` or `tag` filter. | Yes |
205 Removed: | `rail-show` | Report one item in full: body, logbook, checklist, and result. | Yes |
206 Removed: | `rail-verify` | Report the heading, state, `CLOSED` time, and tags of one item. | Yes |
207 Removed: | `rail-capture` | Create a `TODO` item at the top of the stream. | No |
208 Removed: | `rail-set-status` | Set the keyword of an item. Refuses `DONE`. | No |
209 Removed: | `rail-block` | Set `BLOCKED` and record a required reason. | No |
210 Removed: | `rail-cancel` | Set `CANCELLED` and record a required reason. | No |
211 Removed: | `rail-check` | Add, toggle, or list the checklist items of one item. | No |
212 Removed: | `rail-log` | Append a timestamped note to the `:LOGBOOK:` drawer. | No |
213 Removed: | `rail-retag` | Replace the tags of an item with a validated set. | No |
214 Removed: | `rail-set-result` | Write the `- result ::` line. | No |
215 Removed: | `rail-complete` | Set `DONE` after it records the result evidence. | No |
216 Removed:
217 Removed: Use `rail-show` to read one item, and `rail-retag` to re-tag it. These
218 Removed: are the correct tools for those two actions, because the generic Emacs
219 Removed: `org-*` tools cannot operate on this file.
220 Removed:
221 Removed: For the exact arguments and the result shape of each tool, read
222 Removed: `references/tools.md`.
223 Removed:
224 Removed: ## Step 1 — Capture the item
225 Removed:
226 Removed: Run `rail-capture` with these arguments:
227 Removed:
228 Removed: - `title` — a short imperative title under 60 characters.
229 Removed: - `body` — the full item text.
230 Removed: - `tags` — the tags that fit the item, from the file vocabulary.
231 Removed:
232 Removed: The tool inserts the item at the top of the file. It records the capture
233 Removed: time as an inactive `SCHEDULED` timestamp. It applies the tags, wraps the
234 Removed: body to 72 columns, and assigns an Org ID. Report that ID to the user.
235 Removed:
236 Removed: Keep the item text unchanged. If the text holds separate ideas, write the
237 Removed: body as a list. Pass the body as plain text, because the tool wraps it.
238 Removed:
239 Removed: If `rail-capture` rejects a tag, run `rail-inspect` to read the
240 Removed: vocabulary. Then fix the tag and re-run the capture.
241 Removed:
242 Removed: Then proceed to Step 2 when work starts on the item.
243 Removed:
244 Removed: ## Step 2 — Track the status
245 Removed:
246 Removed: First find the item. Run `rail-list` to read the ID, the title, the
247 Removed: state, and the tags of each item. Pass a `state` filter or a `tag` filter
248 Removed: to narrow the list. Use the ID for every later call.
249 Removed:
250 Removed: Read the current state with `rail-verify` before you change a keyword.
251 Removed: Then pick the tool from the target state:
252 Removed:
253 Removed: - Target an open keyword, such as `IN-PROGRESS` → `rail-set-status`
254 Removed: - Target `BLOCKED` → `rail-block`, with a reason
255 Removed: - Target `CANCELLED` → `rail-cancel`, with a reason
256 Removed: - Target `DONE` → `rail-complete`, with commit and test evidence
257 Removed:
258 Removed: Run `rail-verify` again after the change to confirm the keyword.
259 Removed:
260 Removed: ### Block or cancel with a reason
261 Removed:
262 Removed: Run `rail-block` when work cannot continue. Run `rail-cancel` when you
263 Removed: stop work on purpose. Each tool needs a `reason`, and writes it to the
264 Removed: item.
265 Removed:
266 Removed: You **MUST NOT** use `rail-set-status` here, because it records no reason.
267 Removed:
268 Removed: ### Split a large item with a checklist
269 Removed:
270 Removed: Run `rail-check` when one item is large enough to track in parts:
271 Removed:
272 Removed: - `action` `add` — append an unchecked item. Needs `item`.
273 Removed: - `action` `toggle` — flip one matching item. Needs `item`.
274 Removed: - `action` `list` — report the items and their state.
275 Removed:
276 Removed: The tool keeps the items in a `Checklist [/]:` block in the body. The
277 Removed: `[/]` cookie counts the completed items against the total. The items stay
278 Removed: inside the one action item. They are not separate stream entries, and
279 Removed: they carry no keyword of their own. For independent work, capture a new
280 Removed: action item instead.
281 Removed:
282 Removed: ### Record progress in the logbook
283 Removed:
284 Removed: Run `rail-log` with a `note` to record progress during a session. The
285 Removed: tool prepends a timestamped item to the `:LOGBOOK:` drawer. The logbook
286 Removed: is append-only. The tool never rewrites an earlier note, and never
287 Removed: rewrites the body.
288 Removed:
289 Removed: Then proceed to Step 3 when every closing criterion is met.
290 Removed:
291 Removed: ## Step 3 — Close the item
292 Removed:
293 Removed: Check each criterion before you close an item. Copy this list and mark
294 Removed: each item:
295 Removed:
296 Removed: - [ ] The build passes.
297 Removed: - [ ] The tests pass.
298 Removed: - [ ] The files have the required format.
299 Removed: - [ ] A commit exists.
300 Removed:
301 Removed: If one criterion fails, do not close the item. Set the state with
302 Removed: `rail-set-status` instead. Then fix the failure and re-run this check.
303 Removed:
304 Removed: When every item is marked, run `rail-complete` with these arguments:
305 Removed:
306 Removed: - `id` — the Org ID of the item.
307 Removed: - `commit` — the commit hash of the completed work.
308 Removed: - `tests` — a short test recap, for example `215 pass`.
309 Removed: - `model` — optional. The agent that did the work.
310 Removed: - `notes` — optional. A short tail, such as a root cause.
311 Removed:
312 Removed: The tool writes the result line, sets `DONE`, then confirms that Org
313 Removed: recorded the `CLOSED` timestamp. Org writes that timestamp, because the
314 Removed: file sets `org-log-done` to `time`.
315 Removed:
316 Removed: You **MUST NOT** write or edit that timestamp, because Org owns it.
317 Removed:
318 Removed: The result line takes this form. It stays on one line, because the reader
319 Removed: reads one line:
320 Removed:
321 Removed: ```org
322 Removed: - result :: model=example-agent commit=3051af0 tests=243 pass; short root cause
323 Removed: ```
324 Removed:
325 Removed: The item stays in place in the stream. There is no refile step.
326 Removed:
327 Removed: Run `rail-verify` last. Verify that the state is `DONE` and that the
328 Removed: `CLOSED` timestamp is present. To write the result line before you close
329 Removed: the item, run `rail-set-result` on its own.
330 Removed:
331 Removed: ## Test the tools
332 Removed:
333 Removed: The tools carry a test suite. Run it after any change to `rail-tools.el`:
334 Removed:
335 Removed: ```sh
336 Removed: .kiro/skills/rail/run-tests.sh
337 Removed: ```
338 Removed:
339 Removed: The suite needs no MCP framework and no configuration. Each test uses a
340 Removed: temporary stream file, so the project stream file stays unchanged.
341 Removed:
342 Removed: ## Common mistakes
343 Removed:
344 Removed: - **Editing the file as raw text.** The tools keep the timestamps, the
345 Removed: IDs, and the tag alignment correct. Raw edits do not.
346 Removed: - **Selecting an item by title.** Two titles can match. Use the Org ID
347 Removed: from `rail-list`.
348 Removed: - **Using `rail-set-status` for `DONE`.** That call fails by design. Use
349 Removed: `rail-complete`, which demands the evidence.
350 Removed: - **Writing a `CLOSED` timestamp.** Org writes it. The tool only confirms
351 Removed: it.
352 Removed: - **Merging two items.** Record each prompt as its own action item.
353 Removed:
354 Removed: If you cannot complete an item, record the item and record the failure.
.kiro/skills/rail/rail-tests.el
index 0c207f72..00000000 100644..000000
@@ -1,481 +0,0 @@
1 Removed: ;;; rail-tests.el --- End-to-end tests for the RAIL tools -*- lexical-binding: t; -*-
2 Removed:
3 Removed: ;;; Commentary:
4 Removed:
5 Removed: ;; These tests exercise the RAIL 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 rail-tests--preamble
22 Removed: (concat "#+TITLE: Test stream\n"
23 Removed: "#+TODO: TODO IN-PROGRESS TESTING TESTED BLOCKED | CANCELLED DONE\n"
24 Removed: "#+TAGS: [ Kind : feat fix refactor chore docs ]\n"
25 Removed: "#+TAGS: [ Scope : core app web ui ]\n"
26 Removed: "#+TAGS: [ Impact : minor major ]\n"
27 Removed: "\n"
28 Removed: "# Local Variables:\n"
29 Removed: "# org-log-done: time\n"
30 Removed: "# End:\n")
31 Removed: "Preamble of the temporary stream file.
32 Removed: The `#+TODO:' line gives the keyword sequence. The `#+TAGS:' lines give
33 Removed: the tag vocabulary, grouped into axes. The local variable
34 Removed: `org-log-done' makes Org write a CLOSED timestamp.")
35 Removed:
36 Removed: (defun rail-tests--decode (json-string)
37 Removed: "Return JSON-STRING decoded into Lisp with alists for objects.
38 Removed: Decode JSON null to nil and JSON false to `:json-false', which are the
39 Removed: same sentinels that the handlers encode from. A round trip therefore
40 Removed: gives back the value that the handler started with."
41 Removed: (json-parse-string json-string
42 Removed: :object-type 'alist
43 Removed: :null-object nil
44 Removed: :false-object :json-false))
45 Removed:
46 Removed: (defun rail-tests--call (handler args)
47 Removed: "Call HANDLER with ARGS and return the decoded result.
48 Removed: Signal an error when the handler reports one, so a failure is visible."
49 Removed: (let ((result (rail-tests--decode (funcall handler args))))
50 Removed: (when (alist-get 'error result)
51 Removed: (error "Handler failed: %s" (alist-get 'error result)))
52 Removed: result))
53 Removed:
54 Removed: (defmacro rail-tests--with-stream (root &rest body)
55 Removed: "Create a temporary project directory, bind ROOT to it, then run BODY.
56 Removed: Delete the directory and its buffers after BODY."
57 Removed: (declare (indent 1))
58 Removed: `(let* ((,root (file-name-as-directory (make-temp-file "rail-test" t)))
59 Removed: (file (expand-file-name rail-stream-file-name ,root))
60 Removed: (enable-local-variables :all)
61 Removed: (org-id-track-globally nil)
62 Removed: (create-lockfiles nil))
63 Removed: (unwind-protect
64 Removed: (progn
65 Removed: (with-temp-file file (insert rail-tests--preamble))
66 Removed: ,@body)
67 Removed: (dolist (buf (buffer-list))
68 Removed: (when (and (buffer-file-name buf)
69 Removed: (string-prefix-p ,root (buffer-file-name buf)))
70 Removed: (with-current-buffer buf (set-buffer-modified-p nil))
71 Removed: (kill-buffer buf)))
72 Removed: (delete-directory ,root t))))
73 Removed:
74 Removed: (defun rail-tests--capture (root title tags &optional body)
75 Removed: "Capture a request in ROOT with TITLE, TAGS, and optional BODY.
76 Removed: Return the new entry's Org ID."
77 Removed: (alist-get 'id (rail-tests--call
78 Removed: #'rail-tools--capture-handler
79 Removed: (list (cons 'root root)
80 Removed: (cons 'title title)
81 Removed: (cons 'tags tags)
82 Removed: (cons 'body (or body "Request body."))))))
83 Removed:
84 Removed: (defun rail-tests--file-text (root)
85 Removed: "Return the text of the stream file in ROOT."
86 Removed: (with-temp-buffer
87 Removed: (insert-file-contents (expand-file-name rail-stream-file-name root))
88 Removed: (buffer-string)))
89 Removed:
90 Removed: ;;; Root discovery
91 Removed:
92 Removed: (ert-deftest rail-test-locate-root-finds-the-stream-file ()
93 Removed: "`rail-locate-root' finds the root from a nested directory."
94 Removed: (rail-tests--with-stream root
95 Removed: (let ((nested (expand-file-name "a/b/c/" root)))
96 Removed: (make-directory nested t)
97 Removed: (should (equal (rail-locate-root nested) root)))))
98 Removed:
99 Removed: (ert-deftest rail-test-locate-root-returns-nil-without-a-stream-file ()
100 Removed: "`rail-locate-root' returns nil when no ancestor holds the file."
101 Removed: (let ((empty (file-name-as-directory (make-temp-file "rail-empty" t))))
102 Removed: (unwind-protect
103 Removed: (should (null (rail-locate-root empty)))
104 Removed: (delete-directory empty t))))
105 Removed:
106 Removed: (ert-deftest rail-test-root-argument-overrides-the-default ()
107 Removed: "An explicit `root' argument selects the file, not `rail-project-root'."
108 Removed: (rail-tests--with-stream root
109 Removed: (should (equal (rail-tools--file (list (cons 'root root)))
110 Removed: (expand-file-name rail-stream-file-name root)))))
111 Removed:
112 Removed: (ert-deftest rail-test-absent-root-uses-the-default ()
113 Removed: "An absent `root' argument falls back to `rail-project-root'."
114 Removed: (rail-tests--with-stream root
115 Removed: (let ((rail-project-root root))
116 Removed: (should (equal (rail-tools--file nil)
117 Removed: (expand-file-name rail-stream-file-name root))))))
118 Removed:
119 Removed: (ert-deftest rail-test-a-missing-directory-signals-an-error ()
120 Removed: "A `root' that is not a directory signals an error."
121 Removed: (should-error (rail-tools--file
122 Removed: (list (cons 'root "/rail/no/such/directory")))))
123 Removed:
124 Removed: ;;; Capture
125 Removed:
126 Removed: (ert-deftest rail-test-capture-creates-an-addressable-todo ()
127 Removed: "Capture writes a TODO entry with an ID, a SCHEDULED time, and tags."
128 Removed: (rail-tests--with-stream root
129 Removed: (let* ((id (rail-tests--capture root "Add a widget" ["feat" "web"]))
130 Removed: (entry (rail-tests--call #'rail-tools--show-handler
131 Removed: (list (cons 'root root) (cons 'id id)))))
132 Removed: (should (stringp id))
133 Removed: (should (equal (alist-get 'state entry) "TODO"))
134 Removed: (should (equal (alist-get 'heading entry) "Add a widget"))
135 Removed: (should (equal (append (alist-get 'tags entry) nil) '("feat" "web")))
136 Removed: (should (stringp (alist-get 'scheduled entry)))
137 Removed: (should (null (alist-get 'closed entry))))))
138 Removed:
139 Removed: (ert-deftest rail-test-capture-puts-the-newest-request-first ()
140 Removed: "Capture inserts each new request above the previous request."
141 Removed: (rail-tests--with-stream root
142 Removed: (rail-tests--capture root "First request" ["feat" "core"])
143 Removed: (rail-tests--capture root "Second request" ["fix" "app"])
144 Removed: (let ((rows (rail-tests--decode
145 Removed: (rail-tools--list-handler (list (cons 'root root))))))
146 Removed: (should (equal (length rows) 2))
147 Removed: (should (equal (alist-get 'title (aref rows 0)) "Second request"))
148 Removed: (should (equal (alist-get 'title (aref rows 1)) "First request")))))
149 Removed:
150 Removed: (ert-deftest rail-test-capture-rejects-a-tag-outside-the-vocabulary ()
151 Removed: "Capture rejects any tag that the closed vocabulary does not hold."
152 Removed: (rail-tests--with-stream root
153 Removed: (let ((result (rail-tests--decode
154 Removed: (rail-tools--capture-handler
155 Removed: (list (cons 'root root)
156 Removed: (cons 'title "Bad tags")
157 Removed: (cons 'tags ["feat" "trivial"]))))))
158 Removed: (should (string-match-p "Unknown tag" (alist-get 'error result))))))
159 Removed:
160 Removed: ;;; Status
161 Removed:
162 Removed: (ert-deftest rail-test-set-status-moves-through-the-open-keywords ()
163 Removed: "Set-status accepts each open keyword from the file's own sequence."
164 Removed: (rail-tests--with-stream root
165 Removed: (let ((id (rail-tests--capture root "Track status" ["feat" "core"])))
166 Removed: (dolist (state '("IN-PROGRESS" "TESTING" "TESTED"))
167 Removed: (let ((result (rail-tests--call
168 Removed: #'rail-tools--set-status-handler
169 Removed: (list (cons 'root root) (cons 'id id)
170 Removed: (cons 'state state)))))
171 Removed: (should (equal (alist-get 'state result) state))))
172 Removed: (should (equal (alist-get 'state (rail-tests--call
173 Removed: #'rail-tools--verify-handler
174 Removed: (list (cons 'root root) (cons 'id id))))
175 Removed: "TESTED")))))
176 Removed:
177 Removed: (ert-deftest rail-test-set-status-refuses-done ()
178 Removed: "Set-status refuses DONE, because completion needs result evidence."
179 Removed: (rail-tests--with-stream root
180 Removed: (let* ((id (rail-tests--capture root "Refuse done" ["feat" "core"]))
181 Removed: (result (rail-tests--decode
182 Removed: (rail-tools--set-status-handler
183 Removed: (list (cons 'root root) (cons 'id id)
184 Removed: (cons 'state "DONE"))))))
185 Removed: (should (string-match-p "rail-complete" (alist-get 'error result))))))
186 Removed:
187 Removed: (ert-deftest rail-test-block-and-cancel-record-a-reason ()
188 Removed: "Block and cancel write the reason, so the decision is never silent."
189 Removed: (rail-tests--with-stream root
190 Removed: (let ((blocked (rail-tests--capture root "Blocked work" ["feat" "app"]))
191 Removed: (dropped (rail-tests--capture root "Dropped work" ["feat" "ui"])))
192 Removed: (rail-tests--call #'rail-tools--block-handler
193 Removed: (list (cons 'root root) (cons 'id blocked)
194 Removed: (cons 'reason "The route does not exist")))
195 Removed: (rail-tests--call #'rail-tools--cancel-handler
196 Removed: (list (cons 'root root) (cons 'id dropped)
197 Removed: (cons 'reason "The user withdrew the request")))
198 Removed: (let ((text (rail-tests--file-text root)))
199 Removed: (should (string-match-p "- blocked :: The route does not exist" text))
200 Removed: (should (string-match-p "- cancelled :: The user withdrew" text)))
201 Removed: (should (equal "BLOCKED"
202 Removed: (alist-get 'state (rail-tests--call
203 Removed: #'rail-tools--verify-handler
204 Removed: (list (cons 'root root)
205 Removed: (cons 'id blocked))))))
206 Removed: (should (equal "CANCELLED"
207 Removed: (alist-get 'state (rail-tests--call
208 Removed: #'rail-tools--verify-handler
209 Removed: (list (cons 'root root)
210 Removed: (cons 'id dropped)))))))))
211 Removed:
212 Removed: (ert-deftest rail-test-block-requires-a-reason ()
213 Removed: "Block refuses a blank reason."
214 Removed: (rail-tests--with-stream root
215 Removed: (let* ((id (rail-tests--capture root "Needs a reason" ["feat" "app"]))
216 Removed: (result (rail-tests--decode
217 Removed: (rail-tools--block-handler
218 Removed: (list (cons 'root root) (cons 'id id)
219 Removed: (cons 'reason " "))))))
220 Removed: (should (string-match-p "reason" (alist-get 'error result))))))
221 Removed:
222 Removed: ;;; Checklist, logbook, and tags
223 Removed:
224 Removed: (ert-deftest rail-test-checklist-adds-and-toggles-items ()
225 Removed: "The checklist adds an item, then toggles it, and reports booleans."
226 Removed: (rail-tests--with-stream root
227 Removed: (let ((id (rail-tests--capture root "Split the task" ["feat" "core"])))
228 Removed: (let ((added (rail-tests--call
229 Removed: #'rail-tools--check-handler
230 Removed: (list (cons 'root root) (cons 'id id)
231 Removed: (cons 'action "add") (cons 'item "step one")))))
232 Removed: (should (equal (alist-get 'done (aref (alist-get 'items added) 0))
233 Removed: :json-false)))
234 Removed: (let ((toggled (rail-tests--call
235 Removed: #'rail-tools--check-handler
236 Removed: (list (cons 'root root) (cons 'id id)
237 Removed: (cons 'action "toggle") (cons 'item "step one")))))
238 Removed: (should (eq (alist-get 'done (aref (alist-get 'items toggled) 0)) t)))
239 Removed: (should (string-match-p "Checklist \\[1/1\\]" (rail-tests--file-text root))))))
240 Removed:
241 Removed: (ert-deftest rail-test-log-appends-and-keeps-earlier-notes ()
242 Removed: "The logbook keeps every note, newest first."
243 Removed: (rail-tests--with-stream root
244 Removed: (let ((id (rail-tests--capture root "Log progress" ["feat" "core"])))
245 Removed: (rail-tests--call #'rail-tools--log-handler
246 Removed: (list (cons 'root root) (cons 'id id)
247 Removed: (cons 'note "First note")))
248 Removed: (rail-tests--call #'rail-tools--log-handler
249 Removed: (list (cons 'root root) (cons 'id id)
250 Removed: (cons 'note "Second note")))
251 Removed: (let* ((entry (rail-tests--call #'rail-tools--show-handler
252 Removed: (list (cons 'root root) (cons 'id id))))
253 Removed: (notes (append (alist-get 'logbook entry) nil)))
254 Removed: (should (equal (length notes) 2))
255 Removed: (should (string-match-p "Second note" (nth 0 notes)))
256 Removed: (should (string-match-p "First note" (nth 1 notes)))))))
257 Removed:
258 Removed: (ert-deftest rail-test-retag-validates-against-the-vocabulary ()
259 Removed: "Retag replaces the tags, and refuses a tag outside the vocabulary."
260 Removed: (rail-tests--with-stream root
261 Removed: (let ((id (rail-tests--capture root "Retag me" ["feat" "core"])))
262 Removed: (let ((result (rail-tests--call
263 Removed: #'rail-tools--retag-handler
264 Removed: (list (cons 'root root) (cons 'id id)
265 Removed: (cons 'tags ["fix" "web" "minor"])))))
266 Removed: (should (equal (append (alist-get 'tags result) nil)
267 Removed: '("fix" "web" "minor"))))
268 Removed: (let ((result (rail-tests--decode
269 Removed: (rail-tools--retag-handler
270 Removed: (list (cons 'root root) (cons 'id id)
271 Removed: (cons 'tags ["nonsense"]))))))
272 Removed: (should (string-match-p "Unknown tag" (alist-get 'error result)))))))
273 Removed:
274 Removed: ;;; Result and completion
275 Removed:
276 Removed: (ert-deftest rail-test-set-result-writes-model-and-notes ()
277 Removed: "The result line holds the model, the commit, the tests, and the notes."
278 Removed: (rail-tests--with-stream root
279 Removed: (let* ((id (rail-tests--capture root "Record a result" ["fix" "core"]))
280 Removed: (result (rail-tests--call
281 Removed: #'rail-tools--set-result-handler
282 Removed: (list (cons 'root root) (cons 'id id)
283 Removed: (cons 'commit "abc1234") (cons 'tests "246 pass")
284 Removed: (cons 'model "test-agent")
285 Removed: (cons 'notes "the sentinel was wrong")))))
286 Removed: (should (equal (alist-get 'result result)
287 Removed: "model=test-agent commit=abc1234 tests=246 pass; the sentinel was wrong")))))
288 Removed:
289 Removed: (ert-deftest rail-test-set-result-omits-absent-optional-fields ()
290 Removed: "The result line holds only the commit and the tests when nothing else is given."
291 Removed: (rail-tests--with-stream root
292 Removed: (let* ((id (rail-tests--capture root "Plain result" ["fix" "core"]))
293 Removed: (result (rail-tests--call
294 Removed: #'rail-tools--set-result-handler
295 Removed: (list (cons 'root root) (cons 'id id)
296 Removed: (cons 'commit "abc1234") (cons 'tests "246 pass")))))
297 Removed: (should (equal (alist-get 'result result)
298 Removed: "commit=abc1234 tests=246 pass")))))
299 Removed:
300 Removed: (ert-deftest rail-test-set-result-replaces-an-earlier-line ()
301 Removed: "A second result call replaces the earlier result line."
302 Removed: (rail-tests--with-stream root
303 Removed: (let ((id (rail-tests--capture root "Replace result" ["fix" "core"])))
304 Removed: (dolist (commit '("aaa1111" "bbb2222"))
305 Removed: (rail-tests--call #'rail-tools--set-result-handler
306 Removed: (list (cons 'root root) (cons 'id id)
307 Removed: (cons 'commit commit) (cons 'tests "1 pass"))))
308 Removed: (let ((text (rail-tests--file-text root)))
309 Removed: (should-not (string-match-p "aaa1111" text))
310 Removed: (should (string-match-p "bbb2222" text))))))
311 Removed:
312 Removed: (ert-deftest rail-test-complete-sets-done-with-result-and-closed ()
313 Removed: "Completion writes the result, sets DONE, and confirms the CLOSED time."
314 Removed: (rail-tests--with-stream root
315 Removed: (let* ((id (rail-tests--capture root "Finish the work" ["feat" "core"]))
316 Removed: (result (rail-tests--call
317 Removed: #'rail-tools--complete-handler
318 Removed: (list (cons 'root root) (cons 'id id)
319 Removed: (cons 'commit "def5678") (cons 'tests "247 pass")
320 Removed: (cons 'model "test-agent")))))
321 Removed: (should (equal (alist-get 'state result) "DONE"))
322 Removed: (should (string-match-p "commit=def5678" (alist-get 'result result)))
323 Removed: (should (string-match-p "\\[.*\\]" (alist-get 'closed result)))
324 Removed: (let ((entry (rail-tests--call #'rail-tools--verify-handler
325 Removed: (list (cons 'root root) (cons 'id id)))))
326 Removed: (should (equal (alist-get 'state entry) "DONE"))
327 Removed: (should (stringp (alist-get 'closed entry)))))))
328 Removed:
329 Removed: (ert-deftest rail-test-complete-refuses-a-file-without-closed-logging ()
330 Removed: "Completion refuses to run when the file does not log a CLOSED time.
331 Removed: Org must write the CLOSED timestamp, so the tool never writes it."
332 Removed: (let* ((root (file-name-as-directory (make-temp-file "rail-nolog" t)))
333 Removed: (file (expand-file-name rail-stream-file-name root))
334 Removed: (enable-local-variables :all)
335 Removed: (org-id-track-globally nil)
336 Removed: (create-lockfiles nil)
337 Removed: (org-log-done nil))
338 Removed: (unwind-protect
339 Removed: (progn
340 Removed: ;; This preamble holds no `org-log-done' local variable.
341 Removed: (with-temp-file file
342 Removed: (insert "#+TITLE: No logging\n"
343 Removed: "#+TODO: TODO IN-PROGRESS | CANCELLED DONE\n"))
344 Removed: (let ((id (rail-tests--capture root "No logging" ["feat" "core"])))
345 Removed: ;; Close the buffer, so the next open reads the local variables
346 Removed: ;; of the file as it now stands.
347 Removed: (dolist (buf (buffer-list))
348 Removed: (when (equal (buffer-file-name buf) file)
349 Removed: (with-current-buffer buf (set-buffer-modified-p nil))
350 Removed: (kill-buffer buf)))
351 Removed: (let ((result (rail-tests--decode
352 Removed: (rail-tools--complete-handler
353 Removed: (list (cons 'root root) (cons 'id id)
354 Removed: (cons 'commit "def5678")
355 Removed: (cons 'tests "1 pass"))))))
356 Removed: (should (stringp (alist-get 'error result)))
357 Removed: (should (string-match-p "org-log-done"
358 Removed: (alist-get 'error result))))))
359 Removed: (dolist (buf (buffer-list))
360 Removed: (when (and (buffer-file-name buf)
361 Removed: (string-prefix-p root (buffer-file-name buf)))
362 Removed: (with-current-buffer buf (set-buffer-modified-p nil))
363 Removed: (kill-buffer buf)))
364 Removed: (delete-directory root t))))
365 Removed:
366 Removed: ;;; Reading
367 Removed:
368 Removed: (ert-deftest rail-test-list-encodes-an-absent-value-as-json-null ()
369 Removed: "A heading without an ID reports JSON null, not the text \"null\"."
370 Removed: (rail-tests--with-stream root
371 Removed: (let ((file (expand-file-name rail-stream-file-name root)))
372 Removed: (with-current-buffer (find-file-noselect file)
373 Removed: (goto-char (point-min))
374 Removed: (insert "* TODO Entry without an identifier :feat:core:\n")
375 Removed: (save-buffer))
376 Removed: (let* ((json (rail-tools--list-handler (list (cons 'root root))))
377 Removed: (rows (rail-tests--decode json)))
378 Removed: (should (null (alist-get 'id (aref rows 0))))
379 Removed: (should-not (string-match-p "\"null\"" json))))))
380 Removed:
381 Removed: (ert-deftest rail-test-list-filters-by-state-and-tag ()
382 Removed: "The list filters narrow the result by TODO keyword and by tag."
383 Removed: (rail-tests--with-stream root
384 Removed: (let ((first (rail-tests--capture root "Filter one" ["feat" "core"])))
385 Removed: (rail-tests--capture root "Filter two" ["fix" "web"])
386 Removed: (rail-tests--call #'rail-tools--set-status-handler
387 Removed: (list (cons 'root root) (cons 'id first)
388 Removed: (cons 'state "IN-PROGRESS")))
389 Removed: (let ((by-state (rail-tests--decode
390 Removed: (rail-tools--list-handler
391 Removed: (list (cons 'root root) (cons 'state "IN-PROGRESS")))))
392 Removed: (by-tag (rail-tests--decode
393 Removed: (rail-tools--list-handler
394 Removed: (list (cons 'root root) (cons 'tag "web"))))))
395 Removed: (should (equal (length by-state) 1))
396 Removed: (should (equal (alist-get 'title (aref by-state 0)) "Filter one"))
397 Removed: (should (equal (length by-tag) 1))
398 Removed: (should (equal (alist-get 'title (aref by-tag 0)) "Filter two"))))))
399 Removed:
400 Removed: (ert-deftest rail-test-inspect-reports-the-sequence-and-the-axes ()
401 Removed: "Inspect reports the file's keyword sequence and the file's tag axes."
402 Removed: (rail-tests--with-stream root
403 Removed: (let* ((result (rail-tests--call #'rail-tools--inspect-handler
404 Removed: (list (cons 'root root))))
405 Removed: (keywords (append (alist-get 'todo_keywords result) nil))
406 Removed: (axes (alist-get 'tags result)))
407 Removed: (should (member "IN-PROGRESS" keywords))
408 Removed: (should (member "DONE" keywords))
409 Removed: ;; The axes come from the file's own #+TAGS: lines, not from the code.
410 Removed: (should (equal (mapcar #'car axes) '(kind scope impact)))
411 Removed: (should (equal (append (alist-get 'impact axes) nil) '("minor" "major")))
412 Removed: (should (equal (append (alist-get 'scope axes) nil) '("core" "app" "web" "ui"))))))
413 Removed:
414 Removed: (ert-deftest rail-test-a-file-without-tags-accepts-any-tag ()
415 Removed: "A file that declares no #+TAGS: vocabulary accepts any tag.
416 Removed: The tool carries no vocabulary of its own, so an undeclared file places
417 Removed: no restriction on the tags."
418 Removed: (let* ((root (file-name-as-directory (make-temp-file "rail-notags" t)))
419 Removed: (file (expand-file-name rail-stream-file-name root))
420 Removed: (org-id-track-globally nil)
421 Removed: (create-lockfiles nil))
422 Removed: (unwind-protect
423 Removed: (progn
424 Removed: (with-temp-file file
425 Removed: (insert "#+TITLE: No vocabulary\n"
426 Removed: "#+TODO: TODO | DONE\n"))
427 Removed: (let* ((cap (rail-tests--call
428 Removed: #'rail-tools--capture-handler
429 Removed: (list (cons 'root root)
430 Removed: (cons 'title "Any tag is fine")
431 Removed: (cons 'tags ["anything" "at" "all"]))))
432 Removed: (id (alist-get 'id cap)))
433 Removed: (should (stringp id))
434 Removed: (should (equal (append (alist-get 'tags cap) nil)
435 Removed: '("anything" "at" "all")))))
436 Removed: (dolist (buf (buffer-list))
437 Removed: (when (and (buffer-file-name buf)
438 Removed: (string-prefix-p root (buffer-file-name buf)))
439 Removed: (with-current-buffer buf (set-buffer-modified-p nil))
440 Removed: (kill-buffer buf)))
441 Removed: (delete-directory root t))))
442 Removed:
443 Removed: ;;; The full path
444 Removed:
445 Removed: (ert-deftest rail-test-the-full-request-path-runs-end-to-end ()
446 Removed: "One request moves from capture to DONE through every step."
447 Removed: (rail-tests--with-stream root
448 Removed: (let ((id (rail-tests--capture root "Ship the feature" ["feat" "web"]
449 Removed: "Add a widget to the page.")))
450 Removed: (rail-tests--call #'rail-tools--check-handler
451 Removed: (list (cons 'root root) (cons 'id id)
452 Removed: (cons 'action "add") (cons 'item "write the code")))
453 Removed: (rail-tests--call #'rail-tools--set-status-handler
454 Removed: (list (cons 'root root) (cons 'id id)
455 Removed: (cons 'state "IN-PROGRESS")))
456 Removed: (rail-tests--call #'rail-tools--log-handler
457 Removed: (list (cons 'root root) (cons 'id id)
458 Removed: (cons 'note "Started the work")))
459 Removed: (rail-tests--call #'rail-tools--check-handler
460 Removed: (list (cons 'root root) (cons 'id id)
461 Removed: (cons 'action "toggle")
462 Removed: (cons 'item "write the code")))
463 Removed: (rail-tests--call #'rail-tools--set-status-handler
464 Removed: (list (cons 'root root) (cons 'id id)
465 Removed: (cons 'state "TESTED")))
466 Removed: (rail-tests--call #'rail-tools--complete-handler
467 Removed: (list (cons 'root root) (cons 'id id)
468 Removed: (cons 'commit "0badc0de") (cons 'tests "3 pass")
469 Removed: (cons 'model "test-agent")))
470 Removed: (let ((entry (rail-tests--call #'rail-tools--show-handler
471 Removed: (list (cons 'root root) (cons 'id id)))))
472 Removed: (should (equal (alist-get 'state entry) "DONE"))
473 Removed: (should (stringp (alist-get 'closed entry)))
474 Removed: (should (string-match-p "commit=0badc0de" (alist-get 'result entry)))
475 Removed: (should (equal (length (alist-get 'logbook entry)) 1))
476 Removed: (should (eq (alist-get 'done (aref (alist-get 'checklist entry) 0)) t))
477 Removed: (should (string-match-p "Add a widget" (alist-get 'body entry)))))))
478 Removed:
479 Removed: (provide 'rail-tests)
480 Removed:
481 Removed: ;;; rail-tests.el ends here
.kiro/skills/rail/rail-tools.el
index d7bb0f98..00000000 100644..000000
@@ -1,968 +0,0 @@
1 Removed: ;;; rail-tools.el --- RAIL MCP tools for RAIL.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: ;; RAIL means "Rolling Action Item List".
10 Removed: ;;
11 Removed: ;; This file registers dedicated Emacs MCP tools for the RAIL 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: ;; RAIL.org is a flat stream. Each action item is a top-level heading, newest
16 Removed: ;; first, tagged from a vocabulary that the file itself declares. The file
17 Removed: ;; holds no container heading.
18 Removed: ;;
19 Removed: ;; The tools carry no project vocabulary and no project workflow. The stream
20 Removed: ;; file owns both. The `#+TODO:' line declares the status keywords. The
21 Removed: ;; `#+TAGS:' lines declare the tag vocabulary, grouped into axes. The tools
22 Removed: ;; read both from the file, so one tool file serves every project.
23 Removed: ;;
24 Removed: ;; Why dedicated tools instead of `eval-elisp'?
25 Removed: ;;
26 Removed: ;; The `eval-elisp' tool routes its argument through
27 Removed: ;; `mcp-server-security-safe-eval', whose form walker blocks or prompts for
28 Removed: ;; "dangerous" functions such as `find-file-noselect', `write-file', and
29 Removed: ;; `save-buffer'. With `mcp-server-security-prompt-for-permissions' set to t,
30 Removed: ;; every RAIL snippet triggers a minibuffer prompt.
31 Removed: ;;
32 Removed: ;; A registered MCP tool runs through `mcp-server-tools-call', which calls the
33 Removed: ;; handler function directly and does NOT pass through the form walker. The
34 Removed: ;; handlers below therefore run without the repeated security prompt. Each
35 Removed: ;; tool also carries MCP `annotations' so the MCP client can auto-approve the
36 Removed: ;; read-only tools.
37 Removed: ;;
38 Removed: ;; The tools operate only on the file "RAIL.org". They find that file
39 Removed: ;; under `rail-project-root', or under a caller-supplied project ROOT that
40 Removed: ;; overrides it. They never touch any other file.
41 Removed: ;;
42 Removed: ;; `rail-project-root' comes from an upward search for the stream file. The
43 Removed: ;; search starts at this file's own directory, then at `default-directory'. It
44 Removed: ;; assumes no directory layout, so this file needs no absolute path and it
45 Removed: ;; works on every machine.
46 Removed: ;;
47 Removed: ;; Install the tools once per Emacs session. Load this file, and the tools
48 Removed: ;; register themselves. With `mcp-server-emacs-tools-enabled' set to `all',
49 Removed: ;; which is the default, they appear in the MCP tool list at once.
50 Removed: ;;
51 Removed: ;; The MCP framework is a soft dependency. When the framework is absent, for
52 Removed: ;; example in a batch test run, this file still loads and every handler stays
53 Removed: ;; callable. Run the test suite with the run-tests.sh script beside this file.
54 Removed:
55 Removed: ;;; Code:
56 Removed:
57 Removed: (require 'cl-lib)
58 Removed: (require 'org)
59 Removed: (require 'org-id)
60 Removed: (require 'json)
61 Removed: (require 'subr-x)
62 Removed:
63 Removed: ;; Load the MCP tool framework when it is available. When it is absent, for
64 Removed: ;; example in a batch test run, define the two symbols the registrations below
65 Removed: ;; need and discard each registration. The handler functions stay callable, so
66 Removed: ;; the test suite runs on any machine without the framework.
67 Removed: (defconst rail-mcp-available (require 'mcp-server-tools nil t)
68 Removed: "Non-nil when the Emacs MCP tool framework is available.")
69 Removed:
70 Removed: (unless rail-mcp-available
71 Removed: ;; Define plain functions, never a struct. A stub struct would clobber the
72 Removed: ;; real slot layout if the framework loads later in the same session.
73 Removed: (defun make-mcp-server-tool (&rest _args)
74 Removed: "Return nil. The MCP framework is absent."
75 Removed: nil)
76 Removed: (defun mcp-server-register-tool (_tool)
77 Removed: "Discard _TOOL. The MCP framework is absent."
78 Removed: nil))
79 Removed:
80 Removed: (defvar rail-stream-file-name "RAIL.org"
81 Removed: "Name of the Org file that RAIL manages.")
82 Removed:
83 Removed: (defvar rail-tools-path
84 Removed: (let ((file (or load-file-name buffer-file-name)))
85 Removed: (and file (expand-file-name file)))
86 Removed: "Absolute path of this file, or nil when the path is unknown.")
87 Removed:
88 Removed: (defun rail-locate-root (start)
89 Removed: "Return the closest directory at or above START that holds the stream file.
90 Removed: The stream file is `rail-stream-file-name'. Return nil when no
91 Removed: ancestor directory holds that file."
92 Removed: (let ((dir (and start (locate-dominating-file
93 Removed: (file-name-as-directory (expand-file-name start))
94 Removed: rail-stream-file-name))))
95 Removed: (and dir (expand-file-name (file-name-as-directory dir)))))
96 Removed:
97 Removed: (defvar rail-project-root
98 Removed: (or (rail-locate-root (and rail-tools-path
99 Removed: (file-name-directory rail-tools-path)))
100 Removed: (rail-locate-root default-directory)
101 Removed: (expand-file-name default-directory))
102 Removed: "Default project directory that holds the RAIL stream file.
103 Removed: The value comes from an upward search for `rail-stream-file-name',
104 Removed: first from this file's own directory, then from `default-directory'.
105 Removed: The search makes no assumption about the depth of this file in the
106 Removed: project. Set this variable to override the search, or pass a `root'
107 Removed: argument to any tool.")
108 Removed:
109 Removed: ;;; Helpers
110 Removed:
111 Removed: (defun rail-tools--file (args)
112 Removed: "Return the absolute path of the stream file for ARGS.
113 Removed: ARGS may hold a `root' string that names the project directory. When
114 Removed: `root' is absent, use `rail-project-root'. Signal an error when the
115 Removed: selected root is not a directory."
116 Removed: (let ((root (or (alist-get 'root args) rail-project-root)))
117 Removed: (unless (and (stringp root) (> (length root) 0))
118 Removed: (error "No project root: pass `root' or set `rail-project-root'"))
119 Removed: (let ((dir (expand-file-name root)))
120 Removed: (unless (file-directory-p dir)
121 Removed: (error "Not a directory: %s" dir))
122 Removed: (expand-file-name rail-stream-file-name dir))))
123 Removed:
124 Removed: (defun rail-tools--buffer (file)
125 Removed: "Return an org-mode buffer visiting FILE, creating it as needed."
126 Removed: (let ((buf (find-file-noselect file)))
127 Removed: (with-current-buffer buf
128 Removed: (unless (derived-mode-p 'org-mode)
129 Removed: (org-mode)))
130 Removed: buf))
131 Removed:
132 Removed: (defun rail-tools--goto-id (id)
133 Removed: "Move point to the heading with Org ID in the current buffer.
134 Removed: Signal an error when ID is not found."
135 Removed: (let ((marker (org-id-find id 'marker)))
136 Removed: (unless marker
137 Removed: (error "Org ID not found: %s" id))
138 Removed: (goto-char marker)))
139 Removed:
140 Removed: (defun rail-tools--fill-body ()
141 Removed: "Wrap the body of the entry at point to 72 columns.
142 Removed: Fill every paragraph after the metadata (SCHEDULED line, property
143 Removed: drawer) up to the next heading. Use `org-fill-paragraph' so Org
144 Removed: list items and other structure fill correctly. Point must be on
145 Removed: the entry heading."
146 Removed: (let ((fill-column 72))
147 Removed: (org-back-to-heading t)
148 Removed: (let ((end (save-excursion (org-end-of-subtree t t) (point-marker))))
149 Removed: ;; Move past the heading and all metadata (planning line,
150 Removed: ;; property drawer, logbook) to the first line of body text.
151 Removed: (org-end-of-meta-data t)
152 Removed: ;; Fill each body line. `org-fill-paragraph' fills the whole
153 Removed: ;; element and is idempotent, so stepping one line at a time is
154 Removed: ;; safe and does not overshoot a trailing paragraph.
155 Removed: (while (< (point) end)
156 Removed: (unless (looking-at-p "^[ \t]*$")
157 Removed: (org-fill-paragraph))
158 Removed: (forward-line 1))
159 Removed: (set-marker end nil))))
160 Removed:
161 Removed: (defun rail-tools--nonblank (value)
162 Removed: "Return VALUE trimmed when it is a non-blank string, else nil."
163 Removed: (and (stringp value)
164 Removed: (let ((trimmed (string-trim value)))
165 Removed: (and (> (length trimmed) 0) trimmed))))
166 Removed:
167 Removed: (defun rail-tools--set-result (commit tests &optional model notes)
168 Removed: "Write the structured result line for the entry at point.
169 Removed: Replace an existing `- result ::' line, or append one at the end of
170 Removed: the entry body. COMMIT is a commit hash. TESTS is a short recap
171 Removed: such as \"215 pass\". MODEL names the agent that did the work, and
172 Removed: NOTES adds a free-text tail after a semicolon. Both are optional.
173 Removed: Keep the line unwrapped, because the reader captures a single line.
174 Removed: Point must be on the entry heading."
175 Removed: (org-back-to-heading t)
176 Removed: (let* ((model (rail-tools--nonblank model))
177 Removed: (notes (rail-tools--nonblank notes))
178 Removed: (subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
179 Removed: (line (concat "- result :: "
180 Removed: (if model (format "model=%s " model) "")
181 Removed: (format "commit=%s tests=%s" commit tests)
182 Removed: (if notes (format "; %s" notes) ""))))
183 Removed: (org-back-to-heading t)
184 Removed: (if (re-search-forward "^[ \t]*- result ::.*$" subtree-end t)
185 Removed: (replace-match line t t)
186 Removed: (goto-char subtree-end)
187 Removed: (skip-chars-backward "\n")
188 Removed: (insert "\n\n" line))
189 Removed: (set-marker subtree-end nil)))
190 Removed:
191 Removed: (defun rail-tools--append-log (note)
192 Removed: "Append NOTE as a timestamped item to the entry's `:LOGBOOK:' drawer.
193 Removed: Create the drawer directly after the metadata when it is absent.
194 Removed: Insert the newest item first and wrap it to 72 columns. This is
195 Removed: append-only. It never edits an existing item or the body. Point
196 Removed: must be on the entry heading."
197 Removed: (let ((fill-column 72))
198 Removed: (org-back-to-heading t)
199 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
200 Removed: (ts (format-time-string "[%Y-%m-%d %a %H:%M]")))
201 Removed: (org-back-to-heading t)
202 Removed: (let ((drawer-start
203 Removed: (save-excursion
204 Removed: (when (re-search-forward "^[ \t]*:LOGBOOK:[ \t]*$" subtree-end t)
205 Removed: (line-beginning-position)))))
206 Removed: (unless drawer-start
207 Removed: (org-end-of-meta-data t)
208 Removed: (insert ":LOGBOOK:\n:END:\n")
209 Removed: (setq drawer-start
210 Removed: (save-excursion
211 Removed: (org-back-to-heading t)
212 Removed: (re-search-forward "^[ \t]*:LOGBOOK:[ \t]*$" subtree-end t)
213 Removed: (line-beginning-position))))
214 Removed: (goto-char drawer-start)
215 Removed: (forward-line 1)
216 Removed: (let ((item-start (point)))
217 Removed: (insert (format "- %s %s\n" ts note))
218 Removed: (save-excursion
219 Removed: (goto-char item-start)
220 Removed: (org-fill-paragraph))))
221 Removed: (set-marker subtree-end nil))))
222 Removed:
223 Removed: (defmacro rail-tools--json (&rest body)
224 Removed: "Evaluate BODY and return its value as a JSON string.
225 Removed: Catch any error and return a JSON object with an `error' field."
226 Removed: (declare (indent 0))
227 Removed: `(condition-case err
228 Removed: (json-encode (progn ,@body))
229 Removed: (error (json-encode `((error . ,(error-message-string err)))))))
230 Removed:
231 Removed: ;;; Tag vocabulary
232 Removed:
233 Removed: ;; The vocabulary is not hardcoded. Each stream file declares it with
234 Removed: ;; `#+TAGS:' group-tag lines, for example:
235 Removed: ;;
236 Removed: ;; #+TAGS: [ Kind : feat fix chore ]
237 Removed: ;; #+TAGS: [ Scope : core web ]
238 Removed: ;;
239 Removed: ;; Org parses those lines into `org-current-tag-alist'. The functions below
240 Removed: ;; read that alist, so the vocabulary follows the file, not this code. A file
241 Removed: ;; with no `#+TAGS:' line accepts any tag.
242 Removed:
243 Removed: (defun rail-tools--tag-axes ()
244 Removed: "Return the tag vocabulary of the current buffer, grouped by axis.
245 Removed: Read the group tags that Org parsed from the `#+TAGS:' lines into
246 Removed: `org-current-tag-alist'. Return an alist that maps each axis symbol to
247 Removed: its list of tag strings. Return nil when the file declares no axis, and
248 Removed: then the file accepts any tag."
249 Removed: (let ((axes '()) (current nil))
250 Removed: (dolist (entry org-current-tag-alist)
251 Removed: (pcase entry
252 Removed: (`(:startgrouptag) (setq current nil))
253 Removed: (`(:endgrouptag)
254 Removed: (when current
255 Removed: (push (cons (intern (downcase (car current)))
256 Removed: (nreverse (cdr current)))
257 Removed: axes))
258 Removed: (setq current nil))
259 Removed: (`(:grouptags))
260 Removed: (`(,(and tag (pred stringp)) . ,_)
261 Removed: (if current
262 Removed: (setcdr current (cons tag (cdr current)))
263 Removed: ;; The first tag in a group is the axis name.
264 Removed: (setq current (cons tag '()))))))
265 Removed: (nreverse axes)))
266 Removed:
267 Removed: (defun rail-tools--all-tags ()
268 Removed: "Return every tag the current buffer declares, as one flat list.
269 Removed: Return nil when the file declares no vocabulary."
270 Removed: (apply #'append (mapcar #'cdr (rail-tools--tag-axes))))
271 Removed:
272 Removed: (defun rail-tools--check-tags (tags)
273 Removed: "Signal an error when TAGS holds a tag outside the file vocabulary.
274 Removed: TAGS is a list of strings. When the file declares no vocabulary, accept
275 Removed: any tag. Return TAGS unchanged when valid."
276 Removed: (let ((allowed (rail-tools--all-tags)))
277 Removed: (when allowed
278 Removed: (dolist (tag tags)
279 Removed: (unless (member tag allowed)
280 Removed: (error "Unknown tag `%s'; allowed: %s"
281 Removed: tag (string-join allowed ", "))))))
282 Removed: tags)
283 Removed:
284 Removed: (defun rail-tools--goto-stream-top ()
285 Removed: "Move point to the insertion place for a new item.
286 Removed: That place is the start of the first top-level heading, after the
287 Removed: file preamble. When no heading exists, move to the end of the
288 Removed: preamble."
289 Removed: (goto-char (point-min))
290 Removed: (if (re-search-forward "^\\* " nil t)
291 Removed: (goto-char (line-beginning-position))
292 Removed: (goto-char (point-max))))
293 Removed:
294 Removed: ;;; inspect (read-only)
295 Removed:
296 Removed: (defun rail-tools--inspect-handler (args)
297 Removed: "Report the TODO sequence and the tag vocabulary for RAIL.org.
298 Removed: Read both from the stream file, so the report mirrors the file."
299 Removed: (rail-tools--json
300 Removed: (let ((file (rail-tools--file args)))
301 Removed: (with-current-buffer (rail-tools--buffer file)
302 Removed: (org-with-wide-buffer
303 Removed: `((file . ,file)
304 Removed: (todo_keywords . ,(vconcat org-todo-keywords-1))
305 Removed: (tags . ,(mapcar (lambda (axis)
306 Removed: (cons (car axis) (vconcat (cdr axis))))
307 Removed: (rail-tools--tag-axes)))))))))
308 Removed:
309 Removed: (mcp-server-register-tool
310 Removed: (make-mcp-server-tool
311 Removed: :name "rail-inspect"
312 Removed: :title "RAIL Inspect"
313 Removed: :description "Inspect RAIL.org: return its TODO keyword sequence and the tag vocabulary that the file declares, grouped by axis. Read-only."
314 Removed: :input-schema '((type . "object")
315 Removed: (properties . ((root . ((type . "string")
316 Removed: (description . "Absolute path to the project directory containing RAIL.org")))))
317 Removed: (required . []))
318 Removed: :function #'rail-tools--inspect-handler
319 Removed: :annotations '((readOnlyHint . t)
320 Removed: (destructiveHint . :false)
321 Removed: (idempotentHint . t)
322 Removed: (openWorldHint . :false))))
323 Removed:
324 Removed: ;;; list (read-only)
325 Removed:
326 Removed: (defun rail-tools--list-handler (args)
327 Removed: "List the top-level action items in RAIL.org, newest first.
328 Removed: ARGS keys: `root', `state' (optional), `tag' (optional). When STATE is
329 Removed: given, return only items with that TODO keyword. When TAG is given,
330 Removed: return only items that carry that tag. Each row has `id', `title',
331 Removed: `state', `scheduled', and `tags'."
332 Removed: (rail-tools--json
333 Removed: (let ((file (rail-tools--file args))
334 Removed: (state (alist-get 'state args))
335 Removed: (tag (alist-get 'tag args)))
336 Removed: (with-current-buffer (rail-tools--buffer file)
337 Removed: (org-with-wide-buffer
338 Removed: (goto-char (point-min))
339 Removed: (let ((rows '()))
340 Removed: (while (re-search-forward "^\\* " nil t)
341 Removed: (let ((todo (org-get-todo-state))
342 Removed: (tags (org-get-tags nil t)))
343 Removed: (when (and (or (null state) (equal state todo))
344 Removed: (or (null tag) (member tag tags)))
345 Removed: (push `((id . ,(org-id-get))
346 Removed: (title . ,(org-get-heading t t t t))
347 Removed: (state . ,todo)
348 Removed: (scheduled . ,(org-entry-get nil "SCHEDULED"))
349 Removed: (tags . ,(vconcat tags)))
350 Removed: rows))))
351 Removed: ;; The file is newest-first, so reverse to keep that order.
352 Removed: (vconcat (nreverse rows))))))))
353 Removed:
354 Removed: (mcp-server-register-tool
355 Removed: (make-mcp-server-tool
356 Removed: :name "rail-list"
357 Removed: :title "RAIL List"
358 Removed: :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 Removed: :input-schema '((type . "object")
360 Removed: (properties . ((root . ((type . "string")
361 Removed: (description . "Absolute path to the project directory")))
362 Removed: (state . ((type . "string")
363 Removed: (description . "Optional TODO keyword filter, for example TODO or IN-PROGRESS")))
364 Removed: (tag . ((type . "string")
365 Removed: (description . "Optional tag filter, for example web or major")))))
366 Removed: (required . []))
367 Removed: :function #'rail-tools--list-handler
368 Removed: :annotations '((readOnlyHint . t)
369 Removed: (destructiveHint . :false)
370 Removed: (idempotentHint . t)
371 Removed: (openWorldHint . :false))))
372 Removed:
373 Removed: ;;; capture
374 Removed:
375 Removed: (defun rail-tools--capture-handler (args)
376 Removed: "Capture a TODO entry at the top of the RAIL.org stream.
377 Removed: ARGS keys: `root', `title', `body' (optional), `tags' (optional array).
378 Removed: Insert the entry as a top-level heading directly below the file
379 Removed: preamble, so the newest item is first. Record the capture time as an
380 Removed: inactive SCHEDULED timestamp, apply TAGS from the file vocabulary,
381 Removed: assign an Org ID, and wrap the body to 72 columns."
382 Removed: (rail-tools--json
383 Removed: (let* ((file (rail-tools--file args))
384 Removed: (title (or (alist-get 'title args) (error "Missing `title'")))
385 Removed: (body (or (alist-get 'body args) ""))
386 Removed: (raw-tags (append (alist-get 'tags args) nil))
387 Removed: (captured-at (format-time-string "[%Y-%m-%d %a %H:%M]")))
388 Removed: (with-current-buffer (rail-tools--buffer file)
389 Removed: (org-with-wide-buffer
390 Removed: ;; Validate inside the buffer, because the vocabulary lives here.
391 Removed: (let ((tags (rail-tools--check-tags raw-tags)))
392 Removed: (rail-tools--goto-stream-top)
393 Removed: (let ((start (point)))
394 Removed: (insert (format "* TODO %s\nSCHEDULED: %s\n" title captured-at))
395 Removed: (unless (string-empty-p body)
396 Removed: (insert body "\n"))
397 Removed: (goto-char start)
398 Removed: (when tags
399 Removed: (org-set-tags tags))
400 Removed: (let ((id (org-id-get-create)))
401 Removed: (rail-tools--fill-body)
402 Removed: (when (buffer-modified-p) (save-buffer))
403 Removed: (goto-char (org-id-find id 'marker))
404 Removed: `((id . ,id)
405 Removed: (file . ,file)
406 Removed: (heading . ,(org-get-heading t t t t))
407 Removed: (tags . ,(vconcat (org-get-tags nil t))))))))))))
408 Removed:
409 Removed: (mcp-server-register-tool
410 Removed: (make-mcp-server-tool
411 Removed: :name "rail-capture"
412 Removed: :title "RAIL Capture"
413 Removed: :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 Removed: :input-schema '((type . "object")
415 Removed: (properties . ((root . ((type . "string")
416 Removed: (description . "Absolute path to the project directory")))
417 Removed: (title . ((type . "string")
418 Removed: (description . "Imperative title under 60 chars")))
419 Removed: (body . ((type . "string")
420 Removed: (description . "Full request text, verbatim")))
421 Removed: (tags . ((type . "array")
422 Removed: (items . ((type . "string")))
423 Removed: (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 Removed: (required . ["title"]))
425 Removed: :function #'rail-tools--capture-handler
426 Removed: :annotations '((readOnlyHint . :false)
427 Removed: (destructiveHint . :false)
428 Removed: (idempotentHint . :false)
429 Removed: (openWorldHint . :false))))
430 Removed:
431 Removed: ;;; set-status
432 Removed:
433 Removed: (defun rail-tools--set-status-handler (args)
434 Removed: "Change the TODO keyword of an entry.
435 Removed: ARGS keys: `root', `id', `state'. STATE must be one keyword from the
436 Removed: file's own #+TODO sequence, and must not be DONE (use rail-complete)."
437 Removed: (rail-tools--json
438 Removed: (let ((file (rail-tools--file args))
439 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
440 Removed: (state (or (alist-get 'state args) (error "Missing `state'"))))
441 Removed: (when (string-equal state "DONE")
442 Removed: (error "Use rail-complete for DONE, not rail-set-status"))
443 Removed: (with-current-buffer (rail-tools--buffer file)
444 Removed: (rail-tools--goto-id id)
445 Removed: (org-todo state)
446 Removed: (when (buffer-modified-p) (save-buffer))
447 Removed: `((id . ,id)
448 Removed: (state . ,(org-get-todo-state)))))))
449 Removed:
450 Removed: (mcp-server-register-tool
451 Removed: (make-mcp-server-tool
452 Removed: :name "rail-set-status"
453 Removed: :title "RAIL Set Status"
454 Removed: :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 Removed: :input-schema '((type . "object")
456 Removed: (properties . ((root . ((type . "string")))
457 Removed: (id . ((type . "string")
458 Removed: (description . "Org ID of the entry")))
459 Removed: (state . ((type . "string")
460 Removed: (description . "TODO keyword from the file's #+TODO sequence")))))
461 Removed: (required . ["id" "state"]))
462 Removed: :function #'rail-tools--set-status-handler
463 Removed: :annotations '((readOnlyHint . :false)
464 Removed: (destructiveHint . :false)
465 Removed: (idempotentHint . t)
466 Removed: (openWorldHint . :false))))
467 Removed:
468 Removed: ;;; log
469 Removed:
470 Removed: (defun rail-tools--log-handler (args)
471 Removed: "Append a timestamped note to an entry's `:LOGBOOK:' drawer.
472 Removed: ARGS keys: `root', `id', `note'. Append-only progress feedback from an
473 Removed: agentic session. Never edits an existing note or the item body."
474 Removed: (rail-tools--json
475 Removed: (let ((file (rail-tools--file args))
476 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
477 Removed: (note (or (alist-get 'note args) (error "Missing `note'"))))
478 Removed: (with-current-buffer (rail-tools--buffer file)
479 Removed: (rail-tools--goto-id id)
480 Removed: (rail-tools--append-log note)
481 Removed: (when (buffer-modified-p) (save-buffer))
482 Removed: `((id . ,id)
483 Removed: (state . ,(org-get-todo-state)))))))
484 Removed:
485 Removed: (mcp-server-register-tool
486 Removed: (make-mcp-server-tool
487 Removed: :name "rail-log"
488 Removed: :title "RAIL Log"
489 Removed: :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 Removed: :input-schema '((type . "object")
491 Removed: (properties . ((root . ((type . "string")))
492 Removed: (id . ((type . "string")
493 Removed: (description . "Org ID of the entry")))
494 Removed: (note . ((type . "string")
495 Removed: (description . "Progress note to append")))))
496 Removed: (required . ["id" "note"]))
497 Removed: :function #'rail-tools--log-handler
498 Removed: :annotations '((readOnlyHint . :false)
499 Removed: (destructiveHint . :false)
500 Removed: (idempotentHint . :false)
501 Removed: (openWorldHint . :false))))
502 Removed:
503 Removed: ;;; set-result
504 Removed:
505 Removed: (defun rail-tools--set-result-handler (args)
506 Removed: "Write the structured result line for an entry.
507 Removed: ARGS keys: `root', `id', `commit', `tests', `model' (optional),
508 Removed: `notes' (optional). COMMIT is a commit hash. TESTS is a short recap
509 Removed: such as \"215 pass\". Replace an existing result line or append one at
510 Removed: the end of the body."
511 Removed: (rail-tools--json
512 Removed: (let ((file (rail-tools--file args))
513 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
514 Removed: (commit (or (alist-get 'commit args) (error "Missing `commit'")))
515 Removed: (tests (or (alist-get 'tests args) (error "Missing `tests'")))
516 Removed: (model (alist-get 'model args))
517 Removed: (notes (alist-get 'notes args)))
518 Removed: (with-current-buffer (rail-tools--buffer file)
519 Removed: (rail-tools--goto-id id)
520 Removed: (rail-tools--set-result commit tests model notes)
521 Removed: (when (buffer-modified-p) (save-buffer))
522 Removed: (rail-tools--goto-id id)
523 Removed: `((id . ,id)
524 Removed: (result . ,(rail-tools--result-text)))))))
525 Removed:
526 Removed: (mcp-server-register-tool
527 Removed: (make-mcp-server-tool
528 Removed: :name "rail-set-result"
529 Removed: :title "RAIL Set Result"
530 Removed: :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 Removed: :input-schema '((type . "object")
532 Removed: (properties . ((root . ((type . "string")))
533 Removed: (id . ((type . "string")
534 Removed: (description . "Org ID of the entry")))
535 Removed: (commit . ((type . "string")
536 Removed: (description . "Commit hash")))
537 Removed: (tests . ((type . "string")
538 Removed: (description . "Short test recap, for example \"215 pass\"")))
539 Removed: (model . ((type . "string")
540 Removed: (description . "Optional model or agent that did the work, for example the agent name")))
541 Removed: (notes . ((type . "string")
542 Removed: (description . "Optional free-text tail appended after a semicolon, for example a root cause")))))
543 Removed: (required . ["id" "commit" "tests"]))
544 Removed: :function #'rail-tools--set-result-handler
545 Removed: :annotations '((readOnlyHint . :false)
546 Removed: (destructiveHint . :false)
547 Removed: (idempotentHint . t)
548 Removed: (openWorldHint . :false))))
549 Removed:
550 Removed: ;;; check (checklist for sub-tasks)
551 Removed:
552 Removed: (defconst rail-tools--checklist-header "Checklist [/]:"
553 Removed: "Header line that introduces an item's checkbox list.
554 Removed: The `[/]' cookie tracks completed items against the total.")
555 Removed:
556 Removed: (defun rail-tools--checklist-add (item)
557 Removed: "Add ITEM as an unchecked checkbox to the entry at point.
558 Removed: Create the checklist block when it does not exist. Wrap ITEM to 72
559 Removed: columns and refresh the `[/]' cookie. Point must be on the heading."
560 Removed: (let ((fill-column 72))
561 Removed: (org-back-to-heading t)
562 Removed: (let* ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
563 Removed: (line (format "- [ ] %s\n" item))
564 Removed: insert-at)
565 Removed: (org-back-to-heading t)
566 Removed: (if (re-search-forward "^Checklist \\[[0-9]*/[0-9]*\\]:[ \t]*$"
567 Removed: subtree-end t)
568 Removed: ;; Existing block: step past the trailing checkbox items.
569 Removed: (progn (forward-line 1)
570 Removed: (while (looking-at-p "^- \\[.\\] \\|^ ") (forward-line 1))
571 Removed: (setq insert-at (point))
572 Removed: (insert line))
573 Removed: ;; No block: append one at the end of the body.
574 Removed: (goto-char subtree-end)
575 Removed: (skip-chars-backward "\n")
576 Removed: (insert "\n\n" rail-tools--checklist-header "\n")
577 Removed: (setq insert-at (point))
578 Removed: (insert line))
579 Removed: (save-excursion (goto-char insert-at) (org-fill-paragraph))
580 Removed: (org-update-checkbox-count)
581 Removed: (set-marker subtree-end nil))))
582 Removed:
583 Removed: (defun rail-tools--checklist-toggle (item)
584 Removed: "Toggle the checkbox whose text matches ITEM in the entry at point.
585 Removed: Signal an error when no item matches. Refresh the `[/]' cookie.
586 Removed: Point must be on the heading."
587 Removed: (org-back-to-heading t)
588 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker))))
589 Removed: (org-back-to-heading t)
590 Removed: (if (re-search-forward (concat "^- \\[.\\] " (regexp-quote item))
591 Removed: subtree-end t)
592 Removed: (progn (beginning-of-line) (org-toggle-checkbox)
593 Removed: (org-update-checkbox-count))
594 Removed: (set-marker subtree-end nil)
595 Removed: (error "No checklist item matches: %s" item))
596 Removed: (set-marker subtree-end nil)))
597 Removed:
598 Removed: (defun rail-tools--checklist-items ()
599 Removed: "Return the checklist items of the entry at point.
600 Removed: Each item is an alist with `done' and `text'. Point must be on the
601 Removed: heading."
602 Removed: (org-back-to-heading t)
603 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point)))
604 Removed: (items '()))
605 Removed: (org-back-to-heading t)
606 Removed: (while (re-search-forward "^- \\[\\(.\\)\\] \\(.*\\)$" subtree-end t)
607 Removed: (push `((done . ,(if (string-equal (match-string 1) " ") :json-false t))
608 Removed: (text . ,(string-trim (match-string-no-properties 2))))
609 Removed: items))
610 Removed: (vconcat (nreverse items))))
611 Removed:
612 Removed: (defun rail-tools--check-handler (args)
613 Removed: "Manage the checklist of an item, for splitting a complex task.
614 Removed: ARGS keys: `root', `id', `action' (add|toggle|list), `item'.
615 Removed: `add' appends an unchecked item. `toggle' flips a matching item.
616 Removed: `list' returns the items. A `[/]' cookie tracks progress."
617 Removed: (rail-tools--json
618 Removed: (let ((file (rail-tools--file args))
619 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
620 Removed: (action (or (alist-get 'action args) (error "Missing `action'")))
621 Removed: (item (alist-get 'item args)))
622 Removed: (with-current-buffer (rail-tools--buffer file)
623 Removed: (rail-tools--goto-id id)
624 Removed: (cond
625 Removed: ((string-equal action "add")
626 Removed: (unless item (error "`add' needs an `item'"))
627 Removed: (rail-tools--checklist-add item))
628 Removed: ((string-equal action "toggle")
629 Removed: (unless item (error "`toggle' needs an `item'"))
630 Removed: (rail-tools--checklist-toggle item))
631 Removed: ((string-equal action "list") nil)
632 Removed: (t (error "Unknown action `%s'; use add, toggle, or list" action)))
633 Removed: (when (buffer-modified-p) (save-buffer))
634 Removed: (rail-tools--goto-id id)
635 Removed: `((id . ,id)
636 Removed: (items . ,(rail-tools--checklist-items)))))))
637 Removed:
638 Removed: (mcp-server-register-tool
639 Removed: (make-mcp-server-tool
640 Removed: :name "rail-check"
641 Removed: :title "RAIL Checklist"
642 Removed: :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 Removed: :input-schema '((type . "object")
644 Removed: (properties . ((root . ((type . "string")))
645 Removed: (id . ((type . "string")
646 Removed: (description . "Org ID of the item")))
647 Removed: (action . ((type . "string")
648 Removed: (description . "add, toggle, or list")))
649 Removed: (item . ((type . "string")
650 Removed: (description . "Item text for add or toggle")))))
651 Removed: (required . ["id" "action"]))
652 Removed: :function #'rail-tools--check-handler
653 Removed: :annotations '((readOnlyHint . :false)
654 Removed: (destructiveHint . :false)
655 Removed: (idempotentHint . :false)
656 Removed: (openWorldHint . :false))))
657 Removed:
658 Removed: ;;; show (read-only)
659 Removed:
660 Removed: (defun rail-tools--body-text ()
661 Removed: "Return the plain body text of the entry at point.
662 Removed: Read from the first line after the metadata up to the first of: a
663 Removed: `Checklist [' line, a `- result ::' line, or the end of the subtree.
664 Removed: Return the trimmed string. Point must be on the entry heading."
665 Removed: (org-back-to-heading t)
666 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point))))
667 Removed: (org-back-to-heading t)
668 Removed: (org-end-of-meta-data t)
669 Removed: (let ((body-start (point))
670 Removed: (body-end subtree-end))
671 Removed: (save-excursion
672 Removed: (goto-char body-start)
673 Removed: (when (re-search-forward "^\\(Checklist \\[\\|[ \t]*- result ::\\)"
674 Removed: subtree-end t)
675 Removed: (setq body-end (line-beginning-position))))
676 Removed: (string-trim
677 Removed: (buffer-substring-no-properties body-start body-end)))))
678 Removed:
679 Removed: (defun rail-tools--logbook-items ()
680 Removed: "Return the `:LOGBOOK:' drawer item lines of the entry at point.
681 Removed: Each item is a string, in the order stored (newest first). Return an
682 Removed: empty vector when there is no drawer. Point must be on the heading."
683 Removed: (org-back-to-heading t)
684 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point)))
685 Removed: (items '()))
686 Removed: (org-back-to-heading t)
687 Removed: (when (re-search-forward "^[ \t]*:LOGBOOK:[ \t]*$" subtree-end t)
688 Removed: (forward-line 1)
689 Removed: (while (and (< (point) subtree-end)
690 Removed: (not (looking-at-p "^[ \t]*:END:[ \t]*$")))
691 Removed: (when (looking-at "^[ \t]*- \\(.*\\)$")
692 Removed: (push (string-trim (match-string-no-properties 1)) items))
693 Removed: (forward-line 1)))
694 Removed: (vconcat (nreverse items))))
695 Removed:
696 Removed: (defun rail-tools--result-text ()
697 Removed: "Return the text after `- result ::' for the entry at point.
698 Removed: Return nil when there is no result line, so it encodes as JSON null.
699 Removed: Point must be on the heading."
700 Removed: (org-back-to-heading t)
701 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point))))
702 Removed: (org-back-to-heading t)
703 Removed: (when (re-search-forward "^[ \t]*- result ::[ \t]*\\(.*\\)$"
704 Removed: subtree-end t)
705 Removed: (string-trim (match-string-no-properties 1)))))
706 Removed:
707 Removed: (defun rail-tools--show-handler (args)
708 Removed: "Return the full content of an entry.
709 Removed: ARGS keys: `root', `id'. Report the heading, state, tags, scheduled
710 Removed: and closed timestamps, body text, logbook notes, checklist items, and
711 Removed: result line. Read-only."
712 Removed: (rail-tools--json
713 Removed: (let ((file (rail-tools--file args))
714 Removed: (id (or (alist-get 'id args) (error "Missing `id'"))))
715 Removed: (with-current-buffer (rail-tools--buffer file)
716 Removed: (org-with-wide-buffer
717 Removed: (rail-tools--goto-id id)
718 Removed: `((id . ,id)
719 Removed: (heading . ,(org-get-heading t t t t))
720 Removed: (state . ,(org-get-todo-state))
721 Removed: (tags . ,(vconcat (org-get-tags nil t)))
722 Removed: (scheduled . ,(org-entry-get nil "SCHEDULED"))
723 Removed: (closed . ,(org-entry-get nil "CLOSED"))
724 Removed: (body . ,(rail-tools--body-text))
725 Removed: (logbook . ,(rail-tools--logbook-items))
726 Removed: (checklist . ,(rail-tools--checklist-items))
727 Removed: (result . ,(rail-tools--result-text))))))))
728 Removed:
729 Removed: (mcp-server-register-tool
730 Removed: (make-mcp-server-tool
731 Removed: :name "rail-show"
732 Removed: :title "RAIL Show"
733 Removed: :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 Removed: :input-schema '((type . "object")
735 Removed: (properties . ((root . ((type . "string")))
736 Removed: (id . ((type . "string")
737 Removed: (description . "Org ID of the entry")))))
738 Removed: (required . ["id"]))
739 Removed: :function #'rail-tools--show-handler
740 Removed: :annotations '((readOnlyHint . t)
741 Removed: (destructiveHint . :false)
742 Removed: (idempotentHint . t)
743 Removed: (openWorldHint . :false))))
744 Removed:
745 Removed: ;;; retag
746 Removed:
747 Removed: (defun rail-tools--retag-handler (args)
748 Removed: "Replace the tags on an entry with a validated set.
749 Removed: ARGS keys: `root', `id', `tags' (array). Validate TAGS against the
750 Removed: file vocabulary, then set them, keeping the file's default tag
751 Removed: alignment."
752 Removed: (rail-tools--json
753 Removed: (let ((file (rail-tools--file args))
754 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
755 Removed: (raw-tags (append (alist-get 'tags args) nil)))
756 Removed: (with-current-buffer (rail-tools--buffer file)
757 Removed: (let ((tags (rail-tools--check-tags raw-tags)))
758 Removed: (rail-tools--goto-id id)
759 Removed: (org-set-tags tags)
760 Removed: (when (buffer-modified-p) (save-buffer))
761 Removed: `((id . ,id)
762 Removed: (tags . ,(vconcat (org-get-tags nil t)))))))))
763 Removed:
764 Removed: (mcp-server-register-tool
765 Removed: (make-mcp-server-tool
766 Removed: :name "rail-retag"
767 Removed: :title "RAIL Retag"
768 Removed: :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 Removed: :input-schema '((type . "object")
770 Removed: (properties . ((root . ((type . "string")))
771 Removed: (id . ((type . "string")
772 Removed: (description . "Org ID of the entry")))
773 Removed: (tags . ((type . "array")
774 Removed: (items . ((type . "string")))
775 Removed: (description . "Tags from the file vocabulary")))))
776 Removed: (required . ["id" "tags"]))
777 Removed: :function #'rail-tools--retag-handler
778 Removed: :annotations '((readOnlyHint . :false)
779 Removed: (destructiveHint . :false)
780 Removed: (idempotentHint . :false)
781 Removed: (openWorldHint . :false))))
782 Removed:
783 Removed: ;;; cancel and block
784 Removed:
785 Removed: (defun rail-tools--transition-with-reason (id keyword marker-label reason)
786 Removed: "Transition entry ID to KEYWORD and record REASON.
787 Removed: Signal an error when REASON is missing or blank. Write a wrapped body
788 Removed: line `- MARKER-LABEL :: REASON', replacing an existing line of that
789 Removed: form or appending one at the end of the body. Fill the line to 72
790 Removed: columns, then set the TODO keyword and save."
791 Removed: (when (or (null reason) (string-empty-p (string-trim reason)))
792 Removed: (error "A reason is required"))
793 Removed: (rail-tools--goto-id id)
794 Removed: (let ((fill-column 72))
795 Removed: (org-back-to-heading t)
796 Removed: (let ((subtree-end (save-excursion (org-end-of-subtree t t) (point-marker)))
797 Removed: (line (format "- %s :: %s" marker-label reason))
798 Removed: insert-at)
799 Removed: (org-back-to-heading t)
800 Removed: (if (re-search-forward
801 Removed: (format "^[ \t]*- %s ::.*$" (regexp-quote marker-label))
802 Removed: subtree-end t)
803 Removed: (progn (replace-match line t t)
804 Removed: (setq insert-at (line-beginning-position)))
805 Removed: (goto-char subtree-end)
806 Removed: (skip-chars-backward "\n")
807 Removed: (insert "\n\n" line)
808 Removed: (setq insert-at (line-beginning-position)))
809 Removed: (save-excursion (goto-char insert-at) (org-fill-paragraph))
810 Removed: (set-marker subtree-end nil)))
811 Removed: (rail-tools--goto-id id)
812 Removed: (org-todo keyword)
813 Removed: (when (buffer-modified-p) (save-buffer)))
814 Removed:
815 Removed: (defun rail-tools--cancel-handler (args)
816 Removed: "Set an entry to CANCELLED with a required reason.
817 Removed: ARGS keys: `root', `id', `reason'. Record REASON as a `- cancelled ::'
818 Removed: line so the decision is never silent."
819 Removed: (rail-tools--json
820 Removed: (let ((file (rail-tools--file args))
821 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
822 Removed: (reason (alist-get 'reason args)))
823 Removed: (with-current-buffer (rail-tools--buffer file)
824 Removed: (rail-tools--transition-with-reason id "CANCELLED" "cancelled" reason)
825 Removed: (rail-tools--goto-id id)
826 Removed: `((id . ,id)
827 Removed: (state . ,(org-get-todo-state)))))))
828 Removed:
829 Removed: (mcp-server-register-tool
830 Removed: (make-mcp-server-tool
831 Removed: :name "rail-cancel"
832 Removed: :title "RAIL Cancel"
833 Removed: :description "Set a RAIL entry to CANCELLED and record a required reason as a `- cancelled ::' line, so the decision is never silent."
834 Removed: :input-schema '((type . "object")
835 Removed: (properties . ((root . ((type . "string")))
836 Removed: (id . ((type . "string")
837 Removed: (description . "Org ID of the entry")))
838 Removed: (reason . ((type . "string")
839 Removed: (description . "Reason for cancelling the entry")))))
840 Removed: (required . ["id" "reason"]))
841 Removed: :function #'rail-tools--cancel-handler
842 Removed: :annotations '((readOnlyHint . :false)
843 Removed: (destructiveHint . :false)
844 Removed: (idempotentHint . :false)
845 Removed: (openWorldHint . :false))))
846 Removed:
847 Removed: (defun rail-tools--block-handler (args)
848 Removed: "Set an entry to BLOCKED with a required reason.
849 Removed: ARGS keys: `root', `id', `reason'. Record REASON as a `- blocked ::'
850 Removed: line so the blocker is never silent."
851 Removed: (rail-tools--json
852 Removed: (let ((file (rail-tools--file args))
853 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
854 Removed: (reason (alist-get 'reason args)))
855 Removed: (with-current-buffer (rail-tools--buffer file)
856 Removed: (rail-tools--transition-with-reason id "BLOCKED" "blocked" reason)
857 Removed: (rail-tools--goto-id id)
858 Removed: `((id . ,id)
859 Removed: (state . ,(org-get-todo-state)))))))
860 Removed:
861 Removed: (mcp-server-register-tool
862 Removed: (make-mcp-server-tool
863 Removed: :name "rail-block"
864 Removed: :title "RAIL Block"
865 Removed: :description "Set a RAIL entry to BLOCKED and record a required reason as a `- blocked ::' line, so the blocker is never silent."
866 Removed: :input-schema '((type . "object")
867 Removed: (properties . ((root . ((type . "string")))
868 Removed: (id . ((type . "string")
869 Removed: (description . "Org ID of the entry")))
870 Removed: (reason . ((type . "string")
871 Removed: (description . "Reason for blocking the entry")))))
872 Removed: (required . ["id" "reason"]))
873 Removed: :function #'rail-tools--block-handler
874 Removed: :annotations '((readOnlyHint . :false)
875 Removed: (destructiveHint . :false)
876 Removed: (idempotentHint . :false)
877 Removed: (openWorldHint . :false))))
878 Removed:
879 Removed: ;;; complete
880 Removed:
881 Removed: (defun rail-tools--complete-handler (args)
882 Removed: "Set an entry to DONE with its result evidence and confirm CLOSED.
883 Removed: ARGS keys: `root', `id', `commit', `tests', `model' (optional),
884 Removed: `notes' (optional). Write the structured result line, then transition
885 Removed: to DONE. Requires `org-log-done' to be `time' so the normal Org
886 Removed: transition inserts CLOSED; this handler never writes CLOSED."
887 Removed: (rail-tools--json
888 Removed: (let ((file (rail-tools--file args))
889 Removed: (id (or (alist-get 'id args) (error "Missing `id'")))
890 Removed: (commit (or (alist-get 'commit args) (error "Missing `commit'")))
891 Removed: (tests (or (alist-get 'tests args) (error "Missing `tests'")))
892 Removed: (model (alist-get 'model args))
893 Removed: (notes (alist-get 'notes args)))
894 Removed: (with-current-buffer (rail-tools--buffer file)
895 Removed: (hack-local-variables)
896 Removed: (unless (eq org-log-done 'time)
897 Removed: (error "org-log-done is not set to time"))
898 Removed: (rail-tools--goto-id id)
899 Removed: (rail-tools--set-result commit tests model notes)
900 Removed: (rail-tools--goto-id id)
901 Removed: (org-todo "DONE")
902 Removed: (when (buffer-modified-p) (save-buffer))
903 Removed: (let ((closed (org-entry-get nil "CLOSED")))
904 Removed: (unless closed
905 Removed: (error "Org did not record a CLOSED timestamp"))
906 Removed: `((id . ,id)
907 Removed: (state . ,(org-get-todo-state))
908 Removed: (result . ,(rail-tools--result-text))
909 Removed: (closed . ,closed)))))))
910 Removed:
911 Removed: (mcp-server-register-tool
912 Removed: (make-mcp-server-tool
913 Removed: :name "rail-complete"
914 Removed: :title "RAIL Complete"
915 Removed: :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 Removed: :input-schema '((type . "object")
917 Removed: (properties . ((root . ((type . "string")))
918 Removed: (id . ((type . "string")))
919 Removed: (commit . ((type . "string")
920 Removed: (description . "Commit hash for the completed work")))
921 Removed: (tests . ((type . "string")
922 Removed: (description . "Short test recap, for example \"215 pass\"")))
923 Removed: (model . ((type . "string")
924 Removed: (description . "Optional model or agent that did the work, for example the agent name")))
925 Removed: (notes . ((type . "string")
926 Removed: (description . "Optional free-text tail appended after a semicolon, for example a root cause")))))
927 Removed: (required . ["id" "commit" "tests"]))
928 Removed: :function #'rail-tools--complete-handler
929 Removed: :annotations '((readOnlyHint . :false)
930 Removed: (destructiveHint . :false)
931 Removed: (idempotentHint . t)
932 Removed: (openWorldHint . :false))))
933 Removed:
934 Removed: ;;; verify (read-only)
935 Removed:
936 Removed: (defun rail-tools--verify-handler (args)
937 Removed: "Return the current state of the entry with `id'.
938 Removed: ARGS keys: `root', `id'. Read-only."
939 Removed: (rail-tools--json
940 Removed: (let ((file (rail-tools--file args))
941 Removed: (id (or (alist-get 'id args) (error "Missing `id'"))))
942 Removed: (with-current-buffer (rail-tools--buffer file)
943 Removed: (org-with-wide-buffer
944 Removed: (rail-tools--goto-id id)
945 Removed: `((id . ,id)
946 Removed: (heading . ,(org-get-heading t t t t))
947 Removed: (state . ,(org-get-todo-state))
948 Removed: (closed . ,(org-entry-get nil "CLOSED"))
949 Removed: (tags . ,(vconcat (org-get-tags nil t)))))))))
950 Removed:
951 Removed: (mcp-server-register-tool
952 Removed: (make-mcp-server-tool
953 Removed: :name "rail-verify"
954 Removed: :title "RAIL Verify"
955 Removed: :description "Return the heading, TODO state, CLOSED timestamp, and tags of a RAIL entry. Read-only."
956 Removed: :input-schema '((type . "object")
957 Removed: (properties . ((root . ((type . "string")))
958 Removed: (id . ((type . "string")))))
959 Removed: (required . ["id"]))
960 Removed: :function #'rail-tools--verify-handler
961 Removed: :annotations '((readOnlyHint . t)
962 Removed: (destructiveHint . :false)
963 Removed: (idempotentHint . t)
964 Removed: (openWorldHint . :false))))
965 Removed:
966 Removed: (provide 'rail-tools)
967 Removed:
968 Removed: ;;; rail-tools.el ends here
.kiro/skills/rail/references/tools.md
index 3fdd594d..00000000 100644..000000
@@ -1,168 +0,0 @@
1 Removed: # RAIL 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: `rail-project-root`. That variable comes from an upward search for
9 Removed: `RAIL.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: ### rail-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: ### rail-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: ### rail-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: ### rail-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: ### rail-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 item text, as plain text.
66 Removed: - `tags` — optional. An array of tags from the file vocabulary (the #+TAGS: lines).
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: ### rail-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 `rail-complete` for `DONE`.
84 Removed:
85 Removed: ### rail-block and rail-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: `rail-block` writes a `- blocked ::` line and sets `BLOCKED`.
95 Removed: `rail-cancel` writes a `- cancelled ::` line and sets `CANCELLED`.
96 Removed: Each tool refuses a blank reason.
97 Removed:
98 Removed: ### rail-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: ### rail-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: ### rail-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 file vocabulary (the #+TAGS: lines).
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: ### rail-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: ### rail-complete
152 Removed:
153 Removed: Arguments: the same arguments as `rail-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/rail/run-tests.sh
index 3e35738e..00000000 100755..000000
@@ -1,35 +0,0 @@
1 Removed: #!/bin/sh
2 Removed: # Run the RAIL 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 rail-tools.el rail-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/rail-tools.el" \
29 Removed: -l "$dir/rail-tests.el" \
30 Removed: -f ert-run-tests-batch-and-exit; then
31 Removed: echo "RAIL tests passed."
32 Removed: else
33 Removed: echo "Error: RAIL tests failed. Read the ERT output above for the failing test." >&2
34 Removed: exit 1
35 Removed: fi