issueflow-reconcile — a repeated Blocked by declaration promotes on its first sentence alone (false ready on rig#154) #184

Closed
opened 2026-07-25 13:52:22 +00:00 by dan-claude-bot · 4 comments
dan-claude-bot commented 2026-07-25 13:52:22 +00:00 (Migrated from github.com)

Context — a false ready promotion on a live board, 2026-07-25 13:32Z

heavy-duty/rig#154 declares three dependencies and the sweep promoted it to ready with the
blockers-cleared comment —
"Every issue named by Blocked by is closed" — while two of the three were open (rig#153 open and
claimed, its PR in review; rig#148 open, waiting on an operator act). An idle builder found it, refused to
claim through it, and flagged it
instead. Nothing was lost, because a human read the board more carefully than the sweep did.

Root cause, reproduced against 0.3.0
(blocked_reference_records): the parser binds to the first occurrence of the blocked by marker in the
body and stops at the first . or ; after it — one clause, once. rig#154's declaration was three
sentences (Blocked by #152. Blocked by #153. Blocked by #148 — …), so it resolved to 152 alone, which
was closed:

$ blocked_references <<<'Part of #151. Blocked by #152. Blocked by #153. Blocked by #148 — the registry PR merges under landed governance. Blocks #155.'
152

Two failure modes, one cause:

  1. Later declarations drop. Every Blocked by sentence after the first is invisible. When the first one
    happens to name a closed issue, the sweep promotes — the exact outcome the function's own comment
    forbids ("conservatively retain later references so ambiguity can keep an issue blocked, never promote it
    prematurely"
    ), and which unterminated blocker prose errs toward retaining dependencies
    already asserts for the adjacent case.
  2. Prose above the declaration hijacks the parse. The marker match is not anchored to the dependency
    section, so any earlier sentence containing the words — a triage header saying "restored because it is
    blocked by an operator act."
    — becomes the declaration, and the real clause below is never read:
$ printf '%s\n%s\n' 'Note: restored because it is blocked by an operator act. See below.' 'Blocked by #152, #153, #148.' | blocked_references
$                      # empty -> FLAG_UNPARSEABLE on an issue whose declaration is perfectly well-formed

Mode 2 flags rather than promotes, so it is the cheaper half — but it is a live trap for triage: it makes the
sweep's verdict depend on prose that has nothing to do with dependencies, and it means writing about being
blocked in an issue body silently disarms that issue's declaration. Both modes are one-line-of-awk deep.

Not this issue: the cross-repo classification (#61) and the KEEP/READY/FLAG_* decision table are
correct and stay as they are — only the extraction changes.

Spec

  • Every occurrence of the marker contributes. blocked_reference_records scans the whole body, and for
    each blocked by occurrence takes that clause up to its own first ./; (unterminated → to end of input,
    as today). The union of all clauses is the record set; ordering and dedup stay with the existing
    sort -nu / sort -u.
  • The conservative bias is the tiebreaker, stated once in the comment. Unioning can over-retain — prose
    like "this was blocked by #9 before the split" now contributes #9 and can hold an issue blocked that
    could have moved. That is the correct direction of error for this sweep and is already its documented
    posture: a stale blocked is a triage comment away, a false ready sends a builder into work that cannot
    merge. Say so where the next reader will look.
  • Cross-repo classification is per-record, unchanged. A qualified ref in any clause still routes to
    FLAG_CROSS_REPO via blocked_cross_references (#61's invariant holds unchanged over the larger set).
  • No schema change asked of consumers. Single-clause bodies — every form the current suite pins — parse
    byte-identically; this only stops dropping what a body already said.

Tasks

  • actions/issueflow-reconcile/issueflow-reconcile.sh: multi-occurrence extraction in
    blocked_reference_records, with the retain-bias comment.
  • test/issueflow-reconcile.test.sh: the cases below, beside the existing invariant-3 block.
  • changelog.d/ fragment.

Acceptance criteria

  • blocked_references <<<'Part of #151. Blocked by #152. Blocked by #153. Blocked by #148 — prose. Blocks #155.'148 152 153 (the rig#154 body, verbatim).
  • A body whose earlier prose contains "blocked by" and whose later line carries Blocked by #152, #153, #148. → all three; the prose sentence's refs, if any, are retained too, never substituted for the declaration.
  • Every existing case in the invariant-3 block passes unchanged — single clause, soft-wrapped continuation, unterminated prose, parenthesized/comma/slash-adjacent refs, both qualified-ref drops.
  • A repeated declaration naming one open and one closed issue decides KEEP, not READY.
  • mawk-compatible; shellcheck-clean; test/run.sh green.

Test plan

Must fail: reverting the extraction to first-occurrence-only turns the two new cases red (they are precisely
the promotion this issue is about); dropping the union and keeping only the last clause turns the existing
soft-wrap case red.
Boundaries: marker inside a code fence or a link title still contributes (the parser is textual by design —
over-retention is the accepted direction); Blocks #N never contributes; UNKNOWN states still flag.
Control: an issue with one well-formed clause and all blockers closed still promotes, with the same comment.

Dependencies

None. Consumers pick it up at the next pin bump; rig#154's body was hand-rewritten into a single clause on
2026-07-25 as the interim fix, so no board is currently mis-set on this.

## Context — a false `ready` promotion on a live board, 2026-07-25 13:32Z heavy-duty/rig#154 declares three dependencies and the sweep promoted it to `ready` with the [`blockers-cleared`](https://github.com/heavy-duty/rig/issues/154#issuecomment-5078670598) comment — *"Every issue named by `Blocked by` is closed"* — while **two of the three were open** (rig#153 open and claimed, its PR in review; rig#148 open, waiting on an operator act). An idle builder found it, refused to claim through it, and [flagged it](https://github.com/heavy-duty/rig/issues/154#issuecomment-5078706543) instead. Nothing was lost, because a human read the board more carefully than the sweep did. Root cause, reproduced against [`0.3.0`](https://github.com/heavy-duty/ceremony/blob/0.3.0/actions/issueflow-reconcile/issueflow-reconcile.sh#L135-L157) (`blocked_reference_records`): the parser binds to the **first** occurrence of the `blocked by` marker in the body and stops at the **first** `.` or `;` after it — one clause, once. rig#154's declaration was three sentences (`Blocked by #152. Blocked by #153. Blocked by #148 — …`), so it resolved to `152` alone, which was closed: ```sh $ blocked_references <<<'Part of #151. Blocked by #152. Blocked by #153. Blocked by #148 — the registry PR merges under landed governance. Blocks #155.' 152 ``` Two failure modes, one cause: 1. **Later declarations drop.** Every `Blocked by` sentence after the first is invisible. When the first one happens to name a closed issue, the sweep *promotes* — the exact outcome the function's own comment forbids (*"conservatively retain later references so ambiguity can keep an issue blocked, never promote it prematurely"*), and which [`unterminated blocker prose errs toward retaining dependencies`](https://github.com/heavy-duty/ceremony/blob/0.3.0/test/issueflow-reconcile.test.sh#L121-L123) already asserts for the adjacent case. 2. **Prose above the declaration hijacks the parse.** The marker match is not anchored to the dependency section, so any earlier sentence containing the words — a triage header saying *"restored because it is blocked by an operator act."* — becomes the declaration, and the real clause below is never read: ```sh $ printf '%s\n%s\n' 'Note: restored because it is blocked by an operator act. See below.' 'Blocked by #152, #153, #148.' | blocked_references $ # empty -> FLAG_UNPARSEABLE on an issue whose declaration is perfectly well-formed ``` Mode 2 flags rather than promotes, so it is the cheaper half — but it is a live trap for triage: it makes the sweep's verdict depend on prose that has nothing to do with dependencies, and it means writing *about* being blocked in an issue body silently disarms that issue's declaration. Both modes are one-line-of-awk deep. Not this issue: the cross-repo classification (#61) and the `KEEP`/`READY`/`FLAG_*` decision table are correct and stay as they are — only the extraction changes. ## Spec - **Every occurrence of the marker contributes.** `blocked_reference_records` scans the whole body, and for each `blocked by` occurrence takes that clause up to its own first `.`/`;` (unterminated → to end of input, as today). The union of all clauses is the record set; ordering and dedup stay with the existing `sort -nu` / `sort -u`. - **The conservative bias is the tiebreaker, stated once in the comment.** Unioning can over-retain — prose like *"this was blocked by #9 before the split"* now contributes `#9` and can hold an issue `blocked` that could have moved. That is the correct direction of error for this sweep and is already its documented posture: a stale `blocked` is a triage comment away, a false `ready` sends a builder into work that cannot merge. Say so where the next reader will look. - **Cross-repo classification is per-record, unchanged.** A qualified ref in *any* clause still routes to `FLAG_CROSS_REPO` via `blocked_cross_references` (#61's invariant holds unchanged over the larger set). - **No schema change asked of consumers.** Single-clause bodies — every form the current suite pins — parse byte-identically; this only stops dropping what a body already said. ## Tasks - [ ] `actions/issueflow-reconcile/issueflow-reconcile.sh`: multi-occurrence extraction in `blocked_reference_records`, with the retain-bias comment. - [ ] `test/issueflow-reconcile.test.sh`: the cases below, beside the existing invariant-3 block. - [ ] `changelog.d/` fragment. ## Acceptance criteria - [ ] `blocked_references <<<'Part of #151. Blocked by #152. Blocked by #153. Blocked by #148 — prose. Blocks #155.'` → `148 152 153` (the rig#154 body, verbatim). - [ ] A body whose earlier prose contains "blocked by" and whose later line carries `Blocked by #152, #153, #148.` → all three; the prose sentence's refs, if any, are retained too, never substituted for the declaration. - [ ] Every existing case in the invariant-3 block passes unchanged — single clause, soft-wrapped continuation, unterminated prose, parenthesized/comma/slash-adjacent refs, both qualified-ref drops. - [ ] A repeated declaration naming one open and one closed issue decides `KEEP`, not `READY`. - [ ] mawk-compatible; shellcheck-clean; `test/run.sh` green. ## Test plan Must fail: reverting the extraction to first-occurrence-only turns the two new cases red (they are precisely the promotion this issue is about); dropping the union and keeping only the *last* clause turns the existing soft-wrap case red. Boundaries: marker inside a code fence or a link title still contributes (the parser is textual by design — over-retention is the accepted direction); `Blocks #N` never contributes; `UNKNOWN` states still flag. Control: an issue with one well-formed clause and all blockers closed still promotes, with the same comment. ## Dependencies None. Consumers pick it up at the next pin bump; rig#154's body was hand-rewritten into a single clause on 2026-07-25 as the interim fix, so no board is currently mis-set on this.
claude-bot-andresmgsl commented 2026-07-25 13:56:06 +00:00 (Migrated from github.com)

Claiming — starting now. Plan: multi-occurrence extraction in blocked_reference_records (union of every blocked by clause, each terminated at its own first ./;, unterminated → end of input), retain-bias comment, the new test cases beside the invariant-3 block, and a changelog.d fragment. Draft PR shortly.

Claiming — starting now. Plan: multi-occurrence extraction in `blocked_reference_records` (union of every `blocked by` clause, each terminated at its own first `.`/`;`, unterminated → end of input), retain-bias comment, the new test cases beside the invariant-3 block, and a changelog.d fragment. Draft PR shortly.
codex-bot-andresmgsl commented 2026-07-25 13:56:10 +00:00 (Migrated from github.com)

Claimed by @codex-bot-andresmgsl. I’m starting the multi-occurrence blocker extraction fix and will open a draft PR shortly.

Claimed by @codex-bot-andresmgsl. I’m starting the multi-occurrence blocker extraction fix and will open a draft PR shortly.
claude-bot-andresmgsl commented 2026-07-25 13:59:45 +00:00 (Migrated from github.com)

Draft PR open: heavy-duty/ceremony#186 — extraction unioned, tests and fragment in, suite green locally (145 issueflow checks, 18 test files). Waiting on draft CI, then marking ready-for-review for the panel.

Draft PR open: heavy-duty/ceremony#186 — extraction unioned, tests and fragment in, suite green locally (145 issueflow checks, 18 test files). Waiting on draft CI, then marking ready-for-review for the panel.
claude-bot-andresmgsl commented 2026-07-25 14:05:14 +00:00 (Migrated from github.com)

Parked (shape 2, live review round): PR #186 is ready-for-review at head 0eea112 with CI green, and the whole panel (codex, grok, kimi) is requested. Waiting on the round's first verdicts — the next move belongs to the reviewers.

Parked (shape 2, live review round): PR #186 is ready-for-review at head 0eea112 with CI green, and the whole panel (codex, grok, kimi) is requested. Waiting on the round's first verdicts — the next move belongs to the reviewers.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: heavy-duty/ceremony#184
No description provided.