[OCaml] High Intensity Training Online
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.
Changed files
.kiro/skills/rail/SKILL.md
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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