Skip to content

Commit a52bd23

Browse files
committed
feat(admin): loops grouped inside their skill, and graph scope labels (issue #54)
Operator feedback on the shipped graph: not clear which repos or folders it covers, and the loop a skill's own text describes was nowhere to be seen. The second half was a real gap against the original ask: the design carried loop markers grouped inside the skill, but the data layer never produced them. Loops. findLoopHints (graph-model, pure) scans a SKILL.md BODY for iteration vocabulary (until, repeat, iterate, for each, while, keep ...ing), frontmatter excluded so a description like 'repeat daily' never reads as a loop, deduped and capped at three. The hints ride skill nodes as facts, and every surface groups them INSIDE the skill, because a loop lives inside its one job, one container, one budget slot: the HTML renders a loop-carrying skill as a Node-RED group box (tinted rect labelled with the skill, the chip, one rearm marker per hint with the phrase, the ring wire contained in the box; sub-skills nest as small chips), the TUI shows rearm-glyph badges, the text view [loop: ...] badges. A hint is text evidence, never a promise: the mention discipline, applied to a node's insides. Scope. Forge groups now name the repositories their window's records actually ran against (forgeRepoTargets: the repo half of the id-only target string every runs view already shows, capped at five, labelled record-derived, because a github trigger's config names no repository at all). Local folders keep their full path in the TUI and text views; the HTML artifact stays basename-only by default and gains --full-paths, an explicit operator opt-in for the reviewed run.folder paths, because the artifact is a durable, shareable file. Specs: REQ-TOPOLOGY-GRAPH gains (e2) (loops as node facts grouped in the skill; record-derived forge scope); REQ-GRAPH-HTML-EXPORT gains the opt-in with its acceptance twin; SECURITY.md names the opt-in. GRAPH_LIMITS gains maxReposListed, literal-pinned. docs/graph.md gains the loops and scope sections; both READMEs' screenshot is regenerated from a fixture whose skill actually loops, image version bumped. Suite in the CI posture: 2175 pass, 0 skipped; admin bundle builds; the artifact re-verified in headless Chrome, default and --full-paths both. Signed-off-by: Rob Boerman <robboerman@live.nl>
1 parent 2916a0c commit a52bd23

16 files changed

Lines changed: 512 additions & 51 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ dashboard view (`g`), as plain text, and as `/dispatch graph html`, a self conta
387387
opens from disk (still no server and no port) with the topology drawn Node-RED style
388388
([`docs/graph.md`](docs/graph.md)).
389389

390-
![The trigger and flow graph as /dispatch graph html draws it: cron and forge triggers wired to their flows, an observed chain edge carrying its run count, a potential mention, cron re-arm loops with their schedules, an orphan skill dimmed, per-trigger run counts with outcomes, and the legend stating the chain caps](docs/images/graph-view.png?v=0.9.0)
390+
![The trigger and flow graph as /dispatch graph html draws it: cron and forge triggers wired to their flows, an observed chain edge carrying its run count, a potential mention, a skill with its prose loop grouped inside it, cron re-arm loops with their schedules, an orphan skill dimmed, the forge group naming the repos its runs hit, and the legend stating the chain caps](docs/images/graph-view.png?v=0.9.1)
391391

392392
```bash
393393
pi install npm:@edgehero/pi-dispatch-admin # then, in pi: /dispatch

SECURITY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -487,7 +487,8 @@ Stated openly rather than discovered later:
487487
- **The graph export writes one static HTML file, on your keystroke, to a temp path it names.** What
488488
crosses into it: trigger configuration you authored, skill names and frontmatter from the repos you
489489
service, and the PII-free run-record fields the panel already shows. What never does: raw job log
490-
bytes, issue or task text, session material, or a host path beyond a folder's basename. Nothing
490+
bytes, issue or task text, session material, or a host path beyond a folder's basename unless you
491+
pass `--full-paths` yourself (your own reviewed `run.folder` config, on your keystroke). Nothing
491492
listens and nothing serves the file — opening it is a local browser reading local bytes, and the
492493
page makes no network request of any kind. The browser spawn is best-effort, skipped and announced
493494
over SSH or without a display; the printed `file://` URL is the contract.

admin/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ One command puts a live TUI over the whole deployment:
6868
- **The topology, as a graph.** The GRAPH view (`g`) and `/dispatch graph` show what triggers what, what chained to what in the recorded runs, and what a skill's own text says it might chain to, with orphan skills and dangling triggers flagged. `/dispatch graph html` draws the same picture Node-RED style in your browser, from one self contained file on disk: no server, no port, and the page reloads itself when you regenerate it.
6969

7070
<p align="center">
71-
<img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/graph-view.png?v=0.9.0" alt="The trigger and flow graph: triggers wired to their flows, an observed chain edge with its count, a potential mention, cron re-arm loops, an orphan skill dimmed, and the legend stating the chain caps" width="820">
71+
<img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/graph-view.png?v=0.9.1" alt="The trigger and flow graph: triggers wired to their flows, an observed chain edge with its count, a potential mention, a skill with its prose loop grouped inside it, cron re-arm loops, an orphan skill dimmed, the forge group naming the repos its runs hit, and the legend stating the chain caps" width="820">
7272
</p>
7373

7474
- **Triggers, editable live.** cron, label, comment and pull_request triggers with colored drill-ins showing what fires each one, what it runs, and its trust model. Added, edited and deleted without a restart. Triggers that run third-party code or a custom image are badged; opting in or out of either stays an edit to the reviewed `triggers.json`, which neither the console nor a model-callable tool will make for you.

admin/src/dashboard.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { windowEndAt } from "@edgehero/pi-dispatch/pause-windows";
3030
// injected seams (`fetchCosts`/`listPricedModels`/`whatIf`), never the façade, so tests stay fully
3131
// canned and the one worker/pricing coupling sits beside the queue and redis this module already owns.
3232
import * as pricing from "@edgehero/pi-dispatch/pricing";
33-
import { listRuns, readSettingsView, mapSchedulers, readTriggers, readPauseWindows, readStagedPackages, readSubscriptions, scanRunRecords, GRAPH_LIMITS, cronRunStats, joinRunsToTriggers, observedChainEdges, collectGraphInputs } from "./read-model.mjs";
33+
import { listRuns, readSettingsView, mapSchedulers, readTriggers, readPauseWindows, readStagedPackages, readSubscriptions, scanRunRecords, GRAPH_LIMITS, cronRunStats, joinRunsToTriggers, observedChainEdges, collectGraphInputs, forgeRepoTargets } from "./read-model.mjs";
3434
import { renderStatus, renderBudget, renderTriggers, renderSettingsView, renderGraph } from "./render.mjs";
3535
import { buildGraphModel } from "./graph-model.mjs";
3636
import { matchesKey } from "./keys.mjs";
@@ -159,6 +159,7 @@ export function createDashboardDeps(paths: any) {
159159
cronStats: cronRunStats({ records: recs, schedulerIds: triggerList.filter((t) => t.type === "cron" && typeof t.id === "string").map((t) => t.id) }),
160160
runJoin: joinRunsToTriggers({ records: recs, triggerCount: triggersView?.count, triggerTypes: Object.fromEntries(triggerList.map((t) => [t.index, t.type])) }),
161161
chainEdges: observedChainEdges({ records: recs }),
162+
forgeRepos: forgeRepoTargets({ records: recs }),
162163
caps: { chainDepthMax: paths.chainDepthMax, chainMaxPerJob: paths.chainMaxPerJob, windowDays: GRAPH_LIMITS.windowDays },
163164
nowMs,
164165
});
@@ -1636,8 +1637,11 @@ function graphRowLine(row: any, cursor: boolean, inner: number, styler: any): st
16361637
const g = row.group;
16371638
const fold = styler.fg("accent", row.folded ? G.foldClosed : G.foldOpen);
16381639
const label = g.kind === "forge" ? `forge ${g.label}` : `folder ${g.path ?? g.label}`;
1640+
// Record-derived scope for a forge group: which repos its runs actually hit in the window,
1641+
// because a forge trigger's config names none and "github" alone answers nothing.
1642+
const seen = g.kind === "forge" && Array.isArray(g.repos) && g.repos.length > 0 ? styler.fg("dim", ` · ran against ${g.repos.join(", ")}`) : "";
16391643
const state = g.unreachable ? styler.fg("warning", ` · ${g.kind === "forge" ? "skills unverifiable from this host" : g.unreachable}`) : g.head ? styler.fg("dim", ` · HEAD ${String(g.head).slice(0, 7)}`) : "";
1640-
return fitLine(pre + fold + " " + styler.fg("accent", label) + state, inner, styler);
1644+
return fitLine(pre + fold + " " + styler.fg("accent", label) + seen + state, inner, styler);
16411645
}
16421646
if (row.kind === "gtrigger") {
16431647
const t = row.node;
@@ -1657,6 +1661,7 @@ function graphRowLine(row: any, cursor: boolean, inner: number, styler: any): st
16571661
if (missing) marks.push(styler.fg("error", "[missing at HEAD]"));
16581662
if (s.isSub) marks.push(styler.fg("dim", `[sub of ${s.group}]`));
16591663
if (s.aiTrigger) marks.push(styler.fg("success", "[chainable]"));
1664+
for (const loop of Array.isArray(s.loops) ? s.loops : []) marks.push(styler.fg("accent", `${G.rearm} "${loop.hint}"`));
16601665
const badges = graphBadges(row.flags, styler);
16611666
return fitLine(pre + styler.fg("dim", "skill ") + name + (marks.length ? " " + marks.join(" ") : "") + badges, inner, styler);
16621667
}

0 commit comments

Comments
 (0)