Skip to content

v0.7.10

Choose a tag to compare

@ViperJuice ViperJuice released this 21 Jul 02:57
· 920 commits to main since this release
v0.7.10
8900c9d

verification.json records the phase alias on the train re-verify path (#236)

Follow-up to #235 (ah#85b), which threaded the live run alias into run_verification
on the execute path. The train re-verify path (train_runner._live_reverify) also calls
run_verification but passed no phase_alias, so verification.json fell back to
current_phase'unknown' even though a phase alias was already resolved in scope. It
now threads the resolved (reconcile-recomputed) phase, so a re-verify records the actual
phase rather than 'unknown'. (#236)

Panel/advisor-board: a codex review is no longer mislabeled DEGRADED for discussing auth (#252)

_classify_leg scanned a leg's full transcript for auth-error signatures; because the
codex leg's log_text includes its stdout+stderr (codex echoes its own review onto
stderr), a clean, conforming codex review whose prose merely discusses "unauthorized" /
"rate limit exceeded" (routine in security/auth reviews) was forced to DEGRADED and the
operator told to discard a valid review. A conforming review-mode rc==0 verdict is
now classified OK before the auth scan (its terminal-verdict predicate is one a
de-authed CLI can't fake). Advisory mode keeps auth-scan-first (its weaker length
predicate could otherwise fail open on a real banner), preserving the #63 behavior.
Fail-closed for genuine auth failures is unchanged. (#252)

Dual-declared run-family options survive before the subcommand (#233)

Follow-up to the argparse copy-back clobber fixed in #84/#232. Those PRs added
default=argparse.SUPPRESS to the common subparser args (_add_common_subparser_args),
but the same clobber still hit options that are dual-declared (top-level and on the
run/resume/dry-run subparser) outside that helper. So phase-loop --force-replan run
silently dropped to False, phase-loop --allow-cross-phase-dirty REASON run to None,
and likewise --rotate-executors, --rotation-mode, --rotation-on-policy-pin (the last
two also clobbered the top-level "phase"/"skip" defaults to None, masked at the call
site by or "phase"/or "skip"), --full-phase, and --no-deprecation-hints. Each
subparser copy now carries default=argparse.SUPPRESS (identical to #232's pattern), so a
value parsed before the subcommand survives in both option positions. The top-level parser
still declares each option with its concrete default, so the attribute is never absent from
the final namespace. CLI-only fix. (#233)

Planner skills emit and reference goal IDs — goal-ID Increment 2 (#211)

The planner skills now produce roadmaps/plans that use the decidable goal-coverage
mechanism shipped in Increment 1 (#247), so it applies to real roadmaps, not just
fixtures. All four harnesses' phase-roadmap-builder emit each exit-criterion led by a
stable goal ID — - [ ] EC-<ALIAS>-<N> — <assertion> — with the convention documented
(all-or-none per phase, unique + alias-scoped, gaps allowed, never reuse/renumber a
deleted ID). All four harnesses' plan-phase and plan-detailed author acceptance
items that reference the roadmap goal by its EC-<ALIAS>-<N> ID and name the
proving command, instead of restating (and drifting from) the goal text — so validation
always checks the plan against the original goal and a plan can never silently weaken
it. validate_plan_doc.py gains a (P) warn: when the anchored roadmap phase declares
goal IDs, every declared ID should be referenced by an acceptance item and every
reference should resolve (no dangling); legacy phases with no goal IDs get no finding.

Opt-in and non-breaking: existing specs/phase-plans-v*.md roadmaps are untouched and
stay not_applicable until re-authored; a phase that declares no goal IDs uses the
prior testable-assertion authoring. No runtime code change (Increment 1 ships the
mechanism); the skills bundle is regenerated and byte-parity is green. (#211)

Broker reconciles publish scope with the branch's actual content (#202)

publish_committed_branch publishes the whole committed branch by (repo, branch, head_sha), and the admission's approval digest covers the coordinator-supplied
owned_paths — but the broker never checked that those owned_paths actually
matched what the branch changed. The broker now re-derives the branch's diff vs its
declared base itself
(origin/<base>...head_sha, three-dot — the same derivation
the #201 coordinator uses) and refuses to publish when the admitted owned_paths do
not cover what the branch actually changed, catching drift or coordinator bugs
where the declared scope diverges from the real branch content. Every outcome fails
closed before any push, and each is a proven no-effect (no_effect_terminal_proven
— the push is never reached) distinguished only by its detail string: a changed path
outside the admitted scope → owned-scope-exceeded:<paths>; a non-empty owned_paths
with an empty branch diff → owned-scope-empty-diff (also catches a gamed base==head
ref); a git-diff failure → owned-scope-diff-failed. A #202 reject has zero mutation
ambiguity (nothing was pushed), so it is not outcome_ambiguous_blocked — which is
permanent and poisons the repo's broker epoch, a blast radius a purely-local read-only
git failure must not trigger. no_effect_terminal_proven is also the only valid
provider_call_in_flight → reject transition (rejected_before_start is reachable only
pre-intent, and the service records intent before the adapter runs). Directory
owned_paths entries cover files beneath them, so an over-specified scope never
false-rejects.

Scope note: base is a coordinator-supplied ref name (not the digest-bound
base_sha), so this reconciles against the declared base — it closes accidental
drift and coordinator bugs, not a coordinator that deliberately games the base ref
(binding base into the approval digest would be the stronger, separate step). Only
the run_train prebuilt coordinator path reaches this check today, and it runs in the
node workspace where origin/<base> is already present, so there is no added fetch or
regression. BrokerRequest gains a base field (default main);
publish_from_worktree gains a matching base parameter; the run_train prebuilt
path threads the same _DEFAULT_BASE its owned-paths were derived from. Follow-up
hardening from the #201 panel (codex + grok). (#202)

codex review-leg cannot write the reviewed live tree (#177)

The product-loop review action's codex leg was built with --sandbox danger-full-access (or --dangerously-bypass-approvals-and-sandbox) against the
live worktree — the same as write actions — so a codex review leg could mutate the
reviewed tree. The review action now points codex at a staged copy instead of
the live repo (matching the agy review leg), the airtight barrier for
IF-0-SANDBOX-1 (build_codex_command gains a read_only flag;
build_codex_launch_spec threads read_only=(action == "review"); all winning over
bypass_approvals):

  • --cd <staged copy> — the review leg's working directory is a throwaway,
    gitignore-aware copy of the tree (via the same _stage_review_tree agy uses),
    materialized at launch and cleaned afterward. codex cannot reach the live tree at
    all, regardless of which config layer (user / system / enterprise) declares an
    out-of-sandbox MCP server. This is the primary guarantee.
  • --sandbox read-only and --ignore-user-config are kept as defense in depth
    (block shell writes; drop user-config MCP; auth still resolves via CODEX_HOME;
    --model/model_reasoning_effort are passed explicitly so the run is hermetic),
    and --skip-git-repo-check because the staged copy carries no .git.

A cross-vendor review blocked an earlier flag-only fix (--sandbox read-only +
--ignore-user-config) as an overclaim: MCP tools run outside codex's shell
sandbox (openai/codex#4152) and can be declared in config layers --ignore-user-config
does not drop, so only the staged copy makes the guarantee airtight. This brings the
codex review leg to the IF-0-SANDBOX-1 bar the other vendors already meet (agy =
staged copy, grok = read-only --tools allow-list, claude = plan/read-only), with a
filesystem-level write-proof test.

The shared review-stage cleanup was also hardened (benefiting the agy leg too): the
launch-time cleanup now removes the exact copy paths the materializer created,
rather than inferring ownership from an --add-dir/--cd argv basename — so an
execute run against a live repo that happens to be named pl-review-stage-* can
never be deleted. Stage materialization now self-cleans if the copy or a later
launch-time resolution step (e.g. output-schema materialization) fails, closing a leak
window since staging runs before the launch cleanup finally.
(#177)

Goal-ID single source of truth — Increment 1 (#211)

Redefines #211 from a fuzzy text-diff audit (proven undecidable) into a decidable
goal-coverage check by removing the duplication between a roadmap's goals and a
plan's restatement of them. Roadmap phase exit-criteria may now carry stable
EC-<ALIAS>-<N> goal IDs (mirroring the IF-0-<ALIAS>-<N> gate scheme);
roadmap_lint reconciles them (alias-scoped, unique, all-or-none per phase, gaps
allowed so a deleted criterion never forces a renumber that would silently re-bind a
downstream reference). Phase.exit_criteria stays list[str] (API-compatible); IDs
are exposed via an additive accessor.

A plan's ## Acceptance Criteria items reference the goal IDs (item-leading:
- [ ] EC-P1-1 — proven by <test>) instead of restating the goal. The new
goal_coverage.check_goal_coverage verifies — by pure set membership, no
word-matching — that every declared goal ID is referenced by ≥1 acceptance item;
an unreferenced goal or a dangling/typo'd reference is a contract_bug, a prose
mention (not item-leading) does not count, and a phase with no EC-IDs is
not_applicable (legacy, opt-in per phase — nothing existing breaks). It runs at
three points, all pure Python with no EmitPhaseCloseout schema change: a
goal-coverage-audit CLI (exit 0/1/2), the phase-loop preflight, and a
closeout re-check (mirroring the IF-gate Produces precedent, closing the
window where a plan is edited mid-execution and loses a reference). Preflight and
closeout are warn-default / opt-in block via PHASE_LOOP_ACCEPTANCE_ENFORCE=block,
never human_required.

Honest scope: this guarantees completeness (no goal silently forgotten); it
does not verify adequacy (that the referenced test actually discharges the
goal) — that stays with code review + evidence authenticity (#91). It makes weak
evidence human-reviewable at the reference point rather than hidden in a paraphrase.
Increment 2 (planner skills emitting/referencing IDs by default + roadmap migration)
is deferred behind a go/no-go. Design + hardenings converged through a cross-vendor
plan review (codex + gemini + Fable).

Preserve raw failure diagnostics on verification failure (#209)

When runner-executed verification failed, the runner-owned verdict scrubbed the
failing stage down to a bare exit_code — the raw reason survived in
verification.log but never reached the terminal/closeout record, a named
contributor to multi-day thrash (#213). The verification verdict
(VerificationArtifactValidation.to_json(), consumed into the persisted closeout
record) now carries a diagnostics list: one entry per failing stage, in declared
order (commandsenv_refreshsuite), each with a typed failure_kind
(timeout / error / nonzero_exit), the stage's argv/exit_code, and a
bounded raw_tail sliced from that stage's exact log region. A failed verdict
can no longer be diagnostic-empty: a silent (output-less) failure still surfaces
typed context flagged diagnostic_status: missing_output. Multi-step chains keep
per-step order and reduce fail-closed (an earlier failing step blocks even if a
later step passes).

To localize each stage exactly, verification.json is bumped to schema v2
(additive): every stage may carry log_end_offset + a runner-observed
failure_kind, and env_refresh/suite now record their own log_offset (the
suite's start offset was previously discarded, making post-hoc boundaries
uncomputable). failure_kind is captured at execution time, never re-derived from
exit_code, so a child that itself returns 124/127 is not mislabeled a
timeout/missing-executable. v1 artifacts still load (new fields default null;
load_verification_artifact accepts schema_version in {1, 2}). Diagnostics are
built only on the sha-authenticated nonzero_exit branch — an unauthenticated log
(sha mismatch / missing) blocks on the integrity failure without surfacing a
possibly-forged tail. The bounded raw_tail is an excerpt of bytes already
persisted in verification.log at the same trust level; a follow-up covers opt-in
closeout-diagnostic redaction. Design converged through a cross-vendor plan review
(codex + gemini + Fable) that rejected a post-hoc verdict-only approach.

Versioned/absolute suite-interpreter guard, redone robustly (#221)

#219a's interpreter shim prepended a PATH dir whose bare python/python3 resolve to a
requires-python-satisfying interpreter, but a suite (or a plan-level commands verification
bullet) that explicitly named a versioned interpreter — python3.10 — bypassed the bare-name
shim and could run GREEN under an unsupported interpreter. The regex string-scan detector #220
shipped for this was removed as unsound (fail-open on shell metacharacters + wired only to
suite_command; false-block on pythonX.Y string literals / env paths). It is now redone at
executable-resolution level: whenever a requires-python constraint exists, the shim also
shadows every non-satisfying python3.X name (below OR above a bounded specifier, via the
PEP 440 predicate) with a fail-closed wrapper, on every path where the shim is built (pin,
host-default-satisfies, and auto-resolve). Because it intercepts the executable name (not a command
string), python3.10&&pytest is caught while python3.12 -c 'print("python3.10")' and
PYTHONPATH=/opt/python3.10 pytest are no longer false-blocked, and both the suite_command and
commands paths are covered. The shadow set spans a wide fixed name range (python3.0python3.39
plus python2*), decoupled from the bounded host-probe list, so an old python3.7 or a future
python3.15 cannot reopen the hole. Patch-level constraints are handled precisely: a candidate
present on the host is compared at its full version, so python3.11 is shadowed under
<3.11.5 when the host's is 3.11.9 (fail-closed, not fail-open) and is NOT shadowed under
>=3.11.5 when the host's is 3.11.9 (no false-block). When the host default already satisfies, the
shim carries only the shadows and leaves bare python/python3 untouched, so an active venv is
preserved. The interpreter version is probed with cwd=repo, so a version-manager shim
(pyenv/asdf) is measured under the same .python-version context the suite runs in. A login
shell
(bash -lc) that re-sources a PATH-reordering profile is handled by re-prepending the shim
inside the -c payload (which runs after profile loading), so the shim wins even against a profile
that puts a below-floor python3.X first. Escape hatches (the operator's explicit declared
environment, each strictly harder for an operator than the guard it evades): an absolute-path
interpreter (/usr/bin/python3.10) which bypasses PATH entirely; and — deferred to #241 for
hardening with proper planning — exotic bash --login -O opt -c option forms and an interpreter
absent at resolve time but introduced by the login profile under a patch-level constraint.

Reconcile can recover a completed phase from a tracked closeout artifact (#90)

phase-loop reconcile --verification-log <closeout.md> rejected a tracked, committed closeout
markdown as malformed_artifact because it validates the path as a runner-owned
verification.json. Operators whose roadmap already reached CLOSEOUT with durable committed
closeout artifacts — but no ephemeral .phase-loop/runs/verification.json (e.g. after an
interrupted session) — could not rehydrate completed phase state and were stuck re-planning from
the start. Reconcile now accepts a new --closeout-artifact <path> flag for artifact-backed
recovery: it adopts a git-tracked, committed closeout markdown as recovery evidence
(provenance tracked_closeout_artifact), requiring --closeout-commit, --repair-summary, and
--verification-status. This is an audit anchor + provenance label for an explicit,
operator-reasoned manual recovery — not a runner verification pass and not an authorization gate.
The evidence is bound before acceptance: the --closeout-commit must resolve to a real commit
reachable from HEAD (the index :0, tree-ish, and orphan commits are rejected), the path must
be a non-empty regular blob at that commit (a directory, symlink, or gitlink is rejected), and
the basename or content must reference the phase (an unrelated tracked file is rejected). It never
satisfies a phase that hard-requires runner verification (RG / IF-0-RG-1), is mutually exclusive
with --verification-log, and the existing runner-verification path is unchanged (still rejects a
markdown as malformed_artifact). The provenance is surfaced at the status boundary
(Closeout verification: passed (recovery evidence: tracked_closeout_artifact)), so consumers can
tell a recovery apart from a runner pass. This is an audit anchor for an operator-reasoned
recovery, not an authorization gate; ancestry is checked with grafts/replace-refs neutralized, so
an active .git/info/grafts or a shallow clone is rejected (closeout_commit_ancestry_untrusted)
— unshallow (git fetch --unshallow) before recovering in a shallow checkout.

Reconcile/status survive a relocated repo root (#85)

.phase-loop/ state and events persist absolute repo/roadmap paths. When the directory
was copied into a git worktree or the repo root moved/renamed (e.g.
/home/user/code/avatar-client/mnt/workspace/worktrees/…), reconcile/status compared
the stored absolute roadmap path against the live one, found them unequal, and silently
discarded every persisted phase status — replaying as all-unplanned. The roadmap
path-equality gates in reconcile and classifier now match repo-relative (via the new
runtime_paths.roadmap_paths_match, which keeps identical-absolute as a fast path and falls
back to non-match when a path cannot be relativized), so persisted status and operator
breakglass attestations survive relocation. Content-SHA provenance
(roadmap_sha256/phase_sha256) remains the integrity backstop at every gate, so a genuinely
different roadmap sharing the relative path is still rejected. A single informational
repo_relocated ledger warning is emitted once per reconcile (whether the relocation is
detected from the persisted snapshot or, for an events-only replay, from the first relocated
event). Operator breakglass SL-2 attestations (lane_ir_override, closeout_allow_unowned)
deliberately do not relocate: an operator authorization is bound to the repo root it was
granted in, so those two gates stay fail-closed to the original absolute path and require
re-attestation in the new location. verification.json is untouched.

verification.json records the live run phase alias (#85)

verification.json's phase_alias was re-derived from .phase-loop/state.json:current_phase,
so it could disagree with the run's terminal-summary.json phase (and mis-attribute the phase)
after a mid-run roadmap amendment changed current_phase. On the execute path the runner now
threads the live run alias into run_verification (new optional phase_alias param), so the
artifact is attributed to the phase that actually produced it. The PHASE_LOOP_PHASE_ALIAS env
override still wins, and callers with no live run alias (hotfix / train re-verify) keep the
current_phase fallback — behavior unchanged for them.

Common options before the subcommand no longer silently reset (#84)

phase-loop --phase ROOM run (and any common option placed BEFORE the subcommand —
--max-phases, --model, --effort, --json, --dry-run, …) was silently reset to its
default by the subcommand parser's copy-back, because the per-subcommand copies of those
args lacked default=argparse.SUPPRESS. --phase ROOM became phase=None, so the runner
fell through to repairing a blocked phase instead of dispatching the requested one. Every
common subparser arg now carries SUPPRESS (matching --closeout-mode/--pipeline-mode/
--lane-scheduler), so a value placed before the subcommand survives — an explicit
--phase reaches the dispatcher and the requested phase runs. The dispatcher was correct;
this is a CLI-only fix.

Panel/CR grok leg no longer errors on default runs — effort clamped to grok's CLI ceiling (#222)

The panel / advisor-board grok leg hard-coded --reasoning-effort max on the default
path, but grok's CLI accepts only high | medium | low and rejects max, so the grok
leg errored on every default panel/CR run (it never contributed a review). The
panel-path effort is now clamped to grok's ceiling (max → high), so the grok leg runs
at its real maximum instead of failing. Sibling of the grokexec-path clamp in #224.
(#222)

grokexec leg clamps --reasoning-effort to grok's CLI subset (#224)

The grokexec/launcher grok leg passed the requested effort raw to
grok --reasoning-effort, but grok's CLI accepts only high | medium | low, so an
explicit max / xhigh / minimal grokexec run (all in NORMALIZED_EFFORT_LEVELS)
crashed grok (unknown effort level 'max'; use one of: high, medium, low). A new
CLI-boundary clamp launcher._grok_cli_effort translates minimal→low and
xhigh/maxhigh (grok's ceiling), mirroring codex's max→xhigh and the panel-path fix
in #222/#225 — so an explicit high-effort grokexec run is honored at grok's ceiling instead
of crashing. The default path (medium) was already valid. The grok provider policy's
misleading "accepts a superset / no clamp needed" note and its dead identity effort_map
are corrected. Sibling of #222.

Advisor-board available_panel_legs() exposes grok (#171)

The documented panel preflight available_panel_legs() now considers all four vendors
(codex, gemini, claude, grok) and returns those whose CLI is installed — so a caller
whose gemini/agy leg is down transparently reaches a 4th independent vendor (grok)
through invoke_panel(..., available_panel_legs(), ...) instead of hand-rolling the grok
CLI (which naively inlines the bundle into argv and chokes). Availability-aware: grok
appears only when the grok CLI is present, so a host without it still returns the exact
frozen 3-tuple. The byte-frozen PANEL_LEGS keystone and the advisor-board goldens are
unchanged — grok is added only to the separate availability list the preflight iterates.

Panel Fable/Claude TUI leg clears the workspace-trust gate (#196, #223)

The advisor-board Fable/Claude reviewer leg drives Claude Code headless under a
self-allocated PTY. Claude Code shows an interactive workspace-trust modal for
the fresh scratch cwd before it accepts a prompt; the leg used to bracket-paste
the review prompt after a fixed 8s delay, landing it in the Enter y/n: field —
so no reviewer session ever started and the #188 liveness monitor reclaimed it
after 180s and mislabeled it claude_tui_stalled. Net effect: the Fable
correctness seat was effectively dead on panels/CRs. Root cause and the exact
modal/editor rendering were confirmed by a real PTY capture on Claude Code 2.1.208.

  • Startup state machine (fix). _run_claude_tui_session now detects the
    workspace-trust modal against the accumulated de-ANSI'd screen (the modal spans
    multiple lines), answers y exactly once — strictly pre-submit, path-scoped to
    the harness-created scratch cwd, and disarmed the instant the review prompt is
    pasted
    — then submits only when the editor is quiescent after real post-gate
    output (never on a blind timer into a possibly-modal screen). The PTY is opened at
    a wide window so a long /tmp cwd path renders un-wrapped. Because auto-answering
    is disabled once the prompt is submitted, review output or a reviewed diff that
    contains the trust strings can never inject a keystroke or mis-classify a healthy
    review.
  • Typed, fail-closed diagnostics (fix). An uncleared trust gate now yields a
    typed claude_tui_workspace_trust_blocked, and a never-ready editor yields
    claude_tui_editor_not_ready — both evaluated before the 180s generic stall
    and surfaced as DEGRADED (not the misleading claude_tui_stalled). These
    operational failures carry empty review text so the governed-review classifier
    records a non-gating panel_leg_degraded warning (availability-aware degrade) rather
    than a promotion-blocking nonconforming review; the bounded, credential-redacted,
    control-stripped PTY tail (the buffer end) is preserved as a WARNING log for
    diagnosis.

Governed closeout & gate-integrity hardening (#218, #219)

Three bounded fixes so a phase cannot be marked complete/verification: passed
when its evidence does not support it. The wire contract (blocker-class enums,
closeout JSON schema, IF-gate grammar) is unchanged — only derivation logic.

  • Dir-aware ownership classification (fix). When a phase's owned deliverable
    is a brand-new directory and the executor self-reports the collapsed bare
    directory (pkg/newmod/) instead of its member files, a file-level owned glob
    (pkg/newmod/*.py) never matched the bare-directory string, so the path routed
    to the unowned remainder and tripped a spurious closeout_scope_violation. A
    new git_ops.expand_dir_dirty_paths normalizes a directory entry to its member
    files (via git status --porcelain --untracked-files=all -- <dir>) before
    ownership matching, applied in the closeout fallback classifier; the ownership
    matcher also gained a directory-prefix guard as defense-in-depth. A new
    all-owned directory now closes complete with the directory committed.
    (agent-harness#218)
  • Non-zero suite/command exit now fails closed (fix). A VerificationResult
    with any non-zero command/suite/env-refresh exit forces verification_status to
    failed/blocked at closeout, overriding an executor's self-asserted passed, and
    blocks the runner-owned verification reduction — always, irrespective of
    PHASE_LOOP_VERIFY_ENFORCE (which continues to soften only evidence-integrity
    findings like log-sha drift). The exit codes are read directly from the
    artifact's exit_summary
    , not just the first-failing validation code, so a red
    suite accompanied by a tampered/missing log (which reports log_sha256_mismatch/
    missing_log) can no longer shadow the red suite into a warning. A red suite is
    never a warning. Additionally, any governed phase whose plan declares an
    automation.suite_command now requires a VerificationResult artifact, so a
    self-asserted passed with no evidence can no longer close ungated.
    (agent-harness#219) (An unparseable/malformed_artifact supplied with a
    self-asserted passed still only warns under warn — a separate evidence-
    integrity gap, tracked for a follow-up.)
  • requires-python-aware suite interpreter (fix). The verification suite now
    runs under an interpreter satisfying the target repo's requires-python: an
    optional automation.python plan pin wins but is validated against
    requires-python
    (a pin below the floor fails closed), otherwise the lowest
    satisfying host pythonX.Y is resolved and shimmed onto the suite subprocess
    PATH. The shim covers bare python as well as python3 — if either present on
    PATH is below the floor, both names are shimmed onto a satisfying interpreter.
    An env_refresh pip install runs under the same resolved interpreter as the
    suite (not the host sys.executable), so deps are visible to the suite. When no
    satisfying interpreter exists the suite fails closed with a named blocker
    (recorded as a non-zero suite exit). The interpreter resolution (pin +
    requires-python auto-resolve) is honored on all three verification paths —
    execute, train-reverification, and hotfix. This removes the py3.10-vs-
    requires-python>=3.11 false failure that previously needed a manual shim.
    Known limitation: the shim only redirects a bare python/python3; a suite or
    verification command that explicitly names a versioned/absolute interpreter
    (e.g. python3.10, /usr/bin/python3.10) below the floor is not caught and can
    still run under an unsupported Python — tracked separately in agent-harness#221.
    (agent-harness#219)
  • Safe gitignore handling at closeout (fix). The gitignored exclusion no
    longer drops OWNED paths from phase_owned_dirty_paths: it applies to the
    unowned classification only, so a tracked-then-ignored owned file (real work
    that now also matches a .gitignore pattern) commits instead of being silently
    dropped (the #215 data-loss trap). Separately, the closeout fallback classifier
    now filters disposable byproducts the executor over-reports — paths that are
    BOTH untracked AND gitignored (build/, *.egg-info/, .phase-loop/,
    .dev-skills/) — so a disposable-only over-report with a genuinely-clean tree and
    passing verification finalizes as a no-op instead of a false
    dirty_worktree_conflict (the EXTRACT failure). The clean-tree check fails
    closed on an unreadable git probe (a probe error never reads as "clean"). A
    tracked file (even if ignored) is never treated as disposable. Collapsed owned
    directories are expanded to member files on the trusted path as well as the
    fallback, and the closeout git add force-adds only the proven-tracked
    members of the vetted path set — so a tracked-then-ignored file stages without a
    spurious non-zero exit, while an untracked+ignored path an executor wrongly
    reports as owned still fails closed rather than being force-committed.
    (agent-harness#186)

Round-4 cross-vendor CR: two fail-open / data-loss closures (#220)

  • Whole-verification interpreter fencing (fix). When no host interpreter can
    satisfy the target's requires-python (or an automation.python pin is below
    the floor), run_verification now fences the entire verification, not just
    the suite: env-refresh, the commands, and the suite are all skipped and a
    non-zero (127) result is synthesized so the evidence gate hard-blocks.
    Previously the blocker fenced only the suite, so a plan with commands but no
    suite_command ran env-refresh + commands on the host default and a green exit
    produced a passed artifact — silently bypassing the pin/requires-python.
    (agent-harness#220)
  • Closeout disposable filter fails closed on a git-probe failure (fix). The
    untracked-and-gitignored disposable filter no longer drops a path when it cannot
    prove the path is untracked. _tracked_paths now returns None on a git ls-files probe failure (distinct from an empty "nothing tracked" result) and
    the disposable computation drops nothing on None, so a transient probe failure
    can no longer misclassify a genuinely tracked file as a disposable byproduct
    and drop it (the #215 data-loss class under a probe failure). A collapsed
    bare-directory entry (build/) — which reaches the filter only when directory
    expansion's own git probe failed — is also never classified disposable, since
    string membership against git ls-files (which lists member files, never the
    bare-dir string) cannot prove the directory holds no modified tracked file; it is
    kept and blocks rather than being dropped. The closeout git add path likewise
    fails closed on a None probe (all paths get a plain add → a tracked-then-ignored
    file that fails the plain add blocks rather than being force-committed or dropped).
    (agent-harness#220)