simt-dev 0.6.3 → 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 (187) hide show
  1. package/CHANGELOG.md +162 -841
  2. package/README.md +21 -135
  3. package/agents/simt-agent.md +84 -0
  4. package/bin/simt.mjs +19 -55
  5. package/docs/decisions/ADR-009-skill-layout.md +1 -1
  6. package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
  7. package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
  8. package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
  9. package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
  10. package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
  11. package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
  12. package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
  13. package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
  14. package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
  15. package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
  16. package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
  17. package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
  18. package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
  19. package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
  20. package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
  21. 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
  22. package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
  23. package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
  24. package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
  25. package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
  26. package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
  27. package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
  28. package/package.json +13 -3
  29. package/rules/core.md +9 -2
  30. package/rules/mdc/slice-commit.mdc +57 -4
  31. package/rules/mdc/test-gate.mdc +2 -0
  32. package/rules/stacks/android.md +3 -3
  33. package/rules/stacks/connectiq.md +2 -2
  34. package/rules/stacks/dart.md +1 -1
  35. package/rules/stacks/deno.md +1 -1
  36. package/rules/stacks/dotnet.md +1 -1
  37. package/rules/stacks/elixir.md +1 -1
  38. package/rules/stacks/go.md +1 -1
  39. package/rules/stacks/jvm.md +3 -2
  40. package/rules/stacks/node.md +2 -2
  41. package/rules/stacks/python.md +7 -6
  42. package/rules/stacks/ruby.md +1 -1
  43. package/rules/stacks/rust.md +1 -1
  44. package/rules/stacks/swift.md +1 -1
  45. package/rules/stacks/typescript.md +2 -2
  46. package/rules/stacks/uv.md +3 -2
  47. package/skills/simt-commands/SKILL.md +70 -0
  48. package/src/advice/advice.mjs +7 -3
  49. package/src/agenthooks/decide.mjs +141 -9
  50. package/src/agenthooks/opencode.mjs +72 -0
  51. package/src/agenthooks/prompt.mjs +10 -3
  52. package/src/agenthooks/render.mjs +186 -19
  53. package/src/agenthooks/shim.mjs +55 -0
  54. package/src/agents/render.mjs +277 -0
  55. package/src/api.mjs +29 -0
  56. package/src/argv.mjs +37 -3
  57. package/src/bench/ablate.mjs +31 -14
  58. package/src/bench/adapters.json +16 -0
  59. package/src/bench/corpus.mjs +150 -0
  60. package/src/bench/grade.mjs +23 -49
  61. package/src/bench/mine.mjs +247 -36
  62. package/src/bench/probes.mjs +104 -68
  63. package/src/bench/process.mjs +128 -0
  64. package/src/bench/report.mjs +260 -84
  65. package/src/bench/run.mjs +251 -150
  66. package/src/bench/variants.mjs +112 -40
  67. package/src/brief/assemble.mjs +44 -3
  68. package/src/ci/workflow.mjs +46 -14
  69. package/src/cli.mjs +45 -0
  70. package/src/commands/agent.mjs +205 -0
  71. package/src/commands/agenthook.mjs +113 -20
  72. package/src/commands/bench.mjs +192 -18
  73. package/src/commands/brief.mjs +5 -4
  74. package/src/commands/ci.mjs +2 -1
  75. package/src/commands/compare.mjs +50 -49
  76. package/src/commands/completions.mjs +2 -6
  77. package/src/commands/decide.mjs +10 -9
  78. package/src/commands/distill.mjs +4 -3
  79. package/src/commands/doctor.mjs +11 -7
  80. package/src/commands/evidence.mjs +19 -12
  81. package/src/commands/explain.mjs +0 -1
  82. package/src/commands/gate.mjs +98 -28
  83. package/src/commands/graph.mjs +20 -8
  84. package/src/commands/harness.mjs +3 -2
  85. package/src/commands/help.mjs +160 -31
  86. package/src/commands/hooks.mjs +163 -50
  87. package/src/commands/ignore.mjs +2 -1
  88. package/src/commands/impact.mjs +8 -9
  89. package/src/commands/index.mjs +278 -180
  90. package/src/commands/init.mjs +171 -38
  91. package/src/commands/lease.mjs +206 -39
  92. package/src/commands/lessons.mjs +72 -15
  93. package/src/commands/local.mjs +3 -3
  94. package/src/commands/map.mjs +47 -27
  95. package/src/commands/mcp.mjs +0 -1
  96. package/src/commands/pack.mjs +9 -6
  97. package/src/commands/plan.mjs +6 -5
  98. package/src/commands/plugin.mjs +3 -3
  99. package/src/commands/prbody.mjs +3 -3
  100. package/src/commands/repoindex.mjs +5 -3
  101. package/src/commands/review.mjs +2 -2
  102. package/src/commands/rules.mjs +124 -33
  103. package/src/commands/shared.mjs +2 -3
  104. package/src/commands/skills.mjs +82 -45
  105. package/src/commands/skillscompile.mjs +12 -12
  106. package/src/commands/skillsinstall.mjs +14 -2
  107. package/src/commands/slice.mjs +396 -26
  108. package/src/commands/spec.mjs +151 -0
  109. package/src/commands/status.mjs +4 -7
  110. package/src/commands/sync.mjs +50 -33
  111. package/src/commands/tests.mjs +11 -11
  112. package/src/commands/tokens.mjs +2 -2
  113. package/src/commands/undo.mjs +29 -31
  114. package/src/commands/upgrade.mjs +40 -4
  115. package/src/commands/why.mjs +19 -12
  116. package/src/compare/hooksstate.mjs +23 -1
  117. package/src/compare/learn.mjs +82 -57
  118. package/src/compare/parallel.mjs +50 -0
  119. package/src/compare/row.mjs +26 -3
  120. package/src/completions.mjs +5 -0
  121. package/src/context.mjs +64 -0
  122. package/src/decisions/dir.mjs +84 -0
  123. package/src/decisions/index-doc.mjs +46 -0
  124. package/src/decisions/index.mjs +109 -11
  125. package/src/diff.mjs +1 -1
  126. package/src/distill/distill.mjs +17 -2
  127. package/src/distill/engine.mjs +5 -1
  128. package/src/ecosystem/node-runner.mjs +144 -0
  129. package/src/explain.mjs +18 -6
  130. package/src/graph/load.mjs +42 -20
  131. package/src/hooks/capability.mjs +17 -4
  132. package/src/hooks/dispatcher.mjs +513 -36
  133. package/src/hooks/gates.mjs +0 -43
  134. package/src/hooks/resolve.mjs +11 -10
  135. package/src/hooks/shim.mjs +17 -9
  136. package/src/identity.mjs +96 -15
  137. package/src/index/extract.mjs +192 -11
  138. package/src/index/persist.mjs +307 -47
  139. package/src/index/store.mjs +263 -60
  140. package/src/index/tests.mjs +6 -35
  141. package/src/journal.mjs +372 -84
  142. package/src/kernel/card.mjs +37 -12
  143. package/src/kernel/checks.mjs +44 -9
  144. package/src/kernel/variants.mjs +27 -16
  145. package/src/lease.mjs +97 -15
  146. package/src/ledger/index.mjs +120 -25
  147. package/src/local/client.mjs +4 -6
  148. package/src/locate.mjs +33 -9
  149. package/src/manifest/schema.mjs +213 -0
  150. package/src/mcp/resources.mjs +12 -13
  151. package/src/mcp/server.mjs +151 -38
  152. package/src/mcp/tools.mjs +90 -16
  153. package/src/measure/session.mjs +3 -2
  154. package/src/memory/decide.mjs +98 -43
  155. package/src/memory/lessons.mjs +168 -33
  156. package/src/name.mjs +9 -0
  157. package/src/node-version.mjs +37 -0
  158. package/src/output/index.mjs +111 -3
  159. package/src/packs/manage.mjs +17 -3
  160. package/src/packs.mjs +20 -2
  161. package/src/plan/index.mjs +31 -8
  162. package/src/plan/template.mjs +27 -5
  163. package/src/plugins.mjs +6 -2
  164. package/src/prbody.mjs +7 -5
  165. package/src/projection/baselines.mjs +1 -1
  166. package/src/projection/disclosure.mjs +2 -2
  167. package/src/projection/loaders.mjs +41 -1
  168. package/src/projection/lock.mjs +31 -14
  169. package/src/projection/manifest.mjs +89 -53
  170. package/src/projection/markers.mjs +43 -3
  171. package/src/projection/mdc.mjs +34 -7
  172. package/src/projection/project.mjs +162 -18
  173. package/src/projection/rules.mjs +58 -6
  174. package/src/projection/surfaces.mjs +9 -1
  175. package/src/repo-root.mjs +88 -0
  176. package/src/resolve/body.mjs +4 -0
  177. package/src/resolve/fingerprint.mjs +24 -15
  178. package/src/resolve/local.mjs +58 -0
  179. package/src/review/critic.mjs +6 -3
  180. package/src/secrets.mjs +165 -22
  181. package/src/state.mjs +76 -0
  182. package/src/status.mjs +9 -4
  183. package/src/toolwords.mjs +8 -0
  184. package/src/tsconfig.mjs +39 -99
  185. package/src/workspace.mjs +55 -10
  186. package/templates/plan/tracker.json +1 -1
  187. package/docs/decisions/README.md +0 -591
@@ -5,11 +5,13 @@
5
5
  // default and write artifacts to `.simt/bench/` only when authorised with
6
6
  // `--yes` (and not `--dry-run`). `bench report` is read-only.
7
7
 
8
- import { resolve } from 'node:path';
8
+ import { dirname, resolve } from 'node:path';
9
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
9
10
 
10
11
  import { CLI } from '../name.mjs';
11
12
  import { EXIT, emit, emitJson, fail, isJson, writeMode } from '../output/index.mjs';
12
13
  import { DEFAULT_MINE_TARGET, mineTasks } from '../bench/mine.mjs';
14
+ import { buildCorpusDocument, verifyCorpusDocument, writeCorpusDocument } from '../bench/corpus.mjs';
13
15
  import { regradeRun, runBench } from '../bench/run.mjs';
14
16
  import { buildBenchReport } from '../bench/report.mjs';
15
17
  import { formatRecall, measureRecall } from '../index/map.mjs';
@@ -18,14 +20,33 @@ import { runBenchProbes } from '../bench/probes.mjs';
18
20
  import { selectRuleVariants } from '../bench/variants.mjs';
19
21
  import { criticBench } from '../review/critic.mjs';
20
22
  import { listFlag } from './skills.mjs';
23
+ import { commandRoot } from '../repo-root.mjs';
24
+
25
+ /**
26
+ * Refuse an authorised bench run that names no harness.
27
+ *
28
+ * The default harness is `gold`, which applies the reference patch instead of asking
29
+ * an agent: a `--yes` run with no harness measured the grader and stored the result as
30
+ * an arm's evidence (review 0003 Q49). A default cannot be mistaken for a measurement
31
+ * if there is no default.
32
+ */
33
+ function requireHarness(flags, command) {
34
+ if (typeof flags.harness === 'string' && flags.harness.length > 0) return null;
35
+ return fail(
36
+ `${command} --yes needs an explicit --harness`,
37
+ EXIT.USAGE,
38
+ `pass --harness <name>: an agent adapter such as \`--harness claude\`, or a calibration harness (\`gold\`, \`noop\`)`,
39
+ );
40
+ }
21
41
 
22
42
  export async function benchMineCommand(positional, flags) {
23
- const root = resolve(flags.root ?? positional[0] ?? process.cwd());
43
+ const root = resolve(commandRoot(flags, positional));
24
44
  const { dryRun, apply } = writeMode(flags);
25
45
 
26
46
  const result = await mineTasks(root, {
27
47
  apply,
28
48
  target: DEFAULT_MINE_TARGET,
49
+ replace: flags.replace === true,
29
50
  });
30
51
 
31
52
  if (!result.ok) {
@@ -64,7 +85,7 @@ export async function benchMineCommand(positional, flags) {
64
85
  for (const task of result.tasks) {
65
86
  const subject = task.prompt.split('\n')[0];
66
87
  lines.push(
67
- `task ${task.id} f2p=${task.f2p.length} p2p=${task.f2p.length} files=${task.gold.files} +${task.gold.linesAdded}/-${task.gold.linesRemoved} ${subject}`,
88
+ `task ${task.id} f2p=${task.f2p.length} p2p=${task.p2p.length} files=${task.gold.files} +${task.gold.linesAdded}/-${task.gold.linesRemoved} ${subject}`,
68
89
  );
69
90
  }
70
91
  } else {
@@ -75,12 +96,12 @@ export async function benchMineCommand(positional, flags) {
75
96
  lines.push(`remedy run: ${CLI} bench mine --yes`);
76
97
  }
77
98
 
78
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
99
+ emit(lines.join('\n'));
79
100
  return EXIT.OK;
80
101
  }
81
102
 
82
103
  export async function benchRunCommand(positional, flags) {
83
- const root = resolve(flags.root ?? positional[0] ?? process.cwd());
104
+ const root = resolve(commandRoot(flags, positional));
84
105
  const { dryRun, apply } = writeMode(flags);
85
106
 
86
107
  if (flags.regrade !== undefined) {
@@ -123,10 +144,15 @@ export async function benchRunCommand(positional, flags) {
123
144
  `run ${run.runId}/${run.task} arm=${run.arm} resolved=${run.resolved} gates=${run.gatesPassed} reproduced=${run.reproduced}`,
124
145
  );
125
146
  }
126
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
147
+ emit(lines.join('\n'));
127
148
  return code;
128
149
  }
129
150
 
151
+ if (apply) {
152
+ const missing = requireHarness(flags, 'bench run');
153
+ if (missing !== null) return missing;
154
+ }
155
+
130
156
  const arms = listFlag(flags.arm);
131
157
  const tasks = listFlag(flags.task);
132
158
  const harness = flags.harness ?? 'gold';
@@ -189,7 +215,7 @@ export async function benchRunCommand(positional, flags) {
189
215
  lines.push(`remedy run: ${CLI} bench run --yes`);
190
216
  }
191
217
 
192
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
218
+ emit(lines.join('\n'));
193
219
  return EXIT.OK;
194
220
  }
195
221
 
@@ -217,6 +243,7 @@ export async function benchReportCommand(positional, flags) {
217
243
  harness: flags.harness,
218
244
  model: flags.model,
219
245
  compare: compareItems.length === 2 ? compareItems : null,
246
+ crossGroup: Boolean(flags.crossGroup),
220
247
  });
221
248
  } catch (error) {
222
249
  return fail(
@@ -242,7 +269,7 @@ export async function benchReportCommand(positional, flags) {
242
269
  return EXIT.OK;
243
270
  }
244
271
 
245
- emit(`${report.markdown}\n\n### Map\n\n${formatRecall(mapRecall)}`, { maxBytes: flags.maxBytes ?? 0 });
272
+ emit(`${report.markdown}\n\n### Map\n\n${formatRecall(mapRecall)}`);
246
273
  return EXIT.OK;
247
274
  }
248
275
 
@@ -255,8 +282,12 @@ async function recallOrUnmeasured(root) {
255
282
  }
256
283
 
257
284
  export async function benchAblateCommand(positional, flags) {
258
- const root = resolve(flags.root ?? positional[0] ?? process.cwd());
285
+ const root = resolve(commandRoot(flags, positional));
259
286
  const { dryRun, apply } = writeMode(flags);
287
+ if (apply) {
288
+ const missing = requireHarness(flags, 'bench ablate');
289
+ if (missing !== null) return missing;
290
+ }
260
291
  const harness = flags.harness ?? 'gold';
261
292
  const model = flags.model;
262
293
  const reps = flags.reps ?? 1;
@@ -301,13 +332,18 @@ export async function benchAblateCommand(positional, flags) {
301
332
  lines.push(`remedy run: ${CLI} bench ablate --yes`);
302
333
  }
303
334
 
304
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
335
+ emit(lines.join('\n'));
305
336
  return EXIT.OK;
306
337
  }
307
338
 
308
339
  export async function benchProbesCommand(positional, flags) {
309
- const root = resolve(flags.root ?? positional[0] ?? process.cwd());
340
+ const root = resolve(commandRoot(flags, positional));
310
341
  const { dryRun, apply } = writeMode(flags);
342
+ if (apply) {
343
+ const missing = requireHarness(flags, 'bench probes');
344
+ if (missing !== null) return missing;
345
+ }
346
+
311
347
  const arms = listFlag(flags.arm);
312
348
  const probes = listFlag(flags.task);
313
349
  const harness = flags.harness ?? 'gold';
@@ -352,13 +388,17 @@ export async function benchProbesCommand(positional, flags) {
352
388
  lines.push(`remedy run: ${CLI} bench probes --yes`);
353
389
  }
354
390
 
355
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
391
+ emit(lines.join('\n'));
356
392
  return EXIT.OK;
357
393
  }
358
394
 
359
395
  export async function benchVariantsCommand(positional, flags) {
360
- const root = resolve(flags.root ?? positional[0] ?? process.cwd());
396
+ const root = resolve(commandRoot(flags, positional));
361
397
  const { dryRun, apply } = writeMode(flags);
398
+ if (apply) {
399
+ const missing = requireHarness(flags, 'bench variants');
400
+ if (missing !== null) return missing;
401
+ }
362
402
  let result;
363
403
  try {
364
404
  result = await selectRuleVariants(root, {
@@ -389,7 +429,7 @@ export async function benchVariantsCommand(positional, flags) {
389
429
  ),
390
430
  ];
391
431
  if (!result.applied) lines.push(`remedy run: ${CLI} bench variants --yes`);
392
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
432
+ emit(lines.join('\n'));
393
433
  return EXIT.OK;
394
434
  }
395
435
 
@@ -404,13 +444,13 @@ export async function benchVariantsCommand(positional, flags) {
404
444
  // the row says precision >= 0.8 on >= 30 labelled diffs promotes it to a gate,
405
445
  // and until a bench here shows that, every report calls the critic advisory.
406
446
  export async function benchCriticCommand(positional, flags, commands = []) {
407
- const root = resolve(flags.root ?? positional[0] ?? process.cwd());
408
- const labelsPath = flags.labels ?? '.simt/critic-labels.jsonl';
447
+ const root = resolve(commandRoot(flags, positional));
448
+ const labelsPath = flags.labels ?? '.simt/local/critic-labels.jsonl';
409
449
  const result = await criticBench(root, { labelsPath, flags, commands });
410
450
  if (typeof result === 'number') return result;
411
451
 
412
452
  if (isJson(flags)) {
413
- emitJson(result, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
453
+ emitJson(result);
414
454
  return EXIT.OK;
415
455
  }
416
456
  const lines = [
@@ -423,6 +463,140 @@ export async function benchCriticCommand(positional, flags, commands = []) {
423
463
  ];
424
464
  if (result.labelsNote) lines.push(`labels ${result.labelsNote}`);
425
465
  for (const error of result.errors) lines.push(`error ${error}`);
426
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
466
+ emit(lines.join('\n'));
467
+ return EXIT.OK;
468
+ }
469
+
470
+ // `simt bench export` / `simt bench import` (review 0003 Q48).
471
+ //
472
+ // The vault is keyed on the repository's root commit and lives outside every
473
+ // checkout. That is what makes it safe as a hidden verifier, and it is also
474
+ // why a clone cannot read the corpus mined on another machine: the committed
475
+ // tasks failed with ENOENT from every other checkout. A sealed document
476
+ // carries the descriptors and both patches for every task in one file, so an
477
+ // export on one machine and an import on another reproduce the corpus
478
+ // exactly, and the seal is checked before a single byte is trusted.
479
+
480
+ export async function benchExportCommand(positional, flags) {
481
+ const out = positional[0];
482
+ if (!out) {
483
+ return fail(
484
+ 'bench export needs a target file: bench export <corpus.json>',
485
+ EXIT.USAGE,
486
+ `export the sealed corpus: ${CLI} bench export corpus.json --yes`,
487
+ );
488
+ }
489
+ const root = resolve(commandRoot(flags));
490
+ const { dryRun, apply } = writeMode(flags);
491
+
492
+ let doc;
493
+ try {
494
+ doc = await buildCorpusDocument(root);
495
+ } catch (error) {
496
+ return fail(
497
+ `${root}: ${error.message ?? error}`,
498
+ EXIT.CONFIG,
499
+ `mine tasks first: ${CLI} bench mine --yes`,
500
+ );
501
+ }
502
+
503
+ if (doc.taskCount === 0) {
504
+ return fail(
505
+ `${root}: no mined tasks to export`,
506
+ EXIT.USAGE,
507
+ `mine tasks first: ${CLI} bench mine --yes`,
508
+ );
509
+ }
510
+
511
+ if (apply) {
512
+ const absOut = resolve(out);
513
+ await mkdir(dirname(absOut), { recursive: true });
514
+ await writeFile(absOut, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
515
+ }
516
+
517
+ if (isJson(flags)) {
518
+ emitJson({
519
+ ok: true,
520
+ command: 'bench export',
521
+ root,
522
+ dryRun,
523
+ applied: apply,
524
+ out: resolve(out),
525
+ taskCount: doc.taskCount,
526
+ seal: doc.seal,
527
+ });
528
+ return EXIT.OK;
529
+ }
530
+
531
+ const lines = [
532
+ `bench export ${resolve(out)}`,
533
+ `mode ${apply ? 'applied' : dryRun ? 'dry-run (writes nothing)' : 'preview (pass --yes to write)'}`,
534
+ `tasks ${doc.taskCount}`,
535
+ `seal ${doc.seal}`,
536
+ ];
537
+ if (!apply) lines.push(`remedy run: ${CLI} bench export ${out} --yes`);
538
+ emit(lines.join('\n'));
539
+ return EXIT.OK;
540
+ }
541
+
542
+ export async function benchImportCommand(positional, flags) {
543
+ const file = positional[0];
544
+ if (!file) {
545
+ return fail(
546
+ 'bench import needs a corpus file: bench import <corpus.json>',
547
+ EXIT.USAGE,
548
+ `import a sealed corpus: ${CLI} bench import corpus.json --yes`,
549
+ );
550
+ }
551
+ const root = resolve(commandRoot(flags));
552
+ const { dryRun, apply } = writeMode(flags);
553
+
554
+ let doc;
555
+ try {
556
+ doc = JSON.parse(await readFile(file, 'utf8'));
557
+ } catch (error) {
558
+ return fail(
559
+ `${file}: ${error.message ?? error}`,
560
+ EXIT.USAGE,
561
+ `export one with: ${CLI} bench export ${file} --yes`,
562
+ );
563
+ }
564
+
565
+ const problem = verifyCorpusDocument(doc);
566
+ if (problem !== null) {
567
+ return fail(
568
+ `${file}: ${problem}`,
569
+ EXIT.CONFIG,
570
+ `export a fresh corpus: ${CLI} bench export ${file} --yes`,
571
+ );
572
+ }
573
+
574
+ const result = await writeCorpusDocument(root, doc, { apply });
575
+
576
+ if (isJson(flags)) {
577
+ emitJson({
578
+ ok: true,
579
+ command: 'bench import',
580
+ root,
581
+ file: resolve(file),
582
+ dryRun,
583
+ applied: result.applied,
584
+ taskCount: result.tasks,
585
+ tasksDir: result.tasksDir,
586
+ vaultRoot: result.vaultRoot,
587
+ seal: doc.seal,
588
+ });
589
+ return EXIT.OK;
590
+ }
591
+
592
+ const lines = [
593
+ `bench import ${resolve(file)}`,
594
+ `mode ${apply ? 'applied' : dryRun ? 'dry-run (writes nothing)' : 'preview (pass --yes to write)'}`,
595
+ `imported ${result.tasks} task(s) seal=${doc.seal}`,
596
+ `tasks ${result.tasksDir}`,
597
+ `vault ${result.vaultRoot}`,
598
+ ];
599
+ if (!apply) lines.push(`remedy run: ${CLI} bench import ${file} --yes`);
600
+ emit(lines.join('\n'));
427
601
  return EXIT.OK;
428
602
  }
@@ -19,8 +19,9 @@ import { CLI } from '../name.mjs';
19
19
  import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
20
20
  import { DEFAULT_BRIEF_BYTES, explainPacket } from '../brief/packet.mjs';
21
21
  import { assembleBrief } from '../brief/assemble.mjs';
22
+ import { commandRoot } from '../repo-root.mjs';
22
23
 
23
- export async function briefCommand(args, flags) {
24
+ export async function briefCommand(args, flags, commands = []) {
24
25
  if (args.length > 1) {
25
26
  return fail(`brief takes one task, not ${args.length}`, EXIT.USAGE, `run: ${CLI} brief "<task>"`);
26
27
  }
@@ -33,11 +34,12 @@ export async function briefCommand(args, flags) {
33
34
  }
34
35
 
35
36
  const result = await assembleBrief({
36
- root: flags.root ?? process.cwd(),
37
+ root: commandRoot(flags),
37
38
  task: String(args[0] ?? flags.task ?? ''),
38
39
  paths: flags.paths === undefined ? null : flags.paths,
39
40
  budget,
40
41
  flags,
42
+ commands,
41
43
  });
42
44
  if (result.error) return result.error;
43
45
  // Unreachable from here — only a delivered brief asks for a cold index to be reported
@@ -67,12 +69,11 @@ export async function briefCommand(args, flags) {
67
69
  explain: flags.explain === true ? explainPacket(items, { cut }) : undefined,
68
70
  text: packed.text,
69
71
  },
70
- { pretty: flags.pretty === true, maxBytes: Number(flags.maxBytes ?? 0) || 0 },
71
72
  );
72
73
  return EXIT.OK;
73
74
  }
74
75
 
75
76
  const body = flags.explain === true ? [packed.text, '', 'EXPLAIN', ...explainPacket(items, { cut })] : [packed.text];
76
- emit(body.join('\n'), { maxBytes: Number(flags.maxBytes ?? 0) || 0 });
77
+ emit(body.join('\n'));
77
78
  return EXIT.OK;
78
79
  }
@@ -27,6 +27,7 @@ import { EXIT, emit, emitJson, isJson, note, writeMode } from '../output/index.m
27
27
  import { planWorkflow, renderWorkflow, WORKFLOW_FILE } from '../ci/workflow.mjs';
28
28
  import { planDependabot, renderDependabot, DEPENDABOT_FILE } from '../ci/dependabot.mjs';
29
29
  import { CLI } from '../name.mjs';
30
+ import { commandRoot } from '../repo-root.mjs';
30
31
 
31
32
  /**
32
33
  * The two plans, with the render and the plan-time data each needs kept attached.
@@ -43,7 +44,7 @@ function targets() {
43
44
  }
44
45
 
45
46
  export async function ciCommand(positional, flags) {
46
- const root = flags.root ?? positional[0] ?? process.cwd();
47
+ const root = commandRoot(flags, positional);
47
48
  const mode = writeMode(flags);
48
49
  const checkOnly = flags.check === true;
49
50
 
@@ -21,10 +21,10 @@
21
21
 
22
22
  import { basename, resolve } from 'node:path';
23
23
 
24
- import { EXIT, emit, emitJson, fail, isJson, note } from '../output/index.mjs';
24
+ import { EXIT, fail, note, present } from '../output/index.mjs';
25
25
  import { provenanceDigest } from '../projection/project.mjs';
26
26
  import { GATES } from '../hooks/gates.mjs';
27
- import { buildRow, summarise } from '../compare/row.mjs';
27
+ import { buildRows, resolveConcurrency, summarise } from '../compare/row.mjs';
28
28
  import { discover } from '../compare/discover.mjs';
29
29
  import { learnAcrossEstate, renderLearningLines } from '../compare/learn.mjs';
30
30
  import { targetRoot, WORKSPACE_REMEDY } from '../workspace.mjs';
@@ -60,12 +60,13 @@ export async function compareCommand(positional, flags) {
60
60
  const expectedSha = await provenanceDigest();
61
61
  const context = { expectedSha, gates: new Set(Object.keys(GATES)), gateKinds: GATE_KINDS };
62
62
 
63
- // Sequential on purpose. 16 fingerprint+region computations each spawn a git
64
- // process; running them concurrently produces a burst of subprocesses and no
65
- // measurable speedup, because the work is process-spawn bound rather than CPU
66
- // bound. Determinism of the output matters more than wall clock on a dashboard.
67
- const rows = [];
68
- for (const repo of found.repos) rows.push(await buildRow(repo, context));
63
+ // Parallel with a cap, in discovery order. 16 fingerprint+region computations
64
+ // each spawn a git process; unbounded parallelism is a burst of subprocesses
65
+ // with no speedup, and sequential builds are what took 48 s over MCP for 17
66
+ // repos (review 0003 Q38). The cap keeps the burst bounded while overlapping
67
+ // the spawn wait; the rows still come back in input order, so two runs diff
68
+ // cleanly and determinism of the output is untouched.
69
+ const rows = await buildRows(found.repos, context, { concurrency: resolveConcurrency(flags) });
69
70
  const learning = await learnAcrossEstate(found.repos);
70
71
 
71
72
  const summary = summarise(rows, {
@@ -77,50 +78,50 @@ export async function compareCommand(positional, flags) {
77
78
  directories: found.directories.length,
78
79
  });
79
80
 
80
- if (isJson(flags)) {
81
- emitJson({
82
- ok: !rows.some((r) => r.problems.length > 0),
83
- command: 'compare',
84
- root: summary.root,
85
- shape: summary.shape,
86
- source: summary.source,
87
- expectedSha: summary.expectedSha,
88
- // Spread, not a hand-listed copy. A copy is a second place to forget: a field
89
- // added to `summarise` renders in the text output and silently vanishes from
90
- // `--json`, which is the combination automation trusts most. That is not
91
- // hypothetical — `withOwnHooks` and `staleRuleFiles` both did it, and both
92
- // were caught only because a test asserts the two shapes agree.
93
- summary: {
94
- repos: summary.repos,
95
- adopted: summary.adopted,
96
- unmanaged: summary.unmanaged,
97
- broken: summary.broken,
98
- drifted: summary.drifted,
99
- current: summary.current,
100
- noRegion: summary.noRegion,
101
- malformed: summary.malformed,
102
- staleRuleFiles: summary.staleRuleFiles,
103
- packageRegions: summary.packageRegions,
104
- stalePackages: summary.stalePackages,
105
- hooksRouted: summary.hooksRouted,
106
- withProblems: summary.withProblems,
107
- withOwnHooks: summary.withOwnHooks,
108
- shas: summary.shas,
109
- },
110
- rows,
111
- learning,
112
- worktrees: found.worktrees.map((w) => ({
113
- name: w.name,
114
- path: w.path,
115
- owner: w.ownerName,
116
- ownerMatchesDirName: w.ownerMatches,
117
- })),
118
- directories: found.directories.map((d) => basename(d)),
119
- });
81
+ const payload = {
82
+ ok: !rows.some((r) => r.problems.length > 0),
83
+ command: 'compare',
84
+ root: summary.root,
85
+ shape: summary.shape,
86
+ source: summary.source,
87
+ expectedSha: summary.expectedSha,
88
+ // Spread, not a hand-listed copy. A copy is a second place to forget: a field
89
+ // added to `summarise` renders in the text output and silently vanishes from
90
+ // `--json`, which is the combination automation trusts most. That is not
91
+ // hypothetical — `withOwnHooks` and `staleRuleFiles` both did it, and both
92
+ // were caught only because a test asserts the two shapes agree.
93
+ summary: {
94
+ repos: summary.repos,
95
+ adopted: summary.adopted,
96
+ unmanaged: summary.unmanaged,
97
+ broken: summary.broken,
98
+ drifted: summary.drifted,
99
+ current: summary.current,
100
+ noRegion: summary.noRegion,
101
+ malformed: summary.malformed,
102
+ staleRuleFiles: summary.staleRuleFiles,
103
+ packageRegions: summary.packageRegions,
104
+ stalePackages: summary.stalePackages,
105
+ hooksRouted: summary.hooksRouted,
106
+ withProblems: summary.withProblems,
107
+ withOwnHooks: summary.withOwnHooks,
108
+ shas: summary.shas,
109
+ },
110
+ rows,
111
+ learning,
112
+ worktrees: found.worktrees.map((w) => ({
113
+ name: w.name,
114
+ path: w.path,
115
+ owner: w.ownerName,
116
+ ownerMatchesDirName: w.ownerMatches,
117
+ })),
118
+ directories: found.directories.map((d) => basename(d)),
119
+ };
120
+
121
+ if (present(payload, () => render(rows, summary, found, learning), flags) === 'json') {
120
122
  return rows.some((r) => r.problems.length > 0) ? EXIT.FAIL : EXIT.OK;
121
123
  }
122
124
 
123
- emit(render(rows, summary, found, learning), { maxBytes: flags.maxBytes ?? 0 });
124
125
  if (summary.shape === 'repo') {
125
126
  note('note one repository compared; pass --root at a workspace directory to compare its siblings');
126
127
  } else if (target.source === 'none') {
@@ -5,7 +5,7 @@
5
5
  // `$fpath`. A report line above it would be a report line in the user's shell
6
6
  // config.
7
7
 
8
- import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
8
+ import { EXIT, fail, present } from '../output/index.mjs';
9
9
  import { SHELLS, renderCompletions } from '../completions.mjs';
10
10
 
11
11
  export async function completionsCommand(args, flags, commands) {
@@ -18,10 +18,6 @@ export async function completionsCommand(args, flags, commands) {
18
18
  }
19
19
 
20
20
  const script = renderCompletions(shell, commands);
21
- if (isJson(flags)) {
22
- emitJson({ ok: true, command: 'completions', shell, script });
23
- return EXIT.OK;
24
- }
25
- emit(script, { maxBytes: flags.maxBytes ?? 0 });
21
+ present({ ok: true, command: 'completions', shell, script }, () => script, flags);
26
22
  return EXIT.OK;
27
23
  }
@@ -2,22 +2,24 @@
2
2
  // a rulings skeleton, and structured `simt-affects` + `simt-supersedes` frontmatter
3
3
  // (plan 0003 R34).
4
4
 
5
- import { join, resolve } from 'node:path';
5
+ import { join } from 'node:path';
6
6
  import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
7
7
  import { CLI } from '../name.mjs';
8
- import { DECISIONS_DIR } from '../decisions/index.mjs';
8
+ import { decisionsCutoff, decisionsDir } from '../decisions/dir.mjs';
9
9
  import { lintDecisions, scaffoldDecision } from '../memory/decide.mjs';
10
10
  import { commitJournal } from '../journal.mjs';
11
11
  import { diffField, emitDiffs } from '../diff.mjs';
12
12
  import { parsePathList } from '../argv.mjs';
13
+ import { commandRoot } from '../repo-root.mjs';
13
14
 
14
15
  export async function decideCommand(args, flags) {
15
- const root = flags.root ?? process.cwd();
16
+ const root = commandRoot(flags);
16
17
 
17
18
  if (flags.check === true) {
18
- const decisionsDir = join(root, 'docs', 'decisions');
19
- const allowHistorical = resolve(decisionsDir) === DECISIONS_DIR;
20
- const res = await lintDecisions(decisionsDir, { allowHistorical });
19
+ // Q55: one resolution — the manifest key, then the detected directory —
20
+ // and the cutoff from the same config, never a hard-coded number or path.
21
+ const relDir = await decisionsDir(root);
22
+ const res = await lintDecisions(join(root, relDir), { historicalCutoff: await decisionsCutoff(root) });
21
23
 
22
24
  if (isJson(flags)) {
23
25
  emitJson({
@@ -30,7 +32,7 @@ export async function decideCommand(args, flags) {
30
32
  }
31
33
 
32
34
  if (res.findings.length === 0) {
33
- emit('decisions ok', { maxBytes: flags.maxBytes ?? 0 });
35
+ emit('decisions ok');
34
36
  return EXIT.OK;
35
37
  }
36
38
 
@@ -41,7 +43,6 @@ export async function decideCommand(args, flags) {
41
43
  ...(f.remedy ? [` remedy: ${f.remedy}`] : []),
42
44
  ])
43
45
  .join('\n'),
44
- { maxBytes: flags.maxBytes ?? 0 },
45
46
  );
46
47
  return EXIT.FAIL;
47
48
  }
@@ -122,7 +123,7 @@ export async function decideCommand(args, flags) {
122
123
  : `would write ${res.relPath}; apply with: ${CLI} decide ${JSON.stringify(res.title)} --yes`,
123
124
  ];
124
125
 
125
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
126
+ emit(lines.join('\n'));
126
127
  // After the report: the report says what, the diff shows it (R50).
127
128
  emitDiffs(diffs, flags);
128
129
  return EXIT.OK;
@@ -16,7 +16,7 @@ export async function distillCommand(args, flags) {
16
16
  if (typeof result === 'number') return result;
17
17
 
18
18
  if (isJson(flags)) {
19
- emitJson(result, { maxBytes: flags.maxBytes ?? 0 });
19
+ emitJson(result);
20
20
  return EXIT.OK;
21
21
  }
22
22
 
@@ -27,10 +27,11 @@ export async function distillCommand(args, flags) {
27
27
  `engine ${result.engine}`,
28
28
  `budget ${result.budget} bytes`,
29
29
  `citations ${result.citations}`,
30
- ...(result.verified === true ? [] : result.faults.map((f) => `unverified ${f}`)),
30
+ // A cached proposal predates this field; `?? []` keeps the report from crashing on
31
+ // the file the command wrote one release ago (review 0003 Q52).
32
+ ...(result.verified === true ? [] : (result.faults ?? []).map((f) => `unverified ${f}`)),
31
33
  `proposal ${result.proposal.path} (${result.proposal.state}, ${result.proposal.bytes} bytes)`,
32
34
  ].join('\n'),
33
- { maxBytes: flags.maxBytes ?? 0 },
34
35
  );
35
36
  return EXIT.OK;
36
37
  }