simt-dev 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +158 -872
  2. package/README.md +19 -133
  3. package/bin/simt.mjs +19 -55
  4. package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
  5. package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
  6. package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
  7. 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
  8. package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
  9. package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
  10. package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
  11. package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
  12. package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
  13. package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
  14. package/package.json +10 -2
  15. package/rules/core.md +7 -0
  16. package/rules/mdc/slice-commit.mdc +57 -4
  17. package/src/agenthooks/prompt.mjs +5 -2
  18. package/src/agenthooks/shim.mjs +1 -1
  19. package/src/api.mjs +29 -0
  20. package/src/argv.mjs +29 -3
  21. package/src/bench/adapters.json +16 -0
  22. package/src/bench/corpus.mjs +150 -0
  23. package/src/bench/grade.mjs +23 -49
  24. package/src/bench/mine.mjs +199 -35
  25. package/src/bench/probes.mjs +99 -62
  26. package/src/bench/process.mjs +128 -0
  27. package/src/bench/report.mjs +17 -4
  28. package/src/bench/run.mjs +242 -142
  29. package/src/brief/assemble.mjs +38 -3
  30. package/src/ci/workflow.mjs +30 -7
  31. package/src/cli.mjs +45 -0
  32. package/src/commands/agent.mjs +3 -4
  33. package/src/commands/agenthook.mjs +4 -5
  34. package/src/commands/bench.mjs +190 -17
  35. package/src/commands/brief.mjs +5 -4
  36. package/src/commands/ci.mjs +2 -1
  37. package/src/commands/compare.mjs +42 -42
  38. package/src/commands/completions.mjs +2 -6
  39. package/src/commands/decide.mjs +4 -4
  40. package/src/commands/distill.mjs +4 -3
  41. package/src/commands/doctor.mjs +11 -7
  42. package/src/commands/evidence.mjs +6 -9
  43. package/src/commands/explain.mjs +0 -1
  44. package/src/commands/gate.mjs +16 -12
  45. package/src/commands/graph.mjs +2 -3
  46. package/src/commands/harness.mjs +3 -2
  47. package/src/commands/help.mjs +125 -17
  48. package/src/commands/hooks.mjs +100 -30
  49. package/src/commands/ignore.mjs +2 -1
  50. package/src/commands/impact.mjs +4 -7
  51. package/src/commands/index.mjs +273 -188
  52. package/src/commands/init.mjs +2 -1
  53. package/src/commands/lease.mjs +6 -7
  54. package/src/commands/lessons.mjs +10 -9
  55. package/src/commands/local.mjs +3 -3
  56. package/src/commands/map.mjs +14 -9
  57. package/src/commands/mcp.mjs +0 -1
  58. package/src/commands/pack.mjs +9 -6
  59. package/src/commands/plan.mjs +6 -5
  60. package/src/commands/plugin.mjs +2 -2
  61. package/src/commands/prbody.mjs +3 -3
  62. package/src/commands/repoindex.mjs +4 -3
  63. package/src/commands/review.mjs +2 -2
  64. package/src/commands/rules.mjs +33 -16
  65. package/src/commands/shared.mjs +2 -3
  66. package/src/commands/skills.mjs +28 -26
  67. package/src/commands/skillscompile.mjs +4 -5
  68. package/src/commands/skillsinstall.mjs +2 -2
  69. package/src/commands/slice.mjs +21 -10
  70. package/src/commands/spec.mjs +151 -0
  71. package/src/commands/status.mjs +4 -7
  72. package/src/commands/sync.mjs +13 -11
  73. package/src/commands/tests.mjs +6 -9
  74. package/src/commands/tokens.mjs +2 -2
  75. package/src/commands/undo.mjs +5 -15
  76. package/src/commands/upgrade.mjs +7 -3
  77. package/src/commands/why.mjs +9 -8
  78. package/src/compare/row.mjs +0 -2
  79. package/src/context.mjs +64 -0
  80. package/src/decisions/index-doc.mjs +46 -0
  81. package/src/diff.mjs +1 -1
  82. package/src/distill/distill.mjs +14 -1
  83. package/src/distill/engine.mjs +5 -1
  84. package/src/graph/load.mjs +4 -1
  85. package/src/hooks/capability.mjs +3 -3
  86. package/src/hooks/dispatcher.mjs +198 -43
  87. package/src/hooks/gates.mjs +0 -43
  88. package/src/hooks/resolve.mjs +2 -7
  89. package/src/hooks/shim.mjs +5 -2
  90. package/src/identity.mjs +29 -1
  91. package/src/journal.mjs +78 -9
  92. package/src/ledger/index.mjs +30 -1
  93. package/src/local/client.mjs +4 -6
  94. package/src/locate.mjs +33 -9
  95. package/src/manifest/schema.mjs +213 -0
  96. package/src/mcp/resources.mjs +7 -10
  97. package/src/mcp/server.mjs +70 -16
  98. package/src/mcp/tools.mjs +45 -7
  99. package/src/measure/session.mjs +3 -2
  100. package/src/node-version.mjs +37 -0
  101. package/src/output/index.mjs +111 -3
  102. package/src/packs/manage.mjs +17 -3
  103. package/src/packs.mjs +20 -2
  104. package/src/projection/lock.mjs +11 -1
  105. package/src/projection/manifest.mjs +20 -34
  106. package/src/projection/rules.mjs +4 -3
  107. package/src/repo-root.mjs +19 -0
  108. package/src/resolve/fingerprint.mjs +19 -15
  109. package/src/review/critic.mjs +2 -1
  110. package/src/toolwords.mjs +3 -0
  111. package/src/workspace.mjs +55 -10
  112. package/docs/decisions/README.md +0 -600
@@ -8,111 +8,43 @@
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,
17
24
  } from '../output/index.mjs';
18
25
  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';
26
+ import { GROUP_DEFAULTS, SPEC, SPEC_PATHS } from './spec.mjs';
77
27
  import { parse, rawJsonFlag } from '../argv.mjs';
78
28
  import { CLI } from '../name.mjs';
29
+ import { packageVersion } from '../package-version.mjs';
30
+
31
+
32
+ /**
33
+ * Groups whose subcommand may be omitted, and the default for each. Read from the spec
34
+ * (review 0003 Q60) rather than kept here: this was the second copy of a fact the spec
35
+ * owns, and the reason for each default now sits on the row that declares it.
36
+ */
37
+ export const GROUPS = GROUP_DEFAULTS;
38
+
39
+ /** path -> the flags the spec declares for it. The parser knows every flag; this is
40
+ * what one command reads, and the difference is what the caller is told (Q62). */
41
+ const DECLARED = new Map(SPEC.map((row) => [row.path, new Set(row.flags ?? [])]));
79
42
 
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([]);
43
+ /** Flags dispatch reads for every command: the envelope and the guard rails. */
44
+ /** path -> the values a flag of that path accepts, where the set is closed. */
45
+ const VALUES = new Map(SPEC.map((row) => [row.path, row.values ?? {}]));
86
46
 
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
- ]);
47
+ const GLOBAL_FLAGS = new Set(['root', 'json', 'pretty', 'help', 'version', 'maxBytes']);
116
48
 
117
49
  /**
118
50
  * Every command this binary dispatches, in one place.
@@ -124,126 +56,231 @@ const GROUPS = new Map([
124
56
  * an agent choosing a command — the one consumer with no way to discover the omission
125
57
  * except by guessing.
126
58
  *
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.
59
+ * Module scope so `test/cli.test.mjs` can assert help against it. Each value is a
60
+ * loader rather than a handler: dispatch awaits the one the matched path names, so a
61
+ * command pays for its own module graph and not for the other seventy-five (review 0003
62
+ * Q64). A loader that never resolves is a command that cannot run, and `test/spec.test.mjs`
63
+ * still holds the key set against the spec.
129
64
  */
130
65
  export const IMPLEMENTED = {
131
- brief: briefCommand,
132
- doctor: runDoctor,
66
+ brief: async () => {
67
+ const { briefCommand } = await import('./brief.mjs');
68
+ return (args, flags) => briefCommand(args, flags, COMMANDS);
69
+ },
70
+ doctor: async () => (await import('./doctor.mjs')).runDoctor,
133
71
  // Plan 0003 R48. A bare command: the only mode is `--line`, so there is no
134
72
  // 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),
73
+ status: async () => (await import('./status.mjs')).statusCommand,
74
+ 'graph lint': async () => (await import('./graph.mjs')).lintCommand,
75
+ 'skills index': async () => (await import('./skills.mjs')).indexCommand,
76
+ 'skills find': async () => (await import('./skills.mjs')).findCommand,
77
+ 'skills brief': async () => (await import('./skills.mjs')).briefCommand,
78
+ 'skills show': async () => (await import('./skills.mjs')).showCommand,
79
+ 'skills why': async () => (await import('./skills.mjs')).whyCommand,
80
+ 'skills eval': async () => (await import('./skills.mjs')).evalCommand,
81
+ 'skills install': async () => (await import('./skillsinstall.mjs')).installCommand,
82
+ 'skills compile': async () => (await import('./skillscompile.mjs')).compileCommand,
83
+ 'rules decisions': async () => (await import('./rules.mjs')).decisionsCommand,
84
+ 'rules list': async () => (await import('./rules.mjs')).listCommand,
85
+ 'rules show': async () => (await import('./rules.mjs')).showCommand,
86
+ 'rules why': async () => (await import('./rules.mjs')).whyCommand,
87
+ sync: async () => (await import('./sync.mjs')).syncCommand,
88
+ check: async () => (await import('./sync.mjs')).checkCommand,
89
+ 'plan status': async () => {
90
+ const { planCommand } = await import('./plan.mjs');
91
+ return (args, flags) => planCommand('status', args, flags);
92
+ },
93
+ 'plan next': async () => {
94
+ const { planCommand } = await import('./plan.mjs');
95
+ return (args, flags) => planCommand('next', args, flags);
96
+ },
97
+ 'plan done': async () => {
98
+ const { planCommand } = await import('./plan.mjs');
99
+ return (args, flags) => planCommand('done', args, flags);
100
+ },
154
101
  // Plan 0003 R53. A group: `start` isolates the work on a branch with its brief,
155
102
  // `finish` runs the gates, commits and records the evidence, and `abandon`
156
103
  // (review 0003 Q23) takes the slice back down. All three take the slice id
157
104
  // 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,
105
+ 'slice start': async () => {
106
+ const { sliceCommand } = await import('./slice.mjs');
107
+ return (args, flags) => sliceCommand('start', args, flags);
108
+ },
109
+ 'slice finish': async () => {
110
+ const { sliceCommand } = await import('./slice.mjs');
111
+ return (args, flags) => sliceCommand('finish', args, flags);
112
+ },
113
+ 'slice abandon': async () => {
114
+ const { sliceCommand } = await import('./slice.mjs');
115
+ return (args, flags) => sliceCommand('abandon', args, flags);
116
+ },
117
+ 'shared plan': async () => (await import('./shared.mjs')).sharedPlanCommand,
118
+ 'shared install': async () => (await import('./shared.mjs')).sharedInstallCommand,
119
+ evidence: async () => (await import('./evidence.mjs')).evidenceCommand,
120
+ index: async () => (await import('./repoindex.mjs')).indexCommand,
121
+ map: async () => (await import('./map.mjs')).mapCommand,
122
+ tests: async () => (await import('./tests.mjs')).testsCommand,
123
+ why: async () => {
124
+ const { whyCommand } = await import('./why.mjs');
125
+ return (args, flags) => whyCommand(args, flags, COMMANDS);
126
+ },
127
+ gate: async () => (await import('./gate.mjs')).gateCommand,
128
+ impact: async () => {
129
+ const { impactCommand } = await import('./impact.mjs');
130
+ return (args, flags) => impactCommand(args, flags, COMMANDS);
131
+ },
132
+ 'lessons capture': async () => (await import('./lessons.mjs')).lessonsCaptureCommand,
133
+ 'lessons list': async () => (await import('./lessons.mjs')).lessonsListCommand,
134
+ 'lessons review': async () => (await import('./lessons.mjs')).lessonsReviewCommand,
135
+ decide: async () => (await import('./decide.mjs')).decideCommand,
174
136
  // Plan 0003 R46. A bare command: the code is a positional, so there is no
175
137
  // group and no default subcommand to resolve.
176
- explain: explainCommand,
138
+ explain: async () => (await import('./explain.mjs')).explainCommand,
177
139
  // Plan 0003 R49. A bare command: restoring the last journaled set needs no
178
140
  // subcommand, so there is no group and no default to resolve.
179
- undo: undoCommand,
141
+ undo: async () => (await import('./undo.mjs')).undoCommand,
180
142
  // Plan 0003 R55. A bare command: the body is derived from the tree, the
181
143
  // ledger and the last check, so there is no subcommand to resolve and no
182
144
  // write to confirm.
183
- 'pr-body': prBodyCommand,
145
+ 'pr-body': async () => (await import('./prbody.mjs')).prBodyCommand,
184
146
  // Plan 0003 R52. A bare command: the one list is `.simtignore`, so there is no
185
147
  // group and no default subcommand to resolve.
186
- ignore: ignoreCommand,
148
+ ignore: async () => (await import('./ignore.mjs')).ignoreCommand,
187
149
  // Plan 0003 R54. A bare command: paths claim, no paths list, so the argument itself
188
150
  // is the mode and there is no group to resolve.
189
- lease: leaseCommand,
190
- distill: distillCommand,
151
+ lease: async () => (await import('./lease.mjs')).leaseCommand,
152
+ distill: async () => (await import('./distill.mjs')).distillCommand,
191
153
  // R25 machinery needs the runnable command names for Affects command tokens,
192
154
  // 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),
155
+ review: async () => {
156
+ const { reviewCommand } = await import('./review.mjs');
157
+ return (args, flags) => reviewCommand(args, flags, COMMANDS);
158
+ },
159
+ 'local doctor': async () => (await import('./local.mjs')).localDoctorCommand,
160
+ 'evidence check': async () => {
161
+ const { evidenceCommand } = await import('./evidence.mjs');
162
+ return (args, flags) => evidenceCommand(['check', ...args], flags);
163
+ },
164
+ init: async () => (await import('./init.mjs')).initCommand,
165
+ ci: async () => (await import('./ci.mjs')).ciCommand,
166
+ 'hooks install': async () => {
167
+ const { hooksCommand } = await import('./hooks.mjs');
168
+ return (args, flags) => hooksCommand('install', args, flags);
169
+ },
170
+ 'hooks status': async () => {
171
+ const { hooksCommand } = await import('./hooks.mjs');
172
+ return (args, flags) => hooksCommand('status', args, flags);
173
+ },
174
+ 'hooks run': async () => {
175
+ const { hooksCommand } = await import('./hooks.mjs');
176
+ return (args, flags) => hooksCommand('run', args, flags);
177
+ },
178
+ 'agent-hook': async () => (await import('./agenthook.mjs')).agentHookCommand,
179
+ 'agent-hooks install': async () => {
180
+ const { agentHooksCommand } = await import('./agenthook.mjs');
181
+ return (args, flags) => agentHooksCommand('install', args, flags);
182
+ },
183
+ 'agent-hooks status': async () => {
184
+ const { agentHooksCommand } = await import('./agenthook.mjs');
185
+ return (args, flags) => agentHooksCommand('status', args, flags);
186
+ },
204
187
  // ADR-111. The one agent this tool ships, written into each harness's own agents
205
188
  // 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,
189
+ 'agents install': async () => {
190
+ const { agentsCommand } = await import('./agent.mjs');
191
+ return (args, flags) => agentsCommand('install', args, flags);
192
+ },
193
+ 'agents status': async () => {
194
+ const { agentsCommand } = await import('./agent.mjs');
195
+ return (args, flags) => agentsCommand('status', args, flags);
196
+ },
197
+ compare: async () => (await import('./compare.mjs')).compareCommand,
198
+ tokens: async () => (await import('./tokens.mjs')).tokensCommand,
199
+ 'harness detect': async () => (await import('./harness.mjs')).harnessCommand,
200
+ 'harness loaders': async () => (await import('./harness.mjs')).harnessLoadersCommand,
201
+ 'bench mine': async () => (await import('./bench.mjs')).benchMineCommand,
202
+ 'bench run': async () => (await import('./bench.mjs')).benchRunCommand,
203
+ 'bench report': async () => (await import('./bench.mjs')).benchReportCommand,
204
+ 'bench ablate': async () => (await import('./bench.mjs')).benchAblateCommand,
205
+ 'bench probes': async () => (await import('./bench.mjs')).benchProbesCommand,
206
+ 'bench variants': async () => (await import('./bench.mjs')).benchVariantsCommand,
218
207
  // plan 0003 R39. The critic's acceptance bench measures BLOCK-precision and
219
208
  // recall over a labelled set; until it reports its floor, the critic entries
220
209
  // stay advisory and no gate reads them. COMMANDS rides along so the brief
221
210
  // resolves Affects command tokens the way `simt why` does.
222
- 'bench critic': (args, flags) => benchCriticCommand(args, flags, COMMANDS),
223
- upgrade: upgradeCommand,
224
- mcp: mcpCommand,
211
+ 'bench critic': async () => {
212
+ const { benchCriticCommand } = await import('./bench.mjs');
213
+ return (args, flags) => benchCriticCommand(args, flags, COMMANDS);
214
+ },
215
+ // review 0003 Q48. Sealed corpus transfer: export writes one signed JSON
216
+ // document carrying every task and both hidden patches; import verifies the
217
+ // seal before it touches the vault.
218
+ 'bench export': async () => (await import('./bench.mjs')).benchExportCommand,
219
+ 'bench import': async () => (await import('./bench.mjs')).benchImportCommand,
220
+ upgrade: async () => (await import('./upgrade.mjs')).upgradeCommand,
221
+ mcp: async () => (await import('./mcp.mjs')).mcpCommand,
225
222
  // plan 0003 R43. A bare command: the kind is a positional, so there is no group and
226
223
  // no default subcommand to resolve.
227
- plugin: pluginCommand,
224
+ plugin: async () => (await import('./plugin.mjs')).pluginCommand,
228
225
  // Plan 0003 R51. A bare command: the shell is a positional. `COMMANDS` rides in
229
226
  // from here so the engine keeps no second copy of the registry it completes.
230
- completions: (args, flags) => completionsCommand(args, flags, COMMANDS),
227
+ completions: async () => {
228
+ const { completionsCommand } = await import('./completions.mjs');
229
+ return (args, flags) => completionsCommand(args, flags, COMMANDS);
230
+ },
231
231
  // Plan 0003 R45. A group, not a bare command: `new` and `publish` write (or
232
232
  // publish) and `lint` reads, and the dispatcher routes all three through one
233
233
  // 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),
234
+ 'pack new': async () => {
235
+ const { packCommand } = await import('./pack.mjs');
236
+ return (args, flags) => packCommand('new', args, flags);
237
+ },
238
+ 'pack lint': async () => {
239
+ const { packCommand } = await import('./pack.mjs');
240
+ return (args, flags) => packCommand('lint', args, flags);
241
+ },
242
+ 'pack publish': async () => {
243
+ const { packCommand } = await import('./pack.mjs');
244
+ return (args, flags) => packCommand('publish', args, flags);
245
+ },
237
246
  };
238
247
 
248
+ /** The release that turns the tolerated flag into a usage error (review 0003 Q62). */
249
+ export const DEPRECATION_RELEASE = '0.8.0';
250
+
251
+ /**
252
+ * Whether a tolerance that ends at `removedIn` has ended, comparing the way versions
253
+ * are numbered rather than the way strings are ordered: 0.10.0 is later than 0.9.0, and
254
+ * a string comparison says the opposite. An unreadable version refuses nothing - a build
255
+ * whose version cannot be read has not outlived anything, and guessing would break it.
256
+ */
257
+ export function refusesAt(removedIn, version) {
258
+ const parse = (value) => {
259
+ const parts = String(value ?? '').split('.');
260
+ if (parts.length < 2 || parts.some((part) => !/^\d+$/.test(part))) return null;
261
+ return parts.map(Number);
262
+ };
263
+ const limit = parse(removedIn);
264
+ const current = parse(version);
265
+ if (limit === null || current === null) return false;
266
+ const [a, b] = [current[0] - limit[0], current[1] - limit[1]];
267
+ if (a !== 0) return a > 0;
268
+ return b >= 0;
269
+ }
270
+
239
271
  /**
240
272
  * The dispatchable command paths, sorted. The authoritative answer to "what can this
241
- * binary do", as opposed to the answer `--help` gives.
273
+ * binary do", as opposed to the answer `--help` gives - and the same list the spec
274
+ * declares, so a command cannot be dispatchable and undeclared.
242
275
  */
243
- export const COMMANDS = Object.keys(IMPLEMENTED).sort();
276
+ export const COMMANDS = SPEC_PATHS;
244
277
 
245
278
  export async function dispatch(argv) {
246
279
  const { flags, positional, parseError } = parseOrExit(argv);
280
+ // One run, one set of notices: a process can dispatch repeatedly (review 0003 Q62).
281
+ clearWarnings();
282
+ // Cleared per run for the same reason the notices are: a process can dispatch twice.
283
+ jsonCommand(null);
247
284
 
248
285
  // The JSON shape is decided once, here, and every command inherits it. Threading a
249
286
  // `--pretty` through twenty-five call sites would mean twenty-five places to forget,
@@ -254,6 +291,10 @@ export async function dispatch(argv) {
254
291
  // command answers in the same shape a working one does, and the caller's parse
255
292
  // never sees an empty document.
256
293
  jsonIndentDefault(flags.pretty === true);
294
+ // The envelope owns both presentation flags. A command that decided its own ceiling or
295
+ // its own indentation could disagree with the flag the caller passed, and the caller has
296
+ // no way to see which one won (review 0003 Q65).
297
+ maxBytesLimit(flags.maxBytes);
257
298
  jsonFailMode(flags.json === true);
258
299
 
259
300
  // A parse failure is already the whole answer: report it once, in the asked-for
@@ -263,8 +304,12 @@ export async function dispatch(argv) {
263
304
  // Help is shown only when explicitly asked for. An unknown command must fail with
264
305
  // a usage code, not print 40 lines of prose and exit 0 — an agent that mistyped
265
306
  // 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);
307
+ // `-h` is the alias a reader types first, and it used to reach `unknown command '-h'`.
308
+ if (flags.help || flags.version || positional[0] === 'help' || positional[0] === '-h') {
309
+ // The argv is parsed once, here, and help renders what that parse produced: it used
310
+ // to parse the same argv again, so `simt --help` paid for two parses and could in
311
+ // principle answer a different question than the dispatcher asked (review 0003 Q83).
312
+ return help(argv, { flags, positional, parseError });
268
313
  }
269
314
 
270
315
  const [first, second] = positional;
@@ -277,21 +322,61 @@ export async function dispatch(argv) {
277
322
  return fail(`unknown command \`${[first, second].filter(Boolean).join(' ')}\``, EXIT.USAGE);
278
323
  }
279
324
 
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
- }
325
+ // One path, stated once, by the only code that knows which one resolved.
326
+ jsonCommand(path);
327
+
291
328
  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);
329
+
330
+ // Review 0003 Q62, deprecation window. The parser accepts every flag for every
331
+ // command, so `status --frozen` ran, ignored the flag and reported success - the
332
+ // caller's intent discarded with nothing to notice. A flag the spec does not declare
333
+ // for this path is named on stderr and still runs for one release; the next release
334
+ // makes it EXIT.USAGE. `--json` has no warnings array yet, so the JSON half of the
335
+ // window waits on the envelope (Q65).
336
+ const declared = DECLARED.get(path);
337
+ const refusing = refusesAt(DEPRECATION_RELEASE, packageVersion());
338
+ for (const key of Object.keys(flags)) {
339
+ if (!declared || GLOBAL_FLAGS.has(key)) continue;
340
+ const name = key.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
341
+ if (declared.has(name)) continue;
342
+ if (refusing) {
343
+ return fail(`--${name} is not a flag of \`${path}\`; it was tolerated until ${DEPRECATION_RELEASE} and is refused now`, EXIT.USAGE);
344
+ }
345
+ 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}`;
346
+ warn(message);
347
+ if (!isJson(flags)) note(message);
348
+ }
349
+ const load = IMPLEMENTED[path];
350
+ if (!load) return fail(`unknown command \`${path}\``, EXIT.USAGE);
351
+ // Import only the module the matched path needs. The registry used to pull every
352
+ // command's module graph - detection, projection, the bench corpus - before dispatch
353
+ // began, which cost 36.5 ms on a `status --line` that reads one file (review 0003 Q64).
354
+ // A closed set is refused before the command runs, so the caller learns the valid
355
+ // values from the refusal and every reader of the spec sees the same set (review
356
+ // 0003 Q62). Unset is not the same as invalid: the default stays the command's.
357
+ for (const [flag, allowed] of Object.entries(VALUES.get(path) ?? {})) {
358
+ const value = flags[flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase())];
359
+ if (value !== undefined && !allowed.includes(value)) {
360
+ return fail(`--${flag} takes ${allowed.join(', ')}; received ${value}`, EXIT.USAGE);
361
+ }
362
+ }
363
+
364
+ const run = await load();
365
+ if (!isJson(flags)) return run(args, flags);
366
+ // Capture, then write once: the envelope names an exit code that does not exist until
367
+ // the command returns, and a document written before it is a document that can lie.
368
+ jsonCaptureStart();
369
+ const code = await run(args, flags);
370
+ const docs = jsonCaptureTake() ?? [];
371
+ const [only] = docs;
372
+ if (docs.length === 1 && only.value !== null && typeof only.value === 'object' && !Array.isArray(only.value)) {
373
+ emitJson({ ...only.value, ok: only.value.ok ?? code === 0, exitCode: code });
374
+ return code;
375
+ }
376
+ // A stream of documents (NDJSON) has no single place for one exit code; each is written
377
+ // as the command produced it, and the process exit status still carries the code.
378
+ for (const doc of docs) emitJson(doc.value);
379
+ return code;
295
380
  }
296
381
 
297
382
  /**
@@ -315,7 +400,7 @@ function commandPath(first, second) {
315
400
  // `doctor` is not a group and this branch is only reached when `GROUPS.has(first)`.
316
401
  // Two lists of the same fact is the defect S15 was about in the help text.
317
402
  const key = `${first} ${second}`;
318
- if (!STEPS.has(key) && !(key in IMPLEMENTED)) {
403
+ if (!(key in IMPLEMENTED)) {
319
404
  return null;
320
405
  }
321
406
  return key;
@@ -55,6 +55,7 @@ import { MANIFEST_FILE } from '../name.mjs';
55
55
  import { packageVersion, hasNpm } from '../package-version.mjs';
56
56
  import { resolveNodeRunner } from '../ecosystem/node-runner.mjs';
57
57
  import { PACKAGE_NAME } from '../name.mjs';
58
+ import { commandRoot } from '../repo-root.mjs';
58
59
 
59
60
  const run = promisify(execFile);
60
61
 
@@ -62,7 +63,7 @@ const BACKUP_TAG = 'backup-before-simt-dev';
62
63
  const ADOPT_BRANCH = 'simt-dev/adopt';
63
64
 
64
65
  export async function initCommand(positional, flags) {
65
- const root = flags.root ?? positional[0] ?? process.cwd();
66
+ const root = commandRoot(flags, positional);
66
67
 
67
68
  const detected = await fingerprint(root);
68
69
  if (detected.error) {
@@ -27,11 +27,12 @@ import {
27
27
  import { coreIgnoreCase } from '../repo-root.mjs';
28
28
  import { SESSION_ENV } from '../agenthooks/policy.mjs';
29
29
  import { CLI } from '../name.mjs';
30
+ import { commandRoot } from '../repo-root.mjs';
30
31
 
31
32
  const ISO = (ms) => new Date(ms).toISOString();
32
33
 
33
34
  export async function leaseCommand(args, flags) {
34
- const root = flags.root ?? process.cwd();
35
+ const root = commandRoot(flags);
35
36
  const [sub, ...rest] = args;
36
37
  // The denial tells a blocked session to ask the holder to release the path,
37
38
  // so releasing is a subcommand, not a missing flag (review 0003 Q20).
@@ -97,7 +98,7 @@ export async function leaseCommand(args, flags) {
97
98
  ...mine.map((l) => `would lease ${l.path} until ${ISO(l.expiresAt)}`),
98
99
  `apply ${apply}`,
99
100
  ];
100
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
101
+ emit(lines.join('\n'));
101
102
  return EXIT.OK;
102
103
  }
103
104
 
@@ -139,7 +140,7 @@ export async function leaseCommand(args, flags) {
139
140
  `ttl ${ttl}s`,
140
141
  ...mine.map((l) => `LEASED ${l.path} until ${ISO(l.expiresAt)}`),
141
142
  ];
142
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
143
+ emit(lines.join('\n'));
143
144
  return EXIT.OK;
144
145
  }
145
146
 
@@ -212,7 +213,7 @@ async function releaseLeases(root, args, flags) {
212
213
  `apply run: ${CLI} lease release ${wanted.map((p) => JSON.stringify(p)).join(' ')} --session ${session} --yes`,
213
214
  ];
214
215
  if (isJson(flags)) emitJson({ ok: true, command: 'lease release', root, dryRun: true, session, leases: released.preview });
215
- else emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
216
+ else emit(lines.join('\n'));
216
217
  return EXIT.OK;
217
218
  }
218
219
  for (const l of released.dropped) note(`note released ${l.path} (was held by ${l.session})`);
@@ -225,7 +226,6 @@ async function releaseLeases(root, args, flags) {
225
226
  ...released.dropped.map((l) => `RELEASED ${l.path}`),
226
227
  ...(released.dropped.length === 0 ? ['(no active claims on those paths)'] : []),
227
228
  ].join('\n'),
228
- { maxBytes: flags.maxBytes ?? 0 },
229
229
  );
230
230
  }
231
231
  return EXIT.OK;
@@ -278,7 +278,6 @@ async function renewLeases(root, args, flags) {
278
278
  ...rows.map((l) => `${mode.apply ? 'RENEWED ' : 'would renew '} ${l.path} until ${ISO(l.expiresAt)}`),
279
279
  ...(rows.length === 0 ? ['(no active claims on those paths)'] : []),
280
280
  ].join('\n'),
281
- { maxBytes: flags.maxBytes ?? 0 },
282
281
  );
283
282
  }
284
283
  return EXIT.OK;
@@ -299,6 +298,6 @@ async function listLeases(root, flags) {
299
298
  ...held.map((l) => `LEASED ${l.path} ${l.session}${l.harness ? `/${l.harness}` : ''} until ${ISO(l.expiresAt)}`),
300
299
  ];
301
300
  if (held.length === 0) lines.push(`(no active leases)`);
302
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
301
+ emit(lines.join('\n'));
303
302
  return EXIT.OK;
304
303
  }