Skip to content

feat(premerge): route blocked_on_file returns into the next attempt's wave plan - #766

Merged
TheFJK merged 8 commits into
mainfrom
worktree-turbox-issue-755
Aug 24, 2026
Merged

TheFJK merged 8 commits into
mainfrom
worktree-turbox-issue-755

Conversation

@TheFJK

@TheFJK TheFJK commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Closes #755

/premerge's fixer waves are one agent per file, disjoint per wave. A fix whose
honest scope spans two files therefore has no route: the agent can only REFUSE,
and the finding survives to the next attempt — spending a whole repair round to
learn something the agent already knew. Worse is case 3 in the issue, where a
correct fix leaves a consequence in a file nobody in that wave owns, and nobody
reports it in a machine-readable way at all.

This PR makes blocked_on_file part of the fixer-wave return contract, and
routes the paths it returns into the next attempt's wave plan.

What the issue asked, and where this departs from it

The issue's item 3 says:

union those paths into the next attempt's allowed set, so the scope guard admits them.

This PR deliberately does not do that. Two independent reasons:

  1. Security. The commit fence derives the allowed set solely from the wave
    plan (jq -r '.waves[][].file' fix-waves-<NN>.json), and that derivation is
    exactly what keeps committable and owned by exactly one agent the same
    set. Widening the allowed set from agent-returned text makes a path
    committable while nobody owns it — and since _fix_waves never grouped
    it, the disjointness guard says nothing about the two agents in the next wave
    who may now both edit that one file, concurrently, against the single
    un-isolated worktree those waves share.

  2. It would not have worked. Fixer waves are dispatched from the wave plan.
    A path present only in the allowed list has no agent assigned to it, so it
    is edited by nobody — the cross-file consequence survives the round untouched,
    which is the exact failure the issue was filed about. The issue's own
    instruction does not deliver its own acceptance criterion.

Instead, the returned paths become synthesised blocker findings routed
through the unchanged _fix_waves. Each becomes a first-class wave member
with exactly one owner, and the commit fence keeps deriving its allowed set from
fix-waves-<NN>.json exactly as before. The scope guard needed no edit at
all
— P21b locks that it is still derived from the wave plan and that the
assigned list still has exactly one producer.

Bounded by construction

Not "optional, repeatable" with no ceiling — an allow-list that grows every round
constrains nothing by the last round:

  • Exactly one predecessor is read. plan --carry-blocked opens
    blocked-on-<N-1>.json by name, never a range. The sibling in the same
    file (_carry_prior_suggestions) walks range(1, attempt) and copying that
    shape reads as consistency — but it is correct there because a suggestion is
    never fixed and must not be lost, and a blocked-on path is the opposite: it
    widens what a fixer may edit.
  • MAX_BLOCKED_ON = 8, equal to the max fix-wave size, so one attempt can
    add at most one extra wave's worth of files — a bound a reader can check rather
    than a number someone picked.
  • Overflow is dropped, not refused. Refusing would cost the whole attempt over
    one malformed row an agent emitted. Dropping is also the fail-closed direction:
    a dropped path is a path no fixer may touch.
  • Paths are validated lexically and by realpath containment, and must
    already exist on disk (which also kills the newline-separator injection into
    the jq -r derived list).
  • Every drop is counted, never silent — blocked_on_admitted /
    blocked_on_dropped in classified.json, and BLOCKED=<n> appended to the
    PREMERGE_TRIAGE line.

A validator that claimed a guarantee it did not have

_repo_path's docstring claimed it was the "same predicate as
code_fixer_contract.py's scope validator". It is not: that one pairs its
lexical half with beneath(), a realpath + commonpath containment check, and
_repo_path implemented only the lexical half. That was fine while the value was
just a parse-time JSON field; it stops being fine the moment the same path
becomes a filesystem authorization decision. The docstring is corrected to
say what it actually does, and the missing half is added as _beneath_repo_root
— so a tracked symlink that is lexically repo-relative but resolves outside the
repo now dies (B32f).

Agent text is kept out of the fingerprint

  • summary is controller-authored and deterministic, because it feeds
    _fingerprint, which is the cross-attempt identity _blocker_fingerprints
    depends on. Letting an agent author it would make that identity agent-authored
    too. The agent's own words go in failure_scenario, where nothing keys on them.
  • The wave prompt pastes findings verbatim into an <external-untrusted-input>
    envelope with no encoder between them
    , so a row naming the envelope token
    could close the envelope early and read as the controller's words. Such rows are
    dropped.
  • The envelope check covers reason, origin and target. target is the
    agent-chosen string that lands in the finding's file key — the most
    agent-controlled of the three, and the one the plan left unchecked.
  • line is deliberately None: the consequence was observed at coordinates other
    wave members have already moved.

Two latent siblings fixed, not scoped out

Both ran comm without LC_ALL=C — sort orders by the ambient collation while
comm's merge walk assumes its own, and when they disagree the walk
desynchronises:

  • goal-state.sh's PR file-collision detector. This one fails open: a
    comm -12 intersection that misses a shared path reports no collision between
    two PRs that do collide, and the rebase the function exists to trigger never
    fires — the same disjointness property this issue is about.
  • review-consolidate.sh's staged-set scope guard. This one fails closed but
    wrongly: it names a path present in both lists as extra and refuses a merge
    whose staged set was correct.

Both were previously correct only by accident (the sorts happen to share a shell).
Pinning makes it a property of the code rather than of whatever
LC_ALL/LC_COLLATE/LANG the operator inherited — and this guard runs on every
platform UberDev does.

Verification

  • 40 executed fixture rows (B32–B32h) driving the real plan verb, with
    the fence's allowed set derived by executing the fence's own jq line
    extracted live from SKILL.md
    , not a transcription of it (B32h locks the
    extractor, including that it returns a sentinel rather than an empty set when
    extraction fails).
  • 21 mutations, zero survivors — including one that reds 9 rows by
    stopping synthesised findings from reaching the wave plan. That is the
    acceptance criterion itself under test, not a proxy for it. Re-executed while
    preparing this PR: deleting blockers = blockers + synthesised (the only
    producer feeding _fix_waves) takes the fixture from 190 pass / 0 fail to
    181 pass / 9 fail, and the nine are exactly the allowed-set and cap rows.
  • 25 contract shape-lock rows (P21/P21b/P21c), each scoped to an
    extracted §2a block rather than the whole SKILL.md, with extraction-size
    guards so a moved anchor reds loudly instead of passing vacuously.
  • Both locale locks proved non-vacuous against a half-pinned line — one
    command on the line pinned, the other not. That is the exact half-fix a reviewer
    waves through, and the originally-planned line-level grep -v 'LC_ALL=C' filter
    scored it as clean (measured: zero hits). The shipped predicate neutralises
    pinned pairs per token, not per line, and G56.mutation-half /
    RCX15.mutation-half prove it.

Acceptance

# Item Status
1 §2a names the field and both emission cases done (P21)
2 Fixture: attempt N+1's plan widens from attempt N's blocked_on_file, admitting exactly those and no others done (B32, B32a, B32b) — against the wave plan, per the departure above
3 A run where a cross-file fix completes in one attempt rather than two deferred, not dropped

Item 3 needs a live /premerge run over real PRs and real CI, which cannot be
produced inside a PR. The executed B32 fixture is the in-PR proxy: it drives the
real verb end to end and asserts the widened path is assigned to exactly one owner.

Reported, not fixed

Both outside this PR's scope:

  • plugins/uberdev/hooks/session-end:21 runs
    find -H /tmp -maxdepth 1 \( -name 'uberdev-*' -type d \) -exec rm -rf {} +,
    which deletes every /tmp/uberdev-* directory on any session end
    anywhere on the machine — not just the ending session's own. It corrupted three
    separate agents' scratch state during this work. Compounding it: the hook is
    extension-less (hooks/session-end, not session-end.sh), so
    --include='*.sh' greps cannot see it.
  • tests/skill-size.test.sh's merge-pipeline waiver row claims ~27% fenced; the file measures 45.7% (812 fenced body lines of 1778). The
    percentages in that table are prose annotations, not assertions, so nothing
    reds — which is why it drifted.

One consequence of the re-pin

The premerge-pipeline size ceiling is re-pinned to the measured 2391, so the
ceiling now equals the measurement exactly and the next line added to that
SKILL.md reds S2. That is the ratchet working as designed — it was equally
tight at 2323 — but any follow-up touching that file must re-pin in the same
commit.

Version

No version bump: fleet PR, so the bump belongs to the commit that lands the stack.
The size ceiling re-pin is not a version surface.

Full suite

The complete shape-checks run list from .github/workflows/test.yml — all
132 fixtures, argv taken verbatim from the run_one lines (so the five
*-zsh.test.sh rows keep their zsh driver), each invocation's exit code
captured on its own, never through a pager, with per-fixture logs pre-created and
probed so a failed redirect could not masquerade as an ordinary red.

invocations planned : 132
invocations executed: 132
failures            : 1
wall clock          : 1279s

132 result rows, 132 logs, 132 === END rc= markers — every fixture demonstrably
ran. 131 passed, 1 failed.

The one failure — pre-existing, not caused by this PR

tests/vendor-provenance.test.sh — 62 passed, 1 failed
  FAIL  V46 a recorded uberdev_rev does not resolve: agents/code-reviewer.md:
        b7dbe74a36bbc4ff02bc364f0ae554f1c9800872 is not reachable from HEAD

Attributed and verified independently for this PR:

  • b7dbe74a… is reachable only from the unmerged fix/746-agent-description-budget
    branch (git branch -a --contains names no main), and
    git merge-base --is-ancestor b7dbe74a… origin/main returns false.
  • The identical record is already present in origin/main's
    plugins/uberdev/vendor.json, so the row is red on clean origin/main too.
  • This PR does not touch plugins/uberdev/vendor.json (git diff --name-only aa3e54cd..HEAD
    matches nothing under vendor).

It clears when fix/746 lands. Nothing else in the suite is red.

tests/code-fixer-contract-windows.test.py is not part of shape-checks — it is
wired only into the Windows job — so it is not in this run by design.

Rows this PR adds, all green

Fixture Rows What
tests/premerge-findings.test.sh 40 (B32–B32h) executed against the real plan verb
tests/premerge.test.sh 25 (P21/P21b/P21c) contract shape locks
tests/goal.test.sh 4 (G56) collision-detector locale lock + 2 mutants
tests/review-pr-consolidate.test.sh 4 (RCX15) scope-guard locale lock + 2 mutants

@coderabbitai

coderabbitai Bot commented Aug 23, 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: ASSERTIVE

Plan: Pro Plus

Run ID: 9703c3eb-d0f7-4ac8-9faa-818e6daa0486


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.

@TheFJK

TheFJK commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded for review by #768; the trust trail will be emitted there.

@TheFJK
TheFJK merged commit 1565cae into main Aug 24, 2026
14 of 15 checks passed
TheFJK added a commit that referenced this pull request Aug 24, 2026
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.

premerge: formalize blocked_on_file in the fixer-wave return contract — a cross-file fix currently costs a whole repair round

1 participant