|
| 1 | +--- |
| 2 | +name: plan-dag |
| 3 | +description: Render the current plan as a high-DPI PNG dependency DAG — nodes are issues / sub-issues / PRs, edges come from sub-issue links plus dependency-language prose ("Depends on", "Part of", "Blocks", "Closes") and PR / commit cross-references, and every node is color-coded done / in-progress / available-next / blocked so sequencing and critical path are obvious at a glance. Use when asked "plan as dag", "draw a dag", "dag diagram", "show the dependency graph", "what's blocking what", "what's the critical path", "what can be parallelized", "what's left for #N", or right after a `what's next` survey when sequencing the next pick is the actual question. |
| 4 | +allowed-tools: Read, Bash(git log:*), Bash(git status:*), Bash(git branch:*), Bash(.claude/skills/plan-dag/scripts/plan-dag-render.py:*), Bash(~/.claude/skills/plan-dag/scripts/plan-dag-render.py:*), Bash(.claude/skills/plan-dag/scripts/plan-dag-render.test.sh:*), Bash(~/.claude/skills/plan-dag/scripts/plan-dag-render.test.sh:*), mcp__github__issue_read, mcp__github__list_issues, mcp__github__search_issues, mcp__github__list_pull_requests, mcp__github__pull_request_read |
| 5 | +--- |
| 6 | + |
| 7 | +# plan-dag |
| 8 | + |
| 9 | +Render the current plan as a dependency DAG so sequencing, the critical path, and parallelizable work are visible at a glance. Output is a high-DPI PNG with status fills, an "available next" highlight, and a double-bordered close sentinel — rasterised from a styled graphviz layout through headless Chromium, then sent to the user via `SendUserFile`. |
| 10 | + |
| 11 | +This skill is repo-agnostic. It assumes a GitHub-backed issue tracker with sub-issue links and dependency-language prose; it makes no assumptions about specific labels, area taxonomies, or repo-specific dev-process skills. |
| 12 | + |
| 13 | +**Why PNG and not HTML / ASCII?** PNG is the only output that survives every chat surface the skill targets. ASCII / Unicode box-drawing loses column alignment when surfaces reflow whitespace or apply syntax highlighting, and it can't carry the color/status fills that make state legible at a glance. Self-contained HTML pages don't render inline in chat — they download as attachments, which defeats the point of an at-a-glance diagram. PNG renders inline, keeps color, and doesn't depend on the host's font or rendering quirks. |
| 14 | + |
| 15 | +## When to use |
| 16 | + |
| 17 | +- After a `what's next` survey, to commit to a sequence. |
| 18 | +- When asked "what's left for #N" on an umbrella spec with sub-issues. |
| 19 | +- Before picking the next branch — to see what unblocks the most downstream work. |
| 20 | +- When a spec fans out into N sub-issues and the dependency edges aren't all linear. |
| 21 | + |
| 22 | +Skip when: |
| 23 | + |
| 24 | +- A single-PR spec — the plan *is* the Plan section, not a DAG. |
| 25 | +- An unanswered design question — drawing a DAG before alignment is theater. |
| 26 | +- The "graph" is one linear chain of three or fewer nodes — a sentence is shorter than a diagram. |
| 27 | + |
| 28 | +## Inputs |
| 29 | + |
| 30 | +| Parameter | Default | |
| 31 | +|-----------|----------| |
| 32 | +| **Scope** | Inferred — current branch's spec, the umbrella the user named, or the open issues just surveyed. | |
| 33 | +| **Granularity** | Spec-level; drop to sub-issue / PR level for an umbrella that has fanned out. | |
| 34 | +| **Output** | High-DPI PNG (only target). `--out <path>` is required; emoji status indicators are on by default and can be turned off with `--emoji=off` if the rendering system lacks a color emoji font. | |
| 35 | + |
| 36 | +## Workflow |
| 37 | + |
| 38 | +``` |
| 39 | +1. Discover Pull issues / sub-issues / PRs in scope |
| 40 | +2. Classify Mark each node done / in-progress / open |
| 41 | +3. Edge-build Read dependencies from sub-issue links + body prose |
| 42 | +4. Render Emit JSON IR → PNG → SendUserFile + prose commentary |
| 43 | +``` |
| 44 | + |
| 45 | +### 1. Discover |
| 46 | + |
| 47 | +Resolve the scope from the conversation, not from a generic crawl. Typical triggers: |
| 48 | + |
| 49 | +- An umbrella spec → walk its sub-issue list (`mcp__github__issue_read` with `method: get_sub_issues`). |
| 50 | +- A track of follow-ups from a recent merge → use the priority + area filter the user implied. |
| 51 | +- The set of issues just surveyed → reuse that list verbatim, don't refetch. |
| 52 | + |
| 53 | +For each node, capture: |
| 54 | + |
| 55 | +- Issue state (`open` / `closed`) and `state_reason` (`completed` vs other). |
| 56 | +- Status labels (`in-progress`, `planned`, `draft`) and `priority:*`. |
| 57 | +- Linked PRs — look for "merged in PR #N" in the body or comments. |
| 58 | +- Sub-issue list (only for umbrella nodes). |
| 59 | + |
| 60 | +### 2. Classify |
| 61 | + |
| 62 | +Each node gets exactly one status: |
| 63 | + |
| 64 | +| State | Definition | |
| 65 | +|-------|------------| |
| 66 | +| `done` | Closed + `state_reason: completed`, or merged PR. | |
| 67 | +| `in_progress` | Open + `in-progress` label, or an open PR exists for it. | |
| 68 | +| `open` | Open + `planned` / `draft`, no PR. | |
| 69 | + |
| 70 | +Don't render "blocked" as a separate status — the renderer derives it from the graph (any `open` node with a non-done predecessor) and styles it with a dashed muted fill. Conversely, an `open` node whose predecessors are all `done` is dual-encoded as the "available next" highlight. |
| 71 | + |
| 72 | +### 3. Edge-build |
| 73 | + |
| 74 | +Edges come from three sources, in this order of trust. **Don't draw an edge you can't cite from one of these** — speculation pollutes the DAG. |
| 75 | + |
| 76 | +1. **Sub-issue links** (`mcp__github__issue_read` + `method: get_sub_issues`). Edge direction is **child → parent** (prerequisite → dependent) — the parent closes when its children close, so the DAG flows toward closure. |
| 77 | +2. **Explicit prose** in issue body: `Depends on #N`, `Hard depends on #N`, `Blocks #N`, `Part of #N`, `Closes #N`. |
| 78 | +3. **PR/commit references**: `merged in PR #N`, `closed by #N`, `PR-A → PR-B` ordering inside an umbrella spec's Plan. |
| 79 | + |
| 80 | +If a dependency is "obvious to me but uncited", the node label can hint at it; the edge stays out. |
| 81 | + |
| 82 | +### 4. Render |
| 83 | + |
| 84 | +Emit a JSON IR matching the schema below, then invoke the renderer. **Do not hand-draw boxes** — the renderer produces deterministically correct layout that the AI's spatial reasoning will not match, especially with cross-edges and fan-out. |
| 85 | + |
| 86 | +**Schema:** |
| 87 | + |
| 88 | +```json |
| 89 | +{ |
| 90 | + "nodes": [ |
| 91 | + {"id": "288", "label": "MCP", "status": "done"}, |
| 92 | + {"id": "305", "label": "router", "status": "in_progress"}, |
| 93 | + {"id": "306", "label": "cleanup","status": "open"} |
| 94 | + ], |
| 95 | + "edges": [ |
| 96 | + {"from": "288", "to": "304", "source": "sub-issue"}, |
| 97 | + {"from": "305", "to": "306", "source": "depends-on"} |
| 98 | + ], |
| 99 | + "close": "300", |
| 100 | + "critical_path": ["301", "305", "306", "307", "close"] |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +- `status` ∈ `{done, in_progress, open}`; defaults to `open`. Status is rendered as a fill color plus a leading emoji — do not embed markers in `label`. |
| 105 | +- `edges[].source` ∈ `{sub-issue, depends-on, pr-link, closes, part-of}`, required. This is the citation rule from Conventions made enforceable: no edge without a documented source on GitHub. |
| 106 | +- Every `from` / `to` resolves to a declared node id, or the literal `"close"`. |
| 107 | +- `critical_path` is optional; communicate it in prose alongside the rendered PNG, not inside the image. |
| 108 | + |
| 109 | +**Visual encoding.** Status is dual-encoded by fill and a leading emoji so the eye picks up state before reading the label: |
| 110 | + |
| 111 | +| State | Fill | Border | Emoji | |
| 112 | +|-------|------|--------|-------| |
| 113 | +| Done | muted green | green | ✅ | |
| 114 | +| In-progress | amber | thicker amber | 🟡 | |
| 115 | +| Open + all preds done ("available next") | cool blue | thick blue | 🎯 | |
| 116 | +| Open + blocked | near-white | grey, dashed | ⬜ | |
| 117 | +| Close sentinel | white | double | 🏁 | |
| 118 | + |
| 119 | +The "available next" highlight is computed from the graph (open + every predecessor is `done`) — no IR field for it. Critical-path edges are *not* bolded: which path is "the" critical path is a caller judgement, and elevating it visually would conflate the recommendation with the graph's topology. Keep the critical path in `ir.critical_path` and let prose carry the next-pick recommendation under the rendered PNG. |
| 120 | + |
| 121 | +The `--emoji` flag controls whether status emoji are emitted: `on` (default) shows ✅ / 🟡 / 🎯 / ⬜ / 🏁 emoji; `off` falls back to trailing ✓ / … text markers. Turn `off` if the target system lacks a color emoji font and the PNG shows tofu boxes. |
| 122 | + |
| 123 | +**Invocation.** The renderer ships inside the skill. Use the path that matches how the skill was installed: |
| 124 | + |
| 125 | +- **Project-scope install** (default for `npx skills add onsager-ai/onsager-skills` from a repo root): `.claude/skills/plan-dag/scripts/plan-dag-render.py` |
| 126 | +- **User-global install** (`npx skills add -g …`): `~/.claude/skills/plan-dag/scripts/plan-dag-render.py` |
| 127 | + |
| 128 | +Pick whichever exists. If unsure, `test -x .claude/skills/plan-dag/scripts/plan-dag-render.py && echo project || echo global`. |
| 129 | + |
| 130 | +```bash |
| 131 | +SCRIPT=.claude/skills/plan-dag/scripts/plan-dag-render.py # project install |
| 132 | +# SCRIPT=~/.claude/skills/plan-dag/scripts/plan-dag-render.py # global install |
| 133 | + |
| 134 | +# default: high-DPI PNG with emoji status indicators |
| 135 | +"$SCRIPT" /tmp/plan.json --out /tmp/plan-dag.png |
| 136 | + |
| 137 | +# emoji off — falls back to ✓ / … text markers in node labels |
| 138 | +"$SCRIPT" /tmp/plan.json --out /tmp/plan-dag.png --emoji=off |
| 139 | +``` |
| 140 | + |
| 141 | +The renderer needs `dot` (graphviz; `apt install graphviz` / `brew install graphviz`) on PATH for the SVG layout step, and `node` (≥18) + Playwright Chromium (`npm i -g playwright && npx playwright install chromium`) for the rasterisation step. Both checks run upfront and fail loudly with install guidance — there is no silent fallback to text or ASCII, by design (the formats removed had limitations the PNG output exists to avoid). |
| 142 | + |
| 143 | +If the renderer aborts with `IR validation failed`, fix the IR — do not work around it by hand-drawing. The validation surface is the citation rule (`Conventions › No invented edges`) made executable. |
| 144 | + |
| 145 | +**Response handling after running the renderer:** |
| 146 | + |
| 147 | +- Send the PNG via `SendUserFile` so it renders inline as part of the assistant message. |
| 148 | +- Add prose commentary below the file — critical path, next pickable node, sequencing rationale. The PNG carries the topology; the prose carries the recommendation. |
| 149 | +- Do **not** re-render the same plan in another format and attach both — one DAG per response. |
| 150 | +- For very wide graphs (>10 nodes with cross-edges) where the single PNG becomes unwieldy, split the plan into per-track DAGs (one renderer call per track) and render the cross-edges as a final short prose list, per the existing "Cross-edges" convention in §3. |
| 151 | + |
| 152 | +## Conventions |
| 153 | + |
| 154 | +- **No invented edges.** If you can't cite the source (sub-issue link, body prose, PR header), don't draw it. |
| 155 | +- **Summarize done nodes when dense.** More than ~3 done nodes in a track? Collapse to `Landed: #A #B #C ✓` in prose rather than enumerating them in the graph. |
| 156 | +- **One DAG per response.** Don't render the same plan twice under different framings — pick the framing that answers the question that was actually asked. |
| 157 | +- **End with the picked path.** A DAG without a recommended sequence is a wall of boxes. Close with the critical path and the next pickable node in prose, framed so the user can redirect. |
| 158 | +- **Don't editorialize inside the diagram.** Commentary ("this looks risky", "we should reorder") goes in prose above or below, never inside a node label. |
| 159 | + |
| 160 | +## Tests |
| 161 | + |
| 162 | +Tests live next to the renderer at `scripts/plan-dag-render.test.sh` and exercise validation (bad IR, cycle detection, own-id-prefix boundary cases), the styled DOT structural checks (status fills, available-next, blocked-dashed, close double border), and the end-to-end PNG smoke. The PNG smoke is auto-skipped when Playwright Chromium is unavailable so the validator coverage still runs in restricted CI. Run with the same install-aware path the renderer uses: |
| 163 | + |
| 164 | +```bash |
| 165 | +# project-scope install |
| 166 | +.claude/skills/plan-dag/scripts/plan-dag-render.test.sh |
| 167 | + |
| 168 | +# user-global install |
| 169 | +~/.claude/skills/plan-dag/scripts/plan-dag-render.test.sh |
| 170 | +``` |
| 171 | + |
| 172 | +Both forms are in `allowed-tools` so Claude Code doesn't re-prompt for permission. The test script internally `cd`s into the skill root and invokes `scripts/plan-dag-render.py` as a child process — that child invocation runs inside the script's own shell, not through Claude Code's permission engine, so it doesn't need a separate allowlist entry. |
| 173 | + |
| 174 | +Requires `dot` (graphviz) on PATH; the PNG smoke additionally requires `node` + Playwright Chromium. |
| 175 | + |
| 176 | +## Related skills |
| 177 | + |
| 178 | +- The repo's spec-driven-development loop skill (e.g. `onsager-dev-process`, `duhem-dev-process`) — the parent / child / depends-on semantics the DAG visualizes. |
| 179 | +- The repo's `issue-spec` skill — how parent / child / depends-on edges are persisted on GitHub. |
| 180 | +- The repo's PR-lifecycle skill — how "in-progress" status flips on PR open / merge, which drives the amber fill on the rendered PNG. |
0 commit comments