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.
Files changed (138) hide show
  1. python/simt_dev/__init__.py +32 -0
  2. python/simt_dev/cli.py +225 -0
  3. simt_dev/__init__.py +32 -0
  4. simt_dev/cli.py +225 -0
  5. simt_dev/vendored/CHANGELOG.md +166 -0
  6. simt_dev/vendored/bin/simt-dev.mjs +28 -0
  7. simt_dev/vendored/docs/decisions/ADR-001-push-requires-approval.md +42 -0
  8. simt_dev/vendored/docs/decisions/ADR-002-refactor-blast-radius.md +45 -0
  9. simt_dev/vendored/docs/decisions/ADR-003-test-breadth.md +46 -0
  10. simt_dev/vendored/docs/decisions/ADR-004-hook-owns-format.md +45 -0
  11. simt_dev/vendored/docs/decisions/ADR-005-file-length.md +42 -0
  12. simt_dev/vendored/docs/decisions/ADR-006-coverage-bar.md +59 -0
  13. simt_dev/vendored/docs/decisions/ADR-007-section-skeleton.md +54 -0
  14. simt_dev/vendored/docs/decisions/ADR-008-verdict-vocabulary.md +59 -0
  15. simt_dev/vendored/docs/decisions/ADR-009-skill-layout.md +48 -0
  16. simt_dev/vendored/docs/decisions/ADR-010-runner-invocation.md +56 -0
  17. simt_dev/vendored/docs/decisions/ADR-011-windows-support.md +52 -0
  18. simt_dev/vendored/docs/decisions/ADR-012-drop-windows-support.md +59 -0
  19. simt_dev/vendored/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +114 -0
  20. simt_dev/vendored/docs/decisions/ADR-014-workspace-is-configuration.md +211 -0
  21. simt_dev/vendored/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +117 -0
  22. simt_dev/vendored/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +180 -0
  23. simt_dev/vendored/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +89 -0
  24. simt_dev/vendored/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +183 -0
  25. simt_dev/vendored/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +110 -0
  26. simt_dev/vendored/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +100 -0
  27. simt_dev/vendored/docs/decisions/ADR-021-one-surface-per-repo.md +94 -0
  28. simt_dev/vendored/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +98 -0
  29. simt_dev/vendored/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +106 -0
  30. simt_dev/vendored/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +111 -0
  31. simt_dev/vendored/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +153 -0
  32. simt_dev/vendored/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +117 -0
  33. simt_dev/vendored/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +130 -0
  34. 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
  35. simt_dev/vendored/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +120 -0
  36. simt_dev/vendored/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +114 -0
  37. simt_dev/vendored/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +129 -0
  38. simt_dev/vendored/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +141 -0
  39. simt_dev/vendored/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +82 -0
  40. simt_dev/vendored/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +91 -0
  41. simt_dev/vendored/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +119 -0
  42. simt_dev/vendored/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +103 -0
  43. simt_dev/vendored/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +100 -0
  44. simt_dev/vendored/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +95 -0
  45. simt_dev/vendored/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +101 -0
  46. simt_dev/vendored/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +158 -0
  47. simt_dev/vendored/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +92 -0
  48. simt_dev/vendored/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +94 -0
  49. simt_dev/vendored/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +90 -0
  50. simt_dev/vendored/docs/decisions/ADR-044-compatibility-is-not-evidence.md +116 -0
  51. simt_dev/vendored/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +119 -0
  52. simt_dev/vendored/docs/decisions/ADR-046-one-rule-one-copy.md +140 -0
  53. simt_dev/vendored/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +141 -0
  54. simt_dev/vendored/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +150 -0
  55. simt_dev/vendored/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +153 -0
  56. 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
  57. simt_dev/vendored/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +134 -0
  58. simt_dev/vendored/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +101 -0
  59. simt_dev/vendored/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +49 -0
  60. simt_dev/vendored/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +100 -0
  61. simt_dev/vendored/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +98 -0
  62. simt_dev/vendored/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +160 -0
  63. simt_dev/vendored/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +115 -0
  64. simt_dev/vendored/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +148 -0
  65. simt_dev/vendored/docs/decisions/README.md +530 -0
  66. simt_dev/vendored/graph/skills.graph.json +1622 -0
  67. simt_dev/vendored/package.json +43 -0
  68. simt_dev/vendored/rules/core.md +76 -0
  69. simt_dev/vendored/rules/mdc/intake-questions.mdc +50 -0
  70. simt_dev/vendored/rules/mdc/preview-postflight.mdc +61 -0
  71. simt_dev/vendored/rules/mdc/slice-commit.mdc +52 -0
  72. simt_dev/vendored/rules/mdc/test-gate.mdc +54 -0
  73. simt_dev/vendored/rules/stacks/android.md +63 -0
  74. simt_dev/vendored/rules/stacks/cloudflare-workers.md +65 -0
  75. simt_dev/vendored/rules/stacks/connectiq.md +62 -0
  76. simt_dev/vendored/rules/stacks/container.md +31 -0
  77. simt_dev/vendored/rules/stacks/jvm.md +54 -0
  78. simt_dev/vendored/rules/stacks/node.md +45 -0
  79. simt_dev/vendored/rules/stacks/python.md +48 -0
  80. simt_dev/vendored/rules/stacks/react-native.md +58 -0
  81. simt_dev/vendored/rules/stacks/react-web.md +61 -0
  82. simt_dev/vendored/rules/stacks/typescript.md +21 -0
  83. simt_dev/vendored/rules/stacks/uv.md +32 -0
  84. simt_dev/vendored/src/argv.mjs +116 -0
  85. simt_dev/vendored/src/ci/dependabot.mjs +268 -0
  86. simt_dev/vendored/src/ci/workflow.mjs +360 -0
  87. simt_dev/vendored/src/commands/ci.mjs +285 -0
  88. simt_dev/vendored/src/commands/compare.mjs +195 -0
  89. simt_dev/vendored/src/commands/graph.mjs +73 -0
  90. simt_dev/vendored/src/commands/help.mjs +108 -0
  91. simt_dev/vendored/src/commands/hooks.mjs +471 -0
  92. simt_dev/vendored/src/commands/index.mjs +241 -0
  93. simt_dev/vendored/src/commands/init.mjs +632 -0
  94. simt_dev/vendored/src/commands/mcp.mjs +63 -0
  95. simt_dev/vendored/src/commands/rules.mjs +374 -0
  96. simt_dev/vendored/src/commands/skills.mjs +647 -0
  97. simt_dev/vendored/src/commands/skillsinstall.mjs +515 -0
  98. simt_dev/vendored/src/commands/sync.mjs +331 -0
  99. simt_dev/vendored/src/commands/upgrade.mjs +202 -0
  100. simt_dev/vendored/src/compare/discover.mjs +212 -0
  101. simt_dev/vendored/src/compare/hooksstate.mjs +159 -0
  102. simt_dev/vendored/src/compare/row.mjs +299 -0
  103. simt_dev/vendored/src/decisions/index.mjs +536 -0
  104. simt_dev/vendored/src/graph/load.mjs +541 -0
  105. simt_dev/vendored/src/hooks/capability.mjs +129 -0
  106. simt_dev/vendored/src/hooks/dispatcher.mjs +268 -0
  107. simt_dev/vendored/src/hooks/gates.mjs +144 -0
  108. simt_dev/vendored/src/hooks/resolve.mjs +194 -0
  109. simt_dev/vendored/src/hooks/shim.mjs +140 -0
  110. simt_dev/vendored/src/hooks/where.mjs +118 -0
  111. simt_dev/vendored/src/identity.mjs +218 -0
  112. simt_dev/vendored/src/locate.mjs +75 -0
  113. simt_dev/vendored/src/mcp/server.mjs +167 -0
  114. simt_dev/vendored/src/mcp/tools.mjs +410 -0
  115. simt_dev/vendored/src/name.mjs +95 -0
  116. simt_dev/vendored/src/output/index.mjs +96 -0
  117. simt_dev/vendored/src/projection/baselines.mjs +209 -0
  118. simt_dev/vendored/src/projection/conflicts.mjs +205 -0
  119. simt_dev/vendored/src/projection/manifest.mjs +478 -0
  120. simt_dev/vendored/src/projection/markers.mjs +349 -0
  121. simt_dev/vendored/src/projection/mdc.mjs +327 -0
  122. simt_dev/vendored/src/projection/project.mjs +504 -0
  123. simt_dev/vendored/src/projection/rules.mjs +640 -0
  124. simt_dev/vendored/src/projection/surfaces.mjs +398 -0
  125. simt_dev/vendored/src/projection/targets.mjs +221 -0
  126. simt_dev/vendored/src/resolve/fingerprint.mjs +553 -0
  127. simt_dev/vendored/src/resolve/near.mjs +61 -0
  128. simt_dev/vendored/src/resolve/resolve.mjs +457 -0
  129. simt_dev/vendored/src/resolve/source.mjs +105 -0
  130. simt_dev/vendored/src/toolchain.mjs +202 -0
  131. simt_dev/vendored/src/tsconfig.mjs +314 -0
  132. simt_dev/vendored/src/upgrade/plan.mjs +203 -0
  133. simt_dev/vendored/src/workspace.mjs +289 -0
  134. simt_dev-0.2.0.dist-info/METADATA +150 -0
  135. simt_dev-0.2.0.dist-info/RECORD +138 -0
  136. simt_dev-0.2.0.dist-info/WHEEL +4 -0
  137. simt_dev-0.2.0.dist-info/entry_points.txt +2 -0
  138. 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.
@@ -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.