What
Triggers, flows (skills), and chained flows form a graph, and every edge already exists somewhere in data — but no screen shows the topology. Wanted: a graph view on the admin panel showing how triggers and flows interact, in a spot that doesn't complicate the existing UI: one drill-in, one entry key, the LIST stays as it is.
What already works (don't redo this)
- trigger → flow is direct config: every
triggers.json entry names its run.flow; the admin already renders cron/label/comment/pull_request … → flow per entry (render.mjs triggerLine, the LIST triggers section).
- cron trigger ↔ its runs is an exact join. Scheduler ids ride the
repeat:<id>:<millis> jobId into the run-history filenames; the worker does precisely this scan for previousRunAt (run-history.mjs makeFindPreviousRun). The admin just never does it.
- Chain edges are in the records.
parentJobId/chainDepth/chainRefused; RUN_DETAIL already renders "child of · spawned k → …" and the LIST prefixes └ .
- The TUI can draw it. Box glyphs with an ASCII fallback (
panel.mjs GLYPHS), themed styler, and a documented four-point recipe for adding a view to the dashboard's view machine.
Gap 1 — the topology is invisible
Nodes (triggers, flows, cron folders) and edges (trigger→flow, flow→chained flow, trigger→runs) exist across three files and a directory of sidecars, but the operator assembles the picture by hand. A GRAPH drill-in should render it: triggers on one side, flows on the other, edges annotated with run counts and last outcome from the history joins.
Gap 2 — github runs can't be attributed to their trigger
Issue #49's matched: { index, type, … } names exactly which triggers.json entry fired — but it lands only in /job/event.json; buildRecord never persists it, so the graph can count runs per cron trigger exactly while label/comment/PR triggers get at best a flow-name heuristic. Persist matched.index + matched.type on the run record (additive, nullable, no payload text — an integer and an enum, well inside the record's PII posture). Two triggers pointing at the same flow become distinguishable in history, which was half the point of #49.
Gap 3 — flows exist only as strings
The admin has no skill enumeration: SKILL_PATH_RE/git ls-tree live worker-side in materialize.mjs, and the admin can only gate-check one named flow in one folder (readFlowGate). So the graph can't show orphan skills (exist in a folder's .pi/skills/, no trigger points at them) or dangling triggers (flow gate says no-skill). A read-model skill enumeration for cron folders — same object-store read the worker already trusts — closes both.
Gap 4 — chain topology is history, not config
A flow chains by writing /outbox requests at runtime; nothing declares "flow A spawns flow B". So flow→flow edges can only be observed from records over the retention window — the graph must label them as observed, with the depth/fanout caps (PI_CHAIN_DEPTH_MAX, PI_CHAIN_MAX_PER_JOB) stated. Bounded further by OQ-009 (github parents don't chain; same-folder only).
Where it lives
A GRAPH view in the dashboard view machine (g is free), entered from LIST, rendered with the existing glyph/styler primitives — no graph library (panel.mjs's "thin pure module, no TUI framework" precedent holds). A /dispatch graph subcommand renders the same thing as plain text for the degraded path. LIST changes by exactly one key hint.
Explicitly NOT proposed
- A web/HTML surface. The admin binds no port (
DES-ADMIN-VIA-PI-EXTENSION); that property is load-bearing and this feature doesn't get to spend it.
- Graph/diagram libraries, or mermaid in-product — mermaid stays a README concern.
- Declared chain topology in config. Chains are agent-requested at runtime by design; the graph reports what happened, it doesn't promise what will.
Acceptance
The graph shows every trigger → flow edge from the live triggers file; cron trigger nodes carry exact run counts/last outcome; github-triggered runs join to their trigger via the persisted matched fields; observed chain edges render with their caps stated; orphan skills and no-skill triggers are visibly flagged; works in the narrow/ASCII degraded path; no port, no new deps, all fs/redis access stays in read-model.mjs.
What
Triggers, flows (skills), and chained flows form a graph, and every edge already exists somewhere in data — but no screen shows the topology. Wanted: a graph view on the admin panel showing how triggers and flows interact, in a spot that doesn't complicate the existing UI: one drill-in, one entry key, the LIST stays as it is.
What already works (don't redo this)
triggers.jsonentry names itsrun.flow; the admin already renderscron/label/comment/pull_request … → flowper entry (render.mjstriggerLine, the LIST triggers section).repeat:<id>:<millis>jobId into the run-history filenames; the worker does precisely this scan forpreviousRunAt(run-history.mjsmakeFindPreviousRun). The admin just never does it.parentJobId/chainDepth/chainRefused; RUN_DETAIL already renders "child of · spawned k → …" and the LIST prefixes└.panel.mjsGLYPHS), themed styler, and a documented four-point recipe for adding a view to the dashboard's view machine.Gap 1 — the topology is invisible
Nodes (triggers, flows, cron folders) and edges (trigger→flow, flow→chained flow, trigger→runs) exist across three files and a directory of sidecars, but the operator assembles the picture by hand. A GRAPH drill-in should render it: triggers on one side, flows on the other, edges annotated with run counts and last outcome from the history joins.
Gap 2 — github runs can't be attributed to their trigger
Issue #49's
matched: { index, type, … }names exactly whichtriggers.jsonentry fired — but it lands only in/job/event.json;buildRecordnever persists it, so the graph can count runs per cron trigger exactly while label/comment/PR triggers get at best a flow-name heuristic. Persistmatched.index+matched.typeon the run record (additive, nullable, no payload text — an integer and an enum, well inside the record's PII posture). Two triggers pointing at the same flow become distinguishable in history, which was half the point of #49.Gap 3 — flows exist only as strings
The admin has no skill enumeration:
SKILL_PATH_RE/git ls-treelive worker-side inmaterialize.mjs, and the admin can only gate-check one named flow in one folder (readFlowGate). So the graph can't show orphan skills (exist in a folder's.pi/skills/, no trigger points at them) or dangling triggers (flow gate saysno-skill). Aread-modelskill enumeration for cron folders — same object-store read the worker already trusts — closes both.Gap 4 — chain topology is history, not config
A flow chains by writing
/outboxrequests at runtime; nothing declares "flow A spawns flow B". So flow→flow edges can only be observed from records over the retention window — the graph must label them as observed, with the depth/fanout caps (PI_CHAIN_DEPTH_MAX,PI_CHAIN_MAX_PER_JOB) stated. Bounded further by OQ-009 (github parents don't chain; same-folder only).Where it lives
A GRAPH view in the dashboard view machine (
gis free), entered from LIST, rendered with the existing glyph/styler primitives — no graph library (panel.mjs's "thin pure module, no TUI framework" precedent holds). A/dispatch graphsubcommand renders the same thing as plain text for the degraded path. LIST changes by exactly one key hint.Explicitly NOT proposed
DES-ADMIN-VIA-PI-EXTENSION); that property is load-bearing and this feature doesn't get to spend it.Acceptance
The graph shows every trigger → flow edge from the live triggers file; cron trigger nodes carry exact run counts/last outcome; github-triggered runs join to their trigger via the persisted
matchedfields; observed chain edges render with their caps stated; orphan skills andno-skilltriggers are visibly flagged; works in the narrow/ASCII degraded path; no port, no new deps, all fs/redis access stays inread-model.mjs.