@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.30 → 0.1.32

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 (192) hide show
  1. package/bin/adl +2 -2
  2. package/bin/health-check +2 -2
  3. package/bin/reconcile +41 -21
  4. package/bin/setup +2 -1
  5. package/package.json +1 -1
  6. package/packages/agent-hooks/README.md +77 -8
  7. package/packages/agent-hooks/assets/opencode-plugin.template.ts +2 -6
  8. package/packages/agent-hooks/src/deploy/opencode-template.ts +5 -3
  9. package/packages/agent-hooks/src/hooks/crg/background.ts +7 -4
  10. package/packages/agent-hooks/src/hooks/log.ts +12 -12
  11. package/packages/agent-hooks/src/log/writer.ts +170 -24
  12. package/packages/agent-hooks/src/main.ts +1 -1
  13. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
  14. package/packages/agent-hooks/src/rules/shell/invocation.ts +2 -1
  15. package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +24 -2
  16. package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +19 -45
  17. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +9 -0
  18. package/packages/agent-skills/assets/diagnosing-bugs/scripts/hitl-loop.template.sh +1 -12
  19. package/packages/fleet/manifest.json +2 -1
  20. package/packages/fleet/src/repo-maintain.ts +5 -2
  21. package/packages/repo-controls/AGENTS.md +7 -4
  22. package/packages/repo-controls/README.md +346 -56
  23. package/packages/repo-controls/assets/GLOBAL-INVARIANTS.md +21 -0
  24. package/packages/repo-controls/assets/promote.ts +7 -85
  25. package/packages/repo-controls/assets/reconcile-guard.sh +1 -14
  26. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +25 -0
  27. package/packages/repo-controls/assets/scaffold/docs/CLAUDE.md +1 -0
  28. package/packages/repo-controls/assets/scaffold/docs/decisions/index.md +17 -0
  29. package/packages/repo-controls/assets/scaffold/docs/notes.md +20 -0
  30. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +28 -0
  31. package/packages/repo-controls/assets/scaffold/plans/CLAUDE.md +1 -0
  32. package/packages/repo-controls/assets/scaffold/plans/GAPS-AND-FINDINGS.md +12 -0
  33. package/packages/repo-controls/assets/scaffold/plans/LEDGER.md +8 -0
  34. package/packages/repo-controls/assets/scaffold/plans/features/.gitkeep +0 -0
  35. package/packages/repo-controls/assets/scaffold/root/AGENTS.md +22 -0
  36. package/packages/repo-controls/assets/scaffold/root/CLAUDE.md +1 -0
  37. package/packages/repo-controls/assets/scaffold/workflows/AGENTS.md +16 -0
  38. package/packages/repo-controls/assets/scaffold/workflows/CLAUDE.md +1 -0
  39. package/packages/repo-controls/assets/scaffold/workflows/pipelines/.gitkeep +0 -0
  40. package/packages/repo-controls/assets/scaffold/workflows/scripts/.gitkeep +0 -0
  41. package/packages/repo-controls/assets/version-bump.sh +7 -48
  42. package/packages/repo-controls/package/adl-compliance/AGENTS.md +25 -7
  43. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +6 -4
  44. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/check.py +1 -1
  45. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +1 -1
  46. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +7 -42
  47. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/excluded/web/i18n/locales/zh-CN/strings.ts +1 -0
  48. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +10 -44
  49. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +1 -1
  50. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +1 -1
  51. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +1 -1
  52. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/check.py +1 -7
  53. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/test_check.py +12 -3
  54. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +15 -23
  55. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +0 -136
  56. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +18 -0
  57. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +2 -2
  58. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +0 -7
  59. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/check.py +2 -4
  60. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-delivery.md +10 -0
  61. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-recommendation.md +12 -0
  62. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/target-narration.md +10 -0
  63. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +6 -7
  64. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/test_check.py +3 -3
  65. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +10 -12
  66. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +1 -1
  67. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/web/src/renderer/solo/item.ts +1 -0
  68. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/web/src/a.ts +1 -0
  69. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/web/src/b.ts +1 -0
  70. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +1 -1
  71. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/fresh/fresh.ts +1 -0
  72. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub1/only.ts +1 -0
  73. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub10/only.ts +1 -0
  74. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub11/only.ts +1 -0
  75. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub2/only.ts +1 -0
  76. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub3/only.ts +1 -0
  77. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub4/only.ts +1 -0
  78. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub5/only.ts +1 -0
  79. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub6/only.ts +1 -0
  80. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub7/only.ts +1 -0
  81. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub8/only.ts +1 -0
  82. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub9/only.ts +1 -0
  83. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/services/i18n/locales/de/common.json +1 -0
  84. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/solo/item.ts +1 -0
  85. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/vendored-crate/Cargo.toml +2 -0
  86. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +1 -1
  87. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/web/src/renderer/solo/item.ts +1 -0
  88. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +1 -1
  89. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/a.ts +1 -0
  90. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/b.ts +1 -0
  91. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/c.ts +1 -0
  92. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +2 -2
  93. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/web/src/renderer/solo/item.ts +1 -0
  94. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +1 -1
  95. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/fresh/fresh.ts +1 -0
  96. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub1/only.ts +1 -0
  97. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub10/only.ts +1 -0
  98. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub11/only.ts +1 -0
  99. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub2/only.ts +1 -0
  100. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub3/only.ts +1 -0
  101. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub4/only.ts +1 -0
  102. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub5/only.ts +1 -0
  103. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub6/only.ts +1 -0
  104. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub7/only.ts +1 -0
  105. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub8/only.ts +1 -0
  106. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub9/only.ts +1 -0
  107. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/solo/item.ts +1 -0
  108. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unusable-register/web/src/renderer/solo/item.ts +1 -0
  109. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +46 -54
  110. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +16 -76
  111. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +35 -32
  112. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +46 -213
  113. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/README.md +1 -0
  114. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +76 -194
  115. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +18 -2
  116. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +49 -0
  117. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +1 -1
  118. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/links.md +20 -0
  119. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/route-target.md +8 -0
  120. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/links.md +20 -0
  121. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/route-target.md +8 -0
  122. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/test_check.py +4 -4
  123. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +3 -3
  124. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.notes/declared.txt +1 -0
  125. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +8 -1
  126. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +15 -135
  127. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +98 -50
  128. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +1 -1
  129. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +1 -1
  130. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +1 -1
  131. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +14 -10
  132. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/__init__.py +3 -10
  133. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/base.py +42 -0
  134. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_check.py +82 -49
  135. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_scope.py +71 -0
  136. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/__init__.py +7 -0
  137. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/check.py +15 -0
  138. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/test_check.py +32 -0
  139. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +1 -1
  140. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +56 -124
  141. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.lock +7 -0
  142. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.toml +4 -0
  143. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/bun.lock +1 -0
  144. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/package.json +1 -0
  145. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/rust-toolchain.toml +3 -0
  146. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/src/main.rs +1 -0
  147. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.lock +7 -0
  148. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.toml +4 -0
  149. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/bun.lock +0 -0
  150. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/package.json +1 -0
  151. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/rust-toolchain.toml +3 -0
  152. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/src/main.rs +1 -0
  153. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +8 -37
  154. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +95 -130
  155. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_root_lockfiles.py +101 -0
  156. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +25 -13
  157. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/__init__.py +7 -0
  158. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/check.py +124 -0
  159. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/fixtures/valid-register.yaml +4 -0
  160. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/scanner.py +217 -0
  161. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/test_check.py +391 -0
  162. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/__init__.py +7 -0
  163. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/check.py +16 -0
  164. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/test_check.py +32 -0
  165. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +12 -1
  166. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/ast_grep.py +2 -3
  167. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +3 -0
  168. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +1 -1
  169. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/declarations.py +24 -0
  170. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/receipt.py +2 -2
  171. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/scripts.py +120 -0
  172. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -1
  173. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.toml +4 -4
  174. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +93 -99
  175. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +93 -99
  176. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
  177. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +6 -47
  178. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +54 -8
  179. package/packages/repo-controls/scripts/hooks/pre-commit +12 -2
  180. package/packages/repo-controls/scripts/hooks/pre-push +27 -19
  181. package/packages/repo-controls/src/cli.ts +22 -3
  182. package/packages/repo-controls/src/compliance-config.ts +132 -65
  183. package/packages/repo-controls/src/config.ts +55 -46
  184. package/packages/repo-controls/src/constants.ts +9 -0
  185. package/packages/repo-controls/src/coverage-ratchet.ts +19 -4
  186. package/packages/repo-controls/src/doctor.ts +20 -30
  187. package/packages/repo-controls/src/findings-ratchet.ts +14 -0
  188. package/packages/repo-controls/src/gitignore-block.ts +3 -13
  189. package/packages/repo-controls/src/hook-log.ts +7 -10
  190. package/packages/repo-controls/src/hook.ts +127 -83
  191. package/packages/repo-controls/src/install-compliance.ts +112 -0
  192. package/packages/repo-controls/src/install.ts +133 -30
@@ -7,6 +7,7 @@ import { logHookEvent } from './hook-log.ts'
7
7
  import { isHookName, isToolName, runHook, runTool, TOOLS } from './hook.ts'
8
8
  import { resolveRepo } from './runtime.ts'
9
9
  import { type Action, install, uninstall } from './install.ts'
10
+ import { gradeInstalledRepo, UvMissingError } from './install-compliance.ts'
10
11
  import type { Env } from './runtime.ts'
11
12
 
12
13
  export interface Io {
@@ -17,7 +18,8 @@ export interface Io {
17
18
  }
18
19
 
19
20
  const USAGE = `usage:
20
- ${TOOL} install [--repo <path>] [--dry-run]
21
+ ${TOOL} install [--repo <path>] [--dry-run] [--json] [--no-grade] [--rewire]
22
+ ${TOOL} grade [--repo <path>] (prints the compliance verdict as JSON; exit 0 whatever the verdict)
21
23
  ${TOOL} uninstall [--repo <path>] [--dry-run]
22
24
  ${TOOL} doctor [--repo <path>] [--json]
23
25
  ${TOOL} hook <${HOOK_NAMES.join('|')}> [git hook args...]
@@ -82,8 +84,14 @@ async function dispatch(argv: string[], io: Io): Promise<number> {
82
84
  io.out(USAGE)
83
85
  return 0
84
86
  case 'install': {
85
- const parsed = parse(command, args, ['--dry-run'])
86
- const result = install(parsed.repo, { dryRun: parsed.flags.has('--dry-run') })
87
+ const parsed = parse(command, args, ['--dry-run', '--json', '--no-grade', '--rewire'])
88
+ const rewire = parsed.flags.has('--rewire')
89
+ const grade = parsed.flags.has('--no-grade') || rewire ? {} : { grade: { env: io.env } }
90
+ const result = install(parsed.repo, { dryRun: parsed.flags.has('--dry-run'), rewire, ...grade })
91
+ if (parsed.flags.has('--json')) {
92
+ io.out(JSON.stringify(result))
93
+ return 0
94
+ }
87
95
  printActions(io, result.actions, result.dryRun)
88
96
  // Independent review, 2026-09-29, finding 8: a genuine no-op run (and
89
97
  // any --dry-run preview) never claims to have "installed" anything —
@@ -91,6 +99,17 @@ async function dispatch(argv: string[], io: Io): Promise<number> {
91
99
  if (!result.dryRun && result.actions.length > 0) {
92
100
  io.out(`${TOOL} ${VERSION} installed into ${result.repo}`)
93
101
  }
102
+ for (const line of result.compliance?.guidance ?? []) io.out(line)
103
+ return 0
104
+ }
105
+ case 'grade': {
106
+ const parsed = parse(command, args, [])
107
+ try {
108
+ io.out(JSON.stringify(gradeInstalledRepo(resolveRepo(parsed.repo), io.env)))
109
+ } catch (error) {
110
+ if (!(error instanceof UvMissingError)) throw error
111
+ io.out(JSON.stringify({ ok: false, skipped: 'uv-missing', enabled: [], failing: [], guidance: [error.message] }))
112
+ }
94
113
  return 0
95
114
  }
96
115
  case 'uninstall': {
@@ -1,4 +1,4 @@
1
- import { existsSync, lstatSync, readFileSync } from 'node:fs'
1
+ import { existsSync, readFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
3
  import { isTable, Problems, type Table } from './config.ts'
4
4
  import { CONFIG_REL, INSTALL_HINT } from './constants.ts'
@@ -41,6 +41,8 @@ export interface ComplianceConfig {
41
41
  * non-empty `roots` into one `--declare markdown-roots=a,b` flag.
42
42
  */
43
43
  markdown?: MarkdownDeclaration
44
+ /** LAY-008's extra asset directories from `[controls.compliance.placement]`; omitted when undeclared. */
45
+ placement?: PlacementDeclaration
44
46
  /**
45
47
  * The manifest and lockfile paths REP-002 scans and grades, from
46
48
  * `[controls.compliance.lockfiles]`. OMITTED (not `undefined`-valued) when the
@@ -138,61 +140,69 @@ interface LimitsDeclaration {
138
140
  *
139
141
  * Nested under `controls`, not a root-level `[compliance]`: see `OWNED_TABLES`.
140
142
  */
141
- const ENABLED_SEED_COMMENT = `# Nothing is graded until a gate is named here. A gate id (RST-001), a
142
- # domain.name address (layout.references), or a whole domain (layout); an entry
143
- # matching no gate is a hard error, never a silent no-op. Point
144
- # [controls.compliance.trees] at this repository's layout before enabling a row —
145
- # the packaged defaults are shaped after the repo these checks were authored
146
- # against, so an enabled row with unpointed trees grades the wrong files.`
147
-
148
- const SEEDED_COMPLIANCE_ATTRIBUTES: SeededAttribute[] = [
149
- {
150
- key: 'enabled',
151
- text: `${ENABLED_SEED_COMMENT}
152
- enabled = []
143
+ const TREES_SEED: SeededAttribute = {
144
+ key: 'trees',
145
+ text: `
146
+ # docs, plans, workflows and compliance are the four adl standard trees. Edit them
147
+ # if this repository's layout differs; "" declares "this repository has no such
148
+ # tree". compliance is the register folder: it appears when the first ratchet
149
+ # or exemption register is written, not at install. Pre-commit protects it.
150
+ [controls.compliance.trees]
151
+ docs = "docs"
152
+ plans = "plans"
153
+ workflows = "workflows"
154
+ compliance = "compliance"
153
155
  `,
154
- },
155
- ]
156
+ }
156
157
 
157
- /**
158
- * Seeded by `install` when `[controls.compliance]` is missing; per-attribute
159
- * afterwards, like `[controls]`. `registry` and `[controls.compliance.trees]` are
160
- * deliberately NOT seeded: both have working defaults, and writing an empty table
161
- * into every repository is noise that says nothing.
162
- */
163
- export const DEFAULT_COMPLIANCE_CONFIG_TEXT = `[controls.compliance]\n${SEEDED_COMPLIANCE_ATTRIBUTES.map((a) => a.text).join('')}`
158
+ // Every gate in the engine registry, one domain.name address each (strict-gates-registry.test.ts keeps them equal); see README, "Compliance seeding".
159
+ export const STRICT_ENABLED_GATES = [
160
+ 'docs.adr-lifecycle',
161
+ 'docs.english-only',
162
+ 'docs.index-updated',
163
+ 'docs.markdown-ast',
164
+ 'docs.okf-profile',
165
+ 'docs.semantics',
166
+ 'docs.indexes',
167
+ 'docs.ledger',
168
+ 'docs.closed-set',
169
+ 'docs.filename',
170
+ 'docs.directory-size',
171
+ 'layout.agents-layout',
172
+ 'layout.ci-paths',
173
+ 'layout.directory-ratchet',
174
+ 'layout.markdown-trees',
175
+ 'layout.placement',
176
+ 'layout.plans-layout',
177
+ 'layout.references',
178
+ 'layout.root-closed-set',
179
+ 'layout.root-markdown-allowlist',
180
+ 'layout.scripts-size-depth',
181
+ 'layout.open-work-cap',
182
+ 'lfs.integrity',
183
+ 'repository.license-declared',
184
+ 'repository.lockfile-integrity',
185
+ 'repository.no-legacy-search',
186
+ 'repository.ignored-paths',
187
+ 'repository.no-vcs-in-scripts',
188
+ 'repository.no-multiline-comments',
189
+ ] as const
164
190
 
165
- /**
166
- * `install`'s repo-aware `enabled` seed (gate-integrity finding, 2026-09-17:
167
- * "`docs.ledger` is armed in one repository only", closed 2026-09-27). A
168
- * repository that already has a `plans/` tree gets `docs.ledger` pre-armed —
169
- * the ledger contract (DOC-009) a `plans/` tree implies is never left for
170
- * someone to notice missing later. Still only ever scaffolds a MISSING
171
- * `enabled` key (installConfigTable's own "existing[key] === undefined"
172
- * gate) — this never rewrites a table the repository already owns, exactly
173
- * like every other seeded attribute.
174
- */
175
- export function seededComplianceAttributesFor(repo: string): SeededAttribute[] {
176
- const plansPath = join(repo, 'plans')
177
- const hasPlans = existsSync(plansPath) && lstatSync(plansPath).isDirectory()
178
- if (!hasPlans) return SEEDED_COMPLIANCE_ATTRIBUTES
179
- return [
180
- {
181
- key: 'enabled',
182
- text: `${ENABLED_SEED_COMMENT}
183
- # This repository has a \`plans/\` tree, so \`docs.ledger\` is pre-armed: a
184
- # ledger contract with nowhere to enforce it is worse than none (gate-integrity
185
- # finding, 2026-09-17).
186
- enabled = ["docs.ledger"]
187
- `,
188
- },
189
- ]
190
- }
191
+ const ENABLED_SEED_TEXT = `# Strict by default (Bug 570): every gate the engine defines is armed from the first commit.
192
+ # To switch a gate off, delete its line; that is this repository's decision, and adl-config.toml
193
+ # is a protected file landed by the owner. A gate id (RST-001), a domain.name address
194
+ # (layout.references) or a whole domain (layout) is accepted; an entry matching no gate is a
195
+ # hard error, never a silent no-op.
196
+ enabled = [
197
+ ${STRICT_ENABLED_GATES.map((gate) => ` "${gate}",`).join('\n')}
198
+ ]
199
+ `
191
200
 
192
- /** `[controls.compliance]`'s default text for a specific repository — see `seededComplianceAttributesFor`. */
193
- export function defaultComplianceConfigTextFor(repo: string): string {
194
- return `[controls.compliance]\n${seededComplianceAttributesFor(repo).map((a) => a.text).join('')}`
195
- }
201
+ /** Exported: `install` seeds each attribute the repository's `[controls.compliance]` lacks. */
202
+ export const SEEDED_COMPLIANCE_ATTRIBUTES: SeededAttribute[] = [{ key: 'enabled', text: ENABLED_SEED_TEXT }, TREES_SEED]
203
+
204
+ /** Seeded by `install` when `[controls.compliance]` is missing; see README, "Compliance seeding". */
205
+ export const DEFAULT_COMPLIANCE_CONFIG_TEXT = `[controls.compliance]\n${SEEDED_COMPLIANCE_ATTRIBUTES.map((a) => a.text).join('')}`
196
206
 
197
207
  const ENABLED_RE = /^\S+$/
198
208
 
@@ -216,13 +226,11 @@ export function parseComplianceConfig(text: string): ComplianceConfig {
216
226
  const problems = new Problems()
217
227
  problems.unknownKeys(
218
228
  compliance,
219
- ['enabled', 'registry', 'trees', 'inputs', 'license', 'root', 'limits', 'markdown', 'lockfiles', 'directory-ratchet'],
229
+ ['enabled', 'registry', 'trees', 'inputs', 'license', 'root', 'limits', 'markdown', 'placement', 'lockfiles', 'directory-ratchet'],
220
230
  '',
221
231
  )
222
232
 
223
- // Absent is the documented default, never an error: install seeds only `version`
224
- // and `enabled`, and `registry`/`trees` both have working defaults. Present but the
225
- // wrong shape is still a hard error.
233
+ // Absent is the documented default, never an error; present but the wrong shape is a hard error (README, "Compliance seeding").
226
234
  const enabled = compliance.enabled === undefined ? [] : parseEnabled(compliance.enabled, problems)
227
235
  const registry = compliance.registry === undefined ? 'default' : parseRegistry(compliance.registry, problems)
228
236
  const trees = compliance.trees === undefined ? {} : parseTrees(compliance.trees, problems, 'trees', true)
@@ -245,6 +253,7 @@ export function parseComplianceConfig(text: string): ComplianceConfig {
245
253
  if (compliance.root !== undefined) config.root = parseRoot(compliance.root, problems)
246
254
  if (compliance.limits !== undefined) config.limits = parseLimits(compliance.limits, problems)
247
255
  if (compliance.markdown !== undefined) config.markdown = parseMarkdown(compliance.markdown, problems)
256
+ if (compliance.placement !== undefined) config.placement = parsePlacement(compliance.placement, problems)
248
257
  if (compliance.lockfiles !== undefined) {
249
258
  config.lockfiles = parseLockfiles(compliance.lockfiles, problems)
250
259
  rejectSupersededInputs(config.lockfiles, inputs, problems)
@@ -318,15 +327,13 @@ function parseTrees(
318
327
  return trees
319
328
  }
320
329
 
321
- /**
322
- * A root entry is a NAME directly under the repository root, and the whole list
323
- * travels to the engine as ONE comma-separated `--declare root-files=a,b` value.
324
- * So a name may not contain `,` (it would split into two names), `=` (it collides
325
- * with `--declare name=value`'s own separator) or `/` and `..` (a root name is not
326
- * a path: either would escape the root or reach below it). Empty and
327
- * whitespace-only names are refused because the engine rejects an empty item.
328
- */
330
+ /** Root entry rules (dirs are bare names, files may be relative paths); see README, "Root declarations". */
329
331
  const ROOT_NAME_FORBIDDEN = /[,/=]|\.\./
332
+ const ROOT_FILE_FORBIDDEN = /[,=*?[\]{}\\]/
333
+
334
+ function isRootFilePath(item: string): boolean {
335
+ return !ROOT_FILE_FORBIDDEN.test(item) && item.split('/').every((segment) => segment !== '' && segment !== '.' && segment !== '..')
336
+ }
330
337
 
331
338
  function parseRoot(value: unknown, problems: Problems): RootDeclaration {
332
339
  const root: RootDeclaration = { files: [], dirs: [] }
@@ -346,7 +353,9 @@ function parseRoot(value: unknown, problems: Problems): RootDeclaration {
346
353
  const label = `root.${key}[${index}]`
347
354
  if (typeof item !== 'string') problems.add(`${label} must be a string`)
348
355
  else if (item.trim() === '') problems.add(`${label} must not be empty`)
349
- else if (ROOT_NAME_FORBIDDEN.test(item)) problems.add(`${label} must not contain ',', '/', '=' or '..'`)
356
+ else if (key === 'files' && !isRootFilePath(item)) {
357
+ problems.add(`${label} must be a repository-relative path without a leading '/', a '.', '..' or empty segment, a glob character, ',' or '='`)
358
+ } else if (key === 'dirs' && ROOT_NAME_FORBIDDEN.test(item)) problems.add(`${label} must not contain ',', '/', '=' or '..'`)
350
359
  else root[key].push(item)
351
360
  })
352
361
  }
@@ -394,6 +403,46 @@ function parseLimits(value: unknown, problems: Problems): LimitsDeclaration {
394
403
  */
395
404
  const MARKDOWN_ROOT_FORBIDDEN = /[,=]/
396
405
 
406
+ interface PlacementDeclaration {
407
+ asset_dirs: string[]
408
+ }
409
+
410
+ /** `[controls.compliance.placement].asset_dirs`: repository-relative directories, validated like markdown roots (no leading '/', no '.', '..' or empty segment, no ',' or '='). */
411
+ function parsePlacement(value: unknown, problems: Problems): PlacementDeclaration {
412
+ const placement: PlacementDeclaration = { asset_dirs: [] }
413
+ if (!isTable(value)) {
414
+ problems.add('placement must be a table')
415
+ return placement
416
+ }
417
+ problems.unknownKeys(value, ['asset_dirs'], 'placement.')
418
+ const list = value.asset_dirs
419
+ if (list === undefined) return placement
420
+ if (!Array.isArray(list)) {
421
+ problems.add('placement.asset_dirs must be an array of strings')
422
+ return placement
423
+ }
424
+ list.forEach((item: unknown, index) => {
425
+ const label = `placement.asset_dirs[${index}]`
426
+ if (typeof item !== 'string') {
427
+ problems.add(`${label} must be a string`)
428
+ return
429
+ }
430
+ const trimmed = item.trim()
431
+ if (trimmed === '') {
432
+ problems.add(`${label} must not be empty`)
433
+ return
434
+ }
435
+ const dir = trimmed.replace(/\/+$/, '')
436
+ const bad = trimmed.startsWith('/') || dir.split('/').some((part) => part === '' || part === '.' || part === '..')
437
+ if (MARKDOWN_ROOT_FORBIDDEN.test(dir) || bad) {
438
+ problems.add(`${label} must be a repository-relative directory without a leading '/', a '.', '..' or empty segment, ',' or '='`)
439
+ return
440
+ }
441
+ placement.asset_dirs.push(dir)
442
+ })
443
+ return placement
444
+ }
445
+
397
446
  function parseMarkdown(value: unknown, problems: Problems): MarkdownDeclaration {
398
447
  const markdown: MarkdownDeclaration = { roots: [] }
399
448
  if (!isTable(value)) {
@@ -601,6 +650,24 @@ function rejectSupersededInputs(
601
650
  }
602
651
  }
603
652
 
653
+ /** The repo-relative folder `[controls.compliance.trees].compliance` declares; null when there is no such table, key or the value is `""`. A non-string value throws. */
654
+ export function declaredComplianceTree(configText: string): string | null {
655
+ let raw: unknown
656
+ try {
657
+ raw = Bun.TOML.parse(configText)
658
+ } catch (error) {
659
+ throw new Error(`${CONFIG_REL} is not valid TOML: ${(error as Error).message}`)
660
+ }
661
+ const controls = isTable(raw) && isTable(raw.controls) ? raw.controls : undefined
662
+ const compliance = controls !== undefined && isTable(controls.compliance) ? controls.compliance : undefined
663
+ const trees = compliance !== undefined && isTable(compliance.trees) ? compliance.trees : undefined
664
+ const declared = trees?.compliance
665
+ if (declared === undefined) return null
666
+ if (typeof declared !== 'string') throw new Error(`${CONFIG_REL} [controls.compliance.trees] compliance must be a string`)
667
+ const folder = declared.replace(/^(\.\/)+/, '').replace(/\/+$/, '')
668
+ return folder === '' ? null : folder
669
+ }
670
+
604
671
  export function loadComplianceConfig(repo: string): ComplianceConfig {
605
672
  const path = join(repo, CONFIG_REL)
606
673
  if (!existsSync(path)) throw new Error(`${CONFIG_REL} is missing — ${INSTALL_HINT}`)
@@ -79,7 +79,7 @@ export interface RepoControlsConfig {
79
79
  branches: { protected: string[]; prefixes: string[] }
80
80
  preCommit: { staticChecks: StaticCheck[]; stagedCoverage: boolean; docFreshness: boolean; compliance: boolean }
81
81
  commitMsg: { staticChecks: StaticCheck[] }
82
- release: { source: string; target: string; mergeStrategy: MergeStrategy; preBump: string[] }
82
+ release: { optedOut: boolean; source: string; target: string; mergeStrategy: MergeStrategy; preBump: string[] }
83
83
  postCommit: { staticChecks: StaticCheck[] }
84
84
  prePush: {
85
85
  coverageRatchet: boolean
@@ -101,24 +101,7 @@ export interface RepoControlsConfig {
101
101
  * `extractTableSpan`/`removeTableSpan` — so it must start with the
102
102
  * `[controls]` header line and never contain another line starting with `[`.
103
103
  */
104
- /**
105
- * The `[controls]` attributes `install` SCAFFOLDS, each independently.
106
- *
107
- * Install wires the hooks; it does not arm a gate. Only the three attributes below
108
- * are written, and only when absent — an attribute already in the file is never
109
- * rewritten, reordered or reformatted, whatever its value. Everything else
110
- * (`pre_commit.*`, `pre_push.coverage_ratchet`, `pre_push.findings_ratchet`,
111
- * `pre_push.static_checks`) is omitted and defaults to off, so a first install
112
- * changes what no `git push` or `git commit` does until somebody opts in.
113
- *
114
- * `branches` and `pre_push.remote` are seeded rather than omitted because they are
115
- * not gates — they are the vocabulary the gates are phrased in — and because
116
- * `pre-push` reads their env vars with `${VAR:?}`, which aborts on an empty value.
117
- * "No branch policy" therefore cannot be spelled as an empty list.
118
- *
119
- * Granularity is the top-level key inside `[controls]`: an inline table is one
120
- * attribute, because editing inside one means rewriting bytes the user owns.
121
- */
104
+ /** The `[controls]` attributes `install` scaffolds when absent; rationale in README, "No silent skips". */
122
105
  export interface SeededAttribute {
123
106
  key: string
124
107
  text: string
@@ -130,23 +113,40 @@ export const SEEDED_ATTRIBUTES: SeededAttribute[] = [
130
113
  text: `
131
114
  # pre-push refuses to push while one of these branches is checked out; prefixes: <prefix>/<kebab>.
132
115
  branches = { protected = ["main", "integration"], prefixes = ["feat", "fix", "docs", "chore"] }
116
+ `,
117
+ },
118
+ {
119
+ key: 'pre_commit',
120
+ text: `
121
+ # Every gate key is declared; a missing key is a config error. Switch a gate off with an explicit false.
122
+ pre_commit = { staged_coverage = true, doc_freshness = true, compliance = true }
133
123
  `,
134
124
  },
135
125
  {
136
126
  key: 'pre_push',
137
127
  text: `
138
128
  # Remote that pre-push and azdo-push push to and read status from.
139
- #
140
- # Every gate is OFF until its key is added here: installing wires the hooks and arms
141
- # nothing. Opt in by adding pre_commit (static_checks, staged_coverage, doc_freshness) or the
142
- # remaining pre_push keys (coverage_ratchet, coverage_produce, findings_ratchet, static_checks). README,
143
- # "Configuration", lists each one. Deliberately no example assignments in this comment — commented TOML
144
- # is indistinguishable from a real setting to find-and-replace, in an editor or a script.
145
- pre_push = { remote = "origin" }
129
+ pre_push = { remote = "origin", coverage_ratchet = true, coverage_produce = false, findings_ratchet = true }
130
+ `,
131
+ },
132
+ {
133
+ key: 'release',
134
+ text: `
135
+ # Promotion governance is opted out. To promote through a PR, replace this line with a [controls.release] table declaring source and target.
136
+ release = false
146
137
  `,
147
138
  },
148
139
  ]
149
140
 
141
+ /** The `[controls] integrity` seed for a compliance register folder (README, "Compliance seeding"); appended after `SEEDED_ATTRIBUTES`. */
142
+ export function integritySeed(folder: string): SeededAttribute {
143
+ if (!/^[A-Za-z0-9._/-]+$/.test(folder)) throw new Error(`integrity seed folder has unsafe characters: ${folder}`)
144
+ return {
145
+ key: 'integrity',
146
+ text: `\n# Pre-commit refuses a commit touching the compliance register folder; the owner lands it.\nintegrity = { watched = ["${folder}/**"] }\n`,
147
+ }
148
+ }
149
+
150
150
  /**
151
151
  * Seeded by `install` as the `[controls]` table of the shared `adl-config.toml`
152
152
  * when that table is missing; user-owned afterwards. When the table already exists,
@@ -261,14 +261,14 @@ export function extractTableSpan(text: string, tableName: string): string | null
261
261
  */
262
262
  function ownKeysEnd(found: TableRanges): { offset: number; atRangeEnd: boolean } | null {
263
263
  if (found.headerLine === -1) return null
264
- const range = found.ranges.find((candidate) => candidate.startLine === found.headerLine)
264
+ const range = found.ranges.find((candidate) => candidate.startLine <= found.headerLine && found.headerLine < candidate.endLine)
265
265
  if (range === undefined) return null
266
- let sub = range.startLine + 1
266
+ let sub = found.headerLine + 1
267
267
  while (sub < range.endLine && !/^\[/.test(found.lines[sub] ?? '')) sub++
268
268
  if (sub >= range.endLine) return { offset: range.end, atRangeEnd: true }
269
269
  let at = sub
270
- while (at - 1 > range.startLine && (found.lines[at - 1] ?? '').trim().startsWith('#')) at--
271
- while (at - 1 > range.startLine && (found.lines[at - 1] ?? '').trim() === '') at--
270
+ while (at - 1 > found.headerLine && (found.lines[at - 1] ?? '').trim().startsWith('#')) at--
271
+ while (at - 1 > found.headerLine && (found.lines[at - 1] ?? '').trim() === '') at--
272
272
  return { offset: found.starts[at] ?? found.text.length, atRangeEnd: false }
273
273
  }
274
274
 
@@ -313,12 +313,12 @@ export const RETIRED_ATTRIBUTES = ['version'] as const
313
313
  export function removeFromTableSpan(text: string, tableName: string, key: string): string {
314
314
  const found = findTableRanges(text, tableName)
315
315
  const where = ownKeysEnd(found)
316
- const range = found.ranges.find((candidate) => candidate.startLine === found.headerLine)
317
- if (where === null || range === undefined) return text
318
- const body = text.slice(range.start, where.offset)
316
+ if (where === null) return text
317
+ const headerStart = found.starts[found.headerLine] ?? 0
318
+ const body = text.slice(headerStart, where.offset)
319
319
  const assignment = new RegExp(`^${escapeRegExp(key)}\\s*=.*\\n(\\n)?`, 'm')
320
320
  if (!assignment.test(body)) return text
321
- return text.slice(0, range.start) + body.replace(assignment, '') + text.slice(where.offset)
321
+ return text.slice(0, headerStart) + body.replace(assignment, '') + text.slice(where.offset)
322
322
  }
323
323
 
324
324
  /** `text` with every range of a table removed (a no-op if there are none); other tables are untouched. */
@@ -392,7 +392,7 @@ export class Problems {
392
392
  }
393
393
  /**
394
394
  * A table this package SEEDS but does not require. Absent means "every control in
395
- * it is off", never an error — installing wires the hooks and arms nothing, so a
395
+ * it is off", never an error — installing arms no push or commit gate, so a
396
396
  * repository that never opted into a control simply has no key for it. Present but
397
397
  * the wrong type is still a hard error, and unknown keys inside it still are too.
398
398
  */
@@ -400,6 +400,14 @@ export class Problems {
400
400
  if (parent[key] === undefined) return {}
401
401
  return this.table(parent, key, known)
402
402
  }
403
+ /** A gate key has no default: absent is a config error naming the key. */
404
+ requiredBool(table: Table, key: string, name: string): boolean {
405
+ if (table[key] === undefined) {
406
+ this.add(`${name} is required — declare ${name} = true or ${name} = false (a missing gate key is never defaulted)`)
407
+ return false
408
+ }
409
+ return this.bool(table, key, name)
410
+ }
403
411
  /** Absent is the documented default; present-but-not-a-boolean is still an error. */
404
412
  optionalBool(table: Table, key: string, name: string, fallback: boolean): boolean {
405
413
  if (table[key] === undefined) return fallback
@@ -481,8 +489,7 @@ export function parseConfig(text: string): RepoControlsConfig {
481
489
  const branches = problems.table(controls, 'branches', ['protected', 'prefixes'])
482
490
  // `branches` and `pre_push.remote` are REQUIRED: install always seeds them, and
483
491
  // `pre-push` reads their env vars with `${VAR:?}`, which aborts on an empty value —
484
- // so "no branch policy" cannot be spelled as an empty list. Everything else is
485
- // optional and defaults to off.
492
+ // so "no branch policy" cannot be spelled as an empty list.
486
493
  const preCommit = problems.optionalTable(controls, 'pre_commit', ['static_checks', 'staged_coverage', 'doc_freshness', 'compliance'])
487
494
  const prePush = problems.table(controls, 'pre_push', ['coverage_ratchet', 'coverage_produce', 'findings_ratchet', 'static_checks', 'export_links', 'remote'])
488
495
  // Optional, and every field defaults, so a repository that never writes this
@@ -520,7 +527,10 @@ export function parseConfig(text: string): RepoControlsConfig {
520
527
  // Which branch is promoted to which. No default pair: defaulting to
521
528
  // integration -> main would put one repository's branch names back into
522
529
  // this package by the back door.
523
- const release = problems.optionalTable(controls, 'release', ['source', 'target', 'merge_strategy', 'pre_bump'])
530
+ const releaseOptedOut = controls.release === false || (isTable(controls.release) && controls.release.enabled === false)
531
+ if (isTable(controls.release) && controls.release.enabled !== undefined && typeof controls.release.enabled !== 'boolean') problems.add('release.enabled must be a boolean')
532
+ if (controls.release === true) problems.add('release must be a table, or false to opt out of promotion governance')
533
+ const release = controls.release === false || controls.release === true ? {} : problems.optionalTable(controls, 'release', ['enabled', 'source', 'target', 'merge_strategy', 'pre_bump'])
524
534
  const releaseMergeStrategy = problems.optionalString(release, 'merge_strategy', 'release.merge_strategy', 'squash')
525
535
  if (!Object.hasOwn(MERGE_STRATEGIES, releaseMergeStrategy)) {
526
536
  problems.add(`release.merge_strategy must be one of ${Object.keys(MERGE_STRATEGIES).join(', ')}`)
@@ -536,17 +546,17 @@ export function parseConfig(text: string): RepoControlsConfig {
536
546
  // Same shape as `pre_push.static_checks`; this package never interprets
537
547
  // the command and names no tool.
538
548
  staticChecks: preCommit.static_checks === undefined ? [] : parseStaticChecks(preCommit.static_checks, problems, 'pre_commit.static_checks'),
539
- stagedCoverage: problems.optionalBool(preCommit, 'staged_coverage', 'pre_commit.staged_coverage', true),
540
- docFreshness: problems.optionalBool(preCommit, 'doc_freshness', 'pre_commit.doc_freshness', true),
541
- compliance: problems.optionalBool(preCommit, 'compliance', 'pre_commit.compliance', true),
549
+ stagedCoverage: problems.requiredBool(preCommit, 'staged_coverage', 'pre_commit.staged_coverage'),
550
+ docFreshness: problems.requiredBool(preCommit, 'doc_freshness', 'pre_commit.doc_freshness'),
551
+ compliance: problems.requiredBool(preCommit, 'compliance', 'pre_commit.compliance'),
542
552
  },
543
553
  prePush: {
544
- coverageRatchet: problems.optionalBool(prePush, 'coverage_ratchet', 'pre_push.coverage_ratchet', true),
554
+ coverageRatchet: problems.requiredBool(prePush, 'coverage_ratchet', 'pre_push.coverage_ratchet'),
545
555
  // New gates opt in: the coverage measurement moved to pre-push, so a
546
556
  // repository that never writes this key keeps measuring (or not) exactly
547
557
  // where it did before.
548
- coverageProduce: problems.optionalBool(prePush, 'coverage_produce', 'pre_push.coverage_produce', false),
549
- findingsRatchet: problems.optionalBool(prePush, 'findings_ratchet', 'pre_push.findings_ratchet', true),
558
+ coverageProduce: problems.requiredBool(prePush, 'coverage_produce', 'pre_push.coverage_produce'),
559
+ findingsRatchet: problems.requiredBool(prePush, 'findings_ratchet', 'pre_push.findings_ratchet'),
550
560
  staticChecks: prePush.static_checks === undefined ? [] : parseStaticChecks(prePush.static_checks, problems, 'pre_push.static_checks'),
551
561
  // Untracked paths the HEAD export needs, e.g. a dependency directory.
552
562
  // The hook used to hardcode `node_modules`, which assumed JavaScript in a
@@ -571,6 +581,7 @@ export function parseConfig(text: string): RepoControlsConfig {
571
581
  staticChecks: commitMsg.static_checks === undefined ? [] : parseStaticChecks(commitMsg.static_checks, problems, 'commit_msg.static_checks'),
572
582
  },
573
583
  release: {
584
+ optedOut: releaseOptedOut,
574
585
  source: problems.optionalString(release, 'source', 'release.source', ''),
575
586
  target: problems.optionalString(release, 'target', 'release.target', ''),
576
587
  // The MCP server's PR tool admits only Squash, and that enum is the
@@ -757,7 +768,5 @@ export function configEnv(repo: string, config: RepoControlsConfig): Record<stri
757
768
  .map(([name, command]) => `${name}\t${command}`)
758
769
  .join('\n'),
759
770
  }
760
- // enact-m5's pre-commit honours ADL_SKIP_STAGED_COVERAGE=1; disabled config sets it.
761
- if (!config.preCommit.stagedCoverage) env.ADL_SKIP_STAGED_COVERAGE = '1'
762
771
  return env
763
772
  }
@@ -69,6 +69,15 @@ export const VERSION_BUMP_REL = '.workflows/version-bump.sh'
69
69
  /** The standalone CI stage-2 guard: a PR branch must contain integration's tip and be at its version + 1 patch. */
70
70
  export const RECONCILE_GUARD_REL = '.workflows/reconcile-guard.sh'
71
71
 
72
+ /** The folder skeleton `install` bootstraps for the adl standard trees; see README, "Scaffolding". */
73
+ export const SCAFFOLD_ROOT = join(PACKAGE_ROOT, 'assets', 'scaffold')
74
+ export const SCAFFOLD_TREES = ['docs', 'plans', 'workflows'] as const
75
+ /** Repository-root entry files, scaffolded from `assets/scaffold/root/` when absent (never overwritten). */
76
+ export const SCAFFOLD_ROOT_FILES = ['AGENTS.md', 'CLAUDE.md'] as const
77
+ /** The invariants that apply to ALL repositories; install deploys it as `INVARIANTS.md` only when the target has none. */
78
+ export const GLOBAL_INVARIANTS_SOURCE = join(PACKAGE_ROOT, 'assets', 'GLOBAL-INVARIANTS.md')
79
+ export const GLOBAL_INVARIANTS_TARGET = 'INVARIANTS.md'
80
+
72
81
  export const MANAGED_ASSETS = [
73
82
  { rel: PROMOTE_REL, source: join(PACKAGE_ROOT, 'assets', 'promote.ts'), mode: 0o755, doctorId: 'promote' },
74
83
  {
@@ -105,10 +105,9 @@ export function readSummary(read: () => string): CoverageSummary {
105
105
  *
106
106
  * Two asymmetries remain deliberate:
107
107
  *
108
- * a file with NO baseline is not graded — new code cannot exceed a count it
109
- * never had, so enrolling a file is never blocked by this gate.
108
+ * a file with NO baseline is not graded, but `newlyBaselined` reports it so the enrolment is never silent.
110
109
  *
111
- * a file that DISAPPEARS is not graded — there is nothing left to be untested.
110
+ * a file that DISAPPEARS is not graded when it is gone from disk too; one still on disk fails.
112
111
  *
113
112
  * `srcTestRatio` is the exception: if the baseline reported one and the current
114
113
  * summary does not, that is a measurement that stopped happening, and silence
@@ -121,9 +120,18 @@ export function readSummary(read: () => string): CoverageSummary {
121
120
  * uncovered line and flags every time, where a percentage could absorb it in
122
121
  * rounding.
123
122
  */
124
- export function coverageRegressions(baseline: CoverageSummary, current: CoverageSummary): string[] {
123
+ export function coverageRegressions(
124
+ baseline: CoverageSummary,
125
+ current: CoverageSummary,
126
+ existsOnDisk: (path: string) => boolean = () => false,
127
+ ): string[] {
125
128
  const errors: string[] = []
126
129
 
130
+ for (const [path, prior] of Object.entries(baseline)) {
131
+ if (RESERVED.has(path) || !isFileSummary(prior) || path in current) continue
132
+ if (existsOnDisk(path)) errors.push(`${path}: dropped from the coverage summary but still exists on disk`)
133
+ }
134
+
127
135
  for (const [path, entry] of Object.entries(current)) {
128
136
  if (RESERVED.has(path) || !isFileSummary(entry)) continue
129
137
  const prior = baseline[path]
@@ -151,3 +159,10 @@ export function coverageRegressions(baseline: CoverageSummary, current: Coverage
151
159
  }
152
160
  return errors
153
161
  }
162
+
163
+ /** Files in `current` that have no baseline entry: they are baselined by this push, and said so. */
164
+ export function newlyBaselined(baseline: CoverageSummary, current: CoverageSummary): string[] {
165
+ return Object.entries(current)
166
+ .filter(([path, entry]) => !RESERVED.has(path) && isFileSummary(entry) && !isFileSummary(baseline[path]))
167
+ .map(([path]) => path)
168
+ }