Planning UI — analysis notes (2026-08-21)
Status: the cross-plan ready board is built (same day — "right off the bat"): / lists every ready node across every plan, coloured by its server-assigned plan colour, sorted by leverage, with the not-done junctions beneath; the DAG page draws borrowed nodes as ghost cards. The rest of this note is still analysis, to be revisited after a week of real use.
Premise (Jeremy, 2026-08-21): the primary interactions are through MCP; the Stoat UI is a live visualization of the plan, updated over SignalR. Agents (and people driving agents) create plans, add and bind nodes, claim and report; the dashboard shows what is happening, as it happens. No human editing surface is planned until real use asks for one.
What that means for the UI
It is a viewer, and most of it already exists:
PlanViewdraws the DAG from the derived status (PlanStatusViewcarries each node'sdependsOn, so a structural change — a node added byadd_plan_node, an edge fromlink_plan_nodes— re-lays out on the next push with no extra wire);plan_status_updatedarrives within ~a second of any coordination event; the 60s poll is only a backstop.- Claims (
claimedBy, the report_node note) already render on the cards.
What a live, MCP-driven board still wants, roughly in order of value:
- A live activity feed — "agent-a added node
wolverine-4012", "agent-b boundcw-1064to JasperFx/CritterWatch#1064", "agent-a claimedcw-999: reproduced; fix is in the parser". The plan's history already exists (GET /api/plans/{slug}/history); making it LIVE is one moreStoatEvent(plan_changed, thePlanHistoryEntryshape) enqueued by the sameCoordinationChangeHandlerthat pulses the signal — and the claim events the same way. Per plan onPlanView; across plans on the home page. - Structural changes made visible — a node that just appeared or just got bound should read as new for a moment (a highlight that decays), or the board is a still that happens to be current.
"What is ready, across everything" as the home page— built:ReadyBoardView, derived client-side from the pushed statuses (lib/readyBoard.ts), leverage-sorted, with a "blocked on" section.- Agent presence — which agents hold claims right now, with lease expiry; the session-file poller (#10) will make this much richer, but claims alone already answer "who is working".
- History as a page, not just a feed — filter by node, by agent.
What it explicitly does not need (for now)
- Forms for create / add / change / link / bind / archive — those are MCP tools. If a human needs to do one by hand,
curlagainst the REST endpoints is the stopgap, and the decision to add a drawer/dialog waits for evidence that people want it. - Any frontend validation vocabulary — the handler's refusals are the parser's messages.
Open questions real use should answer (next week)
- Is the per-plan DAG the page people watch, or the cross-plan "ready + who" page?
- How much activity is too much — does a GitHub sweep's burst of observations belong in the feed, or only plan/claim changes (the asserted side)? The feed's filter decides the
StoatEventset. - Do people want to SEE unrealized nodes being materialized (bind) as a first-class moment?
- Is any write affordance missed badly enough to build one — the likeliest candidate is "bind this node to the issue I just opened", and even that may stay an agent's job.
If it starts after next week: the minimal first cut
plan_changed+ claim events on the SignalR batch; an activity feed component onPlanViewand on the home page.- "New" highlight on node cards whose
dependsOn/binding just changed. - A cross-plan ready board as the landing page.
- No write UI.
Everything above is additive; the backend change is one handler enqueuing one more event.
Dogfooding notes (2026-09-04)
Real use started answering the open questions above. Jotted as they came; the override question got the analysis it asked for, the rest are captured with just enough shape to build from.
1. Manual override of a node's status — needs analysis (here it is)
The ask: every action in the plan visualization needs a way to manually mark it "done" / "ejected" / "aborted" / "cancelled".
The four words are two concepts. "Done" is force-complete: treat the node as satisfied even though no observation says so (the issue is closed as won't-fix-but-actually-handled, the package was pushed by hand, the gate was verified by eyeball). "Ejected/aborted/cancelled" are all not-doing-this: remove the node from the critical path. For readiness derivation both unblock dependents identically; they differ only in how the card should read. Suggested vocabulary: done and cancelled (with the free-text reason carrying "aborted" vs "ejected" nuance) — two verdicts, not four states.
It fits the model without bending it. This is an asserted event, exactly like a claim: NodeOverridden { Verdict, Reason, By, At } (+ OverrideCleared for undo — assertions append, never erase). Provenance stays distinct on the card: the dashboard can still answer "the plan believes this because a human said so", which is the observe-never-own stance, not a violation of it. Stream-wise it belongs on the claim stream's sibling — per plan node — or on the plan stream; per-node is cleaner because PlanStatus.For already folds per-node inputs.
The fold rule is the real decision. Options:
- (a) Override wins, terminally. Simple, but a GitHub reopen after a human said "done" is silently ignored — the exact "crashed agent renders in-progress forever" failure, mirrored.
- (b) Override wins until evidence contradicts, then the conflict is surfaced: status renders
done (asserted)normally, and grows a badge when the observed state disagrees (issue reopened, package unlisted). Recommended. Nothing is dropped, the card says which source it believed, and a human resolves it by clearing or re-asserting.
Cancellation has a propagation question with no obvious answer. Does cancelling a node satisfy its dependents (skip it and move on) or moot them (the feature isn't happening, so its test gate isn't either)? Default to satisfies — it matches "unblock dependents" — but render cancelled-upstream edges distinctly so a moot subtree is visible, and revisit if real use wants cascade-cancel.
Mechanics: a command through the same pipe as everything else (OverrideNode → PlanHandler or a new OverrideHandler), MCP tool + REST endpoint, license-gated like every mutating surface. UI-wise this is the first write affordance in the viewer — the "no write UI until real use asks" line above has now been crossed by real use asking. A small context menu on the card (done / cancel / clear, reason required) is enough.
2. Contextual icons — built today
Kind glyphs (GitHub mark for issues, PR glyph, package for publish, download for consume, beaker for gates) on both the ready-board rows and the DAG cards (NodeKindIcon.vue); where the binding yields a URL the glyph+kind is an out-link (lib/nodeLinks.ts, hoisted from PlanView — GitHub for owner/repo#n, nuget.org for publishes). Ready rows previously had no external link at all.
3. Ready-board filtering — by kind, repository, plan
Kind and plan are client-side filters over data the board already holds; the legend chips are halfway to plan toggles already. Repository is the interesting one: it is not in the view model. Today it exists only inside ref (owner/repo#n) — publish/consume/gate nodes have no repository at all unless the model grows one (see §4). Build kind+plan now; repository filtering falls out of §4.
4. A formal Repository concept — model built same day
Wanted by §3 (filtering), §7 (swim lanes), and §8 (CI tracking — you can't watch workflows for a repo you can't name). Today "repository" appears as: the repo half of an issue/PR ref, the node-level repository field plans may carry, and the git tree roots the agent roster groups by. A first-class Repository could carry: GitHub slug, the NuGet packages it publishes, its CI workflows (names/expected checks), default branch, local clone/worktree roots seen on this box. Jeremy's instinct: it may live in the memory model — a Repository is exactly the kind of slowly-accreting, cross-plan knowledge memories are for, rather than plan-document config repeated per plan.
Built the same day (see agent-coordination-design.md, "Repositories as first-class memory"): event-sourced RepositoryRecord per repo, license-gated MCP tools, read-only REST, markdown notes as the first formal memory shape. UI (§5's memory view, §3's repo filter, §7's swim lanes) still to come.
5. Memory visualization + human-added memories
The "agent session memory" item in the design doc's not-built list, pulled forward: a UI that shows what Stoat remembers, plus the ability for a human to add a memory as embedded markdown in the page. Note this makes the memory surface the second write affordance (after §1), and an editor is a bigger UI commitment than a context menu — but a memory is append-only prose, which is the easiest possible write to model (asserted event, markdown body, no fold conflicts).
6. Archive / evict plans, and seeing archived ones
Half exists: archive is already a PlanHandler command — removes the plan from the active set, keeps the stream. Missing: (a) any UI affordance to invoke it; (b) an archived-plans listing — GET /api/plans only knows active plans, so a ?includeArchived or /api/plans/archived reading the kept streams, and a read-only plan page for an archived plan (history exists; the status derivation may want a "frozen at archive time" stance rather than deriving against live observations). "Evict" as distinct from archive — actually deleting a junk plan created during testing — is against the event-store grain; if wanted, it's a tombstone event that hides the stream everywhere, not a delete.
7. Swim-lane view: outstanding work by repository
A new view: horizontal lanes per repository, cards = outstanding (not done/cancelled) nodes across every plan, coloured by home plan as everywhere else. This is the cross-plan board re-pivoted: same pushed statuses, a different client-side grouping — buildable today for issue/PR nodes (repo derivable from ref), and honest lanes for everything else need §4. An "(unassigned)" lane for repo-less nodes keeps it shippable before §4 lands.
8. Execution + PR/CI lifecycle tracking — the big one
Wanted, in one line each:
- an at-a-glance "currently executing" indication per step;
- success/failure icons for NuGet pushes and GitHub CI, linking to the action run (or the PR when a PR's CI fails);
- the full PR lifecycle tracked: opened → CI running → CI passed/failed (per-workflow, since our repos run several) → merged, with easy colour coding.
What exists: the status grammar already has the colours (running/passed/failed/retrying tokens), pr-open and running statuses, openPrs on issue nodes, and Bobcat-run drill-in links. What doesn't: CI is not observed at all. GitHubPoller watches issues and PRs, not check runs/workflow runs. The shape of the work:
- Observe: extend the GitHub sweep with the Checks API per open PR head SHA (and workflow runs on the default branch for publish verification) → new observed events, change-detected like everything else. Multiple workflows per repo means the observation is a set of named checks, not one verdict — the per-repo expected-check list is §4 material.
- Derive: PR nodes get a CI facet (
checks: passing m/n, failing, running), issue nodes surface it via their open PRs; "executing" for an issue node is derivable today from claim- session (
claimHealth: held= an agent is actually on it) — that part needs no new observation, just rendering.
- session (
- Render: icon + colour per state reusing the existing tokens; the failure icon links to the failing run's URL (the Checks API carries it), the PR icon to the PR.
- NuGet push success/failure: the feed only shows presence; failure of a push isn't observable from nuget.org — a failed push is a CI-run failure (§ the publish workflow), which the Checks work covers.
Sequencing that falls out: §4 (Repository) unlocks §3, §7, and the per-repo check config in §8; §1 (override) and §2 (icons, done) are independent; §8's "who is executing" render-only slice is a quick win before any Checks API work.