simt-dev 0.2.0__tar.gz → 0.2.1__tar.gz

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 (152) hide show
  1. simt_dev-0.2.1/CHANGELOG.md +332 -0
  2. {simt_dev-0.2.0 → simt_dev-0.2.1}/PKG-INFO +5 -5
  3. {simt_dev-0.2.0 → simt_dev-0.2.1}/README.md +4 -4
  4. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +27 -0
  5. simt_dev-0.2.1/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
  6. simt_dev-0.2.1/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
  7. simt_dev-0.2.1/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
  8. simt_dev-0.2.1/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
  9. simt_dev-0.2.1/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
  10. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/README.md +17 -0
  11. {simt_dev-0.2.0 → simt_dev-0.2.1}/package.json +1 -2
  12. {simt_dev-0.2.0 → simt_dev-0.2.1}/pyproject.toml +0 -2
  13. simt_dev-0.2.1/rules/stacks/dart.md +38 -0
  14. simt_dev-0.2.1/rules/stacks/deno.md +36 -0
  15. simt_dev-0.2.1/rules/stacks/dotnet.md +35 -0
  16. simt_dev-0.2.1/rules/stacks/elixir.md +39 -0
  17. simt_dev-0.2.1/rules/stacks/go.md +34 -0
  18. simt_dev-0.2.1/rules/stacks/ruby.md +36 -0
  19. simt_dev-0.2.1/rules/stacks/rust.md +34 -0
  20. simt_dev-0.2.1/rules/stacks/swift.md +37 -0
  21. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/argv.mjs +35 -6
  22. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/ci/dependabot.mjs +1 -1
  23. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/ci/workflow.mjs +33 -11
  24. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/graph.mjs +13 -4
  25. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/help.mjs +22 -4
  26. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/hooks.mjs +173 -57
  27. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/index.mjs +53 -45
  28. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/init.mjs +89 -13
  29. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/rules.mjs +54 -3
  30. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/skills.mjs +143 -15
  31. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/skillsinstall.mjs +3 -3
  32. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/sync.mjs +153 -11
  33. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/compare/hooksstate.mjs +96 -9
  34. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/compare/row.mjs +34 -5
  35. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/decisions/index.mjs +21 -6
  36. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/graph/load.mjs +125 -14
  37. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/hooks/capability.mjs +48 -4
  38. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/hooks/dispatcher.mjs +50 -5
  39. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/hooks/gates.mjs +0 -2
  40. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/hooks/resolve.mjs +24 -25
  41. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/hooks/shim.mjs +29 -5
  42. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/identity.mjs +0 -11
  43. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/mcp/tools.mjs +2 -2
  44. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/output/index.mjs +8 -7
  45. simt_dev-0.2.1/src/package-version.mjs +74 -0
  46. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/projection/conflicts.mjs +56 -5
  47. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/projection/manifest.mjs +247 -25
  48. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/projection/mdc.mjs +4 -13
  49. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/projection/project.mjs +1 -1
  50. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/projection/rules.mjs +21 -8
  51. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/projection/targets.mjs +53 -7
  52. simt_dev-0.2.1/src/resolve/eval.mjs +64 -0
  53. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/resolve/fingerprint.mjs +1 -0
  54. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/resolve/near.mjs +26 -7
  55. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/resolve/resolve.mjs +1 -1
  56. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/resolve/source.mjs +1 -1
  57. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/workspace.mjs +0 -9
  58. simt_dev-0.2.0/CHANGELOG.md +0 -166
  59. simt_dev-0.2.0/graph/skills.graph.json +0 -1622
  60. {simt_dev-0.2.0 → simt_dev-0.2.1}/.gitignore +0 -0
  61. {simt_dev-0.2.0 → simt_dev-0.2.1}/LICENSE +0 -0
  62. {simt_dev-0.2.0 → simt_dev-0.2.1}/bin/simt-dev.mjs +0 -0
  63. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-001-push-requires-approval.md +0 -0
  64. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-002-refactor-blast-radius.md +0 -0
  65. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-003-test-breadth.md +0 -0
  66. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-004-hook-owns-format.md +0 -0
  67. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-005-file-length.md +0 -0
  68. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-006-coverage-bar.md +0 -0
  69. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-007-section-skeleton.md +0 -0
  70. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-008-verdict-vocabulary.md +0 -0
  71. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-009-skill-layout.md +0 -0
  72. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-010-runner-invocation.md +0 -0
  73. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-011-windows-support.md +0 -0
  74. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-012-drop-windows-support.md +0 -0
  75. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +0 -0
  76. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-014-workspace-is-configuration.md +0 -0
  77. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +0 -0
  78. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +0 -0
  79. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +0 -0
  80. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +0 -0
  81. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +0 -0
  82. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-021-one-surface-per-repo.md +0 -0
  83. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +0 -0
  84. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +0 -0
  85. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +0 -0
  86. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +0 -0
  87. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +0 -0
  88. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +0 -0
  89. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +0 -0
  90. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +0 -0
  91. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +0 -0
  92. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +0 -0
  93. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +0 -0
  94. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -0
  95. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +0 -0
  96. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +0 -0
  97. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +0 -0
  98. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +0 -0
  99. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +0 -0
  100. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +0 -0
  101. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +0 -0
  102. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +0 -0
  103. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +0 -0
  104. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +0 -0
  105. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-044-compatibility-is-not-evidence.md +0 -0
  106. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +0 -0
  107. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-046-one-rule-one-copy.md +0 -0
  108. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +0 -0
  109. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +0 -0
  110. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +0 -0
  111. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-050-a-byte-count-is-not-a-region-and-updated-is-not-a-change-of-rules.md +0 -0
  112. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +0 -0
  113. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +0 -0
  114. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +0 -0
  115. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +0 -0
  116. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +0 -0
  117. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +0 -0
  118. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +0 -0
  119. {simt_dev-0.2.0 → simt_dev-0.2.1}/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +0 -0
  120. {simt_dev-0.2.0 → simt_dev-0.2.1}/python/simt_dev/__init__.py +0 -0
  121. {simt_dev-0.2.0 → simt_dev-0.2.1}/python/simt_dev/cli.py +0 -0
  122. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/core.md +0 -0
  123. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/mdc/intake-questions.mdc +0 -0
  124. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/mdc/preview-postflight.mdc +0 -0
  125. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/mdc/slice-commit.mdc +0 -0
  126. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/mdc/test-gate.mdc +0 -0
  127. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/stacks/android.md +0 -0
  128. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/stacks/cloudflare-workers.md +0 -0
  129. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/stacks/connectiq.md +0 -0
  130. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/stacks/container.md +0 -0
  131. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/stacks/jvm.md +0 -0
  132. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/stacks/node.md +0 -0
  133. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/stacks/python.md +0 -0
  134. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/stacks/react-native.md +0 -0
  135. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/stacks/react-web.md +0 -0
  136. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/stacks/typescript.md +0 -0
  137. {simt_dev-0.2.0 → simt_dev-0.2.1}/rules/stacks/uv.md +0 -0
  138. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/ci.mjs +0 -0
  139. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/compare.mjs +0 -0
  140. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/mcp.mjs +0 -0
  141. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/commands/upgrade.mjs +0 -0
  142. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/compare/discover.mjs +0 -0
  143. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/hooks/where.mjs +0 -0
  144. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/locate.mjs +0 -0
  145. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/mcp/server.mjs +0 -0
  146. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/name.mjs +0 -0
  147. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/projection/baselines.mjs +0 -0
  148. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/projection/markers.mjs +0 -0
  149. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/projection/surfaces.mjs +0 -0
  150. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/toolchain.mjs +0 -0
  151. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/tsconfig.mjs +0 -0
  152. {simt_dev-0.2.0 → simt_dev-0.2.1}/src/upgrade/plan.mjs +0 -0
@@ -0,0 +1,332 @@
1
+ # Changelog
2
+
3
+ All notable changes to this package are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.1] — 2026-10-13
10
+
11
+ ### `init` writes in place; the backup tag and adopt branch are opt-in
12
+
13
+ `simt init --yes` used to always tag the pre-adoption commit and branch onto a dedicated
14
+ adopt branch before writing. The safety argument was sound, but consumers kept reporting
15
+ the branch switch as a surprise — a well-announced surprise is still a surprise.
16
+
17
+ The default now writes in place on the branch you already have: nothing is tagged, nothing
18
+ is branched, and the run says so in an `in place` line. The reviewable diff survives —
19
+ `git diff` shows the whole adoption, and discarding it is a checkout and a clean.
20
+
21
+ The old behaviour moves behind `--backup`: `simt init --yes --backup` tags the pre-adoption
22
+ commit and branches as before, for readers who want the named undo path. ADR-063 records
23
+ the decision and the one behaviour it invalidates: consumers who scripted against the old
24
+ default add `--backup`.
25
+
26
+ ### A fresh adoption seeds the gates it can run — and only those
27
+
28
+ The eight common ecosystems outside the JavaScript world — Rust, Go, .NET, Swift, Deno,
29
+ Ruby, Dart and Elixir — were recognised by the fingerprint and served by nothing: no
30
+ profile selected them, no gate was proposed for them. A fresh Rust repository received the
31
+ core essay and a manifest that runs nothing, while the ecosystems that emit stack tags got
32
+ the full treatment.
33
+
34
+ Each now has a profile that selects on its own `has-*` fact, so one marker file reaches
35
+ both the rules the repository reads and the gates its commits run. Maven's profile predates
36
+ this; its row is gates-only, wrapper-first, like `gradlew`.
37
+
38
+ `simt init` now seeds the canonical commands for a detected ecosystem — `cargo fmt
39
+ --check`, `go vet ./...`, `dotnet test`, and the rest of the table — into the manifest's
40
+ gates, but only where a probe found the tool: a binary on PATH, or a file in the repository
41
+ for the Maven wrapper. A marker says the ecosystem is here; the probe says its program can
42
+ run. A declared gate that cannot run fails closed on every commit, so the seed fires only
43
+ when both are true — and where the probe fails, the gate is dropped with a remedy that
44
+ names the install instead of blocking the first commit.
45
+
46
+ A seed is a default: a command the repository already declared is never re-seeded, and an
47
+ npm runner keeps its own scripts, because `package.json` is the repository's own answer to
48
+ "how do I test and format". ADR-062 rules the shape of all of it.
49
+
50
+ ### The resolver is measured against a gold corpus
51
+
52
+ `simt skills eval` scores the skill resolver against a corpus of real tasks × real
53
+ repositories, each case carrying a gold list of the skills it should select and a list
54
+ of the ones it must not. It reports precision and recall, prints a PASS/FAIL line per
55
+ case naming the skills it missed or selected that it should not have, and exits
56
+ non-zero on a regression — so it can be run from CI the way the gate is run.
57
+
58
+ The scoring lives in `src/resolve/eval.mjs`, which the command and
59
+ `test/eval.test.mjs` both call, so a test cannot drift from what the command measures.
60
+ The corpus lives under `test/` because test data does not ship in the package: an
61
+ installed copy names the path it would have read and says so instead of scoring the
62
+ author's.
63
+
64
+ ### The skill registry is workspace data; the package ships none of it
65
+
66
+ The npm tarball and the wheel carried `graph/skills.graph.json` — the directory
67
+ of skill sources for one workspace, naming the repositories whose bodies it
68
+ resolves. Every installed copy served that catalogue to readers who have none
69
+ of those skills, and `skills install` pointed at paths that exist only in the
70
+ workspace that wrote them. ADR-061 rules the registry out of the published set:
71
+ it is data about one workspace, not content of the package.
72
+
73
+ The default registry path now resolves in tiers, and every tier is file
74
+ existence plus configuration — nothing a pull request cannot diff:
75
+
76
+ 1. an explicit path, the positional of `simt graph lint`,
77
+ 2. the package's own `graph/skills.graph.json` — present in a source checkout,
78
+ absent from an installed copy,
79
+ 3. the member of a configured workspace that holds one — naming the workspace
80
+ it came from, and refusing when several members hold one, because picking
81
+ would be a guess.
82
+
83
+ An installed copy in a workspace with no registry says so, and names the three
84
+ ways to configure one (`--workspace`, `SIMT_DEV_WORKSPACE`, the user config
85
+ file) instead of failing on a path it does not have. `simt graph lint`'s
86
+ no-registry remedy names the path explicitly, because its default is the
87
+ registry it lints.
88
+
89
+ The provenance digest stops walking `graph/` for the same reason it no longer
90
+ leaves the package: the digest covers what a published copy ships, and the
91
+ registry is no longer among it.
92
+
93
+ ### No published file may name a repository from this estate
94
+
95
+ The decision records ship in all three channels — npm, the wheel and the sdist — so a
96
+ private repository name written into one is published to three registries. That happened:
97
+ the records were pseudonymised once, across 28 files, and nothing stopped the next one. The
98
+ sweep's own commit says why, and it is the right reason: the redaction it used runs on the
99
+ way *out*, at print time, and the bytes on disk are what get published.
100
+
101
+ `test/shipped.test.mjs` now fails on an estate name anywhere in a published file, and
102
+ asserts its own scope against `package.json` in **both** directions — a directory added to
103
+ `files` without being guarded fails, and so does one guarded that nothing publishes, because
104
+ a guard nobody can fail is one somebody eventually deletes. It is scoped to the published
105
+ set rather than a hand-written list for the same reason: the sweep took 28 files to undo by
106
+ hand, and the guard should not need undoing at all.
107
+
108
+ Names are not removed, they are described. The records that use a repository as *evidence* —
109
+ a table of how three repositories answer the same question — carry stable `repo-NN` labels,
110
+ so the comparison survives. Everything else is better off naming what the repository *is*:
111
+ "a Gradle module with no wrapper", "a repository with no package.json". That reads to
112
+ someone outside the estate without a lookup table they do not have.
113
+
114
+ The one place estate names remain is `docs/reviews/`, which is not published by either
115
+ channel. A review whose subject is fourteen repositories cannot pseudonymise them without
116
+ becoming a document about `repo-01` that never existed.
117
+
118
+ ### Four shipped strings named a command this package does not install
119
+
120
+ `0.2.0` installs one binary, `simt`, and `package.json`'s `bin` map says so. Four
121
+ places still said `simt-dev`, which a reader types and is met with `unknown command`:
122
+
123
+ - the default remedy on every bad invocation, `run: simt-dev --help` — so the advice
124
+ printed after a bad command could not be followed
125
+ - the usage line, `usage: simt-dev <command>`, sitting above a command list that spelled
126
+ the same tool `simt`
127
+ - `.github/dependabot.yml`'s header, which said `Yours from here: simt-dev` while the
128
+ gate beside it said `Yours from here: simt`
129
+ - the hook shim, whose own resolution list said `simt-dev on PATH` directly above the
130
+ `command -v simt` that does the looking
131
+
132
+ `test/naming.test.mjs` was supposed to prevent this and did not: its rule required a
133
+ verb after the project name, and none of the four is followed by one. It now also
134
+ catches a flag, a `<placeholder>`, the end of a line, and a program name in a
135
+ resolution list — while still accepting the sentences where the project is the
136
+ subject, which is the spelling that is correct. The two `.github` templates get their
137
+ own assertion, because the fourth of these was not a wrong word in a sentence; it was
138
+ two templates disagreeing with each other in the same position.
139
+
140
+ ### Fixed
141
+
142
+ - `hooks install` removes a shim whose gate was dropped from the manifest instead of leaving it to run nothing. A repository that removed its last gate was refused outright, with a remedy telling it to put the gate back; only un-routing git was left to the reader. install also no longer chains a hook to itself.
143
+
144
+ - **Dead exports and duplicated frontmatter, with a test that stops them returning (S21).**
145
+ Eight functions were exported and called by nothing (`gateKind`, `gateWhy`, `gateAdr`,
146
+ `resetWorkspace`, `tracksIdentity`, `isTty`, `surfaceFrontmatter`, `workspaceOrNull`),
147
+ three `__internals` testing seams had no importer, and thirteen more bindings were
148
+ exported while being used only inside their own file — `isExecutable` among them, which
149
+ a first pass through this list wrongly recorded as dead and which its own callers
150
+ proved otherwise. `KNOWN_SUBS` was a hand-kept second copy of the dispatch table, and
151
+ had already drifted: it carried `doctor`, a bare command the group lookup can never
152
+ reach. `mdcFrontmatter` was a second construction of frontmatter the writer now derives
153
+ itself, with a test asserting the copy rather than the behaviour.
154
+
155
+ - **"Did you mean" offered the shortest fragment first, and a misspelt skill was
156
+ suggested for a correctly spelled one (S22).** `nearMatches` had no test at all.
157
+ Both directions of substring scored 0 — the same score as an exact match — so the
158
+ alphabetical tiebreak decided the order, and `skills graphs` suggested `graph`
159
+ before `graphs`. Worse, the containment test had no length floor, so `test` matched
160
+ the candidate `e` and the message read `did you mean e, test-writer?`, which an
161
+ agent has every reason to install. A prefix now scores 1 rather than 0, a fragment
162
+ counts only when it is at least half the query, and the edit bound scales with the
163
+ query's length — three edits is a typo in `slice-commmit` and is most of a
164
+ four-character query.
165
+
166
+ - **The conflict scanner skipped two of the five rule surfaces, and one pointer file
167
+ (S22).** `rulePaths` enumerated filenames by hand, so `.claude/rules/` and
168
+ `.agents/rules/` — added to the surface table after the list was written — were
169
+ never scanned, and a hand-written rule contradicting a generated one there was
170
+ invisible. A conflict detector that only looks where it was pointed looks nowhere
171
+ new. The scan list is now derived from the surface table and the pointer table.
172
+
173
+ ## [0.2.0] — 2026-10-11
174
+
175
+ ### Publishable from PyPI and Homebrew as well as npm
176
+
177
+ simt-dev is a *repository* tool, and the people who adopt a repository are not
178
+ all Node developers. Somebody running `simt init` inside a repository with no
179
+ `package.json` has no reason to install a package manager, and a Homebrew user
180
+ has no reason to install Node to install a tool that needs Node.
181
+
182
+ So it now publishes to three channels, from one version number:
183
+
184
+ ```sh
185
+ npm i -D simt-dev # a tarball of .mjs files
186
+ pip install simt-dev # a wheel carrying those same bytes
187
+ brew install QuazarMedia/tap/simt-dev
188
+ ```
189
+
190
+ The wheel is not a wrapper around `npm install`. It carries the tool's own
191
+ source, and `scripts/verify-wheel.mjs` builds both artefacts and hashes every
192
+ file in both — a release fails when a single byte differs between them. That
193
+ check exists because the obvious defect in this arrangement is the wheel and the
194
+ tarball quietly shipping different code.
195
+
196
+ `simt` is the one command, on every channel. Where node comes from is
197
+ `SIMT_DEV_NODE`, then the bundled node from `pip install "simt-dev[node]"`,
198
+ then `node` on `PATH` — and if none of those resolve you get the remedy rather
199
+ than a placeholder path. There are no required dependencies on any channel.
200
+ *(ADR-058)*
201
+
202
+ The channels are published in a fixed order rather than in parallel, and the
203
+ reason is not taste: the Homebrew formula downloads the npm tarball at install
204
+ time, so a formula naming a version still sitting in npm's staging area
205
+ installs a 404. Both jobs fail with that stated, and are run again after
206
+ `npm stage approve`.
207
+
208
+ ## [0.1.1] — 2026-10-11
209
+
210
+ ### A foreign hook on an event this repo does not gate is chained
211
+
212
+ `0.1.0` chained the wrong set of hooks and is worth replacing rather than
213
+ recommending. It chained on the event *vocabulary* — anything that was not
214
+ `pre-commit` or `pre-push` — when the question worth asking is whether **this
215
+ repository** declares a gate for that event.
216
+
217
+ The difference is a repository whose `pre-push` is `git-lfs`. `pre-push` is an
218
+ event this tool gates in general, so `0.1.0` displaced that hook; and because
219
+ that repository declares no `pre-push` gate there was no shim for the event
220
+ either, so git would have found no `pre-push` hook at all. LFS objects would have
221
+ stopped being uploaded, with nothing to notice it — the exact silent failure
222
+ chaining exists to prevent, reached by fixing the wrong half of it.
223
+
224
+ `0.1.1` asks the manifest. An event the repo declares a gate for is left to
225
+ `gates.commands`, where the hook runs as a gate and its verdict appears in the
226
+ report; every other foreign hook is chained. That also picks up a static
227
+ site's Lighthouse pre-push, which declares no gates at all.
228
+ *(ADR-018, as amended)*
229
+
230
+ Upgrade with `npm i -D simt-dev@0.1.1`, then `simt hooks install --yes`. Any
231
+ repository already routed with `0.1.0` should re-run it: the shims it writes
232
+ will differ where a gated event was previously displaced.
233
+
234
+ ## [0.1.0] — 2026-10-11
235
+
236
+ The first release with real contents in it. `1.0.0` was an empty reservation of the name
237
+ and `0.0.0-stage` was its rehearsal; neither shipped a byte anyone could run, so this entry
238
+ is the whole history. The decisions behind it are readable in the package under
239
+ `docs/decisions/`, and from any checkout with `simt rules why <id>`.
240
+
241
+ ### The command is `simt`
242
+
243
+ The package is `simt-dev` and the command is `simt`. They are not the same thing and were
244
+ never required to be: the command appears only in text the tool itself writes, and that
245
+ text migrates on the next write. The package name, the `.simt-hooks/` directory and the
246
+ `simt-dev-check.yml` gate already exist in every repository that adopted this tool, and
247
+ they did not move. The manifest and the region markers did — see below.
248
+
249
+ ### The manifest is `simt.json`
250
+
251
+ The manifest was called `simt-dev.json`; it is now `simt.json`. The move is a
252
+ read-both/write-new, so nothing in an adopting repository has to be done:
253
+
254
+ - Every read takes `simt.json` if it exists, else `simt-dev.json`, so a repository that
255
+ has not written since the upgrade is still seen as adopted.
256
+ - Every write goes to `simt.json`, and a lone `simt-dev.json` is moved, not shadowed.
257
+ - The old name keeps working until the first `init` or manifest write in each repository,
258
+ and that write closes it. *(ADR-056)*
259
+
260
+ ### The region markers are `simt:`
261
+
262
+ The generated region was stamped `<!-- simt-dev:begin -->` / `<!-- simt-dev:end -->` — and
263
+ so was the provenance header inside it; every one of those lines is now `simt:`. The move
264
+ is the same read-both/write-new as the manifest:
265
+
266
+ - Every read accepts either spelling, so a repository that has not written since the
267
+ upgrade is still seen as adopted.
268
+ - Every write emits `simt:` only, so the first `sync` in each repository migrates its
269
+ regions in place, and that write closes the window.
270
+ - A file holding two regions — one of each spelling — is a state only a hand edit can
271
+ make, and it is reported malformed rather than merged. *(ADR-056)*
272
+
273
+ ### A hook you already had keeps running
274
+
275
+ `hooks install` points `core.hooksPath` at `.simt-hooks/`, and git reads **every** hook from
276
+ that one directory — not just the two events this tool gates. So adopting a repository that
277
+ had its own `post-commit`, `post-checkout` or `post-merge` would have quietly stopped them:
278
+ nothing fails, nothing is logged, and the hook is still on disk looking intact.
279
+
280
+ Those hooks are now **chained**. Install writes a shim for them that runs the repository's
281
+ own hook and forwards git's arguments, so nothing is lost and the reason is in the file. If
282
+ the hook is gone, the shim refuses and names the path instead of passing quietly. A hook on
283
+ an event this tool *does* gate (`pre-commit`, `pre-push`) is deliberately not chained —
284
+ declare it under `gates.commands` so the dispatcher runs it and its verdict appears in the
285
+ gate report. *(ADR-018, as amended)*
286
+
287
+ ### What a first release has to say out loud
288
+
289
+ These are the behaviours a reader is most likely to trip over. Each one is deliberate,
290
+ and each has a numbered decision record behind it — but a behaviour nobody can discover
291
+ without reading the source is not a documented one.
292
+
293
+ - **`sync` and `init` write nothing without `--yes`.** Both print a plan and exit. `sync`
294
+ exits **1** when the plan is non-empty, so bare `sync` doubles as a check. Neither ever
295
+ commits or pushes. *(ADR-035, ADR-036)*
296
+ - **`upgrade` moves a pin and nothing else.** It never adopts a repository, never repairs
297
+ one, and never rewrites a manifest. Every consumer moves to the same version as one set.
298
+ - **A pre-commit hook with no reachable binary fails closed.** The refusal is the correct
299
+ behaviour, not a bug; the remedy it prints is the command that obtains one.
300
+ *(ADR-010, ADR-018)*
301
+ - **A gate whose tool is not installed is dropped, with a remedy naming the package.**
302
+ It is never declared and silently unrunnable. *(ADR-017)*
303
+ - **Two rules are never loaded twice.** Each rule projects to exactly one place, chosen by
304
+ what the harness actually reads. *(ADR-021, ADR-046)*
305
+ - **macOS and Linux only.** Windows `npm i` fails with npm's `EBADPLATFORM`, which npm
306
+ will not let a package explain; use WSL2. See the platform section of the README.
307
+ *(ADR-012)*
308
+
309
+ ### Packaging
310
+
311
+ - Licence set to Apache-2.0; the `LICENSE` file is the canonical, unmodified text.
312
+ - `files` no longer lists `skills`, `agents`, `tooling` or `hooks`, none of which exist,
313
+ and no longer ships `docs/plans`. The tarball is the tool and the decisions behind it,
314
+ and nothing else.
315
+ - The package `description` no longer names an internal ecosystem. It is the first line
316
+ anybody outside the project reads.
317
+ - Unscoped, so the install command is `npm i -D simt-dev` rather than a name under an
318
+ organisation. The `@simt` and `@quazarmedia` scopes were both free; an unscoped name is
319
+ the one a reader can guess.
320
+
321
+ ### Notes for anyone upgrading a hook
322
+
323
+ A shim installed by an earlier version looks for `SIMT_DEV_BIN`, then for
324
+ `node_modules/simt-dev/bin/simt-dev.mjs`, then for a program named `simt` on `PATH`. The
325
+ last of those is new, so a machine that installed the package before this release and
326
+ never ran `simt hooks install` again will find nothing until the dependency is refreshed
327
+ and `simt hooks install --yes` is re-run. That is a real upgrade step and it is called out
328
+ here rather than discovered.
329
+
330
+ [0.2.0]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.2.0
331
+ [0.1.1]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.1.1
332
+ [0.1.0]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simt-dev
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Shared agent rules, skills and git hooks for a repository: one command adopts a repo, and every gate it declares is probed before it is written.
5
5
  Project-URL: Homepage, https://github.com/QuazarMedia/simt-dev
6
6
  Project-URL: Repository, https://github.com/QuazarMedia/simt-dev
@@ -53,13 +53,13 @@ Requires **Node >= 22.20.0** (the PyPI install can supply it, see below).
53
53
 
54
54
  ```sh
55
55
  # pinned, per repository - what CI and a teammate both get
56
- npm i -D simt-dev@0.2.0
56
+ npm i -D simt-dev@0.2.1
57
57
 
58
58
  # or globally, if you would rather not add a dependency
59
- npm i -g simt-dev@0.2.0
59
+ npm i -g simt-dev@0.2.1
60
60
 
61
61
  # or with no install at all, for a one-off
62
- npx -y simt-dev@0.2.0 check
62
+ npx -y simt-dev@0.2.1 check
63
63
  ```
64
64
 
65
65
  ### From PyPI
@@ -111,7 +111,7 @@ simt compare # what changed across every repository that adopted it
111
111
  | Path | What |
112
112
  | --- | --- |
113
113
  | `AGENTS.md` | the projection, between `<!-- simt:begin -->` and `<!-- simt:end -->` (renamed from `simt-dev:`; the old markers are still read, and a region migrates on its next write) |
114
- | `simt.json` | your manifest: which gates to declare, which substitutions to fill (renamed from `simt-dev.json`; the old name is still read, and moves on the next write) |
114
+ | `simt.json` | your manifest: which gates to declare, which substitutions to fill |
115
115
  | `.cursor/rules/*.mdc`, `.claude/rules/*.md`, `.agents/rules/*.md`, `.github/instructions/*.instructions.md` | one file per rule, written only for the harnesses that read their own directory |
116
116
  | `.editorconfig`, `.gitattributes` | baseline regions |
117
117
  | `.simt-hooks/` and `core.hooksPath` | the shared dispatcher, fail-closed |
@@ -23,13 +23,13 @@ Requires **Node >= 22.20.0** (the PyPI install can supply it, see below).
23
23
 
24
24
  ```sh
25
25
  # pinned, per repository - what CI and a teammate both get
26
- npm i -D simt-dev@0.2.0
26
+ npm i -D simt-dev@0.2.1
27
27
 
28
28
  # or globally, if you would rather not add a dependency
29
- npm i -g simt-dev@0.2.0
29
+ npm i -g simt-dev@0.2.1
30
30
 
31
31
  # or with no install at all, for a one-off
32
- npx -y simt-dev@0.2.0 check
32
+ npx -y simt-dev@0.2.1 check
33
33
  ```
34
34
 
35
35
  ### From PyPI
@@ -81,7 +81,7 @@ simt compare # what changed across every repository that adopted it
81
81
  | Path | What |
82
82
  | --- | --- |
83
83
  | `AGENTS.md` | the projection, between `<!-- simt:begin -->` and `<!-- simt:end -->` (renamed from `simt-dev:`; the old markers are still read, and a region migrates on its next write) |
84
- | `simt.json` | your manifest: which gates to declare, which substitutions to fill (renamed from `simt-dev.json`; the old name is still read, and moves on the next write) |
84
+ | `simt.json` | your manifest: which gates to declare, which substitutions to fill |
85
85
  | `.cursor/rules/*.mdc`, `.claude/rules/*.md`, `.agents/rules/*.md`, `.github/instructions/*.instructions.md` | one file per rule, written only for the harnesses that read their own directory |
86
86
  | `.editorconfig`, `.gitattributes` | baseline regions |
87
87
  | `.simt-hooks/` and `core.hooksPath` | the shared dispatcher, fail-closed |
@@ -72,6 +72,33 @@ One manifest. Three answers. The one I would have shipped if I had only run `che
72
72
  every file (`ENOTDIR`) and classified every Gradle repository in the estate as having no
73
73
  runner at all — a wrong answer from an evidence reader, which is worse than no answer
74
74
  because the caller cannot tell them apart.
75
+ 8. **A declared command is a shell command line, not an argv.** *(S17, 2026-10-12)*
76
+
77
+ It was split on whitespace and handed to `execFile` as an argv. That made a declaration
78
+ a repository would plausibly write run the wrong thing rather than fail: a flag whose
79
+ value contains a space became two arguments, and the quotes in a compound line were
80
+ passed to the program as literal characters. There was no way to express a quoted
81
+ argument at all, and nothing said so.
82
+
83
+ So `gates.commands[name]` is one line, run through `sh -c`, and everything the
84
+ repository wrote in it survives — quotes, pipes, `&&`, a path with a space. This is the
85
+ same line the repository would type into its own shell, which is the only definition of
86
+ "what this repository means by that" that does not require a parser here.
87
+
88
+ **The trust boundary is the repository, and it is not a sandbox.** The line comes from
89
+ the repository's own manifest, which it can read and change, and it runs on its author's
90
+ own machine when that author commits. `sh -c` adds no privilege: the same person who
91
+ wrote the line could run it. What `sh -c` must not do is apply to anything this tool
92
+ resolved itself — a formatter path out of `node_modules/.bin` stays an argv, because
93
+ handing it to a shell would change who decided its arguments for no gain. The two
94
+ sources are separate branches in the dispatcher rather than a flag, so the distinction
95
+ cannot be forgotten by a caller that does not know it exists.
96
+
97
+ **Consequence for a repository that relied on the split.** A declaration with no quotes
98
+ behaves identically, because whitespace-splitting a shell word that needed no quoting
99
+ is the same thing the shell does. Every use of `gates.commands` in this estate declares
100
+ a bare path and is unaffected; the change only makes declarations *possible* that were
101
+ previously impossible rather than making any of them wrong.
75
102
 
76
103
  ## Consequences
77
104
 
@@ -0,0 +1,75 @@
1
+ # ADR-059 — The version a repository runs is the one it pinned, and the gate refuses otherwise
2
+
3
+ Status: **accepted** · 2026-10-12 · affects all repos
4
+
5
+ ## Context
6
+
7
+ ADR-010 ruled that a tool which is genuinely not a project dependency may be invoked directly,
8
+ **provided its version is pinned**. For a repository with a `package.json`, simt-dev is that
9
+ dependency, and npm does the pinning: it installs the version in `package.json` into
10
+ `node_modules`, and a stale copy is a stale lockfile that `npm ci` reproduces.
11
+
12
+ For a repository **without** npm — a Gradle build, a Connect IQ project — there is no
13
+ `node_modules` to pin in. simt-dev is a global install, and nothing pins it. The version on
14
+ `PATH` is whatever the last `npm i -g` left there, and it can be anything. The gate that runs
15
+ on commit is the one in that binary. If it is not the version the repository adopted, the gate
16
+ that runs locally is not the one CI runs: a green tick from a check that ran the wrong version
17
+ is the one outcome worse than a red build, because it is a pass that never happened.
18
+
19
+ The same gap sat in the remedy. The shim and the consumer workflow assumed npm everywhere —
20
+ `npm i -D simt-dev@<v>` and `npm ci`. In a Gradle repository that command installs into a
21
+ `package.json` the repository will never have, so the remedy cannot succeed and the CI step
22
+ cannot start. The install and the remedy each needed to know the kind of repository they were
23
+ in, and neither did.
24
+
25
+ ## Ruling
26
+
27
+ 1. **The manifest records the version a repository adopted.** `projection` carries
28
+ `version` alongside the existing `simtDevSha`, written at adoption and refreshed by
29
+ `upgrade`. A node_modules copy is pinned by npm itself, so this field matters only where
30
+ npm is not the pin — but it is written everywhere, because a manifest that records the
31
+ version in one shape of repository and not another is two manifests.
32
+
33
+ 2. **The gate compares the running binary against that pin, and refuses on a mismatch.** The
34
+ comparison is in the dispatcher (a failed `version` gate) and in `check` (a blocking
35
+ finding), not in the shim's rendered bytes: the dispatcher is where every gate already runs,
36
+ and a comparison duplicated into bytes a formatter might rewrite would drift from the one
37
+ that decides. It is placed after the empty-gate check on purpose — a repository that declares
38
+ no gate has nothing the version could get wrong, and failing it would block commits in repos
39
+ that deliberately declare none.
40
+
41
+ 3. **The install and the remedy are per kind.** Where a `package.json` exists, simt-dev is pinned
42
+ as a devDependency and the remedy is `npm i -D simt-dev@<v>`. Where there is no npm, it is a
43
+ pinned global install and the remedy is `npm i -g simt-dev@<v>` (or `export SIMT_DEV_BIN=…`).
44
+ The consumer CI workflow makes the same choice by kind — `npm ci` where the package is a
45
+ declared dependency, the pinned global install otherwise — so the gate that runs in CI is the
46
+ one the machine remedy names, and a check cannot pass in one place and fail to start in the
47
+ other.
48
+
49
+ ## Consequences
50
+
51
+ - A stale global binary can no longer produce a green tick from a check that ran a different gate
52
+ than CI does. The mismatch is a refusal with the one command that fixes it, and the remedy and
53
+ the CI step agree on which version to install.
54
+ - A repository adopted before the field existed — a manifest with `simtDevSha` but no
55
+ `projection.version` — is **skipped, not failed**. There is nothing to compare against, and
56
+ failing every pre-0.2 repository the moment this check ships would turn the gate red for a
57
+ reason it cannot fix. `compare` reports that missing field as a stale manifest (ADR-045's
58
+ defect from the far side), so it is visible and repairable rather than silently absent.
59
+ - The version a repository runs is now an invariant the gate enforces, not a comment: the pin in
60
+ the manifest and the binary on `PATH` must agree, or the commit does not happen.
61
+
62
+ ## Rejected
63
+
64
+ - **Shell out to `simt --version` in the shim.** The dispatcher runs from the binary that
65
+ resolved it, so reading the running package's version is the same answer without a subprocess,
66
+ and it is correct whether the binary came from `node_modules`, a global install, or a source
67
+ checkout. A subprocess would answer only for the binary that started it.
68
+ - **Fail a repository with no recorded pin.** There is nothing to compare against; it would make
69
+ every pre-0.2 repository red on the day this ships, for a mismatch nobody can resolve until
70
+ they re-adopt. The missing pin is reported as drift, which is the repairable form of the same
71
+ fact.
72
+ - **Put the check only in the shim's rendered bytes.** The dispatcher is where every gate runs and
73
+ where `check` already lives, so the comparison belongs in one place. Rendered bytes are read by
74
+ a formatter and a reviewer; a control that lives in both is two copies of one question, which
75
+ is the shape this record's siblings keep finding drifting.
@@ -0,0 +1,131 @@
1
+ # ADR-060 — A repository with nothing to run is adopted and says so, and a gate is verified where it runs
2
+
3
+ Status: **accepted** · 2026-10-12 · filed as S36
4
+ **Affects:** `hooks install`, `compare`, the adoption criterion in review 0002
5
+
6
+ ## Context
7
+
8
+ Review 0002 defined adoption as eight things a repository must have, and gave the estate
9
+ roll-out an acceptance criterion: every repository current, every repository routed, no
10
+ unrunnable gate, and a green CI run in every repository that has CI. Fourteen repositories
11
+ were in scope.
12
+
13
+ Three of them cannot satisfy the second of those, and the row contradicted itself about it.
14
+ The runbook's own step 4, for one of the three, reads: *"none (no runner). Record that in
15
+ the manifest rather than invent one."* That is an instruction not to route it. The
16
+ acceptance criterion then requires that it be routed. One of the two is wrong, and the row
17
+ could not be closed without settling which.
18
+
19
+ This stopped being hypothetical when the roll-out ran. The three are:
20
+
21
+ - **A Gradle module consumed as a submodule.** It has no wrapper of its own — the two
22
+ repositories that embed it own that — so `./gradlew` cannot be run from it. Its
23
+ `package.json` declares no scripts at all; it exists to carry one devDependency.
24
+ - **A repository with no `package.json`.** No runner, no scripts, nothing to probe.
25
+ - **A static site with no gate and its own hook.** No `format:check` and no `test` script to
26
+ declare a gate from, and a Lighthouse pre-push in `.githooks/` that routing would displace.
27
+
28
+ Each is ungated for a reason that was checked rather than assumed, and each records that
29
+ reason in its own commit message.
30
+
31
+ ADR-017 already settled the principle in one direction: a gate is declared only if the
32
+ repository can run it. This record settles the other, which fourteen repositories of real
33
+ use never exercised — what a repository that can run nothing looks like once it has adopted
34
+ the tool.
35
+
36
+ A second thing surfaced at the same moment, and it is not a repository's fault. The roll-out
37
+ pushed to fourteen repositories and GitHub Actions executed no steps: every run completed
38
+ with `failure`, zero steps, and no logs — including a pre-existing lint-and-test workflow
39
+ whose body this work never touched. Actions reports `enabled: true` on all eight
40
+ repositories; the organisation-level setting needs `admin:org` and could not be read. The
41
+ workflow files parse, and the same workflows have run before. The jobs are not starting.
42
+
43
+ ## Ruling
44
+
45
+ **One. A repository that cannot run a gate is adopted when it says so, and routing is not
46
+ required of it.** `hooksComplete` is not the measure of adoption, and "every repository
47
+ routed" was never achievable. The three carry the evidence in the manifest and in their own
48
+ commit messages: what is missing, and why. That is a stronger artefact than a shim that
49
+ runs nothing — which is what forcing them would have produced, and which the runbook already
50
+ forbade inventing.
51
+
52
+ **Two. A gate must be runnable, and a shim must not be installed over one that is working.**
53
+ This is ADR-018 as amended, restated because the roll-out made it concrete. The static site's
54
+ pre-push keeps running in `.githooks/` precisely because its manifest declares no `prePush`
55
+ gate and therefore nothing displaces it. Routing a repository that enforces nothing is not
56
+ a smaller change than routing one that does: `core.hooksPath` moves, git reads every hook
57
+ from the one directory, and the gain is zero.
58
+
59
+ **Three. "Green CI" is verified where CI runs, and is not a property of a repository when it
60
+ does not.** The clause is replaced by two that can be answered: the workflow file is present
61
+ and valid in every repository that has one, and the gate runs locally. Whether GitHub
62
+ executes those workflows is a fact about the account, and a gate that reports a property it
63
+ cannot observe is ADR-039's defect — the step that cannot fail is the one nobody reads.
64
+
65
+ **Four. The estate's CI runs on pull requests, and delivery does not.** A check that fires on
66
+ a timer is not evidence about a change: by the time it reports, the commit is days old and
67
+ nobody is waiting on it. That is why the weekly runs were removed rather than de-scheduled.
68
+ It is also why the two API-contract workflows were split out of the `ci.yml` files they
69
+ shared: republishing to the edge needs a credential a fork does not have, so it fires on
70
+ merge to `main`, and one workflow cannot carry two triggers once its checks and its delivery
71
+ need different ones. The template moved in one commit and the estate followed the template,
72
+ which is the only way to move fourteen repositories that do not own the file.
73
+
74
+ **Five. The measure is `check`, and `compare` is what makes it visible at estate scale.**
75
+ ADR-045 made `compare` count a defect it had been hiding. This cycle found the same row
76
+ reporting every repository current while `check` exited non-zero in all of them. Both are now
77
+ driven at one fixture and compared — a parity test, because two copies of one question, each
78
+ asserted separately, is how they drift. That is ADR-052's shape, reached from a different
79
+ direction.
80
+
81
+ ## Consequences
82
+
83
+ - **The roll-out closes with eleven of fourteen routed, and the three exceptions named in
84
+ three commit messages** rather than in a dashboard. An audit trail beats a count.
85
+ - **If one of the three gains something to run, it routes in one command** and nothing else
86
+ about it changes. This is not a permanent exemption; it is the current answer to "can this
87
+ repository enforce anything", and `compare` re-reads it every run.
88
+ - **The result is stronger than the row asked for.** Every repository's gate exits zero,
89
+ every manifest records a released version, no rule is loaded twice, no declared gate is
90
+ unrunnable, and no region sits past a harness's read limit. Three of those were false when
91
+ the row was written.
92
+ - **The CI clause is recorded as unverified, not met.** Nothing in a repository can make
93
+ GitHub execute a workflow, and a claim about a green build nobody has seen is the thing
94
+ this repository's own rules exist to prevent. Re-run the query and the clause comes back.
95
+
96
+ ## Rejected
97
+
98
+ - **Route the three anyway, with an empty gate list.** `hooks install` already refuses —
99
+ it prints *"declares no gate it can run, so there is nothing to enforce."* Overriding it
100
+ means a shim on every commit that dispatches, reads a manifest, and exits having enforced
101
+ nothing. The cost is a repository that believes it is gated.
102
+ - **Declare a gate to satisfy the count.** That needs a name for a check that does not
103
+ exist. The Connect IQ repository has the shape of this: seven scripts in `scripts/` that
104
+ all run, and a gate vocabulary with no name for five of them. Two were declared, where the
105
+ name matched, and the other five stay manual rather than borrow `test` or `lint` — a gate
106
+ whose printed name misdescribes the command it runs teaches a reader not to trust the
107
+ other seven.
108
+ - **Keep the weekly runs because they are "just checks".** They were the most expensive
109
+ thing in the estate — the Android one booted a headless emulator every Saturday — and the
110
+ least timely. Where the checks were worth keeping they moved to the pull request that
111
+ changes the code, and one repository's own test suite refused the push that deleted them,
112
+ which is the gate working.
113
+ - **Close the row before measuring it.** Every figure in this record comes from a committed
114
+ build run over the estate, not from the working tree that produced it. That distinction
115
+ matters here: earlier in this cycle an apparent fourteen-repository defect turned out to be
116
+ caused by an uncommitted change, and the number would have been reported as a fact about
117
+ the estate.
118
+
119
+ ## Notes
120
+
121
+ This record names no repository. The sweep that pseudonymised the estate used stable
122
+ `repo-NN` labels because several older records use repository names as *evidence* — a table
123
+ of how three repositories answer the same question, where "a repository and a repository"
124
+ would destroy the argument. That argument is not present here: every repository in this
125
+ record is identified by what it *is*, which is both publishable and more useful to a reader
126
+ who has none of the files.
127
+
128
+ `test/shipped.test.mjs` now fails on any estate name in anything this package publishes —
129
+ the records, the changelog, the readme. It is scoped to the published set and asserts that
130
+ scope against `package.json`, in both directions, so a directory added to `files` without
131
+ being guarded fails rather than leaking.