pipeline-status · git:20260924.744983f · 2026-09-24 · sha256 88f2aeceb7dd7fa8
pipeline-status git:20260924.744983fA
Immutable. This exact content never changes and is served at /api/v1/blob/88f2aeceb7dd7fa8.
---
name: pipeline-status
description: >
Cross-room visual status board for every active issue moving through the
Overdeck pipeline. One row per issue, one column per phase
(agent → state → needs → PR review → checks → mergeable), with a checkmark
or X in each cell. Designed to be readable from across the room while agents work
autonomously. Surface this BEFORE the verbose pan-status / agent-status
output whenever the user asks "where are we?" or wants a status overview.
triggers:
- /pipeline-status
- pipeline status
- status board
- issue status
- kanban status
- cross-room status
- status snapshot
- where are we
- what's the pipeline doing
allowed-tools:
- Bash
- Read
---
# pipeline-status — Cross-Room Visual Status Board
## What this skill does
Produces a single dense table for every issue returned by the authoritative
pipeline-membership read door (plus active planning sessions), with one column
per workflow phase. This includes exception-queue drift such as `zombie_pr`
and `post_merge_limbo`, even when the tracker lags the PR.
This is the **first thing** to show when the user asks for status. Anything
more detailed (`pan status`, `agent-status`) goes underneath.
## Output format
```
ISSUE BUCKET TITLE MODEL AGENT STATE NEEDS PR-REVIEW CHECKS MERGEABLE
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
PAN-1028 in_flight TTS: first speech utterance gets clipped kimi-k2.6 ✓ working · · · ·
PAN-945 post_merge_limbo Planning artifact path mismatch gpt-5.4 · merged · · · ·
PAN-1024 zombie_pr Lazy-load per-turn diff summaries gpt-5.4 · changes-requested needs-you changes-requested ✗ ·
PLANNING (Opus drafting xBRIEFs)
PAN-1015 Remove claudish routing in favor of CLIProxy claude-sonnet-4-6 ◐ planning
✓ done/passing ◐ in-progress ✗ failed/blocked · pending/n-a
```
## Cell semantics
| Glyph | Meaning |
|-------|---------|
| `✓` | done / passing / running healthily |
| `◐` | in progress (reviewing, testing, queued, merging, verifying, pending) |
| `✗` | failed / blocked |
| `·` | pending or not applicable for this issue's current phase |
## Columns
| Column | Source | Meaning |
|--------|--------|---------|
| ISSUE | `/api/pipeline/membership` `issueId` | PAN-NNNN; the membership endpoint defines the row universe |
| BUCKET | `/api/pipeline/membership` `bucket` | `in_flight`, `zombie_pr`, `post_merge_limbo`, or `planned_backlog` |
| TITLE | `/api/issues` `title` | truncated to ~52 chars |
| MODEL | `/api/agents` `.model` for `agent-pan-NNN` | Which model the work agent is using |
| AGENT | `/api/agents` `.hasLiveTmuxSession` + `.paneState` | `✓` live pane that is `working`, `◐` live pane that is idle/blocked/done, `·` no live pane |
| STATE | `/api/issues/resource-allocated` `.state` | derived pipeline state: `backlog`, `parked`, `planned`, `working`, `in-review`, `changes-requested`, `ready`, `merged`, `closed`, or `·` if unknown |
| NEEDS | `pan show <id> --json` `.attention` (fetched only when STATE is `in-review`, `changes-requested`, or `ready`) | `needs-you`, `stuck`, `api-error`, or `·` |
| PR-REVIEW | `pan show <id> --json` `.pr.reviewState` (same fetch as NEEDS) | `approved`, `changes-requested`, `review-requested`, `commented`, `none`, or `·` if there's no PR |
| CHECKS | `pan show <id> --json` `.pr.checks` (same fetch) | `✓` green, `✗` red, `◐` pending, `·` no PR |
| MERGEABLE | `pan show <id> --json` `.pr.mergeable` (same fetch) | `✓` true, `✗` false, `·` unknown or no PR |
Always sort by priority: P0 hotfix → P1 bug → P2 enhancement → others. Within
each tier, sort by issue ID descending (newest first).
Show a separate **PLANNING** section beneath the in-flight table for any live
`planning-pan-NNN` agent — those are not yet on the kanban so they don't
appear via `/api/issues` filtering.
Agent liveness comes from `GET /api/agents`, which reads the host's terminal
backend (Herdr by default, tmux only under `terminal.backend: tmux`). Never
probe `tmux -L overdeck` directly: on a Herdr host it sees no agents. Despite
its name, `hasLiveTmuxSession` is true for any live pane on either backend.
The membership endpoint takes the **registered project key**, which is not
always the repo name (this repo is registered as `panopticon-cli`). The script
resolves it by matching the current git repository against
`pan project list --json` (by path, then by `github_repo`); it never
hardcodes a key.
## Steps
### 1. Generate the table
Run this script. It gets the row universe from `GET /api/pipeline/membership`,
then joins issue titles, derived state, and live agents as annotations. A
bare array is a successful answer. An HTTP 200 object with
`status: "unavailable"` is a typed blind spot; show its `projectKey`, `reason`,
and `message`, then stop instead of deriving membership from another source.
HTTP 503 means the first snapshot is still loading and may be retried later.
```bash
python3 - <<'PY'
import json, os, subprocess
from urllib.parse import quote
PR_STATES = {'in-review', 'changes-requested', 'ready'}
def git(*args):
try:
return subprocess.check_output(['git', *args], stderr=subprocess.DEVNULL).decode().strip()
except Exception:
return ''
def project_key():
# The registered key, not the repo name. Match this repo (worktrees resolve
# to their main checkout) against the registry, then fall back to origin.
projects = json.loads(subprocess.check_output(['pan','project','list','--json']))
common = git('rev-parse','--path-format=absolute','--git-common-dir')
repo = os.path.realpath(os.path.dirname(common)) if common else ''
for key, p in projects.items():
if repo and os.path.realpath(os.path.expanduser(p.get('path',''))) == repo: return key
origin = git('remote','get-url','origin').removesuffix('.git')
for key, p in projects.items():
if origin and p.get('github_repo') and origin.endswith('/' + p['github_repo']): return key
raise SystemExit(f"No registered project matches {repo or os.getcwd()}; known keys: {', '.join(projects)}")
PROJECT = project_key()
print(f"project: {PROJECT}")
issues_data = json.loads(subprocess.check_output(['curl','-s','http://localhost:3011/api/issues']))
membership = json.loads(subprocess.check_output([
'curl','-s',f'http://localhost:3011/api/pipeline/membership?project={quote(PROJECT)}'
]))
if isinstance(membership, dict) and 'error' in membership:
raise SystemExit(f"PIPELINE MEMBERSHIP ERROR for project {PROJECT}: {membership['error']}")
if isinstance(membership, dict) and membership.get('status') == 'unavailable':
raise SystemExit(
'PIPELINE MEMBERSHIP UNAVAILABLE: '
f"{membership.get('projectKey')} — {membership.get('reason')}: {membership.get('message')}"
)
resource = json.loads(subprocess.check_output(['curl','-s','http://localhost:3011/api/issues/resource-allocated']))
state_by_id = {(r.get('issueId') or '').upper(): r.get('state') for r in resource}
# Backend-agnostic agent inventory (Herdr or tmux, whichever the host runs).
agents = {a['id']: a for a in json.loads(subprocess.check_output(['curl','-s','http://localhost:3011/api/agents']))}
issues_by_id = {(i.get('identifier') or '').upper(): i for i in issues_data}
panissues = []
for member in membership:
if not member.get('inPipeline'): continue
issue = dict(issues_by_id.get(member['issueId'].upper(), {
'identifier': member['issueId'], 'title': '(missing tracker issue)', 'labels': []
}))
issue['pipelineBucket'] = member['bucket']
panissues.append(issue)
def agent_cell(a):
if not a or not a.get('hasLiveTmuxSession'): return '·'
return '✓' if a.get('paneState') == 'working' else '◐'
def derived(issue_id):
try:
out = subprocess.check_output(['pan','show',issue_id,'--json'],
stderr=subprocess.DEVNULL, timeout=60)
return json.loads(out)
except Exception:
return None
def checks_cell(pr):
c = pr.get('checks')
if c == 'green': return '✓'
if c == 'red': return '✗'
if c == 'pending': return '◐'
return '·'
def mergeable_cell(pr):
m = pr.get('mergeable')
if m is True: return '✓'
if m is False: return '✗'
return '·'
def tier(i):
L = ','.join(i.get('labels',[])).lower()
if 'p0' in L.split(','): return 0
if 'bug' in L.split(',') or 'p1' in L.split(','): return 1
return 2
panissues.sort(key=lambda i: (tier(i), -int(''.join(c for c in i['identifier'] if c.isdigit()) or 0)))
W = {'id':10,'bucket':18,'title':44,'model':22,'agent':6,'state':18,'needs':10,'review':18,'checks':7,'mergeable':10}
print()
print(f"{'ISSUE':<{W['id']}} {'BUCKET':<{W['bucket']}} {'TITLE':<{W['title']}} {'MODEL':<{W['model']}} {'AGENT':<{W['agent']}} {'STATE':<{W['state']}} {'NEEDS':<{W['needs']}} {'PR-REVIEW':<{W['review']}} {'CHECKS':<{W['checks']}} {'MERGEABLE'}")
print('─' * 150)
for i in panissues:
iid = i['identifier']
state = state_by_id.get(iid)
d = derived(iid) if state in PR_STATES else None
pr = (d.get('pr') or {}) if d else {}
attention = (d.get('attention') if d else None) or '·'
agent = agents.get(f"agent-{iid.lower()}")
amodel = (agent or {}).get('model') or '-'
title = i['title']
if len(title) > W['title']: title = title[:W['title']-1] + '…'
if len(amodel) > W['model']: amodel = amodel[:W['model']-1] + '…'
review_cell = pr.get('reviewState') or '·'
print(f"{iid:<{W['id']}} {i['pipelineBucket']:<{W['bucket']}} {title:<{W['title']}} {amodel:<{W['model']}} "
f"{agent_cell(agent):<{W['agent']}} {(state or '·'):<{W['state']}} {attention:<{W['needs']}} "
f"{review_cell:<{W['review']}} {checks_cell(pr):<{W['checks']}} {mergeable_cell(pr)}")
print()
print("PLANNING SESSIONS (Opus drafting xBRIEFs)")
print('─' * 150)
for ps in sorted(n for n, a in agents.items() if n.startswith('planning-pan-') and a.get('hasLiveTmuxSession')):
iid = 'PAN-' + ps.replace('planning-pan-','').upper()
model = agents[ps].get('model') or '?'
title = next((i['title'] for i in issues_data if (i.get('identifier','') or '').upper()==iid), '(unknown)')
if len(title) > W['title']: title = title[:W['title']-1] + '…'
print(f"{iid:<{W['id']}} {title:<{W['title']}} {model:<{W['model']}} ◐ planning")
member_ids = {m['issueId'].upper() for m in membership if m.get('inPipeline') is True}
ready = [r for r in resource
if (r.get('issueId') or '').upper() in member_ids and r.get('state') == 'ready']
print(f"\nAwaiting Merge: {len(ready)} issue(s) ready for human approval")
for r in ready:
print(f" → {r['issueId']} {(r.get('title') or '')[:80]}")
print()
print("✓ done/passing ◐ in-progress ✗ failed/blocked · pending/n-a")
print("Columns: AGENT (work agent alive) → STATE → NEEDS → PR-REVIEW → CHECKS → MERGEABLE (ready for merge)")
PY
```
### 2. Read the Awaiting Merge summary line
The step 1 script also prints an **Awaiting Merge** line: the count and list of
canonical pipeline members whose derived `state` is `ready`. Surface it
directly under the table.
### 3. (Optional) Defer to other status skills if user wants more detail
If the user wants MORE than the table — system health, RAM, CPU per agent,
specialist trees, etc. — invoke `pan-status` next. The pipeline-status table
is the headline; everything else is the appendix.
```
For deeper detail beyond the pipeline view:
• pan status — running agents overview + system health
• pan resources — RAM/swap by agent
• pan-agent-activity — read each live agent's recent output
```
## When to surface this
- User asks "what's running?", "where are we?", "show status", "status snapshot"
- After invoking `/pan-flywheel` to checkpoint progress
- After spawning new agents
- Before reporting completion of a flywheel run
- Whenever the user says "give me a visual" or "snapshot"
## When NOT to use this
- During an active long-running operation (e.g. mid-merge, mid-build) — the
table reflects steady state and will mislead during transitions.
- For Mind Your Now (MIN-*), Auricle (AUR-*), Krux (KRUX-*) — this skill is
Overdeck-specific. Other trackers have their own dashboards.
## Notes
- The table's issue universe comes only from `/api/pipeline/membership`; it uses
`/api/issues`, derived state, and `/api/agents` only to annotate those
members. If the dashboard is down, fall back to `pan status` text.
- This skill is **read-only**. It does not change agent state, merge anything,
or write to the DB. Pure observation.