simt-dev 0.2.1 → 0.3.0

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 (95) hide show
  1. package/CHANGELOG.md +239 -0
  2. package/bin/simt-dev.mjs +21 -2
  3. package/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
  4. package/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
  5. package/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
  6. package/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
  7. package/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
  8. package/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
  9. package/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
  10. package/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
  11. package/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
  12. package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
  13. package/docs/decisions/ADR-074-a-rule-and-a-decision-are-readable-by-uri.md +99 -0
  14. package/docs/decisions/ADR-075-nothing-leaves-the-machine.md +56 -0
  15. package/docs/decisions/ADR-076-a-change-to-what-agents-read-is-measured.md +70 -0
  16. package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +91 -0
  17. package/docs/decisions/ADR-078-a-line-the-repository-already-proves-does-not-ship.md +117 -0
  18. package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md +162 -0
  19. package/docs/decisions/ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md +99 -0
  20. package/docs/decisions/ADR-081-a-tracker-is-read-through-the-repositorys-template.md +36 -0
  21. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +35 -0
  22. package/package.json +2 -1
  23. package/rules/core.md +1 -0
  24. package/rules/stacks/android.md +2 -0
  25. package/rules/stacks/cloudflare-workers.md +2 -0
  26. package/rules/stacks/connectiq.md +1 -0
  27. package/rules/stacks/container.md +2 -0
  28. package/rules/stacks/dart.md +2 -0
  29. package/rules/stacks/deno.md +2 -0
  30. package/rules/stacks/dotnet.md +2 -0
  31. package/rules/stacks/elixir.md +2 -0
  32. package/rules/stacks/go.md +2 -0
  33. package/rules/stacks/jvm.md +2 -0
  34. package/rules/stacks/node.md +2 -0
  35. package/rules/stacks/python.md +2 -0
  36. package/rules/stacks/react-native.md +2 -0
  37. package/rules/stacks/react-web.md +2 -0
  38. package/rules/stacks/ruby.md +2 -0
  39. package/rules/stacks/rust.md +2 -0
  40. package/rules/stacks/swift.md +2 -0
  41. package/rules/stacks/typescript.md +2 -0
  42. package/rules/stacks/uv.md +2 -0
  43. package/src/agenthooks/decide.mjs +123 -0
  44. package/src/agenthooks/policy.mjs +78 -0
  45. package/src/agenthooks/render.mjs +240 -0
  46. package/src/argv.mjs +55 -1
  47. package/src/bench/ablate.mjs +630 -0
  48. package/src/bench/adapters.json +149 -0
  49. package/src/bench/grade.mjs +229 -0
  50. package/src/bench/mine.mjs +811 -0
  51. package/src/bench/probes.mjs +1350 -0
  52. package/src/bench/report.mjs +899 -0
  53. package/src/bench/run.mjs +1002 -0
  54. package/src/commands/agenthook.mjs +282 -0
  55. package/src/commands/bench.mjs +359 -0
  56. package/src/commands/compare.mjs +15 -0
  57. package/src/commands/graph.mjs +45 -21
  58. package/src/commands/harness.mjs +301 -0
  59. package/src/commands/help.mjs +73 -2
  60. package/src/commands/index.mjs +67 -25
  61. package/src/commands/init.mjs +12 -8
  62. package/src/commands/mcp.mjs +20 -5
  63. package/src/commands/plan.mjs +99 -0
  64. package/src/commands/rules.mjs +42 -57
  65. package/src/commands/skills.mjs +67 -92
  66. package/src/commands/skillsinstall.mjs +17 -2
  67. package/src/commands/sync.mjs +62 -6
  68. package/src/commands/tokens.mjs +169 -0
  69. package/src/compare/row.mjs +73 -2
  70. package/src/decisions/index.mjs +23 -5
  71. package/src/graph/spec.mjs +227 -0
  72. package/src/graph/speccheck.mjs +68 -0
  73. package/src/hooks/dispatcher.mjs +35 -3
  74. package/src/kernel/card.mjs +254 -0
  75. package/src/kernel/inferability.mjs +188 -0
  76. package/src/mcp/resources.mjs +157 -0
  77. package/src/mcp/schema.mjs +139 -0
  78. package/src/mcp/server.mjs +228 -20
  79. package/src/mcp/tools.mjs +372 -1
  80. package/src/measure/context.mjs +542 -0
  81. package/src/measure/loaderView.mjs +435 -0
  82. package/src/measure/session.mjs +267 -0
  83. package/src/monorepo.mjs +339 -0
  84. package/src/output/index.mjs +78 -2
  85. package/src/packs.mjs +182 -0
  86. package/src/plan/index.mjs +650 -0
  87. package/src/plan/template.mjs +331 -0
  88. package/src/projection/loaders.mjs +689 -0
  89. package/src/projection/manifest.mjs +114 -3
  90. package/src/projection/project.mjs +140 -9
  91. package/src/projection/rules.mjs +365 -22
  92. package/src/projection/surfaces.mjs +155 -23
  93. package/src/projection/targets.mjs +37 -0
  94. package/src/toolwords.mjs +69 -0
  95. package/templates/plan/tracker.json +36 -0
@@ -0,0 +1,162 @@
1
+ # ADR-079 — The rules are enforced at the tool call, from one policy table every harness renders
2
+
3
+ Status: **accepted** · 2026-10-16 · filed as R15 (plan 0003)
4
+
5
+ Affects: `src/agenthooks/policy.mjs`, `src/agenthooks/decide.mjs`, `src/agenthooks/render.mjs`,
6
+ `src/commands/agenthook.mjs`, `src/commands/index.mjs`, `src/commands/help.mjs`,
7
+ `bin/simt-dev.mjs`, `test/agenthooks.test.mjs`
8
+
9
+ ## Context
10
+
11
+ The rules this tool projects reach an agent as text, and text is advice. Three of them are
12
+ broken by habit rather than by disagreement — a `git push`, a `--no-verify`, an edit inside a
13
+ region that is regenerated — and the reader breaking one is an agent that never sees the
14
+ commit that would have caught it. R15 turns those three into a decision at the tool call.
15
+
16
+ Four harnesses document a pre-tool hook that receives JSON on stdin and answers with a
17
+ decision (each page read 2026-10-16):
18
+
19
+ | Reader | File | What it does with a decision |
20
+ | --- | --- | --- |
21
+ | Claude Code | `.claude/settings.json` | `hookSpecificOutput.permissionDecision: "deny"`, exit 2 also blocks |
22
+ | Cursor | `.cursor/hooks.json` | `{"permission": "deny"}`; exit 2 blocks; a permission hook blocks on invalid JSON even when `failClosed` is false |
23
+ | Codex | `.codex/hooks.json` | the same `hookSpecificOutput` shape as Claude Code, or the older `{"decision": "block"}` |
24
+ | Antigravity | `.agents/hooks.json` | `{"decision": "deny", "reason"}`; the page documents no exit code |
25
+
26
+ They agree on the mechanism and on nothing else. The path differs; Antigravity's file is keyed
27
+ by a hook *name* rather than by event; the event is `PreToolUse` or `preToolUse`; the tool name
28
+ for a shell call is `Bash`, `Shell`, `Bash` or `run_command`; the command sits in
29
+ `tool_input.command` for three of them and in `toolCall.args.CommandLine` for the fourth; the
30
+ write tool is `Edit`/`Write`, `apply_patch` (whose paths are inside the patch text),
31
+ `Write`/`Edit`, and `write_to_file`/`replace_file_content`; and the decision document is four
32
+ different shapes.
33
+
34
+ The plan was written on 2026-10-06 and named `.gemini/settings.json`. The reader table R14
35
+ shipped already carries the correction — the Antigravity page documents
36
+ `<workspace>/.agents/hooks.json` — so this record follows the table and not the plan. The table
37
+ is also the source of the four config paths here: a second list of the same paths is the copy
38
+ ADR-035 exists to prevent.
39
+
40
+ Measured on this machine (node v26, twenty runs each, every figure from a node parent that
41
+ pays the same fork the harness pays): `node -e 0` costs **85.4 ms** at p50 and **91.9 ms** at
42
+ p95, so the runtime's own start is 92% of the plan's budget before a line of this tool runs.
43
+ Loading the reader table adds 2.8 ms at p50 and the region detector 4.9 ms; the whole hook
44
+ path is **97.4 ms p50 / 101.6 ms p95**, down from 127.6 / 139.0 before `bin/` dispatched
45
+ `agent-hook` without loading the registry.
46
+
47
+ So the plan's criterion of a p95 under 100 ms is **not met end to end**, and a sample of
48
+ twenty straddling the line is a claim about this machine rather than about the tool. What
49
+ this slice owns is the part above the runtime, and that part is stated exactly: the decision
50
+ is **0.0023 ms** at p95, and the entry point loads no manifest, no fingerprint, no workspace
51
+ and no command registry — the two modules it takes from `src/projection/` (`markers.mjs` to
52
+ ask whether a file already holds a region, `loaders.mjs` for the reader table's paths) are
53
+ leaves that read nothing at import.
54
+
55
+ ## Rulings
56
+
57
+ **1. One policy table, and one renderer per harness.** `policy.mjs` holds the rows and matches
58
+ them against *facts* — a shell command string, and the files a write touches — so a policy never
59
+ knows which harness asked. Each renderer owns one harness's payload-to-facts mapping, its
60
+ configuration shape and its decision document. The table is data; the dialects are code.
61
+
62
+ **2. Three policies ship: `push` (ADR-001), `rewrite` (ADR-017) and `generated` (ADR-036).**
63
+ Each names the record it enforces and the override that lifts it, in the message the reader
64
+ sees. `push` quotes the projected rule that cites ADR-001; `rewrite` cites the record whose
65
+ own Context is the `--no-verify` spiral a broken gate teaches; `generated` cites the record
66
+ that made the region note ("Do not edit by hand") ship into every consumer repository, and its
67
+ message names the command that regenerates.
68
+
69
+ **3. Every matching policy is reported, not the first.** `git push --force` matches two, and
70
+ the denial carries both messages and both records. A first-match rule would let
71
+ `SIMT_ALLOW=push` carry a force-push through on an override that never mentioned rewriting
72
+ history, which is the one thing an override must not do.
73
+
74
+ **4. `SIMT_ALLOW` lifts exactly the policies it names, and says so.** A lifted policy is
75
+ reported as lifted, so a session that granted something can be read back out of its own output.
76
+ A name that is not a policy is reported as unknown and changes no decision: a typo in an
77
+ override must not silently disable a policy, and it must not block either.
78
+
79
+ **5. Exit 2 means "block", and only a denial uses it.** A usage error — a missing `--harness`,
80
+ an unknown event — exits 1. Every harness reads 2 as a refusal, so a configuration mistake that
81
+ exited 2 would block every tool call in the session, which is a worse failure than the one the
82
+ hook is there to prevent.
83
+
84
+ **6. No facts is not a violation.** An unrecognised payload, an empty stdin or unparseable JSON
85
+ produces no decision: exit 0, nothing on stdout. A hook that blocked on a payload it did not
86
+ understand would turn every tool call into a wall, and the harness's own permission model still
87
+ applies. This is the deliberate direction of the failure, and it is why the facts a policy
88
+ needs are read from named fields rather than guessed.
89
+
90
+ **7. A missing hook is a state; an unreadable config is a defect.** `agent-hooks status` exits 0
91
+ when a harness has no entry and 1 when its config cannot be read, or parses to something that is
92
+ not an object. A harness's settings file is the harness's file, kept for reasons that have
93
+ nothing to do with this tool, so not being in it is a choice; a file this tool cannot parse is
94
+ one it must not write into, in either surface.
95
+
96
+ **8. `agent-hook` has no envelope of its own.** Its stdout is the dialect of the harness that
97
+ called it — no `ok`, no `schemaVersion`. It is the one command whose contract is four
98
+ documents instead of one, and the reason a JSON contract test does not enumerate it.
99
+
100
+ **9. The entry point loads neither the registry nor anything that reads the repository.** `bin/simt-dev.mjs`
101
+ dispatches the exact spelling the renderers write, and only that: `--help` still imports the
102
+ registry, so help remains the single list of commands. Nothing above the decision resolves a
103
+ manifest, fingerprints a tree or walks a workspace, because all three would put the cost of
104
+ answering "what is this repository" on every tool call. The two modules the path does take
105
+ from `src/projection/` are leaves that read nothing at import, and the test asserts the
106
+ modules that would read the repository are absent from the import list rather than asserting
107
+ a duration.
108
+
109
+ **10. Install merges, and identifies its own entry by the command it wrote.** Every other key
110
+ and every foreign handler survives; the entry is found by the command string, because JSON has
111
+ no comment to carry a marker. A config this tool cannot parse is left alone and reported, and
112
+ nothing else is written for that run.
113
+
114
+ **11. The readers this tool does not write are named.** `agent-hooks status` and `install`
115
+ report Copilot and the DeepSeek harness as "no hooks file is documented" and OpenCode as
116
+ "not ours" (its hooks are plugins), with the table's own note. A command that writes four files
117
+ and stays silent about the fifth reader in the repository reads as "that one is covered too".
118
+
119
+ ## Consequences
120
+
121
+ **What has not landed.** Three policies the plan lists are not here: formatting and linting a
122
+ file after an edit, refusing reads of secrets (`.env*`, `*.pem`, `*.keystore`), and the Stop
123
+ policy that blocks once when code changed with no ledger entry for the tree — the last waits on
124
+ R17, the evidence ledger. Plan section 16 step 5 scopes this slice to exactly the three that
125
+ shipped. A policy that cannot name its evidence cannot name its remedy, which is why the
126
+ ledger one waits.
127
+
128
+ **The acceptance criterion's headless run has not been run, and cannot be from here.** R15's
129
+ accept says a headless Claude Code session on probe R05 shows the block. The probes and their
130
+ graders exist and the decision was verified per harness against fixtures; the run itself needs
131
+ a harness, and none is installed on this machine (`claude`, `codex`, `cursor-agent`, `gemini`
132
+ and `aider` are all absent from `PATH`, which is the check that says so rather than a memory of
133
+ one). So this record does not claim it, and the plan's note for the row names it as the one
134
+ piece of R15's accept that is outstanding.
135
+
136
+ **One field was inferred rather than read.** Cursor's page was read to line 1087 of 1490, which
137
+ does not include its write tool's input key, so its adapter reads `file_path ?? path`. If Cursor
138
+ names it otherwise, a generated-region edit through that harness is not caught — the failure is
139
+ silent, which is why it is written down here rather than left in a comment.
140
+
141
+ **Codex's trust step is printed, not performed.** Codex records trust against the hook's current
142
+ hash, so a hook nobody has trusted stays inert: `install` names the `/hooks` review and
143
+ `status` repeats it. Nothing this tool can do from a shell substitutes for it.
144
+
145
+ **The hook name Antigravity keys on is `simt-dev-policies`.** Antigravity's file is a map of
146
+ named hooks, so the entry needs a name; the project name is the one string already exempt from
147
+ the estate-name guard.
148
+
149
+ ## Rejected
150
+
151
+ - **One shared config written for every harness.** They disagree on the path, the top-level
152
+ shape, the event name, the matcher, the tool name, the command field, the write tool and the
153
+ decision document. A shared file would be a fifth dialect none of them reads.
154
+ - **Blocking on a payload the hook did not understand.** Fail-closed is the dispatcher's rule for
155
+ a gate it was asked to run; it is the wrong rule for a hook that runs on every tool call.
156
+ - **Exit 2 for a usage error.** It means "block" to every harness.
157
+ - **A `stop` policy that blocks without a ledger.** The evidence it would need is R17's.
158
+ - **An `ok`/`schemaVersion` envelope on `agent-hook`.** The harness parses its own dialect.
159
+ - **Reading the manifest for the policy decision.** The rules are the tool's, not the
160
+ repository's gates; a hook that required adoption would not run in a repository that has not
161
+ been adopted, which is where the rules matter most.
162
+ - **Guessing a payload shape it cannot recognise.** See ruling 6.
@@ -0,0 +1,99 @@
1
+ # ADR-080 — The tool's own names are not repositories, and neither reader decides by shape
2
+
3
+ Status: **accepted** · 2026-10-16 · settles what `rules why` calls a repository
4
+
5
+ ## Context
6
+
7
+ `ESTATE_NAME` matched `simt-<anything>` that was not `simt-dev`, on the reasoning that
8
+ every other name sharing the prefix is a repository in the estate these records were
9
+ written in. The reasoning is sound about the estate and false about this tool, which
10
+ prints its own prefixed names in its own records: `simt-priority`, `simt-applies`,
11
+ `simt-inferable-when`, `simt-hooks`.
12
+
13
+ Measured over the corpus before anything changed, 21 of the 33 records that named
14
+ anything named a name of ours, across 9 records; 7 of those named nothing else:
15
+
16
+ ```
17
+ ADR-018 simt-hooks ADR-047 simt-applies
18
+ ADR-022 simt-alwaysApply, simt-applies, simt-fallback, simt-gates, simt-id,
19
+ simt-key, simt-opt-in, simt-owned, simt-substitutions, simt-target, simt-title
20
+ ADR-026 simt-package-ecosystem ADR-056 simt-hooks
21
+ ADR-040 simt-hooks ADR-062 simt-applies
22
+ ADR-073 simt-priority ADR-078 simt-inferable-when, simt-priority
23
+ ```
24
+
25
+ Two harms, and the second is the one that matters. `writtenAgainst` listed them under a
26
+ heading that says the record was written against those repositories, which is a false
27
+ claim about evidence — the one thing a decision record is for. Worse, `redactEstate`
28
+ replaced them in the prose, so a reader opening a record through `rules why` read
29
+ `` `a repository: hard | command | convention | style` `` where the record says
30
+ `simt-priority: hard | command | convention | style`. The command whose whole job is to
31
+ explain a citation printed a sentence with no subject.
32
+
33
+ The answer was already in this repository and in the wrong half of it: the shipped-text
34
+ guard derived `TOOL_OWN_WORDS` from `mdc.mjs` and `rules.mjs`, exactly to avoid calling
35
+ `simt-priority` a repository. The reader of the same corpus did not, so the guard passed
36
+ while the command that ships the corpus misdescribed it. ADR-079's own record is the third
37
+ instance of the shape: `simt-inferable-when` was reported as a surveyed repository.
38
+
39
+ ## Rulings
40
+
41
+ **1. The tool's own prefixed names live in `src/toolwords.mjs`, derived from the modules
42
+ that own them.** `KNOWN_KEYS` from `mdc.mjs`, `PROFILE_KEYS` from `rules.mjs`, the
43
+ inferability marker from `kernel/inferability.mjs`, and the three names this tool writes
44
+ on disk (`simt-dev`, `simt-dev-check`, `simt-hooks`). Derived, not listed (ADR-022): a
45
+ second copy of a vocabulary is a copy that drifts and then forgives.
46
+
47
+ **2. Both readers read that one list.** The decision reader and the shipped-text guard
48
+ answer the same question — is this a repository or a name of ours? — and answering it
49
+ twice is how the two answers came apart. `test/shipped.test.mjs` now imports
50
+ `TOOL_WORDS` instead of deriving its own.
51
+
52
+ **3. A record whose subject is the prefix names no repository.** `PREFIX_ARGUING` maps
53
+ two files (ADR-022, ADR-026) to the reason each spells the prefix, and both readers use
54
+ it. A vocabulary cannot settle these: a hypothetical key in an argument and a repository
55
+ name are the same shape. The guard's staleness test is unchanged — an exemption that
56
+ stops being needed still fails.
57
+
58
+ **4. Redaction keeps the tool's names and replaces the estate's.**
59
+ `redactEstate('simt-priority')` is `simt-priority`; `redactEstate` of an estate-shaped
60
+ name is `a repository`. The rule is about what the reader cannot act on, and a key of the
61
+ tool they are running is not that.
62
+
63
+ **5. Detection stays shape-based and strict.** `test/leaks.mjs` keeps `ESTATE_NAME` and
64
+ does not consult the vocabulary. Detection may be stricter than rendering without harm: a
65
+ word of ours that a surface never should have shipped still fails, and ADR-080 adds no
66
+ false negative. This asymmetry is deliberate and is the reason the list did not become a
67
+ filter inside the pattern.
68
+
69
+ **6. `writtenAgainst` still names the estate.** 26 of 80 records list `repo-NN` labels and
70
+ keep listing them, every label one of those; the field's purpose is unchanged and the
71
+ reader still learns which parts of a record are not about them.
72
+
73
+ ## Consequences
74
+
75
+ - 21 misdescribed reads are gone and no record names a name of ours; `simt rules why ADR-073 --json` reports
76
+ `writtenAgainst: []` and the record is served with `simt-priority` intact.
77
+ - `src/toolwords.mjs` imports `mdc.mjs`, `rules.mjs` and `kernel/inferability.mjs`, so the
78
+ decision reader loads the projection stack. `rules why` is interactive and pays ~5 ms;
79
+ no import in that chain has import-time side effects, which is what makes it safe to add
80
+ to a module the MCP server also loads.
81
+ - The guard gained an import and lost a derivation. Its own comment about where the
82
+ vocabulary comes from is now in `src/`, next to what it describes.
83
+ - An estate repository whose name does not begin `simt-` is still undetectable, as
84
+ `ESTATE_NAME` says, and this record does not change that.
85
+
86
+ ## Rejected
87
+
88
+ - **Keeping the shape and documenting the exception.** The defect was not the pattern's
89
+ comment; it was that the pattern was the only definition.
90
+ - **Exempting the words instead of the records.** ADR-022's `simt-gates` and a repository
91
+ sharing that shape are indistinguishable, so a word list would either over-exempt a
92
+ repository or under-exempt the argument.
93
+ - **A second list inside `src/decisions/index.mjs`.** The guard already had one, and the
94
+ two drifting apart is the defect this record is about.
95
+ - **Filtering inside `ESTATE_NAME` itself with a lookahead built from the vocabulary.**
96
+ Detection and rendering want different answers on purpose (ruling 5), and a shared
97
+ pattern would have silently coupled them.
98
+ - **Renaming `writtenAgainst` to something vaguer.** The field is right; the shape it
99
+ matched was wrong.
@@ -0,0 +1,36 @@
1
+ # ADR-081 — A tracker is read through the repository's template
2
+
3
+ Status: **accepted** · 2026-10-16 · rulings 1 and 2 superseded by ADR-082
4
+
5
+ ## Context
6
+
7
+ `plan next` has to open the directory a repository actually keeps its plans in, and it has to
8
+ recognise the columns that repository actually writes. Both were fixed: `docs/plans`,
9
+ `docs/reviews`, a column named State, and the word `review` in a dependency. That is this
10
+ package's own layout. A reader whose plans live somewhere else, or whose tracker says Done
11
+ in a column named Progress, would be told they had no plan.
12
+
13
+ The selection cannot live in the manifest. `init` rewrites that file from the fields it owns,
14
+ and a repository that has not adopted the tool has no manifest. A plan template has to work
15
+ in both, and it has to survive a re-init.
16
+
17
+ ## Rulings
18
+
19
+ 1. **The format is a template, and the one delivered with the tool is named simt-dev.** It
20
+ reads `docs/plans` and `docs/reviews`, binds columns by the headers it lists, and treats
21
+ `done` as complete. A repository that writes nothing gets that template.
22
+ 2. **A repository replaces it with `simt-plan.json` at its root.** The file overlays the
23
+ delivered template: a section it names replaces that section, and a section it omits stays.
24
+ Directories, column headers, state words, which of those words unblocks a dependency, and
25
+ the words a dependency uses to name another document are all in that file.
26
+ 3. **A template that cannot be read is an error.** Falling back to the delivered template
27
+ would index the wrong directories and could mark the wrong row done. A directory path has
28
+ to stay inside the repository.
29
+ 4. **`plan done` still previews unless `--yes`.** The template decides which word is written
30
+ for the completed state. It does not decide that a row may change unseen.
31
+
32
+ ## Consequences
33
+
34
+ - `simt plan status` names the template and the directories it read.
35
+ - Changing the delivered template changes what every repository without `simt-plan.json` is
36
+ indexed as. The file is the format; the parser does not keep a second copy of the headers.
@@ -0,0 +1,35 @@
1
+ # ADR-082 — The tracker override is a manifest key, and init keeps what it did not write
2
+
3
+ Status: **accepted** · 2026-10-16 · supersedes ADR-081 rulings 1 and 2
4
+
5
+ ## Context
6
+
7
+ ADR-081 put the tracker format in a file named for this package, and put a repository's
8
+ override in a second file at the root. The name collided with the package and with the
9
+ manifest's old filename. The second file existed because `init` rebuilt the manifest from
10
+ the keys it knew and dropped the rest, so a `plans` key would not have survived a re-init.
11
+
12
+ A repository that has adopted already has one config file. A second one, present only when
13
+ the tracker differs from the delivered format, is a file most readers will not know to open.
14
+
15
+ ## Rulings
16
+
17
+ 1. **The delivered template is named `tracker`.** It is `templates/plan/tracker.json`. It
18
+ reads `docs/plans` and `docs/reviews`. A repository that sets nothing gets that template.
19
+ 2. **A repository overrides it with the `plans` key of its manifest.** The key overlays the
20
+ delivered template. A section it names replaces that section, and a section it omits
21
+ stays. The manifest's own comment says so, because that comment is the text a reader of
22
+ the file sees.
23
+ 3. **`init` keeps every key it does not write.** Owned keys still refresh, in the order the
24
+ writer emits them. A key absent from that object is copied after them, in the order the
25
+ file had those keys. That is what makes ruling 2 survivable: a re-init no longer deletes
26
+ `plans`, or any other key a repository added.
27
+
28
+ ## Consequences
29
+
30
+ - `simt plan` reads `plans` from `simt.json`, and from `simt-dev.json` while that name is
31
+ still the manifest on disk.
32
+ - A manifest written by `init` carries the explanation of `plans` in its comment. The
33
+ comment is generated, so the next `init` or `sync` refreshes it.
34
+ - ADR-081 rulings 3 and 4 stand: a template that cannot be read is an error, and `plan done`
35
+ still previews unless `--yes`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "simt-dev",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Shared agent rules, skills and git hooks for a repository: one command adopts a repo, and every gate it declares is probed before it is written.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Said Elimam",
@@ -20,6 +20,7 @@
20
20
  "bin",
21
21
  "src",
22
22
  "rules",
23
+ "templates",
23
24
  "docs/decisions",
24
25
  "CHANGELOG.md"
25
26
  ],
package/rules/core.md CHANGED
@@ -2,6 +2,7 @@
2
2
  simt-id: core
3
3
  simt-title: Shared rules
4
4
  simt-applies: always
5
+ simt-priority: hard
5
6
  ---
6
7
 
7
8
  # Shared rules
@@ -6,6 +6,8 @@ simt-title: Android
6
6
  # JVM backend, which is how a Spring or Ktor service used to receive this profile
7
7
  # (S28, ADR-047).
8
8
  simt-applies: [has-android-manifest]
9
+ simt-priority: convention
10
+ simt-scope: ["AndroidManifest.xml", "**/build.gradle", "**/build.gradle.kts", "**/*.kt"]
9
11
  ---
10
12
 
11
13
  # Stack profile: Android
@@ -7,6 +7,8 @@ simt-title: Cloudflare Workers (TypeScript)
7
7
  # happily as on Workers. A wrangler config or the `wrangler` dependency is the
8
8
  # only evidence that this is a Worker (S28, ADR-047).
9
9
  simt-applies: [cloudflare-workers]
10
+ simt-priority: convention
11
+ simt-scope: ["wrangler.jsonc", "wrangler.toml", "**/worker.ts"]
10
12
  ---
11
13
 
12
14
  # Stack profile: Cloudflare Workers (TypeScript)
@@ -2,6 +2,7 @@
2
2
  simt-id: connectiq
3
3
  simt-title: Connect IQ (Monkey C)
4
4
  simt-applies: [connectiq]
5
+ simt-priority: convention
5
6
  ---
6
7
 
7
8
  # Stack profile: Connect IQ (Monkey C)
@@ -2,6 +2,8 @@
2
2
  simt-id: container
3
3
  simt-title: Containers
4
4
  simt-applies: [has-dockerfile]
5
+ simt-priority: convention
6
+ simt-scope: ["Dockerfile", "*.dockerfile"]
5
7
  simt-fallback: false
6
8
  ---
7
9
 
@@ -2,6 +2,8 @@
2
2
  simt-id: dart
3
3
  simt-title: Dart
4
4
  simt-applies: [has-pubspec]
5
+ simt-priority: convention
6
+ simt-scope: ["pubspec.yaml", "**/*.dart"]
5
7
  ---
6
8
 
7
9
  # Stack profile: Dart
@@ -2,6 +2,8 @@
2
2
  simt-id: deno
3
3
  simt-title: Deno
4
4
  simt-applies: [has-deno-config]
5
+ simt-priority: convention
6
+ simt-scope: ["deno.json", "deno.jsonc", "**/*.ts"]
5
7
  ---
6
8
 
7
9
  # Stack profile: Deno
@@ -2,6 +2,8 @@
2
2
  simt-id: dotnet
3
3
  simt-title: .NET
4
4
  simt-applies: [has-nuget]
5
+ simt-priority: convention
6
+ simt-scope: ["*.csproj", "*.sln", "*.fsproj"]
5
7
  ---
6
8
 
7
9
  # Stack profile: .NET
@@ -2,6 +2,8 @@
2
2
  simt-id: elixir
3
3
  simt-title: Elixir
4
4
  simt-applies: [has-mix]
5
+ simt-priority: convention
6
+ simt-scope: ["mix.exs", "lib/**/*.ex"]
5
7
  ---
6
8
 
7
9
  # Stack profile: Elixir
@@ -2,6 +2,8 @@
2
2
  simt-id: go
3
3
  simt-title: Go
4
4
  simt-applies: [has-go-mod]
5
+ simt-priority: convention
6
+ simt-scope: ["go.mod", "**/*.go"]
5
7
  ---
6
8
 
7
9
  # Stack profile: Go
@@ -6,6 +6,8 @@ simt-title: JVM backend
6
6
  # or Ktor service received rules about Compose, Wear OS application ids and
7
7
  # instrumentation tests — advice with nothing to apply it to (S28, ADR-047).
8
8
  simt-applies: [gradle, has-maven-pom, kotlin, java]
9
+ simt-priority: convention
10
+ simt-scope: ["build.gradle", "build.gradle.kts", "pom.xml", "settings.gradle", "settings.gradle.kts"]
9
11
  ---
10
12
 
11
13
  # Stack profile: JVM backend
@@ -2,6 +2,8 @@
2
2
  simt-id: node
3
3
  simt-title: Node (general)
4
4
  simt-applies: [node]
5
+ simt-priority: convention
6
+ simt-scope: ["package.json", "tsconfig.json", "jsconfig.json", ".nvmrc"]
5
7
  simt-fallback: true
6
8
  ---
7
9
 
@@ -2,6 +2,8 @@
2
2
  simt-id: python
3
3
  simt-title: Python
4
4
  simt-applies: [python]
5
+ simt-priority: convention
6
+ simt-scope: ["pyproject.toml", "requirements.txt", "setup.py", "*.py"]
5
7
  ---
6
8
 
7
9
  # Stack profile: Python
@@ -6,6 +6,8 @@ simt-title: React Native
6
6
  # CSS logical properties, a browser matrix, WCAG reflow at 400 % — none of which a
7
7
  # native surface has (S28, ADR-047).
8
8
  simt-applies: [react-native]
9
+ simt-priority: convention
10
+ simt-scope: ["app.json", "metro.config.js", "ios/**", "android/**"]
9
11
  ---
10
12
 
11
13
  # Stack profile: React Native
@@ -6,6 +6,8 @@ simt-title: React web
6
6
  # listing the bare language tags (`typescript`, `vitest`, `node-web`, `less`) put
7
7
  # this profile on backend and static repositories that share them (S28, ADR-047).
8
8
  simt-applies: [react-dom, react-router, maplibre, firebase]
9
+ simt-priority: convention
10
+ simt-scope: ["src/**/*.{ts,tsx,js,jsx}", "index.html"]
9
11
  ---
10
12
 
11
13
  # Stack profile: React web
@@ -2,6 +2,8 @@
2
2
  simt-id: ruby
3
3
  simt-title: Ruby
4
4
  simt-applies: [has-gemfile]
5
+ simt-priority: convention
6
+ simt-scope: ["Gemfile", "**/*.rb"]
5
7
  ---
6
8
 
7
9
  # Stack profile: Ruby
@@ -2,6 +2,8 @@
2
2
  simt-id: rust
3
3
  simt-title: Rust
4
4
  simt-applies: [has-cargo-toml]
5
+ simt-priority: convention
6
+ simt-scope: ["Cargo.toml", "src/**/*.rs"]
5
7
  ---
6
8
 
7
9
  # Stack profile: Rust
@@ -2,6 +2,8 @@
2
2
  simt-id: swift
3
3
  simt-title: Swift
4
4
  simt-applies: [has-swift-package]
5
+ simt-priority: convention
6
+ simt-scope: ["Package.swift", "**/*.swift"]
5
7
  ---
6
8
 
7
9
  # Stack profile: Swift
@@ -7,6 +7,8 @@ simt-title: TypeScript
7
7
  # repositories are selected for. Two fallbacks both apply when nothing more specific
8
8
  # matched.
9
9
  simt-applies: [typescript]
10
+ simt-priority: convention
11
+ simt-scope: ["tsconfig.json", "*.ts", "*.tsx"]
10
12
  simt-fallback: true
11
13
  ---
12
14
 
@@ -5,6 +5,8 @@ simt-title: uv
5
5
  # `uv.lock` in its body while selecting on any Python marker, so a Poetry project was
6
6
  # told to run a tool manager it does not use (S28, ADR-047).
7
7
  simt-applies: [has-uv-lock]
8
+ simt-priority: convention
9
+ simt-scope: ["uv.lock", "pyproject.toml"]
8
10
  ---
9
11
 
10
12
  # Stack profile: uv