chore rework FASOC into FRACAS with dedicated MCP tools

Rename the skill to FRACAS (Feature Requests As a Constant Agent-monitored Stream). Add fracas-tools.el, which registers dedicated Emacs MCP tools for capture, status, completion, refile, and verification. The generic eval-elisp tool routes code through the Emacs security form walker, which prompts for each file function. A registered MCP tool runs through the tool dispatch path, which does not use the walker, so the operations run without repeated prompts. Read-only tools carry a readOnlyHint annotation for client auto-approval. Update the vocabulary from ENHANCEMENTS.org to REQUESTS.org.

Commit
4554df339e3f08a20e340ed5fb43eed2f2554f28
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
.kiro/skills/fasoc/SKILL.md
index 55aef4ab..00000000 100644..000000
@@ -1,233 +0,0 @@
1 Removed: ---
2 Removed: name: fasoc
3 Removed: description: Capture each feature request in ENHANCEMENTS.org with Emacs MCP. FASOC means Feature as Stream of Consciousness. Never implement requests.
4 Removed: ---
5 Removed:
6 Removed: # FASOC — Feature as Stream of Consciousness
7 Removed:
8 Removed: Use this skill when the user asks to capture a feature request or track a
9 Removed: feature in `ENHANCEMENTS.org`.
10 Removed:
11 Removed: Keep the session open across prompts. Treat each prompt as a separate feature
12 Removed: request. Capture each request under `* Dispatch`. Do not merge requests. Do not
13 Removed: build, test, commit, or start an agent.
14 Removed:
15 Removed: The current prompt is the feature request:
16 Removed:
17 Removed: $ARGUMENTS
18 Removed:
19 Removed: If the prompt is empty, ask for one line that describes the feature
20 Removed: request. Then stop. Capture the next prompt as the next request.
21 Removed:
22 Removed: ## Use the Emacs MCP server
23 Removed:
24 Removed: Use the Emacs Model Context Protocol (MCP) server for all Org mode
25 Removed: operations. Do not edit `ENHANCEMENTS.org` as raw text. Do not use
26 Removed: shell or file tools to capture, refile, change status, or verify an
27 Removed: entry.
28 Removed:
29 Removed: Use these Emacs MCP tools:
30 Removed:
31 Removed: - Use `org-get-node` or `org-search` to inspect headings, keywords,
32 Removed: and content.
33 Removed: - Use `org-list-templates` and `org-capture` to inspect templates and
34 Removed: create the `Dispatch` heading when needed.
35 Removed: - Use the Emacs MCP `eval-elisp` tool to run the required code
36 Removed: snippets below.
37 Removed: - Use `org-get-node` after each change to verify the result.
38 Removed:
39 Removed: Use the file and outline path that the Emacs MCP server returns. If an
40 Removed: Emacs MCP operation cannot perform the action, stop and report the
41 Removed: problem. Do not replace the operation with a raw file edit.
42 Removed:
43 Removed: ## Required Emacs code
44 Removed:
45 Removed: Run each snippet in this section with the Emacs MCP `eval-elisp`
46 Removed: tool. Replace `ID`, `TITLE`, `BODY`, and `TARGET` with values that you
47 Removed: observed through the Emacs MCP server. Run the inspection snippet
48 Removed: before you change the file. Run the capture, status, completion, and
49 Removed: refile snippets when needed. Run the verification expression after
50 Removed: each change.
51 Removed:
52 Removed: Inspect the file and its TODO sequence with this expression:
53 Removed:
54 Removed: ```elisp
55 Removed: (let ((file (expand-file-name "ENHANCEMENTS.org" default-directory)))
56 Removed: (with-current-buffer (find-file-noselect file)
57 Removed: (org-mode)
58 Removed: (org-with-wide-buffer
59 Removed: (list :todo-keywords org-todo-keywords-1
60 Removed: :dispatch (org-find-exact-headline-in-buffer "Dispatch")))))
61 Removed: ```
62 Removed:
63 Removed: Run this expression to capture an entry under `Dispatch`. Create
64 Removed: `Dispatch` with the Emacs MCP `org-capture` tool first if it does not
65 Removed: exist.
66 Removed:
67 Removed: ```elisp
68 Removed: (let* ((file (expand-file-name "ENHANCEMENTS.org" default-directory))
69 Removed: (title "TITLE")
70 Removed: (body "BODY")
71 Removed: (captured-at (format-time-string "[%Y-%m-%d %a %H:%M]"))
72 Removed: (org-capture-templates
73 Removed: `(("e" "Enhancement" entry
74 Removed: (file+headline ,file "Dispatch")
75 Removed: "%i")))
76 Removed: (org-capture-string
77 Removed: (format "** TODO %s\nSCHEDULED: %s\n%s\n" title captured-at body)
78 Removed: "e")
79 Removed: (org-capture-finalize))
80 Removed: ```
81 Removed:
82 Removed: Run this expression to change an open entry's keyword. Replace `STATE`
83 Removed: with one keyword from the observed `#+TODO:` sequence.
84 Removed:
85 Removed: ```elisp
86 Removed: (let ((file (expand-file-name "ENHANCEMENTS.org" default-directory))
87 Removed: (id "ID")
88 Removed: (state "STATE"))
89 Removed: (with-current-buffer (find-file-noselect file)
90 Removed: (org-mode)
91 Removed: (let ((marker (org-id-find id 'marker)))
92 Removed: (unless marker
93 Removed: (user-error "Org ID not found: %s" id))
94 Removed: (goto-char marker))
95 Removed: (org-todo state)
96 Removed: (save-buffer)))
97 Removed: ```
98 Removed:
99 Removed: Run this expression to set `DONE` and verify the completion
100 Removed: timestamp. Do not insert a `CLOSED` line yourself. The normal Org TODO
101 Removed: transition must insert it.
102 Removed:
103 Removed: ```elisp
104 Removed: (let ((file (expand-file-name "ENHANCEMENTS.org" default-directory))
105 Removed: (id "ID"))
106 Removed: (with-current-buffer (find-file-noselect file)
107 Removed: (org-mode)
108 Removed: (hack-local-variables)
109 Removed: (unless (eq org-log-done 'time)
110 Removed: (user-error "org-log-done is not set to time"))
111 Removed: (let ((marker (org-id-find id 'marker)))
112 Removed: (unless marker
113 Removed: (user-error "Org ID not found: %s" id))
114 Removed: (goto-char marker))
115 Removed: (org-todo "DONE")
116 Removed: (save-buffer)
117 Removed: (unless (org-entry-get nil "CLOSED")
118 Removed: (user-error "Org did not record a CLOSED timestamp"))
119 Removed: (org-entry-get nil "CLOSED")))
120 Removed: ```
121 Removed:
122 Removed: Run this expression to refile the completed entry under a top-level
123 Removed: section. Replace `TARGET` with the observed top-level heading name.
124 Removed:
125 Removed: ```elisp
126 Removed: (let ((file (expand-file-name "ENHANCEMENTS.org" default-directory))
127 Removed: (id "ID")
128 Removed: (target "TARGET"))
129 Removed: (with-current-buffer (find-file-noselect file)
130 Removed: (org-mode)
131 Removed: (let ((marker (org-id-find id 'marker)))
132 Removed: (unless marker
133 Removed: (user-error "Org ID not found: %s" id))
134 Removed: (goto-char marker))
135 Removed: (let ((source-position (point))
136 Removed: (target-position
137 Removed: (with-current-buffer (find-file-noselect file)
138 Removed: (org-mode)
139 Removed: (goto-char (point-min))
140 Removed: (let ((target-marker
141 Removed: (org-find-exact-headline-in-buffer target)))
142 Removed: (unless target-marker
143 Removed: (user-error "Org target not found: %s" target))
144 Removed: (marker-position target-marker)))))
145 Removed: (goto-char source-position)
146 Removed: (org-refile nil nil (list target file nil target-position))
147 Removed: (save-buffer))))
148 Removed: ```
149 Removed:
150 Removed: Run this expression to verify the entry after a mutation:
151 Removed:
152 Removed: ```elisp
153 Removed: (let ((file (expand-file-name "ENHANCEMENTS.org" default-directory))
154 Removed: (id "ID"))
155 Removed: (with-current-buffer (find-file-noselect file)
156 Removed: (org-mode)
157 Removed: (let ((marker (org-id-find id 'marker)))
158 Removed: (unless marker
159 Removed: (user-error "Org ID not found: %s" id))
160 Removed: (goto-char marker))
161 Removed: (list (org-get-heading t t t t)
162 Removed: (org-get-todo-state)
163 Removed: (org-entry-get nil "CLOSED")
164 Removed: (org-get-outline-path))))
165 Removed: ```
166 Removed:
167 Removed: ## Step 1 — Capture under Dispatch
168 Removed:
169 Removed: Inspect `ENHANCEMENTS.org` with `org-search` or `org-get-node` before
170 Removed: writing. Find the top-level `* Dispatch` heading. If it does not
171 Removed: exist, create it at the top of the file.
172 Removed:
173 Removed: Add the request as a level-2 heading under `* Dispatch`. Run
174 Removed: `org-list-templates` first. Select a matching `org-capture`
175 Removed: template. If no template matches, use `org-capture` direct mode with
176 Removed: the file, title, outline path, and body. Then run the capture snippet.
177 Removed:
178 Removed: Create this entry:
179 Removed:
180 Removed: ```
181 Removed: ** TODO <imperative title under 60 chars>
182 Removed: SCHEDULED: [YYYY-MM-DD Day HH:MM]
183 Removed: <the full idea text, formatted as a list if text contains separate ideas>
184 Removed: ```
185 Removed:
186 Removed: The inactive timestamp records the capture time. Use a short imperative
187 Removed: title. Keep the full request text unchanged. Do not rewrite it. Keep
188 Removed: the file well-formed. Verify the new heading with `org-get-node`. Report
189 Removed: its outline path.
190 Removed:
191 Removed: ## Step 2 — Refile on completion
192 Removed:
193 Removed: Keep the request under `* Dispatch` while its status is open,
194 Removed: `BLOCKED`, or `CANCELLED`. When its status becomes `DONE`, move the
195 Removed: entire heading out of `* Dispatch`.
196 Removed:
197 Removed: Inspect the top-level (`*`) headings with `org-search` or
198 Removed: `org-get-node`. Select the section that best matches the request. If
199 Removed: no section matches, create a short, consistent top-level section with
200 Removed: `org-capture` through the Emacs MCP server.
201 Removed:
202 Removed: Run the refile snippet. The snippet calls `org-refile`. Verify the new
203 Removed: location with `org-get-node`. Report the new outline path.
204 Removed:
205 Removed: ## Step 3 — Track the status
206 Removed:
207 Removed: Read the file's `#+TODO:` header and the current heading with
208 Removed: `org-get-node` before changing a keyword. Use the keyword sequence in
209 Removed: the header. Run the status snippet for every status change. The
210 Removed: snippet calls `org-todo`. Do not make another status change after the
211 Removed: snippet.
212 Removed:
213 Removed: - `TODO` — Capture the request. Do not start work.
214 Removed: - `IN-PROGRESS` — Start work.
215 Removed: - `TESTING` — Run tests. The tests do not pass.
216 Removed: - `TESTED` — Tests pass. No commit exists.
217 Removed: - `DONE` — The build passes. Tests pass. The files have the required
218 Removed: format. A commit exists. Run the completion snippet. The snippet
219 Removed: sets `DONE`, uses the configured `org-log-done` behavior, and
220 Removed: verifies the completion timestamp. Do not add or edit the timestamp
221 Removed: manually. Refile the heading under Step 2.
222 Removed: - `BLOCKED` — Work cannot continue. Mention the cause of the block.
223 Removed: - `CANCELLED` — Stop work intentionally.
224 Removed:
225 Removed: Change the keyword only after you observe the current status. Do not
226 Removed: set `DONE` unless you observe a commit and passing tests. After each
227 Removed: status snippet, use `org-get-node` to verify the keyword. When the
228 Removed: keyword becomes `DONE`, verify the completion timestamp before you
229 Removed: refile the heading. Let Org's configured completion logging add the
230 Removed: timestamp. Add no other field.
231 Removed:
232 Removed: Record every prompt as its own request. If you cannot complete a
233 Removed: request, record the request and the failure.
.kiro/skills/fracas/SKILL.md
index 00000000..4f52f55f 000000..100644
@@ -0,0 +1,157 @@
1 Added: ---
2 Added: name: fracas
3 Added: description: Manage feature requests with Emacs MCP tools.
4 Added: ---
5 Added:
6 Added: # FRACAS — Feature Requests As a Constant Agent-monitored Stream
7 Added:
8 Added: Use this skill when the user asks to capture feature requests or to
9 Added: track a feature in `REQUESTS.org`.
10 Added:
11 Added: Keep the session open across prompts. Treat each prompt as a separate
12 Added: feature request. Capture each request under `* Dispatch`. Do not merge
13 Added: requests. Do not build, test, commit, or start an agent.
14 Added:
15 Added: The current prompt is the feature request:
16 Added:
17 Added: $ARGUMENTS
18 Added:
19 Added: If the prompt is empty, ask for one line that describes the feature
20 Added: request. Then stop. Capture the next prompt as the next request.
21 Added:
22 Added: ## Use the dedicated FRACAS MCP tools
23 Added:
24 Added: This skill provides dedicated Emacs Model Context Protocol (MCP)
25 Added: tools. Use them for every interaction with `REQUESTS.org`. Do not edit
26 Added: the file as raw text. Do not use shell or file tools to capture,
27 Added: refile, change status, or verify an entry.
28 Added:
29 Added: The dedicated tools avoid a security prompt. The generic `eval-elisp`
30 Added: tool passes its code through the Emacs security form walker. The
31 Added: walker prompts for each file function, such as `find-file-noselect`
32 Added: and `save-buffer`. The FRACAS tools register as normal MCP tools. They
33 Added: run through the tool dispatch path, which does not use the walker. The
34 Added: read-only tools also carry a `readOnlyHint` annotation. The MCP client
35 Added: can approve a read-only tool without a prompt.
36 Added:
37 Added: Each FRACAS tool takes a `root` argument. Set `root` to the absolute
38 Added: path of the project directory that contains `REQUESTS.org`. The tools
39 Added: operate only on that one file.
40 Added:
41 Added: ### Install the tools once per session
42 Added:
43 Added: Load the tool file one time in the running Emacs before you use the
44 Added: tools. Run this with the Emacs MCP `eval-elisp` tool:
45 Added:
46 Added: ```elisp
47 Added: (load "/home/blendux/git/hito/.kiro/skills/fracas/fracas-tools.el" nil t)
48 Added: ```
49 Added:
50 Added: Confirm the tools are present. This expression returns the six FRACAS
51 Added: tool names:
52 Added:
53 Added: ```elisp
54 Added: (seq-filter (lambda (name) (string-prefix-p "fracas-" name))
55 Added: (mapcar (lambda (tool) (alist-get 'name tool))
56 Added: (mcp-server-tools-list)))
57 Added: ```
58 Added:
59 Added: If the load fails, stop and report the problem. Do not fall back to
60 Added: raw file edits.
61 Added:
62 Added: ### The FRACAS tools
63 Added:
64 Added: - `fracas-inspect` — return the file's TODO keyword sequence and
65 Added: whether a `Dispatch` heading exists. Read-only.
66 Added: - `fracas-capture` — create a TODO request under `Dispatch`. Records
67 Added: the capture time as an inactive `SCHEDULED` timestamp and assigns an
68 Added: Org ID.
69 Added: - `fracas-set-status` — set the TODO keyword of an entry. Rejects
70 Added: `DONE`.
71 Added: - `fracas-complete` — set an entry to `DONE`. Confirms Org inserted
72 Added: the `CLOSED` timestamp. Never writes the timestamp itself.
73 Added: - `fracas-refile` — move a completed entry under a top-level heading.
74 Added: - `fracas-verify` — return the heading, TODO state, `CLOSED`
75 Added: timestamp, and outline path of an entry. Read-only.
76 Added:
77 Added: Use the Emacs MCP `org-*` tools for tasks the FRACAS tools do not
78 Added: cover:
79 Added:
80 Added: - Use `org-get-node` or `org-search` to read headings and content.
81 Added: - Use `org-capture` to create the `Dispatch` heading, or a top-level
82 Added: section, when it does not exist.
83 Added:
84 Added: ## Step 1 — Capture under Dispatch
85 Added:
86 Added: Run `fracas-inspect` first. Read the TODO keyword sequence. Check
87 Added: whether a `Dispatch` heading exists.
88 Added:
89 Added: If `Dispatch` does not exist, create it with the Emacs MCP
90 Added: `org-capture` tool. Add `* Dispatch` as a top-level heading at the top
91 Added: of the file.
92 Added:
93 Added: Run `fracas-capture` with these arguments:
94 Added:
95 Added: - `root` — the project directory.
96 Added: - `title` — a short imperative title under 60 characters.
97 Added: - `body` — the full request text.
98 Added:
99 Added: Keep the request text unchanged. If the text holds separate ideas,
100 Added: format the body as a list.
101 Added:
102 Added: The tool records the capture time as an inactive `SCHEDULED` timestamp
103 Added: and returns the new entry's Org ID and outline path. Report the
104 Added: outline path.
105 Added:
106 Added: ## Step 2 — Track the status
107 Added:
108 Added: Read the current status with `fracas-verify` before you change a
109 Added: keyword. Use one keyword from the sequence that `fracas-inspect`
110 Added: returned.
111 Added:
112 Added: Run `fracas-set-status` for a status change to any open keyword:
113 Added:
114 Added: - `TODO` — Capture the request. Do not start work.
115 Added: - `IN-PROGRESS` — Start work.
116 Added: - `TESTING` — Run tests. The tests do not pass.
117 Added: - `TESTED` — Tests pass. No commit exists.
118 Added: - `BLOCKED` — Work cannot continue. State the cause of the block.
119 Added: - `CANCELLED` — Stop work on purpose.
120 Added:
121 Added: Change the keyword only after you observe the current status. Run
122 Added: `fracas-verify` after the change to confirm the keyword.
123 Added:
124 Added: For `DONE`, use `fracas-complete`, not `fracas-set-status`. Set `DONE`
125 Added: only when all of these are true:
126 Added:
127 Added: - The build passes.
128 Added: - Tests pass.
129 Added: - The files have the required format.
130 Added: - A commit exists.
131 Added:
132 Added: `fracas-complete` sets `DONE`, lets Org's configured logging add the
133 Added: `CLOSED` timestamp, and confirms the timestamp. Do not add or edit the
134 Added: timestamp yourself. Refile the entry next, under Step 3.
135 Added:
136 Added: ## Step 3 — Refile on completion
137 Added:
138 Added: Keep the request under `* Dispatch` while its status is open,
139 Added: `BLOCKED`, or `CANCELLED`. When its status becomes `DONE`, move the
140 Added: entry out of `Dispatch`.
141 Added:
142 Added: Read the top-level headings with `org-search` or
143 Added: `org-get-node`. Select the section that best matches the request. If
144 Added: no section matches, create a short, consistent top-level section with
145 Added: the Emacs MCP `org-capture` tool.
146 Added:
147 Added: Run `fracas-refile` with these arguments:
148 Added:
149 Added: - `root` — the project directory.
150 Added: - `id` — the Org ID of the entry.
151 Added: - `target` — the top-level heading name.
152 Added:
153 Added: Run `fracas-verify` to confirm the new location. Report the new
154 Added: outline path.
155 Added:
156 Added: Record each prompt as its own request. If you cannot complete a
157 Added: request, record the request and the failure.
.kiro/skills/fracas/fracas-tools.el
index 00000000..11a75757 000000..100644
@@ -0,0 +1,314 @@
1 Added: ;;; fracas-tools.el --- FRACAS MCP tools for REQUESTS.org -*- lexical-binding: t; -*-
2 Added:
3 Added: ;; Copyright (C) 2025
4 Added:
5 Added: ;; This file is NOT part of GNU Emacs.
6 Added:
7 Added: ;;; Commentary:
8 Added:
9 Added: ;; FRACAS means "Feature Requests As a Constant Agent-monitored Stream".
10 Added: ;;
11 Added: ;; This file registers dedicated Emacs MCP tools for the FRACAS skill so the
12 Added: ;; agent does not run raw `eval-elisp' snippets for each capture, status
13 Added: ;; change, refile, or verification. Each operation becomes a named tool.
14 Added: ;;
15 Added: ;; Why dedicated tools instead of `eval-elisp'?
16 Added: ;;
17 Added: ;; The `eval-elisp' tool routes its argument through
18 Added: ;; `mcp-server-security-safe-eval', whose form walker blocks or prompts for
19 Added: ;; "dangerous" functions such as `find-file-noselect', `write-file', and
20 Added: ;; `save-buffer'. With `mcp-server-security-prompt-for-permissions' set to t,
21 Added: ;; every FRACAS snippet triggers a minibuffer prompt.
22 Added: ;;
23 Added: ;; A registered MCP tool runs through `mcp-server-tools-call', which calls the
24 Added: ;; handler function directly and does NOT pass through the form walker. The
25 Added: ;; handlers below therefore run without the repeated security prompt. Each
26 Added: ;; tool also carries MCP `annotations' so the MCP client can auto-approve the
27 Added: ;; read-only tools.
28 Added: ;;
29 Added: ;; The tools operate only on the file "REQUESTS.org" under a caller-
30 Added: ;; supplied project ROOT. They never touch any other file.
31 Added: ;;
32 Added: ;; Install (run once per Emacs session):
33 Added: ;;
34 Added: ;; (load "/home/blendux/git/hito/.kiro/skills/fasoc/fracas-tools.el")
35 Added: ;;
36 Added: ;; The tools self-register on load. With `mcp-server-emacs-tools-enabled' set
37 Added: ;; to `all' (the default), they appear in the MCP tool list immediately.
38 Added:
39 Added: ;;; Code:
40 Added:
41 Added: (require 'mcp-server-tools)
42 Added: (require 'org)
43 Added: (require 'org-id)
44 Added: (require 'org-capture)
45 Added: (require 'org-refile)
46 Added: (require 'json)
47 Added:
48 Added: ;;; Helpers
49 Added:
50 Added: (defun fracas-tools--file (args)
51 Added: "Return the absolute REQUESTS.org path for ARGS.
52 Added: ARGS must contain a `root' string naming the project directory.
53 Added: Signal an error when `root' is missing or is not a directory."
54 Added: (let ((root (alist-get 'root args)))
55 Added: (unless (and (stringp root) (> (length root) 0))
56 Added: (error "Missing required `root' (project directory)"))
57 Added: (let ((dir (expand-file-name root)))
58 Added: (unless (file-directory-p dir)
59 Added: (error "Not a directory: %s" dir))
60 Added: (expand-file-name "REQUESTS.org" dir))))
61 Added:
62 Added: (defun fracas-tools--buffer (file)
63 Added: "Return an org-mode buffer visiting FILE, creating it as needed."
64 Added: (let ((buf (find-file-noselect file)))
65 Added: (with-current-buffer buf
66 Added: (unless (derived-mode-p 'org-mode)
67 Added: (org-mode)))
68 Added: buf))
69 Added:
70 Added: (defun fracas-tools--goto-id (id)
71 Added: "Move point to the heading with Org ID in the current buffer.
72 Added: Signal an error when ID is not found."
73 Added: (let ((marker (org-id-find id 'marker)))
74 Added: (unless marker
75 Added: (error "Org ID not found: %s" id))
76 Added: (goto-char marker)))
77 Added:
78 Added: (defmacro fracas-tools--json (&rest body)
79 Added: "Evaluate BODY and return its value as a JSON string.
80 Added: Catch any error and return a JSON object with an `error' field."
81 Added: (declare (indent 0))
82 Added: `(condition-case err
83 Added: (json-encode (progn ,@body))
84 Added: (error (json-encode `((error . ,(error-message-string err)))))))
85 Added:
86 Added: ;;; inspect (read-only)
87 Added:
88 Added: (defun fracas-tools--inspect-handler (args)
89 Added: "Report the TODO sequence and Dispatch presence for REQUESTS.org."
90 Added: (fracas-tools--json
91 Added: (let ((file (fracas-tools--file args)))
92 Added: (with-current-buffer (fracas-tools--buffer file)
93 Added: (org-with-wide-buffer
94 Added: `((file . ,file)
95 Added: (todo_keywords . ,(vconcat org-todo-keywords-1))
96 Added: (dispatch . ,(if (org-find-exact-headline-in-buffer "Dispatch")
97 Added: t :false))))))))
98 Added:
99 Added: (mcp-server-register-tool
100 Added: (make-mcp-server-tool
101 Added: :name "fracas-inspect"
102 Added: :title "FRACAS Inspect"
103 Added: :description "Inspect REQUESTS.org: return its TODO keyword sequence and whether a top-level `Dispatch' heading exists. Read-only."
104 Added: :input-schema '((type . "object")
105 Added: (properties . ((root . ((type . "string")
106 Added: (description . "Absolute path to the project directory containing REQUESTS.org")))))
107 Added: (required . ["root"]))
108 Added: :function #'fracas-tools--inspect-handler
109 Added: :annotations '((readOnlyHint . t)
110 Added: (destructiveHint . :false)
111 Added: (idempotentHint . t)
112 Added: (openWorldHint . :false))))
113 Added:
114 Added: ;;; capture
115 Added:
116 Added: (defun fracas-tools--capture-handler (args)
117 Added: "Capture a TODO entry under `Dispatch' in REQUESTS.org.
118 Added: ARGS keys: `root', `title', `body' (optional). The Dispatch heading must
119 Added: already exist; create it with the org-capture MCP tool first when absent.
120 Added: Record the capture time as an inactive SCHEDULED timestamp."
121 Added: (fracas-tools--json
122 Added: (let* ((file (fracas-tools--file args))
123 Added: (title (or (alist-get 'title args) (error "Missing `title'")))
124 Added: (body (or (alist-get 'body args) ""))
125 Added: (captured-at (format-time-string "[%Y-%m-%d %a %H:%M]"))
126 Added: (org-capture-templates
127 Added: `(("e" "Request" entry
128 Added: (file+headline ,file "Dispatch")
129 Added: "%i" :immediate-finish t))))
130 Added: (with-current-buffer (fracas-tools--buffer file)
131 Added: (unless (org-find-exact-headline-in-buffer "Dispatch")
132 Added: (error "No `Dispatch' heading in %s; create it first" file)))
133 Added: (org-capture-string
134 Added: (format "** TODO %s\nSCHEDULED: %s\n%s\n" title captured-at body)
135 Added: "e")
136 Added: (with-current-buffer (fracas-tools--buffer file)
137 Added: (let ((marker org-capture-last-stored-marker))
138 Added: (goto-char marker)
139 Added: (let ((id (org-id-get-create)))
140 Added: (when (buffer-modified-p) (save-buffer))
141 Added: `((id . ,id)
142 Added: (file . ,file)
143 Added: (heading . ,(org-get-heading t t t t))
144 Added: (outline_path . ,(vconcat (org-get-outline-path t))))))))))
145 Added:
146 Added: (mcp-server-register-tool
147 Added: (make-mcp-server-tool
148 Added: :name "fracas-capture"
149 Added: :title "FRACAS Capture"
150 Added: :description "Capture a TODO feature request under the `Dispatch' heading in REQUESTS.org. Records the capture time as an inactive SCHEDULED timestamp and assigns an Org ID. The `Dispatch' heading must exist first."
151 Added: :input-schema '((type . "object")
152 Added: (properties . ((root . ((type . "string")
153 Added: (description . "Absolute path to the project directory")))
154 Added: (title . ((type . "string")
155 Added: (description . "Imperative title under 60 chars")))
156 Added: (body . ((type . "string")
157 Added: (description . "Full request text, verbatim")))))
158 Added: (required . ["root" "title"]))
159 Added: :function #'fracas-tools--capture-handler
160 Added: :annotations '((readOnlyHint . :false)
161 Added: (destructiveHint . :false)
162 Added: (idempotentHint . :false)
163 Added: (openWorldHint . :false))))
164 Added:
165 Added: ;;; set-status
166 Added:
167 Added: (defun fracas-tools--set-status-handler (args)
168 Added: "Change the TODO keyword of an entry.
169 Added: ARGS keys: `root', `id', `state'. STATE must be one keyword from the
170 Added: file's own #+TODO sequence, and must not be DONE (use fracas-complete)."
171 Added: (fracas-tools--json
172 Added: (let ((file (fracas-tools--file args))
173 Added: (id (or (alist-get 'id args) (error "Missing `id'")))
174 Added: (state (or (alist-get 'state args) (error "Missing `state'"))))
175 Added: (when (string-equal state "DONE")
176 Added: (error "Use fracas-complete for DONE, not fracas-set-status"))
177 Added: (with-current-buffer (fracas-tools--buffer file)
178 Added: (fracas-tools--goto-id id)
179 Added: (org-todo state)
180 Added: (when (buffer-modified-p) (save-buffer))
181 Added: `((id . ,id)
182 Added: (state . ,(org-get-todo-state)))))))
183 Added:
184 Added: (mcp-server-register-tool
185 Added: (make-mcp-server-tool
186 Added: :name "fracas-set-status"
187 Added: :title "FRACAS Set Status"
188 Added: :description "Set the TODO keyword of a FRACAS entry (for example IN-PROGRESS, TESTING, TESTED, BLOCKED, CANCELLED). Use one keyword from the file's own #+TODO sequence. Does not accept DONE; use fracas-complete for that."
189 Added: :input-schema '((type . "object")
190 Added: (properties . ((root . ((type . "string")))
191 Added: (id . ((type . "string")
192 Added: (description . "Org ID of the entry")))
193 Added: (state . ((type . "string")
194 Added: (description . "TODO keyword from the file's #+TODO sequence")))))
195 Added: (required . ["root" "id" "state"]))
196 Added: :function #'fracas-tools--set-status-handler
197 Added: :annotations '((readOnlyHint . :false)
198 Added: (destructiveHint . :false)
199 Added: (idempotentHint . t)
200 Added: (openWorldHint . :false))))
201 Added:
202 Added: ;;; complete
203 Added:
204 Added: (defun fracas-tools--complete-handler (args)
205 Added: "Set an entry to DONE and confirm Org recorded a CLOSED timestamp.
206 Added: ARGS keys: `root', `id'. Requires `org-log-done' to be `time' so the
207 Added: normal Org transition inserts CLOSED; this handler never writes CLOSED."
208 Added: (fracas-tools--json
209 Added: (let ((file (fracas-tools--file args))
210 Added: (id (or (alist-get 'id args) (error "Missing `id'"))))
211 Added: (with-current-buffer (fracas-tools--buffer file)
212 Added: (hack-local-variables)
213 Added: (unless (eq org-log-done 'time)
214 Added: (error "org-log-done is not set to time"))
215 Added: (fracas-tools--goto-id id)
216 Added: (org-todo "DONE")
217 Added: (when (buffer-modified-p) (save-buffer))
218 Added: (let ((closed (org-entry-get nil "CLOSED")))
219 Added: (unless closed
220 Added: (error "Org did not record a CLOSED timestamp"))
221 Added: `((id . ,id)
222 Added: (state . ,(org-get-todo-state))
223 Added: (closed . ,closed)))))))
224 Added:
225 Added: (mcp-server-register-tool
226 Added: (make-mcp-server-tool
227 Added: :name "fracas-complete"
228 Added: :title "FRACAS Complete"
229 Added: :description "Set a FRACAS entry to DONE and confirm Org inserted a CLOSED timestamp. Requires org-log-done set to time. Never writes the timestamp itself. Refile with fracas-refile afterward."
230 Added: :input-schema '((type . "object")
231 Added: (properties . ((root . ((type . "string")))
232 Added: (id . ((type . "string")))))
233 Added: (required . ["root" "id"]))
234 Added: :function #'fracas-tools--complete-handler
235 Added: :annotations '((readOnlyHint . :false)
236 Added: (destructiveHint . :false)
237 Added: (idempotentHint . t)
238 Added: (openWorldHint . :false))))
239 Added:
240 Added: ;;; refile
241 Added:
242 Added: (defun fracas-tools--refile-handler (args)
243 Added: "Refile the entry with `id' under the top-level heading `target'.
244 Added: ARGS keys: `root', `id', `target'. Both entry and target are in
245 Added: REQUESTS.org."
246 Added: (fracas-tools--json
247 Added: (let ((file (fracas-tools--file args))
248 Added: (id (or (alist-get 'id args) (error "Missing `id'")))
249 Added: (target (or (alist-get 'target args) (error "Missing `target'"))))
250 Added: (with-current-buffer (fracas-tools--buffer file)
251 Added: (org-with-wide-buffer
252 Added: (let ((target-position
253 Added: (let ((tm (org-find-exact-headline-in-buffer target)))
254 Added: (unless tm (error "Org target not found: %s" target))
255 Added: (marker-position tm))))
256 Added: (fracas-tools--goto-id id)
257 Added: (org-refile nil nil (list target file nil target-position))
258 Added: (when (buffer-modified-p) (save-buffer))
259 Added: (fracas-tools--goto-id id)
260 Added: `((id . ,id)
261 Added: (outline_path . ,(vconcat (org-get-outline-path t))))))))))
262 Added:
263 Added: (mcp-server-register-tool
264 Added: (make-mcp-server-tool
265 Added: :name "fracas-refile"
266 Added: :title "FRACAS Refile"
267 Added: :description "Move a completed FRACAS entry out of `Dispatch' and under a top-level `target' heading in REQUESTS.org."
268 Added: :input-schema '((type . "object")
269 Added: (properties . ((root . ((type . "string")))
270 Added: (id . ((type . "string")))
271 Added: (target . ((type . "string")
272 Added: (description . "Top-level heading to receive the entry")))))
273 Added: (required . ["root" "id" "target"]))
274 Added: :function #'fracas-tools--refile-handler
275 Added: :annotations '((readOnlyHint . :false)
276 Added: (destructiveHint . :false)
277 Added: (idempotentHint . :false)
278 Added: (openWorldHint . :false))))
279 Added:
280 Added: ;;; verify (read-only)
281 Added:
282 Added: (defun fracas-tools--verify-handler (args)
283 Added: "Return the current state of the entry with `id'.
284 Added: ARGS keys: `root', `id'. Read-only."
285 Added: (fracas-tools--json
286 Added: (let ((file (fracas-tools--file args))
287 Added: (id (or (alist-get 'id args) (error "Missing `id'"))))
288 Added: (with-current-buffer (fracas-tools--buffer file)
289 Added: (org-with-wide-buffer
290 Added: (fracas-tools--goto-id id)
291 Added: `((id . ,id)
292 Added: (heading . ,(org-get-heading t t t t))
293 Added: (state . ,(org-get-todo-state))
294 Added: (closed . ,(or (org-entry-get nil "CLOSED") :null))
295 Added: (outline_path . ,(vconcat (org-get-outline-path t)))))))))
296 Added:
297 Added: (mcp-server-register-tool
298 Added: (make-mcp-server-tool
299 Added: :name "fracas-verify"
300 Added: :title "FRACAS Verify"
301 Added: :description "Return the heading, TODO state, CLOSED timestamp, and outline path of a FRACAS entry. Read-only."
302 Added: :input-schema '((type . "object")
303 Added: (properties . ((root . ((type . "string")))
304 Added: (id . ((type . "string")))))
305 Added: (required . ["root" "id"]))
306 Added: :function #'fracas-tools--verify-handler
307 Added: :annotations '((readOnlyHint . t)
308 Added: (destructiveHint . :false)
309 Added: (idempotentHint . t)
310 Added: (openWorldHint . :false))))
311 Added:
312 Added: (provide 'fracas-tools)
313 Added:
314 Added: ;;; fracas-tools.el ends here