diff --git a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/STATUS.md b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/STATUS.md index fabf4e6fa..0627c875f 100644 --- a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/STATUS.md +++ b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/STATUS.md @@ -1,15 +1,20 @@ # Status: WS-ENG-008 — Repository-Native SDLC Assurance -- Phase: planning intake review +- Phase: implementation - Active planning chunk: none -- Active implementation chunk: none -- Proposed planning merge: `WS-ENG-008-PLAN` -- Proposed first implementation chunk: `WS-ENG-008-01` -- Current gate: planning-only artifacts require internal review, exact-head - external checks, and explicit human merge approval +- Active implementation chunk: `WS-ENG-008-01` +- Completed planning chunk: `WS-ENG-008-PLAN`, merged through PR #196 as + `bd2203d5e8a972d8afbf833805b92ed70dedee4a` +- Current gate: implement and prove the exact signed chunk, complete all nine + internal reviewer tracks, then stop for exact-PR human review - Original discovery base: `bcf1292e1a591e3e84bf8ee212ee7191d80741fa` -- Final reconciled main: `a04fd1a0a623b7150ec40c9934a9982f80a2dce7` -- Final signed-state tip: `33edd1a682ea5fe5ea973870f89bdd3a75a63da3` -- Concurrent signed state at final reconciliation: REV-03P active; ART stopped - at PLAN2, AUTH stopped at 11, and CON stopped at 02A -- Successor after planning: `WS-ENG-008-01`, separate explicit start required +- Implementation base: `bd2203d5e8a972d8afbf833805b92ed70dedee4a` +- Signed start run: `30191914510`; ENG start projection commit `6923f9ed4a8e48327d3aa4d046c8a8dc3a31ea3a` +- Latest reconciled signed-state tip: `9645fdfcf1f7cfea989612ae656209e311e63388` +- Concurrent signed state: `WS-REV-001-03P`, `WS-AUTH-001-11A`, + `WS-ART-001-03A`, and `WS-ENG-008-01` active in distinct initiatives +- Publication overlap check: active PR #195 has no path overlap. Stale PR #149 + overlaps `scripts/check_internal_review_evidence.py` and + `scripts/test_agent_gates.py`; it is not active ENG-008 authority and must + reconcile independently rather than weaken this cutover. +- Proposed successor after merge: `WS-ENG-008-02`, separate explicit start required diff --git a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-01-machine-checkable-chunk-scope.md b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-01-machine-checkable-chunk-scope.md index 2fb0afba7..bacdf8d5d 100644 --- a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-01-machine-checkable-chunk-scope.md +++ b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-01-machine-checkable-chunk-scope.md @@ -33,6 +33,47 @@ P1 `implementation` +## Machine-checkable scope + +```chunk-scope-json +{ + "schema_version": 1, + "chunk_id": "WS-ENG-008-01", + "phase": "implementation", + "risk_class": "L1", + "allowed_paths": [ + "CONTRIBUTING.md", + "AGENTS.md", + "README.md", + "docs/glossary.md", + "docs/architecture_lockdown.md", + ".agent-loop/templates/CHUNK_CONTRACT.md", + ".agent-loop/policies/repository-engineering-policy.md", + ".agent-loop/policies/definition-of-done.md", + ".github/workflows/agent-gates.yml", + "scripts/check_chunk_contract.py", + "scripts/check_internal_review_evidence.py", + "scripts/test_check_chunk_contract.py", + "scripts/test_agent_gates.py", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/STATUS.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-01-machine-checkable-chunk-scope.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-02-scheduled-signed-state-drift-audit.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-03-risk-routed-adversarial-proof.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-04-loop-memory-property-invariants.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-05-authorization-property-invariants.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-06-changed-module-mutation-pilot.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-07-lossless-review-memory-index.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-internal-review-evidence.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-pr-trust-bundle.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-external-review-response.md", + ".agent-loop/merge-intents/WS-ENG-008-01.json" + ], + "forbidden_paths": ["backend/**", "frontend/**"], + "required_reviewers": ["senior engineering", "qa/test", "security/auth", "product/ops", "architecture", "ci integrity", "docs", "reuse/dedup", "test delta"], + "verification_commands": ["chunk-scope-tests", "agent-gate-tests", "internal-review-evidence", "markdown-links", "stale-wording", "git-diff-check"] +} +``` + ## Allowed files ```text diff --git a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-02-scheduled-signed-state-drift-audit.md b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-02-scheduled-signed-state-drift-audit.md index 119661de0..00f9b5f91 100644 --- a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-02-scheduled-signed-state-drift-audit.md +++ b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-02-scheduled-signed-state-drift-audit.md @@ -32,6 +32,34 @@ P1 `implementation` +## Machine-checkable scope + +```chunk-scope-json +{ + "schema_version": 1, + "chunk_id": "WS-ENG-008-02", + "phase": "implementation", + "risk_class": "L1", + "allowed_paths": [ + ".github/workflows/loop-memory-drift-audit.yml", + "scripts/audit_loop_memory_drift.py", + "scripts/test_audit_loop_memory_drift.py", + "scripts/test_agent_gates.py", + "docs/operations_post_merge_memory.md", + ".agent-loop/policies/repository-engineering-policy.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/STATUS.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-02-scheduled-signed-state-drift-audit.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-02-internal-review-evidence.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-02-pr-trust-bundle.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-02-external-review-response.md", + ".agent-loop/merge-intents/WS-ENG-008-02.json" + ], + "forbidden_paths": ["backend/**", "frontend/**"], + "required_reviewers": ["senior engineering", "qa/test", "security/auth", "product/ops", "architecture", "ci integrity", "docs", "reuse/dedup", "test delta"], + "verification_commands": ["loop-memory-drift-tests", "agent-gate-tests", "markdown-links", "stale-wording", "git-diff-check"] +} +``` + ## Allowed files ```text diff --git a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-03-risk-routed-adversarial-proof.md b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-03-risk-routed-adversarial-proof.md index 2011de9e5..1f00ca57f 100644 --- a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-03-risk-routed-adversarial-proof.md +++ b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-03-risk-routed-adversarial-proof.md @@ -32,6 +32,42 @@ P1 `implementation` +## Machine-checkable scope + +```chunk-scope-json +{ + "schema_version": 1, + "chunk_id": "WS-ENG-008-03", + "phase": "implementation", + "risk_class": "L1", + "allowed_paths": [ + ".agents/skills/risk-router/SKILL.md", + ".agents/skills/security-review/SKILL.md", + ".agents/skills/qa-review/SKILL.md", + ".agents/skills/evidence-gate/SKILL.md", + ".agent-loop/templates/CHUNK_CONTRACT.md", + ".agent-loop/templates/PR_TRUST_BUNDLE.md", + ".agent-loop/templates/ADVERSARIAL_PROOF.md", + ".agent-loop/policies/routing-policy.md", + ".github/pull_request_template.md", + "scripts/check_internal_review_evidence.py", + "scripts/test_agent_gates.py", + "CONTRIBUTING.md", + "AGENTS.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/STATUS.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-03-risk-routed-adversarial-proof.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-03-internal-review-evidence.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-03-pr-trust-bundle.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-03-adversarial-proof.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-03-external-review-response.md", + ".agent-loop/merge-intents/WS-ENG-008-03.json" + ], + "forbidden_paths": ["backend/**", "frontend/**"], + "required_reviewers": ["senior engineering", "qa/test", "security/auth", "product/ops", "architecture", "ci integrity", "docs", "reuse/dedup", "test delta"], + "verification_commands": ["agent-gate-tests", "internal-review-evidence", "markdown-links", "stale-wording", "git-diff-check"] +} +``` + ## Allowed files ```text diff --git a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-04-loop-memory-property-invariants.md b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-04-loop-memory-property-invariants.md index e00087ed3..6bf0d8c51 100644 --- a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-04-loop-memory-property-invariants.md +++ b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-04-loop-memory-property-invariants.md @@ -32,6 +32,34 @@ P2 `implementation` +## Machine-checkable scope + +```chunk-scope-json +{ + "schema_version": 1, + "chunk_id": "WS-ENG-008-04", + "phase": "implementation", + "risk_class": "L1", + "allowed_paths": [ + "scripts/agent-gate-requirements.txt", + "scripts/assurance-requirements.txt", + "scripts/test_loop_memory_properties.py", + "scripts/test_agent_gates.py", + ".github/workflows/agent-gates.yml", + "docs/operations_post_merge_memory.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/STATUS.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-04-loop-memory-property-invariants.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-04-internal-review-evidence.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-04-pr-trust-bundle.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-04-external-review-response.md", + ".agent-loop/merge-intents/WS-ENG-008-04.json" + ], + "forbidden_paths": ["backend/**", "frontend/**"], + "required_reviewers": ["senior engineering", "qa/test", "security/auth", "product/ops", "architecture", "ci integrity", "docs", "reuse/dedup", "test delta"], + "verification_commands": ["loop-memory-property-tests", "agent-gate-tests", "markdown-links", "stale-wording", "git-diff-check"] +} +``` + ## Allowed files ```text diff --git a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-05-authorization-property-invariants.md b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-05-authorization-property-invariants.md index 97cdbd828..ff4435d7b 100644 --- a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-05-authorization-property-invariants.md +++ b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-05-authorization-property-invariants.md @@ -32,6 +32,34 @@ P2 `implementation` +## Machine-checkable scope + +```chunk-scope-json +{ + "schema_version": 1, + "chunk_id": "WS-ENG-008-05", + "phase": "implementation", + "risk_class": "L1", + "allowed_paths": [ + "backend/pyproject.toml", + "backend/tests/test_authorization_properties.py", + ".github/workflows/backend.yml", + "scripts/assurance-requirements.txt", + "docs/operations_authorization_service.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/STATUS.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-05-authorization-property-invariants.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-05-internal-review-evidence.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-05-pr-trust-bundle.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-05-adversarial-proof.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-05-external-review-response.md", + ".agent-loop/merge-intents/WS-ENG-008-05.json" + ], + "forbidden_paths": ["backend/app/**", "backend/alembic/**"], + "required_reviewers": ["senior engineering", "qa/test", "security/auth", "product/ops", "architecture", "ci integrity", "docs", "reuse/dedup", "test delta"], + "verification_commands": ["authorization-property-tests", "authorization-property-lint", "markdown-links", "stale-wording", "git-diff-check"] +} +``` + ## Allowed files ```text diff --git a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-06-changed-module-mutation-pilot.md b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-06-changed-module-mutation-pilot.md index b3b4835f1..bcfc31413 100644 --- a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-06-changed-module-mutation-pilot.md +++ b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-06-changed-module-mutation-pilot.md @@ -32,6 +32,36 @@ P2 `implementation` +## Machine-checkable scope + +```chunk-scope-json +{ + "schema_version": 1, + "chunk_id": "WS-ENG-008-06", + "phase": "implementation", + "risk_class": "L1", + "allowed_paths": [ + "backend/pyproject.toml", + "backend/scripts/mutation_policy.py", + "backend/tests/test_mutation_policy.py", + ".github/workflows/backend.yml", + "scripts/assurance-requirements.txt", + "scripts/test_agent_gates.py", + "docs/operations_backend_testing.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/STATUS.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-06-changed-module-mutation-pilot.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-06-internal-review-evidence.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-06-pr-trust-bundle.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-06-adversarial-proof.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-06-external-review-response.md", + ".agent-loop/merge-intents/WS-ENG-008-06.json" + ], + "forbidden_paths": ["backend/app/**", "backend/alembic/**"], + "required_reviewers": ["senior engineering", "qa/test", "security/auth", "product/ops", "architecture", "ci integrity", "docs", "reuse/dedup", "test delta"], + "verification_commands": ["mutation-policy-tests", "mutation-policy-lint", "agent-gate-tests", "markdown-links", "stale-wording", "git-diff-check"] +} +``` + ## Allowed files ```text diff --git a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-07-lossless-review-memory-index.md b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-07-lossless-review-memory-index.md index dd9232df7..ae5ef12a0 100644 --- a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-07-lossless-review-memory-index.md +++ b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-07-lossless-review-memory-index.md @@ -36,6 +36,42 @@ Execution remains blocked until this planning intake is merged, chunks 01–06 are completed in order, and the explicit post-merge signed start selects this exact contract from current trusted `main`. +## Machine-checkable scope + +```chunk-scope-json +{ + "schema_version": 1, + "chunk_id": "WS-ENG-008-07", + "phase": "implementation", + "risk_class": "L1", + "allowed_paths": [ + ".agent-loop/README.md", + ".agent-loop/REVIEW_LOG.md", + ".agent-loop/review-log-archive/**", + ".agent-loop/policies/repository-engineering-policy.md", + "scripts/check_review_log_archive.py", + "scripts/test_check_review_log_archive.py", + "scripts/check_loop_memory_state.py", + "scripts/check_stale_artifact_contracts.py", + "scripts/check_markdown_links.py", + "scripts/check_stale_workstream_wording.py", + "scripts/test_agent_gates.py", + "AGENTS.md", + "CONTRIBUTING.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/STATUS.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-07-lossless-review-memory-index.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-07-internal-review-evidence.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-07-pr-trust-bundle.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-07-external-review-response.md", + ".agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-07-pre-migration-reconciliation.json", + ".agent-loop/merge-intents/WS-ENG-008-07.json" + ], + "forbidden_paths": ["backend/**", "frontend/**", ".github/workflows/**"], + "required_reviewers": ["senior engineering", "qa/test", "security/auth", "product/ops", "architecture", "ci integrity", "docs", "reuse/dedup", "test delta"], + "verification_commands": ["review-log-archive-tests", "review-log-archive-check", "agent-gate-tests", "loop-memory-state", "stale-artifact-contracts", "markdown-links", "stale-wording", "git-diff-check"] +} +``` + ## Allowed files ```text diff --git a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-external-review-response.md b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-external-review-response.md new file mode 100644 index 000000000..5e180d91c --- /dev/null +++ b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-external-review-response.md @@ -0,0 +1,72 @@ +# External Review Response: WS-ENG-008-01 + +## Review source + +- Pull request: `#203` +- Source: CodeRabbit +- Reviewed external head: `55dcad602f705cec7e6798234bdabb911e6683c3` +- Repaired implementation SHA: `848c6d972eba229478573faddb252eb534f8e5a8` + +## Comments addressed + +1. Provenance SHA consolidation: corrected the genuinely truncated signed-state + commit to `6923f9ed4a8e48327d3aa4d046c8a8dc3a31ea3a` in status and evidence. The + reviewed implementation SHA subclaim was dismissed: shell length and + `git cat-file` prove `1ef5c3bd0bffedec684ae8b6cec2e6affbcb3b21` + was already a resolvable 40-character commit. +2. Added the exact Ruff invocation to the authoritative Commands Run fence. +3. Mapped all nine reviewer tracks to the six sessions that performed them. +4. Replaced hard-coded template phase/risk values with explicit placeholders. +5. Added all conditional reviewer tracks and alignment guidance to the machine + template example, plus a direct-runner regression. +6. Replaced the negated bootstrap check with a positive marker test and explicit + `exit 1` for both trusted scope and evidence bootstrap paths. +7. Split validator imports from guarded execution so ImportError cannot refer to + an unbound `LoopMemoryError`. +8. Normalized strict UTF-8 failures across contract, projection, tree, merge + intent, and authenticated-ledger paths to stable `ContractError` failures. +9. Materialized both internal evidence code and its scope parser from trusted + base after cutover, while an explicit absolute repository-root contract keeps + all candidate Git/filesystem reads bound to `${{ github.workspace }}`. + +## Comments deferred + +None. + +## Human decisions needed + +None. All valid findings were inside the signed chunk contract. The false +reviewed-SHA subclaim was resolved from deterministic Git object evidence. + +## Internal repair review + +- senior engineering: PASS +- QA/test: PASS +- security/auth: PASS +- architecture: PASS WITH LOW RISKS +- CI integrity: PASS WITH LOW RISKS +- docs: PASS +- product/ops: PASS +- test delta: PASS +- reuse/dedup: prior PASS WITH LOW RISKS remains applicable; no new reuse blocker + +## Commands rerun + +```bash +python3 scripts/check_chunk_contract.py --base-ref origin/main --head-ref HEAD --state-ref origin/automation/loop-memory +python3 scripts/test_check_chunk_contract.py +python3 scripts/test_agent_gates.py +python3 scripts/update_post_merge_memory.py validate-merge-intent --repository-root . --base-ref origin/main +python3 scripts/check_internal_review_evidence.py +python3 scripts/check_markdown_links.py +python3 scripts/check_stale_workstream_wording.py +ruff check scripts/check_chunk_contract.py scripts/check_internal_review_evidence.py scripts/test_check_chunk_contract.py scripts/test_agent_gates.py +git diff --check origin/main...HEAD +``` + +## Remaining risks + +Only the previously accepted Low-risk consolidation opportunities remain: +duplicated reviewer/intent parsing and explicit trusted dependency lists. They +do not weaken this gate and require a separate future contract to consolidate. + diff --git a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-internal-review-evidence.md b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-internal-review-evidence.md new file mode 100644 index 000000000..4ed252163 --- /dev/null +++ b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-internal-review-evidence.md @@ -0,0 +1,95 @@ +# Internal Review Evidence: WS-ENG-008-01 + +## Chunk + +`WS-ENG-008-01` — Machine-Checkable Chunk Scope + +## Required Statements + +open sub-agent sessions: none + +valid findings addressed: yes + +## Signed Start Provenance + +- Authorized main SHA: `bd2203d5e8a972d8afbf833805b92ed70dedee4a` +- Signed start run: `30191914510` +- Signed state commit: `6923f9ed4a8e48327d3aa4d046c8a8dc3a31ea3a` +- Contract path: `.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-01-machine-checkable-chunk-scope.md` +- Signed contract blob: `2fb0afba71653e7aefcc074079fe98f44051c068` + +## Reviewed Revision + +Reviewed code SHA: `848c6d972eba229478573faddb252eb534f8e5a8` + +Reviewed at: `2026-07-26T08:29:21Z` + +Reviewer run IDs: `ci02b_lane_runner`, `ci02b_cr_arch`, `ci02b_cr_ci`, `ci02b_cr_docs`, `ci02b_cr_reuse`, `ci02b_cr_test_delta` + +Track/session mapping: senior engineering, QA/test, and security/auth used +`ci02b_lane_runner`; product/ops and docs used `ci02b_cr_docs`; architecture +used `ci02b_cr_arch`; CI integrity used `ci02b_cr_ci`; reuse/dedup used +`ci02b_cr_reuse`; test delta used `ci02b_cr_test_delta`. + +After the reviewed SHA, only initiative evidence, trust-bundle, external-review, +and status files may change. + +## Reviewer Results + +| Reviewer | Result | Blocking findings | Notes | +|---|---:|---|---| +| senior engineering | PASS | None | Exact final SHA; dead scope-free grandfather path removed. | +| QA/test | PASS | None | Twenty-seven focused tests cover the required real-Git mutation classes. | +| security/auth | PASS | None | Signed ledger, exact blob, grandfather, path, and trusted-base boundaries verified. | +| product/ops | PASS | None | Repository process remains separate from product lifecycle and authority. | +| architecture | PASS WITH LOW RISKS | None | Trusted-base execution closes candidate self-authorization; explicit dependency custody is acceptable. | +| CI integrity | PASS | None | No workflow, test, coverage, package, or permission weakening. | +| docs | PASS | None | Entry docs, glossary, lockdown, policies, and templates agree. | +| reuse/dedup | PASS WITH LOW RISKS | None | Local ledger/intent/reviewer parsing creates bounded future consolidation opportunities. | +| test delta | PASS | None | No tests removed or skipped; direct runner includes new regressions. | + +## Valid Findings Addressed + +- Candidate-code self-authorization: post-cutover Agent Gates materialize the + checker and both signed-state validators from the trusted PR base. The only + head-code bootstrap is bounded to a base containing neither the checker nor + the `WS-ENG-008-01` cutover marker. +- Grandfather scope bypass: eligibility and scope now derive from authenticated + ledger events plus the exact signed legacy contract blob; all grandfathered + changes pass normal scope enforcement. +- Untracked and alias bypasses: no-follow regular-file checks reject untracked + symlinks/executables, while full resulting-tree validation rejects byte, NFC, + and casefold collisions. +- Git mutation gaps: real repositories cover staged, dirty, untracked, rename, + copy, executable, symlink, gitlink, type-change, invalid UTF-8, non-NFC, + cancel, stop, restart, and post-cutover schema failures. +- Template/evidence drift: the chunk template includes Start phase, and machine + verification IDs bind the existing Commands Run fence to closed + repository-owned command strings. + +## Commands Run + +```bash +python3 scripts/test_check_chunk_contract.py +python3 scripts/test_agent_gates.py +python3 scripts/check_internal_review_evidence.py +python3 scripts/check_markdown_links.py +python3 scripts/check_stale_workstream_wording.py +ruff check scripts/check_chunk_contract.py scripts/check_internal_review_evidence.py scripts/test_check_chunk_contract.py scripts/test_agent_gates.py +git diff --check origin/main...HEAD +``` + +## Results + +- Machine scope tests: 28 passed. +- Agent Gate regressions: 104 passed. +- Exact signed-state scope selection: passed. +- Schema-v2 merge intent: passed. +- Ruff, Markdown links, stale wording, and diff checks: passed. + +## Remaining Risks + +- Reviewer names, merge-intent identity parsing, and the authenticated ledger + reduction have small duplicate representations across existing gate modules. + Reviewers classified consolidation as Low risk; it must not be performed in + this security-boundary chunk without a separate contract. diff --git a/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-pr-trust-bundle.md b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-pr-trust-bundle.md new file mode 100644 index 000000000..cd305cdaa --- /dev/null +++ b/.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-pr-trust-bundle.md @@ -0,0 +1,135 @@ +# PR Trust Bundle: WS-ENG-008-01 + +## Chunk + +`WS-ENG-008-01` — Machine-Checkable Chunk Scope + +Merge intent: `.agent-loop/merge-intents/WS-ENG-008-01.json` + +## Goal + +Make every post-cutover implementation/specification PR prove its complete Git +delta against the exact machine-readable contract selected by signed start. + +## Human-approved intent + +The approved contract is +`.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-01-machine-checkable-chunk-scope.md`. + +## Signed Start Provenance + +- Signed start run: `30191914510` +- Authorized main SHA: `bd2203d5e8a972d8afbf833805b92ed70dedee4a` +- Phase: `implementation` +- Contract path: `.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/chunks/WS-ENG-008-01-machine-checkable-chunk-scope.md` +- Signed contract blob SHA: `2fb0afba71653e7aefcc074079fe98f44051c068` +- Reviewed implementation SHA: `848c6d972eba229478573faddb252eb534f8e5a8` + +## What changed + +- Added strict schema-v1 contract parsing and closed path/command/reviewer registries. +- Added authenticated signed-ledger start selection and exact contract-blob custody. +- Added byte-safe complete Git delta, mode, Unicode, and collision enforcement. +- Added trusted-base Agent Gates execution with one bounded cutover bootstrap. +- Upgraded ENG-008 contracts 02–07 before naming chunk 02. +- Updated contributor, architecture, glossary, policy, and contract-template guidance. + +## Design chosen + +The signed ledger selects authority; the immutable start blob supplies machine +scope. Agent Gates authenticate generated state, reduce initiative-local events, +compare all base/head/index/worktree paths, and execute only repository-owned +command mappings. After cutover the interpreter and validators come from the +trusted base rather than candidate code. Work already active at cutover may +finish only under its exact signed legacy Allowed-files fence. + +## Scope control + +All 22 implementation files are explicitly listed by the signed human contract +and the new bootstrap machine block. No product, API, database, migration, +authorization, payment, signing, start/cancel, branch-protection, secret, +dependency, or coverage behavior changed. + +Final reconciliation found no overlap with active REV PR #195 or the concurrent +AUTH/ART work. Stale PR #149 overlaps the evidence checker and Agent Gate tests; +it has no ENG-008 authority and must rebase and satisfy the post-cutover scope +gate independently. + +## Acceptance criteria proof + +- Strict schema and human agreement: parser mutations plus all seven ENG-008 contracts pass. +- Closed path grammar and complete Git delta: 27 focused unit/integration tests pass. +- Signed cutover/grandfather custody: authenticated ledger fixtures and exact live state pass. +- Reviewer/command binding: internal evidence gate regression passes. +- Trusted Agent Gate integration: 102 regression tests pass. +- Successor control: one schema-v2 intent names `WS-ENG-008-02` with explicit start required. + +## Tests/checks run + +```bash +python3 scripts/check_chunk_contract.py --base-ref origin/main --head-ref HEAD --state-ref origin/automation/loop-memory +python3 scripts/test_check_chunk_contract.py +python3 scripts/test_agent_gates.py +python3 scripts/update_post_merge_memory.py validate-merge-intent --repository-root . --base-ref origin/main +python3 scripts/check_internal_review_evidence.py +python3 scripts/check_markdown_links.py +python3 scripts/check_stale_workstream_wording.py +ruff check scripts/check_chunk_contract.py scripts/check_internal_review_evidence.py scripts/test_check_chunk_contract.py scripts/test_agent_gates.py +git diff --check origin/main...HEAD +``` + +Result: all passed on the reviewed implementation plus evidence-only publication. + +## Test delta + +- Added 28 focused schema, signed-state, path, Git-mode, collision, and workflow tests. +- Added Agent Gate regressions for trusted execution and evidence binding. +- No test was removed, skipped, deselected, or weakened. + +## CI integrity + +- Coverage thresholds and existing 90-percent loop-memory coverage jobs are unchanged. +- No `continue-on-error`, path exclusion, unpinned action, permission expansion, + credential persistence, package-script bypass, or test weakening was added. + +## Reviewer results + +Reviewed code SHA: `848c6d972eba229478573faddb252eb534f8e5a8` + +Reviewed at: `2026-07-26T08:29:21Z` + +Reviewer run IDs: `ci02b_lane_runner`, `ci02b_cr_arch`, `ci02b_cr_ci`, `ci02b_cr_docs`, `ci02b_cr_reuse`, `ci02b_cr_test_delta` + +Track/session mapping: `ci02b_lane_runner` covered senior engineering, QA/test, +and security/auth; `ci02b_cr_docs` covered product/ops and docs; +`ci02b_cr_arch`, `ci02b_cr_ci`, `ci02b_cr_reuse`, and `ci02b_cr_test_delta` +covered architecture, CI integrity, reuse/dedup, and test delta respectively. + +All nine required tracks passed. Architecture and reuse recorded only Low-risk +future consolidation opportunities; there are no blocking findings. + +## Remaining risks + +Some validated-ledger, merge-intent, and reviewer-name parsing remains locally +duplicated. Consolidating authority-sensitive helpers requires its own reviewed +contract and must not delay this closed enforcement cutover. + +## Human review focus + +- Trusted-base versus one-time bootstrap execution in `agent-gates.yml`. +- Signed-ledger and exact-blob selection, including grandfather reduction. +- NUL/Unicode/mode/collision handling in `check_chunk_contract.py`. +- Exact schema blocks for successors 02–07. + +## External review + +- CodeRabbit: ten threads triaged; nine valid or partially valid findings fixed, + one reviewed-SHA subclaim dismissed after exact 40-character object proof. +- GitHub checks: prior exact head passed; repair-head rerun required after push. +- Response evidence: + `.agent-loop/initiatives/WS-ENG-008-repository-native-sdlc-assurance/reviews/WS-ENG-008-01-external-review-response.md` + +## Human ownership + +The PR remains stopped for explicit review and merge approval. Its merge will +not start `WS-ENG-008-02`. diff --git a/.agent-loop/merge-intents/WS-ENG-008-01.json b/.agent-loop/merge-intents/WS-ENG-008-01.json new file mode 100644 index 000000000..3a17b85c5 --- /dev/null +++ b/.agent-loop/merge-intents/WS-ENG-008-01.json @@ -0,0 +1,9 @@ +{ + "schema_version": 2, + "initiative_id": "WS-ENG-008", + "chunk_id": "WS-ENG-008-01", + "chunk_title": "Machine-Checkable Chunk Scope", + "next_chunk_id": "WS-ENG-008-02", + "next_chunk_title": "Scheduled Signed-State Drift Audit", + "next_requires_explicit_start": true +} diff --git a/.agent-loop/policies/definition-of-done.md b/.agent-loop/policies/definition-of-done.md index 60f30ec86..a95caf22e 100644 --- a/.agent-loop/policies/definition-of-done.md +++ b/.agent-loop/policies/definition-of-done.md @@ -6,6 +6,9 @@ A Workstream engineering chunk is done only when all applicable sections pass. - The chunk maps to one approved contract. - Changed files stay inside the allowed file set, or exceptions are documented. +- For post-cutover implementation/specification work, Agent Gates prove the + complete delta against the contract's schema-v1 machine scope; documentation + cannot waive or widen a failed scope result. - No unrelated refactor, product behavior, schema, dependency, or CI weakening is included. ## Evidence diff --git a/.agent-loop/policies/repository-engineering-policy.md b/.agent-loop/policies/repository-engineering-policy.md index 2ba5e539a..0432fb887 100644 --- a/.agent-loop/policies/repository-engineering-policy.md +++ b/.agent-loop/policies/repository-engineering-policy.md @@ -38,11 +38,18 @@ pytest -q | CI/review gates | `.github/workflows`, `scripts/`, `.agent-loop/` | Gates may be strengthened; weakening requires explicit human approval. | | Generated merge memory | `automation/loop-memory` | Trusted `main` automation owns a closed signed tree containing canonical state, ledger, manifest, loop/queue views, and compact initiative projections. Humans and agents do not edit it manually or trust isolated files without manifest/signature verification. Merge projections remain stopped/next-only until signed start events exist. | | Explicit engineering starts | `.github/workflows/loop-memory-start.yml` | An authenticated dispatcher whose current GitHub repository permission meets `.agent-loop/policies/loop-memory-start-authorities.json` on trusted `main` may dispatch a signed start for a declared successor or exact reviewed contract; the orchestrator may dispatch after an explicit user instruction, but conversation is not canonical evidence. Cancellation retains a protected-environment reviewer distinct from the dispatcher. No automatic start is valid. | +| Machine-checkable chunk scope | `scripts/check_chunk_contract.py` | After the WS-ENG-008-01 cutover, every implementation/specification start selects one strict schema-v1 contract. Agent Gates compare the complete byte-safe Git delta with its closed allowed/forbidden path grammar and never execute contract-provided text. | Explicit starts are initiative-local: each initiative may have at most one active planning or implementation chunk, while distinct initiatives may be active concurrently. Local worktrees are execution isolation, not authority. +Only work already signed-active at the exact machine-scope cutover may finish +under a generated grandfather record. That record is bound to the pre-cutover +start event, initiative/chunk identity, contract path, and immutable blob. +Stopped, proposed, cancelled/restarted, and post-cutover starts cannot inherit +the exception. + The sole first-contract admission is a planning-intake merge for an initiative absent from signed history. It is a closed additive planning tree with canonical `-PLAN` identity, required review/check evidence, one reviewed diff --git a/.agent-loop/templates/CHUNK_CONTRACT.md b/.agent-loop/templates/CHUNK_CONTRACT.md index 8e48e88cd..0af39f957 100644 --- a/.agent-loop/templates/CHUNK_CONTRACT.md +++ b/.agent-loop/templates/CHUNK_CONTRACT.md @@ -30,6 +30,53 @@ L0 / L1 / L2 / L3 / L4 P0 / P1 / P2 / P3 +## Start phase + +`implementation` / `specification` + +## Machine-checkable scope + +Every implementation or specification contract admitted after the +`WS-ENG-008-01` cutover must contain exactly one block in this form. The arrays +contain repository-relative paths or closed directory patterns, canonical +reviewer names, and repository-owned verification identifiers; they never +contain shell commands. + +```chunk-scope-json +{ + "schema_version": 1, + "chunk_id": "", + "phase": "", + "risk_class": "", + "allowed_paths": [ + "" + ], + "forbidden_paths": [ + "" + ], + "required_reviewers": [ + "senior engineering", + "qa/test", + "security/auth", + "product/ops", + "architecture", + "ci integrity", + "docs", + "reuse/dedup", + "test delta" + ], + "verification_commands": [ + "" + ] +} +``` + +The human-readable sections below remain mandatory and must agree with this +block. Replace phase and risk placeholders with the exact human values. Include +every applicable conditional reviewer in both reviewer sections and remove a +conditional track from both only when routing marks it unrelated. See +`CONTRIBUTING.md` for the closed path grammar and cutover rule. + ## Allowed files ```text diff --git a/.github/workflows/agent-gates.yml b/.github/workflows/agent-gates.yml index 2ee30829e..93366a28f 100644 --- a/.github/workflows/agent-gates.yml +++ b/.github/workflows/agent-gates.yml @@ -24,6 +24,38 @@ jobs: - name: Install agent gate dependencies run: python -m pip install --require-hashes -r scripts/agent-gate-requirements.txt + - name: Fetch signed loop-memory state + run: >- + git fetch --no-tags origin + +refs/heads/automation/loop-memory:refs/remotes/origin/automation/loop-memory + + - name: Machine-checkable chunk scope + env: + BASE_REF: ${{ github.base_ref }} + shell: bash + run: | + set -euo pipefail + trusted_scope_dir="$(mktemp -d)" + trap 'rm -rf "${trusted_scope_dir}"' EXIT + trusted_checker="${trusted_scope_dir}/check_chunk_contract.py" + if git cat-file -e "origin/${BASE_REF}:scripts/check_chunk_contract.py"; then + for script in check_chunk_contract.py update_post_merge_memory.py check_loop_memory_state.py; do + git show "origin/${BASE_REF}:scripts/${script}" > "${trusted_scope_dir}/${script}" + done + else + if git cat-file -e "origin/${BASE_REF}:.agent-loop/merge-intents/WS-ENG-008-01.json" 2>/dev/null; then + echo "::error::scope cutover is present; refusing local-script bootstrap" >&2 + exit 1 + fi + for script in check_chunk_contract.py update_post_merge_memory.py check_loop_memory_state.py; do + cp "scripts/${script}" "${trusted_scope_dir}/${script}" + done + fi + PYTHONPATH="${trusted_scope_dir}" python3 "${trusted_checker}" \ + --repo "${GITHUB_WORKSPACE}" \ + --base-ref "origin/${BASE_REF}" --head-ref HEAD \ + --state-ref origin/automation/loop-memory + - name: Static agent sensor advisory env: BASE_REF: ${{ github.base_ref }} @@ -74,4 +106,25 @@ jobs: - name: Internal review evidence gate env: PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: python3 scripts/check_internal_review_evidence.py + BASE_REF: ${{ github.base_ref }} + INTERNAL_REVIEW_REPOSITORY_ROOT: ${{ github.workspace }} + shell: bash + run: | + set -euo pipefail + trusted_evidence_dir="$(mktemp -d)" + trap 'rm -rf "${trusted_evidence_dir}"' EXIT + if git cat-file -e "origin/${BASE_REF}:scripts/check_chunk_contract.py"; then + for script in check_internal_review_evidence.py check_chunk_contract.py; do + git show "origin/${BASE_REF}:scripts/${script}" > "${trusted_evidence_dir}/${script}" + done + else + if git cat-file -e "origin/${BASE_REF}:.agent-loop/merge-intents/WS-ENG-008-01.json" 2>/dev/null; then + echo "::error::scope cutover is present; refusing local evidence bootstrap" >&2 + exit 1 + fi + for script in check_internal_review_evidence.py check_chunk_contract.py; do + cp "scripts/${script}" "${trusted_evidence_dir}/${script}" + done + fi + PYTHONPATH="${trusted_evidence_dir}" python3 \ + "${trusted_evidence_dir}/check_internal_review_evidence.py" diff --git a/AGENTS.md b/AGENTS.md index 8345d9950..dfebc3a8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -72,6 +72,12 @@ Workstream is how Flow measures, certifies, and coordinates useful human-agent w paths. - Every non-trivial task starts with the smallest applicable loop artifact: an initiative plan for large work, or a chunk contract for bounded work. - Do not implement a chunk until its allowed files, not-allowed changes, acceptance criteria, risk class, verification commands, and required reviewers are explicit. +- After the `WS-ENG-008-01` cutover, every implementation or specification + contract must carry one valid schema-v1 `chunk-scope-json` block. Agent Gates + enforce its closed repository-relative path grammar against the complete Git + delta; human prose cannot widen it, forbidden paths win, and contract values + never execute shell text. Only work already signed-active at the exact + cutover may use the generated event/path/blob-bound grandfather record. - Do not begin the next chunk automatically after finishing the current chunk. - Merge-intent schema v2 may name only a successor in the same initiative. Use a null successor when no same-initiative chunk is declared; cross-initiative diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9d82e5c56..2df4d2e3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,6 +34,9 @@ execution; they are not authority. large or ambiguous work, or a bounded chunk contract for smaller work. The contract must state allowed files, forbidden changes, acceptance criteria, risk, verification commands, reviewers, and human review focus. + Post-cutover implementation and specification contracts also carry one + strict `chunk-scope-json` block. Agent Gates compare the complete PR and + local-status delta with that block; prose cannot widen it. 3. Ask an authenticated repository writer to dispatch `Loop Memory Explicit Event` on exact current `main`. A valid start is independently verified signed state on `automation/loop-memory`; chat, an issue, a branch, a commit, @@ -75,6 +78,14 @@ to skip current-main reconciliation, tests, internal review, or human approval. - Work in the exact signed chunk and remain inside its allowed files and acceptance criteria. +- Machine scope paths are repository-relative NFC UTF-8 names. A literal file + names one file; a trailing `/**` names that directory recursively. Absolute + paths, `.`/`..` components, shell/regex syntax, negation, braces, backslashes, + controls, symlinks, gitlinks, executable changes, and byte, normalization, or + casefold collisions fail closed. A forbidden match always overrides an + allowed match. +- Contract verification values are closed repository-owned command identifiers, + never executable text supplied by the contract. - Reconcile with current `main` before publication. If the base changes, inspect the delta and rerun all contract proof; a rebase does not replace the signed contract or authorize scope drift. @@ -85,6 +96,14 @@ to skip current-main reconciliation, tests, internal review, or human approval. - Do not begin another chunk automatically. Distinct initiatives may proceed in parallel only when each has its own valid signed active chunk. +The scope ratchet begins when trusted `main` contains the +`WS-ENG-008-01` merge intent. Every implementation or specification start from +that point requires schema v1. Only a chunk already signed-active at the exact +cutover may finish without it, and only through generated evidence bound to its +pre-cutover start event, initiative/chunk identity, contract path, and contract +blob. Stopped, proposed, cancelled/restarted, and newly started work is never +grandfathered. + ## Before Opening A Pull Request 1. Complete every required internal reviewer track and resolve or document each diff --git a/README.md b/README.md index 0b0269550..85d0f8f02 100644 --- a/README.md +++ b/README.md @@ -161,9 +161,13 @@ and compensation specification control contribution recognition, award eligibility, and fulfillment boundaries. Older chunk specifications remain implementation history until their owning migrations replace the runtime. -## Engineering Loop +## Repository-Native Human-Agent SDLC -Workstream is built with a Codex-native zero-trust engineering loop: +Workstream uses a Repository-Native Human-Agent SDLC: intent, authority, +execution, verification, review, and durable memory are encoded and enforced in +the repository so humans and agents can collaborate without trusting chat +history or unaudited claims. Its enforcement mechanism is the Codex-native +zero-trust engineering loop: ```text Intent @@ -184,6 +188,11 @@ Codex-discoverable skills live in `.agents/skills/`. Codex custom reviewer agents live in `.codex/agents/`. Durable engineering memory, policies, chunk contracts, reviews, and status live in `.agent-loop/`. +Post-cutover implementation and specification contracts include a strict +machine-checkable scope block. Agent Gates fail closed when a changed path, +reviewer requirement, or verification identifier falls outside the reviewed +contract; the block authorizes no command execution. + This engineering loop is separate from Workstream product state. It governs how the repository is changed; it does not define runtime task or review records. Each initiative may have at most one active planning or implementation chunk; diff --git a/docs/architecture_lockdown.md b/docs/architecture_lockdown.md index d26ee5832..a53cbb458 100644 --- a/docs/architecture_lockdown.md +++ b/docs/architecture_lockdown.md @@ -11,6 +11,12 @@ and reputation consequences. The ADR files under `docs/decision_*.md` are the decision record for this lockdown. When a locked rule changes, update or add an ADR before changing implementation specs. +Repository changes use the Repository-Native Human-Agent SDLC documented in +`README.md` and `CONTRIBUTING.md`. Its Codex-native zero-trust engineering loop +binds reviewed intent, signed authority, machine-checkable chunk scope, +evidence, internal review, a human merge checkpoint, and signed merge memory. +That repository process is separate from every product lifecycle below. + The canonical v0.1 scope remains narrower: ```text diff --git a/docs/glossary.md b/docs/glossary.md index d33dc5b52..449eec4fb 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -7,6 +7,23 @@ guides, task queues, submission packets, automated checks, reviewer routing, evaluation sprints, revision loops, contribution records, compensation award and fulfillment state, and reputation signals. +## Repository-Native Human-Agent SDLC + +The broader software-development lifecycle in which intent, authority, +execution, verification, review, and durable memory are encoded and enforced +inside the repository. It allows humans and agents to collaborate without +trusting chat history or unaudited implementation claims. Workstream's +Codex-native zero-trust engineering loop is the enforcement mechanism for this +lifecycle, not a Workstream product review state machine. + +## Machine-Checkable Chunk Scope + +The strict schema-v1 `chunk-scope-json` block in a post-cutover implementation +or specification contract. It binds the chunk identity, phase, risk, allowed +and forbidden repository-relative paths, reviewer tracks, and repository-owned +verification identifiers. Agent Gates compare it with the complete Git delta; +it cannot execute contract-provided commands or widen human-readable scope. + ## Project A configured work program with its own human-facing guide, submission artifact diff --git a/scripts/check_chunk_contract.py b/scripts/check_chunk_contract.py new file mode 100644 index 000000000..eaa54cec2 --- /dev/null +++ b/scripts/check_chunk_contract.py @@ -0,0 +1,772 @@ +#!/usr/bin/env python3 +"""Fail-closed validation of machine-readable chunk scope contracts.""" + +from __future__ import annotations + +import argparse +import json +import os +import re +import stat +import subprocess +import sys +import tempfile +import unicodedata +from dataclasses import dataclass +from pathlib import Path, PurePosixPath +from typing import Any, Iterable, Sequence + +MAX_CONTRACT_BYTES = 128 * 1024 +MAX_ITEMS = 256 +PHASES = {"implementation", "specification"} +RISKS = {"L0", "L1", "L2", "L3", "L4"} +REVIEWERS = { + "senior engineering", "qa/test", "security/auth", "product/ops", + "architecture", "ci integrity", "docs", "reuse/dedup", "test delta", +} +VERIFICATION_COMMANDS = { + "chunk-scope-tests": "python3 scripts/test_check_chunk_contract.py", + "agent-gate-tests": "python3 scripts/test_agent_gates.py", + "internal-review-evidence": "python3 scripts/check_internal_review_evidence.py", + "markdown-links": "python3 scripts/check_markdown_links.py", + "stale-wording": "python3 scripts/check_stale_workstream_wording.py", + "git-diff-check": "git diff --check origin/main...HEAD", + "loop-memory-drift-tests": "python3 scripts/test_audit_loop_memory_drift.py", + "loop-memory-property-tests": "PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m pytest -p hypothesis.extra.pytestplugin -q scripts/test_loop_memory_properties.py", + "authorization-property-tests": "python -m pytest -q tests/test_authorization_properties.py", + "authorization-property-lint": "ruff check tests/test_authorization_properties.py", + "mutation-policy-tests": "python -m pytest -q tests/test_mutation_policy.py", + "mutation-policy-lint": "ruff check scripts/mutation_policy.py tests/test_mutation_policy.py", + "review-log-archive-tests": "python3 scripts/test_check_review_log_archive.py", + "review-log-archive-check": "python3 scripts/check_review_log_archive.py", + "loop-memory-state": "python3 scripts/check_loop_memory_state.py", + "stale-artifact-contracts": "python3 scripts/check_stale_artifact_contracts.py", +} +VERIFICATION_COMMAND_IDS = frozenset(VERIFICATION_COMMANDS) +KEYS = { + "schema_version", "chunk_id", "phase", "risk_class", "allowed_paths", + "forbidden_paths", "required_reviewers", "verification_commands", +} +CHUNK_RE = re.compile(r"[A-Z][A-Z0-9]*(?:-[A-Z0-9]+){3,}") +HEADING_RE = re.compile( + r"\A# (?:Chunk Contract|Parent Chunk):\s+(?P[A-Z][A-Z0-9]*(?:-[A-Z0-9]+){3,})(?:\s+[—-].*)?$", + re.M, +) +FENCE_RE = re.compile(r"^```chunk-scope-json[ \t]*\n(?P.*?)^```[ \t]*$", re.M | re.S) +CONTROL_RE = re.compile(r"[\x00-\x1f\x7f-\x9f]") +GLOB_META_RE = re.compile(r"[*?\[\]{}!\\]") + + +class ContractError(ValueError): + """A stable, user-facing contract validation failure.""" + + +def _decode_utf8(raw: bytes, label: str = "contract") -> str: + """Decode security-sensitive bytes into one stable fail-closed error.""" + try: + return raw.decode("utf-8", "strict") + except UnicodeDecodeError as exc: + raise ContractError(f"{label} is not valid UTF-8") from exc + + +def _object(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise ContractError(f"duplicate JSON key: {key}") + result[key] = value + return result + + +def _text(value: Any, label: str) -> str: + if type(value) is not str or not value: + raise ContractError(f"{label} must be a non-empty string") + if CONTROL_RE.search(value) or unicodedata.normalize("NFC", value) != value: + raise ContractError(f"{label} contains control or non-NFC Unicode") + return value + + +def _items(value: Any, label: str) -> tuple[str, ...]: + if type(value) is not list or not value or len(value) > MAX_ITEMS: + raise ContractError(f"{label} must be a non-empty list of at most {MAX_ITEMS} items") + items = tuple(_text(item, f"{label} item") for item in value) + if len(items) != len(set(items)): + raise ContractError(f"{label} contains duplicate items") + folded = [item.casefold() for item in items] + if len(folded) != len(set(folded)): + raise ContractError(f"{label} contains casefold-colliding items") + return items + + +def validate_pattern(pattern: str) -> str: + """Validate the closed grammar: a file, or a directory ending in '/**'.""" + _text(pattern, "path pattern") + recursive = pattern.endswith("/**") + stem = pattern[:-3] if recursive else pattern + if not stem or stem.startswith("/") or "//" in stem or GLOB_META_RE.search(stem): + raise ContractError(f"noncanonical path pattern: {pattern!r}") + if stem.endswith("/") or PurePosixPath(stem).is_absolute(): + raise ContractError(f"noncanonical path pattern: {pattern!r}") + parts = stem.split("/") + if any(part in {"", ".", ".."} for part in parts): + raise ContractError(f"path traversal is forbidden: {pattern!r}") + if any(part.endswith(" ") or part.endswith(".") for part in parts): + raise ContractError(f"platform-ambiguous path pattern: {pattern!r}") + return pattern + + +@dataclass(frozen=True) +class ScopeContract: + chunk_id: str + phase: str + risk_class: str + allowed_paths: tuple[str, ...] + forbidden_paths: tuple[str, ...] + required_reviewers: tuple[str, ...] + verification_commands: tuple[str, ...] + + +def parse_contract_bytes(raw: bytes) -> ScopeContract: + if len(raw) > MAX_CONTRACT_BYTES: + raise ContractError("contract exceeds size limit") + try: + text = raw.decode("utf-8", "strict") + except UnicodeDecodeError as exc: + raise ContractError("contract is not valid UTF-8") from exc + if "\r" in text or CONTROL_RE.search(text.replace("\n", "")): + raise ContractError("contract contains noncanonical line endings or controls") + if unicodedata.normalize("NFC", text) != text: + raise ContractError("contract is not NFC-normalized") + heading = HEADING_RE.match(text) + if not heading: + raise ContractError("missing canonical chunk heading") + blocks = list(FENCE_RE.finditer(text)) + if len(blocks) != 1: + raise ContractError("contract must contain exactly one `chunk-scope-json` block") + try: + data = json.loads(blocks[0].group("body"), object_pairs_hook=_object) + except (json.JSONDecodeError, ContractError) as exc: + raise ContractError(f"invalid scope JSON: {exc}") from exc + if type(data) is not dict: + raise ContractError("scope JSON must be an object") + if set(data) != KEYS: + raise ContractError(f"scope JSON keys must be exactly {sorted(KEYS)}") + if type(data["schema_version"]) is not int or data["schema_version"] != 1: + raise ContractError("schema_version must be integer 1") + chunk_id = _text(data["chunk_id"], "chunk_id") + if not CHUNK_RE.fullmatch(chunk_id) or chunk_id != heading.group("id"): + raise ContractError("JSON chunk_id does not match canonical heading") + phase = _text(data["phase"], "phase") + if phase not in PHASES: + raise ContractError(f"unsupported phase: {phase}") + phase_match = re.search(r"^## Start phase\n\n`([^`]+)`[ \t]*$", text, re.M) + if not phase_match or phase_match.group(1) != phase: + raise ContractError("machine phase disagrees with human Start phase") + risk = _text(data["risk_class"], "risk_class") + if risk not in RISKS: + raise ContractError(f"unsupported risk class: {risk}") + risk_match = re.search(r"^## Risk class\n\n([^\n]+)$", text, re.M) + if not risk_match or risk_match.group(1).strip(" `") != risk: + raise ContractError("machine risk disagrees with human Risk class") + allowed = tuple(validate_pattern(p) for p in _items(data["allowed_paths"], "allowed_paths")) + forbidden = tuple(validate_pattern(p) for p in _items(data["forbidden_paths"], "forbidden_paths")) + reviewers = _items(data["required_reviewers"], "required_reviewers") + unknown_reviewers = set(reviewers) - REVIEWERS + if unknown_reviewers: + raise ContractError(f"unknown reviewer identifiers: {sorted(unknown_reviewers)}") + commands = _items(data["verification_commands"], "verification_commands") + unknown_commands = set(commands) - VERIFICATION_COMMAND_IDS + if unknown_commands: + raise ContractError(f"unknown verification identifiers: {sorted(unknown_commands)}") + verification_section = _section(text, "Verification commands") + verification_fence = re.search(r"```bash\n(.*?)```", verification_section, re.S) + if not verification_fence: + raise ContractError("Verification commands must contain a bash fence") + human_commands = tuple( + line for line in verification_fence.group(1).splitlines() + if line and not line.startswith("cd ") + ) + expected_commands = tuple(VERIFICATION_COMMANDS[identifier] for identifier in commands) + if set(human_commands) != set(expected_commands) or len(human_commands) != len(expected_commands): + raise ContractError("machine verification identifiers disagree with human commands") + human_allowed = _text_block_items(text, "Allowed files") + if set(human_allowed) != set(allowed): + raise ContractError("machine allowed_paths disagree with human Allowed files") + human_reviewers = tuple(re.findall(r"^- \[[ xX]\] (.+)$", _section(text, "Required reviewers"), re.M)) + if {item.casefold() for item in human_reviewers} != {item.casefold() for item in reviewers}: + raise ContractError("machine required_reviewers disagree with human section") + return ScopeContract(chunk_id, phase, risk, allowed, forbidden, reviewers, commands) + + +def _section(text: str, heading: str) -> str: + match = re.search(rf"^## {re.escape(heading)}\n(?P.*?)(?=^## |\Z)", text, re.M | re.S) + if not match: + raise ContractError(f"missing human section: {heading}") + return match.group("body") + + +def _text_block_items(text: str, heading: str) -> tuple[str, ...]: + section = _section(text, heading) + match = re.search(r"```text\n(.*?)```", section, re.S) + if not match: + raise ContractError(f"{heading} must contain a text fence") + items = tuple(line for line in match.group(1).splitlines() if line) + for item in items: + validate_pattern(item) + return items + + +def validate_path_bytes(paths: Iterable[bytes]) -> tuple[str, ...]: + decoded: list[str] = [] + byte_seen: set[bytes] = set() + nfc_seen: set[str] = set() + fold_seen: set[str] = set() + for raw in paths: + if raw in byte_seen: + raise ContractError("duplicate byte path") + byte_seen.add(raw) + try: + path = raw.decode("utf-8", "strict") + except UnicodeDecodeError as exc: + raise ContractError("Git path is not valid UTF-8") from exc + if CONTROL_RE.search(path): + raise ContractError("Git path contains a control character") + nfc = unicodedata.normalize("NFC", path) + if nfc != path: + raise ContractError("Git path is not NFC-normalized") + validate_pattern(path) + folded = nfc.casefold() + if nfc in nfc_seen or folded in fold_seen: + raise ContractError("Git paths collide after NFC or casefold normalization") + nfc_seen.add(nfc) + fold_seen.add(folded) + decoded.append(path) + return tuple(decoded) + + +def matches(pattern: str, path: str) -> bool: + return path == pattern[:-3] or path.startswith(pattern[:-3] + "/") if pattern.endswith("/**") else path == pattern + + +def enforce_scope(contract: ScopeContract, paths: Iterable[bytes]) -> tuple[str, ...]: + decoded = validate_path_bytes(tuple(dict.fromkeys(paths))) + for path in decoded: + if any(matches(pattern, path) for pattern in contract.forbidden_paths): + raise ContractError(f"forbidden changed path: {path}") + if not any(matches(pattern, path) for pattern in contract.allowed_paths): + raise ContractError(f"changed path is outside allowed scope: {path}") + return decoded + + +def parse_name_status_z(raw: bytes) -> list[tuple[str, tuple[bytes, ...]]]: + if raw and not raw.endswith(b"\0"): + raise ContractError("Git status is not NUL-terminated") + fields = raw.split(b"\0") + if fields[-1:] == [b""]: + fields.pop() + result: list[tuple[str, tuple[bytes, ...]]] = [] + index = 0 + while index < len(fields): + try: + status = fields[index].decode("ascii", "strict") + except UnicodeDecodeError as exc: + raise ContractError("non-ASCII Git status") from exc + index += 1 + if not re.fullmatch(r"[ACDMRTUXB](?:\d{1,3})?", status): + raise ContractError(f"unsupported Git status: {status!r}") + count = 2 if status[0] in "RC" else 1 + if index + count > len(fields): + raise ContractError("truncated NUL-delimited Git status") + names = tuple(fields[index:index + count]) + index += count + result.append((status, names)) + return result + + +def validate_raw_modes_z(raw: bytes) -> None: + """Reject executable, symlink, gitlink and type modes on either diff side.""" + if raw and not raw.endswith(b"\0"): + raise ContractError("raw Git diff is not NUL-terminated") + fields = raw.split(b"\0")[:-1] + index = 0 + while index < len(fields): + metadata = fields[index] + index += 1 + match = re.fullmatch( + br":(?P[0-7]{6}) (?P[0-7]{6}) [0-9a-f]+ [0-9a-f]+ (?P[ACDMRTUXB][0-9]{0,3})", + metadata, + ) + if not match: + raise ContractError("malformed raw Git diff record") + status = match.group("status") + name_count = 2 if status[:1] in {b"R", b"C"} else 1 + if index + name_count > len(fields): + raise ContractError("truncated raw Git diff record") + index += name_count + for mode in (match.group("old"), match.group("new")): + if mode not in {b"000000", b"100644"}: + labels = {b"100755": "executable", b"120000": "symlink", b"160000": "gitlink"} + raise ContractError(f"raw Git diff has forbidden {labels.get(mode, 'type')} mode") + + +def tree_paths_z(raw: bytes) -> tuple[bytes, ...]: + """Return paths from a recursive NUL tree for result-name collision checks.""" + paths: list[bytes] = [] + for record in (item for item in raw.split(b"\0") if item): + metadata, separator, path = record.partition(b"\t") + fields = metadata.split(b" ") + if not separator or len(fields) != 3: + raise ContractError("malformed resulting Git tree record") + mode, kind, _blob = fields + valid_entry = ( + (mode in {b"100644", b"100755", b"120000"} and kind == b"blob") + or (mode == b"160000" and kind == b"commit") + ) + if not valid_entry: + raise ContractError("resulting tree has malformed mode/type") + paths.append(path) + return tuple(paths) + + +def _git(repo: Path, *args: str) -> bytes: + proc = subprocess.run(["git", *args], cwd=repo, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if proc.returncode: + raise ContractError(f"git {' '.join(args)} failed: {proc.stderr.decode('utf-8', 'replace').strip()}") + return proc.stdout + + +def validate_untracked_files(repo: Path, raw_paths: Sequence[bytes]) -> None: + """Accept only regular, non-executable untracked files without symlink walks.""" + paths = validate_path_bytes(raw_paths) + directory_flags = os.O_RDONLY | os.O_DIRECTORY + if hasattr(os, "O_NOFOLLOW"): + directory_flags |= os.O_NOFOLLOW + root_fd = os.open(repo, directory_flags) + try: + for path in paths: + parts = path.split("/") + current_fd = root_fd + opened: list[int] = [] + try: + for component in parts[:-1]: + current_fd = os.open(component, directory_flags, dir_fd=current_fd) + opened.append(current_fd) + info = os.stat(parts[-1], dir_fd=current_fd, follow_symlinks=False) + except (FileNotFoundError, NotADirectoryError, OSError) as exc: + raise ContractError(f"unsafe untracked path: {path}") from exc + finally: + for descriptor in reversed(opened): + os.close(descriptor) + if not stat.S_ISREG(info.st_mode): + raise ContractError(f"untracked path is not a regular file: {path}") + if info.st_mode & 0o111: + raise ContractError(f"untracked path is executable: {path}") + finally: + os.close(root_fd) + + +def _git_json(repo: Path, revision_path: str) -> dict[str, Any]: + raw = _git(repo, "show", revision_path) + try: + value = json.loads( + _decode_utf8(raw, f"signed state at {revision_path}"), + object_pairs_hook=_object, + ) + except (json.JSONDecodeError, ContractError) as exc: + raise ContractError(f"malformed signed state at {revision_path}: {exc}") from exc + if type(value) is not dict: + raise ContractError(f"signed state at {revision_path} is not an object") + return value + + +def added_merge_intent(repo: Path, base: str, head: str) -> dict[str, str]: + raw = _git(repo, "diff", "--name-only", "--diff-filter=A", "-z", f"{base}...{head}") + paths = validate_path_bytes(path for path in raw.split(b"\0") if path) + intents = [ + path for path in paths + if re.fullmatch(r"\.agent-loop/merge-intents/[A-Z][A-Z0-9-]+\.json", path) + ] + if len(intents) != 1: + raise ContractError("delta must add exactly one merge intent") + data = _git_json(repo, f"{head}:{intents[0]}") + initiative = data.get("initiative_id") + chunk = data.get("chunk_id") + if type(initiative) is not str or type(chunk) is not str or not CHUNK_RE.fullmatch(chunk): + raise ContractError("merge intent has invalid initiative/chunk identity") + if not chunk.startswith(initiative + "-"): + raise ContractError("merge intent chunk does not belong to initiative") + return {"path": intents[0], "initiative_id": initiative, "chunk_id": chunk} + + +@dataclass(frozen=True) +class SignedStart: + ledger_index: int + initiative_id: str + chunk_id: str + phase: str + main_sha: str + contract_path: str + contract_blob_sha: str + + +def verify_state_ref(repo: Path, state_ref: str) -> None: + """Authenticate and semantically validate the exact generated state tree.""" + raw = _git(repo, "ls-tree", "-rz", "--full-tree", state_ref) + records = [record for record in raw.split(b"\0") if record] + if not records: + raise ContractError("state ref tree is empty") + parsed: list[tuple[bytes, bytes]] = [] + raw_paths: list[bytes] = [] + for record in records: + metadata, separator, path = record.partition(b"\t") + fields = metadata.split(b" ") + if not separator or len(fields) != 3: + raise ContractError("malformed state-ref tree record") + mode, kind, blob = fields + if mode != b"100644" or kind != b"blob" or not re.fullmatch(b"[0-9a-f]{40,64}", blob): + raise ContractError("state ref contains a non-ordinary blob") + raw_paths.append(path) + parsed.append((blob, path)) + paths = validate_path_bytes(raw_paths) + if any(not path.startswith(".agent-loop/") for path in paths): + raise ContractError("state ref contains a path outside the closed generated tree") + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + for (blob, _raw_path), path in zip(parsed, paths, strict=True): + destination = root / path + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_bytes(_git(repo, "cat-file", "blob", blob.decode("ascii"))) + public_key = repo / ".agent-loop/keys/loop-memory-signing-public.pem" + if not public_key.is_file() or public_key.is_symlink(): + raise ContractError("trusted loop-memory public key is unavailable") + try: + from update_post_merge_memory import ( + LoopMemoryError, + verify_generated_state_signature, + ) + from check_loop_memory_state import generated_state_failures + except ImportError as exc: + raise ContractError(f"state-ref authentication failed: {exc}") from exc + try: + verify_generated_state_signature(root, public_key) + failures = generated_state_failures(root, repo) + except (LoopMemoryError, OSError, UnicodeError, ValueError) as exc: + raise ContractError(f"state-ref authentication failed: {exc}") from exc + if failures: + raise ContractError( + "state-ref semantic validation failed: " + "; ".join(failures) + ) + + +def authenticated_ledger(repo: Path, state_ref: str) -> tuple[dict[str, Any], ...]: + """Return ordered records from the already authenticated tip ledger.""" + raw = _decode_utf8( + _git(repo, "show", f"{state_ref}:.agent-loop/MERGE_LOG.jsonl"), + "authenticated ledger", + ) + records: list[dict[str, Any]] = [] + for number, line in enumerate(raw.splitlines(), 1): + if not line: + raise ContractError(f"authenticated ledger has blank line {number}") + try: + envelope = json.loads(line, object_pairs_hook=_object) + except (json.JSONDecodeError, ContractError) as exc: + raise ContractError(f"authenticated ledger line {number} is malformed: {exc}") from exc + keys = {"schema_version", "previous_entry_hash", "record", "entry_hash"} + if type(envelope) is not dict or set(envelope) != keys or type(envelope.get("record")) is not dict: + raise ContractError(f"authenticated ledger line {number} has invalid envelope") + records.append(envelope["record"]) + if not records: + raise ContractError("authenticated ledger is empty") + return tuple(records) + + +def _start_from_record(index: int, record: dict[str, Any], chunk_id: str) -> SignedStart | None: + event = record.get("event") + if type(event) is not dict or event.get("chunk_id") != chunk_id: + return None + if event.get("type") != "start": + raise ContractError(f"latest signed event for {chunk_id} is not a start") + selection = event.get("selection") + if type(selection) is not dict: + raise ContractError("signed start has no contract selection") + values = ( + event.get("initiative_id"), event.get("chunk_id"), selection.get("phase"), + event.get("main_sha"), selection.get("contract_path"), selection.get("contract_blob_sha"), + ) + if not all(type(value) is str and value for value in values): + raise ContractError("signed start binding is incomplete") + return SignedStart(index, *values) # type: ignore[arg-type] + + +def latest_signed_start(records: Sequence[dict[str, Any]], chunk_id: str) -> SignedStart: + for index in range(len(records) - 1, -1, -1): + record = records[index] + event = record.get("event") + if type(event) is dict and event.get("chunk_id") == chunk_id: + start = _start_from_record(index, record, chunk_id) + if start is None: # pragma: no cover - guarded above + break + return start + raise ContractError(f"no signed event found for {chunk_id}") + + +def reduce_active(records: Sequence[dict[str, Any]]) -> dict[str, tuple[str, str]]: + """Reduce authenticated records into initiative-local active slots.""" + active: dict[str, tuple[str, str]] = {} + for record in records: + event = record.get("event") + completed = record.get("completed_chunk") + # Start/cancel snapshots repeat the latest completed chunk globally; + # only a merge record (no event) completes and clears an initiative. + if (event is None and type(completed) is dict + and type(completed.get("initiative_id")) is str): + active.pop(completed["initiative_id"], None) + if type(event) is not dict: + continue + initiative = event.get("initiative_id") + chunk = event.get("chunk_id") + event_type = event.get("type") + if event_type == "cutover": + continue + if type(initiative) is not str or type(chunk) is not str: + raise ContractError("authenticated ledger event has invalid identity") + if event_type == "start": + selection = event.get("selection") + phase = selection.get("phase") if type(selection) is dict else None + if phase is None: + authority = record.get("authority_state") + slots = authority.get("active") if type(authority) is dict else None + matches = [ + candidate for candidate in ("planning", "implementation") + if type(slots) is dict and slots.get(f"{candidate}_chunk") == chunk + ] + phase = matches[0] if len(matches) == 1 else None + if phase not in {"planning", "implementation", "specification"}: + raise ContractError("authenticated start has invalid phase") + if initiative in active: + raise ContractError("authenticated ledger starts an already-active initiative") + active[initiative] = (phase, chunk) + elif event_type == "cancel": + current = active.get(initiative) + if current is None or current[1] != chunk: + raise ContractError("authenticated cancellation does not target active chunk") + active.pop(initiative) + else: + raise ContractError(f"unsupported authenticated event type: {event_type!r}") + return active + + +def require_active_projection(repo: Path, state_ref: str, start: SignedStart) -> None: + path = f".agent-loop/INITIATIVE_STATE/{start.initiative_id}.md" + text = _decode_utf8( + _git(repo, "show", f"{state_ref}:{path}"), "initiative projection" + ) + label = "planning" if start.phase == "planning" else "implementation" + match = re.search(rf"^- Active {label} chunk: `([^`]+)`$", text, re.M) + if not match or match.group(1) != start.chunk_id: + raise ContractError("current signed initiative projection is not active for target chunk") + + +def signed_contract_blob(repo: Path, start: SignedStart, base: str) -> bytes: + if _git(repo, "merge-base", "--is-ancestor", start.main_sha, base) != b"": + # merge-base --is-ancestor has no output; _git already enforces its exit code. + raise ContractError("unexpected ancestry output") + tree = _decode_utf8( + _git(repo, "ls-tree", start.main_sha, "--", start.contract_path), + "contract tree entry", + ).strip() + expected = f"100644 blob {start.contract_blob_sha}\t{start.contract_path}" + if tree != expected: + raise ContractError("signed start contract path/blob does not match its trusted main") + return _git(repo, "cat-file", "blob", start.contract_blob_sha) + + +def machine_block(raw: bytes) -> bytes: + try: + text = raw.decode("utf-8", "strict") + except UnicodeDecodeError as exc: + raise ContractError("contract is not valid UTF-8") from exc + blocks = list(FENCE_RE.finditer(text)) + if len(blocks) != 1: + raise ContractError("contract must contain exactly one machine scope block") + return blocks[0].group("body").encode("utf-8") + + +def _human_phase_risk(raw: bytes) -> tuple[str, str]: + text = _decode_utf8(raw) + phase = re.search(r"^## Start phase\n\n`([^`]+)`[ \t]*$", text, re.M) + risk = re.search(r"^## Risk class\n\n([^\n]+)$", text, re.M) + if not phase or not risk: + raise ContractError("signed contract lacks phase/risk sections") + return phase.group(1), risk.group(1).strip(" `") + + +def select_contract( + repo: Path, base: str, head: str, state_ref: str +) -> tuple[ScopeContract, SignedStart]: + intent = added_merge_intent(repo, base, head) + verify_state_ref(repo, state_ref) + records = authenticated_ledger(repo, state_ref) + start = latest_signed_start(records, intent["chunk_id"]) + if start.initiative_id != intent["initiative_id"]: + raise ContractError("signed start initiative disagrees with merge intent") + active = reduce_active(records) + if active.get(start.initiative_id) != (start.phase, start.chunk_id): + raise ContractError("authenticated ledger is not active for target chunk") + # Signed projection is a consistency cross-check, never event authority. + require_active_projection(repo, state_ref, start) + signed_raw = signed_contract_blob(repo, start, base) + try: + head_raw = _git(repo, "show", f"{head}:{start.contract_path}") + except ContractError as exc: + raise ContractError("head does not preserve signed contract path") from exc + cutover_path = ".agent-loop/merge-intents/WS-ENG-008-01.json" + cutover_present = subprocess.run( + ["git", "cat-file", "-e", f"{base}:{cutover_path}"], cwd=repo, + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, + ).returncode == 0 + if machine_block_or_none(signed_raw) is not None: + parsed = parse_contract_bytes(signed_raw) + if machine_block(head_raw) != machine_block(signed_raw): + raise ContractError("head changes the signed machine scope block") + if parsed.chunk_id != start.chunk_id or parsed.phase != start.phase: + raise ContractError("machine identity/phase disagrees with signed start") + return parsed, start + if start.chunk_id == "WS-ENG-008-01" and not cutover_present: + parsed = parse_contract_bytes(head_raw) + old_phase, old_risk = _human_phase_risk(signed_raw) + signed_text = _decode_utf8(signed_raw) + old_allowed = _text_block_items(signed_text, "Allowed files") + old_reviewers = tuple(re.findall( + r"^- \[[ xX]\] (.+)$", + _section(signed_text, "Required reviewers"), re.M, + )) + if (parsed.chunk_id != start.chunk_id or parsed.phase != start.phase + or parsed.phase != old_phase or parsed.risk_class != old_risk + or set(parsed.allowed_paths) != set(old_allowed) + or {item.casefold() for item in parsed.required_reviewers} + != {item.casefold() for item in old_reviewers}): + raise ContractError("bootstrap machine scope disagrees with signed human contract") + return parsed, start + if not cutover_present: + raise ContractError("pre-cutover non-bootstrap contract has no machine scope") + require_grandfather(records, start) + return legacy_scope_from_signed_blob(signed_raw, start), start + + +def machine_block_or_none(raw: bytes) -> bytes | None: + text = _decode_utf8(raw) + blocks = list(FENCE_RE.finditer(text)) + if len(blocks) > 1: + raise ContractError("duplicate machine scope blocks") + return blocks[0].group("body").encode() if blocks else None + + +def legacy_scope_from_signed_blob(raw: bytes, start: SignedStart) -> ScopeContract: + """Derive the only grandfather scope from the exact authenticated blob.""" + text = _decode_utf8(raw) + allowed = tuple( + validate_pattern(path) for path in _text_block_items(text, "Allowed files") + ) + phase, risk = _human_phase_risk(raw) + if phase != start.phase: + raise ContractError("grandfather human phase disagrees with signed start") + return ScopeContract(start.chunk_id, phase, risk, allowed, (), (), ()) + + +def require_grandfather(records: Sequence[dict[str, Any]], start: SignedStart) -> None: + cutover_index = None + for index, record in enumerate(records): + completed = record.get("completed_chunk") + if (record.get("event") is None and type(completed) is dict + and completed.get("chunk_id") == "WS-ENG-008-01"): + if cutover_index is not None: + raise ContractError("authenticated ledger contains ambiguous cutover records") + cutover_index = index + if cutover_index is None: + raise ContractError("cannot locate exact cutover ledger record") + active_at_cutover = reduce_active(records[:cutover_index + 1]) + if active_at_cutover.get(start.initiative_id) != (start.phase, start.chunk_id): + raise ContractError("target was not signed-active at exact cutover") + if start.ledger_index > cutover_index: + raise ContractError("target start occurred or restarted after cutover") + + +def discover_changes(repo: Path, base: str, head: str) -> tuple[bytes, ...]: + for ref in (base, head): + _git(repo, "rev-parse", "--verify", f"{ref}^{{commit}}") + merge_base = _git(repo, "merge-base", base, head).strip() + if not merge_base: + raise ContractError("base and head have no merge base") + outputs = [ + _git(repo, "diff", "--name-status", "-z", "--find-renames", "--find-copies-harder", f"{base}...{head}"), + _git(repo, "diff", "--name-status", "-z", "--find-renames", "--find-copies-harder", "--cached"), + _git(repo, "diff", "--name-status", "-z", "--find-renames", "--find-copies-harder"), + ] + raw_outputs = [ + _git(repo, "diff", "--raw", "-z", "--no-abbrev", "--find-renames", "--find-copies-harder", f"{base}...{head}"), + _git(repo, "diff", "--raw", "-z", "--no-abbrev", "--find-renames", "--find-copies-harder", "--cached"), + _git(repo, "diff", "--raw", "-z", "--no-abbrev", "--find-renames", "--find-copies-harder"), + ] + for raw_output in raw_outputs: + validate_raw_modes_z(raw_output) + statuses = [entry for output in outputs for entry in parse_name_status_z(output)] + untracked = [p for p in _git(repo, "ls-files", "--others", "--exclude-standard", "-z").split(b"\0") if p] + validate_untracked_files(repo, untracked) + head_tree_paths = tree_paths_z(_git(repo, "ls-tree", "-rz", "--full-tree", head)) + validate_path_bytes(head_tree_paths) + paths = [path for _status, names in statuses for path in names] + untracked + # Modes are part of authorization: only ordinary non-executable blobs are accepted. + index = _git(repo, "ls-files", "--stage", "-z").split(b"\0") + mode_by_path: dict[bytes, bytes] = {} + for record in index: + if not record: + continue + metadata, sep, name = record.partition(b"\t") + if not sep: + raise ContractError("malformed Git index record") + mode_by_path[name] = metadata.split(b" ", 1)[0] + validate_path_bytes((*mode_by_path, *untracked)) + for path in paths: + mode = mode_by_path.get(path) + if mode is not None and mode != b"100644": + labels = {b"100755": "executable", b"120000": "symlink", b"160000": "gitlink"} + raise ContractError(f"changed path has forbidden {labels.get(mode, 'type')} mode") + # Both sides of rename/copy are checked; type-change and unmerged/unknown states fail. + if any(status[0] in "TUXB" for status, _ in statuses): + raise ContractError("type-changed, unmerged, unknown, or broken Git status") + return tuple(paths) + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--contract", type=Path) + parser.add_argument("--repo", type=Path, default=Path.cwd()) + parser.add_argument("--base-ref", default="origin/main") + parser.add_argument("--head-ref", default="HEAD") + parser.add_argument("--state-ref", default="origin/automation/loop-memory") + parser.add_argument("--chunk-id") + parser.add_argument("--phase", choices=sorted(PHASES)) + args = parser.parse_args(argv) + try: + if args.contract is None: + contract, _start = select_contract( + args.repo, args.base_ref, args.head_ref, args.state_ref + ) + else: + contract = parse_contract_bytes(args.contract.read_bytes()) + if args.chunk_id and args.chunk_id != contract.chunk_id: + raise ContractError("signed-start chunk identity disagrees with contract") + if args.phase and args.phase != contract.phase: + raise ContractError("signed-start phase disagrees with contract") + enforce_scope(contract, discover_changes(args.repo, args.base_ref, args.head_ref)) + except (ContractError, OSError) as exc: + print(f"chunk contract check failed: {exc}", file=sys.stderr) + return 1 + print(f"chunk contract check passed: {contract.chunk_id}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_internal_review_evidence.py b/scripts/check_internal_review_evidence.py index 0a2b37051..f408ad915 100644 --- a/scripts/check_internal_review_evidence.py +++ b/scripts/check_internal_review_evidence.py @@ -3,13 +3,28 @@ from __future__ import annotations import os +import json import re import subprocess import sys from datetime import datetime from pathlib import Path -ROOT = Path(__file__).resolve().parents[1] +from check_chunk_contract import ( + VERIFICATION_COMMANDS, + ContractError, + ScopeContract, + parse_contract_bytes, +) + +_configured_root = os.environ.get("INTERNAL_REVIEW_REPOSITORY_ROOT", "").strip() +if _configured_root and not Path(_configured_root).is_absolute(): + raise RuntimeError("INTERNAL_REVIEW_REPOSITORY_ROOT must be absolute") +ROOT = ( + Path(_configured_root).resolve() + if _configured_root + else Path(__file__).resolve().parents[1] +) ALLOWED_POST_REVIEW_PREFIXES = ( ".agent-loop/initiatives/", @@ -479,6 +494,84 @@ def evidence_chunk_ids(text: str) -> set[str]: } +def active_merge_intent_chunk(paths: list[str]) -> str | None: + """Return the one chunk identity carried by this PR's added merge intent.""" + candidates: list[str] = [] + for path in paths: + if not path.startswith(".agent-loop/merge-intents/") or not path.endswith( + ".json" + ): + continue + intent_path = ROOT / path + if not intent_path.is_file() or intent_path.is_symlink(): + continue + try: + data = json.loads(intent_path.read_text(encoding="utf-8")) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: + raise RuntimeError(f"cannot read changed merge intent {path}") from exc + chunk_id = data.get("chunk_id") if isinstance(data, dict) else None + if not isinstance(chunk_id, str): + raise RuntimeError(f"changed merge intent has no chunk_id: {path}") + candidates.append(chunk_id) + if not candidates: + return None + if len(candidates) != 1: + raise RuntimeError("exactly one changed merge intent is required") + return candidates[0] + + +def machine_contract_for(chunk_id: str) -> ScopeContract | None: + """Load the current chunk's schema-v1 contract, if it has crossed cutover.""" + matches: list[Path] = [] + for path in (ROOT / ".agent-loop/initiatives").glob("*/chunks/*.md"): + try: + first_line = path.read_text(encoding="utf-8").splitlines()[0] + except (OSError, UnicodeDecodeError, IndexError) as exc: + raise RuntimeError(f"cannot read chunk contract {path}") from exc + if chunk_id_from_heading(first_line) == chunk_id.lower(): + matches.append(path) + if len(matches) != 1: + raise RuntimeError( + f"expected one contract for {chunk_id}, found {len(matches)}" + ) + raw = matches[0].read_bytes() + if b"```chunk-scope-json" not in raw: + return None + try: + return parse_contract_bytes(raw) + except ContractError as exc: + raise RuntimeError(f"invalid machine scope for {chunk_id}: {exc}") from exc + + +def validate_machine_evidence( + text: str, contract: ScopeContract, required_tracks: tuple[str, ...] +) -> list[str]: + """Bind reviewer and verification evidence to the machine contract.""" + failures: list[str] = [] + if set(contract.required_reviewers) != set(required_tracks): + failures.append("machine required_reviewers disagree with evidence routing") + commands_section = re.search( + r"^## commands run\n(?P.*?)(?=^## |\Z)", text, re.MULTILINE | re.DOTALL + ) + command_block = ( + re.search(r"```bash\n(?P.*?)```", commands_section.group("body"), re.DOTALL) + if commands_section + else None + ) + observed = { + line.strip() + for line in command_block.group("body").splitlines() + if line.strip() + } if command_block else set() + expected = { + VERIFICATION_COMMANDS[identifier].lower() + for identifier in contract.verification_commands + } + if not expected.issubset(observed): + failures.append("commands run do not prove every machine verification id") + return failures + + def main() -> int: """Check that changed engineering files include complete review evidence.""" try: @@ -513,6 +606,10 @@ def main() -> int: failures: list[str] = [] try: chunk_ids = required_chunk_ids(changed) + intent_chunk = active_merge_intent_chunk(changed) + machine_contract = ( + machine_contract_for(intent_chunk) if intent_chunk is not None else None + ) except RuntimeError as exc: print(f"Internal review evidence gate failed closed: {exc}", file=sys.stderr) return 1 @@ -524,6 +621,14 @@ def main() -> int: continue try: missing = validate_evidence(path, required_tracks, chunk_ids) + if machine_contract is not None: + missing.extend( + validate_machine_evidence( + path.read_text(encoding="utf-8").lower(), + machine_contract, + required_tracks, + ) + ) except (OSError, UnicodeDecodeError) as exc: failures.append( f"{path}: unreadable evidence file ({exc.__class__.__name__})" diff --git a/scripts/test_agent_gates.py b/scripts/test_agent_gates.py index efa7a37d1..cf109a65e 100644 --- a/scripts/test_agent_gates.py +++ b/scripts/test_agent_gates.py @@ -21,6 +21,7 @@ import textwrap from pathlib import Path from types import SimpleNamespace +from unittest import mock import yaml @@ -320,6 +321,61 @@ def test_backend_config_paths_require_review_evidence() -> None: assert "ci integrity" in backend_config_tracks +def test_materialized_evidence_gate_uses_explicit_repository_root() -> None: + """A trusted copied checker must still inspect the candidate workspace.""" + with tempfile.TemporaryDirectory() as directory: + configured = Path(directory).resolve() + with mock.patch.dict( + os.environ, + {"INTERNAL_REVIEW_REPOSITORY_ROOT": str(configured)}, + ): + gate = load_module( + "review_gate_explicit_repository_root", + "scripts/check_internal_review_evidence.py", + ) + assert gate.ROOT == configured + + +def test_machine_scope_binds_reviewer_routing_and_verification_evidence() -> None: + """Machine metadata cannot drift from internal reviewer proof.""" + gate = load_module( + "review_gate_machine_scope", "scripts/check_internal_review_evidence.py" + ) + tracks = ( + "senior engineering", + "qa/test", + "security/auth", + "product/ops", + ) + contract = gate.ScopeContract( + "WS-ENG-008-01", + "implementation", + "L1", + ("scripts/check_chunk_contract.py",), + ("backend/**",), + tracks, + ("chunk-scope-tests", "git-diff-check"), + ) + valid = """## commands run + +```bash +python3 scripts/test_check_chunk_contract.py +git diff --check origin/main...head +``` +""" + assert gate.validate_machine_evidence(valid, contract, tracks) == [] + assert "commands run do not prove every machine verification id" in ( + gate.validate_machine_evidence( + "## commands run\n\n```bash\npython3 scripts/test_check_chunk_contract.py\n```\n", + contract, + tracks, + ) + ) + assert "machine required_reviewers disagree with evidence routing" in ( + gate.validate_machine_evidence(valid, contract, tracks[:-1]) + ) + + def test_review_evidence_files_are_not_relevant_changes() -> None: """Review evidence files satisfy the gate without requiring more evidence.""" gate = load_module( @@ -6456,6 +6512,72 @@ def test_agent_gates_runs_stale_artifact_contracts_fail_closed() -> None: assert all(step.get("if") not in {False, "false", "${{ false }}"} for step in steps) +def test_machine_chunk_scope_gate_is_mandatory_and_signed_state_bound() -> None: + """Agent Gates must enforce exact PR scope from authenticated loop memory.""" + workflow = (ROOT / ".github/workflows/agent-gates.yml").read_text( + encoding="utf-8" + ) + state_refspec = ( + "+refs/heads/automation/loop-memory:" + "refs/remotes/origin/automation/loop-memory" + ) + assert workflow.count(state_refspec) == 1 + for script in ( + "check_chunk_contract.py", + "update_post_merge_memory.py", + "check_loop_memory_state.py", + ): + assert script in workflow + assert 'git show "origin/${BASE_REF}:scripts/${script}"' in workflow + assert 'python3 "${trusted_checker}"' in workflow + assert 'PYTHONPATH="${trusted_scope_dir}"' in workflow + assert ( + 'if git cat-file -e "origin/${BASE_REF}:' + '.agent-loop/merge-intents/WS-ENG-008-01.json"' + ) in workflow + assert workflow.count("refusing local-script bootstrap") == 1 + assert workflow.count("refusing local evidence bootstrap") == 1 + assert workflow.count("exit 1") >= 2 + assert 'cp "scripts/${script}" "${trusted_scope_dir}/${script}"' in workflow + assert ( + 'git show "origin/${BASE_REF}:scripts/${script}" > ' + '"${trusted_evidence_dir}/${script}"' + ) in workflow + assert '"${trusted_evidence_dir}/check_internal_review_evidence.py"' in workflow + assert 'PYTHONPATH="${trusted_evidence_dir}"' in workflow + assert "INTERNAL_REVIEW_REPOSITORY_ROOT: ${{ github.workspace }}" in workflow + assert '--base-ref "origin/${BASE_REF}" --head-ref HEAD' in workflow + assert "--state-ref origin/automation/loop-memory" in workflow + assert "continue-on-error" not in workflow + assert workflow.index("Fetch signed loop-memory state") < workflow.index( + "Machine-checkable chunk scope" + ) + assert workflow.index("Machine-checkable chunk scope") < workflow.index( + "Internal review evidence gate" + ) + + +def test_chunk_contract_template_exposes_all_machine_scope_choices() -> None: + """The canonical template cannot silently under-declare scope metadata.""" + template = (ROOT / ".agent-loop/templates/CHUNK_CONTRACT.md").read_text( + encoding="utf-8" + ) + assert '"phase": ""' in template + assert '"risk_class": ""' in template + for reviewer in ( + "senior engineering", + "qa/test", + "security/auth", + "product/ops", + "architecture", + "ci integrity", + "docs", + "reuse/dedup", + "test delta", + ): + assert template.count(f'"{reviewer}"') == 1 + + def test_agent_gate_dependencies_and_workflow_are_pinned() -> None: """The YAML parser dependency and its installation remain deterministic.""" workflow = yaml.safe_load( @@ -7619,6 +7741,8 @@ def main() -> int: tests = [ test_required_tracks_expand_for_loop_and_ci_paths, test_backend_config_paths_require_review_evidence, + test_materialized_evidence_gate_uses_explicit_repository_root, + test_machine_scope_binds_reviewer_routing_and_verification_evidence, test_review_evidence_files_are_not_relevant_changes, test_evidence_requires_completed_yes_statements, test_evidence_must_reference_changed_chunk, @@ -7704,6 +7828,8 @@ def main() -> int: test_stale_review_contracts_run_fail_closed_in_agent_gates, test_agent_gates_runs_stale_authorization_docs_fail_closed, test_agent_gates_runs_stale_artifact_contracts_fail_closed, + test_machine_chunk_scope_gate_is_mandatory_and_signed_state_bound, + test_chunk_contract_template_exposes_all_machine_scope_choices, test_agent_gate_dependencies_and_workflow_are_pinned, test_local_minio_compose_is_regression_protected, test_backend_coverage_thresholds_are_regression_protected, diff --git a/scripts/test_check_chunk_contract.py b/scripts/test_check_chunk_contract.py new file mode 100644 index 000000000..32ff35f57 --- /dev/null +++ b/scripts/test_check_chunk_contract.py @@ -0,0 +1,535 @@ +#!/usr/bin/env python3 +"""Dedicated mutation tests for check_chunk_contract.py.""" + +from __future__ import annotations + +import json +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from unittest import mock + +sys.path.insert(0, str(Path(__file__).parent)) +import check_chunk_contract as checker + + +ALLOWED = ("scripts/check_chunk_contract.py", "docs/**") +REVIEWERS = tuple(sorted(checker.REVIEWERS)) + + +def contract(**updates: object) -> bytes: + data: dict[str, object] = { + "schema_version": 1, + "chunk_id": "WS-ENG-008-01", + "phase": "implementation", + "risk_class": "L1", + "allowed_paths": list(ALLOWED), + "forbidden_paths": ["docs/private/**"], + "required_reviewers": list(REVIEWERS), + "verification_commands": ["chunk-scope-tests", "git-diff-check"], + } + data.update(updates) + reviewers = "\n".join(f"- [ ] {name}" for name in data.get("required_reviewers", [])) + return f"""# Chunk Contract: WS-ENG-008-01 — Test + +## Risk class + +L1 + +## Start phase + +`implementation` + +## Allowed files + +```text +scripts/check_chunk_contract.py +docs/** +``` + +## Not allowed + +Explicit forbidden scope remains human-reviewed. + +## Machine scope + +```chunk-scope-json +{json.dumps(data, ensure_ascii=False)} +``` + +## Verification commands + +```bash +python3 scripts/test_check_chunk_contract.py +git diff --check origin/main...HEAD +``` + +## Required reviewers + +{reviewers} +""".encode() + + +class ContractSchemaTests(unittest.TestCase): + def test_verification_identifier_registry_is_closed(self) -> None: + self.assertEqual(checker.VERIFICATION_COMMAND_IDS, frozenset({ + "chunk-scope-tests", "agent-gate-tests", "internal-review-evidence", + "markdown-links", "stale-wording", "git-diff-check", + "loop-memory-drift-tests", "loop-memory-property-tests", + "authorization-property-tests", "authorization-property-lint", + "mutation-policy-tests", "mutation-policy-lint", + "review-log-archive-tests", "review-log-archive-check", + "loop-memory-state", "stale-artifact-contracts", + })) + + def test_positive_schema_identity_reviewer_and_command(self) -> None: + parsed = checker.parse_contract_bytes(contract()) + self.assertEqual(parsed.chunk_id, "WS-ENG-008-01") + self.assertEqual(parsed.phase, "implementation") + + def test_human_reviewer_labels_compare_case_insensitively(self) -> None: + raw = contract().replace(b"- [ ] qa/test", b"- [ ] QA/test").replace( + b"- [ ] ci integrity", b"- [ ] CI integrity" + ) + self.assertEqual(checker.parse_contract_bytes(raw).required_reviewers, REVIEWERS) + + def test_human_verification_command_disagreement_is_rejected(self) -> None: + raw = contract().replace( + b"git diff --check origin/main...HEAD\n```", + b"git diff --stat origin/main...HEAD\n```", + ) + with self.assertRaisesRegex(checker.ContractError, "verification identifiers"): + checker.parse_contract_bytes(raw) + + def test_negative_schema_mutations(self) -> None: + mutations = [ + contract(schema_version="1"), contract(extra=True), + contract(allowed_paths=list(ALLOWED) * 2), + contract(required_reviewers=[*REVIEWERS, "mystery"]), + contract(verification_commands=["rm -rf ."]), + ] + duplicate = contract().replace(b'"schema_version": 1', b'"schema_version": 1, "schema_version": 1') + mutations.append(duplicate) + for item in mutations: + with self.subTest(item=item[-100:]), self.assertRaises(checker.ContractError): + checker.parse_contract_bytes(item) + + def test_negative_identity_phase_and_human_agreement(self) -> None: + mutations = [ + contract(chunk_id="WS-ENG-008-02"), + contract(phase="specification"), + contract().replace(b"docs/**\n```", b"other/**\n```", 1), + ] + for item in mutations: + with self.assertRaises(checker.ContractError): + checker.parse_contract_bytes(item) + + def test_negative_unicode_size_and_duplicate_block(self) -> None: + with self.assertRaises(checker.ContractError): + checker.parse_contract_bytes(b"\xff") + with self.assertRaisesRegex(checker.ContractError, "not valid UTF-8"): + checker.machine_block_or_none(b"\xff") + with self.assertRaisesRegex(checker.ContractError, "not valid UTF-8"): + checker._human_phase_risk(b"\xff") + with self.assertRaises(checker.ContractError): + checker.parse_contract_bytes(contract() + b"x" * checker.MAX_CONTRACT_BYTES) + block = b"```chunk-scope-json\n{}\n```\n" + with self.assertRaises(checker.ContractError): + checker.parse_contract_bytes(contract() + block) + + def test_signed_json_decoders_normalize_invalid_utf8(self) -> None: + with mock.patch.object(checker, "_git", return_value=b"\xff"): + with self.assertRaisesRegex(checker.ContractError, "not valid UTF-8"): + checker._git_json(Path("."), "state:.agent-loop/STATE.json") + with self.assertRaisesRegex(checker.ContractError, "not valid UTF-8"): + checker.authenticated_ledger(Path("."), "state") + + +class PathAndStatusTests(unittest.TestCase): + def test_positive_closed_recursive_path(self) -> None: + parsed = checker.parse_contract_bytes(contract()) + self.assertEqual(checker.enforce_scope(parsed, [b"docs/a/b.md"]), ("docs/a/b.md",)) + + def test_negative_path_grammar_mutations(self) -> None: + for path in ("/abs", "../escape", "a//b", "a\\b", "a/*", "a/./b", "a/{b,c}"): + with self.subTest(path=path), self.assertRaises(checker.ContractError): + checker.validate_pattern(path) + + def test_negative_forbidden_precedes_allowed(self) -> None: + parsed = checker.parse_contract_bytes(contract()) + with self.assertRaisesRegex(checker.ContractError, "forbidden"): + checker.enforce_scope(parsed, [b"docs/private/key.md"]) + + def test_negative_foreign_and_colliding_paths(self) -> None: + parsed = checker.parse_contract_bytes(contract()) + cases = [ + [b"foreign.txt"], [b"docs/A", b"docs/a"], + ["docs/cafe\u0301".encode()], [b"docs/tab\tname"], + [b"docs/new\nname"], [b"docs/\xff"], + ] + for paths in cases: + with self.subTest(paths=paths), self.assertRaises(checker.ContractError): + checker.enforce_scope(parsed, paths) + + def test_positive_and_negative_nul_statuses(self) -> None: + self.assertEqual( + checker.parse_name_status_z(b"M\0a\0R100\0old\0new\0C90\0x\0y\0"), + [("M", (b"a",)), ("R100", (b"old", b"new")), ("C90", (b"x", b"y"))], + ) + for raw in (b"R100\0old\0", b"Q\0a\0", b"M\0a"): + with self.subTest(raw=raw), self.assertRaises(checker.ContractError): + checker.parse_name_status_z(raw) + + def test_raw_diff_mode_mutations(self) -> None: + sha = b"a" * 40 + checker.validate_raw_modes_z(b":100644 100644 " + sha + b" " + sha + b" M\0a\0") + for mode in (b"100755", b"120000", b"160000"): + raw = b":" + mode + b" 000000 " + sha + b" " + (b"0" * 40) + b" D\0a\0" + with self.subTest(mode=mode), self.assertRaises(checker.ContractError): + checker.validate_raw_modes_z(raw) + + +class SignedHistorySelectionTests(unittest.TestCase): + def git(self, repo: Path, *args: str, input: bytes | None = None) -> bytes: + return subprocess.run( + ["git", *args], cwd=repo, input=input, check=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ).stdout.strip() + + def test_selects_exact_active_signed_start_and_blob(self) -> None: + with tempfile.TemporaryDirectory() as directory: + repo = Path(directory) + self.git(repo, "init", "-q", "-b", "main") + self.git(repo, "config", "user.email", "test@example.invalid") + self.git(repo, "config", "user.name", "Test") + path = Path(".agent-loop/initiatives/WS-ENG-008-x/chunks/WS-ENG-008-01-test.md") + (repo / path).parent.mkdir(parents=True) + (repo / path).write_bytes(contract()) + key = repo / ".agent-loop/keys/loop-memory-signing-public.pem" + key.parent.mkdir(parents=True, exist_ok=True) + key.write_text("not-a-real-public-key") + self.git(repo, "add", ".") + self.git(repo, "commit", "-qm", "base") + base = self.git(repo, "rev-parse", "HEAD").decode() + blob = self.git(repo, "rev-parse", f"HEAD:{path}").decode() + self.git(repo, "checkout", "--orphan", "automation") + self.git(repo, "rm", "-qrf", ".") + state_path = repo / ".agent-loop" + (state_path / "INITIATIVE_STATE").mkdir(parents=True) + state = { + "event": { + "type": "start", "initiative_id": "WS-ENG-008", + "chunk_id": "WS-ENG-008-01", "main_sha": base, + "selection": {"phase": "implementation", "contract_path": str(path), + "contract_blob_sha": blob}, + } + } + (state_path / "STATE.json").write_text(json.dumps(state)) + (state_path / "STATE.sig").write_text("fixture-signature") + (state_path / "MERGE_LOG.jsonl").write_text(json.dumps({ + "schema_version": 2, "previous_entry_hash": None, + "record": state, "entry_hash": "0" * 64, + }) + "\n") + (state_path / "INITIATIVE_STATE/WS-ENG-008.md").write_text( + "- Active planning chunk: `none`\n" + "- Active implementation chunk: `WS-ENG-008-01`\n" + ) + self.git(repo, "add", ".") + self.git(repo, "commit", "-qm", "signed start") + self.git(repo, "checkout", "-q", "main") + intent_path = repo / ".agent-loop/merge-intents/WS-ENG-008-01.json" + intent_path.parent.mkdir(parents=True) + intent_path.write_text(json.dumps({ + "schema_version": 2, "initiative_id": "WS-ENG-008", + "chunk_id": "WS-ENG-008-01", "chunk_title": "Test", + "next_chunk_id": None, "next_chunk_title": None, + "next_requires_explicit_start": True, + })) + self.git(repo, "add", ".") + self.git(repo, "commit", "-qm", "head") + with ( + mock.patch.object(checker, "verify_state_ref"), + mock.patch.object(checker, "_git", wraps=checker._git) as git_spy, + ): + parsed, start = checker.select_contract(repo, base, "HEAD", "automation") + history_calls = [ + call for call in git_spy.call_args_list + if "rev-list" in call.args or any(":.agent-loop/STATE.json" in str(arg) for arg in call.args) + ] + self.assertEqual(history_calls, [], "parent STATE history must never authorize selection") + self.assertEqual(parsed and parsed.chunk_id, "WS-ENG-008-01") + self.assertEqual(start.contract_blob_sha, blob) + with self.assertRaisesRegex(checker.ContractError, "authentication failed"): + checker.select_contract(repo, base, "HEAD", "automation") + self.git(repo, "checkout", "-q", "automation") + cancel_record = { + "event": {"type": "cancel", "initiative_id": "WS-ENG-008", + "chunk_id": "WS-ENG-008-01"} + } + with (repo / ".agent-loop/MERGE_LOG.jsonl").open("a") as ledger: + ledger.write(json.dumps({ + "schema_version": 2, "previous_entry_hash": "0" * 64, + "record": cancel_record, "entry_hash": "1" * 64, + }) + "\n") + (repo / ".agent-loop/INITIATIVE_STATE/WS-ENG-008.md").write_text( + "- Active planning chunk: `none`\n- Active implementation chunk: `none`\n" + ) + self.git(repo, "add", ".") + self.git(repo, "commit", "-qm", "cancel") + self.git(repo, "checkout", "-q", "main") + with ( + mock.patch.object(checker, "verify_state_ref"), + self.assertRaisesRegex(checker.ContractError, "not a start"), + ): + checker.select_contract(repo, base, "HEAD", "automation") + + def test_rejects_stopped_projection_and_blob_mutation(self) -> None: + # Focused helper mutations prove both state and tree bindings fail closed. + with tempfile.TemporaryDirectory() as directory: + repo = Path(directory) + self.git(repo, "init", "-q") + self.git(repo, "config", "user.email", "test@example.invalid") + self.git(repo, "config", "user.name", "Test") + (repo / "contract.md").write_text("signed") + self.git(repo, "add", ".") + self.git(repo, "commit", "-qm", "base") + sha = self.git(repo, "rev-parse", "HEAD").decode() + start = checker.SignedStart( + 0, "WS-ENG-008", "WS-ENG-008-01", "implementation", sha, + "contract.md", "0" * 40, + ) + with self.assertRaisesRegex(checker.ContractError, "path/blob"): + checker.signed_contract_blob(repo, start, sha) + + def test_grandfather_requires_active_exact_cutover_parent_and_prestart(self) -> None: + with tempfile.TemporaryDirectory() as directory: + repo = Path(directory) + self.git(repo, "init", "-q", "-b", "automation") + self.git(repo, "config", "user.email", "test@example.invalid") + self.git(repo, "config", "user.name", "Test") + projection = repo / ".agent-loop/INITIATIVE_STATE/WS-OLD-001.md" + projection.parent.mkdir(parents=True) + projection.write_text("- Active implementation chunk: `WS-OLD-001-01`\n") + self.git(repo, "add", ".") + self.git(repo, "commit", "-qm", "pre-cutover active") + (repo / ".agent-loop/STATE.json").write_text(json.dumps({ + "completed_chunk": {"chunk_id": "WS-ENG-008-01"} + })) + self.git(repo, "add", ".") + self.git(repo, "commit", "-qm", "cutover") + (repo / ".agent-loop/STATE.json").write_text(json.dumps({ + "event": {"type": "start", "chunk_id": "WS-OTHER-001-01"} + })) + self.git(repo, "add", ".") + self.git(repo, "commit", "-qm", "later") + records = ( + {"event": {"type": "start", "initiative_id": "WS-OLD-001", + "chunk_id": "WS-OLD-001-01", "selection": {"phase": "implementation"}}}, + {"completed_chunk": {"chunk_id": "WS-ENG-008-01", "initiative_id": "WS-ENG-008"}}, + {"event": {"type": "start", "initiative_id": "WS-OTHER-001", + "chunk_id": "WS-OTHER-001-01", "selection": {"phase": "implementation"}}}, + ) + valid = checker.SignedStart( + 0, "WS-OLD-001", "WS-OLD-001-01", "implementation", + "a" * 40, "legacy.md", "b" * 40, + ) + checker.require_grandfather(records, valid) + restarted = checker.SignedStart( + 2, "WS-OLD-001", "WS-OLD-001-01", "implementation", + "a" * 40, "legacy.md", "b" * 40, + ) + with self.assertRaisesRegex(checker.ContractError, "after cutover"): + checker.require_grandfather(records, restarted) + + def test_select_rejects_stopped_and_post_cutover_no_schema_starts(self) -> None: + start_record = { + "event": { + "type": "start", "initiative_id": "WS-OLD-001", + "chunk_id": "WS-OLD-001-01", "main_sha": "a" * 40, + "selection": {"phase": "implementation", "contract_path": "legacy.md", + "contract_blob_sha": "b" * 40}, + } + } + cutover = { + "completed_chunk": {"initiative_id": "WS-ENG-008", "chunk_id": "WS-ENG-008-01"}, + "event": None, + } + stopped = { + "completed_chunk": {"initiative_id": "WS-OLD-001", "chunk_id": "WS-OLD-001-01"}, + "event": None, + } + common = ( + mock.patch.object(checker, "added_merge_intent", return_value={ + "path": ".agent-loop/merge-intents/WS-OLD-001-01.json", + "initiative_id": "WS-OLD-001", "chunk_id": "WS-OLD-001-01", + }), + mock.patch.object(checker, "verify_state_ref"), + ) + with common[0], common[1], mock.patch.object( + checker, "authenticated_ledger", return_value=(start_record, stopped) + ), self.assertRaisesRegex(checker.ContractError, "not active"): + checker.select_contract(Path("."), "base", "head", "state") + with ( + mock.patch.object(checker, "added_merge_intent", return_value={ + "path": ".agent-loop/merge-intents/WS-OLD-001-01.json", + "initiative_id": "WS-OLD-001", "chunk_id": "WS-OLD-001-01", + }), + mock.patch.object(checker, "verify_state_ref"), + mock.patch.object(checker, "authenticated_ledger", return_value=(cutover, start_record)), + mock.patch.object(checker, "require_active_projection"), + mock.patch.object(checker, "signed_contract_blob", return_value=b"legacy contract"), + mock.patch.object(checker, "_git", return_value=b"legacy contract"), + mock.patch.object(subprocess, "run", return_value=mock.Mock(returncode=0)), + self.assertRaisesRegex(checker.ContractError, "not signed-active at exact cutover"), + ): + checker.select_contract(Path("."), "base", "head", "state") + + +class GitDiscoveryIntegrationTests(unittest.TestCase): + def git(self, repo: Path, *args: str) -> bytes: + return subprocess.run( + ["git", *args], cwd=repo, check=True, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, + ).stdout.strip() + + def repo(self, directory: str) -> tuple[Path, str]: + repo = Path(directory) + self.git(repo, "init", "-q", "-b", "main") + self.git(repo, "config", "user.email", "test@example.invalid") + self.git(repo, "config", "user.name", "Test") + (repo / "README.md").write_text("tracked\n") + self.git(repo, "add", ".") + self.git(repo, "commit", "-qm", "base") + return repo, self.git(repo, "rev-parse", "HEAD").decode() + + def scope(self, *allowed: str) -> checker.ScopeContract: + return checker.ScopeContract( + "WS-ENG-008-01", "implementation", "L1", tuple(allowed), (), (), (), + ) + + def test_untracked_symlink_is_rejected_end_to_end(self) -> None: + with tempfile.TemporaryDirectory() as directory: + repo, base = self.repo(directory) + (repo / "allowed-link").symlink_to("README.md") + with self.assertRaisesRegex(checker.ContractError, "not a regular file"): + checker.discover_changes(repo, base, "HEAD") + + def test_untracked_executable_is_rejected_end_to_end(self) -> None: + with tempfile.TemporaryDirectory() as directory: + repo, base = self.repo(directory) + executable = repo / "allowed-tool.py" + executable.write_text("print('ok')\n") + executable.chmod(0o755) + with self.assertRaisesRegex(checker.ContractError, "executable"): + checker.discover_changes(repo, base, "HEAD") + + def test_untracked_case_collision_with_unchanged_head_is_rejected(self) -> None: + with tempfile.TemporaryDirectory() as directory: + repo, base = self.repo(directory) + (repo / "readme.md").write_text("collision\n") + with self.assertRaisesRegex(checker.ContractError, "collide"): + checker.discover_changes(repo, base, "HEAD") + + def test_staged_dirty_and_untracked_paths_are_aggregated(self) -> None: + with tempfile.TemporaryDirectory() as directory: + repo, base = self.repo(directory) + (repo / "staged.txt").write_text("staged\n") + self.git(repo, "add", "staged.txt") + (repo / "README.md").write_text("dirty\n") + (repo / "untracked.txt").write_text("untracked\n") + changed = set(checker.discover_changes(repo, base, "HEAD")) + self.assertTrue({b"staged.txt", b"README.md", b"untracked.txt"} <= changed) + + def test_grandfather_scope_rejects_foreign_path(self) -> None: + raw = b"""# Chunk Contract: WS-OLD-001-01 -- Legacy + +## Risk class + +L1 + +## Start phase + +`implementation` + +## Allowed files + +```text +allowed.txt +``` +""" + start = checker.SignedStart( + 0, "WS-OLD-001", "WS-OLD-001-01", "implementation", + "a" * 40, "legacy.md", "b" * 40, + ) + scope = checker.legacy_scope_from_signed_blob(raw, start) + with self.assertRaisesRegex(checker.ContractError, "outside allowed scope"): + checker.enforce_scope(scope, [b"foreign.txt"]) + + def test_rename_checks_source_and_destination_scope(self) -> None: + with tempfile.TemporaryDirectory() as directory: + repo, base = self.repo(directory) + self.git(repo, "mv", "README.md", "allowed.md") + with self.assertRaisesRegex(checker.ContractError, "outside allowed scope"): + checker.enforce_scope( + self.scope("allowed.md"), checker.discover_changes(repo, base, "HEAD") + ) + + def test_copy_checks_source_and_destination_scope(self) -> None: + with tempfile.TemporaryDirectory() as directory: + repo, base = self.repo(directory) + (repo / "allowed.md").write_bytes((repo / "README.md").read_bytes()) + self.git(repo, "add", "allowed.md") + changed = checker.discover_changes(repo, base, "HEAD") + self.assertIn(b"README.md", changed, "copy source must be present") + with self.assertRaisesRegex(checker.ContractError, "outside allowed scope"): + checker.enforce_scope(self.scope("allowed.md"), changed) + + def test_staged_executable_symlink_and_gitlink_are_rejected(self) -> None: + for mode in ("executable", "symlink", "gitlink"): + with self.subTest(mode=mode), tempfile.TemporaryDirectory() as directory: + repo, base = self.repo(directory) + if mode == "executable": + path = repo / "mode-path" + path.write_text("tool\n") + path.chmod(0o755) + self.git(repo, "add", "mode-path") + elif mode == "symlink": + (repo / "mode-path").symlink_to("README.md") + self.git(repo, "add", "mode-path") + else: + commit = self.git(repo, "rev-parse", "HEAD").decode() + self.git(repo, "update-index", "--add", "--cacheinfo", f"160000,{commit},mode-path") + with self.assertRaisesRegex(checker.ContractError, mode): + checker.discover_changes(repo, base, "HEAD") + + def test_staged_type_change_is_rejected(self) -> None: + with tempfile.TemporaryDirectory() as directory: + repo, base = self.repo(directory) + (repo / "README.md").unlink() + (repo / "README.md").symlink_to("missing") + self.git(repo, "add", "README.md") + with self.assertRaises(checker.ContractError): + checker.discover_changes(repo, base, "HEAD") + + def test_git_permitted_invalid_utf8_and_non_nfc_names_are_rejected(self) -> None: + for raw_name in (b"bad-\xff", "cafe\u0301".encode("utf-8")): + with self.subTest(raw_name=raw_name), tempfile.TemporaryDirectory() as directory: + repo, base = self.repo(directory) + descriptor = os.open(os.fsencode(repo), os.O_RDONLY | os.O_DIRECTORY) + try: + file_descriptor = os.open( + raw_name, os.O_WRONLY | os.O_CREAT, 0o644, dir_fd=descriptor + ) + os.write(file_descriptor, b"bad\n") + os.close(file_descriptor) + finally: + os.close(descriptor) + with self.assertRaises(checker.ContractError): + checker.discover_changes(repo, base, "HEAD") + + +if __name__ == "__main__": + unittest.main()