feat: bead graph, static and interactive - #2
Merged
Merged
Conversation
Viewing the queue meant either `bd graph --html`, which loads D3 from d3js.org and so needs network access and cannot be archived, or hermes-beads, which reads a .beads/issues.jsonl this Dolt-backed store does not have. Neither is offline or reviewable. `relay graph [bead-id] --out path.html` computes the page from `bd export` instead of asking a model to draw it: layout coordinates come from the script, so identical input yields identical bytes and the output is unit-testable. The page carries no script tag and no external host, which makes it work offline, diff cleanly, and satisfy an Artifact CSP as written. Layering is longest-path over ordering edges only, so layer 0 is startable work; containment is drawn but never pushes a child rightward, because an epic does not block its own children. Kahn's algorithm rather than recursion, so a dependency cycle surfaces as leftover nodes instead of an unbounded stack - it is reported in the page and the beads pinned to layer 0, since bd permits a cycle and refusing to draw would make the store unviewable. relates-to is an annotation and draws nothing. The export is read on stdout and never written to disk. An exported .beads/issues.jsonl carries created_by account names plus every title, description, and comment, it is not covered by .beads/.gitignore, and scan-privacy.mjs skips .beads - two blind guards on a public repository. A gitignore line now covers a hand-run export as well. Titles are truncated before they are escaped, never after: the reverse order would let the character budget cut an entity in half and emit a bare `&am`. A test pins that ordering. Verified: 125 pass / 0 fail, lint and check green. Rendering this repository's own n95 epic produced a 8KB page with zero script tags and zero external hosts, and byte-identical output across runs. The page's visual appearance is unverified - the sandbox blocked both file:// navigation and a local server.
halaprix
force-pushed
the
feat/bead-graph
branch
from
July 27, 2026 19:30
a00ade6 to
c03c3ab
Compare
`relay graph` renders an artifact; `relay view` opens an editor on it. Together they cover both halves of looking at a queue - one you attach to a PR, one you fix a dependency in. The viewer is an optional external tool and never a dependency. Agent Relay ships zero runtime dependencies and @halaprix/beads-viewer ships a browser bundle, so `relay view` fetches it through npx on demand; AGENT_RELAY_VIEWER_BIN points at a local checkout the same way AGENT_RELAY_BD_BIN does for bd. The store is passed explicitly rather than discovered. An exported BEADS_DIR aimed at another project silently beats repository discovery, and having the viewer edit the wrong issue database is not a risk worth taking for a line of code. Failures map onto the existing exit classes rather than crashing: a missing store is reported before the viewer launches, a viewer that cannot be executed is project-misconfigured, and a viewer exiting non-zero is human-action-required. One bug found while testing: passing the viewer's exit code as `exitCode` in the payload shadowed the exit class's own code, so `relay view` would have exited 3 - outside the documented set that the whole contract rests on. It is `viewerExitCode` now, and a test pins both values. No other command had the same shadowing. Verified: 133 tests pass, lint and check clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Two halves of the same job:
relay graph [bead-id] [--out path.html]renders the dependency graph as one self-contained HTML file — no scripts, no CDN, no network. Computed by the script, so identical input yields identical bytes: reviewable, diffable, attachable to a PR.relay view [bead-id]opens@halaprix/beads-vieweron the same store, where the graph is editable.Why the split
A static page cannot collapse an epic or answer "what unblocks if I finish this"; an interactive app cannot be attached to a pull request or read in five years. Neither replaces the other.
Changes
src/lib/bead-graph.mjs— parsebd export, classify dependencies, layer by longest path.src/lib/bead-graph-html.mjs— inline SVG from computed coordinates. No script tag, no external host.src/lib/viewer.mjs— launch the viewer as an external tool through npx, overridable viaAGENT_RELAY_VIEWER_BIN.beads.mjsexportBeadRecords— readsbd export --readonlyon stdout, never to disk..gitignorecovers a hand-run.beads/issues.jsonl.Evidence
npm test— 133 pass / 0 fail (was 116; graph, layout and viewer suites added).npm run lint,npm run check— green.Design notes for review
Layering. Longest path over ordering edges only, so column 0 is startable work. Containment is drawn but never pushes a child rightward — an epic does not block its own children, and treating it as a blocker would misreport every epic child as unstartable.
Cycles. Kahn rather than recursion, so a cycle surfaces as leftover nodes instead of an unbounded stack. It is reported in the page with those beads pinned to layer 0 rather than throwing:
bdpermits cycles, so refusing to draw would blank the store exactly when you need to see it.Escaping order. Titles are truncated before escaping. The reverse lets the character budget cut an entity in half and emit a bare
&am; a test pins it.No export file on disk. An exported
.beads/issues.jsonlcarriescreated_byaccount names plus every title and comment, is not covered by.beads/.gitignore, andscan-privacy.mjsskips.beads— two blind guards on a public repo. Reading stdout removes the hazard; a test asserts the file is never created.relay viewpassesBEADS_DIRexplicitly. An exported value aimed at another project silently beats repository discovery, and editing the wrong issue database is not worth risking to save a line.One bug worth naming: passing the viewer's exit code as
exitCodeshadowed the exit class's own code, sorelay viewwould have exited 3 — outside the documented set the contract rests on. NowviewerExitCode, with a test on both. No other command had the same shadowing.Not verified
relay graph's rendered appearance. The sandbox blocks browser access, so its structure, determinism and self-containment are verified but nobody has looked at the page.