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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/bin/adl +2 -2
  2. package/bin/health-check +2 -2
  3. package/bin/reconcile +41 -21
  4. package/bin/setup +2 -1
  5. package/package.json +1 -1
  6. package/packages/agent-hooks/README.md +77 -8
  7. package/packages/agent-hooks/assets/opencode-plugin.template.ts +2 -6
  8. package/packages/agent-hooks/src/deploy/opencode-template.ts +5 -3
  9. package/packages/agent-hooks/src/hooks/crg/background.ts +7 -4
  10. package/packages/agent-hooks/src/hooks/log.ts +12 -12
  11. package/packages/agent-hooks/src/log/writer.ts +170 -24
  12. package/packages/agent-hooks/src/main.ts +1 -1
  13. package/packages/agent-hooks/src/rules/impl/mcp-tool.ts +1 -1
  14. package/packages/agent-hooks/src/rules/shell/invocation.ts +2 -1
  15. package/packages/agent-skills/assets/agent-search-tool-audit/SKILL.md +24 -2
  16. package/packages/agent-skills/assets/agent-search-tool-audit/scripts/audit.sh +19 -45
  17. package/packages/agent-skills/assets/diagnosing-bugs/SKILL.md +9 -0
  18. package/packages/agent-skills/assets/diagnosing-bugs/scripts/hitl-loop.template.sh +1 -12
  19. package/packages/fleet/manifest.json +2 -1
  20. package/packages/fleet/src/repo-maintain.ts +5 -2
  21. package/packages/repo-controls/AGENTS.md +7 -4
  22. package/packages/repo-controls/README.md +346 -56
  23. package/packages/repo-controls/assets/GLOBAL-INVARIANTS.md +21 -0
  24. package/packages/repo-controls/assets/promote.ts +7 -85
  25. package/packages/repo-controls/assets/reconcile-guard.sh +1 -14
  26. package/packages/repo-controls/assets/scaffold/docs/AGENTS.md +25 -0
  27. package/packages/repo-controls/assets/scaffold/docs/CLAUDE.md +1 -0
  28. package/packages/repo-controls/assets/scaffold/docs/decisions/index.md +17 -0
  29. package/packages/repo-controls/assets/scaffold/docs/notes.md +20 -0
  30. package/packages/repo-controls/assets/scaffold/plans/AGENTS.md +28 -0
  31. package/packages/repo-controls/assets/scaffold/plans/CLAUDE.md +1 -0
  32. package/packages/repo-controls/assets/scaffold/plans/GAPS-AND-FINDINGS.md +12 -0
  33. package/packages/repo-controls/assets/scaffold/plans/LEDGER.md +8 -0
  34. package/packages/repo-controls/assets/scaffold/plans/features/.gitkeep +0 -0
  35. package/packages/repo-controls/assets/scaffold/root/AGENTS.md +22 -0
  36. package/packages/repo-controls/assets/scaffold/root/CLAUDE.md +1 -0
  37. package/packages/repo-controls/assets/scaffold/workflows/AGENTS.md +16 -0
  38. package/packages/repo-controls/assets/scaffold/workflows/CLAUDE.md +1 -0
  39. package/packages/repo-controls/assets/scaffold/workflows/pipelines/.gitkeep +0 -0
  40. package/packages/repo-controls/assets/scaffold/workflows/scripts/.gitkeep +0 -0
  41. package/packages/repo-controls/assets/version-bump.sh +7 -48
  42. package/packages/repo-controls/package/adl-compliance/AGENTS.md +25 -7
  43. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/__init__.py +6 -4
  44. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/check.py +1 -1
  45. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/adr_lifecycle/test_check.py +1 -1
  46. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/check.py +7 -42
  47. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/fixtures/excluded/web/i18n/locales/zh-CN/strings.ts +1 -0
  48. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/english_only/test_check.py +10 -44
  49. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/architecture/ui-contract.md +1 -1
  50. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/fixtures/docs/research/UI-CONTRACT.md +1 -1
  51. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/filename/test_check.py +1 -1
  52. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/check.py +1 -7
  53. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/index_updated/test_check.py +12 -3
  54. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/check.py +15 -23
  55. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/docs-knowledge-indexes.yaml +0 -136
  56. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/indexes/test_check.py +18 -0
  57. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/okf_profile/docs-okf-profiles.yaml +2 -2
  58. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/register.py +0 -7
  59. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/check.py +2 -4
  60. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-delivery.md +10 -0
  61. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/stable-recommendation.md +12 -0
  62. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/fixtures/semantics-violations/docs/architecture/guides/target-narration.md +10 -0
  63. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/docs/semantics/test_check.py +6 -7
  64. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/ci_paths/test_check.py +3 -3
  65. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/check.py +10 -12
  66. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/grants.yaml +1 -1
  67. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-clean/web/src/renderer/solo/item.ts +1 -0
  68. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/web/src/a.ts +1 -0
  69. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-empty-register/web/src/b.ts +1 -0
  70. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/grants.yaml +1 -1
  71. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/fresh/fresh.ts +1 -0
  72. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub1/only.ts +1 -0
  73. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub10/only.ts +1 -0
  74. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub11/only.ts +1 -0
  75. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub2/only.ts +1 -0
  76. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub3/only.ts +1 -0
  77. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub4/only.ts +1 -0
  78. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub5/only.ts +1 -0
  79. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub6/only.ts +1 -0
  80. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub7/only.ts +1 -0
  81. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub8/only.ts +1 -0
  82. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/many/sub9/only.ts +1 -0
  83. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/services/i18n/locales/de/common.json +1 -0
  84. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/renderer/solo/item.ts +1 -0
  85. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-excluded/web/src/vendored-crate/Cargo.toml +2 -0
  86. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/grants.yaml +1 -1
  87. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-missing-tree/web/src/renderer/solo/item.ts +1 -0
  88. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/grants.yaml +1 -1
  89. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/a.ts +1 -0
  90. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/b.ts +1 -0
  91. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-no-ceilings/web/src/c.ts +1 -0
  92. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/grants.yaml +2 -2
  93. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-stale-entry/web/src/renderer/solo/item.ts +1 -0
  94. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/grants.yaml +1 -1
  95. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/fresh/fresh.ts +1 -0
  96. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub1/only.ts +1 -0
  97. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub10/only.ts +1 -0
  98. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub11/only.ts +1 -0
  99. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub2/only.ts +1 -0
  100. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub3/only.ts +1 -0
  101. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub4/only.ts +1 -0
  102. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub5/only.ts +1 -0
  103. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub6/only.ts +1 -0
  104. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub7/only.ts +1 -0
  105. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub8/only.ts +1 -0
  106. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/many/sub9/only.ts +1 -0
  107. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unbaselined/web/src/renderer/solo/item.ts +1 -0
  108. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/fixtures/directory-ratchet-unusable-register/web/src/renderer/solo/item.ts +1 -0
  109. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/directory_ratchet/test_check.py +46 -54
  110. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/check.py +16 -76
  111. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/markdown_trees/test_check.py +35 -32
  112. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/check.py +46 -213
  113. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/fixtures/placement-allowed/README.md +1 -0
  114. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/placement/test_check.py +76 -194
  115. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/check.py +18 -2
  116. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/plans_layout/test_check.py +49 -0
  117. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/check.py +1 -1
  118. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/links.md +20 -0
  119. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-broken/docs/architecture/guides/route-target.md +8 -0
  120. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/links.md +20 -0
  121. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/fixtures/link-targets-clean/docs/architecture/guides/route-target.md +8 -0
  122. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/references/test_check.py +4 -4
  123. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/check.py +3 -3
  124. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/fixtures/.notes/declared.txt +1 -0
  125. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_closed_set/test_check.py +8 -1
  126. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/check.py +15 -135
  127. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/root_markdown_allowlist/test_check.py +98 -50
  128. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/check.py +1 -1
  129. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/layout/scripts_size_depth/test_check.py +1 -1
  130. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/check.py +1 -1
  131. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/lfs/integrity/test_check.py +14 -10
  132. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/__init__.py +3 -10
  133. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/base.py +42 -0
  134. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_check.py +82 -49
  135. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/banned_commands/test_scope.py +71 -0
  136. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/__init__.py +7 -0
  137. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/check.py +15 -0
  138. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/legacy_search/test_check.py +32 -0
  139. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/cargo_lock.py +1 -1
  140. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/check.py +56 -124
  141. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.lock +7 -0
  142. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/Cargo.toml +4 -0
  143. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/bun.lock +1 -0
  144. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/package.json +1 -0
  145. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/rust-toolchain.toml +3 -0
  146. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/clean/src/main.rs +1 -0
  147. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.lock +7 -0
  148. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/Cargo.toml +4 -0
  149. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/bun.lock +0 -0
  150. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/package.json +1 -0
  151. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/rust-toolchain.toml +3 -0
  152. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/fixtures/empty-bun-lock/src/main.rs +1 -0
  153. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_check.py +8 -37
  154. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_declared_paths.py +95 -130
  155. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_root_lockfiles.py +101 -0
  156. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/lockfile_integrity/test_stale_lock.py +25 -13
  157. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/__init__.py +7 -0
  158. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/check.py +124 -0
  159. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/fixtures/valid-register.yaml +4 -0
  160. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/scanner.py +217 -0
  161. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/no_multiline_comments/test_check.py +391 -0
  162. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/__init__.py +7 -0
  163. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/check.py +16 -0
  164. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/checks/repository/vcs_in_scripts/test_check.py +32 -0
  165. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/cli.py +12 -1
  166. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/ast_grep.py +2 -3
  167. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/check.py +3 -0
  168. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/constants.py +1 -1
  169. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/declarations.py +24 -0
  170. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/receipt.py +2 -2
  171. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/core/scripts.py +120 -0
  172. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.py +1 -1
  173. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/config.toml +4 -4
  174. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.alt.yaml +93 -99
  175. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/gate-registry.yaml +93 -99
  176. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/registry.py +1 -1
  177. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/policy/root_shape.py +6 -47
  178. package/packages/repo-controls/package/adl-compliance/src/adl_compliance/runner.py +54 -8
  179. package/packages/repo-controls/scripts/hooks/pre-commit +12 -2
  180. package/packages/repo-controls/scripts/hooks/pre-push +27 -19
  181. package/packages/repo-controls/src/cli.ts +22 -3
  182. package/packages/repo-controls/src/compliance-config.ts +132 -65
  183. package/packages/repo-controls/src/config.ts +55 -46
  184. package/packages/repo-controls/src/constants.ts +9 -0
  185. package/packages/repo-controls/src/coverage-ratchet.ts +19 -4
  186. package/packages/repo-controls/src/doctor.ts +20 -30
  187. package/packages/repo-controls/src/findings-ratchet.ts +14 -0
  188. package/packages/repo-controls/src/gitignore-block.ts +3 -13
  189. package/packages/repo-controls/src/hook-log.ts +7 -10
  190. package/packages/repo-controls/src/hook.ts +127 -83
  191. package/packages/repo-controls/src/install-compliance.ts +112 -0
  192. package/packages/repo-controls/src/install.ts +133 -30
@@ -74,19 +74,19 @@ rows:
74
74
  # Same reasoning, the TypeScript-shaped colocated test file: this scope
75
75
  # excluded `test_*.py` but nothing that names a TypeScript test the way
76
76
  # every managed repo's own convention does. Caught live against
77
- # enact-vps-m5: `global-tools/test/install-embedding-model.test.ts`
77
+ # a managed repository: `global-tools/test/install-embedding-model.test.ts`
78
78
  # asserts that `selectVocabulary` DROPS non-Latin-script tokens, and
79
79
  # embeds a CJK literal as the negative-test input -- exactly
80
80
  # the "test asserting a non-English string" case above, just in `.ts`
81
81
  # rather than `.py`. COVERAGE COST, stated not buried: 548 colocated
82
82
  # `*.test.ts`/`*.test.tsx` files across the four managed repos
83
- # (enact-m5 400, enact-vps-m5 134, enact-repo-controls-m5 14,
84
- # enact-extensions 0) stop being graded for english-only.
83
+ # (400, 134, 14 and 0 in the four
84
+ # repositories) stop being graded for english-only.
85
85
  - '!**/*.test.ts'
86
86
  - '!**/*.test.tsx'
87
87
  # The four exclusions removed here named
88
- # `<typescript>/packages/desktop/...` and `<typescript>/tests/unit/...`
89
- # -- one repository's files written into the shared package. Measured
88
+ # one repository's application and unit-test files, written into the
89
+ # shared package under a tree placeholder. Measured
90
90
  # before removing: 0 of the 4 exist in ANY of the four managed
91
91
  # repositories, so they excluded nothing anywhere. If a consumer ever
92
92
  # has legitimately non-English source, it declares that itself.
@@ -363,7 +363,6 @@ rows:
363
363
  scope:
364
364
  - '{AGENTS,CLAUDE}.md'
365
365
  - '**/{AGENTS,CLAUDE}.md'
366
- - <compliance>/checks/layout/agents-layout/check.sh
367
366
  - '!**/fixtures/**'
368
367
  grants: 0
369
368
  timeout_ms: 60000
@@ -386,7 +385,7 @@ rows:
386
385
  #
387
386
  # Same shape as LAY-014's `!<workflows>/scripts/**/fixtures/**`, one
388
387
  # level up. COVERAGE COST, stated rather than buried: this drops 19 of
389
- # enact-m5's 44 `workflows/scripts/` files (43%) from the gate. The risk
388
+ # one managed repository's 44 `workflows/scripts/` files (43%) from the gate. The risk
390
389
  # it trades away is small — a TEST that truly needs a gitignored file
391
390
  # fails loudly in CI, whereas a production CI script referencing one can
392
391
  # silently skip, and production scripts are all still graded.
@@ -410,29 +409,14 @@ rows:
410
409
  # recorded state the register file is deleted. No ceilings declared
411
410
  # means nothing to enforce — the gate is inert.
412
411
  scope:
413
- - <typescript>/**
414
- - <rust>/**
415
- - <infra>/**
416
- - <dev>/**
417
- - <workflows>/**
418
- # A single '*' cannot cross a '/': it is one path segment, not "the rest
419
- # of the subtree". Confirmed live against this checkout, not just the
420
- # ported test fixture — locales/de/common.json sits two segments below
421
- # locales/, and real crate/build/terraform files sit at varying depths
422
- # below their prefix (`rg`/`find` measured 2 and 4 segments for
423
- # Cargo.toml, 4 for build.rs, 2 through 5 for main.tf) — every nested
424
- # occurrence was silently unexcluded until these five became '**'.
425
- - '!<typescript>/packages/desktop/src/renderer/services/i18n/locales/**'
426
- - '!<rust>/crates/enact-app/assets/builtin-skills/**'
427
- - '!<rust>/crates/enact-app/assets/builtin-harnesses/*'
428
- - '!<dev>/seed/access/*'
429
- - '!<dev>/seed/infra/*'
430
- - '!<dev>/seed/tenancy/*'
431
- - '!<dev>/scripts/*'
432
- - '!<dev>/ops/*'
433
- - '!**/Cargo.toml'
434
- - '!**/build.rs'
435
- - '!**/main.tf'
412
+ # The WHOLE repository (Tarun, 2026-10-08, PBI 598): the ratchet is
413
+ # repo-wide and hardcodes no exclusion. `**` matches every path of the
414
+ # Git-visible universe the CLI hands over (`ls-files --cached --others
415
+ # --exclude-standard`), and the measurement itself skips dot-prefixed
416
+ # components. What a repository exempts it declares itself: a ceiling
417
+ # per tree in [controls.compliance.directory-ratchet].max_children and
418
+ # its over-limit / single-file baselines in the ratchet register below.
419
+ - '**'
436
420
  # The ratchet register is a named INPUT of this gate, not a scope entry:
437
421
  # it is the CONSUMER's own file (<compliance>/ratchets/
438
422
  # LAY-004-over-limit-directory.yaml -- the estate's settled location for
@@ -485,21 +469,21 @@ rows:
485
469
  - id: LAY-008
486
470
  name: placement
487
471
  domain: layout
488
- title: Run retained placement gate
472
+ title: Reject throwaway files and undeclared image placement
489
473
  severity: M
490
474
  controls:
491
475
  - EC-A.5.36
492
476
  engine:
477
+ # Every file: a throwaway extension is reported anywhere, and an image or
478
+ # screenshot-named file is reported unless it sits in a directory the
479
+ # repository declares in `asset-dirs`. (The root allowlist is LAY-012's
480
+ # `root-closed-set`, not this row's.)
493
481
  scope:
494
- - '*'
495
- - '*.{log,tmp,bak,swp}'
496
- - '**/*.{log,tmp,bak,swp}'
497
- - <compliance>/checks/layout/placement/check.sh
482
+ - '**'
498
483
  # The corpora under placement-allowed/ and placement-stray/ are the
499
- # gate's own test input: enumerated asset paths, a screenshot, a
500
- # markdown file inside a crate src/ tree, and a file under retired
501
- # apps/. Graded as production content they are exactly the findings
502
- # the gate exists to raise.
484
+ # gate's own test input: a screenshot and a throwaway file. Graded as
485
+ # production content they are exactly the findings the gate exists to
486
+ # raise.
503
487
  - '!**/fixtures/**'
504
488
  grants: 0
505
489
  timeout_ms: 60000
@@ -567,7 +551,6 @@ rows:
567
551
  - '!.git/**'
568
552
  - '!.DS_Store'
569
553
  - '!.cache/**'
570
- - '!.enactrs/**'
571
554
  - '!.pytest_cache/**'
572
555
  - '!.ruff_cache/**'
573
556
  - '!.scratch/**'
@@ -652,27 +635,6 @@ rows:
652
635
  grants: 0
653
636
  timeout_ms: 60000
654
637
 
655
- # ---- pipeline ----
656
- - id: PIP-004
657
- name: phase-disjoint
658
- domain: pipeline
659
- title: Enforce phase mutual exclusion across every registry row
660
- severity: M
661
- controls:
662
- - EC-A.5.36
663
- engine:
664
- # Scope is SELECTION, not resolution. The check finds the registry
665
- # structurally, from its own position in the package, and that is why it
666
- # needs no path here to READ it -- but scope decides whether a changed
667
- # file makes this row run at all, and an empty scope matches nothing. The
668
- # gate that enforces phase disjointness across the registry was therefore
669
- # never selected by a change to the registry: the one edit that can break
670
- # it was the one edit that did not run it.
671
- scope:
672
- - <compliance>/policy/gate-registry.yaml
673
- grants: 0
674
- timeout_ms: 60000
675
-
676
638
  # ---- repository ----
677
639
  - id: REP-001
678
640
  name: license-declared
@@ -707,53 +669,32 @@ rows:
707
669
  - EC-A.5.21
708
670
  engine:
709
671
  scope:
710
- # The tree-derived DEFAULT: each half is exactly two entries under its own
711
- # tree. A repository whose manifests do not sit one level under its tree
712
- # (enact-combined: a brace-union `typescript` tree, manifests at the root and
713
- # under apps/*/) states the paths itself with `[controls.compliance.lockfiles]`
714
- # (`bun-manifests`/`bun-lockfiles`, `cargo-manifests`/`cargo-lockfiles`); a
715
- # declared half REPLACES that half's two entries, in the scope and in what the
716
- # check grades. See `LockfileIntegrity.declared_scope`.
717
- - <rust>/Cargo.toml
718
- - <rust>/Cargo.lock
719
- - <typescript>/package.json
720
- - <typescript>/bun.lock
672
+ # The REPOSITORY-ROOT pairs, graded automatically (Tarun, 2026-10-08,
673
+ # PBI 598): `Cargo.toml` -> `Cargo.lock` and `package.json` -> `bun.lock`.
674
+ # Every other lockfile is graded only because the repository declares it
675
+ # with `[controls.compliance.lockfiles]` (`bun-manifests`/`bun-lockfiles`,
676
+ # `cargo-manifests`/`cargo-lockfiles`); a declaration ADDS its manifests
677
+ # and lockfiles to this scope and to what the check grades. See
678
+ # `LockfileIntegrity.declared_scope`.
679
+ - Cargo.toml
680
+ - Cargo.lock
681
+ - package.json
682
+ - bun.lock
721
683
  grants: 0
722
684
  timeout_ms: 60000
723
685
  - id: REP-004
724
- name: no-banned-commands
686
+ name: no-legacy-search
725
687
  domain: repository
726
- title: Forbid banned command invocations in shell scripts and justfiles
688
+ title: Forbid standalone legacy search in shell scripts and justfiles
727
689
  severity: M
728
690
  controls:
729
691
  - EC-A.5.36
730
692
  engine:
731
693
  scope:
732
- # Both GIT-001 (no version-control invocation) and GRP-001 (no
733
- # standalone grep, use rg) are restricted to the compliance package --
734
- # a deliberate, explicit decision (both are compliance gates), not an
735
- # inherited default. Neither rule is about the rest of the repository,
736
- # so nothing here is expressed as an extension glob any more: the old
737
- # repository-wide `*.{sh,bash,...}` list existed only because GRP-001
738
- # used to run everywhere and needed to exclude non-shell files from a
739
- # much larger candidate set. Restricted to compliance/**, that ceiling
740
- # is gone -- check.sh's own shell/justfile filter is enough.
741
- - <compliance>/**
742
- # The scope contract itself. `discover_files` lists the tree through the
743
- # version-control plumbing; every other file in this package reaches it
744
- # through this one, which is why it is exempted here rather than at each
745
- # caller. Out of scope, not granted: the rule does not apply to the
746
- # implementation of the mechanism it exists to make optional everywhere
747
- # else.
748
- - '!<compliance>/lib/discovery.sh'
749
- # The same exemption, on the test side. `_corpus-set.sh` is the one
750
- # place a fixture enumerates its corpus, and it asks the corpus the
751
- # same question the runner asks the repository, through the same
752
- # plumbing -- deliberately, so the set a test hands over is the same
753
- # KIND of set production hands over. Every converted gate test reaches
754
- # it through this file, which is why it is exempted here rather than at
755
- # each caller.
756
- - '!<compliance>/checks/framework/_corpus-set.sh'
694
+ # Repo-wide: every file, narrowed by the check to the shell scripts
695
+ # and justfiles `core/scripts.py` selects (by suffix, name or
696
+ # shebang), the same selector REP-007 uses.
697
+ - '**'
757
698
  # EVERY gate's fixture corpus, not just this row's. A fixture is
758
699
  # deliberate violation material -- both halves of this row's own corpus
759
700
  # contain real invocations on purpose -- and graded as production
@@ -775,3 +716,56 @@ rows:
775
716
  - '**'
776
717
  grants: 0
777
718
  timeout_ms: 60000
719
+ # Split from REP-004 (Tarun 2026-10-08: PBI 605). Same repo-wide scope; a
720
+ # repository whose git hooks and release scripts must call the version-control
721
+ # tool leaves this gate out of `enabled` and keeps REP-004.
722
+ - id: REP-006
723
+ name: no-vcs-in-scripts
724
+ domain: repository
725
+ title: Forbid version-control calls in shell scripts and justfiles
726
+ severity: M
727
+ controls:
728
+ - EC-A.5.36
729
+ engine:
730
+ scope:
731
+ # Repo-wide: every file, narrowed by the check to the shell scripts
732
+ # and justfiles `core/scripts.py` selects (by suffix, name or
733
+ # shebang), the same selector REP-007 uses.
734
+ - '**'
735
+ # EVERY gate's fixture corpus, not just this row's. A fixture is
736
+ # deliberate violation material -- both halves of this row's own corpus
737
+ # contain real invocations on purpose -- and graded as production
738
+ # content it fails the gate it exists to prove. Declared here rather
739
+ # than skipped inside the check: path exclusions carry no default in
740
+ # this tree.
741
+ - '!**/fixtures/**'
742
+ # The managed projections adl rebuilds on every install (Tarun,
743
+ # 2026-10-08): their git hooks must call the version-control tool and
744
+ # the repository cannot edit them. Only this gate leaves them out.
745
+ - '!.workflows/**'
746
+ - '!.agents/**'
747
+ - '!.claude/**'
748
+ - '!.codex/**'
749
+ - '!.cursor/**'
750
+ - '!.kimi-code/**'
751
+ - '!.opencode/**'
752
+ grants: 0
753
+ timeout_ms: 60000
754
+ # Bug 602 (Tarun 2026-10-08): no multi-line comment in code; the explanation
755
+ # moves to docs/. Grades every code file by suffix (the check's own question,
756
+ # `scanner.is_code_file`), so the scope is the whole repository less every
757
+ # gate's fixture corpus. Exempt files are named by the repository's own
758
+ # register, `<compliance>/ratchets/REP-007-multiline-comments.yaml`.
759
+ - id: REP-007
760
+ name: no-multiline-comments
761
+ domain: repository
762
+ title: Forbid multi-line comments in code files
763
+ severity: M
764
+ controls:
765
+ - EC-A.8.28
766
+ engine:
767
+ scope:
768
+ - '**'
769
+ - '!**/fixtures/**'
770
+ grants: 0
771
+ timeout_ms: 60000
@@ -69,7 +69,7 @@ class EngineSpec(StrictModel):
69
69
  """Ordered list, last match wins, '!' excludes. Not a grant: it carries no
70
70
  signature and there is nothing to shrink.
71
71
 
72
- Stored EXPANDED: `<rust>/crates/**` is `backend/crates/**` by the time
72
+ Stored EXPANDED: `<docs>/guides/**` is `documentation/guides/**` by the time
73
73
  anything reads this. Nothing downstream knows placeholders exist, which is
74
74
  the point — one mechanism resolves the consumer's layout, at load, and the
75
75
  matcher sees ordinary repository-relative globs."""
@@ -57,9 +57,7 @@ VCS_AND_META: frozenset[str] = frozenset({
57
57
 
58
58
  GUIDES: frozenset[str] = frozenset({
59
59
  "AGENTS.md",
60
- "CLAUDE-INVARIANTS.md",
61
60
  "CLAUDE.md",
62
- "INDEPENDENT-VERIFICATION.md",
63
61
  "INVARIANTS.md",
64
62
  "README.md",
65
63
  })
@@ -93,33 +91,18 @@ NODE_TOOLCHAIN: frozenset[str] = frozenset({
93
91
  ".nvmrc",
94
92
  ".prettierrc",
95
93
  ".prettierignore",
96
- # Same tool as `.prettierrc` above, the same relationship `.dockerignore`
97
- # already has to Docker in `VCS_AND_META` — a paired root-required ignore
98
- # file, not a look-alike added on resemblance. Deliberately NOT extended
99
- # to `.markdownlint.json`/`.markdownlintignore`/`.yamllint.yml`: grepped
100
- # for real in the one consumer repository that carries all three
101
- # (enact-extensions) — zero hits in `package.json`, `bun.lock`, any
102
- # `scripts/` file, or `azure-pipelines.yml`. Nothing in that repository's
103
- # own tooling invokes markdownlint or yamllint; they read as editor-only
104
- # preference files there, not a root file some build step requires to
105
- # find, so they stay off this list until a consumer shows otherwise.
94
+ # Paired root-required ignore file like `.dockerignore`; markdownlint/yamllint files are declared by the repository.
106
95
  "biome.json",
107
96
  "bun.lock",
108
97
  "bunfig.toml",
109
98
  "eslint.config.js",
110
- "index.html",
111
- "knip.json",
112
99
  "opencode.json",
113
100
  "opencode.jsonc",
114
101
  "package-lock.json",
115
102
  "package.json",
116
- "playwright.config.ts",
117
103
  "pnpm-lock.yaml",
118
- "svelte.config.js",
119
104
  "tsconfig.json",
120
105
  "tsconfig.base.json",
121
- "vite.config.ts",
122
- "vitest.config.ts",
123
106
  "yarn.lock",
124
107
  })
125
108
 
@@ -147,18 +130,10 @@ PROJECT_CONFIG: frozenset[str] = frozenset({
147
130
  # `azure-pipelines.yml` at their root, and `layout.placement`/
148
131
  # `layout.root-closed-set` both reported it as undeclared, a false
149
132
  # failure on every Azure-DevOps-driven consumer this package can
150
- # plausibly serve. Same shape as `docker-compose.yml` below: a
151
- # build/deploy pipeline definition naming THIS repository's own stages,
133
+ # plausibly serve. A build/deploy
134
+ # pipeline definition naming THIS repository's own stages,
152
135
  # not a language-specific toolchain file.
153
- "coverage-floor.json",
154
- "ctx_overlays.json",
155
- "docker-compose.yml",
156
- "drizzle.config.ts",
157
136
  "adl-config.toml",
158
- # An adl-controlled Enact repository carries BOTH config files at its root,
159
- # permanently (Tarun, 2026-10-01: "there will be both"). Built in here so no
160
- # repository declares it by hand under `[controls.compliance.root] files`.
161
- "enact-config.toml",
162
137
  "justfile",
163
138
  })
164
139
 
@@ -195,37 +170,21 @@ AGENT_TREES: frozenset[str] = frozenset({
195
170
  })
196
171
 
197
172
  KNOWLEDGE_TREES: frozenset[str] = frozenset({
198
- ".changeset",
199
- ".docs",
200
- ".donors",
201
173
  "docs",
202
174
  "plans",
203
- "specs",
204
175
  })
205
176
 
206
- # Only the standard source trees. `apps/` and `packaging/` are not standard — a
207
- # repository that has them declares them under `[controls.compliance.root] dirs`.
208
- # (Tarun, 2026-10-01 — reverses the 2026-09-27 central listing.)
177
+ # Only adl's own or language-neutral trees (PBI 601); any other layout is declared under `[controls.compliance.root] dirs`.
209
178
  SOURCE_TREES: frozenset[str] = frozenset({
210
- "backend",
211
- "compliance",
212
- "docker",
213
179
  "bin",
214
- "dev",
215
- "frontend",
216
- "infra",
217
- "live-probes",
180
+ "compliance",
218
181
  "packages",
219
182
  "scripts",
220
- "server",
221
- "shared",
222
183
  "src",
223
- "src-tauri",
224
- "static",
225
184
  "tests",
226
185
  # The CI tree: pipelines, pipeline-templates, scripts, tools. Distinct from
227
186
  # `.workflows/`, which is adl-repo-controls' managed hook stubs and lock, and
228
- # both are live — one is what Azure runs, the other is what git runs. A repository
187
+ # both are live -- one is what Azure runs, the other is what git runs. A repository
229
188
  # carrying both is not mid-migration, and LAY-012 calling this one undeclared said
230
189
  # it was. (Owner decision, 2026-09-15.)
231
190
  "workflows",
@@ -27,6 +27,12 @@ from .policy.grants import granted
27
27
  from .policy.registry import Row
28
28
 
29
29
 
30
+ def _names_unknown_tree(relative: str, trees: Mapping[str, str] | None) -> bool:
31
+ """Does `relative` name a `<tree>` the effective map has no entry for at all?"""
32
+ known = trees or {}
33
+ return any(match.group(1) not in known for match in PLACEHOLDER.finditer(relative))
34
+
35
+
30
36
  def resolve_inputs(
31
37
  check: type[Check],
32
38
  root: Path,
@@ -34,6 +40,7 @@ def resolve_inputs(
34
40
  stack: ExitStack,
35
41
  trees: Mapping[str, str] | None = None,
36
42
  declared_trees: Mapping[str, str] | None = None,
43
+ dropped: dict[str, str] | None = None,
37
44
  ) -> dict[str, Path]:
38
45
  """Declared logical names -> absolute paths. The only place inputs are joined.
39
46
 
@@ -80,6 +87,8 @@ def resolve_inputs(
80
87
  # Declared-absent behaves like undeclared here; which of the two it
81
88
  # was decides below whether that is fatal or merely narrowing.
82
89
  if not absent(relative, trees)
90
+ # A WIDENING input (register of grants) with an unknown tree is "no register", never an error (PBI 599).
91
+ and not (name in check.widening_inputs and _names_unknown_tree(relative, trees))
83
92
  }
84
93
  except TreeMapError as exc:
85
94
  raise CouldNotRun(f"{check.id} declares an input naming an unknown tree: {exc}") from exc
@@ -99,6 +108,9 @@ def resolve_inputs(
99
108
  # only produce MORE findings. The check reads it with
100
109
  # `.get()` and must never assume it is present.
101
110
  resolved.pop(name, None)
111
+ if dropped is not None and name not in check.widening_inputs:
112
+ # Bug 597: a non-widening input dropped because its tree is declared ""; `run` turns a subscript of it into could-not-run.
113
+ dropped[name] = tree
102
114
  break
103
115
  raise CouldNotRun(
104
116
  f"{check.id} requires `{name}`; declare "
@@ -116,7 +128,7 @@ def resolve_inputs(
116
128
  # Overrides are keyed BY GATE ID. A flat map was handed to every selected
117
129
  # row, and the unknown-name guard below then failed every check that did not
118
130
  # happen to declare that name -- so declaring the one input `LAY-004` needs
119
- # took enact-m5 from 14 PASS to 18 ERROR / 1 FAIL / 0 PASS. The channel was
131
+ # took a managed repository from 14 PASS to 18 ERROR / 1 FAIL / 0 PASS. The channel was
120
132
  # unusable the moment anyone used it, which is why no repository in the
121
133
  # fleet had ever declared an input.
122
134
  mine = override.get(check.id) if override else None
@@ -168,7 +180,7 @@ def applicable_files(
168
180
 
169
181
  given_paths = tuple(given)
170
182
  narrowed = FileSet(paths=scope.intersect(patterns, given_paths))
171
- if not narrowed and exhaustive:
183
+ if not narrowed and exhaustive and not (check.empty_scope_is_clean and restated is None):
172
184
  raise CouldNotRun(
173
185
  f"engine.scope matches no file on disk ({' | '.join(patterns)}); "
174
186
  "a scope selecting nothing is could-not-run, never a clean pass"
@@ -236,6 +248,9 @@ def prepare_batches(
236
248
  except CouldNotRun as exc:
237
249
  results[row.id] = BatchResult(error=str(exc))
238
250
  continue
251
+ except Exception as exc:
252
+ results[row.id] = BatchResult(error=_crashed(check, exc))
253
+ continue
239
254
  if not files:
240
255
  results[row.id] = BatchResult()
241
256
  continue
@@ -259,13 +274,17 @@ def prepare_batches(
259
274
  )
260
275
  try:
261
276
  produced = provider.run(request)
262
- except (CouldNotRun, OSError) as exc:
263
- reason = f"batch provider {key!r} could not run: {type(exc).__name__}: {exc}"
264
- results.update({gate: BatchResult(error=reason) for gate in files_by_gate})
277
+ if not isinstance(produced, Mapping):
278
+ raise TypeError(
279
+ f"batch provider {key!r} returned {type(produced).__name__}, "
280
+ "not a mapping of gate id to BatchResult"
281
+ )
282
+ extra = sorted(set(produced) - set(files_by_gate))
283
+ except Exception as exc:
284
+ results.update({gate: BatchResult(error=f"{gate} crashed: {type(exc).__name__}: {exc}") for gate in files_by_gate})
265
285
  continue
266
286
 
267
287
  expected = set(files_by_gate)
268
- extra = sorted(set(produced) - expected)
269
288
  if extra:
270
289
  reason = (
271
290
  f"batch provider {key!r} returned unrequested gate result(s): "
@@ -288,6 +307,10 @@ def prepare_batches(
288
307
  return results
289
308
 
290
309
 
310
+ def _crashed(check_class: type[Check], exc: Exception) -> str:
311
+ return f"{check_class.id} crashed: {type(exc).__name__}: {exc}"
312
+
313
+
291
314
  def run(
292
315
  row: Row,
293
316
  root: Path,
@@ -338,8 +361,10 @@ def run(
338
361
  )
339
362
  except CouldNotRun as exc:
340
363
  return could_not_run(row, str(exc))
364
+ except Exception as exc:
365
+ return could_not_run(row, _crashed(check_class, exc))
341
366
 
342
- if (check_class.whole_scope or restated is not None) and not narrowed:
367
+ if (check_class.whole_scope or check_class.empty_scope_is_clean or restated is not None) and not narrowed:
343
368
  return decide(row, (), (), ())
344
369
 
345
370
  missing = [tool for tool in check_class.requires if shutil.which(tool) is None]
@@ -352,6 +377,7 @@ def run(
352
377
  if batch is None:
353
378
  batch = BatchResult(error=f"no batch result was prepared for {row.id}")
354
379
 
380
+ dropped: dict[str, str] = {}
355
381
  try:
356
382
  with ExitStack() as stack:
357
383
  # A structural grant is already spent by the time the findings
@@ -363,7 +389,9 @@ def run(
363
389
  allowance = granted(row.resolved_grants(trees, declared_trees), root)
364
390
  context = Context(
365
391
  root=root, row=row,
366
- inputs=resolve_inputs(check_class, root, inputs, stack, trees, declared_trees),
392
+ inputs=resolve_inputs(
393
+ check_class, root, inputs, stack, trees, declared_trees, dropped
394
+ ),
367
395
  declarations=dict(declarations or {}),
368
396
  committed_sizes=dict(committed_sizes or {}),
369
397
  batch_reports=batch.reports if batch is not None else None,
@@ -373,11 +401,29 @@ def run(
373
401
  # valid while `as_file` holds it, and a lazy generator would read it
374
402
  # after the stack closed.
375
403
  reported = tuple(check_class().run(context, narrowed))
404
+ for item in reported:
405
+ if not isinstance(item, (Finding, Advisory)):
406
+ raise TypeError(f"produced {type(item).__name__}, not a Finding or Advisory")
376
407
  except CouldNotRun as exc:
377
408
  return could_not_run(row, str(exc))
378
409
  except OSError as exc:
379
410
  # An unreadable input is a tool failure, not a violation (F-05).
380
411
  return could_not_run(row, f"{type(exc).__name__}: {exc}")
412
+ except KeyError as exc:
413
+ # Bug 597: an input dropped for a tree declared "" is could-not-run, never clean; any other KeyError is a crash (below).
414
+ name = exc.args[0] if exc.args else None
415
+ if isinstance(name, str) and name in dropped:
416
+ return could_not_run(
417
+ row,
418
+ f"{check_class.id} requires input `{name}` from tree <{dropped[name]}>, "
419
+ f"which adl-config.toml declares absent ([controls.compliance.trees]."
420
+ f"{dropped[name]} = \"\"); nothing to grade, and a gate that graded "
421
+ "nothing is could-not-run, never a clean pass",
422
+ )
423
+ return could_not_run(row, _crashed(check_class, exc))
424
+ except Exception as exc:
425
+ # Bug 597: one check's unexpected exception is that row's could-not-run, not every row's.
426
+ return could_not_run(row, _crashed(check_class, exc))
381
427
 
382
428
  # Partitioned by TYPE, not by a flag on one record. An advisory that could be
383
429
  # mistaken for a finding eventually is one, and then it moves a verdict.
@@ -19,11 +19,21 @@ scratchpad_allowlisted() {
19
19
  # `scripts/hooks/pre-commit` is where it LIVES in adl-repo-controls. Both
20
20
  # are this same policy naming the thing it polices, which is what this
21
21
  # allowlist is for.
22
- .gitignore | .dockerignore | AGENTS.md | .workflows/hooks/pre-commit | scripts/hooks/pre-commit | src/integrity.ts | workflows/scripts/tests/hooks/*) return 0 ;;
22
+ .gitignore | .dockerignore | AGENTS.md | .workflows/hooks/pre-commit | scripts/hooks/pre-commit | packages/repo-controls/scripts/hooks/pre-commit | src/integrity.ts | workflows/scripts/tests/hooks/*) return 0 ;;
23
23
  esac
24
24
  return 1
25
25
  }
26
26
 
27
+ added_lines_mention_scratchpad() {
28
+ local diff line
29
+ diff="$(git diff --cached -- "$1")"
30
+ [[ "$diff" == *.scratchpad* ]] || return 1
31
+ while IFS= read -r line; do
32
+ [[ "$line" == +* && "$line" != +++* && "$line" == *.scratchpad* ]] && return 0
33
+ done <<<"$diff"
34
+ return 1
35
+ }
36
+
27
37
  staged_files=()
28
38
  scratchpad_paths=()
29
39
  while IFS= read -r -d '' file; do
@@ -43,7 +53,7 @@ fi
43
53
  offenders=()
44
54
  for file in "${staged_files[@]}"; do
45
55
  scratchpad_allowlisted "$file" && continue
46
- if git diff --cached -- "$file" | grep '^+' | grep -v '^+++' | grep -Fq '.scratchpad'; then
56
+ if added_lines_mention_scratchpad "$file"; then
47
57
  offenders+=("$file")
48
58
  fi
49
59
  done
@@ -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