Skip to content

Workflows: author and run agent graphs (opt-in plugin) - #94367

Open
OutThisLife wants to merge 23 commits into
mainfrom
bb/workflow-plugin
Open

Workflows: author and run agent graphs (opt-in plugin)#94367
OutThisLife wants to merge 23 commits into
mainfrom
bb/workflow-plugin

Conversation

@OutThisLife

@OutThisLife OutThisLife commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

An opt-in desktop plugin at /workflows. You author a graph of steps — agent, gate, human approval, wait, trigger — wire it up, and run it. Play starts a gateway walk of that graph. Cron, inbound webhooks, hermes workflow run, and the workflow tool's action=run all hit the same runner. The canvas folds the event log; there is no slideshow fallback.

The composer on the canvas is the real Hermes chat (SessionChat), not a lookalike transcript. Each workflow has its own hidden session. Hermes can also read and edit any workflow from any desktop chat through one workflow tool; those ops are the same mutations the inspector already runs. Adding or removing a step re-tidies the ranks and leaves the camera where you put it. The node inspector opens only when you click a card.

The document

scenario.ts names every field a kind can carry and is the single source of truth for what a step is. graph.ts is the only door onto mutating the document — a barrel over one file per concern (the document itself, steps, a gate's arms, wiring, validation, and the authored form the runner and disk speak). graph-tools.ts publishes those same mutations as JSON Schema tool descriptors, so an agent edit and a hand edit are the same operation against the same graph.

Nothing in the plugin is a god file. The canvas page keeps the canvas and hands layout, committing an op, the wiring gestures and the keymap to a hook each. The inspector is the sheet and the tab it shows — Config is the step as authored, Data the step as run. protocol.ts is the wire vocabulary and nothing else: what the canvas derives from a stream of it (the world it renders, the readable feed) sits beside it, and the run's workflow.run.* calls are named once so the player reads as the state machine it is. Same on the Python side — the runner keeps the advance loop and hands out topology (what a scenario's shape says), runtime (locks, signals, threads), and the steps that park.

A workspace holds many workflows. A header dropdown switches, creates, renames and deletes; deleting the last falls through to an empty state offering a blank canvas or the worked example. Documents persist under HERMES_HOME/workflows (the desktop still caches locally so a drag does not wait on a socket). A webhook trigger shows its route and HMAC on the inspector.

Running a graph

The runner walks the authored topology, runs ready agents together, parks on humans and waits (timer, named event, or a poll expressed as an event), and resumes after a crash from inFlight. Prose gate arms call a short agent turn.

What you can do to a run while it is going: pause and cancel are signals the loop absorbs at a step boundary, so in-flight work finishes instead of being torn out. A step can send you backwards — loop edges are part of the topology. Poll triggers arm a timer and fire on an HTTP probe. A run whose worker thread is gone is marked failed rather than left spinning forever. Steps resolve their model from the card, then the named profile, then this Hermes home.

Event sequence numbers come from the caller's counter now. Numbering a line by reloading the run JSON raced an in-flight save, reused numbers, and the canvas silently dropped every event after the collision.

hermes workflow list|run|event|status is the CLI door. A workflow's webhook is n8n-shaped — the unguessable URL is the credential, so an unsigned POST from curl or a generic tool reaches it; a sender that does sign is still verified, and a workflow route with nothing bound to it answers 404 instead of falling through to generic agent dispatch.

Reading a run back

Dragging the timeline used to tell you nothing about where you were going. It carries the elapsed time and the step under the pointer while you scrub, the way a media scrubber does.

The chat band under the canvas was measured in JavaScript, so its ceiling moved with whatever was in it and a short exchange got a tiny box. The stylesheet owns one height: the band grows with the conversation up to a cap and scrolls past it, and the band paints its own backing rather than a second element mirroring its inset. /new in that composer remints the canvas session instead of jumping to a workspace draft, and a session the gateway reaped is replaced rather than latching a "couldn't open" error.

Fitting the app

Everything visible is the host's, and the parts that were not yet are now shared. Kanban and Workflows had each hand-rolled the same detail panel, page header, advisory callout and stepper — those are promoted to components/ui, re-exported through the plugin SDK, and Kanban is moved onto them. EmptyState absorbs PanelEmpty.

Validation was the last thing with nowhere host-shaped to go. It landed in a banner at the top of the inspector, every line naming the step and the arm you were already looking at, and the panel kept a local control layer to build the rows those messages could not reach into. A Field takes a status now and puts the message under the control it is about, FieldStatusSlot does the same for a control in neither a Field nor a SidePanelMetaRow, and the inspector's local control layer is deleted. Kanban's two silent dead ends — Enter on an empty task title, a board name of pure punctuation — say something for the first time. An editor with no submit should not scold you for a draft, so an unfinished step reads as a quiet hint until you press play; then the same problems turn destructive and mark their controls invalid.

The canvas adds only what no app token covers — card geometry, activity-lane metrics, edge colour — and every rule is nested under .wf-root. Roundness, colour and mode all come from the theme.

The graph flows top to bottom by default, with a toggle in the header. Dagre already took the direction as rankdir; a context carries the same value to the components React Flow renders from a type map, so handles, gate arms and the loop-back path transpose with it.

Tours of a canvas

Asking for a tour broke on contact. Dragging to see the thing a step pointed at ended it, because driver.js dismisses on any overlay click and on a pannable surface the overlay is where you drag; and a one-off showTourStep had no close button at all, because driver.js drops the footer for a step with no next or previous. The backdrop is inert, Esc and the popover's own close stay live, and a lone step asks for its button by name.

Reaching the target is the other half: driver.js brings a target into view by scrolling the scrollport above it, and a canvas has none — cards are placed by a transform, so a node parked off screen stayed there while the step spotlighted a rectangle nobody could see. The engine asks the surface to reveal its own target and re-measures when an animated reveal lands. The canvas answers by panning, at the zoom you chose, and only for a card that is not already fully in view.

Test plan

  • scripts/run_tests.sh on tests/workflow, tests/tui_gateway, tests/gateway — green except six that fail the same way on a clean checkout (systemd/scale-to-zero probes that want Linux)
  • tsc --noEmit, eslint src and the full vitest run in apps/desktop — no errors, 7501 passing
  • Built and run in Electron: enable in Settings ▸ Plugins, open /workflows
  • Dismissing the human step's question and pressing play reopens it, rather than wedging the transport on "pausing"
  • Create / rename / switch / delete workflows; deleting the last lands on the empty state and survives a reload
  • Light and dark, both correct; the plugin follows the host's mode rather than owning one
  • Scrub the timeline mid-run: the tooltip tracks the pointer and names the step
  • Chat band shrink-wraps a one-line exchange and scrolls once it passes the cap
  • Tour: drag the canvas mid-step (survives), Esc and ✕ (both exit), a step on an off-screen card (pans, no zoom change), a step on a visible card (no pan)
  • Restart the desktop backend, enable the plugin, press Play — cards light from the live log
  • A webhook trigger shows /webhooks/wf-<id> and the HMAC; an unsigned curl -X POST to it starts the run
  • hermes workflow run <name> starts the stored graph
  • Pause mid-run finishes the in-flight step and stops; cancel ends it
  • Ask Hermes to add and remove steps: ranks tidy, camera does not jump
  • Click a node to open the inspector; a Hermes edit does not open it
  • CI

@OutThisLife
OutThisLife requested a review from a team August 25, 2026 02:21
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on b2b4d53 — feat(cli): add hermes workflow for list, run, event, and sta

❌ Job failures

JS & TS checks / JS & TS checks · View job

Job JS & TS checks / JS & TS checks failed.


⚠️ Action required

package-lock.json · View job

Locked npm dependency versions changed.

package-lock.json

Package Before After
➕ @dagrejs/dagre 1.1.8
➕ @dagrejs/graphlib 2.2.4
➕ @xyflow/react 12.11.2
➕ zustand (nested under @xyflow/react) 4.5.7
➕ @xyflow/system 0.0.79
➕ classcat 5.0.5

How to fix:

Add the ci-reviewed label after verifying the version changes are expected.


⚠️ Warnings

CI timings · View report · View job

Wall time 5m43s vs 4m4s (+40.6%). 8 job(s) slower, 4 faster, 1 unchanged.

  • Python tests / Run tests: -29.0s
  • OSV scan / Scan lockfiles / osv-scan: -6.0s
  • Python lints / Windows footguns (blocking): +5.0s
  • JS & TS checks / JS & TS checks: +5.0s
  • Python tests / e2e: -4.0s

OSV vulnerability scan · View job

7 known vulnerabilities found in pinned dependencies.

How to fix:

Review the findings in the Security tab. Update the affected dependencies if a patched version is available.

@alt-glitch alt-glitch added type/feature New feature or request comp/desktop Electron desktop app (apps/desktop/*) needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have labels Aug 25, 2026
@Enough1122

Copy link
Copy Markdown
Contributor

AI code review — automated review for reference, author can ignore or act on any point.

Diff too large for automated batch review (24 changed files, +11205/-30) — recommending manual human review.

The node canvas the Workflows plugin ships needs a graph renderer and an
auto-layout pass. Pinned exact, like every other dependency here.
A plugin that carries its own composer, controls and spinners had no way to
wear the real ones — it approximated them, which is how a plugin starts
looking bolted on. Re-exports the composer dock and its control-row button
vocabulary, the Select group/label parts, the control variants, and the
spinner name type.
An opt-in plugin at /workflows. You author a graph of steps — agent, gate,
human approval, wait — wire it up, and run it; the run walks the graph you
drew rather than a script, so a step you add behaves like a step.

The document is schema-driven. scenario.ts names every field a kind can
carry, graph.ts is the only thing that mutates it, and graph-tools.ts
publishes those same mutations as JSON Schema tool descriptors, so an agent
edit and a hand edit are one operation. The composer's planner is a pile of
regexes today, but it emits tool calls through that dispatcher — the seam
where a real provider drops in is one function.

Everything visible is the app's: the composer chrome and its control-row
buttons, Select/Switch/SegmentedControl/Dialog, the shimmer on an in-flight
label, the Kanban page's header row. The canvas adds only what no app token
covers — card geometry, lane metrics, edge colour — and every rule is nested
under .wf-root, because a plugin stylesheet shares one class namespace with
the whole document.

Ships off by default.
Kanban and Workflows had independently hand-rolled the same four things,
and the second copy is where they start to drift. Each one is promoted to
`components/ui` and re-exported from the plugin SDK, so a third plugin
inherits the answer instead of arriving at its own.

- `SidePanel` — the non-modal detail panel pinned to a page edge, with its
  header, title, toolbar, scrolling body, section, meta grid, and close.
  NOT `Sheet`: this one leaves the surface behind it live, which is why
  both plugins skipped `Sheet` and wrote a div.
- `PageShell` + `PageHeader` — the page root and titlebar strip. The two
  pages had different padding and a 5px height difference, so moving
  between them shifted the header; the header now has one definite height.
- `Callout` (from Kanban) and `Stepper` (from Workflows) — an advisory
  block that takes a tone the data already picked, and a bounded number
  with nudges.
- `Field` gains a `row` variant on `SidePanelMeta`'s label column, so a
  panel's editable rows line up with its read-only ones.
- `EmptyState` absorbs `PanelEmpty`'s icon and action. There were two
  components for one idea because the richer case had nowhere to go;
  `PanelEmpty` is now a thin call through it.

Kanban moves onto all of it, which is what proves the primitives are
general rather than Workflows-shaped.
…olds more than one

The canvas was a single hard-coded scenario laid out left to right, edited
by a regex that pretended to be an agent. This makes it a real document
surface.

- Flows top to bottom by default, with a toggle in the header. Dagre
  already took the direction as `rankdir`; a context carries the same
  value to the node and edge components React Flow renders from a type
  map, so handles, gate arms, and the loop-back's hand-drawn path all
  transpose with it rather than being placed by hand.
- The composer plans against the model. `GRAPH_TOOLS` goes over verbatim
  as the contract, so the model authors in the same schema the canvas
  stores — and what comes back is applied, validated, and handed back once
  with the problems attached when the schema says it won't run. That
  repair round is the difference between generating a workflow and
  generating one that runs. The old regex planner stays as the offline
  fallback, emitting the same tool calls.
- Many workflows, not one. A header dropdown switches, creates, renames,
  and deletes; deleting the last falls through to an empty state that
  offers a blank canvas or the worked example. Documents persist to the
  plugin's own namespaced storage, debounced, because a card drag
  republishes the whole document every frame.
- Adopts the shared page, panel, and field primitives, so the page reads
  as the same app as the board.
- Drops the floating control cluster, the minimap, and the event-log
  toggle — the live log already says what the run is doing.
- The step inspector moves to its own file. It closes over nothing in the
  canvas, and at 1516 lines `page.tsx` was three times its largest
  sibling.

Fixes a dead end where dismissing a human step's question left the
transport offering a pause it could never complete: the run reads as
running while parked, but the pump won't be re-entered until the question
is answered, so the request sat on "pausing" forever with restart as the
only way out. Parked runs now refuse the pause, and the transport reopens
the question instead — the thing that actually moves the run.
Hovering any wire lifted the whole `.react-flow__edges` layer above the
nodes, so every line on the canvas — not just the hovered one — drew over
every card it passed.

The lift was on the wrong layer. It was there so the wire's + and trash
would clear a neighbouring card, but those ride in React Flow's
edge-label renderer, which is a separate div from the edges <svg>; the
toolbar's own z-index already put it where it needed to be. Raising the
edges did nothing for the buttons and everything to the wires.

Dropped, and the toolbar goes to 1001 so it also clears a SELECTED card,
which `elevateNodesOnSelect` parks at 1000.
…rsation

Tiles already had the full chat stack. Pull it out of the pane so a detached
surface (the workflows canvas) can resume a session without forking the
transcript, composer, or tool cards.
The canvas is pointless if Hermes cannot read or edit it. Expose one
desktop_ui tool whose ops are the same mutations the inspector already
runs, so a chat turn and a hand edit stay one document.
The hand-rolled transcript could not keep up with the app's chat. Mount
SessionChat in the dock, give each workflow its own hidden session, and
let the workflow tool paint live. Add/remove re-tidies the ranks without
moving the camera; the inspector opens only when you click a node.
Play, cron, and inbound webhooks need a durable walker that parks for people and the world, not a slideshow in the renderer.
Play now starts a real run and folds its event log. The fake pump is gone, and the starter graph has a trigger so the first step is an entry rather than an accident.
A stored graph should be startable and signalable without opening the desktop app.
@OutThisLife OutThisLife changed the title Workflows: a node canvas for agent scenarios (opt-in plugin) Workflows: author and run agent graphs (opt-in plugin) Aug 26, 2026
Every form that wanted to flag a bad value hand-rolled the line under the
control, so a dialog, the Kanban drawer and a side panel each said it
differently and none of them marked the control invalid.

`Field` takes a `status`, `FieldStatusSlot` does the same for a control that
is in neither, and `useFieldControl` is how a control picks up the invalid
border and the description id. Two voices: `error` for a form you submit,
`notice` for an editor with no submit, where an unfinished draft should not
shout.

`SidePanelMetaRow` stacks like a `Field` now rather than sitting in a 6rem
key/value grid, so a panel's editable rows and a dialog's fields line up.
`controlVariants` grows a `chrome` variant for a control wearing no box —
an editable heading, an id in toolbar chrome.
A play button on a canvas card sits inside a node's title row, and the
smallest icon size in the family still overwhelmed it.

`icon-2xs` is that size, and `PRIMARY_ICON_BTN` splits: `PRIMARY_ICON_FACE`
is the solid-circle look on its own, so a button can wear it at any size
while the composer's send keeps the measure it always had.
Two ways a chat that is not the workspace pane lost its thread. `/new` in
its composer started a workspace draft and jumped away from the surface the
conversation was part of; and a stored id the gateway had reaped latched the
"couldn't open" overlay forever, because nothing else was going to mint a
replacement.

A surface claims `/new` through `$detachedNewSession` while it is mounted,
and `onMissing` hands the gone-session case back to the host that knows how
to remint. Both are opt-in — a detached chat that passes neither behaves
exactly as before.
Dragging the canvas to see the thing a step points at ended the tour —
driver.js dismisses on any overlay click, and on a pannable surface the
overlay is where you drag. The backdrop is inert now; Esc and the popover's
own close stay live. A one-off `showTourStep` had no close button at all,
because driver.js drops the footer when a step has no next or previous, so
that step asks for the button by name.

Reaching the target is the other half. driver.js scrolls a target into view
through the scrollport above it, and a canvas has none — its cards are
placed by a transform, so a node parked off screen stayed there and the step
spotlighted a rectangle nobody could see. The engine now asks the surface to
reveal the target and re-measures when an animated reveal lands; the canvas
answers by panning, at the zoom the user chose, and only for a card that is
not already fully in view.

The workflows bridge tells the model how to write one: open centred on what
the workflow is FOR, then one short step per card, in run order.
The runner could start a graph and finish it. Everything you do to a run
while it is going was missing, and a run whose thread died looked like it
was still going forever.

Adds pause and cancel as signals the loop absorbs at a step boundary, loop
edges (a step can send you back), and poll triggers that arm a timer and
fire on an HTTP probe. A run whose worker thread is gone is marked failed
rather than left spinning. Steps resolve their model from the card, then the
named profile, then this Hermes home, and the demo slugs the starter canvas
shipped resolve to nothing rather than 404ing every step in 300ms.

Event sequence numbers come from the caller's counter now: reloading the run
JSON to number a line raced an in-flight save, reused numbers, and the
canvas dropped every event after the collision.
A workflow's webhook is n8n-shaped — the unguessable URL is the credential,
and the point of it is that you can aim curl or a generic tool at it. Every
route demanded an HMAC signature, so none of that worked. A sender that does
sign is still verified, and a workflow route with nothing bound to it now
404s instead of falling through to generic agent dispatch.

`session.open` grows `persist`, for a surface that owns its conversation
rather than painting a draft: the row exists from the start, so a refresh
resumes the same id instead of 404ing "session not found". Everything else
keeps the lazy row it has today.
… knob

Validation had one place to land: a banner of sentences at the top of the
panel, each naming the step and the arm you were already looking at. So the
inspector kept its own control layer to build the rows those messages
couldn't reach into, and that layer was a second copy of the app's.

A problem now carries the field it's about and a short form of itself, and
the panel puts it under that control. The local control layer goes with it —
the inspector is the app's `Field`, `Input`, `Select`, `Stepper` and
`SidePanelMetaRow` directly, so it inherits the chrome instead of tracking it.

An editor with no submit shouldn't scold you for a draft, so an unfinished
step reads as a quiet hint until you press play; then the same problems turn
destructive and mark their controls invalid, because now they're the reason
it won't run.
…zes itself

The transport was a play button and a bar you couldn't read. Dragging it
told you nothing about where you were going, so scrubbing a run meant
letting go and looking. It now carries the elapsed time and the step under
the pointer while you drag, the way a media scrubber does, and the play
button is the primary circle at a size that fits a node's title row rather
than dominating it.

The chat band under the canvas was measured in JavaScript, which meant its
ceiling moved with whatever was in it — a short exchange got a tiny box. The
stylesheet owns one height now: the band grows with the conversation up to a
cap and scrolls past it, and the backing sheet is the band itself rather
than a second element mirroring its inset. `/new` in that composer remints
the canvas session instead of jumping to a workspace draft, and a session
the gateway reaped is replaced rather than latching an error.

@andrexibiza andrexibiza left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head 7b03138dfdecbfb8e39aabf975eb6b840fbe688e against live main@9aa7530f7b53699e2c6d648ded8f6300503b3dc7. The branch is currently 20 commits ahead / 275 behind its live-main merge base fab931fa28f8f446f23be17a3e4df14fa084e267, touches 92 files (+16,616/-447), and has no prior formal review submission on the current head. I read the runtime/store/trigger seams, workflow tool/graph mutation surface, webhook integration, runner tests, current adjacent webhook/workflow issues, and exact-head evidence state.

There is a lot of good systems work here. In particular, using one graph mutation implementation for both UI and agent edits is the right anti-drift shape; routing CLI/tool/cron/webhook starts into one runner avoids four execution semantics; human/wait/gate nodes are explicit rather than prompt conventions; and the event/run persistence model is much more reviewable than hiding orchestration in a lead agent's SOUL. The UI consolidation into shared host components is also thoughtful rather than building an isolated mini-app.

I have three merge blockers on this exact object.

P1 — wf- is being treated as authentication authority, so an ordinary configured webhook can silently lose HMAC enforcement

gateway/platforms/webhook.py::_is_workflow_route() currently says:

return bool(route_config.get("hermes_workflow")) or str(route_name).startswith("wf-")

and the request path does:

workflow_route = _is_workflow_route(route_name, route_config)
if secret != INSECURE_NO_AUTH:
    if self._request_carries_signature(request) or not workflow_route:
        if not validate_signature(...):
            return 401

That means any normal static webhook route whose operator happened to name it wf-* accepts an unsigned request despite having a configured secret, even when it is not owned by the workflow registry. Static webhook route names are operator-controlled config keys, so this is a presentation/name coordinate conferring mutation authority.

The branch already has a trustworthy distinction available: workflow/triggers.py::sync_webhook_routes() emits hermes_workflow: True and persists the workflow's opaque route token/secret. The string-prefix fallback is therefore both weaker and unnecessary for current-owned routes.

Required repair:

  • Do not let a bare wf- route name bypass configured HMAC.
  • Bind capability-URL semantics to an explicit trusted workflow-route record / marker derived from the persisted workflow route, not a naming convention.
  • Add an end-to-end regression for a generic static route named e.g. wf-release, configured with an HMAC secret and without workflow ownership metadata: unsigned POST must remain 401.
  • Keep a positive case proving a genuinely persisted workflow capability route can accept its intended unsigned POST without falling through to generic agent dispatch.

This is especially important with #95068 in flight on the same gateway/platforms/webhook.py surface: that PR strengthens route-configurable HMAC/replay semantics. Whichever lands second must semantically preserve the stronger verifier; a workflow route must not become an accidental downgrade lane.

P1 — HTTP poll waits introduce a raw gateway-side SSRF sink, and the regression suite currently blesses localhost access

workflow/runner.py::_http_ok() uses raw urllib.request.urlopen(url, timeout=10), and both _arm_poll() and tick_polls() feed authored workflow URLs into it. There is no tools.url_safety admission, no connect-time IP binding, and no redirect revalidation.

This is reachable from the agent-authorable workflow surface. apps/desktop/src/plugins/workflows/graph-tools.ts exposes until: {type: "poll", spec: ...} through graph_add_step, graph_update_step, and graph_set_scenario; tools/workflow_tools.py then allows a Desktop-sourced agent to edit and run that stored graph. So an authored workflow is sufficient to make the gateway issue the network request.

The current test suite confirms the unsafe side rather than guarding it: tests/workflow/test_runner.py::test_poll_url_resumes_when_the_world_answers starts an HTTP server on 127.0.0.1 and asserts the workflow poll reaches it.

Current main already has a canonical SSRF policy in tools/url_safety.py: private/loopback/link-local targets are denied by default; cloud metadata remains always denied; Hermes-owned direct clients are expected to use the SSRF-safe client path so DNS rebinding and redirects do not escape validation. The workflow runtime should consume that same authority instead of creating a second network policy.

Required repair:

  • Route workflow polling through the canonical URL-safety/SSRF-safe connection path, including redirect handling and the always-blocked metadata floor.
  • Preserve the existing security.allow_private_urls contract if private polling is intentionally supported; do not invent a workflow-only bypass.
  • Add negative regressions for loopback, RFC1918, link-local/cloud metadata, and a redirect from public → private; metadata must remain denied even under private-URL opt-in.
  • Keep a public-target positive test. If loopback polling is needed for tests, make the security opt-in explicit in the fixture rather than treating localhost as the default contract.

The important boundary here is not “is the workflow trusted?” The workflow tool makes the document model-editable. The network mutation therefore needs the same proof that every other Hermes-owned URL sink consumes.

Hard invariant — the new stylesheet is already over the repository's 2,000-line ceiling

apps/desktop/src/plugins/workflows/workflows.css is a new file with 2,051 added lines on this head. The repository's current development invariant is a hard ceiling: no file over 2,000 lines, and the ceiling is not a target.

Please split this before landing—e.g. canvas/edges, inspector/forms, timeline/run-state, and page/composer styles (exact decomposition is up to you). This is a structural blocker, not a style nit, and this is the cheapest point to keep the plugin from beginning life as a godfile.

Graph / ownership / merge-order notes

  • #88061 (@teknium1) remains open with needs-decision. It is the existing in-tree design owner for reliable per-task graph execution + trace, ported from Hermes-Bot-Mode#108. #94367 is not a duplicate—the scope here is a general workflow product, not specifically Bot Mode—but it is a concrete realization of the same reliable graph-execution class. Please interlock them explicitly rather than letting two orchestration vocabularies evolve independently. The useful distinction from that thread is still: dispatch acceptance is not execution settlement, and pipeline/round-table semantics should not be flattened into one ambiguous outcome.
  • #95068 directly collides in gateway/platforms/webhook.py and owns configurable signature/replay verification. Compose after/with it; do not resolve the conflict by retaining this PR's weaker prefix-based exemption.
  • #90385 also directly collides in gateway/platforms/webhook.py and owns durable webhook admission/session-handoff fencing. The workflow trigger path should reuse that admission truth where applicable rather than become a parallel webhook ownership model.
  • #93009 is complementary on cron settlement/transport receipts. Workflow starts may originate from cron, but a workflow completion should not infer delivery or settlement more strongly than the cron owner can prove.
  • #88589 is adjacent topology-contract work, not a duplicate runtime. Its typed-topology vocabulary/guarantee boundaries are worth reconciling with scenario.ts before this schema becomes another de facto IR.

Contributor credit: the submitted implementation/history I inspected is @OutThisLife / Brooklyn Nicholson's work; the design lineage above remains separate and should be credited as interlock/provenance rather than silently absorbed.

Exact-object acceptance

There are currently zero GitHub Actions workflow runs and zero commit status checks associated with exact head 7b03138d…. The old bot CI comment is for a superseded head and cannot certify this object. The branch is also 275 commits behind live main and GitHub currently reports it non-mergeable.

After the three blockers are repaired, this needs a current-main semantic restack—especially across the two webhook collisions—followed by exact-final-head CI (including the dependency-review gate triggered by the new Desktop packages) and the remaining real run-path checks listed in the PR body.

This is ambitious work, and the architecture has real coherence. The graph/runtime foundation is worth preserving. The blockers are concentrated at exactly the places where the new product crosses an existing authority boundary: webhook admission, outbound network access, and the repository's decomposition law. Close those, then the rest can be reviewed as the workflow product it is rather than as three exceptions around it. 🚀

A NoteNode typed against the bare data shape rather than the Node, two
functions asking for a mutable WorkflowDoc[] they only ever read, and a
handful of dead imports left behind by earlier passes.

The session mock in profile-routing spreads the real store rather than
enumerating it: the SDK pulls in more of the store graph than this suite
cares about, and a listed mock breaks whenever an unrelated module reads
a key nobody thought to add.
…ncern

Five files were doing several jobs each. Each one splits along a seam
that was already there, and nothing changes behaviour:

  graph.ts (1195) is now a barrel over graph-core / -steps / -arms /
  -wiring / -validate / -scenario. Which file an op lives in is an
  implementation detail; the one door stays.

  page.tsx (1054) keeps the canvas and hands the rest to hooks that own
  one job each — use-canvas-layout (where the cards sit and where the
  camera looks), use-commit (applying an op, painting an agent build),
  use-wiring (the connect and delete gestures), use-canvas-keys.

  inspector.tsx (745) is the sheet and the tab it shows. Config is the
  step as authored, Data the step as run, and a gate's routing rules —
  the one part of Config that edits the graph — are their own file.

  protocol.ts (723) is the wire vocabulary. What you derive from a
  stream of it moved next door: protocol-world folds events into the
  world the canvas renders, protocol-feed projects them as readable
  lines.

  graph-tools.ts (559) is the catalog a model is handed; running one of
  them is graph-dispatch. The player keeps its state machine and hands
  the workflow.run.* calls to run-rpc, which names them once.
…d the stand-in

runner.py was 1242 lines of four different jobs. It keeps the advance
loop and hands out the rest along the layers that were already implicit
in it:

  topology  reads a scenario's shape — preds, succs, loops, the poll and
            wait specs. Pure functions, no state and no I/O.
  runtime   the plumbing every layer needs: the run lock, the signal
            table, emitting an event, spawning and arming a thread.
  waits     the steps that park — human, timer, poll — and the ticks
            that wake them.
  fake      the recording stand-in, off unless a caller asks for it.

Same behaviour throughout; the split is what makes the advance loop
readable as a state machine again.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/desktop Electron desktop app (apps/desktop/*) needs-decision Awaiting maintainer decision before any implementation P3 Low — cosmetic, nice to have type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants