simt-dev 0.6.3 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/CHANGELOG.md +162 -841
  2. package/README.md +21 -135
  3. package/agents/simt-agent.md +84 -0
  4. package/bin/simt.mjs +19 -55
  5. package/docs/decisions/ADR-009-skill-layout.md +1 -1
  6. package/docs/decisions/ADR-014-workspace-is-configuration.md +1 -1
  7. package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +1 -1
  8. package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
  9. package/docs/decisions/ADR-106-the-harness-hook-runs-through-a-repo-shim.md +46 -0
  10. package/docs/decisions/ADR-107-an-install-can-name-one-harness-and-the-skipped-ones-are-named.md +51 -0
  11. package/docs/decisions/ADR-108-an-adoption-builds-the-index-and-a-command-refreshes-it-on-read.md +59 -0
  12. package/docs/decisions/ADR-109-simt-dev-is-its-own-consumer-and-the-guards-that-kept-it-from-being-one-are-retired.md +47 -0
  13. package/docs/decisions/ADR-110-the-lock-file-follows-the-manifest-name-and-the-old-name-is-read-until-the-next-write.md +43 -0
  14. package/docs/decisions/ADR-111-the-tool-ships-one-agent-rendered-per-harness-and-installs-it-with-init-and-a-command.md +75 -0
  15. package/docs/decisions/ADR-112-the-registry-is-what-simt-dev-ships-and-a-repo-owns-its-own-skills.md +81 -0
  16. package/docs/decisions/ADR-113-a-pre-push-that-carries-only-documentation-skips-the-test-gate-and-says-so.md +87 -0
  17. package/docs/decisions/ADR-114-the-bench-pairs-at-the-task-level-a-missing-cost-is-not-zero-and-an-unasked-cross-group-compare-is-refused.md +74 -0
  18. package/docs/decisions/ADR-115-the-index-stamps-its-schema-version-migrates-older-databases-in-place-and-serves-newer-ones-from-a-sidecar.md +69 -0
  19. package/docs/decisions/ADR-116-rust-support-reaches-the-example-path-the-indent-and-the-bench-runner.md +100 -0
  20. package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
  21. package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
  22. package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
  23. package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
  24. package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
  25. package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
  26. package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
  27. package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
  28. package/package.json +13 -3
  29. package/rules/core.md +9 -2
  30. package/rules/mdc/slice-commit.mdc +57 -4
  31. package/rules/mdc/test-gate.mdc +2 -0
  32. package/rules/stacks/android.md +3 -3
  33. package/rules/stacks/connectiq.md +2 -2
  34. package/rules/stacks/dart.md +1 -1
  35. package/rules/stacks/deno.md +1 -1
  36. package/rules/stacks/dotnet.md +1 -1
  37. package/rules/stacks/elixir.md +1 -1
  38. package/rules/stacks/go.md +1 -1
  39. package/rules/stacks/jvm.md +3 -2
  40. package/rules/stacks/node.md +2 -2
  41. package/rules/stacks/python.md +7 -6
  42. package/rules/stacks/ruby.md +1 -1
  43. package/rules/stacks/rust.md +1 -1
  44. package/rules/stacks/swift.md +1 -1
  45. package/rules/stacks/typescript.md +2 -2
  46. package/rules/stacks/uv.md +3 -2
  47. package/skills/simt-commands/SKILL.md +70 -0
  48. package/src/advice/advice.mjs +7 -3
  49. package/src/agenthooks/decide.mjs +141 -9
  50. package/src/agenthooks/opencode.mjs +72 -0
  51. package/src/agenthooks/prompt.mjs +10 -3
  52. package/src/agenthooks/render.mjs +186 -19
  53. package/src/agenthooks/shim.mjs +55 -0
  54. package/src/agents/render.mjs +277 -0
  55. package/src/api.mjs +29 -0
  56. package/src/argv.mjs +37 -3
  57. package/src/bench/ablate.mjs +31 -14
  58. package/src/bench/adapters.json +16 -0
  59. package/src/bench/corpus.mjs +150 -0
  60. package/src/bench/grade.mjs +23 -49
  61. package/src/bench/mine.mjs +247 -36
  62. package/src/bench/probes.mjs +104 -68
  63. package/src/bench/process.mjs +128 -0
  64. package/src/bench/report.mjs +260 -84
  65. package/src/bench/run.mjs +251 -150
  66. package/src/bench/variants.mjs +112 -40
  67. package/src/brief/assemble.mjs +44 -3
  68. package/src/ci/workflow.mjs +46 -14
  69. package/src/cli.mjs +45 -0
  70. package/src/commands/agent.mjs +205 -0
  71. package/src/commands/agenthook.mjs +113 -20
  72. package/src/commands/bench.mjs +192 -18
  73. package/src/commands/brief.mjs +5 -4
  74. package/src/commands/ci.mjs +2 -1
  75. package/src/commands/compare.mjs +50 -49
  76. package/src/commands/completions.mjs +2 -6
  77. package/src/commands/decide.mjs +10 -9
  78. package/src/commands/distill.mjs +4 -3
  79. package/src/commands/doctor.mjs +11 -7
  80. package/src/commands/evidence.mjs +19 -12
  81. package/src/commands/explain.mjs +0 -1
  82. package/src/commands/gate.mjs +98 -28
  83. package/src/commands/graph.mjs +20 -8
  84. package/src/commands/harness.mjs +3 -2
  85. package/src/commands/help.mjs +160 -31
  86. package/src/commands/hooks.mjs +163 -50
  87. package/src/commands/ignore.mjs +2 -1
  88. package/src/commands/impact.mjs +8 -9
  89. package/src/commands/index.mjs +278 -180
  90. package/src/commands/init.mjs +171 -38
  91. package/src/commands/lease.mjs +206 -39
  92. package/src/commands/lessons.mjs +72 -15
  93. package/src/commands/local.mjs +3 -3
  94. package/src/commands/map.mjs +47 -27
  95. package/src/commands/mcp.mjs +0 -1
  96. package/src/commands/pack.mjs +9 -6
  97. package/src/commands/plan.mjs +6 -5
  98. package/src/commands/plugin.mjs +3 -3
  99. package/src/commands/prbody.mjs +3 -3
  100. package/src/commands/repoindex.mjs +5 -3
  101. package/src/commands/review.mjs +2 -2
  102. package/src/commands/rules.mjs +124 -33
  103. package/src/commands/shared.mjs +2 -3
  104. package/src/commands/skills.mjs +82 -45
  105. package/src/commands/skillscompile.mjs +12 -12
  106. package/src/commands/skillsinstall.mjs +14 -2
  107. package/src/commands/slice.mjs +396 -26
  108. package/src/commands/spec.mjs +151 -0
  109. package/src/commands/status.mjs +4 -7
  110. package/src/commands/sync.mjs +50 -33
  111. package/src/commands/tests.mjs +11 -11
  112. package/src/commands/tokens.mjs +2 -2
  113. package/src/commands/undo.mjs +29 -31
  114. package/src/commands/upgrade.mjs +40 -4
  115. package/src/commands/why.mjs +19 -12
  116. package/src/compare/hooksstate.mjs +23 -1
  117. package/src/compare/learn.mjs +82 -57
  118. package/src/compare/parallel.mjs +50 -0
  119. package/src/compare/row.mjs +26 -3
  120. package/src/completions.mjs +5 -0
  121. package/src/context.mjs +64 -0
  122. package/src/decisions/dir.mjs +84 -0
  123. package/src/decisions/index-doc.mjs +46 -0
  124. package/src/decisions/index.mjs +109 -11
  125. package/src/diff.mjs +1 -1
  126. package/src/distill/distill.mjs +17 -2
  127. package/src/distill/engine.mjs +5 -1
  128. package/src/ecosystem/node-runner.mjs +144 -0
  129. package/src/explain.mjs +18 -6
  130. package/src/graph/load.mjs +42 -20
  131. package/src/hooks/capability.mjs +17 -4
  132. package/src/hooks/dispatcher.mjs +513 -36
  133. package/src/hooks/gates.mjs +0 -43
  134. package/src/hooks/resolve.mjs +11 -10
  135. package/src/hooks/shim.mjs +17 -9
  136. package/src/identity.mjs +96 -15
  137. package/src/index/extract.mjs +192 -11
  138. package/src/index/persist.mjs +307 -47
  139. package/src/index/store.mjs +263 -60
  140. package/src/index/tests.mjs +6 -35
  141. package/src/journal.mjs +372 -84
  142. package/src/kernel/card.mjs +37 -12
  143. package/src/kernel/checks.mjs +44 -9
  144. package/src/kernel/variants.mjs +27 -16
  145. package/src/lease.mjs +97 -15
  146. package/src/ledger/index.mjs +120 -25
  147. package/src/local/client.mjs +4 -6
  148. package/src/locate.mjs +33 -9
  149. package/src/manifest/schema.mjs +213 -0
  150. package/src/mcp/resources.mjs +12 -13
  151. package/src/mcp/server.mjs +151 -38
  152. package/src/mcp/tools.mjs +90 -16
  153. package/src/measure/session.mjs +3 -2
  154. package/src/memory/decide.mjs +98 -43
  155. package/src/memory/lessons.mjs +168 -33
  156. package/src/name.mjs +9 -0
  157. package/src/node-version.mjs +37 -0
  158. package/src/output/index.mjs +111 -3
  159. package/src/packs/manage.mjs +17 -3
  160. package/src/packs.mjs +20 -2
  161. package/src/plan/index.mjs +31 -8
  162. package/src/plan/template.mjs +27 -5
  163. package/src/plugins.mjs +6 -2
  164. package/src/prbody.mjs +7 -5
  165. package/src/projection/baselines.mjs +1 -1
  166. package/src/projection/disclosure.mjs +2 -2
  167. package/src/projection/loaders.mjs +41 -1
  168. package/src/projection/lock.mjs +31 -14
  169. package/src/projection/manifest.mjs +89 -53
  170. package/src/projection/markers.mjs +43 -3
  171. package/src/projection/mdc.mjs +34 -7
  172. package/src/projection/project.mjs +162 -18
  173. package/src/projection/rules.mjs +58 -6
  174. package/src/projection/surfaces.mjs +9 -1
  175. package/src/repo-root.mjs +88 -0
  176. package/src/resolve/body.mjs +4 -0
  177. package/src/resolve/fingerprint.mjs +24 -15
  178. package/src/resolve/local.mjs +58 -0
  179. package/src/review/critic.mjs +6 -3
  180. package/src/secrets.mjs +165 -22
  181. package/src/state.mjs +76 -0
  182. package/src/status.mjs +9 -4
  183. package/src/toolwords.mjs +8 -0
  184. package/src/tsconfig.mjs +39 -99
  185. package/src/workspace.mjs +55 -10
  186. package/templates/plan/tracker.json +1 -1
  187. package/docs/decisions/README.md +0 -591
@@ -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.
@@ -0,0 +1,94 @@
1
+ ---
2
+ simt-affects: ["src/hooks/shim.mjs", "src/commands/hooks.mjs", "src/journal.mjs", "src/agenthooks/shim.mjs", "test/hooks.test.mjs", "test/agenthooks.test.mjs"]
3
+ simt-supersedes: []
4
+ ---
5
+
6
+ # ADR-122 — the shims move into the state directory, and the legacy directory removes itself
7
+
8
+ Status: **accepted** · 2026-10-31
9
+
10
+ ## Context
11
+
12
+ The git shims live in a top-level `.simt-hooks/` directory, and `core.hooksPath` points
13
+ git at it. Every other piece of per-repo state — the journal, the ledger, the evidence,
14
+ the index — lives under the state directory `.simt/`, which the repository ignores as a
15
+ single line. The top-level split has no technical reason: `core.hooksPath` accepts any
16
+ path, and nothing about the shims requires a location of its own. It does have a cost —
17
+ two roots for what is one body of state, and an ignore line that exists only for the
18
+ shims.
19
+
20
+ The move has one genuine collision. The journal's stated boundary is that nothing under
21
+ the state directory is journaled: the journal recording itself is recursion, and the
22
+ ledger, index and candidates are evidence and caches whose byte-shape must not be
23
+ rolled back. Review 0003 Q22 ruled the opposite for the shims: `undo` restores
24
+ `core.hooksPath` and the shims together, because a routing restored without its shims is
25
+ a hook that no longer runs. Shims that land under the state directory unjournaled would
26
+ recreate Q22 exactly — the undo restores the routing and the shims stay. The move is
27
+ therefore not a rename; it is a rename plus a named exception to a recorded boundary.
28
+
29
+ Repositories installed before the move carry the legacy directory. The move must be
30
+ self-healing in those repositories, and the cleanup must obey the rule that already
31
+ governs the hooks directory: a file this tool cannot recognise as its own is never
32
+ deleted on its way.
33
+
34
+ ## Ruling
35
+
36
+ ### 1. The shims live in the state directory, and everything derives from one constant
37
+
38
+ `HOOKS_DIR` is `.simt/hooks`. The shim file names (`fileFor`), the agent-hook shim
39
+ (`.simt/hooks/agent-hook`), the `core.hooksPath` value a write installs, the hooks
40
+ status, and the agent-hooks `rm` policy all derive from that one constant, so the
41
+ location has exactly one home. The preceding location is `HOOKS_DIR_LEGACY`
42
+ (`.simt-hooks`), named because it is read during cleanup and cited in the plan.
43
+
44
+ ### 2. The legacy directory removes itself on install, judged in the plan
45
+
46
+ On `hooks install` (and `doctor --fix`, which shares the plan), a present legacy
47
+ directory is scanned and judged removable in the plan: every entry in it must be a
48
+ regular file carrying a simt-dev marker — the shim marker or the chain marker. Removable
49
+ means the plan announces the removal and the apply removes each file journaled and then
50
+ takes the directory down, so `undo` re-creates the files and their directory. A single
51
+ entry the tool cannot recognise as its own — a foreign file, or anything that is not a
52
+ regular file — leaves the directory in place, unannounced and untouched. The judgment
53
+ lives in the plan and the deletion in the apply, so preview and apply agree by
54
+ construction, the way every other write in this command does.
55
+
56
+ ### 3. The shim directory is the one named exception to the unjournaled state directory
57
+
58
+ `journaledWriteFile` and `journaledRemoveFile` record writes under the state directory
59
+ only when the path is the shim directory. The exception is named, not broadened: the
60
+ recursion and evidence arguments for the rest of the directory are untouched, and the
61
+ module header states the exception alongside the rule. Without it, `undo` after an
62
+ install restores `core.hooksPath` to unset and leaves the shims behind — the Q22
63
+ defect, relocated.
64
+
65
+ ## Consequences
66
+
67
+ - `src/hooks/shim.mjs` exports both constants; `src/commands/hooks.mjs` gains
68
+ `legacyHooksScan` (the plan-side judgment) and the apply-side removal; `doctor --fix`
69
+ inherits both through the shared plan.
70
+ - `src/journal.mjs` imports the shim location from `src/hooks/shim.mjs` — the
71
+ dependency direction is the journal learning one named path, not the location module
72
+ learning about the journal.
73
+ - The repository's ignore file drops the `.simt-hooks/` line: the state directory line
74
+ already covers the shims.
75
+ - A repository installed before the move self-heals on its next `hooks install --yes`:
76
+ the routing re-points, the new shims land, and the legacy directory is gone — journaled,
77
+ so an `undo` brings it back.
78
+ - `src/toolwords.mjs` keeps `simt-hooks` as a tool word: the legacy name is still a
79
+ reference to the tool in remedies and history, and the estate pattern that exempts it
80
+ is unchanged.
81
+ - The agent-hook shim is re-rendered at its new location by the agent-hooks installer,
82
+ which reads the same constant.
83
+
84
+ ## Rejected
85
+
86
+ - **Leaving the shims at the top level.** The split buys nothing and costs an ignore
87
+ line, a second state root, and a location that the state-directory story does not
88
+ explain.
89
+ - **Removing the legacy directory without the marker check.** A file a person put there
90
+ is not the tool's to delete, and the hooks directory already carries exactly this rule
91
+ for its own files; the cleanup is the same principle applied to the directory.
92
+ - **Journaled everything under the state directory.** The recursion and evidence
93
+ arguments are load-bearing for the ledger and the index; the exception must be the one
94
+ directory git's routing points at, and no wider.
@@ -0,0 +1,57 @@
1
+ ---
2
+ simt-affects: ["src/commands/index.mjs", "src/commands/spec.mjs", "test/argv-repeat.test.mjs"]
3
+ simt-supersedes: []
4
+ ---
5
+
6
+ # ADR-123 — a surplus positional is not refused, because a command's arity is not in its source
7
+
8
+ Status: **accepted** · 2026-10-31
9
+
10
+ ## Context
11
+
12
+ Review 0003 Q62 asked dispatch to validate a command's positionals the way it validates
13
+ its flags, and the cheapest place to read the maximum looked like the module that
14
+ receives `args`: a command that reads `args[0]` takes one, a command that reads nothing
15
+ takes none, and a command that joins or spreads `args` has no maximum at all.
16
+
17
+ Measured against the tree, that rule is wrong for five of the 76 rows. `init`, `tokens`,
18
+ `explain`, `graph lint` and `bench report` all accept a positional that their own module
19
+ does not index — it is forwarded to a helper in another module, or deliberately ignored.
20
+ Enforcing the derived maximum failed 27 tests, 11 of them on `init` alone (`init` a path
21
+ is the command's most ordinary call). The source tells you where an argument is *read*;
22
+ it does not tell you what the command *accepts*, and for the five commands whose reading
23
+ happens elsewhere the two answers differ.
24
+
25
+ ## Ruling
26
+
27
+ ### 1. Dispatch does not enforce a maximum number of positionals
28
+
29
+ Dispatch enforces what a spec row can state truthfully: the flags a command reads, the values a closed set accepts, and that a single-valued flag is given once. It does not enforce a positional maximum, and a surplus positional is accepted exactly as it was before the rule was attempted.
30
+
31
+ ### 2. A command that refuses a surplus positional says so itself, in its own words
32
+
33
+
34
+
35
+ ### 3. The withdrawn arity is recorded here rather than carried as a spec field
36
+
37
+
38
+
39
+ ## Consequences
40
+
41
+ - Q62's surplus-positional clause is withdrawn. The arity it would have needed (29 rows
42
+ with a maximum, 47 unbounded) is not shipped, and no row carries an `args` field.
43
+ - `test/argv-repeat.test.mjs` keeps the refusal of a repeated flag and the closed set;
44
+ the surplus-positional test is gone with the rule that would have made it pass.
45
+ - The source-reading heuristic is recorded here rather than kept in a comment, because
46
+ the next agent to read Q62 will propose it again, and it will fail the same 27 tests.
47
+
48
+ ## Rejected
49
+
50
+ - **Reading arity from the module, as written.** A command's accepted positionals are a
51
+ contract, and the source is an implementation of it; `init` proves they diverge. A
52
+ check that refuses `init <path>` is worse than the silence it replaces.
53
+ - **Declaring arity by hand for all 76 rows.** A number nobody derives and no caller
54
+ checks drifts silently, and a wrong maximum refuses a real call — the same failure,
55
+ arriving a release later and without a test to find it.
56
+ - **Making every surplus positional an error.** The forwarding commands would need a
57
+ per-command exception list, which is the manual declaration above with extra steps.
@@ -0,0 +1,64 @@
1
+ ---
2
+ simt-affects: ["src/manifest/schema.mjs", "src/resolve/fingerprint.mjs", "test/manifest-schema.test.mjs"]
3
+ simt-supersedes: []
4
+ ---
5
+
6
+ # ADR-124 — the manifest has a schema in the tool, and its unknown keys stay the repository's
7
+
8
+ Status: **accepted** · 2026-10-31
9
+
10
+ ## Context
11
+
12
+ `simt.json` is read in 29 places and 41 keys are read ad hoc across the tree. There was
13
+ no list of the keys, so `"mode": "kernal"` was accepted by every reader and reached the
14
+ projection as a mode nothing had heard of, and one reader (`fingerprint`) returned
15
+ `{error}` as the manifest, so a file that did not parse looked like a declaration of a
16
+ key called `error`.
17
+
18
+ Writing that list is where this repository's characteristic mistake is easiest to make:
19
+ the first draft was written from memory and called `limits.fileLines` a positive integer
20
+ and `surfaces` an object. In the only manifest the tool has to hand, the first is an
21
+ object of two tiers and the second is a list.
22
+
23
+ ## Ruling
24
+
25
+ ### 1. One table declares the manifest keys, their owner and their check
26
+
27
+ One table in `src/manifest/schema.mjs` declares every key with its owner (`tool` or
28
+ `repo`), the version it arrived in, and a check that returns a sentence naming what is
29
+ wrong. One loader, `loadManifest(root)`, returns the manifest, whether a file exists,
30
+ the version, and the problems. The table is held to this repository's own `simt.json` by
31
+ a test, because a schema written from memory fails that test in the same run that proves
32
+ the loader works.
33
+
34
+ ### 2. An unparseable file is no manifest, and an absent version field is version 1
35
+
36
+ A caller that receives a half-read object cannot tell it from a declaration, so a file
37
+ that does not parse yields no manifest and one problem naming the parse error. `version`
38
+ arrived after every manifest now on disk, so absent means 1 rather than a problem.
39
+
40
+ ### 3. An unknown key is not a problem, because the file invites the repository's own
41
+
42
+ The manifest comment tells a repository that a value it adds survives a re-init. A
43
+ validator that refused an unknown key would make the tool the owner of a file it calls
44
+ the repository's, so only a known key with an impossible value is reported.
45
+
46
+ ## Consequences
47
+
48
+ - `fingerprint` reads through the loader, so a broken manifest no longer arrives as an
49
+ object with an `error` key; a dependent read sees no declaration.
50
+ - The 29 direct `JSON.parse` call sites remain. Migrating them is the rest of Q69 and
51
+ changes no behaviour this table already answers for a single call.
52
+ - `GENERATED_KEYS` is not yet derived from the table; the table distinguishes `tool`
53
+ from `repo` keys, so the derivation is possible and is not taken here.
54
+
55
+ ## Rejected
56
+
57
+ - **A JSON Schema file and a validator dependency.** `package.json` declares an empty
58
+ dependency set (ADR-012), and a schema in a second language would be a second place
59
+ the keys are stated.
60
+ - **Refusing unknown keys.** The file comment invites a repository to keep its own
61
+ keys, and refusing them breaks every repository that already does.
62
+ - **Throwing on a bad value.** A manifest is read on the path of every command; a throw
63
+ turns a typo into a crash with no remedy, where a named problem can be printed beside
64
+ the answer.
@@ -0,0 +1,65 @@
1
+ ---
2
+ simt-affects: ["src/context.mjs", "src/commands/hooks.mjs", "test/hooks-context.test.mjs"]
3
+ simt-supersedes: []
4
+ ---
5
+
6
+ # ADR-125 — a shared context belongs to a read, and a writer reads directly
7
+
8
+ Status: **accepted** · 2026-10-31
9
+
10
+ ## Context
11
+
12
+ `repoContext` holds one pending promise per question for the length of a command. It
13
+ was threaded through the reads — `check`, the four skills reads, `diagnose`, the rules
14
+ catalogue — and Q67's last clause was to finish the job, which raised the question the
15
+ row did not answer: should a command that *writes* hold a context too?
16
+
17
+ The two cases are not the same. A reader asks several questions about an unchanging
18
+ tree and pays once. A writer changes the tree between its questions: `init` writes a
19
+ manifest and then renders against it, `upgrade` replaces files and then reports drift,
20
+ `sync` rewrites the region and then computes the lock. A context that survived those
21
+ writes would answer the second question with the first answer, and the command would
22
+ report a repository it had already changed.
23
+
24
+ ## Ruling
25
+
26
+ ### 1. A read command may hold one context for its whole run
27
+
28
+ A command that only reads creates one `repoContext` at its entry and passes it to
29
+ every reader beneath it. Two questions with the same answer cost one computation, and
30
+ the cache reports its own hits so a context that is wired but never used cannot pass
31
+ for a fast machine.
32
+
33
+ ### 2. A writer reads its repository directly
34
+
35
+ A writer calls the readers as it always has. It is not an optimisation left undone: a
36
+ shared context would make a stale answer the reward for a correct write, and the
37
+ failure would surface as a drifted manifest in a repository that had just been fixed.
38
+
39
+ ### 3. The hook path keeps its dynamic import
40
+
41
+ `planHooksInstall` resolves the node runner from a fingerprint, and it imports the
42
+ fingerprint module dynamically so that loading the hooks command does not load it.
43
+ Static-importing a context into `src/commands/hooks.mjs` costs about 3 ms of module
44
+ graph on the shim path, which Q64 measured and removed once. A caller that holds a
45
+ context passes it and shares the walk; a caller that does not keeps the light path.
46
+
47
+ ## Consequences
48
+
49
+ - Q67 closes with the reads threaded and the writers explained rather than converted.
50
+ - The 3 ms module cost is paid only when a command actually uses a context; `hooks
51
+ run` keeps the dynamic path.
52
+ - A future writer that wants a context has this record to argue against, instead of
53
+ discovering the staleness as a bug report.
54
+
55
+ ## Rejected
56
+
57
+ - **Giving every writer a context and invalidating entries after each write.** The
58
+ invalidation is the whole bug surface: forget one entry and the failure is a stale
59
+ answer that looks like a correct one, in the command whose job is to make the tree
60
+ correct.
61
+ - **A process-lifetime context.** `bench` writes manifests mid-run; a process-wide
62
+ memo would answer a benchmark for a repository it had just changed.
63
+ - **Making the context required on every reader signature.** It would put the cache in
64
+ the contract of functions that do not need one, and a caller with no context would
65
+ have to invent an empty one.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simt-dev",
3
- "version": "0.6.3",
3
+ "version": "0.7.1",
4
4
  "description": "An intelligence amplifier for AI coding agents: rules and dual gates adopted by one command, plus local retrieval, task briefs, benches and repo memory — deterministic by default, with local-model adapters that propose and never select.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Said Elimam",
@@ -25,6 +25,8 @@
25
25
  "src",
26
26
  "rules",
27
27
  "templates",
28
+ "agents",
29
+ "skills",
28
30
  "docs/decisions",
29
31
  "CHANGELOG.md"
30
32
  ],
@@ -41,7 +43,15 @@
41
43
  "skills"
42
44
  ],
43
45
  "scripts": {
44
- "test": "node --test --test-concurrency=4 test/*.test.mjs",
45
- "lint": "node bin/simt.mjs doctor"
46
+ "test": "node --test --test-concurrency=$(node -p 'os.cpus().length') test/*.test.mjs",
47
+ "lint": "node bin/simt.mjs graph lint",
48
+ "verify:shipped": "node --test test/shipped.test.mjs",
49
+ "build:sea": "node scripts/build-sea.mjs",
50
+ "verify:wheel": "node scripts/verify-wheel.mjs"
51
+ },
52
+ "exports": {
53
+ ".": "./src/api.mjs",
54
+ "./package.json": "./package.json",
55
+ "./src/*": "./src/*"
46
56
  }
47
57
  }
package/rules/core.md CHANGED
@@ -24,9 +24,16 @@ commands belongs in this repository, not here.
24
24
 
25
25
  ## Git
26
26
 
27
+ The commit and push procedure - order, message format, worktrees - is the `slice-commit`
28
+ rule; follow it for every commit and push.
29
+
27
30
  - Commit locally, report the SHA. **Push only on explicit approval.** (ADR-001)
28
31
  - Before asking for push approval, state whether the push triggers a deployment, a migration
29
32
  or any remote mutation.
33
+ - Once approved, you may push directly without the pre-push gate re-running the full suite
34
+ when you already ran that suite green on the current state and changed nothing since: cite
35
+ the already-passing run as the pass, and the gate recognizes the evidence and skips the
36
+ re-run rather than paying for it again. (ADR-120)
30
37
  - Never use `--no-verify`, amend, rebase, reset or rewrite history unless explicitly asked.
31
38
  - Never commit failing, partial, secret-bearing or unrelated work.
32
39
 
@@ -45,8 +52,8 @@ commands belongs in this repository, not here.
45
52
  - Name the exact commands and test files. Partial pass is a blocker; `6/8 pass` is the
46
53
  canonical example of a blocked state. Never justify a skip without a pre-existing written
47
54
  reason. (ADR-006)
48
- - Evidence discipline is mandatory everywhere. A **numeric** bar is declared per repo in
49
- `simt-dev.json`, never assumed centrally. (ADR-006)
55
+ - Evidence discipline is mandatory everywhere. A **numeric** bar is declared per repo in the
56
+ repository's manifest (`{manifest}`), never assumed centrally. (ADR-006)
50
57
  - The pre-commit hook owns format and lint for staged files; do not re-run what it just ran.
51
58
  Manual runs are correct when the hook is absent, when diagnosing a failure, or for a check
52
59
  the hook does not cover. (ADR-004)