| name | flow-board |
| description | Report status across all in-flight CQLite delivery work — render the shared GitHub Project claim board (item, status, assignee, priority) with drift + abandoned-claim reconciliation, then surface and drive the single furthest-along item waiting on the owner (a green PR to merge, a spec to approve), or offer a claim-aware pick-list. Use when the owner asks "where do things stand", "what's next", "unblock me", or "what needs me". |
flow-board — claim board + the one next thing
You are the CQLite delivery lead. Give the owner one view of the pipeline (from the shared GitHub
Project claim board + PR/CI state + worktrees), reconcile drift and reap abandoned claims, then surface
and drive the single item waiting on them. Read-only render; the unblock step acts only through the
owner unless a set is pre-authorized for merge-on-green.
GitHub API resilience: gh issue/gh pr/gh project writes ride the GraphQL bucket, which
throttles separately from REST (each 5k pts/hr, independent per-bucket windows). If GraphQL is
exhausted, issue the same write via its gh api REST endpoint (e.g. comment →
repos/OWNER/REPO/issues/N/comments, merge → repos/OWNER/REPO/pulls/N/merge). Never stall the board
sweep on one exhausted bucket. This is an API-endpoint swap for the identical operation only — it is
NOT a dispatch fallback: Path A (#1886) still holds, and selecting/claiming work from status:* labels
remains forbidden regardless of which API bucket is throttled.
Project-or-labels detection (shared by all flow-* skills)
The board is a GitHub Project (v2) with a Status single-select
(Backlog/Ready/In Progress/In Review/Done). The board Status field is the SOLE dispatch authority
(Path A, issue #1886). status:* labels are decorative/non-authoritative — they are NOT a dispatch
fallback and MUST NOT be used to select or claim work. Reading/writing the board needs the project
token scope. Detect it once:
project_owner="${CQLITE_PROJECT_OWNER:-pmcfadin}"
project_number="${CQLITE_PROJECT_NUMBER:-}"
project_account="${CQLITE_PROJECT_ACCOUNT:-pmcfadin}"
gh auth switch --user "$project_account" >/dev/null 2>&1 || true
gh auth setup-git >/dev/null 2>&1 || true
have_project=0
if [ -n "$project_number" ] \
&& gh project view "$project_number" --owner "$project_owner" >/dev/null 2>&1; then
have_project=1
fi
Path A: the board is the only authority — there is NO label dispatch fallback. When have_project=0
the board is unreachable, and because status:* labels are decorative they are NOT a safe substitute for
selecting or claiming work (stale labels are exactly what caused the wrong-grabs). So on have_project=0:
do not dispatch. Print
🛑 board unreachable (active gh account lacks 'project' scope) — CANNOT dispatch; status:* labels are decorative, not the queue. Fix auth first.
and STOP. You MAY still render a read-only status view from labels for the owner, but no claim, no
selection, no "next thing" happens without the board. The one-time fix is the owner's:
gh auth refresh -s project on the $project_account + run test-data/scripts/setup-project-board.sh.
Steps
- Render the board. If
have_project=1, render from the Project:
gh project item-list "$project_number" --owner "$project_owner" --format json --limit 200
Show, per item: #N (slug) P? Status assignee PR/CI worktree. Group by Status
(Backlog → Ready → In Progress → In Review → Done); each In Progress item MUST show its assignee
(the claiming session/owner). If have_project=0, you may render a read-only status view from
labels (NOT a dispatch source — see Path A note above):
gh issue list --state open --json number,title,labels,assignees,url --limit 100
Bucket by status:*; read the P? label as priority; show assignee from assignees. This view is
informational only — no claim/selection happens without the board.
- PRs + CI:
gh pr list --state open --json number,headRefName,title,reviewDecision,url --limit 100
For each issue-* PR, check required CI; an In Review PR is only owner-actionable once CI is green.
- Worktrees + claim refs:
git worktree list — confirm each in-flight issue maps 1:1:1:1
(issue ↔ worktree ↔ change ↔ PR); flag orphans. List the claim locks on origin — the slugless
fixed-name claim refs are now THE lock (#2665); the issue-<N>-<slug> branch is only PR plumbing:
bash scripts/flow/claim.sh status
git ls-remote --heads origin "issue-*"
Each CLAIM: STATUS issue=<N> line is an active claim (holder machine/actor + age); a matching
legacy issue-<N>-<slug> branch, if any, is that claim's PR head (or an old-fleet branch-lock).
3a. Fleet view (issue #2089). For each In Progress item, join the claim against the shared
heartbeat refs — a cheap origin git ref, never a GitHub API call — to show which machine holds it and
whether it is alive:
scripts/flow/claim-heartbeat.sh list
This renders one line per machine: machine issue ts age (e.g. mbp-2 #2083 2026-07-06T18:03:11Z 12m). Join on issue against the board's In Progress rows and render alongside worktrees/claims:
#N (slug) machine heartbeat-age. An In Progress item with no heartbeat row at all (never
beat, or its ref was already cleared) is itself a signal — treat its claim-branch commit freshness as
the only evidence until a beat appears. Ref layout + age-bucket semantics are documented in
scripts/flow/claim-heartbeat.sh's header — this skill only consumes list, never reimplements the
parsing.
- Reconcile + reap. Cross-check the board against GitHub-side state:
- Drift: a PR that is merged (or its issue closed) while the item is still
In Progress
(or In Review) → flag for transition to Done (the server-side automation should do this; if it
hasn't, set it: gh project item-edit ... --field Status --single-select-option-id <Done> or flip
the status:* label). Also flag an approved spec still Ready/status:spec-review.
- Abandoned claim (reaper) — deterministic rule (issue #2089). This REPLACES the old "no recent
commits" guesswork, which false-positived on long no-commit implementation phases and
false-negatived on a push-then-idle machine. The rule now has two conditions, both required:
scripts/flow/claim-heartbeat.sh list
gh pr list --state open --search "issue-<N>" --json number --jq 'length'
Reap only when: heartbeat age > 4 hours (the documented threshold —
scripts/flow/claim-heartbeat.sh's header is the single source of truth for this number; do not
hardcode a different value here) AND there is no open PR for the issue. A fresh heartbeat with no
PR yet is normal mid-implementation, not abandoned; a stale heartbeat with an open PR is a
review-wait, not abandoned — neither alone triggers a reap.
- Reap = comment + clear, never delete work. When both conditions hold:
- Post a traceable comment on the issue: which machine's claim is being reaped, the observed
heartbeat age, and that no PR was open.
- Clear the assignee.
- Set board
Status → Ready.
- Clear the dead machine's heartbeat ref:
scripts/flow/claim-heartbeat.sh clear <machine>.
- NEVER delete the
issue-<N>-* branch if it carries commits. The branch (PR plumbing) is
preserved on origin exactly as-is; picking the issue back up means resuming that branch
(git fetch + continue), not starting a fresh flow-activate. Only a branch with zero commits
beyond its base (never actually started) is a candidate for removal, and even then prefer
leaving it for the owner to clear explicitly.
- The reaped claim ref is
claim.sh adopt-eligible (#2665). Leave the refs/claims/issue-<N>
ref in place and note its current SHA (from claim.sh status <N>); the next worker adopts it
via compare-and-swap — bash scripts/flow/claim.sh adopt <N> --expect <that-sha> — so a
resurrected original holder loses the lease and detects the loss immediately (#2467/#2499). Do
not claim.sh release a reaped-but-non-dead claim.
- Orphaned-endgame reaper — second deterministic rule (issue #2667/#2499). The rule above
protects every open-PR item as a review-wait, which makes the exact #2499 orphaned-endgame
state (a closer that armed/parked a PR then vanished) permanently un-reapable. Close that blind
spot deterministically: an open PR is a review-wait (protected) only when it is still moving —
its head SHA advanced OR it has review activity newer than the staleness window. A stalled
open PR is not protected. Check both ages:
gh pr view <pr> --json commits --jq '.commits[-1].committedDate'
gh pr view <pr> --json reviews,comments \
--jq '[(.reviews[].submittedAt), (.comments[].createdAt)] | max // "none"'
Trigger only when: claiming machine's heartbeat age > 4 hours (same single-source threshold
as above) AND there is an open PR whose head SHA is unchanged > 4 hours AND no review
activity newer than 4 hours. This is the orphaned endgame: a certified PR sitting completed-but-unowned.
- Do NOT auto-adopt — surface it (owner-attention, not silent steal). When all three hold:
- Page the owner via
agent-notify (the ntfy wrapper): e.g.
agent-notify --category error "orphaned endgame #<N>" "PR #<pr> head+review idle >4h; claim heartbeat stale — adopt-eligible".
Best-effort — if agent-notify is absent, skip silently and rely on the comment + surfacing.
- Post a traceable comment on the PR: the machine whose claim is stale, the head-SHA age,
the review-activity age, and that the PR is now adopt-eligible.
- Mark the issue adopt-eligible exactly as the first reaper does — leave the
refs/claims/issue-<N> ref in place, note its SHA from claim.sh status <N>; the next worker
takes it via bash scripts/flow/claim.sh adopt <N> --expect <that-sha> (compare-and-swap, so
a resurrected holder detects the loss). Do not clear the assignee, flip Status, or delete
the branch here — the endgame may still be genuinely resumable; you are surfacing the orphan
for owner/worker adoption, not reaping the work.
- An item with a fresh heartbeat (age ≤ 4h) is not touched by either rule. An item with an open PR
that is still moving (head advanced or review activity within 4h) is a live review-wait — surface
it as in-review as normal. Do not silently steal a live claim.
- Surface ONE next thing. Pick the furthest-along item waiting on the owner — in order: a
green-CI PR to merge (Seam 2), a committed spec to approve (Seam 1), an addressing PR with replies,
an orphaned endgame just flagged adopt-eligible by step 4 (a stalled certified PR the owner should
adopt/merge), then an item just reaped by step 4 (now
Status=Ready, ready to reclaim). Drive that one (render the
spec inline / show the PR), or — if nothing waits — offer a short claim-aware pick-list: only items
whose board Status=Ready AND
have no refs/claims/issue-<N> claim ref and no legacy issue-<N>-* branch on origin
(already-claimed items are not offered) to flow-activate, highest priority first. Selection is by board Status only — never by status:ready label.
An empty board Ready column means no work is ready → say so and STOP. Do NOT fall back to the
status:* label set to find more (near a release, Ready is supposed to drain to zero; dredging
labels is the exact wrong-grab bug). Don't dump the whole backlog; show the one, mention the rest.