feat(gate-preconditions): establish environment/repo-state preconditions before gates run - #1675
Merged
Merged
Conversation
Decomposes the re-verified Acceptance Criteria Map into four tasks (registry preconditions field, shallow-clone auto-establish, dependency precondition fail-closed, worktree merge-base assertion) across two waves. Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lv5N1QfPRh5sruiGzzHA43
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1675 +/- ##
========================================
Coverage 99.66% 99.66%
========================================
Files 149 152 +3
Lines 24479 24725 +246
Branches 2960 2967 +7
========================================
+ Hits 24397 24643 +246
Misses 82 82 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Adds an optional preconditions object to the gate $defs in ssot.schema.json (additionalProperties: false, matching every other object in this schema), with two optional sub-keys: requires_full_history (boolean) and requires_python_packages (array of non-empty strings, minItems: 1). Bumps ssot.json's meta.schema_version from 1.4.0 to 1.5.0 per the schema file's own docstring requirement. Declares the field on the two gate entries whose scripts genuinely need it, confirmed by reading each script directly rather than trusting the plan's prior read: harden-checkout-pin-drift gets requires_full_history: true (its own gitapex_scan_harden_checkout_pin_drift.py raises RuntimeError on a shallow-clone boundary artifact when resolving a file's true last-touching commit via unbounded git log -- distinct from the other local-plane gates touching _gitapex_base_ref.py, which either fetch their own specific base ref or gracefully degrade to "unverifiable" rather than hard-failing). skill-audit-disclosure gets requires_python_packages: ["pydantic"] (its own gitapex_gate_skill_audit_disclosure.py transitively imports pydantic via gitapex_compute_skill_audit_flags.py -> gitapex_detect_changed_gate_scripts.py, confirmed by reading each import chain directly). Adds a matching optional GatePreconditions submodel (extra="forbid") to gitapex_scan_ssot_schema.py's own pydantic Gate model, so a preconditions-carrying entry does not fail that script's stricter parse. Adds two test cases to test_gitapex_scan_ssot_schema.py: a preconditions-carrying gate entry validates against both the JSON Schema and the pydantic model, and an entry with an unrecognized precondition sub-key is rejected by both layers. This is the foundational task other tasks in this branch plan build on; field names kept exactly as specified for the later-wave tasks that read this shape. Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…anch tip Task 4 of the gate-preconditions-mechanism branch plan. Adds gitapex_check_task_worktree_base.py, a stdlib-only classifier chained into check_task_bash_safety.sh's own existing PreToolUse "Bash" hook (as a second sibling call alongside gitapex_check_task_bash_safety.py, matching that script's own convention -- no second hooks.PreToolUse frontmatter entry exists to hang it off instead, confirmed by reading .claude/agents/branch-plan-task.md directly first). Resolves the shared plan branch's own name purely from local git state (the worktree's own branch reflog "branch: Created from <name>" entry, verified to resolve to an existing local branch -- no value threaded in from the main thread), then asserts the shared branch's own current tip is still an ancestor of the worktree's HEAD, denying with both SHAs named on a confirmed mismatch. Reproduces the exact original defect shape (a worktree forked from a now-stale base, issue #1508) in a fixture-repo test, confirms the check denies there and allows once the fork point matches, and confirms an unresolvable branch name (no worktree, a detached HEAD, or an unrelated worktree's own reflog) fails open with a warning rather than blocking. Documents the mechanism, its asymmetric fail-open/fail-closed design, and its disclosed residual (piggybacks on the task's own first Bash call, not a true before-any-tool-call gate) in references/execution-and-dispatch.md and references/threat-model-and-authorization.md, and adds a prompt-only paragraph to the plugin-distributed agents/branch-plan-task.md variant (no hooks field there, matching this skill's own established two-variant asymmetry convention). Also adds tests/test_gitapex_check_task_worktree_base_properties.py (a top-level tests/ file, outside this task's originally-listed Owns list): two already-wired deterministic gates (detection-logic-property-coverage, function-body-test-coverage) required it to accept the new module's own detection logic; a Hypothesis property test in it found and fixed a real bug in the new module (a str.splitlines()-based scan over-splitting on non-newline Unicode line-boundary characters). Squashes three worktree-local commits (feat: assert worktree base matches shared branch tip; test: add worktree-base property coverage, fix line-split bug; test: call main directly for function-body-test-coverage) into one clean commit for merge-back, and restores docs/superpowers/plans/2026-09-02-gate-preconditions-mechanism-c0wevy.md (this branch's own task-list file), which the dispatched task's own worktree had inadvertently deleted outside its Owns scope -- caught by this skill's own step 6 file-ownership-consistency screening before merge, corrected here rather than merged as-is. Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merges Task 4 of the gate-preconditions-mechanism branch plan onto the shared branch. Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Refs #1566. Consolidates #1546/#1489: a shallow clone reaching harden-checkout-pin-drift directly used to surface RuntimeError only reactively, mid-run, from that one gate's own git-log call. Add .github/scripts/_gitapex_preconditions.py: is_shallow_clone() wraps `git rev-parse --is-shallow-repository`, ensure_full_history() wraps `git fetch --unshallow`. Both raise a typed PreconditionsError on any subprocess failure -- never a silent "not shallow". gitapex_gate_local_preflight.py's load_local_checks() now carries a gate's own preconditions.requires_full_history off the same registry parse (LocalCheck grows that field), and a new ensure_wired_gate_preconditions() runs before any wired gate: no subprocess at all when no wired gate needs it, the local shallow probe only when at least one does, and the network fetch only when that probe confirms the repo actually is shallow. A failure at either step aborts with one top-line stderr message and exit 1 before any gate runs. Tests: tests/test__gitapex_preconditions.py unit-tests the two new helpers against real local bare-repo/shallow-clone fixtures. tests/test_gitapex_gate_local_preflight.py adds registry-parsing tests plus ensure_wired_gate_preconditions coverage (no-op when unneeded, fetch-skip when already non-shallow, fetch-once when shallow, abort-with-message on either failure), and two @pytest.mark.slow main() integration tests: one reproduces the original defect directly (the fixture gate script, shaped like harden-checkout-pin-drift's own current_action_sha(), fails against a still-shallow clone run directly, then passes once main() establishes full history first) and one confirms the abort happens before any wired gate runs when the unshallow fetch itself fails. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ention Refs #1566. The stdlib-only-claim-drift gate flags any file whose diff adds a new top-level import if that file's own content contains an un-negated "stdlib-only" phrase with no nearby unindented "uv run" mention. This file's own docstring already named the registered stdlib-only-claim-drift gate by its literal registry id in an unrelated bullet about origin/main-needing local_stdin producers -- harmless before this change, but the new `import _gitapex_preconditions` line added in the prior commit is itself exactly the kind of new top-level import that trigger checks for, so it now scans this file's own content and finds that unrelated gate-name mention. Move the "uv run" disclosure onto the bullet's own opening (unindented) line rather than a continuation line, since the gate's own proximity check deliberately excludes indented lines (issue #1052's own Usage::-boilerplate false-positive fix) -- and the disclosure is accurate on its own merits: every local_stdin producer in the registry genuinely is invoked via `uv run`. Verified against the real gitapex_gate_stdlib_only_claim_drift.py, fed this branch's own diff directly (bypassing this worktree's stale origin/main): failed before this commit, passes after. The full local preflight (44 wired gates) now passes end to end. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Refs #1566. Closes #1547(a): hooks/check-pr-skill-audit-disclosure.sh's tier-1 path attempted a bare python3 invocation of gitapex_gate_skill_audit_disclosure.py, which transitively imports pydantic. When that package was not importable by python3, the invocation crashed with no recognizable FAIL: line, and the existing fall-through logic silently downgraded that to a warning and degraded to tier 2's weaker, SKILL.md-only check -- never telling the caller a dependency was missing. Add hooks/gitapex_check_python_precondition.py, a stdlib-only checker that probes each given module's importability in a separate python3 subprocess (never this process's own import statement), so a missing module cannot crash the checker itself. Wire it into check-pr-skill-audit-disclosure.sh's tier-1 block: before attempting the full_gate invocation, read the skill-audit-disclosure gate's preconditions.requires_python_packages array from .gitapex/ssot.json (via jq, never hardcoded) and invoke the new checker against it. If any required package is missing, deny immediately with an actionable "uv sync --group dev" message instead of falling through. Every other tier-1 failure cause (unresolvable git state, unreadable registry, a bug in the gate script itself) still falls through to tier 2 with a warning exactly as before -- this fix is narrowly scoped to the dependency-missing cause only, and fails open (skips the new check entirely) when the registry cannot be read or declares no required packages for this gate. New tests in hooks/test_gitapex_check_pr_skill_audit_disclosure_shell.py reproduce the original defect (a deliberately-unimportable module name denies with the actionable message rather than warning-and-falling- through), confirm a genuinely unrelated tier-1 failure (a corrupted gate script) still falls through unchanged, and confirm the existing happy-path and skip-when-no-preconditions-declared behavior is unaffected. hooks/test_gitapex_check_python_precondition.py unit-tests the new probe directly. Amended by this branch's own step-6 screening (review-persona check 4): the python3 "$precondition_script" "${required_packages[@]}" call had no `--` separator before the array expansion, so a registry-declared package name shaped like an argparse flag (e.g. "--help") would be read as CLI option syntax instead of data -- triggering the probe script's own --help handling (usage text, exit 0) rather than an import check, which this hook's jq parse would then read as an empty, inconclusive result and silently skip the new deny path. Fixed by adding `--` before the array expansion, and by adding an explicit Warning: line (matching this file's own established convention) when the precondition subprocess's output cannot be parsed, instead of silently treating that as "nothing missing". New regression test test_tier1_precondition_treats_a_flag_shaped_package_name_as_data reproduces the exact shape and confirms it now denies correctly. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merges Task 2 of the gate-preconditions-mechanism branch plan onto the shared branch. Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merges Task 3 of the gate-preconditions-mechanism branch plan onto the shared branch, including the step-6 screening fix (argv `--` separator and an explicit parse-failure warning). Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ition probe Codecov flagged 3 uncovered lines in the new gitapex_check_python_precondition.py (89.65% patch coverage): the except OSError branch in is_importable(), reached when the python executable itself cannot be launched at all (not on PATH, not executable) -- distinct from the probed module simply being missing. Adds a direct regression test calling is_importable() with a nonexistent interpreter path, confirming it returns False and warns rather than letting the OSError escape uncaught. Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… branch Refs #1566. Step 8 refactor/simplify pass over the full accumulated diff of the four gate-preconditions tasks. Behavior-preserving throughout: no test removed or weakened, the same 7954 tests pass, and all 44 wired local gates still pass. Reuse, .github/scripts/_gitapex_preconditions.py: is_shallow_clone and ensure_full_history each carried their own near-identical subprocess.run wrapper (fixed argv, capture_output, errors="replace", check=False, plus a TimeoutExpired and an OSError/ValueError/SubprocessError handler). _gitapex_base_ref.run_git in the same directory already is that wrapper, and its own error_cls parameter exists precisely so a sibling keeps raising its own exception type -- its docstring names "a second, near-identical subprocess.run wrapper in that caller" as the duplicate-then-drift it exists to prevent. Both functions now call it with error_cls=PreconditionsError, and GIT_TIMEOUT_SECONDS is imported from it rather than redefined as a second literal 60. The nonzero-exit messages are byte-identical to before; the subprocess-layer messages keep every fact (the git command, the repo root, the timeout) in run_git's own established phrasing. The module docstring's stdlib-only paragraph is rewritten to describe the import it now takes. Reuse, tests/: tests/test__gitapex_preconditions.py and tests/test_gitapex_gate_local_preflight.py each defined a verbatim copy of the same five-step shallow-clone fixture chain (run git, init repo, commit file, bare origin with two commits, depth-1 file:// clone). Moved to tests/conftest.py, this suite's own established home for a helper two files must agree on, and imported by both. Documentation accuracy: the property suite's strategy comment described _reflog_created_from as scanning a splitlines() loop, which that same task's own Hypothesis-found fix had already replaced with an explicit split("\n"); and _reflog_created_from's docstring pointed at its module docstring "for why more than one entry can exist", which that docstring never says. Both now state what the code actually does, the second grounded in observed git output (reflog show prints newest-first, so a later "branch: Reset to <x>" entry pushes the creation entry down). Verified: uv run --frozen python3 -m pytest --no-cov -q, and uv run --frozen python3 .github/scripts/gitapex_gate_local_preflight.py. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merges the mandatory aggregate refactor/simplify pass over the full accumulated diff: reuses _gitapex_base_ref.py's run_git wrapper instead of a near-duplicate subprocess wrapper in _gitapex_preconditions.py, dedupes a verbatim-duplicated shallow-clone test fixture into tests/conftest.py, and fixes two cross-task doc staleness spots. No behavior change (7954 tests, all 44 local-preflight gates, before and after). Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nshallow fetch Refs #1566 Step 8 adversarial review, deterministic gate/check script scrutiny for .github/scripts/gitapex_gate_local_preflight.py's precondition gating. Defeat case, live-verified rather than reasoned about: when a repository's own origin is itself a shallow clone, "git fetch --unshallow" exits 0 with empty stderr and the repository is STILL shallow -- git propagates the source's own shallow boundary, deepens as far as the source can offer, and reports success. ensure_wired_gate_preconditions treated that zero exit as proof of full history and returned "safe to proceed", so the requires_full_history gate then ran against a still-shallow repo and failed reactively, mid-run, with its own confusing error -- exactly the outcome issue #1489 asked to replace with one clean top-line abort. This is an ordinary fixable gap, not a structural limit, so it is fixed rather than disclosed. The fix applies the same "never trust the fetch's exit code alone" discipline _gitapex_base_ref.py already establishes for its sibling fetch_destination_refspec/peeled_ref_exists pair, where the post-fetch verification is likewise the caller's job and not the fetch helper's: re-probe is_shallow_clone after the fetch and abort with exit 1 plus a message naming the likely cause. The re-probe runs only on the path that actually fetched, so an ordinary already-full-history run still makes exactly one git call. tests/conftest.py gains shallow_clone_of_a_shallow_origin, shared by both suites that pin the case. test__gitapex_preconditions.py pins the git behavior itself at the helper level (ensure_full_history's contract is deliberately unchanged -- it runs the fetch and raises on failure to run it; the guarantee belongs to the caller that needs it). test_gitapex_gate_local_preflight.py pins the runner-level abort, the probe ordering, the second probe failing to answer, and the same case end-to-end through main() against a real git fixture with nothing stubbed. One pre-existing test's stub described an impossible world (a fetch that succeeds while the repo stays shallow forever) and now models a genuinely successful unshallow instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…condition check Refs #1566 Step 8 adversarial review, deterministic gate/check script scrutiny for hooks/gitapex_check_python_precondition.py and its caller hooks/check-pr-skill-audit-disclosure.sh. Two confirmed defeat cases, both ordinary fixable gaps rather than structural limits, so both are fixed. 1. is_importable ran "python3 -c import <mod>" with no subprocess timeout, unlike both sibling precondition checkers (_gitapex_preconditions.py bounds at 60s via the shared run_git wrapper; gitapex_check_task_worktree _base.py at 10s). Defeat case: the condition this probe exists to catch -- a declared package python3 cannot actually use -- reshaped to fall just outside its own returncode == 0 heuristic, as a module whose import neither succeeds nor fails because its import-time code blocks. Verified live: a module sleeping 6s took the probe a full 6.0s, so no bound was applied at all. That probe runs inside a PreToolUse hook firing on every mcp__github__create_pull_request/update_pull_request call in this repository, so an unbounded probe stalls the very operation the hook gates and produces no verdict at all -- strictly worse than the silent tier-2 degrade #1547(a) reports. Now bounded by PROBE_TIMEOUT_SECONDS (10.0, a hang guard rather than a budget) with TimeoutExpired handled exactly like the already-handled OSError case: fail closed to "cannot confirm this is importable", with a warning naming what happened. find_missing_modules threads the same bound through per module. 2. The shell caller's own inconclusive-output path never fired. Its jq filter was '.missing // [] | join(", ")', and jq exits 0 with empty output when its input contains no values (verified directly) -- which is exactly what a crashed, missing-interpreter, or syntactically broken precondition script produces on stdout. That made missing_packages empty, skipped the documented Warning line, and skipped the deny: a completely silent no-op of the whole block, reintroducing #1547(a)'s own silent degrade through this check's own failure path while its comment claimed a warning it never printed. The filter now PROVES the output is the expected {"missing": [...]} object instead of merely failing to contradict it, so empty input, invalid JSON, a bare null or array, and an object without a missing array all reach the Warning. It still falls through to tier 2 rather than denying: a checker that cannot run is not evidence a dependency is missing, so the fix's narrow scope ("dependency-missing cause only") is preserved. Both cases are committed as permanent regression tests in the scripts' own suites, not run once and discarded. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…op does not fire here Refs #1566 Step 8 adversarial review, deterministic gate/check script scrutiny for skills/executing-a-branch-plan/scripts/gitapex_check_task_worktree_base.py. Defeat case, and it succeeds: issue #1508's own defect condition -- a task worktree forked from a base the shared plan branch has since advanced past -- reshaped to fall just outside this module's reflog heuristic, as a worktree whose branch was created from a remote-tracking ref rather than from a local branch by name. _verify_local_branch correctly refuses to resolve refs/heads/origin/main, the whole resolution chain returns "cannot resolve", and the check fails OPEN while the stale base it exists to catch is severely present. This is not hypothetical. It is the observed shape of a real branch-plan-task worktree in this repository: the step-8 review dispatch that found it was itself handed a worktree whose reflog read exactly "branch: Created from origin/main", sitting at the plan branch's merge-base with every one of that branch's commits missing. The check did not fire and the agent had to notice and reset by hand. Disclosed rather than fixed, because it is a structural limit of the reflog-based approach and not an ordinary gap this diff introduced. Resolving refs/remotes/origin/main instead would NOT be a fix: main is not the shared plan branch, so that comparison would deny every legitimately based task worktree the moment main advanced -- precisely the false, blast-radius-widening DENY the module's own design rejected the main-checkout heuristic for. A real fix needs the shared plan branch's name threaded in from the dispatching thread, a change to that dispatch step rather than to this module, named as the open follow-up it earns. The module's own docstring previously asserted the opposite of what was observed -- that a startpoint which is not a local branch is "essentially never" a genuine dispatch. The premise (such a startpoint is not a resolvable shared plan branch) is sound; the conclusion drawn from it was wrong, so it is corrected here rather than left as a claim the evidence contradicts. The fail-open warn message shown to an operator said the case was "expected" only for an unrelated worktree; it now names the genuine dispatch case too and tells the reader to verify the base by hand. Matching disclosures land in references/execution-and-dispatch.md and references/threat-model-and-authorization.md, and the case is committed as a permanent regression test rather than run once and discarded. Also pins the ssot registry's two-layer validation contract. The pydantic GatePreconditions model accepts requires_python_packages values the JSON Schema rejects ([] and [""]), but that asymmetry is deliberate and matches every sibling model in gitapex_scan_ssot_schema.py -- Gate.rule, Gate.policy_refs, GateFailMode.rationale and GateTargetEntry.ref all carry the schema's minLength/minItems on the schema side only, and GateFailMode's own docstring states the division outright. find_drift runs the strict layer first, so a degenerate list is rejected by the wired gate regardless. Tightening GatePreconditions alone would make it the single model duplicating a schema rule. The end-to-end rejection and the layering convention are pinned instead, so a future edit confronts the convention rather than silently diverging from it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merges the mandatory independent adversarial code review over the full accumulated diff. Constructed defeat cases against all three new deterministic gate/check scripts: gitapex_check_task_worktree_base.py (defeated by a worktree created from origin/main instead of a local branch name -- reproduced live in this review's own worktree; a real fix needs the branch name threaded in at dispatch, so disclosed as a structural limitation rather than patched around), the Python-import precondition probe (defeated by a blocking import with no timeout -- fixed, 10s bound added), and the local-preflight shallow-clone gating (defeated by an origin that is itself shallow, where `git fetch --unshallow` exits 0 but the repo stays shallow -- fixed by re-probing after the fetch). Also fixes a silent-skip bug in the shell hook's own jq parse of the precondition checker's output, and resolves the four items the prior refactor pass flagged as out of scope (one real fix: bounded timeout on the import probe; three confirmed correct as designed, with reasoning recorded in each script's own docs). Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…xture repo CI's pytest job failed on test_stale_base_is_NOT_detected_when_the_worktree_was_created_from_a_remote_tracking_ref with `git commit` exit 128 (unable to auto-detect email address). The test clones a fixture repo (`git clone`, which does not carry over the source repo's own local `user.*` config -- `_init_repo` sets that per-repo, not globally) and then commits directly into the clone. This passed locally only because this environment's own global git config already supplied an identity, papering over the gap; a CI runner with no global git identity configured hits it directly. Fixed by configuring user.email/user.name on the clone before its first commit, matching what _init_repo already does for a freshly-initialized repo. Reproduced the CI failure locally with an isolated HOME and GIT_CONFIG_NOSYSTEM=1 (no ambient git identity) before the fix, and confirmed it passes with the fix applied under the same isolation. Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Step 8's independent inner-layer security review (issue #1566) found a HIGH-severity vulnerability in gitapex_check_python_precondition.py's is_importable(): its subprocess.run call had no cwd=/env=, so it inherited this process's own ambient working directory. python3 -c implicitly prepends that cwd to sys.path[0], so a file shaped like the probed module's own name (e.g. pydantic.py) placed in whatever directory this checker happens to run from would shadow the real module and have its own top-level code executed inside the probe subprocess (CWE-427/CWE-829). check-pr-skill-audit-disclosure.sh's own new call site lacks the cd "$repo_root" its pre-existing sibling invocation has, so the cwd here is genuinely uncontrolled. Reproduced directly: a shadow pydantic.py printing a marker executed when probed without -I; python3 -I -c ... (isolated mode) blocks it, raising ModuleNotFoundError instead, while still finding both stdlib modules and a legitimately uv-installed pydantic. Fixed by adding -I to the probe's subprocess.run argv. -I also ignores PYTHONPATH, which broke three existing tests that used PYTHONPATH to place a fixture "blocking import" module on the probe's search path; fixed by substituting a fake python3-shaped interpreter that unconditionally sleeps past the timeout, decoupling those tests from PYTHONPATH entirely. Added a new regression test reproducing the shadow-file attack and asserting it is blocked. Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ture bootstrap Step 8's independent inner-layer reuse-and-simplification review (issue #1566) found gitapex_check_task_worktree_base.py's own _resolve_cwd was a byte-for-byte duplicate of gitapex_check_task_full_verification.py's identical helper. Extracted both into a shared _gitapex_hook_cwd.resolve_cwd, matching this directory's own established _gitapex_path_normalize.py convention for a sibling-import shared helper. The same review found tests/test_gitapex_check_task_worktree_base_properties.py's own _fixture_repo fixture reimplemented tests/conftest.py's init_git_repo/commit_file bootstrap inline instead of importing it (the same helpers test__gitapex_preconditions.py already imports from that directory). Switched to the shared helpers. Also fixes a doc-staleness finding from the same review: CONTRIBUTING.md and gitapex_gate_local_preflight.py's own DEFAULT_TIMEOUT_SECONDS comment both still claimed behind-base (issue #985) is this runner's first gate that makes a network call -- no longer accurate now that ensure_wired_gate_preconditions's own shallow-clone auto-unshallow fetch (issue #1566) can run even earlier, before any wired gate at all. Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ctly The prior dedup commit's own new _gitapex_hook_cwd.resolve_cwd had no direct test of its own -- each pre-existing test file only drove it indirectly through gitapex_check_task_worktree_base.py's or gitapex_check_task_full_verification.py's own main(). This repository's function-body-test-coverage gate (issue #1498) caught it: a new/changed function body in a skills/*/scripts/*.py file needs a same-diff test in tests/test_<stem>.py or tests/test_<stem>_properties.py that mentions it by name. Adds tests/test__gitapex_hook_cwd.py directly against the extracted helper, matching tests/test__gitapex_preconditions.py's own precedent for a leading-underscore shared module's test naming. Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…test Round-2's own independent review (issue #1566) found two convention deviations in tests/test__gitapex_hook_cwd.py: it used double-backtick RST-style code spans, inconsistent with every other test file in this repository (single backtick throughout, including this file's own sibling test__gitapex_preconditions.py); and its placement in top-level tests/ was not explained against skills/executing-a-branch-plan/scripts/'s own established convention of colocating a plain unit-test file next to its source (e.g. test_gitapex__path_normalize.py next to _gitapex_path_normalize.py). Fixed the backtick style directly. The placement is a deliberate, gate-driven choice, not an oversight -- kept in tests/ specifically because gitapex_gate_function_body_test_coverage.py's own "Existing-coverage check" unconditionally accepts only tests/test_{stem}.py or tests/test_{stem}_properties.py, regardless of the source file's own directory, so a colocated-only test would not clear that deterministic, PR-blocking gate for the new resolve_cwd function. Documented the reasoning directly in the file's own module docstring rather than moving it and breaking the gate. Refs #1566. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tvna
pushed a commit
that referenced
this pull request
Sep 2, 2026
…ll-implementation-lkdypz Brings in two independently-merged main-branch efforts that landed while this branch was in flight: - PR #1677 (issue #1676): rubric.md's Mixed-portability substitute for a Dimension-5-exempted target, plus that same session's own re-grade of executing-a-branch-plan's Mixed-portability finding against it (kept as its own deferral decision-log entry, appended after mine rather than overwritten). - PR #1675 (issues #1508/#1566): a new worktree-base precondition backstop (gitapex_check_task_worktree_base.py), chained into check_task_bash_safety.sh. Conflict resolution: - metadata/gitapex.yaml: kept this branch's full decision log and Frontier/requires/relatedTo values (main's own copy predates this branch's own fork and never carried any of that), appended main's new #1676 deferral entry, and added one missing decision-log entry for this branch's own prior failure-and-recovery.md reformatting commit. - references/execution-and-dispatch.md: this branch already retired this file (split into decomposition-and-dispatch.md / events-and-review-gate.md / failure-and-recovery.md, per issue #1662's own Round 10). Ported main's new "Worktree-base precondition backstop" subsection into decomposition-and-dispatch.md's own "Execution and dispatch" section (matching where its sibling subsections already live), demoted from ## to ###, and repointed every stale execution-and-dispatch.md cross-reference (in threat-model-and-authorization.md and gitapex_check_task_worktree_base.py/its test) to decomposition-and-dispatch.md. Verified post-merge: shape checker 54/54, drift scanner unchanged (same 4 pre-existing findings), full pytest suite passing (the one run mid- merge showed 7 gitapex_gate_commit_citation.py failures -- confirmed a transient artifact of running the suite while .git/MERGE_HEAD existed, merge_in_progress()'s own by-design merge-commit exemption, not a real defect; re-verified clean after this commit), all 44 local-preflight gates passed. Refs #1648, #1662, #1676
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a shared
preconditionsmechanism to the gate registry(
.gitapex/ssot.json), plus three concrete precondition-establishmentfixes (shallow-clone auto-unshallow before local-preflight runs, a
fail-closed dependency check before a PreToolUse hook's own bare-python3
tier-1 invocation, and a worktree merge-base assertion at task-dispatch
time), closing the shared root cause behind four duplicate-closed
findings consolidated under issue #1566.
Facts
closed as
duplicateof gate-proposal-umbrella: environment/repo-state preconditions #1566; its own re-verified Acceptance CriteriaMap (re-verification marker
2026-09-02T13:34:58Z) is restated inVerification below.
git rev-parse --is-shallow-repositoryreportedtruebefore an explicitgit fetch --unshallowwas run to unblock this session's own work -- the exactgate-proposal: retro #1545 repair 1: Shallow clone caused a false harden-checkout-pin-drift failure on first push #1546/gate-proposal: retro #1487 repair 2: harden-checkout-pin-drift failed locally due to a shallow clone #1489 defect class, reproduced directly rather than assumed.
harden-checkout-pin-drift(
.github/scripts/gitapex_scan_harden_checkout_pin_drift.py) is theonly currently-wired local-plane gate that needs full
git logdepth(to resolve a pinned action SHA's true last-touching commit); confirmed
by reading every local-plane gate's own
local_exclusion/local_stdintext in
.gitapex/ssot.json.hooks/check-pr-skill-audit-disclosure.sh's tier-1 path bare-invokes.github/scripts/gitapex_gate_skill_audit_disclosure.pyviapython3 "$full_gate"; that script transitively importspydantic(
gitapex_compute_skill_audit_flags.py->gitapex_detect_changed_gate_scripts.py,from pydantic import BaseModel, ValidationError, field_validator), confirmed by reading theimport chain directly.
SubagentStart-equivalent hook event -- confirmed byreading every hook reference in this skill's own scripts/references
tree; only
SubagentStopis used anywhere in this repository.gitapex_check_task_worktree_base.py: its own worktree started fromorigin/main(a remote-tracking ref) rather than a local branch name,which the checker's own local-branch resolution correctly refuses --
so the check failed open exactly when the base was, separately,
severely stale. Confirmed the checker's own docstring asserted the
opposite of this observed behavior; corrected the docstring and
disclosed the gap (see Risk / blast radius below) rather than papering
over it with a comparison against
mainthat would have widened theblast radius (denying every legitimate worktree whenever
mainadvances, since
mainis not the shared plan branch).Assumptions
None outstanding -- Task 4's own dispatch confirmed the shared plan
branch's name is not threaded in from the main thread today; it resolves
it from the worktree's own branch reflog instead (see Execution log).
Risk / blast radius
preconditionsfield is additive-only (.gitapex/ssot.schema.jsonkeeps
additionalProperties: false, bumpingmeta.schema_version1.4.0 -> 1.5.0); no existing gate entry's behavior changes unless it
explicitly declares a new
preconditionssub-key.gitapex_gate_local_preflight.py's new auto-unshallow path only fireswhen a wired gate declares
requires_full_historyAND the repo iscurrently shallow -- a no-op (no new subprocess) on an already-full
clone, which is the common case for most contributors' local machines.
check-pr-skill-audit-disclosure.sh's new precondition check narrowsscope precisely to the dependency-missing cause; every other tier-1
failure cause keeps falling through to the existing tier-2 warning path
unchanged -- CI's
skill-audit-gate.ymlremains the authoritativebackstop regardless.
own first Bash call (no
SubagentStarthook exists) -- disclosed as anasymmetric-strength backstop, not a true "before any tool call" gate.
Step 8's own adversarial review found and disclosed a second,
structural limitation on top of that: the checker's own branch-name
resolution only recognizes a local branch reflog entry, so a
worktree created directly from a remote-tracking ref (e.g.
origin/main) resolves to "cannot determine" and the check failsopen -- reproduced live inside that review's own dispatched worktree.
A real fix needs the shared branch name threaded in at dispatch time
(a separate, larger change to this skill's own dispatch mechanism);
documented as a known limitation in the checker's own docstring, the
operator-facing warning it emits, and both reference docs, rather than
patched around with a comparison against
main(which is not theshared plan branch and would falsely deny unrelated legitimate work).
screening-a-low-trust-contributionchecks2-8, dispatched to
review-persona) hard-flagged check 3 and/or check 4on all four tasks -- unconditional per that skill's own rule (fires on
path alone:
.gitapex/ssot.json/ssot.schema.json,agents/branch-plan-task.md,and every touched
.github/scripts/**/hooks/**/skills/*/scripts/**file). All are exactly this PR's own plan-sanctioned deliverable --
gate/hook infrastructure is what issue gate-proposal-umbrella: environment/repo-state preconditions #1566 asks to change. Every
review-personareport read the substantive diff content and found noweakened fail-closed logic and no instruction-bearing/injected content
(check 8 clear on all four). Disclosed here rather than silently
cleared, per this skill's own screening discipline.
each wave-1 task's own worktree had, outside its Owns list and
undisclosed in its own summary, deleted
docs/superpowers/plans/2026-09-02-gate-preconditions-mechanism-c0wevy.mdas an apparent unintended side effect. Corrected by restoring the
file's original blob into each task's merged commit rather than merging
the deletion.
before merge:
check-pr-skill-audit-disclosure.sh's newpython3 "$precondition_script" "${required_packages[@]}"call had no--separator before the array expansion, so a registry-declaredpackage name shaped like an argparse flag (e.g.
--help) would be readas CLI option syntax instead of data, silently skipping the new deny
path. Fixed by adding
--, plus an explicitWarning:line on theprecondition subprocess's own unparseable-output case.
defeat cases beyond wave-2's own screening: (1) the Python-import
precondition probe (
hooks/gitapex_check_python_precondition.py) hadno subprocess timeout -- a module whose import-time code blocks (a
network call, a lock,
time.sleep) would stall the PreToolUse hookindefinitely; fixed with a bounded (10s) timeout, fail-closed on
expiry, same as the existing
OSErrorhandling. (2) The local-preflightshallow-clone gate assumed
git fetch --unshallow's own exit 0 meansthe repo is now full history -- but when
originis itself a shallowclone, that fetch exits 0 while the repo stays shallow, letting the
gated run proceed and fail mid-run exactly as before this PR; fixed by
re-probing
is_shallow_cloneafter the fetch rather than trusting itsexit code alone. (3) A related bug found beyond the four handed-off
items: the shell hook's own
jq -r '.missing // []...'parse exits 0with empty output on empty/malformed input from a crashed precondition
checker -- silently skipping both the deny and the diagnostic warning,
reintroducing gate-proposal: retro #1545 repair 7: PR-body verdict line rejected by skill-audit-disclosure due to a degraded local fallback and an overly strict regex #1547(a)'s own silent-degrade shape through the new
check's own failure path. Fixed to verify the
{"missing": [...]}shape explicitly before trusting an empty result.
found and fixed a fourth:
hooks/gitapex_check_python_precondition.py'sis_importable()ran itspython3 -cprobe with nocwd=/env=control, so the probe's ambient working directory was implicitly
prepended to
sys.path[0]-- a HIGH-severity module-shadowingvulnerability (CWE-427/CWE-829). Fixed with
-I(isolated mode).Rollback
Revert this PR's merge commit. No schema/data migration beyond the
additive
ssot.schema.jsonfield andschema_versionbump (bothrevertible in the same commit); no new permission scope; no PR-blocking
CI behavior changed for any existing gate.
Verification
Restated from issue #1566's own re-verified Acceptance Criteria Map:
hooks/gitapex_check_python_precondition.pyadded (now with a bounded import-probe timeout and an isolated-mode (-I) probe subprocess, per two independent review rounds' own defeat cases);check-pr-skill-audit-disclosure.sh's tier-1 block denies immediately with an actionableuv sync --group devmessage when a required package is not importable, reading the required-package list from.gitapex/ssot.jsonrather than hardcoding it, and now verifies the checker's own JSON output shape before trusting an empty result. Regression tests cover the flag-shaped-argv defeat case, the blocking-import defeat case, the malformed-checker-output case, and the cwd-shadowing defeat case; every other tier-1 failure cause still falls through to tier 2 unchanged..github/scripts/_gitapex_preconditions.pyadded (is_shallow_clone/ensure_full_history, reusing_gitapex_base_ref.py's sharedrun_gitwrapper after Step 8's own refactor pass);gitapex_gate_local_preflight.pyreads every wired gate's ownpreconditions.requires_full_historyflag from the registry (no gate id hardcoded) and auto-runsgit fetch --unshallowonce, before any wired gate runs, when needed -- now re-verifying the repo is actually non-shallow afterward rather than trusting the fetch's own exit code alone (Step 8's own defeat case against an itself-shallow origin). Aborts with one clear top-line message if either step fails, never surfacing reactively mid-run. No-op when the repo is already non-shallow or no wired gate needs it. Includes a live reproduction of the original defect against a real shallow clone of this repository (fails pre-fix, passes post-fix).gitapex_check_task_worktree_base.pyadded, chained intocheck_task_bash_safety.sh's existing PreToolUse Bash hook; reproduces the exact stale-fork-point shape and confirms deny, plus confirms allow on a matching fork point and fail-open on an unresolvable branch name (including the remote-tracking-ref case Step 8's own adversarial review found and documented).tests/test_gitapex_check_task_worktree_base_properties.py(Hypothesis property tests, required by the already-wireddetection-logic-property-coverage/function-body-test-coveragegates) found and fixed a real line-splitting bug in the new module.Full repo suite (7976 tests) and all 44
gitapex_gate_local_preflight.pygates pass on the current head (
724cd21d).Checklist
skills/*/SKILL.md, adocs/superpowers/specs/*.mddesign doc, a security-relevant skill, or a deterministic checker script (skills/*/scripts/*.py,evals/scripts/*.py,.github/scripts/*.py), a## Skill audit evidencesection discloses the required verdicts/waivers (see.github/scripts/gitapex_gate_skill_audit_disclosure.py)evals/*/split.md, that entry discloses a Transfer check line (see.github/scripts/gitapex_gate_transfer_check_disclosure.py) -- N/A, noevals/*/split.mdtouchedskills/*/SKILL.md's Stop-boundary bullets or named dispatch branches,evals//tasks/*.yamlgained at least as many new fixtures (see.github/scripts/gitapex_gate_skill_branch_fixture_coverage.py) -- N/A, noSKILL.mdtouchedSkill audit evidence
This PR adds/modifies deterministic checker scripts (
.github/scripts/_gitapex_preconditions.py,.github/scripts/gitapex_gate_local_preflight.py,.github/scripts/gitapex_scan_ssot_schema.py,hooks/gitapex_check_python_precondition.py,hooks/check-pr-skill-audit-disclosure.sh,skills/executing-a-branch-plan/scripts/gitapex_check_task_worktree_base.py,skills/executing-a-branch-plan/scripts/_gitapex_hook_cwd.py) and.gitapex/ssot.jsonitself.Step 8 dispatched a fresh, independent subagent (no prior design
context) to review the full accumulated diff. It found and fixed a
real bug beyond the mandatory defeat-case scope:
check-pr-skill-audit-disclosure.sh'sown
jq -r '.missing // []...'parse silently accepted empty/malformedprecondition-checker output as "nothing missing," reintroducing
gate-proposal: retro #1545 repair 7: PR-body verdict line rejected by skill-audit-disclosure due to a degraded local fallback and an overly strict regex #1547(a)'s own silent-degrade shape through the new check's own
failure path. Fixed to verify the checker's own JSON shape explicitly.
A second, post-merge independent review round (see Independent review
verdict below) found and fixed the HIGH-severity cwd-shadowing bug in
the same probe script.
skills/evaluating-deterministic-gate-quality/references/dimensions.md,dimension 15 (fail-closed default on incomplete or malformed input)
specifically: the same Step 8 adversarial-review dispatch
independently constructed and ran, live, a boundary/missing-dependency
input against each of the three new gate/check scripts (see the three
defeat cases below) rather than relying on the tasks' own bundled
happy-path tests alone.
defeat each of the three new deterministic gate/check scripts on its
own terms (not merely exercising its happy path), per
references/refactor-and-review-gate.md's own mandatory"Deterministic gate/check script scrutiny":
gitapex_check_task_worktree_base.py: a worktree created fromorigin/main(a remote-tracking ref) instead of a local branchname -- reproduced live inside the review's own dispatched
worktree, whose own reflog read exactly this shape with the branch
severely stale. The check failed open. A real fix needs the shared
branch name threaded in at dispatch time (a larger, separate change
to this skill's own dispatch mechanism); explicitly disclosed as a
known structural limitation in the checker's own docstring, its
operator-facing warning, and both reference docs, rather than
patched around in a way that would widen the check's own blast
radius (a comparison against
mainwould deny legitimate workwhenever
main-- not the shared plan branch -- simply advances).hooks/gitapex_check_python_precondition.py: a module whoseimport-time code blocks (
time.sleep) rather than succeeding orfailing, falling outside the probe's own
returncode == 0heuristic entirely. Fixed with a bounded (10s) timeout,
fail-closed on expiry. The same script's own probe subprocess was
later found, in the post-merge independent review round, to also
be defeatable by a cwd-planted shadow file -- see Independent
review verdict below.
gitapex_gate_local_preflight.py's new precondition-gating logic:an
originthat is itself a shallow clone, wheregit fetch --unshallowexits 0 while the repo remains shallow. Fixed byre-verifying
is_shallow_clone()after the fetch rather thantrusting its exit code alone.
Each defeat case is committed as a permanent regression test in its
own script's test suite, not a throwaway.
Merge gate: independent review
This PR is also subject to the
independent-review-pendingrequiredstatus check (see
.github/workflows/independent-review-pending.yml/.github/scripts/gitapex_gate_independent_review_pending.py). It stayspending/failing until a
## Independent review verdictsection namingthis PR's current head commit is recorded in this body --
drafting-a-pr-to-merge's own Step 8 records it once its independentreview completes. There is nothing for you to do here now: do not
pre-fill this section yourself, and do not remove this note.
Independent review verdict
Outer layer: GitHub Copilot review requested twice
(mcp__github__request_copilot_review) against this PR, most recently
against this exact head commit. No response was posted from
copilot-pull-request-reviewer[bot] either time (confirmed via
pull_request_read method=get_reviews, empty result). CodeRabbit posted
only its own automatic skip notice ("This repository does not receive
automatic reviews because it has fewer than 10 stars"). No "Claude Code
Review" GitHub App is configured on this repository. This layer is
treated as unreachable / not configured, disclosed rather than silently
omitted.
Inner layer (reviewing-an-artifact skill, low effort): two rounds ran
against this PR's diff.
Round 1 (against commit a70fec4): 5-axis fan-out found one
HIGH-severity confirmed finding and three lower-severity confirmed
findings, all fixed:
gitapex_check_python_precondition.py'sis_importable()spawned its
python3 -cprobe subprocess with nocwd=/env=control.
python3 -cimplicitly prepends the process's own ambientworking directory to
sys.path[0], so a file shaped like theprobed module's own name (e.g.
pydantic.py) placed in thatdirectory would be imported -- and its top-level code executed --
instead of the real module (CWE-427/CWE-829). Independently
reproduced directly (a planted shadow file executed without the
fix) before fixing. Fixed by adding
-I(isolated mode) to theprobe's
subprocess.runargv.gitapex_check_task_worktree_base.py's own_resolve_cwdwas a byte-for-byte duplicate ofgitapex_check_task_full_verification.py's identical helper --extracted into a new shared
skills/executing-a-branch-plan/scripts/_gitapex_hook_cwd.py,matching this directory's own established
_gitapex_path_normalize.pysibling-import convention.tests/test_gitapex_check_task_worktree_base_properties.py's owngit-repo fixture reimplemented
tests/conftest.py'sinit_git_repo/commit_filebootstrap inline instead of importingit -- switched to the shared helpers.
CONTRIBUTING.mdandgitapex_gate_local_preflight.py's ownDEFAULT_TIMEOUT_SECONDScomment both still claimedbehind-base(issue feat(gates): fail the local preflight when the branch is behind its base #985) is this runner's first gate that makes a network
call -- no longer accurate once
ensure_wired_gate_preconditions'sown shallow-clone auto-unshallow fetch (this PR) can run even
earlier, before any wired gate at all. Both updated.
All four fixed and pushed (commits f41e9fd, 31b627b, d338d52),
plus one deterministic-gate-driven addition the fix itself required:
the extracted
resolve_cwdhad no test of its own directly mentioningit by name, which
gitapex_gate_function_body_test_coverage.py's own"Existing-coverage check" requires in the same diff -- added
tests/test__gitapex_hook_cwd.py(commit d338d52), confirmed via aclean local
gitapex_gate_local_preflight.pyrun (44/44) afterward.Round 2 (against the diff between commits a70fec4 and d338d52, i.e.
re-reviewing round 1's own fix): correctness, blast-radius, and
reuse-and-simplification axes report zero findings. Convention axis
found two real nits, both fixed (commit 724cd21): the new
tests/test__gitapex_hook_cwd.pyused double-backtick RST-style codespans where every comparable test file in this repository uses single
backticks; and its top-level
tests/placement (rather thancolocated next to
_gitapex_hook_cwd.py, this directory's own usualconvention for a plain unit test) was undocumented -- now explained
directly in the file's own module docstring as a deliberate,
gate-driven placement (
gitapex_gate_function_body_test_coverage.pyonly ever accepts
tests/test_{stem}.py, unconditionally, regardlessof the source file's own directory).
Security axis raised one concern on the
-Ifix itself, at highconfidence on documented semantics but explicitly flagged as not
independently executed by that reviewer: current CPython docs attach
"Added in version 3.11" to the
-Pflag specifically, and describe-Ias implying-P-- read literally, this could mean-Ialonedid not exclude the probe's cwd from
sys.pathbefore Python 3.11,leaving a residual gap on an older ambient
python3. Independentlyverified empirically rather than taken on the reviewer's own
confidence: ran the exact shadow-file reproduction directly against
every Python interpreter available in this session's own environment
-- 3.10.20, 3.11.15, 3.12.3, and 3.13.12 -- confirming via
sys.pathinspection that
-Iexcludes the empty-string cwd entry on 3.10 (aversion that predates
-Pexisting as a flag at all:python3.10 -Pitself errors "Unknown option: -P") exactly as it does on 3.11+, and
confirming the shadow-import attempt correctly raises
ModuleNotFoundErrorunder-Ion all four versions tested. Thisfinding did not reproduce and is not treated as a confirmed defect;
disclosed anyway per this skill's own unconditional security-tier
rule, together with the one residual gap this verification does not
close: Python versions older than 3.10 were not available to test
directly in this environment. Practical exposure is low regardless --
this repository's own
requires-python = ">=3.12"and its documenteduv-provisioned install path are what the probe's PATH-resolvedpython3is expected to resolve to in this repository's owndeployment.
Related Issue
Refs #1566
Execution log
PlanApproved{run_id: 1481e3d}
TaskStarted{run_id: 1481e3d, task_id: task-1}
TaskStarted{run_id: 1481e3d, task_id: task-4}
TaskCompleted{run_id: 1481e3d, task_id: task-1, commit_sha: 28f01ed}
TaskCompleted{run_id: 1481e3d, task_id: task-4, commit_sha: 0427602}
TaskStarted{run_id: 1481e3d, task_id: task-2}
TaskStarted{run_id: 1481e3d, task_id: task-3}
TaskCompleted{run_id: 1481e3d, task_id: task-2, commit_sha: 9765d45}
TaskCompleted{run_id: 1481e3d, task_id: task-3, commit_sha: c00a847}
StageDeviated{run_id: 1481e3d, task_id: null, reason: "step 8 refactor/simplify pass merged (commit 98f35e1)", action: retry}
StageDeviated{run_id: 1481e3d, task_id: null, reason: "step 8 adversarial code review merged, 3 defeat cases found and fixed/disclosed (commit 6a650fe)", action: retry}
StageDeviated{run_id: 1481e3d, task_id: null, reason: "step 8 independent review round 1 findings fixed and verified (commits f41e9fd, 31b627b, d338d52)", action: retry}
StageDeviated{run_id: 1481e3d, task_id: null, reason: "step 8 independent review round 2 findings fixed and verified (commit 724cd21)", action: retry}