simt-dev 0.7.0 → 0.8.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 (235) hide show
  1. package/CHANGELOG.md +191 -870
  2. package/README.md +188 -213
  3. package/bin/simt.mjs +19 -55
  4. package/docs/decisions/ADR-014-workspace-is-configuration.md +2 -2
  5. package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
  6. package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
  7. package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
  8. package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
  9. package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
  10. package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
  11. package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
  12. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
  13. package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
  14. package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
  15. package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
  16. package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
  17. package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
  18. package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
  19. package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
  20. package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
  21. package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
  22. package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
  23. package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
  24. package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
  25. package/ecosystems/bun.json +6 -0
  26. package/ecosystems/cargo.json +25 -0
  27. package/ecosystems/composer.json +6 -0
  28. package/ecosystems/dart.json +24 -0
  29. package/ecosystems/deno.json +21 -0
  30. package/ecosystems/devcontainers.json +6 -0
  31. package/ecosystems/docker-compose.json +6 -0
  32. package/ecosystems/docker.json +6 -0
  33. package/ecosystems/dotnet.json +25 -0
  34. package/ecosystems/elixir.json +23 -0
  35. package/ecosystems/gitsubmodule.json +6 -0
  36. package/ecosystems/go.json +25 -0
  37. package/ecosystems/gradle.json +13 -0
  38. package/ecosystems/maven.json +23 -0
  39. package/ecosystems/node.json +25 -0
  40. package/ecosystems/python.json +20 -0
  41. package/ecosystems/ruby.json +23 -0
  42. package/ecosystems/swift.json +25 -0
  43. package/ecosystems/terraform.json +6 -0
  44. package/ecosystems/uv.json +6 -0
  45. package/harnesses/antigravity.json +162 -0
  46. package/harnesses/claude.json +206 -0
  47. package/harnesses/codex.json +136 -0
  48. package/harnesses/copilot.json +88 -0
  49. package/harnesses/cursor.json +140 -0
  50. package/harnesses/deepseek.json +99 -0
  51. package/harnesses/gemini.json +122 -0
  52. package/harnesses/opencode.json +173 -0
  53. package/package.json +12 -2
  54. package/rules/core.md +7 -0
  55. package/rules/mdc/intake-questions.mdc +8 -0
  56. package/rules/mdc/slice-commit.mdc +57 -4
  57. package/src/advice/advice.mjs +3 -2
  58. package/src/agenthooks/prompt.mjs +5 -2
  59. package/src/agenthooks/render.mjs +86 -180
  60. package/src/agenthooks/shim.mjs +1 -1
  61. package/src/agents/render.mjs +15 -18
  62. package/src/api.mjs +31 -0
  63. package/src/argv.mjs +48 -3
  64. package/src/bench/ablate.mjs +3 -4
  65. package/src/bench/adapters.json +163 -105
  66. package/src/bench/adapters.mjs +466 -0
  67. package/src/bench/arms.mjs +333 -0
  68. package/src/bench/grade.mjs +33 -73
  69. package/src/bench/graders.mjs +42 -0
  70. package/src/bench/mine.mjs +121 -475
  71. package/src/bench/probe-ledger.mjs +149 -0
  72. package/src/bench/probes.catalog.mjs +918 -0
  73. package/src/bench/probes.mjs +79 -1064
  74. package/src/bench/render.mjs +111 -0
  75. package/src/bench/report.mjs +120 -962
  76. package/src/bench/run.mjs +185 -802
  77. package/src/bench/variants.mjs +4 -5
  78. package/src/bench/workspace.mjs +160 -0
  79. package/src/brief/assemble.mjs +48 -15
  80. package/src/ci/dependabot.mjs +25 -55
  81. package/src/ci/workflow.mjs +30 -7
  82. package/src/cli.mjs +45 -0
  83. package/src/commands/agent.mjs +4 -13
  84. package/src/commands/agenthook.mjs +70 -40
  85. package/src/commands/bench.mjs +191 -18
  86. package/src/commands/brief.mjs +5 -4
  87. package/src/commands/ci.mjs +13 -14
  88. package/src/commands/compare.mjs +42 -42
  89. package/src/commands/completions.mjs +2 -6
  90. package/src/commands/decide.mjs +4 -4
  91. package/src/commands/distill.mjs +4 -3
  92. package/src/commands/docs.mjs +88 -0
  93. package/src/commands/doctor.mjs +11 -7
  94. package/src/commands/evidence.mjs +6 -9
  95. package/src/commands/explain.mjs +0 -1
  96. package/src/commands/gate.mjs +26 -25
  97. package/src/commands/graph.mjs +2 -3
  98. package/src/commands/harness.mjs +6 -5
  99. package/src/commands/help.mjs +168 -19
  100. package/src/commands/hooks.mjs +119 -49
  101. package/src/commands/ignore.mjs +13 -8
  102. package/src/commands/impact.mjs +8 -15
  103. package/src/commands/index.mjs +318 -193
  104. package/src/commands/init-scan.mjs +90 -0
  105. package/src/commands/init.mjs +124 -105
  106. package/src/commands/lease.mjs +73 -25
  107. package/src/commands/lessons.mjs +10 -9
  108. package/src/commands/local.mjs +3 -3
  109. package/src/commands/map.mjs +24 -14
  110. package/src/commands/mcp.mjs +2 -18
  111. package/src/commands/pack.mjs +9 -6
  112. package/src/commands/plan.mjs +6 -5
  113. package/src/commands/plugin.mjs +2 -2
  114. package/src/commands/prbody.mjs +3 -3
  115. package/src/commands/repoindex.mjs +12 -11
  116. package/src/commands/review.mjs +2 -2
  117. package/src/commands/rules.mjs +37 -16
  118. package/src/commands/shared.mjs +14 -19
  119. package/src/commands/skills.mjs +33 -32
  120. package/src/commands/skillscompile.mjs +7 -8
  121. package/src/commands/skillsinstall.mjs +7 -6
  122. package/src/commands/slice.mjs +220 -74
  123. package/src/commands/spec.mjs +167 -0
  124. package/src/commands/status.mjs +27 -18
  125. package/src/commands/sync.mjs +27 -25
  126. package/src/commands/tests.mjs +18 -21
  127. package/src/commands/tokens.mjs +21 -26
  128. package/src/commands/undo.mjs +5 -15
  129. package/src/commands/upgrade.mjs +15 -7
  130. package/src/commands/why.mjs +15 -21
  131. package/src/compare/hooksstate.mjs +2 -6
  132. package/src/compare/learn.mjs +3 -1
  133. package/src/compare/row.mjs +0 -2
  134. package/src/context.mjs +64 -0
  135. package/src/decisions/index-doc.mjs +46 -0
  136. package/src/decisions/index.mjs +3 -2
  137. package/src/diff.mjs +1 -1
  138. package/src/distill/distill.mjs +14 -1
  139. package/src/distill/engine.mjs +5 -1
  140. package/src/docs/commands.mjs +65 -0
  141. package/src/ecosystem/records.mjs +219 -0
  142. package/src/explain.mjs +31 -0
  143. package/src/gates/index.mjs +385 -0
  144. package/src/graph/compile.mjs +8 -4
  145. package/src/graph/load.mjs +6 -2
  146. package/src/graph/spec.mjs +10 -8
  147. package/src/harness/records.mjs +143 -0
  148. package/src/hooks/capability.mjs +7 -5
  149. package/src/hooks/dispatcher.mjs +209 -60
  150. package/src/hooks/gates.mjs +10 -43
  151. package/src/hooks/resolve.mjs +4 -14
  152. package/src/hooks/shim.mjs +5 -2
  153. package/src/hooks/where.mjs +2 -7
  154. package/src/identity.mjs +31 -12
  155. package/src/index/coverage.mjs +58 -0
  156. package/src/index/extract.mjs +6 -16
  157. package/src/index/gate.mjs +1 -1
  158. package/src/index/govern.mjs +26 -9
  159. package/src/index/impact.mjs +4 -8
  160. package/src/index/map.mjs +14 -15
  161. package/src/index/open.mjs +173 -0
  162. package/src/index/persist.mjs +224 -57
  163. package/src/index/store.mjs +82 -39
  164. package/src/index/tests.mjs +2 -6
  165. package/src/journal.mjs +94 -20
  166. package/src/kernel/card.mjs +37 -63
  167. package/src/kernel/checks.mjs +4 -10
  168. package/src/lease.mjs +102 -7
  169. package/src/ledger/index.mjs +150 -14
  170. package/src/local/client.mjs +4 -6
  171. package/src/locate.mjs +33 -9
  172. package/src/manifest/schema.mjs +213 -0
  173. package/src/mcp/paginate.mjs +33 -0
  174. package/src/mcp/resources.mjs +7 -10
  175. package/src/mcp/schemas.mjs +512 -0
  176. package/src/mcp/server.mjs +119 -25
  177. package/src/mcp/tools-facts.mjs +326 -0
  178. package/src/mcp/tools-read.mjs +221 -0
  179. package/src/mcp/tools.mjs +4 -967
  180. package/src/measure/corpus.mjs +150 -0
  181. package/src/measure/metrics.mjs +236 -0
  182. package/src/measure/records.mjs +211 -0
  183. package/src/measure/session.mjs +3 -2
  184. package/src/measure/stats.mjs +538 -0
  185. package/src/measure/task-schema.mjs +125 -0
  186. package/src/measure/tasks.mjs +69 -0
  187. package/src/memory/decide.mjs +6 -15
  188. package/src/memory/lesson-text.mjs +61 -0
  189. package/src/memory/lessons.mjs +29 -82
  190. package/src/name.mjs +7 -0
  191. package/src/node-version.mjs +37 -0
  192. package/src/output/index.mjs +209 -10
  193. package/src/packs/manage.mjs +32 -12
  194. package/src/packs.mjs +20 -2
  195. package/src/plan/index.mjs +1 -1
  196. package/src/plugins.mjs +22 -16
  197. package/src/prbody.mjs +7 -11
  198. package/src/projection/loaders.mjs +7 -692
  199. package/src/projection/lock.mjs +11 -1
  200. package/src/projection/manifest.mjs +29 -468
  201. package/src/projection/mdc.mjs +7 -8
  202. package/src/projection/project.mjs +126 -221
  203. package/src/projection/region.mjs +561 -0
  204. package/src/projection/rules.mjs +20 -550
  205. package/src/projection/surfaces.mjs +43 -210
  206. package/src/projection/targets.mjs +5 -5
  207. package/src/repo-root.mjs +31 -0
  208. package/src/resolve/eval.mjs +2 -1
  209. package/src/resolve/fingerprint.mjs +19 -22
  210. package/src/resolve/resolve.mjs +1 -41
  211. package/src/review/critic.mjs +10 -13
  212. package/src/run.mjs +98 -0
  213. package/src/secrets.mjs +14 -22
  214. package/src/state/events.mjs +129 -0
  215. package/src/status-snapshot.mjs +233 -0
  216. package/src/status.mjs +2 -6
  217. package/src/sys/git.mjs +82 -0
  218. package/src/sys/proc.mjs +151 -0
  219. package/src/testing/parsers.mjs +265 -0
  220. package/src/testing/patches.mjs +23 -0
  221. package/src/testing/paths.mjs +49 -0
  222. package/src/testing/runners.mjs +186 -0
  223. package/src/testing/vault.mjs +68 -0
  224. package/src/text/frontmatter.mjs +49 -0
  225. package/src/text/tokens.mjs +52 -0
  226. package/src/toolwords.mjs +3 -0
  227. package/src/util/fs.mjs +20 -0
  228. package/src/util/sort.mjs +12 -0
  229. package/src/workspace.mjs +58 -11
  230. package/src/write/plan.mjs +82 -0
  231. package/templates/plan/tracker.json +1 -0
  232. package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
  233. package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
  234. package/docs/decisions/README.md +0 -600
  235. package/src/bench/tasks.mjs +0 -36
@@ -1,6 +1,6 @@
1
1
  // Command registry and dispatch.
2
2
  //
3
- // Every command named in docs/plans/0002-simt-dev-cli.md section 5 is registered
3
+ // Every command named in docs/research/0002-simt-dev-cli.md section 5 is registered
4
4
  // here from day one. A command that is declared but not yet implemented exits
5
5
  // UNSUPPORTED with a pointer to its implementation step; it never silently
6
6
  // succeeds. An agent that calls `simt compare` on an unreleased build must get a
@@ -8,111 +8,47 @@
8
8
 
9
9
  import {
10
10
  EXIT,
11
+ jsonCaptureStart,
12
+ jsonCaptureTake,
13
+ jsonCommand,
11
14
  emit,
12
15
  emitJson,
13
16
  fail,
17
+ clearWarnings,
18
+ note,
19
+ warn,
14
20
  isJson,
15
21
  jsonFailMode,
16
22
  jsonIndentDefault,
23
+ maxBytesLimit,
24
+ withOutputState,
17
25
  } from '../output/index.mjs';
18
26
  import { help } from './help.mjs';
19
- import { lintCommand } from './graph.mjs';
20
- import {
21
- indexCommand as skillsIndex,
22
- findCommand as skillsFind,
23
- briefCommand as skillsBrief,
24
- showCommand as skillsShow,
25
- whyCommand as skillsWhy,
26
- evalCommand as skillsEval,
27
- } from './skills.mjs';
28
- import { briefCommand } from './brief.mjs';
29
- import { syncCommand, checkCommand } from './sync.mjs';
30
- import { initCommand } from './init.mjs';
31
- import { ciCommand } from './ci.mjs';
32
- import { hooksCommand } from './hooks.mjs';
33
- import { agentHookCommand, agentHooksCommand } from './agenthook.mjs';
34
- import { agentsCommand } from './agent.mjs';
35
- import { compareCommand } from './compare.mjs';
36
- import { upgradeCommand } from './upgrade.mjs';
37
- import { mcpCommand } from './mcp.mjs';
38
- import { pluginCommand } from './plugin.mjs';
39
- import { packCommand } from './pack.mjs';
40
- import { installCommand } from './skillsinstall.mjs';
41
- import { compileCommand } from './skillscompile.mjs';
42
- import { decisionsCommand as rulesDecisions, listCommand as rulesList, showCommand as rulesShow, whyCommand as rulesWhy } from './rules.mjs';
43
- import { tokensCommand } from './tokens.mjs';
44
- import { harnessCommand, harnessLoadersCommand } from './harness.mjs';
45
- import {
46
- benchMineCommand,
47
- benchRunCommand,
48
- benchReportCommand,
49
- benchAblateCommand,
50
- benchProbesCommand,
51
- benchVariantsCommand,
52
- benchCriticCommand,
53
- } from './bench.mjs';
54
- import { planCommand } from './plan.mjs';
55
- import { sliceCommand } from './slice.mjs';
56
- import { leaseCommand } from './lease.mjs';
57
- import { sharedPlanCommand, sharedInstallCommand } from './shared.mjs';
58
- import { runDoctor } from './doctor.mjs';
59
- import { statusCommand } from './status.mjs';
60
- import { evidenceCommand } from './evidence.mjs';
61
- import { indexCommand } from './repoindex.mjs';
62
- import { mapCommand } from './map.mjs';
63
- import { testsCommand } from './tests.mjs';
64
- import { whyCommand } from './why.mjs';
65
- import { gateCommand } from './gate.mjs';
66
- import { impactCommand } from './impact.mjs';
67
- import { lessonsCaptureCommand, lessonsListCommand, lessonsReviewCommand } from './lessons.mjs';
68
- import { decideCommand } from './decide.mjs';
69
- import { explainCommand } from './explain.mjs';
70
- import { undoCommand } from './undo.mjs';
71
- import { prBodyCommand } from './prbody.mjs';
72
- import { ignoreCommand } from './ignore.mjs';
73
- import { completionsCommand } from './completions.mjs';
74
- import { reviewCommand } from './review.mjs';
75
- import { localDoctorCommand } from './local.mjs';
76
- import { distillCommand } from './distill.mjs';
27
+ import { ALIASES, GROUP_DEFAULTS, SPEC, SPEC_PATHS } from './spec.mjs';
77
28
  import { parse, rawJsonFlag } from '../argv.mjs';
78
29
  import { CLI } from '../name.mjs';
30
+ import { packageVersion } from '../package-version.mjs';
31
+
32
+
33
+ /**
34
+ * Groups whose subcommand may be omitted, and the default for each. Read from the spec
35
+ * (review 0003 Q60) rather than kept here: this was the second copy of a fact the spec
36
+ * owns, and the reason for each default now sits on the row that declares it.
37
+ */
38
+ export const GROUPS = GROUP_DEFAULTS;
79
39
 
80
- /** command path -> implementation sequence step (plan 0002 section 11). */
81
- // Every command named in plan 0002 section 5 is now implemented, so this map is empty.
82
- // It stays, rather than being deleted, because the next command should be added here
83
- // first: a declared-but-unimplemented command must exit UNSUPPORTED with a pointer to
84
- // its step, never silently succeed with empty output an agent will read as a result.
85
- const STEPS = new Map([]);
40
+ /** path -> the flags the spec declares for it. The parser knows every flag; this is
41
+ * what one command reads, and the difference is what the caller is told (Q62). */
42
+ const DECLARED = new Map(SPEC.map((row) => [row.path, new Set(row.flags ?? [])]));
86
43
 
87
- /** groups whose subcommands may be omitted, and the default for each. */
88
- const GROUPS = new Map([
89
- ['skills', 'index'],
90
- ['graph', 'lint'],
91
- ['rules', 'list'],
92
- ['hooks', 'status'],
93
- ['plan', 'status'],
94
- // Plan 0003 R53. Both halves write, so a bare `slice` has no read to default to.
95
- // `start` is the default because with the id it is missing it fails with a usage
96
- // error before anything is created — the one default here that cannot mutate.
97
- ['slice', 'start'],
98
- ['agent-hooks', 'status'],
99
- // A group like `agent-hooks`: `install` writes the agent file into each harness's
100
- // own directory, `status` reports what each harness carries. The default is the
101
- // read, the way `agent-hooks`'s is.
102
- ['agents', 'status'],
103
- ['harness', 'detect'],
104
- ['bench', 'mine'],
105
- // ADR-084. `shared` is the one group whose default writes nothing, and it is `plan` for
106
- // the same reason `skills`'s is `index`: the read answers "what would this do, and
107
- // where", and `install` — the subcommand that touches a reader's home directory — is the
108
- // one that needs `--yes` before it does anything.
109
- ['shared', 'plan'],
110
- ['lessons', 'list'],
111
- ['local', 'doctor'],
112
- // Plan 0003 R45. The default is the read: `pack` alone lints the working
113
- // directory, the way `skills` alone indexes and `graph` alone lints.
114
- ['pack', 'lint'],
115
- ]);
44
+ /** Flags dispatch reads for every command: the envelope and the guard rails. */
45
+ /** path -> the values a flag of that path accepts, where the set is closed. */
46
+ const VALUES = new Map(SPEC.map((row) => [row.path, row.values ?? {}]));
47
+
48
+ // `dashdash` is the parser's record that `--` appeared, not a flag any reader typed:
49
+ // it is an envelope fact like `root`, so the per-command declaration check skips it
50
+ // and lets the one command that reads it (slice start) act on it.
51
+ export const GLOBAL_FLAGS = new Set(['root', 'json', 'pretty', 'help', 'version', 'maxBytes', 'dashdash']);
116
52
 
117
53
  /**
118
54
  * Every command this binary dispatches, in one place.
@@ -124,126 +60,265 @@ const GROUPS = new Map([
124
60
  * an agent choosing a command — the one consumer with no way to discover the omission
125
61
  * except by guessing.
126
62
  *
127
- * Module scope so `test/cli.test.mjs` can assert help against it. Module scope is also
128
- * just correct: nothing in the map depends on the call.
63
+ * Module scope so `test/cli.test.mjs` can assert help against it. Each value is a
64
+ * loader rather than a handler: dispatch awaits the one the matched path names, so a
65
+ * command pays for its own module graph and not for the other seventy-five (review 0003
66
+ * Q64). A loader that never resolves is a command that cannot run, and `test/spec.test.mjs`
67
+ * still holds the key set against the spec.
129
68
  */
130
69
  export const IMPLEMENTED = {
131
- brief: briefCommand,
132
- doctor: runDoctor,
70
+ brief: async () => {
71
+ const { briefCommand } = await import('./brief.mjs');
72
+ return (args, flags) => briefCommand(args, flags, COMMANDS);
73
+ },
74
+ doctor: async () => (await import('./doctor.mjs')).runDoctor,
133
75
  // Plan 0003 R48. A bare command: the only mode is `--line`, so there is no
134
76
  // group and no default subcommand to resolve.
135
- status: statusCommand,
136
- 'graph lint': lintCommand,
137
- 'skills index': skillsIndex,
138
- 'skills find': skillsFind,
139
- 'skills brief': skillsBrief,
140
- 'skills show': skillsShow,
141
- 'skills why': skillsWhy,
142
- 'skills eval': skillsEval,
143
- 'skills install': installCommand,
144
- 'skills compile': compileCommand,
145
- 'rules decisions': rulesDecisions,
146
- 'rules list': rulesList,
147
- 'rules show': rulesShow,
148
- 'rules why': rulesWhy,
149
- sync: syncCommand,
150
- check: checkCommand,
151
- 'plan status': (args, flags) => planCommand('status', args, flags),
152
- 'plan next': (args, flags) => planCommand('next', args, flags),
153
- 'plan done': (args, flags) => planCommand('done', args, flags),
77
+ status: async () => (await import('./status.mjs')).statusCommand,
78
+ 'graph lint': async () => (await import('./graph.mjs')).lintCommand,
79
+ 'skills index': async () => (await import('./skills.mjs')).indexCommand,
80
+ 'skills find': async () => (await import('./skills.mjs')).findCommand,
81
+ 'skills brief': async () => (await import('./skills.mjs')).briefCommand,
82
+ 'skills show': async () => (await import('./skills.mjs')).showCommand,
83
+ 'skills why': async () => (await import('./skills.mjs')).whyCommand,
84
+ 'skills eval': async () => (await import('./skills.mjs')).evalCommand,
85
+ 'skills install': async () => (await import('./skillsinstall.mjs')).installCommand,
86
+ 'skills compile': async () => (await import('./skillscompile.mjs')).compileCommand,
87
+ 'rules decisions': async () => (await import('./rules.mjs')).decisionsCommand,
88
+ 'rules list': async () => (await import('./rules.mjs')).listCommand,
89
+ 'rules show': async () => (await import('./rules.mjs')).showCommand,
90
+ 'rules why': async () => (await import('./rules.mjs')).whyCommand,
91
+ sync: async () => (await import('./sync.mjs')).syncCommand,
92
+ check: async () => (await import('./sync.mjs')).checkCommand,
93
+ 'plan status': async () => {
94
+ const { planCommand } = await import('./plan.mjs');
95
+ return (args, flags) => planCommand('status', args, flags);
96
+ },
97
+ 'plan next': async () => {
98
+ const { planCommand } = await import('./plan.mjs');
99
+ return (args, flags) => planCommand('next', args, flags);
100
+ },
101
+ 'plan done': async () => {
102
+ const { planCommand } = await import('./plan.mjs');
103
+ return (args, flags) => planCommand('done', args, flags);
104
+ },
154
105
  // Plan 0003 R53. A group: `start` isolates the work on a branch with its brief,
155
106
  // `finish` runs the gates, commits and records the evidence, and `abandon`
156
107
  // (review 0003 Q23) takes the slice back down. All three take the slice id
157
108
  // as a positional.
158
- 'slice start': (args, flags) => sliceCommand('start', args, flags),
159
- 'slice finish': (args, flags) => sliceCommand('finish', args, flags),
160
- 'slice abandon': (args, flags) => sliceCommand('abandon', args, flags),
161
- 'shared plan': sharedPlanCommand,
162
- 'shared install': sharedInstallCommand,
163
- evidence: evidenceCommand,
164
- index: indexCommand,
165
- map: mapCommand,
166
- tests: testsCommand,
167
- why: (args, flags) => whyCommand(args, flags, COMMANDS),
168
- gate: gateCommand,
169
- impact: (args, flags) => impactCommand(args, flags, COMMANDS),
170
- 'lessons capture': lessonsCaptureCommand,
171
- 'lessons list': lessonsListCommand,
172
- 'lessons review': lessonsReviewCommand,
173
- decide: decideCommand,
109
+ 'slice start': async () => {
110
+ const { sliceCommand } = await import('./slice.mjs');
111
+ return (args, flags) => sliceCommand('start', args, flags);
112
+ },
113
+ 'slice finish': async () => {
114
+ const { sliceCommand } = await import('./slice.mjs');
115
+ return (args, flags) => sliceCommand('finish', args, flags);
116
+ },
117
+ 'slice abandon': async () => {
118
+ const { sliceCommand } = await import('./slice.mjs');
119
+ return (args, flags) => sliceCommand('abandon', args, flags);
120
+ },
121
+ 'shared plan': async () => (await import('./shared.mjs')).sharedPlanCommand,
122
+ 'shared install': async () => (await import('./shared.mjs')).sharedInstallCommand,
123
+ evidence: async () => (await import('./evidence.mjs')).evidenceCommand,
124
+ index: async () => (await import('./repoindex.mjs')).indexCommand,
125
+ map: async () => (await import('./map.mjs')).mapCommand,
126
+ tests: async () => (await import('./tests.mjs')).testsCommand,
127
+ why: async () => {
128
+ const { whyCommand } = await import('./why.mjs');
129
+ return (args, flags) => whyCommand(args, flags, COMMANDS);
130
+ },
131
+ gate: async () => (await import('./gate.mjs')).gateCommand,
132
+ impact: async () => {
133
+ const { impactCommand } = await import('./impact.mjs');
134
+ return (args, flags) => impactCommand(args, flags, COMMANDS);
135
+ },
136
+ 'lessons capture': async () => (await import('./lessons.mjs')).lessonsCaptureCommand,
137
+ 'lessons list': async () => (await import('./lessons.mjs')).lessonsListCommand,
138
+ 'lessons review': async () => (await import('./lessons.mjs')).lessonsReviewCommand,
139
+ decide: async () => (await import('./decide.mjs')).decideCommand,
174
140
  // Plan 0003 R46. A bare command: the code is a positional, so there is no
175
141
  // group and no default subcommand to resolve.
176
- explain: explainCommand,
142
+ explain: async () => (await import('./explain.mjs')).explainCommand,
177
143
  // Plan 0003 R49. A bare command: restoring the last journaled set needs no
178
144
  // subcommand, so there is no group and no default to resolve.
179
- undo: undoCommand,
145
+ undo: async () => (await import('./undo.mjs')).undoCommand,
180
146
  // Plan 0003 R55. A bare command: the body is derived from the tree, the
181
147
  // ledger and the last check, so there is no subcommand to resolve and no
182
148
  // write to confirm.
183
- 'pr-body': prBodyCommand,
149
+ 'pr-body': async () => (await import('./prbody.mjs')).prBodyCommand,
184
150
  // Plan 0003 R52. A bare command: the one list is `.simtignore`, so there is no
185
151
  // group and no default subcommand to resolve.
186
- ignore: ignoreCommand,
152
+ ignore: async () => (await import('./ignore.mjs')).ignoreCommand,
187
153
  // Plan 0003 R54. A bare command: paths claim, no paths list, so the argument itself
188
154
  // is the mode and there is no group to resolve.
189
- lease: leaseCommand,
190
- distill: distillCommand,
155
+ lease: async () => (await import('./lease.mjs')).leaseCommand,
156
+ // Review 0003 Q60. The README's command list is the last copy of the command table
157
+ // the spec owns; the renderer lives in src/docs so a test can hold the file to it.
158
+ 'docs readme': async () => {
159
+ const { docsCommand } = await import('./docs.mjs');
160
+ return (args, flags) => docsCommand('readme', args, flags);
161
+ },
162
+ distill: async () => (await import('./distill.mjs')).distillCommand,
191
163
  // R25 machinery needs the runnable command names for Affects command tokens,
192
164
  // same wiring `why` and `impact` get through their lambdas.
193
- review: (args, flags) => reviewCommand(args, flags, COMMANDS),
194
- 'local doctor': localDoctorCommand,
195
- 'evidence check': (args, flags) => evidenceCommand(['check', ...args], flags),
196
- init: initCommand,
197
- ci: ciCommand,
198
- 'hooks install': (args, flags) => hooksCommand('install', args, flags),
199
- 'hooks status': (args, flags) => hooksCommand('status', args, flags),
200
- 'hooks run': (args, flags) => hooksCommand('run', args, flags),
201
- 'agent-hook': agentHookCommand,
202
- 'agent-hooks install': (args, flags) => agentHooksCommand('install', args, flags),
203
- 'agent-hooks status': (args, flags) => agentHooksCommand('status', args, flags),
165
+ review: async () => {
166
+ const { reviewCommand } = await import('./review.mjs');
167
+ return (args, flags) => reviewCommand(args, flags, COMMANDS);
168
+ },
169
+ 'local doctor': async () => (await import('./local.mjs')).localDoctorCommand,
170
+ 'evidence check': async () => {
171
+ const { evidenceCommand } = await import('./evidence.mjs');
172
+ return (args, flags) => evidenceCommand(['check', ...args], flags);
173
+ },
174
+ init: async () => (await import('./init.mjs')).initCommand,
175
+ ci: async () => (await import('./ci.mjs')).ciCommand,
176
+ 'hooks install': async () => {
177
+ const { hooksCommand } = await import('./hooks.mjs');
178
+ return (args, flags) => hooksCommand('install', args, flags);
179
+ },
180
+ 'hooks status': async () => {
181
+ const { hooksCommand } = await import('./hooks.mjs');
182
+ return (args, flags) => hooksCommand('status', args, flags);
183
+ },
184
+ 'hooks run': async () => {
185
+ const { hooksCommand } = await import('./hooks.mjs');
186
+ return (args, flags) => hooksCommand('run', args, flags);
187
+ },
188
+ 'agent-hook': async () => (await import('./agenthook.mjs')).agentHookCommand,
189
+ 'agent-hooks install': async () => {
190
+ const { agentHooksCommand } = await import('./agenthook.mjs');
191
+ return (args, flags) => agentHooksCommand('install', args, flags);
192
+ },
193
+ 'agent-hooks status': async () => {
194
+ const { agentHooksCommand } = await import('./agenthook.mjs');
195
+ return (args, flags) => agentHooksCommand('status', args, flags);
196
+ },
204
197
  // ADR-111. The one agent this tool ships, written into each harness's own agents
205
198
  // directory. A group like `agent-hooks`: `install` writes, `status` reads.
206
- 'agents install': (args, flags) => agentsCommand('install', args, flags),
207
- 'agents status': (args, flags) => agentsCommand('status', args, flags),
208
- compare: compareCommand,
209
- tokens: tokensCommand,
210
- 'harness detect': harnessCommand,
211
- 'harness loaders': harnessLoadersCommand,
212
- 'bench mine': benchMineCommand,
213
- 'bench run': benchRunCommand,
214
- 'bench report': benchReportCommand,
215
- 'bench ablate': benchAblateCommand,
216
- 'bench probes': benchProbesCommand,
217
- 'bench variants': benchVariantsCommand,
199
+ 'agents install': async () => {
200
+ const { agentsCommand } = await import('./agent.mjs');
201
+ return (args, flags) => agentsCommand('install', args, flags);
202
+ },
203
+ 'agents status': async () => {
204
+ const { agentsCommand } = await import('./agent.mjs');
205
+ return (args, flags) => agentsCommand('status', args, flags);
206
+ },
207
+ compare: async () => (await import('./compare.mjs')).compareCommand,
208
+ tokens: async () => (await import('./tokens.mjs')).tokensCommand,
209
+ 'harness detect': async () => (await import('./harness.mjs')).harnessCommand,
210
+ 'harness loaders': async () => (await import('./harness.mjs')).harnessLoadersCommand,
211
+ 'bench mine': async () => (await import('./bench.mjs')).benchMineCommand,
212
+ 'bench run': async () => (await import('./bench.mjs')).benchRunCommand,
213
+ 'bench report': async () => (await import('./bench.mjs')).benchReportCommand,
214
+ 'bench ablate': async () => (await import('./bench.mjs')).benchAblateCommand,
215
+ 'bench probes': async () => (await import('./bench.mjs')).benchProbesCommand,
216
+ 'bench variants': async () => (await import('./bench.mjs')).benchVariantsCommand,
218
217
  // plan 0003 R39. The critic's acceptance bench measures BLOCK-precision and
219
218
  // recall over a labelled set; until it reports its floor, the critic entries
220
219
  // stay advisory and no gate reads them. COMMANDS rides along so the brief
221
220
  // resolves Affects command tokens the way `simt why` does.
222
- 'bench critic': (args, flags) => benchCriticCommand(args, flags, COMMANDS),
223
- upgrade: upgradeCommand,
224
- mcp: mcpCommand,
221
+ 'bench critic': async () => {
222
+ const { benchCriticCommand } = await import('./bench.mjs');
223
+ return (args, flags) => benchCriticCommand(args, flags, COMMANDS);
224
+ },
225
+ // review 0003 Q48. Sealed corpus transfer: export writes one signed JSON
226
+ // document carrying every task and both hidden patches; import verifies the
227
+ // seal before it touches the vault.
228
+ 'bench export': async () => (await import('./bench.mjs')).benchExportCommand,
229
+ 'bench import': async () => (await import('./bench.mjs')).benchImportCommand,
230
+ upgrade: async () => (await import('./upgrade.mjs')).upgradeCommand,
231
+ mcp: async () => (await import('./mcp.mjs')).mcpCommand,
225
232
  // plan 0003 R43. A bare command: the kind is a positional, so there is no group and
226
233
  // no default subcommand to resolve.
227
- plugin: pluginCommand,
234
+ plugin: async () => (await import('./plugin.mjs')).pluginCommand,
228
235
  // Plan 0003 R51. A bare command: the shell is a positional. `COMMANDS` rides in
229
236
  // from here so the engine keeps no second copy of the registry it completes.
230
- completions: (args, flags) => completionsCommand(args, flags, COMMANDS),
237
+ completions: async () => {
238
+ const { completionsCommand } = await import('./completions.mjs');
239
+ return (args, flags) => completionsCommand(args, flags, COMMANDS);
240
+ },
231
241
  // Plan 0003 R45. A group, not a bare command: `new` and `publish` write (or
232
242
  // publish) and `lint` reads, and the dispatcher routes all three through one
233
243
  // subcommand switch the way `hooks` does.
234
- 'pack new': (args, flags) => packCommand('new', args, flags),
235
- 'pack lint': (args, flags) => packCommand('lint', args, flags),
236
- 'pack publish': (args, flags) => packCommand('publish', args, flags),
244
+ 'pack new': async () => {
245
+ const { packCommand } = await import('./pack.mjs');
246
+ return (args, flags) => packCommand('new', args, flags);
247
+ },
248
+ 'pack lint': async () => {
249
+ const { packCommand } = await import('./pack.mjs');
250
+ return (args, flags) => packCommand('lint', args, flags);
251
+ },
252
+ 'pack publish': async () => {
253
+ const { packCommand } = await import('./pack.mjs');
254
+ return (args, flags) => packCommand('publish', args, flags);
255
+ },
237
256
  };
238
257
 
258
+ /**
259
+ * Resolve one command path to its handler, its declared flags and its closed value sets.
260
+ *
261
+ * The library surface needs the same answer `dispatch` gets and must not build a second
262
+ * registry to get it: a command reachable from the CLI and absent from `run` is the drift
263
+ * `test/spec.test.mjs` exists to catch, and it would be invisible here (review 0003 Q63).
264
+ */
265
+ export async function resolveCommand(path) {
266
+ const load = IMPLEMENTED[path];
267
+ if (!load) return null;
268
+ return {
269
+ run: await load(),
270
+ declared: DECLARED.get(path) ?? new Set(),
271
+ values: VALUES.get(path) ?? {},
272
+ };
273
+ }
274
+
275
+ /** The release that turns the tolerated flag into a usage error (review 0003 Q62). */
276
+ export const DEPRECATION_RELEASE = '0.9.0';
277
+
278
+ /**
279
+ * Whether a tolerance that ends at `removedIn` has ended, comparing the way versions
280
+ * are numbered rather than the way strings are ordered: 0.10.0 is later than 0.9.0, and
281
+ * a string comparison says the opposite. An unreadable version refuses nothing - a build
282
+ * whose version cannot be read has not outlived anything, and guessing would break it.
283
+ */
284
+ export function refusesAt(removedIn, version) {
285
+ const parse = (value) => {
286
+ const parts = String(value ?? '').split('.');
287
+ if (parts.length < 2 || parts.some((part) => !/^\d+$/.test(part))) return null;
288
+ return parts.map(Number);
289
+ };
290
+ const limit = parse(removedIn);
291
+ const current = parse(version);
292
+ if (limit === null || current === null) return false;
293
+ const [a, b] = [current[0] - limit[0], current[1] - limit[1]];
294
+ if (a !== 0) return a > 0;
295
+ return b >= 0;
296
+ }
297
+
239
298
  /**
240
299
  * The dispatchable command paths, sorted. The authoritative answer to "what can this
241
- * binary do", as opposed to the answer `--help` gives.
300
+ * binary do", as opposed to the answer `--help` gives - and the same list the spec
301
+ * declares, so a command cannot be dispatchable and undeclared.
242
302
  */
243
- export const COMMANDS = Object.keys(IMPLEMENTED).sort();
303
+ export const COMMANDS = SPEC_PATHS;
244
304
 
305
+ /**
306
+ * Dispatch one argv.
307
+ *
308
+ * The state is per call: an MCP server and the tests both dispatch more than once in a
309
+ * process, and a shared envelope let one run's ceiling, notices and command path land in
310
+ * another run's document (review 0003 Q63/Q65).
311
+ */
245
312
  export async function dispatch(argv) {
313
+ return withOutputState(() => dispatchInState(argv));
314
+ }
315
+
316
+ async function dispatchInState(argv) {
246
317
  const { flags, positional, parseError } = parseOrExit(argv);
318
+ // One run, one set of notices: a process can dispatch repeatedly (review 0003 Q62).
319
+ clearWarnings();
320
+ // Cleared per run for the same reason the notices are: a process can dispatch twice.
321
+ jsonCommand(null);
247
322
 
248
323
  // The JSON shape is decided once, here, and every command inherits it. Threading a
249
324
  // `--pretty` through twenty-five call sites would mean twenty-five places to forget,
@@ -254,6 +329,10 @@ export async function dispatch(argv) {
254
329
  // command answers in the same shape a working one does, and the caller's parse
255
330
  // never sees an empty document.
256
331
  jsonIndentDefault(flags.pretty === true);
332
+ // The envelope owns both presentation flags. A command that decided its own ceiling or
333
+ // its own indentation could disagree with the flag the caller passed, and the caller has
334
+ // no way to see which one won (review 0003 Q65).
335
+ maxBytesLimit(flags.maxBytes);
257
336
  jsonFailMode(flags.json === true);
258
337
 
259
338
  // A parse failure is already the whole answer: report it once, in the asked-for
@@ -263,8 +342,12 @@ export async function dispatch(argv) {
263
342
  // Help is shown only when explicitly asked for. An unknown command must fail with
264
343
  // a usage code, not print 40 lines of prose and exit 0 — an agent that mistyped
265
344
  // needs to be told so, not handed a wall of text it may misread as output.
266
- if (flags.help || flags.version || positional[0] === 'help') {
267
- return help(argv);
345
+ // `-h` is the alias a reader types first, and it used to reach `unknown command '-h'`.
346
+ if (flags.help || flags.version || positional[0] === 'help' || positional[0] === '-h') {
347
+ // The argv is parsed once, here, and help renders what that parse produced: it used
348
+ // to parse the same argv again, so `simt --help` paid for two parses and could in
349
+ // principle answer a different question than the dispatcher asked (review 0003 Q83).
350
+ return help(argv, { flags, positional, parseError });
268
351
  }
269
352
 
270
353
  const [first, second] = positional;
@@ -277,31 +360,75 @@ export async function dispatch(argv) {
277
360
  return fail(`unknown command \`${[first, second].filter(Boolean).join(' ')}\``, EXIT.USAGE);
278
361
  }
279
362
 
280
- const step = STEPS.get(path);
281
- if (step !== undefined) {
282
- // Two things a reader must be able to act on, and neither may name a path inside
283
- // simt-dev: they run this tool, not its source tree. The step number is the
284
- // stable handle; the file that documents it is an implementation detail.
285
- return fail(
286
- `\`${CLI} ${path}\` is declared but not implemented`,
287
- EXIT.UNSUPPORTED,
288
- `implementation step ${step} is not built yet`,
289
- );
290
- }
363
+ // One path, stated once, by the only code that knows which one resolved.
364
+ jsonCommand(path);
365
+
291
366
  const args = positional.slice(offset);
292
- const run = IMPLEMENTED[path];
293
- if (run) return run(args, flags);
294
- return fail(`unknown command \`${path}\``, EXIT.USAGE);
367
+
368
+ // Review 0003 Q62, deprecation window. The parser accepts every flag for every
369
+ // command, so `status --frozen` ran, ignored the flag and reported success - the
370
+ // caller's intent discarded with nothing to notice. A flag the spec does not declare
371
+ // for this path is named on stderr and still runs for one release; the next release
372
+ // makes it EXIT.USAGE. `--json` has no warnings array yet, so the JSON half of the
373
+ // window waits on the envelope (Q65).
374
+ const declared = DECLARED.get(path);
375
+ const refusing = refusesAt(DEPRECATION_RELEASE, packageVersion());
376
+ for (const key of Object.keys(flags)) {
377
+ if (!declared || GLOBAL_FLAGS.has(key)) continue;
378
+ const name = key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
379
+ if (declared.has(name)) continue;
380
+ if (refusing) {
381
+ return fail(`--${name} is not a flag of \`${path}\`; it was tolerated until ${DEPRECATION_RELEASE} and is refused now`, EXIT.USAGE);
382
+ }
383
+ const message = `--${name} is not a flag of \`${path}\`; it was ignored, and naming one this command does not take is a usage error from ${DEPRECATION_RELEASE}`;
384
+ warn(message);
385
+ if (!isJson(flags)) note(message);
386
+ }
387
+ const load = IMPLEMENTED[path];
388
+ if (!load) return fail(`unknown command \`${path}\``, EXIT.USAGE);
389
+ // Import only the module the matched path needs. The registry used to pull every
390
+ // command's module graph - detection, projection, the bench corpus - before dispatch
391
+ // began, which cost 36.5 ms on a `status --line` that reads one file (review 0003 Q64).
392
+ // A closed set is refused before the command runs, so the caller learns the valid
393
+ // values from the refusal and every reader of the spec sees the same set (review
394
+ // 0003 Q62). Unset is not the same as invalid: the default stays the command's.
395
+ for (const [flag, allowed] of Object.entries(VALUES.get(path) ?? {})) {
396
+ const value = flags[flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase())];
397
+ if (value !== undefined && !allowed.includes(value)) {
398
+ return fail(`--${flag} takes ${allowed.join(', ')}; received ${value}`, EXIT.USAGE);
399
+ }
400
+ }
401
+
402
+ const run = await load();
403
+ if (!isJson(flags)) return run(args, flags);
404
+ // Capture, then write once: the envelope names an exit code that does not exist until
405
+ // the command returns, and a document written before it is a document that can lie.
406
+ jsonCaptureStart();
407
+ const code = await run(args, flags);
408
+ const docs = jsonCaptureTake() ?? [];
409
+ const [only] = docs;
410
+ if (docs.length === 1 && only.value !== null && typeof only.value === 'object' && !Array.isArray(only.value)) {
411
+ emitJson({ ...only.value, ok: only.value.ok ?? code === 0, exitCode: code });
412
+ return code;
413
+ }
414
+ // A stream of documents (NDJSON) has no single place for one exit code; each is written
415
+ // as the command produced it, and the process exit status still carries the code.
416
+ for (const doc of docs) emitJson(doc.value);
417
+ return code;
295
418
  }
296
419
 
297
420
  /**
298
421
  * Normalise argv into a registry key, or null when unrecognised.
299
422
  * A group with an omitted subcommand resolves to that group's default, so
300
423
  * `simt skills` is `simt skills index`.
424
+ *
425
+ * An alias resolves here rather than as a second registry entry: the spec path stays the
426
+ * one dispatch key, so an alias cannot load a different handler or escape the flag
427
+ * declaration the path carries (review 0003 Q81).
301
428
  */
302
429
  function commandPath(first, second) {
303
430
  if (first === undefined) return null;
304
- if (!GROUPS.has(first)) return first;
431
+ if (!GROUPS.has(first)) return ALIASES.get(first) ?? first;
305
432
 
306
433
  const group = GROUPS.get(first);
307
434
  if (second === undefined) return `${first} ${group}`;
@@ -315,10 +442,8 @@ function commandPath(first, second) {
315
442
  // `doctor` is not a group and this branch is only reached when `GROUPS.has(first)`.
316
443
  // Two lists of the same fact is the defect S15 was about in the help text.
317
444
  const key = `${first} ${second}`;
318
- if (!STEPS.has(key) && !(key in IMPLEMENTED)) {
319
- return null;
320
- }
321
- return key;
445
+ if (key in IMPLEMENTED) return key;
446
+ return ALIASES.get(key) ?? null;
322
447
  }
323
448
 
324
449
  function parseOrExit(argv) {