simt-dev 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +129 -75
  3. package/bin/simt-dev.mjs +21 -2
  4. package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +1 -1
  5. package/docs/decisions/ADR-074-a-rule-and-a-decision-are-readable-by-uri.md +99 -0
  6. package/docs/decisions/ADR-075-nothing-leaves-the-machine.md +56 -0
  7. package/docs/decisions/ADR-076-a-change-to-what-agents-read-is-measured.md +70 -0
  8. package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +91 -0
  9. package/docs/decisions/ADR-078-a-line-the-repository-already-proves-does-not-ship.md +117 -0
  10. package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md +162 -0
  11. package/docs/decisions/ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md +99 -0
  12. package/docs/decisions/ADR-081-a-tracker-is-read-through-the-repositorys-template.md +36 -0
  13. package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +35 -0
  14. package/docs/decisions/ADR-083-a-budget-is-met-by-cutting-the-kernel-and-hard-is-the-floor.md +153 -0
  15. package/docs/decisions/ADR-084-a-workspace-carries-its-repeated-rules-once.md +100 -0
  16. package/docs/decisions/ADR-085-the-region-ends-with-the-names-it-kept-out.md +72 -0
  17. package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +69 -0
  18. package/docs/decisions/ADR-087-a-run-is-evidence-for-one-tree.md +67 -0
  19. package/docs/decisions/ADR-088-a-marked-line-is-enforced-by-a-checker.md +59 -0
  20. package/docs/decisions/ADR-089-the-local-index-is-a-cache-of-git-blobs.md +52 -0
  21. package/docs/decisions/ADR-090-an-optional-adapter-is-off-until-named.md +55 -0
  22. package/docs/decisions/ADR-091-the-map-ranks-files-a-task-mentions.md +60 -0
  23. package/docs/decisions/ADR-092-tests-are-imports-conventions-and-coverage.md +60 -0
  24. package/docs/decisions/ADR-093-why-names-what-governs-a-file.md +58 -0
  25. package/docs/decisions/ADR-094-a-change-runs-the-tests-that-import-it.md +44 -0
  26. package/docs/decisions/ADR-095-gate-output-keeps-the-failures.md +31 -0
  27. package/docs/decisions/ADR-096-impact-names-who-a-change-reaches.md +38 -0
  28. package/package.json +2 -1
  29. package/rules/core.md +1 -1
  30. package/src/agenthooks/decide.mjs +123 -0
  31. package/src/agenthooks/policy.mjs +78 -0
  32. package/src/agenthooks/render.mjs +240 -0
  33. package/src/argv.mjs +54 -1
  34. package/src/bench/ablate.mjs +630 -0
  35. package/src/bench/adapters.json +149 -0
  36. package/src/bench/grade.mjs +229 -0
  37. package/src/bench/mine.mjs +811 -0
  38. package/src/bench/probes.mjs +1350 -0
  39. package/src/bench/report.mjs +899 -0
  40. package/src/bench/run.mjs +1002 -0
  41. package/src/ci/workflow.mjs +11 -1
  42. package/src/commands/agenthook.mjs +282 -0
  43. package/src/commands/bench.mjs +371 -0
  44. package/src/commands/evidence.mjs +96 -0
  45. package/src/commands/gate.mjs +165 -0
  46. package/src/commands/harness.mjs +156 -5
  47. package/src/commands/help.mjs +89 -1
  48. package/src/commands/hooks.mjs +45 -5
  49. package/src/commands/impact.mjs +205 -0
  50. package/src/commands/index.mjs +60 -1
  51. package/src/commands/map.mjs +91 -0
  52. package/src/commands/plan.mjs +99 -0
  53. package/src/commands/repoindex.mjs +52 -0
  54. package/src/commands/shared.mjs +398 -0
  55. package/src/commands/sync.mjs +72 -3
  56. package/src/commands/tests.mjs +192 -0
  57. package/src/commands/tokens.mjs +61 -1
  58. package/src/commands/why.mjs +177 -0
  59. package/src/decisions/index.mjs +23 -5
  60. package/src/graph/spec.mjs +1 -1
  61. package/src/hooks/dispatcher.mjs +15 -0
  62. package/src/hooks/shim.mjs +31 -0
  63. package/src/index/compact.mjs +228 -0
  64. package/src/index/extract.mjs +331 -0
  65. package/src/index/gate.mjs +101 -0
  66. package/src/index/govern.mjs +222 -0
  67. package/src/index/impact.mjs +186 -0
  68. package/src/index/map.mjs +393 -0
  69. package/src/index/persist.mjs +172 -0
  70. package/src/index/store.mjs +301 -0
  71. package/src/index/tests.mjs +294 -0
  72. package/src/kernel/budget.mjs +100 -0
  73. package/src/kernel/card.mjs +254 -0
  74. package/src/kernel/checks.mjs +378 -0
  75. package/src/kernel/inferability.mjs +188 -0
  76. package/src/ledger/index.mjs +112 -0
  77. package/src/mcp/resources.mjs +157 -0
  78. package/src/mcp/server.mjs +37 -6
  79. package/src/mcp/tools.mjs +84 -0
  80. package/src/measure/context.mjs +184 -11
  81. package/src/measure/loaderView.mjs +435 -0
  82. package/src/measure/session.mjs +267 -0
  83. package/src/plan/index.mjs +650 -0
  84. package/src/plan/template.mjs +331 -0
  85. package/src/projection/disclosure.mjs +92 -0
  86. package/src/projection/loaders.mjs +689 -0
  87. package/src/projection/lock.mjs +93 -0
  88. package/src/projection/manifest.mjs +78 -3
  89. package/src/projection/markers.mjs +56 -17
  90. package/src/projection/project.mjs +24 -17
  91. package/src/projection/rules.mjs +452 -43
  92. package/src/projection/shared.mjs +428 -0
  93. package/src/projection/surfaces.mjs +22 -8
  94. package/src/projection/targets.mjs +2 -1
  95. package/src/toolwords.mjs +71 -0
  96. package/templates/plan/tracker.json +36 -0
@@ -0,0 +1,153 @@
1
+ # ADR-083 — A budget is met by cutting the kernel, and the hard constraints are the floor
2
+
3
+ Status: **accepted** · 2026-10-17 · filed as R08 (plan 0003) · ruling 7 narrowed by ADR-084
4
+
5
+ **Affects:** `src/kernel/budget.mjs`, `src/projection/rules.mjs`, `src/projection/manifest.mjs`,
6
+ `src/measure/context.mjs`, `src/projection/project.mjs`, `src/commands/sync.mjs`,
7
+ `test/kernel.test.mjs`, `test/tokens.test.mjs`, `test/surfaces.test.mjs`
8
+
9
+ ## Context
10
+
11
+ ADR-072 shipped the number: `budget.bytes` at the top level of the manifest, 30 000 bytes by
12
+ default, reported by `simt tokens` and raised by `check`, both non-blocking. It recorded what had
13
+ not landed and why — the packing needed a kernel, and the kernel was R07.
14
+
15
+ ADR-073 shipped the kernel. A repository declares `mode: kernel`, every rule declares its class
16
+ in its own frontmatter, and the mode packs `hard` and `command` while naming everything else in
17
+ the region with the command that fetches it. R08's accept criterion — the budget is never
18
+ exceeded — is now reachable, and this record is the part that reaches it.
19
+
20
+ Three facts about the number made the shape of this a decision rather than a loop.
21
+
22
+ The first is that the budget is one number and the meter bills more than the region. A generated
23
+ rule file is billed whole, a managed skill by its name and description, the region by the span
24
+ between its markers. A repository whose rules live in per-rule files has paid for those files
25
+ whether or not the region fits; packing the region against the whole declared number ships a
26
+ projection that fits on its own and is over the line the moment its rule files are written. On a
27
+ fixture with a Cursor surface, three `.mdc` files billed 6,552 B beside a 15,725 B region.
28
+
29
+ The second is the region's own span. It is the one number that moves when the projection is
30
+ packed, so a ceiling derived from it would make `sync` rewrite the file it had just written and
31
+ then report drift on a clean tree.
32
+
33
+ The third is what ADR-072 already ruled and R08's text has not caught up with: the key is
34
+ `budget.bytes`, not `projection.budget.bytes`. The plan is not edited. `projection` is a
35
+ generated key, `upgrade` replaces it wholesale, and a budget written there would disappear on a
36
+ green gate.
37
+
38
+ ## Rulings
39
+
40
+ 1. **One reader for the declared number.** `declaredBudget(manifest)` in `manifest.mjs` is the
41
+ only place `budget.bytes` is interpreted, and both the packer and `measureRoot` call it. A
42
+ finite non-negative value is taken as declared, including `0` — "this repository wants an
43
+ empty projection" is a decision somebody can make — and anything else (a string, `NaN`, a
44
+ negative) falls back to the review figure, because the one reading that cannot silently
45
+ produce compliance is the one where a typo is not a ceiling.
46
+
47
+ 2. **Nothing is cut in `full` mode.** ADR-072's finding already tells the reader "nothing was cut:
48
+ the projection is still in full mode", and a full projection that quietly lost a rule would
49
+ make the tool lie about itself in its own words. `desiredRegion` returns `budget: null` there,
50
+ not an empty cut list, so a reader of the field cannot conclude a cut happened when the mode
51
+ made none possible.
52
+
53
+ 3. **`hard` is the floor.** A priority class exists to say what a reader loses by not having the
54
+ line, and a byte count is not a reader. The packer skips `hard` instead of cutting it, and the
55
+ leftover is a distinct fact — `floorHeld` — reported as such: the notes say the kernel is still
56
+ over after every rule outside the hard class went, and that a hard constraint is not cut to
57
+ reach a byte count. The levers that remain are the repository's own, which is exactly what
58
+ ADR-072's kernel wording already tells the reader. One case gets its own sentence: when the
59
+ reserve alone reaches the declared budget the ceiling is zero or less, which is arithmetically
60
+ true and unreadable — it does not say that the files loading beside the region already spent
61
+ the whole budget — so the notes say that, because a reader left to infer it from a subtraction
62
+ is a reader who demotes a hard rule to fix a number they set too low.
63
+
64
+ 4. **The ceiling is the declared budget minus a reserve, and the region's own span is in
65
+ neither term.** The reserve is the larger of what the meter already bills outside the region
66
+ (what is installed) and what this projection is about to write as rule files (what a
67
+ repository that has not run `sync` yet will pay). Both are lower bounds; the larger is taken
68
+ because the failure each would otherwise cause is the one that must not happen — under-
69
+ reserving reports compliance for a projection that is over. The region's span is excluded
70
+ deliberately, per the second fact above. A folded single-surface repository reserves nothing,
71
+ which is correct rather than untested: `opencode` reads `AGENTS.md` and writes no rule files,
72
+ so there is nothing beside the region to pay for.
73
+
74
+ 5. **The cut order is the region's own order reversed, and it is fixed before any cost is
75
+ computed.** Class, then rule id — the comparator the deferred section already uses, least
76
+ valuable first. Which class a reader can best afford to lose is declared by the rules
77
+ themselves (ADR-073 ruling 3); what is left here is arithmetic, so the order is handed in and
78
+ the module that packs does not second-guess it.
79
+
80
+ 6. **A cut costs what the document then weighs, not the rule's own length.** Three things move
81
+ when a rule leaves: the provenance header drops a source, the deferred section gains a line
82
+ naming what left, and both move again as the cut list grows. So each attempt re-renders the
83
+ region and weighs it — `costs(cut)` returning `Buffer.byteLength(compose(...))`, markers and
84
+ provenance header included, one byte conservative because the file's trailing newline falls
85
+ outside the meter's span and conservative is the only direction a ceiling can afford. The loop
86
+ stops the moment it fits, so a budget never costs a rule it did not have to. This is the
87
+ command card's `CARD_LIMIT_BYTES` idiom (ADR-077) applied to the region instead of the card.
88
+
89
+ 7. **One list, two reasons.** Class-deferred and budget-cut rules share the region's existing
90
+ `## Deferred rules` section, and each line says which it was: `deferred in kernel mode` or
91
+ `cut to fit the declared budget of N bytes`, both naming `simt rules show <id>`. A second
92
+ heading would spend region bytes on a distinction the reader acts on identically, and a cut
93
+ line that did not say why would be indistinguishable from a kernel decision nobody made.
94
+
95
+ 8. **`BUDGET-CUT` is a finding, non-blocking in both commands, and carries the numbers a reader
96
+ needs to judge it.** The detail gives `before → after`, the declared budget, the reserve when
97
+ it is not zero, and every cut rule by id and class; the remedy is a decision
98
+ (`raise budget.bytes to <before + reserve>`, the budget that would have admitted the region
99
+ as it stood — `before` alone would leave the cut in place, because the ceiling it was cut
100
+ against is the budget less the reserve), not a place to go and look. Non-blocking for ADR-072's
101
+ reason: the cut is the tool doing what the repository asked, and a gate that failed here would
102
+ fail a repository for shipping a smaller projection. `sync` prints the same two facts as notes
103
+ so the cut is never silent from either command.
104
+
105
+ 9. **`simt tokens` keeps no second opinion.** ADR-064 kept the meter to one measurement, and the
106
+ packer's arithmetic is not substituted for it anywhere: the meter reports the bytes it finds
107
+ against the declared line, and the end-to-end test judges the cut with the meter's number
108
+ rather than the packer's. A packer that reports compliance for a projection the meter says is
109
+ over budget is the failure this arrangement is arranged against, so the test asserts the
110
+ meter's number even where the packer's own numbers are already visible in the same object.
111
+
112
+ ## Consequences
113
+
114
+ A repository that declares `mode: kernel` and a budget its rules do not fit gets a smaller
115
+ projection, a list of what left and why, and a note from both `sync` and `check`. The accept
116
+ criterion is checkable end to end rather than asserted: a fixture declares a budget a thousand
117
+ bytes under its own projection, syncs, and `simt tokens` reports `over: false` with the declared
118
+ line intact — and a second fixture, whose rule files are billed beside the region, is cut further
119
+ than the region alone would have needed, which is the only way the reserve can be seen to work.
120
+
121
+ The costs are accepted rather than avoided. In kernel mode a projection is now measured
122
+ (`measureRoot`) and its rule targets rendered a second time (`ruleTargets`) on every
123
+ `desiredRegion`, where previously the region's bytes were composed once. `measureRoot` is reached
124
+ through `await import` inside `budgetReserve` because `measure/context.mjs` already imports
125
+ `readLimitFindings` from `projection/project.mjs`; a static import from `rules.mjs` would close a
126
+ three-module loop that has to survive being loaded in any order.
127
+
128
+ Determinism is structural and partly argued. `packToBudget` takes its cost function as an
129
+ argument and reads no clock, no network and no PATH, so the cut set is a function of the rules,
130
+ the classes, the ids and the declared number; the test that renders one repository twice catches
131
+ the failure a test can catch, which is state leaking between calls. Two machines is the claim
132
+ R08 actually makes and no test in this repository can execute it — the argument is the one ADR-073
133
+ ruling 7 already makes.
134
+
135
+ What has not landed, and is not this slice's to land: R02's evaluation, which is still what would
136
+ justify `kernel` as a default rather than a repository's own choice; and the cross-machine half
137
+ of the accept criterion, which is argued from the code's inputs rather than demonstrated.
138
+
139
+ ## Rejected
140
+
141
+ - **Cutting in `full` mode.** The mode is a promise about what a reader gets, and the number in
142
+ ADR-072's finding says nothing was cut; making that sentence untrue to save bytes is the tool
143
+ misreporting itself.
144
+ - **Cutting `hard` to reach the line.** It would remove the one class whose claim is that its
145
+ absence is a defect, and report the result as compliance.
146
+ - **A second `## Cut` section beside `## Deferred rules`.** Two headings for one reader-facing
147
+ fact, and the second one spends bytes to be distinguished from the first.
148
+ - **Subtracting each rule's own length instead of re-rendering.** The provenance header and the
149
+ deferred line both change when a rule leaves, so the arithmetic would either overshoot or stop
150
+ short — and it would be a second definition of the region's size next to the meter's.
151
+ - **A binary search or two-pass packer.** The cost function is called once per candidate and the
152
+ candidate list is the length of the kernel; a cleverer search would add a failure mode to a
153
+ loop whose whole value is that it is the same computation every time.
@@ -0,0 +1,100 @@
1
+ # ADR-084 — A workspace carries its repeated rules once, and the region does not read the reader's home
2
+
3
+ Status: **accepted** · 2026-10-17 · filed as R11 (plan 0003)
4
+
5
+ **Affects:** `src/projection/shared.mjs`, `src/commands/shared.mjs`, `src/projection/rules.mjs`,
6
+ `src/projection/manifest.mjs`, `src/projection/markers.mjs`, `src/commands/tokens.mjs`,
7
+ `src/commands/index.mjs`
8
+
9
+ ## Context
10
+
11
+ The same rules are projected into every repository of a workspace, and each repository's
12
+ region carries the full text. The plan asked for the intersection to be installed once, at
13
+ user level, and for a repository to switch to the remainder when its manifest said
14
+ `projection.shared`. It also named home paths for four harnesses, and said CI should keep
15
+ rendering the full region so a teammate without the user file loses nothing in review.
16
+
17
+ Two of those would not survive contact with the rest of the tool. `projection` is generated:
18
+ `upgrade` replaces it, which is why a repository's own budget already lives beside it rather
19
+ than inside it. And a region that is full in CI and short on a reader's machine is a region
20
+ `check` cannot agree with, because `check` renders the same function the reader gets.
21
+
22
+ ## Rulings
23
+
24
+ 1. **The unit that is shared is a rule, and only a rule every repository renders with the
25
+ same bytes.** The meter can report a repeated paragraph; it cannot store one, and
26
+ `simt rules show` prints one rule. A rule one repository does not ship, or renders
27
+ differently, stays in every region. The comparison is the rendered text, because one
28
+ repository can drop a line the others keep.
29
+
30
+ 2. **The declaration is `shared` on the manifest, beside `budget` and `mode`.** `level` is
31
+ `user` and `rules` is the list of ids. It is not a key of `projection`, because that
32
+ object is replaced on upgrade. A missing key, a level other than `user`, or an empty
33
+ list means the region carries everything it carried before. A value that is not that
34
+ shape is an error from the one reader the region, `doctor` and `simt shared` share.
35
+
36
+ 3. **The file that is written is the one the loader table says the harness reads.** The
37
+ plan named paths. A path the table does not record would be written and never loaded, so
38
+ a surface with no user-level row, and a pointer surface with no loader row, gets a block
39
+ to paste and no path. Where the table names a flat user rules directory, the file is
40
+ `simt.md`. Where a harness reads one of two files and the reader already has a non-empty
41
+ one, that file is the one written. A cap the table states in tokens is reported, not
42
+ compared with a byte count, and a copy over its cap is reported rather than refused.
43
+
44
+ 4. **`simt shared plan` writes nothing. `simt shared install` writes only with `--yes`.**
45
+ The write is the user-level file, keeping whatever prose sits outside the generated
46
+ region, and the same declaration in every repository that already has a manifest. A
47
+ repository without one is skipped, with the command that adopts it. A workspace with
48
+ nothing to share writes neither an empty home file nor a declaration. The regions
49
+ change on the next `simt sync`, not during the install.
50
+
51
+ 5. **The region is a function of the declaration, not of the reader's home directory.** CI
52
+ and the reader therefore render the same delta. A teammate without the user file loses
53
+ the shared rules from the region; `simt doctor` says so — current, stale, absent,
54
+ unreadable, or no target — and does not fail the gate. The index line names the rule,
55
+ why it is not in the file, and `simt rules show <id>`. It does not name a home path,
56
+ because that path is not true on both machines.
57
+
58
+ 6. **One section holds every reason a rule is missing, and its heading names the fact.**
59
+ Class-deferred, cut to a budget, and carried once are three decisions and one fact for
60
+ the reader: the rule is not in this file. The heading is `## Rules not in this region`.
61
+ This narrows ADR-073 ruling 6 and ADR-083 ruling 7, which named that section
62
+ `## Deferred rules` when a class deferral was the only reason to be in it. The rest of
63
+ both rulings stands: one section, each line says which reason, and the remedy is the
64
+ same command.
65
+
66
+ 7. **A repository's own profile or pack is not shareable.** It is policy that repository
67
+ owns. Putting it in a home file would change one repository by editing a file every
68
+ sibling reads. Only rules this package ships are candidates, and the shareable set is
69
+ computed before the declaration drops them, so a second install does not shrink the
70
+ intersection against an already-reduced list.
71
+
72
+ 8. **`simt tokens` counts the user-level copy once.** `tokens --repos` adds the whole file
73
+ to the estate total, prose outside the region included, because that prose is also
74
+ always-on for the reader. A repository on its own counts the same file, so a single
75
+ root does not look cheaper than the workspace it belongs to.
76
+
77
+ ## Consequences
78
+
79
+ `simt shared plan` is the default of `simt shared`, for the same reason `simt skills` lists
80
+ before it installs: the read answers what would happen, and the write needs `--yes`.
81
+
82
+ The plan's accept bar — always-on bytes for the seven-root estate at most 30 KB, and no R
83
+ loss in bench — is **unmeasured**. The tests prove the mechanism on fixtures: identical
84
+ rules leave the region, a divergent rule stays, the reader's own prose survives, and the
85
+ meter counts the copy once. They do not measure the estate, and they do not run the bench.
86
+
87
+ ## Rejected
88
+
89
+ - **`projection.shared`.** The next upgrade would delete it, and the region would silently
90
+ grow back.
91
+ - **CI renders the full region while a reader renders the delta.** `check` would report
92
+ drift on every machine that had installed the copy, or would have to read a home
93
+ directory the projection is forbidden to depend on.
94
+ - **The union of rules across repositories.** A rule one repository does not ship is that
95
+ repository's absence, and installing it at user level would give the rule to a reader of
96
+ a repository that declined it.
97
+ - **Writing the paths the plan named when the loader table records a different file, or
98
+ none.** A file the harness does not read is a second copy the reader never sees.
99
+ - **An empty shared file for a workspace with nothing to share.** It would look like a
100
+ decision and contain none.
@@ -0,0 +1,72 @@
1
+ # ADR-085 — The region ends with the names of what it kept out
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R12 (plan 0003)
4
+
5
+ **Affects:** `src/projection/disclosure.mjs`, `src/projection/rules.mjs`, `src/graph/spec.mjs`,
6
+ `test/disclosure.test.mjs`, `test/scoped.test.mjs`
7
+
8
+ ## Context
9
+
10
+ A profile that declares a scope already leaves the always-on region when every selected
11
+ surface can load it narrowly, and the region keeps a pointer (ADR-071). A package in a
12
+ monorepo already gets its own region (ADR-066). What a reader still cannot see is an
13
+ installed skill: the body lives in its own file, and a surface with no skill channel never
14
+ mentions the name, so the reader has no reason to open it.
15
+
16
+ The plan asked for one index at the end of the kernel, one line per scoped rule and per
17
+ skill, carrying the name and the trigger. The accept is that the scoped body is absent
18
+ from what loads at session start and present in the file a matching path loads, checked
19
+ per surface.
20
+
21
+ ## Rulings
22
+
23
+ 1. **The index is the last section of the region, and it is one section.** Its heading is
24
+ `## Disclosure`. A scoped profile keeps the pointer ADR-071 ruling 7 requires: the
25
+ patterns, and the files the profile moved to. A skill keeps its name and its
26
+ description, which is the trigger. A second heading for the second kind would spend
27
+ region bytes on a distinction the reader acts on the same way — open that thing when
28
+ the line says to.
29
+
30
+ 2. **A skill contributes nothing but that line.** The body stays in the skill file. The
31
+ description is read with the same frontmatter reader the skill lint uses, so a folded
32
+ description is the sentence the author wrote and not the marker in front of it. A
33
+ skill with no description says so and names `simt skills show`, because a line that
34
+ invents a trigger would send the reader to the wrong file.
35
+
36
+ 3. **The directories are the four a skill is installed into.** They are the directory
37
+ `skills install` chooses and the directories a loader row records, which together are
38
+ four and not a walk of the repository. A symlink to a skill already counted is not a
39
+ second skill. Two different files that share a name are both listed, with the
40
+ directory, because keeping one would hide a trigger.
41
+
42
+ 4. **The index is region text, so every selected surface sees the same lines.** A surface
43
+ that already shows a skill's name and description at session start sees the line
44
+ twice. The alternative is a region that depends on which surface is reading it, and
45
+ `check` would then disagree with a reader whose surface has no skill channel. The
46
+ body is the copy that stays out. ADR-071 ruling 1 still decides the profile itself:
47
+ it leaves the region only when every selected surface can narrow it.
48
+
49
+ 5. **The index is not a candidate for a budget cut.** Cutting it would remove the only
50
+ mention of the rules a cut had already removed. It is weighed, because it does cost
51
+ bytes, and it is kept.
52
+
53
+ ## Consequences
54
+
55
+ On a surface that can narrow, a fixture's scoped profile is absent from the region and
56
+ present in the file that surface loads when a matching path is in play. On a surface that
57
+ cannot, the profile stays in the region. That is the accept, checked by rendering each
58
+ surface rather than by driving the harness: this tool writes the file, and the harness
59
+ is what opens it.
60
+
61
+ A repository with no installed skill and no scoped profile gains no section. An empty
62
+ heading would be a line about nothing.
63
+
64
+ ## Rejected
65
+
66
+ - **Putting the skill body in the region.** That is the cost the index exists to avoid,
67
+ and it is what a surface with a skill channel already refuses to do.
68
+ - **Omitting the line on a surface that shows skill metadata itself.** The region is one
69
+ document. A line present for one reader and absent for another is drift.
70
+ - **Truncating a description to keep the line short.** The description is the trigger. A
71
+ shorter line that no longer says when to open the skill is a line that cannot do its
72
+ job.
@@ -0,0 +1,69 @@
1
+ # ADR-086 — The stable prefix comes first, and the lock records both digests
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R13 (plan 0003)
4
+
5
+ **Affects:** `src/projection/rules.mjs`, `src/projection/lock.mjs`, `src/commands/sync.mjs`,
6
+ `src/argv.mjs`, `test/lock.test.mjs`, `test/card.test.mjs`
7
+
8
+ ## Context
9
+
10
+ A reader caches the instruction file. A change in the first bytes throws the rest away,
11
+ including the rules that did not change. The plan asks for the stable content first —
12
+ core, then stack, then what this repository added — and for a lock that fails when the
13
+ output moves and the input did not.
14
+
15
+ The command card was the first bytes of the region (ADR-077 ruling 1), because the
16
+ commands are what a reader cannot infer. They are also the bytes a repository edit is
17
+ most likely to move: a script change would then invalidate the core that follows them.
18
+
19
+ The content digest stays in the generated header. It is a digest of what this package
20
+ ships (ADR-032), so a repository edit does not move it, and `check` uses it to tell a
21
+ hand-edit from a version mismatch. Taking it out of the file would make those two
22
+ failures look the same. No date and no absolute path is written into the region.
23
+
24
+ ## Rulings
25
+
26
+ 1. **Shipped rules, then this repository's own bytes.** Core first, then the other
27
+ profiles this package ships, by id. Then the command card, then profiles the
28
+ repository added, then the index of what the region does not carry. This narrows
29
+ ADR-077 ruling 1: the card still leads the repository's own bytes, and it no longer
30
+ leads the region. The rest of that record stands, including that the card is derived
31
+ from committed evidence only.
32
+
33
+ 2. **The order inside the shipped prefix is by id, not by directory order.** A directory
34
+ listing is not a fact about the rules. Two machines rendering one repository produce
35
+ the same bytes; the test renders one repository twice, which is the half a single
36
+ process can see. The other half is the same argument ADR-073 ruling 7 makes: the
37
+ inputs are the files, and nothing in the render reads the clock, the home directory
38
+ or the network.
39
+
40
+ 3. **`simt-dev.lock.json` records an input digest and an output digest.** The input is
41
+ the package digest, the detected signals, the manifest keys that change the region,
42
+ the repository's own profiles and its `package.json` scripts. The output is the
43
+ region body. `sync --yes` rewrites the lock when it differs. `check` without
44
+ `--frozen` does not require one, because every repository adopted before this record
45
+ would otherwise fail a gate it had not been asked to keep.
46
+
47
+ 4. **`check --frozen` fails in two ways, and names which.** No lock, or a lock whose
48
+ input digest differs, is a stale lock: rewrite it with `sync --yes`. A lock whose
49
+ input matches and whose output does not is unexplained: the projection changed and
50
+ nothing it read did. An output change that an input change explains is the stale
51
+ case, not the unexplained one.
52
+
53
+ ## Consequences
54
+
55
+ Editing a repository profile leaves the bytes before that profile identical. A frozen
56
+ check on a matching lock passes, a tampered output fails as unexplained, and a changed
57
+ profile fails as a stale lock.
58
+
59
+ ## Rejected
60
+
61
+ - **Removing the content digest from the header.** The digest does not move when the
62
+ repository edits a line, and without it a hand-edit and a version mismatch are one
63
+ finding.
64
+ - **Hashing the whole manifest, or the whole `package.json`.** A comment, or a
65
+ dependency bump that the projection does not read, would fail the lock for a region
66
+ that had not changed.
67
+ - **Failing an ordinary `check` when the lock is absent.** The lock is new. The
68
+ repositories this package already adopted do not have one, and a gate that turned red
69
+ for that absence would be red everywhere until someone ran `sync`.
@@ -0,0 +1,67 @@
1
+ # ADR-087 — A run is evidence for one tree
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R17 (plan 0003)
4
+
5
+ **Affects:** `src/ledger/index.mjs`, `src/commands/evidence.mjs`, `src/hooks/dispatcher.mjs`,
6
+ `test/ledger.test.mjs`
7
+
8
+ ## Context
9
+
10
+ The rule that a test was run is otherwise a sentence in a commit message. Nothing checks
11
+ it, so a message can say the tests passed for a tree they were never run against, and
12
+ for a tree that has been edited since. The plan asks for a ledger, a proof block, and a
13
+ commit-message check that warns unless the repository asks it to block.
14
+
15
+ The tree has to be named without staging. `git add -A` on the real index would take the
16
+ author's unstaged work into the commit that is about to happen, which is the opposite of
17
+ what a hook is allowed to do.
18
+
19
+ ## Rulings
20
+
21
+ 1. **The tree is everything on disk, taken with a private index.** `git add -A` and
22
+ `git write-tree` run against a temporary `GIT_INDEX_FILE`, which is deleted
23
+ afterwards. The repository's own index is not read for this and not written. A file
24
+ the author left unstaged stays unstaged. The ledger file is left out of that add,
25
+ because the line is a record of the tree and writing it must not replace the tree
26
+ it names.
27
+
28
+ 2. **A ledger line records the run, not its output.** The command, the exit code, the
29
+ duration, a one-line summary, and a digest of the output. The output itself is not
30
+ stored. The line is appended when the dispatcher actually runs a gate. A gate that
31
+ does not run leaves no line. A tree that cannot be named leaves no line, and the
32
+ claim check then finds nothing.
33
+
34
+ 3. **A verification claim is a passing line for this tree.** The phrases are `tests
35
+ pass`, `tests passed`, `test pass`, `test passed`, and `verified`. `unverified` and
36
+ `not verified` are not claims. A line with a non-zero exit does not support the
37
+ claim. Editing any file changes the tree, so the line stops supporting it.
38
+
39
+ 4. **The check warns, and blocks only when the repository says so.** `gates.evidence`
40
+ set to `block` fails the commit. Unset, or `warn`, prints the warning and exits 0.
41
+ Any other value is an error, because a misspelt `block` must not become a warning.
42
+ `simt evidence` prints the lines for the current tree and writes nothing.
43
+ `simt hooks install --yes` writes a commit-msg shim only when `gates.evidence` is
44
+ `warn` or `block`. The shim forwards the message file. Unset installs nothing,
45
+ and removing the setting removes the shim, so a hook the repository did not ask
46
+ for does not stay behind and replace git's own.
47
+
48
+ ## Consequences
49
+
50
+ A green run followed by an edit of any file leaves the new tree with no evidence. The
51
+ old line is still in the ledger, and it names the tree it was true for. A commit
52
+ message that claims the tests passed is blocked only in a repository that set
53
+ `gates.evidence` to `block`.
54
+
55
+ ## Rejected
56
+
57
+ - **Staging with the real index in order to name the tree.** The next commit would
58
+ contain files the author had not staged.
59
+ - **Storing the command's output in the ledger.** A test log is not evidence the next
60
+ reader needs, and it is where a secret printed by a failing command would be kept.
61
+ - **Treating any ledger line as proof the tests passed.** A failing run is a line. It
62
+ is not a pass.
63
+ - **Blocking every repository by default.** The check is new. A repository that has
64
+ not asked for it gets a warning, which it can read, and its commit still happens.
65
+ - **Adding commit-msg to the dispatcher events.** Those shims do not forward git's
66
+ arguments, because a pre-push remote would be read as one. The message file is
67
+ the argument, so the claim check is its own shim.
@@ -0,0 +1,59 @@
1
+ # ADR-088 — A marked line is enforced by a checker
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R19 (plan 0003) · ruling 3 narrowed by ADR-090
4
+
5
+ **Affects:** `src/kernel/checks.mjs`, `rules/core.md`, `src/commands/sync.mjs`,
6
+ `test/checks.test.mjs`
7
+
8
+ ## Context
9
+
10
+ A line an agent is trusted to follow is a line the next agent can skip. The file-length
11
+ rule is already two numbers with two meanings, and the check that was supposed to apply
12
+ them was never run. The plan asks for a line to name a checker, for the projection to
13
+ keep the name, and for the checker to be what actually decides.
14
+
15
+ One of the named checkers, `import-direction`, needs a module graph this tool does not
16
+ have yet. Removing that instruction now would leave a name and no check.
17
+
18
+ ## Rulings
19
+
20
+ 1. **A list item may name one checker.** The marker is `simt-check`, on the item,
21
+ including the lines wrapped under it. The checkers are `max-file-lines`,
22
+ `forbid-regex`, `forbidden-path-edit`, `required-file`, and `import-direction`.
23
+ Anything else is a broken rule. A marker inside a fenced sample is not a checker.
24
+
25
+ 2. **A checker that can run replaces the item with its name.** The projection says
26
+ `Enforced:` and the checker. The prose does not. `simt check` runs the checker
27
+ against the rule as written, so the line that left the prompt is the line that
28
+ still decides. `max-file-lines` reads `limits.fileLines`: over the trigger warns,
29
+ over the ceiling fails. A path is exempt when `limits.fileLines.generated` names
30
+ it, or when the file carries the generated-region marker this tool writes, which
31
+ is this tool naming itself as the generator. The numbers stay 400 and 1000 where
32
+ a repository has not named its own.
33
+
34
+ 3. **`import-direction` does not replace its item.** The module graph it reads is not
35
+ built. The instruction stays, the marker does not reach the reader, and `simt check`
36
+ says the checker is not built. It does not fail the repository for that.
37
+
38
+ 4. **A diff checker reads the change.** `forbid-regex` and `forbidden-path-edit` look
39
+ at added lines and changed paths, including untracked files. A match already in the
40
+ last commit is not a new match. A directory that is not a git repository is not a
41
+ failure of these checkers: there is no tree to name.
42
+
43
+ ## Consequences
44
+
45
+ The file-length sentence no longer occupies the region. The bytes it occupied are
46
+ smaller by the length of that sentence. Whether agents still comply with the rule is
47
+ unmeasured: the probes were not run.
48
+
49
+ `simt check` now fails a tracked file past the ceiling in every repository that
50
+ selects the core rules, which is every repository.
51
+
52
+ ## Rejected
53
+
54
+ - **Dropping `import-direction` before it can run.** The name would say the line is
55
+ enforced. Nothing would be enforcing it.
56
+ - **Failing `simt check` outside a git repository.** There is no tracked tree to count
57
+ and no diff to read. A missing repository is not a long file.
58
+ - **Exempting a file because it looks generated.** The repository names the generator.
59
+ A guess would hide the ceiling from the file that most needs it.
@@ -0,0 +1,52 @@
1
+ # ADR-089 — The local index is a cache of git blobs
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R20 (plan 0003)
4
+
5
+ **Affects:** `src/index/store.mjs`, `src/commands/repoindex.mjs`, `test/index.test.mjs`
6
+
7
+ ## Context
8
+
9
+ Later slices ask which file defines a name, which test covers a change, and which
10
+ decision governs a path. Those questions need a store, and the store has to be
11
+ rebuildable: a cache that cannot be reconstructed from the repository is a second
12
+ source of truth. The plan names `node:sqlite`, with a fallback for a Node that does
13
+ not have it, and it names the blob hash from `git ls-files` as the unit of change.
14
+
15
+ The floor this package declares is older than the Node that first shipped that module
16
+ as something a program can rely on. A store that only works on the machine that
17
+ happened to build it is not the store the plan describes.
18
+
19
+ ## Rulings
20
+
21
+ 1. **The store is sqlite when this process can open it and create an FTS5 table.**
22
+ The file is `.simt-dev/index.sqlite`. The tables are the ones the plan names:
23
+ files, symbols, edges, an FTS5 table, and meta. When the module is absent or FTS5
24
+ cannot be created, the same file rows are written to `.simt-dev/index.jsonl`.
25
+ Nothing is added to the dependency set.
26
+
27
+ 2. **A row is keyed by the git blob.** `git ls-files -s` supplies the path and the
28
+ blob. A path whose blob has not changed is not read again, and its row is not
29
+ rewritten. A path that left the index leaves the store, and so does anything
30
+ recorded against that path. Two indexes of the same blobs produce the same file
31
+ rows. The language comes from the extension, and the package comes from the
32
+ nearest manifest walking up from the file.
33
+
34
+ 3. **`simt index` rebuilds the cache.** It does not preview, because the file it
35
+ writes is not repository content. It says when the path is not gitignored. A
36
+ directory that is not a git repository cannot be indexed.
37
+
38
+ ## Consequences
39
+
40
+ Reindexing one changed file reads that blob and leaves the others. The symbols and
41
+ edges tables exist and are cleared for a path whose blob changed. Nothing fills them
42
+ until a later slice extracts names from the bytes.
43
+
44
+ ## Rejected
45
+
46
+ - **Requiring sqlite.** The floor this package already ships does not promise the
47
+ module. A repository on that floor still gets an index.
48
+ - **Hashing the worktree instead of the blob.** An unstaged edit is not what
49
+ `git ls-files` reports. The index follows the index, so two machines with the same
50
+ committed blobs agree.
51
+ - **Treating the cache as a writer that previews.** A preview that does not write
52
+ leaves the reader with no index. The command's only effect is the cache.
@@ -0,0 +1,55 @@
1
+ # ADR-090 — An optional adapter is off until a repository names it
2
+
3
+ Status: **accepted** · 2026-10-18 · filed as R21 (plan 0003)
4
+
5
+ **Affects:** `src/index/extract.mjs`, `src/index/store.mjs`, `src/index/persist.mjs`,
6
+ `test/extract.test.mjs`
7
+
8
+ ## Context
9
+
10
+ The index can say which file defines a name only if something reads the blob. A
11
+ structural parser does that more accurately than a scan of lines, and it is also a
12
+ dependency: grammars, a WASM runtime, a package this tool would then require. The
13
+ floor this package ships is zero dependencies. An adapter that is always loaded is
14
+ not optional, and an adapter this package names for one organisation is a name the
15
+ next repository does not have.
16
+
17
+ The module graph of imports now exists. ADR-088 ruling 3 kept `import-direction` in
18
+ the prose because that graph did not. The graph still does not say which way an
19
+ import is allowed to point.
20
+
21
+ ## Rulings
22
+
23
+ 1. **The built-in extractors are the index.** They read one line at a time and
24
+ record definitions, exports and imports for the languages a map needs. The same
25
+ blob produces the same symbol rows and the same import edges. A blob that has
26
+ not changed is not read again. Recall against a hand-labelled set of exported
27
+ names, for TypeScript, Kotlin, Python and Go, is at least 90%. A comparison
28
+ against a structural parser was not run.
29
+
30
+ 2. **An optional adapter is a separate package, and it is off until named.**
31
+ `index.parsers` is the package name. Absent, nothing is imported. This package
32
+ does not depend on a parser and does not ship a default name for one. When the
33
+ named package does not load, the built-in rows are the cache and the index says
34
+ so. When it loads, its symbols and edges replace the built-in rows for that
35
+ blob. It writes those rows into the cache. It does not edit a source file.
36
+
37
+ 3. **`import-direction` still does not replace its line.** ADR-088 ruling 3 stands,
38
+ except for the sentence that the module graph is not built. The edges are
39
+ recorded. No rule names the direction they must follow, so the instruction stays
40
+ and `simt check` does not fail a repository for the missing direction.
41
+
42
+ ## Consequences
43
+
44
+ `simt index` now reports how many symbols and edges the cache holds. Whether the
45
+ line extractor misses a name a parser would have found, on a real repository, is
46
+ unmeasured: the bar above is the labelled fixtures, not a live parser.
47
+
48
+ ## Rejected
49
+
50
+ - **Depending on a parser, or probing one this package names.** The dependency set
51
+ would no longer be empty, and a name that only one organisation publishes is not
52
+ a default for every repository.
53
+ - **Treating the import edges as a direction check.** An edge says that A imports B.
54
+ It does not say that A was allowed to. Dropping the instruction would report the
55
+ line as enforced.