simt-dev 0.2.0__py3-none-any.whl
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.
- python/simt_dev/__init__.py +32 -0
- python/simt_dev/cli.py +225 -0
- simt_dev/__init__.py +32 -0
- simt_dev/cli.py +225 -0
- simt_dev/vendored/CHANGELOG.md +166 -0
- simt_dev/vendored/bin/simt-dev.mjs +28 -0
- simt_dev/vendored/docs/decisions/ADR-001-push-requires-approval.md +42 -0
- simt_dev/vendored/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
- simt_dev/vendored/docs/decisions/ADR-003-test-breadth.md +46 -0
- simt_dev/vendored/docs/decisions/ADR-004-hook-owns-format.md +45 -0
- simt_dev/vendored/docs/decisions/ADR-005-file-length.md +42 -0
- simt_dev/vendored/docs/decisions/ADR-006-coverage-bar.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-007-section-skeleton.md +54 -0
- simt_dev/vendored/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-009-skill-layout.md +48 -0
- simt_dev/vendored/docs/decisions/ADR-010-runner-invocation.md +56 -0
- simt_dev/vendored/docs/decisions/ADR-011-windows-support.md +52 -0
- simt_dev/vendored/docs/decisions/ADR-012-drop-windows-support.md +59 -0
- simt_dev/vendored/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
- simt_dev/vendored/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
- simt_dev/vendored/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
- simt_dev/vendored/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
- simt_dev/vendored/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
- simt_dev/vendored/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
- simt_dev/vendored/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
- simt_dev/vendored/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
- simt_dev/vendored/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
- simt_dev/vendored/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
- simt_dev/vendored/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
- simt_dev/vendored/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
- simt_dev/vendored/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
- simt_dev/vendored/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
- simt_dev/vendored/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +131 -0
- simt_dev/vendored/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
- simt_dev/vendored/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
- simt_dev/vendored/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
- simt_dev/vendored/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
- simt_dev/vendored/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
- simt_dev/vendored/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
- simt_dev/vendored/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
- simt_dev/vendored/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
- simt_dev/vendored/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
- simt_dev/vendored/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
- simt_dev/vendored/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
- simt_dev/vendored/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
- simt_dev/vendored/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
- simt_dev/vendored/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
- simt_dev/vendored/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
- simt_dev/vendored/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
- simt_dev/vendored/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
- simt_dev/vendored/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
- simt_dev/vendored/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
- simt_dev/vendored/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
- simt_dev/vendored/docs/decisions/ADR-050-a-byte-count-is-not-a-region-and-updated-is-not-a-change-of-rules.md +102 -0
- simt_dev/vendored/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
- simt_dev/vendored/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
- simt_dev/vendored/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
- simt_dev/vendored/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
- simt_dev/vendored/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
- simt_dev/vendored/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
- simt_dev/vendored/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
- simt_dev/vendored/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
- simt_dev/vendored/docs/decisions/README.md +530 -0
- simt_dev/vendored/graph/skills.graph.json +1622 -0
- simt_dev/vendored/package.json +43 -0
- simt_dev/vendored/rules/core.md +76 -0
- simt_dev/vendored/rules/mdc/intake-questions.mdc +50 -0
- simt_dev/vendored/rules/mdc/preview-postflight.mdc +61 -0
- simt_dev/vendored/rules/mdc/slice-commit.mdc +52 -0
- simt_dev/vendored/rules/mdc/test-gate.mdc +54 -0
- simt_dev/vendored/rules/stacks/android.md +63 -0
- simt_dev/vendored/rules/stacks/cloudflare-workers.md +65 -0
- simt_dev/vendored/rules/stacks/connectiq.md +62 -0
- simt_dev/vendored/rules/stacks/container.md +31 -0
- simt_dev/vendored/rules/stacks/jvm.md +54 -0
- simt_dev/vendored/rules/stacks/node.md +45 -0
- simt_dev/vendored/rules/stacks/python.md +48 -0
- simt_dev/vendored/rules/stacks/react-native.md +58 -0
- simt_dev/vendored/rules/stacks/react-web.md +61 -0
- simt_dev/vendored/rules/stacks/typescript.md +21 -0
- simt_dev/vendored/rules/stacks/uv.md +32 -0
- simt_dev/vendored/src/argv.mjs +116 -0
- simt_dev/vendored/src/ci/dependabot.mjs +268 -0
- simt_dev/vendored/src/ci/workflow.mjs +360 -0
- simt_dev/vendored/src/commands/ci.mjs +285 -0
- simt_dev/vendored/src/commands/compare.mjs +195 -0
- simt_dev/vendored/src/commands/graph.mjs +73 -0
- simt_dev/vendored/src/commands/help.mjs +108 -0
- simt_dev/vendored/src/commands/hooks.mjs +471 -0
- simt_dev/vendored/src/commands/index.mjs +241 -0
- simt_dev/vendored/src/commands/init.mjs +632 -0
- simt_dev/vendored/src/commands/mcp.mjs +63 -0
- simt_dev/vendored/src/commands/rules.mjs +374 -0
- simt_dev/vendored/src/commands/skills.mjs +647 -0
- simt_dev/vendored/src/commands/skillsinstall.mjs +515 -0
- simt_dev/vendored/src/commands/sync.mjs +331 -0
- simt_dev/vendored/src/commands/upgrade.mjs +202 -0
- simt_dev/vendored/src/compare/discover.mjs +212 -0
- simt_dev/vendored/src/compare/hooksstate.mjs +159 -0
- simt_dev/vendored/src/compare/row.mjs +299 -0
- simt_dev/vendored/src/decisions/index.mjs +536 -0
- simt_dev/vendored/src/graph/load.mjs +541 -0
- simt_dev/vendored/src/hooks/capability.mjs +129 -0
- simt_dev/vendored/src/hooks/dispatcher.mjs +268 -0
- simt_dev/vendored/src/hooks/gates.mjs +144 -0
- simt_dev/vendored/src/hooks/resolve.mjs +194 -0
- simt_dev/vendored/src/hooks/shim.mjs +140 -0
- simt_dev/vendored/src/hooks/where.mjs +118 -0
- simt_dev/vendored/src/identity.mjs +218 -0
- simt_dev/vendored/src/locate.mjs +75 -0
- simt_dev/vendored/src/mcp/server.mjs +167 -0
- simt_dev/vendored/src/mcp/tools.mjs +410 -0
- simt_dev/vendored/src/name.mjs +95 -0
- simt_dev/vendored/src/output/index.mjs +96 -0
- simt_dev/vendored/src/projection/baselines.mjs +209 -0
- simt_dev/vendored/src/projection/conflicts.mjs +205 -0
- simt_dev/vendored/src/projection/manifest.mjs +478 -0
- simt_dev/vendored/src/projection/markers.mjs +349 -0
- simt_dev/vendored/src/projection/mdc.mjs +327 -0
- simt_dev/vendored/src/projection/project.mjs +504 -0
- simt_dev/vendored/src/projection/rules.mjs +640 -0
- simt_dev/vendored/src/projection/surfaces.mjs +398 -0
- simt_dev/vendored/src/projection/targets.mjs +221 -0
- simt_dev/vendored/src/resolve/fingerprint.mjs +553 -0
- simt_dev/vendored/src/resolve/near.mjs +61 -0
- simt_dev/vendored/src/resolve/resolve.mjs +457 -0
- simt_dev/vendored/src/resolve/source.mjs +105 -0
- simt_dev/vendored/src/toolchain.mjs +202 -0
- simt_dev/vendored/src/tsconfig.mjs +314 -0
- simt_dev/vendored/src/upgrade/plan.mjs +203 -0
- simt_dev/vendored/src/workspace.mjs +289 -0
- simt_dev-0.2.0.dist-info/METADATA +150 -0
- simt_dev-0.2.0.dist-info/RECORD +138 -0
- simt_dev-0.2.0.dist-info/WHEEL +4 -0
- simt_dev-0.2.0.dist-info/entry_points.txt +2 -0
- simt_dev-0.2.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# ADR-017 — A declared gate must be one the repo can actually run
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-06
|
|
4
|
+
Affects: `hooks/capability.mjs`, `projection/manifest.mjs`, `commands/init.mjs`, `commands/sync.mjs` (`check`), `hooks/dispatcher.mjs`
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
The first real adoption wrote a manifest declaring five gates into `repo-13`.
|
|
9
|
+
Three of them could not run:
|
|
10
|
+
|
|
11
|
+
| Gate | Kind | Why it could not run |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `format` | staged | prettier is not a dependency and is not in `node_modules/.bin` |
|
|
14
|
+
| `lint` | script | there is no `lint` script in `package.json` |
|
|
15
|
+
| `i18n-completeness` | command | nothing is declared under `gates.commands` |
|
|
16
|
+
|
|
17
|
+
It reported as healthy. `simt-dev check` was green, the projected files were current, and the
|
|
18
|
+
repo's own 37 tests passed. The defect was invisible to all of that because nothing had asked
|
|
19
|
+
whether the gates could run — the stack table proposed them from the repo's tags, and the tags
|
|
20
|
+
said `node` and `cloudflare-workers`, which is true.
|
|
21
|
+
|
|
22
|
+
The dispatcher would have caught it, and correctly: it fails closed. Installing it on that
|
|
23
|
+
manifest blocks **every commit** with `FAILED format … [ADR-004]`. So the first thing a new
|
|
24
|
+
user does after adopting simt-dev is discover that the tool makes committing impossible, and
|
|
25
|
+
the only escape any of them will find is `--no-verify`.
|
|
26
|
+
|
|
27
|
+
That is the exact spiral the dispatcher was built to prevent. Step 8's design note says a gate
|
|
28
|
+
that cannot run must be a failure rather than a skip, precisely because `repo-15` removed
|
|
29
|
+
tests from `pre-commit` for being slow and the team's answer was `--no-verify`. Failing closed
|
|
30
|
+
is right; declaring an unrunnable gate is wrong.
|
|
31
|
+
|
|
32
|
+
Two further facts made it survivable for as long as it was:
|
|
33
|
+
|
|
34
|
+
- **Nothing else owns the manifest.** `sync` writes the projected rules; `init` writes the
|
|
35
|
+
manifest. A manifest written by an older version is therefore never revisited, which is the
|
|
36
|
+
same class of bug as the stale `$comment` fixed in `c607d0a`.
|
|
37
|
+
- **`hooks status` already said so** — *"gate `i18n-completeness` in preCommit is a command
|
|
38
|
+
gate with no declared command; the hook will fail"*. The information existed. It was printed
|
|
39
|
+
by the one command nobody runs before installing hooks.
|
|
40
|
+
|
|
41
|
+
## Rulings
|
|
42
|
+
|
|
43
|
+
1. **A gate is declared only if the repo can run it.** `init` filters the stack table's
|
|
44
|
+
proposals through what the repo demonstrably has: a script that exists in `package.json`,
|
|
45
|
+
a binary installed in `node_modules/.bin`, a command the manifest itself names. This is
|
|
46
|
+
`has-*` fact discipline applied to gates (ADR-013): detection reports evidence, it does not
|
|
47
|
+
assume capability.
|
|
48
|
+
|
|
49
|
+
2. **Every drop is named, with the remedy that undoes it.** A silently shorter gate list reads
|
|
50
|
+
as "this repo needs no formatting check", when the truth is "declaring one would block every
|
|
51
|
+
commit". The three ways to make a gate runnable are a real choice the repo owns, so the
|
|
52
|
+
reader is given all three — install the tool, add the script, or name the command.
|
|
53
|
+
|
|
54
|
+
3. **A gate the repo already declared is never dropped.** Quietly disarming a gate somebody
|
|
55
|
+
declared on purpose is a worse surprise than a visible one, and a re-init that silently
|
|
56
|
+
removed gates would make the gate list look like simt-dev's opinion rather than the repo's.
|
|
57
|
+
|
|
58
|
+
4. **`check` reports an unrunnable declared gate as a finding.** The repo must be red *before*
|
|
59
|
+
anyone installs hooks, not after. `check` is the gate that runs in CI, so this is where a
|
|
60
|
+
landmine belongs; `hooks status` saying so is not enough when it is the command nobody runs.
|
|
61
|
+
|
|
62
|
+
5. **The remedy differs by whether a manifest already exists.** A re-init never re-adds a gate
|
|
63
|
+
to a manifest that already declares one, so on an existing manifest "install prettier, then
|
|
64
|
+
re-run init" would send the reader round the loop and back to where they started. The remedy
|
|
65
|
+
there names the declaration as the second half of the fix.
|
|
66
|
+
|
|
67
|
+
## Consequences
|
|
68
|
+
|
|
69
|
+
- `simt-dev init` needs to read the repo before it writes the manifest, so `init` now depends on
|
|
70
|
+
the hooks subsystem. The dependency runs one way: `gates.mjs` imports nothing.
|
|
71
|
+
- `check` is no longer only about the generated rules. It is the gate over what the manifest
|
|
72
|
+
promises, which is what a gate is for.
|
|
73
|
+
- A repo whose only answer is "no tool for any of these" gets `preCommit: []`. That is a valid
|
|
74
|
+
manifest, and the dispatcher says so in one line rather than passing in silence. A string
|
|
75
|
+
where the caller expected an array once printed that line one character at a time.
|
|
76
|
+
|
|
77
|
+
## Rejected
|
|
78
|
+
|
|
79
|
+
- **Fail open in the dispatcher when a gate cannot run.** The spiral is the point; failing open
|
|
80
|
+
makes the tool's green meaningless, which is worse than a blocked commit because it is silent.
|
|
81
|
+
- **Infer capability from the dependency graph.** A repo with `prettier` in `devDependencies`
|
|
82
|
+
and no `node_modules` cannot run a formatter. Reading the manifest and the installed binaries
|
|
83
|
+
are two facts; guessing a third from a lockfile is how the two get to disagree.
|
|
84
|
+
- **Have `init` refuse to adopt a repo that cannot run its gates.** Refusing is worse than
|
|
85
|
+
writing fewer gates: the repo gets no rules at all, and the obstacle is a formatting tool.
|
|
86
|
+
- **Leave it to `hooks status`.** It is the command run after installing hooks, which is one
|
|
87
|
+
step after the commit that fails.
|
|
88
|
+
- **Drop unrunnable gates on re-init too.** Silently; the reader sees a shorter list and no
|
|
89
|
+
explanation, and cannot tell a removed gate from one that was never there.
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# An unset hooks path is not an absent hook
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-06
|
|
4
|
+
|
|
5
|
+
Affects: `simt-dev hooks install`, `simt-dev hooks status`, `simt-dev compare`, `src/hooks/where.mjs`, `src/hooks/shim.mjs`
|
|
6
|
+
|
|
7
|
+
## Context
|
|
8
|
+
|
|
9
|
+
Phase 4 began by piloting `simt-dev init` on `repo-15`. The dry run was
|
|
10
|
+
unremarkable. The defect appeared when I read what `hooks install` would actually
|
|
11
|
+
do to that repository, which is something I had not done in any of the earlier
|
|
12
|
+
phases.
|
|
13
|
+
|
|
14
|
+
`repo-15` has a hand-written `.git/hooks/pre-commit` — 4088 bytes, with prose
|
|
15
|
+
explaining why it deliberately does not run tests. `core.hooksPath` is unset.
|
|
16
|
+
`simt-dev hooks status --root ../repo-15 --json` reported:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{ "hooksPath": null, "installed": [{ "event": "pre-commit", "present": false }],
|
|
20
|
+
"problems": ["core.hooksPath is (unset), expected .simt-hooks",
|
|
21
|
+
"pre-commit not installed"] }
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
and exited with `ok: true`. A live, working gate was reported as not installed.
|
|
25
|
+
|
|
26
|
+
`hooks install` then did this, with no warning of any kind:
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
core.hooksPath created (unset) -> .simt-hooks
|
|
30
|
+
CREATED .simt-hooks/pre-commit
|
|
31
|
+
CREATED .simt-hooks/pre-push
|
|
32
|
+
WROTE shims and set core.hooksPath
|
|
33
|
+
gates preCommit = (none declared)
|
|
34
|
+
prePush = (none declared)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Three things went wrong at once, and the first one is the load-bearing fact:
|
|
38
|
+
|
|
39
|
+
1. **An unset `core.hooksPath` was read as "git runs no hooks".** It means the
|
|
40
|
+
opposite: git falls back to its default directory and runs whatever is in it.
|
|
41
|
+
2. **The takeover guard only looked at `core.hooksPath`.** It existed to stop
|
|
42
|
+
simt-dev silently replacing routing somebody else had chosen — and it checked
|
|
43
|
+
only for a *configured* value. The most common way to have hooks is the one it
|
|
44
|
+
could not see.
|
|
45
|
+
3. **The install created two shims that enforced nothing.** `simt-dev init` had
|
|
46
|
+
already said, in prose, that this repo declares no gate it can run and there is
|
|
47
|
+
nothing to install. `hooks install` disagreed, and in doing so replaced a
|
|
48
|
+
working gate with a gate that checks nothing.
|
|
49
|
+
|
|
50
|
+
The consequence in a real adoption is worse than a misreport. A repo that adopts
|
|
51
|
+
and then installs hooks gets, in place of its working formatter, two shims that
|
|
52
|
+
fail closed because `@quazarmedia/simt-dev` is not yet a dependency. Every commit
|
|
53
|
+
is blocked. The only exit is `--no-verify` — the exact spiral the dispatcher was
|
|
54
|
+
built in step 8 to prevent.
|
|
55
|
+
|
|
56
|
+
The same class of error sat under the two commands that already existed.
|
|
57
|
+
`src/compare/hooksstate.mjs` was written as "the read half of what `hooks status`
|
|
58
|
+
does inline", but `hooks status` never called it: it had its own `readHooksPath`,
|
|
59
|
+
its own `describeShim` and its own `collectProblems`. Three copies of the same
|
|
60
|
+
question, which is why fixing the shared one changed nothing visible.
|
|
61
|
+
|
|
62
|
+
## Ruling
|
|
63
|
+
|
|
64
|
+
1. **Ask git where hooks are, never infer it from whether config exists.**
|
|
65
|
+
`resolveHooksDir(root)` implements the two rules directly: `core.hooksPath`
|
|
66
|
+
when set (a relative value resolves against the top of the working tree, which
|
|
67
|
+
is what makes `repo-05`'s `hooks` work), otherwise `<git-common-dir>/hooks`.
|
|
68
|
+
|
|
69
|
+
`git rev-parse --git-path hooks` is the obvious shortcut and is unusable. It
|
|
70
|
+
returns `.git/hooks` in a normal repository, `hooks` in a linked worktree, and
|
|
71
|
+
**`./`** in a linked worktree with `core.hooksPath` unset — because a worktree's
|
|
72
|
+
`.git` is a file, not a directory. An implementation that trusted it would
|
|
73
|
+
enumerate a worktree's entire root as its hooks directory and report every file
|
|
74
|
+
in the repository as a hook.
|
|
75
|
+
|
|
76
|
+
2. **A hook git runs and simt-dev did not write is a first-class fact.** It is
|
|
77
|
+
named in `hooks status` text and JSON, in `hooks install`'s refusal and its
|
|
78
|
+
post-install notes, and as a row-level `foreignHooks` field. Identity is decided
|
|
79
|
+
by content, never by name: a simt-dev shim is also called `pre-commit`, so only
|
|
80
|
+
the bytes can tell ours from somebody else's.
|
|
81
|
+
|
|
82
|
+
3. **Taking over a foreign hook requires `--yes`, and the refusal names the hook.**
|
|
83
|
+
The existing guard was right about the principle and incomplete about the facts.
|
|
84
|
+
It now covers both a configured path and an unconfigured default directory.
|
|
85
|
+
|
|
86
|
+
4. **A shim is installed only for an event that declares at least one gate, and
|
|
87
|
+
`install` refuses when no event does.** An empty-gate shim enforces nothing but
|
|
88
|
+
still displaces whatever was there, so installing one over a working hook is a
|
|
89
|
+
downgrade. `init` and `install` must not describe different tools.
|
|
90
|
+
|
|
91
|
+
5. **In `compare`, a repo running its own hooks is a note, not a problem.** Notes
|
|
92
|
+
do not affect the exit code. "Not adopted yet" is the normal state of a
|
|
93
|
+
migration, and step 9 established — deliberately, after finding that a dashboard
|
|
94
|
+
which goes red for the normal state is a dashboard everybody ignores — that only
|
|
95
|
+
defects drive the exit code. Six of the sixteen repositories run their own hooks,
|
|
96
|
+
so putting this in `problems` would have made the entire estate red on day one.
|
|
97
|
+
|
|
98
|
+
6. **A missing shim is only a defect when a gate needs it.** A repository that
|
|
99
|
+
declares no `preCommit` gates has no reason to want a `pre-commit` shim. A
|
|
100
|
+
repository with *no manifest* is a different case: we do not know what it wants,
|
|
101
|
+
so we cannot rule the shim out. Absence of knowledge is not a decision.
|
|
102
|
+
|
|
103
|
+
7. **A foreign hook for an event this tool does not gate is chained, not stopped.**
|
|
104
|
+
`core.hooksPath` does not scope this tool to the events it cares about and leave
|
|
105
|
+
the rest to git; git reads *every* hook from the named directory and from nowhere
|
|
106
|
+
else. So the moment routing lands, `repo-05`'s `post-checkout`, `post-commit`
|
|
107
|
+
and `post-merge` — events this tool gates nothing about — stop running. Nothing
|
|
108
|
+
fails, nothing is logged, and the hook is still on disk looking intact: the
|
|
109
|
+
failure is indistinguishable from the hook having passed. Ruling 3's refusal
|
|
110
|
+
names the hook, but a refusal the operator accepts is not a chain.
|
|
111
|
+
|
|
112
|
+
`hooks install` therefore writes a second kind of shim for those events, which
|
|
113
|
+
runs the repository's own hook and forwards git's arguments to it. There is
|
|
114
|
+
nothing to enforce on an event this tool does not gate, so it does not run the
|
|
115
|
+
dispatcher at all. Git's arguments *are* forwarded here, unlike in the dispatcher
|
|
116
|
+
shim, because the program on the other end is the repository's hook and expects
|
|
117
|
+
them.
|
|
118
|
+
|
|
119
|
+
The target is written relative to the repository's top level wherever the hook
|
|
120
|
+
lives inside it, so a clone or a move does not silently stop resolving it. If the
|
|
121
|
+
hook is not there at all, the shim refuses and names the path — a hook that
|
|
122
|
+
silently stops running is the exact outcome this ruling exists to prevent.
|
|
123
|
+
|
|
124
|
+
A gate-type foreign hook is deliberately **not** chained — *when this repository
|
|
125
|
+
declares a gate for that event*. The dispatcher replaces the hook there by design, the
|
|
126
|
+
hook's real place is `gates.commands`, and its verdict belongs in the gate report next
|
|
127
|
+
to the others. Chaining it would run it *after* the gates had already passed, outside
|
|
128
|
+
anything the author sees.
|
|
129
|
+
|
|
130
|
+
"Does this tool gate it" is a fact about the manifest, not about the event vocabulary,
|
|
131
|
+
and reading it the second way cost `repo-05` its `pre-push`. That hook is git-lfs.
|
|
132
|
+
`pre-push` is an event this tool gates *in general*, so a vocabulary check displaced it
|
|
133
|
+
— and because `repo-05` declares no `pre-push` gate there is no shim for that event
|
|
134
|
+
either, so git would have found no `pre-push` hook at all. LFS objects would have
|
|
135
|
+
stopped being uploaded, with nothing to notice it: the same silent failure this ruling
|
|
136
|
+
exists to prevent, reached by fixing the wrong half of it. The test asks the manifest.
|
|
137
|
+
|
|
138
|
+
The install note says which of the two happened, because "stopped running" was wrong
|
|
139
|
+
about a carried hook and would have pushed an operator to restore a hook that was
|
|
140
|
+
already running for them.
|
|
141
|
+
|
|
142
|
+
## Consequences
|
|
143
|
+
|
|
144
|
+
`src/hooks/where.mjs` is new and is the single answer to "where are this repo's
|
|
145
|
+
hooks". `hooksstate.mjs` and `hooks status` no longer hold separate
|
|
146
|
+
implementations — the three duplicated functions in `hooks.mjs` are gone, and
|
|
147
|
+
`hooksstatus` reads through the shared module.
|
|
148
|
+
|
|
149
|
+
Two guard tests are recorded in `test/compare.test.mjs` because the JSON surface
|
|
150
|
+
had already lost a field once: the `summary` block in `compare --json` was a
|
|
151
|
+
hand-listed copy of `summarise()`'s output, and `withOwnHooks` reached the text
|
|
152
|
+
render while silently vanishing from the machine surface. A copied list of names is
|
|
153
|
+
a list that rots, so one test now asserts the two agree and another asserts every
|
|
154
|
+
row carries `notes` and `foreignHooks` even when empty.
|
|
155
|
+
|
|
156
|
+
Measured across the estate after the fix, six repositories run hooks simt-dev did
|
|
157
|
+
not write — `repo-15` (4088 B), `repo-05` (four hooks), `repo-14` (two),
|
|
158
|
+
`repo-11` (two), `repo-10` (one), and the `.wt-wear-redesign`
|
|
159
|
+
worktree (five, including the `git-lfs` `pre-push`). Every one of them would have
|
|
160
|
+
been silently taken over.
|
|
161
|
+
|
|
162
|
+
## Rejected
|
|
163
|
+
|
|
164
|
+
- **Treat an unset `core.hooksPath` as "no hooks", and document it.** This is the
|
|
165
|
+
bug. The documentation would have been accurate about the code and wrong about
|
|
166
|
+
git.
|
|
167
|
+
- **Ask the user before overwriting, and leave `hooks status` as it was.** The
|
|
168
|
+
question was never asked, because the tool did not know a hook existed. A guard
|
|
169
|
+
that cannot see the thing it guards is decoration.
|
|
170
|
+
- **Move a foreign hook aside instead of blocking.** Blocking is right; the file is
|
|
171
|
+
left on disk untouched either way, so the outcome already is "move it aside".
|
|
172
|
+
- **Chain every foreign hook, gate-type ones included.** Rejected when ruling 7 was
|
|
173
|
+
written, and the reason is the difference between a verdict and a side effect. A
|
|
174
|
+
`pre-commit` hook chained behind the dispatcher runs *after* the gates have already
|
|
175
|
+
passed, so its failure is reported by git as a hook failure with no gate output
|
|
176
|
+
above it — an author cannot tell which of the two things said no. The hook's real
|
|
177
|
+
place is `gates.commands`, where it runs as a gate and appears in the report next
|
|
178
|
+
to the others.
|
|
179
|
+
- **Install the shims anyway and warn.** The gate list was empty. Warning about a
|
|
180
|
+
replacement that enforces less than what it replaces is the wrong direction.
|
|
181
|
+
- **Put foreign hooks in `compare`'s `problems`.** Six of sixteen repositories, all
|
|
182
|
+
of them unadopted, on the day the dashboard is introduced.
|
|
183
|
+
- **Use `git rev-parse --git-path hooks`.** Returns `./` for a linked worktree.
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# A declared command is the runner; the remedy must be one the reader can take
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-07
|
|
4
|
+
|
|
5
|
+
Affects: `simt-dev init`, `simt-dev check`, `simt-dev hooks status`, `simt-dev hooks run`,
|
|
6
|
+
`src/hooks/capability.mjs`, `src/hooks/resolve.mjs`, `src/projection/manifest.mjs`
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
ADR-017 made `simt-dev init` declare only gates a repository can actually run, and it shipped
|
|
11
|
+
green because the first adopter — `repo-13` — is a Node repository whose scripts
|
|
12
|
+
and binaries are exactly what the filter understood. It was verified by running that
|
|
13
|
+
repository's own tests.
|
|
14
|
+
|
|
15
|
+
`repo-05`, the third pilot, is a Gradle repository. It has **no `package.json` at all**. The
|
|
16
|
+
dry run dropped all five proposed gates, and the remedy for each one was an instruction the
|
|
17
|
+
reader cannot follow. The output as it stood, before this record was written:
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
$ simt-dev init --root ../repo-05
|
|
21
|
+
dropped gate test: no "test" script in package.json
|
|
22
|
+
remedy: add a "test" script to package.json, then re-run: simt-dev init --yes
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Ruling 4 changes both lines. `repo-05` will never have a `package.json`. "Install prettier as
|
|
26
|
+
a dev dependency" is equally impossible. The filter was right about the facts and wrong about
|
|
27
|
+
what followed from them, and the reader's only honest reaction to that remedy is to stop
|
|
28
|
+
trusting the next one.
|
|
29
|
+
|
|
30
|
+
Then the repo declared what it does have, using the escape hatch the remedy for command gates
|
|
31
|
+
had always advertised:
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
"gates": { "preCommit": ["i18n-completeness"], "prePush": ["test"],
|
|
35
|
+
"commands": { "i18n-completeness": "scripts/check_string_translations.sh",
|
|
36
|
+
"test": "./gradlew check" } }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
and three surfaces answered three different things about it:
|
|
40
|
+
|
|
41
|
+
- `simt-dev hooks run pre-commit` — **PASSED**, running the declared script.
|
|
42
|
+
- `simt-dev hooks run pre-push` — would have demanded an npm script that does not exist,
|
|
43
|
+
because `resolveGate` only consulted `gates.commands` for gates of kind `command`.
|
|
44
|
+
- `simt-dev check` — **UNRUNNABLE** on both, because `readCapability` never read
|
|
45
|
+
`simt-dev.json` at all and so always reported an empty command map.
|
|
46
|
+
|
|
47
|
+
One manifest. Three answers. The one I would have shipped if I had only run `check`.
|
|
48
|
+
|
|
49
|
+
## Rulings
|
|
50
|
+
|
|
51
|
+
1. **A gate name is a requirement, not a mechanism.** The repository declares "prove `test`
|
|
52
|
+
somehow"; `gates.commands[test]` is how it says how. Once declared, that wins for **every**
|
|
53
|
+
gate kind — `script`, `staged` and `command` alike — because a repository naming its own
|
|
54
|
+
program is a more specific statement than any table this tool can ship.
|
|
55
|
+
2. **`gates.commands` is part of what a repository can run.** `readCapability` reads
|
|
56
|
+
`simt-dev.json` alongside `package.json` and `node_modules/.bin`. This is the one
|
|
57
|
+
capability source that is a declaration rather than an observation, and that asymmetry is
|
|
58
|
+
the point: the only evidence that can prove a command gate is the declaration.
|
|
59
|
+
3. **The dispatcher, `check` and `hooks status` read one manifest once and answer identically.**
|
|
60
|
+
Two surfaces must never disagree about what a gate will do; the disagreement is what made
|
|
61
|
+
this a defect rather than a rough edge.
|
|
62
|
+
4. **A remedy names an action the reader can take.** Where npm is the runner, the npm remedy
|
|
63
|
+
stands. Where it demonstrably is not, the remedy is to name the program under
|
|
64
|
+
`gates.commands` — the same instruction already given for command gates.
|
|
65
|
+
5. **An empty declared command is a failure, not a silent pass.** It would resolve to an empty
|
|
66
|
+
argv and report `PASSED` while proving nothing, which is the worst outcome a gate can have.
|
|
67
|
+
6. **`'none'` is an answer; absent is not.** A capability that does not report its runner (one
|
|
68
|
+
built by hand, or by an older caller) is treated as npm. Claiming a repository has no
|
|
69
|
+
`package.json` on the strength of a missing field is a much worse guess than assuming it.
|
|
70
|
+
7. **Runner detection reads files, and must know the difference.** The marker for Gradle is
|
|
71
|
+
the `gradlew` file. My first cut used `readdir` to test existence, which answers "no" for
|
|
72
|
+
every file (`ENOTDIR`) and classified every Gradle repository in the estate as having no
|
|
73
|
+
runner at all — a wrong answer from an evidence reader, which is worse than no answer
|
|
74
|
+
because the caller cannot tell them apart.
|
|
75
|
+
|
|
76
|
+
## Consequences
|
|
77
|
+
|
|
78
|
+
- `repo-05` declares `i18n-completeness: scripts/check_string_translations.sh` on
|
|
79
|
+
pre-commit — the gate its own hand-written pre-commit hook already runs — and
|
|
80
|
+
`test: ./gradlew check` on pre-push.
|
|
81
|
+
- The `repo-13` and `repo-15` manifests are unaffected: they declare no commands.
|
|
82
|
+
- A Node repository that wants to override a gate now can, without abandoning the gate name it
|
|
83
|
+
shares with every other repository. `gates.commands.format` is a supported thing to write.
|
|
84
|
+
- `readCapability` reads one more file per invocation, and it reads it in a `try` that cannot
|
|
85
|
+
throw: a repository with no manifest simply has no declared commands.
|
|
86
|
+
|
|
87
|
+
## Rejected
|
|
88
|
+
|
|
89
|
+
- **Add a `gradle` gate kind, and a `python` one, and a `cargo` one.** Each would need its own
|
|
90
|
+
dispatcher branch, its own remedy, its own tests, and its own entry in `GATES`. One rule —
|
|
91
|
+
a declared command wins — covers every ecosystem including the next one, which is the reason
|
|
92
|
+
the `command` kind exists at all.
|
|
93
|
+
- **Let the reader name the gate.** A repository could declare a gate called `gradle-check`
|
|
94
|
+
rather than `test`. Then `test` is dropped for lack of a script while a second, unrelated
|
|
95
|
+
gate appears, and the two estates of gate names stop being comparable.
|
|
96
|
+
- **Guess the Gradle task.** Printing `./gradlew test` as the remedy for a Gradle repository
|
|
97
|
+
would be shorter and would be wrong in exactly the repositories where it is printed — plenty
|
|
98
|
+
of Gradle projects have no `test` task and a `spotlessCheck` that is not `spotlessFormat`.
|
|
99
|
+
- **Treat an unreadable marker file as a runner.** A permission error is not evidence that a
|
|
100
|
+
repository runs Gradle; it is evidence that this tool cannot see. Absent is the safe reading.
|
|
101
|
+
- **Leave `check` reporting the gate unrunnable and let `hooks run` be the truth.** Then the
|
|
102
|
+
gate a reader is told to fix is not the gate that fails, and the two disagree on the same
|
|
103
|
+
manifest — this defect, with the direction reversed.
|
|
104
|
+
|
|
105
|
+
## Evidence
|
|
106
|
+
|
|
107
|
+
`repo-05` is the first adopter with a non-npm runner and the first to declare a command for a
|
|
108
|
+
gate that is not of kind `command`. Every earlier adopter was a Node repository, which is why
|
|
109
|
+
ADR-017's own verification passed. The estate's three runners — npm, Gradle and Python — were
|
|
110
|
+
all in the repository set the whole time; only the pilot order had not reached them.
|
simt_dev/vendored/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# A rule is written per surface, and pointed at — never copied
|
|
2
|
+
|
|
3
|
+
Status: **accepted** · 2026-10-07
|
|
4
|
+
|
|
5
|
+
Affects: `simt-dev init`, `simt-dev sync`, `simt-dev check`, `simt-dev compare`,
|
|
6
|
+
`src/projection/surfaces.mjs`, `src/projection/project.mjs`, `src/compare/row.mjs`
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
`simt-dev` writes rules into a repository. Until this record it wrote them into **two** places:
|
|
11
|
+
the `AGENTS.md` generated region, and `.cursor/rules/*.mdc`. So the estate's three
|
|
12
|
+
always-on process rules — `test-gate`, `intake-questions`, `preview-postflight` — reached
|
|
13
|
+
**Cursor users and nobody else**. Every other editor got whatever happened to be in
|
|
14
|
+
`AGENTS.md`, by the accident that those editors read it.
|
|
15
|
+
|
|
16
|
+
That accident is not support. Claude Code reads `CLAUDE.md` by default and only falls back to
|
|
17
|
+
`AGENTS.md` when no `CLAUDE.md` exists anywhere above the working directory. Copilot's editor
|
|
18
|
+
and code-review surfaces read `.github/copilot-instructions.md` and
|
|
19
|
+
`.github/instructions/*.instructions.md`, which this tool wrote nowhere — while its conflict
|
|
20
|
+
scanner already *scanned* `.github/instructions/`, because a detector that reads a surface is
|
|
21
|
+
half a promise to write it. Antigravity's native surface is `.agents/rules/*.md` with a
|
|
22
|
+
required `trigger:` key it will silently discard if it does not recognise. So the three
|
|
23
|
+
repositories in the pilot had three different answers to "what rules apply here", and only
|
|
24
|
+
one of the three was written down.
|
|
25
|
+
|
|
26
|
+
The conventions were not guessed. Each was read from that vendor's own documentation, and
|
|
27
|
+
`src/projection/surfaces.mjs` records the document beside every row.
|
|
28
|
+
|
|
29
|
+
## Rulings
|
|
30
|
+
|
|
31
|
+
1. **A surface is named data.** Every editor is a row: its id, its label, the readers it
|
|
32
|
+
serves, its rule-file path template, its frontmatter, and **the document it was verified
|
|
33
|
+
against**. A row that cannot name that document does not belong in the table.
|
|
34
|
+
|
|
35
|
+
2. **A pointer is not a copy.** `CLAUDE.md` and `GEMINI.md` are symlinks to `AGENTS.md`.
|
|
36
|
+
Both vendors document the symlink. The estate contains a byte-identical `CLAUDE.md` and
|
|
37
|
+
`AGENTS.md` pair, which is the one true content duplicate in the corpus and the reason this
|
|
38
|
+
tool exists. DeepSeek's harness documents the failure from the other side: candidates that
|
|
39
|
+
are byte-identical after trimming collapse, so a symlink renders once — while **a real copy
|
|
40
|
+
that has drifted loads in full alongside** `AGENTS.md`. A copy is not a second copy of the
|
|
41
|
+
rule; it is a second rule that nobody reviews.
|
|
42
|
+
|
|
43
|
+
3. **Per-rule files are generated, not linked.** `rulePath()` writes the same body to each
|
|
44
|
+
surface under that surface's own naming. Symlink support is documented for
|
|
45
|
+
`.claude/rules/`; writing a symlink into `.agents/rules/` or `.github/instructions/` would
|
|
46
|
+
be an unverified guess carrying a mode bit. A machine-written file derived from one
|
|
47
|
+
canonical source and verified by `simt-dev check` is a build output, not duplication —
|
|
48
|
+
nobody can hand-edit one and survive a sync, which is the test the estate's own review
|
|
49
|
+
agent applies to duplicated policy text.
|
|
50
|
+
|
|
51
|
+
4. **An always-on rule emits no scoping frontmatter.** Claude documents that a rule file with
|
|
52
|
+
no `paths:` loads eagerly at session start, and carries a documented parser defect: quoted
|
|
53
|
+
values and YAML sequences in `paths:` silently fail to lazy-load, and its own documented
|
|
54
|
+
examples are not valid YAML. Emitting a scope we do not need, on an editor where a
|
|
55
|
+
malformed scope fails silently, buys nothing.
|
|
56
|
+
|
|
57
|
+
5. **The default is the global standard, and the set is always reported.** Every rule surface
|
|
58
|
+
unless the repository narrows it with `"surfaces"` in `simt-dev.json`; `--surfaces` wins
|
|
59
|
+
over the manifest. An empty list is a real choice meaning `AGENTS.md` only, not an
|
|
60
|
+
accident. Every command prints the set and **where it came from** — an invisible
|
|
61
|
+
configuration is one nobody can debug, which is the same ruling ADR-014 made about the
|
|
62
|
+
workspace.
|
|
63
|
+
|
|
64
|
+
6. **A surface row states its own failure mode.** An unrecognised frontmatter key is inert;
|
|
65
|
+
an unrecognised *value* can make an editor discard the rule with no error. So each row
|
|
66
|
+
carries `documentedKeys`, and `undocumentedKeys()` — used by the test suite and by any
|
|
67
|
+
consumer that wants the same guarantee — fails when the table emits a key its own vendor
|
|
68
|
+
does not document.
|
|
69
|
+
|
|
70
|
+
## Consequences
|
|
71
|
+
|
|
72
|
+
`simt-dev init`, `sync` and `check` write and verify up to four rule files per rule plus two
|
|
73
|
+
pointers. `compare` reports `rules N/M current` per repository, because a repository with a
|
|
74
|
+
current `AGENTS.md` and three stale rule files is now a state that can exist, and reporting
|
|
75
|
+
only the `AGENTS.md` finding would hide it.
|
|
76
|
+
|
|
77
|
+
A missing pointer is **not** a failing check. A repository that has never been adopted has no
|
|
78
|
+
`CLAUDE.md` and no `simt-dev.json`, and reddening every commit for that is the adoption
|
|
79
|
+
problem, not a defect — the same reasoning ADR-018 applied to a repository's own hooks. A
|
|
80
|
+
*mispointed* pointer is likewise not failing; a pointer simt-dev did not write, which means a
|
|
81
|
+
human's `CLAUDE.md` is in the way, **is** reported, because overwriting it silently would
|
|
82
|
+
destroy prose nobody backed up.
|
|
83
|
+
|
|
84
|
+
## Rejected
|
|
85
|
+
|
|
86
|
+
- **Keep two surfaces and call it support.** The rules a reader most needs are the ones that
|
|
87
|
+
never reached three of the four editors that read this repository's rules.
|
|
88
|
+
- **Copy `AGENTS.md` to `CLAUDE.md`.** The one true duplicate in the corpus. DeepSeek's
|
|
89
|
+
harness loads a drifted copy in full; Claude's own docs steer away from it.
|
|
90
|
+
- **Symlink every rule file to a canonical directory.** Symlink support is documented only for
|
|
91
|
+
`.claude/rules/`. A symlink into `.agents/rules/` or `.github/instructions/` would be a
|
|
92
|
+
guess with a mode bit attached.
|
|
93
|
+
- **Emit `paths:`/`globs:` on an always-on rule** and rely on it. Claude's parser defect makes
|
|
94
|
+
it a silent-failure surface for zero benefit; Antigravity discards a rule whose `trigger`
|
|
95
|
+
it does not recognise.
|
|
96
|
+
- **Guessing Antigravity's or DeepSeek's convention and moving on.** Both were read from the
|
|
97
|
+
vendors' documentation before anything was written, and the document is recorded per row so
|
|
98
|
+
a future maintainer knows what a claim rests on.
|
|
99
|
+
- **Making the default a single surface** because it is the one most repositories use. A
|
|
100
|
+
default that leaves three editors uninstructed is the defect this record exists to close.
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# ADR-021 — One surface per repo, chosen or defaulted
|
|
2
|
+
|
|
3
|
+
Status: accepted · 2026-10-07 · ruling 3 superseded by ADR-046
|
|
4
|
+
**Affects:** `simt-dev init`, `sync`, `check`, `compare`, `skills install`, `src/projection/surfaces.mjs`, `src/projection/rules.mjs`
|
|
5
|
+
|
|
6
|
+
> **Ruling 3 superseded by ADR-046.** It rested on a factual claim about Cursor — that a Cursor
|
|
7
|
+
> reader getting `.mdc` files "does not serve them", i.e. that Cursor cannot read `AGENTS.md` — and
|
|
8
|
+
> Cursor's own documentation says it can. Five repositories were loading 28,101 bytes of rule text
|
|
9
|
+
> twice. The claim had been sitting on the wrong row of simt-dev's own surface table all along.
|
|
10
|
+
> Every other ruling here stands; ruling 6 in particular is what makes ADR-046's orphan handling a
|
|
11
|
+
> note rather than a deletion.
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
ADR-020 answered a question that had been answered wrongly for months. `simt-dev init`
|
|
16
|
+
wrote `AGENTS.md` and `.cursor/rules/*.mdc` and reported that it supported the
|
|
17
|
+
estate's editors. It did not: the three always-on process rules lived only in a
|
|
18
|
+
directory only Cursor reads, so a Claude Code, Copilot or Antigravity user received
|
|
19
|
+
the core and stack rules and none of the process rules.
|
|
20
|
+
|
|
21
|
+
ADR-020 fixed that by writing one file per verified editor — four rule directories
|
|
22
|
+
and two pointer symlinks. That was correct and it was too much. A repo whose team
|
|
23
|
+
lives in one editor now acquires three rule directories and two symlinks it will
|
|
24
|
+
never open, every rule change lands four times, and `check` must be told which
|
|
25
|
+
surface to ask about or it answers a different question from the one `sync` was
|
|
26
|
+
asked.
|
|
27
|
+
|
|
28
|
+
"Support every known editor" is satisfied by knowing how to write each one and
|
|
29
|
+
writing the one that was asked for. It is not satisfied by writing them all.
|
|
30
|
+
|
|
31
|
+
## Rulings
|
|
32
|
+
|
|
33
|
+
1. **One surface per repo by default; a repo may name another.** `--surfaces` takes a
|
|
34
|
+
comma-separated list, so one, several, or none are all reachable. An empty list is
|
|
35
|
+
a real choice and means the primary region and nothing else.
|
|
36
|
+
2. **The default is opencode**, because it is the agent this tool is developed
|
|
37
|
+
against and its paths — `AGENTS.md` and `.agents/skills/` — are already this
|
|
38
|
+
workspace's. It is also the least private choice available: seven of the eight
|
|
39
|
+
readers on that row read `AGENTS.md` natively, so the default file is the one most
|
|
40
|
+
editors already read even when they were not asked for it.
|
|
41
|
+
3. **A folding surface carries its rules inside the region.** opencode documents no
|
|
42
|
+
per-rule directory, so its surface folds every rule into the one generated region.
|
|
43
|
+
Folding is `some`, not `all`: a repo that also opts into Cursor still folds, because
|
|
44
|
+
an opencode reader's only channel is `AGENTS.md` and a Cursor reader getting `.mdc`
|
|
45
|
+
files does not serve them.
|
|
46
|
+
4. **Skills follow the same rule.** One canonical skills directory per repo, `.agents/skills`,
|
|
47
|
+
which opencode, Claude Code and Antigravity all document and which the estate
|
|
48
|
+
already uses. Not four copies of every skill.
|
|
49
|
+
5. **The choice is always visible.** Every command prints the surface set and its
|
|
50
|
+
origin — flag, manifest, or global standard — because an invisible configuration is
|
|
51
|
+
one nobody can debug (ADR-014).
|
|
52
|
+
6. **A written file is not removed when a surface is dropped.** Narrowing the surface
|
|
53
|
+
set stops new files appearing; `sync` never deletes a file it once generated,
|
|
54
|
+
because a deletion is not something a projection should do silently. Removing them
|
|
55
|
+
is a human decision with a visible diff.
|
|
56
|
+
7. **A pointer belongs to the surface that needs it.** ADR-020 wrote `CLAUDE.md` and
|
|
57
|
+
`GEMINI.md` unconditionally, so narrowing the surface set left both behind — the
|
|
58
|
+
same over-writing ruling 1 exists to stop, one file down. Each pointer now names the
|
|
59
|
+
surface that requires it (`claude-md` → `claude`, `gemini-md` → `antigravity`) and
|
|
60
|
+
is planned only when that surface was selected. On the default surface simt-dev
|
|
61
|
+
writes exactly one file.
|
|
62
|
+
|
|
63
|
+
*Added after ADR-021 was first implemented: this was found by running the command
|
|
64
|
+
against the four adopted repos rather than by reading it, which is the third time
|
|
65
|
+
that has been the only way to find anything.*
|
|
66
|
+
|
|
67
|
+
## Consequences
|
|
68
|
+
|
|
69
|
+
- **Which surface a repo already has is the strongest signal of which it wants.**
|
|
70
|
+
Measured across the four pilot repos: three track `.cursor/rules/*.mdc` (3, 3 and 12
|
|
71
|
+
files) and one tracks no rules layer at all. Those three adopt `opencode,cursor`
|
|
72
|
+
because they already use Cursor; the fourth takes the bare default. A repo's own
|
|
73
|
+
tracked files answer this better than any default could.
|
|
74
|
+
|
|
75
|
+
- `simt-dev init` on a fresh repo writes one file: `AGENTS.md`.
|
|
76
|
+
- `check` and `sync` must be given the same surfaces or they ask different questions.
|
|
77
|
+
The manifest records the choice, which is why `init` and `upgrade` write it.
|
|
78
|
+
- The four repos adopted under ADR-020 carry rule files for surfaces they did not ask
|
|
79
|
+
for. Narrowing stops the next sync writing them; removing the existing ones is
|
|
80
|
+
Phase 6 work with a visible diff.
|
|
81
|
+
|
|
82
|
+
## Rejected
|
|
83
|
+
|
|
84
|
+
- **Write every verified surface by default.** Rejected: gives a single-editor repo
|
|
85
|
+
three unread directories, and makes every rule change a four-file change.
|
|
86
|
+
- **Make the default no surface at all.** Rejected: that is what the tool did before
|
|
87
|
+
ADR-020 and it is how three editors received no process rules.
|
|
88
|
+
- **Delete generated files for a dropped surface automatically.** Rejected: a
|
|
89
|
+
projection that deletes files is a projection that can lose work, and it does it
|
|
90
|
+
without a diff anyone reads.
|
|
91
|
+
- **One surface per rule, chosen by rule.** Rejected: the same rule then exists in two
|
|
92
|
+
places, which is the estate's original problem restated.
|
|
93
|
+
- **Ask on every run.** Rejected: a prompt is a hang for an agent, and the default
|
|
94
|
+
exists precisely so nothing has to be asked.
|