simt-dev 0.6.0 → 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.
Files changed (79) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/README.md +44 -15
  3. package/bin/simt.mjs +19 -6
  4. package/docs/decisions/ADR-098-the-fourth-channel-is-one-file-and-it-needs-no-toolchain.md +1 -1
  5. package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +79 -0
  6. package/docs/decisions/ADR-100-a-staged-diff-is-the-last-gate-before-history-and-it-runs-inside-the-tool.md +94 -0
  7. package/docs/decisions/ADR-101-a-file-lease-belongs-to-one-session-and-the-pre-edit-hook-refuses-another.md +95 -0
  8. package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +49 -0
  9. package/docs/decisions/ADR-103-co-projected-duplicates-merge-down.md +74 -0
  10. package/docs/decisions/ADR-104-the-pre-tool-use-policy-reads-the-command-the-way-a-shell-reads-it.md +76 -0
  11. package/docs/decisions/ADR-105-the-json-ceiling-shrinks-arrays-and-never-cuts-bytes.md +44 -0
  12. package/docs/decisions/README.md +44 -0
  13. package/package.json +1 -1
  14. package/rules/mdc/preview-postflight.mdc +5 -6
  15. package/rules/stacks/android.md +7 -8
  16. package/rules/stacks/cloudflare-workers.md +1 -1
  17. package/rules/stacks/container.md +2 -0
  18. package/rules/stacks/jvm.md +1 -1
  19. package/rules/stacks/react-native.md +3 -5
  20. package/rules/stacks/react-web.md +9 -9
  21. package/src/agenthooks/decide.mjs +101 -10
  22. package/src/agenthooks/policy.mjs +81 -5
  23. package/src/agenthooks/shell.mjs +523 -0
  24. package/src/argv.mjs +54 -9
  25. package/src/ci/workflow.mjs +7 -5
  26. package/src/commands/agenthook.mjs +25 -3
  27. package/src/commands/ci.mjs +28 -3
  28. package/src/commands/completions.mjs +27 -0
  29. package/src/commands/decide.mjs +14 -1
  30. package/src/commands/doctor.mjs +367 -0
  31. package/src/commands/explain.mjs +52 -0
  32. package/src/commands/help.mjs +66 -17
  33. package/src/commands/hooks.mjs +208 -60
  34. package/src/commands/ignore.mjs +152 -0
  35. package/src/commands/index.mjs +54 -100
  36. package/src/commands/init.mjs +82 -12
  37. package/src/commands/lease.mjs +136 -0
  38. package/src/commands/lessons.mjs +12 -0
  39. package/src/commands/mcp.mjs +2 -2
  40. package/src/commands/pack.mjs +9 -0
  41. package/src/commands/plan.mjs +13 -1
  42. package/src/commands/plugin.mjs +16 -1
  43. package/src/commands/prbody.mjs +45 -0
  44. package/src/commands/repoindex.mjs +3 -0
  45. package/src/commands/rules.mjs +145 -105
  46. package/src/commands/shared.mjs +36 -5
  47. package/src/commands/skills.mjs +293 -205
  48. package/src/commands/skillsinstall.mjs +100 -30
  49. package/src/commands/slice.mjs +353 -0
  50. package/src/commands/status.mjs +62 -0
  51. package/src/commands/sync.mjs +110 -15
  52. package/src/commands/undo.mjs +106 -0
  53. package/src/commands/upgrade.mjs +70 -8
  54. package/src/completions.mjs +191 -0
  55. package/src/diff.mjs +254 -0
  56. package/src/explain.mjs +258 -0
  57. package/src/federation/config.mjs +44 -0
  58. package/src/graph/load.mjs +14 -5
  59. package/src/hooks/dispatcher.mjs +110 -0
  60. package/src/hooks/gates.mjs +14 -3
  61. package/src/hooks/resolve.mjs +14 -0
  62. package/src/ignore.mjs +133 -0
  63. package/src/index/persist.mjs +66 -9
  64. package/src/journal.mjs +326 -0
  65. package/src/lease.mjs +113 -0
  66. package/src/mcp/server.mjs +15 -1
  67. package/src/mcp/tools.mjs +296 -256
  68. package/src/memory/decide.mjs +6 -1
  69. package/src/memory/lessons.mjs +12 -0
  70. package/src/output/index.mjs +82 -12
  71. package/src/package-version.mjs +49 -1
  72. package/src/packs/manage.mjs +13 -4
  73. package/src/plan/index.mjs +6 -0
  74. package/src/plugins.mjs +27 -6
  75. package/src/prbody.mjs +251 -0
  76. package/src/projection/manifest.mjs +13 -2
  77. package/src/projection/shared.mjs +4 -1
  78. package/src/secrets.mjs +231 -0
  79. package/src/status.mjs +152 -0
@@ -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.
@@ -0,0 +1,76 @@
1
+ ---
2
+ simt-affects: ["src/agenthooks/shell.mjs", "src/agenthooks/policy.mjs", "src/agenthooks/decide.mjs"]
3
+ simt-supersedes: []
4
+ ---
5
+
6
+ # ADR-104 — the pre-tool-use policy reads the command the way a shell reads it
7
+
8
+ Status: **accepted** · 2026-10-28
9
+
10
+ ## Context
11
+
12
+ The two command policies — push (ADR-001) and rewrite (ADR-017) — matched the
13
+ payload's command string with a regular expression. Review 0003 (Q01) verified
14
+ against the shipped binary that an agent routes around both of them: `git -C .
15
+ push`, `git --no-pager push`, `sh -c 'git push'`, `$(git push)`, `(git push)`,
16
+ `git push;echo ok`, the alias `git p`, `git send-pack`, `git commit -n`,
17
+ `--amen`, `--no-verif`, `git reset --har`, `git -C . commit --amend`,
18
+ `git pull --rebase`, and with `SIMT_ALLOW=push`, `git push -fu` and
19
+ `git push origin +main`. Three acts no policy named at all: `git -c
20
+ core.hooksPath=/dev/null commit`, `git config core.hooksPath …` and
21
+ `rm -rf .simt-hooks` — the repository's gates going away without a policy ever
22
+ seeing them. And two false positives: `echo --no-verify` and `grep -r "git
23
+ rebase" docs` were denied because a flag or a phrase sat in a command that was
24
+ not git.
25
+
26
+ Every bypass is the same act spelled another way, and a policy that reads text
27
+ cannot tell them apart. The decision is made on the act.
28
+
29
+ ## Ruling
30
+
31
+ 1. **The command is normalised before a policy matches it.**
32
+ `src/agenthooks/shell.mjs` tokenises the command the way POSIX sh tokenises
33
+ it — quotes, escapes, `; && || | &`, newlines, subshells, `$()` and
34
+ backticks — descends into the wrappers a shell executes for you (`sh|bash|
35
+ zsh|dash -c`, `eval`, `env`, `command`, `exec`, `xargs`, `nice`, `time`,
36
+ `sudo`), and reduces every git invocation to the fact a policy can read:
37
+ which subcommand, with which options, in which repository. Long options are
38
+ resolved by git's unique-prefix rule (`--amen` is `--amend`), short clusters
39
+ are expanded (`-fu` is `-f -u`), and a leading `+` on a refspec is force.
40
+ The policies match that fact, not the text.
41
+ 2. **A command the tokenizer cannot read is judged by the row's regex, never by
42
+ nothing.** Unbalanced quotes and an unclosed `$()` mark the command as
43
+ unparseable, and the pre-normaliser regex runs on it, so a parse failure
44
+ weakens nothing. Facts that were never normalised — a test that hands
45
+ `decide` a bare command string — take the same path. The push and rewrite
46
+ ids and messages are unchanged, so an adopted repository reads the same
47
+ denial it read before.
48
+ 3. **A third command policy, `hooks-disable`, names the act the other two
49
+ cannot:** removing or rerouting the git hooks a repository's gates run in —
50
+ `git -c core.hooksPath=…`, `git config … core.hooksPath`, and `rm` of the
51
+ hooks directory. It carries no regex fallback because it did not exist
52
+ before this ruling: a command that cannot be tokenised is judged exactly as
53
+ it was judged before the row was written.
54
+ 4. **The author's aliases are resolved with one `git config --get-regexp
55
+ '^alias\.'` per command, and only when a fact's subcommand is not one git
56
+ ships.** A value that starts with `!` is a shell command and is read the
57
+ way any other command string is; an alias that resolves to nothing the
58
+ table can read is left as-is, which is the same position the pre-lookup
59
+ policy found it in. A git that is missing or cannot answer yields no
60
+ aliases, and the facts stand where the pre-lookup policy would have left
61
+ them.
62
+
63
+ ## Consequences
64
+
65
+ - The false positives go with the text: `echo --no-verify` and `grep -r "git
66
+ rebase" docs` carry no git fact, so the rewrite policy does not fire on
67
+ them.
68
+ - Every force spelling — `--force`, `-f` in a cluster, `+refspec` — is both a
69
+ push and a rewrite, so lifting `push` no longer carries a force-push
70
+ through, in any of the spellings the regex let past.
71
+ - The decision stays pure and fast: normalisation is a fact-gathering step in
72
+ `factsFor`, and `decide` reads facts. The latency test, which measures
73
+ `decide` over prebuilt facts, is unchanged.
74
+ - A command that names only builtin subcommands pays nothing for the lookup;
75
+ one that names an alias pays a single `git config` spawn with a one-second
76
+ ceiling.
@@ -0,0 +1,44 @@
1
+ ---
2
+ simt-affects: ["src/output/index.mjs"]
3
+ simt-supersedes: []
4
+ ---
5
+
6
+ # ADR-105 — the JSON ceiling shrinks arrays and never cuts bytes
7
+
8
+ Status: **accepted** · 2026-10-29 · filed as review 0003 Q10
9
+
10
+ ## Context
11
+
12
+ ADR-070 ruling 5 gave `emitJson` the byte ceiling: a document over `--max-bytes`
13
+ was cut at the byte count with a `TRUNCATED` line after it, the same announcement
14
+ the text renderer prints. Compact JSON has no newlines, so the cut landed
15
+ mid-token and the command exited 0: `why --json --max-bytes 120` answered with a
16
+ document no parser accepts and a status that called it a success.
17
+
18
+ ## Ruling
19
+
20
+ 1. **JSON is never byte-truncated.** `emitJson` shrinks the payload to fit: the
21
+ longest arrays empty first, and the document carries
22
+ `truncated: { field, omitted, limit }` (or `fields: [{ field, omitted }]`
23
+ when several lists shrank) naming exactly which list lost how many items.
24
+ 2. **A ceiling no trimming can fit is a `MAX_BYTES` error envelope, not a cut.**
25
+ `{ ok: false, error: { code: 'MAX_BYTES', message, remedy } }` with
26
+ `truncated: { omitted, limit }`, still one parseable document. The remedy
27
+ reads `rerun with --max-bytes 0 for the whole document` — the same ceiling
28
+ the text renderer names, in the same words.
29
+ 3. **The text ceiling is unchanged.** `emit` still cuts lines and announces the
30
+ omitted bytes; prose has line boundaries worth cutting at, JSON does not.
31
+
32
+ ## Consequences
33
+
34
+ - Every `--json --max-bytes` answer parses, trimmed or enveloped. A reader can
35
+ trust that a document on stdout is the whole answer when no `truncated` key
36
+ rides with it.
37
+ - `truncated` is additive: readers that ignore unknown fields see today's shape
38
+ with shorter lists, so no schema bump.
39
+
40
+ ## Rejected
41
+
42
+ - **Cutting at a newline for JSON too.** Compact output has none and pretty
43
+ output's newlines are indentation, not item boundaries; either cut still
44
+ breaks the parse the caller was promised.
@@ -68,6 +68,50 @@ record's status line — never by editing the ruling in place.
68
68
  | [058](ADR-058-one-tool-three-channels-and-the-order-is-forced.md) | One tool, three channels, and the order is forced | accepted |
69
69
  | [059](ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md) | The version a repository runs is the one it pinned, and the gate refuses otherwise | accepted |
70
70
  | [060](ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md) | A repository with nothing to run is adopted and says so; a gate is verified where it runs | accepted |
71
+ | [061](ADR-061-the-skill-registry-is-workspace-data.md) | The skill registry is workspace data, and the package ships none of it | **accepted** · 2026-10-12 · filed as S61 |
72
+ | [062](ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md) | A fresh adoption seeds the canonical gates it can run, and never more | **accepted** · 2026-10-13 · filed as S40 |
73
+ | [063](ADR-063-the-adoption-escape-hatch-is-opt-in.md) | The adoption escape hatch is opt-in, and the default writes in place | **accepted** · 2026-10-13 · filed after a consumer asked why every adoption branched |
74
+ | [064](ADR-064-the-context-meter-counts-what-the-tool-wrote.md) | The context meter counts what the tool wrote, and says what it did not | **accepted** · 2026-10-13 · filed after review 0002 S38 |
75
+ | [065](ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md) | A pack is one shared rule set, vendored into the repository and pinned by digest | **accepted** · 2026-10-13 · filed as S39 |
76
+ | [066](ADR-066-a-monorepo-projects-once-per-package.md) | A monorepo projects once per package | **accepted** · 2026-10-13 · filed as S41 |
77
+ | [067](ADR-067-which-harnesses-read-agents-md-is-a-column.md) | Which harnesses read AGENTS.md is a column, and a row without a document gets none | **accepted** · 2026-10-13 · filed as S42 |
78
+ | [068](ADR-068-the-mcp-revision-list-cites-what-was-read.md) | The revision list is a claim about revisions that were read | **accepted** · 2026-10-13 · filed as S43 |
79
+ | [069](ADR-069-the-conformance-lint-checks-the-published-specification.md) | The conformance lint checks the published specification | **accepted** · 2026-10-13 · filed as S44 |
80
+ | [070](ADR-070-the-json-payload-carries-its-own-version.md) | The JSON payload carries its own version | **accepted** · 2026-10-13 · filed as S51 |
81
+ | [071](ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md) | A rule that can be narrowed is written narrow | **accepted** · 2026-10-14 · filed as S50 |
82
+ | [072](ADR-072-the-budget-is-a-declaration-beside-the-gates.md) | The budget is a declaration beside the gates, not a number inside the projection | **accepted** · 2026-10-14 · filed as S38 |
83
+ | [073](ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md) | A kernel is a smaller projection the repository asks for, not one it is given | **accepted** · 2026-10-14 · filed as S38 (R07) · ruling 6 narrowed by ADR-084 |
84
+ | [074](ADR-074-a-rule-and-a-decision-are-readable-by-uri.md) | A rule and a decision are addressable, so they are readable by URI | **accepted** · 2026-10-14 · filed as S43 |
85
+ | [075](ADR-075-nothing-leaves-the-machine.md) | Nothing leaves the machine: no telemetry, local transcripts, loopback models by default | **accepted** · 2026-10-14 · filed as D7 (R06) |
86
+ | [076](ADR-076-a-change-to-what-agents-read-is-measured.md) | A change to what agents read is measured before it ships, or labelled unmeasured in its record | **accepted** · 2026-10-15 · filed as D1 (R01) |
87
+ | [077](ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md) | The command card leads the region, and is derived from committed evidence only | **accepted** · 2026-10-16 · filed as R10 (plan 0003) · ruling 1 narrowed by ADR-086 |
88
+ | [078](ADR-078-a-line-the-repository-already-proves-does-not-ship.md) | A line the repository already proves does not ship, and the author says so | **accepted** · 2026-10-16 · filed as R09 (plan 0003) |
89
+ | [079](ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md) | The rules are enforced at the tool call, from one policy table every harness renders | **accepted** · 2026-10-16 · filed as R15 (plan 0003) |
90
+ | [080](ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md) | The tool's own names are not repositories, and neither reader decides by shape | **accepted** · 2026-10-16 · settles what `rules why` calls a repository |
91
+ | [081](ADR-081-a-tracker-is-read-through-the-repositorys-template.md) | A tracker is read through the repository's template | **accepted** · 2026-10-16 · rulings 1 and 2 superseded by ADR-082 |
92
+ | [082](ADR-082-the-tracker-override-is-a-manifest-key.md) | The tracker override is a manifest key, and init keeps what it did not write | **accepted** · 2026-10-16 · supersedes ADR-081 rulings 1 and 2 |
93
+ | [083](ADR-083-a-budget-is-met-by-cutting-the-kernel-and-hard-is-the-floor.md) | A budget is met by cutting the kernel, and the hard constraints are the floor | **accepted** · 2026-10-17 · filed as R08 (plan 0003) · ruling 7 narrowed by ADR-084 |
94
+ | [084](ADR-084-a-workspace-carries-its-repeated-rules-once.md) | A workspace carries its repeated rules once, and the region does not read the reader's home | **accepted** · 2026-10-17 · filed as R11 (plan 0003) |
95
+ | [085](ADR-085-the-region-ends-with-the-names-it-kept-out.md) | The region ends with the names of what it kept out | **accepted** · 2026-10-18 · filed as R12 (plan 0003) |
96
+ | [086](ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md) | The stable prefix comes first, and the lock records both digests | **accepted** · 2026-10-18 · filed as R13 (plan 0003) |
97
+ | [087](ADR-087-a-run-is-evidence-for-one-tree.md) | A run is evidence for one tree | **accepted** · 2026-10-18 · filed as R17 (plan 0003) |
98
+ | [088](ADR-088-a-marked-line-is-enforced-by-a-checker.md) | A marked line is enforced by a checker | **accepted** · 2026-10-18 · filed as R19 (plan 0003) · ruling 3 narrowed by ADR-090 |
99
+ | [089](ADR-089-the-local-index-is-a-cache-of-git-blobs.md) | The local index is a cache of git blobs | **accepted** · 2026-10-18 · filed as R20 (plan 0003) |
100
+ | [090](ADR-090-an-optional-adapter-is-off-until-named.md) | An optional adapter is off until a repository names it | **accepted** · 2026-10-18 · filed as R21 (plan 0003) |
101
+ | [091](ADR-091-the-map-ranks-files-a-task-mentions.md) | The map ranks the files a task mentions, and the files next to them | **accepted** · 2026-10-18 · filed as R22 (plan 0003) |
102
+ | [092](ADR-092-tests-are-imports-conventions-and-coverage.md) | A test is an import, a conventional neighbour, or a coverage record | **accepted** · 2026-10-18 · filed as R24 (plan 0003) |
103
+ | [093](ADR-093-why-names-what-governs-a-file.md) | why names what governs a file | **accepted** · 2026-10-18 · filed as R25 (plan 0003) |
104
+ | [094](ADR-094-a-change-runs-the-tests-that-import-it.md) | a change runs the tests that import it | **accepted** · 2026-10-18 · filed as R16 (plan 0003) |
105
+ | [095](ADR-095-gate-output-keeps-the-failures.md) | gate output keeps the failures | **accepted** · 2026-10-18 · filed as R18 (plan 0003) |
106
+ | [096](ADR-096-impact-names-who-a-change-reaches.md) | impact names who a change reaches | **accepted** · 2026-10-18 · filed as R23 (plan 0003) |
107
+ | [097](ADR-097-the-channels-publish-by-hand-and-the-version-rides-in-as-an-input.md) | the channels publish by hand, and the version rides in as an input | **accepted** · 2026-10-25 · ruling 3's Homebrew precondition is superseded by [ADR-098](ADR-098-the-fourth-channel-is-one-file-and-it-needs-no-toolchain.md), which also restates what each channel now waits on |
108
+ | [098](ADR-098-the-fourth-channel-is-one-file-and-it-needs-no-toolchain.md) | The fourth channel is one file, and it needs no toolchain | **accepted** · 2026-10-26 · supersedes ADR-058 ruling 5 · narrows ADR-058 ruling 7 and ADR-097 ruling 3 · supersedes ADR-059 ruling 3 · ruling 3's cross-host byte-identicality narrowed by [ADR-099](ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md) |
109
+ | [099](ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md) | the release verifies served bytes, and the formula mirrors them | **accepted** · 2026-10-27 · narrows ADR-098 ruling 3 |
110
+ | [100](ADR-100-a-staged-diff-is-the-last-gate-before-history-and-it-runs-inside-the-tool.md) | a staged diff is the last gate before history, and it runs inside the tool | **accepted** · 2026-10-27 |
111
+ | [101](ADR-101-a-file-lease-belongs-to-one-session-and-the-pre-edit-hook-refuses-another.md) | a file lease belongs to one session, and the pre-edit hook refuses another's edit | **accepted** · 2026-10-28 |
112
+ | [102](ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md) | the two rows review 0002 left queued are closed by ruling, not by authoring | **accepted** · 2026-10-28 |
113
+ | [103](ADR-103-co-projected-duplicates-merge-down.md) | duplicates stand alone, fingerprints generalize, references spell out | **accepted** · 2026-10-28 |
114
+ | [104](ADR-104-the-pre-tool-use-policy-reads-the-command-the-way-a-shell-reads-it.md) | the pre-tool-use policy reads the command the way a shell reads it | **accepted** · 2026-10-28 |
71
115
 
72
116
  ADR-001 to ADR-010 resolve the contradictions found across the 16 repos in the 2026-10-05
73
117
  audit; each record names the repos whose text it replaces. ADR-011 and ADR-012 are the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simt-dev",
3
- "version": "0.6.0",
3
+ "version": "0.6.3",
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",
@@ -27,18 +27,17 @@ simt-substitutions:
27
27
  # neither, declares that. A worker that serves derivatives and calls no API has no
28
28
  # API base URL to set, so it declares only its own custom-domain binding.
29
29
  - key: previewApiBaseUrlNote
30
- default: "- Point the preview build at the preview API Worker: set `SOCIAL_API_BASE_URL` in the Cloudflare Workers Builds preview environment to the preview API Worker URL."
30
+ default: "- Point the preview build at the preview API: set the preview environment's API base URL to the preview API URL."
31
31
  describe: build-config instruction, when this repo actually has a preview API base URL to set
32
32
  - key: googleOnlyNote
33
- default: "- Production and public browser flows remain Google-only."
33
+ default: "- Production and public browser flows use the production identity only."
34
34
  describe: the identity policy this repo enforces in production
35
35
  ---
36
36
 
37
37
  # Preview-Only Authenticated Postflight
38
38
 
39
39
  Authenticated remote postflight and release checks use only {{previewWorkers}}, deployed
40
- with `AUTH_PROVIDER_POLICY=preview_postflight_password` and a secret-bound Firebase password
41
- identity (`POSTFLIGHT_PASSWORD_UID`).
40
+ with a preview-only auth provider policy and a secret-bound password identity.
42
41
 
43
42
  ## Requirements
44
43
 
@@ -52,8 +51,8 @@ identity (`POSTFLIGHT_PASSWORD_UID`).
52
51
  ## Forbidden
53
52
 
54
53
  - Authenticated postflight against production domains ({{productionDomains}}).
55
- - Using real Social Google accounts for feature slice verification.
56
- - Demanding, pasting, or storing production Google OAuth tokens in chat or tickets.
54
+ - Using real user accounts for feature slice verification.
55
+ - Demanding, pasting, or storing production OAuth tokens in chat or tickets.
57
56
 
58
57
  ## Allowed Without Account
59
58
 
@@ -27,17 +27,17 @@ this profile; see `jvm`.
27
27
 
28
28
  ## Modules and layers
29
29
 
30
- - `:core:domain` depends on no other internal module. Enforce it; do not add an
30
+ - The core domain module depends on no other internal module. Enforce it; do not add an
31
31
  import that breaks the direction.
32
32
  - Layer suffixes: `*Repository`, `*ViewModel`, `*UiState`, `*Screen`, `*Service`.
33
- - Feature modules never import `androidx.compose.ui.window.Dialog`, M3 `AlertDialog`
34
- or M3 `ModalBottomSheet`. The raw `Dialog` window belongs to allowlisted `core:ui`
35
- surfaces, and `checkOverlayModalPolicy` fails `./gradlew check` when a feature
36
- bypasses it.
33
+ - Feature modules never present a raw `Dialog` window (`androidx.compose.ui.window.Dialog`,
34
+ M3 `AlertDialog`, M3 `ModalBottomSheet`) that bypasses the shared modal policy.
35
+ Modals go through the design system's dialog surfaces, and the repo's check fails
36
+ the build on a bypass.
37
37
 
38
38
  ## Accessibility
39
39
 
40
- - Every user-facing string is a resource, with a `cd_` content description where an
40
+ - Every user-facing string is a resource, with a content description where an
41
41
  icon or image carries meaning. Decorative images declare `contentDescription = null`
42
42
  explicitly.
43
43
  - Touch targets meet the platform minimum, and the minimum is asserted rather than
@@ -61,5 +61,4 @@ this profile; see `jvm`.
61
61
 
62
62
  - Unit tests for view models, repositories and mappers; instrumentation tests for UI.
63
63
  - `Dispatchers.setMain` in a `@BeforeTest`, restored in `@AfterTest`.
64
- - Assert behaviour, not implementation. Never weaken an assertion, an accessibility
65
- check or a fixture to match current behaviour.
64
+ - Assert behaviour, not implementation — including accessibility checks and fixtures. Never weaken one to match current behaviour.
@@ -41,7 +41,7 @@ Applies to: `cloudflare-workers` — a `wrangler.json`, `wrangler.jsonc` or
41
41
  - Validate at the edge with the schema layer. Authorization is server-authoritative; a
42
42
  client-side role, claim, cache entry or URL parameter grants nothing.
43
43
  - `Request`/`Response` are globals. Do not pass them through as values into domain code.
44
- - Caching defaults to `private, no-store`. Never cache token, account, block, reveal, admin
44
+ - Caching defaults to `private, no-store`. Never cache tokens, account data, admin
45
45
  decisions or authenticated errors.
46
46
  - Streaming is the default for large bodies; do not buffer without a stated reason.
47
47
 
@@ -7,6 +7,8 @@ simt-scope: ["Dockerfile", "*.dockerfile"]
7
7
  simt-fallback: false
8
8
  ---
9
9
 
10
+ # Stack profile: Containers
11
+
10
12
  ## Containers
11
13
 
12
14
  A Dockerfile is the build. If a rule here and the repo's own instructions disagree, the
@@ -26,7 +26,7 @@ rules live in the `android` profile, which selects on `has-android-manifest`.
26
26
 
27
27
  ## Modules and layers
28
28
 
29
- - `:core:domain` depends on no other internal module. Enforce it; do not add an
29
+ - The core domain module depends on no other internal module. Enforce it; do not add an
30
30
  import that breaks the direction.
31
31
  - Layer suffixes: `*Repository`, `*Service`, `*Mapper`, `*Controller`. A class named
32
32
  `*Repository` that talks to HTTP is not a repository.
@@ -10,11 +10,9 @@ simt-priority: convention
10
10
  simt-scope: ["app.json", "metro.config.js", "ios/**", "android/**"]
11
11
  ---
12
12
 
13
- # Stack profile: React Native
14
-
15
- Applies to: `react-native` — a `react-native` or `expo` dependency. A repository whose
16
- UI is rendered in a browser gets the `react-web` profile instead; see that profile for
17
- the rules this one does not repeat.
13
+ # Stack profile: React NativeApplies to: `react-native` — a `react-native` or `expo` dependency. A repository whose
14
+ UI is rendered in a browser follows the browser rules instead; nothing here assumes a
15
+ DOM, a stylesheet cascade, or URL-shared state.
18
16
 
19
17
  ## What does not apply here
20
18
 
@@ -17,8 +17,10 @@ UI is rendered in a browser.
17
17
 
18
18
  ## Types
19
19
 
20
- - `strict` plus the full strictness set listed in the Workers profile. Generated API DTOs only;
21
- never hand-write a duplicate of a type the API already produces.
20
+ - `strict` plus `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`,
21
+ `noImplicitOverride`, `noFallthroughCasesInSwitch`, `noImplicitReturns`,
22
+ `isolatedModules`. Generated API DTOs only; never hand-write a duplicate of a
23
+ type the API already produces.
22
24
  - No `any`, no unsafe casts, no non-null boundary bypasses, no floating promises, no
23
25
  `console.*`.
24
26
 
@@ -39,10 +41,9 @@ UI is rendered in a browser.
39
41
 
40
42
  ## RTL and localisation
41
43
 
42
- - Arabic mirrors application chrome only. North, coordinate order, geographic pan and zoom,
43
- map symbols, media and universal status meaning do not mirror.
44
- - Every user-visible string exists in every supported locale in the same change. Adding a
45
- locale touches tests, which assert locale parity.
44
+ - RTL locales mirror application chrome only. North, coordinate order, geographic pan and
45
+ zoom, map symbols, media and universal status meaning do not mirror.
46
+ - Every user-visible string exists in every supported locale in the same change.
46
47
 
47
48
  ## Accessibility
48
49
 
@@ -58,6 +59,5 @@ UI is rendered in a browser.
58
59
  - Assert behaviour, not implementation. Never weaken an assertion, an accessibility check or a
59
60
  fixture to match current behaviour.
60
61
  - No arbitrary sleeps. No `.only`. No committed `.skip`. No flaky reruns.
61
- - Inject a sentinel token, UID, text, gender, coordinate, URL and private media value, and
62
- prove its absence from unauthorised UI, storage, cache, telemetry, URL history and
63
- artifacts.
62
+ - Inject sentinel values (a token, an id, a coordinate, a URL) and prove their absence
63
+ from unauthorised UI, storage, cache, telemetry, URL history and artifacts.
@@ -2,9 +2,9 @@
2
2
  //
3
3
  // Pure, so the whole decision is testable without a harness: hand it the facts a
4
4
  // renderer would extract and an environment, and it answers. The filesystem is
5
- // consulted in exactly one place — `generatedBytes`, which asks whether a file being
6
- // written to holds bytes this tool generates — because that is the one fact a payload
7
- // cannot carry.
5
+ // consulted in `factsFor` for exactly the facts a payload cannot carry — whether a
6
+ // file holds bytes this tool generates, who has leased it (ADR-101), and what the
7
+ // author's git aliases expand to (ADR-104) — and nothing else decides from disk.
8
8
  //
9
9
  // Two properties are deliberate and both are about an author standing still:
10
10
  //
@@ -19,11 +19,14 @@
19
19
  // understand this" into "blocked" makes the hook a false-positive machine, and a hook
20
20
  // that blocks real work is uninstalled the same afternoon.
21
21
 
22
+ import { spawnSync } from 'node:child_process';
22
23
  import { readFile } from 'node:fs/promises';
23
24
  import { join, relative, resolve } from 'node:path';
24
25
 
25
26
  import { parse } from '../projection/markers.mjs';
26
- import { ALLOW_ENV, EVENTS, POLICY_IDS, policiesFor } from './policy.mjs';
27
+ import { activeLeases, leasePathFor, readLeases } from '../lease.mjs';
28
+ import { ALLOW_ENV, EVENTS, POLICY_IDS, SESSION_ENV, policiesFor } from './policy.mjs';
29
+ import { applyAliases, commandFacts, GIT_BUILTINS } from './shell.mjs';
27
30
 
28
31
  /**
29
32
  * The policies this session has lifted, and the names in `SIMT_ALLOW` that are not
@@ -42,14 +45,39 @@ export function overrides(env = {}) {
42
45
  };
43
46
  }
44
47
 
45
- /** Whether one row applies to the facts. A row with no matcher reads the facts itself. */
46
- function applies(row, facts) {
48
+ /**
49
+ * Whether one row applies to the facts. A row with no matcher reads the facts itself.
50
+ *
51
+ * `env` is only consulted by `lease`, which is the one row asking who is doing this
52
+ * rather than what is being done — and the one row with a fact the payload never
53
+ * carries: an unreadable claim file denies a write, but a shell command with no file
54
+ * fact is not an edit (ADR-101 ruling 4).
55
+ *
56
+ * A command row is judged on the normalised facts when they exist: the shell tokenised,
57
+ * wrappers unwrapped, each git invocation reduced to subcommand and options (ADR-104).
58
+ * When the command could not be tokenised, or the facts were never normalised at all —
59
+ * a test that hands `decide` a bare command string — the row's regex runs instead, so
60
+ * a parse failure and a pre-normalised fact are both judged the way they were before
61
+ * the normaliser existed.
62
+ */
63
+ function applies(row, facts, env) {
47
64
  if (row.inspects === 'command') {
48
- return typeof facts.command === 'string' && row.match !== null && row.match.test(facts.command);
65
+ if (typeof facts.command !== 'string') return false;
66
+ if (facts.shell !== null && facts.shell !== undefined && facts.shell.parsed === true) {
67
+ return row.match(facts);
68
+ }
69
+ return row.regex !== null && row.regex.test(facts.command);
49
70
  }
50
71
  if (row.inspects === 'write') {
51
72
  return (facts.writes ?? []).some((w) => w.generated !== null && w.generated !== undefined);
52
73
  }
74
+ if (row.inspects === 'lease') {
75
+ const writes = facts.writes ?? [];
76
+ if (writes.length === 0) return false;
77
+ if (facts.leaseError) return true;
78
+ const session = typeof env[SESSION_ENV] === 'string' ? env[SESSION_ENV] : '';
79
+ return writes.some((w) => w.lease && w.lease.session !== session);
80
+ }
53
81
  return false;
54
82
  }
55
83
 
@@ -62,7 +90,7 @@ function applies(row, facts) {
62
90
  */
63
91
  export function decide({ event = EVENTS[0], facts = {}, env = {} } = {}) {
64
92
  const { ids: lifted, unknown } = overrides(env);
65
- const matched = policiesFor(event).filter((p) => applies(p, facts));
93
+ const matched = policiesFor(event).filter((p) => applies(p, facts, env));
66
94
  const blocked = matched.filter((p) => !lifted.has(p.id));
67
95
  return {
68
96
  event,
@@ -70,7 +98,14 @@ export function decide({ event = EVENTS[0], facts = {}, env = {} } = {}) {
70
98
  blocked: blocked.map((p) => p.id),
71
99
  lifted: matched.filter((p) => lifted.has(p.id)).map((p) => p.id),
72
100
  unknown,
73
- messages: blocked.map((p) => `${p.message} (override: ${ALLOW_ENV}=${p.id})`),
101
+ // A row may carry a `detail` for the parts of its message that are only knowable
102
+ // at decision time (the lease holder's name and expiry, ADR-101 ruling 5). Every
103
+ // row without one renders exactly as before, override hint included.
104
+ messages: blocked.map((p) => {
105
+ const detail = typeof p.detail === 'function' ? p.detail(facts, env) : null;
106
+ const body = detail ? `${p.message} ${detail}` : p.message;
107
+ return `${body} (override: ${ALLOW_ENV}=${p.id})`;
108
+ }),
74
109
  adrs: blocked.map((p) => p.adr),
75
110
  };
76
111
  }
@@ -114,10 +149,66 @@ export async function generatedBytes(root, file) {
114
149
  return null;
115
150
  }
116
151
 
152
+ /**
153
+ * The author's git aliases, name to value, as `git config` spells them.
154
+ *
155
+ * One spawn for the whole command, and only when a fact needs it: the lookup reads the
156
+ * configuration the agent's own `git p` would read, and a git that is missing or cannot
157
+ * answer yields no aliases, which leaves the facts exactly where the pre-lookup policy
158
+ * would have found them.
159
+ */
160
+ function gitAliases(root) {
161
+ const result = spawnSync('git', ['-C', root, 'config', '--get-regexp', '^alias\\.'], {
162
+ encoding: 'utf8',
163
+ timeout: 1000,
164
+ });
165
+ const aliases = new Map();
166
+ if (result.status !== 0) return aliases;
167
+ for (const line of String(result.stdout ?? '').split('\n')) {
168
+ const m = /^alias\.(\S+)\s+(.*)$/.exec(line);
169
+ if (m !== null) aliases.set(m[1], m[2]);
170
+ }
171
+ return aliases;
172
+ }
173
+
174
+ /**
175
+ * The normalised command facts the table asks about: the shell tokenised, wrappers
176
+ * unwrapped, each git or rm invocation reduced to what it runs.
177
+ *
178
+ * A subcommand git does not ship may be the author's alias, and what it expands to is
179
+ * read from their git configuration - the one step in this path that leaves the
180
+ * module. A command that cannot be tokenised is marked, not dropped: the policies fall
181
+ * back to their regex rather than seeing nothing (ADR-104).
182
+ */
183
+ async function normalizeCommand(root, command) {
184
+ const parsed = commandFacts(command); // non-null: `command` is a string here
185
+ if (parsed === null || !parsed.parsed) return { parsed: false };
186
+ if (parsed.facts.some((f) => f.kind === 'git' && !GIT_BUILTINS.has(f.sub))) {
187
+ parsed.facts = applyAliases(parsed.facts, gitAliases(root));
188
+ }
189
+ return parsed;
190
+ }
191
+
117
192
  /** Turn a renderer's extraction into the facts the table asks about. */
118
193
  export async function factsFor(root, { command = null, writes = [] } = {}) {
119
194
  const resolved = await Promise.all(
120
195
  writes.filter(Boolean).map(async (path) => ({ path, generated: await generatedBytes(root, path) })),
121
196
  );
122
- return { command, writes: resolved };
197
+ // Shell facts, read only when the payload carried a command: the same "pay for what
198
+ // you use" rule as the lease facts below, which a payload with no command cannot touch.
199
+ const shell = typeof command === 'string' ? await normalizeCommand(root, command) : null;
200
+ // Lease facts, read only when the payload named a file to write: the claim file is
201
+ // irrelevant to a shell command, and a payload with no file pays nothing for a
202
+ // feature it cannot touch. Expired claims are dropped here because the hook runs
203
+ // now, so a TTL needs no sweeper; ownership is left whole because `decide` is the
204
+ // one that knows which session is asking (ADR-101 ruling 3).
205
+ if (resolved.length === 0) return { command, writes: resolved, leaseError: null, shell };
206
+ const read = await readLeases(root);
207
+ if (read.error !== null) return { command, writes: resolved, leaseError: read.error, shell };
208
+ const live = new Map(activeLeases(read).map((l) => [l.path, l]));
209
+ for (const w of resolved) {
210
+ const rel = leasePathFor(root, w.path);
211
+ w.lease = rel === null ? null : live.get(rel) ?? null;
212
+ }
213
+ return { command, writes: resolved, leaseError: null, shell };
123
214
  }