Skip to content

dp#51 deferrals + dp#60: meta opener walk, qagent returns, typed claim recovery, truncation guards, render fences (v7.51.0) - #65

Open
SSFSKIM wants to merge 41 commits into
mainfrom
dp51-deferrals-dp60
Open

dp#51 deferrals + dp#60: meta opener walk, qagent returns, typed claim recovery, truncation guards, render fences (v7.51.0)#65
SSFSKIM wants to merge 41 commits into
mainfrom
dp51-deferrals-dp60

Conversation

@SSFSKIM

@SSFSKIM SSFSKIM commented Aug 12, 2026

Copy link
Copy Markdown
Owner

What this is

The tail of the dp#51/dp#60 program: the five non-consumption deferrals recorded on #51 after PR #61 shipped, plus #60, the live gh-mode meta-truncation bug found during that work. Six bounded items, spec-driven (docs/doperpowers/specs/2026-08-12-dp51-deferrals-dp60-design.md, v1.0 → v1.2.4 across two pre-implementation codex reviews and four flow-backs), executed subagent-driven over 11 plan tasks.

Closes #60. Closes #51.

What shipped

  • gh-mode meta writes truncate bodies that quote a board:meta marker (strip_meta leftmost-match bug) #60 — the meta opener is now chosen by content, not position. meta_match/clean_meta in _board.py: candidates are line-start openers; a candidate wins iff its whole interior (to the final closer) is block-legal under parse_meta's own line model (all 11 splitlines() separators); segment-aware fallback preserves pre-grammar blocks for the raw splice. Values are normalized at the write (clean_meta), validated before any external write so a refusal can no longer tear a transition (label moved, meta dead). board-body.sh and board-migrate-gh.sh ride the shared helper — the migration was the last one-shot truncation path. The rule took four formulations to stabilize; the round was ended by a committed property fuzzer (140k generated legacy bodies, 7 seeds, zero divergences, self-validated by catching 2004/182/954 known divergences in the three superseded implementations — 0.3s per 5000 bodies, the judge for any future revision of this rule).
  • gh qagent parks return to in-reviewrole: QAGENT stamped at reviewer spawn (with a legacy rung inferring the role from the deterministic review-pr-*/review-epic-* worker name), and the ticket's own pr: meta re-supplied to the transition so the --pr gate is satisfied on its own terms. Fix-wave catch: _stamp_meta now preserves file modes (a bookkeeping stamp was republishing 0600 run bearers at 0644).
  • Successor-claim recovery is typed. nonce-consumed/stale-resume mean the journal is obsolete: dropped uncharged. Real faults charge the existing 3-cycle ladder. One recovery attempt per ticket per tick (tick ledger, read by both the feed and the replay arm), phase order now lift → reconcile → feed, reconcile honors suppression — closing both double-charge and unliftable-suppression modes measured in review.
  • A truncated /tickets read can no longer lift a suppression (absent row = unknown, mirroring the write-side "an empty read is not a state" doctrine), and the outward half is pinned as arkho#9: /queue/decisions + /tickets stay unbounded until a paged envelope exists.
  • The review bootstrap renderer fails closed on unresolved placeholders (all four call sites first proven complete), critical bindings pinned non-empty per lane, and a 94-assertion static parity fence guards the four mode blocks' shared tail, rosters, and load-bearing sentences — the 203-line analogue of the relay-prompt drift drill.
  • Drill assertions anchored (emitter delimiters, closed JSON ids, per-side refusal phrases) and the transcript comparator's argv leg made honest (it is capture-provenance; exit status + normalized output carry the drift fence).

Verification

  • All suites green on the merged tree at v7.51.0: test-board-scripts.sh 501, board-api 12/12, reviewing-prs three suites (dispatch 308, entrypoint, parity 94), full runner PASSED with the 8 integration drills against the live service, lint-shell.sh clean.
  • Spec acceptance executed item-by-item (Task 11 evidence log in the session workspace), including a bite-check on the parity fence and live verification of arkho#9.
  • Review chain: two pre-implementation codex reviews (spec + plan), 11 per-task opus review cycles with RED-first fix waves, final codex panel (2 findings) + three convergence rounds + the fuzz pass to a clean verdict.

Env note for re-runners: the integration tier needs ARKHO_DIR=/Users/new/Developer/GitHub/arkho — the arkho-a1-board-service checkout is a stale branch and produces 32 false failures.

Version: 7.51.0 (main's 7.50.1 merged in).

SSFSKIM added 30 commits August 12, 2026 23:02
…09s, name-inference rung, fail-closed renderer)
…-write trigger); validate before external writes
board-body.sh's gh half carried the proven rightmost walk inline; it now
calls B.meta_match. board-migrate-gh.sh still inlined the PRE-FIX
META_RE.sub("") strip, keeping #60's truncation alive on the one-shot
migration path — it now strips through B.strip_meta.

Adds a migration-path regression drill: a linked issue whose prose quotes
a marker-shaped example keeps its prose (RED against the parent commit),
and a legacy note carrying an opening marker dies loudly rather than
minting a second marker inside the real block.
Review finding I1: write_body stripped twice — B.strip_meta(body) fed
B.render_body, which strips again internally. Harmless while the first
strip was leftmost (it had already eaten everything from the first marker
on); once that strip became correct, the second one landed on prose. A
marker-shaped example ENDING the base still satisfies META_RE's \s*$, so
it was deleted as if it were the block — a #60-class truncation surviving
on the one path where the write is one-shot.

Splits compose_body(base, meta) out of render_body: prose the caller has
already stripped, plus a rendered block, stripping nothing. render_body
is now compose_body(strip_meta(body), meta), so every other caller is
byte-identical. The migration composes through it.

Drill grows the tail-example variant (RED against d62127b, one FAIL) and
pins the refusal's own message rather than only its exit code. Also
corrects the board-body.sh comment's causal clause: the offset invariant
is why the splice must supply the separator, not why it normalizes.
…e inference, pr re-supply

gh-mode review-dispatch never stamped a role, and board-answer's
unrecorded-pre-park fallback had no QAGENT arm, so an answered reviewer
park landed in in-progress — where the sweep's stale-reviewer arm retires
the resumed worker out from under it.

- _spawn_reviewer stamps role: QAGENT into the registry meta after the
  bind succeeds, via the file's own _stamp_meta helper (non-fatal).
- board-answer's fallback is a three-way (ARCHITECT / QAGENT / else),
  with a legacy rung that infers QAGENT from a review-pr-* / review-epic-*
  registry name so the fix is not upgrade-gated.
- the QAGENT arm re-supplies the ticket's recorded pr: as --pr, which is
  what board-transition's in-review gate requires; with no pr: meta it
  demotes to in-progress and warns rather than dying on the answer path.

The probe heredoc moves into a function: bash 3.2 re-scans an inline
"$(...)" at expansion time with a matcher that ignores the heredoc, so
the body's prose apostrophes made the next double-quoted string with a
space fail to parse. Reproducible against the parent commit; bash -n
never saw it because -n does not expand.
The QAGENT stamp sat in _spawn_reviewer's shared tail, so the API claim
path ran it one line after board-bind had written the run bearer at a
forced 0600. _stamp_meta rewrote the file at the umask default, and the
api path's own stamp — which preserves whatever mode it finds — then made
the widened 0644 permanent, leaving the bearer world-readable at rest.

- gate the stamp on [ -z "${CLAIM_JOURNAL:-}" ] (gh mode only; the api
  path stamps role itself, in the write that owns the bearer's mode)
- give _stamp_meta board-bind write_meta's discipline: 0600 when a
  run_bearer is present, else the mode os.stat finds; unlink the stale
  tmp, O_EXCL, chmod. This also closes the hole for the helper's existing
  callers (retired_from on an api meta, closure_package).

Review follow-ups: restate the _probe_binding comment to the measured
mechanism (apostrophe parity in bash 3.2's command-substitution matcher,
not spaced strings), and drop a new board-answer assertion that was green
at base and so proved nothing.
…osmetics)

Every id the A2 integration drills assert on ended what it was printed in,
and `t`/`nt` match with `grep -qF`, which has no anchor: `owner=1` is a
substring of `owner=12`, so a drill could pass — or an `nt` fail — on a run
or ticket that merely prefixed the one it named.

Closed once per emitter rather than per assertion: `owner_line` and `eol`
move into drill-lib.sh (the four identical local copies go away), `row()`
brackets its scalar fields the way it already bracketed its lists, and the
lane's pick gets a `drew()` emitter. JSON ids are closed with the next token
(`"ticketId":$T1,`), the live `grep -q` in test-escalation's poll with
`[,}]`, and env-dump lines with `eol`'s terminator.

The transcript-diff walk now records its argv twice — as executed and as
written, `%T` unsubstituted — and transcript-compare.py compares the
unsubstituted form. The two walks number their own tickets and only agreed by
coincidence on a scratch board where both were #1; the alternative, a digit
normalizer, would have erased step 6's deliberate literal 4242.
…re id wants

Review I-1: the STRICT argv comparison is a capture-integrity check, not a
behavioral fence — both walks iterate one shared STEPS array, so the argv is
the drill's own input and no gh/api divergence is constructible. That was
equally true before this task (the executed argv differed only by the ticket
id, which is a way to fail falsely, not a way to catch drift), so the header,
the walk's comment and the comparator's docstring now say so and hand the
fence to the exit status, where it actually lives.

M-1: the gh walk's id is asserted against the stub board that holds it —
title and all — instead of against itself, mirroring the API half's
ticket_state read. M-2: test-escalation's skip line closed with eol. M-3:
the resume-feed membership assertion closed the same way, rather than a bare
id against a one-per-line list. M-4: the executed argv is marked forensic.
M-5 needs no change (the env-dump line is the only surface that carries
BOARD_RUN_ID, and it is fully covered).
…ack: legacy nested markers + quadratic rescan)
The rightmost walk landed in Task 1 to beat the QUOTED shape (#60), where
prose documents the block and a marker-shaped example sits above the real
one. It regressed the mirror shape: a pre-grammar client could store a meta
VALUE carrying a verbatim `<!-- board:meta`, so the real block's interior
holds a second opener. Rightmost anchors on that nested one, and strip_meta
cuts INSIDE the block — half of it is left behind as prose, and every
rewrite re-emits it. The old leftmost code mis-parsed that body too, but it
cut at the correct byte boundary.

Neither end wins by position, so the interior decides. Candidates are the
leftmost match's opener plus every later line-start opener that still
precedes the (unique) closer; walking left to right, a candidate is real iff
every line between it and the next candidate is a legal `key: value` block
line. A quoted example's `-->`, a blank line or any prose disqualifies it;
the last candidate wins by default.

The nested block's forged keys still parse — that content is legacy
corruption and unrecoverable — but the BOUNDARY is not, so the new pins are
the strip boundary and rewrite stability: strip_meta lands on the outer
opener, a rewrite leaves the prose byte-stable and collapses the body to one
clean block, and that block is a fixed point.

This also removes the quadratic rescan: the walk re-ran the end-anchored
regex once per opener, 3.4s on a 4000-marker body, paid per issue by
snapshot() against a server that accepts 1MB bodies. Two regex scans and
linear slicing now — 0.004s on the same body, pinned by a loose-bound smoke.
dc89641 asked only whether the lines between two ADJACENT candidates were
block-legal. Convergence review found the hole (spec v1.2.4): prose that
QUOTES a legacy-nested example has nothing but that example's own `note:`
entry between its outer and nested opener, so the gap reads legal and the
quoted opener wins — parse leaks the example's keys and strip truncates the
body from the example onward, losing every line after it.

Since every META_RE match runs to the same closer, the interior that decides
a candidate is its WHOLE interior: legal iff every line from its opener to
that closer is a known-key `key: value` or a line-start nested marker. A
blank line, prose, or an intermediate `-->` cannot sit in a block, so the
answer is the FIRST opener standing after the LAST such line — one
classification pass over the span, no per-candidate rescan, so the linear
cost stands (0.004s on the 4000-marker body).

Shape B still resolves to the outer opener; the quoted shapes, plain and
nested, resolve to the real block. When no opener clears the last illegal
line the block is noncanonical — an unknown key, a comment, hand spacing —
and the last candidate wins, which is the old rightmost behavior and what
board-body.sh's raw splice carries through untouched; that path is now
pinned too. The degenerate-opener guard survives as the `pos + head <= close`
room check, and META_OPENER_RE is gone — the pass finds its own openers.
…egment

Two boundary regressions in c71a867's selector, both found by convergence
review.

The interior pass split on `\n` while parse_meta reads the block with
str.splitlines(), which also honours U+2028, bare CR, \v, \f, \x1c-\x1e and
\x85. A quoted example folded on any of those hides its own `-->` and the
prose behind it inside one line that reads like a legal `note:` entry, so the
quoted opener wins and the next meta write truncates the body. The pass now
cuts on LINE_SEP_RE — the same boundaries, offsets kept — and the (f)
separator roster gains a mirror in (j): (f) stops a value forging a key, (j)
stops one hiding a disqualifying line.

The fallback took the LAST candidate, which reopened shape B whenever a
legacy block carried a nested marker AND an unknown key: the unknown key is
not block-legal, so it fenced off every candidate, and opens[-1] is the
nested opener — strip left the outer header behind as prose again. The
fallback belongs to the segment the illegal line landed in, so each candidate
now records the fence standing when it was seen and the fallback takes that
segment's FIRST opener. A noncanonical block with no nesting still resolves
to its own opener, so the splice path is unchanged.

Shapes A, B and C, the degenerate trailing opener, and the linear cost all
hold (4000-marker body at 0.006s).
The opener rule was hand-corrected three times, and twice the correction
broke a shape the PREVIOUS formulation had handled — dc89641's adjacent-gap
rule got the mixed legacy block right, and c71a867's whole-interior rule
regressed it. Every case pin in the meta-grammar section is therefore a
record of what someone thought to look for, not evidence of coverage. Three
rounds of hand-reasoning is enough of a signal to stop hand-reasoning.

The fuzz composes bodies from a component grammar — prose lines including
colon-bearing and known-key-looking ones, quoted examples at column 0 and
indented, with and without closers, quoting a legacy-nested block, folded
onto one line by any of the eleven splitlines() separators — around exactly
one intended trailing block, canonical or legacy-nested or noncanonical. The
intended opener, prose and block bytes are recorded at generation time, and
four properties are checked per body: the opener chosen, the strip boundary,
rewrite idempotence, and the verbatim block bytes board-body.sh splices.

It has teeth where the case pins do not: at the default seed and size it
reports 2004, 182 and 954 divergences against the three superseded
implementations, and zero against this one. 140k bodies across seven seeds
are clean. Costs ~0.3s.

Two regions are excluded and documented in the header, both because the
bytes genuinely do not determine the answer: the v1.2.4 all-legal-no-closer
ambiguity, and a block whose illegal interior line precedes its own nested
marker. BOARD_SCRIPTS=<dir> points it at another implementation, which is
how a candidate rewrite of the rule should be judged before it lands.
… ledger+reset reach phase 4; indented openers; validate before labels)
board-answer's fail-open demotion for a recognized QAGENT return re-created
the stranding it was meant to avoid: after the demote-and-resume the ticket
sits in-progress, review-dispatch's stale-reviewer arm retires the resumed
reviewer once idle, and the ticket is left in a lane with no PR binding and
nobody bound. A reviewer-lane ticket with no pr: meta is an anomaly (the
in-review entry gate stamps it), and anomalies pause rather than guess.

The relay is now REFUSED: nonzero exit, the ticket stays parked at
needs-human, and the message names the missing meta and the recovery
(restore pr: in the body's board:meta block, then re-run --posted). The
answers comment lands before the refusal, so nothing is lost. The
IMPLEMENT/no-role fallback is unchanged.

Spec v1.2.5 (1). The demotion pins are rewritten to pin the refusal.
Two halves of one invariant that stopped at the phase boundary.

The ledger of tickets this tick already attempted a recovery for travelled
no further than phase_resume, so after a replay FAULT left a ticket unowned
an ordinary lane claim could pick that same ticket seconds later — a second
attempt inside the tick the ledger exists to hold to one. It now rides to
both dispatchers as BOARD_RESUMED_LEDGER, the way BOARD_SUPPRESS_DIR does,
and a claim that yields a tick-ledgered ticket is released head-of-line
exactly as a suppressed one is; the next tick serves it if still unowned.

And only _resume_one reset the failed-cycle count, so a ticket the
DISPATCHER recovered kept its stale .attempts-<tid> and a much later,
unrelated fault escalated early. A successful bind now clears it.

Spec v1.2.5 (2).
META_RE's opener is unanchored — it matches after leading spaces mid-line —
but meta_match admitted a candidate only where the WHOLE line equalled the
opener. A body with a column-zero QUOTED example and an INDENTED real
trailing block therefore had no candidate for its real opener, fell back to
the example, and the next meta write deleted the prose between them (the
panel reproduced it).

The candidate test is now lstrip() == the opener, with the offset pointing
at the '<' so every byte consumer keeps the indent on the prose side.
Indented quoted examples stay excluded by interior legality: their own
closer line carries no colon, so it fences.

The fuzzer grammar gains indented real blocks and is the acceptance: 711
divergences against the column-zero rule, 0 after. Header counts for the
superseded rules re-derived under the extended grammar.

Spec v1.2.5 (3).
…l write

apply_state validates the merged meta ahead of its own label write, but
board-transition.sh writes labels of its own first — ensure_labels, then the
surface re-match's create+add — so a note the grammar refuses still tore the
ticket: surface labels persisted, transition failed, and nothing rolls them
back. The ticket then reads as a lane member it never entered.

The merge-and-validate step is now check_meta_write, called by apply_state at
its own top and by board-transition.sh ahead of every label call with the
same extra it will hand apply_state — one implementation, two call sites.

Spec v1.2.5 (4).
…t survives a crash

Convergence review on the F2 fix, two holes in its mechanics.

The ledger recorded only the tickets RECONCILIATION replayed for, so a
ticket served by the ordinary needing-resume feed whose recovery then
faulted or released was never fenced: unowned, spent, and claimable by
phase 4 in the same tick. The feed loop now writes the ledger before the
attempt, exactly as the replay arm does; a recovery that succeeds owns the
ticket, so the record costs it nothing.

And the failed-cycle reset was an unjournaled post-bind step: a dispatcher
that died between the durable journal mark and the rm left a delivered
recovery beside a stale count, and reconciliation skips completed journals
forever. The reset now runs one line AHEAD of the marker write, which puts
the whole remaining window inside the crash reconciliation already sees —
the `repaired` arm (bind landed, marker lost) — and that arm clears the
count too, carrying the journal's ticket for the purpose. Blanket-clearing
on every completed journal was rejected: those records stand indefinitely,
so a later unrelated fault would have its ladder reset out from under it.

Spec v1.2.5 (2).
… acked delivery

Second convergence round on the repaired arm.

The seal (spawn_completed) is what makes a journal invisible to every later
pass, so anything left until after it happens once or never — and the reset
sat in the shell action that runs after the classifier already wrote the
seal. A crash or a failed reset in that window left a durable recovery
beside a stale counter with nothing able to revisit it: the same
early-escalation bug, one window later. The reset now runs in the classifier
immediately BEFORE the seal write, so the two are ordered the way the
dispatcher orders them; both steps are idempotent, and a crash between them
leaves the journal open for the next pass to redo both. The suppression
directory reaches the classifier through a new _claim_suppress_dir callback,
which replaces the _claim_attempts_clear one.

And a bound journal whose control dir holds no ack was falling through to
repaired whenever its writer was still alive — a peer between its bind and
the ack it waits on. That delivery is not durable: if the ack never lands
the stranded arm retires the worker and releases the run, so clearing the
ladder there erases it for a delivery about to be undone. It now classifies
as in-flight and is left entirely alone, journal and counter both.

Spec v1.2.5 (2).
The reset ahead of the seal swallowed every OSError, so a removal that
genuinely failed — a read-only or unmounted registry, a permission change —
still sealed spawn_completed=true on top of a counter that was still
standing. A sealed journal is invisible to every later pass, which is the
early-escalation failure one door further along.

FileNotFoundError alone is benign: no file is a finished reset. Any other
removal error now skips the seal and reports the errno, leaving the journal
open so the next pass retries the idempotent pair.

Spec v1.2.5 (2).
os.remove answers ENOENT both when the counter is already gone (benign) and
when the directory holding it cannot be seen at all — an operator
BOARD_SUPPRESS_DIR whose volume unmounted, a path that moved. Sealing on the
second reading loses the way swallowing EACCES did: the mount returns
carrying the stale count and the journal that would have cleared it is
closed forever.

Reachability is what separates them, not the directory's own existence: the
sweep creates that directory the first time it counts anything, so on a
registry that has never had a failed cycle it legitimately does not exist —
and reading THAT as a fault would refuse to seal every repair on every
healthy fleet, forever. So the benign test is the directory or its parent
being present; both gone is a path this process cannot see, where absence
proves nothing and the seal is skipped.

Pinned both ways: an unreachable path leaves the journal open, and a
registry with no suppression directory repairs normally.

Spec v1.2.5 (2).
@SSFSKIM

SSFSKIM commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Post-open review round (user-invoked /codex-companion review, full PR diff via the code-review panel, gpt-5.6-sol): 4 findings, all adopted — including one P1 that overturned a spec ruling: a PR-less QAGENT park now REFUSES the relay and stays parked (the demote-and-resume path stranded the ticket in-progress with its reviewer retired — the very failure the qagent fix targets). The three P2s: the one-attempt-per-tick fence and the attempts reset now reach phase 4 (dispatchers skip tick-ledgered tickets; a delivered recovery clears the ladder), an indented real trailing meta block can no longer lose to a column-zero quoted example (fuzzer grammar extended — it caught 711 divergences pre-fix, 0 after), and board-transition validates meta BEFORE any label write, closing the last torn-write window.

Five convergence rounds followed, hardening the reconcile-side reset: crash-durable (reset lands before the journal seal), ack-gated (a live-but-unacked handoff keeps its ladder), and honest about ENOENT (a missing counter ≠ a missing registry; the fixer correctly rejected a bare existence probe that would have broken every healthy fleet). Round 5's residual — a mountpoint-preserved unmount that remounts with a stale counter — is accepted, not fixed: inexpressible in default config, worst case is a liftable premature escalation, and both misclassification directions degrade gracefully. Spec at v1.2.5.

All suites green at e6314d4: board-scripts, all board-api suites, both dispatch-claim suites, reviewing-prs, fuzz divergences=0, lint.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant