simt-dev 0.7.1 → 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 (186) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +169 -80
  3. package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
  4. package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
  5. package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
  6. package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
  7. package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
  8. package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
  9. package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
  10. package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
  11. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
  12. package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
  13. package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
  14. package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
  15. package/ecosystems/bun.json +6 -0
  16. package/ecosystems/cargo.json +25 -0
  17. package/ecosystems/composer.json +6 -0
  18. package/ecosystems/dart.json +24 -0
  19. package/ecosystems/deno.json +21 -0
  20. package/ecosystems/devcontainers.json +6 -0
  21. package/ecosystems/docker-compose.json +6 -0
  22. package/ecosystems/docker.json +6 -0
  23. package/ecosystems/dotnet.json +25 -0
  24. package/ecosystems/elixir.json +23 -0
  25. package/ecosystems/gitsubmodule.json +6 -0
  26. package/ecosystems/go.json +25 -0
  27. package/ecosystems/gradle.json +13 -0
  28. package/ecosystems/maven.json +23 -0
  29. package/ecosystems/node.json +25 -0
  30. package/ecosystems/python.json +20 -0
  31. package/ecosystems/ruby.json +23 -0
  32. package/ecosystems/swift.json +25 -0
  33. package/ecosystems/terraform.json +6 -0
  34. package/ecosystems/uv.json +6 -0
  35. package/harnesses/antigravity.json +162 -0
  36. package/harnesses/claude.json +206 -0
  37. package/harnesses/codex.json +136 -0
  38. package/harnesses/copilot.json +88 -0
  39. package/harnesses/cursor.json +140 -0
  40. package/harnesses/deepseek.json +99 -0
  41. package/harnesses/gemini.json +122 -0
  42. package/harnesses/opencode.json +173 -0
  43. package/package.json +3 -1
  44. package/rules/mdc/intake-questions.mdc +8 -0
  45. package/src/advice/advice.mjs +3 -2
  46. package/src/agenthooks/render.mjs +86 -180
  47. package/src/agents/render.mjs +15 -18
  48. package/src/api.mjs +7 -5
  49. package/src/argv.mjs +19 -0
  50. package/src/bench/ablate.mjs +3 -4
  51. package/src/bench/adapters.json +155 -113
  52. package/src/bench/adapters.mjs +466 -0
  53. package/src/bench/arms.mjs +333 -0
  54. package/src/bench/grade.mjs +12 -26
  55. package/src/bench/graders.mjs +42 -0
  56. package/src/bench/mine.mjs +17 -535
  57. package/src/bench/probe-ledger.mjs +149 -0
  58. package/src/bench/probes.catalog.mjs +918 -0
  59. package/src/bench/probes.mjs +15 -1037
  60. package/src/bench/render.mjs +111 -0
  61. package/src/bench/report.mjs +116 -971
  62. package/src/bench/run.mjs +164 -881
  63. package/src/bench/variants.mjs +4 -5
  64. package/src/bench/workspace.mjs +160 -0
  65. package/src/brief/assemble.mjs +11 -13
  66. package/src/ci/dependabot.mjs +25 -55
  67. package/src/commands/agent.mjs +1 -9
  68. package/src/commands/agenthook.mjs +66 -35
  69. package/src/commands/bench.mjs +2 -2
  70. package/src/commands/ci.mjs +11 -13
  71. package/src/commands/docs.mjs +88 -0
  72. package/src/commands/gate.mjs +10 -13
  73. package/src/commands/harness.mjs +3 -3
  74. package/src/commands/help.mjs +44 -3
  75. package/src/commands/hooks.mjs +20 -20
  76. package/src/commands/ignore.mjs +11 -7
  77. package/src/commands/impact.mjs +4 -8
  78. package/src/commands/index.mjs +49 -9
  79. package/src/commands/init-scan.mjs +90 -0
  80. package/src/commands/init.mjs +122 -104
  81. package/src/commands/lease.mjs +67 -18
  82. package/src/commands/map.mjs +10 -5
  83. package/src/commands/mcp.mjs +2 -17
  84. package/src/commands/repoindex.mjs +8 -8
  85. package/src/commands/rules.mjs +8 -4
  86. package/src/commands/shared.mjs +12 -16
  87. package/src/commands/skills.mjs +5 -6
  88. package/src/commands/skillscompile.mjs +3 -3
  89. package/src/commands/skillsinstall.mjs +5 -4
  90. package/src/commands/slice.mjs +199 -64
  91. package/src/commands/spec.mjs +78 -62
  92. package/src/commands/status.mjs +24 -12
  93. package/src/commands/sync.mjs +14 -14
  94. package/src/commands/tests.mjs +12 -12
  95. package/src/commands/tokens.mjs +19 -24
  96. package/src/commands/upgrade.mjs +8 -4
  97. package/src/commands/why.mjs +6 -13
  98. package/src/compare/hooksstate.mjs +2 -6
  99. package/src/compare/learn.mjs +3 -1
  100. package/src/decisions/index.mjs +3 -2
  101. package/src/docs/commands.mjs +65 -0
  102. package/src/ecosystem/records.mjs +219 -0
  103. package/src/explain.mjs +31 -0
  104. package/src/gates/index.mjs +385 -0
  105. package/src/graph/compile.mjs +8 -4
  106. package/src/graph/load.mjs +2 -1
  107. package/src/graph/spec.mjs +10 -8
  108. package/src/harness/records.mjs +143 -0
  109. package/src/hooks/capability.mjs +5 -3
  110. package/src/hooks/dispatcher.mjs +26 -32
  111. package/src/hooks/gates.mjs +12 -2
  112. package/src/hooks/resolve.mjs +2 -7
  113. package/src/hooks/where.mjs +2 -7
  114. package/src/identity.mjs +2 -11
  115. package/src/index/coverage.mjs +58 -0
  116. package/src/index/extract.mjs +6 -16
  117. package/src/index/gate.mjs +1 -1
  118. package/src/index/govern.mjs +26 -9
  119. package/src/index/impact.mjs +4 -8
  120. package/src/index/map.mjs +14 -15
  121. package/src/index/open.mjs +173 -0
  122. package/src/index/persist.mjs +224 -57
  123. package/src/index/store.mjs +82 -39
  124. package/src/index/tests.mjs +2 -6
  125. package/src/journal.mjs +16 -11
  126. package/src/kernel/card.mjs +37 -63
  127. package/src/kernel/checks.mjs +4 -10
  128. package/src/lease.mjs +102 -7
  129. package/src/ledger/index.mjs +124 -17
  130. package/src/mcp/paginate.mjs +33 -0
  131. package/src/mcp/schemas.mjs +512 -0
  132. package/src/mcp/server.mjs +49 -9
  133. package/src/mcp/tools-facts.mjs +326 -0
  134. package/src/mcp/tools-read.mjs +221 -0
  135. package/src/mcp/tools.mjs +4 -1005
  136. package/src/{bench → measure}/corpus.mjs +1 -1
  137. package/src/measure/metrics.mjs +236 -0
  138. package/src/measure/records.mjs +211 -0
  139. package/src/measure/stats.mjs +538 -0
  140. package/src/measure/task-schema.mjs +125 -0
  141. package/src/measure/tasks.mjs +69 -0
  142. package/src/memory/decide.mjs +6 -15
  143. package/src/memory/lesson-text.mjs +61 -0
  144. package/src/memory/lessons.mjs +29 -82
  145. package/src/name.mjs +7 -0
  146. package/src/output/index.mjs +122 -31
  147. package/src/packs/manage.mjs +15 -9
  148. package/src/plan/index.mjs +1 -1
  149. package/src/plugins.mjs +22 -16
  150. package/src/prbody.mjs +7 -11
  151. package/src/projection/loaders.mjs +7 -692
  152. package/src/projection/manifest.mjs +10 -435
  153. package/src/projection/mdc.mjs +7 -8
  154. package/src/projection/project.mjs +126 -221
  155. package/src/projection/region.mjs +561 -0
  156. package/src/projection/rules.mjs +16 -547
  157. package/src/projection/surfaces.mjs +43 -210
  158. package/src/projection/targets.mjs +5 -5
  159. package/src/repo-root.mjs +13 -1
  160. package/src/resolve/eval.mjs +2 -1
  161. package/src/resolve/fingerprint.mjs +0 -7
  162. package/src/resolve/resolve.mjs +1 -41
  163. package/src/review/critic.mjs +8 -12
  164. package/src/run.mjs +98 -0
  165. package/src/secrets.mjs +14 -22
  166. package/src/state/events.mjs +129 -0
  167. package/src/status-snapshot.mjs +233 -0
  168. package/src/status.mjs +2 -6
  169. package/src/sys/git.mjs +82 -0
  170. package/src/{bench/process.mjs → sys/proc.mjs} +28 -5
  171. package/src/testing/parsers.mjs +265 -0
  172. package/src/testing/patches.mjs +23 -0
  173. package/src/testing/paths.mjs +49 -0
  174. package/src/testing/runners.mjs +186 -0
  175. package/src/testing/vault.mjs +68 -0
  176. package/src/text/frontmatter.mjs +49 -0
  177. package/src/text/tokens.mjs +52 -0
  178. package/src/toolwords.mjs +1 -1
  179. package/src/util/fs.mjs +20 -0
  180. package/src/util/sort.mjs +12 -0
  181. package/src/workspace.mjs +3 -1
  182. package/src/write/plan.mjs +82 -0
  183. package/templates/plan/tracker.json +1 -0
  184. package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
  185. package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
  186. package/src/bench/tasks.mjs +0 -36
@@ -21,98 +21,100 @@
21
21
  * @property {string} path the dispatch key: `status`, or `group subcommand`
22
22
  * @property {string} summary one phrase, printed by help and by the generated docs
23
23
  * @property {string} [defaultFor] the group whose bare form resolves to this row
24
+ * @property {string[]} [aliases] spellings that resolve to this row, never listed in help
24
25
  * @property {string} [mcp] the MCP tool name that answers this row, when one does
25
26
  */
26
27
 
27
28
  /** @type {SpecRow[]} */
28
29
  export const SPEC = [
29
- { path: 'agent-hook', summary: 'the harness entry point: decide one tool call, or deliver the prompt packet', flags: ["diff","harness"], },
30
- { path: 'agent-hooks install', summary: "write this repository's policies into each harness hooks file", writes: 'repo', flags: ["yes","diff","harness"], },
30
+ { path: 'agent-hook', usage: '<event> --harness', summary: 'the harness entry point: decide one tool call, or deliver the prompt packet', aliases: ['harness hook'], flags: ["diff","harness"], },
31
+ { path: 'agent-hooks install', usage: '--yes', summary: "write this repository's policies into each harness hooks file", writes: 'repo', aliases: ['harness install'], flags: ["yes","diff","harness"], },
31
32
  { path: 'agent-hooks status', summary: 'what each harness will run, and what is not trusted yet', defaultFor: 'agent-hooks', flags: ["diff","harness"], },
32
- { path: 'agents install', summary: 'write the Simt-Agent into each harness agents file', writes: 'repo', flags: ["yes","diff","harness"], },
33
+ { path: 'agents install', usage: '--yes', summary: 'write the Simt-Agent into each harness agents file', writes: 'repo', flags: ["yes","diff","harness"], },
33
34
  // A group like `agent-hooks`: `install` writes the agent file into each harness's own
34
35
  // directory and `status` reports what a harness carries. The default is the read.
35
36
  { path: 'agents status', summary: 'what each harness carries, and what is not installed yet', defaultFor: 'agents', flags: ["diff","harness"], },
36
- { path: 'bench ablate', summary: 'measure one arm of the brief against its ablation', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
37
+ { path: 'bench ablate', usage: '--yes', summary: 'measure one arm of the brief against its ablation', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
37
38
  { path: 'bench critic', summary: 'score the review critic against the labelled bench set', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
38
- { path: 'bench export', summary: 'write the sealed bench corpus as one signed document', writes: 'repo', flags: ["yes","all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
39
+ { path: 'bench export', usage: '--yes', summary: 'write the sealed bench corpus as one signed document', writes: 'repo', flags: ["yes","all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
39
40
  { path: 'bench import', summary: 'verify a sealed corpus and load it into the vault', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
40
- { path: 'bench mine', summary: 'mine tasks out of a repository history', writes: 'repo', defaultFor: 'bench', flags: ["yes","all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
41
- { path: 'bench probes', summary: 'run the deterministic probes against a workspace', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
42
- { path: 'bench report', summary: 'summarise the runs recorded in the vault', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
43
- { path: 'bench run', summary: 'run agents against the mined tasks', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
44
- { path: 'bench variants', summary: 'run one task across its variant prompts', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
45
- { path: 'brief', mcp: { name: 'brief', flags: ['budget', 'paths', 'task'] }, summary: 'the packet for a task: rules, skills and the files it names, within a budget', flags: ["budget","explain","paths","task"], },
41
+ { path: 'bench mine', usage: '--yes', summary: 'mine tasks out of a repository history', writes: 'repo', flags: ["yes","all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
42
+ { path: 'bench probes', usage: '--yes', summary: 'run the deterministic probes against a workspace', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
43
+ { path: 'bench report', usage: '[--compare a b]', summary: 'summarise the runs recorded in the vault', defaultFor: 'bench', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
44
+ { path: 'bench run', usage: '--yes', summary: 'run agents against the mined tasks', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
45
+ { path: 'bench variants', usage: '--yes', summary: 'run one task across its variant prompts', flags: ["all","arm","compare","cross-group","harness","labels","model","regrade","replace","reps","task"], },
46
+ { path: 'brief', usage: '"<task>"', mcp: { name: 'brief', flags: ['budget', 'paths', 'task'] }, summary: 'the packet for a task: rules, skills and the files it names, within a budget', flags: ["budget","explain","paths","task"], },
46
47
  { path: 'check', mcp: { name: 'check', flags: [] }, summary: 'report stale or missing generated regions and every check finding', flags: ["check","diff","dry-run"], },
47
48
  { path: 'ci', summary: "refresh simt-dev's own .github workflow and Dependabot files", flags: ["check","diff"], },
48
49
  { path: 'compare', mcp: { name: 'compare', flags: [] }, summary: 'the adoption and drift matrix across every sibling repository', flags: [], },
49
- { path: 'completions', summary: 'print a bash, zsh or fish completion script generated from this spec', flags: [], },
50
- { path: 'decide', summary: 'scaffold a numbered decision record with its frontmatter', writes: 'repo', flags: ["yes","check","diff","dry-run","paths","supersedes","task"], },
51
- { path: 'distill', summary: 'turn a repository into a skill or rule proposal', flags: [], },
52
- { path: 'doctor', mcp: { name: 'doctor', flags: [] }, summary: 'report what is wrong with this installation', writes: 'repo', flags: ["yes","diff","fix"], },
50
+ { path: 'completions', usage: '<bash|zsh|fish>', summary: 'print a bash, zsh or fish completion script generated from this spec', flags: [], },
51
+ { path: 'decide', usage: '"<title>" --yes', summary: 'scaffold a numbered decision record with its frontmatter', writes: 'repo', flags: ["yes","check","diff","dry-run","paths","supersedes","task"], },
52
+ { path: 'distill', usage: '<skill|doc>', summary: 'turn a repository into a skill or rule proposal', flags: [], },
53
+ { path: 'docs readme', summary: 'render the README command list from the spec', writes: 'repo', defaultFor: 'docs', flags: ["yes","diff"], },
54
+ { path: 'doctor', usage: '[--fix]', mcp: { name: 'doctor', flags: [] }, summary: 'report what is wrong with this installation', writes: 'repo', flags: ["yes","diff","fix"], },
53
55
  { path: 'evidence', summary: 'the runs recorded against the tree on disk', flags: [], },
54
- { path: 'evidence check', summary: 'a commit message claiming a run this tree does not have', flags: [], },
55
- { path: 'explain', summary: 'what one check finding code means, and how to fix it', flags: [], },
56
- { path: 'gate', summary: 'the tests a change reaches, and only those', flags: ["changed"], },
57
- { path: 'graph lint', summary: 'check the skill registry against the skills on disk', defaultFor: 'graph', flags: ["spec"], },
58
- { path: 'harness detect', summary: 'which harness is installed, and what it carries', defaultFor: 'harness', flags: ["at","reader","table","user"], },
59
- { path: 'harness loaders', summary: 'the instruction files each harness loads, and what they cost', flags: ["at","reader","table","user"], },
60
- { path: 'hooks install', summary: 'install the shared dispatcher and report what it would run', writes: 'repo', flags: ["yes","diff","dry-run"], },
61
- { path: 'hooks run', summary: "run one git event's gates now, as git would", writes: 'repo', flags: ["yes","diff","dry-run"], },
56
+ { path: 'evidence check', usage: '<msg-file>', summary: 'a commit message claiming a run this tree does not have', flags: [], },
57
+ { path: 'explain', usage: '<CODE>', summary: 'what one check finding code means, and how to fix it', flags: [], },
58
+ { path: 'gate', usage: '--changed', summary: 'the tests a change reaches, and only those', flags: ["changed"], },
59
+ { path: 'graph lint', usage: '[--spec agentskills]', summary: 'check the skill registry against the skills on disk', defaultFor: 'graph', aliases: ['skills lint'], flags: ["spec"], },
60
+ { path: 'harness detect', usage: '[--user]', summary: 'which harness is installed, and what it carries', defaultFor: 'harness', flags: ["at","reader","table","user"], },
61
+ { path: 'harness loaders', usage: '[--table]', summary: 'the instruction files each harness loads, and what they cost', flags: ["at","reader","table","user"], },
62
+ { path: 'hooks install', usage: '--yes', summary: 'install the shared dispatcher and report what it would run', writes: 'repo', flags: ["yes","diff","dry-run"], },
63
+ { path: 'hooks run', usage: '<event>', summary: "run one git event's gates now, as git would", writes: 'repo', flags: ["yes","diff","dry-run"], },
62
64
  { path: 'hooks status', summary: 'what each git event routes to, and whether it is wired', defaultFor: 'hooks', flags: ["diff","dry-run"], },
63
- { path: 'ignore', summary: 'render the one ignore list into every harness ignore file', flags: ["diff"], },
64
- { path: 'impact', summary: 'who a change reaches: importers, their tests and the contract', flags: [], },
65
- { path: 'index', summary: 'rebuild the local file index from git blob hashes', flags: ["allow-remote","embed"], },
66
- { path: 'init', summary: 'adopt this repository: manifest, region, hooks and toolchain pins', writes: 'repo', flags: ["yes","accept-conflicts","backup","diff","surfaces"], },
67
- { path: 'lease', summary: 'claim files for one session, or list the claims', flags: ["harness","session","ttl"], },
65
+ { path: 'ignore', usage: '[--yes] [--diff]', summary: 'render the one ignore list into every harness ignore file', flags: ["diff"], },
66
+ { path: 'impact', usage: '<file|symbol>', summary: 'who a change reaches: importers, their tests and the contract', flags: [], },
67
+ { path: 'index', usage: '[--embed]', summary: 'rebuild the local file index from git blob hashes', flags: ["allow-remote","embed"], },
68
+ { path: 'init', usage: '[path]', summary: 'adopt this repository: manifest, region, hooks, agents and agent-hooks', writes: 'repo', flags: ["yes","accept-conflicts","backup","diff","harness","agents","surfaces"], },
69
+ { path: 'lease', usage: '[<paths>] --session', summary: 'claim files for one session, or list the claims', flags: ["harness","session","ttl"], },
68
70
  { path: 'lessons capture', summary: 'scan the ledger and history for new lesson candidates', flags: ["diff","expires","paths","task"], },
69
71
  { path: 'lessons list', summary: 'deterministic lesson candidates and the promoted lessons', defaultFor: 'lessons', flags: ["diff","expires","paths","task"], },
70
- { path: 'lessons review', summary: 'promote one lesson candidate into docs/lessons', flags: ["diff","expires","paths","task"], },
71
- { path: 'local doctor', summary: 'the local model endpoints, and whether they answer', defaultFor: 'local', flags: ["allow-remote"], },
72
- { path: 'map', summary: 'the files a task is most likely to touch, within a byte budget', flags: ["allow-remote","budget","embed","level","recall","task"], values: { level: ['outline', 'names', 'signatures'] }, },
73
- { path: 'mcp', summary: 'serve the read-only MCP surface over stdio', flags: ["list"], },
72
+ { path: 'lessons review', usage: '<id> --yes', summary: 'promote one lesson candidate into docs/lessons', flags: ["diff","expires","paths","task"], },
73
+ { path: 'local doctor', usage: '[--allow-remote]', summary: 'the local model endpoints, and whether they answer', defaultFor: 'local', flags: ["allow-remote"], },
74
+ { path: 'map', usage: '--task "<task>"', summary: 'the files a task is most likely to touch, within a byte budget', flags: ["allow-remote","budget","embed","level","recall","task"], values: { level: ['outline', 'names', 'signatures'] }, },
75
+ { path: 'mcp', usage: '[--list]', summary: 'serve the read-only MCP surface over stdio', flags: ["list"], },
74
76
  // Plan 0003 R45. The default is the read: `pack` alone lints the working directory,
75
77
  // the way `skills` alone indexes and `graph` alone lints.
76
- { path: 'pack lint', summary: 'lint a rule pack in the working directory', defaultFor: 'pack', flags: ["diff","require-measured","source"], },
77
- { path: 'pack new', summary: 'scaffold a rule pack', flags: ["diff","require-measured","source"], },
78
- { path: 'pack publish', summary: 'publish a rule pack', flags: ["diff","require-measured","source"], },
79
- { path: 'plan done', mcp: { name: 'plan_done', flags: ['state', 'test'] }, summary: 'mark one tracker row complete, with its SHA and its evidence', flags: ["diff","state","test"], },
80
- { path: 'plan next', mcp: { name: 'plan_next', flags: [] }, summary: "one ready slice's section, or the one named", flags: ["diff","state","test"], },
78
+ { path: 'pack lint', usage: '[dir] [--require-measured]', summary: 'lint a rule pack in the working directory', defaultFor: 'pack', flags: ["diff","require-measured","source"], },
79
+ { path: 'pack new', usage: '<name> --source <spec>', summary: 'scaffold a rule pack', flags: ["diff","require-measured","source"], },
80
+ { path: 'pack publish', usage: '[dir] --yes', summary: 'publish a rule pack', flags: ["diff","require-measured","source"], },
81
+ { path: 'plan done', usage: '<id> [<sha>] --yes', mcp: { name: 'plan_done', flags: ['state', 'test'] }, summary: 'mark one tracker row complete, with its SHA and its evidence', flags: ["diff","state","test"], },
82
+ { path: 'plan next', usage: '[<id>]', mcp: { name: 'plan_next', flags: [] }, summary: "one ready slice's section, or the one named", flags: ["diff","state","test"], },
81
83
  { path: 'plan status', summary: 'count the tracker rows', defaultFor: 'plan', flags: ["diff","state","test"], },
82
- { path: 'plugin', summary: 'generate a harness package from the same tables the hooks use', flags: ["diff","out"], },
83
- { path: 'pr-body', mcp: { name: 'pr_body', flags: ['base'] }, summary: 'the pull request body: evidence, decisions, gate state and residual risk', flags: ["base"], },
84
- { path: 'review', summary: 'review the staged diff against the estate rules', flags: [], },
84
+ { path: 'plugin', usage: '<claude|gemini|cursor|opencode>', summary: 'generate a harness package from the same tables the hooks use', aliases: ['harness plugin'], flags: ["diff","out"], },
85
+ { path: 'pr-body', usage: '[--base <ref>]', mcp: { name: 'pr_body', flags: ['base'] }, summary: 'the pull request body: evidence, decisions, gate state and residual risk', flags: ["base"], },
86
+ { path: 'review', usage: '[--local]', summary: 'review the staged diff against the estate rules', flags: [], },
85
87
  { path: 'rules decisions', summary: 'every known decision, and which layer each came from', writes: 'repo', flags: ["yes","all"], },
86
- { path: 'rules list', mcp: { name: 'rules_list', flags: [] }, summary: 'the rule catalogue, marked with what applies to this repository', defaultFor: 'rules', flags: ["all"], },
87
- { path: 'rules show', summary: 'one rule body', flags: ["all"], },
88
- { path: 'rules why', mcp: { name: 'rules_why', flags: [] }, summary: 'the decision that settled a rule, and whether it is still in force', flags: ["all"], },
89
- { path: 'shared install', summary: 'install the shared rules into a reader home directory', writes: 'repo', flags: ["yes","diff"], },
88
+ { path: 'rules list', usage: '[--root <path>]', mcp: { name: 'rules_list', flags: [] }, summary: 'the rule catalogue, marked with what applies to this repository', defaultFor: 'rules', flags: ["all"], },
89
+ { path: 'rules show', usage: '<id>', summary: 'one rule body', flags: ["all"], },
90
+ { path: 'rules why', usage: '<id|ADR-NNN> [--all]', mcp: { name: 'rules_why', flags: [] }, summary: 'the decision that settled a rule, and whether it is still in force', flags: ["all"], },
91
+ { path: 'shared install', usage: '--yes', summary: 'install the shared rules into a reader home directory', writes: 'repo', flags: ["yes","diff"], },
90
92
  // ADR-084. `shared` is the one group whose default writes nothing, and it is `plan`
91
93
  // for the same reason `skills`'s is `index`: the read answers "what would this do, and
92
94
  // where", and `install` - the subcommand that touches a reader's home directory - is
93
95
  // the one that needs `--yes` before it does anything.
94
96
  { path: 'shared plan', summary: 'what the shared install would write, and where', writes: 'repo', defaultFor: 'shared', flags: ["yes","diff"], },
95
- { path: 'skills brief', mcp: { name: 'skills_brief', flags: ['all', 'task'] }, summary: 'the packet for a task, read from the skill registry', flags: ["all", "plan", "task"], },
96
- { path: 'skills compile', summary: 'compile a skill to the Agent Skills standard', flags: [], },
97
- { path: 'skills eval', summary: 'score the skill registry against transcripts', flags: ["all","plan"], },
98
- { path: 'skills find', mcp: { name: 'skills_find', flags: [] }, summary: 'skills that match a query', flags: ["all","plan"], },
99
- { path: 'skills index', mcp: { name: 'skills_index', flags: [] }, summary: 'rebuild the skill index', defaultFor: 'skills', flags: ["all","plan"], },
100
- { path: 'skills install', summary: 'install skills into the workspace', writes: 'repo', flags: ["yes","diff","from","variant"], },
101
- { path: 'skills show', mcp: { name: 'skills_show', flags: [] }, summary: 'one skill packet', flags: ["all","plan"], },
102
- { path: 'skills why', mcp: { name: 'skills_why', flags: [] }, summary: 'which skill covers a task', flags: ["all","plan"], },
103
- { path: 'slice abandon', summary: 'remove a slice worktree, delete its branch, and return the row', flags: ["session","test","type"], },
104
- { path: 'slice finish', summary: 'run the gates, commit the slice and record its evidence', writes: 'repo', flags: ["yes","session","test","type"], },
97
+ { path: 'skills brief', usage: '--task "<task>"', mcp: { name: 'skills_brief', flags: ['all', 'task'] }, summary: 'the packet for a task, read from the skill registry', aliases: ['skills select'], flags: ["all", "plan", "task"], },
98
+ { path: 'skills compile', usage: '[<name>]', summary: 'compile a skill to the Agent Skills standard', flags: [], },
99
+ { path: 'skills eval', usage: '[corpus]', summary: 'score the skill registry against transcripts', flags: ["all","plan"], },
100
+ { path: 'skills find', usage: '<query>', mcp: { name: 'skills_find', flags: [] }, summary: 'skills that match a query', flags: ["all","plan"], },
101
+ { path: 'skills index', mcp: { name: 'skills_index', flags: [] }, summary: 'rebuild the skill index', defaultFor: 'skills', aliases: ['skills list'], flags: ["all","plan"], },
102
+ { path: 'skills install', usage: '<name> --yes', summary: 'install skills into the workspace', writes: 'repo', flags: ["yes","diff","from","variant"], },
103
+ { path: 'skills show', usage: '<name>', mcp: { name: 'skills_show', flags: [] }, summary: 'one skill packet', flags: ["all","plan"], },
104
+ { path: 'skills why', usage: '<name>', mcp: { name: 'skills_why', flags: [] }, summary: 'which skill covers a task', flags: ["all","plan"], },
105
+ { path: 'slice abandon', usage: '<id>', summary: 'remove a slice worktree, delete its branch, and return the row', aliases: ['plan abandon'], flags: ["session","test","type"], },
106
+ { path: 'slice finish', usage: '<id> --yes', summary: 'run the gates, commit the slice and record its evidence', writes: 'repo', aliases: ['plan finish'], flags: ["yes","session","test","type"], },
105
107
  // Plan 0003 R53. Both halves write, so a bare `slice` has no read to default to.
106
108
  // `start` is the default because with the id it is missing it fails with a usage error
107
109
  // before anything is created - the one default here that cannot mutate.
108
- { path: 'slice start', summary: 'give a slice its own worktree, branch and brief', defaultFor: 'slice', flags: ["session","test","type"], },
109
- { path: 'status', summary: 'gate state from the last check', flags: ["line"], },
110
- { path: 'sync', summary: 're-render the generated regions from the rule content', writes: 'repo', flags: ["yes","check","diff","dry-run"], },
111
- { path: 'tests', summary: 'the tests that exercise a file', flags: ["cochange"], },
112
- { path: 'tokens', summary: 'count tokens per instruction file and per skill', flags: ["repos","session"], },
113
- { path: 'undo', summary: 'restore the last journaled set of writes, if untouched', writes: 'repo', flags: ["yes","diff"], },
114
- { path: 'upgrade', summary: 'replace this installation with the pinned release', writes: 'repo', flags: ["yes","all","diff","workspace"], },
115
- { path: 'why', summary: 'what governs a file: decisions, rules, skills, gates and lessons', flags: [], },
110
+ { path: 'slice start', usage: '<id> --yes | -- "<text>"', summary: 'give a slice its worktree, branch and brief; -- starts one from a prompt, no row', defaultFor: 'slice', aliases: ['plan start'], flags: ["id","session","test","type"], },
111
+ { path: 'status', usage: '[--line]', summary: 'the repository snapshot, or one line of the last check', flags: ["line"], },
112
+ { path: 'sync', usage: '[--explain] --yes', summary: 're-render the generated regions from the rule content', writes: 'repo', flags: ["yes","check","diff","dry-run"], },
113
+ { path: 'tests', usage: '<path> [--cochange]', summary: 'the tests that exercise a file', flags: ["cochange"], },
114
+ { path: 'tokens', usage: '[--repos] [--session]', summary: 'count tokens per instruction file and per skill', aliases: ['harness cost'], flags: ["repos","session"], },
115
+ { path: 'undo', usage: '--yes', summary: 'restore the last journaled set of writes, if untouched', writes: 'repo', flags: ["yes","diff"], },
116
+ { path: 'upgrade', usage: '--yes', summary: 'replace this installation with the pinned release', writes: 'repo', flags: ["yes","all","diff","workspace"], },
117
+ { path: 'why', usage: '[path]', summary: 'what governs a file: decisions, rules, skills, gates and lessons', flags: [], },
116
118
  ];
117
119
 
118
120
 
@@ -149,3 +151,17 @@ export const SPEC_PATHS = SPEC.map((row) => row.path).sort();
149
151
 
150
152
  /** group -> the subcommand its bare form resolves to. */
151
153
  export const GROUP_DEFAULTS = new Map(SPEC.filter((row) => row.defaultFor !== undefined).map((row) => [row.defaultFor, row.path.split(' ')[1]]));
154
+
155
+ /**
156
+ * The spellings this binary answers that are not paths (review 0003 Q81).
157
+ *
158
+ * A rename costs every script and every habit a reader already has, so a new name
159
+ * arrives as an alias instead of a replacement: the path stays the dispatch key, help
160
+ * lists the path, and the alias resolves before the registry is consulted. An alias is
161
+ * deliberately absent from help and from the generated docs — the point of a second
162
+ * spelling is that the reader who already knows it can type it, not that a new reader
163
+ * has to choose between two names for one command.
164
+ */
165
+ export const ALIASES = new Map(
166
+ SPEC.flatMap((row) => (row.aliases ?? []).map((alias) => [alias, row.path])),
167
+ );
@@ -1,26 +1,38 @@
1
- // `simt status --line`
1
+ // `simt status [--line]`
2
2
  //
3
- // One line from the last `check`, for shell prompts, tmux and agent status
4
- // lines: `simt PASS ✓rules ✓hooks 3/3gates`. The line is the gate state *as
5
- // of the last check* — it reads only `.simt/check.json` plus one
6
- // `git rev-parse HEAD`, so it answers in milliseconds while `check` itself
7
- // takes the time projection takes.
3
+ // Two questions, one command. `--line` is for shell prompts, tmux and agent status lines:
4
+ // one line from the last `check`, read only from `.simt/check.json` plus one
5
+ // `git rev-parse HEAD`, so it answers in milliseconds while `check` itself takes the time
6
+ // projection takes. Bare `status` is the aggregate a UI's home view asks for - last
7
+ // check, git hooks, agent hooks, evidence for the tree on disk, active leases, tracker
8
+ // state, the last journal set - and it was a usage error until review 0003 Q82.
8
9
  //
9
- // Bare `status` is a usage error, the way `plan` without a subcommand is: the
10
- // only mode this command has is `--line`, and running something else by
11
- // default would invent a second answer to what the status is.
10
+ // The line's cache lives in `status.mjs`; the aggregate lives in `status-snapshot.mjs`.
11
+ // Both report the gate *as of the last check* rather than measuring: a status command
12
+ // that ran the gate would cost what the gate costs, and a prompt that waited for it is a
13
+ // prompt nobody reads.
12
14
 
13
- import { EXIT, emit, emitJson, fail, isJson, present } from '../output/index.mjs';
15
+ import { EXIT, emit, emitJson, isJson, present } from '../output/index.mjs';
14
16
  import { CLI } from '../name.mjs';
15
17
  import { currentHead, readCheckCache, renderStatusLine } from '../status.mjs';
18
+ import { renderSnapshot, statusSnapshot } from '../status-snapshot.mjs';
16
19
  import { commandRoot } from '../repo-root.mjs';
17
20
 
18
21
  export async function statusCommand(args, flags) {
19
22
  const root = commandRoot(flags, args);
20
- if (flags.line !== true) {
21
- return fail('status needs --line', EXIT.USAGE, `run: ${CLI} status --line --root ${root}`);
23
+ if (flags.line === true) return statusLine(root, flags);
24
+
25
+ const snapshot = await statusSnapshot(root);
26
+ const code = snapshot.ok ? EXIT.OK : EXIT.FAIL;
27
+ if (isJson(flags)) {
28
+ emitJson(snapshot);
29
+ return code;
22
30
  }
31
+ emit(renderSnapshot(snapshot));
32
+ return code;
33
+ }
23
34
 
35
+ async function statusLine(root, flags) {
24
36
  const record = await readCheckCache(root);
25
37
  if (record === null) {
26
38
  // Informational, not a failure: a repository that never ran `check` — or
@@ -29,7 +29,8 @@ import { surfacesLine } from '../projection/surfaces.mjs';
29
29
  import { CLI, LOCK_FILE, MANIFEST_FILE, MANIFEST_LEGACY } from '../name.mjs';
30
30
  import { assertVersionPin, packageVersion } from '../package-version.mjs';
31
31
  import { resolvePacks } from '../packs.mjs';
32
- import { commitJournal, journaledWriteFile } from '../journal.mjs';
32
+ import { commitJournal } from '../journal.mjs';
33
+ import { applyPlan, change } from '../write/plan.mjs';
33
34
  import { buildCacheRecord, hooksSummary, invalidateCheckCache, writeCheckCache } from '../status.mjs';
34
35
  import { computeLock, readLock } from '../projection/lock.mjs';
35
36
  import { runDeclaredChecks } from '../kernel/checks.mjs';
@@ -326,19 +327,18 @@ export async function syncCommand(positional, flags) {
326
327
  await writeManifest(root, manifestPlan.manifest, journal);
327
328
  note(`wrote ${manifestPlan.file} (generated field(s) ${manifestPlan.stale.join(', ')})`);
328
329
  }
329
- for (const t of changed) {
330
- // Three of the four rule surfaces live in directories that do not exist yet in a
331
- // repository that has only ever been written to by hand. `init` grew its own
332
- // mkdir; `sync` did not, so the first `sync` after a surface was added failed with
333
- // ENOENT on the second target and had already written the first. The parent is
334
- // derived from the target, never from the surface table, so a future surface with a
335
- // deeper path needs no change here. The lock is one of these targets (Q29): it
336
- // reaches this loop like every other file, so a deleted lock is a write the plan
337
- // shows and the journal records.
338
- await mkdir(dirname(t.path), { recursive: true });
339
- await journaledWriteFile(root, journal, t.path, t.text);
340
- note(`wrote ${t.file}`);
341
- }
330
+ // The parent is derived from the target, never from the surface table, so a future
331
+ // surface with a deeper path needs no change here. Three of the four rule surfaces
332
+ // live in directories that do not exist yet in a repository that has only ever been
333
+ // written to by hand, and the first `sync` after a surface was added failed with
334
+ // ENOENT on the second target having already written the first.
335
+ const targetChanges = await Promise.all(
336
+ changed.map(async (t) =>
337
+ change({ path: t.path, before: await readBefore(t.path), after: t.text, owner: 'sync', reason: 'the region is stale' }),
338
+ ),
339
+ );
340
+ await applyPlan(root, targetChanges, { mode, journal, commit: false });
341
+ for (const t of changed) note(`wrote ${t.file}`);
342
342
  await commitJournal(root, 'sync', journal);
343
343
  // The regions moved, so the last `check` no longer describes this tree.
344
344
  // Invalidated rather than refreshed: `sync` does not run gates, checkers
@@ -5,19 +5,17 @@
5
5
  // batch. Coverage is read only from a fixed list of paths. History is the last
6
6
  // 200 non-merge commits, which is a map and not the whole changelog.
7
7
 
8
- import { execFile } from 'node:child_process';
9
8
  import { readFile } from 'node:fs/promises';
10
9
  import { join } from 'node:path';
11
- import { promisify } from 'node:util';
12
10
 
13
11
  import { EXIT, emit, emitJson, fail, isJson, present } from '../output/index.mjs';
14
12
  import { CLI } from '../name.mjs';
15
- import { isTestPath } from '../bench/mine.mjs';
13
+ import { isTestPath } from '../testing/paths.mjs';
16
14
  import { blobTexts, isIndexStale, langOf, readEdges, readFiles, reindex } from '../index/store.mjs';
17
- import { HISTORY, importsOf, parseCoverage, testEdges, testsFor } from '../index/tests.mjs';
15
+ import { coverage } from '../index/coverage.mjs';
16
+ import { HISTORY, importsOf, parseCoverage, testEdges } from '../index/tests.mjs';
18
17
  import { commandRoot } from '../repo-root.mjs';
19
-
20
- const exec = promisify(execFile);
18
+ import { git } from '../sys/git.mjs';
21
19
 
22
20
  const COVERAGE_PATHS = [
23
21
  'coverage/lcov.info',
@@ -36,7 +34,7 @@ export async function testsCommand(args, flags) {
36
34
  const loaded = await loadFiles(root);
37
35
  if (loaded.error) return loaded.error;
38
36
  const { files } = loaded;
39
- const [stored, texts, coverage, commits] = await Promise.all([
37
+ const [stored, texts, reports, commits] = await Promise.all([
40
38
  readEdges(root),
41
39
  testBlobs(root, files),
42
40
  coverageTexts(root),
@@ -50,13 +48,16 @@ export async function testsCommand(args, flags) {
50
48
  if (!text) continue;
51
49
  imports.push(...importsOf(file.path, text, langOf(file.path), paths));
52
50
  }
53
- const edges = testEdges({ files, imports, coverage, commits });
51
+ const edges = testEdges({ files, imports, coverage: reports, commits });
54
52
  const target = args[0];
55
53
  if (!target) return summarise(root, flags, edges);
56
54
  if (!paths.has(target)) {
57
55
  return fail(`not in the index: ${target}`, EXIT.USAGE, `pass a path ${CLI} index knows`);
58
56
  }
59
- const tests = testsFor(target, edges, { cochange: flags.cochange === true });
57
+ const tests = coverage({ files, imports, coverage: reports, commits }, [target], {
58
+ transitive: false,
59
+ cochange: flags.cochange === true,
60
+ });
60
61
  const payload = { ok: true, command: 'tests', root, path: target, tests };
61
62
  if (isJson(flags)) {
62
63
  emitJson(payload);
@@ -125,10 +126,9 @@ async function coverageTexts(root) {
125
126
  async function history(root) {
126
127
  let stdout = '';
127
128
  try {
128
- ({ stdout } = await exec('git', ['log', '-n', String(HISTORY), '--no-merges', '--name-only', '--pretty=format:%x1e'], {
129
- cwd: root,
129
+ stdout = await git(root, ['log', '-n', String(HISTORY), '--no-merges', '--name-only', '--pretty=format:%x1e'], {
130
130
  maxBuffer: 16 << 20,
131
- }));
131
+ });
132
132
  } catch {
133
133
  return [];
134
134
  }
@@ -35,15 +35,10 @@ import {
35
35
  } from '../measure/context.mjs';
36
36
  import { measureSession } from '../measure/session.mjs';
37
37
  import { sharedTotals } from '../projection/shared.mjs';
38
-
39
- /** `1234567`, never `1,234,567`. `toLocaleString` output depends on the locale, and
40
- * this command exists so two machines produce two numbers a pull request can diff. */
41
- function plain(n) {
42
- return String(n);
43
- }
38
+ import { plainNumber } from '../util/fs.mjs';
44
39
 
45
40
  function files(n) {
46
- return n === 1 ? '1 file' : `${plain(n)} files`;
41
+ return n === 1 ? '1 file' : `${plainNumber(n)} files`;
47
42
  }
48
43
 
49
44
  export async function tokensCommand(positional, flags) {
@@ -68,10 +63,10 @@ export async function tokensCommand(positional, flags) {
68
63
  }
69
64
  const lines = [
70
65
  `tokens ${session.path} (session)`,
71
- `rules ${plain(session.rules.bytes)} B ~${plain(session.rules.tokens)} tokens (${plain(session.rules.share)}%)`,
72
- `file reads ${plain(session.fileReads.bytes)} B ~${plain(session.fileReads.tokens)} tokens (${plain(session.fileReads.share)}%)`,
73
- `tool output ${plain(session.toolOutput.bytes)} B ~${plain(session.toolOutput.tokens)} tokens (${plain(session.toolOutput.share)}%)`,
74
- `total ${plain(session.total.bytes)} B ~${plain(session.total.tokens)} tokens`,
66
+ `rules ${plainNumber(session.rules.bytes)} B ~${plainNumber(session.rules.tokens)} tokens (${plainNumber(session.rules.share)}%)`,
67
+ `file reads ${plainNumber(session.fileReads.bytes)} B ~${plainNumber(session.fileReads.tokens)} tokens (${plainNumber(session.fileReads.share)}%)`,
68
+ `tool output ${plainNumber(session.toolOutput.bytes)} B ~${plainNumber(session.toolOutput.tokens)} tokens (${plainNumber(session.toolOutput.share)}%)`,
69
+ `total ${plainNumber(session.total.bytes)} B ~${plainNumber(session.total.tokens)} tokens`,
75
70
  ];
76
71
  emit(lines.join('\n'));
77
72
  return EXIT.OK;
@@ -140,27 +135,27 @@ export async function tokensCommand(positional, flags) {
140
135
 
141
136
  const lines = [];
142
137
  if (flags.repos) {
143
- lines.push(`tokens ${root} ${plain(summary.roots)} repositories (${target.source})`);
138
+ lines.push(`tokens ${root} ${plainNumber(summary.roots)} repositories (${target.source})`);
144
139
  for (const row of rows) {
145
- const budget = row.budget.over ? ` over budget by ${plain(row.budget.overBy)} B` : '';
140
+ const budget = row.budget.over ? ` over budget by ${plainNumber(row.budget.overBy)} B` : '';
146
141
  lines.push(
147
- `${row.name} generated ${plain(row.generatedBytes)} B duplicate ${plain(row.redundancy.duplicateBytes)} B unique ${plain(row.redundancy.uniqueBytes)} B${budget}`,
142
+ `${row.name} generated ${plainNumber(row.generatedBytes)} B duplicate ${plainNumber(row.redundancy.duplicateBytes)} B unique ${plainNumber(row.redundancy.uniqueBytes)} B${budget}`,
148
143
  );
149
144
  for (const finding of row.findings) lines.push(`note ${row.name}: ${finding}`);
150
145
  }
151
146
  const share =
152
147
  summary.generatedBytes === 0 ? 0 : Math.round((summary.duplicateBytes / summary.generatedBytes) * 100);
153
148
  lines.push(
154
- `totals generated ${plain(summary.generatedBytes)} B duplicate ${plain(summary.duplicateBytes)} B (${plain(share)}% of generated) unique ${plain(summary.uniqueBytes)} B repeat ${plain(summary.repeatBytes)} B ~${plain(summary.tokens)} tokens`,
149
+ `totals generated ${plainNumber(summary.generatedBytes)} B duplicate ${plainNumber(summary.duplicateBytes)} B (${plainNumber(share)}% of generated) unique ${plainNumber(summary.uniqueBytes)} B repeat ${plainNumber(summary.repeatBytes)} B ~${plainNumber(summary.tokens)} tokens`,
155
150
  );
156
151
  // Only when there is a copy to count. The line a reader sees most is the one that
157
152
  // says what their whole estate loads, and adding a second number to it that is
158
153
  // usually zero is how a report stops being read.
159
154
  if (summary.sharedBytes > 0) {
160
155
  lines.push(
161
- `shared ${plain(summary.sharedBytes)} B carried once in ${plain(summary.sharedFiles.length)} user-level file(s) estate total ${plain(summary.estateBytes)} B`,
156
+ `shared ${plainNumber(summary.sharedBytes)} B carried once in ${plainNumber(summary.sharedFiles.length)} user-level file(s) estate total ${plainNumber(summary.estateBytes)} B`,
162
157
  );
163
- for (const f of summary.sharedFiles) lines.push(` ${f.path} ${plain(f.bytes)} B ${plain(f.rules)} rule(s)`);
158
+ for (const f of summary.sharedFiles) lines.push(` ${f.path} ${plainNumber(f.bytes)} B ${plainNumber(f.rules)} rule(s)`);
164
159
  }
165
160
  } else {
166
161
  const row = rows[0];
@@ -168,29 +163,29 @@ export async function tokensCommand(positional, flags) {
168
163
  lines.push(`state ${row.adoption} surfaces ${row.surfaces.length === 0 ? '(AGENTS.md only)' : row.surfaces.join(', ')}`);
169
164
  for (const channel of row.channels) {
170
165
  lines.push(
171
- `${channel.channel.padEnd(9)}${files(channel.files)} ${plain(channel.generatedBytes)} B generated ${plain(channel.readerBytes)} B read`,
166
+ `${channel.channel.padEnd(9)}${files(channel.files)} ${plainNumber(channel.generatedBytes)} B generated ${plainNumber(channel.readerBytes)} B read`,
172
167
  );
173
168
  }
174
- const verdict = row.budget.over ? `over by ${plain(row.budget.overBy)} B` : 'within budget';
169
+ const verdict = row.budget.over ? `over by ${plainNumber(row.budget.overBy)} B` : 'within budget';
175
170
  lines.push(
176
- `always-on ${plain(row.generatedBytes)} B generated ~${plain(row.generatedTokens)} tokens budget ${plain(row.budget.bytes)} B ${verdict}`,
171
+ `always-on ${plainNumber(row.generatedBytes)} B generated ~${plainNumber(row.generatedTokens)} tokens budget ${plainNumber(row.budget.bytes)} B ${verdict}`,
177
172
  );
178
173
  // The same reason as the `--repos` line, and it matters more here: measuring one
179
174
  // repository is the default, so a reader who owns the workspace copy and never saw it
180
175
  // counted would read a saving their own session does not get.
181
176
  if (summary.sharedBytes > 0) {
182
177
  lines.push(
183
- `shared ${plain(summary.sharedBytes)} B carried once in ${plain(summary.sharedFiles.length)} user-level file(s) always-on for you: ${plain(summary.estateBytes)} B`,
178
+ `shared ${plainNumber(summary.sharedBytes)} B carried once in ${plainNumber(summary.sharedFiles.length)} user-level file(s) always-on for you: ${plainNumber(summary.estateBytes)} B`,
184
179
  );
185
- for (const f of summary.sharedFiles) lines.push(` ${f.path} ${plain(f.bytes)} B ${plain(f.rules)} rule(s)`);
180
+ for (const f of summary.sharedFiles) lines.push(` ${f.path} ${plainNumber(f.bytes)} B ${plainNumber(f.rules)} rule(s)`);
186
181
  }
187
182
  lines.push(
188
- `redundant ${plain(row.redundancy.duplicateBytes)} B repeats an earlier repository ${plain(row.redundancy.repeatBytes)} B repeats an earlier document here ${plain(row.redundancy.uniqueBytes)} B unique of ${plain(row.redundancy.blockBytes)} B counted in blocks`,
183
+ `redundant ${plainNumber(row.redundancy.duplicateBytes)} B repeats an earlier repository ${plainNumber(row.redundancy.repeatBytes)} B repeats an earlier document here ${plainNumber(row.redundancy.uniqueBytes)} B unique of ${plainNumber(row.redundancy.blockBytes)} B counted in blocks`,
189
184
  );
190
185
  for (const finding of row.findings) lines.push(`note ${finding}`);
191
186
  }
192
187
  lines.push(
193
- `mcp tools/list ${plain(mcp.bytes)} B of ${plain(mcp.tools)} tools (the same for every repository, so it is not in any total)`,
188
+ `mcp tools/list ${plainNumber(mcp.bytes)} B of ${plainNumber(mcp.tools)} tools (the same for every repository, so it is not in any total)`,
194
189
  );
195
190
 
196
191
  emit(lines.join('\n'));
@@ -26,12 +26,14 @@
26
26
  // default resolved the workspace and a dry run inside one repo planned all sixteen.
27
27
 
28
28
  import { mkdir } from 'node:fs/promises';
29
- import { commitJournal, journaledWriteFile } from '../journal.mjs';
29
+ import { commitJournal } from '../journal.mjs';
30
+ import { applyPlan, change } from '../write/plan.mjs';
30
31
  import { invalidateCheckCache } from '../status.mjs';
31
32
 
32
33
  import { basename, dirname, resolve } from 'node:path';
33
34
 
34
35
  import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
36
+ import { namedRoot } from '../repo-root.mjs';
35
37
  import { provenanceDigest, plan } from '../projection/project.mjs';
36
38
  import { MANIFEST, MANIFEST_COMMENT, manifestPath, manifestText, readManifest, writeManifest } from '../projection/manifest.mjs';
37
39
  import { diffField, emitDiffs, readBefore } from '../diff.mjs';
@@ -42,7 +44,7 @@ import { CLI } from '../name.mjs';
42
44
  import { MANIFEST_FILE } from '../name.mjs';
43
45
 
44
46
  export async function upgradeCommand(positional, flags) {
45
- const explicit = flags.root ?? positional[0];
47
+ const explicit = namedRoot(flags, positional);
46
48
 
47
49
  // `--all` is a claim about SCOPE, so it is checked rather than inferred. "All" of
48
50
  // nothing is a contradiction, and defaulting it to the current directory would make
@@ -179,12 +181,14 @@ export async function upgradeCommand(positional, flags) {
179
181
  // `undo` in one consumer must never reach into another (R49).
180
182
  const journal = [];
181
183
  const { targets } = await plan(p.path);
184
+ const targetChanges = [];
182
185
  for (const t of targets) {
183
186
  if (t.action === 'unchanged' || t.action === 'malformed') continue;
184
- await mkdir(dirname(t.path), { recursive: true });
185
- await journaledWriteFile(p.path, journal, t.path, t.text);
187
+ targetChanges.push(change({ path: t.path, before: await readBefore(t.path), after: t.text, owner: 'upgrade', reason: t.action }));
186
188
  writtenFiles.add(t.file);
187
189
  }
190
+ // The caller's journal, committed once when the workspace walk ends.
191
+ await applyPlan(p.path, targetChanges, { mode: { apply: true }, journal, commit: false });
188
192
  const { manifest } = await readManifest(p.path);
189
193
  // Preserve what the repo chose; refresh what simt-dev generates. $comment is
190
194
  // ours - it describes this file's provenance - so carrying the previous copy
@@ -5,10 +5,8 @@
5
5
  // is `git ls-files`. The index is not rebuilt: governance is the records, and
6
6
  // writing the cache to answer a question would change the tree.
7
7
 
8
- import { execFile } from 'node:child_process';
9
8
  import { readFile, readdir } from 'node:fs/promises';
10
9
  import { join } from 'node:path';
11
- import { promisify } from 'node:util';
12
10
 
13
11
  import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
14
12
  import { CLI } from '../name.mjs';
@@ -16,18 +14,15 @@ import { FORMATTABLE, GATES } from '../hooks/gates.mjs';
16
14
  import { loadManifest } from '../manifest/schema.mjs';
17
15
  import { decisionsDir } from '../decisions/dir.mjs';
18
16
  import {
19
- decisionHits,
20
- scopeHits,
17
+ governs,
21
18
  scopePaths,
22
- sortHits,
23
19
  sourceRoots,
24
20
  unresolvedAffects,
25
21
  } from '../index/govern.mjs';
26
- import { splitFrontmatter } from '../memory/decide.mjs';
22
+ import { parseFrontmatter } from '../text/frontmatter.mjs';
27
23
  import { listPromotedLessons } from '../memory/lessons.mjs';
28
24
  import { commandRoot } from '../repo-root.mjs';
29
-
30
- const exec = promisify(execFile);
25
+ import { gitPaths } from '../sys/git.mjs';
31
26
 
32
27
  const RULE_DIRS = ['rules', 'rules/stacks', 'rules/mdc', '.simt/profiles'];
33
28
 
@@ -43,8 +38,7 @@ export async function whyCommand(args, flags, commands = []) {
43
38
  if (!target) return reportUnresolved(root, flags, unresolvedAffects(records, ctx));
44
39
  if (!files.has(target)) return fail(`not in the tree: ${target}`, EXIT.USAGE, `pass a path git tracks`);
45
40
  const corpus = await loadCorpus(root);
46
- const { hits } = decisionHits(records, target, ctx);
47
- const items = sortHits([...hits, ...scopeHits(target, corpus)]);
41
+ const items = governs(target, { records, ...corpus, ctx });
48
42
  const payload = { ok: true, command: 'why', root, path: target, items };
49
43
  if (isJson(flags)) {
50
44
  emitJson(payload);
@@ -72,8 +66,7 @@ export async function tracked(root) {
72
66
  try {
73
67
  // `-z`: without it git C-quotes a non-ASCII path and the reader's real
74
68
  // filename never matches it (review 0003 Q39).
75
- const { stdout } = await exec('git', ['ls-files', '-z'], { cwd: root, maxBuffer: 16 << 20 });
76
- return { files: stdout.split('\0').filter(Boolean) };
69
+ return { files: await gitPaths(root, ['ls-files']) };
77
70
  } catch (error) {
78
71
  return {
79
72
  error: fail(
@@ -99,7 +92,7 @@ export async function decisionRecords(root) {
99
92
  const records = [];
100
93
  for (const name of names) {
101
94
  const text = await readFile(join(dir, name), 'utf8');
102
- const { body } = splitFrontmatter(text);
95
+ const { body } = parseFrontmatter(text);
103
96
  const id = /^ADR-\d{3,}/i.exec(name)[0].toUpperCase();
104
97
  const heading = /^#\s+(.*)$/m.exec(body)?.[1] ?? id;
105
98
  const title = heading.replace(/^ADR-\d{3,}\s*[—–-]\s*/i, '').trim();