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
@@ -17,12 +17,13 @@ import {
17
17
  promoteLesson,
18
18
  readLessonCandidates,
19
19
  } from '../memory/lessons.mjs';
20
+ import { commandRoot } from '../repo-root.mjs';
20
21
 
21
22
  export async function lessonsListCommand(args, flags) {
22
23
  if (args.length > 0) {
23
24
  return fail(`unknown lessons argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} lessons`);
24
25
  }
25
- const root = resolve(flags.root ?? process.cwd());
26
+ const root = resolve(commandRoot(flags));
26
27
  // Pure: listing must not capture. Capture used to run here, so `list` wrote
27
28
  // to the store it was reading and a read-only question had write side
28
29
  // effects. Capturing is `lessons capture` now (review 0003 Q36).
@@ -48,7 +49,7 @@ export async function lessonsListCommand(args, flags) {
48
49
  };
49
50
 
50
51
  if (isJson(flags)) {
51
- emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
52
+ emitJson(payload);
52
53
  return errors.length === 0 ? EXIT.OK : EXIT.FAIL;
53
54
  }
54
55
 
@@ -81,7 +82,7 @@ export async function lessonsListCommand(args, flags) {
81
82
 
82
83
  for (const err of errors) lines.push(`error ${err}`);
83
84
 
84
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
85
+ emit(lines.join('\n'));
85
86
  return errors.length === 0 ? EXIT.OK : EXIT.FAIL;
86
87
  }
87
88
 
@@ -96,7 +97,7 @@ export async function lessonsCaptureCommand(args, flags) {
96
97
  if (args.length > 0) {
97
98
  return fail(`unknown lessons capture argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} lessons capture`);
98
99
  }
99
- const root = resolve(flags.root ?? process.cwd());
100
+ const root = resolve(commandRoot(flags));
100
101
  const fromLedger = await captureFromLedger(root);
101
102
  const reverts = await captureRevertedCommits(root);
102
103
 
@@ -109,7 +110,7 @@ export async function lessonsCaptureCommand(args, flags) {
109
110
  captured: [...fromLedger, ...reverts],
110
111
  };
111
112
  if (isJson(flags)) {
112
- emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
113
+ emitJson(payload);
113
114
  return EXIT.OK;
114
115
  }
115
116
  const lines = [
@@ -120,7 +121,7 @@ export async function lessonsCaptureCommand(args, flags) {
120
121
  for (const c of payload.captured) {
121
122
  lines.push(` ${c.id} ${c.kind} ${c.summary}`);
122
123
  }
123
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
124
+ emit(lines.join('\n'));
124
125
  return EXIT.OK;
125
126
  }
126
127
 
@@ -128,7 +129,7 @@ export async function lessonsReviewCommand(args, flags) {
128
129
  if (args.length > 1) {
129
130
  return fail(`unknown lessons review argument ${args[1]}`, EXIT.USAGE, `run: ${CLI} lessons review [<id>] [--yes]`);
130
131
  }
131
- const root = resolve(flags.root ?? process.cwd());
132
+ const root = resolve(commandRoot(flags));
132
133
  const mode = writeMode(flags);
133
134
  const parsedPaths = parsePathList(flags.paths);
134
135
  const paths = parsedPaths.length > 0 ? parsedPaths : undefined;
@@ -173,7 +174,7 @@ export async function lessonsReviewCommand(args, flags) {
173
174
  };
174
175
 
175
176
  if (isJson(flags)) {
176
- emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
177
+ emitJson(payload);
177
178
  return EXIT.OK;
178
179
  }
179
180
 
@@ -185,7 +186,7 @@ export async function lessonsReviewCommand(args, flags) {
185
186
  result.markdown.trimEnd(),
186
187
  ];
187
188
 
188
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
189
+ emit(lines.join('\n'));
189
190
  // After the report: the report says what, the diff shows it (R50).
190
191
  emitDiffs(diffs, flags);
191
192
  if (!mode.apply) {
@@ -8,6 +8,7 @@ import { resolve } from 'node:path';
8
8
  import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
9
9
  import { CLI, MANIFEST_FILE } from '../name.mjs';
10
10
  import { probeLocal } from '../local/client.mjs';
11
+ import { commandRoot } from '../repo-root.mjs';
11
12
 
12
13
  function formatRoleLine(role, report) {
13
14
  if (!report) return `${role.padEnd(8)} (unset)`;
@@ -55,7 +56,7 @@ function remedyFor(result) {
55
56
  }
56
57
 
57
58
  export async function localDoctorCommand(args, flags) {
58
- const root = resolve(flags.root ?? args[0] ?? process.cwd());
59
+ const root = resolve(commandRoot(flags, args));
59
60
  const allowRemote = flags.allowRemote === true;
60
61
  const result = await probeLocal(root, { allowRemote });
61
62
 
@@ -86,7 +87,6 @@ export async function localDoctorCommand(args, flags) {
86
87
  findings: result.findings,
87
88
  ...(error ? { error, remedy, exitCode: code } : {}),
88
89
  },
89
- { maxBytes: flags.maxBytes ?? 0 },
90
90
  );
91
91
  return code;
92
92
  }
@@ -99,7 +99,7 @@ export async function localDoctorCommand(args, flags) {
99
99
  `sockets ${result.socketsOpened}`,
100
100
  ];
101
101
 
102
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
102
+ emit(lines.join('\n'));
103
103
 
104
104
  if (!result.ok) {
105
105
  return fail(error, code, remedy);
@@ -5,7 +5,7 @@
5
5
  // `build` on are ones a person typed at a terminal, where a few hundred
6
6
  // milliseconds is not a bar to meet.
7
7
 
8
- import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
8
+ import { EXIT, emit, emitJson, fail, isJson, present } from '../output/index.mjs';
9
9
  import { CLI } from '../name.mjs';
10
10
  import { isIndexStale, readEdges, readEmbeddings, readFiles, readFts, readSymbols, reindex, searchFtsBm25 } from '../index/store.mjs';
11
11
  import {
@@ -16,12 +16,13 @@ import {
16
16
  rankFiles,
17
17
  renderMap,
18
18
  } from '../index/map.mjs';
19
+ import { commandRoot } from '../repo-root.mjs';
19
20
 
20
21
  const LEVELS = new Set(['outline', 'names', 'signatures']);
21
22
 
22
23
  export async function mapCommand(args, flags) {
23
24
  if (args.length > 0) return fail(`unknown map argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} map`);
24
- const root = flags.root ?? process.cwd();
25
+ const root = commandRoot(flags);
25
26
  if (flags.recall === true) return recall(root, flags);
26
27
  const level = flags.level ?? 'outline';
27
28
  if (!LEVELS.has(level)) {
@@ -31,9 +32,9 @@ export async function mapCommand(args, flags) {
31
32
  if (!Number.isInteger(budget) || budget < 0) {
32
33
  return fail(`budget must be a non-negative integer`, EXIT.USAGE, `run: ${CLI} map --budget 2048`);
33
34
  }
34
- const loaded = await loadGraph(root);
35
- if (loaded.error) return loaded.error;
36
35
  const task = typeof flags.task === 'string' ? flags.task : '';
36
+ const loaded = await loadGraph(root, { task, embed: flags.embed === true });
37
+ if (loaded.error) return loaded.error;
37
38
  const bm25 = task.trim().length > 0 ? await searchFtsBm25(root, task, { fts: loaded.fts }) : [];
38
39
  const queryVector =
39
40
  flags.embed === true && loaded.embeddings.length > 0
@@ -44,12 +45,19 @@ export async function mapCommand(args, flags) {
44
45
  : null;
45
46
  const ranked = rankFiles({ ...loaded, bm25, queryVector, task });
46
47
  const rendered = renderMap(ranked, loaded.symbols, { level, budget });
48
+ // What the ranking is an answer to. `graph-only` says no file or symbol mentioned the
49
+ // task and the list is the graph hub; reading that as a hit is how `map --task
50
+ // zebracorn` looked useful (review 0003 Q52).
51
+ const matched = ranked.filter((row) => (row.score ?? 0) > 0).length;
52
+ const basis = task.trim().length === 0 ? 'no-task' : matched > 0 ? 'matched' : 'graph-only';
47
53
  const payload = {
48
54
  ok: true,
49
55
  command: 'map',
50
56
  root,
51
57
  level,
52
58
  budget,
59
+ matched,
60
+ basis,
53
61
  shown: rendered.shown,
54
62
  total: rendered.total,
55
63
  truncated: rendered.truncated,
@@ -57,17 +65,18 @@ export async function mapCommand(args, flags) {
57
65
  text: rendered.text,
58
66
  };
59
67
  if (isJson(flags)) {
60
- emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
68
+ emitJson(payload);
61
69
  return EXIT.OK;
62
70
  }
63
71
  const lines = [
64
72
  `level ${level}`,
73
+ `basis ${basis}${basis === 'graph-only' ? ' (no file or symbol mentions this task; the list is the graph hub)' : ''}`,
65
74
  `shown ${rendered.shown}`,
66
75
  `total ${rendered.total}`,
67
76
  `budget ${budget}`,
68
77
  ];
69
78
  if (rendered.text) lines.push(rendered.text);
70
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
79
+ emit(lines.join('\n'));
71
80
  return EXIT.OK;
72
81
  }
73
82
 
@@ -82,11 +91,7 @@ async function recall(root, flags) {
82
91
  return fail(`could not measure recall (${error.message})`, EXIT.UNSUPPORTED, `run ${CLI} index in a git repository`);
83
92
  }
84
93
  const payload = { ok: true, command: 'map', root, recall: result };
85
- if (isJson(flags)) {
86
- emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
87
- return EXIT.OK;
88
- }
89
- emit(formatRecall(result), { maxBytes: flags.maxBytes ?? 0 });
94
+ present(payload, () => formatRecall(result), flags);
90
95
  return EXIT.OK;
91
96
  }
92
97
 
@@ -104,8 +109,12 @@ async function recall(root, flags) {
104
109
  * first prompt in a fresh clone wait, and a prompt that takes a minute to appear is
105
110
  * how a hook gets uninstalled. The next prompt gets the packet, and a command a
106
111
  * person typed at a terminal always builds what it needs.
112
+ *
113
+ * FTS bodies and stored vectors load only when the caller names them (review 0003
114
+ * Q76): BM25 runs in SQLite, so the graph read pays for files, symbols and edges
115
+ * and nothing more.
107
116
  */
108
- export async function loadGraph(root, { build = true } = {}) {
117
+ export async function loadGraph(root, { build = true, task = '', embed = false } = {}) {
109
118
  if (build) {
110
119
  try {
111
120
  await reindex(root);
@@ -119,12 +128,13 @@ export async function loadGraph(root, { build = true } = {}) {
119
128
  };
120
129
  }
121
130
  }
131
+ const wantFts = String(task ?? '').trim().length > 0;
122
132
  const [files, symbols, edges, fts, embeddings] = await Promise.all([
123
133
  readFiles(root),
124
134
  readSymbols(root),
125
135
  readEdges(root),
126
- readFts(root),
127
- readEmbeddings(root),
136
+ wantFts ? readFts(root) : Promise.resolve([]),
137
+ embed === true ? readEmbeddings(root) : Promise.resolve([]),
128
138
  ]);
129
139
  // A present index that cannot be read is named, not served as empty
130
140
  // (review 0003 Q44): `cold` is "nothing here", `broken` is "something is
@@ -9,17 +9,14 @@
9
9
  // misconfigured the command needs to be able to ask what it exposes without hanging, and
10
10
  // a command that only speaks when spoken to cannot answer that.
11
11
 
12
- import { readFile } from 'node:fs/promises';
13
- import { fileURLToPath } from 'node:url';
14
- import { dirname, join } from 'node:path';
15
-
16
12
  import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
17
13
  import { TOOLS } from '../mcp/tools.mjs';
18
14
  import { PROTOCOL_VERSION, SUPPORTED_VERSIONS, serve } from '../mcp/server.mjs';
19
15
  import { CLI } from '../name.mjs';
16
+ import { packageVersion } from '../package-version.mjs';
20
17
 
21
18
  export async function mcpCommand(positional, flags) {
22
- const { version } = await packageVersion();
19
+ const version = packageVersion();
23
20
 
24
21
  if (flags.list === true) {
25
22
  if (isJson(flags)) {
@@ -53,7 +50,6 @@ export async function mcpCommand(positional, flags) {
53
50
  '',
54
51
  `serve them with: ${positional[0] ?? `${CLI} mcp`}`,
55
52
  ].join('\n'),
56
- { maxBytes: flags.maxBytes ?? 0 },
57
53
  );
58
54
  return EXIT.OK;
59
55
  }
@@ -63,16 +59,4 @@ export async function mcpCommand(positional, flags) {
63
59
  } catch (error) {
64
60
  return fail(`mcp transport failed: ${error.message}`, EXIT.FAIL, `run: ${CLI} mcp --list to check the surface loads`);
65
61
  }
66
- }
67
-
68
- async function packageVersion() {
69
- try {
70
- const here = dirname(fileURLToPath(import.meta.url));
71
- const pkg = JSON.parse(await readFile(join(here, '..', '..', 'package.json'), 'utf8'));
72
- return { version: pkg.version };
73
- } catch {
74
- // A missing version must not stop the server: the client already knows which
75
- // build it launched, and a hard failure here would look like a protocol bug.
76
- return { version: 'unknown' };
77
- }
78
62
  }
@@ -28,6 +28,7 @@ import {
28
28
  } from '../packs/manage.mjs';
29
29
  import { readManifest } from '../projection/manifest.mjs';
30
30
  import { diffField, emitDiffs } from '../diff.mjs';
31
+ import { commandRoot } from '../repo-root.mjs';
31
32
 
32
33
  export async function packCommand(sub, args, flags) {
33
34
  if (sub === 'new') return packNew(args, flags);
@@ -40,9 +41,13 @@ export async function packCommand(sub, args, flags) {
40
41
  * Where a lint or publish resolves its pack: the positional directory, the
41
42
  * `--root`, or the working directory. `hooks` reads the same three in the same
42
43
  * order, so the two commands cannot disagree about which directory was meant.
44
+ *
45
+ * The positional wins, as this always said it did. The code read `--root` first,
46
+ * so `pack lint .simt/packs/a --root ../b` judged `b` while the prose and the
47
+ * command's own usage promised `a` (review 0003 Q58).
43
48
  */
44
49
  function packDir(args, flags) {
45
- return resolve(flags.root ?? args[0] ?? process.cwd());
50
+ return resolve(args[0] ?? commandRoot(flags));
46
51
  }
47
52
 
48
53
  /**
@@ -98,7 +103,7 @@ async function packNew(args, flags) {
98
103
  return fail(sourceProblem, EXIT.USAGE, 'a pack source names where the bytes come from: `npm:<package>` or `git+https://…`');
99
104
  }
100
105
 
101
- const dir = join(resolve(flags.root ?? process.cwd()), name);
106
+ const dir = join(resolve(commandRoot(flags)), name);
102
107
  const files = scaffoldPack({ name, source });
103
108
  const { apply, dryRun } = writeMode(flags);
104
109
  // A scaffold always creates into a directory that must not exist, so every
@@ -129,7 +134,6 @@ async function packNew(args, flags) {
129
134
  '',
130
135
  dryRun ? 'DRY-RUN no files written' : `PLAN nothing written; apply with: ${CLI} pack new ${name} --source ${source} --yes`,
131
136
  ].join('\n'),
132
- { maxBytes: flags.maxBytes ?? 0 },
133
137
  );
134
138
  // After the report: the report says what, the diff shows it (R50).
135
139
  emitDiffs(diffs, flags);
@@ -149,7 +153,6 @@ async function packNew(args, flags) {
149
153
  '',
150
154
  `WROTE ${files.length} scaffold file(s); write the rule, then: ${CLI} pack lint ${dir}`,
151
155
  ].join('\n'),
152
- { maxBytes: flags.maxBytes ?? 0 },
153
156
  );
154
157
  // After the report: the report says what, the diff shows it (R50).
155
158
  emitDiffs(diffs, flags);
@@ -187,7 +190,7 @@ async function packLint(args, flags) {
187
190
  lines.push(` remedy: ${f.remedy}`);
188
191
  }
189
192
  lines.push('', ok ? 'PASSED the pack is well-formed' : `FAILED ${findings.length} finding(s)`);
190
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
193
+ emit(lines.join('\n'));
191
194
  return ok ? EXIT.OK : EXIT.FAIL;
192
195
  }
193
196
 
@@ -247,7 +250,7 @@ async function packPublish(args, flags) {
247
250
  } else {
248
251
  lines.push('', `FAILED ${result.findings.length} finding(s)`);
249
252
  }
250
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
253
+ emit(lines.join('\n'));
251
254
  if (result.ok && apply && result.published) {
252
255
  note(`note never committed or pushed by this tool (ADR-001); the registry now has the bytes, the repos still need vendoring`);
253
256
  }
@@ -3,12 +3,13 @@ import { EXIT, emit, emitJson, fail, isJson, writeMode } from '../output/index.m
3
3
  import { formatCounts, planDone, planNext, planStatus } from '../plan/index.mjs';
4
4
  import { diffField, emitDiffs } from '../diff.mjs';
5
5
  import { CLI } from '../name.mjs';
6
+ import { commandRoot } from '../repo-root.mjs';
6
7
 
7
8
  // Preview unless --yes, and --dry-run wins even then. A tracker row marked done
8
9
  // for a commit that was never written is a claim the next reader will believe.
9
10
 
10
11
  export async function planCommand(sub, args, flags) {
11
- const root = flags.root ?? process.cwd();
12
+ const root = commandRoot(flags);
12
13
  if (sub === 'status') return runStatus(root, flags);
13
14
  if (sub === 'next') return runNext(root, args[0] ?? null, flags);
14
15
  if (sub === 'done') {
@@ -35,7 +36,7 @@ async function runStatus(root, flags) {
35
36
  }
36
37
  lines.push('', `total ${formatCounts(result.totals, result.stateOrder) || '-'}`);
37
38
  lines.push(`ready ${result.ready}`);
38
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
39
+ emit(lines.join('\n'));
39
40
  return EXIT.OK;
40
41
  }
41
42
 
@@ -55,7 +56,7 @@ async function runNext(root, id, flags) {
55
56
  lines.push(` ${row.id} ${row.doc} depends on: ${row.dependsOn || '(none)'}${unknown}`);
56
57
  }
57
58
  }
58
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
59
+ emit(lines.join('\n'));
59
60
  return EXIT.OK;
60
61
  }
61
62
  const next = result.next;
@@ -68,7 +69,7 @@ async function runNext(root, id, flags) {
68
69
  '',
69
70
  next.section || '(no section for this id)',
70
71
  ];
71
- emit(head.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
72
+ emit(head.join('\n'));
72
73
  return EXIT.OK;
73
74
  }
74
75
 
@@ -104,7 +105,7 @@ async function runDone(root, id, sha, flags) {
104
105
  }
105
106
  if (result.evidenceRecorded) lines.push(`evidence ${result.sha}${result.test ? ` test: ${result.test}` : ''}`);
106
107
  else if (result.evidenceNote) lines.push(`note ${result.evidenceNote}`);
107
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
108
+ emit(lines.join('\n'));
108
109
  // After the report: the report says what, the diff shows it (R50).
109
110
  emitDiffs(diffs, flags);
110
111
  return EXIT.OK;
@@ -10,6 +10,7 @@ import { EXIT, emit, emitJson, fail, isJson, writeMode } from '../output/index.m
10
10
  import { CLI } from '../name.mjs';
11
11
  import { buildPackage, PLUGIN_KINDS } from '../plugins.mjs';
12
12
  import { diffField, emitDiffs } from '../diff.mjs';
13
+ import { commandRoot } from '../repo-root.mjs';
13
14
 
14
15
  export async function pluginCommand(args, flags) {
15
16
  const kind = args[0];
@@ -27,7 +28,7 @@ export async function pluginCommand(args, flags) {
27
28
  }
28
29
 
29
30
  const { apply } = writeMode(flags);
30
- const root = flags.root ?? process.cwd();
31
+ const root = commandRoot(flags);
31
32
  const plan = await buildPackage(kind, { root, out, apply });
32
33
 
33
34
  // The `--diff` pairs: the generated files, created or refreshed (R50). The
@@ -68,7 +69,6 @@ export async function pluginCommand(args, flags) {
68
69
  ? `WROTE ${plan.files.length} generated file(s) plus the runtime`
69
70
  : `PLAN nothing written; apply with: ${CLI} plugin ${kind} --out ${out} --yes`,
70
71
  ].join('\n'),
71
- { maxBytes: flags.maxBytes ?? 0 },
72
72
  );
73
73
  // After the report: the report says what, the diff shows it (R50).
74
74
  emitDiffs(diffs, flags);
@@ -12,9 +12,10 @@
12
12
  import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
13
13
  import { CLI } from '../name.mjs';
14
14
  import { buildPrBody, renderPrBody } from '../prbody.mjs';
15
+ import { commandRoot } from '../repo-root.mjs';
15
16
 
16
17
  export async function prBodyCommand(args, flags) {
17
- const root = flags.root ?? process.cwd();
18
+ const root = commandRoot(flags);
18
19
  if (args.length > 0) {
19
20
  return fail(`pr-body takes no positional arguments`, EXIT.USAGE, `run: ${CLI} pr-body [--base <ref>]`);
20
21
  }
@@ -36,10 +37,9 @@ export async function prBodyCommand(args, flags) {
36
37
  verdict: report.verdict,
37
38
  unreviewed: report.unreviewed,
38
39
  },
39
- { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 },
40
40
  );
41
41
  return EXIT.OK;
42
42
  }
43
- emit(renderPrBody(report), { maxBytes: flags.maxBytes ?? 0 });
43
+ emit(renderPrBody(report));
44
44
  return EXIT.OK;
45
45
  }
@@ -2,15 +2,15 @@
2
2
  // content: it is gitignored, and a second run with the same blobs writes the same
3
3
  // rows (ADR-089).
4
4
 
5
- import { execFile } from 'node:child_process';
6
-
7
5
  import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
8
6
  import { CLI } from '../name.mjs';
9
7
  import { JSONL_FILE, SQLITE_FILE, reindex } from '../index/store.mjs';
8
+ import { commandRoot } from '../repo-root.mjs';
9
+ import { git } from '../sys/git.mjs';
10
10
 
11
11
  export async function indexCommand(args, flags) {
12
12
  if (args.length > 0) return fail(`unknown index argument ${args[0]}`, EXIT.USAGE, `run: ${CLI} index`);
13
- const root = flags.root ?? process.cwd();
13
+ const root = commandRoot(flags);
14
14
  let result;
15
15
  try {
16
16
  result = await reindex(root, {
@@ -31,7 +31,7 @@ export async function indexCommand(args, flags) {
31
31
  const ignored = await isIgnored(root, file);
32
32
  const payload = { ok: true, command: 'index', root, file, ignored, ...result };
33
33
  if (isJson(flags)) {
34
- emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
34
+ emitJson(payload);
35
35
  return EXIT.OK;
36
36
  }
37
37
  const lines = [
@@ -46,14 +46,15 @@ export async function indexCommand(args, flags) {
46
46
  ];
47
47
  if (result.parsers === 'missing') lines.push('note index.parsers did not load, so the built-in extractors ran');
48
48
  if (!ignored) lines.push(`note ${file} is not gitignored, and the cache is not meant to be committed`);
49
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
49
+ emit(lines.join('\n'));
50
50
  return EXIT.OK;
51
51
  }
52
52
 
53
- function isIgnored(root, file) {
54
- return new Promise((resolve) => {
55
- execFile('git', ['check-ignore', '-q', '--', file], { cwd: root }, (error) => {
56
- resolve(!error);
57
- });
58
- });
53
+ async function isIgnored(root, file) {
54
+ try {
55
+ await git(root, ['check-ignore', '-q', '--', file]);
56
+ return true;
57
+ } catch {
58
+ return false;
59
+ }
59
60
  }
@@ -23,7 +23,7 @@ export async function reviewCommand(args, flags, commands = []) {
23
23
  if (typeof result === 'number') return result;
24
24
 
25
25
  if (isJson(flags)) {
26
- emitJson(result, { maxBytes: flags.maxBytes ?? 0 });
26
+ emitJson(result);
27
27
  return EXIT.OK;
28
28
  }
29
29
 
@@ -60,6 +60,6 @@ export async function reviewCommand(args, flags, commands = []) {
60
60
  }
61
61
  if (result.metric) lines.push(`metric ${result.metric.note}`);
62
62
 
63
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
63
+ emit(lines.join('\n'));
64
64
  return EXIT.OK;
65
65
  }
@@ -15,7 +15,11 @@
15
15
  // reviewer needs and exactly the fact an empty output would hide.
16
16
 
17
17
  import { readdir } from 'node:fs/promises';
18
+ import { repoContext } from '../context.mjs';
18
19
  import { join } from 'node:path';
20
+ import { renderDecisionsIndex } from '../decisions/index-doc.mjs';
21
+ import { applyPlan, change } from '../write/plan.mjs';
22
+ import { readBefore } from '../diff.mjs';
19
23
  import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
20
24
  import { loadRules, select } from '../projection/project.mjs';
21
25
  import { loadMdcRules } from '../projection/mdc.mjs';
@@ -49,8 +53,8 @@ function sorted(rules) {
49
53
  return [...rules].sort((a, b) => a.id.localeCompare(b.id));
50
54
  }
51
55
 
52
- async function load(root) {
53
- const rules = await loadRules(root ?? null);
56
+ async function load(root, ctx = repoContext(root)) {
57
+ const rules = await ctx.rules();
54
58
  const broken = rules.filter((r) => r.error);
55
59
  return { rules: [...rules, ...(await loadPerSurfaceRules())], broken };
56
60
  }
@@ -153,12 +157,12 @@ export async function listCommand(args, flags) {
153
157
  if (locals.length > 0) lines.push('', `local profiles come from ${locals.map((r) => r.source).join(', ')}`);
154
158
  }
155
159
 
156
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
160
+ emit(lines.join('\n'));
157
161
  return broken.length === 0 ? EXIT.OK : EXIT.FAIL;
158
162
  }
159
163
 
160
- async function selectionFor(rules, root) {
161
- const { stack, facts } = await fingerprint(root);
164
+ async function selectionFor(rules, root, ctx = repoContext(root)) {
165
+ const { stack, facts } = await ctx.fingerprint();
162
166
  const { selected, notes } = select(rules, stack, facts);
163
167
  return { selected: new Set(selected.map((r) => r.id)), notes };
164
168
  }
@@ -210,9 +214,11 @@ async function loadDecisionsFor(root, all) {
210
214
  }
211
215
 
212
216
  /** The catalogue both surfaces answer from: every rule, its decisions, and the selection. */
213
- export async function rulesCatalogue(root, { selectFor = null } = {}) {
214
- const { rules, broken } = await load(root);
215
- const selection = selectFor ? await selectionFor(rules, selectFor) : null;
217
+ export async function rulesCatalogue(root, { selectFor = null, ctx = null } = {}) {
218
+ const { rules, broken } = await load(root, ctx ?? repoContext(root));
219
+ // `selectFor` is a different repository: the caller asked for the catalogue here and
220
+ // the selection there, so it gets its own context rather than this repository answer.
221
+ const selection = selectFor ? await selectionFor(rules, selectFor, repoContext(selectFor)) : null;
216
222
  return {
217
223
  rules: sorted(rules).map((r) => ({
218
224
  id: r.id,
@@ -236,8 +242,8 @@ export async function rulesCatalogue(root, { selectFor = null } = {}) {
236
242
  * The provenance facts for one rule or one decision record, in the shape the
237
243
  * JSON documents carry. Text renders from the same facts; nothing re-derives.
238
244
  */
239
- export async function rulesWhyFacts(root, what, { all = false } = {}) {
240
- const { rules } = await load(root);
245
+ export async function rulesWhyFacts(root, what, { all = false, ctx = null } = {}) {
246
+ const { rules } = await load(root, ctx ?? repoContext(root));
241
247
  if (/^ADR-\d{3,}$/i.test(what)) {
242
248
  const id = what.toUpperCase();
243
249
  const { ok, decisions, errors, origins } = await loadDecisionsFor(root, all);
@@ -312,7 +318,7 @@ export async function showCommand(args, flags) {
312
318
 
313
319
  const adrs = citations(rule.body);
314
320
  if (isJson(flags)) {
315
- emitJson({ ok: !rule.error, ...rule, adrs }, { maxBytes: flags.maxBytes ?? 0 });
321
+ emitJson({ ok: !rule.error, ...rule, adrs });
316
322
  return rule.error ? EXIT.FAIL : EXIT.OK;
317
323
  }
318
324
 
@@ -329,7 +335,6 @@ export async function showCommand(args, flags) {
329
335
  '',
330
336
  rule.body,
331
337
  ].join('\n'),
332
- { maxBytes: flags.maxBytes ?? 0 },
333
338
  );
334
339
  return rule.error ? EXIT.FAIL : EXIT.OK;
335
340
  }
@@ -370,7 +375,7 @@ function ruleDirection(facts, flags) {
370
375
  ' this rule cites no decision, so nothing settles it and nothing can overturn it.',
371
376
  ' Either cite the ADR that produced it, or accept that it is this repo\'s own policy.',
372
377
  );
373
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
378
+ emit(lines.join('\n'));
374
379
  return EXIT.OK;
375
380
  }
376
381
 
@@ -395,7 +400,7 @@ function ruleDirection(facts, flags) {
395
400
  lines.push('', `shares a decision with ${facts.others.join(', ')}`);
396
401
  }
397
402
 
398
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
403
+ emit(lines.join('\n'));
399
404
  return EXIT.OK;
400
405
  }
401
406
 
@@ -486,7 +491,7 @@ function adrDirection(facts, flags) {
486
491
  );
487
492
  }
488
493
 
489
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
494
+ emit(lines.join('\n'));
490
495
  // A superseded record is not a failure; it is the supersession rule working.
491
496
  // The caller asked a question and got a true answer.
492
497
  return EXIT.OK;
@@ -519,6 +524,22 @@ export async function decisionsCommand(args, flags) {
519
524
  file: d.file,
520
525
  }));
521
526
 
527
+ // The index is output, not authorship (review 0003 Q91): the table is rendered from
528
+ // the same records this command lists, so a new ADR cannot leave it behind. The
529
+ // write is journaled like every other repo-file write, so `undo` restores the old
530
+ // table - the index is not internal state even though it is generated.
531
+ if (flags.yes === true) {
532
+ const rootDir = root ?? process.cwd();
533
+ const index = join(rootDir, 'docs', 'decisions-index.md');
534
+ await applyPlan(
535
+ rootDir,
536
+ [change({ path: index, before: await readBefore(index), after: renderDecisionsIndex(rows), owner: 'rules decisions', reason: 'the index follows the decision files' })],
537
+ { mode: { apply: true }, label: 'rules decisions' },
538
+ );
539
+ emit(`wrote docs/decisions-index.md (${rows.length} decisions)`);
540
+ return EXIT.OK;
541
+ }
542
+
522
543
  if (isJson(flags)) {
523
544
  emitJson({ ok: true, command: 'rules decisions', root, decisions: rows });
524
545
  return EXIT.OK;
@@ -534,6 +555,6 @@ export async function decisionsCommand(args, flags) {
534
555
  const head = r.status.length > 80 ? `${r.status.slice(0, 80)}...` : r.status;
535
556
  lines.push(`${r.id.padEnd(8)} ${r.origin.padEnd(7)} ${r.title} — ${head}`);
536
557
  }
537
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
558
+ emit(lines.join('\n'));
538
559
  return EXIT.OK;
539
560
  }