Draws the blocking relationships between your GitHub Issues as a one-page dev map: which issues can be picked up now, which are waiting on what, and where the critical path runs.
bunx issue-map@latestRun it inside the repo you want to look at. A browser tab opens; every refresh re-fetches from GitHub. GitHub Issues stays the source of truth — this page is a snapshot and cannot change status.
An agent takes one issue per round, so what you decide each round is which one. That answer is in no single issue; it lives between them — who blocks whom, how many sub-issues a parent is still waiting on, how long the longest chain is. GitHub shows you one issue at a time. This page shows the picture.
It is built around mattpocock/skills — write the workflow as a skill, let the agent run it — which is where the default labels and commands come from.
| Command | What you get |
|---|---|
bunx issue-map@latest |
Server on a free port, URL printed |
bunx issue-map@latest --open |
Same, plus a browser tab opens |
bunx -p issue-map@latest issue-map-build |
Static file at dist/issue-map.html |
bunx -p issue-map@latest issue-map-build out.html |
Static file at a path you name |
- The repo comes from
ghreading the git remote in your cwd — nothing to fill in. ISSUE_MAP_PORTpins the port. Pinned is strict: it fails rather than moving if taken.--openopens a browser tab; without it the server only prints the URL.- A static file goes stale. Use the server when you need the current state.
- Where scripts cannot run (a strict CSP, some preview panes) the file falls back to a plain listing of the issues rather than a blank page.
@latesttakes the newest version on npm; pin one withbunx issue-map@0.2.0.
- Bun — these scripts use
Bun.build,Bun.serveandBun.file; Node will not run them. ghCLI, logged in, with read access to the target repo.- A git remote pointing at GitHub.
No runtime dependencies.
Switch in the top right: English (default), 繁體中文, 简体中文, 日本語. The choice is remembered in the browser and is not tied to a repo — language is the reader's preference, not a project setting.
CLI output (build messages, errors) is English only.
Everything has a default — it runs with nothing set. The defaults live in CONFIG in
src/issue-map.ts.
| Environment variable | Default | Meaning |
|---|---|---|
GH_REPO |
inferred from the git in cwd | Map another repo (gh's own variable; forks and multiple remotes are its job) |
ISSUE_MAP_PARENT_HEADING |
unset (bodies are not fetched) | Body heading under which a sub-issue points at its parent. Set it to opt in |
ISSUE_MAP_LABELS_UNREADY |
needs-triage,needs-info |
Not assessed yet, cannot be handed to anyone |
ISSUE_MAP_LABELS_READY |
ready-for-agent,ready-for-human |
Assessed and ready to work on |
ISSUE_MAP_LABELS_ACTIVE |
in-progress |
Somebody is on it, with or without an assignee |
ISSUE_MAP_LABELS_HUMAN |
ready-for-human |
Needs a person, so the next step is not an implementation command |
ISSUE_MAP_CMD_IMPLEMENT |
/implement |
Command the map suggests when an issue is ready |
ISSUE_MAP_CMD_TRIAGE |
/triage |
Command the map suggests when it still needs assessing |
ISSUE_MAP_PORT |
an OS-assigned free port | Port for the server |
Three worth knowing:
- Label vocabulary. The ready/unready defaults are four of the five canonical
triage labels
from mattpocock/skills. If the target repo uses different names, set them. When none of these
labels appear at all, triage stops being a gate — otherwise every issue would come out as "needs
triage". (
in-progressis this tool's own; the skills have no "somebody is on it" label.) - Command names.
/implementand/triageare theimplementandtriageskills. Point them at something that exists in the target repo, or the map tells people to run what isn't there. - The body convention is opt-in. Issue bodies are over 90% of what the GitHub response
weighs, and they feed one regex — nothing else on the page uses them. So they are only fetched
when
ISSUE_MAP_PARENT_HEADINGis set. Native sub-issues need no setting and always win. - Closed siblings need native sub-issues. The map asks GitHub only for the closed issues an
open one still points at, and children come from the native sub-issue relation. With the
## Parentbody convention a group's closed children never appear, so its progress looks smaller than it is. Linking them once in the issue's Sub-issues panel brings them back; the body convention can stay, the native relation wins anyway.
gh api graphql failed: … — gh is not logged in, or your cwd is not inside the target repo's git
tree.
| File | Responsibility |
|---|---|
src/issue-map.ts |
Snapshot, status and next step, produces the HTML. Knobs in its CONFIG |
src/issue-map-model.ts |
Pure data model: grouping, critical path, layout. Shared by both sides |
src/issue-map-i18n.ts |
Strings for the four languages, plus the lookup |
src/issue-map-page.ts |
Browser-side code, bundled into the HTML at build time |
src/issue-map.html |
The template. Two placeholder blocks get filled in |
src/issue-map-serve.ts |
Local server, re-fetches on every request |
tools/mutate.ts |
Mutation testing: break one line, see whether a test goes red |
bun install
bun run issue-map:serve # --watch; no browser tab (you would get one per save)
bun run issue-map # build the file only, to dist/issue-map.html
bun run check # lint + format:check + typecheck
bun test # the pure model layerThis repo has no issues of its own yet, so point GH_REPO=<owner>/<repo> at one that has tickets to
get anything drawn.
Tests. tests/ only guards what would make the map lie or make it unreadable; looks (colour,
shape, spacing) are deliberately not asserted. A new guard test has to be verified in reverse — put
the defect it claims to catch back into the product code and confirm it goes red:
bun run mutate src/issue-map-model.ts tests/issue-map-layout.test.tsAdding to src/issue-map-i18n.ts. EN is the original and defines the keys; the three
translations are typed from it, so a missing key or a missing {n} placeholder turns
bun run typecheck red. Keys needing English plurals are written { one, other }; Chinese and
Japanese take a single string. The model never builds sentences — nextStep is a structured value
like { kind: 'waitChildren', count: 2 }, assembled into words here.
- The page cannot change status. No button writes back to GitHub. Status has exactly one source of truth, and a second entry point makes them disagree.
- No separate short name. Stations are labelled with the first few characters of the title. A hand-maintained short name in the issue body would be a second source of truth — editing the title would not update it. The list below carries the full titles.