poll digest: a human's inline ask prints 166 lines below an empty review header, with no unreplied marker nf-7ykw ← Beads

closed priority 2 task unassigned

Twinned from https://github.com/cfsh/ninefold/issues/732 by tools/beads/import_github.py's reconcile pass.

Filed unclaimed and unlabelled from a strategic-uplift window, working #702. Backlog.

CLAUDE.md makes reading @aedanpope's feedback the highest-priority thing a tick does — *"Read them every poll and at session start"*, *"A silent fix isn't enough"*, and three channels are the floor. poll.py reads all three correctly. What it then does with them is the problem.

Measured, on PR #713, 2026-08-07

@aedanpope reviewed at 09:02:21Z. Via the API:

``` body_len=0 state=COMMENTED ```

His review body was empty — the entire ask was in an inline comment on CLAUDE.md. That is not unusual; it is how a line-specific review is written, and inline threads are one of the three channels CLAUDE.md names.

In the digest for that tick:

| line | content | |---|---| | 4 | REV aedanpope [COMMENTED] 2026-08-07T09:02:21Z: ← nothing after the colon | | 5-169 | the NFR verdict, then 165 lines of the session's own previous comment bodies | | 170 | INL [CLAUDE.md] aedanpope …: We shouldn't say "Report the PR URL and stop"… |

166 lines between the announcement and the payload, and the announcement carries no indication that a payload exists.

Three things, and the third is the one with the machinery already present
  1. An empty-bodied review renders as a content-free line. poll.py:887 prints REV {who} [{state}] {ts}: {body}; with body == "" a session sees a header and nothing. Nothing says "the content is in an inline comment below".
  2. Inline comments are printed last, structurally. The block order is REV (:887) → NFR (:920) → CMT bodies (:975) → INL (:979). So a human's line-specific ask always lands *after* every comment body — and the bulkiest comment bodies are usually the session's own review replies, which it wrote and does not need re-read. Composes badly with #726.
  3. ⚠️ There is no "you owe a human a reply" marker at all. last_reply and unanswered are computed at :898/:940 — and used only for NFR verdicts, to raise ⚠ NFR GAPS — action the ledger. A comment, review or inline from @aedanpope with no later reply produces no warning line and no counter. The ⚠ NEEDS ATTENTION — N unreplied comment(s) block exists in the *issues* section and has no counterpart for tracked PRs.

The session is still *woken* — human comments do enter the fingerprint, correctly — so this is not #587/#623. It is what happens after the wake: the tick fires, the digest prints, and the one line that mattered is 166 lines down with nothing flagging it.

Why the existing backstop is not enough

merge_pr.py's feedback ledger does catch an unanswered human comment — at merge time, which is hours later and only if the session tries to merge. The digest is the surface that exists to catch it *now*, it has the data and the predicate, and it spends them only on the bot.

⚠️ This is #679's shape with the roles swapped. That issue is about a CONVERGED verdict carrying work being invisible on the quiet path; this is about a human's ask being invisible in a digest that printed it. Same conclusion: the loop needs *"is there anything here to do?"*, and result= — or here, "did the bot say GAPS" — is the wrong key.

Shapes, cheapest first

⚠️ Do not truncate a human's text to close the gap. CLAUDE.md's doctrine is that the session must read what he actually wrote, verbatim — the fix is ordering and flagging, never abbreviation.

Related: #679 (same key-on-the-wrong-thing shape, bot side), #726 (digest volume — the 165 lines here are its tracked-PR half), #451/#623/#587 (fingerprint scoping — distinct; the wake works, the presentation does not).

Dependencies

None.

Comments

No comments.

Add a comment