Commit 4d29ec3
authored
fix: make backlog grooming reflect what actually blocks an issue (#623)
* fix: make backlog grooming reflect what actually blocks an issue
The digest misclassified enough of the board that grooming could not be
trusted, and every misclassification pushed work in the same direction:
towards looking more ready than it was. Measured against the live board,
Ready went 1 -> 0 and In Progress 5 -> 1.
Five defects, each traced to a real item.
1. `analyzed` was tested BEFORE any wait, so an analysed-but-blocked item
reported Ready. #96 was labelled `analyzed`, prioritised P2, carried no
blocking label, and still could not be built because its approach was
undecided. It read as dispatchable, a session was dispatched at it, and
that session deadlocked on three design questions. Waits now outrank
`analyzed`.
2. The board's `Awaiting` field was never read. 17 items have it set
(`discussion` x11, `reporter` x6) and the digest derived its own value from
labels instead, so recorded grooming had no effect on anything. The field is
now authoritative, with `awaiting_source` and `awaiting_suggested` exposed
so an unset field can be reconciled rather than silently invented.
3. `awaiting: discussion` was returned for ANY human comment, which is not a
blocker -- thanks, a "me too" and a follow-up question all pushed an item to
Analysis. Only a recorded wait or a blocking label does that now.
4. A worktree left on disk pinned its issue to In Progress forever. #593,
#571, #542 and #466 all reported In Progress while their PRs (#618, #579,
#591, #517) had merged. Staleness is decided by comparing the worktree's own
branch against merged PRs -- an exact match, deliberately not the fuzzy
issue-number match used to associate a worktree with an issue.
5. `blocked` did not fail Ready, so #571 reported Ready for Dev while labelled
`blocked`. Definition of Ready criterion 5 now holds, including an
unresolved `Blocked by #N`.
Ready for Dev also finally requires a Priority, which the design always
specified and the code deferred "until a board exists". It exists.
New: `last_comment` {author, days, is_reporter, is_bot}. Without it the digest
could not represent the transition that matters most to grooming -- the
reporter answering us. A count and a date cannot tell that from a nudge we
posted, which is why #621 crossed the Definition of Ready line unnoticed.
REJECTED while building this: mapping a merged PR to Done. It reclassified 7
open issues (#118, #120, #403 among them) as finished, and it contradicts this
project's rule that beta PRs omit `Closes #N` until graduation -- an open issue
with a merged fix is the NORMAL state. `merged_pr` is reported; it moves no
column, and a test pins that.
Merged PR bodies are reduced to their closing references before reaching jq;
passing 200 of them through --argjson overflows the argument list.
Tests: 25 pass. The `gh` shim now branches on the full argument string, because
`pr list` is called twice with different `--state` values and matching only the
subcommand returned the open list for both -- which would have made every open
PR look merged. Fixtures gained `createdAt` on comments, which real gh always
sends and whose absence failed `strptime` rather than testing anything. `_run`
now surfaces the digest's stderr instead of a bare "exit status 5".
* fix: only a still-open Blocked by #N fails Ready
Review of #623 found a real gap, and it contradicted an explicit claim: both
the commit message and SKILL.md said this enforced an "unresolved" blocker,
while `blocked_by` was a pure text scan that never checked whether the blocker
was still open.
A `Blocked by #N` line is never edited out of an issue body once N lands, so
that scan pins the item out of Ready for Dev permanently. That is the same
failure this script exists to fix, pointing the other way: an item reading
wrong relative to its real state.
`$issues` is already the open-issue list, so membership decides it with no
extra API call. `blocked_by` keeps the raw parse so the reference stays
visible; the new `blocked_by_open` is the subset that actually blocks, and
`$blocked` reads that.
Also pins the precedence question the review asked me to confirm rather than
guess at: a recorded wait DOES outrank a live worktree in `column`, because
unsettled scope must not read as progress. The risk is hiding active
undelivered code, so the worktree stays reported on the item — the wait
changes the column, not the evidence. Now tested and documented in the
reconciliation table instead of being implied.
27 tests pass.
* feat: a Rhythm pass that carries work from incoming to a ready PR
Every follow-up rule in the backlog skill had been written down and NONE had
ever fired. The 14-day reporter chase, the 28-day park, the reporter-replied
re-check, the stale-worktree handoff: all decoration, because each needed a
model to notice it and nothing scheduled one.
So the noticing is deterministic now and lives in scripts/backlog-rhythm.sh.
Every rule is a comparison over the digest — no judgement, no tokens. A quiet
backlog prints "RHYTHM: nothing due." for the cost of one process, which is
what makes it worth running on a timer at all. The PO agent is needed only to
ACT, and only when something is due.
The pass covers BOTH halves of the path to an approvable PR:
Issue side — recheck_ready, nudge_reporter, park, surface_discussion,
set_awaiting, set_priority, triage_labels, dispatchable.
PR side — and this is the half that actually hands the maintainer something:
mark_ready approved but still a draft <- the finish line
awaiting_maintainer approved and out of draft
request_review draft with no review at all
rework changes requested
resolve_conflict CONFLICTING (produces no CI run, so it reads as
"checks never fired" and nobody investigates)
Those two states were invisible in practice. #615 and #617 sat APPROVED and
still drafts overnight with nothing left but the merge; #619 was never
reviewed at all. Nothing was watching either transition.
Ordering is load-bearing. PR actions come first because they are closest to
the finish line, and recheck_ready outranks the chases: nudging someone who
has already replied is the worst output this pass could produce.
Quiet time is measured from the LAST COMMENT, not updatedAt — a label change
or a board move bumps updatedAt, so an issue nobody has spoken on for a month
would look active and never age into a chase.
A bare COMMENTED review is not treated as a verdict, because the review bot
posts its inline notes as one before the summary.
Against the live board the pass finds 30 due actions, including PR #490
awaiting the maintainer, #162 park (quiet 54d), three stale worktrees and
three conflicted PRs.
RHYTHM_DIGEST_FILE / RHYTHM_PRS_FILE are test seams, the same shape as
BESS_ENV_FILE in gh-agent.sh. 16 tests pin the rules, including that a quiet
backlog is a noop and that a reply beats the chase.
Still not wired to a schedule — that is the invocation, not the logic, and it
is deliberately a separate step.
* refactor: hand unfinished PRs back to implement-issue instead of duplicating it
The previous commit built request_review / mark_ready / rework into the Rhythm
pass, which is a second copy of implement-issue Step 11. That contradicts the
argument used to put resume in Step 0 rather than in a separate skill: two
copies of one review loop means one of them goes stale.
It also mis-diagnosed the symptom. #615 and #617 did not sit APPROVED-but-draft
because nothing was watching for that state; they sat there because the
sessions that owned them exited before Step 11 finished. The fix belongs where
the loop already lives.
So every unfinished draft now resolves to ONE action, `resume_implementation`,
carrying the issue number so the handoff is directly runnable. Step 0 re-enters
at the earliest incomplete step, whether the PR needs a first review, a rework,
or just the ready flag it never got. Two fleet-level exceptions stay in the
pass, because implement-issue deliberately does not widen to them:
`awaiting_maintainer` (report only) and `resolve_conflict` (sweep-prs).
Adds the stalled-work rule this was missing: a LIVE worktree with no session
behind it is an implementation that stopped mid-flight -- the machine
restarted, the session was killed, or the agent exited between steps. Nothing
picked these up, and an audit found 34 such worktrees, 8 holding real unpushed
commits and one with 32. Against the live board it finds #466 and #602.
`pr == null` guards that rule so work with a PR is reported once, by the PR
branch, rather than twice.
It is always a RESUME, never a restart: Step 4 branches fresh from origin/main
and would delete commits that exist nowhere else. The detail string says so,
and a test pins it.
SKILL.md gains the reasons a future pass must not re-learn this: do not drive
the review loop here; a session reporting `working` may have written nothing
(three dispatches produced zero writes in one day while reporting healthy
state); and read `claude agents --json` unsandboxed, since ~/.claude/jobs is
sandbox-denied and a sandboxed listing returned 1 session where the truth was
17.
19 rhythm tests, 27 digest tests, gate green.
* fix: match Blocked by #N per line, and drop dead code the refactor left
Review of #623 found a real misclassification path in a PR whose whole point is
eliminating them.
`blocked_by` was a free `scan` over the issue body, so it matched the substring
regardless of what preceded it. "not blocked by #500 anymore" and "no longer
blocked by #500" both registered as live blockers -- and those are the natural
way to update an issue once its blocker resolves, so the false positive fired
exactly when the blocker was GONE. The item would be pinned out of Ready for
Dev permanently.
The severity is new, not latent: on main `blocked_by` was extracted and never
fed into `column()`, so a bad parse was inert. Gating `column()` on it is what
gave it teeth.
Matched per LINE and anchored to the line start now, optionally bulleted, which
is the convention the skill documents ("a `Blocked by #N` line in the issue
body"). Anchoring rejects the negations without a blacklist that would only
cover the phrasings someone happened to think of. It also fixes the reviewer's
third point: an untriaged issue merely mentioning a blocker in prose no longer
moves Backlog -> Analysis with no human triage behind it.
Four tests: the bulleted form still counts, three negated phrasings do not, and
an incidental mid-sentence mention does not reclassify.
Also from the same review:
- `human_comments` became dead code when `awaiting` stopped deriving
`discussion` from comment activity. Removed, and the stale comment on
`comments:` that still described that mechanism is corrected.
- SKILL.md claimed the board's custom-field JSON shape was "confirmed" where
the previous text had explicitly said unconfirmed, without showing the
evidence. It was verified live; the command and its result are now recorded,
with a note that the tests fabricate that shape and so cannot prove it.
49 tests pass (27 digest + 19 rhythm, plus the 3 new negation cases).
* fix: resume a PR by its own number when no issue is linked
`implement-issue` is used for TODO.md items and refactors, not only for
issues, so a draft PR with no linked issue is normal rather than a defect. The
pass reported "no issue references this PR; finish it by hand", which left every
self-directed PR with no owner in the loop -- exactly how #620, #622 and #623
all ended up driven by hand today.
No flag distinguishes the two cases: GitHub numbers issues and PRs from ONE
sequence per repo, so a bare number is already unambiguous and Step 0 can
resolve whichever it is. An earlier draft of this used `--pr <n>`; that
distinction carries no information.
Where an issue IS linked it is still named, because it carries the diagnosis.
Where none is, the PR number is the handle, and a strong one: it holds the
branch, the diff, the scope assessment and the review verdict, which is
everything Step 0 reads.
Step 0 accepting a PR number is a matching change to implement-issue's SKILL.md,
which lives on the fix/review-verdict-placeholder branch (#622) where Step 0 was
added. Both have to land for the loop to cover this case.1 parent 3d964fa commit 4d29ec3
5 files changed
Lines changed: 1250 additions & 133 deletions
File tree
- .claude/skills/backlog
- backend/tests
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
| 17 | + | |
| 18 | + | |
18 | 19 | | |
19 | 20 | | |
20 | 21 | | |
21 | 22 | | |
22 | 23 | | |
23 | | - | |
24 | | - | |
25 | | - | |
26 | | - | |
27 | | - | |
28 | | - | |
29 | | - | |
30 | | - | |
31 | | - | |
32 | | - | |
33 | | - | |
34 | | - | |
35 | | - | |
36 | | - | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
37 | 58 | | |
38 | 59 | | |
39 | 60 | | |
| |||
54 | 75 | | |
55 | 76 | | |
56 | 77 | | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
57 | 108 | | |
58 | 109 | | |
59 | 110 | | |
| |||
64 | 115 | | |
65 | 116 | | |
66 | 117 | | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
67 | 128 | | |
68 | 129 | | |
69 | 130 | | |
| |||
90 | 151 | | |
91 | 152 | | |
92 | 153 | | |
93 | | - | |
94 | | - | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
95 | 158 | | |
96 | 159 | | |
97 | 160 | | |
| |||
106 | 169 | | |
107 | 170 | | |
108 | 171 | | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
109 | 236 | | |
110 | 237 | | |
111 | 238 | | |
| |||
0 commit comments