@amsterdamdatalabs/adl-agent-control-linux-x64 0.1.29 → 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 (201) 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/cli/args.ts +2 -2
  9. package/packages/agent-hooks/src/cli/doctor.ts +2 -1
  10. package/packages/agent-hooks/src/cli/rules.ts +2 -1
  11. package/packages/agent-hooks/src/deploy/opencode-template.ts +5 -3
  12. package/packages/agent-hooks/src/gating/config.ts +11 -0
  13. package/packages/agent-hooks/src/gating/running-adl.ts +24 -0
  14. package/packages/agent-hooks/src/hooks/crg/background.ts +7 -4
  15. package/packages/agent-hooks/src/hooks/log.ts +12 -12
  16. package/packages/agent-hooks/src/hooks/user-prompt-submit/handler.ts +2 -2
  17. package/packages/agent-hooks/src/log/writer.ts +170 -24
  18. package/packages/agent-hooks/src/main.ts +11 -4
  19. package/packages/agent-hooks/src/pre-tool-use/handler.ts +5 -4
  20. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
  21. package/packages/agent-hooks/src/rules/shell/invocation.ts +2 -1
  22. package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +24 -2
  23. package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +19 -45
  24. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +9 -0
  25. package/packages/agent-skills/assets/diagnosing-bugs/scripts/hitl-loop.template.sh +1 -12
  26. package/packages/azdo/src/azdo-wit.ts +8 -2
  27. package/packages/azdo/src/mcp-server.ts +1 -1
  28. package/packages/fleet/manifest.json +2 -1
  29. package/packages/fleet/src/repo-maintain.ts +5 -2
  30. package/packages/repo-controls/AGENTS.md +7 -4
  31. package/packages/repo-controls/README.md +346 -56
  32. package/packages/repo-controls/assets/GLOBAL-INVARIANTS.md +21 -0
  33. package/packages/repo-controls/assets/promote.ts +7 -85
  34. package/packages/repo-controls/assets/reconcile-guard.sh +1 -14
  35. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +25 -0
  36. package/packages/repo-controls/assets/scaffold/docs/CLAUDE.md +1 -0
  37. package/packages/repo-controls/assets/scaffold/docs/decisions/index.md +17 -0
  38. package/packages/repo-controls/assets/scaffold/docs/notes.md +20 -0
  39. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +28 -0
  40. package/packages/repo-controls/assets/scaffold/plans/CLAUDE.md +1 -0
  41. package/packages/repo-controls/assets/scaffold/plans/GAPS-AND-FINDINGS.md +12 -0
  42. package/packages/repo-controls/assets/scaffold/plans/LEDGER.md +8 -0
  43. package/packages/repo-controls/assets/scaffold/plans/features/.gitkeep +0 -0
  44. package/packages/repo-controls/assets/scaffold/root/AGENTS.md +22 -0
  45. package/packages/repo-controls/assets/scaffold/root/CLAUDE.md +1 -0
  46. package/packages/repo-controls/assets/scaffold/workflows/AGENTS.md +16 -0
  47. package/packages/repo-controls/assets/scaffold/workflows/CLAUDE.md +1 -0
  48. package/packages/repo-controls/assets/scaffold/workflows/pipelines/.gitkeep +0 -0
  49. package/packages/repo-controls/assets/scaffold/workflows/scripts/.gitkeep +0 -0
  50. package/packages/repo-controls/assets/version-bump.sh +7 -48
  51. package/packages/repo-controls/package/adl-compliance/AGENTS.md +25 -7
  52. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +6 -4
  53. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/check.py +1 -1
  54. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +1 -1
  55. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +7 -42
  56. 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
  57. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +10 -44
  58. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +1 -1
  59. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +1 -1
  60. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +1 -1
  61. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/check.py +1 -7
  62. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/test_check.py +12 -3
  63. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +15 -23
  64. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +0 -136
  65. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +18 -0
  66. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +2 -2
  67. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +0 -7
  68. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/check.py +2 -4
  69. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-delivery.md +10 -0
  70. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-recommendation.md +12 -0
  71. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/target-narration.md +10 -0
  72. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +6 -7
  73. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/test_check.py +3 -3
  74. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +10 -12
  75. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +1 -1
  76. 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
  77. 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
  78. 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
  79. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +1 -1
  80. 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
  81. 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
  82. 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
  83. 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
  84. 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
  85. 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
  86. 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
  87. 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
  88. 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
  89. 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
  90. 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
  91. 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
  92. 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
  93. 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
  94. 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
  95. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +1 -1
  96. 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
  97. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +1 -1
  98. 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
  99. 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
  100. 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
  101. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +2 -2
  102. 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
  103. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +1 -1
  104. 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
  105. 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
  106. 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
  107. 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
  108. 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
  109. 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
  110. 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
  111. 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
  112. 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
  113. 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
  114. 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
  115. 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
  116. 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
  117. 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
  118. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +46 -54
  119. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +16 -76
  120. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +35 -32
  121. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +46 -213
  122. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/README.md +1 -0
  123. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +76 -194
  124. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +18 -2
  125. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +49 -0
  126. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +1 -1
  127. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/links.md +20 -0
  128. 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
  129. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/links.md +20 -0
  130. 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
  131. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/test_check.py +4 -4
  132. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +3 -3
  133. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.notes/declared.txt +1 -0
  134. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +8 -1
  135. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +15 -135
  136. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +98 -50
  137. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +1 -1
  138. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +1 -1
  139. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +1 -1
  140. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +14 -10
  141. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/__init__.py +3 -10
  142. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/base.py +42 -0
  143. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_check.py +82 -49
  144. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_scope.py +71 -0
  145. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/__init__.py +7 -0
  146. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/check.py +15 -0
  147. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/test_check.py +32 -0
  148. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +1 -1
  149. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +56 -124
  150. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.lock +7 -0
  151. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.toml +4 -0
  152. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/bun.lock +1 -0
  153. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/package.json +1 -0
  154. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/rust-toolchain.toml +3 -0
  155. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/src/main.rs +1 -0
  156. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.lock +7 -0
  157. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.toml +4 -0
  158. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/bun.lock +0 -0
  159. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/package.json +1 -0
  160. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/rust-toolchain.toml +3 -0
  161. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/src/main.rs +1 -0
  162. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +8 -37
  163. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +95 -130
  164. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_root_lockfiles.py +101 -0
  165. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +25 -13
  166. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/__init__.py +7 -0
  167. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/check.py +124 -0
  168. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/fixtures/valid-register.yaml +4 -0
  169. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/scanner.py +217 -0
  170. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/test_check.py +391 -0
  171. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/__init__.py +7 -0
  172. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/check.py +16 -0
  173. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/test_check.py +32 -0
  174. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +12 -1
  175. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/ast_grep.py +2 -3
  176. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +3 -0
  177. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +1 -1
  178. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/declarations.py +24 -0
  179. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/receipt.py +2 -2
  180. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/scripts.py +120 -0
  181. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -1
  182. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.toml +4 -4
  183. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +93 -99
  184. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +93 -99
  185. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
  186. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +6 -47
  187. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +54 -8
  188. package/packages/repo-controls/scripts/hooks/pre-commit +12 -2
  189. package/packages/repo-controls/scripts/hooks/pre-push +38 -20
  190. package/packages/repo-controls/src/cli.ts +22 -3
  191. package/packages/repo-controls/src/compliance-config.ts +132 -65
  192. package/packages/repo-controls/src/config.ts +55 -46
  193. package/packages/repo-controls/src/constants.ts +9 -0
  194. package/packages/repo-controls/src/coverage-ratchet.ts +19 -4
  195. package/packages/repo-controls/src/doctor.ts +20 -30
  196. package/packages/repo-controls/src/findings-ratchet.ts +14 -0
  197. package/packages/repo-controls/src/gitignore-block.ts +3 -13
  198. package/packages/repo-controls/src/hook-log.ts +7 -10
  199. package/packages/repo-controls/src/hook.ts +127 -83
  200. package/packages/repo-controls/src/install-compliance.ts +112 -0
  201. package/packages/repo-controls/src/install.ts +133 -30
@@ -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
+ }
@@ -59,8 +59,8 @@ const errorText = (error: unknown): string => (error as Error).message
59
59
  /**
60
60
  * The declared Azure DevOps promotion-governance checks alone: the
61
61
  * configured remote parsed, then the target branch's remote policies read
62
- * over the network with a minted token. Returns `[]` when the repository
63
- * declares no `[controls.release]` at all — there is nothing to prove.
62
+ * over the network with a minted token. Returns `[]` only for an explicit
63
+ * `release = false`; an absent `[controls.release]` is a failed check.
64
64
  *
65
65
  * Split out of `doctor()` (Tarun, 2026-09-28) so the pre-push gate can run
66
66
  * exactly these checks on their own, without re-running (or skipping) every
@@ -72,7 +72,16 @@ export function azureDevOpsPromotionGovernanceChecks(
72
72
  readAzureDevOpsPolicyEvidence?: AzureDevOpsPolicyReader,
73
73
  ): Check[] {
74
74
  const config = loadConfig(repo)
75
- if (!config.release.source && !config.release.target) return []
75
+ if (config.release.optedOut) return []
76
+ if (!config.release.source && !config.release.target) {
77
+ return [
78
+ {
79
+ id: 'release-declaration',
80
+ status: 'fail',
81
+ message: '[controls.release] is not declared — declare [controls.release] source/target, or release = false to opt out',
82
+ },
83
+ ]
84
+ }
76
85
  const remote = capture(['git', 'remote', 'get-url', config.prePush.remote], repo, env)
77
86
  if (remote.code !== 0) {
78
87
  return [
@@ -150,51 +159,31 @@ export function doctor(repoArg: string, env: Env, options: string | DoctorOption
150
159
  ? '[controls.integrity].watched is empty — this repository\'s own gate-defining files (tsconfig, lint config, coverage baselines, CI pipeline) are not protected by the pre-commit integrity gate; declare them under [controls.integrity].watched'
151
160
  : null,
152
161
  `[controls.integrity].watched declares ${config.integrity.watched.length} pattern(s)`,
153
- 'warn',
154
162
  )
155
- // The path the pre-push ratchet grades, not a default: a repository that
156
- // declares `[controls.coverage].summary` elsewhere was probed at the wrong
157
- // file and passed with its real summary missing. The gate has a safety
158
- // gate: with no summary on either side of the index, the step is a no-op
159
- // (see hook.ts), so this is informational rather than blocking.
163
+ // Armed gates fail when their input is missing; an explicit false in adl-config.toml switches a gate off.
160
164
  const coverageSummaryExists = existsSync(join(repo, config.coverage.summary))
161
165
  add(
162
166
  'coverage-summary',
163
167
  config.prePush.coverageRatchet && !coverageSummaryExists
164
- ? `pre_push.coverage_ratchet is enabled but ${config.coverage.summary} does not exist — the gate is a no-op until a baseline lands; commit one or set pre_push.coverage_ratchet = false`
168
+ ? `pre_push.coverage_ratchet is enabled but ${config.coverage.summary} does not exist — every push fails until a baseline lands; commit one or set pre_push.coverage_ratchet = false`
165
169
  : null,
166
170
  `${config.coverage.summary} is present or the ratchet is disabled`,
167
- 'warn',
168
171
  )
169
- // Same shape: `coverage_produce` defaults OFF, but armed with no declared
170
- // `[controls.coverage].produce_push` it is NOT a no-op — the push gate
171
- // fails loud (a step that measures nothing is not a pass), so doctor names
172
- // the misconfiguration before the first push does.
173
172
  add(
174
173
  'coverage-produce',
175
174
  config.prePush.coverageProduce && config.coverage.producePush.trim() === ''
176
175
  ? 'pre_push.coverage_produce is enabled but [controls.coverage].produce_push declares no command — the push gate fails every push until one is declared; add produce_push or set pre_push.coverage_produce = false'
177
176
  : null,
178
177
  'pre_push.coverage_produce is disabled or [controls.coverage].produce_push is declared',
179
- 'warn',
180
178
  )
181
- // Same shape: `findings_ratchet` defaults ON but is a no-op when the floor
182
- // file is absent. A repo with no lint tooling at all is never asked for a
183
- // floor it has no use for.
184
179
  const findingsFloorExists = existsSync(join(repo, config.findings.floor))
185
180
  add(
186
181
  'findings-floor',
187
182
  config.prePush.findingsRatchet && !findingsFloorExists
188
- ? `pre_push.findings_ratchet is enabled but ${config.findings.floor} does not exist — the gate is a no-op until a floor lands; commit one or set pre_push.findings_ratchet = false`
183
+ ? `pre_push.findings_ratchet is enabled but ${config.findings.floor} does not exist — every push fails until a floor lands; commit one or set pre_push.findings_ratchet = false`
189
184
  : null,
190
185
  `${config.findings.floor} is present or the ratchet is disabled`,
191
- 'warn',
192
186
  )
193
- // A floor on its own measures nothing. With the safety gate in hook.ts
194
- // (counters empty + no floor → no-op), this is informational when the
195
- // floor is missing; it stays a hard error only when a floor IS committed
196
- // but no counter is declared to drive it (the runtime hard-fails too:
197
- // `findingsRatchetReport` returns 1 when counters is empty).
198
187
  if (config.prePush.findingsRatchet && findingsFloorExists && Object.keys(config.findings.counters).length === 0) {
199
188
  add(
200
189
  'findings-counters',
@@ -205,9 +194,9 @@ export function doctor(repoArg: string, env: Env, options: string | DoctorOption
205
194
  } else if (config.prePush.findingsRatchet && Object.keys(config.findings.counters).length === 0) {
206
195
  add(
207
196
  'findings-counters',
208
- 'pre_push.findings_ratchet is enabled but [controls.findings.counters] declares none — the gate is a no-op until a counter is declared',
197
+ 'pre_push.findings_ratchet is enabled but [controls.findings.counters] declares none — every push fails until a counter is declared',
209
198
  'at least one findings counter is declared, or the ratchet is disabled',
210
- 'warn',
199
+ 'fail',
211
200
  )
212
201
  } else {
213
202
  add('findings-counters', null, 'at least one findings counter is declared, or the ratchet is disabled')
@@ -226,7 +215,9 @@ export function doctor(repoArg: string, env: Env, options: string | DoctorOption
226
215
  }
227
216
 
228
217
  let complianceEnabled = false
218
+ let complianceFlag = false
229
219
  try {
220
+ complianceFlag = loadConfig(repo).preCommit.compliance
230
221
  const complianceConfig = loadComplianceConfig(repo)
231
222
  add('compliance-config', null, 'adl-config.toml [compliance] is valid')
232
223
  complianceEnabled = complianceConfig.enabled.length > 0
@@ -244,7 +235,7 @@ export function doctor(repoArg: string, env: Env, options: string | DoctorOption
244
235
  ? 'this repository has a plans/ tree but docs.ledger is not in [controls.compliance].enabled — the knowledge-ledger contract (DOC-009) is unenforced'
245
236
  : null,
246
237
  hasPlans ? 'docs.ledger is armed for this plans/ tree' : 'no plans/ tree, so docs.ledger is not required',
247
- 'warn',
238
+ complianceFlag ? 'fail' : 'warn',
248
239
  )
249
240
  } catch (error) {
250
241
  add('compliance-config', errorText(error), '')
@@ -262,7 +253,6 @@ export function doctor(repoArg: string, env: Env, options: string | DoctorOption
262
253
  ? 'ast-grep not found on PATH — every compliance row that needs it reports could-not-run, never a pass; 26 rows are affected (see README)'
263
254
  : null,
264
255
  'ast-grep on PATH, or [compliance].enabled is empty so it is not required',
265
- 'warn',
266
256
  )
267
257
 
268
258
  let lock: Lock | null = null
@@ -161,3 +161,17 @@ export function findingsProblems(
161
161
  }
162
162
  return problems
163
163
  }
164
+
165
+ /** A floor entry for a counter the repository does not declare grades nothing, so it fails. */
166
+ export function undeclaredFloorProblems(floor: Floor, counters: Record<string, string>): FindingProblem[] {
167
+ const undeclared = Object.keys(floor).filter((counter) => !Object.hasOwn(counters, counter)).sort()
168
+ if (undeclared.length === 0) return []
169
+ return [
170
+ {
171
+ fails: true,
172
+ message:
173
+ `the floor lists counter(s) not declared in [controls.findings.counters]: ${undeclared.join(', ')} — ` +
174
+ 'declare the counter or delete its floor entry',
175
+ },
176
+ ]
177
+ }
@@ -14,12 +14,7 @@
14
14
  //
15
15
  // Evidence for each entry, gathered 2026-09-27 by reading this monorepo's
16
16
  // own source (not guessed):
17
- // - `/.enact/logs/` — pre-hard-cut evidence only. Neither repo-controls' git
18
- // hooks (`src/hook-log.ts`, 2026-09-27) nor
19
- // agent-hooks' own hook log write here any more —
20
- // both moved to `.adl/logs/` JSON-Lines files
21
- // (docs/architecture/cli-design.md §5a). Existing rows are
22
- // quarantined in place, never migrated.
17
+ // - `/.enact/logs/` — pre-hard-cut evidence only; nothing writes there (README, "Log folders").
23
18
  // - `/.coverage-cache/` — constants.ts `GITIGNORE_LINES` (retired) /
24
19
  // install.ts `installGitignore`'s coverage-cache
25
20
  // handling; populated by the coverage ratchet.
@@ -34,10 +29,7 @@
34
29
  // a static, always-present pattern that matches
35
30
  // nothing costs a consumer repo nothing, and one
36
31
  // static list is the whole point of this file.
37
- // - `/.adl/` — packages/cli/src/log.ts's `.adl/logs` (repo
38
- // root's session/tool-call log; docs/architecture/cli-design.md
39
- // §5a). Whole `.adl/` dir ignored, matching this
40
- // monorepo's own hand-authored line.
32
+ // - `/.adl/` — the whole `.adl/` dir, including `.adl/logs/<owner>/` (README, "Log folders").
41
33
  // - `/.scratchpad/`, `packages/*/.scratchpad/`
42
34
  // — not created by any single tool's code, but
43
35
  // mandated by the global agent convention (every
@@ -83,10 +75,8 @@ export const GITIGNORE_BLOCK_LINES: readonly string[] = [
83
75
  '# repo-controls: coverage ratchet cache',
84
76
  '/.coverage-cache/',
85
77
  '',
86
- '# repo-controls / agent-hooks: canonical repository-local hook and session logs',
87
- '/.enact/logs/',
88
- '',
89
78
  '# repo-controls: pre-hardcut hook-log quarantine (evidence only, never read or written)',
79
+ '/.enact/logs/',
90
80
  '/.workflows/hooks/logs/',
91
81
  '/.enact-runtime/repo-controls/hooks/',
92
82
  '',
@@ -15,10 +15,7 @@
15
15
  *
16
16
  * BEHAVIOUR CHANGE (Tarun, 2026-09-27, §5a): this replaces two CSV files
17
17
  * under `.enact/logs/` (`adl-repo-controls-{activity,deny}.log`) with the
18
- * shared format's `.adl/logs/repo-controls.log` (every invocation) and
19
- * `.adl/logs/hooks-deny.log` (FAIL rows only — shared with agent-hooks' own
20
- * PreToolUse denies, so a human `git commit` denial and an agent's blocked
21
- * shell command land in one timeline). `.enact/logs/` is a hard cut: this
18
+ * shared format's `.adl/logs/git-hooks/{activity,deny}.log` (README, "Log folders"). `.enact/logs/` is a hard cut: this
22
19
  * module never reads or writes it; whatever evidence is already there is left exactly where it is.
23
20
  *
24
21
  * Field mapping onto `LogEntryInput` (`../../agent-hooks/src/log/writer.ts`)
@@ -26,7 +23,7 @@
26
23
  * fields (Tarun, 2026-09-28: host second with the host alone, tool_call
27
24
  * third with the hook kind):
28
25
  * - `host` is the constant `'git'` — distinguishes a git-hook row from an
29
- * agent-hook row sharing the same files.
26
+ * row in agent-hooks' folder; the two owners no longer share a file.
30
27
  * - `tool_call` is the hook name (`commit-msg`, `pre-commit`, ...),
31
28
  * unchanged from `hook_log_init`'s first argument.
32
29
  * - `event` is the hook name too, unchanged from `hook_log_init`'s first
@@ -45,7 +42,7 @@
45
42
  * here — git hooks run outside an `adl run` child env.)
46
43
  *
47
44
  * Best-effort by construction, the same contract `hook-log.sh`'s CSV writer
48
- * always had: `writeLogEntry` throwing is swallowed here rather than
45
+ * always had: `writeLogEntry` throwing is reported on stderr (one line) rather than
49
46
  * propagated, so a genuinely unwritable `.adl/logs/` can never change a git
50
47
  * hook's exit code.
51
48
  */
@@ -63,9 +60,9 @@ export function logHookEvent(hook: HookName, reason: string, context: string, me
63
60
  ...(message !== '' ? { result: message } : {}),
64
61
  }
65
62
  try {
66
- writeLogEntry('repo-controls', entry, { cwd })
67
- if (reason.startsWith('FAIL')) writeLogEntry('hooks-deny', entry, { cwd })
68
- } catch {
69
- // Best-effort: a log write must never change a git hook's outcome.
63
+ writeLogEntry('git-hooks/activity', entry, { cwd })
64
+ if (reason.startsWith('FAIL')) writeLogEntry('git-hooks/deny', entry, { cwd })
65
+ } catch (error) {
66
+ process.stderr.write(`adl repo controls: could not write log row: ${error instanceof Error ? error.message : String(error)}\n`)
70
67
  }
71
68
  }
@@ -1,16 +1,16 @@
1
1
  import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs'
2
2
  import { tmpdir } from 'node:os'
3
3
  import { join } from 'node:path'
4
- import { LOCKFILE_KEYS, loadComplianceConfig } from './compliance-config.ts'
4
+ import { declaredComplianceTree, LOCKFILE_KEYS, loadComplianceConfig, parseComplianceConfig } from './compliance-config.ts'
5
5
  import { configEnv, type FindingsConfig, loadConfig, parseConfig, type StaticCheck } from './config.ts'
6
- import { BUNDLE_ROOT, COMPLIANCE_ROOT, HOOK_NAMES, type HookName } from './constants.ts'
6
+ import { BUNDLE_ROOT, COMPLIANCE_ROOT, CONFIG_REL, HOOK_NAMES, type HookName } from './constants.ts'
7
7
  import { docFreshnessProblems, inScope } from './doc-freshness.ts'
8
- import { azureDevOpsPromotionGovernanceChecks, type Check, doctor, type DoctorReport } from './doctor.ts'
8
+ import { doctor, type DoctorReport } from './doctor.ts'
9
9
  import { commitRequiredProblems, integrityProblems } from './integrity.ts'
10
10
  import { lfsChain } from './lfs.ts'
11
11
  import { logHookEvent } from './hook-log.ts'
12
- import { type CoverageSummary, coverageRegressions, readSummary } from './coverage-ratchet.ts'
13
- import { type Floor, findingsProblems, parseCounterOutput, parseFloor } from './findings-ratchet.ts'
12
+ import { type CoverageSummary, coverageRegressions, newlyBaselined, readSummary } from './coverage-ratchet.ts'
13
+ import { type Floor, findingsProblems, parseCounterOutput, parseFloor, undeclaredFloorProblems } from './findings-ratchet.ts'
14
14
  import { bashProblem, capture, childEnv, type Env, resolveRepo, which } from './runtime.ts'
15
15
  import { postCheckoutWorktreeGuardStep } from './worktree-guard.ts'
16
16
 
@@ -30,6 +30,8 @@ export interface RunContext {
30
30
  err: (line: string) => void
31
31
  /** Progress the hook wants seen. Defaults to stderr's sink when not supplied. */
32
32
  out?: (line: string) => void
33
+ /** Discard the compliance engine's stdout rows; see README, "Compliance seeding". */
34
+ quietEngine?: boolean
33
35
  /**
34
36
  * What the bundled script's stdin is wired to. `inherit` is the default and
35
37
  * the production answer: git pipes the pushed refs into the hook process, and
@@ -129,6 +131,8 @@ function runUv(args: string[], repo: string, context: RunContext): number {
129
131
  // (the engine rejects an empty `--declare` value); `parseMarkdown` already
130
132
  // guaranteed each root is comma-free, so the join is unambiguous.
131
133
  ...(config.markdown?.roots.length ? ['--declare', `markdown-roots=${config.markdown.roots.join(',')}`] : []),
134
+ // LAY-008's extra asset directories, one comma list. Empty is skipped, never sent; `parsePlacement` guaranteed each entry is comma-free.
135
+ ...(config.placement?.asset_dirs.length ? ['--declare', `asset-dirs=${config.placement.asset_dirs.join(',')}`] : []),
132
136
  // REP-002's declared manifest and lockfile paths, one comma list per key, in
133
137
  // `LOCKFILE_KEYS` order (`bun-manifests`, `bun-lockfiles`, `cargo-manifests`,
134
138
  // `cargo-lockfiles`). An empty list is skipped, never sent; `parseLockfiles` already
@@ -140,7 +144,7 @@ function runUv(args: string[], repo: string, context: RunContext): number {
140
144
  ]
141
145
  const proc = Bun.spawnSync(
142
146
  [executable, 'run', '--project', COMPLIANCE_ROOT, 'adl-compliance', '--root', repo, ...injected, ...args],
143
- { cwd: repo, env: childEnv(context.env), stdin: 'inherit', stdout: 'inherit', stderr: 'inherit' },
147
+ { cwd: repo, env: childEnv(context.env), stdin: 'inherit', stdout: context.quietEngine ? 'ignore' : 'inherit', stderr: 'inherit' },
144
148
  )
145
149
  return proc.exitCode ?? 1
146
150
  }
@@ -198,8 +202,13 @@ export function withStagedIndexSnapshot<T>(
198
202
  }
199
203
  }
200
204
 
201
- /** Whether the candidate commit itself arms compliance. */
202
- export function stagedComplianceEnabled(repo: string, env: Env): boolean {
205
+ export interface StagedComplianceGate {
206
+ armed: boolean
207
+ enabled: string[]
208
+ }
209
+
210
+ /** The compliance flag and the enabled gate list, both read from the candidate commit's adl-config.toml. */
211
+ export function stagedComplianceGate(repo: string, env: Env): StagedComplianceGate {
203
212
  const shown = capture(['git', 'show', ':adl-config.toml'], repo, env)
204
213
  if (shown.code !== 0) {
205
214
  if (loadConfig(repo).preCommit.compliance) {
@@ -207,9 +216,20 @@ export function stagedComplianceEnabled(repo: string, env: Env): boolean {
207
216
  'compliance is enabled in the working tree but adl-config.toml is absent from the staged index',
208
217
  )
209
218
  }
210
- return false
219
+ return { armed: false, enabled: [] }
220
+ }
221
+ return { armed: parseConfig(shown.stdout).preCommit.compliance, enabled: parseComplianceConfig(shown.stdout).enabled }
222
+ }
223
+
224
+ /** Flag true with an empty gate list fails loud; only an explicit pre_commit.compliance = false skips. */
225
+ export function stagedComplianceStep(gate: StagedComplianceGate, run: () => number, err: (line: string) => void): number {
226
+ if (!gate.armed) return 0
227
+ if (gate.enabled.length === 0) {
228
+ err('pre-commit: FAIL — pre_commit.compliance is on but [controls.compliance].enabled is empty.')
229
+ err(' enabled is empty; list gates or set pre_commit.compliance = false.')
230
+ return 1
211
231
  }
212
- return parseConfig(shown.stdout).preCommit.compliance
232
+ return run()
213
233
  }
214
234
 
215
235
  /**
@@ -272,6 +292,10 @@ export function findingsRatchetReport(
272
292
  }
273
293
 
274
294
  let failed = false
295
+ for (const problem of undeclaredFloorProblems(floor, findings.counters)) {
296
+ failed = true
297
+ err(`pre-push: FAIL — ${problem.message}`)
298
+ }
275
299
  for (const [name, command] of counters) {
276
300
  const run = runCounter(command)
277
301
  if (run.code !== 0) {
@@ -398,9 +422,18 @@ export function coverageRatchetStep(
398
422
  readBaseline: () => string | null,
399
423
  readCurrent: () => string,
400
424
  err: (line: string) => void,
425
+ options: { firstBaseline?: boolean; existsOnDisk?: (path: string) => boolean } = {},
401
426
  ): number {
402
427
  const baselineText = readBaseline()
403
- if (baselineText === null) return 0
428
+ if (baselineText === null) {
429
+ if (options.firstBaseline === true) {
430
+ err(`pre-push: note — ${summaryPath} is added in HEAD: first baseline, nothing to grade it against.`)
431
+ return 0
432
+ }
433
+ err(`pre-push: FAIL — no HEAD~1 baseline for ${summaryPath} and the summary is not added in HEAD.`)
434
+ err(' An armed ratchet with nothing to compare against grades nothing; commit the baseline first.')
435
+ return 1
436
+ }
404
437
 
405
438
  let baseline: CoverageSummary
406
439
  let current: CoverageSummary
@@ -418,13 +451,37 @@ export function coverageRatchetStep(
418
451
  return 1
419
452
  }
420
453
 
421
- const regressions = coverageRegressions(baseline, current)
454
+ for (const path of newlyBaselined(baseline, current)) err(`pre-push: note — ${path} is new, now baselined.`)
455
+ const regressions = coverageRegressions(baseline, current, options.existsOnDisk)
422
456
  if (regressions.length === 0) return 0
423
457
  err(`pre-push: FAIL — coverage regressed in ${summaryPath}:`)
424
458
  for (const line of regressions) err(` - ${line}`)
425
459
  return 1
426
460
  }
427
461
 
462
+ export interface CoveragePushProbes {
463
+ rel: string
464
+ worktree: string | null
465
+ head: string | null
466
+ baseline: string | null
467
+ addedInHead: boolean
468
+ existsOnDisk: (path: string) => boolean
469
+ }
470
+
471
+ /** The armed coverage ratchet at push: every missing input fails, only a summary added in HEAD is a first baseline. */
472
+ export function coverageRatchetPush(probes: CoveragePushProbes, err: (line: string) => void): number {
473
+ const current = probes.head ?? probes.worktree
474
+ if (current === null) {
475
+ err(`pre-push: FAIL — pre_push.coverage_ratchet is on but ${probes.rel} exists in neither HEAD nor the work tree.`)
476
+ err(' Commit a summary or set pre_push.coverage_ratchet = false.')
477
+ return 1
478
+ }
479
+ return coverageRatchetStep(probes.rel, () => probes.baseline, () => current, err, {
480
+ firstBaseline: probes.addedInHead,
481
+ existsOnDisk: probes.existsOnDisk,
482
+ })
483
+ }
484
+
428
485
  /**
429
486
  * The repository's declared commands for one hook.
430
487
  *
@@ -544,6 +601,13 @@ function dirtyPaths(porcelain: string): string[] {
544
601
  return paths
545
602
  }
546
603
 
604
+ /** The repository's `[controls.integrity] watched` plus the declared compliance register folder (see README, "Managed dot paths and protected controls"). */
605
+ export function integrityWatched(repo: string): string[] {
606
+ const watched = loadConfig(repo).integrity.watched
607
+ const tree = declaredComplianceTree(readFileSync(join(repo, CONFIG_REL), 'utf8'))
608
+ return tree === null ? watched : [...watched, `${tree}/**`]
609
+ }
610
+
547
611
  /** Refuses a commit while a protected worktree edit is excluded from its index. */
548
612
  export function integrityStep(
549
613
  porcelain: string,
@@ -600,10 +664,10 @@ export function integrityGate(
600
664
  * `azureDevOpsPromotionGovernanceGate` below. A bare `adl-repo-controls
601
665
  * doctor` run (not through a hook) keeps reporting everything.
602
666
  */
603
- export function doctorGate(readDoctor: () => DoctorReport, err: (line: string) => void): number {
667
+ export function doctorGate(readDoctor: () => DoctorReport, err: (line: string) => void, hook = 'pre-commit'): number {
604
668
  const report = readDoctor()
605
669
  if (report.ok) return 0
606
- err('pre-commit: FAIL — adl-repo-controls doctor found an unarmed control:')
670
+ err(`${hook}: FAIL — adl-repo-controls doctor found an unarmed control:`)
607
671
  for (const check of report.checks) {
608
672
  if (check.status === 'fail') err(` ${check.id} ${check.message}`)
609
673
  }
@@ -621,21 +685,16 @@ export function doctorGate(readDoctor: () => DoctorReport, err: (line: string) =
621
685
  * `loadConfig`) can throw on a malformed config; that is reported the same
622
686
  * way as an unarmed check rather than crashing the hook process.
623
687
  */
624
- function azureDevOpsPromotionGovernanceGate(readChecks: () => Check[], err: (line: string) => void): number {
625
- let checks: Check[]
688
+ function azureDevOpsPromotionGovernanceGate(readReport: () => DoctorReport, err: (line: string) => void): number {
689
+ let report: DoctorReport
626
690
  try {
627
- checks = readChecks()
691
+ report = readReport()
628
692
  } catch (error) {
629
- err('pre-push: FAIL — could not evaluate Azure DevOps promotion governance:')
693
+ err('pre-push: FAIL — could not evaluate adl-repo-controls doctor:')
630
694
  err(` ${error instanceof Error ? error.message : String(error)}`)
631
695
  return 1
632
696
  }
633
- const failed = checks.filter((check) => check.status === 'fail')
634
- if (failed.length === 0) return 0
635
- err('pre-push: FAIL — adl-repo-controls doctor found an unarmed Azure DevOps promotion control:')
636
- for (const check of failed) err(` ${check.id} ${check.message}`)
637
- err(' Fix the named control, then rerun: adl-repo-controls doctor --repo . --json')
638
- return 1
697
+ return doctorGate(() => report, err, 'pre-push')
639
698
  }
640
699
 
641
700
  /** Real I/O around `findingsRatchetReport`. */
@@ -643,7 +702,12 @@ function runFindingsRatchet(repo: string, context: RunContext): number {
643
702
  const config = loadConfig(repo)
644
703
  return findingsRatchetReport(
645
704
  config.findings,
646
- () => readFileSync(join(repo, config.findings.floor), 'utf8'),
705
+ () => {
706
+ if (!existsSync(join(repo, config.findings.floor))) {
707
+ throw new Error(`${config.findings.floor} does not exist; commit a floor or set pre_push.findings_ratchet = false`)
708
+ }
709
+ return readFileSync(join(repo, config.findings.floor), 'utf8')
710
+ },
647
711
  (command) => capture(['bash', '-c', command], repo, context.env),
648
712
  context.err,
649
713
  )
@@ -673,7 +737,7 @@ export function runHook(name: HookName, args: string[], context: RunContext): nu
673
737
  const code = integrityGate(
674
738
  () => capture(['git', 'status', '--porcelain=v1', '-z', '--untracked-files=all'], repo, context.env),
675
739
  () => capture(['git', 'diff', '--cached', '--name-only', '--no-renames', '-z'], repo, context.env),
676
- loadConfig(repo).integrity.watched,
740
+ integrityWatched(repo),
677
741
  context.err,
678
742
  )
679
743
  if (code !== 0) return code
@@ -698,37 +762,28 @@ export function runHook(name: HookName, args: string[], context: RunContext): nu
698
762
  // when `[controls.compliance].enabled` is empty -- the same "wrapper on,
699
763
  // work gated by configured state" shape as stagedCoverage/coverageRatchet/
700
764
  // findingsRatchet above and the docFreshness floor's empty-files case.
701
- if (name === 'pre-commit' && stagedComplianceEnabled(repo, context.env)) {
702
- if (loadComplianceConfig(repo).enabled.length > 0) {
703
- const code = withStagedIndexSnapshot(repo, context.env, (snapshot) => {
704
- // Activation is itself read from the candidate commit. An unstaged config
705
- // edit cannot turn a staged gate off (or on), and a commit introducing the
706
- // gate proves it is armed before it lands.
707
- return complianceStep(() =>
708
- runUv(['--files', snapshot.files, '--exhaustive'], snapshot.root, context),
709
- )
710
- })
711
- if (code !== 0) return code
712
- }
765
+ if (name === 'pre-commit') {
766
+ const code = stagedComplianceStep(
767
+ stagedComplianceGate(repo, context.env),
768
+ () =>
769
+ withStagedIndexSnapshot(repo, context.env, (snapshot) =>
770
+ complianceStep(() => runUv(['--files', snapshot.files, '--exhaustive'], snapshot.root, context)),
771
+ ),
772
+ context.err,
773
+ )
774
+ if (code !== 0) return code
713
775
  }
714
776
  // Staged coverage, through the repository's declared command. Was a hardcoded
715
777
  // shell-out inside the bundled pre-commit into this package's copy of the
716
778
  // consumer's coverage code; see `stagedCoverageStep`.
717
- // Safety gate: `stagedCoverage` defaults ON; a repository that has not declared
718
- // `[controls.coverage].produce` has nothing to run, so the step is a no-op.
719
- // Same shape as `compliance` below: the wrapper is on, the work is gated by
720
- // whether the repository opted into the configuration it needs.
721
779
  if (name === 'pre-commit' && loadConfig(repo).preCommit.stagedCoverage) {
722
- const config = loadConfig(repo)
723
- if (config.coverage.produce.trim() !== '') {
724
- const code = stagedCoverageStep(
725
- config.coverage,
726
- (command) => capture(['bash', '-c', command], repo, context.env),
727
- null,
728
- context.err,
729
- )
730
- if (code !== 0) return code
731
- }
780
+ const code = stagedCoverageStep(
781
+ loadConfig(repo).coverage,
782
+ (command) => capture(['bash', '-c', command], repo, context.env),
783
+ null,
784
+ context.err,
785
+ )
786
+ if (code !== 0) return code
732
787
  }
733
788
  // The Azure DevOps promotion-governance checks, moved here from pre-commit
734
789
  // (2026-09-28, Tarun): network access and a minted token are already
@@ -737,10 +792,7 @@ export function runHook(name: HookName, args: string[], context: RunContext): nu
737
792
  // no `[controls.release]` at all, so this runs on every pre-push rather than
738
793
  // behind its own config flag.
739
794
  if (name === 'pre-push') {
740
- const code = azureDevOpsPromotionGovernanceGate(
741
- () => azureDevOpsPromotionGovernanceChecks(repo, context.env),
742
- context.err,
743
- )
795
+ const code = azureDevOpsPromotionGovernanceGate(() => doctor(repo, context.env), context.err)
744
796
  if (code !== 0) return code
745
797
  }
746
798
  // The coverage MEASUREMENT at push, before the ratchet below grades what it
@@ -757,37 +809,29 @@ export function runHook(name: HookName, args: string[], context: RunContext): nu
757
809
  }
758
810
  // The coverage ratchet at push. Was `command.ts check-push` inside the bundled
759
811
  // pre-push, running the package's copy of the consumer's coverage code.
760
- // Safety gate: `coverageRatchet` defaults ON; the ratchet has no baseline
761
- // unless the repository has staged a coverage summary file at the path
762
- // `[controls.coverage].summary` declares. With no file on either side of the
763
- // index, the step is a no-op — same shape as compliance below.
764
812
  if (name === 'pre-push' && loadConfig(repo).prePush.coverageRatchet) {
765
- const config = loadConfig(repo)
766
- const rel = config.coverage.summary
767
- if (rel !== '' && (existsSync(join(repo, rel)) || capture(['git', 'cat-file', '-e', `HEAD:${rel}`], repo, context.env).code === 0)) {
768
- const at = (ref: string) => {
769
- const shown = capture(['git', 'show', `${ref}:${rel}`], repo, context.env)
770
- return shown.code === 0 ? shown.stdout : null
771
- }
772
- const code = coverageRatchetStep(
773
- rel,
774
- () => at('HEAD~1'),
775
- () => at('HEAD') ?? readFileSync(join(repo, rel), 'utf8'),
776
- context.err,
777
- )
778
- if (code !== 0) return code
813
+ const rel = loadConfig(repo).coverage.summary
814
+ const at = (ref: string): string | null => {
815
+ const shown = capture(['git', 'show', `${ref}:${rel}`], repo, context.env)
816
+ return shown.code === 0 ? shown.stdout : null
779
817
  }
818
+ const added = capture(['git', 'diff-tree', '--root', '--no-commit-id', '--name-status', '-r', 'HEAD', '--', rel], repo, context.env)
819
+ const code = coverageRatchetPush(
820
+ {
821
+ rel,
822
+ worktree: existsSync(join(repo, rel)) ? readFileSync(join(repo, rel), 'utf8') : null,
823
+ head: at('HEAD'),
824
+ baseline: at('HEAD~1'),
825
+ addedInHead: added.code === 0 && added.stdout.startsWith('A'),
826
+ existsOnDisk: (path) => existsSync(join(repo, path)),
827
+ },
828
+ context.err,
829
+ )
830
+ if (code !== 0) return code
780
831
  }
781
- // The findings ratchet, before the bundled script, same shape as compliance
782
- // above. Safety gate: `findingsRatchet` defaults ON; the gate has nothing
783
- // to grade unless `[controls.findings].floor` exists. With no floor file,
784
- // the step is a no-op — same shape as compliance and coverageRatchet.
785
832
  if (name === 'pre-push' && loadConfig(repo).prePush.findingsRatchet) {
786
- const floorPath = loadConfig(repo).findings.floor
787
- if (floorPath !== '' && existsSync(join(repo, floorPath))) {
788
- const code = runFindingsRatchet(repo, context)
789
- if (code !== 0) return code
790
- }
833
+ const code = runFindingsRatchet(repo, context)
834
+ if (code !== 0) return code
791
835
  }
792
836
  // git writes pre-push's ref lines on stdin exactly once. The bundled script
793
837
  // reads them for the branch rules and `git lfs pre-push` needs them too, so