simt-dev 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +158 -872
  2. package/README.md +19 -133
  3. package/bin/simt.mjs +19 -55
  4. package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
  5. package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
  6. package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
  7. package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
  8. package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
  9. package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
  10. package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
  11. package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
  12. package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
  13. package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
  14. package/package.json +10 -2
  15. package/rules/core.md +7 -0
  16. package/rules/mdc/slice-commit.mdc +57 -4
  17. package/src/agenthooks/prompt.mjs +5 -2
  18. package/src/agenthooks/shim.mjs +1 -1
  19. package/src/api.mjs +29 -0
  20. package/src/argv.mjs +29 -3
  21. package/src/bench/adapters.json +16 -0
  22. package/src/bench/corpus.mjs +150 -0
  23. package/src/bench/grade.mjs +23 -49
  24. package/src/bench/mine.mjs +199 -35
  25. package/src/bench/probes.mjs +99 -62
  26. package/src/bench/process.mjs +128 -0
  27. package/src/bench/report.mjs +17 -4
  28. package/src/bench/run.mjs +242 -142
  29. package/src/brief/assemble.mjs +38 -3
  30. package/src/ci/workflow.mjs +30 -7
  31. package/src/cli.mjs +45 -0
  32. package/src/commands/agent.mjs +3 -4
  33. package/src/commands/agenthook.mjs +4 -5
  34. package/src/commands/bench.mjs +190 -17
  35. package/src/commands/brief.mjs +5 -4
  36. package/src/commands/ci.mjs +2 -1
  37. package/src/commands/compare.mjs +42 -42
  38. package/src/commands/completions.mjs +2 -6
  39. package/src/commands/decide.mjs +4 -4
  40. package/src/commands/distill.mjs +4 -3
  41. package/src/commands/doctor.mjs +11 -7
  42. package/src/commands/evidence.mjs +6 -9
  43. package/src/commands/explain.mjs +0 -1
  44. package/src/commands/gate.mjs +16 -12
  45. package/src/commands/graph.mjs +2 -3
  46. package/src/commands/harness.mjs +3 -2
  47. package/src/commands/help.mjs +125 -17
  48. package/src/commands/hooks.mjs +100 -30
  49. package/src/commands/ignore.mjs +2 -1
  50. package/src/commands/impact.mjs +4 -7
  51. package/src/commands/index.mjs +273 -188
  52. package/src/commands/init.mjs +2 -1
  53. package/src/commands/lease.mjs +6 -7
  54. package/src/commands/lessons.mjs +10 -9
  55. package/src/commands/local.mjs +3 -3
  56. package/src/commands/map.mjs +14 -9
  57. package/src/commands/mcp.mjs +0 -1
  58. package/src/commands/pack.mjs +9 -6
  59. package/src/commands/plan.mjs +6 -5
  60. package/src/commands/plugin.mjs +2 -2
  61. package/src/commands/prbody.mjs +3 -3
  62. package/src/commands/repoindex.mjs +4 -3
  63. package/src/commands/review.mjs +2 -2
  64. package/src/commands/rules.mjs +33 -16
  65. package/src/commands/shared.mjs +2 -3
  66. package/src/commands/skills.mjs +28 -26
  67. package/src/commands/skillscompile.mjs +4 -5
  68. package/src/commands/skillsinstall.mjs +2 -2
  69. package/src/commands/slice.mjs +21 -10
  70. package/src/commands/spec.mjs +151 -0
  71. package/src/commands/status.mjs +4 -7
  72. package/src/commands/sync.mjs +13 -11
  73. package/src/commands/tests.mjs +6 -9
  74. package/src/commands/tokens.mjs +2 -2
  75. package/src/commands/undo.mjs +5 -15
  76. package/src/commands/upgrade.mjs +7 -3
  77. package/src/commands/why.mjs +9 -8
  78. package/src/compare/row.mjs +0 -2
  79. package/src/context.mjs +64 -0
  80. package/src/decisions/index-doc.mjs +46 -0
  81. package/src/diff.mjs +1 -1
  82. package/src/distill/distill.mjs +14 -1
  83. package/src/distill/engine.mjs +5 -1
  84. package/src/graph/load.mjs +4 -1
  85. package/src/hooks/capability.mjs +3 -3
  86. package/src/hooks/dispatcher.mjs +198 -43
  87. package/src/hooks/gates.mjs +0 -43
  88. package/src/hooks/resolve.mjs +2 -7
  89. package/src/hooks/shim.mjs +5 -2
  90. package/src/identity.mjs +29 -1
  91. package/src/journal.mjs +78 -9
  92. package/src/ledger/index.mjs +30 -1
  93. package/src/local/client.mjs +4 -6
  94. package/src/locate.mjs +33 -9
  95. package/src/manifest/schema.mjs +213 -0
  96. package/src/mcp/resources.mjs +7 -10
  97. package/src/mcp/server.mjs +70 -16
  98. package/src/mcp/tools.mjs +45 -7
  99. package/src/measure/session.mjs +3 -2
  100. package/src/node-version.mjs +37 -0
  101. package/src/output/index.mjs +111 -3
  102. package/src/packs/manage.mjs +17 -3
  103. package/src/packs.mjs +20 -2
  104. package/src/projection/lock.mjs +11 -1
  105. package/src/projection/manifest.mjs +20 -34
  106. package/src/projection/rules.mjs +4 -3
  107. package/src/repo-root.mjs +19 -0
  108. package/src/resolve/fingerprint.mjs +19 -15
  109. package/src/review/critic.mjs +2 -1
  110. package/src/toolwords.mjs +3 -0
  111. package/src/workspace.mjs +55 -10
  112. package/docs/decisions/README.md +0 -600
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # simt-dev
2
2
 
3
- ![simt-dev, the intelligence amplifier for coding agents, and its four claims. One: say less, always — a kernel under 2 KB whose bytes stay stable so the prompt cache survives. Two: answers over exploration — a local code graph and a short task brief. Three: enforce, don't instruct — dual gates, before a tool call and at git time. Four: close the loop — benches and memory. Beneath them, the harnesses it works across and its three channels.](https://raw.githubusercontent.com/QuazarMedia/simt-dev/main/res/59e21e6d-6bf2-4d7d-b6ea-c7349a697d11.jpg)
3
+ ![simt-dev, the intelligence amplifier for coding agents, and its four claims. One: say less, always — a kernel under 2 KB whose bytes stay stable so the prompt cache survives. Two: answers over exploration — a local code graph and a short task brief. Three: enforce, don't instruct — dual gates, before a tool call and at git time. Four: close the loop — benches and memory. Beneath them, the harnesses it works across and its three channels.](https://raw.githubusercontent.com/QuazarMedia/simt-dev/main/res/hero.jpg)
4
+
5
+ [![simt-dev own gate](https://github.com/QuazarMedia/simt-dev/actions/workflows/simt-dev-check.yml/badge.svg)](https://github.com/QuazarMedia/simt-dev/actions/workflows/simt-dev-check.yml)
4
6
 
5
7
  `simt` adopts a repository for the agents that work in it, and builds an intelligence
6
8
  amplifier around them: a brief that answers before the agent explores, gates that
@@ -72,13 +74,20 @@ beside its `.sha256`. Pin the release you download,
72
74
  and run this from the repository's top level:
73
75
 
74
76
  ```sh
75
- mkdir -p .simt/bin && os=$(uname -s | tr A-Z a-z) && arch=$(uname -m | sed "s/x86_64/x64/") && curl -fsSL "https://github.com/QuazarMedia/simt-dev/releases/download/v<version>/simt-dev-<version>-$os-$arch" -o .simt/bin/simt && chmod +x .simt/bin/simt
77
+ version=<version>; os=$(uname -s | tr A-Z a-z); arch=$(uname -m | sed "s/x86_64/x64/")
78
+ base="https://github.com/QuazarMedia/simt-dev/releases/download/v$version"
79
+ asset="simt-dev-$version-$os-$arch"; tmp=$(mktemp -d)
80
+ curl -fsSL "$base/$asset" -o "$tmp/$asset"
81
+ curl -fsSL "$base/$asset.sha256" -o "$tmp/$asset.sha256"
82
+ (cd "$tmp" && shasum -a 256 -c "$asset.sha256")
83
+ mkdir -p .simt/bin && mv "$tmp/$asset" .simt/bin/simt && chmod +x .simt/bin/simt
76
84
  ```
77
85
 
78
86
  The locator checks `.simt/bin/simt` before `PATH`, so the repository's own copy is the
79
87
  one its hooks and its CI run — this is the same command the hook shim prints as its
80
- remedy. The generated CI workflow downloads the same asset and verifies it against the
81
- published `.sha256` before running anything.
88
+ remedy. Both the snippet above and the generated CI workflow verify the asset against the
89
+ published `.sha256` before it runs: a digest that does not match stops the copy, so a
90
+ truncated or substituted download never becomes the command.
82
91
 
83
92
  ## What `simt` does
84
93
 
@@ -185,134 +194,11 @@ line warns past 400 lines and fails past 1000. `simt evidence check` warns when
185
194
  message claims a passing run this tree does not have, and the commit fails when
186
195
  `gates.evidence` is `block`. Every finding `check` can emit is explained by `simt explain <CODE>` (DRIFT, UNMANAGED, PACK-DIGEST-MISMATCH, ...).
187
196
 
188
- ## Agent playbook: how and when to use each module
189
-
190
- An agent working in an adopted repository should use `simt` as its primary orientation,
191
- retrieval, validation and memory layer rather than scanning the tree by hand. Every
192
- read-only command supports `--json` (and `--max-bytes <n>`) or can be called over stdio
193
- through `simt mcp`.
194
-
195
- ### Decision tree: which command to run when
196
-
197
- | Situation | First command | If you need more |
198
- | --- | --- | --- |
199
- | New task, no context yet | `simt brief "<task>"` | `simt map --task "<task>"` for broader file ranking |
200
- | About to edit a file | `simt why <file>` | `simt impact <file>` for blast radius |
201
- | Need to know what tests to run | `simt tests <file>` | `simt gate --changed` to run them |
202
- | Rule violation or error | `simt lessons list` | `simt rules why <rule-id>` for the decision |
203
- | Working from a plan | `simt plan next` | `simt plan done <id> <sha> --yes` when finished |
204
- | Before committing | `simt check` | `simt evidence` to verify test claims |
205
- | Opening a pull request | `simt pr-body` | `--base <ref>` to diff against another fork point |
206
- | Repository adoption | `simt init --yes` | `simt doctor` to verify stack detection |
207
- | Multi-repo workspace | `simt compare` | `simt shared plan` to find deduplication opportunities |
208
- | Local model available | `simt local doctor` | `simt index --embed` to build vector embeddings |
209
-
210
- ### 1. Start of task — orient in one call instead of broad exploration
211
-
212
- 1. **Assemble the task packet first.** Run `simt brief "<task>"` (or call MCP `brief`, or
213
- read the packet injected automatically when `agent-hooks install --yes` wired
214
- `prompt-submit`). Within `--budget 2048` bytes (pass `--budget 0` for unlimited or
215
- `--explain` to see why items were kept or cut), the packet gives:
216
- - `TASK`: the classified task kind (`bugfix`, `feature`, `refactor`, `test`, `docs`,
217
- `config`) and the evidence behind it.
218
- - `COMMANDS`: the exact `build`, `test`, `lint`, `format`, `typecheck`, and focused
219
- test templates for this repository.
220
- - `LIKELY FILES`: top files ranked by reciprocal-rank fusion (`RRF_K = 40`) across
221
- SQLite FTS5 `bm25()`, PageRank graph proximity, and resolver signals.
222
- - `TESTS`: the test files mapped to those likely files.
223
- - `DECISIONS`, `SKILLS`, `LESSONS`, `CAUTIONS`, and `FEDERATION`: governing ADRs,
224
- matched skills, promoted lessons for those paths, active worktree/hook cautions, and
225
- any structural answer from a declared external code-graph server.
226
- 2. **Pull the next tracker slice when working from a plan.** Instead of reading a multi-thousand-line
227
- plan in `docs/plans/` or `docs/reviews/`, run `simt plan next` (or MCP `plan_next`) to
228
- load only the next queued slice (~1 KB) whose dependencies are already `Complete` or
229
- `Accepted`.
230
-
231
- **When to use `--json` format.** When piping output to other tools or when the agent needs
232
- structured data, add `--json` to any read-only command. For example, `simt brief "<task>" --json`
233
- returns a JSON object with `task`, `commands`, `files`, `tests`, `decisions`, `skills`,
234
- `lessons`, `cautions`, and `federation` fields that can be programmatically consumed.
235
-
236
- ### 2. Before editing — query the graph, blast radius, and governance
237
-
238
- 1. **Rank files or symbols at the right granularity.** When `brief`'s file list is too
239
- narrow or the task evolves, run `simt map --task "<task>" --level outline` for paths,
240
- `--level names` for exported symbols, or `--level signatures` for declaration lines.
241
- Pass `--embed` when `local.embed` is configured in `simt.json` and clears the +10-point
242
- recall@10 bar.
243
- 2. **Check blast radius before changing a shared file or export.** Run
244
- `simt impact <file|symbol>` to see every direct and transitive importer, the tests
245
- covering them, the governing rules and ADRs, and any sibling repository in the
246
- workspace that imports that symbol.
247
- 3. **Read the governance on a path before changing its behaviour.** Run `simt why <path>`
248
- to list every ADR whose `simt-affects` matches the file, every scoped rule, skill,
249
- gate, and promoted lesson. Run `simt why` with no path to audit the repository for
250
- `UNRESOLVED-AFFECTS` tokens.
251
- 4. **Resolve deferred rules and citations on demand.** When `simt.json` sets
252
- `"mode": "kernel"`, only `hard` and `command` rules are loaded at session start;
253
- `convention` and `style` rules are listed by ID at the foot of the projection. Fetch a
254
- deferred rule with `simt rules show <id>`, and read any cited `ADR-NNN` with
255
- `simt rules why ADR-NNN` (or MCP `rules_why`) to see its rulings and whether any ruling
256
- was superseded or narrowed.
257
- 5. **Load skills progressively (Tier 0 → Tier 1 → Tier 2).** Never read every skill file
258
- upfront:
259
- - Tier 0: `simt skills index` (or MCP `skills_index`) lists one line per skill.
260
- - Tier 1: `simt skills brief --task "<task>"` (or MCP `skills_brief`) ranks compatible
261
- skills within budget (`--plan` emits a fetch plan without bodies).
262
- - Tier 2: `simt skills show <name> --max-bytes 8192` (or MCP `skills_show`) loads only
263
- the chosen skill's body.
264
-
265
- **Optimal retrieval strategy.** For best performance, use this sequence:
266
- 1. `simt brief "<task>"` — gets the most relevant files and tests in one call
267
- 2. If more files are needed, `simt map --task "<task>" --level outline` — broader ranking
268
- 3. If you need to understand a specific file's impact, `simt impact <file>` — blast radius
269
- 4. If you need to understand governance, `simt why <file>` — ADRs and rules
270
-
271
- ### 3. During implementation — validate incrementally under dual gates
272
-
273
- 1. **Keep the local index current.** After adding, renaming, or changing exports/imports,
274
- run `simt index`. It hashes tracked blobs via `git ls-files --stage` and re-extracts
275
- only changed files in under 300 ms.
276
- 2. **Run only the tests your change reaches.** Use `simt tests <path>` to inspect which
277
- tests cover a file (via import edges, naming conventions, or coverage records; add
278
- `--cochange` to include historical co-change pairs). Run `simt gate --changed` (or
279
- `simt gate <path...>`) to execute only the tests reachable from the diff, with gate
280
- output compacted so every failure line survives while passing noise is trimmed.
281
- 3. **Respect agent-time hooks and learn from failures.** With `simt agent-hooks install --yes`,
282
- `pre-tool-use` blocks unapproved `git push`, history rewrites (`--no-verify`,
283
- `--amend`, `reset --hard`, `rebase`), and direct edits to generated regions before the
284
- tool executes. When a test or gate fails, `post-tool-use` hashes the normalised first
285
- error line against `docs/lessons/*.md` and injects the matching lesson into context.
286
-
287
- **Incremental validation workflow.** After each meaningful code change:
288
- 1. Run `simt tests <changed-file>` to see which tests cover your changes
289
- 2. Run `simt gate --changed` to execute only the affected tests
290
- 3. If a test fails, check `simt lessons list` for known issues and their remedies
291
- 4. If the failure is new, fix it and consider promoting the lesson with `simt lessons review <id> --yes`
292
-
293
- ### 4. Before committing and closing a slice — verify drift, evidence, and memory
294
-
295
- 1. **Run the repository gate.** Run `simt check` (or MCP `check`). Fix any reported
296
- `DRIFT` with `simt sync --yes`, and resolve any rule checker or manifest findings.
297
- 2. **Verify execution evidence matches the current tree.** Run `simt evidence` to confirm
298
- that the tests and gates you plan to cite were recorded against the current
299
- `git write-tree` digest in `.simt/ledger.jsonl`. Any file edit invalidates earlier
300
- evidence.
301
- 3. **Record decisions, lessons, and tracker state:**
302
- - If the change alters what the repository or tool believes, scaffold an ADR with
303
- `simt decide "<title>" [--supersedes ADR-NNN[:<ruling>]] --yes`.
304
- - If a failing gate or test was fixed during the session, inspect captured candidates
305
- with `simt lessons list` and promote reusable ones with
306
- `simt lessons review <id> --yes`.
307
- - After committing locally (never push without explicit approval per `ADR-001`), close
308
- the tracker slice with `simt plan done <id> <sha> --test "<cmd>" --yes`.
309
-
310
- **Pre-commit checklist.** Before committing, run this sequence:
311
- 1. `simt check` — verify no drift and all rules pass
312
- 2. `simt evidence` — confirm test runs are recorded against current tree
313
- 3. `simt lessons list` — check for any unreviewed lesson candidates
314
- 4. `git commit` — commit locally (never push without explicit approval)
315
- 5. `simt plan done <id> <sha> --yes` — close the tracker slice if working from a plan
197
+ ## Agent playbook
198
+
199
+ The full agent playbook — the decision tree for which command to run when, and the
200
+ workflow for orienting, editing, implementing and closing a slice — lives in
201
+ [PLAYBOOK.md](PLAYBOOK.md).
316
202
 
317
203
  ## Optimal setup for every project, repository, and workspace shape
318
204
 
@@ -372,7 +258,7 @@ history. All remain off until named and propose rather than write projected text
372
258
  | `simt.lock.json` | the digest of the inputs, the digest of the projection, and locked `variants` winners from `simt bench variants`. The old `simt-dev.lock.json` is still read, and a lock migrates on its next write |
373
259
  | `.cursor/rules/*.mdc`, `.claude/rules/*.md`, `.agents/rules/*.md`, `.github/instructions/*.instructions.md` | one file per rule, and only for a harness that reads that directory |
374
260
  | `.editorconfig`, `.gitattributes` | baseline regions, placed first so a later setting still wins |
375
- | `.simt-hooks/` and `core.hooksPath` | the shared dispatcher. It fails closed |
261
+ | `.simt/hooks/` and `core.hooksPath` | the shared dispatcher. It fails closed |
376
262
  | `docs/decisions/ADR-NNN-<slug>.md` | numbered decision records scaffolded by `simt decide --yes` with `simt-affects` and `simt-supersedes` frontmatter |
377
263
  | `docs/lessons/NNNN-<slug>.md` | reviewed lessons promoted by `simt lessons review <id> --yes` with `simt-trigger`, `simt-evidence`, and `simt-expires` frontmatter |
378
264
 
package/bin/simt.mjs CHANGED
@@ -1,60 +1,24 @@
1
1
  #!/usr/bin/env node
2
- // simt-dev - developer and agent tooling for the SIMT ecosystem.
2
+ // The entry point, and the one place the runtime floor is enforced.
3
3
  //
4
- // POSIX only (ADR-012). package.json declares os: [darwin, linux], so npm refuses
5
- // the install elsewhere rather than letting a hook fail obscurely.
6
-
7
- import { parse, rawJsonFlag } from '../src/argv.mjs';
8
- import { EXIT, fail, jsonFailMode, jsonIndentDefault } from '../src/output/index.mjs';
9
-
10
- try {
11
- const argv = process.argv.slice(2);
12
- const parsed = parse(argv);
13
- if (!parsed.ok) {
14
- // The flags do not exist yet, so the JSON mode is read off the raw words:
15
- // a parse failure under `--json` is a JSON document on stdout, not text on
16
- // stderr with an empty stdout (Q11, ADR-070 ruling 4).
17
- jsonFailMode(rawJsonFlag(argv));
18
- process.exit(fail(parsed.error.message, parsed.error.code));
19
- }
20
- const { flags, positional } = parsed.value;
21
-
22
- // Set before anything can fail: `no command given` under `--json` is a JSON
23
- // document too, and a failure that prints as text with an empty stdout breaks
24
- // the same ruling the parse failure above honours (Q11, ADR-070 ruling 4).
25
- jsonIndentDefault(flags.pretty === true);
26
- jsonFailMode(flags.json === true);
27
-
28
- // No arguments at all is a usage error, not a silent help dump: an agent that
29
- // ran the wrong thing should be told so rather than handed 40 lines of prose.
30
- if (positional.length === 0 && !flags.help && !flags.version) {
31
- process.exit(fail('no command given', EXIT.USAGE));
32
- }
4
+ // `package.json` declares `engines.node >= 22.20.0`, and npm only warns: an older
5
+ // runtime used to reach the CLI and fail somewhere inside the module graph — a syntax
6
+ // error or a missing API — instead of the one sentence that says what to install
7
+ // (review 0003 Q54). Nothing but this tiny module is imported before the check, so the
8
+ // message is what a reader gets; the CLI is imported only once the floor holds.
9
+ //
10
+ // 5 is EXIT.UNSUPPORTED. `output/index.mjs` is the CLI graph this file exists to avoid
11
+ // loading before the check, so the literal is repeated here and asserted by
12
+ // `test/node-version.test.mjs`.
33
13
 
34
- // The one command that runs on somebody else's hot path, and therefore the one that
35
- // does not load the registry. `agent-hook` is called before every tool call an agent
36
- // makes, so its latency is a product requirement rather than a nicety (plan 0003
37
- // R15), and the registry's imports - detection, projection, the bench corpus - cost
38
- // tens of milliseconds before the command itself starts. Measured on the machine
39
- // this was written on: 127.7 ms through the registry against 91.5 ms with only this
40
- // module's imports, where the bare runtime is 86.9 ms of it.
41
- //
42
- // Only the exact spelling the renderers write takes this path. Anything else -
43
- // including `agent-hook --help` - loads the registry, so help stays the one list of
44
- // every command and this shortcut cannot become a second, thinner command surface.
45
- if (positional[0] === 'agent-hook' && !flags.help && !flags.version) {
46
- const { agentHookCommand } = await import('../src/commands/agenthook.mjs');
47
- process.exit((await agentHookCommand(positional.slice(1), flags)) ?? EXIT.OK);
48
- }
14
+ import { MIN_NODE, satisfiesMinimum } from '../src/node-version.mjs';
49
15
 
50
- const { dispatch } = await import('../src/commands/index.mjs');
51
- const code = (await dispatch(argv)) ?? EXIT.OK;
52
- // Assigned, not forced: a large document on stdout (a `--json --diff` over a
53
- // skill-sized body exceeds the 64 KiB pipe buffer) is still draining when the
54
- // command resolves, and `process.exit()` cuts it — exit 0 with half a JSON
55
- // document on stdout. Letting the loop empty first makes the exit honest
56
- // (plan 0003 R50; the S10 contract asserts the document parses).
57
- process.exitCode = code;
58
- } catch (error) {
59
- process.exit(fail(error?.message ?? String(error), EXIT.FAIL));
16
+ if (!satisfiesMinimum(process.versions.node)) {
17
+ process.stderr.write(
18
+ `ERROR 5 simt-dev needs Node >= ${MIN_NODE}; this is ${process.versions.node}\n` +
19
+ `REMEDY install Node ${MIN_NODE} or newer, then rerun: simt --version\n`,
20
+ );
21
+ process.exit(5);
60
22
  }
23
+
24
+ await import('../src/cli.mjs');
@@ -1,6 +1,6 @@
1
1
  # ADR-014 — The workspace is configuration, and a single repo is a first-class case
2
2
 
3
- Status: **accepted** · 2026-10-05
3
+ Status: **accepted** · 2026-10-05 · ruling 1 superseded by [ADR-119](ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md)
4
4
  Affects: `simt-dev` CLI (workspace resolution, `compare`, `skills`, registry validation, packaging)
5
5
 
6
6
  ## Context
@@ -5,7 +5,7 @@ simt-supersedes: [{ adr: "ADR-098", ruling: 3, kind: "narrowed" }]
5
5
 
6
6
  # ADR-099 — the release verifies served bytes, and the formula mirrors them
7
7
 
8
- Status: **accepted** · 2026-10-27 · narrows ADR-098 ruling 3
8
+ Status: **accepted** · 2026-10-27 · narrows ADR-098 ruling 3 · ruling 1 superseded by [ADR-117](ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md)
9
9
 
10
10
  Affects: `.github/workflows/publish.yml`, `scripts/build-sea.mjs`,
11
11
  `scripts/verify-release-asset.mjs`, `Formula/simt-dev.rb`
@@ -0,0 +1,81 @@
1
+ ---
2
+ simt-affects: [".github/workflows/publish.yml", "test/binary.test.mjs"]
3
+ simt-supersedes: [{ adr: "ADR-099", ruling: 1, kind: "superseded" }]
4
+ ---
5
+
6
+ # ADR-117 — the darwin release builds on supported macOS 15 runners, and a re-run is idempotent
7
+
8
+ Status: **accepted** · 2026-10-31 · supersedes ADR-099 ruling 1
9
+
10
+ ## Context
11
+
12
+ The v0.7.0 release run `41215349142` reached the first binary release: the npm stage
13
+ succeeded, the release record was created, and `linux-x64` built. Both darwin jobs failed
14
+ in the same place — `macos-14`, the runner ADR-099 ruling 1 named, is deprecated, and
15
+ GitHub was running scheduled brownouts of that image ahead of its retirement.
16
+
17
+ The old matrix also used one label for two architectures. GitHub's runner-images table
18
+ now names `macos-14` as arm64 and `macos-14-large` as Intel, so "both targets on
19
+ `macos-14`" was not the native-runner arrangement ADR-099 claimed. The replacement has
20
+ to name the arm64 and Intel images explicitly.
21
+
22
+ The same run exposed a second release-path defect. `publish-npm` decided whether to skip
23
+ by asking the registry whether the version was published. A staged version is not
24
+ published, so re-running the workflow after every other job succeeded would try to stage
25
+ the same version again and fail on the duplicate. "Re-run all jobs" is the normal way to
26
+ recover a partially failed release, and it must not turn a completed stage into a red
27
+ job.
28
+
29
+ ## Ruling
30
+
31
+ 1. **Darwin targets build on the supported native macOS 15 images.**
32
+ `darwin-arm64` runs on `macos-15` and `darwin-x64` runs on `macos-15-intel`;
33
+ `linux-x64` stays on `ubuntu-latest`. These are the standard, non-deprecated labels
34
+ for the two architectures, not the larger-runner variants. ADR-099's per-host
35
+ determinism and upload-integrity claims are unchanged; only the label that provides
36
+ the native host moves.
37
+
38
+ 2. **A re-run is idempotent for a stage that already succeeded.**
39
+ `publish-npm` checks the npm staging queue as well as the registry. A version that is
40
+ published *or* already staged sets `SKIP=true`, so the tests, the wheel check and
41
+ `npm stage publish` are not repeated. The staging check runs after npm 11 is
42
+ available, because `npm stage list` is the command that makes the queue visible. A
43
+ failed stage list is not treated as proof of absence: the job attempts the stage and
44
+ fails loudly rather than silently skipping a release that does not exist.
45
+
46
+ The release record, asset upload and formula mirror were already idempotent —
47
+ `create-release` edits an existing release, `gh release upload --clobber` replaces
48
+ assets, and `sync-formula` exits when the mirror already names the served bytes.
49
+ They stay that way. A re-run does not skip a build on the grounds that an asset with
50
+ the same name exists: the tag can have moved, and the asset must be built from the
51
+ tree the tag now names.
52
+
53
+ ## Consequences
54
+
55
+ - A darwin failure is recovered on a runner GitHub supports, and the arm64 and Intel
56
+ assets are produced on the architectures they claim.
57
+ - "Re-run all jobs" after a successful stage is green: the stage job says the version is
58
+ already staged, the release record is refreshed in place, and the formula is compared
59
+ rather than overwritten.
60
+ - "Re-run failed jobs" remains the narrower recovery: it rebuilds only the targets that
61
+ failed, which is the path the v0.7.0 run needed.
62
+ - ADR-099 ruling 1 is superseded. Its ruling 2 (upload integrity) and ruling 3 (the
63
+ formula mirrors served bytes) still govern the release.
64
+ - `test/binary.test.mjs` reads the committed workflow and asserts the two darwin labels,
65
+ so a future edit that returns to a deprecated or wrong-architecture runner fails the
66
+ suite instead of a release.
67
+
68
+ ## Rejected
69
+
70
+ - **`macos-latest` for arm64.** It moves with GitHub's newest stable image. A release
71
+ builder should change when this repository decides to change it, not when a label
72
+ migrates.
73
+ - **`macos-26`.** It is newer than the release needs, and no run has proven the pinned
74
+ Node base and `codesign` pipeline on it. Moving to it is a separate reviewed change.
75
+ - **`macos-14-large`.** It is the Intel half of a deprecated image, so it inherits the
76
+ brownout that caused this failure.
77
+ - **Skipping a build because the asset name already exists.** The tag this workflow runs
78
+ for can be moved, and an asset left from an earlier tree would then be served as the
79
+ new tag's binary. Rebuilding and clobbering keeps the bytes tied to the tagged tree.
80
+ - **Detecting a prior stage through the registry alone.** A stage is deliberately not
81
+ public, so the registry cannot answer whether it exists.
@@ -0,0 +1,91 @@
1
+ ---
2
+ simt-affects: ["src/bench/mine.mjs", "src/bench/corpus.mjs", "src/bench/grade.mjs", "src/argv.mjs", "src/commands/bench.mjs", "src/commands/index.mjs", "src/commands/help.mjs", "test/bench.test.mjs"]
3
+ simt-supersedes: []
4
+ ---
5
+
6
+ # ADR-118 — the bench corpus is keyed on history, is append-only, and is portable as a sealed document
7
+
8
+ Status: **accepted** · 2026-10-31 · filed as review 0003 Q48
9
+
10
+ ## Context
11
+
12
+ Review 0003 (Q48) verified four failures in the mined bench corpus:
13
+
14
+ - The vault was keyed on the absolute checkout path, so the 20 committed tasks
15
+ failed from every clone (`ENOENT …/vault/repo-ca1b5891/…/gold.patch`): a corpus
16
+ was unrunnable from any machine that was not the one that mined it.
17
+ - Re-mining silently replaced the corpus: `mineTasks(target 3)` deleted all 20
18
+ committed tasks and wrote 3 others, with no signal that anything was lost.
19
+ - The Gradle grader read one report file and the Maven grader read none, while
20
+ both runners write one JUnit XML *per test class* under a directory: both
21
+ graders graded on an empty parse.
22
+ - Code-read: 7-character ids with a fixed `-01` suffix can collide, and the
23
+ exclusion list and the per-test timeout were hardcoded, so a repository could
24
+ not declare its own.
25
+
26
+ ## Ruling
27
+
28
+ 1. **The vault is keyed on the repository's history, not its checkout path.**
29
+ The key is the checkout basename plus the root commit
30
+ (`git rev-list --max-parents=0 HEAD`), read synchronously like
31
+ `package-version.mjs`: a vault path that throws is worse than one that is
32
+ occasionally less specific. The root commit is identical in every clone and
33
+ survives remote renames; a root with no git history degrades to the path
34
+ digest, because a vault path must never fail because git is absent.
35
+ 2. **Re-mining is append-only, and taking over the corpus is explicit.** Tasks
36
+ the current mine does not reproduce survive a re-mine; only `--replace`
37
+ deletes them. An id that already holds a task from a different
38
+ `sourceCommit` is a hard error naming `--replace`, not a silent overwrite —
39
+ guessing which corpus the reader wants is worse than refusing.
40
+ 3. **New task ids are 12 hex characters plus `-01`.** The 20 committed tasks
41
+ keep their 7-character ids: the corpus is not regenerated to widen them, and
42
+ the collision check — not a wider prefix — is what makes a collision an
43
+ error.
44
+ 4. **JUnit XML is read from the directory the runners write.**
45
+ `readReports(paths)` walks report directories recursively in sort order
46
+ (Gradle `build/test-results/test/`, Maven `target/surefire-reports/`), reads
47
+ single files, and skips missing paths. The miner and the grader clean
48
+ `reportPaths` before every run — a report left by a prior run would be
49
+ graded as this run's result — and read through `readReports`.
50
+ 5. **Exclusion and timeout are repository-declared.** `simt.json`
51
+ `bench.mine.{exclude,timeoutMs}`; an explicit option wins, then the manifest,
52
+ then the default (`['test/pack.test.mjs']`, 5000 ms).
53
+ 6. **The corpus is portable as a sealed document.** `bench export <corpus.json>`
54
+ and `bench import <corpus.json>` (in `src/bench/corpus.mjs`) carry the task
55
+ descriptors and both hidden patches for every task in a single file: tasks
56
+ sorted by id, canonical key-sorted JSON, base64 patches, no timestamp, and a
57
+ sha256 seal over everything but the seal. The seal is verified before a
58
+ single byte is trusted, and two exports of the same corpus are
59
+ byte-identical.
60
+ 7. **The mine records where the corpus came from.** `.simt/bench/corpus.json`
61
+ carries `{minedFrom, target, simtVersion}` — no timestamp, so a re-mine at
62
+ the same HEAD is byte-identical.
63
+
64
+ ## Consequences
65
+
66
+ - A `bench export` on the machine that mined a corpus and a `bench import` on
67
+ any other machine reproduce it exactly, hidden test patches included: the
68
+ committed corpus is runnable from a clone.
69
+ - 7-character and 12-character ids coexist in one corpus; the schema test
70
+ asserts the committed corpus keeps its 7-character ids and the miner asserts
71
+ 12 for every new mine.
72
+ - `bench import` and `bench export` preview like every other write: without
73
+ `--yes` nothing is written, and a dry-run import writes nothing.
74
+ - The sealed document lives in its own module (`src/bench/corpus.mjs`);
75
+ `mine.mjs` is trimmed to the 1000-line ceiling so the slice adds no new
76
+ file-lines finding.
77
+
78
+ ## Rejected
79
+
80
+ - **Keying the vault on the remote URL.** It breaks on a remote rename, and it
81
+ is a second mutable identity where the root commit is immutable and
82
+ clone-invariant.
83
+ - **Regenerating the committed corpus to 12-character ids.** It churns 20 task
84
+ files and their vault layout for a width only new mines need; the collision
85
+ check, not a wider prefix, is what protects the corpus.
86
+ - **A timestamp in the exported document.** It breaks byte-identity: two
87
+ exports of the same corpus differ, and a reader cannot tell content drift
88
+ from time drift.
89
+ - **Silent overwrite on re-mine.** The failure this ADR exists to fix: a corpus
90
+ is data, and a routine command deleting twenty tasks is a data loss wearing a
91
+ success exit code.
@@ -0,0 +1,90 @@
1
+ ---
2
+ simt-affects: ["src/workspace.mjs", "src/graph/load.mjs", "test/workspace.test.mjs", "simt.json"]
3
+ simt-supersedes: [{ adr: "ADR-014", ruling: 1, kind: "superseded" }]
4
+ ---
5
+
6
+ # ADR-119 — the environment variable points into a workspace the repo declared
7
+
8
+ Status: **accepted** · 2026-10-31 · supersedes ADR-014 ruling 1
9
+
10
+ ## Context
11
+
12
+ ADR-014 made the workspace configuration, resolved in a fixed, reported order:
13
+ `--workspace`, then `SIMT_DEV_WORKSPACE`, then the user config file, then the sibling
14
+ heuristic. Its ruling 1 named the variable's purpose as "CI and one-off invocations".
15
+
16
+ A variable that is *exported* is not a one-off invocation. Measured: a developer whose
17
+ shell exports `SIMT_DEV_WORKSPACE` for a multi-repo workspace, working in a single repo
18
+ that declared no workspace, gets that repo treated as a member of the exported workspace
19
+ — `repo/path` skill sources resolve against it, `compare` discovers its siblings, and
20
+ `doctor` reports `env` as the source. The default case ADR-014 ruling 3 exists to keep
21
+ first-class — a single repo with no workspace — silently depends on ambient environment.
22
+
23
+ The asymmetry is the defect: the user config file is a *per-developer* fact, and an
24
+ exported variable is one too, but a workspace is a *per-repo* fact. Nothing in the
25
+ precedence list asked the repository whether it has a workspace at all.
26
+
27
+ ## Ruling
28
+
29
+ ### 1. The precedence gains the repository's own declaration, above the environment variable
30
+
31
+ Precedence, highest first:
32
+
33
+ 1. `--workspace <path>` — an explicit flag always wins.
34
+ 2. The repository's own manifest declaration — a `workspace` key in `simt.json` (or the
35
+ legacy `simt-dev.json`, read by the same reader every other command uses). An `off`
36
+ value declines exactly as it does from any other source, and the environment variable
37
+ cannot override a decline: a repo that said it has no workspace is not one.
38
+ 3. `SIMT_DEV_WORKSPACE` — a value that names a directory is honoured **only where
39
+ the manifest declares a non-declining workspace**: the variable is a pointer INTO
40
+ a workspace the repo opted into, not one imposed on a repo that never declared
41
+ one. A value that declines is honoured from anywhere, because a decline points
42
+ at nothing and so needs no declaration to work.
43
+ 4. The manifest's value — a relative path resolves against the manifest's directory, not
44
+ the process's, so `workspace: ".."` means the same thing from any subdirectory.
45
+ 5. The user config file — unchanged, and honoured regardless of the declaration, because
46
+ it is a per-developer fact that predates the repo.
47
+ 6. The sibling heuristic — unchanged: consulted last, accepted only when it verifies.
48
+
49
+ Every command that consults the workspace reports where it came from, as before; the
50
+ declaration introduces the source label `repo`.
51
+
52
+ ### 2. A manifest that exists and does not parse is an error, and it names the file
53
+
54
+ Same shape as the unparseable-config error ADR-014 ruling 2 established: precedence
55
+ decides which *answer* wins, but a file that exists and does not parse is a defect in
56
+ the reader's setup, and hiding it because some other source outranked it would send
57
+ them looking in the wrong place.
58
+
59
+ ### 3. The memoisation key names the directory whose manifest decides
60
+
61
+ `src/graph/load.mjs` memoised the resolution per flag, variable and config location.
62
+ The current directory's manifest now decides part of the answer, so the key names it;
63
+ a memo that cannot see the directory serves one caller's declaration to a later caller
64
+ standing in another repo.
65
+
66
+ ## Consequences
67
+
68
+ This repository dogfoods the declaration: its own `simt.json` carries
69
+ `"workspace": ".."`. The resolved root is unchanged — the sibling heuristic already
70
+ verified the same directory — but the source label moves from `heuristic` to `repo`,
71
+ which is the point: the workspace this repo resolves is declared, not guessed.
72
+
73
+ An exported `SIMT_DEV_WORKSPACE` is inert in a repo that declared no workspace, which
74
+ is the measured case this record exists to fix. In a repo that did declare one, the
75
+ variable still overrides the declared value, so CI pinning a workspace for a known
76
+ multi-repo checkout is unchanged.
77
+
78
+ ## Rejected
79
+
80
+ - **Honouring the variable only in CI.** The tool has no CI detector, and "CI" is not
81
+ a fact about the repository; a detector that is wrong in one direction reproduces
82
+ the measured defect, and wrong in the other breaks the one-off use the variable
83
+ exists for.
84
+ - **Making the declaration a precondition for the config file too.** The config file
85
+ is per-developer and works from any repo, including one that has no manifest yet;
86
+ requiring a declaration would break the single-repo reader with a config, which is
87
+ the audience ADR-014 ruling 3 protects.
88
+ - **Renaming the variable.** It is the documented pointer in every remedy string this
89
+ tool ships; renaming it moves the defect into the reader's muscle memory rather
90
+ than deleting it.
@@ -0,0 +1,96 @@
1
+ ---
2
+ simt-affects: ["src/hooks/dispatcher.mjs", "rules/core.md", "rules/mdc/slice-commit.mdc", "test/hooks.test.mjs"]
3
+ simt-supersedes: []
4
+ ---
5
+
6
+ # ADR-120 — a pre-push whose tree is already proven green skips the test gate on that evidence
7
+
8
+ Status: **accepted** · 2026-10-31
9
+
10
+ ## Context
11
+
12
+ The shared dispatcher runs a repository's declared pre-push gates on every push. For
13
+ this repository that is the full test suite — minutes of wall clock — and the common
14
+ shape of push pays it twice: the author ran the suite green on the very tree being
15
+ pushed, committed nothing further, and the gate runs it again at push time.
16
+
17
+ ADR-113 already ruled that a push carrying only documentation skips the test gate and
18
+ says so. It is the right shape of the answer and the template for this one: a skip
19
+ earned by a property of the push, reported with a reason, and the record that earned it
20
+ named in the report. This record extends it with a second property: a push whose
21
+ committed tree is one the suite has already passed.
22
+
23
+ The dispatcher records every gate run against the tree it ran on (ADR-087). A passing
24
+ run is evidence about one tree and no other; editing any file afterwards produces a
25
+ different tree, and the old line no longer proves anything about what is on disk. The
26
+ evidence the ledger already keeps is exactly the evidence this skip needs, and no
27
+ other state is required.
28
+
29
+ ## Ruling
30
+
31
+ ### 1. The skip is unconditional, evidence-driven, and fail-closed
32
+
33
+ A pre-push `test` gate is skipped when — and only when — the ledger records a passing
34
+ `test` run whose tree equals the committed tree being pushed (`HEAD^{tree}`). There is
35
+ no manifest key to declare: the condition is a fact about the evidence, not a policy
36
+ choice. Every uncertainty fails closed to the suite — an unborn HEAD, an unreadable
37
+ ledger, no entry for this tree, or a later failure on the same tree. A gate that cannot
38
+ prove it need not run does not run; a gate that cannot prove it is a failure, not a
39
+ skip.
40
+
41
+ "No changes since" is enforced by the digest, not by trusting the author: a clean
42
+ working tree's recorded `worktreeTree` equals `HEAD^{tree}`, and any later edit changes
43
+ the tree id, so the match fails and the suite runs.
44
+
45
+ ### 2. The skip is reported, and names the record that earned it
46
+
47
+ The report prints the skip with its reason and the ADR that earned it — the same shape
48
+ ADR-113 fixed for the docs-only path. The `test` gate line reads
49
+ `SKIPPED test … [ADR-120]`, so a skip is never silent and the two skip paths are
50
+ distinguishable in the same report.
51
+
52
+ ### 3. The docs-only skip keeps its own record
53
+
54
+ The two skips are independent and compose: the docs-only classification is consulted
55
+ first (it is cheaper — no ledger read), and where it does not fire, the evidence check
56
+ decides. A push that earns both is reported under whichever fired first; both earn the
57
+ same outcome, and the report names the one that did the work.
58
+
59
+ ### 4. The rule tells the agent to cite the pass, not re-run it
60
+
61
+ `rules/core.md`'s Git section and the `slice-commit` rule both state the agent-side
62
+ half: once a push is approved and the suite ran green on the current state with nothing
63
+ changed since, the agent pushes directly and cites the already-passing run as the pass.
64
+ The gate recognising the evidence is what makes that citation verifiable — the agent
65
+ claims a run the ledger records, and a claim the ledger does not record is a claim the
66
+ evidence check flags.
67
+
68
+ ## Consequences
69
+
70
+ - `src/hooks/dispatcher.mjs` gains `resolveAlreadyPassedSkip(root)` and threads the
71
+ earning record (`adr`) through `skipTest` so the report distinguishes ADR-113 from
72
+ ADR-120.
73
+ - The `slice-commit` rule becomes the git-workflow home — commit order, message
74
+ format, push approval, worktrees — and the Git section of `rules/core.md` points
75
+ at it. This is a change to shipped rule text: every adopting repository's
76
+ projection re-renders on its next sync.
77
+ - A repository that never records a run (a fresh clone, a clean checkout) is
78
+ unaffected: the ledger is empty, the check fails closed, and the suite runs exactly
79
+ as before.
80
+ - The skip is strictly weaker than the re-run — it proves the same tree, with the
81
+ same suite, already passed — so it cannot admit a state the suite would have
82
+ rejected.
83
+
84
+ ## Rejected
85
+
86
+ - **A manifest key (opt-in).** Consistent with ADR-113's declared skip, but the
87
+ condition here is a fact about the evidence, not a policy a repository chooses; a
88
+ key would let a repository declare a skip it never earns and hide the evidence
89
+ requirement. The docs-only skip stays declared because its condition — the shape of
90
+ the push — is not observable from the ledger.
91
+ - **Matching the working tree.** The pre-push gate runs at HEAD, so the evidence that
92
+ matters is a pass on the committed tree. Matching the working tree would let a
93
+ dirty tree skip on a pass that did not include its changes.
94
+ - **A TTL or recency window.** The tree digest is a stronger condition than age: an
95
+ entry from a week ago is valid if the tree is the one being pushed, and a fresh
96
+ entry is void the moment a file changes. Age is not evidence; the digest is.