simt-dev 0.2.0__tar.gz → 0.2.2__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 (174) hide show
  1. simt_dev-0.2.2/CHANGELOG.md +534 -0
  2. {simt_dev-0.2.0 → simt_dev-0.2.2}/PKG-INFO +6 -6
  3. {simt_dev-0.2.0 → simt_dev-0.2.2}/README.md +4 -4
  4. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-019-a-declared-command-is-the-runner.md +27 -0
  5. simt_dev-0.2.2/docs/decisions/ADR-059-the-version-a-repository-runs-is-the-one-it-pinned.md +75 -0
  6. simt_dev-0.2.2/docs/decisions/ADR-060-a-repository-with-nothing-to-run-is-adopted-and-says-so.md +131 -0
  7. simt_dev-0.2.2/docs/decisions/ADR-061-the-skill-registry-is-workspace-data.md +113 -0
  8. simt_dev-0.2.2/docs/decisions/ADR-062-a-fresh-adoption-seeds-the-gates-it-can-run.md +94 -0
  9. simt_dev-0.2.2/docs/decisions/ADR-063-the-adoption-escape-hatch-is-opt-in.md +69 -0
  10. simt_dev-0.2.2/docs/decisions/ADR-064-the-context-meter-counts-what-the-tool-wrote.md +107 -0
  11. simt_dev-0.2.2/docs/decisions/ADR-065-a-pack-is-one-shared-rule-set-pinned-by-digest.md +97 -0
  12. simt_dev-0.2.2/docs/decisions/ADR-066-a-monorepo-projects-once-per-package.md +115 -0
  13. simt_dev-0.2.2/docs/decisions/ADR-067-which-harnesses-read-agents-md-is-a-column.md +110 -0
  14. simt_dev-0.2.2/docs/decisions/ADR-068-the-mcp-revision-list-cites-what-was-read.md +111 -0
  15. simt_dev-0.2.2/docs/decisions/ADR-069-the-conformance-lint-checks-the-published-specification.md +90 -0
  16. simt_dev-0.2.2/docs/decisions/ADR-070-the-json-payload-carries-its-own-version.md +91 -0
  17. simt_dev-0.2.2/docs/decisions/ADR-071-a-rule-that-can-be-narrowed-is-written-narrow.md +106 -0
  18. simt_dev-0.2.2/docs/decisions/ADR-072-the-budget-is-a-declaration-beside-the-gates.md +104 -0
  19. simt_dev-0.2.2/docs/decisions/ADR-073-a-kernel-is-a-smaller-projection-the-repository-asks-for.md +92 -0
  20. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/README.md +17 -0
  21. {simt_dev-0.2.0 → simt_dev-0.2.2}/package.json +1 -2
  22. {simt_dev-0.2.0 → simt_dev-0.2.2}/pyproject.toml +0 -2
  23. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/core.md +1 -0
  24. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/stacks/android.md +2 -0
  25. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/stacks/cloudflare-workers.md +2 -0
  26. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/stacks/connectiq.md +1 -0
  27. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/stacks/container.md +2 -0
  28. simt_dev-0.2.2/rules/stacks/dart.md +40 -0
  29. simt_dev-0.2.2/rules/stacks/deno.md +38 -0
  30. simt_dev-0.2.2/rules/stacks/dotnet.md +37 -0
  31. simt_dev-0.2.2/rules/stacks/elixir.md +41 -0
  32. simt_dev-0.2.2/rules/stacks/go.md +36 -0
  33. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/stacks/jvm.md +2 -0
  34. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/stacks/node.md +2 -0
  35. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/stacks/python.md +2 -0
  36. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/stacks/react-native.md +2 -0
  37. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/stacks/react-web.md +2 -0
  38. simt_dev-0.2.2/rules/stacks/ruby.md +38 -0
  39. simt_dev-0.2.2/rules/stacks/rust.md +36 -0
  40. simt_dev-0.2.2/rules/stacks/swift.md +39 -0
  41. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/stacks/typescript.md +2 -0
  42. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/stacks/uv.md +2 -0
  43. simt_dev-0.2.2/src/argv.mjs +164 -0
  44. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/ci/dependabot.mjs +1 -1
  45. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/ci/workflow.mjs +33 -11
  46. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/commands/compare.mjs +15 -0
  47. simt_dev-0.2.2/src/commands/graph.mjs +106 -0
  48. simt_dev-0.2.2/src/commands/harness.mjs +150 -0
  49. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/commands/help.mjs +37 -5
  50. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/commands/hooks.mjs +173 -57
  51. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/commands/index.mjs +95 -69
  52. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/commands/init.mjs +100 -20
  53. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/commands/mcp.mjs +20 -5
  54. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/commands/rules.mjs +96 -60
  55. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/commands/skills.mjs +189 -86
  56. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/commands/skillsinstall.mjs +20 -5
  57. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/commands/sync.mjs +213 -15
  58. simt_dev-0.2.2/src/commands/tokens.mjs +138 -0
  59. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/compare/hooksstate.mjs +96 -9
  60. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/compare/row.mjs +107 -7
  61. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/decisions/index.mjs +21 -6
  62. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/graph/load.mjs +125 -14
  63. simt_dev-0.2.2/src/graph/spec.mjs +227 -0
  64. simt_dev-0.2.2/src/graph/speccheck.mjs +68 -0
  65. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/hooks/capability.mjs +48 -4
  66. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/hooks/dispatcher.mjs +84 -7
  67. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/hooks/gates.mjs +0 -2
  68. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/hooks/resolve.mjs +24 -25
  69. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/hooks/shim.mjs +29 -5
  70. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/identity.mjs +0 -11
  71. simt_dev-0.2.2/src/mcp/schema.mjs +139 -0
  72. simt_dev-0.2.2/src/mcp/server.mjs +344 -0
  73. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/mcp/tools.mjs +290 -3
  74. simt_dev-0.2.2/src/measure/context.mjs +371 -0
  75. simt_dev-0.2.2/src/monorepo.mjs +339 -0
  76. simt_dev-0.2.2/src/output/index.mjs +173 -0
  77. simt_dev-0.2.2/src/package-version.mjs +74 -0
  78. simt_dev-0.2.2/src/packs.mjs +182 -0
  79. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/projection/conflicts.mjs +56 -5
  80. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/projection/manifest.mjs +335 -25
  81. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/projection/mdc.mjs +4 -13
  82. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/projection/project.mjs +139 -8
  83. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/projection/rules.mjs +308 -27
  84. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/projection/surfaces.mjs +133 -15
  85. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/projection/targets.mjs +90 -7
  86. simt_dev-0.2.2/src/resolve/eval.mjs +64 -0
  87. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/resolve/fingerprint.mjs +1 -0
  88. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/resolve/near.mjs +26 -7
  89. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/resolve/resolve.mjs +1 -1
  90. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/resolve/source.mjs +1 -1
  91. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/workspace.mjs +0 -9
  92. simt_dev-0.2.0/CHANGELOG.md +0 -166
  93. simt_dev-0.2.0/graph/skills.graph.json +0 -1622
  94. simt_dev-0.2.0/src/argv.mjs +0 -116
  95. simt_dev-0.2.0/src/commands/graph.mjs +0 -73
  96. simt_dev-0.2.0/src/mcp/server.mjs +0 -167
  97. simt_dev-0.2.0/src/output/index.mjs +0 -96
  98. {simt_dev-0.2.0 → simt_dev-0.2.2}/.gitignore +0 -0
  99. {simt_dev-0.2.0 → simt_dev-0.2.2}/LICENSE +0 -0
  100. {simt_dev-0.2.0 → simt_dev-0.2.2}/bin/simt-dev.mjs +0 -0
  101. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-001-push-requires-approval.md +0 -0
  102. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-002-refactor-blast-radius.md +0 -0
  103. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-003-test-breadth.md +0 -0
  104. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-004-hook-owns-format.md +0 -0
  105. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-005-file-length.md +0 -0
  106. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-006-coverage-bar.md +0 -0
  107. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-007-section-skeleton.md +0 -0
  108. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-008-verdict-vocabulary.md +0 -0
  109. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-009-skill-layout.md +0 -0
  110. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-010-runner-invocation.md +0 -0
  111. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-011-windows-support.md +0 -0
  112. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-012-drop-windows-support.md +0 -0
  113. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-013-genericity-by-facts-and-local-profiles.md +0 -0
  114. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-014-workspace-is-configuration.md +0 -0
  115. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-015-a-source-is-content-not-a-location.md +0 -0
  116. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-016-a-divergent-rule-is-retired-not-merged.md +0 -0
  117. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-017-a-declared-gate-must-be-runnable.md +0 -0
  118. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-018-an-unset-hooks-path-is-not-an-absent-hook.md +0 -0
  119. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-020-a-rule-is-written-per-surface-and-pointed-not-copied.md +0 -0
  120. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-021-one-surface-per-repo.md +0 -0
  121. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-022-a-key-simt-dev-reads-is-prefixed.md +0 -0
  122. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-023-the-gate-ships-as-a-file-the-repo-owns.md +0 -0
  123. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-024-simt-dev-overwrites-its-own-gate-file.md +0 -0
  124. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +0 -0
  125. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +0 -0
  126. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +0 -0
  127. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +0 -0
  128. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +0 -0
  129. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +0 -0
  130. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-031-the-suite-stages-the-environment-it-needs.md +0 -0
  131. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-032-provenance-is-a-content-digest-not-a-commit.md +0 -0
  132. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -0
  133. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-034-a-staged-tool-refuses-a-partially-staged-file.md +0 -0
  134. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-035-one-write-decision-and-one-json-document.md +0 -0
  135. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-036-a-writer-that-writes-says-so-and-the-instructions-naming-it-move-with-it.md +0 -0
  136. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-037-unchanged-means-writing-would-not-change-a-byte.md +0 -0
  137. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-038-the-gate-filter-belongs-to-every-manifest-writer.md +0 -0
  138. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-039-a-ci-step-runs-a-form-that-can-fail.md +0 -0
  139. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-040-the-last-writer-that-wrote-by-default-was-not-the-one-named.md +0 -0
  140. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-041-the-suite-installs-the-package-in-order-to-test-it.md +0 -0
  141. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-042-a-declared-glob-must-match-a-path-built-from-it.md +0 -0
  142. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-043-a-keyword-is-a-phrase-and-two-letters-is-a-word.md +0 -0
  143. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-044-compatibility-is-not-evidence.md +0 -0
  144. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-045-a-dashboard-that-counts-a-defect.md +0 -0
  145. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-046-one-rule-one-copy.md +0 -0
  146. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-047-a-profile-selects-on-what-a-repository-runs-on.md +0 -0
  147. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +0 -0
  148. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-049-a-ruling-that-a-later-record-moved-says-so-where-it-is-printed.md +0 -0
  149. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-050-a-byte-count-is-not-a-region-and-updated-is-not-a-change-of-rules.md +0 -0
  150. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-051-a-pointer-is-the-readers-only-route-so-it-is-not-a-convenience.md +0 -0
  151. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-052-a-dependency-is-counted-against-the-budget-and-there-is-one-budget.md +0 -0
  152. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-053-the-node-floor-is-22-20-0.md +0 -0
  153. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-054-the-digest-covers-every-byte-that-leaves-the-package.md +0 -0
  154. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-055-a-status-line-travels-so-it-may-not-name-a-location.md +0 -0
  155. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-056-three-names-one-of-which-is-cheap-to-move.md +0 -0
  156. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-057-an-identity-is-per-developer-and-a-repository-does-not-own-it.md +0 -0
  157. {simt_dev-0.2.0 → simt_dev-0.2.2}/docs/decisions/ADR-058-one-tool-three-channels-and-the-order-is-forced.md +0 -0
  158. {simt_dev-0.2.0 → simt_dev-0.2.2}/python/simt_dev/__init__.py +0 -0
  159. {simt_dev-0.2.0 → simt_dev-0.2.2}/python/simt_dev/cli.py +0 -0
  160. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/mdc/intake-questions.mdc +0 -0
  161. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/mdc/preview-postflight.mdc +0 -0
  162. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/mdc/slice-commit.mdc +0 -0
  163. {simt_dev-0.2.0 → simt_dev-0.2.2}/rules/mdc/test-gate.mdc +0 -0
  164. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/commands/ci.mjs +0 -0
  165. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/commands/upgrade.mjs +0 -0
  166. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/compare/discover.mjs +0 -0
  167. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/hooks/where.mjs +0 -0
  168. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/locate.mjs +0 -0
  169. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/name.mjs +0 -0
  170. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/projection/baselines.mjs +0 -0
  171. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/projection/markers.mjs +0 -0
  172. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/toolchain.mjs +0 -0
  173. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/tsconfig.mjs +0 -0
  174. {simt_dev-0.2.0 → simt_dev-0.2.2}/src/upgrade/plan.mjs +0 -0
@@ -0,0 +1,534 @@
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.2] — 2026-10-14
10
+
11
+ ### A repository can ask for a smaller projection
12
+
13
+ `simt tokens` measured the cost and `check` now holds the line the repository draws: `budget.bytes`
14
+ in `simt.json` is how many always-on bytes that repository has agreed to carry, and both the meter and
15
+ the gate report the overflow with the two numbers in them. It sits beside `gates` rather than inside
16
+ `projection`, because that key belongs to this tool and an upgrade replaces it.
17
+
18
+ `mode: kernel` is the other half. It keeps the rules a reader cannot do without — the ones a hard
19
+ constraint or a command depends on — and defers the rest behind a `## Deferred rules` section that
20
+ names each one and the command that shows it. A Python fixture projects 16% smaller. `full` is still
21
+ the default, because nobody has measured yet whether the smaller projection loses compliance, and a
22
+ tool that made that trade silently would be deciding it on the reader's behalf (ADR-073).
23
+
24
+ ### A repository can declare how many always-on bytes it is willing to carry
25
+
26
+ `simt tokens` has reported that number since the meter landed, against a constant nobody
27
+ could argue with. A repository can now say what it will carry — `budget: { bytes: N }`
28
+ beside `limits` and `gates` in the manifest — and both `simt tokens` and `simt-dev check`
29
+ measure against that line. One declaration, one meter, one function that words the finding.
30
+
31
+ The declaration is deliberately *not* inside `projection`, which is the one block this tool
32
+ owns and `upgrade` is allowed to replace: a number written there would be deleted silently
33
+ on the next upgrade and never reported as drift. And a repository that declares nothing is
34
+ reported as declaring nothing, rather than being quietly handed the default as though it
35
+ had chosen it.
36
+
37
+ Over budget is reported and never fails the gate. Nothing was cut — the projection is
38
+ still full mode, every rule loaded as written — so the honest thing to say is "you are
39
+ over your line", not to fail a repository over a ceiling it wrote down itself. Which rules
40
+ move behind skills stays with the evaluation that has to decide it (ADR-072).
41
+
42
+ ### A repository can declare the always-on bytes it intends to carry
43
+
44
+ `simt tokens` measured the number; it could not be held to one. A manifest now carries
45
+ `budget.bytes` — a declaration beside `limits` and `gates`, not inside `projection`,
46
+ because that block is regenerated on every upgrade and nothing in the repair would notice
47
+ its budget disappearing. A repository that declares nothing is measured against the
48
+ review's 30 KB and reports `declared: false` rather than looking like it chose it.
49
+
50
+ Over budget is reported, never enforced, and the report says what was not done: nothing
51
+ was cut, because the projection is still in full mode. Which rules move behind skills is a
52
+ measurement that has not been taken yet, and a budget that quietly dropped rules would be
53
+ enforcing a decision nobody made (ADR-072).
54
+
55
+ ### A stack profile loads only when a matching file is in play
56
+
57
+ A stack profile used to be always-on everywhere, because profiles reached a reader through
58
+ the generated region in AGENTS.md and through nothing else. A profile can now declare
59
+ `simt-scope`, and a repository on Cursor, Copilot or Antigravity gets it as a file the
60
+ surface attaches on patterns: `.cursor/rules/python.mdc` with `globs` and
61
+ `alwaysApply: false`, an Antigravity rule with `trigger: glob`, a Copilot
62
+ `.instructions.md` with `applyTo`. AGENTS.md keeps one line naming the profile, the
63
+ patterns and the file it moved to — a profile that vanishes from the region reads as one
64
+ that was deleted.
65
+
66
+ The narrowing happens only when every selected surface can do it. Claude Code's `paths:`
67
+ key is documented and silently fails to lazy-load for quoted values, and opencode has no
68
+ per-rule channel at all, so a repository carrying either keeps its profiles in the region
69
+ and is told which surface stopped it. A rule that never loads is worse than a rule that
70
+ loads eagerly.
71
+
72
+ Reading the two vendors' docs changed the shape of this: Antigravity requires patterns
73
+ starting with `*` to be quoted or YAML reads them as an alias anchor and the rule is
74
+ discarded with no message, and it documents a 20,000-token budget for always_on rules past
75
+ which they are demoted to pointers — so eager profiles cost the reader a real budget. A
76
+ `simt-scope` that matches everything is now refused, since that is the same behaviour with
77
+ a narrowing-looking spelling (ADR-071).
78
+
79
+ ### Every `--json` payload says which version it is
80
+
81
+ An agent that parses `--json` had no way to notice that a field had moved: the parse
82
+ succeeds, the lookup returns `undefined`, and the answer is quietly wrong. Every payload now
83
+ opens with `schemaVersion`, and a version in a comment is not a version, so it travels in
84
+ the document every reader is guaranteed to see.
85
+
86
+ The same document is now one line rather than four columns of whitespace, because the reader
87
+ of `--json` is a program and it pays for every byte. `--pretty` asks for the other form, and
88
+ a terminal gets it without being asked, because there the reader is a person.
89
+
90
+ Writing the contract test found two things the review that asked for this missed. Eight
91
+ commands — `doctor`, `mcp --list`, `graph lint`, `rules list`, and the `skills` ones — had a
92
+ private `emit(JSON.stringify(..., null, 2))` instead of the shared helper, so `--json` meant
93
+ something different depending on which command you asked. And a command that *failed* under
94
+ `--json` wrote only to stderr, so a scripted caller got an empty document and a non-zero
95
+ exit and no way to learn what went wrong. It now answers in the shape it was asked in (ADR-070).
96
+
97
+ ### `graph lint` now checks skills against the published specification
98
+
99
+ The Agent Skills specification sets limits nobody here was checking: a name of lowercase
100
+ letters, digits and single hyphens that equals the directory it lives in; a description
101
+ under 1,024 characters; a compatibility note under 500; a body recommended under 5,000
102
+ tokens because an agent loads all of it once the skill activates. `graph lint --spec
103
+ agentskills` checks every skill against those, and the specification was fetched rather
104
+ than recalled — reading it turned up four rules the review that asked for this did not
105
+ list, including the one that breaks installs.
106
+
107
+ What the specification calls a must fails the exit code; what it calls a recommendation
108
+ warns. A lint that fails on advice is a lint that gets skipped. `skills install` now asks
109
+ the same name rules, since the directory is created from the name and an unaddressable
110
+ skill would be the last thing anyone notices.
111
+
112
+ ### The MCP surface speaks three protocol revisions, and answers in the shape each asks for
113
+
114
+ The server was pinned to one revision and returned pretty JSON in a text block, so a
115
+ client could read the answer but could not validate it, could not cache it, and could not
116
+ tell a read-only tool from one that writes. All three published revisions were read rather
117
+ than guessed at, and each is answered in its own shape: the newest drops the handshake for
118
+ a version carried on every request, adds a discovery method servers must implement,
119
+ requires a result-type field and a cache lifetime on list results, and deleted the liveness
120
+ method — which older clients still get, and newest clients are told is gone by name.
121
+
122
+ Every tool now declares what it returns and says it is a reader, and every call returns
123
+ the answer in both shapes: the text block for a client that predates structured content,
124
+ the structured block for one that validates against the declared schema. The byte ceiling
125
+ bounds the text only, so the block a client can check still checks. The schema checker
126
+ that makes this testable lives in the package rather than in a dependency.
127
+
128
+ ### Which harnesses a repository is set up for is now answerable
129
+
130
+ `simt-dev harness detect` looks at the configuration directories that exist and says
131
+ which harnesses this repository — and, with `--user`, its reader — is set up for. It
132
+ claims presence and nothing else: a directory found is not a claim about what its owner
133
+ reads, because the tool cannot answer that without a vendor document, and a guess here
134
+ would be the reader's problem the first time it was wrong. A harness that reads
135
+ AGENTS.md is served by the standard surface whatever else it reads, and every surface row
136
+ now records that in three values rather than two, because "reads AGENTS.md" and "is
137
+ covered for this harness" are not the same sentence — Copilot's CLI reads it and its
138
+ editor, code review and cloud agent do not.
139
+
140
+ No new harness rows ship with it. The candidates are recorded as directory names, which
141
+ claim presence and not coverage; a row still requires the vendor document it cites, and
142
+ one that reads nothing has to be added on purpose.
143
+
144
+ ### A monorepo gets rules for each of its packages
145
+
146
+ One repository, one projection: every package under the root was given the root's rules,
147
+ so a React application inside a node workspace was handed the node profile and nothing
148
+ else, and the profile that should have applied to it was applied by nobody. The rule that
149
+ package needed was not wrong — it was missing.
150
+
151
+ Packages are now discovered from the declarations that already list them (npm and pnpm
152
+ workspaces, turbo, Cargo members, `go.work`, Gradle includes) rather than by walking the
153
+ tree, because a declaration states which directories are members and a marker file only
154
+ says there is a build here. The root keeps what every package shares; each package that
155
+ selects something new gets a generated region carrying exactly its own delta, and a
156
+ package that selects nothing new gets no file at all. `check` reports every package
157
+ region and fails on any one of them, and `compare` counts the drifted regions rather than
158
+ the repositories that hold them.
159
+
160
+ A gate can also name the directory it runs in, which is what a monorepo with no root test
161
+ script needs: `gates.cwd` maps a gate to a directory, a directory that does not exist or
162
+ resolves outside the repository fails the event and is reported by `check` before the
163
+ commit rather than at it, and a directory named for a gate that is not declared is
164
+ reported as the inert entry it is.
165
+
166
+ ### One rule set, shared by the repositories that agreed on it
167
+
168
+ Several repositories want the same rules and each kept its own copy, so a policy change
169
+ was several edits that could be several-minus-one, and the repository that missed it
170
+ carried instructions nobody there wrote. A pack is that shared set: `packs` in the manifest
171
+ declares it, the bytes live in the repository, and a digest of them is recorded so nobody
172
+ has to trust that the two agree.
173
+
174
+ A pack's rules are selected by the same matcher as any profile and appear in the region's
175
+ source list under the pack's own directory. Precedence is the repository, then a pack it
176
+ declares, then what the tool ships, and every replacement is reported rather than silent.
177
+
178
+ A pack that disagrees with its pin still projects — the gate blocks and prints both
179
+ digests, because dropping it would let the repair delete the rules it is reporting. One
180
+ that is missing blocks with a remedy that restores it, and one with no digest is reported
181
+ without failing anything, since its rules render correctly. `compare` lists every pack and
182
+ the digest it is pinned to, so a repository behind a shared change is visible before
183
+ anyone follows stale instructions. Nothing is fetched: the bytes are in the repository,
184
+ which is what makes the rendered region reproducible from what the reader has (ADR-065).
185
+
186
+ ### `simt tokens` counts what a reader loads
187
+
188
+ Review 0002 filed the estate's always-on context at 90 KB per turn, 72% of it a paragraph
189
+ an earlier repository had already loaded — a number produced by reading seven
190
+ repositories by hand, which ages the moment a rule is edited and cannot be re-derived by
191
+ the reader who has to act on it.
192
+
193
+ `simt tokens` is that reading, as a command. It reports the bytes this tool wrote per
194
+ channel (region, rule files, skill metadata), an approximate token figure, and — with
195
+ `--repos` — the same figure across a workspace, with every repeated paragraph attributed
196
+ to the first root that carries it. A region that ends past a harness's read limit is
197
+ reported with its byte offset, using the same numbers the existing check uses.
198
+
199
+ Three choices keep the figure honest, and all three are recorded in ADR-064: the meter
200
+ counts what the projection wrote and reports hand-written bytes beside it rather than
201
+ folding them into the budget; a region is the span between its two markers, so the blank
202
+ line a person left above the marker is billed to nobody; and an over-budget repository is
203
+ reported rather than failed, because a meter that went red would train people to ignore
204
+ the number.
205
+
206
+ S38 stays open — this is the instrument. Whether the estate gets under 30 KB is R07's
207
+ kernel, R08's budget field and R11's dedupe, measured with this.
208
+
209
+ ## [0.2.1] — 2026-10-13
210
+
211
+ ### `init` writes in place; the backup tag and adopt branch are opt-in
212
+
213
+ `simt init --yes` used to always tag the pre-adoption commit and branch onto a dedicated
214
+ adopt branch before writing. The safety argument was sound, but consumers kept reporting
215
+ the branch switch as a surprise — a well-announced surprise is still a surprise.
216
+
217
+ The default now writes in place on the branch you already have: nothing is tagged, nothing
218
+ is branched, and the run says so in an `in place` line. The reviewable diff survives —
219
+ `git diff` shows the whole adoption, and discarding it is a checkout and a clean.
220
+
221
+ The old behaviour moves behind `--backup`: `simt init --yes --backup` tags the pre-adoption
222
+ commit and branches as before, for readers who want the named undo path. ADR-063 records
223
+ the decision and the one behaviour it invalidates: consumers who scripted against the old
224
+ default add `--backup`.
225
+
226
+ ### A fresh adoption seeds the gates it can run — and only those
227
+
228
+ The eight common ecosystems outside the JavaScript world — Rust, Go, .NET, Swift, Deno,
229
+ Ruby, Dart and Elixir — were recognised by the fingerprint and served by nothing: no
230
+ profile selected them, no gate was proposed for them. A fresh Rust repository received the
231
+ core essay and a manifest that runs nothing, while the ecosystems that emit stack tags got
232
+ the full treatment.
233
+
234
+ Each now has a profile that selects on its own `has-*` fact, so one marker file reaches
235
+ both the rules the repository reads and the gates its commits run. Maven's profile predates
236
+ this; its row is gates-only, wrapper-first, like `gradlew`.
237
+
238
+ `simt init` now seeds the canonical commands for a detected ecosystem — `cargo fmt
239
+ --check`, `go vet ./...`, `dotnet test`, and the rest of the table — into the manifest's
240
+ gates, but only where a probe found the tool: a binary on PATH, or a file in the repository
241
+ for the Maven wrapper. A marker says the ecosystem is here; the probe says its program can
242
+ run. A declared gate that cannot run fails closed on every commit, so the seed fires only
243
+ when both are true — and where the probe fails, the gate is dropped with a remedy that
244
+ names the install instead of blocking the first commit.
245
+
246
+ A seed is a default: a command the repository already declared is never re-seeded, and an
247
+ npm runner keeps its own scripts, because `package.json` is the repository's own answer to
248
+ "how do I test and format". ADR-062 rules the shape of all of it.
249
+
250
+ ### The resolver is measured against a gold corpus
251
+
252
+ `simt skills eval` scores the skill resolver against a corpus of real tasks × real
253
+ repositories, each case carrying a gold list of the skills it should select and a list
254
+ of the ones it must not. It reports precision and recall, prints a PASS/FAIL line per
255
+ case naming the skills it missed or selected that it should not have, and exits
256
+ non-zero on a regression — so it can be run from CI the way the gate is run.
257
+
258
+ The scoring lives in `src/resolve/eval.mjs`, which the command and
259
+ `test/eval.test.mjs` both call, so a test cannot drift from what the command measures.
260
+ The corpus lives under `test/` because test data does not ship in the package: an
261
+ installed copy names the path it would have read and says so instead of scoring the
262
+ author's.
263
+
264
+ ### The skill registry is workspace data; the package ships none of it
265
+
266
+ The npm tarball and the wheel carried `graph/skills.graph.json` — the directory
267
+ of skill sources for one workspace, naming the repositories whose bodies it
268
+ resolves. Every installed copy served that catalogue to readers who have none
269
+ of those skills, and `skills install` pointed at paths that exist only in the
270
+ workspace that wrote them. ADR-061 rules the registry out of the published set:
271
+ it is data about one workspace, not content of the package.
272
+
273
+ The default registry path now resolves in tiers, and every tier is file
274
+ existence plus configuration — nothing a pull request cannot diff:
275
+
276
+ 1. an explicit path, the positional of `simt graph lint`,
277
+ 2. the package's own `graph/skills.graph.json` — present in a source checkout,
278
+ absent from an installed copy,
279
+ 3. the member of a configured workspace that holds one — naming the workspace
280
+ it came from, and refusing when several members hold one, because picking
281
+ would be a guess.
282
+
283
+ An installed copy in a workspace with no registry says so, and names the three
284
+ ways to configure one (`--workspace`, `SIMT_DEV_WORKSPACE`, the user config
285
+ file) instead of failing on a path it does not have. `simt graph lint`'s
286
+ no-registry remedy names the path explicitly, because its default is the
287
+ registry it lints.
288
+
289
+ The provenance digest stops walking `graph/` for the same reason it no longer
290
+ leaves the package: the digest covers what a published copy ships, and the
291
+ registry is no longer among it.
292
+
293
+ ### No published file may name a repository from this estate
294
+
295
+ The decision records ship in all three channels — npm, the wheel and the sdist — so a
296
+ private repository name written into one is published to three registries. That happened:
297
+ the records were pseudonymised once, across 28 files, and nothing stopped the next one. The
298
+ sweep's own commit says why, and it is the right reason: the redaction it used runs on the
299
+ way *out*, at print time, and the bytes on disk are what get published.
300
+
301
+ `test/shipped.test.mjs` now fails on an estate name anywhere in a published file, and
302
+ asserts its own scope against `package.json` in **both** directions — a directory added to
303
+ `files` without being guarded fails, and so does one guarded that nothing publishes, because
304
+ a guard nobody can fail is one somebody eventually deletes. It is scoped to the published
305
+ set rather than a hand-written list for the same reason: the sweep took 28 files to undo by
306
+ hand, and the guard should not need undoing at all.
307
+
308
+ Names are not removed, they are described. The records that use a repository as *evidence* —
309
+ a table of how three repositories answer the same question — carry stable `repo-NN` labels,
310
+ so the comparison survives. Everything else is better off naming what the repository *is*:
311
+ "a Gradle module with no wrapper", "a repository with no package.json". That reads to
312
+ someone outside the estate without a lookup table they do not have.
313
+
314
+ The one place estate names remain is `docs/reviews/`, which is not published by either
315
+ channel. A review whose subject is fourteen repositories cannot pseudonymise them without
316
+ becoming a document about `repo-01` that never existed.
317
+
318
+ ### Four shipped strings named a command this package does not install
319
+
320
+ `0.2.0` installs one binary, `simt`, and `package.json`'s `bin` map says so. Four
321
+ places still said `simt-dev`, which a reader types and is met with `unknown command`:
322
+
323
+ - the default remedy on every bad invocation, `run: simt-dev --help` — so the advice
324
+ printed after a bad command could not be followed
325
+ - the usage line, `usage: simt-dev <command>`, sitting above a command list that spelled
326
+ the same tool `simt`
327
+ - `.github/dependabot.yml`'s header, which said `Yours from here: simt-dev` while the
328
+ gate beside it said `Yours from here: simt`
329
+ - the hook shim, whose own resolution list said `simt-dev on PATH` directly above the
330
+ `command -v simt` that does the looking
331
+
332
+ `test/naming.test.mjs` was supposed to prevent this and did not: its rule required a
333
+ verb after the project name, and none of the four is followed by one. It now also
334
+ catches a flag, a `<placeholder>`, the end of a line, and a program name in a
335
+ resolution list — while still accepting the sentences where the project is the
336
+ subject, which is the spelling that is correct. The two `.github` templates get their
337
+ own assertion, because the fourth of these was not a wrong word in a sentence; it was
338
+ two templates disagreeing with each other in the same position.
339
+
340
+ ### Fixed
341
+
342
+ - `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.
343
+
344
+ - **Dead exports and duplicated frontmatter, with a test that stops them returning (S21).**
345
+ Eight functions were exported and called by nothing (`gateKind`, `gateWhy`, `gateAdr`,
346
+ `resetWorkspace`, `tracksIdentity`, `isTty`, `surfaceFrontmatter`, `workspaceOrNull`),
347
+ three `__internals` testing seams had no importer, and thirteen more bindings were
348
+ exported while being used only inside their own file — `isExecutable` among them, which
349
+ a first pass through this list wrongly recorded as dead and which its own callers
350
+ proved otherwise. `KNOWN_SUBS` was a hand-kept second copy of the dispatch table, and
351
+ had already drifted: it carried `doctor`, a bare command the group lookup can never
352
+ reach. `mdcFrontmatter` was a second construction of frontmatter the writer now derives
353
+ itself, with a test asserting the copy rather than the behaviour.
354
+
355
+ - **"Did you mean" offered the shortest fragment first, and a misspelt skill was
356
+ suggested for a correctly spelled one (S22).** `nearMatches` had no test at all.
357
+ Both directions of substring scored 0 — the same score as an exact match — so the
358
+ alphabetical tiebreak decided the order, and `skills graphs` suggested `graph`
359
+ before `graphs`. Worse, the containment test had no length floor, so `test` matched
360
+ the candidate `e` and the message read `did you mean e, test-writer?`, which an
361
+ agent has every reason to install. A prefix now scores 1 rather than 0, a fragment
362
+ counts only when it is at least half the query, and the edit bound scales with the
363
+ query's length — three edits is a typo in `slice-commmit` and is most of a
364
+ four-character query.
365
+
366
+ - **The conflict scanner skipped two of the five rule surfaces, and one pointer file
367
+ (S22).** `rulePaths` enumerated filenames by hand, so `.claude/rules/` and
368
+ `.agents/rules/` — added to the surface table after the list was written — were
369
+ never scanned, and a hand-written rule contradicting a generated one there was
370
+ invisible. A conflict detector that only looks where it was pointed looks nowhere
371
+ new. The scan list is now derived from the surface table and the pointer table.
372
+
373
+ ## [0.2.0] — 2026-10-11
374
+
375
+ ### Publishable from PyPI and Homebrew as well as npm
376
+
377
+ simt-dev is a *repository* tool, and the people who adopt a repository are not
378
+ all Node developers. Somebody running `simt init` inside a repository with no
379
+ `package.json` has no reason to install a package manager, and a Homebrew user
380
+ has no reason to install Node to install a tool that needs Node.
381
+
382
+ So it now publishes to three channels, from one version number:
383
+
384
+ ```sh
385
+ npm i -D simt-dev # a tarball of .mjs files
386
+ pip install simt-dev # a wheel carrying those same bytes
387
+ brew install QuazarMedia/tap/simt-dev
388
+ ```
389
+
390
+ The wheel is not a wrapper around `npm install`. It carries the tool's own
391
+ source, and `scripts/verify-wheel.mjs` builds both artefacts and hashes every
392
+ file in both — a release fails when a single byte differs between them. That
393
+ check exists because the obvious defect in this arrangement is the wheel and the
394
+ tarball quietly shipping different code.
395
+
396
+ `simt` is the one command, on every channel. Where node comes from is
397
+ `SIMT_DEV_NODE`, then the bundled node from `pip install "simt-dev[node]"`,
398
+ then `node` on `PATH` — and if none of those resolve you get the remedy rather
399
+ than a placeholder path. There are no required dependencies on any channel.
400
+ *(ADR-058)*
401
+
402
+ The channels are published in a fixed order rather than in parallel, and the
403
+ reason is not taste: the Homebrew formula downloads the npm tarball at install
404
+ time, so a formula naming a version still sitting in npm's staging area
405
+ installs a 404. Both jobs fail with that stated, and are run again after
406
+ `npm stage approve`.
407
+
408
+ ## [0.1.1] — 2026-10-11
409
+
410
+ ### A foreign hook on an event this repo does not gate is chained
411
+
412
+ `0.1.0` chained the wrong set of hooks and is worth replacing rather than
413
+ recommending. It chained on the event *vocabulary* — anything that was not
414
+ `pre-commit` or `pre-push` — when the question worth asking is whether **this
415
+ repository** declares a gate for that event.
416
+
417
+ The difference is a repository whose `pre-push` is `git-lfs`. `pre-push` is an
418
+ event this tool gates in general, so `0.1.0` displaced that hook; and because
419
+ that repository declares no `pre-push` gate there was no shim for the event
420
+ either, so git would have found no `pre-push` hook at all. LFS objects would have
421
+ stopped being uploaded, with nothing to notice it — the exact silent failure
422
+ chaining exists to prevent, reached by fixing the wrong half of it.
423
+
424
+ `0.1.1` asks the manifest. An event the repo declares a gate for is left to
425
+ `gates.commands`, where the hook runs as a gate and its verdict appears in the
426
+ report; every other foreign hook is chained. That also picks up a static
427
+ site's Lighthouse pre-push, which declares no gates at all.
428
+ *(ADR-018, as amended)*
429
+
430
+ Upgrade with `npm i -D simt-dev@0.1.1`, then `simt hooks install --yes`. Any
431
+ repository already routed with `0.1.0` should re-run it: the shims it writes
432
+ will differ where a gated event was previously displaced.
433
+
434
+ ## [0.1.0] — 2026-10-11
435
+
436
+ The first release with real contents in it. `1.0.0` was an empty reservation of the name
437
+ and `0.0.0-stage` was its rehearsal; neither shipped a byte anyone could run, so this entry
438
+ is the whole history. The decisions behind it are readable in the package under
439
+ `docs/decisions/`, and from any checkout with `simt rules why <id>`.
440
+
441
+ ### The command is `simt`
442
+
443
+ The package is `simt-dev` and the command is `simt`. They are not the same thing and were
444
+ never required to be: the command appears only in text the tool itself writes, and that
445
+ text migrates on the next write. The package name, the `.simt-hooks/` directory and the
446
+ `simt-dev-check.yml` gate already exist in every repository that adopted this tool, and
447
+ they did not move. The manifest and the region markers did — see below.
448
+
449
+ ### The manifest is `simt.json`
450
+
451
+ The manifest was called `simt-dev.json`; it is now `simt.json`. The move is a
452
+ read-both/write-new, so nothing in an adopting repository has to be done:
453
+
454
+ - Every read takes `simt.json` if it exists, else `simt-dev.json`, so a repository that
455
+ has not written since the upgrade is still seen as adopted.
456
+ - Every write goes to `simt.json`, and a lone `simt-dev.json` is moved, not shadowed.
457
+ - The old name keeps working until the first `init` or manifest write in each repository,
458
+ and that write closes it. *(ADR-056)*
459
+
460
+ ### The region markers are `simt:`
461
+
462
+ The generated region was stamped `<!-- simt-dev:begin -->` / `<!-- simt-dev:end -->` — and
463
+ so was the provenance header inside it; every one of those lines is now `simt:`. The move
464
+ is the same read-both/write-new as the manifest:
465
+
466
+ - Every read accepts either spelling, so a repository that has not written since the
467
+ upgrade is still seen as adopted.
468
+ - Every write emits `simt:` only, so the first `sync` in each repository migrates its
469
+ regions in place, and that write closes the window.
470
+ - A file holding two regions — one of each spelling — is a state only a hand edit can
471
+ make, and it is reported malformed rather than merged. *(ADR-056)*
472
+
473
+ ### A hook you already had keeps running
474
+
475
+ `hooks install` points `core.hooksPath` at `.simt-hooks/`, and git reads **every** hook from
476
+ that one directory — not just the two events this tool gates. So adopting a repository that
477
+ had its own `post-commit`, `post-checkout` or `post-merge` would have quietly stopped them:
478
+ nothing fails, nothing is logged, and the hook is still on disk looking intact.
479
+
480
+ Those hooks are now **chained**. Install writes a shim for them that runs the repository's
481
+ own hook and forwards git's arguments, so nothing is lost and the reason is in the file. If
482
+ the hook is gone, the shim refuses and names the path instead of passing quietly. A hook on
483
+ an event this tool *does* gate (`pre-commit`, `pre-push`) is deliberately not chained —
484
+ declare it under `gates.commands` so the dispatcher runs it and its verdict appears in the
485
+ gate report. *(ADR-018, as amended)*
486
+
487
+ ### What a first release has to say out loud
488
+
489
+ These are the behaviours a reader is most likely to trip over. Each one is deliberate,
490
+ and each has a numbered decision record behind it — but a behaviour nobody can discover
491
+ without reading the source is not a documented one.
492
+
493
+ - **`sync` and `init` write nothing without `--yes`.** Both print a plan and exit. `sync`
494
+ exits **1** when the plan is non-empty, so bare `sync` doubles as a check. Neither ever
495
+ commits or pushes. *(ADR-035, ADR-036)*
496
+ - **`upgrade` moves a pin and nothing else.** It never adopts a repository, never repairs
497
+ one, and never rewrites a manifest. Every consumer moves to the same version as one set.
498
+ - **A pre-commit hook with no reachable binary fails closed.** The refusal is the correct
499
+ behaviour, not a bug; the remedy it prints is the command that obtains one.
500
+ *(ADR-010, ADR-018)*
501
+ - **A gate whose tool is not installed is dropped, with a remedy naming the package.**
502
+ It is never declared and silently unrunnable. *(ADR-017)*
503
+ - **Two rules are never loaded twice.** Each rule projects to exactly one place, chosen by
504
+ what the harness actually reads. *(ADR-021, ADR-046)*
505
+ - **macOS and Linux only.** Windows `npm i` fails with npm's `EBADPLATFORM`, which npm
506
+ will not let a package explain; use WSL2. See the platform section of the README.
507
+ *(ADR-012)*
508
+
509
+ ### Packaging
510
+
511
+ - Licence set to Apache-2.0; the `LICENSE` file is the canonical, unmodified text.
512
+ - `files` no longer lists `skills`, `agents`, `tooling` or `hooks`, none of which exist,
513
+ and no longer ships `docs/plans`. The tarball is the tool and the decisions behind it,
514
+ and nothing else.
515
+ - The package `description` no longer names an internal ecosystem. It is the first line
516
+ anybody outside the project reads.
517
+ - Unscoped, so the install command is `npm i -D simt-dev` rather than a name under an
518
+ organisation. The `@simt` and `@quazarmedia` scopes were both free; an unscoped name is
519
+ the one a reader can guess.
520
+
521
+ ### Notes for anyone upgrading a hook
522
+
523
+ A shim installed by an earlier version looks for `SIMT_DEV_BIN`, then for
524
+ `node_modules/simt-dev/bin/simt-dev.mjs`, then for a program named `simt` on `PATH`. The
525
+ last of those is new, so a machine that installed the package before this release and
526
+ never ran `simt hooks install` again will find nothing until the dependency is refreshed
527
+ and `simt hooks install --yes` is re-run. That is a real upgrade step and it is called out
528
+ here rather than discovered.
529
+
530
+ [0.2.2]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.2.2
531
+ [0.2.1]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.2.1
532
+ [0.2.0]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.2.0
533
+ [0.1.1]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.1.1
534
+ [0.1.0]: https://github.com/QuazarMedia/simt-dev/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: simt-dev
3
- Version: 0.2.0
3
+ Version: 0.2.2
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