Skip to content

fix(drive): trust an escalation only when this tick wrote it - #429

Merged
kjgbot merged 2 commits into
mainfrom
fix/drive-nonsticky-escalation-0916
Sep 16, 2026
Merged

kjgbot merged 2 commits into
mainfrom
fix/drive-nonsticky-escalation-0916

Conversation

@kjgbot

@kjgbot kjgbot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

The loop has been dead-locked since 2026-09-12

ops/NEEDS_HUMAN.md is committed on main (added 2026-09-06, 082c62aa). assess-gate escalated on the mere existence of that file. Nothing in the repo has ever deleted it — no rm, no git rm, and git log --diff-filter=D over that path is empty — and ops/launch-gate.sh builds each run's worktree from origin/main without stripping it.

So every drive tick escalated and died at the gate before doing any work. PRs #417, #420, #422, #424, #426, #427 and #428 are seven consecutive cloud runs whose entire diff is ops/NEEDS_HUMAN.md and ops/NEXT.md, re-litigating the same conflict. None merged. A full cloud run was burned on each.

What the escalation actually said, and the root cause nobody had found

The escalation reported that ops/TARGET.md pinned the run to gate 3 while describing gate 2 hn-monitor work that PR #120 had already merged, and that ops/NEXT.md described unrelated gate-3 review-swarm documentation. The assessor could not tell which was real and asked for a human decision.

It was right, and it was reporting a real defect — once per run, for four days. ops/autodrive.sh launched every run with sh ops/launch-gate.sh 3 "$brief" while passing it ops/AUTODRIVE_BRIEF.md, whose content is gate-2 work. The launcher synthesised # TARGET — gate 3 wrapped around a gate-2 brief, on every single tick. The contradiction was manufactured fresh each run.

Operator decision: the next gate is Gate 2, not Gate 3. This matches ops/AUTODRIVE_BRIEF.md:67 and the gate ladder in ops/STATE.md.

What this PR does

1. Unwedges. Resolves the escalation and deletes ops/NEEDS_HUMAN.md from main. Its durable content is preserved in a dated block in ops/STATE.md — the file an assessor with no git history reads instead — including the one question that outlives the gate-2/3 confusion: ops/TARGET.md is synthesised per run into a throwaway worktree and is never in the delivered diff, so escalations keep quoting a file no reviewer can open. Review lenses failed #422 and #428 for exactly that. Recorded as still-open.

2. Makes escalation non-sticky. assess-gate now trusts ops/NEEDS_HUMAN.md only when two independent signals agree: the file exists and this tick wrote it. Freshness reuses the idiom already in the file a few lines below (git log --oneline main..HEAD -- <path>), widened by the uncommitted case, because propagation between per-step sandboxes is lossy and assess may write the file without committing it. Losing a live escalation is the worse of the two errors, so a dirty working-tree file still parks the run. Same principle as cloud #3671.

A stale file now prints ASSESS_STALE_NEEDS_HUMAN_IGNORED and the tick proceeds. A live one still prints ASSESS_BLOCKED_NEEDS_HUMAN and exits 75.

3. Fixes the three stale sources so the next assessor is not re-confused:

  • ops/autodrive.sh — launches gate 2, matching the brief it passes.
  • ops/AUTODRIVE_BRIEF.md — retargeted off the already-merged hn-monitor runner.
  • ops/NEXT.md — rewritten as a Gate 2 package.
  • ops/STATE.md — clause 1 was stale and dangerous: it said trigger-plane liveness was "Not implemented inside relayflowd", but PR feat(kernel): trigger-plane liveness sweep — RFC-0001 gate 2 done-when #122 shipped it two weeks ago (server/liveness.rs + tests/subscription_liveness.rs). That entry would have sent the next run to rebuild working code — the third time an escalation proposed rebuilding something already merged.

4. The new work package is genuinely open work. I checked before writing it: liveness (#122) and the hn-monitor runner (#120) are both shipped. The remaining Gate 2 blocker is RFC-0001's own deviation D1, half-closed by PR #252 — the journal scan error propagates, but rule 10a's split by cause was never implemented and wake_context_unresolved appears nowhere in the kernel. resolve_wake_context's own doc comment says so: "none of that classification exists yet." D2 stays out of scope: the RFC sequences it rollover → D2 → gate 2 and says D2 is not implementable in isolation.

Verification — both directions, by execution

New test ops/drive-assess-gate.test.mjs extracts the gate script out of workflows/drive.yaml rather than copying it, so it fails if the workflow changes and the test does not.

$ node --test ops/drive-assess-gate.test.mjs
✔ a stale escalation committed on main does not wedge the tick
✔ an escalation committed by this tick still parks the run
✔ an uncommitted escalation from this tick still parks the run
✔ a live escalation parks the run when there is no git repo at all
✔ a live escalation parks the run when main does not exist
✔ a tick with no escalation at all passes the gate
ℹ tests 6   ℹ pass 6   ℹ fail 0

Verified by mutation — the same test against the unmodified main gate reproduces the wedge:

✖ a stale escalation committed on main does not wedge the tick
  AssertionError: the gate escalated on a stale file:
  ASSESS_BLOCKED_NEEDS_HUMAN: the Lead escalated a decision it cannot make.
  actual: 75, expected: 75, operator: 'notStrictEqual'
ℹ tests 4   ℹ pass 3   ℹ fail 1

A gate that cannot fire is as broken as one that always fires, so the live-escalation direction is pinned by two of the four cases.

Command Exit Result
node --test ops/drive-assess-gate.test.mjs 0 4/4 pass (new)
node --test ops/local-work-*.test.mjs (5 suites) 0 57/57 pass
node --test scripts/cloud-artifact.test.mjs 0 14/14 pass (CI-run)
node --test scripts/publish.test.mjs 0 7/7 pass (CI-run)
python3 -c "yaml.safe_load(...)" on both drive YAMLs 0 parse OK
sh -n on autodrive.sh, launch-gate.sh, deliver-run.sh 0 syntax OK
validateNextWorkPackage(ops/NEXT.md) — {"accepted":true}

flows check was deliberately not run against these workflows. Per workflows/README.md:12-13 and ops/RUNTIME-STATUS.md:123-126, drive.yaml / drive-cloud.yaml are the previous relayflows engine and all five legacy YAMLs return exit 2 REFUSED [invalid_spec]. That refusal is expected and is not a defect to fix.

On the drive-cloud.yaml diff size

workflows/drive-cloud.yaml is generated, so it was regenerated with python3 ops/gen-drive-cloud.py rather than hand-edited. Most of its ~199-line diff is pre-existing drift: the committed file on main already did not match its own generator's output, because a different PyYAML version wrapped the literal blocks at different columns. Regenerating from unmodified main reproduces that reflow with no change of mine involved.

Proven semantically clean by parsing both and comparing structures:

step names identical: True
steps: ['sync', 'assess-1', 'assess-gate-1', 'build-1', 'verify-1', 'commit-1', 'handoff']
steps differing semantically: ['assess-gate-1']

Only the gate step differs. Every other step is identical after parsing.

Review response — Bugbot found a real defect (fixed in 89ff6ce3)

Bugbot's check conclusion was neutral, but its summary said "found 1 potential issue" and the finding was correct, so it is recorded here rather than waved through.

The finding: the freshness check could not distinguish "git says this file is stale" from "git could not answer". Both print nothing, so with no .git, with main absent, or on any git failure, a live escalation classified as stale and the builder continued past a human decision — inverting the exact tradeoff the comment above it claims to make. Bugbot also correctly noted the tests never exercised a tree where git is absent or main is missing.

Why it mattered more than it looks: a sandbox is precisely where git cannot answer. SYNC_MODE=snapshot runs git init over an extracted tarball, so main does not exist until sync creates it. The first commit would have dropped live escalations in exactly the environment this gate exists to serve.

The fix: the default is now to trust the escalation. Only a positive, successful answer from git downgrades it to stale — the tree must be a repo, main must resolve, and both git log and git status must exit 0. Anything else prints ASSESS_ESCALATION_FRESHNESS_UNPROVABLE and exits 75. Exit codes are now checked rather than inferred from empty output, which also removes the | grep -q . that swallowed git's own exit status.

Two tests cover the shapes Bugbot named. Both fail against 7a17d31b and pass here:

=== gate under test = 7a17d31b (the version Bugbot flagged) ===
✖ a live escalation parks the run when there is no git repo at all
✖ a live escalation parks the run when main does not exist
ℹ tests 6   ℹ pass 4   ℹ fail 2

Other automated reviewers produced no signal

Recorded so their greens are not mistaken for review:

  • cubic — NEUTRAL; its summary says the monthly line limit is exhausted and reviews resume 1 October. It reviewed nothing.
  • CodeRabbit — commented that the repo "does not receive automatic reviews because it has fewer than 10 stars." It reviewed nothing.

Not done here

  • ops/drive-assess-gate.test.mjs is not wired into CI — no CI workflow runs ops/*.test.mjs today, and adding one means editing .github/workflows/, which AGENTS.md forbids to the author of the work it would judge. Flagging rather than doing it.
  • ops/autodrive.sh, ops/AUTODRIVE_BRIEF.md and ops/STATE.md are listed in ops/IMMUTABLE_PATHS. That list exists to stop a drive run silently rewriting the guards and ground truth that judge its own output. This is an operator-directed repair of those exact files, made by hand and not delivered by ops/deliver-run.sh, and the launcher defect could not be fixed without touching one of them. Calling it out explicitly rather than letting it pass unnoticed.

Status

HOLD — do not merge. Awaiting operator authorization.

🤖 Generated with Claude Code


Note

Medium Risk
Changes drive-loop gating and ops ground truth that every autodrive tick depends on; incorrect freshness logic could either re-wedge the loop or skip real human blocks. New tests are not wired into CI per the PR description.

Overview
Unwedges the autodrive loop after seven consecutive ticks died at assess-gate because committed ops/NEEDS_HUMAN.md triggered escalation on file existence alone.

assess-gate now treats ops/NEEDS_HUMAN.md as live only when this tick wrote it (committed in main..HEAD, uncommitted dirty file, or fail-safe unprovable when git cannot answer). Stale escalations emit ASSESS_STALE_NEEDS_HUMAN_IGNORED and the tick continues; live ones still exit 75 with ASSESS_BLOCKED_NEEDS_HUMAN. The same logic lands in workflows/drive.yaml and regenerated workflows/drive-cloud.yaml.

Ops alignment: removes ops/NEEDS_HUMAN.md, archives the wedge story in ops/STATE.md (including marking trigger-plane liveness closed per PR #122), retargets ops/AUTODRIVE_BRIEF.md and ops/NEXT.md to gate 2 / RFC D1 (kernel wake_context_unresolved — not implemented in this diff), and fixes ops/autodrive.sh to call launch-gate.sh 2 so the launcher gate matches the brief.

Tests: adds ops/drive-assess-gate.test.mjs, which extracts the assess-gate script from workflows/drive.yaml and covers stale vs live escalation paths (including no-git / no-main sandbox shapes).

Reviewed by Cursor Bugbot for commit 89ff6ce. Bugbot is set up for automated code reviews on this repo. Configure here.

ops/NEEDS_HUMAN.md is committed on main (082c62a, 2026-09-06) and nothing
has ever deleted it. assess-gate escalated on the file's mere existence, so
every drive tick since 2026-09-12 exited 75 before doing any work: PRs #417,
#420, #422, #424, #426, #427 and #428 are seven consecutive cloud runs whose
entire diff is that file and ops/NEXT.md. None merged.

The escalation was also correct about a real defect nobody had traced:
ops/autodrive.sh launched every run with `launch-gate.sh 3` while passing it
the gate-2 brief, so launch-gate.sh synthesised "TARGET — gate 3" wrapped
around gate-2 work on every tick. The assessors were reporting a launcher
bug, once per run, for four days.

Operator decision: the next gate is Gate 2, not Gate 3.

- assess-gate now requires two independent signals before trusting an
  escalation — the file exists AND this tick wrote it. Freshness reuses the
  `git log --oneline main..HEAD -- <path>` idiom already used for
  ops/NEXT.md a few lines below, widened by the uncommitted case because
  per-step propagation is lossy and losing a live escalation is the worse
  error. A stale file is ignored loudly; a live one still exits 75.
- ops/drive-assess-gate.test.mjs pins both directions, extracting the gate
  script from workflows/drive.yaml so the test cannot drift from it.
  Verified by mutation: against unmodified main the stale case fails with
  exit 75, reproducing the wedge.
- ops/NEEDS_HUMAN.md deleted; its durable content preserved in a dated
  ops/STATE.md block, including the still-open question that ops/TARGET.md
  is synthesised into a throwaway worktree and never reaches the diff.
- ops/autodrive.sh launches gate 2, matching the brief it passes.
- ops/AUTODRIVE_BRIEF.md retargeted off the hn-monitor runner PR #120
  already shipped, onto the open half of RFC-0001 deviation D1.
- ops/NEXT.md rewritten as that Gate 2 package.
- ops/STATE.md gate-2 clause 1 corrected: it claimed trigger-plane liveness
  was unimplemented, but PR #122 shipped it two weeks ago. That entry would
  have sent the next run to rebuild working code.
- workflows/drive-cloud.yaml regenerated with ops/gen-drive-cloud.py; only
  assess-gate-1 differs semantically, the rest is pre-existing PyYAML reflow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 31f4e754-f658-43b4-b045-e183e2b1b678


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This was referenced Sep 16, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 7a17d31. Configure here.

Comment thread workflows/drive.yaml
Bugbot found a real defect in the first commit, and it was the one that
mattered: the freshness check could not tell "git says this file is stale"
from "git could not answer". Both printed nothing, so with no .git, with
main absent, or on any git failure, a LIVE escalation classified as stale
and the builder walked straight past a human decision — inverting the
tradeoff the comment right above it claims to make.

A sandbox is exactly where git cannot answer. SYNC_MODE=snapshot runs
`git init` over an extracted tarball, so main does not exist until sync
creates it, and the gate would have dropped live escalations there.

The default is now to TRUST the escalation. Only a positive, SUCCESSFUL
answer from git downgrades it to stale: the tree must be a repo, main must
resolve, and both `git log` and `git status` must exit 0. Anything else
prints ASSESS_ESCALATION_FRESHNESS_UNPROVABLE and exits 75, because
ignoring a real escalation is the worse of the two errors.

Exit codes are now checked rather than inferred from empty output, which
also drops the `| grep -q .` that silently swallowed git's own exit status.

Two tests cover the shapes Bugbot correctly noted were unexercised: no git
repo at all, and a repo whose branch is not main with the escalation
COMMITTED (the shape where a naive main..HEAD prints nothing and the file
looks stale). Both fail against 7a17d31 and pass here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kjgbot

kjgbot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Review swarm: maintainability

No fresh transcript was produced for run 28917cfb-ae74-4ac1-939c-0343d179f368 (MISSING).

@kjgbot

kjgbot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Review swarm: history

No fresh transcript was produced for run 28917cfb-ae74-4ac1-939c-0343d179f368 (MISSING).

@kjgbot

kjgbot commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor Author

Review swarm: structure

No fresh transcript was produced for run 28917cfb-ae74-4ac1-939c-0343d179f368 (MISSING).

@kjgbot

kjgbot commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

🎯 review-swarm: FAILED (M:fail H:fail S:missing)

Lens transcripts posted as sibling comments above.

@github-actions

github-actions Bot commented Sep 16, 2026 •

Copy link
Copy Markdown

Review swarm: FAILED

  • maintainability: MISSING
  • history: MISSING
  • structure: MISSING

Cloud run: 28917cfb-ae74-4ac1-939c-0343d179f368

@kjgbot
kjgbot merged commit eb465b3 into main Sep 16, 2026
4 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant