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