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
@@ -0,0 +1,167 @@
1
+ // The command spec: one row per path this binary dispatches (review 0003 Q60).
2
+ //
3
+ // The list of commands existed in four places that disagreed - the dispatch map, a
4
+ // hand-written `--help` template, the completions table and the README - so a command
5
+ // could work and be undiscoverable, or be documented and absent. A row here is the one
6
+ // place a command is declared; `src/commands/index.mjs` derives its dispatchable paths
7
+ // and group defaults from it, and `test/spec.test.mjs` fails when the registry and the
8
+ // spec stop agreeing in either direction.
9
+ //
10
+ // The fields that only some consumers need (`tier`, `writes`, `args`, `flags`) arrive
11
+ // with the slices that read them: help (Q61), validation (Q62), MCP (Q66). A field no
12
+ // code reads is a field that drifts, and a wrong `writes` claim is worse than none.
13
+ //
14
+ // `defaultFor` marks the subcommand a bare group resolves to. It sits on the row that
15
+ // IS the default rather than on a separate map, so the reason for a default lives beside
16
+ // the command it names - `skills` alone indexes because the read is the safe default,
17
+ // `slice` alone starts because an id-less `start` fails before it writes anything.
18
+
19
+ /**
20
+ * @typedef {object} SpecRow
21
+ * @property {string} path the dispatch key: `status`, or `group subcommand`
22
+ * @property {string} summary one phrase, printed by help and by the generated docs
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
25
+ * @property {string} [mcp] the MCP tool name that answers this row, when one does
26
+ */
27
+
28
+ /** @type {SpecRow[]} */
29
+ export const SPEC = [
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"], },
32
+ { path: 'agent-hooks status', summary: 'what each harness will run, and what is not trusted yet', defaultFor: 'agent-hooks', flags: ["diff","harness"], },
33
+ { path: 'agents install', usage: '--yes', summary: 'write the Simt-Agent into each harness agents file', writes: 'repo', flags: ["yes","diff","harness"], },
34
+ // A group like `agent-hooks`: `install` writes the agent file into each harness's own
35
+ // directory and `status` reports what a harness carries. The default is the read.
36
+ { path: 'agents status', summary: 'what each harness carries, and what is not installed yet', defaultFor: 'agents', flags: ["diff","harness"], },
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"], },
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"], },
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"], },
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"], },
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"], },
47
+ { path: 'check', mcp: { name: 'check', flags: [] }, summary: 'report stale or missing generated regions and every check finding', flags: ["check","diff","dry-run"], },
48
+ { path: 'ci', summary: "refresh simt-dev's own .github workflow and Dependabot files", flags: ["check","diff"], },
49
+ { path: 'compare', mcp: { name: 'compare', flags: [] }, summary: 'the adoption and drift matrix across every sibling repository', flags: [], },
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"], },
55
+ { path: 'evidence', summary: 'the runs recorded against the tree on disk', flags: [], },
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"], },
64
+ { path: 'hooks status', summary: 'what each git event routes to, and whether it is wired', defaultFor: 'hooks', flags: ["diff","dry-run"], },
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"], },
70
+ { path: 'lessons capture', summary: 'scan the ledger and history for new lesson candidates', flags: ["diff","expires","paths","task"], },
71
+ { path: 'lessons list', summary: 'deterministic lesson candidates and the promoted lessons', defaultFor: 'lessons', flags: ["diff","expires","paths","task"], },
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"], },
76
+ // Plan 0003 R45. The default is the read: `pack` alone lints the working directory,
77
+ // the way `skills` alone indexes and `graph` alone lints.
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"], },
83
+ { path: 'plan status', summary: 'count the tracker rows', defaultFor: 'plan', flags: ["diff","state","test"], },
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: [], },
87
+ { path: 'rules decisions', summary: 'every known decision, and which layer each came from', writes: 'repo', flags: ["yes","all"], },
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"], },
92
+ // ADR-084. `shared` is the one group whose default writes nothing, and it is `plan`
93
+ // for the same reason `skills`'s is `index`: the read answers "what would this do, and
94
+ // where", and `install` - the subcommand that touches a reader's home directory - is
95
+ // the one that needs `--yes` before it does anything.
96
+ { path: 'shared plan', summary: 'what the shared install would write, and where', writes: 'repo', defaultFor: 'shared', flags: ["yes","diff"], },
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"], },
107
+ // Plan 0003 R53. Both halves write, so a bare `slice` has no read to default to.
108
+ // `start` is the default because with the id it is missing it fails with a usage error
109
+ // before anything is created - the one default here that cannot mutate.
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: [], },
118
+ ];
119
+
120
+
121
+ /**
122
+ * Which tier a command belongs to, decided by its group: the commands a reader reaches
123
+ * for while working are core and appear in `--help`; the rest are advanced and appear
124
+ * under `help --all`. A tier is a presentation fact, not a permission, so a command
125
+ * cannot be missing from both pages - `test/help.test.mjs` checks that every path is on
126
+ * one of them (review 0003 Q61).
127
+ */
128
+ const CORE_GROUPS = new Set(['brief', 'check', 'gate', 'graph', 'hooks', 'impact', 'init', 'map', 'plan', 'rules', 'skills', 'slice', 'status', 'sync', 'tests', 'why']);
129
+
130
+ /** The tier a path is listed under: `core` or `advanced`. */
131
+ export function tierOf(path) {
132
+ return CORE_GROUPS.has(path.split(' ')[0]) ? 'core' : 'advanced';
133
+ }
134
+
135
+ /**
136
+ * The tools that answer a question no command asks.
137
+ *
138
+ * `federation_query` reads the federation config and consults a sibling repository
139
+ * directly (src/federation/query.mjs). There is no CLI verb for it, so giving it a spec
140
+ * row would advertise a command this binary cannot dispatch; leaving it undeclared is
141
+ * how a tool list and a command list drift apart. It is listed here with the reason
142
+ * instead, and test/mcp-spec.test.mjs accepts a tool without a row only when this table
143
+ * says why.
144
+ */
145
+ export const MCP_ONLY = [
146
+ { tool: 'federation_query', why: 'a transport-only read: no command in this binary asks a sibling repository a question' },
147
+ ];
148
+
149
+ /** The dispatchable paths, sorted - the answer to "what can this binary do". */
150
+ export const SPEC_PATHS = SPEC.map((row) => row.path).sort();
151
+
152
+ /** group -> the subcommand its bare form resolves to. */
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,35 +1,44 @@
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 } 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';
19
+ import { commandRoot } from '../repo-root.mjs';
16
20
 
17
21
  export async function statusCommand(args, flags) {
18
- const root = flags.root ?? args[0] ?? process.cwd();
19
- if (flags.line !== true) {
20
- return fail('status needs --line', EXIT.USAGE, `run: ${CLI} status --line --root ${root}`);
22
+ const root = commandRoot(flags, args);
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;
21
30
  }
31
+ emit(renderSnapshot(snapshot));
32
+ return code;
33
+ }
22
34
 
35
+ async function statusLine(root, flags) {
23
36
  const record = await readCheckCache(root);
24
37
  if (record === null) {
25
38
  // Informational, not a failure: a repository that never ran `check` — or
26
39
  // one a writer just invalidated — has no state to report, and a prompt
27
40
  // that errored until the first check would train its reader to ignore it.
28
- if (isJson(flags)) {
29
- emitJson({ ok: true, command: 'status', root, line: null, cached: false, stale: false });
30
- return EXIT.OK;
31
- }
32
- emit('simt no cache - run check');
41
+ present({ ok: true, command: 'status', root, line: null, cached: false, stale: false }, () => 'simt no cache - run check', flags);
33
42
  return EXIT.OK;
34
43
  }
35
44
 
@@ -19,7 +19,8 @@
19
19
  import { mkdir, readFile, stat, symlink, unlink } from 'node:fs/promises';
20
20
  import { dirname, join, resolve, sep } from 'node:path';
21
21
  import { EXIT, emit, emitJson, fail, isJson, note, writeMode } from '../output/index.mjs';
22
- import { plan, checkTargets, checkTarget, provenanceDigest, loadRules, select, pointerTargets, pointerState } from '../projection/project.mjs';
22
+ import { repoContext } from '../context.mjs';
23
+ import { plan, checkTargets, checkTarget, select, pointerTargets, pointerState } from '../projection/project.mjs';
23
24
  import { fingerprint } from '../resolve/fingerprint.mjs';
24
25
  import { manifestDrift, readManifest, unrunnableGates, buildManifest, writeManifest, withGenerated, manifestText, manifestPath } from '../projection/manifest.mjs';
25
26
  import { diffField, emitDiffs, readBefore } from '../diff.mjs';
@@ -28,12 +29,14 @@ import { surfacesLine } from '../projection/surfaces.mjs';
28
29
  import { CLI, LOCK_FILE, MANIFEST_FILE, MANIFEST_LEGACY } from '../name.mjs';
29
30
  import { assertVersionPin, packageVersion } from '../package-version.mjs';
30
31
  import { resolvePacks } from '../packs.mjs';
31
- import { commitJournal, journaledWriteFile } from '../journal.mjs';
32
+ import { commitJournal } from '../journal.mjs';
33
+ import { applyPlan, change } from '../write/plan.mjs';
32
34
  import { buildCacheRecord, hooksSummary, invalidateCheckCache, writeCheckCache } from '../status.mjs';
33
35
  import { computeLock, readLock } from '../projection/lock.mjs';
34
36
  import { runDeclaredChecks } from '../kernel/checks.mjs';
35
37
  import { decisionsCutoff, decisionsDir } from '../decisions/dir.mjs';
36
38
  import { lintDecisions } from '../memory/decide.mjs';
39
+ import { commandRoot } from '../repo-root.mjs';
37
40
 
38
41
  /** Findings `sync` reports and does not fail on.
39
42
  *
@@ -123,7 +126,7 @@ export const CHECK_NON_BLOCKING = new Set([
123
126
  ]);
124
127
 
125
128
  export async function syncCommand(positional, flags) {
126
- const root = flags.root ?? positional[0] ?? process.cwd();
129
+ const root = commandRoot(flags, positional);
127
130
  const checkOnly = flags.check === true;
128
131
  const mode = writeMode(flags);
129
132
 
@@ -324,19 +327,18 @@ export async function syncCommand(positional, flags) {
324
327
  await writeManifest(root, manifestPlan.manifest, journal);
325
328
  note(`wrote ${manifestPlan.file} (generated field(s) ${manifestPlan.stale.join(', ')})`);
326
329
  }
327
- for (const t of changed) {
328
- // Three of the four rule surfaces live in directories that do not exist yet in a
329
- // repository that has only ever been written to by hand. `init` grew its own
330
- // mkdir; `sync` did not, so the first `sync` after a surface was added failed with
331
- // ENOENT on the second target and had already written the first. The parent is
332
- // derived from the target, never from the surface table, so a future surface with a
333
- // deeper path needs no change here. The lock is one of these targets (Q29): it
334
- // reaches this loop like every other file, so a deleted lock is a write the plan
335
- // shows and the journal records.
336
- await mkdir(dirname(t.path), { recursive: true });
337
- await journaledWriteFile(root, journal, t.path, t.text);
338
- note(`wrote ${t.file}`);
339
- }
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}`);
340
342
  await commitJournal(root, 'sync', journal);
341
343
  // The regions moved, so the last `check` no longer describes this tree.
342
344
  // Invalidated rather than refreshed: `sync` does not run gates, checkers
@@ -409,7 +411,7 @@ export async function syncCommand(positional, flags) {
409
411
  : `would write ${changed.length + pointersAdded.length + (manifestChanged ? 1 : 0)} file(s); apply with: ${CLI} sync --yes`,
410
412
  );
411
413
  }
412
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
414
+ emit(lines.join('\n'));
413
415
  // After the report: the report says what, the diff shows it (R50).
414
416
  emitDiffs(diffs, flags);
415
417
  }
@@ -544,8 +546,9 @@ async function gateCwdFindings(root, manifest) {
544
546
  // reports 77. The command below renders these facts for terminals; the tool
545
547
  // returns them as JSON. The CLI keeps its field names; the tool adopts them.
546
548
  export async function checkRepo(root, opts = {}) {
547
- const findings = await checkTargets(root, opts);
548
- const rules = await loadRules(root);
549
+ const ctx = opts.ctx ?? repoContext(root);
550
+ const findings = await checkTargets(root, { ...opts, ctx });
551
+ const rules = await ctx.rules();
549
552
 
550
553
  // A rule file with no id would be projected without a traceable source, so it
551
554
  // is a gate failure rather than a warning: `rules why` could not resolve it.
@@ -568,7 +571,7 @@ export async function checkRepo(root, opts = {}) {
568
571
  // A declared gate this repo cannot run fails closed on every commit once hooks are
569
572
  // installed. Reported here rather than left to `hooks status`, so the repo is red
570
573
  // before anyone installs anything rather than after.
571
- const { manifest, exists } = await readManifest(root);
574
+ const { manifest, exists } = await ctx.manifest();
572
575
  // The gates slot, counted — not just reported — so the line can print run/total.
573
576
  // A cwd finding names a declared gate that will not run, so it counts with the
574
577
  // unrunnable; an unknown-gate-cwd names no declared gate, so it does not.
@@ -603,14 +606,14 @@ export async function checkRepo(root, opts = {}) {
603
606
  // hand either: the next `init` puts simt-dev's own bytes back and the formatter
604
607
  // takes them away again. Reporting it here is what stops that being a surprise
605
608
  // discovered at upgrade time.
606
- const { stack } = await fingerprint(root);
607
- const drift = await manifestDrift(root, stack, await provenanceDigest());
609
+ const { stack } = await ctx.fingerprint();
610
+ const drift = await manifestDrift(root, stack, await ctx.digest());
608
611
  if (drift) findings.push(drift);
609
612
  }
610
613
  if (opts.frozen) findings.push(...(await frozenFindings(root, opts)));
611
614
  // The checkers a selected rule declared (ADR-088). They read the rule source,
612
615
  // not the folded region, so a line that left the prompt is still the line that runs.
613
- const fp = await fingerprint(root);
616
+ const fp = await ctx.fingerprint();
614
617
  const { selected } = select(rules.filter((r) => r.error === null), fp.stack ?? [], fp.facts ?? []);
615
618
  findings.push(...(await runDeclaredChecks(root, selected, manifest ?? null)));
616
619
 
@@ -652,7 +655,7 @@ export async function checkRepo(root, opts = {}) {
652
655
  }
653
656
 
654
657
  export async function checkCommand(positional, flags) {
655
- const root = flags.root ?? positional[0] ?? process.cwd();
658
+ const root = commandRoot(flags, positional);
656
659
  const { ok, findings } = await checkRepo(root, flags);
657
660
  const payload = {
658
661
  ok,
@@ -678,7 +681,6 @@ export async function checkCommand(positional, flags) {
678
681
  return f.remedy ? [head, `${' '.repeat(11)}remedy: ${f.remedy}`] : [head];
679
682
  })
680
683
  .join('\n'),
681
- { maxBytes: flags.maxBytes ?? 0 },
682
684
  );
683
685
  return ok ? EXIT.OK : EXIT.FAIL;
684
686
  }
@@ -5,18 +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
- import { EXIT, emit, emitJson, fail, isJson } from '../output/index.mjs';
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';
18
-
19
- const exec = promisify(execFile);
15
+ import { coverage } from '../index/coverage.mjs';
16
+ import { HISTORY, importsOf, parseCoverage, testEdges } from '../index/tests.mjs';
17
+ import { commandRoot } from '../repo-root.mjs';
18
+ import { git } from '../sys/git.mjs';
20
19
 
21
20
  const COVERAGE_PATHS = [
22
21
  'coverage/lcov.info',
@@ -31,11 +30,11 @@ const COVERAGE_PATHS = [
31
30
 
32
31
  export async function testsCommand(args, flags) {
33
32
  if (args.length > 1) return fail(`unknown tests argument ${args[1]}`, EXIT.USAGE, `run: ${CLI} tests [path]`);
34
- const root = flags.root ?? process.cwd();
33
+ const root = commandRoot(flags);
35
34
  const loaded = await loadFiles(root);
36
35
  if (loaded.error) return loaded.error;
37
36
  const { files } = loaded;
38
- const [stored, texts, coverage, commits] = await Promise.all([
37
+ const [stored, texts, reports, commits] = await Promise.all([
39
38
  readEdges(root),
40
39
  testBlobs(root, files),
41
40
  coverageTexts(root),
@@ -49,20 +48,23 @@ export async function testsCommand(args, flags) {
49
48
  if (!text) continue;
50
49
  imports.push(...importsOf(file.path, text, langOf(file.path), paths));
51
50
  }
52
- const edges = testEdges({ files, imports, coverage, commits });
51
+ const edges = testEdges({ files, imports, coverage: reports, commits });
53
52
  const target = args[0];
54
53
  if (!target) return summarise(root, flags, edges);
55
54
  if (!paths.has(target)) {
56
55
  return fail(`not in the index: ${target}`, EXIT.USAGE, `pass a path ${CLI} index knows`);
57
56
  }
58
- 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
+ });
59
61
  const payload = { ok: true, command: 'tests', root, path: target, tests };
60
62
  if (isJson(flags)) {
61
- emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
63
+ emitJson(payload);
62
64
  return EXIT.OK;
63
65
  }
64
66
  const lines = [`tests ${tests.length}`, ...tests];
65
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
67
+ emit(lines.join('\n'));
66
68
  return EXIT.OK;
67
69
  }
68
70
 
@@ -74,11 +76,7 @@ function summarise(root, flags, edges) {
74
76
  else if (edge.kind === 'cochange') cochange += 1;
75
77
  }
76
78
  const payload = { ok: true, command: 'tests', root, test, cochange };
77
- if (isJson(flags)) {
78
- emitJson(payload, { pretty: flags.pretty === true, maxBytes: flags.maxBytes ?? 0 });
79
- return EXIT.OK;
80
- }
81
- emit(`test ${test}\ncochange ${cochange}`, { maxBytes: flags.maxBytes ?? 0 });
79
+ present(payload, () => `test ${test}\ncochange ${cochange}`, flags);
82
80
  return EXIT.OK;
83
81
  }
84
82
 
@@ -128,10 +126,9 @@ async function coverageTexts(root) {
128
126
  async function history(root) {
129
127
  let stdout = '';
130
128
  try {
131
- ({ stdout } = await exec('git', ['log', '-n', String(HISTORY), '--no-merges', '--name-only', '--pretty=format:%x1e'], {
132
- cwd: root,
129
+ stdout = await git(root, ['log', '-n', String(HISTORY), '--no-merges', '--name-only', '--pretty=format:%x1e'], {
133
130
  maxBuffer: 16 << 20,
134
- }));
131
+ });
135
132
  } catch {
136
133
  return [];
137
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,12 +63,12 @@ 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
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
71
+ emit(lines.join('\n'));
77
72
  return EXIT.OK;
78
73
  }
79
74
 
@@ -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,31 +163,31 @@ 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
- emit(lines.join('\n'), { maxBytes: flags.maxBytes ?? 0 });
191
+ emit(lines.join('\n'));
197
192
  return EXIT.OK;
198
193
  }