@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
@@ -31,13 +31,12 @@ from typing import ClassVar
31
31
 
32
32
  from ....core.check import Check
33
33
  from ....core.context import Context, FileSet
34
+ from ....core.declarations import declared_root_files
34
35
  from ....core.finding import Report
35
36
  from ....policy.root_shape import (
36
37
  GOVERNANCE,
37
38
  GUIDES,
38
39
  REGISTERS,
39
- ROOT_FILES_DECLARATION,
40
- declared_root_names,
41
40
  )
42
41
 
43
42
 
@@ -49,12 +48,10 @@ class RootMarkdownAllowlist(Check):
49
48
  "workflows_root": "<workflows>",
50
49
  "plans_root": "<plans>",
51
50
  "docs_root": "<docs>",
52
- "rust_root": "<rust>",
53
- "frontend_root": "<frontend>",
54
51
  }
55
52
  """FIXED 2026-09-16 (K-42 sweep, LOUD form, approved): every
56
- `backend/...`/`frontend/...`/`.workflows/...`/`plans/...`/`.specs/...`/
57
- `docs/...` entry below was a hardcoded literal, tested with `==`/
53
+ `.workflows/...`/`plans/...`/`docs/...` entry below was a hardcoded
54
+ literal, tested with `==`/
58
55
  `startswith` inside `_allowed_markdown`. A repository whose corresponding
59
56
  root is not the packaged default had a real, legitimately-allowed
60
57
  Markdown file fail every one of those tests, and the gate reported it as
@@ -63,19 +60,8 @@ class RootMarkdownAllowlist(Check):
63
60
  stays literal: named documents and code-asset trees, not resolvable
64
61
  paths."""
65
62
 
66
- # A tree that EXISTS needs a guide. Previously these were three absolute
67
- # requirements, which meant a repository without a `.specs/` tree was told
68
- # to create a guide for a directory it had deliberately never made — and
69
- # `.plans/` was named with a dot this layout no longer uses, so the guide
70
- # that does exist at `plans/AGENTS.md` went unseen while a phantom one was
71
- # demanded. The rule is about trees being documented, not about these exact
72
- # paths being present.
73
- #
74
- # `.workflows/` is deliberately not listed (owner decision, 2026-10-01): it is
75
- # a machine-managed tree, written wholesale by `adl-repo-controls install` and
76
- # never hand-authored, so it does not need a guide of its own. A guide that
77
- # does exist there is still admitted by `_allowed_markdown`.
78
- GUIDED_TREES: ClassVar[tuple[str, ...]] = ("plans", "specs", "docs")
63
+ # A tree that EXISTS needs a guide; `.workflows/` is deliberately not listed (machine-managed).
64
+ GUIDED_INPUTS: ClassVar[tuple[str, ...]] = ("plans_root", "docs_root")
79
65
 
80
66
  # Trees whose Markdown this repository does not author: vendored provider
81
67
  # documentation, and archives kept for reference. Renaming a README inside
@@ -83,7 +69,6 @@ class RootMarkdownAllowlist(Check):
83
69
  UNOWNED_ROOTS: ClassVar[tuple[str, ...]] = (
84
70
  ".agents-archive/",
85
71
  ".claude-plugin/",
86
- ".donors/",
87
72
  "node_modules/",
88
73
  "scripts-archive/",
89
74
  "workflows-archived/",
@@ -112,53 +97,8 @@ class RootMarkdownAllowlist(Check):
112
97
  ".opencode/",
113
98
  )
114
99
 
115
- # Ported from repo-hygiene/check-markdown-placement.py during the
116
- # repo-hygiene -> .workflows dissolution (2026-08-04): explicit non-guide
117
- # Markdown assets retained outside the published knowledge trees.
118
- # Donor absorb triage: every commit of the 2026-08-12 donor fast-forward
119
- # with an ABSORB / NOT USEFUL verdict. Tracked deliberately so the
120
- # verdicts stay reviewable. Genuinely no resolvable tree prefix.
121
- GOVERNANCE_FILES: ClassVar[frozenset[str]] = frozenset({".donors/MANIFEST.md"})
122
- RUST_GOVERNANCE_SUFFIXES: ClassVar[frozenset[str]] = frozenset(
123
- {"CHANGELOG.md", "crates/enact-session/src/backend/codex_init_prompt.md"}
124
- )
125
- FRONTEND_GOVERNANCE_SUFFIXES: ClassVar[frozenset[str]] = frozenset({"CHANGELOG.md"})
126
- # repo-hygiene/CHANGELOG.md and repo-hygiene/LORE_COMMIT_IDS.md were
127
- # deleted outright during the repo-hygiene -> .workflows dissolution — no
128
- # successor entry.
129
- WORKFLOWS_GOVERNANCE_SUFFIXES: ClassVar[frozenset[str]] = frozenset({"COVERAGE.md"})
130
-
131
- # Genuinely no resolvable tree prefix -- "compliance" here is not
132
- # `<compliance>` (that resolves to `compliance_py`); this is a distinct,
133
- # literal historical path, unexamined by this fix.
134
- RETAINED_MARKDOWN_FILES: ClassVar[frozenset[str]] = frozenset({"compliance/fixtures.md"})
135
- # Three entries for now-parked trees stood here. Both trees are gone from
136
- # the checkout and neither path is tracked, so the entries allowlisted
137
- # nothing while keeping this file in violation of meta.parked-references.
138
- WORKFLOWS_RETAINED_SUFFIXES: ClassVar[frozenset[str]] = frozenset(
139
- {"config/exceptions/KNOWN-ISSUES.md"}
140
- )
141
100
  CODE_ASSET_ROOTS: ClassVar[tuple[str, ...]] = (
142
101
  ".claude/skills/",
143
- "agent/docs/",
144
- )
145
- RUST_CODE_ASSET_SUFFIXES: ClassVar[tuple[str, ...]] = (
146
- "crates/enact-app/assets/builtin-assistants/rules/",
147
- "crates/enact-app/assets/builtin-skills/",
148
- "crates/enact-extension/tests/fixtures/builtin-skills-fixture/",
149
- )
150
- FRONTEND_CODE_ASSET_SUFFIXES: ClassVar[tuple[str, ...]] = (
151
- ".claude/",
152
- ".gemini/",
153
- "examples/",
154
- )
155
- FRONTEND_LOCAL_DOC_SUFFIXES: ClassVar[tuple[str, ...]] = (
156
- "packages/",
157
- "scripts/",
158
- "tests/e2e/",
159
- )
160
- FRONTEND_LOCAL_DOC_NAMES: ClassVar[frozenset[str]] = frozenset(
161
- {"README.en.md", "MIGRATION.md"}
162
102
  )
163
103
 
164
104
  def structural(self, ctx: Context) -> Iterable[Report]:
@@ -167,7 +107,11 @@ class RootMarkdownAllowlist(Check):
167
107
  # file in the set" but "is the tree missing a required guide", which
168
108
  # no file set can answer — the set never contains a path that was
169
109
  # never created.
170
- for tree in self.GUIDED_TREES:
110
+ for name in self.GUIDED_INPUTS:
111
+ declared = ctx.inputs.get(name)
112
+ if declared is None:
113
+ continue
114
+ tree = ctx.relative(declared)
171
115
  if not ctx.path(tree).is_dir():
172
116
  continue
173
117
  guide = f"{tree}/AGENTS.md"
@@ -187,6 +131,7 @@ class RootMarkdownAllowlist(Check):
187
131
  # A path in the set that is not a file on disk is a DELETION, not a
188
132
  # finding: a removed Markdown file cannot violate an allowlist. Same
189
133
  # filter check.sh's `is_file()` applied to `ADL_CHANGED_FILES`.
134
+ declared_root_files(ctx.declarations)
190
135
  for path in sorted(files.existing(ctx.root)):
191
136
  # check.sh: `path.endswith("/README.md") or (path != "README.md"
192
137
  # and path.endswith("README.md"))` — the first clause already
@@ -209,9 +154,9 @@ class RootMarkdownAllowlist(Check):
209
154
  yield self.report(
210
155
  path,
211
156
  "tracked Markdown is outside the root allowlist",
212
- f"move {path} under an allowed tree (docs/, plans/, .specs/, "
213
- ".changeset/, etc.), or, if it is a root-level Markdown file "
214
- "this repository genuinely carries, declare it under "
157
+ f"move {path} under an allowed tree (docs/, plans/, etc.), or, "
158
+ "if it is a Markdown file this repository genuinely carries, "
159
+ "declare it under "
215
160
  "[controls.compliance.root] files in this repository's "
216
161
  "adl-config.toml",
217
162
  )
@@ -219,9 +164,6 @@ class RootMarkdownAllowlist(Check):
219
164
  def _allowed_markdown(self, ctx: Context, path: str) -> bool:
220
165
  if path == "README.md":
221
166
  return True
222
- # ADR-095 admits this one root-relative verification contract by
223
- # literal name.
224
- #
225
167
  # `policy/root_shape.py` is THE list of what may sit at EVERY repository's
226
168
  # root — GUIDES (AGENTS.md/CLAUDE.md/INVARIANTS.md/…), GOVERNANCE
227
169
  # (CONTRIBUTING.md/SECURITY.md/…) and REGISTERS (GAPS-AND-FINDINGS.md,
@@ -239,9 +181,7 @@ class RootMarkdownAllowlist(Check):
239
181
  # (tests/core/test_root_shape.py passes `ctx=None`).
240
182
  if path in (GUIDES | GOVERNANCE | REGISTERS):
241
183
  return True
242
- if "/" not in path and path in declared_root_names(
243
- ctx.declarations, ROOT_FILES_DECLARATION
244
- ):
184
+ if path in declared_root_files(ctx.declarations):
245
185
  return True
246
186
  # Ported from check-markdown-placement.py: any AGENTS.md/CLAUDE.md
247
187
  # guide file is allowed regardless of folder — the AGENTS.md/CLAUDE.md
@@ -259,19 +199,6 @@ class RootMarkdownAllowlist(Check):
259
199
  plans_root = ctx.relative(ctx.inputs["plans_root"])
260
200
  if path.startswith(f"{plans_root}/") and path.endswith(".md"):
261
201
  return True
262
- # Widening: a repository with no specs tree simply allows nothing on its
263
- # account. Shrinking an allowlist can only add findings, never hide one.
264
- specs_input = ctx.inputs.get("specs_root")
265
- if specs_input is not None:
266
- specs_root = ctx.relative(specs_input)
267
- if path == f"{specs_root}/AGENTS.md" or (
268
- path.startswith(f"{specs_root}/") and path.endswith(".md")
269
- ):
270
- return True
271
- if path.startswith(".changeset/") and path.endswith(".md"):
272
- return True
273
- if path == ".donors/MANIFEST.md":
274
- return True
275
202
  docs_root = ctx.relative(ctx.inputs["docs_root"])
276
203
  if path.startswith(f"{docs_root}/") and path.endswith(".md"):
277
204
  return True
@@ -279,53 +206,6 @@ class RootMarkdownAllowlist(Check):
279
206
  # shell (I-41) — its Markdown is a separate concern from docs/.
280
207
  if path.startswith(".docs/") and path.endswith(".md"):
281
208
  return True
282
- # Ported enact-integration-specific exceptions (see class attributes
283
- # above).
284
- if path in self.GOVERNANCE_FILES or path in self.RETAINED_MARKDOWN_FILES:
285
- return True
286
- # Decision for a tree declared absent (`rust = ""` / `frontend = ""`, which
287
- # drops `rust_root` / `frontend_root` from the resolved inputs): both only
288
- # ever ADD allowlist entries -- the governance suffixes and code-asset
289
- # roots beneath the tree -- so a repository with no such tree has nothing
290
- # there to admit and they contribute nothing. Strictly stricter, never a
291
- # silent pass. These were read unguarded (`ctx.inputs["rust_root"]`): a
292
- # stray root Markdown file, which every allowance above declines, reached
293
- # them and raised `KeyError` -- a traceback with exit 1, not a verdict.
294
- rust_input = ctx.inputs.get("rust_root")
295
- rust_root = None if rust_input is None else ctx.relative(rust_input)
296
- frontend_input = ctx.inputs.get("frontend_root")
297
- frontend_root = None if frontend_input is None else ctx.relative(frontend_input)
298
- if rust_root is not None and path in {
299
- f"{rust_root}/{suffix}" for suffix in self.RUST_GOVERNANCE_SUFFIXES
300
- }:
301
- return True
302
- if frontend_root is not None and path in {
303
- f"{frontend_root}/{suffix}" for suffix in self.FRONTEND_GOVERNANCE_SUFFIXES
304
- }:
305
- return True
306
- if path in {f"{workflows_root}/{suffix}" for suffix in self.WORKFLOWS_GOVERNANCE_SUFFIXES}:
307
- return True
308
- if path in {f"{workflows_root}/{suffix}" for suffix in self.WORKFLOWS_RETAINED_SUFFIXES}:
309
- return True
310
209
  if any(path.startswith(code_root) for code_root in self.CODE_ASSET_ROOTS):
311
210
  return True
312
- if rust_root is not None and any(
313
- path.startswith(f"{rust_root}/{suffix}") for suffix in self.RUST_CODE_ASSET_SUFFIXES
314
- ):
315
- return True
316
- if frontend_root is not None and any(
317
- path.startswith(f"{frontend_root}/{suffix}")
318
- for suffix in self.FRONTEND_CODE_ASSET_SUFFIXES
319
- ):
320
- return True
321
- name = path.rsplit("/", 1)[-1]
322
- if (
323
- frontend_root is not None
324
- and name in self.FRONTEND_LOCAL_DOC_NAMES
325
- and any(
326
- path.startswith(f"{frontend_root}/{suffix}")
327
- for suffix in self.FRONTEND_LOCAL_DOC_SUFFIXES
328
- )
329
- ):
330
- return True
331
211
  return False
@@ -7,7 +7,7 @@ Markdown file — the two violators live inside the same conforming tree, so a
7
7
  corpus flips between clean and broken by changing only which paths are in the
8
8
  given set, exactly as test.sh's own comment describes. `root-markdown-missing-
9
9
  guide/` holds only the four files the structural rule reads, with
10
- `.specs/AGENTS.md` absent.
10
+ `docs/AGENTS.md` absent.
11
11
 
12
12
  This check is not yet wired into `checks/__init__.py` (a later step wires this
13
13
  port's whole wave into the registry in one shot, to avoid concurrent edits to
@@ -29,7 +29,7 @@ Registry-row note, not fixed here (this task does not touch
29
29
  `'**/README.md'`, with no `'**/*.md'` companion of the kind LAY-006 carries for
30
30
  the identical reason (see that row's own comment). Read through `runner.run`,
31
31
  every nested non-README path in the clean corpus (`.plans/AGENTS.md`,
32
- `docs/AGENTS.md`, `.changeset/some-change.md`, ...) would be narrowed away
32
+ `docs/AGENTS.md`, `docs/architecture/note.md`, ...) would be narrowed away
33
33
  before this check ever ran — which is exactly why every test below calls the
34
34
  check directly with the full given set, rather than through the row's scope.
35
35
  """
@@ -38,9 +38,12 @@ from __future__ import annotations
38
38
 
39
39
  import pathlib
40
40
 
41
+ import pytest
42
+
41
43
  from adl_compliance.checks.layout.root_markdown_allowlist import RootMarkdownAllowlist
42
44
  from adl_compliance.core import scope
43
45
  from adl_compliance.core.context import Context, FileSet
46
+ from adl_compliance.core.errors import CouldNotRun
44
47
  from adl_compliance.core.verdict import Outcome, decide
45
48
  from adl_compliance.policy.registry import load
46
49
 
@@ -59,14 +62,11 @@ CLEAN_GIVEN = (
59
62
  "CLAUDE.md",
60
63
  "CHANGELOG.md",
61
64
  "RELEASE.md",
62
- "INDEPENDENT-VERIFICATION.md",
63
65
  "README.md",
64
66
  ".workflows/AGENTS.md",
65
67
  ".plans/AGENTS.md",
66
- ".specs/AGENTS.md",
67
68
  "docs/AGENTS.md",
68
69
  "docs/architecture/note.md",
69
- ".changeset/some-change.md",
70
70
  )
71
71
  # Every file the clean corpus actually holds on disk, violators included —
72
72
  # the shape a tree-scoped phase (or test.sh's `corpus_set` with no explicit
@@ -91,10 +91,7 @@ def graded(root: pathlib.Path, given: tuple[str, ...]):
91
91
  inputs={
92
92
  "workflows_root": root / ".workflows",
93
93
  "plans_root": root / "plans",
94
- "specs_root": root / ".specs",
95
94
  "docs_root": root / "docs",
96
- "rust_root": root / "backend",
97
- "frontend_root": root / "frontend",
98
95
  },
99
96
  )
100
97
  findings = tuple(RootMarkdownAllowlist().graded(ctx, FileSet(paths=given)))
@@ -110,10 +107,7 @@ def structural(root: pathlib.Path):
110
107
  inputs={
111
108
  "workflows_root": root / ".workflows",
112
109
  "plans_root": root / "plans",
113
- "specs_root": root / ".specs",
114
110
  "docs_root": root / "docs",
115
- "rust_root": root / "backend",
116
- "frontend_root": root / "frontend",
117
111
  },
118
112
  )
119
113
  findings = tuple(RootMarkdownAllowlist().structural(ctx))
@@ -130,10 +124,7 @@ def full(root: pathlib.Path, given: tuple[str, ...]):
130
124
  inputs={
131
125
  "workflows_root": root / ".workflows",
132
126
  "plans_root": root / "plans",
133
- "specs_root": root / ".specs",
134
127
  "docs_root": root / "docs",
135
- "rust_root": root / "backend",
136
- "frontend_root": root / "frontend",
137
128
  },
138
129
  )
139
130
  findings = tuple(RootMarkdownAllowlist().run(ctx, FileSet(paths=given)))
@@ -239,7 +230,6 @@ def test_a_tree_that_does_not_exist_is_not_asked_for_a_guide() -> None:
239
230
  did."""
240
231
  reported = {f.path for f in structural(MISSING_GUIDE).findings}
241
232
  assert "specs/AGENTS.md" not in reported
242
- assert ".specs/AGENTS.md" not in reported
243
233
 
244
234
 
245
235
  def test_a_workflows_tree_without_a_guide_is_not_a_finding(tmp_path: pathlib.Path) -> None:
@@ -292,9 +282,9 @@ def test_a_plugin_root_readme_is_a_published_description(tmp_path: pathlib.Path)
292
282
  given = (
293
283
  "AGENTS.md",
294
284
  "README.md",
295
- ".claude-plugin/enact-repo/README.md",
296
- ".codex-plugin/enact-repo/README.md",
297
- ".cursor-plugin/enact-repo/README.md",
285
+ ".claude-plugin/my-repo/README.md",
286
+ ".codex-plugin/my-repo/README.md",
287
+ ".cursor-plugin/my-repo/README.md",
298
288
  )
299
289
  verdict = graded(tmp_path, given)
300
290
  assert not [
@@ -309,7 +299,7 @@ def test_a_redeclared_estate_still_allows_the_resolved_tree_exceptions(
309
299
  """The LOUD false-finding this check shipped with.
310
300
 
311
301
  Every `backend/...`/`frontend/...`/`.workflows/...`/`plans/...`/
312
- `.specs/...`/`docs/...` entry in `_allowed_markdown` was a hardcoded
302
+ `docs/...` entry in `_allowed_markdown` was a hardcoded
313
303
  literal. A repository whose roots are not the packaged defaults had a
314
304
  real, legitimately-allowed Markdown file (a crate's CHANGELOG, a
315
305
  workflows coverage note, a docs page) fail every one of those tests and
@@ -327,19 +317,13 @@ def test_a_redeclared_estate_still_allows_the_resolved_tree_exceptions(
327
317
  "documentation/AGENTS.md",
328
318
  "documentation/architecture/note.md",
329
319
  "automation/AGENTS.md",
330
- "automation/COVERAGE.md",
331
- "rustcode/CHANGELOG.md",
332
- "webapp/CHANGELOG.md",
333
320
  )
334
321
  ctx = Context(
335
322
  root=tmp_path, row=ROW,
336
323
  inputs={
337
324
  "workflows_root": tmp_path / "automation",
338
325
  "plans_root": tmp_path / "roadmap",
339
- "specs_root": tmp_path / "specifications",
340
326
  "docs_root": tmp_path / "documentation",
341
- "rust_root": tmp_path / "rustcode",
342
- "frontend_root": tmp_path / "webapp",
343
327
  },
344
328
  )
345
329
  findings = tuple(RootMarkdownAllowlist().graded(ctx, FileSet(paths=given)))
@@ -350,6 +334,19 @@ def test_a_redeclared_estate_still_allows_the_resolved_tree_exceptions(
350
334
  )
351
335
 
352
336
 
337
+ def test_no_legacy_estate_file_is_admitted_by_name(tmp_path: pathlib.Path) -> None:
338
+ given = (
339
+ "compliance/fixtures.md",
340
+ ".workflows/config/exceptions/KNOWN-ISSUES.md",
341
+ ".workflows/COVERAGE.md",
342
+ )
343
+ for rel in given:
344
+ (tmp_path / rel).parent.mkdir(parents=True, exist_ok=True)
345
+ (tmp_path / rel).write_text("x\n", encoding="utf-8")
346
+ verdict = graded(tmp_path, given)
347
+ assert sorted(f.path for f in verdict.findings) == sorted(given), verdict.render()
348
+
349
+
353
350
  # ---------------------------------------------- per-repository root declarations
354
351
  def graded_declaring(root: pathlib.Path, declarations: dict[str, str], given: tuple[str, ...]):
355
352
  """`graded`, plus what the repository declared about its own root
@@ -361,10 +358,7 @@ def graded_declaring(root: pathlib.Path, declarations: dict[str, str], given: tu
361
358
  inputs={
362
359
  "workflows_root": root / ".workflows",
363
360
  "plans_root": root / "plans",
364
- "specs_root": root / ".specs",
365
361
  "docs_root": root / "docs",
366
- "rust_root": root / "backend",
367
- "frontend_root": root / "frontend",
368
362
  },
369
363
  )
370
364
  findings = tuple(RootMarkdownAllowlist().graded(ctx, FileSet(paths=given)))
@@ -448,18 +442,12 @@ def test_the_central_names_are_allowed_without_reading_the_context() -> None:
448
442
  assert RootMarkdownAllowlist()._allowed_markdown(None, "GAPS-AND-FINDINGS.md") # type: ignore[arg-type]
449
443
 
450
444
 
451
- # ------------------------------------------------- a tree declared absent
452
- def test_rust_and_frontend_declared_absent_contribute_nothing_to_the_allowlist(
445
+ # ------------------------------------------- deleted rust and frontend trees
446
+ def test_the_former_rust_and_frontend_entries_admit_nothing(
453
447
  tmp_path: pathlib.Path,
454
448
  ) -> None:
455
- """`rust = ""` and `frontend = ""` drop `rust_root` and `frontend_root` from
456
- the resolved inputs. Here they only ever ADD allowlist entries (the governance
457
- suffixes and code-asset roots beneath them), so absent they contribute
458
- nothing and the file is reported -- strictly stricter, never a silent pass.
459
- A stray root-level Markdown file reached `ctx.inputs["rust_root"]` unguarded
460
- and raised `KeyError`, a traceback with exit 1, measured against enact-combined
461
- with `--tree rust= --tree frontend=`."""
462
- _touch(tmp_path, "NOTES.md", "backend/crates/enact-app/assets/builtin-skills/s.md")
449
+ """PBI 598: the rust and frontend suffix admissions are deleted with their trees."""
450
+ _touch(tmp_path, "NOTES.md", "backend/crates/app/assets/builtin-skills/s.md")
463
451
  ctx = Context(
464
452
  root=tmp_path, row=ROW,
465
453
  inputs={
@@ -468,23 +456,83 @@ def test_rust_and_frontend_declared_absent_contribute_nothing_to_the_allowlist(
468
456
  "docs_root": tmp_path / "docs",
469
457
  },
470
458
  )
471
- given = ("NOTES.md", "backend/crates/enact-app/assets/builtin-skills/s.md")
459
+ given = ("NOTES.md", "backend/crates/app/assets/builtin-skills/s.md")
472
460
  findings = tuple(RootMarkdownAllowlist().graded(ctx, FileSet(paths=given)))
473
461
  assert sorted(f.path for f in findings) == sorted(given), findings
474
462
  assert all(f.message == "tracked Markdown is outside the root allowlist" for f in findings)
475
463
 
476
464
 
477
- def test_a_declared_rust_tree_still_admits_its_governance_files(tmp_path: pathlib.Path) -> None:
478
- """The other half: with the tree declared, the same entries still admit."""
479
- _touch(tmp_path, "rust/CHANGELOG.md")
465
+ # ---------------------------------------- PBI 601: INDEPENDENT-VERIFICATION.md
466
+ def test_independent_verification_is_a_declaration_not_a_central_name(
467
+ tmp_path: pathlib.Path,
468
+ ) -> None:
469
+ """PBI 601: INDEPENDENT-VERIFICATION.md left the central list; reported undeclared, silent once declared."""
470
+ _touch(tmp_path, "INDEPENDENT-VERIFICATION.md")
471
+ undeclared = graded_declaring(tmp_path, {}, ("INDEPENDENT-VERIFICATION.md",))
472
+ assert [f.path for f in undeclared.findings] == ["INDEPENDENT-VERIFICATION.md"]
473
+ declared = graded_declaring(
474
+ tmp_path, {"root-files": "INDEPENDENT-VERIFICATION.md"}, ("INDEPENDENT-VERIFICATION.md",)
475
+ )
476
+ assert declared.findings == ()
477
+
478
+
479
+ def test_an_arbitrary_tree_markdown_file_is_outside_the_allowlist(tmp_path: pathlib.Path) -> None:
480
+ _touch(tmp_path, "changes/some-change.md")
481
+ verdict = graded_declaring(tmp_path, {}, ("changes/some-change.md",))
482
+ assert [f.path for f in verdict.findings] == ["changes/some-change.md"], verdict.render()
483
+
484
+
485
+ def test_the_remedy_names_only_trees_the_gate_admits(tmp_path: pathlib.Path) -> None:
486
+ _touch(tmp_path, "STRAY.md")
487
+ [finding] = graded_declaring(tmp_path, {}, ("STRAY.md",)).findings
488
+ assert "docs/, plans/" in finding.fix
489
+ assert "etc.)" in finding.fix
490
+
491
+
492
+ def _guides(tmp_path: pathlib.Path, plans: str, docs: str):
480
493
  ctx = Context(
481
494
  root=tmp_path, row=ROW,
482
- inputs={
483
- "workflows_root": tmp_path / "workflows",
484
- "plans_root": tmp_path / "plans",
485
- "docs_root": tmp_path / "docs",
486
- "rust_root": tmp_path / "rust",
487
- "frontend_root": tmp_path / "web",
488
- },
495
+ inputs={"workflows_root": tmp_path / ".workflows", "plans_root": tmp_path / plans, "docs_root": tmp_path / docs},
489
496
  )
490
- assert tuple(RootMarkdownAllowlist().graded(ctx, FileSet(paths=("rust/CHANGELOG.md",)))) == ()
497
+ return [f.path for f in RootMarkdownAllowlist().structural(ctx)]
498
+
499
+
500
+ def test_the_guided_trees_are_the_declared_ones(tmp_path: pathlib.Path) -> None:
501
+ (tmp_path / "roadmap").mkdir()
502
+ (tmp_path / "documentation").mkdir()
503
+ (tmp_path / "plans").mkdir()
504
+ assert _guides(tmp_path, "roadmap", "documentation") == ["roadmap/AGENTS.md", "documentation/AGENTS.md"]
505
+
506
+
507
+ def test_an_undeclared_default_tree_is_not_asked_for_a_guide(tmp_path: pathlib.Path) -> None:
508
+ (tmp_path / "plans").mkdir()
509
+ (tmp_path / "docs").mkdir()
510
+ assert _guides(tmp_path, "roadmap", "documentation") == []
511
+
512
+
513
+ DONORS = ".donors/MANIFEST.md"
514
+
515
+
516
+ def test_a_declared_nested_root_file_is_allowed(tmp_path: pathlib.Path) -> None:
517
+ _touch(tmp_path, DONORS)
518
+ verdict = graded_declaring(tmp_path, {"root-files": DONORS}, (DONORS,))
519
+ assert verdict.outcome is Outcome.CLEAN, verdict.render()
520
+
521
+
522
+ def test_an_undeclared_nested_root_file_is_a_finding(tmp_path: pathlib.Path) -> None:
523
+ _touch(tmp_path, DONORS)
524
+ verdict = graded_declaring(tmp_path, {}, (DONORS,))
525
+ assert [f.path for f in verdict.findings] == [DONORS], verdict.render()
526
+
527
+
528
+ def test_a_declaration_admits_only_the_exact_nested_path(tmp_path: pathlib.Path) -> None:
529
+ _touch(tmp_path, "other/MANIFEST.md")
530
+ verdict = graded_declaring(tmp_path, {"root-files": DONORS}, ("other/MANIFEST.md",))
531
+ assert [f.path for f in verdict.findings] == ["other/MANIFEST.md"], verdict.render()
532
+
533
+
534
+ @pytest.mark.parametrize("bad", ["../x.md", "/x.md", "a/../x.md", "a//x.md", "docs/*.md", "a/./x.md"])
535
+ def test_a_malformed_root_file_declaration_is_could_not_run(tmp_path: pathlib.Path, bad: str) -> None:
536
+ _touch(tmp_path, "STATUS.md")
537
+ with pytest.raises(CouldNotRun, match="root-files"):
538
+ graded_declaring(tmp_path, {"root-files": bad}, ("STATUS.md",))
@@ -66,7 +66,7 @@ class ScriptsSizeDepth(Check):
66
66
 
67
67
  # `<workflows>` indirection, not the literal `.workflows/scripts` this held
68
68
  # until 2026-09-15. That literal is a consumer path inside the package
69
- # (K-42), and it was not a near-miss: enact-m5 keeps CI in `workflows/`
69
+ # (K-42), and it was not a near-miss: a managed repository keeps CI in `workflows/`
70
70
  # while `.workflows/` is repo-controls' own install artefact, so the gate
71
71
  # reported `missing .workflows/scripts` and then called all 44 real scripts
72
72
  # "outside scripts root" — 45 findings, none of them true. The remedy it
@@ -131,7 +131,7 @@ def test_the_rows_scope_selects_nothing_from_an_empty_given_set() -> None:
131
131
  def test_the_scripts_root_follows_the_declared_tree(tmp_path: Path) -> None:
132
132
  """The regression guard for a literal `.workflows/scripts` in the package.
133
133
 
134
- enact-m5 keeps CI in `workflows/` while `.workflows/` is repo-controls' own
134
+ a managed repository keeps CI in `workflows/` while `.workflows/` is repo-controls' own
135
135
  install artefact, so the hardcoded constant made this gate report
136
136
  `missing .workflows/scripts` and then call all 44 real scripts "outside
137
137
  scripts root" — 45 findings, not one of them true. Its printed remedy was
@@ -44,7 +44,7 @@ length: the WORKTREE file is not a safe source, because a correctly-tracked LFS
44
44
  file is a pointer in Git and is smudged back to full content on disk, so a check
45
45
  reading the worktree flags the very files that are doing the right thing.
46
46
  check.sh called that risk "latent, not hypothetical". It was measured on
47
- 2026-10-01 against enact-combined: `live-probes/local/chat-local/output/
47
+ 2026-10-01 against the combined repository: `live-probes/local/chat-local/output/
48
48
  evidence/probe-result-video.webm` has `git check-attr filter` = lfs, a committed
49
49
  blob of 132 bytes (`git cat-file -s :path`) and a 1,170,147-byte worktree file,
50
50
  and the worktree-reading version of this check reported it as a violation. No
@@ -356,7 +356,7 @@ def test_a_pointer_header_over_the_threshold_under_a_non_matching_rule_is_report
356
356
  # graded where. A path with NO committed fact is graded by its worktree file, as
357
357
  # before -- the existing cases above all run that way, with no facts at all.
358
358
 
359
- SMUDGED = 1_170_147 # the measured size of a real smudged LFS file on enact-combined
359
+ SMUDGED = 1_170_147 # the measured size of a real smudged LFS file on the combined repository
360
360
  POINTER_SIZE = 132 # `git cat-file -s` of the pointer committed for it
361
361
 
362
362
 
@@ -808,20 +808,24 @@ def test_a_compliance_tree_declared_absent_drops_the_register(tmp_path: Path) ->
808
808
  assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
809
809
 
810
810
 
811
- def test_a_compliance_tree_missing_from_the_map_is_could_not_run_naming_what_to_declare(
811
+ def test_a_compliance_tree_missing_from_the_map_drops_the_register_and_grades_bare(
812
812
  tmp_path: Path,
813
813
  ) -> None:
814
- # PINS A GAP, not a goal. `resolve_inputs` expands every declared input before
815
- # it reaches the widening branch, so a tree ABSENT from the map (neither a path
816
- # nor "") is could-not-run even for a widening input -- reproduced through the
817
- # real CLI on enact-combined with no `--tree compliance=...`. The message names
818
- # exactly what to declare, and both repositories in this estate declare it. The
819
- # fix is in `runner.resolve_inputs`, not in this check; this test flips with it.
814
+ # PBI 599/601: an absent register means no grants, never could-not-run; the excused file is reported.
820
815
  root = ratcheted(tmp_path, {"photo.png": BIG}, {"photo.png": RECORDED})
821
816
  trees = {name: path for name, path in fixture_trees().items() if name != "compliance"}
822
817
  verdict = runner.run(ROW, root, corpus_files(root), trees=trees, declared_trees=trees)
823
- assert verdict.outcome is Outcome.COULD_NOT_RUN, verdict.render()
824
- assert "[controls.compliance.trees].compliance" in verdict.message
818
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
819
+ assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
820
+
821
+
822
+ def test_a_declared_compliance_tree_with_no_folder_grants_nothing(tmp_path: Path) -> None:
823
+ root = build(tmp_path, {"photo.png": BIG})
824
+ trees = {**fixture_trees(), "compliance": "compliance"}
825
+ assert not (root / "compliance").exists()
826
+ verdict = runner.run(ROW, root, corpus_files(root), trees=trees, declared_trees=trees)
827
+ assert verdict.outcome is Outcome.FINDINGS, verdict.render()
828
+ assert [(f.rule, f.path) for f in verdict.findings] == [("LFS-002", "photo.png")]
825
829
 
826
830
 
827
831
  # --------------------------------------------------------- the register's shape
@@ -1,12 +1,5 @@
1
- """The `repository.no-banned-commands` gate.
1
+ """Shared scanner (`base.py`) and fixture corpus for REP-004 and REP-006.
2
2
 
3
- A folder even though this gate grades no document of its own: uniform depth
4
- means every `check.py` sits at exactly `checks/*/*/check.py`, so a relative
5
- import out of a check is right for all of them or wrong for all of them.
3
+ Holds no `check.py`: each gate has its own folder, `legacy_search/` and
4
+ `vcs_in_scripts/`, because the layout is one `check.py` per registry row.
6
5
  """
7
-
8
- from __future__ import annotations
9
-
10
- from .check import BannedCommands
11
-
12
- __all__ = ["BannedCommands"]
@@ -0,0 +1,42 @@
1
+ """The scanner behind REP-004 and REP-006; see `docs/operations/adl-compliance-invariants.md`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from collections.abc import Iterable
7
+ from typing import ClassVar
8
+
9
+ from ....core.check import Check
10
+ from ....core.context import Context, FileSet
11
+ from ....core.finding import Report
12
+ from ....core.scripts import is_shell_script
13
+
14
+
15
+ class BannedCommand(Check):
16
+ """The shared scanner. Not registered; a gate subclasses it."""
17
+
18
+ WORD: ClassVar[str]
19
+ """The forbidden command, assembled by the subclass and never spelled whole."""
20
+
21
+ MESSAGE: ClassVar[str]
22
+ FIX: ClassVar[str]
23
+
24
+ BOUNDARY: ClassVar[str] = r"(?:^|[;&|()`\s'\"])"
25
+ """A command START, backtick included."""
26
+
27
+ TRAILING: ClassVar[str] = r"(?:\s|$)"
28
+
29
+ COMMENT: ClassVar[re.Pattern[str]] = re.compile(r"^\s*#")
30
+ """A whole-line comment only; a trailing comment stays flagged."""
31
+
32
+ def graded(self, ctx: Context, files: FileSet) -> Iterable[Report]:
33
+ targets = sorted(p for p in files if is_shell_script(p, lambda p=p: self.text(ctx, p)))
34
+ pattern = re.compile(f"{self.BOUNDARY}{re.escape(self.WORD)}{self.TRAILING}")
35
+ for relative in FileSet(paths=tuple(targets)).existing(ctx.root):
36
+ for line, found in self.scan(ctx, relative, pattern):
37
+ # `found.string` is the line: `scan` matches line by line.
38
+ if self.COMMENT.match(found.string):
39
+ continue
40
+ yield self.report(
41
+ relative, f"{self.MESSAGE} ({found.string.strip()})", self.FIX, line=line,
42
+ )