simt-dev 0.0.0-stage → 0.1.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 +130 -0
- package/LICENSE +202 -0
- package/README.md +92 -2
- package/bin/simt-dev.mjs +28 -0
- package/docs/decisions/ADR-001-push-requires-approval.md +42 -0
- package/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
- package/docs/decisions/ADR-003-test-breadth.md +46 -0
- package/docs/decisions/ADR-004-hook-owns-format.md +45 -0
- package/docs/decisions/ADR-005-file-length.md +42 -0
- package/docs/decisions/ADR-006-coverage-bar.md +59 -0
- package/docs/decisions/ADR-007-section-skeleton.md +54 -0
- package/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
- package/docs/decisions/ADR-009-skill-layout.md +48 -0
- package/docs/decisions/ADR-010-runner-invocation.md +56 -0
- package/docs/decisions/ADR-011-windows-support.md +52 -0
- package/docs/decisions/ADR-012-drop-windows-support.md +59 -0
- package/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
- package/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
- package/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
- package/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
- package/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
- package/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
- package/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
- package/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
- package/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
- package/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
- package/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
- package/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
- package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
- package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
- package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
- package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +131 -0
- package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
- package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
- package/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
- package/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
- package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
- package/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
- package/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
- package/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
- package/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
- package/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
- package/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
- package/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
- package/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
- package/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
- package/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
- package/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
- package/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
- package/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
- package/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
- package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
- package/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
- package/docs/decisions/ADR-050-a-byte-count-is-not-a-region-and-updated-is-not-a-change-of-rules.md +102 -0
- package/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
- package/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
- package/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
- package/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
- package/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
- package/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
- package/docs/decisions/README.md +528 -0
- package/graph/skills.graph.json +1622 -0
- package/package.json +41 -4
- package/rules/core.md +76 -0
- package/rules/mdc/intake-questions.mdc +50 -0
- package/rules/mdc/preview-postflight.mdc +61 -0
- package/rules/mdc/slice-commit.mdc +52 -0
- package/rules/mdc/test-gate.mdc +54 -0
- package/rules/stacks/android.md +63 -0
- package/rules/stacks/cloudflare-workers.md +65 -0
- package/rules/stacks/connectiq.md +62 -0
- package/rules/stacks/container.md +31 -0
- package/rules/stacks/jvm.md +54 -0
- package/rules/stacks/node.md +45 -0
- package/rules/stacks/python.md +48 -0
- package/rules/stacks/react-native.md +58 -0
- package/rules/stacks/react-web.md +61 -0
- package/rules/stacks/typescript.md +21 -0
- package/rules/stacks/uv.md +32 -0
- package/src/argv.mjs +116 -0
- package/src/ci/dependabot.mjs +268 -0
- package/src/ci/workflow.mjs +360 -0
- package/src/commands/ci.mjs +285 -0
- package/src/commands/compare.mjs +195 -0
- package/src/commands/graph.mjs +73 -0
- package/src/commands/help.mjs +108 -0
- package/src/commands/hooks.mjs +471 -0
- package/src/commands/index.mjs +241 -0
- package/src/commands/init.mjs +566 -0
- package/src/commands/mcp.mjs +63 -0
- package/src/commands/rules.mjs +374 -0
- package/src/commands/skills.mjs +647 -0
- package/src/commands/skillsinstall.mjs +515 -0
- package/src/commands/sync.mjs +331 -0
- package/src/commands/upgrade.mjs +202 -0
- package/src/compare/discover.mjs +212 -0
- package/src/compare/hooksstate.mjs +159 -0
- package/src/compare/row.mjs +299 -0
- package/src/decisions/index.mjs +512 -0
- package/src/graph/load.mjs +541 -0
- package/src/hooks/capability.mjs +129 -0
- package/src/hooks/dispatcher.mjs +268 -0
- package/src/hooks/gates.mjs +144 -0
- package/src/hooks/resolve.mjs +194 -0
- package/src/hooks/shim.mjs +140 -0
- package/src/hooks/where.mjs +118 -0
- package/src/locate.mjs +75 -0
- package/src/mcp/server.mjs +167 -0
- package/src/mcp/tools.mjs +410 -0
- package/src/name.mjs +95 -0
- package/src/output/index.mjs +96 -0
- package/src/projection/baselines.mjs +209 -0
- package/src/projection/conflicts.mjs +205 -0
- package/src/projection/manifest.mjs +478 -0
- package/src/projection/markers.mjs +349 -0
- package/src/projection/mdc.mjs +327 -0
- package/src/projection/project.mjs +504 -0
- package/src/projection/rules.mjs +640 -0
- package/src/projection/surfaces.mjs +398 -0
- package/src/projection/targets.mjs +221 -0
- package/src/resolve/fingerprint.mjs +553 -0
- package/src/resolve/near.mjs +61 -0
- package/src/resolve/resolve.mjs +457 -0
- package/src/resolve/source.mjs +105 -0
- package/src/toolchain.mjs +202 -0
- package/src/tsconfig.mjs +314 -0
- package/src/upgrade/plan.mjs +203 -0
- package/src/workspace.mjs +289 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# ADR-033 — three plan rows closed by decision, not by authoring
|
|
2
|
+
|
|
3
|
+
Status: accepted · 2026-10-08
|
|
4
|
+
**Affects:** `docs/plans/0001-centralise-agent-rules.md` §3.1 row 3, §6 phase 6, §6 phase 7.
|
|
5
|
+
Ships no rule text. **Refuses:** the seven agent policies row 3 asks for, the twin collapse
|
|
6
|
+
phase 6 asks for, and untracking the two committed `local.properties` files phase 7 does not
|
|
7
|
+
mention but whose existence made the sweep look unfinished.
|
|
8
|
+
|
|
9
|
+
## Context
|
|
10
|
+
|
|
11
|
+
Closing plan 0001 meant auditing each row against the tree rather than against the plan's own
|
|
12
|
+
prose, because the plan was written on 2026-10-05 and three things changed underneath it. This
|
|
13
|
+
record is the audit's residue: three items that read as unfinished work and are not.
|
|
14
|
+
|
|
15
|
+
Row 3 asked for eight agent policies as separate files. Seven were never authored. A search for
|
|
16
|
+
their source material across every tracked `*.md` and `*.mdc` in the estate returns two files,
|
|
17
|
+
neither of which is a policy: they are a progress ledger and a completed UX plan that happen to
|
|
18
|
+
contain the phrases. The three most specific of the eight — the 70/30 quality-versus-performance
|
|
19
|
+
split, the 12-state coverage list, the matrix-trigger heuristic — have no source anywhere. Writing
|
|
20
|
+
them would mean inventing doctrine this estate never agreed to, into `rules/core.md`, from where
|
|
21
|
+
every adopted repo would inherit it.
|
|
22
|
+
|
|
23
|
+
Phase 6 asked to "collapse the 7 drifted Social skill twins to canonical + stub". The registry's
|
|
24
|
+
`sources.role` mechanism landed the same evening, two hours after the plan was authored, and
|
|
25
|
+
answers the row directly. All three resolvers select `role === 'canonical'` and nothing else —
|
|
26
|
+
`src/commands/skills.mjs` twice, `src/commands/skillsinstall.mjs:291`, `src/mcp/tools.mjs:192` — so
|
|
27
|
+
a copy declared `duplicate` or `variant` is never installed. The one pair that genuinely had to be
|
|
28
|
+
collapsed, `add-country-galaxy`, is already a stub in `simt-social` pointing at the API repo.
|
|
29
|
+
|
|
30
|
+
Phase 7 asked for a sweep of hardcoded absolute paths. The sweep found 17 tracked files. Six were
|
|
31
|
+
fixed. The two Android `local.properties` files were not, and were nearly fixed by reflex.
|
|
32
|
+
|
|
33
|
+
## Rulings
|
|
34
|
+
|
|
35
|
+
1. **A row is closed by ruling on it when the thing it asks for was never a decision anyone made.**
|
|
36
|
+
Row 3's eight policies are closed: the four claims `rules/core.md` already carries — the
|
|
37
|
+
minimum viable team, never weakening a security control, anti-overclaim, and a stated agent
|
|
38
|
+
budget that never converts a failure into an approval — are the doctrine, and they ship. The
|
|
39
|
+
other four, including the 70/30 split, the 12-state list and the matrix-trigger heuristic, are
|
|
40
|
+
recorded here as unmade. If one is wanted it needs someone who held the original
|
|
41
|
+
conversation, not someone reconstructing it from a table cell.
|
|
42
|
+
|
|
43
|
+
2. **A mechanism that supersedes a row retires the row, and the deletion is the wrong move.** The
|
|
44
|
+
`sources.role` vocabulary already distinguishes `canonical`, `duplicate`, `variant`, `stub` and
|
|
45
|
+
`deprecated`, and `graph lint` passes with all of them in use. Deleting a `duplicate` copy would
|
|
46
|
+
remove a version-pinned tool from a repo that can still run it, and nothing would detect the loss
|
|
47
|
+
because resolution never read those bytes. Phase 6's twin collapse is therefore closed as
|
|
48
|
+
already answered, at `a9983c8`.
|
|
49
|
+
|
|
50
|
+
3. **`local.properties` stays tracked.** Both `simt-app` and `wakt-app` commit it, and both
|
|
51
|
+
`.gitignore` files have the conventional `# local.properties` line commented out at lines 22 and
|
|
52
|
+
24 — a deliberate override of Android's own convention, not an oversight. Untracking them would
|
|
53
|
+
be a house-style change made while closing a plan, and a build machine that checks out without
|
|
54
|
+
an SDK would stop building. It stays, and plan 0001 says so, so the next sweep does not
|
|
55
|
+
rediscover it as a defect.
|
|
56
|
+
|
|
57
|
+
## Consequences
|
|
58
|
+
|
|
59
|
+
Row 3 reads "done, four of eight" rather than "partial". Phase 6's twin item reads "superseded"
|
|
60
|
+
rather than "outstanding". The estate keeps two `local.properties` files whose `sdk.dir` names a
|
|
61
|
+
developer's home directory, by decision.
|
|
62
|
+
|
|
63
|
+
The registry's byte counts moved with the two skill bodies corrected in the same pass, because a
|
|
64
|
+
declared source's identity is its size (ADR-015): `cloudflare` 384277 → 384869 and
|
|
65
|
+
`production-quality-review` 5869 → 5904. That changed the provenance digest (ADR-032), so all
|
|
66
|
+
fourteen adopted repos were re-stamped afterwards rather than left pinning a value this tool can no
|
|
67
|
+
longer produce.
|
|
68
|
+
|
|
69
|
+
## Rejected
|
|
70
|
+
|
|
71
|
+
**Authoring the seven agent policies.** The plan's one-line summaries are not enough to write seven
|
|
72
|
+
files of rule text, and these rules reach every adopted repo. This is ADR-030's exact shape: an
|
|
73
|
+
unmade decision is not a gap to fill by invention.
|
|
74
|
+
|
|
75
|
+
**Collapsing `graphify` and `parallel-feature-team`.** Both are declared `duplicate` with matching
|
|
76
|
+
byte counts, so they look like the obvious candidates. `graphify` is `vendored: true` — an
|
|
77
|
+
84 534-byte, ten-file, version-pinned tool present in both Social repos on purpose. Deleting
|
|
78
|
+
simt-social's copy would leave nothing to detect the loss, because resolution only ever read the
|
|
79
|
+
canonical copy.
|
|
80
|
+
|
|
81
|
+
**Untracking `local.properties`.** An Android SDK path is machine state, and two repos chose to
|
|
82
|
+
share it. That is the repo's call to reverse, not a plan's defect sweep.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# ADR-034 — A staged tool refuses a partially staged file rather than staging around it
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-08 · affects every repo with a `pre-commit` hook
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
`src/hooks/gates.mjs` defines a `staged` gate as one that "run[s] a formatter over the STAGED
|
|
8
|
+
FILES ONLY, then re-stage[s] them", and gives the reason: whole-repo formatting from a
|
|
9
|
+
pre-commit hook "would rewrite unstaged work the author did not ask to touch". (The rule
|
|
10
|
+
that hooks own format and lint for staged files is ADR-004; this record settles what the
|
|
11
|
+
dispatcher does when "staged files" is ambiguous, which ADR-004 does not say.)
|
|
12
|
+
|
|
13
|
+
The dispatcher did not implement that promise. After a passing `format` gate it ran
|
|
14
|
+
`git add` on **every** path from `git diff --cached --name-only`, and reported success
|
|
15
|
+
whenever anything was staged.
|
|
16
|
+
|
|
17
|
+
Two defects, both measured against the shipped dispatcher rather than reasoned about:
|
|
18
|
+
|
|
19
|
+
- **A partial stage was destroyed outright.** With `a.md` staged as `one\nSTAGED\n` and the
|
|
20
|
+
working tree at `one\nSTAGED\nUNSTAGED-WORK\n`, a formatter that rewrites its inputs left
|
|
21
|
+
**both** the index and the working tree as `FORMATTED`. The author's unstaged hunk was not
|
|
22
|
+
merely committed — it was replaced. This is the promise above, broken on the one path that
|
|
23
|
+
runs on every commit in every adopted repo, and it is silent.
|
|
24
|
+
- **A staged deletion silently skipped the restage.** `--name-only` with no `--diff-filter`
|
|
25
|
+
lists deleted paths, `git add -- <deleted>` fails, and the `catch` returned `false`. A
|
|
26
|
+
commit containing a deletion plus a formattable file recorded the **unformatted** bytes and
|
|
27
|
+
printed no restage line at all.
|
|
28
|
+
|
|
29
|
+
A third, smaller one: restage reported "re-staged files the formatter rewrote" whenever
|
|
30
|
+
anything was staged, whether or not a byte moved.
|
|
31
|
+
|
|
32
|
+
## Ruling
|
|
33
|
+
|
|
34
|
+
**1. A staged gate refuses to run when a file it would format also has unstaged changes.**
|
|
35
|
+
It fails the commit and names the file. It does not stash, and it does not format anyway.
|
|
36
|
+
|
|
37
|
+
The author staged part of a file deliberately; `git add -p` is how you say "this commit is
|
|
38
|
+
part of that file". A hook cannot know which part, so any answer it picks is a guess about
|
|
39
|
+
someone's intent, and a guess that rewrites their working tree is the one they cannot see
|
|
40
|
+
coming. Failing closed with a remedy the author can act on — `stage the whole file`, or run
|
|
41
|
+
the formatter yourself and stage the result — costs one command. Guessing costs work.
|
|
42
|
+
|
|
43
|
+
**2. Restage covers the files the formatter was actually given, and only those.** The
|
|
44
|
+
resolved staged tool's `argv` already holds the paths; nothing needed re-deriving them from
|
|
45
|
+
the index. For a gate declared through `gates.commands`, the command line is arbitrary and may
|
|
46
|
+
take a directory or a glob rather than paths, so ruling 1 applies to **every** staged file with
|
|
47
|
+
unstaged changes, because there is no way to know which files it will open. That is the
|
|
48
|
+
conservative reading, and being wrong in that direction fails a commit the author can retry.
|
|
49
|
+
|
|
50
|
+
**3. Restage reports a file only when that file's staged bytes actually moved**, compared as
|
|
51
|
+
the blob hash before and after. A restage line that fires on every commit is a line nobody
|
|
52
|
+
reads; the ones that fire are the ones that will be read.
|
|
53
|
+
|
|
54
|
+
A path staged as **deleted** is not a partial stage. The index and the working tree agree it
|
|
55
|
+
is gone, so there is no unstaged work to protect and nothing to refuse.
|
|
56
|
+
|
|
57
|
+
## Consequences
|
|
58
|
+
|
|
59
|
+
- `restage` returns the files it staged rather than a boolean, and a failed `git add` is now
|
|
60
|
+
reported as a `restage` **failure** with a remedy. It was previously swallowed by a `catch`
|
|
61
|
+
that returned `false`, which is what made defect 2 invisible.
|
|
62
|
+
- `resolveGate` now returns `declared: true` for a `gates.commands` resolution. The dispatcher
|
|
63
|
+
needed to distinguish argv it chose from words it was handed, and sniffing the `detail`
|
|
64
|
+
string would have let the guard in ruling 2 quietly stop working the day that wording
|
|
65
|
+
changed.
|
|
66
|
+
- Repos whose authors habitually commit partially staged files will now see the gate refuse
|
|
67
|
+
once, with a remedy naming the file, and not again. That is the intended cost: it is
|
|
68
|
+
visible, and it is a command they choose to run.
|
|
69
|
+
- `rules/core.md` inherits nothing new — this changes when a hook runs, not what an agent is
|
|
70
|
+
told to do — so the provenance digest is unaffected and no repo needs re-stamping.
|
|
71
|
+
|
|
72
|
+
## Rejected
|
|
73
|
+
|
|
74
|
+
- **Stash the unstaged changes, format, restore.** The review offered this as an alternative.
|
|
75
|
+
It is a hook making the same guess by a longer route: it assumes the stash will apply
|
|
76
|
+
cleanly after a formatter has rewritten the file underneath it, and a conflict there
|
|
77
|
+
leaves the author with a stash and no working copy.
|
|
78
|
+
- **Format the staged content rather than the working tree** (e.g. piping `git show :file`
|
|
79
|
+
through the formatter and updating the index). Correct in principle, and it does not destroy
|
|
80
|
+
unstaged work — but it leaves the index holding a file the working tree no longer matches,
|
|
81
|
+
so the next commit sees a phantom edit. Worth revisiting only with a real consumer of the
|
|
82
|
+
format, which no repo in this estate has.
|
|
83
|
+
- **Skip the restage for deleted paths.** Treating a deletion as a restage failure would make
|
|
84
|
+
the common case of a renamed-or-deleted file block every commit that also formats.
|
|
85
|
+
- **Keep the boolean return and just filter the file list.** That leaves defect 2's swallowed
|
|
86
|
+
`catch` in place: the command would silently record unformatted bytes again the next time
|
|
87
|
+
any `git add` fails.
|
|
88
|
+
|
|
89
|
+
Review 0001 §`S08` attributes the "would rewrite unstaged work" promise to ADR-004. It is in
|
|
90
|
+
`src/hooks/gates.mjs:7`, the gate definition; ADR-004 settles who runs what, not what the
|
|
91
|
+
dispatcher does with an ambiguous index.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# ADR-035 — One place decides whether a run may write, and one document says whether it did
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-08 · affects every command that writes or reports
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Review 0001's S10 tabulated eight inconsistencies in the `--yes` / `--dry-run` / `--json`
|
|
8
|
+
contract across six commands. Each was measured against the shipped binary before anything
|
|
9
|
+
was changed, and the table did not survive intact: **six were real, one was partly wrong, one
|
|
10
|
+
was inverted, and one defect the table missed turned up during the fix.**
|
|
11
|
+
|
|
12
|
+
The six, all reproduced:
|
|
13
|
+
|
|
14
|
+
- `upgrade --yes --dry-run` **wrote**. `upgrade.mjs` tested `flags.yes === true` and never
|
|
15
|
+
consulted `flags.dryRun`, so the one flag that says "do not write" was not read on the one
|
|
16
|
+
path that moves pins across every adopted repo.
|
|
17
|
+
- `skills install --yes --json` **never wrote**. An early `if (isJson(flags)) { emit(plan);
|
|
18
|
+
return }` returned before the write — the exact combination a script uses.
|
|
19
|
+
- `skills install --yes` printed `note dry-run nothing written` and *then* wrote, because the
|
|
20
|
+
note was emitted unconditionally.
|
|
21
|
+
- `skills install` read **neither** write flag. `grep -rn "dryRun"` over the file returned
|
|
22
|
+
nothing.
|
|
23
|
+
- `doctor --json` exited **0** where the same command in a terminal exited **3** on an
|
|
24
|
+
unparseable workspace config, and omitted the workspace answer entirely: its `isJson`
|
|
25
|
+
branch returned *before* the `ws.error` check with `ok` hardcoded `true`.
|
|
26
|
+
- `hooks status --json` wrote `ok: true` and exited **0** while text mode exited **1** with
|
|
27
|
+
four problems listed.
|
|
28
|
+
|
|
29
|
+
The one the table got backwards: it claimed `sync` and `init` emit JSON errors while `hooks`,
|
|
30
|
+
`skills` and `rules` emit only stderr text. Forcing genuine error paths showed `sync --json`
|
|
31
|
+
emits a JSON error correctly. The actual error-path lie was the `doctor` row above.
|
|
32
|
+
|
|
33
|
+
`init --json` looked like the same defect and is not one. `grep -rn "resolveWorkspace"
|
|
34
|
+
src/commands/` shows `init` never consults the workspace at all, and both modes agree — exit
|
|
35
|
+
`0`, `ok: true`, and no workspace line in text either. Two modes answering one question two
|
|
36
|
+
different ways is the defect; when neither asks the question there is nothing to disagree
|
|
37
|
+
about. Making `init` fail on a corrupt workspace config would be a new decision, not this one.
|
|
38
|
+
|
|
39
|
+
## Ruling
|
|
40
|
+
|
|
41
|
+
**1. Whether a run may write is decided in exactly one function.**
|
|
42
|
+
`writeMode(flags)` in `src/output/index.mjs` returns `{ dryRun, apply }` with
|
|
43
|
+
`apply = flags.yes === true && !dryRun`, and every mutating command reads it. A command that
|
|
44
|
+
consults `flags.yes` directly is a defect whether or not its arithmetic happens to agree
|
|
45
|
+
today, because the third copy is the one that will not.
|
|
46
|
+
|
|
47
|
+
**2. `dryRun` is reported separately from `apply`.** A run that was not authorised is a dry
|
|
48
|
+
run in effect even when nobody asked for one, and the JSON payload's `dryRun` says so.
|
|
49
|
+
|
|
50
|
+
**3. Under `--json`, stdout is exactly one JSON document, and its `ok` agrees with the exit
|
|
51
|
+
code.** A caller reads `ok`; a shell reads the exit code. If they disagree, one is a lie and
|
|
52
|
+
the caller cannot tell which, so the invariant is checked across the whole command surface
|
|
53
|
+
rather than per command.
|
|
54
|
+
|
|
55
|
+
**4. A non-zero exit under `--json` carries `error` and `remedy` in that same document.**
|
|
56
|
+
`ok: false` alone says it failed without saying what to do, and ADR-010 requires the remedy.
|
|
57
|
+
`doctor --json` also carries `workspace`, because the text path prints the workspace answer
|
|
58
|
+
and a JSON document that omits it answers a different question than the terminal does
|
|
59
|
+
(ADR-014).
|
|
60
|
+
|
|
61
|
+
**5. A caveat is a field, not prose on stdout.** `skills brief` with no task and no staged
|
|
62
|
+
files warns rather than silently answering; under `--json` that warning is a `warning` field
|
|
63
|
+
in the one document, and `--all` carries it too. `ok` stays `true` — the command did what was
|
|
64
|
+
asked, and it is the *answer* that is weak.
|
|
65
|
+
|
|
66
|
+
**6. Every command that reports honours `--json`, including the ones the review did not
|
|
67
|
+
name.** `skills why` built its output as lines and had no `isJson` branch at all, so
|
|
68
|
+
`skills why <skill> --json` printed prose. Its `gate` is a field rather than a sentence
|
|
69
|
+
because a skill that can never be selected in this repo is a decision, not a line of output.
|
|
70
|
+
|
|
71
|
+
## Consequences
|
|
72
|
+
|
|
73
|
+
`test/contract.test.mjs` is a new file because neither contract belongs to one command: a
|
|
74
|
+
test that runs `doctor` and a test that runs `skills install` would each assert their own
|
|
75
|
+
command and miss the other. It drives the shipped binary rather than calling functions,
|
|
76
|
+
because a flag the dispatcher never routes is invisible to a direct unit test — which is how
|
|
77
|
+
`--dry-run` reached `skills install` unreached while every unit test passed. Nineteen
|
|
78
|
+
commands are asserted to emit one parseable document whose `ok` matches the exit code; two
|
|
79
|
+
mutating commands are asserted to leave the filesystem byte-identical without `--yes` and
|
|
80
|
+
under `--dry-run`; and one test asserts an authorised, non-rehearsal run **does** write, so
|
|
81
|
+
"refuse everything" cannot pass.
|
|
82
|
+
|
|
83
|
+
Two of those tests were themselves broken when first written, and both were found only
|
|
84
|
+
because a mutation failed to fail them. One watched a path that nothing writes to, and in
|
|
85
|
+
doing so **self-adopted simt-dev**: `skills install` reads `flags.root ?? process.cwd()`, the
|
|
86
|
+
row passed the fixture root positionally to a subcommand that takes no positional, and the
|
|
87
|
+
install landed in this checkout, leaving an untracked `.agents/` and `simt-dev.json`. The
|
|
88
|
+
other asserted a fixture was a clean repo when it was not — `init` writes `simt-dev.json`
|
|
89
|
+
without staging it, so `dirtyPaths` was never empty and the no-input branch was never
|
|
90
|
+
reached. Both are the shape of harm ADR-031 exists to prevent, arrived at from the other
|
|
91
|
+
direction. The fixture now commits what `init` wrote and asserts the tree is clean, passes
|
|
92
|
+
`--root` to every command, and asserts the root is under the temp directory.
|
|
93
|
+
|
|
94
|
+
Six mutations were applied and reverted, each caught by its own test: `upgrade` and
|
|
95
|
+
`skills install` back to reading `flags.yes` alone, `hooks` and `doctor` back to a hardcoded
|
|
96
|
+
`ok: true`, and the `isJson` guard removed from the `skills brief` no-input branch and from
|
|
97
|
+
`skills why`. The `sync --yes` requirement is **not** part of this record; it is review 0002's
|
|
98
|
+
S24 and stays queued, because `sync` already honours `--dry-run` and what it lacks is the
|
|
99
|
+
authorisation gate, which is a separate decision.
|
|
100
|
+
|
|
101
|
+
## Rejected
|
|
102
|
+
|
|
103
|
+
- **Make `sync` require `--yes` as part of this record.** It is a real inconsistency and it is
|
|
104
|
+
someone else's slice. S24 names S10 as its dependency, which is what makes S10 the
|
|
105
|
+
prerequisite rather than half of it.
|
|
106
|
+
- **Keep a per-command `ok` field and rely on reviewers to notice disagreements.** The
|
|
107
|
+
disagreement is the defect. It has to be checked where it is introduced, which is a table
|
|
108
|
+
over the surface, not a discipline applied to each command in turn.
|
|
109
|
+
- **Add an `--json` mode to `skills why` by routing it through the text renderer and parsing
|
|
110
|
+
the result back.** Printing prose and parsing it is how the no-input path went wrong: the
|
|
111
|
+
warning was in the output shape and the document was nowhere.
|
|
112
|
+
- **Treat a missing `workspace` in `doctor --json` as cosmetic.** The text path prints the
|
|
113
|
+
workspace answer; a JSON document that omits it is a second, quieter answer to the same
|
|
114
|
+
question, and the caller has no way to know one of them is missing.
|
|
115
|
+
- **Record the review's table as the defect list.** Two of its eight rows did not survive
|
|
116
|
+
measurement — one was backwards and one was not reproducible — and one real defect
|
|
117
|
+
(`skills why --json`) was absent from it. A table of claimed defects is not a list of the
|
|
118
|
+
actual ones, and writing one down as if it were is how the `skills why` gap survived.
|
|
119
|
+
</content>
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# ADR-036 — A command that writes says so, and every instruction that names it moves with it
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-08 · affects `sync`, every remedy naming it, and the
|
|
4
|
+
generated-region note shipped into every consumer repository
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
ADR-035 gave the command surface one write decision — `writeMode(flags)` — and
|
|
9
|
+
deliberately left `sync` alone, recording the omission twice: in its consequences ("it is
|
|
10
|
+
review 0002's S24 and stays queued") and in its rejected list ("it is a real inconsistency
|
|
11
|
+
and it is someone else's slice"). The reasoning was sound then. S24 depends on S10, so
|
|
12
|
+
making the gate part of S10 would have made the prerequisite half of the change it
|
|
13
|
+
prerequisite'd.
|
|
14
|
+
|
|
15
|
+
Measured against the shipped binary, `sync` was the last writer writing by default. A
|
|
16
|
+
fixture repository was adopted with `--yes`, a stray line was then inserted inside its
|
|
17
|
+
generated region, and a bare `simt-dev sync` **rewrote the file**: the stray line was gone
|
|
18
|
+
and the file was 14 bytes shorter. It reported success, because it had done what it was
|
|
19
|
+
built to do. Among the six writers in the surface, the other five preview unless told
|
|
20
|
+
otherwise, so this was also the only command where an agent that had learned the
|
|
21
|
+
interface would pattern-match onto the wrong behaviour — the preview was opt-in by command
|
|
22
|
+
line and opt-out by habit.
|
|
23
|
+
|
|
24
|
+
The second finding was less comfortable. Twenty-six places in `src/` name `simt-dev sync`
|
|
25
|
+
in a remedy, and two of them are not printed to a terminal at all: the note inside every
|
|
26
|
+
generated region, which ships into every consumer's `AGENTS.md`, and the `$comment` in
|
|
27
|
+
every consumer's manifest. Both instruct the reader to run the command that regenerates the
|
|
28
|
+
text they are looking at. Changing the write gate without changing them would have shipped
|
|
29
|
+
an instruction that is no longer true — and it would have shipped it *to the repository
|
|
30
|
+
being written*, which is precisely the reader with no other context. A gate that only
|
|
31
|
+
protects the tool from itself is not a gate on what the tool says.
|
|
32
|
+
|
|
33
|
+
## Ruling
|
|
34
|
+
|
|
35
|
+
**1. `sync` decides whether to write through `writeMode(flags)`, like every other writer.**
|
|
36
|
+
Not its own condition, and not a check of `--check` and `--dry-run` read separately. ADR-035
|
|
37
|
+
already ruled that a second copy of the decision is a defect whether or not its arithmetic
|
|
38
|
+
agrees today; this is the copy.
|
|
39
|
+
|
|
40
|
+
**2. A bare `sync` is a check that prints the plan, and a check that finds drift exits
|
|
41
|
+
non-zero.** It exits 0 only when nothing would change, which is what lets a caller run
|
|
42
|
+
`sync` in place of `sync --check` and get the same verdict. The preview names the command
|
|
43
|
+
that applies it. Exiting 0 after reporting a change would be the one-question-two-answers
|
|
44
|
+
shape ADR-035 exists to prevent, reached from the opposite direction.
|
|
45
|
+
|
|
46
|
+
**3. `--json` agrees with the exit code, as it must.** `ok` is `false` on a previewing run
|
|
47
|
+
that found work, `written` is `0`, and `dryRun` is `true`. `written` used to be a boolean
|
|
48
|
+
and is now a count, because a preview that says "nothing written" in a field typed as a
|
|
49
|
+
flag cannot distinguish "no changes" from "changes withheld".
|
|
50
|
+
|
|
51
|
+
**4. Every string that names the command moves with the behaviour, including the ones that
|
|
52
|
+
ship.** The region note, the manifest comment, all 26 remedies, and the help synopsis.
|
|
53
|
+
This is the part that is easy to skip and expensive to miss: the two shipped strings are
|
|
54
|
+
compared by `upgrade` and written into files the reader owns, so a change to the gate with
|
|
55
|
+
no change to them leaves a permanently-false instruction in every repository in the estate.
|
|
56
|
+
|
|
57
|
+
**5. The test asserts the flag, not the command.** The existing guard read
|
|
58
|
+
`/simt-dev sync/` and passed identically before and after this change — it could not have
|
|
59
|
+
prevented the defect it appeared to cover. It now reads `/simt-dev sync --yes/`, and a new
|
|
60
|
+
test walks `src/` for any occurrence of the bare spelling so the next command to grow an
|
|
61
|
+
authorisation flag cannot inherit twenty-six no-op remedies unnoticed.
|
|
62
|
+
|
|
63
|
+
## Consequences
|
|
64
|
+
|
|
65
|
+
The contract table in `test/contract.test.mjs` gains a `sync` row, which is the row that
|
|
66
|
+
would have caught this defect: every writer is asserted to leave the filesystem
|
|
67
|
+
byte-identical without `--yes`. `sync` needs stale region bytes rather than a stale pin,
|
|
68
|
+
because what it writes is the region and the pin was already current — a fixture that
|
|
69
|
+
staled the wrong file would have passed a `sync` that wrote nothing for the wrong reason,
|
|
70
|
+
which is the shape ADR-031 rules out. For `sync` alone the unauthorised case also asserts a
|
|
71
|
+
non-zero exit, because the byte comparison alone passes on a preview that returned success.
|
|
72
|
+
|
|
73
|
+
Three mutations were applied and reverted, each caught by its own test: the write gate back
|
|
74
|
+
to reading `--check` and `--dry-run` directly, the preview back to exiting 0 on drift, and
|
|
75
|
+
one remedy back to the bare spelling. The third matters most — it is what proves the
|
|
76
|
+
remedy test sees individual occurrences rather than the template literal around them.
|
|
77
|
+
|
|
78
|
+
One item in this slice's spec could not be done. It asks for a CHANGELOG entry, and there
|
|
79
|
+
is no CHANGELOG in this repository; the row that would add one is queued as S18 and is
|
|
80
|
+
named as its dependency here rather than created, because inventing the file would settle
|
|
81
|
+
a question S18 has not been closed on.
|
|
82
|
+
|
|
83
|
+
## Rejected
|
|
84
|
+
|
|
85
|
+
- **Keep `sync --check` as the only no-write mode and teach the difference in help text.**
|
|
86
|
+
Two spellings for one behaviour means every caller has to know which one it wants, and
|
|
87
|
+
the one that writes by default is the one a caller reaches for. Help text is not a gate;
|
|
88
|
+
it is only evidence that a gate was considered.
|
|
89
|
+
- **Make `sync` preview only when it detects a non-interactive caller.** The tool cannot
|
|
90
|
+
tell an agent from a terminal, and the argument for the flag is that guessing wrong
|
|
91
|
+
destroys work — which is not a cost worth paying for a convenience nobody asked for.
|
|
92
|
+
- **Exit 0 on a preview so that existing scripts keep passing.** This preserves callers
|
|
93
|
+
that were checking whether `sync` *ran*, not whether the repository was *current*. The
|
|
94
|
+
second is the question every one of them was actually asking, and the answer was `ok:
|
|
95
|
+
true` while a file was rewritten.
|
|
96
|
+
- **Update only the terminal output and leave the shipped region note and manifest comment
|
|
97
|
+
alone, since they are documentation.** They are the only copy of the instruction a
|
|
98
|
+
consumer repository's reader ever sees. Changing them is the whole second half of this
|
|
99
|
+
slice.
|
|
100
|
+
- **Fold this into ADR-035 by editing its ruling in place.** ADR-035 is accepted and its
|
|
101
|
+
rejected list names this exact change as someone else's slice. Rewriting the ruling would
|
|
102
|
+
erase the evidence that the omission was deliberate, and a reader diffing the two
|
|
103
|
+
records would have no way to tell which writer had been considered.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# ADR-037 — `unchanged` means writing would not change a byte, not that the body matches
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-08 · affects every generated region, in every file style
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Review 0002's S29 reported that a region appended after a single newline fails
|
|
8
|
+
`prettier --check`, and pointed at `markers.mjs:232`, the `absent` branch. Measured against
|
|
9
|
+
the shipped binary, that branch was already correct: it inserts the blank line, and its
|
|
10
|
+
output passes `prettier --check` unchanged.
|
|
11
|
+
|
|
12
|
+
The defect was two branches over.
|
|
13
|
+
|
|
14
|
+
**The seam in `splice` had no blank line.** Every fixture built by putting a region into a
|
|
15
|
+
document that already had one — a paragraph, a heading, a list, a fenced block, a
|
|
16
|
+
blockquote — came back rewritten by Prettier 3.8.1, and `--check` exited 1 on every one.
|
|
17
|
+
The list case is worse than a missing blank line: with no gap, Prettier reads the region as
|
|
18
|
+
a continuation of the last list item and indents all of it, so the repair is not one byte
|
|
19
|
+
but a reindent.
|
|
20
|
+
|
|
21
|
+
**A body that matched behind a wrong seam was reported `unchanged`.** The comparison was on
|
|
22
|
+
the region body alone, so a file whose body was correct and whose seam was not produced no
|
|
23
|
+
drift. `check` found nothing, `sync` had nothing to rewrite, and the repository stayed red
|
|
24
|
+
on its own formatter with no remedy this tool offers. This is the shape ADR-034 and
|
|
25
|
+
ADR-036 both exist to prevent, and it is why the second half of the fix mattered more than
|
|
26
|
+
the first: without it, the seam fix would have applied only to files whose body happened to
|
|
27
|
+
change, and every already-adopted repository would have kept a seam nothing would repair.
|
|
28
|
+
|
|
29
|
+
Measured end to end: a repo whose region body is current and whose seam is wrong exits 1 on
|
|
30
|
+
`check`, `sync` rewrites 11,217 → 11,218 bytes, Prettier is then satisfied, and `check`
|
|
31
|
+
exits 0.
|
|
32
|
+
|
|
33
|
+
## Ruling
|
|
34
|
+
|
|
35
|
+
**1. `seam(before)` is the one place the separator is decided.** It normalises whatever
|
|
36
|
+
follows hand-written text to a blank line, and returns `''` for an empty document so a new
|
|
37
|
+
file does not open with a blank line. The `absent` branch had a hand-rolled equivalent that
|
|
38
|
+
agreed with it exactly; keeping both would have left the next reader looking at the branch
|
|
39
|
+
that looks wrong and is not.
|
|
40
|
+
|
|
41
|
+
**2. `unchanged` means writing would not change a byte.** Not that the body matches. The
|
|
42
|
+
question `sync` asks is whether it has anything to write, and a file whose seam disagrees
|
|
43
|
+
has something to write even when its body does not.
|
|
44
|
+
|
|
45
|
+
**3. Top placement is exempt from the byte comparison, and the reason is recorded.** There,
|
|
46
|
+
`splice` is a repair: it moves a region that sits below the file's own settings back above
|
|
47
|
+
them, which moves bytes outside the markers by design. A misplaced region is reported by
|
|
48
|
+
`check` as `misplaced` with its own remedy, and re-laying-out the file there was correct
|
|
49
|
+
before this change and stays correct. Making the exemption explicit is the point — the
|
|
50
|
+
alternative is to discover the reason by breaking `baselines.test.mjs`.
|
|
51
|
+
|
|
52
|
+
**4. A repair must settle.** After `sync` rewrites the seam, the next run reports
|
|
53
|
+
`unchanged` byte for byte. A rule that reports drift on a file nobody has touched is a
|
|
54
|
+
defect with the sign flipped, and the test asserts the second run rather than the first.
|
|
55
|
+
|
|
56
|
+
**5. The assertions are about bytes, not about Prettier.** Prettier cannot be a dependency
|
|
57
|
+
of this package (ADR-012), so the tests state the shape the formatter requires and cite the
|
|
58
|
+
measurement in a comment. Every fixture in the seam table was run through
|
|
59
|
+
`prettier --check` before the assertion was written, and every one failed.
|
|
60
|
+
|
|
61
|
+
## Consequences
|
|
62
|
+
|
|
63
|
+
`test/markers.test.mjs` gains two tests. The first walks six shapes of preceding text and
|
|
64
|
+
asserts exactly one blank line — not a run of them, because a formatter collapses those
|
|
65
|
+
and `check` must not manufacture drift out of a file nobody touched. The second covers the
|
|
66
|
+
`unchanged` verdict, which no formatter-shaped assertion could reach.
|
|
67
|
+
|
|
68
|
+
The second test was itself wrong on its first version and a mutation found it. It built its
|
|
69
|
+
fixture from a hand-written region that lacked the do-not-edit note, so the body genuinely
|
|
70
|
+
differed — meaning it passed with the byte comparison removed, while claiming to police it.
|
|
71
|
+
It now builds the fixture from a real composed region and deletes the blank line, which is
|
|
72
|
+
the file the defect actually produced.
|
|
73
|
+
|
|
74
|
+
Two mutations were applied and reverted, each caught by its own test: `seam` reduced to a
|
|
75
|
+
single newline, and the byte comparison dropped from the verdict.
|
|
76
|
+
|
|
77
|
+
The second half of the spec's acceptance — the pack-install test running `format:check` in
|
|
78
|
+
a fixture that has Prettier — belongs to S20 and is not done here. S20 is the row that
|
|
79
|
+
stages a packed install, and a test that needs Prettier cannot live in this package.
|
|
80
|
+
|
|
81
|
+
## Rejected
|
|
82
|
+
|
|
83
|
+
- **Fix only the branch the review named.** It is already correct. The finding was measured
|
|
84
|
+
against the shipped binary before anything changed precisely because a plausible line
|
|
85
|
+
number is not an observation, and stopping there would have shipped a no-op and closed the
|
|
86
|
+
row.
|
|
87
|
+
- **Leave `unchanged` comparing the body, and let `check` ignore the seam.** Then the seam
|
|
88
|
+
fix only helps a repository the first time its rules change, which is the one moment
|
|
89
|
+
nobody is reading. The other half is the defect.
|
|
90
|
+
- **Have `check` report the seam as its own finding rather than as drift.** A formatter is
|
|
91
|
+
not a consumer of this tool's verdict; the bytes are simply wrong and `sync` fixes them.
|
|
92
|
+
A second status for one wrong byte gives the reader two questions to ask about it.
|
|
93
|
+
- **Compare bytes for the `top` style too, and re-lay-out its splice to match.** That is
|
|
94
|
+
the change that would have broken a misplaced region finding. Re-laying-out a file to
|
|
95
|
+
match a formatting rule is a move onto somebody else's territory, and the region moving
|
|
96
|
+
home is the only reason those bytes are the tool's business.
|
|
97
|
+
- **Ship Prettier as a devDependency so the seam table can assert against the real
|
|
98
|
+
formatter.** ADR-012 rules it, and the rule this record settles is expressible in bytes:
|
|
99
|
+
which is the same reason the projection emits the formatter's shape rather than calling
|
|
100
|
+
it.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# ADR-038 — The gate filter belongs to every writer of a manifest, not to `init`
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-08 · amends [ADR-017](ADR-017-a-declared-gate-must-be-runnable.md) ruling 1 · affects every code path that calls `buildManifest`
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
ADR-017 ruled that a gate is declared only if the repo can run it, and attributed the
|
|
8
|
+
mechanism to one command: *"`init` filters the stack table's proposals through what the
|
|
9
|
+
repo demonstrably has."* The belief is general. The attribution is what was read, and it
|
|
10
|
+
stayed true only as long as `init` was the sole writer of a manifest.
|
|
11
|
+
|
|
12
|
+
Review 0002's S27 is what the attribution bought. `recordLocal` in `skills install` writes a
|
|
13
|
+
manifest for a repo that has none, and called `buildManifest` with no capability. Measured
|
|
14
|
+
end to end: a bare Node repo — a `package.json` with no scripts and no `node_modules` — ran
|
|
15
|
+
`skills install api-contract-sync --yes`, and the manifest it was handed declared `format`
|
|
16
|
+
and `lint` on pre-commit and `test` and `typecheck` on pre-push. `check` then reported four
|
|
17
|
+
`UNRUNNABLE` findings and exited 1. After `hooks install`, a real commit ran the pre-commit
|
|
18
|
+
gate, which failed both staged gates with their remedies and blocked. The hook's own exit
|
|
19
|
+
code is not the commit's, so the reader's first evidence is a commit that silently did not
|
|
20
|
+
happen.
|
|
21
|
+
|
|
22
|
+
Four declared gates the repo cannot run, written by a command whose entire job is to pin
|
|
23
|
+
one skill. ADR-017's own wording names this as the failure: *"declaring one would block
|
|
24
|
+
every commit"*.
|
|
25
|
+
|
|
26
|
+
The test that should have caught it asserted the opposite of the truth. It checked that
|
|
27
|
+
`gates` was present and that `preCommit` and `prePush` were **non-empty**, and read
|
|
28
|
+
non-empty as correct — so the test about manifests being right passed on a manifest
|
|
29
|
+
declaring four gates nobody could run.
|
|
30
|
+
|
|
31
|
+
## Ruling
|
|
32
|
+
|
|
33
|
+
**1. `buildManifest` requires a capability from every caller.** The filtering happens in one
|
|
34
|
+
place, and that place is reached only with the evidence it filters on. A caller that passes
|
|
35
|
+
no `capability` is a caller asking for the defaults, and today it gets them — which is
|
|
36
|
+
correct for a pure function and wrong for a writer. Rather than make the parameter
|
|
37
|
+
mandatory and break the three call sites that legitimately want defaults, the ruling is on
|
|
38
|
+
who writes: **a command that writes a manifest for a repo that has none must probe first**,
|
|
39
|
+
because it is making a promise on that repo's behalf.
|
|
40
|
+
|
|
41
|
+
**2. A command that writes a manifest declares nothing it cannot prove.** This is ADR-017
|
|
42
|
+
ruling 1 with its attribution removed, and it covers a shape ADR-017 did not anticipate:
|
|
43
|
+
a writer that is not an adoption command. `skills install` is not adopting the repo. It
|
|
44
|
+
pins one file into `.agents/skills/`. Writing a full manifest was already the wrong shape
|
|
45
|
+
for it and was ruled on once already; the record for that ruling said *"a `{localSkills}`
|
|
46
|
+
stub would read as authoritative while omitting `gates`"*, and the fix was to write the full
|
|
47
|
+
canonical shape. The reasoning was right and the conclusion drawn from it was wrong, because
|
|
48
|
+
canonical shape and default gates are different things and only one of them is a fact about
|
|
49
|
+
the file's schema.
|
|
50
|
+
|
|
51
|
+
**3. A manifest that declares no gates is a valid answer.** A repo with no scripts and no
|
|
52
|
+
formatter gets `preCommit: []` and `prePush: []`. ADR-017's consequences section already
|
|
53
|
+
ruled this — `init` has been writing it — so the only new fact is that a reader of the
|
|
54
|
+
record can now see the invariant is not `init`'s to keep.
|
|
55
|
+
|
|
56
|
+
**4. The test asserts the manifest is true, not that the field is present.** The new test
|
|
57
|
+
runs `unrunnableGates(manifest, await readCapability(root))` — the function `check` runs,
|
|
58
|
+
with the probe `check` uses — and requires an empty result. It also runs against a fixture
|
|
59
|
+
that *can* run gates and requires them to be there, because a fix that hardcoded two empty
|
|
60
|
+
lists would pass the first half and arm the project with no gates at all.
|
|
61
|
+
|
|
62
|
+
## Consequences
|
|
63
|
+
|
|
64
|
+
`test/install.test.mjs`'s existing assertion changed from
|
|
65
|
+
`assert.ok(manifest.gates.preCommit.length > 0 && …)` to
|
|
66
|
+
`assert.deepEqual(manifest.gates, { preCommit: [], prePush: [] })`. That is a test whose
|
|
67
|
+
pass condition was the defect, and it is the fourth in this repository to read presence as
|
|
68
|
+
correctness — the shape ADR-030 exists to catch.
|
|
69
|
+
|
|
70
|
+
Three mutations were applied and reverted, each caught by its own test: the `capability`
|
|
71
|
+
argument removed; a hand-built empty capability substituted for the probe (caught by the
|
|
72
|
+
capable half, not the bare one); and the capable half's non-empty assertion deleted — which
|
|
73
|
+
the bare half alone did not notice, confirming both halves earn their place.
|
|
74
|
+
|
|
75
|
+
## Rejected
|
|
76
|
+
|
|
77
|
+
- **Make `capability` a required argument of `buildManifest`.** Three callers legitimately
|
|
78
|
+
want the unfiltered defaults, including the tests that exercise `planGates` directly. A
|
|
79
|
+
signature that cannot be called wrongly is worth having, and the wrong call here is one of
|
|
80
|
+
two lines in a function whose purpose is obvious from its name; ruling on the caller keeps
|
|
81
|
+
`buildManifest` a pure function that a test can call with any evidence it likes.
|
|
82
|
+
- **Have `skills install` write a `{localSkills}`-only manifest after all.** That was ruled
|
|
83
|
+
on and the ruling holds: a partial manifest reads as authoritative while omitting `gates`,
|
|
84
|
+
and `init` would then have to be run to complete it. Probing is the smaller change.
|
|
85
|
+
- **Let `check` repair it, since `check` already reports the finding.** `check` reports
|
|
86
|
+
findings; it is not the command a reader runs before `skills install` has already blocked
|
|
87
|
+
their commits. The landmine has to be defused at the moment it is laid.
|
|
88
|
+
- **Leave ADR-017 as written and put the generalisation only in the code.** The code is
|
|
89
|
+
where the second writer is, and a record that names `init` is a record a reader trusts to
|
|
90
|
+
have already covered the case. ADR-017's own context section says nothing else owns the
|
|
91
|
+
manifest — that was true in 2026-10-06 and this is what made it false.
|
|
92
|
+
- **Amend ADR-017's ruling 1 in place.** ADR-017 is accepted and its context is the evidence
|
|
93
|
+
for why the gate filter exists at all. Rewriting the sentence would leave no trace that the
|
|
94
|
+
rule was ever narrower than it reads, which is the same trick the gate list itself was
|
|
95
|
+
performing on its reader.
|