diff --git a/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/CHUNK_MAP.md b/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/CHUNK_MAP.md index c975717b..15271748 100644 --- a/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/CHUNK_MAP.md +++ b/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/CHUNK_MAP.md @@ -4,12 +4,14 @@ |---:|---|---|---:|---| | 0 | `WS-ENG-007-00R1` | Repair planning-intake file/tree parity and recover PR #187 exactly once | L1/P0 | Merged; recovery superseded after rerun-cardinality failure | | 0 | `WS-ENG-007-00R2` | Canonicalize repeated trusted check evidence and reconcile PRs #187 and #188 exactly once | L1/P0 | Merged; superseded by mutable-history failure | -| 0 | `WS-ENG-007-00R3` | Freeze accepted checks at merge time and give explicit starts deterministic recovery parity | L1/P0 | Active fail-closed recovery | -| 1 | `WS-ENG-007-01` | Add deterministic reviewed-patch identity and conservative base-delta review preservation | L1 | Blocked on 00R3 merge, successful four-merge reconciliation, and explicit start | +| 0 | `WS-ENG-007-00R3` | Freeze accepted checks at merge time and give explicit starts deterministic recovery parity | L1/P0 | Merged; exposed cross-initiative projection mixing | +| 0 | `WS-ENG-007-00R4` | Separate global merge evidence from initiative-local authority projections | L1/P0 | Active fail-closed recovery after the first cross-initiative start exposed projection mixing | +| 1 | `WS-ENG-007-01` | Add deterministic reviewed-patch identity and conservative base-delta review preservation | L1 | Blocked on 00R4 merge, successful reconciliation, and explicit start | | 2 | `WS-ENG-007-02` | Add structured reviewer-track and upstream-finding reconciliation | L1 | Blocked on 01 merge and explicit start | | 3 | `WS-ENG-007-03` | Add merge-group CI parity and queue-readiness proof | L1 | Blocked on 02 merge and explicit start | -Recovery chunks are exceptional ordered prerequisites; `00R3` supersedes the -incomplete mutable-evidence behavior left after `00R2`. Implementation chunks +Recovery chunks are exceptional ordered prerequisites; `00R4` preserves the +merge-bound evidence fixed by `00R3` while repairing cross-initiative authority +projection composition. Implementation chunks remain one PR each and stop after merge. Every successor requires a separate explicit signed start. diff --git a/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/STATUS.md b/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/STATUS.md index b6ef06d7..a273495f 100644 --- a/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/STATUS.md +++ b/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/STATUS.md @@ -1,18 +1,19 @@ # STATUS: WS-ENG-007 - Concurrent PR Review Reconciliation -- Phase: merge-bound check-evidence recovery +- Phase: cross-initiative authority-projection recovery - Gate: fail-closed automation repair - Active planning chunk: none - Active implementation chunk: none - Completed but unreconciled recovery chunk: `WS-ENG-007-00R1` - Merged but unreconciled recovery chunk: `WS-ENG-007-00R2` -- Active recovery chunk: `WS-ENG-007-00R3` +- Completed recovery chunk: `WS-ENG-007-00R3` +- Active recovery chunk: `WS-ENG-007-00R4` - Proposed implementation successor after recovery: `WS-ENG-007-01` - Separate explicit start required: true -- Current gate: PRs #187–#189 are merged, but signed reconciliation still fails - because mutable post-merge check history is re-evaluated and the explicit - start workflow lacks the merge workflow's recovery path. Signed state remains - at `73b457925b02301587b83d01ced0adb66319d134`; no successor is active. -- Review gate: all required internal tracks passed reviewed implementation - `4a3eee7fdd80970675ae2574542073b8a92e1ef9`; awaiting external checks and the +- Current gate: PR #190 reconciled signed memory through + `a3eecadcf847ac70fc28c58dad642f2d761015e0`, but the first ENG-006 start failed + because authority validation mixed ENG-006 lifecycle identity with ENG-007 + protected-check evidence. No successor is active. +- Review gate: all nine internal tracks passed exact implementation head + `c59bff278945c3d8e63a6ea776a6bf6206df8af8`; awaiting external checks and the user-owned merge decision. diff --git a/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/chunks/WS-ENG-007-00R4-cross-initiative-authority-projection.md b/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/chunks/WS-ENG-007-00R4-cross-initiative-authority-projection.md new file mode 100644 index 00000000..0358df02 --- /dev/null +++ b/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/chunks/WS-ENG-007-00R4-cross-initiative-authority-projection.md @@ -0,0 +1,95 @@ +# Chunk Contract: WS-ENG-007-00R4 — Cross-Initiative Authority Projection Repair + +## Parent initiative + +`WS-ENG-007` — Concurrent PR Review Reconciliation + +## Goal + +Restore signed starts for an idle initiative when the latest global merge belongs +to another initiative, without weakening merge-bound evidence validation. + +## Why this chunk exists + +After PR #190 reconciled trusted `main`, the signed start of `WS-ENG-006-01` +failed closed. Authority-event validation combined ENG-006's earlier signed +lifecycle with ENG-007's latest protected-check evidence, then correctly rejected +that synthetic record because the evidence belonged to a different PR. + +## Risk class + +L1 / P0 authority-event reliability repair. + +## Start phase + +Recovery implementation. The signed start mechanism fails before it can record +this repair, so this contract documents the exact bounded recovery exception. + +## Allowed files + +```text +scripts/update_post_merge_memory.py +scripts/check_loop_memory_state.py +scripts/test_update_post_merge_memory.py +.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/** +.agent-loop/merge-intents/WS-ENG-007-00R4.json +``` + +## Not allowed + +```text +workflow, permission, dispatcher, branch-protection, or cancel-authority changes +protected-check, internal-review, CI, coverage, or human-merge weakening +application, API, database, auth, payment, or product changes +automatic successor starts or unsigned implementation authority +``` + +## Acceptance criteria + +- [ ] Authority events retain the latest global merge record as their canonical + base and retain its exact protected-check evidence unchanged. +- [ ] The selected initiative projection contains only its signed lifecycle + identity and active/gate transition; it does not borrow evidence from the + latest global PR. +- [ ] Ledger transition validation still binds the authority source and completed + chunk byte-for-byte to the selected initiative's latest signed record. +- [ ] Malformed or forged authority sources, initiative identities, chunk + identities, active state, and gates still fail closed. +- [ ] A regression fixture proves an older idle initiative can start after a + different initiative's merge-bound evidence becomes the global state. +- [ ] The independent checker applies the same separation and validates the + complete generated state and ledger. +- [ ] Exactly one schema-v2 merge intent names `WS-ENG-007-01` as the stopped + explicit successor; neither ENG-006 nor ENG-007 starts automatically. + +## Verification commands + +```bash +PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m pytest -q scripts/test_update_post_merge_memory.py scripts/test_check_loop_memory_state.py +python3 scripts/check_markdown_links.py +python3 scripts/check_stale_workstream_wording.py +python3 scripts/update_post_merge_memory.py validate-merge-intent --repository-root . --base-ref origin/main +git diff --check origin/main...HEAD +``` + +## Required reviewers + +- senior engineering +- QA/test +- security/auth +- product/ops +- architecture +- CI integrity +- docs +- reuse/dedup +- test delta + +## Human review focus + +Does the repair separate global merge evidence from initiative-local lifecycle +state while keeping both independently and immutably bound? + +## Stop conditions + +Stop if the repair requires weakening protected evidence, accepting an unsigned +basis, changing start authority, or automatically starting any successor. diff --git a/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/reviews/WS-ENG-007-00R4-external-review-response.md b/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/reviews/WS-ENG-007-00R4-external-review-response.md new file mode 100644 index 00000000..c1879185 --- /dev/null +++ b/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/reviews/WS-ENG-007-00R4-external-review-response.md @@ -0,0 +1,35 @@ +# External Review Response: WS-ENG-007-00R4 + +## Comments addressed + +- GitHub Agent Gates failed because independent checker branch coverage was + 89.13 percent, below the protected 90 percent floor. +- Added seven focused fail-closed mutations for invalid PR identity, canonical + PR URL, bounded title, malformed and timezone-naive timestamps, and authority + schema shape. +- The exact failing GitHub command now passes 296 tests with 90.40 percent + checker branch coverage. +- CodeRabbit identified a stale chunk-map dependency that still named `00R3`. + The `WS-ENG-007-01` row now names `00R4` and successful reconciliation as its + prerequisites. + +## Comments deferred + +None. + +## Human decisions needed + +None. This response changes tests and process documentation only; it does not +alter CI, thresholds, workflow behavior, permissions, or authority semantics. + +## Commands rerun + +```bash +PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m pytest -p pytest_cov.plugin -q --cov=scripts.check_loop_memory_state --cov-branch --cov-report=term-missing --cov-fail-under=90 scripts/test_agent_gates.py scripts/test_update_post_merge_memory.py scripts/test_check_loop_memory_state.py +git diff --check +``` + +## Remaining risks + +The updated exact PR head must complete GitHub Agent Gates, Backend, and +CodeRabbit before the PR is ready for human merge review. diff --git a/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/reviews/WS-ENG-007-00R4-internal-review-evidence.md b/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/reviews/WS-ENG-007-00R4-internal-review-evidence.md new file mode 100644 index 00000000..3ac8c952 --- /dev/null +++ b/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/reviews/WS-ENG-007-00R4-internal-review-evidence.md @@ -0,0 +1,77 @@ +# Internal Review Evidence: WS-ENG-007-00R4 + +## Chunk + +`WS-ENG-007-00R4` — Cross-Initiative Authority Projection Repair + +open sub-agent sessions: none + +valid findings addressed: yes + +## Reviewed Revision + +Reviewed code SHA: c59bff278945c3d8e63a6ea776a6bf6206df8af8 + +Reviewed at: 2026-07-23T10:50:17Z + +The `/root/eng006_*` sessions were inherited from the ENG-006 reviewer pool and +explicitly reassigned to this WS-ENG-007-00R4 review. Every track is rebound to +the exact reviewed implementation SHA above; session names grant no +cross-initiative authority. + +Reviewer run IDs: senior-engineering=/root/eng006_senior_arch_docs; QA/test=/root/eng006_qa_ci_tests; security/auth=/root/eng006_security_ops_reuse; product/ops=/root/eng006_security_ops_reuse; architecture=/root/eng006_senior_arch_docs; docs=/root/eng006_senior_arch_docs; CI-integrity=/root/eng006_qa_ci_tests; reuse/dedup=/root/eng006_security_ops_reuse; test-delta=/root/eng006_qa_ci_tests + +## Reviewer Results + +| Reviewer | Result | Blocking findings | Notes | +|---|---:|---|---| +| senior engineering | PASS AFTER FIXES | None | Authority lifecycle and global evidence remain separate and ledger-bound. | +| QA/test | PASS AFTER FIXES | None | Exact cross-initiative regression and malformed-basis cases pass. | +| security/auth | PASS | None | Start authority, permissions, and signed-basis binding are unchanged. | +| product/ops | PASS | None | Per-initiative concurrency remains isolated; no product lifecycle change. | +| architecture | PASS AFTER FIXES | None | Removed invalid synthetic record composition without adding a parallel path. | +| CI integrity | PASS AFTER FIXES | None | 296 tests pass; updater and checker retain literal protected coverage floors. | +| docs | PASS | None | Contract, chunk map, and status describe the bounded recovery. | +| reuse/dedup | PASS | None | Existing transition validators remain the single ledger-binding path. | +| test delta | PASS AFTER FIXES | None | Additive adversarial tests; no skips, removals, or weakened assertions. | + +## Valid Findings Addressed + +- Prevented the independent checker from crashing on non-object authority + metadata and restored explicit source scalar, timestamp, PR, metadata, and + intent-path validation. +- Added an independent checker regression for an older idle initiative starting + after a different initiative's protected merge became global state. +- Added six updater mutation cases for malformed authority source and completed + metadata. +- Raised exact updater branch coverage from 89.96 percent to 90.18 percent + without rounding, threshold changes, exclusions, or test weakening. +- Responded to the external Agent Gates failure by raising independent checker + branch coverage from 89.13 percent to 90.40 percent with seven adversarial + cases and no threshold or workflow change. + +## Commands Run + +```bash +PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m pytest -q scripts/test_update_post_merge_memory.py scripts/test_check_loop_memory_state.py scripts/test_agent_gates.py +PYTEST_DISABLE_PLUGIN_AUTOLOAD=1 python3 -m pytest -p pytest_cov -q scripts/test_update_post_merge_memory.py scripts/test_check_loop_memory_state.py scripts/test_agent_gates.py --cov=scripts.update_post_merge_memory --cov-branch --cov-report=term --cov-fail-under=90 --cov-precision=2 +python3 scripts/check_markdown_links.py +python3 scripts/check_stale_workstream_wording.py +python3 scripts/update_post_merge_memory.py validate-merge-intent --repository-root . --base-ref origin/main +git diff --check +``` + +The exact updater gate passed 289 tests at 90.18 percent branch coverage. The +exact independent-checker gate passed 296 tests at 90.40 percent branch +coverage. Both retain the literal 90.00 percent blocking floor. + +## Remaining Risks + +The current failed start run remains unsigned and inert. After this recovery +merges and generated memory succeeds, ENG-006 still requires a fresh explicit +start against exact current `main`; nothing starts automatically. + +## Stop Condition + +No successor is active. `WS-ENG-007-01` and `WS-ENG-006-01` each retain their +own explicit signed start gate. diff --git a/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/reviews/WS-ENG-007-00R4-pr-trust-bundle.md b/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/reviews/WS-ENG-007-00R4-pr-trust-bundle.md new file mode 100644 index 00000000..f72f9dd7 --- /dev/null +++ b/.agent-loop/initiatives/WS-ENG-007-concurrent-pr-review-reconciliation/reviews/WS-ENG-007-00R4-pr-trust-bundle.md @@ -0,0 +1,45 @@ +# PR Trust Bundle: WS-ENG-007-00R4 + +## Intent + +Allow a signed start for an idle initiative after another initiative becomes the +latest global merge, without attaching one PR's protected evidence to another +initiative's lifecycle. + +## Scope + +- Keep the latest global merge and its protected evidence unchanged. +- Validate the selected initiative's authority projection independently. +- Bind that projection byte-for-byte to its prior signed ledger basis. +- Add updater and independent-checker regressions for the exact failure. + +## Reviewed Revision + +`c59bff278945c3d8e63a6ea776a6bf6206df8af8` + +## CI Integrity + +- No workflow, permission, required check, coverage floor, test, reviewer, signed + start, or human merge checkpoint was removed or weakened. +- The exact updater gate passes 289 tests at 90.18 percent branch coverage. +- The exact independent-checker gate passes 296 tests at 90.40 percent branch + coverage. + +## Reviewer Result + +All nine required internal tracks passed after fixes. No Critical or High +finding remains open. + +## Human Review Focus + +- Confirm authority projection validation no longer constructs a synthetic merge + record with foreign protected evidence. +- Confirm source/completed identity remains bound to the prior signed initiative + record by both transition validators. +- Confirm malformed authority metadata fails closed in updater and checker. +- Confirm no successor starts automatically. + +## Remaining Risk + +A new `main` merge before this recovery lands requires the branch and evidence to +be refreshed normally; the signed event still requires exact current `main`. diff --git a/.agent-loop/merge-intents/WS-ENG-007-00R4.json b/.agent-loop/merge-intents/WS-ENG-007-00R4.json new file mode 100644 index 00000000..0bb0edf6 --- /dev/null +++ b/.agent-loop/merge-intents/WS-ENG-007-00R4.json @@ -0,0 +1,9 @@ +{ + "chunk_id": "WS-ENG-007-00R4", + "chunk_title": "Cross-Initiative Authority Projection Repair", + "initiative_id": "WS-ENG-007", + "next_chunk_id": "WS-ENG-007-01", + "next_chunk_title": "Reviewed Patch and Base-Delta Reconciliation", + "next_requires_explicit_start": true, + "schema_version": 2 +} diff --git a/scripts/check_loop_memory_state.py b/scripts/check_loop_memory_state.py index dd19dadc..5a881d84 100644 --- a/scripts/check_loop_memory_state.py +++ b/scripts/check_loop_memory_state.py @@ -465,6 +465,7 @@ def _record_failures( authority = base.pop("authority_state", None) metadata = base.get("completed_chunk", {}) source = base.get("source", {}) + repository = base.get("repository") base["updated_at"] = source.get("merged_at") failures = _record_failures(base, label, repository_root) if not isinstance(authority, dict) or set(authority) != { @@ -472,24 +473,56 @@ def _record_failures( }: failures.append(f"{label}: invalid authority lifecycle state") return failures + authority_source = authority["source"] + if not isinstance(authority_source, dict) or set(authority_source) != { + "main_sha", "first_parent_sha", "pr_number", "pr_url", "pr_title", + "head_sha", "head_ref", "merged_at", "merged_by", "intent_path", + "intent_blob_sha", + }: + failures.append(f"{label}: invalid authority lifecycle source") + return failures + for field in ("main_sha", "first_parent_sha", "head_sha", "intent_blob_sha"): + value = authority_source.get(field) + if not isinstance(value, str) or not SHA_PATTERN.fullmatch(value): + failures.append(f"{label}: invalid authority source {field}") + authority_pr = authority_source.get("pr_number") + if ( + not isinstance(authority_pr, int) + or isinstance(authority_pr, bool) + or authority_pr <= 0 + ): + failures.append(f"{label}: invalid authority source pr_number") + elif authority_source.get("pr_url") != ( + f"https://github.com/{repository}/pull/{authority_pr}" + ): + failures.append(f"{label}: invalid authority source pr_url") + for field, maximum in ( + ("pr_title", 240), ("head_ref", 240), ("merged_by", 160) + ): + if not _is_bounded_single_line(authority_source.get(field), maximum): + failures.append(f"{label}: invalid authority source {field}") + try: + authority_time = datetime.fromisoformat( + authority_source["merged_at"].replace("Z", "+00:00") + ) + except (AttributeError, ValueError): + failures.append(f"{label}: invalid authority source merged_at") + else: + if authority_time.tzinfo is None: + failures.append(f"{label}: authority source merged_at has no timezone") metadata = authority["completed_chunk"] - lifecycle = json.loads(json.dumps(base)) - lifecycle.update(authority) - lifecycle_source = lifecycle.get("source", {}) - lifecycle["updated_at"] = lifecycle_source.get("merged_at") - lifecycle_metadata = lifecycle.get("completed_chunk", {}) - lifecycle["active"] = {"planning_chunk": None, "implementation_chunk": None} - lifecycle["gate"] = { - "status": "stopped_after_merge", - "next_chunk_id": lifecycle_metadata.get("next_chunk_id"), - "next_chunk_title": lifecycle_metadata.get("next_chunk_title"), - "next_requires_explicit_start": lifecycle_metadata.get( - "next_requires_explicit_start" - ), - } - failures.extend( - _record_failures(lifecycle, f"{label} authority basis", repository_root) - ) + metadata_failures = _metadata_failures(metadata, f"{label} authority basis") + failures.extend(metadata_failures) + if not isinstance(metadata, dict): + return failures + if authority_source.get("intent_path") != ( + f".agent-loop/merge-intents/{metadata.get('chunk_id')}.json" + ): + failures.append(f"{label}: authority intent path does not match completed chunk") + # The authority projection is bound independently to its exact prior + # initiative record by _authority_transition_failures. Rebuilding a + # synthetic merge record here would incorrectly attach the latest + # global PR's protected-check evidence to that older initiative basis. historical_event_keys = { "type", "event_id", "run_id", "created_at", "dispatcher", "approvers", "reason", "main_sha", "prior_state_tip", diff --git a/scripts/test_check_loop_memory_state.py b/scripts/test_check_loop_memory_state.py index a3b98ad8..6abe1240 100644 --- a/scripts/test_check_loop_memory_state.py +++ b/scripts/test_check_loop_memory_state.py @@ -153,6 +153,20 @@ def _authority_record(tmp_path: Path) -> dict: lambda record: record["event"].update(initiative_id="WS-ART-001"), lambda record: record["event"].update(chunk_id="WS-ENG-001-99"), lambda record: record["authority_state"].update(source={}), + lambda record: record["authority_state"]["source"].update(head_sha="bad"), + lambda record: record["authority_state"]["source"].update(pr_number=0), + lambda record: record["authority_state"]["source"].update(pr_number=True), + lambda record: record["authority_state"]["source"].update(pr_url="wrong"), + lambda record: record["authority_state"]["source"].update(pr_title=""), + lambda record: record["authority_state"]["source"].update(merged_at="wrong"), + lambda record: record["authority_state"]["source"].update( + merged_at="2026-07-20T10:00:00" + ), + lambda record: record["authority_state"]["source"].update( + intent_path=".agent-loop/merge-intents/WS-BAD-001.json" + ), + lambda record: record["authority_state"].update(completed_chunk=[]), + lambda record: record["authority_state"].pop("gate"), lambda record: record["authority_state"]["completed_chunk"].update( initiative_id="WS-ART-001" ), @@ -169,6 +183,15 @@ def test_checker_rejects_malformed_authority_records( assert checker._record_failures(record, "fixture") +def test_checker_accepts_cross_initiative_start_without_borrowed_evidence( + tmp_path: Path, +) -> None: + state_root, repository_root = fixtures._cross_initiative_merge_bound_state( + tmp_path + ) + assert checker.generated_state_failures(state_root, repository_root) == [] + + def test_checker_rejects_invalid_legacy_exemption(tmp_path: Path) -> None: record = fixtures._record() record["legacy_exemptions"] = [ diff --git a/scripts/test_update_post_merge_memory.py b/scripts/test_update_post_merge_memory.py index 75f0cb65..52da6591 100644 --- a/scripts/test_update_post_merge_memory.py +++ b/scripts/test_update_post_merge_memory.py @@ -1477,6 +1477,34 @@ def test_authority_transition_requires_prior_basis(tmp_path: Path) -> None: loop._validate_authority_transition(authority, []) +@pytest.mark.parametrize( + "mutation", + [ + lambda record: record["authority_state"]["source"].update(head_sha="bad"), + lambda record: record["authority_state"]["source"].update(pr_number=0), + lambda record: record["authority_state"]["source"].update(pr_url="wrong"), + lambda record: record["authority_state"]["source"].update(merged_at="wrong"), + lambda record: record["authority_state"]["source"].update( + intent_path=".agent-loop/merge-intents/WS-BAD-001.json" + ), + lambda record: record["authority_state"].update(completed_chunk=[]), + ], +) +def test_authority_record_rejects_malformed_basis_source( + tmp_path: Path, mutation +) -> None: + state_root, repository_root = tmp_path / "state", tmp_path / "repo" + _contract(repository_root) + loop.apply_merge_record(state_root, _record()) + loop.apply_authority_event( + state_root, _event("start"), repository_root=repository_root + ) + authority = json.loads((state_root / loop.STATE_PATH).read_text()) + mutation(authority) + with pytest.raises(loop.LoopMemoryError): + loop._validate_record(authority) + + @pytest.mark.parametrize( "mutation", [ @@ -2010,6 +2038,45 @@ def _merge_bound_record() -> dict: return record +def _cross_initiative_merge_bound_state(tmp_path: Path) -> tuple[Path, Path]: + state_root, repository_root = tmp_path / "state", tmp_path / "repo" + _contract(repository_root) + loop.apply_merge_record(state_root, _record()) + later = _merge_bound_record() + later["source"].update( + main_sha="c" * 40, + first_parent_sha="a" * 40, + pr_number=190, + pr_url="https://github.com/Flow-Research/workstream/pull/190", + intent_path=".agent-loop/merge-intents/WS-ART-001-02.json", + ) + later["completed_chunk"].update( + initiative_id="WS-ART-001", + chunk_id="WS-ART-001-02", + chunk_title="Artifact Custody", + next_chunk_id="WS-ART-001-03", + next_chunk_title="Artifact Recovery", + ) + later["gate"].update( + next_chunk_id="WS-ART-001-03", next_chunk_title="Artifact Recovery" + ) + loop.apply_merge_record(state_root, later) + event = _event("start") + event["main_sha"] = later["source"]["main_sha"] + + assert loop.apply_authority_event( + state_root, event, repository_root=repository_root + ) + return state_root, repository_root + + +def test_cross_initiative_start_does_not_borrow_latest_merge_evidence( + tmp_path: Path, +) -> None: + state_root, _repository_root = _cross_initiative_merge_bound_state(tmp_path) + loop.validate_generated_state(state_root) + + @pytest.mark.parametrize("bad_value", ["2026-07-23 05:01:00Z", "not-a-time"]) def test_independent_checker_rejects_noncanonical_protected_timestamps(bad_value: str) -> None: record = _merge_bound_record() diff --git a/scripts/update_post_merge_memory.py b/scripts/update_post_merge_memory.py index ef69ed88..85284a7c 100644 --- a/scripts/update_post_merge_memory.py +++ b/scripts/update_post_merge_memory.py @@ -1838,18 +1838,41 @@ def _validate_record(record: dict[str, Any]) -> LoopMetadata: "source", "completed_chunk", "active", "gate" }: raise LoopMemoryError("authority lifecycle state has an invalid schema") - lifecycle = json.loads(_canonical_json(base)) - lifecycle.update(authority) - lifecycle["updated_at"] = lifecycle["source"]["merged_at"] - metadata = parse_loop_metadata(_canonical_json(lifecycle["completed_chunk"])) - lifecycle["active"] = {"planning_chunk": None, "implementation_chunk": None} - lifecycle["gate"] = { - "status": "stopped_after_merge", - "next_chunk_id": metadata.next_chunk_id, - "next_chunk_title": metadata.next_chunk_title, - "next_requires_explicit_start": metadata.next_requires_explicit_start, - } - _validate_record(lifecycle) + authority_source = authority["source"] + if not isinstance(authority_source, dict) or set(authority_source) != { + "main_sha", "first_parent_sha", "pr_number", "pr_url", "pr_title", + "head_sha", "head_ref", "merged_at", "merged_by", "intent_path", + "intent_blob_sha", + }: + raise LoopMemoryError("authority lifecycle source has an invalid schema") + _validate_repository_and_sha(record.get("repository"), authority_source["main_sha"]) + for field in ("first_parent_sha", "head_sha", "intent_blob_sha"): + _validate_sha(authority_source[field]) + authority_pr = authority_source["pr_number"] + if ( + not isinstance(authority_pr, int) + or isinstance(authority_pr, bool) + or authority_pr <= 0 + ): + raise LoopMemoryError("authority lifecycle source has no positive PR number") + if authority_source["pr_url"] != ( + f"https://github.com/{record['repository']}/pull/{authority_pr}" + ): + raise LoopMemoryError("authority lifecycle source has an invalid PR URL") + for field, maximum in ( + ("pr_title", 240), ("head_ref", 240), ("merged_by", 160) + ): + _bounded_text(authority_source[field], field, maximum=maximum) + _parse_timestamp(authority_source["merged_at"], "authority merged_at") + # The authority lifecycle is a projection of an earlier signed record + # for the selected initiative. Its source and completed chunk are + # bound to that ledger record by _validate_authority_transition. Do + # not combine them with the latest global merge's checks or + # protected-check evidence: those belong to a different PR and would + # make a valid cross-initiative start fail provenance validation. + metadata = parse_loop_metadata(_canonical_json(authority["completed_chunk"])) + if authority_source["intent_path"] != _intent_path(metadata): + raise LoopMemoryError("authority lifecycle intent path is inconsistent") if metadata.initiative_id != event["initiative_id"]: raise LoopMemoryError("authority lifecycle initiative does not match event") selection = event.get("selection")