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
@@ -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.7.0",
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",
@@ -44,6 +44,14 @@
44
44
  ],
45
45
  "scripts": {
46
46
  "test": "node --test --test-concurrency=$(node -p 'os.cpus().length') test/*.test.mjs",
47
- "lint": "node bin/simt.mjs doctor"
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/*"
48
56
  }
49
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
 
@@ -15,6 +15,35 @@ one is a mechanical procedure with a fixed order, because every step after it de
15
15
  on the one before: you cannot review a diff you have not staged, and you cannot claim
16
16
  evidence for a test you have not run.
17
17
 
18
+ ## Commit message
19
+
20
+ A message is read by the next agent before they are told what to do. It says what the
21
+ commit is, what it answers, and what it changed, so a reader never reconstructs that
22
+ from the diff.
23
+
24
+ The title is Conventional Commits: imperative, no trailing period, a short scope slug
25
+ that names the subsystem — not a ticket id.
26
+
27
+ feat(dispatcher): skip the test gate on a push that already passed
28
+
29
+ The body is plain sections in this order. Drop any section that has nothing to say; a
30
+ one-line commit needs none of them.
31
+
32
+ What: <one sentence — what this commit is>
33
+ Why: <what it answers — the task, defect, or decision that asked for it>
34
+ Before: <the state this replaces, briefly>
35
+ After: <the state it leaves, briefly>
36
+
37
+ "Before" and "After" are a short state summary, not a diff: enough to see what moved,
38
+ not enough to need the patch. Cite a decision record where one settles the change, and
39
+ never claim a test in the body that was not run on this tree.
40
+
41
+ The message ends with a trailer on its own line, after the body - attribution to the
42
+ tool, in the form git renders as a co-author. It is part of the message, not a
43
+ section, so it stays even when every section is dropped:
44
+
45
+ Co-Authored-By: Simt Dev <noreply@simt.space>
46
+
18
47
  ## Order
19
48
 
20
49
  1. **Run the focused test the change needs**, through the repository's own runner,
@@ -24,10 +53,8 @@ evidence for a test you have not run.
24
53
  alone. Report what you left alone; silence about it reads as "I reverted it".
25
54
  3. **Read the staged diff.** Not the working tree — the diff. It is what will be
26
55
  committed, and it is the last chance to notice a file that does not belong.
27
- 4. **Write the message.** Conventional Commits, imperative subject, no trailing period:
28
- `fix(gates): declare only gates a repo can run, and report the ones it cannot`.
29
- The subject says what changed; the body says why, and what was deliberately not
30
- changed.
56
+ 4. **Write the message in the format above.** The title names what changed; the body
57
+ says why, and what was deliberately not changed.
31
58
  5. **Commit.** `git commit`, non-interactive. Report the SHA.
32
59
  6. **Stop.**
33
60
 
@@ -44,6 +71,32 @@ Never use `--no-verify`. A hook that fails is telling you something about the ch
44
71
  skipping it turns a gate into a suggestion and the next person has no way to know it
45
72
  was skipped.
46
73
 
74
+ Once the push is approved, you may push directly without the pre-push gate re-running
75
+ the full suite when you already ran that suite green on the current state and changed
76
+ nothing since: the gate records every run against the tree it ran on, recognises that
77
+ evidence, and skips the re-run, reporting the skip and the record that earned it.
78
+ Cite the already-passing run as the pass — do not claim a re-run that did not happen.
79
+ (ADR-120)
80
+
81
+ ## Worktrees
82
+
83
+ A worktree is the unit of isolation for concurrent work. The rules that keep it honest:
84
+
85
+ - **One agent, one worktree, one slice.** Never direct two agents at the same files
86
+ concurrently; give each its own worktree and a slice whose files do not overlap.
87
+ - **Start from the record, not from memory.** `simt slice start <id>` creates the
88
+ worktree for the tracker row and returns its path; work in that path, not the main
89
+ checkout.
90
+ - **Finish where you started.** `simt slice finish <id>` runs the declared gates and
91
+ commits from the worktree; `simt plan done <id> <sha>` records the SHA in the main
92
+ checkout's tracker. A slice is not done when its worktree is green — it is done
93
+ when the tracker row is complete and the SHA is recorded.
94
+ - **Leave the worktree when the slice is done.** A finished worktree that still holds
95
+ a dirty tree is a claim about work that is not committed; remove it, and report what
96
+ you left behind if there was any.
97
+ - **Never rewrite history across worktrees.** A rebase, reset or amend in one worktree
98
+ changes the base every other worktree branched from; coordinate it, or do not do it.
99
+
47
100
  ## What never belongs in a commit
48
101
 
49
102
  - Failing, partial, or unrun work. A commit is a claim; a partial one is a false one.
@@ -60,7 +60,7 @@ export async function promptOptIn(root) {
60
60
  * a minute while a repository is walked — is how the hook gets uninstalled, and the
61
61
  * packet is not worth a prompt that cannot be typed.
62
62
  */
63
- export async function deliverPrompt(row, payload) {
63
+ export async function deliverPrompt(row, payload, { root: flagRoot } = {}) {
64
64
  const task = promptText(row, payload);
65
65
  if (task === null) return { skipped: 'the payload carries no prompt this harness documents' };
66
66
 
@@ -68,7 +68,10 @@ export async function deliverPrompt(row, payload) {
68
68
  // and resolved to the repository's top level the same way: the manifest, like the
69
69
  // lease file, is a repository-relative fact, and a prompt submitted from a
70
70
  // subdirectory must find it (review 0003 Q16).
71
- const root = repoRoot(factsFrom(row, payload).root);
71
+ // `--root` outranks the payload here as it does on the decision path: a packaged hook
72
+ // runs from the plugin directory, so the payload's cwd would name the wrong repository
73
+ // and find no manifest, which delivered nothing and looked like an opt-out.
74
+ const root = repoRoot(factsFrom(row, payload, { root: flagRoot }).root);
72
75
  const optIn = await promptOptIn(root);
73
76
  if (!optIn.optedIn) return { skipped: optIn.why };
74
77
 
@@ -4,7 +4,7 @@
4
4
  // harness's PATH. A harness config naming the bare `simt` binary therefore
5
5
  // runs nothing on Claude (a non-2 exit is silent: no enforcement) and blocks
6
6
  // every tool call on Cursor (its hook fails closed). The fix is a shim beside
7
- // the git shims, resolved the same way: `.simt-hooks/agent-hook` locates the
7
+ // the git shims, resolved the same way: `.simt/hooks/agent-hook` locates the
8
8
  // dispatcher with the shared `renderLocator` block and hands over, so every
9
9
  // harness config names the shim and never the bare binary (review 0003 Q21).
10
10
  //
package/src/api.mjs ADDED
@@ -0,0 +1,29 @@
1
+ // The library surface: the read commands as data (review 0003 Q63).
2
+ //
3
+ // Every function here is the one the CLI and the MCP tools already call. Nothing in
4
+ // this file renders: a caller that wants text, JSON or an MCP result renders these
5
+ // results itself, which is the whole point - a UI, a harness and a terminal asking the
6
+ // same question must not get three answers, and they cannot disagree if they are
7
+ // reading one function's return value.
8
+ //
9
+ // This is the read half only. The commands that write (`init`, `sync`, `hooks
10
+ // install`, `skills install`, `upgrade`) still print their plans from the command
11
+ // module, because their output is a diff a person reviews before it is applied
12
+ // (ADR-001). They migrate in the next slice, when `run(ctx, input)` exists to carry
13
+ // a plan back to a caller that can show it.
14
+ //
15
+ // The exports map in package.json points `import 'simt-dev'` here, and keeps
16
+ // `./src/*` because the plugin runtime and the harness shims import deep paths.
17
+ // Removing that subpath would break them to make the surface look tidier.
18
+
19
+ export { checkRepo, checkTargets, checkTarget, frozenFindings } from './commands/sync.mjs';
20
+ export { diagnose, serialisedFindings } from './commands/doctor.mjs';
21
+ export { rulesCatalogue, rulesWhyFacts } from './commands/rules.mjs';
22
+ export { skillsBriefFacts, skillsFindFacts, skillsShowFacts, skillsWhyFacts } from './commands/skills.mjs';
23
+ export { assembleBrief } from './brief/assemble.mjs';
24
+ export { loadRegistry } from './graph/load.mjs';
25
+ export { discover } from './compare/discover.mjs';
26
+ export { buildRows, resolveConcurrency, summarise } from './compare/row.mjs';
27
+ export { planNext } from './plan/index.mjs';
28
+ export { buildPrBody } from './prbody.mjs';
29
+ export { federationAccess, readFederation } from './federation/config.mjs';
package/src/argv.mjs CHANGED
@@ -26,6 +26,11 @@ import { EXIT } from './output/index.mjs';
26
26
  // mean something, which it could not while three accepted flags had no description.
27
27
 
28
28
  export const BOOL_FLAGS = new Set([
29
+ // bench ablate: compare across harness, model and experiment groups. The command
30
+ // read `flags.crossGroup` while the parser accepted no such flag, so the override
31
+ // existed only in the error message that told the reader to pass it (review 0003
32
+ // Q62, found by the spec-vs-parser drift test).
33
+ 'cross-group',
29
34
  'json',
30
35
  'all',
31
36
  'dry-run',
@@ -89,6 +94,10 @@ export const BOOL_FLAGS = new Set([
89
94
  // bench.json marked unmeasured, and a lint that failed it on arrival would make
90
95
  // every new pack red before its author wrote the rule (plan 0003 R45).
91
96
  'require-measured',
97
+ // bench mine: restore the old "reflect exactly this history" semantics by
98
+ // deleting task files the current history does not reproduce. Off by default
99
+ // because mining is append-only (review 0003 Q48).
100
+ 'replace',
92
101
  // distill: propose the summary from the configured local chat model instead of
93
102
  // the deterministic outline. Off by default: no model in the default path (P1),
94
103
  // and a model that is not asked for should not be opened against (ADR-090).
@@ -166,6 +175,9 @@ export const VALUE_FLAGS = new Set([
166
175
  // lease: how long the claim lasts, in seconds. A value, because "a lease is short"
167
176
  // is not a number any reader can act on, and an expiry nobody chose is an expiry
168
177
  // nobody can plan around (plan 0003 R54, ADR-101).
178
+ // lessons capture: how long a captured candidate stays eligible. A value, because a
179
+ // default would be a deadline nobody chose (review 0003 Q62, found by the drift test).
180
+ 'expires',
169
181
  'ttl',
170
182
  // map: outline (paths), names (exported symbols) or signatures (the line).
171
183
  // A boolean would not say which of the three the caller wanted.
@@ -281,9 +293,23 @@ export function parse(argv) {
281
293
  flags.reps = n;
282
294
  } else {
283
295
  const key = camel(raw);
284
- flags[key] = REPEATABLE_FLAGS.has(raw)
285
- ? [...(Array.isArray(flags[key]) ? flags[key] : []), value]
286
- : value;
296
+ if (REPEATABLE_FLAGS.has(raw)) {
297
+ flags[key] = [...(Array.isArray(flags[key]) ? flags[key] : []), value];
298
+ } else if (flags[key] !== undefined) {
299
+ // `--root a --root b` kept `b`, so half of what the caller asked for vanished
300
+ // with no error and no way to tell which half. Only a flag declared repeatable
301
+ // accumulates (review 0003 Q62); a second value for any other is a usage error.
302
+ return {
303
+ ok: false,
304
+ error: usage(
305
+ `--${raw} was given twice; it takes one value (repeatable: ${[...REPEATABLE_FLAGS]
306
+ .map((name) => `--${name}`)
307
+ .join(', ')})`,
308
+ ),
309
+ };
310
+ } else {
311
+ flags[key] = value;
312
+ }
287
313
  }
288
314
  continue;
289
315
  }
@@ -10,6 +10,8 @@
10
10
  "args": ["-p", "{prompt}", "--output-format", "json", "--max-turns", "{turns}"],
11
11
  "modelArgs": ["--model", "{model}"],
12
12
  "format": "json",
13
+ "isolation": "env",
14
+ "env": ["ANTHROPIC_API_KEY"],
13
15
  "usageSource": "usage.*, total_cost_usd, num_turns",
14
16
  "fields": {
15
17
  "input": ["usage.input_tokens", "usage.input"],
@@ -30,6 +32,8 @@
30
32
  "args": ["exec", "--json", "{prompt}"],
31
33
  "modelArgs": ["--model", "{model}"],
32
34
  "format": "jsonl",
35
+ "isolation": "env",
36
+ "env": ["OPENAI_API_KEY"],
33
37
  "usageSource": "token events in JSONL",
34
38
  "fields": {
35
39
  "input": ["usage.input_tokens", "input_tokens", "usage.input"],
@@ -50,6 +54,8 @@
50
54
  "args": ["-p", "{prompt}", "--output-format", "json"],
51
55
  "modelArgs": ["--model", "{model}"],
52
56
  "format": "json",
57
+ "isolation": "env",
58
+ "env": ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
53
59
  "usageSource": "stats block",
54
60
  "fields": {
55
61
  "input": ["stats.inputTokens", "stats.tokens.prompt", "stats.prompt_tokens", "usage.input_tokens"],
@@ -70,6 +76,8 @@
70
76
  "args": ["run", "--format", "json", "{prompt}"],
71
77
  "modelArgs": ["--model", "{model}"],
72
78
  "format": "jsonl",
79
+ "isolation": "env",
80
+ "env": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
73
81
  "usageSource": "session export",
74
82
  "fields": {
75
83
  "input": ["tokens.input", "usage.input_tokens", "part.tokens.input"],
@@ -90,6 +98,8 @@
90
98
  "args": ["-p", "{prompt}", "--output-format", "json"],
91
99
  "modelArgs": ["--model", "{model}"],
92
100
  "format": "json",
101
+ "isolation": "env",
102
+ "env": ["CURSOR_API_KEY"],
93
103
  "usageSource": "result JSON",
94
104
  "fields": {
95
105
  "input": ["usage.input_tokens", "usage.input", "stats.inputTokens"],
@@ -110,6 +120,8 @@
110
120
  "args": ["--message", "{prompt}", "--yes-always", "--analytics-log", "{analyticsLog}"],
111
121
  "modelArgs": ["--model", "{model}"],
112
122
  "format": "analytics-log",
123
+ "isolation": "env",
124
+ "env": ["OPENAI_API_KEY", "ANTHROPIC_API_KEY"],
113
125
  "usageSource": "--analytics-log file",
114
126
  "fields": {
115
127
  "input": ["prompt_tokens", "properties.prompt_tokens", "usage.input_tokens"],
@@ -130,6 +142,8 @@
130
142
  "args": [],
131
143
  "modelArgs": [],
132
144
  "format": "builtin",
145
+ "isolation": "none",
146
+ "env": [],
133
147
  "usageSource": "vault gold.patch",
134
148
  "fields": {}
135
149
  },
@@ -142,6 +156,8 @@
142
156
  "args": [],
143
157
  "modelArgs": [],
144
158
  "format": "builtin",
159
+ "isolation": "none",
160
+ "env": [],
145
161
  "usageSource": "none",
146
162
  "fields": {}
147
163
  }