@holmes-lab/holmes-kit 0.1.0

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 (107) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -0
  4. package/bin/holmes-hook-antigravity.js +31 -0
  5. package/bin/holmes-kit.js +23 -0
  6. package/bin/holmes-mcp.js +34 -0
  7. package/bin/holmes-stop-antigravity.js +29 -0
  8. package/dist/.build-id +1 -0
  9. package/dist/holmes/cli/agents.js +168 -0
  10. package/dist/holmes/cli/doctor.js +625 -0
  11. package/dist/holmes/cli/gitignore-merge.js +84 -0
  12. package/dist/holmes/cli/governed-precondition.js +157 -0
  13. package/dist/holmes/cli/index.js +384 -0
  14. package/dist/holmes/cli/init.js +462 -0
  15. package/dist/holmes/cli/playbook-skills.js +711 -0
  16. package/dist/holmes/cli/roles-readme.js +134 -0
  17. package/dist/holmes/cli/settings-merge.js +122 -0
  18. package/dist/holmes/config/config.js +70 -0
  19. package/dist/holmes/context/bundler.js +114 -0
  20. package/dist/holmes/context/render.js +29 -0
  21. package/dist/holmes/context/tiers.js +110 -0
  22. package/dist/holmes/context/tokens.js +8 -0
  23. package/dist/holmes/cpg/cpg-scanner.js +213 -0
  24. package/dist/holmes/cpg/hash-cache.js +86 -0
  25. package/dist/holmes/cpg/language-parser-walk.js +917 -0
  26. package/dist/holmes/cpg/language-parser-worker.js +81 -0
  27. package/dist/holmes/cpg/language-parser.js +234 -0
  28. package/dist/holmes/cpg/scan-cache.js +108 -0
  29. package/dist/holmes/cpg/source-path.js +44 -0
  30. package/dist/holmes/cpg/test-files.js +84 -0
  31. package/dist/holmes/governance/constitution-debt.js +73 -0
  32. package/dist/holmes/governance/constitution-report.js +25 -0
  33. package/dist/holmes/governance/constitution.js +129 -0
  34. package/dist/holmes/governance/identity.js +30 -0
  35. package/dist/holmes/governance/ledger-lock.js +165 -0
  36. package/dist/holmes/governance/ledger-store.conformance.js +90 -0
  37. package/dist/holmes/governance/ledger-store.js +106 -0
  38. package/dist/holmes/governance/progress-ledger.js +83 -0
  39. package/dist/holmes/governance/provenance-chain.js +365 -0
  40. package/dist/holmes/governance/provenance-ledger.js +0 -0
  41. package/dist/holmes/governance/provenance-schema.js +47 -0
  42. package/dist/holmes/governance/replica-id.js +106 -0
  43. package/dist/holmes/governance/role-policy.js +137 -0
  44. package/dist/holmes/governance/trust-score.js +43 -0
  45. package/dist/holmes/guardrail/anchors.js +31 -0
  46. package/dist/holmes/guardrail/blind-spots.js +38 -0
  47. package/dist/holmes/guardrail/decision-ledger.js +107 -0
  48. package/dist/holmes/guardrail/executable-artifact.js +129 -0
  49. package/dist/holmes/guardrail/governance-history.js +101 -0
  50. package/dist/holmes/guardrail/phase.js +169 -0
  51. package/dist/holmes/guardrail/risk-classifier.js +450 -0
  52. package/dist/holmes/guardrail/risk-gate.js +160 -0
  53. package/dist/holmes/guardrail/risk-types.js +6 -0
  54. package/dist/holmes/guardrail/tspec-state.js +392 -0
  55. package/dist/holmes/guardrail/write-target.js +224 -0
  56. package/dist/holmes/hooks/adapters/antigravity.js +194 -0
  57. package/dist/holmes/hooks/pre-tool-use.js +1262 -0
  58. package/dist/holmes/hooks/stop.js +416 -0
  59. package/dist/holmes/mcp/basis.js +162 -0
  60. package/dist/holmes/mcp/handlers.js +1831 -0
  61. package/dist/holmes/mcp/server.js +71 -0
  62. package/dist/holmes/mcp/stdio-client.js +165 -0
  63. package/dist/holmes/mcp/supervisor.js +178 -0
  64. package/dist/holmes/mcp/tool-schemas.js +394 -0
  65. package/dist/holmes/mcp/validate-args.js +281 -0
  66. package/dist/holmes/messages/registry.js +50 -0
  67. package/dist/holmes/project/baseline.js +210 -0
  68. package/dist/holmes/project/change-source.js +233 -0
  69. package/dist/holmes/project/ignore.js +145 -0
  70. package/dist/holmes/project/root.js +113 -0
  71. package/dist/holmes/reverse/anchor.js +162 -0
  72. package/dist/holmes/reverse/cluster.js +187 -0
  73. package/dist/holmes/reverse/draft.js +151 -0
  74. package/dist/holmes/reverse/dynamic-wiring.js +47 -0
  75. package/dist/holmes/reverse/scan.js +194 -0
  76. package/dist/holmes/reverse/surface.js +154 -0
  77. package/dist/holmes/reverse/test-map.js +263 -0
  78. package/dist/holmes/review/coverage.js +33 -0
  79. package/dist/holmes/review/findings.js +123 -0
  80. package/dist/holmes/review/package.js +40 -0
  81. package/dist/holmes/review/review-targets.js +92 -0
  82. package/dist/holmes/review/scope.js +57 -0
  83. package/dist/holmes/review/test-evidence.js +77 -0
  84. package/dist/holmes/review/test-runner.js +572 -0
  85. package/dist/holmes/rtm/dataflow-taint.js +262 -0
  86. package/dist/holmes/rtm/gap-analyzer.js +27 -0
  87. package/dist/holmes/rtm/git-changes.js +72 -0
  88. package/dist/holmes/rtm/incremental.js +45 -0
  89. package/dist/holmes/rtm/localize.js +100 -0
  90. package/dist/holmes/rtm/rtm-builder.js +191 -0
  91. package/dist/holmes/rtm/rtm-check.js +89 -0
  92. package/dist/holmes/rtm/rtm-graph.js +232 -0
  93. package/dist/holmes/rtm/taint.js +92 -0
  94. package/dist/holmes/rtm/test-scope.js +336 -0
  95. package/dist/holmes/spec/approval-blockers.js +204 -0
  96. package/dist/holmes/spec/breaking-change.js +89 -0
  97. package/dist/holmes/spec/legacy-format.js +87 -0
  98. package/dist/holmes/spec/spec-digest.js +71 -0
  99. package/dist/holmes/spec/spec-parser.js +106 -0
  100. package/dist/holmes/spec/spec-store.conformance.js +118 -0
  101. package/dist/holmes/spec/spec-store.js +331 -0
  102. package/dist/holmes/spec/spec-types.js +177 -0
  103. package/dist/holmes/spec/validator.js +280 -0
  104. package/package.json +76 -0
  105. package/playbooks/adopt/PLAYBOOK.md +125 -0
  106. package/playbooks/author-slice/PLAYBOOK.md +119 -0
  107. package/playbooks/promote-slice/PLAYBOOK.md +134 -0
@@ -0,0 +1,394 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TOOL_SCHEMAS = exports.HOOK_ENFORCED_TOOLS = void 0;
4
+ const spec_types_1 = require("../spec/spec-types");
5
+ const phase_1 = require("../guardrail/phase");
6
+ const str = (description) => ({ type: 'string', description });
7
+ const num = (description) => ({ type: 'number', description });
8
+ const strArray = (description) => ({ type: 'array', items: { type: 'string' }, description });
9
+ // @implements A-SPEC-189 §10 (round 11) — this said "must equal `git rev-parse --show-toplevel`",
10
+ // and A-SPEC-128 had already replaced `assertRepoTopLevel` with `resolveProjectRoot`: measured,
11
+ // `phase_status` answers normally on a tree with no `.git` at all. The git top-level is required by
12
+ // the CHANGE SOURCE when a range is supplied, not by the root argument — an advertisement that
13
+ // demands more than the tool does sends callers looking for a repository they do not need.
14
+ const ROOT = str('Absolute path to the project root (resolution anchors on the `.ax/` directory; a subdirectory resolves up to it). Version control is optional here — a supplied git range additionally requires the repository top-level.');
15
+ /**
16
+ * A root that does NOT have to be a git repository. Resolution anchors on the `.ax/` marker, so a
17
+ * purely local project works; a git range, when one is supplied, still requires the top-level.
18
+ */
19
+ const ROOT_ANY = str('Absolute path to the project root. Version control is optional — resolution anchors on the `.ax/` directory, and a subdirectory resolves up to it.');
20
+ // Tools whose governance is enforced by hooks (PreToolUse phase/risk gates + the Stop-hook rtm/spec
21
+ // gate) and which are therefore NOT advertised in tools/list by default — their schemas would tax
22
+ // every session's context for no gain (benchmark: all schemas loaded, 0/143 ever invoked). They stay
23
+ // callable by name. `HOLMES_MCP_PROFILE=full` re-advertises them.
24
+ //
25
+ // Lives here rather than in server.ts (A-SPEC-129 rev.2): server.ts connects a StdioServerTransport
26
+ // at module scope, so anything importing it for this constant attaches to stdio. This module holds
27
+ // the rest of the tool metadata and has no import side effects.
28
+ exports.HOOK_ENFORCED_TOOLS = new Set(['phase_check', 'risk_check', 'rtm_check', 'spec_validate']);
29
+ exports.TOOL_SCHEMAS = {
30
+ // @implements A-SPEC-156
31
+ // The full basis behind every other answer. It is a separate tool, not a field, because carrying
32
+ // the whole object on all responses is what REQ-122 forbids — the digest rides along, the detail
33
+ // is fetched only when a caller has seen two digests disagree.
34
+ basis_detail: {
35
+ description: 'The full basis behind this server\'s answers: which build it loaded, which build is on disk now, and fingerprints of the scan and spec corpus. Compare loadedBuild against diskBuild to see whether the running server predates the installed code.',
36
+ inputSchema: {
37
+ type: 'object',
38
+ properties: {
39
+ root: { type: 'string', description: 'Absolute path to the project root. Optional — omit for build identity only.' },
40
+ },
41
+ },
42
+ },
43
+ spec_create: {
44
+ description: 'Create a new spec (.md) of the given kind with its required sections stubbed as TODO. Extra keys are stored as frontmatter.',
45
+ inputSchema: {
46
+ type: 'object',
47
+ properties: {
48
+ type: str('Spec kind, e.g. REQ | H-SPEC | A-SPEC | C-SPEC | T-SPEC.'),
49
+ // @implements A-SPEC-174
50
+ // The rule belongs HERE, where the caller reads it before choosing a value. Stating only an
51
+ // example left `REQ-1` looking reasonable, and the id is the filename — a wrong one costs
52
+ // the author the whole document. The guard in the handler is the backstop, not the teacher.
53
+ id: str('Spec id. The number must be at least 3 digits: REQ-001, H-SPEC-001, A-SPEC-001 '
54
+ + '(A-SPEC may add a dot suffix for a sub-slice, e.g. A-SPEC-125.4). Shorter numbers are refused.'),
55
+ title: str('Human-readable spec title.'),
56
+ // The relation is GENERATED, not restated — a hand-written copy drifted and prose assertions
57
+ // could not detect it.
58
+ depends_on: strArray(`Ids of the PARENT specs. The kind is fixed: ${(0, spec_types_1.parentRuleText)()}. `
59
+ + 'A parent that does not exist yet is fine; a parent of the wrong kind is refused.'),
60
+ // @implements A-SPEC-189 §6 — CONTROL arguments must be declared even where extra keys are
61
+ // data. `additionalProperties: true` exempts this tool from typo judgement (caller-wins
62
+ // frontmatter), so an undeclared `root` meant a misspelled `rot: /Users/<name>/…` was
63
+ // written into a git-shared document as frontmatter — a machine-local absolute path,
64
+ // silently, with no refusal. Declaring it makes the control surface visible; the data
65
+ // exemption stays for genuine frontmatter.
66
+ root: ROOT,
67
+ },
68
+ required: ['type', 'id', 'title'],
69
+ additionalProperties: true,
70
+ },
71
+ },
72
+ spec_validate: {
73
+ description: 'Validate a single spec by id against its kind rules and dependency chain; returns { ok, findings }.',
74
+ inputSchema: {
75
+ type: 'object',
76
+ properties: { id: str('Id of the spec to validate.') },
77
+ required: ['id'],
78
+ },
79
+ },
80
+ spec_upgrade: {
81
+ description: 'Raise ONE named spec from an older format to the current one: declare `type:` (inferred from the id), preserve the previous status as `legacy_status`, and restart at `status: draft`. Does NOT confer approval, and never touches a document you did not name — there is no bulk path. Refuses a kind this tool does not have.',
82
+ inputSchema: {
83
+ type: 'object',
84
+ properties: { id: str('Id of the spec to raise to the current format.') },
85
+ required: ['id'],
86
+ },
87
+ },
88
+ spec_approve: {
89
+ description: 'Approve a spec as a sealing ACT: confirm the ledger destination BEFORE sealing → validate (zero errors) → record approved_digest + parent_digests snapshots → status: approved (written only at the version this act read; a concurrent edit wins and the approval is refused for retry) → append spec-approved to the provenance ledger. Requires a valid out-of-band HOLMES_APPROVAL in the SERVER environment (fail-closed; nothing in the request can substitute). Refuses an unsealed approved parent — seal parents first.',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ root: str('Optional when the server is bound to a file store — the ledger location is derived from the store itself; if supplied it must resolve to the SAME project, otherwise the approval is refused before anything is written.'),
94
+ id: str('Id of the spec to approve and seal.'),
95
+ },
96
+ required: ['id'],
97
+ },
98
+ },
99
+ spec_list: {
100
+ description: `List all specs (id, type, status; plus parent when depends_on names one, and legacy: true when the status is outside the canonical ${spec_types_1.SPEC_STATUSES.join('|')} union — absence of legacy speaks only to the STATUS being canonical, not to governance), optionally filtered to a single kind.`,
101
+ inputSchema: {
102
+ type: 'object',
103
+ properties: { type: str('Optional spec kind to filter by (e.g. A-SPEC).') },
104
+ },
105
+ },
106
+ spec_next: {
107
+ description: 'Return the next spec kind in the pipeline that has no approved instance yet ({ next }).',
108
+ inputSchema: { type: 'object', properties: {} },
109
+ },
110
+ citation_pin: {
111
+ description: "Compute and record the content digest (sha256) of a REQ's cited sources that resolve inside the repo, so a citation's rev is derived from what is actually on disk. Dry-run by default; never overwrites an existing digest.",
112
+ inputSchema: {
113
+ type: 'object',
114
+ properties: {
115
+ root: ROOT_ANY,
116
+ id: { type: 'string', description: 'REQ id whose citations should be pinned, e.g. REQ-126.' },
117
+ dryRun: { type: 'boolean', description: 'Default true — pass false to write the digests into the spec.' },
118
+ },
119
+ required: ['root', 'id'],
120
+ },
121
+ },
122
+ rtm_check: {
123
+ description: 'Check the governed spec set for RTM integrity (orphans, duplicate ids, dangling/wrong-kind @implements). With root, also checks code @implements anchors.',
124
+ inputSchema: {
125
+ type: 'object',
126
+ properties: { root: ROOT },
127
+ },
128
+ },
129
+ phase_status: {
130
+ description: 'Return every spec with its id/type/status (raw material for deriving phase via phase_check). '
131
+ + 'The corpus is the store this server is BOUND to; a root naming a different project is refused rather than answered '
132
+ + "with this server's specs (round-9: it was advertised, ignored, and answered success-shaped for someone else's root).",
133
+ inputSchema: {
134
+ type: 'object',
135
+ properties: { root: ROOT },
136
+ },
137
+ },
138
+ phase_check: {
139
+ description: 'Gate an edit against the No-Spec-No-Code phase machine for a target path; returns an allow/deny decision with remediation.',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {
143
+ target: str('Repo-relative path of the file about to be written/edited.'),
144
+ action: {
145
+ type: 'string',
146
+ // The handler's own runtime list — a hand-copied literal here was a second truth that
147
+ // drifts (round-3: two members deleted survived the suite while the wire falsely refused
148
+ // actions phaseCheck accepts).
149
+ enum: [...phase_1.ACTIONS],
150
+ description: 'Optional pre-classified Action; omit to classify from `target`.',
151
+ },
152
+ targetAspecId: str('A-SPEC id this edit implements (e.g. A-SPEC-121.1).'),
153
+ },
154
+ required: ['target'],
155
+ },
156
+ },
157
+ cpg_scan: {
158
+ description: 'Scan the repository tree with tree-sitter across the 8 supported languages; returns { files, symbols } counts plus the skip report ({ skipped, skippedCount }) naming any claimed file the scan could not ingest — an empty report distinguishes a clean tree from one with casualties.',
159
+ inputSchema: {
160
+ type: 'object',
161
+ properties: { root: ROOT },
162
+ required: ['root'],
163
+ },
164
+ },
165
+ taint_scan: {
166
+ description: 'Call-graph taint REACHABILITY screen: source-named functions (getenv/argv/req.body/…) that reach sink-named functions (exec/eval/query/…) through call edges. Returns { kind:"call-reachability", limits, maxPaths, truncated, pairs } where each pair is a source→sink candidate call path. This is a SCREENING signal that routes a security review — NOT a data-flow proof and NOT a vulnerability verdict (no def-use, no sanitizers, name-based matching, call-edge-only, as the limits state).',
167
+ inputSchema: {
168
+ type: 'object',
169
+ properties: { root: ROOT },
170
+ required: ['root'],
171
+ },
172
+ },
173
+ test_run: {
174
+ description: 'Compute the intelligent regression scope, RUN the selected tier (jest or pytest), and record per-A-SPEC EXECUTION evidence. Scopes against a git range when given, otherwise against a baseline — so it works on a project with no version control. A passing run records the baseline the next run scopes against.',
175
+ inputSchema: {
176
+ type: 'object',
177
+ properties: {
178
+ root: ROOT_ANY,
179
+ base: str('Base rev (e.g. HEAD~1). Optional — omit to compare against a baseline.'),
180
+ head: str('Head rev (e.g. HEAD). Optional — omit to compare against a baseline.'),
181
+ since: str('Baseline label to compare against when no git range is given (default `last-green`, recorded by a passing test_run). Works with or without version control.'),
182
+ mark: str('Baseline label to record when the run passes (default `last-green`). Nothing is recorded for a red or skipped run.'),
183
+ },
184
+ required: ['root'],
185
+ },
186
+ },
187
+ issue_localize: {
188
+ description: 'N1 localization: free-text issue -> structured, ranked candidate locations (files+symbols+evidence) fusing CPG lexical match with the RTM spec link ({ terms, hits, matchedSpecs }).',
189
+ inputSchema: {
190
+ type: 'object',
191
+ properties: {
192
+ root: ROOT_ANY,
193
+ issue: str('Free-text issue/bug/task description to localize.'),
194
+ topN: num('Max ranked hits to return (default 10).'),
195
+ },
196
+ required: ['root', 'issue'],
197
+ },
198
+ },
199
+ rtm_impact: {
200
+ description: 'Given changed symbol qualified-names, return the impacted SPEC node ids reachable through @implements/depends_on edges ({ impacted }).',
201
+ inputSchema: {
202
+ type: 'object',
203
+ properties: {
204
+ root: ROOT_ANY,
205
+ changed: strArray('Changed symbol qualified-names (e.g. ["CpgScanner","CpgScanner.scan"]) — NOT file paths.'),
206
+ },
207
+ required: ['root', 'changed'],
208
+ },
209
+ },
210
+ rtm_reindex: {
211
+ description: 'Rebuild the RTM graph over base..head changes and exercise the incremental machinery; returns { changed, nodes, edges }.',
212
+ inputSchema: {
213
+ type: 'object',
214
+ properties: {
215
+ root: ROOT_ANY,
216
+ base: str('Base git ref of the diff range. Optional — omit to compare against a baseline.'),
217
+ head: str('Head git ref of the diff range. Optional — omit to compare against a baseline.'),
218
+ since: str('Baseline label to compare against when no git range is given (default `last-green`, recorded by a passing test_run). Works with or without version control.'),
219
+ },
220
+ required: ['root'],
221
+ },
222
+ },
223
+ context_bundle: {
224
+ description: 'Assemble a JIT context bundle rooted at a seed node id within a token budget (graph-scoped spec+code slice).',
225
+ inputSchema: {
226
+ type: 'object',
227
+ properties: {
228
+ root: ROOT,
229
+ seedId: str('Seed node id to expand from, e.g. SPEC:A-SPEC-121.1 or CODE:CpgScanner@src/holmes/cpg/cpg-scanner.ts.'),
230
+ budget: num('Token budget for the assembled bundle.'),
231
+ },
232
+ required: ['root', 'seedId', 'budget'],
233
+ },
234
+ },
235
+ review_scope: {
236
+ description: 'Compute the graph-scoped review scope for base..head (impacted specs/tests + unscanned changed files).',
237
+ inputSchema: {
238
+ type: 'object',
239
+ properties: { root: ROOT_ANY, base: str('Base git ref. Optional — omit to compare against a baseline instead.'), head: str('Head git ref. Optional — omit to compare against a baseline instead.'), since: str('Baseline label to compare against when no git range is given (default `last-green`, recorded by a passing test_run). Works with or without version control.'), },
240
+ required: ['root'],
241
+ },
242
+ },
243
+ review_prepare: {
244
+ description: 'Assemble the full review package for base..head within a token budget ({ package, unscannedChangedFiles }).',
245
+ inputSchema: {
246
+ type: 'object',
247
+ properties: {
248
+ root: ROOT_ANY,
249
+ base: str('Base git ref. Optional — omit to compare against a baseline instead.'),
250
+ head: str('Head git ref. Optional — omit to compare against a baseline instead.'),
251
+ since: str('Baseline label to compare against when no git range is given (default `last-green`, recorded by a passing test_run). Works with or without version control.'),
252
+ budget: num('Optional token budget for the review package (default 4000).'),
253
+ },
254
+ required: ['root'],
255
+ },
256
+ },
257
+ review_record: {
258
+ description: 'Append review findings to the findings ledger; severities must be critical|important|minor and statuses open|resolved.',
259
+ inputSchema: {
260
+ type: 'object',
261
+ properties: {
262
+ root: ROOT,
263
+ findings: {
264
+ type: 'array',
265
+ description: 'Findings to record.',
266
+ items: {
267
+ type: 'object',
268
+ properties: {
269
+ id: str('Finding id.'),
270
+ severity: { type: 'string', enum: ['critical', 'important', 'minor'], description: 'Finding severity.' },
271
+ status: { type: 'string', enum: ['open', 'resolved'], description: 'Finding status.' },
272
+ // 'summary' matches what the ledger actually persists — the advertised 'message'
273
+ // was silently DROPPED by the serializer's whitelist, so clients following the ad
274
+ // lost their text (round-2 finding; same schema-vs-handler class as phase_check).
275
+ summary: str('Finding description — persisted to the ledger and quoted by ART-7 violations.'),
276
+ category: str('Finding category (e.g. correctness, security) — persisted to the ledger; round-5: it was persisted but never advertised, so ad-following clients wrote category-less audit lines.'),
277
+ // Round-9: the same defect class as `category`, two keys further — both survive the
278
+ // ledger whitelist, neither was advertised, so ad-following clients wrote findings
279
+ // that could not be located.
280
+ file: str('File the finding is in — persisted to the ledger.'),
281
+ specRef: str('Spec id the finding is about — persisted to the ledger.'),
282
+ },
283
+ required: ['id', 'severity', 'status'],
284
+ },
285
+ },
286
+ },
287
+ required: ['root', 'findings'],
288
+ },
289
+ },
290
+ review_status: {
291
+ description: 'Summarize open findings by severity and whether the review is blocked ({ blocked, open }).',
292
+ inputSchema: {
293
+ type: 'object',
294
+ properties: { root: ROOT },
295
+ required: ['root'],
296
+ },
297
+ },
298
+ // @implements A-SPEC-126
299
+ reverse_scan: {
300
+ description: 'Brownfield inventory: scan a legacy target read-only and return candidate A-SPEC clusters, cohesion evidence, and an honest coverage report (unresolved wiring, uncalibrated languages). Writes nothing.',
301
+ inputSchema: {
302
+ type: 'object',
303
+ properties: {
304
+ root: str('Absolute path to the target repository root. Unlike the other tools this need not be a git repository — a non-git tree is scanned whole and reported as isGit:false.'),
305
+ maxFlagged: num('Cap on the listed dynamic-wiring files (default 50). Counts stay complete.'),
306
+ },
307
+ required: ['root'],
308
+ },
309
+ },
310
+ reverse_draft: {
311
+ description: "Draft H-SPEC/A-SPEC/T-SPEC documents for the recovered clusters under an EXISTING parent REQ. Refuses without one — a REQ states business intent, which is not in the code. Everything is emitted as status:draft; dry-run by default. Documents land in the SERVER's spec store (HOLMES_SPECS, default .ax/specs), which is the target's own store only when the server runs inside it.",
312
+ inputSchema: {
313
+ type: 'object',
314
+ properties: {
315
+ root: str('Absolute path to the target repository root.'),
316
+ parentReqId: str('parentReqId is required. A REQ states business intent, which does not exist in the code and cannot be recovered from it — write the REQ first, then re-run with its id (e.g. REQ-126). No code path creates a REQ.'),
317
+ cluster: str('Optional cluster key (its directory, or the file itself for a per-file split) to draft only that slice.'),
318
+ dryRun: { type: 'boolean', description: 'Default true — pass false to write the drafted documents into the spec store.' },
319
+ },
320
+ required: ['root', 'parentReqId'],
321
+ },
322
+ },
323
+ reverse_anchor: {
324
+ description: 'Insert `@implements` anchors into source files for a file→A-SPEC mapping. Dry-run by default; anchoring to a non-approved A-SPEC is refused.',
325
+ inputSchema: {
326
+ type: 'object',
327
+ properties: {
328
+ root: str('Absolute path to the target repository root.'),
329
+ mapping: {
330
+ type: 'array',
331
+ description: 'File→A-SPEC pairs to anchor.',
332
+ items: {
333
+ type: 'object',
334
+ properties: {
335
+ file: str('Repo-root-relative POSIX path of the file to anchor.'),
336
+ aspec: str('Target A-SPEC id. Must be approved.'),
337
+ },
338
+ required: ['file', 'aspec'],
339
+ },
340
+ },
341
+ dryRun: { type: 'boolean', description: 'Default true — pass false to actually write the anchors.' },
342
+ },
343
+ required: ['root', 'mapping'],
344
+ },
345
+ },
346
+ risk_check: {
347
+ description: 'Assess a risk Action, apply the graded HITL gate, and record a DecisionLedger entry; returns { assessment, gate }.',
348
+ inputSchema: {
349
+ type: 'object',
350
+ properties: {
351
+ action: {
352
+ type: 'object',
353
+ description: 'The action to assess.',
354
+ properties: {
355
+ kind: str('Action kind, e.g. shell | file.'),
356
+ target: str('Action target (e.g. the shell command string or file path).'),
357
+ command: str('Full shell command, when kind is shell.'),
358
+ },
359
+ required: ['kind', 'target'],
360
+ },
361
+ ts: str('ISO-8601 timestamp for the DecisionLedger entry.'),
362
+ root: ROOT_ANY,
363
+ changedSymbols: strArray('Optional changed symbol qualified-names, to compute a blast-radius axis via impact analysis.'),
364
+ // @implements A-SPEC-189 §6 — the ad declared {actor, rationale}, but the gate requires
365
+ // `token` too (isValidApproval): an approval built exactly as advertised was IGNORED in
366
+ // silence, and the caller read the resulting block as a policy decision. The ad now names
367
+ // every field the gate reads, and says which ones narrow it.
368
+ approval: {
369
+ type: 'object',
370
+ description: 'Optional out-of-band approval token (never settable from AI/tool-input). '
371
+ + 'actor, token and rationale are ALL required for the gate to honor it; scope/expires/nonce narrow it '
372
+ + '(scope entries are {kind, pattern} full-match, expires is an ISO instant, nonce makes it single-use).',
373
+ properties: {
374
+ actor: str('Approving actor.'),
375
+ token: str('Approval token — REQUIRED; an approval without it is ignored by the gate.'),
376
+ rationale: str('Approval rationale.'),
377
+ expires: str('Optional ISO instant after which the approval is refused (unparseable values fail closed).'),
378
+ nonce: str('Optional single-use marker — spent once, then refused like an absent approval. A blank value is refused.'),
379
+ scope: {
380
+ type: 'array',
381
+ description: 'Optional narrowing: the approval covers only these {kind, pattern} entries (full match; trailing "/" is a directory prefix).',
382
+ items: { type: 'object', properties: { kind: str('Action kind, or "*".'), pattern: str('Target pattern; "*" is a wildcard run.') }, required: ['kind', 'pattern'] },
383
+ },
384
+ },
385
+ required: ['actor', 'token', 'rationale'],
386
+ },
387
+ enforcement: { ...str('Optional enforcement mode override.'), enum: ['block', 'warn', 'off'] },
388
+ actor: str('Actor attributed in the ledger entry.'),
389
+ rationale: str('Rationale attributed in the ledger entry.'),
390
+ },
391
+ required: ['action', 'ts'],
392
+ },
393
+ },
394
+ };