simt-dev 0.6.2 → 0.6.3
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.
- package/CHANGELOG.md +48 -1
- package/README.md +44 -15
- package/bin/simt.mjs +19 -6
- package/docs/decisions/ADR-100-a-staged-diff-is-the-last-gate-before-history-and-it-runs-inside-the-tool.md +94 -0
- package/docs/decisions/ADR-101-a-file-lease-belongs-to-one-session-and-the-pre-edit-hook-refuses-another.md +95 -0
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +49 -0
- package/docs/decisions/ADR-103-co-projected-duplicates-merge-down.md +74 -0
- package/docs/decisions/ADR-104-the-pre-tool-use-policy-reads-the-command-the-way-a-shell-reads-it.md +76 -0
- package/docs/decisions/ADR-105-the-json-ceiling-shrinks-arrays-and-never-cuts-bytes.md +44 -0
- package/docs/decisions/README.md +44 -0
- package/package.json +1 -1
- package/rules/mdc/preview-postflight.mdc +5 -6
- package/rules/stacks/android.md +7 -8
- package/rules/stacks/cloudflare-workers.md +1 -1
- package/rules/stacks/container.md +2 -0
- package/rules/stacks/jvm.md +1 -1
- package/rules/stacks/react-native.md +3 -5
- package/rules/stacks/react-web.md +9 -9
- package/src/agenthooks/decide.mjs +101 -10
- package/src/agenthooks/policy.mjs +81 -5
- package/src/agenthooks/shell.mjs +523 -0
- package/src/argv.mjs +54 -9
- package/src/commands/agenthook.mjs +25 -3
- package/src/commands/ci.mjs +28 -3
- package/src/commands/completions.mjs +27 -0
- package/src/commands/decide.mjs +14 -1
- package/src/commands/doctor.mjs +367 -0
- package/src/commands/explain.mjs +52 -0
- package/src/commands/help.mjs +66 -17
- package/src/commands/hooks.mjs +208 -60
- package/src/commands/ignore.mjs +152 -0
- package/src/commands/index.mjs +54 -100
- package/src/commands/init.mjs +82 -12
- package/src/commands/lease.mjs +136 -0
- package/src/commands/lessons.mjs +12 -0
- package/src/commands/mcp.mjs +2 -2
- package/src/commands/pack.mjs +9 -0
- package/src/commands/plan.mjs +13 -1
- package/src/commands/plugin.mjs +16 -1
- package/src/commands/prbody.mjs +45 -0
- package/src/commands/repoindex.mjs +3 -0
- package/src/commands/rules.mjs +145 -105
- package/src/commands/shared.mjs +36 -5
- package/src/commands/skills.mjs +293 -205
- package/src/commands/skillsinstall.mjs +100 -30
- package/src/commands/slice.mjs +353 -0
- package/src/commands/status.mjs +62 -0
- package/src/commands/sync.mjs +110 -15
- package/src/commands/undo.mjs +106 -0
- package/src/commands/upgrade.mjs +70 -8
- package/src/completions.mjs +191 -0
- package/src/diff.mjs +254 -0
- package/src/explain.mjs +258 -0
- package/src/federation/config.mjs +44 -0
- package/src/graph/load.mjs +14 -5
- package/src/hooks/dispatcher.mjs +110 -0
- package/src/hooks/gates.mjs +14 -3
- package/src/hooks/resolve.mjs +14 -0
- package/src/ignore.mjs +133 -0
- package/src/index/persist.mjs +66 -9
- package/src/journal.mjs +326 -0
- package/src/lease.mjs +113 -0
- package/src/mcp/server.mjs +15 -1
- package/src/mcp/tools.mjs +296 -256
- package/src/memory/decide.mjs +6 -1
- package/src/memory/lessons.mjs +12 -0
- package/src/output/index.mjs +82 -12
- package/src/package-version.mjs +49 -1
- package/src/packs/manage.mjs +13 -4
- package/src/plan/index.mjs +6 -0
- package/src/plugins.mjs +27 -6
- package/src/prbody.mjs +251 -0
- package/src/projection/manifest.mjs +13 -2
- package/src/projection/shared.mjs +4 -1
- package/src/secrets.mjs +231 -0
- package/src/status.mjs +152 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,53 @@ All notable changes to this package are recorded here. The format follows
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.3] — 2026-10-29
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
`simt explain <CODE>` names what a `check` finding means, the decision behind
|
|
14
|
+
it, how to fix it, and a concrete trigger — one table row per finding code, so
|
|
15
|
+
every code `check` can emit has an entry.
|
|
16
|
+
|
|
17
|
+
`simt doctor --fix` applies only the remedies flagged safe (local, idempotent,
|
|
18
|
+
journaled) and lists the rest; a fixture with three safe and two unsafe findings
|
|
19
|
+
fixes exactly three.
|
|
20
|
+
|
|
21
|
+
`simt status --line` prints the gate state from the last `check` in under
|
|
22
|
+
50 ms from cache — `simt PASS ✓rules ✓hooks 3/3gates` — for shell prompts, tmux
|
|
23
|
+
and agent status lines.
|
|
24
|
+
|
|
25
|
+
Every write records `{path, beforeDigest, afterDigest, beforeBytes}` in
|
|
26
|
+
`.simt/journal/`, and `simt undo` restores the last set when every file in it
|
|
27
|
+
is untouched since.
|
|
28
|
+
|
|
29
|
+
`--diff` on every writer prints a unified diff of the plan next to the report,
|
|
30
|
+
changing nothing about what the run writes.
|
|
31
|
+
|
|
32
|
+
`simt completions <bash|zsh|fish>` prints a completion script generated from the
|
|
33
|
+
command registry, so the three shells complete exactly the commands the binary
|
|
34
|
+
dispatches.
|
|
35
|
+
|
|
36
|
+
One ignore list (`.simtignore`) renders `.cursorignore`, `.aiderignore`,
|
|
37
|
+
`.geminiignore` and Claude `permissions.deny`, and a zero-dependency
|
|
38
|
+
staged-diff secret scan (vendor patterns plus Shannon entropy over added lines
|
|
39
|
+
only) blocks a commit carrying a probable secret without printing the bytes.
|
|
40
|
+
|
|
41
|
+
`simt slice start <id>` isolates the work in a linked worktree on branch
|
|
42
|
+
`slice/<id>` with the brief preloaded and flips the tracker row In progress;
|
|
43
|
+
`simt slice finish <id>` runs the declared gates, commits a Conventional
|
|
44
|
+
subject from the row title, and records the SHA and test command on the row.
|
|
45
|
+
|
|
46
|
+
`simt lease <paths> --session <id>` claims files until a TTL in
|
|
47
|
+
`.simt/leases.json`, and the pre-edit hook denies an edit leased by another
|
|
48
|
+
session (ADR-101).
|
|
49
|
+
|
|
50
|
+
`simt pr-body [--base <ref>]` assembles the pull request body from the evidence
|
|
51
|
+
ledger, the decision records the diff touches, the gate state from the last
|
|
52
|
+
check, and the residual risk in ADR-008 vocabulary. It is also exposed as the
|
|
53
|
+
`pr_body` MCP tool; the two longest tool descriptions were shortened to hold
|
|
54
|
+
the 2 KB `tools/list` budget.
|
|
55
|
+
|
|
9
56
|
## [0.6.2] — 2026-10-27
|
|
10
57
|
|
|
11
58
|
### Fixed
|
|
@@ -16,7 +63,7 @@ shapes and the leaking-paths count, both designed to move with the corpus).
|
|
|
16
63
|
Same product as v0.6.1; this tag exists so the release automation is proven by
|
|
17
64
|
a green run rather than described by a red one.
|
|
18
65
|
|
|
19
|
-
## [0.6.1] — 2026-10-27
|
|
66
|
+
## [0.6.1] — 2026-10-27 (tagged, never published to npm)
|
|
20
67
|
|
|
21
68
|
### Fixed
|
|
22
69
|
|
package/README.md
CHANGED
|
@@ -26,18 +26,18 @@ to merge. It does not write the projection.
|
|
|
26
26
|
## Install
|
|
27
27
|
|
|
28
28
|
Requires **Node >= 22.20.0** for the npm and PyPI channels; the release binary carries
|
|
29
|
-
its own runtime.
|
|
29
|
+
its own runtime. Pin the release you install. The tool writes a
|
|
30
30
|
content digest into the files it generates, and an unpinned upgrade changes them under
|
|
31
31
|
you.
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
npm i -D simt-dev
|
|
34
|
+
npm i -D simt-dev
|
|
35
35
|
|
|
36
36
|
# or globally
|
|
37
|
-
npm i -g simt-dev
|
|
37
|
+
npm i -g simt-dev
|
|
38
38
|
|
|
39
39
|
# or once, with no install
|
|
40
|
-
npx -y simt-dev
|
|
40
|
+
npx -y simt-dev check
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
### From PyPI
|
|
@@ -68,7 +68,7 @@ runtime: the command works with no Node installed.
|
|
|
68
68
|
### As a single-file binary
|
|
69
69
|
|
|
70
70
|
Each release carries one executable per platform — macOS arm64 and x64, Linux x64 —
|
|
71
|
-
beside its `.sha256`.
|
|
71
|
+
beside its `.sha256`. Pin the release you download,
|
|
72
72
|
and run this from the repository's top level:
|
|
73
73
|
|
|
74
74
|
```sh
|
|
@@ -85,7 +85,7 @@ published `.sha256` before running anything.
|
|
|
85
85
|
```sh
|
|
86
86
|
# Adopt, inspect, and maintain a repository
|
|
87
87
|
simt init # adopt or scaffold; prints a plan; --yes writes
|
|
88
|
-
simt doctor
|
|
88
|
+
simt doctor [--fix] # stack, facts, evidence, and config health (--fix applies safe remedies, journaled)
|
|
89
89
|
simt check [--frozen] # projection drift, manifest, lock, and declared checkers
|
|
90
90
|
simt sync [--explain] --yes # regenerate the projection
|
|
91
91
|
simt upgrade --yes # move every consumer in the workspace to this SHA
|
|
@@ -103,6 +103,7 @@ simt why [path] # ADRs, rules, skills, gates, and lessons go
|
|
|
103
103
|
simt rules list [--root <path>] # rule catalogue and what applies to a repository
|
|
104
104
|
simt rules show <id> # read one rule body (including rules deferred in kernel mode)
|
|
105
105
|
simt rules why <id|ADR-NNN> [--all] # the decision that settled a rule, and whether it still stands
|
|
106
|
+
simt explain <CODE> [--json] # what a check finding means, why it is reported, and how to fix it
|
|
106
107
|
simt decide "<title>" --yes # scaffold the next ADR with structured simt-affects / simt-supersedes
|
|
107
108
|
simt lessons list # fail-to-fix, revert, review-block, and hook-denial candidates
|
|
108
109
|
simt lessons review <id> --yes # promote a candidate to docs/lessons/NNNN-<slug>.md
|
|
@@ -125,11 +126,20 @@ simt agent-hooks status # what each harness will run, and whether Co
|
|
|
125
126
|
simt agent-hook <event> --harness # evaluate pre-tool-use, post-tool-use, or prompt-submit
|
|
126
127
|
simt evidence # gate and test runs recorded against the current git tree
|
|
127
128
|
simt evidence check <msg-file> # verify a commit message does not claim unrecorded passing runs
|
|
129
|
+
simt pr-body [--base <ref>] # a PR body: the evidence block for this tree, changed
|
|
130
|
+
# decisions, gate state, and residual risk in ADR-008 vocabulary
|
|
128
131
|
|
|
129
132
|
# Plans, context metering, workspace learning, benchmarks, local models, and MCP
|
|
130
133
|
simt plan status # count queued, in-progress, complete, and accepted tracker rows
|
|
131
134
|
simt plan next [<id>] # print the next ready slice section (~1 KB)
|
|
132
135
|
simt plan done <id> [<sha>] --yes # mark a tracker row complete with its commit SHA and test command
|
|
136
|
+
simt status --line # one-line gate state from the last check, for prompts (< 50 ms)
|
|
137
|
+
simt undo --yes # restore the last journaled set of writes, if untouched since
|
|
138
|
+
simt slice start <id> --yes # a worktree on branch slice/<id> with the brief preloaded; row In progress
|
|
139
|
+
simt slice finish <id> --yes # run the pre-commit gates, commit a Conventional subject from the
|
|
140
|
+
# row's title, and record the SHA and --test command on the row
|
|
141
|
+
simt lease [<paths>] --session # claim files for this session (--ttl, default 1h) or list claims;
|
|
142
|
+
# the pre-edit hook refuses another session's edit until the TTL
|
|
133
143
|
simt tokens [--repos] [--session] # measure bytes loaded at session start or split a transcript
|
|
134
144
|
simt harness detect [--user] # detect configured harnesses at repo and user level
|
|
135
145
|
simt harness loaders [--table] # what each harness loads, in what order, and where its cap truncates
|
|
@@ -143,25 +153,37 @@ simt bench report [--compare a b] # resolved rate, quality, cost, CPI, bootstr
|
|
|
143
153
|
simt bench ablate --yes # leave-one-out rule ablation ranked by value per KB
|
|
144
154
|
simt bench probes --yes # 12 deterministic rule-compliance probes
|
|
145
155
|
simt bench variants --yes # evaluate simt-variants slots and lock winners in simt-dev.lock.json
|
|
156
|
+
simt review [--local] # an advisory ADR-008 verdict skeleton over the staged diff
|
|
157
|
+
simt distill <skill|doc> # a budgeted proposal summary with line citations, for a person to merge
|
|
146
158
|
simt pack new <name> --source <spec> # scaffold a versioned pack: pack.json, bench.json, one rule
|
|
147
159
|
simt pack lint [dir] [--require-measured] # validate a pack and state its measured ΔR
|
|
148
160
|
simt pack publish [dir] --yes # publish an npm-sourced pack (git packs name the tag-and-push)
|
|
149
|
-
simt mcp [--list] # serve the
|
|
161
|
+
simt mcp [--list] # serve the 15 read-only tools over stdio JSON-RPC
|
|
162
|
+
simt completions <bash|zsh|fish> # a completion script generated from the command registry
|
|
163
|
+
simt ignore [--yes] [--diff] # render the one .simtignore list into .cursorignore,
|
|
164
|
+
# .aiderignore, .geminiignore and Claude's permissions.deny
|
|
150
165
|
```
|
|
151
166
|
|
|
152
167
|
`init`, `sync`, `upgrade`, `hooks install`, `agent-hooks install`, `shared install`,
|
|
153
|
-
`skills install`, `plan done`, `lessons review`, `decide`, `
|
|
154
|
-
and the `bench` writers print
|
|
155
|
-
a plan and write nothing without `--yes`. None of them
|
|
168
|
+
`skills install`, `plan done`, `lessons review`, `decide`, `ignore`, `pack new`,
|
|
169
|
+
`pack publish`, `slice start`, `slice finish`, `lease` and the `bench` writers print
|
|
170
|
+
a plan and write nothing without `--yes`. None of them push, and only
|
|
171
|
+
`slice finish` commits — locally, through the pre-commit gates, and only what is
|
|
172
|
+
staged. `simt index`
|
|
156
173
|
writes its cache without asking: the file is gitignored and `git ls-files` can rebuild it.
|
|
157
174
|
|
|
175
|
+
Declare `secret-scan` in `gates.preCommit` to block a commit whose staged diff
|
|
176
|
+
carries a probable secret: the scan runs inside the binary (vendor patterns plus
|
|
177
|
+
Shannon entropy, over added lines only), needs no install, and its report never
|
|
178
|
+
prints the matched bytes.
|
|
179
|
+
|
|
158
180
|
`check` runs the checkers a rule names (`max-file-lines`, `forbid-regex`,
|
|
159
181
|
`forbidden-path-edit`, `required-file`), verifies structured ADR supersessions
|
|
160
182
|
(`prose-partial-supersession`), and flags expired lessons (`LESSON-EXPIRED`). A marked
|
|
161
183
|
rule line leaves the projection as `Enforced:` and the checker's name. The file-length
|
|
162
184
|
line warns past 400 lines and fails past 1000. `simt evidence check` warns when a commit
|
|
163
185
|
message claims a passing run this tree does not have, and the commit fails when
|
|
164
|
-
`gates.evidence` is `block`.
|
|
186
|
+
`gates.evidence` is `block`. Every finding `check` can emit is explained by `simt explain <CODE>` (DRIFT, UNMANAGED, PACK-DIGEST-MISMATCH, ...).
|
|
165
187
|
|
|
166
188
|
## Agent playbook: how and when to use each module
|
|
167
189
|
|
|
@@ -180,6 +202,7 @@ through `simt mcp`.
|
|
|
180
202
|
| Rule violation or error | `simt lessons list` | `simt rules why <rule-id>` for the decision |
|
|
181
203
|
| Working from a plan | `simt plan next` | `simt plan done <id> <sha> --yes` when finished |
|
|
182
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 |
|
|
183
206
|
| Repository adoption | `simt init --yes` | `simt doctor` to verify stack detection |
|
|
184
207
|
| Multi-repo workspace | `simt compare` | `simt shared plan` to find deduplication opportunities |
|
|
185
208
|
| Local model available | `simt local doctor` | `simt index --embed` to build vector embeddings |
|
|
@@ -312,16 +335,16 @@ returns a JSON object with `task`, `commands`, `files`, `tests`, `decisions`, `s
|
|
|
312
335
|
| **Governance & Provenance** | `simt why [path]`, `simt rules list`, `simt rules show <id>`, `simt rules why <id\|ADR-NNN>`, `simt decide` | `rules_list`, `rules_why` | Before touching governed files (`why`), when reading deferred kernel rules (`rules show`), or when checking/scaffolding ADRs (`rules why`, `decide`) |
|
|
313
336
|
| **Skills & Disclosure** | `simt skills index\|brief\|show\|why\|find\|install\|compile\|eval`, `simt graph lint` | `skills_index`, `skills_find`, `skills_brief`, `skills_show`, `skills_why` | When a task requires domain workflows or reference docs without blowing the session token budget |
|
|
314
337
|
| **Memory & Lessons** | `simt lessons list`, `simt lessons review <id> --yes` | — | After fixing a failing test/gate or hitting a hook denial, to inspect or promote reusable failure-to-fix lessons |
|
|
315
|
-
| **Plans & Trackers** | `simt plan status`, `simt plan next [<id>]`, `simt plan done <id> [<sha>] --yes` | `plan_next`, `plan_done` | When executing a phased implementation or review plan row by row |
|
|
338
|
+
| **Plans & Trackers** | `simt plan status`, `simt plan next [<id>]`, `simt plan done <id> [<sha>] --yes`, `simt slice start\|finish <id>`, `simt lease`, `simt pr-body`, `simt status --line`, `simt undo` | `plan_next`, `plan_done`, `pr_body` | When executing a phased implementation or review plan row by row |
|
|
316
339
|
| **Adoption & Projection** | `simt init`, `simt doctor`, `simt check [--frozen]`, `simt sync`, `simt upgrade`, `simt ci` | `doctor`, `check` | When adopting a repo, diagnosing stack/config health, or verifying zero drift before commit |
|
|
317
340
|
| **Dual Gates & Evidence** | `simt hooks install\|status\|run`, `simt agent-hooks install\|status`, `simt agent-hook`, `simt evidence [check]` | — | When installing git/agent hooks, running gates manually, or verifying test claims before committing |
|
|
318
341
|
| **Context & Harnesses** | `simt tokens [--repos] [--session]`, `simt harness detect\|loaders`, `simt shared plan\|install` | — | When auditing startup token cost across harnesses (`claude`, `codex`, `cursor`, `opencode`, `antigravity`, `copilot`, `deepseek`) or deduplicating workspace rules |
|
|
319
|
-
| **Workspace & Benchmarks** | `simt compare`, `simt bench mine\|run\|report\|ablate\|probes\|variants`, `simt local doctor` | `compare` | When auditing an entire multi-repo estate, measuring rule uplift/cost, or probing local model endpoints |
|
|
342
|
+
| **Workspace & Benchmarks** | `simt compare`, `simt bench mine\|run\|report\|ablate\|probes\|variants`, `simt local doctor`, `simt review [--local]`, `simt distill` | `compare` | When auditing an entire multi-repo estate, measuring rule uplift/cost, or probing local model endpoints |
|
|
320
343
|
|
|
321
344
|
## Where the plan finishes
|
|
322
345
|
|
|
323
|
-
Slices `R01`–`
|
|
324
|
-
|
|
346
|
+
Slices `R01`–`R55` are all complete. The small ergonomics (`R46`–`R55`) shipped
|
|
347
|
+
on top of `0.6.0`:
|
|
325
348
|
|
|
326
349
|
- **The small doors (`R46`–`R55`).** `simt explain <CODE>` for every finding code, safe
|
|
327
350
|
idempotent `simt doctor --fix`, a `< 50 ms` `simt status --line`, a write journal with
|
|
@@ -329,6 +352,12 @@ small ergonomics:
|
|
|
329
352
|
plus a staged-diff secret scan, `simt slice start/finish <ID>` worktrees, file leases
|
|
330
353
|
for parallel agents, and `simt pr-body` assembled from the evidence ledger.
|
|
331
354
|
|
|
355
|
+
The last two rows of review `0002` (`S38` workspace token cost, `S39` estate policy
|
|
356
|
+
as a pack) are closed by ruling in ADR-102: the instrument, the declaration and the
|
|
357
|
+
non-blocking finding shipped, the blocking gate and the registry-carrying pack were
|
|
358
|
+
refused by ruling, and the estate-side move is recorded as whose commit it waits on.
|
|
359
|
+
Everything in plans `0001`–`0003` and reviews `0001`–`0002` is Complete or Accepted.
|
|
360
|
+
|
|
332
361
|
The local-model slices (`R38`–`R40`) shipped as they lab-written: `simt distill` proposes
|
|
333
362
|
budgeted summaries with line citations, `simt review` proposes an advisory ADR-008
|
|
334
363
|
verdict over the staged diff, and `simt brief` carries model/effort advice from bench
|
package/bin/simt.mjs
CHANGED
|
@@ -4,26 +4,33 @@
|
|
|
4
4
|
// POSIX only (ADR-012). package.json declares os: [darwin, linux], so npm refuses
|
|
5
5
|
// the install elsewhere rather than letting a hook fail obscurely.
|
|
6
6
|
|
|
7
|
-
import { parse } from '../src/argv.mjs';
|
|
7
|
+
import { parse, rawJsonFlag } from '../src/argv.mjs';
|
|
8
8
|
import { EXIT, fail, jsonFailMode, jsonIndentDefault } from '../src/output/index.mjs';
|
|
9
9
|
|
|
10
10
|
try {
|
|
11
11
|
const argv = process.argv.slice(2);
|
|
12
12
|
const parsed = parse(argv);
|
|
13
13
|
if (!parsed.ok) {
|
|
14
|
-
|
|
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));
|
|
15
19
|
}
|
|
16
20
|
const { flags, positional } = parsed.value;
|
|
17
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
|
+
|
|
18
28
|
// No arguments at all is a usage error, not a silent help dump: an agent that
|
|
19
29
|
// ran the wrong thing should be told so rather than handed 40 lines of prose.
|
|
20
30
|
if (positional.length === 0 && !flags.help && !flags.version) {
|
|
21
31
|
process.exit(fail('no command given', EXIT.USAGE));
|
|
22
32
|
}
|
|
23
33
|
|
|
24
|
-
jsonIndentDefault(flags.pretty === true);
|
|
25
|
-
jsonFailMode(flags.json === true);
|
|
26
|
-
|
|
27
34
|
// The one command that runs on somebody else's hot path, and therefore the one that
|
|
28
35
|
// does not load the registry. `agent-hook` is called before every tool call an agent
|
|
29
36
|
// makes, so its latency is a product requirement rather than a nicety (plan 0003
|
|
@@ -41,7 +48,13 @@ try {
|
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
const { dispatch } = await import('../src/commands/index.mjs');
|
|
44
|
-
|
|
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;
|
|
45
58
|
} catch (error) {
|
|
46
59
|
process.exit(fail(error?.message ?? String(error), EXIT.FAIL));
|
|
47
60
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-affects: ["src/hooks/gates.mjs", "src/hooks/resolve.mjs", "src/hooks/dispatcher.mjs", "src/projection/manifest.mjs", "src/secrets.mjs"]
|
|
3
|
+
simt-supersedes: []
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ADR-100 — a staged diff is the last gate before history, and it runs inside the tool
|
|
7
|
+
|
|
8
|
+
Status: **accepted** · 2026-10-27
|
|
9
|
+
|
|
10
|
+
Affects: `src/hooks/gates.mjs`, `src/hooks/resolve.mjs`,
|
|
11
|
+
`src/hooks/dispatcher.mjs`, `src/projection/manifest.mjs`, `src/secrets.mjs`
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
Git history is append-only: a secret committed is a rotation exercise, not a
|
|
16
|
+
revert, and the one moment it can still be kept out is the commit. Every other
|
|
17
|
+
leak path in this tool is already gated — the projection, the manifest, the
|
|
18
|
+
evidence claim — but nothing gated the staged diff.
|
|
19
|
+
|
|
20
|
+
The three existing gate kinds cannot express this check. `staged` hands files
|
|
21
|
+
to a formatter binary in `node_modules/.bin`, `script` runs a `package.json`
|
|
22
|
+
script, and `command` runs a line the manifest declares: each resolves
|
|
23
|
+
something ON DISK, and each therefore has a way to be missing. A secret scan
|
|
24
|
+
has nothing to resolve — the implementation is the question being asked — and
|
|
25
|
+
making it a `command` gate would mean a repository declares `simt secrets`
|
|
26
|
+
(or installs a third-party scanner) before the check exists at all. That is
|
|
27
|
+
backwards for the repositories most at risk, which are frequently the ones with
|
|
28
|
+
the least toolchain to install into.
|
|
29
|
+
|
|
30
|
+
Plan 0003 R52 asks for both halves from one list and one binary: a
|
|
31
|
+
zero-dependency staged-diff scan (vendor patterns plus entropy), and no
|
|
32
|
+
repository needing anything to run it.
|
|
33
|
+
|
|
34
|
+
## Ruling
|
|
35
|
+
|
|
36
|
+
1. **A fourth gate kind, `builtin`, runs inside this binary.** It is declared
|
|
37
|
+
in the manifest like any other gate, a `gates.commands` entry overrides it
|
|
38
|
+
exactly as it overrides every other kind (the dispatcher's precedence is
|
|
39
|
+
unchanged), and its result flows through the same ledger line, the same
|
|
40
|
+
fail-closed reporting and the same `hooks status` view. What it never does
|
|
41
|
+
is resolve anything on disk: there is no script to miss, no formatter to
|
|
42
|
+
install and no command to declare, so `gateRunnable` reports it runnable and
|
|
43
|
+
`check` counts it as such. A `builtin` name with no implementation fails the
|
|
44
|
+
event with the name of the gate rather than passing.
|
|
45
|
+
|
|
46
|
+
2. **`secret-scan` reads only the added lines of `git diff --cached`.** Two
|
|
47
|
+
detectors, both local: a pattern library for the shapes vendors publish
|
|
48
|
+
(AWS key ids, private key blocks, GitHub/Slack/Stripe/Google/OpenAI-style
|
|
49
|
+
tokens, JWTs, assigned secrets) and Shannon entropy (≥ 4.5 bits per
|
|
50
|
+
character over runs of ≥ 32 base64-ish characters) for the blobs no vendor
|
|
51
|
+
named. Values carrying placeholder markers (`example`, `changeme`,
|
|
52
|
+
`your_`, …) are documentation, not findings. Context lines are not
|
|
53
|
+
scanned: blocking a commit over bytes the author did not stage reproduces
|
|
54
|
+
the prohibition `format`'s staged-only rule exists for. If git cannot
|
|
55
|
+
answer, the gate fails rather than reading silence as a clean diff.
|
|
56
|
+
|
|
57
|
+
3. **A finding never prints the matched bytes.** The report carries the
|
|
58
|
+
pattern id, the file, the line and a four-character prefix — the part a
|
|
59
|
+
vendor publishes as a type (`AKIA`, `sk-`, `ghp_`). Hook output lands in a
|
|
60
|
+
terminal, a CI log and often an agent's own transcript, and echoing the
|
|
61
|
+
secret into the report about the secret is the leak with extra steps.
|
|
62
|
+
|
|
63
|
+
4. **The gate is opt-in, not an `init` default.** Entropy has a false-positive
|
|
64
|
+
rate no threshold eliminates, and a gate that blocks a first commit over a
|
|
65
|
+
sha256 digest teaches exactly the `--no-verify` habit the dispatcher exists
|
|
66
|
+
to prevent. A repository opts in by adding `secret-scan` to
|
|
67
|
+
`gates.preCommit`; from then on it is reported, counted and fail-closed like
|
|
68
|
+
every other declared gate.
|
|
69
|
+
|
|
70
|
+
## Consequences
|
|
71
|
+
|
|
72
|
+
- A repository gets the scan by declaring the gate; nothing is installed,
|
|
73
|
+
nothing is fetched at hook time, and a fresh clone runs it identically.
|
|
74
|
+
- `unresolved()` and `gateRunnable()` know `builtin` as a runnable kind, so
|
|
75
|
+
`check` no longer has a gate it must call unrunnable-but-isn't; `hooks
|
|
76
|
+
status` prints `run builtin` for the entry.
|
|
77
|
+
- A `gates.commands["secret-scan"]` declaration replaces the built-in scan
|
|
78
|
+
with whatever line the repository names — the escape hatch every other kind
|
|
79
|
+
has, preserved for this one.
|
|
80
|
+
- The matcher lives in `src/secrets.mjs` and is unit-tested against sentinel
|
|
81
|
+
values, including the negative cases (hex digests, UUIDs, placeholders) that
|
|
82
|
+
decide whether the threshold is usable rather than merely strict.
|
|
83
|
+
|
|
84
|
+
## Rejected
|
|
85
|
+
|
|
86
|
+
- **Shipping a third-party scanner (gitleaks, trufflehog).** It violates the
|
|
87
|
+
zero-dependency rule outright, and a hook that downloads a scanner at commit
|
|
88
|
+
time is a hook that fails on an offline machine — the failure mode ADR-010's
|
|
89
|
+
no-fetch-at-hook-time rule already refuses for `npx`.
|
|
90
|
+
- **Scanning the working tree or the whole repository.** That flags secrets
|
|
91
|
+
already in history on every subsequent commit (unfixable by the author of
|
|
92
|
+
this commit) and scans unstaged work nobody was about to record. The staged
|
|
93
|
+
diff is precisely the bytes the next commit would make permanent, and it is
|
|
94
|
+
the only bytes this gate claims to judge.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-affects: ["src/lease.mjs", "src/commands/lease.mjs", "src/agenthooks/policy.mjs", "src/agenthooks/decide.mjs"]
|
|
3
|
+
simt-supersedes: []
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ADR-101 — a file lease belongs to one session, and the pre-edit hook refuses another's edit
|
|
7
|
+
|
|
8
|
+
Status: **accepted** · 2026-10-28
|
|
9
|
+
|
|
10
|
+
Affects: `src/lease.mjs`, `src/commands/lease.mjs`,
|
|
11
|
+
`src/agenthooks/policy.mjs`, `src/agenthooks/decide.mjs`
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
Parallel agents share one working tree, and the cheapest collision between them
|
|
16
|
+
is two sessions editing the same file at once: each edit is valid alone, and
|
|
17
|
+
the pair is neither — one session's work is overwritten by the other's save,
|
|
18
|
+
with no gate, no diff and no hook that ever sees a conflict. Git notices this
|
|
19
|
+
only at merge time, which is far after the fact either session can explain.
|
|
20
|
+
|
|
21
|
+
Plan 0003 R54 asks for a claim mechanism: `simt lease <paths>` records which
|
|
22
|
+
session is working on which path, with a TTL, and the pre-edit half of the
|
|
23
|
+
agent hook denies an edit from a session that does not hold the claim.
|
|
24
|
+
|
|
25
|
+
The policy table is where every other agent-time refusal lives (`push`,
|
|
26
|
+
`rewrite`, `generated`), so a lease refusal belongs there too — one table,
|
|
27
|
+
four harness dialects, one message. What the table has never had is a fact
|
|
28
|
+
that is not in the payload: which session is asking, and who else has claimed
|
|
29
|
+
the path.
|
|
30
|
+
|
|
31
|
+
## Ruling
|
|
32
|
+
|
|
33
|
+
1. **A lease is a claim, not a lock.** `.simt/leases.json` records one entry
|
|
34
|
+
per path: the repo-relative path, the session name, and an absolute expiry
|
|
35
|
+
timestamp. It is taken with `simt lease <paths>` (preview unless `--yes`),
|
|
36
|
+
the session comes from `--session` or `SIMT_SESSION`, and the TTL defaults
|
|
37
|
+
to an hour. There is no lock on the file and no daemon: a claim is advisory
|
|
38
|
+
data that expires on its own, last writer wins, and a claim lost to a race
|
|
39
|
+
is simply re-made. Coordination machinery would be a system built around a
|
|
40
|
+
file a TTL already cleans up.
|
|
41
|
+
|
|
42
|
+
2. **A live claim held by a different session refuses a re-claim.** Stealing
|
|
43
|
+
is what expiry is for: the refusal names the holder and the expiry, and
|
|
44
|
+
the session that wants the path waits for the TTL or asks the holder. The
|
|
45
|
+
holder may re-claim (refresh) its own paths at any time.
|
|
46
|
+
|
|
47
|
+
3. **The pre-edit hook denies an edit to a path another session holds.** The
|
|
48
|
+
check joins two facts at decision time: the active claims in the file, and
|
|
49
|
+
the asking session's name from `SIMT_SESSION` — one variable for the claim
|
|
50
|
+
and the check, because a denial is only explainable if the session it
|
|
51
|
+
protected is the same name the claim was filed under. A session that cannot
|
|
52
|
+
prove ownership is treated as another session: an unrecognised edit is
|
|
53
|
+
refused with the holder's name, not waved through. Expiry is read when the
|
|
54
|
+
hook runs, so a TTL needs no sweeper, and a claim with no file (the default
|
|
55
|
+
state of every repository) denies nothing.
|
|
56
|
+
|
|
57
|
+
4. **An unreadable claim file fails closed for edits and only for edits.**
|
|
58
|
+
A `.simt/leases.json` that does not parse is evidence someone — possibly a
|
|
59
|
+
session routing around the check — broke the claims the hook is about to
|
|
60
|
+
enforce, so any write is refused until the file is fixed or removed; a
|
|
61
|
+
`lease` command refuses to write over it for the same reason. A shell
|
|
62
|
+
command with no file fact is not an edit and is not refused. This mirrors
|
|
63
|
+
the fail-closed contract every gate has, scoped to the question the lease
|
|
64
|
+
can actually answer.
|
|
65
|
+
|
|
66
|
+
5. **The refusal is the `lease` policy, with the table's full machinery.** It
|
|
67
|
+
carries `ADR-101`, is lifted by `SIMT_ALLOW=lease` exactly as every other
|
|
68
|
+
policy is, exits with the harness's deny code, and its message names the
|
|
69
|
+
held paths, the holders and the expiry in ISO — the three things the
|
|
70
|
+
blocked session needs to act. The per-instance detail is a `detail` hook on
|
|
71
|
+
the row, so the message that quotes a timestamp does not become a special
|
|
72
|
+
case inside `decide`.
|
|
73
|
+
|
|
74
|
+
## Consequences
|
|
75
|
+
|
|
76
|
+
- Two sessions in one working tree can coordinate with one command and no
|
|
77
|
+
shared process; a crashed session's claims lapse on their own.
|
|
78
|
+
- The hook's hot path gains one small `readFile` of a file that is usually
|
|
79
|
+
absent, and only when the payload named a file to write.
|
|
80
|
+
- `learn.mjs` reads policy rows by `id` and `adr` only, so the `lease` row
|
|
81
|
+
joins the estate's violation aggregation with no further wiring.
|
|
82
|
+
- The claim file is local state under `.simt` like the index and the journal:
|
|
83
|
+
never projected, never committed.
|
|
84
|
+
|
|
85
|
+
## Rejected
|
|
86
|
+
|
|
87
|
+
- **File locking (`flock`, a lockfile protocol).** A lock that blocks is a
|
|
88
|
+
deadlock with a crashed session in it, and it needs the very daemon ruling 1
|
|
89
|
+
refuses. The TTL already bounds every claim.
|
|
90
|
+
- **Inferring leased paths from shell commands.** Only the edit tools name a
|
|
91
|
+
file with no guesswork; guessing at what a `sh -c` line will write is the
|
|
92
|
+
payload-shaped false positive the policy table was built to avoid — no fact
|
|
93
|
+
is not a violation.
|
|
94
|
+
- **A lease the holder must release to finish.** Forgetting to release is the
|
|
95
|
+
expected failure of any release-based protocol; an expiry is not.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-affects: ["docs/reviews/0002-full-review-adoption-and-public-release.md"]
|
|
3
|
+
simt-supersedes: []
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ADR-102 — the two rows review 0002 left queued are closed by ruling
|
|
7
|
+
|
|
8
|
+
Status: **accepted** · 2026-10-28
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
Review 0002's §3 tracker is the only tracker of slice state, and two rows in it
|
|
13
|
+
still read Queued while everything around them shipped: S38 (workspace token
|
|
14
|
+
cost) and S39 (estate policy as a pack). Both rows' evidence cells are
|
|
15
|
+
palimpsests — notes from three different landings disagreeing about what is
|
|
16
|
+
still open — so reading them as a work list sends the next reader to build what
|
|
17
|
+
is already built. Closing plan 0001 meant the same audit, and ADR-033 is the
|
|
18
|
+
precedent: rows closed by saying what shipped, what was refused by ruling, and
|
|
19
|
+
where the remainder lives, not by authoring what they asked for.
|
|
20
|
+
|
|
21
|
+
## Ruling
|
|
22
|
+
|
|
23
|
+
1. **S38 is Complete.** The instrument shipped (`simt tokens`, ADR-064), the
|
|
24
|
+
declaration shipped (`budget.bytes` beside `limits` and `gates`, ADR-072),
|
|
25
|
+
and the gate raises it as a finding through one `budgetFinding` read from
|
|
26
|
+
one place by both `tokens` and `check` — the "still open" clause the row's
|
|
27
|
+
first note ends on. Kernel-line packing shipped with it (R07's `mode:
|
|
28
|
+
kernel`, R08's budget compilation). Blocking enforcement is refused, not
|
|
29
|
+
outstanding: a gate that fails a repository for exceeding a ceiling it set
|
|
30
|
+
itself, when nothing was cut, has no repair except lowering the declared
|
|
31
|
+
number (ADR-072). The one genuine remainder is the kernel-default
|
|
32
|
+
compliance study, and it is owned by plan 0003 R07's own note, not by this
|
|
33
|
+
row — the study gates making `kernel` the default, which no row asks to
|
|
34
|
+
decide here.
|
|
35
|
+
|
|
36
|
+
2. **S39 is Complete.** The mechanism shipped (packs vendored and digest-pinned,
|
|
37
|
+
ADR-065; measured ΔR, plan 0003 R45). P4's pack-carries-the-registry line was
|
|
38
|
+
superseded by ADR-061, which made the registry workspace data, so there is
|
|
39
|
+
no registry-carrying pack to build. The remaining half — moving estate
|
|
40
|
+
policy into a pack — is the estate's own commit: no estate content may enter
|
|
41
|
+
this repository, and a row that waits on another repository's commit is
|
|
42
|
+
closed by recording whose commit it waits on.
|
|
43
|
+
|
|
44
|
+
## Consequences
|
|
45
|
+
|
|
46
|
+
- The §3 tracker reads Complete on both rows, with this record as the evidence,
|
|
47
|
+
so the next audit starts from rulings rather than from mid-flight notes.
|
|
48
|
+
- A reader asking for a blocking budget gate or a registry-carrying pack gets
|
|
49
|
+
the refusal and its reason instead of an open row that looks like a promise.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
simt-affects: ["rules/stacks/jvm.md", "rules/stacks/android.md", "rules/stacks/react-web.md", "rules/stacks/react-native.md", "rules/stacks/cloudflare-workers.md", "rules/stacks/container.md", "rules/mdc/preview-postflight.mdc"]
|
|
3
|
+
simt-supersedes: []
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ADR-103 — duplicates stand alone, fingerprints generalize, references spell out
|
|
7
|
+
|
|
8
|
+
Status: **accepted** · 2026-10-28
|
|
9
|
+
|
|
10
|
+
## Context
|
|
11
|
+
|
|
12
|
+
Twenty-four rule files ship: one core, nineteen stack profiles, four process
|
|
13
|
+
rule files. An audit of all of them found three kinds of defect and one thing
|
|
14
|
+
that looks like a defect but is not:
|
|
15
|
+
|
|
16
|
+
1. **Verbatim duplicates inside one projection.** An Android repository selects
|
|
17
|
+
both `jvm` (gradle) and `android` (manifest), so it received the gradlew
|
|
18
|
+
invocation, the Spotless entrypoint, the `:core:domain` direction rule and
|
|
19
|
+
the assert-behaviour testing paragraph twice, in two sections of one region.
|
|
20
|
+
2. **Estate fingerprints.** Literal module paths (`:core:domain`), a custom
|
|
21
|
+
check name (`checkOverlayModalPolicy`), an allowlisted surface (`core:ui`),
|
|
22
|
+
a string-resource prefix (`cd_`), product nouns (`block`, `reveal`),
|
|
23
|
+
an Arabic-only mirroring rule, a locale-parity claim about the estate's
|
|
24
|
+
tests, a privacy-probe list naming the estate's data categories, and a
|
|
25
|
+
preview-postflight file whose defaults name the estate's env vars, Firebase
|
|
26
|
+
identity and Google-only policy.
|
|
27
|
+
3. **Two dangling references.** `react-web` named "the full strictness set
|
|
28
|
+
listed in the Workers profile", which a browser-only repository never
|
|
29
|
+
receives; `react-native` said "see that profile", pointing at a profile a
|
|
30
|
+
native-only repository never receives. Both fail the receiving-repo test:
|
|
31
|
+
a reader who has only their own repository cannot act on them.
|
|
32
|
+
4. **Not a defect: duplicates across mutually exclusive profiles.**
|
|
33
|
+
`react-web` and `react-native`, `go` and `rust`, `deno` and `node` repeat
|
|
34
|
+
maxims, but no repository receives both sides. Merging them needs an
|
|
35
|
+
include mechanism profiles do not have, and a pointer to a non-projected
|
|
36
|
+
profile is defect 3 again. Each profile stands alone.
|
|
37
|
+
|
|
38
|
+
## Ruling
|
|
39
|
+
|
|
40
|
+
1. **Co-projected duplicates stay duplicated: the merge was attempted and
|
|
41
|
+
refused by a render test.** `android` pointed at the `jvm` section for
|
|
42
|
+
invocation, formatting and the shared suffixes — and a manifest-only
|
|
43
|
+
fixture (an `AndroidManifest.xml` with no root Gradle file) selected
|
|
44
|
+
`android` without `jvm`, leaving the pointers dangling. Gradle at the root
|
|
45
|
+
is usual, not guaranteed, and a rule that is present only usually is
|
|
46
|
+
silently absent exactly when its reader needs it. So `android` stays
|
|
47
|
+
self-contained and the standing jvm/android overlaps are recorded rather
|
|
48
|
+
than merged: merging them needs a profile-implication mechanism profiles
|
|
49
|
+
do not have, and that mechanism needs its own record. No hard-priority
|
|
50
|
+
lines move, so kernel bytes do not move either way.
|
|
51
|
+
2. **Fingerprints generalize in place; no profile is removed and no id is
|
|
52
|
+
renamed.** `:core:domain` becomes the core domain module; the modal rule
|
|
53
|
+
keeps the public M3 names and loses the estate check and surface; `cd_`
|
|
54
|
+
becomes a content description; `block`/`reveal` become the generic nouns;
|
|
55
|
+
mirroring covers RTL locales, not Arabic alone; the locale-parity sentence
|
|
56
|
+
and the product data categories go; preview-postflight's env vars, Firebase
|
|
57
|
+
identity and Google-only defaults become neutral wording with the slots
|
|
58
|
+
repos already declare. `rules list`, the catalogue and selection behaviour
|
|
59
|
+
are unchanged.
|
|
60
|
+
3. **The two dangling references are spelled out.** `react-web` inlines the
|
|
61
|
+
strictness flags; `react-native` names browser rules instead of pointing at
|
|
62
|
+
them.
|
|
63
|
+
|
|
64
|
+
## Consequences
|
|
65
|
+
|
|
66
|
+
- Regions change only where a fingerprint generalized or a dangling reference
|
|
67
|
+
was spelled out; `android` is byte-comparable to before modulo the
|
|
68
|
+
generalizations. `mdc.test.mjs`'s build-config test follows the renamed
|
|
69
|
+
default; its intent (the union survives) is unchanged.
|
|
70
|
+
- A repository that opted into preview-postflight on the estate defaults sees
|
|
71
|
+
neutral wording and declares its own provider values through the slots that
|
|
72
|
+
already exist.
|
|
73
|
+
- Mutually exclusive duplicates stay duplicated on purpose. A future include
|
|
74
|
+
mechanism needs its own record; this one refuses to invent it by pointer.
|