Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 144 additions & 17 deletions .claude/skills/backlog/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,47 @@ never implement, and you never assign. Implementers pull the top of Ready.
Every pass starts from one command. Do not read issues one by one to build a
picture:

./scripts/backlog-digest.sh
./scripts/backlog-rhythm.sh # what is DUE now — start here
./scripts/backlog-digest.sh # the full evidence, when you need detail

Open an individual issue only when you are deciding about that issue.

## Prerequisites

Board reads need `PROJECT_NUMBER` set and the board created (deferred —
`scripts/backlog-board-init.sh`). Board writes need `BESS_PO_TOKEN` with
`project` scope (also deferred). Until both exist, a pass fails loudly at the
first board access in `backlog-digest.sh` — that failure is expected, not a
bug to route around.

**One thing to verify the first time a board exists.** No board has ever
existed, so the JSON shape `gh project item-list --format json` uses for a
custom field is unconfirmed; the digest assumes each item carries a top-level
`.priority`. On the first real run, check that `priority` is populated rather
than `null` for an item you have set a priority on. If it is `null`, fix the
jq path in `scripts/backlog-digest.sh` — do not add a fallback that tries
several shapes. A silently-null `priority` disables ranking axis 2 without
any error.
The board exists: **Project #1, "BESS Manager Backlog"**. `PROJECT_NUMBER=1`
lives in the repo's `.env`, which `backlog-digest.sh` does **not** source — so
export it first, or the digest exits with a "board has not been created yet"
message that is misleading rather than wrong:

set -a; . ./.env; set +a; ./scripts/backlog-digest.sh

Board writes need `BESS_PO_TOKEN` with `project` scope.

The custom-field JSON shape is **confirmed against the live board**, not
assumed: `gh project item-list --format json` puts each single-select value at
the item's top level, so `.priority` and `.awaiting` read directly. Re-verify
with the command itself rather than trusting this paragraph — the tests fabricate
that shape, so they cannot prove it:

gh project item-list 1 --owner johanzander --format json \
| jq '[.items[] | {n: .content.number, p: .priority, a: .awaiting}] | .[0:3]'

That returned populated `P1`–`P3` values and 17 items with `Awaiting` set. It
matters because a wrong path fails silently — `.priority?` / `.awaiting?` resolve
to `null` for every item with no error, which reads exactly like an ungroomed
board.

Field options, as they actually exist — do not invent values outside these sets:

| Field | Options |
|---|---|
| `Status` | `Backlog`, `Analysis`, `Ready for Dev`, `In Progress`, `In Review`, `Done` |
| `Priority` | `P1`, `P2`, `P3`, `P4` — **there is no `P0`** |
| `Awaiting` | `reporter`, `discussion`, `upstream`, `analysis` |
| `Source` | `issue`, `TODO` |

The digest's `column` values match `Status` exactly, so reconciling a card is a
string comparison, not a translation.

## When to Use

Expand All @@ -54,6 +75,36 @@ Post as the PO identity: `scripts/gh-agent.sh --as po issue comment ...`.
If a board write fails for missing scope, stop and report
`gh auth refresh -s project`. Never fall back to a file.

## What the digest tells you, and why each field exists

Four fields carry the grooming signal. Each was added because its absence
caused a real misclassification:

| Field | Meaning |
|---|---|
| `awaiting` | The blocking wait. **Outranks `analyzed`** — an item whose scope is unsettled is in *Analysis* no matter how far it got. Comes from the board field when set, else from a blocking label. |
| `awaiting_source` | `board` or `label`. A `label` source with no board value is a triage action: set the field. |
| `awaiting_suggested` | What the labels imply, so an unset field can be reconciled without guessing. |
| `last_comment` | `{author, days, is_reporter, is_bot}`. **The reporter-replied signal.** A comment count and a last-activity date cannot tell "the reporter answered us" from "we posted a nudge", which is why the follow-up chase never fired. |
| `stale_worktree` | The worktree's own branch has already merged, so it is rot, not progress. Hand it to `sweep-prs`. |
| `blocked` | `blocked` label, or a `Blocked by #N` whose blocker is **still open**. Fails Ready outright. |
| `blocked_by` / `blocked_by_open` | Every parsed reference, and the subset still open. Only the latter blocks — a `Blocked by #N` line is never edited out once N lands, so treating the raw scan as unresolved pins an item out of Ready forever. |

**A wait outranks a live worktree, deliberately.** An item with a recorded wait
reports *Analysis* even when a worktree is checked out for it, because unsettled
scope must not read as progress. The worktree is still reported on the item
(`worktree`, `worktree_branch`), so active undelivered code stays visible — the
wait changes the column, not the evidence. Check those fields before assuming an
*Analysis* item has no code behind it.

**A human comment is not a wait.** `awaiting: discussion` used to be returned
for any human comment, which pushed items to *Analysis* for ordinary traffic —
thanks, a "me too", a follow-up question. Only a recorded wait or a blocking
label does that now; `last_comment` is what you read to judge the rest.

**`Ready for Dev` requires a `Priority`.** An analysed item with no priority is
un-ranked, so it cannot be "next" — it stays in *Analysis* as a triage action.

## Definition of Ready

Nothing is dispatched that has not crossed this line. A bug is Ready when:
Expand All @@ -64,6 +115,16 @@ Nothing is dispatched that has not crossed this line. A bug is Ready when:
4. An approach is agreed (Stage 2 analysis, or the maintainer's say-so)
5. No unresolved blocker

**Criterion 4 is the one that gets skipped, and the `analyzed` label is not
proof of it.** Stage 2 can diagnose a request correctly and still leave its
design open — #96 was labelled `analyzed`, prioritised `P2`, carried no blocking
label, and was still not implementable, because *how* to build it was undecided.
It reported *Ready*, an implementation session was dispatched at it, and that
session deadlocked on three design questions nobody was there to answer. When
an approach is genuinely undecided, record it: set `Awaiting` and post the open
questions on the issue, so a later attempt can rehydrate them rather than
rediscover them.

An enhancement is Ready when 3–5 hold and the user-visible outcome is stated.
An item failing any criterion stays in Backlog or Analysis and becomes your
follow-up, not a developer's problem.
Expand All @@ -90,8 +151,10 @@ always wins** — never trust a card's current position. Act on each mismatch:

| Mismatch | Action |
|---|---|
| card *In progress*, no worktree, no PR | abandoned — move to *Ready*, report it |
| worktree present, no session, no PR | the session died mid-issue. Report it and offer to relaunch; the branch's commits survive. **Never silently relaunch** — a session that died twice is telling you something |
| card *In Progress*, no worktree, no PR | abandoned — move to *Ready for Dev*, report it |
| `stale_worktree: true` | the branch already merged; the worktree is rot, not work. Hand to `sweep-prs`, and do not read it as progress |
| worktree present, no session, no PR | the session died mid-issue. The branch's commits survive — resuming is `/implement-issue <n>`, whose Step 0 detects the prior work and re-enters at the right step. **Never silently relaunch**: a session that died twice is telling you something, and a background dispatch that reports `working` may have written nothing at all — verify by work product (`git -C <wt> log`, file mtimes), never by session state |
| `last_comment.is_reporter` and `awaiting: reporter` | the reporter answered. Re-check the Definition of Ready — this wait may be satisfied, and it is the transition nothing used to notice |
| PR `CONFLICTING` | hand to `sweep-prs` |
| worktree whose PR merged | prune via `sweep-prs` |
| issue closed, card not *Done* | move the card |
Expand All @@ -106,6 +169,70 @@ Also review the digest's `orphans` list (worktrees with no matching open
issue, PRs with no `fixes/closes/resolves` reference) and hand any worktree
or PR rot found there to `sweep-prs`.

## Verb: rhythm — the unattended pass

The one that carries work from incoming to a **ready PR**. Start here on every
`/loop /backlog` tick:

scripts/backlog-rhythm.sh

It answers "what is due right now" deterministically — every rule is a
comparison over the digest, so a quiet backlog costs one process instead of a
model pass, and `RHYTHM: nothing due.` is a legitimate noop tick. **Do not
re-derive these by reading issues; act on what it lists.**

Why it exists: every follow-up rule in this skill had been written down and
**none had ever fired.** They each needed a model to notice them and nothing
scheduled one, so the 14-day chase, the 28-day park and the reporter-replied
re-check were decoration.

Actions, and who does what:

| Action | Do |
|---|---|
| `resume_implementation` | `/implement-issue <n>`. **The action that produces a ready PR** — Step 11 requests the review, acts on the verdict and runs `gh pr ready`. Covers a draft needing a first review, a rework, an approved PR that never got flipped, *and* a worktree whose session died |
| `awaiting_maintainer` | nothing; report it. Out of draft is the finish line |
| `resolve_conflict` | hand to `sweep-prs` |
| `recheck_ready` | the reporter answered: re-check Definition of Ready, clear `Awaiting` if satisfied |
| `nudge_reporter` | one nudge, as the PO identity |
| `park` | move to *Backlog* — the chase went unanswered |
| `surface_discussion` | summarise the thread, put the open question to the maintainer. **Never auto-park an open conversation** |
| `set_awaiting` / `set_priority` / `triage_labels` | grooming debt: write the board field or label |
| `dispatchable` | propose for dispatch — needs the maintainer's go-ahead |

**This pass does not drive the review loop, and must not learn to.**
`implement-issue` owns a PR from its first commit to `gh pr ready`; Step 11
already requests the review, acts on the verdict and flips the PR. So every
unfinished draft resolves to one action — hand it back — and a second copy of
that loop is never built here. It is the same argument that put resume in Step 0
instead of a separate skill: two copies of one loop means one of them goes stale.

**Restarting a stalled issue is always a resume, never a fresh start.** Step 0
re-enters at the earliest incomplete step. A restart runs Step 4, which branches
from `origin/main` and deletes commits that exist nowhere else — an audit found 8
abandoned branches carrying real work, one with 32 commits.

**A session reporting `working` may have written nothing.** Three background
dispatches in one day produced zero tracked-file writes while reporting healthy
state, and `claude logs` returns only spinner frames. Read `claude agents --json`
**unsandboxed** (`~/.claude/jobs` is sandbox-denied, so a sandboxed listing
silently truncates — it returned 1 session where the truth was 17), and confirm
progress by work product: commits, `MERGE_HEAD`, file mtimes.

**Ordering matters.** Work `resume_implementation` first — it is the only action
that ends in something approvable. `recheck_ready` outranks the chases, because
nudging someone who has already replied is the worst output this pass can
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 otherwise look active and never age into a chase.

**A `dispatchable` item is a proposal, not a launch.** Dispatch spends real
money and needs the go-ahead. And verify the item truly meets criterion 4 first:
`Ready for Dev` is derived, and a design-heavy item will stop and ask a question
no unattended session can answer.

## Verb: next

Rank Backlog and Ready items in this order:
Expand Down
Loading
Loading