6. Workflows + live fleet view (the orchestration layer)
Run a multi-step workflow - a DAG of tasks where each step fans out to all available adapters,
gets judged, and its winning output flows into dependent steps via {{stepId}} templating:
npm run wf -- --sample # built-in 2-step demo (answer -> critique)
npm run wf -- src/engine/sampleDagWorkflow.ts # branching diamond (start -> pros/cons -> verdict)
npm run wf -- path/to/def.ts # your own workflow (default-exports a WorkflowDef)
npm run wf -- path/to/script.ts # imperative SCRIPT (exports meta + a default async body)npm run wf accepts two shapes. A DAG module default-exports a WorkflowDef (static steps, judged,
templated). An imperative SCRIPT module instead exports a meta object plus a default async
function that receives a host context and drives the fleet by hand:
import type { WorkflowScriptMeta, HostContext } from './src/engine/workflowScript.ts';
export const meta: WorkflowScriptMeta = { name: 'flow-demo', description: 'imperative example' };
export default async function (host: HostContext) {
host.phase('Work');
const a = await host.agent('answer X'); // single-pick worker -> text
const obj = await host.agent('return JSON', { schema }); // { schema } -> parsed + validated object
const xs = await host.parallel([() => host.agent('a'), () => host.agent('b')]); // concurrent
host.log(`spent ${host.budget.spent()} / ${host.budget.total ?? 'unbounded'} tokens`);
}Host helpers: agent(prompt, opts?) (single-pick; opts.schema returns a validated object, opts.model/
opts.agentType force a worker), parallel(thunks), pipeline(items, ...stages), phase(title),
log(msg), args, and budget (total/spent()/remaining()). Concurrency is capped by a semaphore
(min(16, cores-2)); set a token ceiling with CYBERINE_WF_BUDGET (agent() stops running once it is hit).
workflow-cli auto-detects the shape (a meta + default-function module routes to the script runtime;
a WorkflowDef default export routes to the DAG engine).
Per-step model override (DAG defs). A WorkflowStep accepts an optional flat model field that
overrides the dispatched loop worker's model for that step only — a cheap model for grunt steps, a
stronger one for a gated verify step. It flows through Task.modelOverride into LoopAgentAdapter
(CLI workers ignore it); omit it and the worker's default model is unchanged.
const def: WorkflowDef = {
name: 'tiered',
steps: [
{ id: 'draft', prompt: '...', model: 'deepseek-v4-flash' }, // cheap grunt
{ id: 'verify', prompt: '...', model: 'claude-opus-4-8', deps: ['draft'] }, // strong gate
],
};Runs launched via the script runtime are persisted to RunStore with source: "workflow-cli" and
registered in the global registry, so they appear live in cyberine agents / npm run tui alongside
DAG-engine runs — no separate monitoring needed.
Known Limitations and Deferred Work (script runtime, src/engine/WorkflowScriptRuntime.ts).
host.agent() is single-pick per call — no DiffJudge race across candidates, deliberately out of
scope for this runtime's first pass. host.workflow() (calling one script from another) is capped
at nesting depth 1 — a script cannot itself call a script that calls a script. Journaled resume
(npm run wf -- <script> --resume <runId>) replays a prior host.agent() call only when its
call index and its prompt+opts hash both match the journal exactly; a mismatch is not surfaced
as a warning, it just re-runs that call (and every call after it) as if nothing was cached — so
editing the script upstream of a resumed run can silently invalidate the whole replay.
Memory consolidation — retired
src/workflows/dream-consolidate.ts (flow-dream-consolidate) was RETIRED on 2026-09-09 (k0wf7,
operator decision). It was a June port of the operator's private Claude Code flow-dream flow,
decision-only (its write phases were never built), never run outside three smoke runs, and already a
different design from the batched read-rewrite engine the operator rewrote in July -- two engines with
one name cleaning one memory tree two ways. Cleaning ~/.claude/memory is Claude Code tooling's job.
Cyberine's own memory is ~/.cyberine/memory/ (cyberine memory list|show|search|chain, the
Long-term memory section below) and stays vendor-independent, so the same orchestrator serves Codex or
any other CLI without depending on one vendor's memory layout. src/workflows/ and the
.cyberine/workflows/flow-dream-consolidate.ts shim were removed with it.
YAML recipes (T323) — portable, parameterized, nestable
The same engine accepts a YAML file as a third shape (alongside the DAG module and the imperative
script). The loader (src/engine/yamlRecipe.ts) parses the file, materialises a WorkflowDef, and
feeds it to the engine unchanged — the YAML surface is a strict subset of the TS surface plus a few
recipe-only conveniences (params, env defaults, extends).
Run recipe
npm run wf -- examples/recipes/sample-research.yaml
npm run wf -- examples/recipes/sample-research.yaml --param topic="Rust async runtimes"
npm run wf -- examples/recipes/sample-research.yaml --param topic="Bun vs Deno" --param max_questions=2
# Saved-name form (works when the file is in <repo>/.cyberine/workflows/ or ~/.cyberine/workflows/):
cyberine wf run sample-research --param topic="DuckDB vs SQLite"The runner auto-detects the shape by file extension: .yaml / .yml go through the recipe loader;
.ts files keep their existing DAG / script dispatch. The same --param key=value flag is forwarded
by cyberine wf run <name> --param ... so a saved name plus overrides works the same as a path.
Extend recipe — recipe shape
extends: ./base.yaml # optional; parent recipe (path relative to this file)
name: my-recipe # surfaced in the run record
description: one-line summary # not consumed by the engine
params: # parameter declarations with defaults
topic: TypeScript
depth: 2
prompt_style: { default: terse, description: "controls tone" }
env: # process.env defaults (never clobber an existing var)
LOG_LEVEL: info
steps:
- id: plan
title: Pick a research goal
prompt: |
Pick a question about ${params.topic} with ${params.depth} angles.
- id: research
title: Web research fan-out
depends_on: [plan]
workers: [opencode, loop-deepseek-direct]
search_fan_out:
questions:
- "How does ${params.topic} compare to alternatives?"
- id: verify
title: Verify findings
depends_on: [research]
verify_claims: { claims_from: research, skeptics: 2 }
- id: report
title: Synthesize a cited report
depends_on: [verify]
synthesize_claims: { verified_from: verify }Substitution tokens (resolved at materialisation, before the engine sees the def):
| Token | Source |
|---|---|
${params.X} | recipe parameter (override or default) |
${defaults.X} | alias for params.X |
${env.X} | process.env[X] after env: defaults applied |
${anything.else} | passes through unchanged (so ${VAR} survives in shell snippets) |
The engine's own {{stepId}} placeholder is left intact by the loader — recipe substitution runs
first, then the engine substitutes the winning outputs of completed steps. So a YAML step prompt can
mix both: Pick a question about ${params.topic} using this prior answer: {{plan}}.
Nesting — extends: <relative-yaml> loads the parent (cycle-guarded), concatenates parent steps
first then child steps, and overlays the child's params on the parent's. A child override reaches
into the parent's step prompts because substitution runs on the merged step list + merged params.
The child may add new steps that depend on parent steps via depends_on: [parent_step_id]. A
reused step id is a hard error.
Available step fields mirror the TS WorkflowStep type: id, title, prompt, depends_on,
workers, isolate, verify_cmd, swarm_use_lane_runner, swarm_chunks, search_fan_out,
verify_claims, synthesize_claims, await_run, projection. Snake-case keys map to the
camelCase TS fields internally; the engine is never modified.
Save under a short name to use cyberine wf run <name> form:
cp examples/recipes/sample-research.yaml .cyberine/workflows/research.yaml # project-local
cp examples/recipes/sample-research.yaml ~/.cyberine/workflows/research.yaml # global
cyberine wf run research --param topic="TypeScript vs Rust"Each run is persisted under .local/runs/<runId>/ (gitignored):
| File | Contents |
|---|---|
run.json | full run state tree: steps -> workerRuns (state / tempo / detail / usage / output) |
events.jsonl | append-only lifecycle stream (run:start, step:start, worker:start, progress, worker:done, step:done, run:done) |
<step>__<agent>.output | per-worker streamed output (live tail-able from the TUI) |
Then watch runs live in the terminal fleet view (modelled on claude agents):
npm run tuiThe header is a breadcrumb trail (Cyberine Fleet › <workflow> › <worker>) so you always know how deep
you are. Press ? on any screen for the full keybinding overlay — it shows the keys you have
actually bound (from CYBERINE_KEYBINDINGS / ~/.cyberine/keybindings.json), not hard-coded defaults, so a
remapped quit/help/inspect/new-session key is reflected there. See "Keybindings — remap fleet + chat
actions" below for the full schema and action table (both cyberine agents and cyberine chat share
one file, one namespace per app).
Since k534j the footer hint bar and that ? overlay are DERIVED from one action registry
(src/tui/fleetActions.ts) instead of three hand-maintained copies, so remapping a fleet.* key
moves the handler, the footer hint and the help row together — and a help row can no longer name a
key that does something else. npm run selftest:fleet-action-registry guards it (each remap check
carries a positive control, so a derivation that degenerated into a constant fails).
Keys (arrows or vim hjkl):
- List:
↑↓/jknavigate runs,Ctrl+D/Ctrl+Uhalf-page jump,g/Gtop/bottom,⏎/→/linspect,/filter the list (persistent; Esc clears),ffuzzy-jump to a run (moves the selection, keeps no filter),xstop the run (thefleet.stopbinding),Ctrl+X Ctrl+Kstop EVERY running agent (a chord —Ctrl+Xonly arms it),scycle the landing view (hub / sessions / fleet),oopen the selected run's folder,?help,qquit. Since kfw8v each of these has exactly ONE owner: Ink runs every mounted handler on every keypress, sosused to both cycle the view and stop a run, and/used to open two filtering surfaces at once. - Run detail:
↑↓/jkselect a worker,⏎/→/lpeek,ptoggle the full step prompt,dopen the winning diff,sstop the run,xstop the selected worker,esc/←/hback. - Peek:
Ctrl+U/Ctrl+Dscroll the output (gtop,Gfollow live),sstop this worker,esc/←/hback,?help,qquit. - Diff viewer:
↑↓/jkscroll,Ctrl+U/Ctrl+Dpage,g/Gtop/bottom,esc/←/h/qback.
The web UI — cyberine web (browser fleet monitor + chat)
cyberine web starts a dependency-free HTTP server (Node's built-in http, no framework) that serves a single-page browser UI mirroring the fleet TUI plus the chat planner. Same data as cyberine...
Meter tracks belong to their own meter (kw1e9)
Two leftovers from the sub-cell meter work.