From 2cd7a2035278c96db903bbd2cef0ef6629e70ab3 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 11:28:06 +0200 Subject: [PATCH 1/2] Fix cross-platform CI regressions --- .../plan-anvil/tests/test_hooks_and_edge_states.py | 2 +- .../skills/plan-anvil/tests/test_preflight_and_git.py | 6 +++++- .../skills/plan-anvil/tests/test_safety_regressions.py | 6 +++--- .codex/hooks/plan_anvil_hooklib.py | 1 + CHANGELOG.md | 10 ++++++++++ .../run/reports/plan-review/blind-review.json | 1 + .../run/reports/plan-review/comparison.json | 2 +- .../run/reports/plan-review/blind-review.json | 1 + .../run/reports/plan-review/comparison.json | 2 +- 9 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.agents/skills/plan-anvil/tests/test_hooks_and_edge_states.py b/.agents/skills/plan-anvil/tests/test_hooks_and_edge_states.py index ea9d379..2569770 100644 --- a/.agents/skills/plan-anvil/tests/test_hooks_and_edge_states.py +++ b/.agents/skills/plan-anvil/tests/test_hooks_and_edge_states.py @@ -196,7 +196,7 @@ def test_compaction_accepts_schema_and_git_valid_checkpoint(self) -> None: ) message = recovery["hookSpecificOutput"]["additionalContext"] self.assertIn("Validated checkpoint", message) - self.assertIn(str(checkpoint_path), message) + self.assertIn(str(checkpoint_path.resolve()), message) def test_detached_head_with_multiple_containing_branches_is_ambiguous(self) -> None: with tempfile.TemporaryDirectory() as directory: diff --git a/.agents/skills/plan-anvil/tests/test_preflight_and_git.py b/.agents/skills/plan-anvil/tests/test_preflight_and_git.py index 2ce1ebb..cdba6eb 100644 --- a/.agents/skills/plan-anvil/tests/test_preflight_and_git.py +++ b/.agents/skills/plan-anvil/tests/test_preflight_and_git.py @@ -85,7 +85,11 @@ def test_probe_failure_cleans_resources_and_reports_hook_block(self) -> None: hooks = repo / hooks hooks.mkdir(parents=True, exist_ok=True) hook = hooks / "pre-commit" - hook.write_text("#!/bin/sh\nexit 1\n", encoding="utf-8", newline="\n") + hook.write_text( + "#!/bin/sh\necho 'pre-commit hook failed' >&2\nexit 1\n", + encoding="utf-8", + newline="\n", + ) hook.chmod(0o755) result = probe_git_capabilities(repo, "probe-hook-block", root / "external") self.assertFalse(result["ok"], result) diff --git a/.agents/skills/plan-anvil/tests/test_safety_regressions.py b/.agents/skills/plan-anvil/tests/test_safety_regressions.py index 4c9730f..1f3d759 100644 --- a/.agents/skills/plan-anvil/tests/test_safety_regressions.py +++ b/.agents/skills/plan-anvil/tests/test_safety_regressions.py @@ -76,15 +76,15 @@ def test_seal_artifacts_holds_lock_through_validation(self) -> None: author_valid_plan(context) observed: dict[str, str] = {} - real_validate_plan = seal_module.validate_plan + real_validate_plan_contract = seal_module.validate_plan_contract def validate_while_locked(*args, **kwargs): lock_path = context["run_root"] / ".generation-lock" payload = json.loads(lock_path.read_text(encoding="utf-8")) observed["command"] = payload["command"] - return real_validate_plan(*args, **kwargs) + return real_validate_plan_contract(*args, **kwargs) - with patch.object(seal_module, "validate_plan", side_effect=validate_while_locked): + with patch.object(seal_module, "validate_plan_contract", side_effect=validate_while_locked): result = seal_module.seal_artifacts(context["planning"], context["run_root"]) self.assertTrue(result["ok"]) diff --git a/.codex/hooks/plan_anvil_hooklib.py b/.codex/hooks/plan_anvil_hooklib.py index 34102f5..e8c2d67 100644 --- a/.codex/hooks/plan_anvil_hooklib.py +++ b/.codex/hooks/plan_anvil_hooklib.py @@ -114,6 +114,7 @@ def active_run_candidates_for_event(event: dict[str, Any]) -> list[ActiveRun]: repo = git_root(raw_cwd) if repo is None: return [] + repo = repo.resolve() all_runs = active_runs(repo) exact = [item for item in all_runs if item.worktree == repo] source = [item for item in all_runs if _source_worktree(item) == repo] diff --git a/CHANGELOG.md b/CHANGELOG.md index d0f025d..74c0e62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to PlanAnvil are documented here. +## [Unreleased] + +### Fixed + +- canonicalize event repository paths before active-run routing so source-worktree matching is stable across macOS symlink aliases and Windows path aliases; +- keep checkpoint recovery assertions platform-neutral by comparing canonical paths; +- update the artifact-sealing lock regression test to observe the current `validate_plan_contract` gate; +- make the Git-hook probe fixture emit explicit hook diagnostics while preserving fail-closed classification for unrelated commit failures; +- synchronize golden blind-review fixtures and dependent comparison hashes with the required independent `plan-anvil-reviewer` author role. + ## [0.1.0] - 2026-07-12 ### Added diff --git a/examples/small-change/run/reports/plan-review/blind-review.json b/examples/small-change/run/reports/plan-review/blind-review.json index f15e873..f4c5a8e 100644 --- a/examples/small-change/run/reports/plan-review/blind-review.json +++ b/examples/small-change/run/reports/plan-review/blind-review.json @@ -1,4 +1,5 @@ { + "author_role": "plan-anvil-reviewer", "created_at": "2026-07-12T18:00:00Z", "findings": [], "inputs": { diff --git a/examples/small-change/run/reports/plan-review/comparison.json b/examples/small-change/run/reports/plan-review/comparison.json index 468bd1e..8fd6711 100644 --- a/examples/small-change/run/reports/plan-review/comparison.json +++ b/examples/small-change/run/reports/plan-review/comparison.json @@ -1,6 +1,6 @@ { "blind_review_hash": "sha256:0b278c608ef8973584e451cb036f19566af028be8bd5b6c0a4fdac484f30341f", - "blind_review_sidecar_hash": "sha256:bfd4b664dea9860ec56d60e87628006165410f59004e3e68250aadcec213a2d9", + "blind_review_sidecar_hash": "sha256:389a86eb5fb0d2f8d652c12a0612f21a9ce7073931dabd7146984233ec56f8d5", "created_at": "2026-07-12T18:00:00Z", "deterministic_validation": "PASS", "reasons": [], diff --git a/examples/stateful-change/run/reports/plan-review/blind-review.json b/examples/stateful-change/run/reports/plan-review/blind-review.json index 43fe48e..6cd57e0 100644 --- a/examples/stateful-change/run/reports/plan-review/blind-review.json +++ b/examples/stateful-change/run/reports/plan-review/blind-review.json @@ -1,4 +1,5 @@ { + "author_role": "plan-anvil-reviewer", "created_at": "2026-07-12T18:00:00Z", "findings": [], "inputs": { diff --git a/examples/stateful-change/run/reports/plan-review/comparison.json b/examples/stateful-change/run/reports/plan-review/comparison.json index b1478a0..3990ca5 100644 --- a/examples/stateful-change/run/reports/plan-review/comparison.json +++ b/examples/stateful-change/run/reports/plan-review/comparison.json @@ -1,6 +1,6 @@ { "blind_review_hash": "sha256:0b278c608ef8973584e451cb036f19566af028be8bd5b6c0a4fdac484f30341f", - "blind_review_sidecar_hash": "sha256:f1a0f88f3221571225909d189b4f77553447c30b322686afd52f39e87e74f8fb", + "blind_review_sidecar_hash": "sha256:97963f012ba8eee2d8f16a4aa7a3f803b100935d0c43d5f591a5ff37e9e8d08b", "created_at": "2026-07-12T18:00:00Z", "deterministic_validation": "PASS", "reasons": [], From f2c93a587cb2095ab883641339f65bd37cd381a8 Mon Sep 17 00:00:00 2001 From: KeyffMS <124252104+KeyffMS@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:57:04 +0200 Subject: [PATCH 2/2] Canonicalize active run path identity --- .codex/hooks/plan_anvil_hooklib.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/.codex/hooks/plan_anvil_hooklib.py b/.codex/hooks/plan_anvil_hooklib.py index e8c2d67..fb4d3cb 100644 --- a/.codex/hooks/plan_anvil_hooklib.py +++ b/.codex/hooks/plan_anvil_hooklib.py @@ -109,15 +109,24 @@ def _source_worktree(active: ActiveRun) -> Path | None: return Path(raw).resolve() if isinstance(raw, str) and raw else None +def _path_identity(path: Path) -> str: + return os.path.normcase(os.path.normpath(str(path.resolve()))) + + def active_run_candidates_for_event(event: dict[str, Any]) -> list[ActiveRun]: raw_cwd = event.get("cwd") if isinstance(event.get("cwd"), str) else None repo = git_root(raw_cwd) if repo is None: return [] - repo = repo.resolve() + repo_identity = _path_identity(repo) all_runs = active_runs(repo) - exact = [item for item in all_runs if item.worktree == repo] - source = [item for item in all_runs if _source_worktree(item) == repo] + exact = [item for item in all_runs if _path_identity(item.worktree) == repo_identity] + source = [ + item + for item in all_runs + if (source_worktree := _source_worktree(item)) is not None + and _path_identity(source_worktree) == repo_identity + ] if exact: candidates = exact elif source: @@ -128,7 +137,7 @@ def active_run_candidates_for_event(event: dict[str, Any]) -> list[ActiveRun]: explicit = _explicit_run_id(event) if explicit is not None: candidates = [item for item in candidates if item.run_root.name == explicit] - unique: dict[Path, ActiveRun] = {item.run_root: item for item in candidates} + unique: dict[str, ActiveRun] = {_path_identity(item.run_root): item for item in candidates} return sorted(unique.values(), key=lambda item: item.run_root.name)