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

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 (199) 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/adl-git-push-policy.ts +5 -4
  14. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
  15. package/packages/agent-hooks/src/rules/shell/invocation.ts +2 -1
  16. package/packages/agent-hooks/src/rules/shell/protected-carriers.ts +19 -5
  17. package/packages/agent-settings/assets/GLOBAL-AGENTS.md +1 -1
  18. package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +24 -2
  19. package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +19 -45
  20. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +9 -0
  21. package/packages/agent-skills/assets/diagnosing-bugs/scripts/hitl-loop.template.sh +1 -12
  22. package/packages/azdo/src/lib/commit-types.ts +8 -0
  23. package/packages/azdo/src/lib/validate-pr-metadata.ts +4 -1
  24. package/packages/fleet/manifest.json +2 -1
  25. package/packages/fleet/src/repo-maintain.ts +5 -2
  26. package/packages/repo-controls/AGENTS.md +8 -5
  27. package/packages/repo-controls/README.md +348 -58
  28. package/packages/repo-controls/assets/GLOBAL-INVARIANTS.md +21 -0
  29. package/packages/repo-controls/assets/promote.ts +7 -85
  30. package/packages/repo-controls/assets/reconcile-guard.sh +1 -14
  31. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +25 -0
  32. package/packages/repo-controls/assets/scaffold/docs/CLAUDE.md +1 -0
  33. package/packages/repo-controls/assets/scaffold/docs/decisions/index.md +17 -0
  34. package/packages/repo-controls/assets/scaffold/docs/notes.md +20 -0
  35. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +28 -0
  36. package/packages/repo-controls/assets/scaffold/plans/CLAUDE.md +1 -0
  37. package/packages/repo-controls/assets/scaffold/plans/GAPS-AND-FINDINGS.md +12 -0
  38. package/packages/repo-controls/assets/scaffold/plans/LEDGER.md +8 -0
  39. package/packages/repo-controls/assets/scaffold/plans/features/.gitkeep +0 -0
  40. package/packages/repo-controls/assets/scaffold/root/AGENTS.md +22 -0
  41. package/packages/repo-controls/assets/scaffold/root/CLAUDE.md +1 -0
  42. package/packages/repo-controls/assets/scaffold/workflows/AGENTS.md +16 -0
  43. package/packages/repo-controls/assets/scaffold/workflows/CLAUDE.md +1 -0
  44. package/packages/repo-controls/assets/scaffold/workflows/pipelines/.gitkeep +0 -0
  45. package/packages/repo-controls/assets/scaffold/workflows/scripts/.gitkeep +0 -0
  46. package/packages/repo-controls/assets/version-bump.sh +7 -48
  47. package/packages/repo-controls/package/adl-compliance/AGENTS.md +25 -7
  48. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +6 -4
  49. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/check.py +1 -1
  50. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +1 -1
  51. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +7 -42
  52. 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
  53. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +10 -44
  54. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +1 -1
  55. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +1 -1
  56. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +1 -1
  57. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/check.py +1 -7
  58. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/test_check.py +12 -3
  59. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +15 -23
  60. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +0 -136
  61. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +18 -0
  62. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +2 -2
  63. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +0 -7
  64. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/check.py +2 -4
  65. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-delivery.md +10 -0
  66. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-recommendation.md +12 -0
  67. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/target-narration.md +10 -0
  68. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +6 -7
  69. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/test_check.py +3 -3
  70. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +10 -12
  71. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +1 -1
  72. 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
  73. 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
  74. 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
  75. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +1 -1
  76. 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
  77. 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
  78. 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
  79. 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
  80. 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
  81. 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
  82. 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
  83. 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
  84. 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
  85. 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
  86. 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
  87. 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
  88. 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
  89. 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
  90. 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
  91. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +1 -1
  92. 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
  93. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +1 -1
  94. 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
  95. 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
  96. 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
  97. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +2 -2
  98. 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
  99. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +1 -1
  100. 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
  101. 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
  102. 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
  103. 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
  104. 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
  105. 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
  106. 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
  107. 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
  108. 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
  109. 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
  110. 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
  111. 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
  112. 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
  113. 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
  114. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +46 -54
  115. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +16 -76
  116. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +35 -32
  117. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +46 -213
  118. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/README.md +1 -0
  119. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +76 -194
  120. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +18 -2
  121. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +49 -0
  122. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +1 -1
  123. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/links.md +20 -0
  124. 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
  125. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/links.md +20 -0
  126. 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
  127. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/test_check.py +4 -4
  128. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +3 -3
  129. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.notes/declared.txt +1 -0
  130. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +8 -1
  131. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +15 -135
  132. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +98 -50
  133. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +1 -1
  134. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +1 -1
  135. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +1 -1
  136. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +14 -10
  137. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/__init__.py +3 -10
  138. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/base.py +42 -0
  139. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_check.py +82 -49
  140. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_scope.py +71 -0
  141. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/__init__.py +7 -0
  142. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/check.py +15 -0
  143. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/test_check.py +32 -0
  144. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +1 -1
  145. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +56 -124
  146. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.lock +7 -0
  147. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.toml +4 -0
  148. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/bun.lock +1 -0
  149. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/package.json +1 -0
  150. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/rust-toolchain.toml +3 -0
  151. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/src/main.rs +1 -0
  152. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.lock +7 -0
  153. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.toml +4 -0
  154. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/bun.lock +0 -0
  155. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/package.json +1 -0
  156. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/rust-toolchain.toml +3 -0
  157. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/src/main.rs +1 -0
  158. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +8 -37
  159. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +95 -130
  160. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_root_lockfiles.py +101 -0
  161. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +25 -13
  162. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/__init__.py +7 -0
  163. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/check.py +124 -0
  164. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/fixtures/valid-register.yaml +4 -0
  165. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/scanner.py +217 -0
  166. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/test_check.py +391 -0
  167. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/__init__.py +7 -0
  168. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/check.py +16 -0
  169. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/test_check.py +32 -0
  170. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +12 -1
  171. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/ast_grep.py +2 -3
  172. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +3 -0
  173. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +1 -1
  174. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/declarations.py +24 -0
  175. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/receipt.py +2 -2
  176. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/scripts.py +120 -0
  177. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -1
  178. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.toml +4 -4
  179. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +93 -99
  180. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +93 -99
  181. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
  182. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +6 -47
  183. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +54 -8
  184. package/packages/repo-controls/scripts/hooks/commit-msg +1 -1
  185. package/packages/repo-controls/scripts/hooks/pre-commit +12 -2
  186. package/packages/repo-controls/scripts/hooks/pre-push +27 -19
  187. package/packages/repo-controls/src/cli.ts +22 -3
  188. package/packages/repo-controls/src/compliance-config.ts +132 -65
  189. package/packages/repo-controls/src/config.ts +62 -52
  190. package/packages/repo-controls/src/constants.ts +9 -0
  191. package/packages/repo-controls/src/coverage-ratchet.ts +19 -4
  192. package/packages/repo-controls/src/doctor.ts +20 -30
  193. package/packages/repo-controls/src/findings-ratchet.ts +14 -0
  194. package/packages/repo-controls/src/gitignore-block.ts +3 -13
  195. package/packages/repo-controls/src/hook-log.ts +7 -10
  196. package/packages/repo-controls/src/hook.ts +127 -83
  197. package/packages/repo-controls/src/install-compliance.ts +112 -0
  198. package/packages/repo-controls/src/install.ts +133 -30
  199. package/packages/repo-controls/src/integrity.ts +5 -18
@@ -102,12 +102,10 @@ if [[ "$git_config_count" =~ ^[0-9]+$ ]]; then
102
102
  fi
103
103
  unset GIT_CONFIG_COUNT
104
104
 
105
- # Static checks run against HEAD (the tip being pushed), not the dirty index or
106
- # worktree, so same-checkout staged/unstaged foreign breakage cannot block delivery.
107
- # so same-checkout uncommitted foreign breakage cannot block delivery.
108
- # Reuse a prior pass for the same immutable tree OID under .coverage-cache/.
105
+ # Static checks run against HEAD, keyed by tree + declared checks + export links (see README, No silent skips).
109
106
  HEAD_TREE_OID="$(git rev-parse 'HEAD^{tree}')"
110
- STATIC_CACHE="$ROOT/.coverage-cache/pre-push-static/${HEAD_TREE_OID}.ok"
107
+ STATIC_CACHE_KEY="$(printf '%s\n%s\n%s\n' "$HEAD_TREE_OID" "${ADL_REPO_CONTROLS_STATIC_CHECKS?}" "${ADL_REPO_CONTROLS_EXPORT_LINKS:-}" | git hash-object --stdin)"
108
+ STATIC_CACHE="$ROOT/.coverage-cache/pre-push-static/${HEAD_TREE_OID}-${STATIC_CACHE_KEY}.ok"
111
109
  if [[ -f "$STATIC_CACHE" ]]; then
112
110
  echo "pre-push: reusing static-check pass for tree ${HEAD_TREE_OID}"
113
111
  else
@@ -120,32 +118,42 @@ else
120
118
  git archive --format=tar -o "$PUSH_ARCHIVE" HEAD
121
119
  tar -xf "$PUSH_ARCHIVE" -C "$PUSH_TREE"
122
120
  rm -f -- "$PUSH_ARCHIVE"
123
- # Untracked paths the export needs, declared by the repository. This was
124
- # `ln -s "$ROOT/node_modules"` -- a package that calls itself language
125
- # agnostic assuming JavaScript. A Rust repo links `target/`, a Python one its
126
- # virtualenv, and a repo with no ecosystem links nothing.
121
+ # Declared untracked paths the export needs; a missing target fails.
127
122
  while IFS= read -r export_link; do
128
123
  [[ -n "$export_link" ]] || continue
129
- [[ -e "$ROOT/$export_link" ]] || continue
124
+ if [[ ! -e "$ROOT/$export_link" ]]; then
125
+ echo "pre-push: FAIL — export_links entry '${export_link}' does not exist in the checkout." >&2
126
+ exit 1
127
+ fi
130
128
  mkdir -p "$(dirname -- "$PUSH_TREE/$export_link")"
131
129
  ln -s "$ROOT/$export_link" "$PUSH_TREE/$export_link"
132
130
  done <<<"${ADL_REPO_CONTROLS_EXPORT_LINKS:-}"
133
131
 
134
132
  static_check_labels=()
135
- while IFS=$'\t' read -r static_check_label static_check_command; do
136
- [[ -n "$static_check_label" ]] || continue
133
+ while IFS= read -r static_check_line; do
134
+ [[ -n "$static_check_line" ]] || continue
135
+ # Split on the first tab by hand; `read` with a tab IFS collapses a leading tab.
136
+ static_check_label="${static_check_line%%$'\t'*}"
137
+ static_check_command=""
138
+ [[ "$static_check_line" == *$'\t'* ]] && static_check_command="${static_check_line#*$'\t'}"
139
+ if [[ -z "$static_check_label" ]]; then
140
+ echo 'pre-push: FAIL — static check has an empty label.' >&2
141
+ exit 1
142
+ fi
143
+ if [[ -z "$static_check_command" ]]; then
144
+ echo "pre-push: FAIL — static check '${static_check_label}' has an empty command." >&2
145
+ exit 1
146
+ fi
137
147
  echo "pre-push: ${static_check_label} (HEAD)"
138
148
  (cd "$PUSH_TREE" && bash -c "$static_check_command" </dev/null)
139
149
  static_check_labels+=("$static_check_label")
140
150
  done <<<"${ADL_REPO_CONTROLS_STATIC_CHECKS?}"
141
- # The PreToolUse deny corpus is the only thing asserting that destructive
142
- # commands stay denied, and nothing else ran it — `git update-ref` sat in
143
- # its ALLOW list while being exactly what strands a shared checkout. Cheap
144
- # enough to assert on every push.
145
151
 
146
- mkdir -p "$ROOT/.coverage-cache/pre-push-static"
147
- printf '%s\n%s\n' "${static_check_labels[*]}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$STATIC_CACHE"
148
- echo "pre-push: recorded static-check pass for tree ${HEAD_TREE_OID}"
152
+ if ((${#static_check_labels[@]} > 0)); then
153
+ mkdir -p "$ROOT/.coverage-cache/pre-push-static"
154
+ printf '%s\n%s\n' "${static_check_labels[*]}" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$STATIC_CACHE"
155
+ echo "pre-push: recorded static-check pass for tree ${HEAD_TREE_OID}"
156
+ fi
149
157
  fi
150
158
 
151
159
  # The outgoing-commit coverage ratchet moved to `coverageRatchetStep` in
@@ -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}`)
@@ -1,5 +1,6 @@
1
1
  import { existsSync, readFileSync } from 'node:fs'
2
2
  import { join } from 'node:path'
3
+ import { BRANCH_PREFIXES, COMMIT_TYPES } from '../../azdo/src/lib/commit-types.ts'
3
4
  import { CONFIG_REL, INSTALL_HINT } from './constants.ts'
4
5
  import { MERGE_STRATEGIES, type MergeStrategy } from './promote.ts'
5
6
 
@@ -76,10 +77,10 @@ export interface FindingsConfig {
76
77
  }
77
78
 
78
79
  export interface RepoControlsConfig {
79
- branches: { protected: string[]; prefixes: string[] }
80
+ branches: { protected: string[] }
80
81
  preCommit: { staticChecks: StaticCheck[]; stagedCoverage: boolean; docFreshness: boolean; compliance: boolean }
81
82
  commitMsg: { staticChecks: StaticCheck[] }
82
- release: { source: string; target: string; mergeStrategy: MergeStrategy; preBump: string[] }
83
+ release: { optedOut: boolean; source: string; target: string; mergeStrategy: MergeStrategy; preBump: string[] }
83
84
  postCommit: { staticChecks: StaticCheck[] }
84
85
  prePush: {
85
86
  coverageRatchet: boolean
@@ -101,24 +102,7 @@ export interface RepoControlsConfig {
101
102
  * `extractTableSpan`/`removeTableSpan` — so it must start with the
102
103
  * `[controls]` header line and never contain another line starting with `[`.
103
104
  */
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
- */
105
+ /** The `[controls]` attributes `install` scaffolds when absent; rationale in README, "No silent skips". */
122
106
  export interface SeededAttribute {
123
107
  key: string
124
108
  text: string
@@ -128,25 +112,42 @@ export const SEEDED_ATTRIBUTES: SeededAttribute[] = [
128
112
  {
129
113
  key: 'branches',
130
114
  text: `
131
- # pre-push refuses to push while one of these branches is checked out; prefixes: <prefix>/<kebab>.
132
- branches = { protected = ["main", "integration"], prefixes = ["feat", "fix", "docs", "chore"] }
115
+ # pre-push refuses to push while one of these branches is checked out; branch prefixes are fixed to the commit types.
116
+ branches = { protected = ["main", "integration"] }
117
+ `,
118
+ },
119
+ {
120
+ key: 'pre_commit',
121
+ text: `
122
+ # Every gate key is declared; a missing key is a config error. Switch a gate off with an explicit false.
123
+ pre_commit = { staged_coverage = true, doc_freshness = true, compliance = true }
133
124
  `,
134
125
  },
135
126
  {
136
127
  key: 'pre_push',
137
128
  text: `
138
129
  # 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" }
130
+ pre_push = { remote = "origin", coverage_ratchet = true, coverage_produce = false, findings_ratchet = true }
131
+ `,
132
+ },
133
+ {
134
+ key: 'release',
135
+ text: `
136
+ # Promotion governance is opted out. To promote through a PR, replace this line with a [controls.release] table declaring source and target.
137
+ release = false
146
138
  `,
147
139
  },
148
140
  ]
149
141
 
142
+ /** The `[controls] integrity` seed for a compliance register folder (README, "Compliance seeding"); appended after `SEEDED_ATTRIBUTES`. */
143
+ export function integritySeed(folder: string): SeededAttribute {
144
+ if (!/^[A-Za-z0-9._/-]+$/.test(folder)) throw new Error(`integrity seed folder has unsafe characters: ${folder}`)
145
+ return {
146
+ key: 'integrity',
147
+ text: `\n# Pre-commit refuses a commit touching the compliance register folder; the owner lands it.\nintegrity = { watched = ["${folder}/**"] }\n`,
148
+ }
149
+ }
150
+
150
151
  /**
151
152
  * Seeded by `install` as the `[controls]` table of the shared `adl-config.toml`
152
153
  * when that table is missing; user-owned afterwards. When the table already exists,
@@ -261,14 +262,14 @@ export function extractTableSpan(text: string, tableName: string): string | null
261
262
  */
262
263
  function ownKeysEnd(found: TableRanges): { offset: number; atRangeEnd: boolean } | null {
263
264
  if (found.headerLine === -1) return null
264
- const range = found.ranges.find((candidate) => candidate.startLine === found.headerLine)
265
+ const range = found.ranges.find((candidate) => candidate.startLine <= found.headerLine && found.headerLine < candidate.endLine)
265
266
  if (range === undefined) return null
266
- let sub = range.startLine + 1
267
+ let sub = found.headerLine + 1
267
268
  while (sub < range.endLine && !/^\[/.test(found.lines[sub] ?? '')) sub++
268
269
  if (sub >= range.endLine) return { offset: range.end, atRangeEnd: true }
269
270
  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--
271
+ while (at - 1 > found.headerLine && (found.lines[at - 1] ?? '').trim().startsWith('#')) at--
272
+ while (at - 1 > found.headerLine && (found.lines[at - 1] ?? '').trim() === '') at--
272
273
  return { offset: found.starts[at] ?? found.text.length, atRangeEnd: false }
273
274
  }
274
275
 
@@ -313,12 +314,12 @@ export const RETIRED_ATTRIBUTES = ['version'] as const
313
314
  export function removeFromTableSpan(text: string, tableName: string, key: string): string {
314
315
  const found = findTableRanges(text, tableName)
315
316
  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)
317
+ if (where === null) return text
318
+ const headerStart = found.starts[found.headerLine] ?? 0
319
+ const body = text.slice(headerStart, where.offset)
319
320
  const assignment = new RegExp(`^${escapeRegExp(key)}\\s*=.*\\n(\\n)?`, 'm')
320
321
  if (!assignment.test(body)) return text
321
- return text.slice(0, range.start) + body.replace(assignment, '') + text.slice(where.offset)
322
+ return text.slice(0, headerStart) + body.replace(assignment, '') + text.slice(where.offset)
322
323
  }
323
324
 
324
325
  /** `text` with every range of a table removed (a no-op if there are none); other tables are untouched. */
@@ -340,7 +341,6 @@ export function extractControlsSpan(text: string): string | null {
340
341
  }
341
342
 
342
343
  const BRANCH_RE = /^[a-z0-9][a-z0-9/-]*$/
343
- const PREFIX_RE = /^[a-z0-9][a-z0-9-]*$/
344
344
  const LABEL_RE = /^[a-z0-9][a-z0-9-]*$/
345
345
  const REMOTE_RE = /^\S+$/
346
346
 
@@ -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
@@ -479,10 +487,10 @@ export function parseConfig(text: string): RepoControlsConfig {
479
487
  problems.unknownKeys(controls, ['branches', 'pre_commit', 'pre_push', 'commit_msg', 'post_commit', 'compliance', 'coverage', 'findings', 'integrity', 'release'], '')
480
488
 
481
489
  const branches = problems.table(controls, 'branches', ['protected', 'prefixes'])
490
+ if (branches.prefixes !== undefined) problems.add('branches.prefixes is no longer configurable — delete it; branch prefixes are fixed to the commit types')
482
491
  // `branches` and `pre_push.remote` are REQUIRED: install always seeds them, and
483
492
  // `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.
493
+ // so "no branch policy" cannot be spelled as an empty list.
486
494
  const preCommit = problems.optionalTable(controls, 'pre_commit', ['static_checks', 'staged_coverage', 'doc_freshness', 'compliance'])
487
495
  const prePush = problems.table(controls, 'pre_push', ['coverage_ratchet', 'coverage_produce', 'findings_ratchet', 'static_checks', 'export_links', 'remote'])
488
496
  // Optional, and every field defaults, so a repository that never writes this
@@ -520,7 +528,10 @@ export function parseConfig(text: string): RepoControlsConfig {
520
528
  // Which branch is promoted to which. No default pair: defaulting to
521
529
  // integration -> main would put one repository's branch names back into
522
530
  // this package by the back door.
523
- const release = problems.optionalTable(controls, 'release', ['source', 'target', 'merge_strategy', 'pre_bump'])
531
+ const releaseOptedOut = controls.release === false || (isTable(controls.release) && controls.release.enabled === false)
532
+ if (isTable(controls.release) && controls.release.enabled !== undefined && typeof controls.release.enabled !== 'boolean') problems.add('release.enabled must be a boolean')
533
+ if (controls.release === true) problems.add('release must be a table, or false to opt out of promotion governance')
534
+ const release = controls.release === false || controls.release === true ? {} : problems.optionalTable(controls, 'release', ['enabled', 'source', 'target', 'merge_strategy', 'pre_bump'])
524
535
  const releaseMergeStrategy = problems.optionalString(release, 'merge_strategy', 'release.merge_strategy', 'squash')
525
536
  if (!Object.hasOwn(MERGE_STRATEGIES, releaseMergeStrategy)) {
526
537
  problems.add(`release.merge_strategy must be one of ${Object.keys(MERGE_STRATEGIES).join(', ')}`)
@@ -529,24 +540,23 @@ export function parseConfig(text: string): RepoControlsConfig {
529
540
  const config: RepoControlsConfig = {
530
541
  branches: {
531
542
  protected: problems.names(branches, 'protected', 'branches.protected', BRANCH_RE),
532
- prefixes: problems.names(branches, 'prefixes', 'branches.prefixes', PREFIX_RE),
533
543
  },
534
544
  preCommit: {
535
545
  // The repository declares WHAT runs at commit and WHICH hook it runs in.
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
@@ -736,7 +747,8 @@ export function configEnv(repo: string, config: RepoControlsConfig): Record<stri
736
747
  const env: Record<string, string> = {
737
748
  ADL_REPO_CONTROLS_ROOT: repo,
738
749
  ADL_REPO_CONTROLS_PROTECTED_BRANCHES: config.branches.protected.join('|'),
739
- ADL_REPO_CONTROLS_BRANCH_PREFIXES: config.branches.prefixes.join('|'),
750
+ ADL_REPO_CONTROLS_BRANCH_PREFIXES: BRANCH_PREFIXES.join('|'),
751
+ ADL_REPO_CONTROLS_COMMIT_TYPES: COMMIT_TYPES.join('|'),
740
752
  ADL_REPO_CONTROLS_COVERAGE_RATCHET: config.prePush.coverageRatchet ? '1' : '0',
741
753
  ADL_REPO_CONTROLS_FINDINGS_RATCHET: config.prePush.findingsRatchet ? '1' : '0',
742
754
  ADL_REPO_CONTROLS_EXPORT_LINKS: config.prePush.exportLinks.join('\n'),
@@ -757,7 +769,5 @@ export function configEnv(repo: string, config: RepoControlsConfig): Record<stri
757
769
  .map(([name, command]) => `${name}\t${command}`)
758
770
  .join('\n'),
759
771
  }
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
772
  return env
763
773
  }