Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
114 commits
Select commit Hold shift + click to select a range
94731a5
9559e7d4 - Build the error-fix fixer: drive an error.fix conclusion t…
Danswar Sep 2, 2026
dac389c
Fix the review-prompt gap and 9 rounds of cross-vendor review findings.
Danswar Sep 2, 2026
3bceac9
Fix the origin_seq ordering race across three rounds of review.
Danswar Sep 2, 2026
2ae2e68
Fix the grok-pr gate findings from the first formal review pass.
Danswar Sep 2, 2026
37e2a7e
Close the remaining gaps from the first two formal gate rounds.
Danswar Sep 2, 2026
1929643
Add a branch-identity check before the unattended first push.
Danswar Sep 2, 2026
2c95c18
Close the error_id normalization gap across write and read paths.
Danswar Sep 2, 2026
8dbadcb
Normalize the remaining raw error_id comparison sites.
Danswar Sep 2, 2026
8848d0c
Restore fail-closed push behavior for ordinary tasks.
Danswar Sep 2, 2026
cd5fe9a
Gate the push shortcut on error_fix_confirmed, not bare error_id.
Danswar Sep 2, 2026
1ab2cad
Fail the task on empty-review-diff instead of looping forever
Danswar Sep 2, 2026
e191878
Distinguish transient git-probe failure from a genuinely empty review…
Danswar Sep 2, 2026
c0c8901
Close two gaps in the transient-vs-empty diff distinction, fence brie…
Danswar Sep 2, 2026
0360db1
Give the auto-generated German summary a real German sentence.
Danswar Sep 2, 2026
a10647f
Fix a stale function reference and unfinished comment.
Danswar Sep 2, 2026
2594983
Stop the driver's control file from leaking into the pushed worktree.
Danswar Sep 2, 2026
4828b3a
Pin the push-destination check to github.com and decouple PR-number b…
Danswar Sep 2, 2026
085f28f
Replace parse-then-validate with a strict allowlist for push destinat…
Danswar Sep 2, 2026
12e333d
Fail closed when an error-fix task's repo can't be resolved for the p…
Danswar Sep 2, 2026
fccb775
Make push_branch and PR-matching enforce their own invariants, not th…
Danswar Sep 2, 2026
4d41347
Fix the third place round 42's payload-first repo resolution missed.
Danswar Sep 2, 2026
f32d303
Dispatch same-vendor PR-review dimensions in parallel, as CONTRIBUTIN…
Danswar Sep 3, 2026
5f5a6dc
Stop recording each PR-review dimension from discarding a sibling's r…
Danswar Sep 3, 2026
8823980
Finish recording every dimension before a launch-phase exception prop…
Danswar Sep 3, 2026
5b5abfd
Fix a comment that misdescribed _first_sentence's punctuation handling.
Danswar Sep 3, 2026
40ffb29
Stop _drive_one from acting on error-fix tasks whose session closed.
Danswar Sep 3, 2026
fe5a190
Guard retry-phase exceptions in _drive_parallel_pr_pair's finish loop…
Danswar Sep 3, 2026
b01fa07
Enforce a deadline on the vendor CLI subprocess in lane.py.
Danswar Sep 3, 2026
d8539d2
Strip service/environment in fingerprint() to match consumer normaliz…
Danswar Sep 3, 2026
160a4e2
Type ExecArgv as a Protocol instead of Callable[..., Any].
Danswar Sep 3, 2026
9794935
Replace preexec_fn with start_new_session in the vendor CLI runner.
Danswar Sep 3, 2026
e0b6337
Thread the resolved checkout base through review diffing and PR-open.
Danswar Sep 3, 2026
c393b10
Re-prefix the bare PR base before healing task.ref from it.
Danswar Sep 3, 2026
06ef0b5
Detect an already-prefixed PR base by prefix, not by slash presence.
Danswar Sep 3, 2026
b4c5c7a
Accept the new base_ref keyword in the parallel-pair test's fake buil…
Danswar Sep 3, 2026
34b62c1
Normalize stored fingerprints before comparison at all five sites.
Danswar Sep 3, 2026
dcb861e
Strip origin/ prefix at the gh pr create boundary and simplify heal.
Danswar Sep 3, 2026
2b97356
Retry kill and wait once more if the post-SIGKILL reap also times out.
Danswar Sep 3, 2026
b30736f
Strip error_class and stack_sig whitespace in fingerprint construction.
Danswar Sep 3, 2026
162cab0
Fail closed when an explicit review base_ref does not resolve locally.
Danswar Sep 3, 2026
c1fc86c
Send vendor-appropriate tool instructions in PR-review spec prompts.
Danswar Sep 3, 2026
00d876a
Give local_check_pass its own longer, configurable timeout.
Danswar Sep 3, 2026
09c9c23
Accept the new vendor keyword in the parallel-pair test's fake build …
Danswar Sep 3, 2026
7257870
Re-resolve the created PR's actual base from GitHub instead of the re…
Danswar Sep 3, 2026
4013f45
Kill the whole process group when a check command times out.
Danswar Sep 3, 2026
9d100ec
Drop unused local_check_timeout_sec import in fixer_act.py.
Danswar Sep 3, 2026
6c08b66
Fix stale comment claiming this PR does not touch errors.py.
Danswar Sep 3, 2026
aa640ef
Reword the shared review-diff line to be vendor-neutral.
Danswar Sep 3, 2026
5ce2bd7
Fail closed on a stale local_check_pass before pushing.
Danswar Sep 3, 2026
8b50119
Retry base resolution instead of freezing an unverified fallback.
Danswar Sep 3, 2026
4613ab9
Share the second kill+wait tier between runtime and lane.
Danswar Sep 3, 2026
677efad
Drop the dead signal import and align a fake HEAD sha with its push m…
Danswar Sep 3, 2026
e383b5d
Close the unbound-checked-sha loophole in the push freshness gate.
Danswar Sep 3, 2026
d36eb5d
Retry the resume-path gh pr view call on a transient failure too.
Danswar Sep 3, 2026
85e1f95
Give the push-time SHA-freshness gate real git repos and consistent t…
Danswar Sep 3, 2026
343411f
Restore two round-59 regression tests deleted by an earlier mistake.
Danswar Sep 3, 2026
c49b5cb
Unify gh failure classification in github_act.py's pr.open resume path.
Danswar Sep 3, 2026
b18b046
Fix grok-pr round 31 findings: misleading pr.open status, origin/-onl…
Danswar Sep 3, 2026
e1d8fd7
Expect two scan_github calls for a stable pending pr.open message.
Danswar Sep 3, 2026
da90bd4
Report the recorded PR number when an error-status pr.open still carr…
Danswar Sep 3, 2026
7220653
Drop the orphaned pending-only PR-number lookup helper.
Danswar Sep 3, 2026
8888a0c
Re-pend error-status pr.open rows with a recorded number before resum…
Danswar Sep 3, 2026
e222131
Pin the exact pending-retry wording instead of an ambiguous substring.
Danswar Sep 3, 2026
992486c
Check error-with-number pr.open rows before bare-pending ones in the …
Danswar Sep 3, 2026
9e7d5a6
Update the pushed-to-pr.open docstring and DESIGN.md to the current t…
Danswar Sep 3, 2026
cbc1f08
Fix 6 test-quality findings: exact counts, split OR-asserts, drop tau…
Danswar Sep 3, 2026
8800d6f
Fix 4 test-quality findings: parametrize expected_repo URL matrix, dr…
Danswar Sep 3, 2026
48783f1
Drop the unused drive-until-stable result binding.
Danswar Sep 3, 2026
63041ef
Pin push_branch to the freshness-gate-verified SHA, not mutable HEAD.
Danswar Sep 3, 2026
d9392aa
Fix gh failure classification and resume-path PR identity checks.
Danswar Sep 3, 2026
ff64fbe
Preflight-check the round cap before driving an error-fix task.
Danswar Sep 3, 2026
609e736
Fix round-cap off-by-one and 5 review-gate quality findings.
Danswar Sep 3, 2026
9e86d7e
Rename the past-cap test correctly and pass base_ref/vendor through t…
Danswar Sep 3, 2026
43969fe
Rename a test to match what it actually asserts.
Danswar Sep 3, 2026
bc995de
Block a duplicate draft when an error-status pr.open row still has a …
Danswar Sep 3, 2026
d9c2752
Check gh exit code 4 before text heuristics in _classify_gh_failure.
Danswar Sep 3, 2026
862c01c
Bound gh calls in the error-fix-work driver with a timeout.
Danswar Sep 3, 2026
e905487
Add a control-case assertion, a blank line, and drop a redundant import.
Danswar Sep 3, 2026
7d678c8
Serialize scan_github with a device-wide exclusive lock to close a cr…
Danswar Sep 3, 2026
2a87f71
Strip credential-shaped env vars from the local-check subprocess and …
Danswar Sep 3, 2026
9b7f282
Retry a reviewer lane instead of auto-passing when GAPS discloses rea…
Danswar Sep 3, 2026
d355381
Redact secrets from local-check output before truncating it.
Danswar Sep 3, 2026
a9ca352
Close GAPS-disqualification bypass: count and scan every GAPS: header…
Danswar Sep 3, 2026
4df1429
Document GAPS-disqualification in README and DESIGN §21.7.
Danswar Sep 3, 2026
b943476
Strengthen credential-redaction test against a vacuous empty-output p…
Danswar Sep 3, 2026
5cdf49c
Rename thread-lock test to reflect what it actually exercises.
Danswar Sep 3, 2026
ce2cf99
Add GAPS: 0 zero-numeral test case alongside the existing none-token …
Danswar Sep 3, 2026
af83634
Strip AGENT_ERROR_FIX_*/AGENT_PG_DSN from the vendor-CLI launch env too.
Danswar Sep 3, 2026
3fd9134
Document the state=failed exception for rejected gates in pr-review/S…
Danswar Sep 3, 2026
b6b8bb4
Dedup PR-number extraction in fixer_act.py via error_fix_act's shared…
Danswar Sep 3, 2026
81e0a67
Strengthen local-check credential-redaction test and fix two stale te…
Danswar Sep 3, 2026
48559a1
Fix AGENT_PG_DSN credential test breaking open_store() with an invali…
Danswar Sep 3, 2026
e6f4c15
Stop the implementer spec from claiming push/PR-open as its own job.
Danswar Sep 3, 2026
45122c0
Add a test proving output-redaction independently of env-popping.
Danswar Sep 3, 2026
4453e3c
Exercise the cross-process advisory lock with two Store instances.
Danswar Sep 3, 2026
1fd037c
Narrow the implementer spec's git constraint and close a lock-test ti…
Danswar Sep 3, 2026
a45d058
Fire the advisory-lock test's attempting signal at the actual SQL call.
Danswar Sep 3, 2026
91f9980
Bring SKILL.md contracts and the review-work path into documentation …
Danswar Sep 3, 2026
837ee28
Teach GAPS: none as the reviewer contract's zero form and treat [...]…
Danswar Sep 4, 2026
f020fe6
Thread the current round number into PR-reviewer review-spec filenames.
Danswar Sep 4, 2026
cee1f25
Sort secret redaction longest-first so a prefix value can't leak a lo…
Danswar Sep 4, 2026
0f0cf22
Stop splicing raw English brief text into DE-labeled PR body content.
Danswar Sep 4, 2026
2f52ce8
Confirm genuine Postgres-level blocking via pg_stat_activity before t…
Danswar Sep 4, 2026
0560d9d
Fix a stale comment, a DE-text cross-reference, and lock-test thread …
Danswar Sep 4, 2026
2a02b50
Fix review-workflow crashes from reused round_num and unconditional r…
Danswar Sep 4, 2026
818daf7
Explain GAPS: semantics to reviewer lanes.
Danswar Sep 4, 2026
1169ebd
Fix a stale comment about PR-reviewer round semantics.
Danswar Sep 4, 2026
8339641
Gate the round-cap failure on confirmed error-fix origin, and fix a s…
Danswar Sep 4, 2026
45fb044
Fix a vacuous test assertion and tighten a comment's mirror claim.
Danswar Sep 4, 2026
63b69d7
Force origin_seq and physical write order to disagree in the ordering…
Danswar Sep 4, 2026
ca19e24
Add an adversarial origin_seq test for load_task_dict's check ordering.
Danswar Sep 4, 2026
5ef4f0e
Harden credential stripping, redact the persisted check command, vali…
Danswar Sep 4, 2026
d46495c
Extend the local-check credential test to cover the known-name branch…
Danswar Sep 4, 2026
0e681af
Cover ANTHROPIC_API_KEY in the shared credential-shaped env-key predi…
Danswar Sep 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ agent watch grok-usage # one scan; knock child (under th
agent watch assigned [--follow] # allowlisted GitHub assignments → runner session + knock
agent watch errors # one scan; $AGENT_HOME/error-fix.json; knock daemon polls with grok-usage
agent watch error-fix # one scan; find-or-create implement task + isolated worktree; knock daemon polls with grok-usage
agent watch error-fix-work # one scan; drains error-fix implement tasks from spec_written through PR gates to done (§21.7); not wired into agent daemon
agent supervise --session ID [--repo OWNER/REPO --number N] [--once|--follow]
agent status
agent dashboard [--port 7845]
Expand Down Expand Up @@ -497,7 +498,7 @@ The rules below were already implied by §§1–17. They are now explicit so a l

A worker report such as “analysis complete” or “tests passed” is **input**. It is not the transition. Opening a draft is not done.

No transition that needs deterministic evidence may be satisfied by model text alone. Malformed structured output is rejected (unknown `activity.type` → `execution_status=error`; empty, partial, timeout, or unavailable review output is not zero findings). A patch that does not apply is a failed check, not a debate.
No transition that needs deterministic evidence may be satisfied by model text alone. Malformed structured output is rejected (unknown `activity.type` → `execution_status=error`; empty, partial, timeout, or unavailable review output is not zero findings). A patch that does not apply is a failed check, not a debate. The structured `FINDINGS:` section is the sole source of truth for a review verdict; a real finding stated only in a reviewer's free-text preamble and not restated there is not mechanically detectable and is accepted residual risk, not something a heuristic tries to close.

### 19.2 Untrusted inputs

Expand Down Expand Up @@ -665,10 +666,26 @@ The model never receives production credentials. Analysis that only reads the ex

`agent watch error-fix` find-or-creates the implement task and clones `https://github.com/<repo>.git` into `$AGENT_HOME/error-fix-work/<task_id>`; `agent github pending` still opens drafts, and a retry draft uses the existing head `error-fix-<id8>`.

`spec_written` stays human-only for ordinary implement tasks (`HUMAN_KEYS`, step kind `human`). Exception: when the task is error-fix-originated — payload carries `error_id` **and** a matching validated `error.fix` activity exists for that `error_id` in the same session — `close-step --source script` may set `spec_written=ja` with evidence. Evidence is still mandatory. An `error.seen` or `error.skip` alone does not qualify.

For confirmed error-fix tasks only (same `error_fix_confirmed` condition), `close-step --source script --status n_a` may close `deviation_declared` / `deviation_granted` with evidence. Any other status (e.g. `--status ja`), or any task without a confirmed `error.fix` origin, stays human-only. Without this carve-out no error-fix task can reach `done`, since both keys are `HUMAN_KEYS` with no other script path to close them.

### 21.6 Not in this revision

- A second hub state machine, leases, or autonomous merge

### 21.7 Automated fixer driver

`agent watch error-fix-work` drains open error-fix `implement` tasks on this device (`payload.error_id` set and a matching `error.fix` confirmed for that id in the same session, state not `done`/`failed`) from `spec_written` through a draft `pr.open`, the PR gates (`grok_pr_quality`, `grok_pr_logic`, `codex_pr_quality`, `codex_pr_logic`, plus the scripted `contributing_ok` carve-out), and task state `done`, using only script control flow and the `grok`/`codex` CLIs via `lane.launch()`. A human still merges the PR; the spine has no `merged` step. It is not wired into `agent daemon`.

- Scripts the five-part spec under `$AGENT_HOME/error-fix-specs/<task_id>/.spec.md` (a sibling of `error-fix-work`, never inside the pushed git worktree) from the `error.fix` brief plus `error.seen` metadata (never raw log excerpts), closes `spec_written` via the script carve-out above, then `agent round start`.
- Walks the spine with the same step executor as `agent run` (including auto pass/fail for reviewer and PR-reviewer lanes from `STATUS:` + `FINDINGS:` + `GAPS:` — a zero-`FINDINGS:` report with a non-trivial disclosed `GAPS:` body, or more than one `GAPS:` header, retries instead of auto-passing). Each reviewer/PR-reviewer lane's four-part prompt and its diff are scripted under `$AGENT_HOME/review-work/<task_id>/` (a sibling of `error-fix-work`/`error-fix-specs`, never inside the pushed git worktree), one `review-<role>-round<N>.md`/`.diff` pair per round. Round retries reset the relevant checklist keys to `nein` and call `agent round start`. Cap is `task.current_round` against 5: exceeding it sets `task state failed` and stops touching that task.
- When both dimensions of one vendor PR-review pair (`grok_pr_quality`/`grok_pr_logic`, or `codex_pr_quality`/`codex_pr_logic`) are ready simultaneously, the driver prepares both on the store-owning thread, launches any still-pending dimensions concurrently via a thread pool (worker threads only call the lane launch itself, never touch the store), then fully finishes both on that same thread (gate row, checklist, agent finish — no abandon/discard). Task-level continue-vs-message and the combined rejection-feedback write happen once afterward, aggregated across the batch so either dimension's rejection is preserved regardless of pair order; a `failed` outcome in the batch skips the deferred `round start` and wins over a sibling's `cont=True`. On an unhandled exception mid-prepare/launch/finish, any still-working agent row for either dimension is released before the exception propagates, and a rejection reset already committed earlier in the batch still receives its deferred `round start` (best-effort) when no outcome failed the task. `agent round start` itself refuses `state=failed` the same way it refuses `state=done`. `agent gate record --verdict rejected` also leaves `state=failed` unchanged (rather than its usual auto-transition to `implementing`) when the sibling already failed the task in the same batch — the rejected gate row is still recorded either way, for audit, even though the task stays permanently stopped. Ordinary `agent run` and every other spine step remain one-at-a-time.
- If a vendor CLI binary is missing (`OSError` / `FileNotFoundError` before any `LaneResult`) or a lane returns `LaneResult(status="unavailable")` on both the initial attempt and the one retry, the driver leaves task and checklist state untouched for retry, but releases any already-started agent row (`cmd_agent finish --verdict unavailable`) rather than leaving it `working` forever — notes the CLI looks unavailable, and moves on; the next scan retries after a human fixes PATH/auth.
- Each scan re-checks from the ledger (not per-call local state) whether `pushed` is closed but no successful (`done`) `pr.open` activity row exists yet for that task's branch head. A mid-flight `pending` row is resumed via `scan_github` (no duplicate insert); an `error` row carrying a recorded PR number is re-pended (preserving the number) then resumed via `scan_github` rather than re-inserted, so resuming never spuriously re-creates the PR; an `error` row with no recorded number, or no row at all, triggers a fresh `insert_pr_open_and_scan` — so a failed create is not silently skipped by the next scan.
- After `pushed`, inserts a pending `pr.open` (title/body per CONTRIBUTING) and runs `agent github pending`, then continues through the PR gates to `done`.
- Failing a task via lane retry-exhaustion also finishes the still-working agent row (`blocked` for implementer, `rejected` for reviewer/pr-reviewer roles) so the row does not block a later manual round-start recovery.

## 22. Static supervise loop (v1)

A second model must not orchestrate the first. `agent supervise` is a **script** with locked questions and locked answers. Model text is not a state transition.
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ agent pg status
agent pg stop
```

`agent run` records a local check when `local_check_pass` is open and the snapshot has no local checks yet (it does not rerun an existing failed check). It closes an agent step when the session store already has the artifact, and with `--spec-file` launches the vendor lane (tmux by default; `--no-tmux` for a subprocess). When `pushed` is open it git-pushes (no force) and closes with the HEAD sha; when `mergeable` is open it measures GitHub mergeability and checks and closes only if both are green. Reviewer lanes are not auto-approved from `STATUS: complete`.
`agent run` records a local check when `local_check_pass` is open and there is no fresh pass/skip for the current HEAD (a prior fail on that HEAD is rerun). It closes an agent step when the session store already has the artifact, and with `--spec-file` launches the vendor lane (tmux by default; `--no-tmux` for a subprocess). When `pushed` is open it git-pushes (no force) and closes with the HEAD sha; when `mergeable` is open it measures GitHub mergeability and checks and closes only if both are green. Reviewer and PR-reviewer lanes auto-pass only when the lane output is a single terminal report with `STATUS: complete` and an explicit, present `FINDINGS:` header that parses to zero — a missing or duplicated `FINDINGS:` header, or multiple STATUS:/FINDINGS: blocks in the output, resolves to retry instead of an automatic pass. A zero-`FINDINGS:` report still resolves to retry, not an automatic pass, when its `GAPS:` section discloses genuine, non-trivial content (anything other than a zero token like `none`/`0`), or when the output contains more than one `GAPS:` header. Each reviewer/PR-reviewer round's prompt and diff are written under `$AGENT_HOME/review-work/<task_id>/`.

`agent github pending` is one scan: owned pending `pr.open`, `comment.post`, `review.post`, and `issue.write` rows via `gh`. Pull requests are drafts. A retry reuses an existing open draft, issue, or comment instead of creating a second one.

Expand All @@ -96,6 +96,7 @@ agent watch grok-usage # one scan of SuperGrok weekly credits into usage.snapsh
agent watch assigned [--follow] # allowlisted assignments; needs `gh` and `$AGENT_HOME/watch.json`
agent watch errors # one scan; $AGENT_HOME/error-fix.json; no log host in this package
agent watch error-fix # one scan; find-or-create implement task + isolated worktree
agent watch error-fix-work # one scan; drains error-fix implement tasks from spec_written through PR gates to done; not wired into agent daemon
agent supervise --session ID [--repo OWNER/REPO --number N] [--once|--follow]
# agent knock (daemon, no --once) polls grok-usage, pending, pr.merged, github pending, mail pending, errors, and error-fix every 60s
```
Expand Down
86 changes: 81 additions & 5 deletions src/agent_cli/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,25 @@ def required_source(step: Step) -> str:
return "script"


def is_error_fix_originated(snapshot: dict[str, Any] | None) -> bool:
"""True when this is a validated error-fix task.

Requires both payload.error_id and snapshot.error_fix_confirmed (a matching
error.fix activity in the same session). Payload alone is not enough —
an error.seen / error.skip without error.fix must not get the
spec_written script carve-out.
"""
from .error_fix_act import _nonempty_str

if not isinstance(snapshot, dict):
return False
payload = snapshot.get("payload")
raw = payload.get("error_id") if isinstance(payload, dict) else None
if not isinstance(payload, dict) or not _nonempty_str(raw):
return False
return bool(snapshot.get("error_fix_confirmed"))


@dataclass(frozen=True)
class CloseVerdict:
allowed: bool
Expand All @@ -261,6 +280,7 @@ def close_allowed(
source: str,
evidence: str | None,
snapshot: dict[str, Any] | None = None,
status: str = "ja",
) -> CloseVerdict:
"""May this key be set to ja/n_a NOW? Only the current next step."""
step = find_step(workflow, key)
Expand All @@ -270,7 +290,18 @@ def close_allowed(
return CloseVerdict(False, f"{key} requires evidence", step)
want = required_source(step)
if source != want:
return CloseVerdict(False, f"{key} requires --source {want} (got {source})", step)
# error-fix implement tasks may script-author spec_written (any status)
# and, when status=="n_a", deviation_declared/deviation_granted too —
# HUMAN_KEYS and Step.kind stay human so every other task keeps the
# human-only requirement for all three keys, unchanged.
script_carveout = source == "script" and is_error_fix_originated(snapshot)
deviation_na = (
key in ("deviation_declared", "deviation_granted") and status == "n_a"
)
if not (script_carveout and (key == "spec_written" or deviation_na)):
return CloseVerdict(
False, f"{key} requires --source {want} (got {source})", step
)
ready = next_steps(workflow, checklist, spine_only=False)
if step not in ready:
pending = ",".join(s.key for s in next_steps(workflow, checklist, spine_only=True)) or "-"
Expand Down Expand Up @@ -300,10 +331,24 @@ def _latest_agent(snapshot: dict[str, Any], role: str, vendor: str | None) -> di
def _latest_gate(
snapshot: dict[str, Any], stage: str, dimension: str
) -> dict[str, Any] | None:
"""Latest gate for stage/dimension.

`load_task_dict` orders gates by payload origin_seq (oldest→newest), so the
last matching entry is the true latest write. When snapshot.head_sha is set,
only gates for that exact head are considered — a stale approval for another
head must not satisfy the step (same strict scoping as local_check_pass).
When no head is bound yet, all gates for the stage/dimension are considered.
"""
want = str(snapshot.get("head_sha") or "").strip().lower()
hit = None
for g in snapshot.get("gates") or []:
if g.get("stage") == stage and g.get("dimension") == dimension:
hit = g
if g.get("stage") != stage or g.get("dimension") != dimension:
continue
if want:
g_head = str(g.get("head_sha") or "").strip().lower()
if g_head != want:
continue
hit = g
return hit


Expand Down Expand Up @@ -344,9 +389,40 @@ def _artifact_ok(step: Step, snapshot: dict[str, Any]) -> str:
checks = list(snapshot.get("local_checks") or [])
if not checks:
return "no local_check recorded"
if any(c.get("result") == "fail" for c in checks):
want = str(snapshot.get("head_sha") or "").strip().lower()
if want:
# Scope to this head so a stale pass/fail for another (or empty) head
# cannot mask a fresh check — load_task_dict keeps the full history
# and same-second ran_at ties are otherwise order-unstable.
for_head = [
c
for c in checks
if str(c.get("head_sha") or "").strip().lower() == want
]
# Last row per name wins (list is oldest→newest), same as unbound.
latest: dict[str, Any] = {}
for c in for_head:
name = c.get("name")
if name is not None:
latest[str(name)] = c
latest_list = list(latest.values())
if any(c.get("result") == "fail" for c in latest_list):
return "local_check fail"
if not any(
str(c.get("result") or "") in ("pass", "skip") for c in latest_list
):
return "no local_check for current head"
return ""
# No bound head: last row per name wins (list is oldest→newest).
latest: dict[str, Any] = {}
for c in checks:
name = c.get("name")
if name is not None:
latest[str(name)] = c
latest_list = list(latest.values())
if any(c.get("result") == "fail" for c in latest_list):
return "local_check fail"
if not any(c.get("result") in ("pass", "skip") for c in checks):
if not any(c.get("result") in ("pass", "skip") for c in latest_list):
return "local_check without pass/skip"
return ""
if step.key == "pushed":
Expand Down
Loading
Loading