rcf-lite 0.0.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/CHANGELOG.md +288 -0
  2. package/LICENSE +202 -0
  3. package/README.md +92 -4
  4. package/bin/rcf-verify.js +122 -0
  5. package/bin/rcf.js +174 -0
  6. package/bin/view-supervisor-child.mjs +14 -0
  7. package/fixtures/canary-manifest.json +103 -0
  8. package/guidance/README.md +25 -0
  9. package/guidance/build-cycle-playbook.md +492 -0
  10. package/guidance/build-cycle.md +40 -0
  11. package/guidance/document-model.md +45 -0
  12. package/guidance/elicitation-playbook.md +442 -0
  13. package/guidance/harness-template.md +246 -0
  14. package/guidance/managed/README.md +63 -0
  15. package/guidance/managed/agent-instructions-block.hash +1 -0
  16. package/guidance/managed/agent-instructions-block.md +220 -0
  17. package/guidance/managed/legacy-fragment-hashes.json +22 -0
  18. package/guidance/manifest.json +21 -0
  19. package/guidance/overview.md +48 -0
  20. package/guidance/persona-programme.md +44 -0
  21. package/package.json +69 -8
  22. package/rcf/adrs/adr-001.json +25 -0
  23. package/rcf/adrs/adr-002.json +25 -0
  24. package/rcf/adrs/adr-003.json +25 -0
  25. package/rcf/adrs/adr-004.json +20 -0
  26. package/rcf/adrs/adr-005.json +20 -0
  27. package/rcf/adrs/adr-006.json +25 -0
  28. package/rcf/adrs/adr-007.json +25 -0
  29. package/rcf/adrs/adr-008.json +25 -0
  30. package/rcf/adrs/adr-009.json +25 -0
  31. package/rcf/build-sequence.json +11 -0
  32. package/rcf/code-nodes/cn-001.json +14 -0
  33. package/rcf/code-nodes/cn-002.json +14 -0
  34. package/rcf/code-nodes/cn-003.json +15 -0
  35. package/rcf/code-nodes/cn-004.json +17 -0
  36. package/rcf/code-nodes/cn-005.json +19 -0
  37. package/rcf/code-nodes/cn-006.json +17 -0
  38. package/rcf/code-nodes/cn-007.json +16 -0
  39. package/rcf/code-nodes/cn-008.json +19 -0
  40. package/rcf/code-nodes/cn-009.json +16 -0
  41. package/rcf/code-nodes/cn-010.json +18 -0
  42. package/rcf/code-nodes/cn-011.json +18 -0
  43. package/rcf/code-nodes/cn-012.json +18 -0
  44. package/rcf/code-nodes/cn-013.json +19 -0
  45. package/rcf/code-nodes/cn-014.json +16 -0
  46. package/rcf/code-nodes/cn-015.json +16 -0
  47. package/rcf/code-nodes/cn-016.json +16 -0
  48. package/rcf/code-nodes/cn-017.json +16 -0
  49. package/rcf/code-nodes/cn-018.json +15 -0
  50. package/rcf/code-nodes/cn-019.json +16 -0
  51. package/rcf/code-nodes/cn-020.json +16 -0
  52. package/rcf/code-nodes/cn-021.json +16 -0
  53. package/rcf/code-nodes/cn-022.json +16 -0
  54. package/rcf/code-nodes/cn-023.json +16 -0
  55. package/rcf/code-nodes/cn-024.json +16 -0
  56. package/rcf/code-nodes/cn-025.json +16 -0
  57. package/rcf/code-nodes/cn-026.json +16 -0
  58. package/rcf/code-nodes/cn-027.json +16 -0
  59. package/rcf/code-nodes/cn-028.json +16 -0
  60. package/rcf/code-nodes/cn-029.json +19 -0
  61. package/rcf/code-nodes/cn-030.json +14 -0
  62. package/rcf/code-nodes/cn-031.json +14 -0
  63. package/rcf/code-nodes/cn-032.json +14 -0
  64. package/rcf/code-nodes/cn-033.json +14 -0
  65. package/rcf/code-nodes/cn-034.json +14 -0
  66. package/rcf/code-nodes/cn-035.json +14 -0
  67. package/rcf/code-nodes/cn-036.json +14 -0
  68. package/rcf/code-nodes/cn-037.json +14 -0
  69. package/rcf/code-nodes/cn-038.json +14 -0
  70. package/rcf/code-nodes/cn-039.json +14 -0
  71. package/rcf/code-nodes/cn-040.json +14 -0
  72. package/rcf/code-nodes/cn-041.json +14 -0
  73. package/rcf/code-nodes/cn-042.json +14 -0
  74. package/rcf/code-nodes/cn-043.json +14 -0
  75. package/rcf/code-nodes/cn-044.json +14 -0
  76. package/rcf/code-nodes/cn-045.json +14 -0
  77. package/rcf/code-nodes/cn-046.json +14 -0
  78. package/rcf/code-nodes/cn-047.json +14 -0
  79. package/rcf/code-nodes/cn-048.json +14 -0
  80. package/rcf/code-nodes/cn-049.json +14 -0
  81. package/rcf/code-nodes/cn-050.json +14 -0
  82. package/rcf/code-nodes/cn-051.json +14 -0
  83. package/rcf/code-nodes/cn-052.json +14 -0
  84. package/rcf/code-nodes/cn-053.json +14 -0
  85. package/rcf/code-nodes/cn-054.json +14 -0
  86. package/rcf/code-nodes/cn-055.json +14 -0
  87. package/rcf/code-nodes/cn-056.json +14 -0
  88. package/rcf/code-nodes/cn-057.json +14 -0
  89. package/rcf/fbs/fbs-001.json +49 -0
  90. package/rcf/fbs/fbs-002.json +42 -0
  91. package/rcf/fbs/fbs-003.json +37 -0
  92. package/rcf/fbs/fbs-004.json +39 -0
  93. package/rcf/fbs/fbs-005.json +38 -0
  94. package/rcf/fbs/fbs-006.json +48 -0
  95. package/rcf/fbs/fbs-007.json +39 -0
  96. package/rcf/fbs/fbs-008.json +40 -0
  97. package/rcf/fbs/fbs-009.json +36 -0
  98. package/rcf/fbs/fbs-010.json +41 -0
  99. package/rcf/fbs/fbs-011.json +36 -0
  100. package/rcf/fbs/fbs-012.json +46 -0
  101. package/rcf/fbs/fbs-013.json +42 -0
  102. package/rcf/fbs/fbs-014.json +49 -0
  103. package/rcf/fbs/fbs-015.json +32 -0
  104. package/rcf/manifest.json +17 -0
  105. package/rcf/prd.json +47 -0
  106. package/rcf/requirements/req-001.json +19 -0
  107. package/rcf/requirements/req-002.json +19 -0
  108. package/rcf/requirements/req-003.json +19 -0
  109. package/rcf/requirements/req-004.json +19 -0
  110. package/rcf/requirements/req-005.json +19 -0
  111. package/rcf/requirements/req-006.json +19 -0
  112. package/rcf/requirements/req-007.json +19 -0
  113. package/rcf/requirements/req-008.json +19 -0
  114. package/rcf/requirements/req-009.json +19 -0
  115. package/rcf/tacs/tac-001.json +45 -0
  116. package/rcf/tacs/tac-002.json +109 -0
  117. package/rcf/tacs/tac-003.json +40 -0
  118. package/rcf/tacs/tac-004.json +51 -0
  119. package/rcf/tacs/tac-005.json +52 -0
  120. package/rcf/tacs/tac-006.json +104 -0
  121. package/rcf/tacs/tac-007.json +38 -0
  122. package/rcf/tacs/tac-008.json +51 -0
  123. package/rcf/tad.json +51 -0
  124. package/rcf/test-suites/PENDING.md +23 -0
  125. package/rcf/test-suites/ts-001.json +38 -0
  126. package/rcf/test-suites/ts-002.json +38 -0
  127. package/rcf/test-suites/ts-003.json +43 -0
  128. package/rcf/test-suites/ts-004.json +44 -0
  129. package/rcf/test-suites/ts-005.json +30 -0
  130. package/rcf/test-suites/ts-006.json +36 -0
  131. package/rcf/test-suites/ts-007.json +43 -0
  132. package/rcf/test-suites/ts-008.json +37 -0
  133. package/rcf/test-suites/ts-009.json +38 -0
  134. package/rcf/test-suites/ts-010.json +38 -0
  135. package/rcf/test-suites/ts-011.json +44 -0
  136. package/rcf/test-suites/ts-012.json +36 -0
  137. package/rcf/test-suites/ts-013.json +38 -0
  138. package/rcf/test-suites/ts-014.json +38 -0
  139. package/rcf/test-suites/ts-015.json +38 -0
  140. package/rcf/test-suites/ts-016.json +37 -0
  141. package/rcf/test-suites/ts-017.json +38 -0
  142. package/rcf/test-suites/ts-018.json +38 -0
  143. package/rcf/test-suites/ts-019.json +37 -0
  144. package/rcf/test-suites/ts-020.json +46 -0
  145. package/rcf/test-suites/ts-021.json +46 -0
  146. package/rcf/test-suites/ts-022.json +46 -0
  147. package/rcf/test-suites/ts-023.json +46 -0
  148. package/rcf/test-suites/ts-024.json +46 -0
  149. package/rcf/test-suites/ts-025.json +52 -0
  150. package/rcf/user-stories/us-101.json +40 -0
  151. package/rcf/user-stories/us-102.json +40 -0
  152. package/rcf/user-stories/us-201.json +40 -0
  153. package/rcf/user-stories/us-202.json +40 -0
  154. package/rcf/user-stories/us-203.json +32 -0
  155. package/rcf/user-stories/us-301.json +40 -0
  156. package/rcf/user-stories/us-302.json +40 -0
  157. package/rcf/user-stories/us-303.json +40 -0
  158. package/rcf/user-stories/us-304.json +40 -0
  159. package/rcf/user-stories/us-401.json +40 -0
  160. package/rcf/user-stories/us-402.json +40 -0
  161. package/rcf/user-stories/us-403.json +40 -0
  162. package/rcf/user-stories/us-501.json +40 -0
  163. package/rcf/user-stories/us-502.json +40 -0
  164. package/rcf/user-stories/us-503.json +40 -0
  165. package/rcf/user-stories/us-601.json +40 -0
  166. package/rcf/user-stories/us-602.json +40 -0
  167. package/rcf/user-stories/us-701.json +40 -0
  168. package/rcf/user-stories/us-702.json +40 -0
  169. package/rcf/user-stories/us-801.json +49 -0
  170. package/rcf/user-stories/us-802.json +49 -0
  171. package/rcf/user-stories/us-803.json +49 -0
  172. package/rcf/user-stories/us-804.json +49 -0
  173. package/rcf/user-stories/us-805.json +49 -0
  174. package/rcf/user-stories/us-901.json +40 -0
  175. package/src/.gitkeep +0 -0
  176. package/src/browser-verify/auth-smoke.js +109 -0
  177. package/src/browser-verify/index.js +29 -0
  178. package/src/browser-verify/invariants.js +336 -0
  179. package/src/browser-verify/manifest-writer.js +189 -0
  180. package/src/browser-verify/runner.js +171 -0
  181. package/src/build/bundle.js +198 -0
  182. package/src/build/formatters/json.js +18 -0
  183. package/src/build/formatters/markdown.js +406 -0
  184. package/src/build/index.js +14 -0
  185. package/src/build/mark.js +177 -0
  186. package/src/build/queue.js +285 -0
  187. package/src/cli/browser-verify.js +231 -0
  188. package/src/cli/build.js +584 -0
  189. package/src/cli/coverage.js +219 -0
  190. package/src/cli/create.js +355 -0
  191. package/src/cli/delete.js +127 -0
  192. package/src/cli/design.js +353 -0
  193. package/src/cli/doctor.js +548 -0
  194. package/src/cli/fbs.js +171 -0
  195. package/src/cli/finalise.js +377 -0
  196. package/src/cli/guidance.js +206 -0
  197. package/src/cli/help.js +156 -0
  198. package/src/cli/impact.js +119 -0
  199. package/src/cli/init.js +282 -0
  200. package/src/cli/intake.js +153 -0
  201. package/src/cli/link.js +128 -0
  202. package/src/cli/mcp.js +160 -0
  203. package/src/cli/preflight.js +220 -0
  204. package/src/cli/read.js +162 -0
  205. package/src/cli/req-baseline.js +269 -0
  206. package/src/cli/req-classify.js +135 -0
  207. package/src/cli/review.js +295 -0
  208. package/src/cli/test-suite.js +216 -0
  209. package/src/cli/trace.js +172 -0
  210. package/src/cli/ui-baseline.js +292 -0
  211. package/src/cli/ui-classify.js +108 -0
  212. package/src/cli/update.js +197 -0
  213. package/src/cli/validate.js +168 -0
  214. package/src/cli/view.js +452 -0
  215. package/src/core/baseline-catalog/data/auth.json +42 -0
  216. package/src/core/baseline-catalog/data/http-api.json +42 -0
  217. package/src/core/baseline-catalog/data/notifications.json +33 -0
  218. package/src/core/baseline-catalog/data/persistence.json +33 -0
  219. package/src/core/baseline-catalog/data/web-ui.json +60 -0
  220. package/src/core/baseline-catalog/index.js +121 -0
  221. package/src/core/errors/index.js +167 -0
  222. package/src/core/fixtures/register-canary/canary-prompt-01.json +18 -0
  223. package/src/core/fixtures/register-canary/canary-prompt-02.json +21 -0
  224. package/src/core/fixtures/register-canary/canary-prompt-03.json +17 -0
  225. package/src/core/isolation/index.js +60 -0
  226. package/src/core/mcp/framing.js +103 -0
  227. package/src/core/mcp/index.js +8 -0
  228. package/src/core/mcp/server.js +228 -0
  229. package/src/core/patterns/register-canary.js +209 -0
  230. package/src/core/patterns/req-shapes.js +158 -0
  231. package/src/core/patterns/services.js +358 -0
  232. package/src/core/patterns/ui-shapes.js +166 -0
  233. package/src/core/store/cn-resolve.js +134 -0
  234. package/src/core/store/derive-deps.js +93 -0
  235. package/src/core/store/ids.js +78 -0
  236. package/src/core/store/index.js +20 -0
  237. package/src/core/store/init.js +255 -0
  238. package/src/core/store/loader.js +211 -0
  239. package/src/core/store/tp-resolve.js +176 -0
  240. package/src/core/store/validator.js +191 -0
  241. package/src/core/store/walker.js +898 -0
  242. package/src/core/store/writer.js +1849 -0
  243. package/src/design/index.js +11 -0
  244. package/src/design/writer.js +271 -0
  245. package/src/finalise/detect.js +129 -0
  246. package/src/finalise/index.js +17 -0
  247. package/src/finalise/ingest.js +119 -0
  248. package/src/finalise/install.js +119 -0
  249. package/src/finalise/ship-without-verified.js +131 -0
  250. package/src/finalise/spawn.js +84 -0
  251. package/src/intake/fidelity.js +105 -0
  252. package/src/intake/index.js +6 -0
  253. package/src/intake/manifest-writer.js +100 -0
  254. package/src/intake/orchestrator.js +138 -0
  255. package/src/intake/validate.js +80 -0
  256. package/src/mcp/map-errors.js +131 -0
  257. package/src/mcp/prompts.js +49 -0
  258. package/src/mcp/resources.js +244 -0
  259. package/src/mcp/tools.js +1204 -0
  260. package/src/preflight/design-shapes.js +185 -0
  261. package/src/preflight/index.js +90 -0
  262. package/src/preflight/manifest-writer.js +264 -0
  263. package/src/preflight/scanner.js +206 -0
  264. package/src/preflight/secrets.js +134 -0
  265. package/src/preflight/session.js +246 -0
  266. package/src/query/attestation.js +285 -0
  267. package/src/query/coverage.js +308 -0
  268. package/src/query/formatters/json.js +21 -0
  269. package/src/query/formatters/mermaid.js +209 -0
  270. package/src/query/formatters/table.js +203 -0
  271. package/src/query/impact.js +173 -0
  272. package/src/query/index.js +9 -0
  273. package/src/query/trace.js +345 -0
  274. package/src/register-canary/fixture-loader.js +87 -0
  275. package/src/register-canary/index.js +10 -0
  276. package/src/register-canary/record-writer.js +132 -0
  277. package/src/register-canary/runner.js +156 -0
  278. package/src/req-baseline/gate.js +86 -0
  279. package/src/req-baseline/index.js +27 -0
  280. package/src/req-baseline/open-candidates.js +143 -0
  281. package/src/req-baseline/opt-out.js +195 -0
  282. package/src/req-baseline/sweep.js +230 -0
  283. package/src/req-detection/classifier.js +181 -0
  284. package/src/req-detection/index.js +9 -0
  285. package/src/req-detection/persist.js +55 -0
  286. package/src/review/index.js +325 -0
  287. package/src/review/mutation.js +117 -0
  288. package/src/review/ui-baseline-drift.js +138 -0
  289. package/src/server/index.js +178 -0
  290. package/src/server/routes.js +110 -0
  291. package/src/server/sse.js +118 -0
  292. package/src/setup/agent-setup.js +362 -0
  293. package/src/setup/identity-seed.js +104 -0
  294. package/src/setup/knowledge-seed.js +123 -0
  295. package/src/setup/managed-block.js +193 -0
  296. package/src/setup/managed-gitignore.js +166 -0
  297. package/src/setup/managed-markers.js +49 -0
  298. package/src/ui-baseline/defaults.js +119 -0
  299. package/src/ui-baseline/index.js +25 -0
  300. package/src/ui-baseline/manifest-writer.js +282 -0
  301. package/src/ui-baseline/session.js +178 -0
  302. package/src/ui-detection/classifier.js +192 -0
  303. package/src/verify/chain/index.js +190 -0
  304. package/src/verify/cli/cleanup.js +61 -0
  305. package/src/verify/cli/help.js +56 -0
  306. package/src/verify/cli/mcp.js +98 -0
  307. package/src/verify/cli/provision.js +71 -0
  308. package/src/verify/cli/report.js +71 -0
  309. package/src/verify/cli/run.js +155 -0
  310. package/src/verify/engine/brief.js +87 -0
  311. package/src/verify/engine/index.js +177 -0
  312. package/src/verify/engine/launcher.js +307 -0
  313. package/src/verify/mcp/tools.js +107 -0
  314. package/src/verify/profile/index.js +146 -0
  315. package/src/verify/provision/index.js +256 -0
  316. package/src/verify/report/index.js +139 -0
  317. package/src/verify/report/renderer.js +118 -0
  318. package/src/verify/verdict/index.js +246 -0
  319. package/src/view/doc-renderers/adr.js +44 -0
  320. package/src/view/doc-renderers/build-sequence.js +40 -0
  321. package/src/view/doc-renderers/fbs.js +128 -0
  322. package/src/view/doc-renderers/helpers.js +159 -0
  323. package/src/view/doc-renderers/index.js +12 -0
  324. package/src/view/doc-renderers/prd.js +45 -0
  325. package/src/view/doc-renderers/req.js +43 -0
  326. package/src/view/doc-renderers/tac.js +38 -0
  327. package/src/view/doc-renderers/tad.js +74 -0
  328. package/src/view/doc-renderers/test-suite.js +45 -0
  329. package/src/view/doc-renderers/user-story.js +63 -0
  330. package/src/view/html-page.js +462 -0
  331. package/src/view/index.js +63 -0
  332. package/src/view/live-client.js +338 -0
  333. package/src/view/mermaid-diagram.js +178 -0
  334. package/src/view/style.css +735 -0
  335. package/src/view/tree-model.js +152 -0
  336. package/src/view/vendored/mermaid.min.js +2607 -0
  337. package/src/view-supervisor/index.js +26 -0
  338. package/src/view-supervisor/logs.js +32 -0
  339. package/src/view-supervisor/manifest-writer.js +178 -0
  340. package/src/view-supervisor/persist-until.js +85 -0
  341. package/src/view-supervisor/supervisor.js +276 -0
  342. package/src/watch/index.js +152 -0
@@ -0,0 +1,1204 @@
1
+ // The MCP tool registry (Phase 7 §D5-D9, §D17). Eleven tools:
2
+ // definitions (name, title, description, inputSchema, outputSchema,
3
+ // annotations) plus handlers binding straight to the pure layers -
4
+ // src/query/*, src/store/* and src/build/* - in-process, never
5
+ // spawning the CLI (D12). Every handler re-walks the tree fresh (D14).
6
+ //
7
+ // Envelope discipline (D8/D9): the query and validate tools return the
8
+ // shipped Phase 5 envelopes verbatim as structuredContent (the same
9
+ // objects `--format json` serialises); rcf_build returns the as-built
10
+ // Phase 6 D14 json envelope verbatim; the read / write envelopes are
11
+ // defined by the Phase 7 spec and stable-by-convention from here on.
12
+ //
13
+ // Input schemas are JSON Schema 2020-12, camelCase, closed objects
14
+ // (D7). Argument validation happens here, before dispatch, and maps to
15
+ // tool execution errors (isError: true) per D10 - the self-correction
16
+ // channel, not a protocol error.
17
+
18
+ import { JsonRpcError, INVALID_PARAMS } from '#core/mcp-shell';
19
+ import {
20
+ errorResult,
21
+ issuesFromErrors,
22
+ unexpectedFailureResult,
23
+ usageErrorResult,
24
+ walkerBlockedResult,
25
+ writerErrorResult,
26
+ } from './map-errors.js';
27
+ import { isRcfError } from '#core/errors';
28
+ import {
29
+ checkCodeNodeResolution,
30
+ createDocument,
31
+ deleteDocument,
32
+ deriveSlug,
33
+ resolveTestPointers,
34
+ updateDocument,
35
+ walkTree,
36
+ } from '#core/store';
37
+ import {
38
+ classifyCoverageScope,
39
+ computeCoverage,
40
+ computeImpact,
41
+ computeTrace,
42
+ kindOf,
43
+ } from '../query/index.js';
44
+ import { assembleBundle } from '../build/index.js';
45
+ import { hasAgentMarker, SETUP_FUNNEL_INSTRUCTION } from '../setup/agent-setup.js';
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Shared output-schema fragments
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const ISSUE_SCHEMA = {
52
+ type: 'object',
53
+ properties: {
54
+ id: { type: ['string', 'null'] },
55
+ kind: { type: 'string' },
56
+ rule: { type: ['string', 'null'] },
57
+ filePath: { type: ['string', 'null'] },
58
+ field: { type: ['string', 'null'] },
59
+ message: { type: 'string' },
60
+ },
61
+ required: ['id', 'kind', 'rule', 'filePath', 'field', 'message'],
62
+ };
63
+
64
+ // D11: every isError result carries {ok: false, errors[]} as its
65
+ // structuredContent. The 2025-11-25 tools spec says structured results
66
+ // MUST conform to a declared outputSchema (and the official SDK client
67
+ // enforces it on error results too), so every tool's outputSchema is
68
+ // the union of its success envelope and this error payload - the
69
+ // success branch stays the verbatim envelope transcription (D8/D9).
70
+ const ERROR_PAYLOAD_SCHEMA = {
71
+ type: 'object',
72
+ description: 'Tool execution error payload (accompanies isError: true): the same issue shape rcf validate --json ships',
73
+ properties: {
74
+ ok: { const: false },
75
+ errors: { type: 'array', items: ISSUE_SCHEMA },
76
+ },
77
+ required: ['ok', 'errors'],
78
+ };
79
+
80
+ /**
81
+ * Wrap a success envelope schema as the declared outputSchema: the
82
+ * envelope verbatim, or the D11 error payload.
83
+ *
84
+ * @param {object} successSchema
85
+ * @returns {object}
86
+ */
87
+ function withErrorPayload(successSchema) {
88
+ return { type: 'object', anyOf: [successSchema, ERROR_PAYLOAD_SCHEMA] };
89
+ }
90
+
91
+ const TRACE_NODE_SCHEMA = {
92
+ type: 'object',
93
+ properties: {
94
+ id: { type: 'string' },
95
+ kind: { type: 'string' },
96
+ depth: { type: 'integer', description: '0 for the pivot; positive for descendants; negative for ancestors' },
97
+ },
98
+ required: ['id', 'kind', 'depth'],
99
+ };
100
+
101
+ const TRACE_EDGE_SCHEMA = {
102
+ type: 'object',
103
+ properties: {
104
+ from: { type: 'string' },
105
+ to: { type: 'string' },
106
+ kind: { type: 'string', enum: ['parentChild', 'crossLink'] },
107
+ },
108
+ required: ['from', 'to', 'kind'],
109
+ };
110
+
111
+ const VALIDATE_OUTPUT_SCHEMA = {
112
+ type: 'object',
113
+ properties: {
114
+ ok: { type: 'boolean' },
115
+ issues: { type: 'array', items: ISSUE_SCHEMA },
116
+ },
117
+ required: ['ok', 'issues'],
118
+ };
119
+
120
+ const COVERAGE_OUTPUT_SCHEMA = {
121
+ type: 'object',
122
+ properties: {
123
+ ok: { type: 'boolean', description: 'true when every requirement in scope is covered' },
124
+ strict: { type: 'boolean', description: 'the strict flag echoed in the envelope' },
125
+ totals: {
126
+ type: 'object',
127
+ properties: {
128
+ requirements: { type: 'integer' },
129
+ covered: { type: 'integer' },
130
+ uncovered: { type: 'integer' },
131
+ },
132
+ required: ['requirements', 'covered', 'uncovered'],
133
+ },
134
+ requirements: {
135
+ type: 'array',
136
+ items: {
137
+ type: 'object',
138
+ properties: {
139
+ id: { type: 'string' },
140
+ covered: { type: 'boolean' },
141
+ acs: {
142
+ type: 'array',
143
+ items: {
144
+ type: 'object',
145
+ properties: {
146
+ id: { type: 'string' },
147
+ covered: { type: 'boolean' },
148
+ testCases: { type: 'array', items: { type: 'string' } },
149
+ cnIds: { type: 'array', items: { type: 'string' }, description: 'present when withCode is true (Phase 10)' },
150
+ codeClass: {
151
+ type: 'string',
152
+ enum: ['implemented-and-covered', 'implemented-uncovered', 'unimplemented'],
153
+ description: 'present when withCode is true (Phase 10, D11)',
154
+ },
155
+ },
156
+ required: ['id', 'covered', 'testCases'],
157
+ },
158
+ },
159
+ },
160
+ required: ['id', 'covered', 'acs'],
161
+ },
162
+ },
163
+ withCode: { type: 'boolean', description: 'Phase 10: echoes the withCode flag' },
164
+ codeNodeOrphans: { type: 'array', items: { type: 'string' }, description: 'Phase 10: CN ids with empty implementsAcIds; present when withCode is true' },
165
+ codeTotals: {
166
+ type: 'object',
167
+ description: 'Phase 10: present when withCode is true',
168
+ properties: {
169
+ implementedAndCovered: { type: 'integer' },
170
+ implementedUncovered: { type: 'integer' },
171
+ unimplemented: { type: 'integer' },
172
+ },
173
+ },
174
+ },
175
+ required: ['ok', 'strict', 'totals', 'requirements'],
176
+ };
177
+
178
+ const TRACE_OUTPUT_SCHEMA = {
179
+ type: 'object',
180
+ properties: {
181
+ pivot: { type: 'string' },
182
+ direction: { type: 'string', enum: ['forward', 'back', 'both'] },
183
+ found: { type: 'boolean' },
184
+ nodes: { type: 'array', items: TRACE_NODE_SCHEMA, description: 'present for direction forward | back' },
185
+ edges: { type: 'array', items: TRACE_EDGE_SCHEMA, description: 'present for direction forward | back' },
186
+ ancestors: { type: 'array', items: TRACE_NODE_SCHEMA, description: 'present for direction both; excludes the pivot' },
187
+ descendants: { type: 'array', items: TRACE_NODE_SCHEMA, description: 'present for direction both; excludes the pivot' },
188
+ matches: {
189
+ type: 'array',
190
+ description: 'Phase 10: present instead of pivot/nodes/edges when `id` resolved as a source path matching more than one Code Node',
191
+ items: {
192
+ type: 'object',
193
+ properties: {
194
+ cnId: { type: 'string' },
195
+ path: { type: 'string' },
196
+ nodes: { type: 'array', items: TRACE_NODE_SCHEMA },
197
+ edges: { type: 'array', items: TRACE_EDGE_SCHEMA },
198
+ },
199
+ },
200
+ },
201
+ },
202
+ required: ['pivot', 'direction', 'found'],
203
+ };
204
+
205
+ const IMPACT_OUTPUT_SCHEMA = {
206
+ type: 'object',
207
+ properties: {
208
+ pivot: { type: 'string' },
209
+ found: { type: 'boolean' },
210
+ nodes: {
211
+ type: 'array',
212
+ items: {
213
+ type: 'object',
214
+ properties: {
215
+ id: { type: 'string' },
216
+ kind: { type: 'string' },
217
+ role: { type: 'string', enum: ['pivot', 'ancestor', 'descendant'] },
218
+ actionNeeded: { type: ['string', 'null'] },
219
+ },
220
+ required: ['id', 'kind', 'role', 'actionNeeded'],
221
+ },
222
+ },
223
+ edges: { type: 'array', items: TRACE_EDGE_SCHEMA },
224
+ },
225
+ required: ['pivot', 'found'],
226
+ };
227
+
228
+ const READ_OUTPUT_SCHEMA = {
229
+ type: 'object',
230
+ properties: {
231
+ id: { type: 'string' },
232
+ field: { type: ['string', 'null'], description: 'null when the whole body was requested' },
233
+ value: { description: 'the document body or the extracted field; any JSON type' },
234
+ },
235
+ required: ['id', 'field', 'value'],
236
+ };
237
+
238
+ const CREATE_OUTPUT_SCHEMA = {
239
+ type: 'object',
240
+ properties: {
241
+ ok: { type: 'boolean' },
242
+ id: { type: 'string' },
243
+ kind: { type: 'string' },
244
+ filePath: { type: 'string' },
245
+ dryRun: { type: 'boolean', description: 'present and true when nothing was written' },
246
+ },
247
+ required: ['ok', 'id', 'kind', 'filePath'],
248
+ };
249
+
250
+ const UPDATE_OUTPUT_SCHEMA = {
251
+ type: 'object',
252
+ properties: {
253
+ ok: { type: 'boolean' },
254
+ id: { type: 'string' },
255
+ changedPaths: { type: 'array', items: { type: 'string' }, description: 'the dot-paths and top-level patch keys applied' },
256
+ filePath: { type: 'string' },
257
+ dryRun: { type: 'boolean', description: 'present and true when nothing was written' },
258
+ },
259
+ required: ['ok', 'id', 'changedPaths'],
260
+ };
261
+
262
+ const DELETE_OUTPUT_SCHEMA = {
263
+ type: 'object',
264
+ properties: {
265
+ ok: { type: 'boolean' },
266
+ deleted: { type: 'array', items: { type: 'string' }, description: 'removed document ids (singular without cascade)' },
267
+ mutated: {
268
+ type: 'array',
269
+ items: {
270
+ type: 'object',
271
+ properties: { id: { type: 'string' }, filePath: { type: 'string' } },
272
+ required: ['id', 'filePath'],
273
+ },
274
+ description: 'documents edited to drop backrefs',
275
+ },
276
+ plan: { type: 'array', items: { type: 'string' } },
277
+ dryRun: { type: 'boolean', description: 'present and true when the plan was not executed' },
278
+ },
279
+ required: ['ok', 'deleted'],
280
+ };
281
+
282
+ const LINK_OUTPUT_SCHEMA = {
283
+ type: 'object',
284
+ properties: {
285
+ ok: { type: 'boolean' },
286
+ usId: { type: 'string' },
287
+ tacIds: { type: 'array', items: { type: 'string' }, description: 'the post-state of the user story tacIds' },
288
+ dryRun: { type: 'boolean', description: 'present and true when nothing was written' },
289
+ },
290
+ required: ['ok', 'usId', 'tacIds'],
291
+ };
292
+
293
+ // Transcribes the AS-BUILT Phase 6 D14 json envelope verbatim
294
+ // (src/build/formatters/json.js over src/build/bundle.js), including
295
+ // the additive bs / prd identity blocks. Ids are distributed across
296
+ // the sectioned envelope - there is deliberately no flat included-ids
297
+ // field (reconciliation 2026-07-06, carry 3).
298
+ const BUILD_OUTPUT_SCHEMA = {
299
+ type: 'object',
300
+ properties: {
301
+ ok: { type: 'boolean' },
302
+ mode: { type: 'string', enum: ['bundle'] },
303
+ fbs: {
304
+ type: 'object',
305
+ properties: {
306
+ fbsId: { type: 'string' },
307
+ title: { type: 'string' },
308
+ buildOrder: { type: 'integer' },
309
+ executionStatus: { type: 'string' },
310
+ summary: { type: 'string' },
311
+ approach: { type: 'string' },
312
+ deliverables: { type: 'array' },
313
+ notes: { type: 'string' },
314
+ estimatedSize: { type: 'string' },
315
+ estimatedHours: { type: 'number' },
316
+ riskLevel: { type: 'string' },
317
+ domain: { type: 'string' },
318
+ updatedAt: { type: 'string' },
319
+ },
320
+ required: ['fbsId'],
321
+ },
322
+ queue: {
323
+ type: 'object',
324
+ properties: { position: { type: 'integer' }, total: { type: 'integer' } },
325
+ required: ['position', 'total'],
326
+ },
327
+ bs: {
328
+ type: ['object', 'null'],
329
+ properties: {
330
+ bsId: { type: 'string' },
331
+ title: { type: 'string' },
332
+ buildPhilosophy: { type: 'string' },
333
+ generationStrategy: { type: 'string' },
334
+ },
335
+ },
336
+ prd: {
337
+ type: ['object', 'null'],
338
+ properties: { prdId: { type: 'string' }, productName: { type: 'string' } },
339
+ },
340
+ blockedBy: { type: 'array', items: { type: 'string' }, description: 'unsatisfied dependency FBS ids; a non-empty list means the item is blocked (data, not an error)' },
341
+ dependencies: {
342
+ type: 'array',
343
+ items: {
344
+ type: 'object',
345
+ properties: {
346
+ fbsId: { type: 'string' },
347
+ title: { type: 'string' },
348
+ executionStatus: { type: 'string' },
349
+ },
350
+ required: ['fbsId'],
351
+ },
352
+ },
353
+ dependents: { type: 'array', items: { type: 'string' } },
354
+ acceptanceCriteria: {
355
+ type: 'array',
356
+ items: {
357
+ type: 'object',
358
+ properties: {
359
+ id: { type: 'string' },
360
+ description: { type: 'string' },
361
+ given: { type: 'string' },
362
+ when: { type: 'string' },
363
+ then: { type: 'string' },
364
+ testable: { type: 'boolean' },
365
+ usId: { type: 'string' },
366
+ reqId: { type: 'string' },
367
+ },
368
+ required: ['id', 'usId', 'reqId'],
369
+ },
370
+ },
371
+ userStories: {
372
+ type: 'array',
373
+ items: {
374
+ type: 'object',
375
+ properties: {
376
+ usId: { type: 'string' },
377
+ title: { type: 'string' },
378
+ asA: { type: 'string' },
379
+ iWant: { type: 'string' },
380
+ soThat: { type: 'string' },
381
+ status: { type: 'string' },
382
+ },
383
+ required: ['usId'],
384
+ },
385
+ },
386
+ requirements: {
387
+ type: 'array',
388
+ items: {
389
+ type: 'object',
390
+ properties: {
391
+ reqId: { type: 'string' },
392
+ title: { type: 'string' },
393
+ description: { type: 'string' },
394
+ category: { type: 'string' },
395
+ priority: { type: 'string' },
396
+ rationale: { type: 'string' },
397
+ },
398
+ required: ['reqId'],
399
+ },
400
+ },
401
+ context: {
402
+ type: 'object',
403
+ description: 'architectural context; omitted entirely when the FBS has no contextRequirements',
404
+ properties: {
405
+ tacs: { type: 'array' },
406
+ adrs: { type: 'array' },
407
+ tadSections: { type: 'object' },
408
+ prdSections: { type: 'object' },
409
+ unresolvedSections: { type: 'array', items: { type: 'string' } },
410
+ passThrough: {
411
+ type: 'object',
412
+ properties: {
413
+ existingModules: { type: 'array' },
414
+ schemas: { type: 'array' },
415
+ externalDocs: { type: 'array' },
416
+ other: { type: 'array' },
417
+ },
418
+ },
419
+ },
420
+ },
421
+ tests: {
422
+ type: 'array',
423
+ items: {
424
+ type: 'object',
425
+ properties: {
426
+ acId: { type: 'string' },
427
+ covered: { type: 'boolean' },
428
+ suites: { type: 'array', items: { type: 'string' } },
429
+ cases: { type: 'array' },
430
+ },
431
+ required: ['acId', 'covered', 'suites', 'cases'],
432
+ },
433
+ },
434
+ completionContract: {
435
+ type: 'object',
436
+ properties: {
437
+ markInProgress: { type: 'string' },
438
+ markComplete: { type: 'string' },
439
+ finalise: { type: 'string' },
440
+ },
441
+ required: ['markInProgress', 'markComplete', 'finalise'],
442
+ },
443
+ },
444
+ required: [
445
+ 'ok', 'mode', 'fbs', 'queue', 'bs', 'prd', 'blockedBy', 'dependencies',
446
+ 'dependents', 'acceptanceCriteria', 'userStories', 'requirements',
447
+ 'tests', 'completionContract',
448
+ ],
449
+ };
450
+
451
+ // ---------------------------------------------------------------------------
452
+ // Tool definitions (D5-D7, D17)
453
+ // ---------------------------------------------------------------------------
454
+
455
+ const KIND_ENUM = ['req', 'us', 'ac', 'tac', 'adr', 'fbs', 'ts', 'tc', 'cn'];
456
+
457
+ const DEFINITIONS = [
458
+ {
459
+ name: 'rcf_validate',
460
+ title: 'Validate the RCF tree',
461
+ description: 'Reports whether the RCF tree is structurally sound: schema-validation, broken-reference and Code Node staleness issues across every document. A tree with issues returns {ok: false, issues: [...]} as data, not an error - the issues ARE the answer. Run this first in any session, and again after every tree edit (the build-cycle playbook, rcf_execute_build_cycle, prescribes it).',
462
+ inputSchema: {
463
+ type: 'object',
464
+ properties: {
465
+ noCode: { type: 'boolean', description: 'Phase 10: skip the Code Node staleness pass (spec-graph checks only); defaults to false (full validation)' },
466
+ },
467
+ additionalProperties: false,
468
+ },
469
+ outputSchema: withErrorPayload(VALIDATE_OUTPUT_SCHEMA),
470
+ annotations: { readOnlyHint: true },
471
+ },
472
+ {
473
+ name: 'rcf_coverage',
474
+ title: 'Structural coverage report',
475
+ description: 'Reports which requirements have at least one complete chain to a test case (PRD -> REQ -> US -> AC -> TS -> TC). A TC counts as covering its AC only when its testPointer (filePath::testName) resolves to a real test in the working tree; a TC whose pointer does not resolve is reported as covered-unresolved (its own class and counter), never counted as coverage. This is a mechanical, deterministic structural check: it does NOT judge whether the AC set adequately captures a requirement\'s intent. In strict mode, gaps are returned as data ({ok: false} in the envelope), never as a tool error - unlike the CLI, which exits 4 for CI gating. Method: TS / TC docs are authored deliverables - a coverage gap means the test layer is not finished, not a stat to report.',
476
+ inputSchema: {
477
+ type: 'object',
478
+ properties: {
479
+ scopeId: { type: 'string', description: 'Optional PRD / REQ / US id to scope coverage; below-AC ids are refused' },
480
+ strict: { type: 'boolean', description: 'Per-AC-strict mode (every AC needs TC coverage); defaults to false (shallow-any)' },
481
+ withCode: { type: 'boolean', description: 'Phase 10: layer the code axis onto every AC (implemented-and-covered / implemented-uncovered / unimplemented) plus a codeNodeOrphans list. Informational only - never affects ok or the exit-code twin.' },
482
+ },
483
+ additionalProperties: false,
484
+ },
485
+ outputSchema: withErrorPayload(COVERAGE_OUTPUT_SCHEMA),
486
+ annotations: { readOnlyHint: true },
487
+ },
488
+ {
489
+ name: 'rcf_trace',
490
+ title: 'Trace the graph from an id or a source path',
491
+ description: 'Answers "what hangs off this document" (forward), "what does it hang off" (back), or both, from any document id. Back-traces follow parent-child edges only; cross-link fan-out is what rcf_impact is for. Phase 10: when id does not resolve to a document, it is tried as a source path (optionally #symbol-suffixed) and traced backward from the matching Code Node(s) up to the root PRD; toCode extends a forward/both trace into the code layer. Method: trace before touching anything that other documents hang off.',
492
+ inputSchema: {
493
+ type: 'object',
494
+ properties: {
495
+ id: { type: 'string', description: 'Pivot document id (e.g. REQ-101, AC-201-1) or a repo-relative source path, optionally #symbol-suffixed' },
496
+ direction: { type: 'string', enum: ['forward', 'back', 'both'], description: 'Walk direction; defaults to forward, matching the CLI' },
497
+ toCode: { type: 'boolean', description: 'Phase 10: extend a forward/both trace into implementing/dependent Code Nodes; defaults to false (byte-identical to pre-Phase-10 behaviour)' },
498
+ },
499
+ required: ['id'],
500
+ additionalProperties: false,
501
+ },
502
+ outputSchema: withErrorPayload(TRACE_OUTPUT_SCHEMA),
503
+ annotations: { readOnlyHint: true },
504
+ },
505
+ {
506
+ name: 'rcf_impact',
507
+ title: 'Impact fan-out for a change',
508
+ description: 'Answers "if this document changes, what needs re-checking": ancestors and descendants with a per-node action label (re-run, re-verify, re-approve, review-scope, review-arch, review-plan, re-execute, review-context, re-verify-code). Phase 10: toCode extends the descendant fan-out into Code Nodes implementing an affected AC. Method: run this before changing any document with dependents.',
509
+ inputSchema: {
510
+ type: 'object',
511
+ properties: {
512
+ id: { type: 'string', description: 'Pivot document id' },
513
+ toCode: { type: 'boolean', description: 'Phase 10: extend the descendant fan-out into Code Nodes; defaults to false' },
514
+ },
515
+ required: ['id'],
516
+ additionalProperties: false,
517
+ },
518
+ outputSchema: withErrorPayload(IMPACT_OUTPUT_SCHEMA),
519
+ annotations: { readOnlyHint: true },
520
+ },
521
+ {
522
+ name: 'rcf_read',
523
+ title: 'Read a document',
524
+ description: 'Returns one document\'s body (or a single dot-path field) by id. Resolves standalone documents, inline acceptance criteria (AC-...), inline test cases (TC-...) and MANIFEST. Method: read the real document before editing it - updates patch what exists, never a remembered body.',
525
+ inputSchema: {
526
+ type: 'object',
527
+ properties: {
528
+ id: { type: 'string', description: 'Document id, e.g. REQ-002, US-101, AC-101-1, MANIFEST' },
529
+ field: { type: 'string', description: 'Optional dot-path to a single field, e.g. acceptanceCriteria[0].description' },
530
+ },
531
+ required: ['id'],
532
+ additionalProperties: false,
533
+ },
534
+ outputSchema: withErrorPayload(READ_OUTPUT_SCHEMA),
535
+ annotations: { readOnlyHint: true },
536
+ },
537
+ {
538
+ name: 'rcf_create',
539
+ title: 'Create a document',
540
+ description: 'Creates a new RCF document of the given kind. Inline kinds (ac, tc) mutate their parent document; every other kind writes one new file. Phase 10: cn (Code Node) has no parent - its identity is path, optionally #symbol-suffixed; implementsAcIds (via acIds) and dependencies (via deps) are optional cross-links, validated against known ACs / Code Nodes. Body fields beyond the dedicated properties go in the body object; dedicated properties win on conflict. Method: RCF layers (PRD -> REQ -> US -> AC -> TS -> TC, plus TAD / TAC / ADR) are elicited with the stakeholder - see the rcf_elicit_requirements prompt - never fabricated single-shot.',
541
+ inputSchema: {
542
+ type: 'object',
543
+ properties: {
544
+ kind: { type: 'string', enum: KIND_ENUM, description: 'Document kind' },
545
+ parent: { type: 'string', description: 'Parent document id; required for every kind except cn (which has no parent)' },
546
+ id: { type: 'string', description: 'Override the auto-assigned id (refuses on collision)' },
547
+ title: { type: 'string', description: 'Required for req / us / tac / adr / fbs / ts' },
548
+ description: { type: 'string', description: 'Required for ac / tc' },
549
+ purpose: { type: 'string', description: 'Required for ts' },
550
+ testLevel: { type: 'string', enum: ['unit', 'integration', 'e2e', 'contract', 'manual'], description: 'Required for ts' },
551
+ acIds: { type: 'array', items: { type: 'string' }, description: 'Required for fbs and ts: one or more AC ids. For cn: implementsAcIds (may be empty - an orphan CN is legitimate).' },
552
+ acId: { type: 'string', description: 'Required for tc: the single AC this test case exercises' },
553
+ slug: { type: 'string', description: 'Optional for tc; derived from description if absent' },
554
+ testPointer: { type: 'string', description: 'Required for tc; format filePath::testName. Coverage counts a TC only when this pointer resolves to a real test in the working tree.' },
555
+ buildOrder: { type: 'integer', minimum: 1, description: 'Optional for fbs; defaults to max+1 within its build sequence' },
556
+ path: { type: 'string', description: 'Required for cn: repo-relative source path, optionally #symbol-suffixed' },
557
+ deps: { type: 'array', items: { type: 'string' }, description: 'Optional for cn: Code Node ids this node depends on' },
558
+ body: { type: 'object', description: 'Further body fields as JSON (the MCP twin of --from-file)' },
559
+ dryRun: { type: 'boolean', description: 'Report the intended id / path without writing; defaults to false' },
560
+ },
561
+ required: ['kind'],
562
+ additionalProperties: false,
563
+ },
564
+ outputSchema: withErrorPayload(CREATE_OUTPUT_SCHEMA),
565
+ annotations: { destructiveHint: false },
566
+ },
567
+ {
568
+ name: 'rcf_update',
569
+ title: 'Update a document',
570
+ description: 'Patches fields on an existing document: dot-path sets, a deep-merge patch object, or both. Refuses to touch id, createdAt and schemaVersion. Values are any JSON type - no string re-encoding. Method: document content comes from stakeholder elicitation (rcf_elicit_requirements prompt), not invention.',
571
+ inputSchema: {
572
+ type: 'object',
573
+ properties: {
574
+ id: { type: 'string', description: 'Target document id (supports inline AC / TC ids)' },
575
+ sets: {
576
+ type: 'array',
577
+ items: {
578
+ type: 'object',
579
+ properties: {
580
+ path: { type: 'string', description: 'Dot-path, e.g. status or acceptanceCriteria[0].description' },
581
+ value: { description: 'The value to set; any JSON type' },
582
+ },
583
+ required: ['path', 'value'],
584
+ additionalProperties: false,
585
+ },
586
+ description: 'Dot-path assignments',
587
+ },
588
+ patch: { type: 'object', description: 'Deep-merge body fields (arrays replace); the MCP twin of --from-file' },
589
+ dryRun: { type: 'boolean', description: 'Report the intended write without executing; defaults to false' },
590
+ },
591
+ required: ['id'],
592
+ additionalProperties: false,
593
+ },
594
+ outputSchema: withErrorPayload(UPDATE_OUTPUT_SCHEMA),
595
+ annotations: { destructiveHint: false },
596
+ },
597
+ {
598
+ name: 'rcf_delete',
599
+ title: 'Delete a document',
600
+ description: 'Deletes a document. Refuses by default when the document has dependents; cascade: true also deletes dependents and drops backrefs. dryRun returns the deletion plan without executing. Method: when unsure what hangs off the target, run rcf_impact first.',
601
+ inputSchema: {
602
+ type: 'object',
603
+ properties: {
604
+ id: { type: 'string' },
605
+ cascade: { type: 'boolean', description: 'Also delete dependents and drop backrefs; defaults to false (refuse when dependents exist)' },
606
+ dryRun: { type: 'boolean', description: 'Return the deletion plan without executing; defaults to false' },
607
+ },
608
+ required: ['id'],
609
+ additionalProperties: false,
610
+ },
611
+ outputSchema: withErrorPayload(DELETE_OUTPUT_SCHEMA),
612
+ annotations: { destructiveHint: true, idempotentHint: true },
613
+ },
614
+ {
615
+ name: 'rcf_link',
616
+ title: 'Link a user story to TACs',
617
+ description: 'Appends one or more TAC ids to a user story\'s tacIds. Idempotent: linking an already-linked TAC is a no-op. Returns the post-state of tacIds. Method: TAC links record the elicited tech layer (TAD / TAC / ADR) of the chain - author it, do not skip it.',
618
+ inputSchema: {
619
+ type: 'object',
620
+ properties: {
621
+ usId: { type: 'string', description: 'The user story id' },
622
+ tacIds: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'TAC ids to link' },
623
+ dryRun: { type: 'boolean', description: 'Report the intended write without executing; defaults to false' },
624
+ },
625
+ required: ['usId', 'tacIds'],
626
+ additionalProperties: false,
627
+ },
628
+ outputSchema: withErrorPayload(LINK_OUTPUT_SCHEMA),
629
+ annotations: { idempotentHint: true },
630
+ },
631
+ {
632
+ name: 'rcf_unlink',
633
+ title: 'Unlink a user story from TACs',
634
+ description: 'Removes one or more TAC ids from a user story\'s tacIds. Idempotent: unlinking an absent TAC is a no-op. Returns the post-state of tacIds. Method: keep tech-layer links honest - unlink only what the stakeholder agreed no longer applies.',
635
+ inputSchema: {
636
+ type: 'object',
637
+ properties: {
638
+ usId: { type: 'string', description: 'The user story id' },
639
+ tacIds: { type: 'array', items: { type: 'string' }, minItems: 1, description: 'TAC ids to unlink' },
640
+ dryRun: { type: 'boolean', description: 'Report the intended write without executing; defaults to false' },
641
+ },
642
+ required: ['usId', 'tacIds'],
643
+ additionalProperties: false,
644
+ },
645
+ outputSchema: withErrorPayload(LINK_OUTPUT_SCHEMA),
646
+ annotations: { idempotentHint: true },
647
+ },
648
+ {
649
+ name: 'rcf_build',
650
+ title: 'Assemble an FBS spec bundle',
651
+ description: 'Assembles the complete spec bundle for one FBS item: the work, queue and dependency context, acceptance criteria with US / REQ ancestry, architectural context, existing test surface and the completion contract. Addresses FBS ids ONLY - the FBS is the queue unit. A blocked item still returns its bundle; blockedBy in the envelope carries the fact as data. Bundle assembly is mechanical: it projects what the tree says and does not judge whether the FBS is well-specified. Method: execute the bundle via the five-stage runbook in the rcf_execute_build_cycle prompt (Define, Build, Review, Test, Finalise).',
652
+ inputSchema: {
653
+ type: 'object',
654
+ properties: {
655
+ fbsId: { type: 'string', description: 'The FBS item to bundle, e.g. FBS-003. User story ids are refused - to find the FBS items behind a story, call rcf_trace with the US id' },
656
+ },
657
+ required: ['fbsId'],
658
+ additionalProperties: false,
659
+ },
660
+ outputSchema: withErrorPayload(BUILD_OUTPUT_SCHEMA),
661
+ annotations: { readOnlyHint: true },
662
+ },
663
+ ];
664
+
665
+ // ---------------------------------------------------------------------------
666
+ // Minimal argument validation against the D7 schema subset
667
+ // ---------------------------------------------------------------------------
668
+
669
+ /**
670
+ * Validate a value against the closed-object JSON Schema subset the
671
+ * D7 input schemas use: type, enum, properties, required,
672
+ * additionalProperties: false, items, minimum, minItems. Not a general
673
+ * JSON Schema validator - exactly the checks our schemas need, so the
674
+ * runtime stays dependency-free (D21). The devDependency conformance
675
+ * layer exercises the schemas through the official SDK client.
676
+ *
677
+ * @param {object} schema
678
+ * @param {unknown} value
679
+ * @param {string} at - path label for messages
680
+ * @param {string[]} problems - accumulator
681
+ */
682
+ function checkSchema(schema, value, at, problems) {
683
+ if (schema.type !== undefined) {
684
+ const types = Array.isArray(schema.type) ? schema.type : [schema.type];
685
+ if (!types.some((t) => matchesType(t, value))) {
686
+ problems.push(`${at}: expected ${types.join(' | ')}`);
687
+ return;
688
+ }
689
+ }
690
+ if (schema.enum !== undefined && !schema.enum.includes(value)) {
691
+ problems.push(`${at}: expected one of ${schema.enum.join(' | ')}`);
692
+ return;
693
+ }
694
+ if (typeof value === 'number' && schema.minimum !== undefined && value < schema.minimum) {
695
+ problems.push(`${at}: expected >= ${schema.minimum}`);
696
+ }
697
+ if (Array.isArray(value)) {
698
+ if (schema.minItems !== undefined && value.length < schema.minItems) {
699
+ problems.push(`${at}: expected at least ${schema.minItems} item(s)`);
700
+ }
701
+ if (schema.items) {
702
+ value.forEach((item, i) => checkSchema(schema.items, item, `${at}[${i}]`, problems));
703
+ }
704
+ return;
705
+ }
706
+ if (value && typeof value === 'object' && (schema.properties || schema.required || schema.additionalProperties === false)) {
707
+ const properties = schema.properties ?? {};
708
+ for (const key of schema.required ?? []) {
709
+ if (!(key in value)) problems.push(`${at}.${key}: required`.replace(/^args\./, ''));
710
+ }
711
+ for (const [key, v] of Object.entries(value)) {
712
+ const propSchema = properties[key];
713
+ if (!propSchema) {
714
+ if (schema.additionalProperties === false) {
715
+ problems.push(`${at}.${key}: unknown property`.replace(/^args\./, ''));
716
+ }
717
+ continue;
718
+ }
719
+ checkSchema(propSchema, v, `${at}.${key}`.replace(/^args\./, ''), problems);
720
+ }
721
+ }
722
+ }
723
+
724
+ function matchesType(t, value) {
725
+ switch (t) {
726
+ case 'string': return typeof value === 'string';
727
+ case 'boolean': return typeof value === 'boolean';
728
+ case 'number': return typeof value === 'number' && Number.isFinite(value);
729
+ case 'integer': return typeof value === 'number' && Number.isInteger(value);
730
+ case 'array': return Array.isArray(value);
731
+ case 'object': return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
732
+ case 'null': return value === null;
733
+ default: return false;
734
+ }
735
+ }
736
+
737
+ /**
738
+ * @param {object} schema - a D7 inputSchema
739
+ * @param {unknown} args
740
+ * @returns {string[]} problems (empty = valid)
741
+ */
742
+ export function validateToolArgs(schema, args) {
743
+ const problems = [];
744
+ checkSchema(schema, args ?? {}, 'args', problems);
745
+ return problems;
746
+ }
747
+
748
+ // ---------------------------------------------------------------------------
749
+ // rcf_read target resolution (mirrors src/cli/read.js, which does not
750
+ // export its private helpers and is out of this phase's edit surface)
751
+ // ---------------------------------------------------------------------------
752
+
753
+ function resolveTarget(tree, id) {
754
+ if (id === 'MANIFEST' && tree.manifest) return { doc: tree.manifest };
755
+ const doc = tree.byId.get(id);
756
+ if (doc) return { doc };
757
+ if (/^AC-\d+(-\d+)?$/.test(id)) {
758
+ const parentId = tree.parentByChild.get(id);
759
+ if (!parentId) return null;
760
+ const us = tree.byId.get(parentId);
761
+ if (!us) return null;
762
+ const entry = (us.acceptanceCriteria ?? []).find((ac) => ac.id === id);
763
+ return entry ? { doc: entry } : null;
764
+ }
765
+ if (/^TC-\d{3}-[a-z0-9-]+$/.test(id)) {
766
+ const parentId = tree.parentByChild.get(id);
767
+ if (!parentId) return null;
768
+ const ts = tree.byId.get(parentId);
769
+ if (!ts) return null;
770
+ const entry = (ts.testCases ?? []).find((tc) => tc.id === id);
771
+ return entry ? { doc: entry } : null;
772
+ }
773
+ return null;
774
+ }
775
+
776
+ /**
777
+ * Phase 10 (X2 CodeNode bridge): resolve a source-path query to Code Node
778
+ * ids. Mirrors src/cli/trace.js's resolveCodeNodesForPath. Matches a CN
779
+ * when its `path` equals the query (file-level or file#symbol form) or
780
+ * when the query names the file that a symbol-level CN lives in.
781
+ *
782
+ * @param {object} tree - walker TreeModel
783
+ * @param {string} query - a repo-relative path, optionally #symbol-suffixed
784
+ * @returns {string[]} matching CN ids, sorted
785
+ */
786
+ function resolveCodeNodesForPath(tree, query) {
787
+ const out = [];
788
+ for (const cn of tree.codeNodes ?? []) {
789
+ const cnPath = cn.path ?? '';
790
+ const cnFile = cnPath.split('#')[0];
791
+ if (cnPath === query || cnFile === query) out.push(cn.cnId);
792
+ }
793
+ return out.sort();
794
+ }
795
+
796
+ function extractField(root, path) {
797
+ const parts = parseDotPath(path);
798
+ if (!parts) return undefined;
799
+ let cur = root;
800
+ for (const seg of parts) {
801
+ if (cur === undefined || cur === null) return undefined;
802
+ if (seg.kind === 'index') {
803
+ if (!Array.isArray(cur)) return undefined;
804
+ cur = cur[seg.value];
805
+ } else {
806
+ cur = cur[seg.value];
807
+ }
808
+ }
809
+ return cur;
810
+ }
811
+
812
+ function parseDotPath(path) {
813
+ if (typeof path !== 'string' || path.length === 0) return null;
814
+ const parts = [];
815
+ for (const seg of path.split('.')) {
816
+ const m = /^([^\[\]]+)((?:\[\d+\])*)$/.exec(seg);
817
+ if (!m) return null;
818
+ parts.push({ kind: 'prop', value: m[1] });
819
+ if (m[2]) {
820
+ const indices = m[2].match(/\d+/g) ?? [];
821
+ for (const n of indices) parts.push({ kind: 'index', value: Number(n) });
822
+ }
823
+ }
824
+ return parts;
825
+ }
826
+
827
+ // ---------------------------------------------------------------------------
828
+ // Handlers
829
+ // ---------------------------------------------------------------------------
830
+
831
+ /**
832
+ * Build a successful tools/call result: the envelope verbatim as
833
+ * structuredContent plus its serialisation as a text block (the
834
+ * 2025-11-25 backwards-compatibility SHOULD).
835
+ *
836
+ * @param {object} envelope
837
+ * @returns {object}
838
+ */
839
+ function okResult(envelope) {
840
+ return {
841
+ content: [{ type: 'text', text: JSON.stringify(envelope, null, 2) }],
842
+ structuredContent: envelope,
843
+ };
844
+ }
845
+
846
+ /**
847
+ * Create the tool registry bound to one project root (D13: the root is
848
+ * fixed for the process lifetime).
849
+ *
850
+ * @param {object} opts
851
+ * @param {string} opts.projectRoot
852
+ * @param {{info: (line: string) => void, error: (line: string) => void}} opts.log
853
+ * @returns {{definitions: object[], call: (name: string, args: unknown) => Promise<object>}}
854
+ */
855
+ export function createToolRegistry({ projectRoot, log }) {
856
+ const byName = new Map(DEFINITIONS.map((d) => [d.name, d]));
857
+
858
+ const handlers = {
859
+ rcf_validate: async (args) => {
860
+ const { tree, errors } = await walkTree({ projectRoot });
861
+ // Phase 10 (X2 CodeNode bridge, D6/D8): the staleness pass runs by
862
+ // default, folded into the same issue list; noCode skips it.
863
+ const allErrors = Boolean(args?.noCode)
864
+ ? errors
865
+ : [...errors, ...(await checkCodeNodeResolution({ projectRoot, tree }))];
866
+ // D10: for validate, the issues ARE the answer - never isError.
867
+ return okResult({ ok: allErrors.length === 0, issues: issuesFromErrors(allErrors) });
868
+ },
869
+
870
+ rcf_coverage: async (args) => {
871
+ const { tree, errors } = await walkTree({ projectRoot });
872
+ if (errors.length > 0) return walkerBlockedResult(errors);
873
+ const scopeId = args.scopeId ?? null;
874
+ if (scopeId !== null) {
875
+ const classification = classifyCoverageScope(tree, scopeId);
876
+ if (classification === 'below-ac') {
877
+ return usageErrorResult(
878
+ `coverage: scopeId ${scopeId} is below the AC layer or off the REQ chain; coverage scope must be a PRD / REQ / US id`,
879
+ { documentId: scopeId },
880
+ );
881
+ }
882
+ if (classification !== 'valid') {
883
+ return usageErrorResult(`coverage: id ${scopeId} not found`, { documentId: scopeId });
884
+ }
885
+ }
886
+ // OQ-P7-8: strict gaps return data ({ok: false}), never isError.
887
+ // Phase 10 (D11): withCode layers the informational code axis on.
888
+ // w-2026-07-28-005: pointer resolution gates "covered" - same rule
889
+ // as the CLI, one resolution pass against the working tree.
890
+ const testPointers = await resolveTestPointers({ projectRoot, tree });
891
+ return okResult(computeCoverage(tree, { strict: Boolean(args.strict), scopeId, withCode: Boolean(args.withCode), testPointers }));
892
+ },
893
+
894
+ rcf_trace: async (args) => {
895
+ const { tree, errors } = await walkTree({ projectRoot });
896
+ if (errors.length > 0) return walkerBlockedResult(errors);
897
+ const includeCode = Boolean(args.toCode);
898
+ const direction = args.direction ?? 'forward';
899
+
900
+ // Phase 10 (X2 CodeNode bridge, D9): path mode. If `id` is not a
901
+ // known document, try it as a source path resolving to one or more
902
+ // Code Nodes, then trace each backward.
903
+ if (!kindOf(tree, args.id)) {
904
+ const cnIds = resolveCodeNodesForPath(tree, args.id);
905
+ if (cnIds.length === 0) {
906
+ return usageErrorResult(`trace: id ${args.id} not found (no document or code node matches)`, { documentId: args.id });
907
+ }
908
+ if (cnIds.length === 1) {
909
+ const res = computeTrace(tree, { id: cnIds[0], direction: 'back' });
910
+ return okResult(res);
911
+ }
912
+ const matches = cnIds.map((cnId) => {
913
+ const res = computeTrace(tree, { id: cnId, direction: 'back' });
914
+ return { cnId, path: tree.byId.get(cnId)?.path ?? null, nodes: res.nodes, edges: res.edges };
915
+ });
916
+ return okResult({ pivot: args.id, direction: 'back', found: true, matches });
917
+ }
918
+
919
+ const result = computeTrace(tree, { id: args.id, direction, includeCode });
920
+ if (!result.found) {
921
+ return usageErrorResult(`trace: id ${args.id} not found`, { documentId: args.id });
922
+ }
923
+ return okResult(result);
924
+ },
925
+
926
+ rcf_impact: async (args) => {
927
+ const { tree, errors } = await walkTree({ projectRoot });
928
+ if (errors.length > 0) return walkerBlockedResult(errors);
929
+ const result = computeImpact(tree, { id: args.id, includeCode: Boolean(args.toCode) });
930
+ if (!result.found) {
931
+ return usageErrorResult(`impact: id ${args.id} not found`, { documentId: args.id });
932
+ }
933
+ return okResult(result);
934
+ },
935
+
936
+ rcf_read: async (args) => {
937
+ const { tree, errors } = await walkTree({ projectRoot });
938
+ if (errors.length > 0) return walkerBlockedResult(errors);
939
+ const target = resolveTarget(tree, args.id);
940
+ if (!target) {
941
+ return usageErrorResult(`read: id ${args.id} not found`, { documentId: args.id });
942
+ }
943
+ let value = target.doc;
944
+ if (args.field !== undefined) {
945
+ value = extractField(target.doc, args.field);
946
+ if (value === undefined) {
947
+ return usageErrorResult(`read: field ${args.field} not present on ${args.id}`, {
948
+ documentId: args.id,
949
+ field: args.field,
950
+ });
951
+ }
952
+ }
953
+ return okResult({ id: args.id, field: args.field ?? null, value });
954
+ },
955
+
956
+ rcf_create: async (args) => {
957
+ // B5: pre-existing tree breakage no longer blocks write tools -
958
+ // the writer gates on the POST-write tree state (net-new breakage
959
+ // still refuses; repairing a broken tree is allowed).
960
+ const { tree, errors } = await walkTree({ projectRoot });
961
+ const kind = args.kind;
962
+ const body = { ...(args.body ?? {}) };
963
+ if (args.title !== undefined) body.title = args.title;
964
+ if (args.description !== undefined) body.description = args.description;
965
+ if (args.purpose !== undefined) body.purpose = args.purpose;
966
+ if (args.testLevel !== undefined) body.testLevel = args.testLevel;
967
+ // Phase 10: cn's AC cross-link field is implementsAcIds, not acIds
968
+ // (fbs/ts share acIds) - acIds maps to whichever the kind expects.
969
+ if (args.acIds !== undefined) {
970
+ if (kind === 'cn') body.implementsAcIds = args.acIds;
971
+ else body.acIds = args.acIds;
972
+ }
973
+ if (kind === 'cn') {
974
+ if (args.path !== undefined) body.path = args.path;
975
+ if (args.deps !== undefined) body.dependencies = args.deps;
976
+ }
977
+
978
+ const options = {
979
+ id: args.id,
980
+ parentId: args.parent,
981
+ dryRun: Boolean(args.dryRun),
982
+ };
983
+
984
+ // Per-kind mandatory fields (mirrors src/cli/create.js).
985
+ if (kind === 'ac' || kind === 'tc') {
986
+ if (!body.description) return usageErrorResult(`create ${kind}: description is required`);
987
+ } else if (kind === 'cn') {
988
+ if (!body.path) return usageErrorResult('create cn: path is required');
989
+ } else if (!body.title) {
990
+ return usageErrorResult(`create ${kind}: title is required`);
991
+ }
992
+ if (kind === 'ts') {
993
+ if (!body.purpose) return usageErrorResult('create ts: purpose is required');
994
+ if (!body.testLevel) return usageErrorResult('create ts: testLevel is required');
995
+ if (!Array.isArray(body.acIds) || body.acIds.length === 0) {
996
+ return usageErrorResult('create ts: acIds is required (one or more AC ids)');
997
+ }
998
+ }
999
+ if (kind === 'fbs') {
1000
+ if (!Array.isArray(body.acIds) || body.acIds.length === 0) {
1001
+ return usageErrorResult('create fbs: acIds is required (one or more AC ids)');
1002
+ }
1003
+ if (args.buildOrder !== undefined) options.buildOrder = args.buildOrder;
1004
+ }
1005
+ if (kind === 'tc') {
1006
+ if (!args.acId) return usageErrorResult('create tc: acId is required');
1007
+ body.acId = args.acId;
1008
+ options.slug = args.slug ?? deriveSlug(body.description);
1009
+ if (args.testPointer !== undefined) options.testPointer = args.testPointer;
1010
+ }
1011
+
1012
+ const result = await createDocument({ projectRoot, tree, kind, body, options, walkErrors: errors });
1013
+ if (isRcfError(result)) return writerErrorResult(result, log);
1014
+ return okResult({
1015
+ ok: true,
1016
+ id: result.id,
1017
+ kind,
1018
+ filePath: result.filePath,
1019
+ ...(result.dryRun ? { dryRun: true } : {}),
1020
+ });
1021
+ },
1022
+
1023
+ rcf_update: async (args) => {
1024
+ // B5: no pre-write walk gate - repairing a broken doc IS an update.
1025
+ const { tree, errors } = await walkTree({ projectRoot });
1026
+ const sets = args.sets ?? [];
1027
+ const patch = args.patch ?? null;
1028
+ if (sets.length === 0 && !patch) {
1029
+ return usageErrorResult('update: at least one of sets or patch is required', { documentId: args.id });
1030
+ }
1031
+ const result = await updateDocument({
1032
+ projectRoot,
1033
+ tree,
1034
+ id: args.id,
1035
+ patch,
1036
+ sets,
1037
+ options: { dryRun: Boolean(args.dryRun) },
1038
+ walkErrors: errors,
1039
+ });
1040
+ if (isRcfError(result)) return writerErrorResult(result, log);
1041
+ const changedPaths = [
1042
+ ...Object.keys(patch ?? {}),
1043
+ ...sets.map((s) => s.path),
1044
+ ];
1045
+ return okResult({
1046
+ ok: true,
1047
+ id: result.id,
1048
+ changedPaths,
1049
+ filePath: result.filePath,
1050
+ ...(result.dryRun ? { dryRun: true } : {}),
1051
+ });
1052
+ },
1053
+
1054
+ rcf_delete: async (args) => {
1055
+ // B5: no pre-write walk gate - deleting the offending doc is the
1056
+ // canonical repair for a wedged tree.
1057
+ const { tree, errors } = await walkTree({ projectRoot });
1058
+ const dryRun = Boolean(args.dryRun);
1059
+ const result = await deleteDocument({
1060
+ projectRoot,
1061
+ tree,
1062
+ id: args.id,
1063
+ options: { cascade: Boolean(args.cascade), dryRun },
1064
+ walkErrors: errors,
1065
+ });
1066
+ if (isRcfError(result)) return writerErrorResult(result, log);
1067
+ return okResult({
1068
+ ok: true,
1069
+ deleted: result.deleted,
1070
+ mutated: result.mutated,
1071
+ plan: result.plan,
1072
+ ...(dryRun ? { dryRun: true } : {}),
1073
+ });
1074
+ },
1075
+
1076
+ rcf_link: (args) => linkHandler(args, false),
1077
+ rcf_unlink: (args) => linkHandler(args, true),
1078
+
1079
+ rcf_build: async (args) => {
1080
+ const { tree, errors } = await walkTree({ projectRoot });
1081
+ if (errors.length > 0) return walkerBlockedResult(errors);
1082
+ const fbsId = args.fbsId;
1083
+ // FBS-id-only addressing (reconciliation carry 1, mirroring the
1084
+ // shipped Phase 6 D1 behaviour): a US id is a usage error that
1085
+ // points the agent at rcf_trace.
1086
+ const kind = kindOf(tree, fbsId);
1087
+ if (kind !== 'fbs') {
1088
+ if (kind === 'userStory') {
1089
+ return usageErrorResult(
1090
+ `build: ${fbsId} is a user story, not an FBS id; the FBS is the queue unit. `
1091
+ + `To list the FBS items linked to this story, call rcf_trace with id ${fbsId} and direction forward`,
1092
+ { documentId: fbsId },
1093
+ );
1094
+ }
1095
+ if (kind) {
1096
+ return usageErrorResult(`build: ${fbsId} is a ${kind} id; rcf_build addresses FBS items only`, { documentId: fbsId });
1097
+ }
1098
+ return usageErrorResult(`build: id ${fbsId} not found`, { documentId: fbsId });
1099
+ }
1100
+ const bundle = assembleBundle(tree, { fbsId });
1101
+ // Same construction as src/build/formatters/json.js over the
1102
+ // same object - envelope identity with `--format json` by
1103
+ // construction, then locked by the parity tests (D20). No
1104
+ // strict gate here: blockedBy carries the fact as data
1105
+ // (reconciliation carry 2, OQ-P7-8 posture).
1106
+ return okResult({ ok: true, mode: 'bundle', ...bundle });
1107
+ },
1108
+ };
1109
+
1110
+ async function linkHandler(args, removing) {
1111
+ // B5: no pre-write walk gate on write tools (post-write gate applies).
1112
+ const { tree, errors } = await walkTree({ projectRoot });
1113
+ const verb = removing ? 'unlink' : 'link';
1114
+ const us = tree.byId.get(args.usId);
1115
+ if (!us || tree.kindById.get(args.usId) !== 'userStory') {
1116
+ return usageErrorResult(`${verb}: ${args.usId} is not an existing US`, { documentId: args.usId });
1117
+ }
1118
+ for (const tacId of args.tacIds) {
1119
+ if (tree.kindById.get(tacId) !== 'tac') {
1120
+ return errorResult([{
1121
+ kind: 'brokenReference',
1122
+ message: `${verb}: ${tacId} is not an existing TAC`,
1123
+ documentId: tacId,
1124
+ field: 'tacIds',
1125
+ rule: 'resolveTo:tac',
1126
+ }]);
1127
+ }
1128
+ }
1129
+ const current = new Set(us.tacIds ?? []);
1130
+ const target = new Set(current);
1131
+ for (const tacId of args.tacIds) {
1132
+ if (removing) target.delete(tacId); else target.add(tacId);
1133
+ }
1134
+ const next = [...target].sort();
1135
+ const currentSorted = [...current].sort();
1136
+ const changed = next.length !== currentSorted.length || next.some((id, i) => id !== currentSorted[i]);
1137
+ const dryRun = Boolean(args.dryRun);
1138
+ if (!changed) {
1139
+ // Idempotent no-op: the post-state is already the target state.
1140
+ return okResult({ ok: true, usId: args.usId, tacIds: next, ...(dryRun ? { dryRun: true } : {}) });
1141
+ }
1142
+ const result = await updateDocument({
1143
+ projectRoot,
1144
+ tree,
1145
+ id: args.usId,
1146
+ patch: { tacIds: next },
1147
+ sets: [],
1148
+ options: { dryRun },
1149
+ walkErrors: errors,
1150
+ });
1151
+ if (isRcfError(result)) return writerErrorResult(result, log);
1152
+ return okResult({ ok: true, usId: args.usId, tacIds: next, ...(dryRun ? { dryRun: true } : {}) });
1153
+ }
1154
+
1155
+ /**
1156
+ * Dispatch one tools/call. Unknown tool names are protocol errors
1157
+ * (-32602, spec-mandated); argument-schema misses are tool execution
1158
+ * errors the model can self-correct from (D10).
1159
+ *
1160
+ * @param {string} name
1161
+ * @param {unknown} args
1162
+ * @returns {Promise<object>}
1163
+ */
1164
+ // Theme 1 setup funnel (touchpoint iii): server running, tree present,
1165
+ // but the rcf marker block is absent from the project-root agent
1166
+ // instructions - the session started without the init bootstrap.
1167
+ // Every tool response then carries ONE firm instruction routing back
1168
+ // to `npx rcf init` + a session restart. Cheap file check; a positive
1169
+ // (marker present) is cached for the server-process lifetime so the
1170
+ // notice disappears permanently once setup is complete.
1171
+ let markerSeen = false;
1172
+ async function setupFunnelNotice() {
1173
+ if (markerSeen) return null;
1174
+ markerSeen = await hasAgentMarker(projectRoot);
1175
+ return markerSeen ? null : SETUP_FUNNEL_INSTRUCTION;
1176
+ }
1177
+
1178
+ async function call(name, args) {
1179
+ const definition = byName.get(name);
1180
+ if (!definition) {
1181
+ throw new JsonRpcError(INVALID_PARAMS, `Unknown tool: ${name}`);
1182
+ }
1183
+ const problems = validateToolArgs(definition.inputSchema, args);
1184
+ if (problems.length > 0) {
1185
+ return usageErrorResult(`${name}: invalid arguments: ${problems.join('; ')}`);
1186
+ }
1187
+ let result;
1188
+ try {
1189
+ result = await handlers[name](args ?? {});
1190
+ } catch (err) {
1191
+ // Defensive: pure-layer throws become execution errors with the
1192
+ // stack on stderr only.
1193
+ const e = /** @type {Error} */ (err);
1194
+ return unexpectedFailureResult({ kind: 'ioFailure', message: e.message, stack: e.stack }, log);
1195
+ }
1196
+ const notice = await setupFunnelNotice();
1197
+ if (notice) {
1198
+ result = { ...result, content: [...(result.content ?? []), { type: 'text', text: notice }] };
1199
+ }
1200
+ return result;
1201
+ }
1202
+
1203
+ return { definitions: DEFINITIONS, call };
1204
+ }