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.
- package/CHANGELOG.md +100 -0
- package/README.md +129 -75
- package/bin/simt-dev.mjs +21 -2
- package/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +1 -1
- package/docs/decisions/ADR-074-a-rule-and-a-decision-are-readable-by-uri.md +99 -0
- package/docs/decisions/ADR-075-nothing-leaves-the-machine.md +56 -0
- package/docs/decisions/ADR-076-a-change-to-what-agents-read-is-measured.md +70 -0
- package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md +91 -0
- package/docs/decisions/ADR-078-a-line-the-repository-already-proves-does-not-ship.md +117 -0
- package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md +162 -0
- package/docs/decisions/ADR-080-the-tools-own-names-are-not-repositories-and-neither-reader-decides-by-shape.md +99 -0
- package/docs/decisions/ADR-081-a-tracker-is-read-through-the-repositorys-template.md +36 -0
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +35 -0
- package/docs/decisions/ADR-083-a-budget-is-met-by-cutting-the-kernel-and-hard-is-the-floor.md +153 -0
- package/docs/decisions/ADR-084-a-workspace-carries-its-repeated-rules-once.md +100 -0
- package/docs/decisions/ADR-085-the-region-ends-with-the-names-it-kept-out.md +72 -0
- package/docs/decisions/ADR-086-the-stable-prefix-comes-first-and-the-lock-records-both-digests.md +69 -0
- package/docs/decisions/ADR-087-a-run-is-evidence-for-one-tree.md +67 -0
- package/docs/decisions/ADR-088-a-marked-line-is-enforced-by-a-checker.md +59 -0
- package/docs/decisions/ADR-089-the-local-index-is-a-cache-of-git-blobs.md +52 -0
- package/docs/decisions/ADR-090-an-optional-adapter-is-off-until-named.md +55 -0
- package/docs/decisions/ADR-091-the-map-ranks-files-a-task-mentions.md +60 -0
- package/docs/decisions/ADR-092-tests-are-imports-conventions-and-coverage.md +60 -0
- package/docs/decisions/ADR-093-why-names-what-governs-a-file.md +58 -0
- package/docs/decisions/ADR-094-a-change-runs-the-tests-that-import-it.md +44 -0
- package/docs/decisions/ADR-095-gate-output-keeps-the-failures.md +31 -0
- package/docs/decisions/ADR-096-impact-names-who-a-change-reaches.md +38 -0
- package/package.json +2 -1
- package/rules/core.md +1 -1
- package/src/agenthooks/decide.mjs +123 -0
- package/src/agenthooks/policy.mjs +78 -0
- package/src/agenthooks/render.mjs +240 -0
- package/src/argv.mjs +54 -1
- package/src/bench/ablate.mjs +630 -0
- package/src/bench/adapters.json +149 -0
- package/src/bench/grade.mjs +229 -0
- package/src/bench/mine.mjs +811 -0
- package/src/bench/probes.mjs +1350 -0
- package/src/bench/report.mjs +899 -0
- package/src/bench/run.mjs +1002 -0
- package/src/ci/workflow.mjs +11 -1
- package/src/commands/agenthook.mjs +282 -0
- package/src/commands/bench.mjs +371 -0
- package/src/commands/evidence.mjs +96 -0
- package/src/commands/gate.mjs +165 -0
- package/src/commands/harness.mjs +156 -5
- package/src/commands/help.mjs +89 -1
- package/src/commands/hooks.mjs +45 -5
- package/src/commands/impact.mjs +205 -0
- package/src/commands/index.mjs +60 -1
- package/src/commands/map.mjs +91 -0
- package/src/commands/plan.mjs +99 -0
- package/src/commands/repoindex.mjs +52 -0
- package/src/commands/shared.mjs +398 -0
- package/src/commands/sync.mjs +72 -3
- package/src/commands/tests.mjs +192 -0
- package/src/commands/tokens.mjs +61 -1
- package/src/commands/why.mjs +177 -0
- package/src/decisions/index.mjs +23 -5
- package/src/graph/spec.mjs +1 -1
- package/src/hooks/dispatcher.mjs +15 -0
- package/src/hooks/shim.mjs +31 -0
- package/src/index/compact.mjs +228 -0
- package/src/index/extract.mjs +331 -0
- package/src/index/gate.mjs +101 -0
- package/src/index/govern.mjs +222 -0
- package/src/index/impact.mjs +186 -0
- package/src/index/map.mjs +393 -0
- package/src/index/persist.mjs +172 -0
- package/src/index/store.mjs +301 -0
- package/src/index/tests.mjs +294 -0
- package/src/kernel/budget.mjs +100 -0
- package/src/kernel/card.mjs +254 -0
- package/src/kernel/checks.mjs +378 -0
- package/src/kernel/inferability.mjs +188 -0
- package/src/ledger/index.mjs +112 -0
- package/src/mcp/resources.mjs +157 -0
- package/src/mcp/server.mjs +37 -6
- package/src/mcp/tools.mjs +84 -0
- package/src/measure/context.mjs +184 -11
- package/src/measure/loaderView.mjs +435 -0
- package/src/measure/session.mjs +267 -0
- package/src/plan/index.mjs +650 -0
- package/src/plan/template.mjs +331 -0
- package/src/projection/disclosure.mjs +92 -0
- package/src/projection/loaders.mjs +689 -0
- package/src/projection/lock.mjs +93 -0
- package/src/projection/manifest.mjs +78 -3
- package/src/projection/markers.mjs +56 -17
- package/src/projection/project.mjs +24 -17
- package/src/projection/rules.mjs +452 -43
- package/src/projection/shared.mjs +428 -0
- package/src/projection/surfaces.mjs +22 -8
- package/src/projection/targets.mjs +2 -1
- package/src/toolwords.mjs +71 -0
- package/templates/plan/tracker.json +36 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# ADR-076 — A change to what agents read is measured before it ships, or labelled unmeasured in its record
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-15 · filed as D1 (R01)
|
|
4
|
+
**Affects:** `src/bench/mine.mjs`, `src/commands/bench.mjs`, `src/commands/index.mjs`, `src/commands/help.mjs`, `test/bench.test.mjs`
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Every rule and profile projected by this package enters an agent's context window on every
|
|
9
|
+
turn, yet nothing in the repository could say whether a paragraph improved resolution,
|
|
10
|
+
increased token cost, or did neither. Controlled studies on repository context files show
|
|
11
|
+
that unmeasured instructions routinely raise inference cost by 20% or more without moving
|
|
12
|
+
task resolution when they repeat facts the repository already proves.
|
|
13
|
+
|
|
14
|
+
Without a repository-native task suite and a rule requiring evidence, changes to projected
|
|
15
|
+
context are judged by how plausible the prose sounds to a reviewer rather than by what an
|
|
16
|
+
agent does with it. And a task suite whose verification tests sit inside the agent's
|
|
17
|
+
worktree is trivial to game: an agent can read or weaken the test it is being graded by.
|
|
18
|
+
|
|
19
|
+
## Rulings
|
|
20
|
+
|
|
21
|
+
1. **A change to projected context is measured against the benchmark suite before it ships,
|
|
22
|
+
or its decision record states `unmeasured` and why.** Context changes include core
|
|
23
|
+
rules, stack profiles, process rules, kernel compilation, command cards and brief
|
|
24
|
+
assembly. When a slice cannot be run through the benchmark (for example, before the
|
|
25
|
+
runner is wired or when a rule governs an external release gate), the decision record
|
|
26
|
+
must carry the explicit label `unmeasured` rather than implying an empirical win.
|
|
27
|
+
|
|
28
|
+
2. **Benchmark tasks are mined deterministically from non-merge git history.** `simt bench mine`
|
|
29
|
+
walks non-merge commits with a parent, selects commits that modify both test paths and
|
|
30
|
+
implementation paths, splits each commit into `test.patch` (test files) and `gold.patch`
|
|
31
|
+
(implementation files), and assigns the deterministic identifier `t-<shortSha>-01`.
|
|
32
|
+
|
|
33
|
+
3. **Every mined task is validated twice in an isolated worktree before acceptance.** At
|
|
34
|
+
the parent commit `BASE`:
|
|
35
|
+
- `BASE` plus `test.patch` must fail (`RED`) on two consecutive runs with the same
|
|
36
|
+
failing and passing test identifiers;
|
|
37
|
+
- `BASE` plus `test.patch` plus `gold.patch` must pass (`GOLD`) on two consecutive runs
|
|
38
|
+
with zero failing tests and the same passing test identifiers;
|
|
39
|
+
- any commit whose test outcome differs between the two runs is discarded as flaky.
|
|
40
|
+
|
|
41
|
+
4. **Test identifiers are extracted from runner-native machine output, never from human
|
|
42
|
+
console summaries.** The miner parses TAP (`node --test --test-reporter=tap`), Vitest
|
|
43
|
+
JSON (`--reporter=json`), JUnit XML (Gradle, Maven and `pytest --junitxml`) and Go JSON
|
|
44
|
+
event streams (`go test -json`) to record exact `f2p` (`FAIL_TO_PASS`) and `p2p`
|
|
45
|
+
(`PASS_TO_PASS`) test identifiers.
|
|
46
|
+
|
|
47
|
+
5. **Hidden test patches live in a verifier vault outside every worktree.** Task metadata
|
|
48
|
+
is written to the adopting repository's benchmark task directory so the suite
|
|
49
|
+
membership is versioned and reviewable, while `test.patch` and `gold.patch` are written
|
|
50
|
+
to the user's cache vault (`~/.cache/simt-dev/vault/<repo-id>/<id>/`, or the platform
|
|
51
|
+
cache equivalent) so an agent running inside a task workspace cannot inspect the
|
|
52
|
+
verifier test diff or the oracle fix.
|
|
53
|
+
|
|
54
|
+
## Consequences
|
|
55
|
+
|
|
56
|
+
`simt bench mine --yes` mines a deterministic, flake-filtered task suite from a
|
|
57
|
+
repository's own commits without network access or model calls. Re-running the miner over
|
|
58
|
+
the same commit history produces byte-identical task JSON files and vault patches.
|
|
59
|
+
|
|
60
|
+
## Rejected
|
|
61
|
+
|
|
62
|
+
- **Keeping `test.patch` inside the repository's committed task directory.** An agent
|
|
63
|
+
evaluated in a checkout of the repository could read the hidden test expectations or the
|
|
64
|
+
oracle patch directly from disk.
|
|
65
|
+
- **Accepting any commit that touches a test file without running `RED` and `GOLD` twice.**
|
|
66
|
+
Refactors, formatting commits and flaky tests touch test files without failing on `BASE`,
|
|
67
|
+
producing tasks where a no-op agent scores 100% or a valid fix fails at random.
|
|
68
|
+
- **Scraping human-formatted test summaries with regular expressions.** Console summaries
|
|
69
|
+
vary by locale, terminal width and runner version, and do not name individual
|
|
70
|
+
`FAIL_TO_PASS` test cases needed to grade partial regressions.
|
package/docs/decisions/ADR-077-the-command-card-leads-the-region-and-reads-committed-evidence.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# ADR-077 — The command card leads the region, and is derived from committed evidence only
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-16 · filed as R10 (plan 0003) · ruling 1 narrowed by ADR-086
|
|
4
|
+
**Affects:** `src/kernel/card.mjs`, `src/projection/rules.mjs`, `rules/core.md` (indirectly, as
|
|
5
|
+
the region's first bytes), `test/card.test.mjs`
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Plan 0003 R10 asks for the highest-value, non-inferable bytes: the exact commands for *this*
|
|
10
|
+
repository, at the top of the region, in a block of at most 600 B, "derived from gates and
|
|
11
|
+
runner detection alone, so it needs no index". A reader who has the rules has the same rules
|
|
12
|
+
every other repository has; what they cannot infer is that *their* tests run under
|
|
13
|
+
`./gradlew :app:testDebugUnitTest` and not `npm test`.
|
|
14
|
+
|
|
15
|
+
Two things the plan leaves open, and both are the kind an implementer gets wrong once:
|
|
16
|
+
|
|
17
|
+
1. **Which modes carry the card.** R07's kernel is a smaller projection a repository asks
|
|
18
|
+
for (ADR-073), and R10 says "at the top of the kernel". Read literally, the card would
|
|
19
|
+
appear only for the repositories that opted into kernel mode — that is, only for the
|
|
20
|
+
readers who already measured that they can do without guidance. The reader who kept
|
|
21
|
+
`full` is the one who has the commands nowhere.
|
|
22
|
+
|
|
23
|
+
2. **What evidence the card may read.** "Runner detection" has two possible meanings here.
|
|
24
|
+
`readCapability` answers "can this machine run it" by probing PATH and `node_modules`,
|
|
25
|
+
and that answer is machine-specific. But the projected region is required to be
|
|
26
|
+
byte-identical on two machines (plan 0003 P4 and R13): the whole reason the tool has no
|
|
27
|
+
model and no network is that a pull request can meaningfully diff a projection. A card
|
|
28
|
+
that named whichever linter happened to be installed would differ between a laptop and
|
|
29
|
+
CI for no reason a diff could explain, and `check` would flake on a clean tree.
|
|
30
|
+
|
|
31
|
+
## Rulings
|
|
32
|
+
|
|
33
|
+
1. **The card leads the region in both modes.** `full` and `kernel` differ in which *rules*
|
|
34
|
+
are packed (ADR-073 ruling 5); neither withholds the commands. The card is bytes the
|
|
35
|
+
reader most needs and the rules never carry, so it is above even the core, and it is not
|
|
36
|
+
a rule the kernel can defer.
|
|
37
|
+
|
|
38
|
+
2. **It is derived from committed evidence only.** Three sources, in precedence order: the
|
|
39
|
+
manifest's declared `gates.commands`, the repository's own `package.json` scripts, and
|
|
40
|
+
the marker facts that name its ecosystem. It never reads PATH or `node_modules`.
|
|
41
|
+
Runnability is a property of the machine; a command is a property of the repository, and
|
|
42
|
+
only the second belongs in bytes that leave it.
|
|
43
|
+
|
|
44
|
+
3. **The ecosystem table is the single source, and the gate list cannot disagree with it.**
|
|
45
|
+
The fallback for a marker that has no declared command is the same `ECOSYSTEMS` table
|
|
46
|
+
`manifest.mjs` seeds gates from. Two tables would eventually name two different test
|
|
47
|
+
commands for one repository, which is the drift this tool exists to remove. Python is the
|
|
48
|
+
one ecosystem the table does not seed, because its runner varies (uv, poetry, tox, nox); the
|
|
49
|
+
card still names the command a committed lockfile proves — `uv run -m pytest`, never
|
|
50
|
+
`uv run pytest`, because the uv profile is explicit that the console entrypoint can run a
|
|
51
|
+
different copy of the package than the one under test.
|
|
52
|
+
|
|
53
|
+
4. **A template is labelled as a template, per path kind.** The focused-test entries carry
|
|
54
|
+
`{file}`, `{pkg}`, `{Test}` and `{class}` placeholders and are labelled by the kind of
|
|
55
|
+
path they apply to, so a reader never pastes a brace into a shell. Which test a given
|
|
56
|
+
`{file}` maps to is R24's refinement, not R10's.
|
|
57
|
+
|
|
58
|
+
5. **The ceiling is 600 B, enforced by dropping whole lines, never silently.** Lines drop in
|
|
59
|
+
a stated order — `format`, `typecheck`, `lint`, `build`, then focused templates from the
|
|
60
|
+
last — and `test` is never among them, because a card that dropped the test command would
|
|
61
|
+
have dropped the command it exists to carry. Every drop is named in the projection notes;
|
|
62
|
+
a card that lost a command in silence is a card the reader cannot tell is incomplete.
|
|
63
|
+
|
|
64
|
+
6. **It is computed in `desiredRegion`, the one place the region is computed.** `plan` and
|
|
65
|
+
`checkTarget` therefore cannot hold two opinions about the same card, which is the same
|
|
66
|
+
property that keeps a clean tree from reporting drift (the comment on `desiredRegion`).
|
|
67
|
+
|
|
68
|
+
## Consequences
|
|
69
|
+
|
|
70
|
+
Every adopted repository's region gains the card on the next `sync`, in both modes. That is
|
|
71
|
+
the point of the slice: the bytes were absent everywhere, and the ones added are the ones a
|
|
72
|
+
reader can act on. The card is derived, so it moves only when the repository's own commands
|
|
73
|
+
move — a new `package.json` script, a declared command, or a marker file appearing.
|
|
74
|
+
|
|
75
|
+
What is not here: which test a `{file}` should be mapped to (R24), the inferability filter
|
|
76
|
+
that would drop a card line the repository already states (R09), and the cost measurement
|
|
77
|
+
that would say whether the card earns its bytes (R02, R27). This ruling is what makes those
|
|
78
|
+
measurable rather than what concludes them.
|
|
79
|
+
|
|
80
|
+
## Rejected
|
|
81
|
+
|
|
82
|
+
- **Kernel mode only.** It withholds the card from the readers who kept the larger
|
|
83
|
+
projection, which is all of them until R02 has run.
|
|
84
|
+
- **Probing PATH or `node_modules`** so the card shows only commands that will run here. The
|
|
85
|
+
answer would not survive a second machine, and the region is required to.
|
|
86
|
+
- **Writing the commands as rule text.** The commands are data about one repository; a rule
|
|
87
|
+
is the same in every repository, and a rule that stated one repository's test command would
|
|
88
|
+
be wrong in every other.
|
|
89
|
+
- **Letting the card grow past 600 B, or trimming it silently.** An unbounded card is a
|
|
90
|
+
second copy of the rules, and a silently short one is a reader who trusts a card that is
|
|
91
|
+
missing a command.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# ADR-078 — A line the repository already proves does not ship, and the author says so
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-16 · filed as R09 (plan 0003)
|
|
4
|
+
**Affects:** `src/kernel/inferability.mjs`, `src/projection/rules.mjs`, `src/kernel/card.mjs`
|
|
5
|
+
(the verb vocabulary), `src/argv.mjs`, `src/commands/help.mjs`, `test/inferability.test.mjs`,
|
|
6
|
+
`test/shipped.test.mjs` (the estate-name guard's allowlist of the tool's own words)
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Plan 0003 R09 asks for an inferability filter: a rule line may declare
|
|
11
|
+
`simt-inferable-when: <fact expression>`, and when the repository already proves the
|
|
12
|
+
expression the line is dropped — "use `./gradlew`" when the wrapper is the only runner and
|
|
13
|
+
the card names it, "use pnpm" when only `pnpm-lock.yaml` exists. The basis is that content
|
|
14
|
+
the repository already states does not help its reader.
|
|
15
|
+
|
|
16
|
+
ADR-077 built the card that names this repository's commands and left R09 open as the filter
|
|
17
|
+
"that would drop a card line the repository already states". What the plan leaves open is
|
|
18
|
+
the part that decides whether the mechanism is usable or a trap:
|
|
19
|
+
|
|
20
|
+
1. **Who may drop a line.** An inferability filter that guesses is a filter that deletes
|
|
21
|
+
rules a reader needed. "The repository has a `.editorconfig`, so the indentation rule is
|
|
22
|
+
inferable" is exactly the reasoning that is right most of the time and silent when it is
|
|
23
|
+
wrong.
|
|
24
|
+
|
|
25
|
+
2. **What evidence may be read.** The same choice ADR-077 ruling 2 had to make, and the same
|
|
26
|
+
answer applies: the projected region is required to be byte-identical on two machines
|
|
27
|
+
(plan 0003 P4, R13), so evidence read off the machine cannot decide what leaves.
|
|
28
|
+
|
|
29
|
+
3. **What a removal costs the author.** A line that leaves a projection with no trace is the
|
|
30
|
+
mistake ADR-073 rejected for deferred rules. But a `## Dropped` section would spend region
|
|
31
|
+
bytes on lines the reader is being told not to read, which is the opposite of the slice's
|
|
32
|
+
purpose.
|
|
33
|
+
|
|
34
|
+
## Rulings
|
|
35
|
+
|
|
36
|
+
1. **A line leaves only when its author says it may.** The mechanism is opt-in per block, by
|
|
37
|
+
a marker the rule author writes — `<!-- simt-inferable-when: <expression> -->`. Nothing is
|
|
38
|
+
dropped by inference alone. The tool checks a claim; it does not make one.
|
|
39
|
+
|
|
40
|
+
2. **The unit is the blank-line-delimited block, and the marker never reaches the region.**
|
|
41
|
+
A wrapped bullet is marked once and leaves whole, rather than leaving half a sentence
|
|
42
|
+
behind. The comment is stripped from the region whether or not the line is dropped:
|
|
43
|
+
`simt-` vocabulary is for the rule's author, not for its reader.
|
|
44
|
+
|
|
45
|
+
3. **The expression is a conjunction of positive terms over a closed vocabulary.** A term is
|
|
46
|
+
a fact or stack tag in `KNOWN_TAGS`, a `card:<verb>` for one of the card's five verbs, or
|
|
47
|
+
a `gate:<name>` the repository declares. There is no negation: every reason a line is
|
|
48
|
+
redundant is a positive proof the repository offers, and a grammar for a case nobody has
|
|
49
|
+
is a grammar nobody can be taught. A term outside the vocabulary is a defect, reported
|
|
50
|
+
through the same field as a broken frontmatter key, so `check` and `rules list` need no
|
|
51
|
+
second path to a broken rule.
|
|
52
|
+
|
|
53
|
+
4. **The proofs are committed bytes only.** The repository's own facts and stack, the gates
|
|
54
|
+
its manifest declares, and the verbs its card names. Never PATH and never `node_modules`,
|
|
55
|
+
for the reason ADR-077 ruling 2 already settled: a proof read off the machine would make
|
|
56
|
+
the region differ between a laptop and CI for no reason a diff could explain.
|
|
57
|
+
|
|
58
|
+
5. **The filter is about the repository's evidence, not about the mode.** It runs in both
|
|
59
|
+
`full` and `kernel`, for the same reason the card does (ADR-077 ruling 1): inferability is
|
|
60
|
+
a property of what the repository commits, and inferability of the mode would withhold the
|
|
61
|
+
saving from every reader who kept the larger projection.
|
|
62
|
+
|
|
63
|
+
6. **Never silent, and never a heading.** By default a removal is reported as one count in
|
|
64
|
+
the projection notes, with the command that names each one: `--explain` prints every
|
|
65
|
+
removal and the evidence behind it. A count is enough to act on; the per-line detail is
|
|
66
|
+
what an author needs, and a reader is not an author.
|
|
67
|
+
|
|
68
|
+
7. **The vocabulary has one definition.** `card.mjs` exports the five verbs the card can
|
|
69
|
+
name, and the filter validates `card:<verb>` against that same list. A copy of the list in
|
|
70
|
+
the filter would let the card name a verb the filter called unknown.
|
|
71
|
+
|
|
72
|
+
## Consequences
|
|
73
|
+
|
|
74
|
+
The region of a repository whose rules carry no marker does not move a byte: the filter
|
|
75
|
+
returns the body by identity when there is nothing marked, so this ruling cannot drift a
|
|
76
|
+
repository that did not ask for it. Where a marker is written — in a repository's own
|
|
77
|
+
`.simt-dev/profiles`, or in a pack — the marked line leaves the region on the next `sync`,
|
|
78
|
+
and `sync --explain` names it and the evidence.
|
|
79
|
+
|
|
80
|
+
**What has not landed: no shipped rule carries a marker.** `rules/stacks/go.md` states
|
|
81
|
+
"`go build ./...`, `go test ./...` are the entrypoints; `go vet ./...` runs with them" and
|
|
82
|
+
`rules/stacks/uv.md` states "`uv run -m pytest`; **never** `uv run pytest`" — both duplicate a
|
|
83
|
+
card line, and neither is *wholly* inferable, because each also carries a reason the card does
|
|
84
|
+
not state. Dropping them would lose the reason. Marking shipped rule text is therefore a
|
|
85
|
+
content pass with its own blast radius (a change to shipped rule text changes every repository
|
|
86
|
+
at once), not a step of this slice; the mechanism ships first so that pass can be measured.
|
|
87
|
+
|
|
88
|
+
A marker stays visible in `simt rules show <id>`, which prints the rule's own text. That is
|
|
89
|
+
deliberate: it is the author's annotation, and it is the reason a line they can see in the
|
|
90
|
+
file is absent from the region. It is not region text, and it never reaches one.
|
|
91
|
+
|
|
92
|
+
`--explain` is a `sync` flag and not a `check` flag, because `sync` is the command that prints
|
|
93
|
+
projection notes and the preview a reader reads before writing. The JSON payload of `sync`
|
|
94
|
+
does not carry notes, so this adds no contract.
|
|
95
|
+
|
|
96
|
+
One visible oddity, stated because the reader will meet it: `simt rules why ADR-078` prints
|
|
97
|
+
`simt-inferable-when` as a repository this record surveyed, because the decisions reader
|
|
98
|
+
treats every `simt-` word outside its allowlist as one. That is a defect in the reader rather
|
|
99
|
+
than in this ruling — ADR-073 carries the identical one for `simt-priority` — and it is not
|
|
100
|
+
fixed here.
|
|
101
|
+
|
|
102
|
+
## Rejected
|
|
103
|
+
|
|
104
|
+
- **Dropping a line the tool judges inferable without a marker.** It is the failure the slice
|
|
105
|
+
exists to avoid: a rule deleted by a heuristic with no way for the author to see the
|
|
106
|
+
heuristic was wrong.
|
|
107
|
+
- **A marker on every physical line.** A wrapped bullet is one sentence across two lines;
|
|
108
|
+
marking lines would let a removal leave half of one behind.
|
|
109
|
+
- **Negation, parentheses or disjunction in the expression.** No shipped reason for a removal
|
|
110
|
+
is negative, and a grammar with a case nobody uses is a grammar nobody can be taught.
|
|
111
|
+
- **Letting the filter run in `kernel` mode only.** It would withhold a saving from the
|
|
112
|
+
readers who kept the larger projection, which is all of them until R02 has run.
|
|
113
|
+
- **A `## Dropped` section in the region.** It spends bytes on lines the reader is told not
|
|
114
|
+
to read, and it moves the rule set for a reason that is not about the reader.
|
|
115
|
+
- **Marking shipped rules now.** No shipped line is wholly inferable, so it would delete
|
|
116
|
+
rationales to save bytes that the card already saves, and it would change every
|
|
117
|
+
repository's region in a slice about a mechanism.
|
package/docs/decisions/ADR-079-the-rules-are-enforced-at-the-tool-call-from-one-policy-table.md
ADDED
|
@@ -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`.
|