@juzser/blacksmith 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 (172) hide show
  1. package/.claude/agents/auditor.md +146 -0
  2. package/.claude/agents/coder.md +164 -0
  3. package/.claude/agents/grader.md +110 -0
  4. package/.claude/agents/merger.md +107 -0
  5. package/.claude/agents/planner.md +92 -0
  6. package/.claude/agents/researcher.md +105 -0
  7. package/.claude/agents/reviewer.md +171 -0
  8. package/.claude/agents/scribe.md +72 -0
  9. package/.claude/agents/security-reviewer.md +130 -0
  10. package/.claude/agents/spec-reviewer.md +215 -0
  11. package/.claude/agents/tester.md +121 -0
  12. package/.claude/agents/uiux.md +126 -0
  13. package/.claude/agents/verifier.md +103 -0
  14. package/.claude/agents/wave-runner.md +74 -0
  15. package/LICENSE +21 -0
  16. package/README.md +389 -0
  17. package/factory/orchestrator/dist/actors.js +46 -0
  18. package/factory/orchestrator/dist/agents-registry.js +308 -0
  19. package/factory/orchestrator/dist/args.js +84 -0
  20. package/factory/orchestrator/dist/artifacts.js +111 -0
  21. package/factory/orchestrator/dist/attribution.js +160 -0
  22. package/factory/orchestrator/dist/audit.js +812 -0
  23. package/factory/orchestrator/dist/autonomy.js +151 -0
  24. package/factory/orchestrator/dist/budgetAlarm.js +366 -0
  25. package/factory/orchestrator/dist/budgets.js +196 -0
  26. package/factory/orchestrator/dist/claims.js +633 -0
  27. package/factory/orchestrator/dist/cli.js +3185 -0
  28. package/factory/orchestrator/dist/commit.js +152 -0
  29. package/factory/orchestrator/dist/coverage.js +188 -0
  30. package/factory/orchestrator/dist/crossFinding.js +479 -0
  31. package/factory/orchestrator/dist/crosscheck.js +396 -0
  32. package/factory/orchestrator/dist/daemon.js +707 -0
  33. package/factory/orchestrator/dist/db/projector.js +1317 -0
  34. package/factory/orchestrator/dist/db/queries.js +1714 -0
  35. package/factory/orchestrator/dist/db/schema.js +305 -0
  36. package/factory/orchestrator/dist/delegation.js +400 -0
  37. package/factory/orchestrator/dist/diffstat.js +164 -0
  38. package/factory/orchestrator/dist/dispatchAudit.js +275 -0
  39. package/factory/orchestrator/dist/dotenv.js +72 -0
  40. package/factory/orchestrator/dist/effort.js +202 -0
  41. package/factory/orchestrator/dist/epic.js +1018 -0
  42. package/factory/orchestrator/dist/epicWidth.js +238 -0
  43. package/factory/orchestrator/dist/errorIssues.js +249 -0
  44. package/factory/orchestrator/dist/errors.js +11 -0
  45. package/factory/orchestrator/dist/escalation.js +500 -0
  46. package/factory/orchestrator/dist/events.js +1015 -0
  47. package/factory/orchestrator/dist/findingAge.js +91 -0
  48. package/factory/orchestrator/dist/findingContext.js +103 -0
  49. package/factory/orchestrator/dist/findings.js +1156 -0
  50. package/factory/orchestrator/dist/gate.js +911 -0
  51. package/factory/orchestrator/dist/gh.js +146 -0
  52. package/factory/orchestrator/dist/git.js +87 -0
  53. package/factory/orchestrator/dist/goalCheck.js +380 -0
  54. package/factory/orchestrator/dist/graph.js +61 -0
  55. package/factory/orchestrator/dist/hookDecision.js +106 -0
  56. package/factory/orchestrator/dist/immutability.js +171 -0
  57. package/factory/orchestrator/dist/impact.js +280 -0
  58. package/factory/orchestrator/dist/integration.js +141 -0
  59. package/factory/orchestrator/dist/judgePreflight.js +155 -0
  60. package/factory/orchestrator/dist/judges.js +310 -0
  61. package/factory/orchestrator/dist/lessonAudit.js +399 -0
  62. package/factory/orchestrator/dist/lessons.js +1329 -0
  63. package/factory/orchestrator/dist/mcp.js +555 -0
  64. package/factory/orchestrator/dist/paths.js +82 -0
  65. package/factory/orchestrator/dist/plan.js +502 -0
  66. package/factory/orchestrator/dist/planQuorum.js +421 -0
  67. package/factory/orchestrator/dist/policy.js +1308 -0
  68. package/factory/orchestrator/dist/policyHook.js +28 -0
  69. package/factory/orchestrator/dist/preconditions.js +47 -0
  70. package/factory/orchestrator/dist/projects.js +134 -0
  71. package/factory/orchestrator/dist/prompts.js +57 -0
  72. package/factory/orchestrator/dist/provenance.js +201 -0
  73. package/factory/orchestrator/dist/providers/api-transport.js +117 -0
  74. package/factory/orchestrator/dist/providers/cli-transport.js +208 -0
  75. package/factory/orchestrator/dist/providers/index.js +54 -0
  76. package/factory/orchestrator/dist/providers/schema-validate.js +152 -0
  77. package/factory/orchestrator/dist/providers/types.js +49 -0
  78. package/factory/orchestrator/dist/queue.js +263 -0
  79. package/factory/orchestrator/dist/quorum.js +347 -0
  80. package/factory/orchestrator/dist/quorumEscalations.js +132 -0
  81. package/factory/orchestrator/dist/results.js +65 -0
  82. package/factory/orchestrator/dist/roadmap.js +437 -0
  83. package/factory/orchestrator/dist/runtime.js +86 -0
  84. package/factory/orchestrator/dist/sameMistakeKpi.js +243 -0
  85. package/factory/orchestrator/dist/sandbox.js +165 -0
  86. package/factory/orchestrator/dist/scaffold.js +373 -0
  87. package/factory/orchestrator/dist/scheduler.js +408 -0
  88. package/factory/orchestrator/dist/schemas.js +195 -0
  89. package/factory/orchestrator/dist/security.js +222 -0
  90. package/factory/orchestrator/dist/severity.js +293 -0
  91. package/factory/orchestrator/dist/spec.js +376 -0
  92. package/factory/orchestrator/dist/specChange.js +497 -0
  93. package/factory/orchestrator/dist/stack.js +324 -0
  94. package/factory/orchestrator/dist/symbols.js +966 -0
  95. package/factory/orchestrator/dist/taskEvents.js +399 -0
  96. package/factory/orchestrator/dist/taskId.js +126 -0
  97. package/factory/orchestrator/dist/taxonomy.js +113 -0
  98. package/factory/orchestrator/dist/testSelect.js +129 -0
  99. package/factory/orchestrator/dist/testerAudit.js +264 -0
  100. package/factory/orchestrator/dist/testgate.js +171 -0
  101. package/factory/orchestrator/dist/usage.js +874 -0
  102. package/factory/orchestrator/dist/waivers.js +228 -0
  103. package/factory/orchestrator/dist/waveBudget.js +223 -0
  104. package/factory/orchestrator/dist/waveConcurrency.js +250 -0
  105. package/factory/orchestrator/dist/waveNext.js +210 -0
  106. package/factory/orchestrator/dist/waveSchedule.js +146 -0
  107. package/factory/orchestrator/dist/worktree.js +207 -0
  108. package/factory/orchestrator/drizzle/0000_broken_rockslide.sql +183 -0
  109. package/factory/orchestrator/drizzle/0001_luxuriant_zaladane.sql +1 -0
  110. package/factory/orchestrator/drizzle/0002_yellow_wild_pack.sql +8 -0
  111. package/factory/orchestrator/drizzle/0003_damp_riptide.sql +12 -0
  112. package/factory/orchestrator/drizzle/0004_broad_lady_vermin.sql +1 -0
  113. package/factory/orchestrator/drizzle/0005_romantic_jocasta.sql +2 -0
  114. package/factory/orchestrator/drizzle/0006_bent_madelyne_pryor.sql +16 -0
  115. package/factory/orchestrator/drizzle/0007_overjoyed_goliath.sql +1 -0
  116. package/factory/orchestrator/drizzle/0008_smart_iron_patriot.sql +2 -0
  117. package/factory/orchestrator/drizzle/0009_agents_epic_id.sql +2 -0
  118. package/factory/orchestrator/drizzle/0010_milestone_kind.sql +1 -0
  119. package/factory/orchestrator/drizzle/meta/_journal.json +83 -0
  120. package/factory/policies/budgets.yml +262 -0
  121. package/factory/policies/crosscheck.yml +308 -0
  122. package/factory/policies/delegation.yml +44 -0
  123. package/factory/policies/effort.yml +142 -0
  124. package/factory/policies/guardrails.yml +306 -0
  125. package/factory/policies/lessons.md +239 -0
  126. package/factory/policies/scheduler.yml +90 -0
  127. package/factory/policies/sensitive-paths.yml +148 -0
  128. package/factory/policies/severity.yml +83 -0
  129. package/factory/policies/stack.yml +60 -0
  130. package/factory/policies/taxonomy.yml +340 -0
  131. package/factory/policies/worktree.yml +95 -0
  132. package/factory/scaffold/base/.github/workflows/ci.yml +34 -0
  133. package/factory/scaffold/base/.gitignore.tmpl +11 -0
  134. package/factory/scaffold/base/AGENTS.md +33 -0
  135. package/factory/scaffold/base/README.md +25 -0
  136. package/factory/scaffold/base/biome.json.tmpl +30 -0
  137. package/factory/scaffold/base/package.json +25 -0
  138. package/factory/scaffold/base/pnpm-workspace.yaml.tmpl +16 -0
  139. package/factory/scaffold/base/src/index.ts +4 -0
  140. package/factory/scaffold/base/test/index.test.ts +8 -0
  141. package/factory/scaffold/base/tsconfig.json +20 -0
  142. package/factory/scaffold/base/tsconfig.test.json +9 -0
  143. package/factory/scaffold/base/vitest.config.ts +29 -0
  144. package/factory/scaffold/mcp/mcp.manifest.json +22 -0
  145. package/factory/scaffold/mcp/package.fragment.json +9 -0
  146. package/factory/scaffold/mcp/src/mcp/README.md +63 -0
  147. package/factory/scaffold/mcp/src/mcp/guard.ts +110 -0
  148. package/factory/scaffold/mcp/src/mcp/redact.ts +69 -0
  149. package/factory/scaffold/mcp/src/mcp/server.ts +55 -0
  150. package/factory/scaffold/mcp/src/mcp/tools/health.ts +60 -0
  151. package/factory/scaffold/mcp/test/mcp/guard.test.ts +108 -0
  152. package/factory/scaffold/mcp/test/mcp/redact.test.ts +61 -0
  153. package/factory/scaffold/mcp/test/mcp/surface.test.ts +100 -0
  154. package/factory/scaffold/ui/index.html +12 -0
  155. package/factory/scaffold/ui/package.fragment.json +13 -0
  156. package/factory/scaffold/ui/src/App.vue +10 -0
  157. package/factory/scaffold/ui/src/main.ts +5 -0
  158. package/factory/scaffold/ui/src/shims.d.ts +11 -0
  159. package/factory/scaffold/ui/vite.config.ts +20 -0
  160. package/factory/scaffold/ui-tailwind/package.fragment.json +6 -0
  161. package/factory/scaffold/ui-tailwind/vite.config.ts +26 -0
  162. package/factory/specs/schema/event.schema.json +82 -0
  163. package/factory/specs/schema/finding-evidence.schema.json +45 -0
  164. package/factory/specs/schema/finding.schema.json +115 -0
  165. package/factory/specs/schema/grader-verdict.schema.json +50 -0
  166. package/factory/specs/schema/judge-verdict.schema.json +19 -0
  167. package/factory/specs/schema/lesson.schema.json +85 -0
  168. package/factory/specs/schema/mcp-manifest.schema.json +230 -0
  169. package/factory/specs/schema/result.schema.json +81 -0
  170. package/factory/specs/schema/spec-change-request.schema.json +46 -0
  171. package/factory/specs/schema/task-spec.schema.json +134 -0
  172. package/package.json +98 -0
@@ -0,0 +1,874 @@
1
+ /**
2
+ * What `smith` can be asked to do, written down once (P9-21).
3
+ *
4
+ * Before this, `smith` and `smith --help` both answered
5
+ * `{"error":{"message":"Unknown command: "}}`, so the only way to learn a
6
+ * command's argument shape was to read cli.ts or SKILL.md — and the usage
7
+ * strings that DID exist were literals typed at each call site of
8
+ * requirePositionals, invisible until you had already made the mistake.
9
+ *
10
+ * This table is the single register. cli.ts reads its usage lines from here
11
+ * instead of retyping them, prints it for `--help`, and refuses a command it
12
+ * does not contain before dispatch — which makes "documented ⊇ reachable"
13
+ * structural. test/usage.test.ts parses cli.ts's dispatcher and asserts the
14
+ * reverse inclusion, so a new command cannot be dispatched without a line
15
+ * describing it.
16
+ *
17
+ * It lives here, not in cli.ts, because cli.ts calls main() at import time:
18
+ * anything declared there can only be tested by running the binary.
19
+ */
20
+ import { SmithError } from './errors.js';
21
+ /** The event-writing flags shared by every command that appends to a session log. */
22
+ const EVENTS = '--session <id> --causal-parent <event-id> [--plan-version <n>] [--actor <name>]';
23
+ /** …plus the state-dir override, on the commands whose module accepts EventOpts. */
24
+ const EVENTS_DIR = `${EVENTS} [--state-dir <dir>]`;
25
+ /**
26
+ * Every `stats` page reads the same projection. `--lineage` widens `--session`
27
+ * from one session to the chain it continues (D-263): an epic that outlasts a
28
+ * window carries on in a session whose root names the first, and without this
29
+ * every page answered with the part of that epic which fell inside the window
30
+ * asked about.
31
+ */
32
+ const STATS = '[--db <file>] [--session <id>] [--lineage]';
33
+ export const COMMANDS = [
34
+ {
35
+ command: 'audit open',
36
+ positionals: '<project-dir>',
37
+ flags: EVENTS_DIR,
38
+ summary: 'Open an audit on a project: cut a detached read-only worktree at HEAD, fingerprint it, write .blacksmith/audit.json, print the axes and the live findings.',
39
+ },
40
+ {
41
+ command: 'audit record',
42
+ positionals: '<project-dir>',
43
+ flags: `--axis <performance|code-quality|architecture|security> --evidence <file> ${EVENTS_DIR}`,
44
+ summary: "Record one axis's evidence: validate, fingerprint, drop what the store already suppresses, append the rest as raised.",
45
+ },
46
+ {
47
+ command: 'audit consolidate',
48
+ positionals: '<project-dir>',
49
+ flags: '',
50
+ summary: 'Fold the store and print the path-clusters ranked by severity, convergence, confidence. Reads only.',
51
+ },
52
+ {
53
+ command: 'audit decide',
54
+ positionals: '<project-dir>',
55
+ flags: `--fingerprint <fp> --decision <accept|decline|merge> [--same-as <fp>] [--note <text>] ${EVENTS_DIR}`,
56
+ summary: "Append the operator's answer on one finding. merge requires --same-as; accept and decline refuse it.",
57
+ },
58
+ {
59
+ command: 'audit cut',
60
+ positionals: '<project-dir>',
61
+ flags: `--epic <epic-id> --title <text> ${EVENTS_DIR}`,
62
+ summary: 'Render a roadmap milestone and an epic spec from the accepted findings and stamp the epic id onto each.',
63
+ },
64
+ {
65
+ command: 'audit resolve',
66
+ positionals: '<project-dir>',
67
+ flags: `--epic <epic-id> ${EVENTS_DIR}`,
68
+ summary: 'Append a fixed line for every finding the epic carried. Run from the epic-close step.',
69
+ },
70
+ {
71
+ command: 'audit close',
72
+ positionals: '<project-dir>',
73
+ flags: `[--force] ${EVENTS_DIR}`,
74
+ summary: 'Verify the worktree against its opening fingerprint, remove it, delete the manifest, close the audit in the log.',
75
+ },
76
+ {
77
+ command: 'plan validate',
78
+ positionals: '<plan.json>',
79
+ flags: '',
80
+ summary: 'Check a plan against the task schema, the taxonomy, and the edge DAG. Exit 1 on red.',
81
+ },
82
+ {
83
+ command: 'plan diff',
84
+ positionals: '<plan-a.json> <plan-b.json>',
85
+ flags: '',
86
+ summary: 'Report which tasks were added, removed, superseded, or carried between two versions.',
87
+ },
88
+ {
89
+ command: 'plan quorum',
90
+ positionals: '',
91
+ flags: '--epic <id> --plan-version <n> --session <id> --causal-parent <event-id> [--confidence <0-1>] [--actor <name>] [--state-dir <dir>]',
92
+ summary: 'Critique-only review of a drafted plan. Exit 1 means the operator must look first.',
93
+ },
94
+ {
95
+ command: 'plan amend',
96
+ positionals: '',
97
+ flags: `--plan <plan.json> --findings <id[,id...]> --rationale <text> --sites <path[,path...]> [--changes <changes.json>] [--specs-dir <dir>] ${EVENTS_DIR}`,
98
+ summary: 'Cut a new plan version against the spec findings that forced it. No finding, no amendment. --sites is every place the shape occurs, not only where it was reported.',
99
+ },
100
+ {
101
+ command: 'plan propose',
102
+ positionals: '',
103
+ flags: `--plan <plan.json> --task <task-id> --proposed-by <role> --request <request.json> [--proposed-by-provider <name>] [--specs-dir <dir>] ${EVENTS_DIR}`,
104
+ summary: "Record a worker's spec_change_request: the criterion it found wrong and the plan diff it proposes. Validated against the plan's own validator now, so an unappliable proposal never reaches your queue. Writes no plan version.",
105
+ },
106
+ {
107
+ command: 'plan proposals',
108
+ positionals: '',
109
+ flags: '--session <id> [--epic <id>] [--task <task-id>] [--status <status>] [--specs-dir <dir>] [--state-dir <dir>]',
110
+ summary: 'List spec change proposals with their diffs. --status is open, approved, rejected or stale; stale means a newer plan version has already overtaken the one the proposal was written against.',
111
+ },
112
+ {
113
+ command: 'plan approve',
114
+ positionals: '<proposal-id>',
115
+ flags: `--plan <plan.json> --decided-by <role> [--rationale <text>] [--specs-dir <dir>] ${EVENTS_DIR}`,
116
+ summary: "Approve a proposal and cut the version it asks for. Runs `plan amend` with the worker's own finding, sites and diff; --rationale overrides the argument it recorded.",
117
+ },
118
+ {
119
+ command: 'plan reject',
120
+ positionals: '<proposal-id>',
121
+ flags: `--decided-by <role> --rationale <text> [--specs-dir <dir>] ${EVENTS_DIR}`,
122
+ summary: 'Reject a proposal and refute the finding it raised. --rationale is required: the log already holds the case for, and a rejection is the only place the case against is written down.',
123
+ },
124
+ {
125
+ command: 'plan ingest',
126
+ positionals: '<plan.json>',
127
+ flags: EVENTS_DIR,
128
+ summary: 'Record every task and dependency the plan declares, before any wave names them.',
129
+ },
130
+ {
131
+ command: 'wave check',
132
+ positionals: '<plan.json> <task-id>...',
133
+ flags: `[--dry] [--budget-policy <file>] [--override-rationale <text>] [--repo <dir>] ${EVENTS_DIR}`,
134
+ summary: 'Admit a wave whose claims are disjoint, whose files share no import edge, and whose ' +
135
+ 'declared cost fits under the epic token cap. --dry asks without writing the admission; ' +
136
+ '--override-rationale admits a cost-refused wave and records the machine verdict beside ' +
137
+ 'the human reason; --repo names the checkout the symbol graph is read from.',
138
+ },
139
+ {
140
+ command: 'wave next',
141
+ positionals: '<plan.json>',
142
+ flags: `[--repo <dir>] ${EVENTS_DIR}`,
143
+ summary: 'Compute the widest wave this plan admits right now, and name a reason for every task ' +
144
+ 'left out. Reads, never writes: `wave check` stays the only command that admits a wave. ' +
145
+ '--session reads live task status and follow-up tasks from the lineage log; --repo names ' +
146
+ 'the checkout the symbol graph is read from.',
147
+ },
148
+ {
149
+ command: 'wave schedule',
150
+ positionals: '<plan.json>',
151
+ flags: `[--repo <dir>] ${EVENTS_DIR}`,
152
+ summary: 'Replay the wave loop against a plan to exhaustion and report how many rounds it needs, ' +
153
+ 'the widest round it ever gets, and which tasks were held back by claim geometry rather ' +
154
+ 'than by a dependency. Reads and simulates, never writes. Exit 1 when the plan stalls, ' +
155
+ '2 when it runs but loses width to something a re-slice could fix.',
156
+ },
157
+ {
158
+ command: 'wave audit',
159
+ positionals: '',
160
+ flags: '--session <id> [--epic <id>] [--state-dir <dir>]',
161
+ summary: 'Read the log back and say whether the waves that were admitted N wide actually ran N ' +
162
+ 'wide, or were dispatched one task at a time. Reads, never writes. Exit 1 on a wave that ' +
163
+ 'ran strictly serially, 2 when a wave was admitted and the log shows no work for it.',
164
+ },
165
+ {
166
+ command: 'new',
167
+ positionals: '<project>',
168
+ flags: '[--ui] [--skip-toolchain] [--target-dir <dir>] [--roadmap-path <file>]',
169
+ summary: 'Scaffold a project workspace and register it in the roadmap.',
170
+ },
171
+ {
172
+ command: 'mcp init',
173
+ positionals: '<project>',
174
+ flags: '[--target-dir <dir>] [--roadmap-path <file>]',
175
+ summary: 'Add the MCP surface and its end-of-project milestone to an existing project.',
176
+ },
177
+ {
178
+ command: 'mcp check',
179
+ positionals: '<project>',
180
+ flags: '[--target-dir <dir>] [--roadmap-path <file>]',
181
+ summary: 'Render the MCP verdict the epic gate reads for the checkout you are in. Exit 1 on red.',
182
+ },
183
+ {
184
+ command: 'research check',
185
+ positionals: '',
186
+ flags: '--brief <brief.json>',
187
+ summary: 'Check a research brief cites what it claims. Exit 1 on an uncited claim.',
188
+ },
189
+ {
190
+ command: 'daemon run',
191
+ positionals: '',
192
+ flags: `[--interval <seconds>] [--once] [--dir <dir>] [--project <dir>...] [--no-self] [--db <path>] [--no-db] [--state-dir <dir>]`,
193
+ summary: 'Watch the event log in the foreground: budgets, stale agents, work that is due. ' +
194
+ 'Never dispatches.',
195
+ },
196
+ {
197
+ command: 'daemon start',
198
+ positionals: '',
199
+ flags: `[--interval <seconds>] [--dir <dir>] [--project <dir>...] [--no-self] [--db <path>] [--no-db] [--state-dir <dir>]`,
200
+ summary: 'Spawn `daemon run` detached, logging to <dir>/daemon.log. Prints the spawned pid.',
201
+ },
202
+ {
203
+ command: 'daemon status',
204
+ positionals: '',
205
+ flags: '[--dir <dir>]',
206
+ summary: 'Print whether a daemon holds the lock, whether it has gone quiet, and what its last tick found. Exit 1 unless one is watching and current.',
207
+ },
208
+ {
209
+ command: 'daemon stop',
210
+ positionals: '',
211
+ flags: '[--dir <dir>]',
212
+ summary: 'SIGTERM the daemon the lock names and clear the lock, live or already dead.',
213
+ },
214
+ {
215
+ command: 'projects list',
216
+ positionals: '',
217
+ flags: '[--roadmap <file>] [--json]',
218
+ summary: 'List every repo this factory maintains -- itself and what it built -- with the ' +
219
+ '--project flags that watch them, and flag any declared project it found no ' +
220
+ 'checkout for.',
221
+ },
222
+ {
223
+ command: 'scheduler run',
224
+ positionals: '',
225
+ flags: `[--dry] [--now <iso>] [--project <dir>...] [--no-self] ${EVENTS_DIR}`,
226
+ summary: 'Propose and enact the next scheduler moves for a session. --dry proposes only.',
227
+ },
228
+ {
229
+ command: 'scheduler admit',
230
+ positionals: '',
231
+ flags: '--session <id> [--state-dir <dir>] [--now <iso>] [--project <dir>...] [--no-self] ' +
232
+ '[--policy <file>] [--crosscheck <file>]',
233
+ summary: "Classify what's due into auto or operator per scheduler.yml autonomy. Enacts nothing.",
234
+ },
235
+ {
236
+ command: 'worktree create',
237
+ positionals: '<project-dir> <epic> <task-id>',
238
+ flags: '',
239
+ summary: 'Cut the isolated worktree and task branch one task is allowed to write in.',
240
+ },
241
+ {
242
+ command: 'worktree rm',
243
+ positionals: '<project-dir> <epic> <task-id>',
244
+ flags: '',
245
+ summary: 'Remove a task worktree once its branch has landed.',
246
+ },
247
+ {
248
+ command: 'worktree fingerprint',
249
+ positionals: '<worktree-dir>',
250
+ flags: '',
251
+ summary: 'Record the tree state before a read-only judge runs against it.',
252
+ },
253
+ {
254
+ command: 'worktree verify',
255
+ positionals: '<worktree-dir>',
256
+ flags: '--before <fingerprint.json>',
257
+ summary: 'Prove a judge changed nothing. Exit 1 on drift — the verdict is then untrustworthy.',
258
+ },
259
+ {
260
+ command: 'worktree stale',
261
+ positionals: '<project-dir> <epic>',
262
+ flags: '',
263
+ summary: 'List worktrees left behind by an epic.',
264
+ },
265
+ {
266
+ command: 'queue run',
267
+ positionals: '<epic>',
268
+ flags: '--project <dir> --test-cmd <cmd> --tasks <tasks.json> [--select-test-cmd <cmd>] [--session <id> --causal-parent <event-id> --plan <plan.json> [--plan-version <n>] [--actor <name>]] [--state-dir <dir>]',
269
+ summary: 'Serially rebase, test, and merge task branches into the integration branch.',
270
+ },
271
+ {
272
+ command: 'queue adopt',
273
+ positionals: '<task-id>',
274
+ // The event flags come from the constant, not spelled out again: this entry
275
+ // wrote them by hand and dropped the two optional ones its handler reads
276
+ // through `eventContextFromFlags`, which is the drift D-136's guard exists
277
+ // to catch. Every event-writing verb shares the group, so it shares the name.
278
+ flags: `--project <dir> --merge-commit <sha> --plan <plan.json> ${EVENTS_DIR}`,
279
+ summary: 'Log a merge that landed outside the queue, after verifying it landed.',
280
+ },
281
+ {
282
+ command: 'session start',
283
+ positionals: '<session-id>',
284
+ flags: '[--continues <event-id>] [--actor <name>] [--state-dir <dir>]',
285
+ summary: 'Open a session log with the one root event it is allowed. Exit 1 if the session is already open.',
286
+ },
287
+ {
288
+ command: 'event append',
289
+ positionals: '<json>',
290
+ flags: '[--state-dir <dir>]',
291
+ summary: 'Append one hash-chained record to a session log. The receipt says whether the timeline reads its type.',
292
+ },
293
+ {
294
+ command: 'event tail',
295
+ positionals: '<session-id>',
296
+ flags: '[--n <count>] [--task <task-id>] [--lineage] [--state-dir <dir>]',
297
+ summary: 'Print the last n records of a session that exists. Exit 1 if it does not.',
298
+ },
299
+ {
300
+ command: 'event lineage',
301
+ positionals: '<session-id>',
302
+ flags: '[--state-dir <dir>]',
303
+ summary: "Print every session in this one's lineage: what it continues, and what continued it.",
304
+ },
305
+ {
306
+ command: 'prompt wrap',
307
+ positionals: '<file>',
308
+ flags: '--kind <kind> --source <label> [--json]',
309
+ summary: 'Wrap ingested text in its provenance block, so a prompt never quotes an unlabelled source.',
310
+ },
311
+ {
312
+ command: 'prompt record',
313
+ positionals: '<file|->',
314
+ flags: EVENTS_DIR,
315
+ summary: "Put the operator's own words on the timeline verbatim, and print the event id a dispatch hangs off.",
316
+ },
317
+ {
318
+ command: 'claims check',
319
+ form: '--roots',
320
+ positionals: '<root-dir>',
321
+ flags: '--roots <glob> [--roots <glob>...] [--since <ref>]',
322
+ summary: 'Write-root mode, for a role with no worktree and no task branch. Exit 1 on violation.',
323
+ },
324
+ {
325
+ command: 'claims check',
326
+ form: 'spec',
327
+ positionals: '<worktree-dir> <spec.json>',
328
+ flags: '',
329
+ summary: 'Post-run mode: every file a task touched must fall inside its claims. Exit 1 on violation.',
330
+ },
331
+ {
332
+ command: 'claims impact',
333
+ form: '--plan',
334
+ positionals: '<task-id>...',
335
+ flags: '--plan <file> [--repo <dir>]',
336
+ summary: 'Pre-run: refuse a wave whose tasks sit on either end of an import edge. Exit 1 when coupled.',
337
+ },
338
+ {
339
+ command: 'claims impact',
340
+ form: 'spec',
341
+ positionals: '<worktree-dir> <spec.json>',
342
+ flags: '',
343
+ summary: 'Post-run: an export this task removed, still imported outside its claims. Exit 1 on a proven break.',
344
+ },
345
+ {
346
+ command: 'dispatch check',
347
+ positionals: '<session-id>',
348
+ flags: '[--task <id>] [--policy <path>] [--state-dir <dir>]',
349
+ summary: 'Assert crosscheck.yml’s role asymmetry against the log. Exit 1 on a violation OR an unverifiable answer.',
350
+ },
351
+ {
352
+ command: 'tester check',
353
+ positionals: '<session-id>',
354
+ flags: '[--task <id>] [--policy <path>] [--state-dir <dir>]',
355
+ summary: 'Assert crosscheck.yml’s role_isolation against the log: a tester of its own per test gate. Exit 1 on a violation OR an unverifiable answer.',
356
+ },
357
+ {
358
+ command: 'delegation check',
359
+ positionals: '<session-id>',
360
+ flags: '[--task <id>] [--policy <path>] [--crosscheck <path>] [--state-dir <dir>]',
361
+ summary: 'Assert delegation.yml\u2019s dispatch grants: who may hold `Agent`, and whether each grantee opened its own log. Exit 1 on a violation OR an unverifiable answer.',
362
+ },
363
+ {
364
+ command: 'escalation check',
365
+ positionals: '<session-id>',
366
+ flags: '[--task <id>] [--policy <path>] [--state-dir <dir>]',
367
+ summary: 'Assert budgets.yml’s escalation ladder against the log. Exit 1 on a violation OR an unverifiable answer.',
368
+ },
369
+ {
370
+ command: 'effort show',
371
+ positionals: '',
372
+ flags: '[--plan <plan.json>] [--effort <tier>] [--policy <file>] [--crosscheck <file>]',
373
+ summary: 'Which tier an epic runs at and what it buys. A security-sensitive plan floors at medium.',
374
+ },
375
+ {
376
+ command: 'security triggers',
377
+ positionals: '',
378
+ flags: '--task <task.json> [--policy <file>] [--case <case>] [--epic-tag <tag>...] [--recheck]',
379
+ summary: 'Compute whether the security reviewer must be dispatched. A fired trigger is not a red.',
380
+ },
381
+ {
382
+ command: 'sandbox open',
383
+ positionals: '<worktree-dir>',
384
+ flags: '--role <role> --task <id> --session <id> [--at <iso>] [--lease-dir <dir>]',
385
+ summary: 'Open a judge sandbox over a worktree. While it is open the judge-* rules apply. Exit 1 if one is already open.',
386
+ },
387
+ {
388
+ command: 'sandbox close',
389
+ positionals: '<worktree-dir>',
390
+ flags: '[--lease-dir <dir>]',
391
+ summary: 'Release a judge sandbox. Closing one that is not open is not an error.',
392
+ },
393
+ {
394
+ command: 'sandbox status',
395
+ positionals: '',
396
+ flags: '[--worktree <dir>] [--lease-dir <dir>]',
397
+ summary: 'Open judge sandboxes — all of them, or the one binding commands run in --worktree.',
398
+ },
399
+ {
400
+ command: 'stack show',
401
+ positionals: '',
402
+ flags: '[--policy <file>]',
403
+ summary: "The operator's stack answers from factory/policies/stack.yml, parsed.",
404
+ },
405
+ {
406
+ command: 'stack check',
407
+ positionals: '',
408
+ flags: '[--policy <file>]',
409
+ summary: 'Which stack answers the shipped templates honour, which they only record, and which make `smith new` refuse. Exit 1 on a refusal.',
410
+ },
411
+ {
412
+ command: 'policy check',
413
+ positionals: '',
414
+ flags: '--command <cmd> [--tool <name>] [--file <path>] [--branch <branch>] [--sandbox <role>] [--lease-dir <dir>]',
415
+ summary: 'Would the guard hook deny this command? --tool Write --file <path> asks the same of a file write. Branch and sandbox auto-detect unless given. Exit 1 on deny.',
416
+ },
417
+ {
418
+ command: 'policy hook',
419
+ positionals: '',
420
+ flags: '',
421
+ summary: 'The PreToolUse hook body: reads a hook payload from stdin. Prints a deny envelope, or nothing at all when no rule fires. Non-zero exit means it could not decide.',
422
+ },
423
+ {
424
+ command: 'gate run',
425
+ positionals: '<task-id>',
426
+ flags: `--worktree <dir> --checks <checks.json> --result <result.json> [--base <ref>] [--grader <file>] [--agent <role> --provider <name> --model-tier <tier> --input-tokens <n> --output-tokens <n>] [--evidence <file> --found-by <role> [--found-by-provider <name>]] [--findings <file>] [--no-findings <role>] [--lessons <lessons.md>] [--plan <file>] [--run-all] [--artifacts-dir <dir>] ${EVENTS_DIR}`,
427
+ summary: 'The per-task gate: schema, tests, coverage, findings. Exit 1 when blocked.',
428
+ },
429
+ {
430
+ command: 'coverage check',
431
+ positionals: '<worktree-dir>',
432
+ flags: '[--plan <plan.json> --task <task-id>] [--summary <path>]',
433
+ summary: "The gate's coverage evidence, without staging a gate run. Exit 1 when a claimed file has no number.",
434
+ },
435
+ {
436
+ command: 'budget alarm',
437
+ positionals: '<session-id>',
438
+ flags: '[--epic <id>] [--policy <path>] [--state-dir <dir>]',
439
+ summary: 'Epic spend against budgets.yml’s epic.alarm_ratio. Exit 1 on a crossing OR on a record too holey to rule one out.',
440
+ },
441
+ {
442
+ command: 'kpi same-mistake',
443
+ positionals: '<session-id>',
444
+ flags: '[--lessons <lessons.md>] [--state-dir <dir>]',
445
+ summary: 'Same-mistake rate per day against §9.7’s decreasing target. Exit 1 on a rise OR when the corpus could not have caught one.',
446
+ },
447
+ {
448
+ command: 'integration check',
449
+ positionals: '',
450
+ flags: `--epic <id> --project <dir> --checks <checks.json> [--run-all <true|false>] ${EVENTS_DIR}`,
451
+ summary: 'Run the checks against the assembled integration branch, not a worktree. Exit 1 on red.',
452
+ },
453
+ {
454
+ command: 'epic verdict',
455
+ positionals: '',
456
+ flags: `--epic <id> --project <dir> [--roadmap-path <file>] [--specs-dir <dir>] ${EVENTS_DIR}`,
457
+ summary: 'Render the read-only epic verdict without writing a close. Exit 1 on hold.',
458
+ },
459
+ {
460
+ command: 'epic spec-review',
461
+ positionals: '',
462
+ flags: `--epic <id> --project <dir> --plan <plan.json> --reviewed-by <role> [--reviewed-by-provider <name>] [--evidence <file>] ${EVENTS_DIR}`,
463
+ summary: 'Record the closing spec review, pinned to the integration head it was read at.',
464
+ },
465
+ {
466
+ command: 'epic goal',
467
+ positionals: '',
468
+ flags: '--epic <id> [--roadmap-path <file>]',
469
+ summary: "The epic's roadmap goal, split into the clauses a coverage map has to answer.",
470
+ },
471
+ {
472
+ command: 'epic goal-check',
473
+ positionals: '',
474
+ flags: `--epic <id> --plan <plan.json> --coverage <file> --checked-by <role> [--checked-by-provider <name>] [--roadmap-path <file>] ${EVENTS_DIR}`,
475
+ summary: 'Record the plan checked clause by clause against the roadmap goal — the one reference the planner did not write.',
476
+ },
477
+ {
478
+ command: 'epic close',
479
+ positionals: '',
480
+ flags: `--epic <id> --project <dir> [--roadmap-path <file>] [--specs-dir <dir>] [--override-rationale <text>] ${EVENTS_DIR}`,
481
+ summary: 'Write the close down. A hold closes only with an operator rationale, recorded.',
482
+ },
483
+ {
484
+ command: 'epic width',
485
+ positionals: '',
486
+ flags: '[--session <id>] [--state-dir <dir>]',
487
+ summary: 'Read every epic close back and say whether this factory builds in parallel or one epic ' +
488
+ 'at a time. Reads, never writes. Without --session it folds every session in the state ' +
489
+ 'dir, because the question is about the workshop and not about where you are standing; ' +
490
+ 'with one it folds that lineage alone. Exit 1 on a closed epic whose record holds a wave ' +
491
+ 'admitted wide that ran narrow, 2 when nothing could be judged.',
492
+ },
493
+ {
494
+ command: 'findings raise',
495
+ positionals: '',
496
+ flags: `[--task <task-id>] [--scope <diff|spec>] [--plan <file>] [--findings <file>] [--evidence <file> --found-by <role> [--found-by-provider <name>]] ${EVENTS_DIR}`,
497
+ summary: 'Raise a finding outside any gate, routed to whoever claims the file it is about.',
498
+ },
499
+ {
500
+ command: 'findings for-dispatch',
501
+ positionals: '',
502
+ flags: '--session <id> --plan <plan.json> --task <task-id> [--state-dir <dir>]',
503
+ summary: "Print the open findings a dispatching task's own claims cover, as a prompt block.",
504
+ },
505
+ {
506
+ command: 'findings list',
507
+ positionals: '',
508
+ flags: '--session <id> [--task <task-id>] [--epic <id>] [--status <status>] [--severity <severity>] [--category <category>] [--state-dir <dir>]',
509
+ summary: 'List the findings of a session that exists.',
510
+ },
511
+ {
512
+ command: 'findings transition',
513
+ positionals: '<finding-id> <status>',
514
+ flags: EVENTS_DIR,
515
+ // Not "a status the transition table allows": the table allows two the
516
+ // command line cannot carry evidence for, and promising them cost a full
517
+ // disposition pass (D-136).
518
+ summary: 'Move one finding along the transition table. The amendment edges are not typeable: amend-pending is entered by "plan amend", amended by "epic close".',
519
+ },
520
+ {
521
+ command: 'findings reverify',
522
+ positionals: '<finding-id>',
523
+ flags: `[--note <text>] ${EVENTS_DIR}`,
524
+ summary: 'Re-open a finding for another look without claiming it was wrong.',
525
+ },
526
+ {
527
+ command: 'findings repair-obligation',
528
+ positionals: '<finding-id>',
529
+ flags: `--replace-with <task-id[,task-id...]> --reason <text> ${EVENTS_DIR}`,
530
+ summary: 'Correct a malformed amends_task_ids entry on an amend-pending finding (D-21 Part 4). Repairs corruption only -- never renegotiates a well-formed obligation.',
531
+ },
532
+ {
533
+ command: 'waivers pending',
534
+ positionals: '<epic>',
535
+ flags: '--session <id> [--state-dir <dir>]',
536
+ summary: 'List the waiver decisions an epic is waiting on.',
537
+ },
538
+ {
539
+ command: 'waivers apply',
540
+ positionals: '<decisions.json>',
541
+ flags: EVENTS_DIR,
542
+ summary: 'Apply a batch of operator waiver decisions.',
543
+ },
544
+ {
545
+ command: 'lessons candidates',
546
+ positionals: '',
547
+ flags: STATS,
548
+ summary: 'List the lesson candidates awaiting an approve or a reject.',
549
+ },
550
+ {
551
+ command: 'lessons raise',
552
+ // `<0<n<=1>` and not `<0-1>`, which is what `plan quorum --confidence`
553
+ // twenty entries up genuinely means. The two ranges differ by one value and
554
+ // the handler enforces the difference: a novelty threshold of 0 rejects
555
+ // every candidate as a duplicate, so scheduler.yml may not hold it and
556
+ // (D-208) neither may the flag that overrides it. A usage line that admits
557
+ // it advertises the one value guaranteed to be refused.
558
+ positionals: '',
559
+ flags: `--statement <text> --lesson-type <type> --lesson-scope <scope> --provenance <event-id[,...]> [--provenance-session <id>] [--evidence <text>] [--finding-category <cat>] [--claim-path <glob>] [--agent-role <role>] [--case-type <case>] [--lesson-id <id>] [--novelty-threshold <0<n<=1>] [--policy <path>] ${EVENTS_DIR}`,
560
+ summary: 'Raise a hand-authored lesson through the novelty gate (exit 1 if rejected).',
561
+ },
562
+ {
563
+ command: 'lessons approve',
564
+ positionals: '<lesson-id>',
565
+ flags: `[--note <text>] [--statement <text>] [--lesson-type <type>] [--lesson-scope <scope>] [--agent-role <role>] [--case-type <case>] [--accept-duplicate] [--novelty-threshold <0<n<=1>] [--policy <path>] ${EVENTS_DIR}`,
566
+ summary: 'Approve a candidate into memory, editing it on the way in through the same novelty gate (exit 1 if not novel).',
567
+ },
568
+ {
569
+ command: 'lessons reject',
570
+ positionals: '<lesson-id>',
571
+ // approve and reject share one dispatcher branch, so reject reaches the same
572
+ // edit flags whether or not it has any use for them. Documenting the shared
573
+ // shape is the honest half of that; narrowing it belongs to the handler.
574
+ flags: `[--note <text>] [--statement <text>] [--lesson-type <type>] [--lesson-scope <scope>] [--agent-role <role>] [--case-type <case>] [--accept-duplicate] [--novelty-threshold <0<n<=1>] [--policy <path>] ${EVENTS_DIR}`,
575
+ summary: 'Invalidate a candidate, with the reason recorded.',
576
+ },
577
+ {
578
+ command: 'lessons compile',
579
+ positionals: '',
580
+ flags: '[--db <file>] [--out <lessons.md>] [--session <id>]',
581
+ summary: 'Render every approved lesson into the markdown dispatch reads.',
582
+ },
583
+ {
584
+ command: 'lessons for-dispatch',
585
+ positionals: '<role>',
586
+ flags: '[--plan <file> --task <task-id>] [--case-type <case>] [--lessons <lessons.md>] [--agents-dir <dir>]',
587
+ summary: 'Print the lesson block to splice into one role prompt.',
588
+ },
589
+ {
590
+ command: 'lessons audit',
591
+ positionals: '<session-id>',
592
+ flags: '[--lessons <lessons.md>] [--state-dir <dir>]',
593
+ summary: 'Which entries fire, which are shadowed dead, which contradict. Recommends only. Exit 1 on anything but clean.',
594
+ },
595
+ {
596
+ command: 'dream',
597
+ positionals: '',
598
+ flags: `[--since <iso>] [--policy <path>] ${EVENTS_DIR}`,
599
+ summary: 'Distil a session log into lesson candidates.',
600
+ },
601
+ {
602
+ command: 'judge dispatch',
603
+ positionals: '',
604
+ flags: `--task <task-id> --role <role> --artifact <file> --model <id> [--round <n>] [--provider <name>] [--model-tier <tier>] ${EVENTS_DIR}`,
605
+ summary: 'Declare the file a judge owes, so a turn that never came back is visible.',
606
+ },
607
+ {
608
+ command: 'judge report',
609
+ positionals: '',
610
+ flags: `--task <task-id> --role <role> [--round <n>] [--artifact <file>] [--no-findings] ${EVENTS_DIR}`,
611
+ summary: 'Close a judge turn against the artifact it declared. --no-findings attests the clean case.',
612
+ },
613
+ {
614
+ command: 'judge outstanding',
615
+ positionals: '',
616
+ flags: '--session <id> --task <task-id> [--state-dir <dir>]',
617
+ summary: 'List the judge turns still owed on a task. Exit 1 while the list is non-empty.',
618
+ },
619
+ {
620
+ command: 'judge escalations',
621
+ positionals: '',
622
+ flags: '--session <id> [--state-dir <dir>]',
623
+ summary: 'List the cross-provider disagreements still owed an answer. Exit 1 on an open disagreement, 2 when nothing was gated at all.',
624
+ },
625
+ {
626
+ command: 'judge preflight',
627
+ positionals: '',
628
+ flags: '[--policy <file>]',
629
+ summary: 'Can the enabled providers be called at all, and would a promotion decide anything? Exit 1 on a provider that costs a call it cannot make.',
630
+ },
631
+ {
632
+ command: 'judge run',
633
+ positionals: '',
634
+ flags: '--provider <name> --request <request.json> [--shadow]',
635
+ summary: 'Call one provider by hand for calibration. Touches neither the log nor quorum.',
636
+ },
637
+ {
638
+ command: 'crossfind request',
639
+ positionals: '',
640
+ flags: '--task <id> --diff <file> --diff-ref <ref> [--criterion <text>...] [--timeout-ms <n>] [--max-output-bytes <n>] [--policy <file>]',
641
+ summary: 'Print the finder request without sending it — exactly what would leave the machine. Refuses when send_diff is false.',
642
+ },
643
+ {
644
+ command: 'crossfind reconcile',
645
+ positionals: '',
646
+ flags: '--task <id> --native <findings.json> --independent <runs.json> [--policy <file>]',
647
+ summary: 'Reconcile two saved finding lists offline. No provider, no log. Exit 1 when the result would change a gate.',
648
+ },
649
+ {
650
+ command: 'crossfind run',
651
+ positionals: '',
652
+ flags: `--task <id> --diff <file> --diff-ref <ref> [--criterion <text>...] [--status <status>] [--timeout-ms <n>] [--max-output-bytes <n>] [--policy <file>] ${EVENTS_DIR}`,
653
+ summary: 'Run the independent finder over a diff and reconcile it against the native findings. Exit 1 when the result would change a gate.',
654
+ },
655
+ {
656
+ command: 'ui serve',
657
+ positionals: '',
658
+ flags: '[--port <n>] [--db <file>] [--state-dir <dir>] [--roadmap-path <file>] [--specs-dir <dir>]',
659
+ summary: 'Serve the dashboard from the built ui/server.',
660
+ },
661
+ {
662
+ command: 'db rebuild',
663
+ positionals: '',
664
+ flags: '[--db <file>] [--session <id>] [--state-dir <dir>] [--roadmap-path <file>] [--specs-dir <dir>]',
665
+ summary: 'Rebuild the projection from the logs. Without --session, from all of them.',
666
+ },
667
+ {
668
+ command: 'db apply',
669
+ positionals: '',
670
+ flags: '--session <id> [--db <file>] [--state-dir <dir>] [--roadmap-path <file>] [--specs-dir <dir>]',
671
+ summary: 'Apply one session log onto the existing projection.',
672
+ },
673
+ {
674
+ command: 'stats overview',
675
+ positionals: '',
676
+ flags: STATS,
677
+ summary: 'Counts across tasks, findings, and lessons.',
678
+ },
679
+ {
680
+ command: 'stats timeline',
681
+ positionals: '',
682
+ flags: `[--task <task-id>] [--epic <id>] [--causal-chain-for <event-id>] ${STATS}`,
683
+ summary: 'The event timeline, optionally narrowed to one task, epic, or causal chain.',
684
+ },
685
+ {
686
+ command: 'stats kanban',
687
+ positionals: '',
688
+ flags: `--epic <id> ${STATS}`,
689
+ summary: 'One epic as columns of task status.',
690
+ },
691
+ {
692
+ command: 'stats task',
693
+ positionals: '',
694
+ flags: `--task <task-id> ${STATS}`,
695
+ summary: 'Everything the projection holds about one task. Exit 1 if it holds nothing.',
696
+ },
697
+ {
698
+ command: 'stats lessons',
699
+ positionals: '',
700
+ flags: STATS,
701
+ summary: 'Lesson candidates and approvals as the dashboard shows them.',
702
+ },
703
+ {
704
+ command: 'stats errors',
705
+ positionals: '',
706
+ flags: STATS,
707
+ summary: 'Errors grouped by taxonomy class.',
708
+ },
709
+ {
710
+ command: 'stats analytics',
711
+ positionals: '',
712
+ flags: STATS,
713
+ summary: 'Throughput, rework, and budget aggregates.',
714
+ },
715
+ {
716
+ command: 'stats roadmap',
717
+ positionals: '',
718
+ flags: STATS,
719
+ summary: 'Roadmap progress as the projection sees it.',
720
+ },
721
+ {
722
+ command: 'stats providers',
723
+ positionals: '',
724
+ flags: `[--since <iso>] ${STATS}`,
725
+ summary: 'Cross-provider judge agreement, for calibration.',
726
+ },
727
+ ];
728
+ /** The key a command is looked up by: the dispatch pair, plus the form where there are two. */
729
+ function keyOf(doc) {
730
+ return doc.form ? `${doc.command} ${doc.form}` : doc.command;
731
+ }
732
+ function namespaceOf(doc) {
733
+ return doc.command.split(' ')[0];
734
+ }
735
+ const BY_KEY = new Map(COMMANDS.map((doc) => [keyOf(doc), doc]));
736
+ const COMMAND_NAMES = new Set(COMMANDS.map((doc) => doc.command));
737
+ const NAMESPACES = new Set(COMMANDS.map(namespaceOf));
738
+ /** The line that would have worked. Empty parts are dropped, never printed as a double space. */
739
+ export function usageLine(doc) {
740
+ return ['smith', doc.command, doc.positionals, doc.flags].filter((part) => part !== '').join(' ');
741
+ }
742
+ /** The positional argument names, in order — what a missing-argument error names. */
743
+ export function positionalNames(doc) {
744
+ return [...doc.positionals.matchAll(/<([^>]+)>/g)].map((m) => m[1]);
745
+ }
746
+ /**
747
+ * Every command answers `--help`, and no command's flag string says so.
748
+ * Declaring it here rather than in each entry keeps the help line out of 63
749
+ * usage strings that would all have to repeat it.
750
+ */
751
+ const UNIVERSAL_FLAGS = [['help', false]];
752
+ /**
753
+ * The flags one doc entry declares: name → whether it carries a value.
754
+ *
755
+ * `--session <id>` takes one; `[--run-all]` takes none. The brackets mark
756
+ * optionality, which the parser does not care about — a missing required flag
757
+ * is already `requireFlag`'s job, and it names the flag better than a parser
758
+ * could.
759
+ */
760
+ function flagsOf(doc) {
761
+ return [...doc.flags.matchAll(/--([a-z0-9-]+)(\s+<[^>]*>)?/g)].map((m) => [
762
+ m[1],
763
+ m[2] !== undefined,
764
+ ]);
765
+ }
766
+ /**
767
+ * What a command line is allowed to contain, derived from the same table the
768
+ * help prints (D-132), or `undefined` when nothing here documents the command.
769
+ *
770
+ * The `undefined` matters: an empty map would read as "accepts no flags", so an
771
+ * unknown command would answer with a pile of unknown-flag errors instead of
772
+ * "unknown command". The caller distinguishes them.
773
+ *
774
+ * Both documented forms of a two-form command are unioned. `claims check` is
775
+ * the case: the operator has not yet chosen a form at the moment the flags are
776
+ * parsed, so neither form's flags can be treated as unknown.
777
+ */
778
+ export function flagSpecFor(namespace, action) {
779
+ if (namespace === undefined)
780
+ return undefined;
781
+ const key = action === undefined ? namespace : `${namespace} ${action}`;
782
+ // The second arm is what keeps the action-less verbs working: `smith new
783
+ // my-project` spends its action slot on a positional. Same fallback as
784
+ // isDocumented, deliberately — a command that dispatches must also parse.
785
+ const docs = COMMANDS.filter((doc) => doc.command === key || doc.command === namespace).filter((doc) => doc.command === key || !COMMAND_NAMES.has(key));
786
+ if (docs.length === 0)
787
+ return undefined;
788
+ const spec = new Map(UNIVERSAL_FLAGS);
789
+ for (const doc of docs) {
790
+ for (const [name, takesValue] of flagsOf(doc)) {
791
+ // A flag documented with a value in one form and bare in another takes a
792
+ // value: the parser must be able to read the longer of the two shapes.
793
+ spec.set(name, (spec.get(name) ?? false) || takesValue);
794
+ }
795
+ }
796
+ return spec;
797
+ }
798
+ export function usageFor(key) {
799
+ const doc = BY_KEY.get(key);
800
+ if (!doc) {
801
+ throw new SmithError('usage.unknown-command', `No usage entry for "${key}".`, { key });
802
+ }
803
+ return doc;
804
+ }
805
+ function renderCommands(docs) {
806
+ return docs.map((doc) => ` ${usageLine(doc)}\n ${doc.summary}\n`).join('');
807
+ }
808
+ /**
809
+ * The whole command list, or one namespace's slice of it. An unknown namespace
810
+ * throws rather than printing an empty list: "smith has no such namespace" and
811
+ * "that namespace has no commands" must not be the same observable answer.
812
+ */
813
+ export function usageText(namespace) {
814
+ if (namespace !== undefined && !NAMESPACES.has(namespace)) {
815
+ throw new SmithError('usage.unknown-namespace', `No such namespace: ${namespace}. Run "smith --help" for the list.`, { namespace });
816
+ }
817
+ const docs = namespace === undefined ? COMMANDS : COMMANDS.filter((doc) => namespaceOf(doc) === namespace);
818
+ const header = namespace === undefined
819
+ ? [
820
+ 'Usage: smith <namespace> <action> [args] [--flags]',
821
+ '',
822
+ ' smith <namespace> --help list one namespace',
823
+ ' smith <command> --help show one command',
824
+ '',
825
+ ].join('\n')
826
+ : `Usage: smith ${namespace} <action> [args] [--flags]\n`;
827
+ let out = `${header}\n`;
828
+ let current = '';
829
+ for (const doc of docs) {
830
+ const ns = namespaceOf(doc);
831
+ if (ns !== current) {
832
+ if (current !== '')
833
+ out += '\n';
834
+ current = ns;
835
+ }
836
+ out += renderCommands([doc]);
837
+ }
838
+ return out;
839
+ }
840
+ /**
841
+ * The help for a topic, or undefined when there is no such topic. Callers
842
+ * distinguish the two: `--help` on a real topic prints and exits 0, while an
843
+ * unknown one falls through to the unknown-command path.
844
+ */
845
+ export function helpText(namespace, action) {
846
+ if (namespace === undefined)
847
+ return usageText();
848
+ const key = action === undefined ? namespace : `${namespace} ${action}`;
849
+ const exact = COMMANDS.filter((doc) => doc.command === key);
850
+ if (exact.length > 0)
851
+ return renderCommands(exact);
852
+ // `smith new my-project --help`: the action slot holds a positional, so the
853
+ // namespace alone is the command.
854
+ const bare = COMMANDS.filter((doc) => doc.command === namespace);
855
+ if (bare.length > 0)
856
+ return renderCommands(bare);
857
+ if (NAMESPACES.has(namespace))
858
+ return usageText(namespace);
859
+ return undefined;
860
+ }
861
+ /**
862
+ * Whether cli.ts's dispatcher has a branch for this pair. Checked before
863
+ * dispatch, so an undocumented command cannot run — which is the half of
864
+ * "the table and the dispatcher agree" that a test cannot enforce at runtime.
865
+ * The second arm is what keeps the action-less verbs working: `smith new
866
+ * my-project` spends its action slot on a positional.
867
+ */
868
+ export function isDocumented(namespace, action) {
869
+ if (namespace === undefined)
870
+ return false;
871
+ if (action !== undefined && COMMAND_NAMES.has(`${namespace} ${action}`))
872
+ return true;
873
+ return COMMAND_NAMES.has(namespace);
874
+ }