Render the current plan as a dependency DAG so sequencing, the critical path, and parallelizable work are visible at a glance. Default output is monospace text using Unicode box-drawing glyphs (─, ►, ┐ ├ ┤ └) so it lands cleanly in chat, terminals, and PR descriptions.
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.
When to use
- After a
what's nextsurvey, to commit to a sequence. - When asked "what's left for #N" on an umbrella spec with sub-issues.
- Before picking the next branch — to see what unblocks the most downstream work.
- When a spec fans out into N sub-issues and the dependency edges aren't all linear.
Skip when:
- A single-PR spec — the plan is the Plan section, not a DAG.
- An unanswered design question — drawing a DAG before alignment is theater.
- The "graph" is one linear chain of three or fewer nodes — a sentence is shorter than a diagram.
Inputs
| Parameter | Default |
|---|---|
| Scope | Inferred — current branch's spec, the umbrella the user named, or the open issues just surveyed. |
| Granularity | Spec-level; drop to sub-issue / PR level for an umbrella that has fanned out. |
| Output format | Monospace text (Unicode box-drawing) laid out top-to-bottom via graphviz (default). --as=html --out <path> for a self-contained HTML page with the SVG and critical-path footer (preferred on web surfaces that render HTML inline — Claude Code on the web, claude.ai); --as=svg for raw inline SVG; --as=png --out <path> for a high-DPI rasterised image; --as=ascii for a pure-ASCII tree; --as=dot for raw DOT. |
Workflow
1. Discover Pull issues / sub-issues / PRs in scope
2. Classify Mark each node done / in-progress / open
3. Edge-build Read dependencies from sub-issue links + body prose
4. Render Group by track; cross-edges last; critical path callout
1. Discover
Resolve the scope from the conversation, not from a generic crawl. Typical triggers:
- An umbrella spec → walk its sub-issue list (
mcp__github__issue_readwithmethod: get_sub_issues). - A track of follow-ups from a recent merge → use the priority + area filter the user implied.
- The set of issues just surveyed → reuse that list verbatim, don't refetch.
For each node, capture:
- Issue state (
open/closed) andstate_reason(completedvs other). - Status labels (
in-progress,planned,draft) andpriority:*. - Linked PRs — look for "merged in PR #N" in the body or comments.
- Sub-issue list (only for umbrella nodes).
2. Classify
Each node gets exactly one marker:
| State | Marker | Definition |
|---|---|---|
| Done | ✓ |
Closed + state_reason: completed, or merged PR. |
| In-progress | … |
Open + in-progress label, or an open PR exists for it. |
| Open | (none) | Open + planned / draft, no PR. |
Don't render "blocked" as a separate marker — the inbound edge to a non-done node already shows it. The marker is for the reader's eye, not the graph topology.
3. Edge-build
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.
- 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. - Explicit prose in issue body:
Depends on #N,Hard depends on #N,Blocks #N,Part of #N,Closes #N. - PR/commit references:
merged in PR #N,closed by #N,PR-A → PR-Bordering inside an umbrella spec's Plan.
If a dependency is "obvious to me but uncited", the node label can hint at it; the edge stays out.
4. Render
Emit a JSON IR matching the schema below, then invoke the renderer. Do not hand-draw ASCII boxes — the renderer produces deterministically correct layout that the AI's spatial reasoning will not match, especially with cross-edges and fan-out.
Schema:
{
"nodes": [
{"id": "288", "label": "MCP", "status": "done"},
{"id": "305", "label": "router", "status": "in_progress"},
{"id": "306", "label": "cleanup","status": "open"}
],
"edges": [
{"from": "288", "to": "304", "source": "sub-issue"},
{"from": "305", "to": "306", "source": "depends-on"}
],
"close": "300",
"critical_path": ["301", "305", "306", "307", "close"]
}
status∈{done, in_progress, open}; defaults toopen. Status markers (✓,…in box-drawing mode;[done]/[wip]/[open]in ASCII mode) are added by the renderer — do not embed them inlabel.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.- Every
from/toresolves to a declared node id, or the literal"close". critical_pathis optional; renderer appends it as a callout footer under the box-drawing, ASCII, and HTML targets (and asCritical path:text in the ASCII output).
Invocation. Default emits top-to-bottom box-drawing via graphviz (requires dot on PATH — apt install graphviz, or brew install graphviz) and is the right choice for terminal-only surfaces. --as=html --out <path> writes a self-contained HTML page that wraps the styled SVG with a critical-path footer — preferred on web surfaces that render HTML inline (Claude Code on the web, claude.ai web/mobile). --as=svg emits the same styled SVG without the HTML chrome — useful for embedding in markdown / GitHub / external docs. Both need only dot. --as=png --out <path> rasterises the same DOT through dot -Tsvg and a headless Chromium screenshot at deviceScaleFactor=2 — sharper than dot -Tpng, but heavier (needs dot, node, and Playwright Chromium); use it on surfaces that show inline PNGs but not inline HTML. --as=ascii produces a pure-ASCII indented tree with no external dependency — used explicitly for restricted terminals, and selected automatically (with a stderr note) when dot is missing. --as=dot emits raw DOT source for piping or debugging.
Visual encoding (DOT / SVG / HTML / PNG targets only). Status is dual-encoded by fill and a leading emoji so the eye picks up state before reading the label:
| State | Fill | Border | Emoji |
|---|---|---|---|
| Done | muted green | green | ✅ |
| In-progress | amber | thicker amber | 🟡 |
| Open + all preds done ("available next") | cool blue | thick blue | 🎯 |
| Open + blocked | near-white | grey, dashed | ⬜ |
| Close sentinel | white | double | 🏁 |
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 the renderer print it as a footer / let prose carry the next-pick recommendation.
The text box-drawing and ASCII targets stay glyph-only (✓ / … markers) because their layout math counts characters, not visual columns, and emoji are East Asian Wide. The --emoji flag controls whether emoji are emitted in DOT / SVG / HTML / PNG labels: auto (default) is on for those four styled targets, off for text targets; on / off force it. Turn off if a target system lacks a color emoji font and you see tofu boxes in the rendered SVG / HTML / PNG.
The renderer ships inside the skill. Use the path that matches how the skill was installed:
- Project-scope install (default for
npx skills add onsager-ai/onsager-skillsfrom a repo root):.claude/skills/plan-dag/scripts/plan-dag-render.py - User-global install (
npx skills add -g …):~/.claude/skills/plan-dag/scripts/plan-dag-render.py
Pick whichever exists. If unsure, test -x .claude/skills/plan-dag/scripts/plan-dag-render.py && echo project || echo global.
SCRIPT=.claude/skills/plan-dag/scripts/plan-dag-render.py # project install
# SCRIPT=~/.claude/skills/plan-dag/scripts/plan-dag-render.py # global install
# default: top-to-bottom box-drawing via graphviz
"$SCRIPT" /tmp/plan.json
# self-contained HTML page (preferred on Claude Code on the web / claude.ai; then SendUserFile)
"$SCRIPT" /tmp/plan.json --as=html --out /tmp/plan-dag.html
# raw inline SVG (embed in markdown / GitHub / docs; stdout by default)
"$SCRIPT" /tmp/plan.json --as=svg --out /tmp/plan-dag.svg
# high-DPI PNG (fallback for surfaces that render PNGs inline but not HTML)
"$SCRIPT" /tmp/plan.json --as=png --out /tmp/plan-dag.png
# pure ASCII tree (no external deps; auto-selected when `dot` is missing)
"$SCRIPT" /tmp/plan.json --as=ascii
# raw DOT for piping / debugging (styled by default; --emoji=off for portability)
"$SCRIPT" /tmp/plan.json --as=dot
"$SCRIPT" /tmp/plan.json --as=dot --emoji=off
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.
Response handling after running the renderer:
- Wrap the renderer's stdout in a fenced block tagged
```text— neverbashor unlabeled. Syntax highlighting recolors box-drawing characters (─,│,┌,►) and breaks the visual. - Do not retype or paraphrase the renderer output. Copy the tool result verbatim. A single shifted character destroys column alignment, and the AI's spatial reasoning is the failure mode the script was introduced to eliminate.
- Surface rules:
- Claude Code (terminal runtime): the stdout is already visible in the terminal pane. Do not duplicate it in the reply. Add commentary only — critical path summary, next pickable node, sequencing rationale.
- Claude Code on the web, claude.ai web / mobile (HTML-capable, no real terminal pane): prefer the HTML path. Render with
--as=html --out /tmp/plan-dag.htmland send the file viaSendUserFileso the user sees a scalable diagram with the legend and critical-path footer baked in. PNG (--as=png --out /tmp/plan-dag.png) is a fine fallback on surfaces that render inline images but not HTML. Add prose commentary below the file — critical path, next pickable node — but don't echo the text-art version too. - Markdown / GitHub PR descriptions: prefer the default text box-drawing in a
```textblock — GitHub renders it faithfully and it stays diff-able. For richer rendering in external docs or notion-style surfaces,--as=svgproduces a few-KB embeddable SVG. - No image surface available (raw terminals, restricted runtimes): the default stdout box-drawing remains the right output.
- For very wide graphs (>10 nodes with cross-edges) where the default box-drawing is 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. The SVG / HTML / PNG targets scale further before they become unwieldy than the text target does.
Conventions
- No invented edges. If you can't cite the source (sub-issue link, body prose, PR header), don't draw it.
- Summarize done nodes when dense. More than ~3 done nodes in a track? Collapse to
Landed: #A #B #C ✓rather than enumerating. - 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.
- 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, framed so the user can redirect.
- Don't editorialize inside the diagram. Commentary ("this looks risky", "we should reorder") goes in prose above or below, never inside a node label.
Tests
Golden tests live next to the renderer at scripts/plan-dag-render.test.sh and exercise the validator, both render targets, and the auto-fallback path against fixtures in fixtures/. Run with the same install-aware path the renderer uses:
# project-scope install
.claude/skills/plan-dag/scripts/plan-dag-render.test.sh
# user-global install
~/.claude/skills/plan-dag/scripts/plan-dag-render.test.sh
Both forms are in allowed-tools so Claude Code doesn't re-prompt for permission. The test script internally cds 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.
Requires dot (graphviz) on PATH.
Related skills
- 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. - The repo's
issue-specskill — how parent / child / depends-on edges are persisted on GitHub. - The repo's PR-lifecycle skill — how "in-progress" status flips on PR open / merge, which drives the
…marker.