Status: ✅ Complete (Product Phase 1, build phase 2) — all workstreams 2.A–2.S merged as of PR #58 (2026-06-28); the published CLI is cut as v0.1.1. 2.A (CLI skeleton + process contract) and 2.B (config resolution) are ✅ Done (PR #40, 2026-06-22), behind ADR-0047 + ADR-0048; 2.D (
run→ engine, the M3 keystone) is ✅ Done (PR #41, 2026-06-22), and 2.F (the--jsonCI machine-output contract) is ✅ Done (PR #42, 2026-06-22), behind ADR-0049, and 2.K (the engine regression harness) is ✅ Done (PR #43, 2026-06-23) — so global milestone M3 is reached; 2.H (durable run history) is ✅ Done (PR #44, 2026-06-23), behind ADR-0050; and 2.C (provider/key commands — OS keychain via@napi-rs/keyring) is ✅ Done (PR #45, 2026-06-23), behind ADR-0019 + ADR-0006; and 2.E (theinkstreaming TUI) is ✅ Done (PR #46, 2026-06-24), behind ADR-0047; and 2.G (the interactive human-gate prompt +relavium gatecross-process resume) is ✅ Done (PR #47, 2026-06-24), behind ADR-0047 (@clack/prompts; no new ADR) — fully closing 2.K's deferred gate-resume half; and 2.I (the read commandslist/logs/status/gate listover durable history) is ✅ Done (PR #48, 2026-06-24) (no new ADR — additive@relavium/dbread seam +@relavium/coreparseAgent); and 2.L (packaging, distribution & install verification) is ✅ Done (PR #49, 2026-06-24), behind ADR-0051 — closing go/no-go #7, so the Phase-2 spine is complete and all seven Phase-3 exit criteria now hold; and 2.S (media host-wiring — the surface half of the multimodal sub-spine) is ✅ Done (PR #52, 2026-06-25), behind ADR-0042–ADR-0046 (no new ADR), the first additive lane done; and 2.M (therelavium chatREPL — the first user-facingAgentSessionsurface) is ✅ Done (PR #54, 2026-06-26) (no new ADR — covered by ADR-0024/ADR-0047/ADR-0028/ADR-0050/ADR-0029;read_mediainput access split into a dedicated follow-up); and the rest of the agent-first chat family — 2.N (chat-resume), 2.O (chat-list), 2.P (chat-export+ in-REPL/export), and 2.Q (chat --json+ one-shotagent run+--fixturecassette) — is ✅ Done (PR #55, 2026-06-26) (no new ADR), completing the agent-first CLI lane; and 2.R (the inbound MCP client) is ✅ Done — the@relavium/mcpSDK-fenced package (the dependency-free JSON-Schema→Zod compiler + the fail-loud connect-all manager) landed PR #56, 2026-06-26, and the host wiring (chat/run/agent run), the network transports (http/sse/websocket) behind the SSRF pre-connect floor +allow_local_endpoint, named secrets via the isolatedmcp-secret:*keychain namespace, the by-namerefform, and the real-spawn e2e landed PR #57, 2026-06-27, behind ADR-0034 + ADR-0052 + ADR-0053 — the first two additive lanes (2.S, 2.R) now done, off the Phase-3 go/no-go; and 2.J (thecreate/import/exportYAML-authoring lifecycle — the last in-phase lane) is ✅ Done (PR #58, 2026-06-28) (no new ADR — covered by ADR-0026/ADR-0047). With 2.J merged, every Phase-2 workstream is complete and the in-phase CLI is feature-complete (published as v0.1.1); Phase 3 was already unblocked at 2.L. The (now-empty) build queue is the Remaining build order section.
- Related: ../README.md, phase-1-engine-and-llm.md, phase-3-desktop.md, ../../reference/cli/commands.md, ../../reference/contracts/config-spec.md, ../../reference/desktop/keychain-and-secrets.md, ../../reference/contracts/sse-event-schema.md, ../../reference/shared-core/database-schema.md, ../../architecture/execution-model.md, ../../architecture/shared-core-engine.md
Ship the relavium CLI (apps/cli) as the first real consumer of the
engine, proving @relavium/core and @relavium/llm end-to-end with zero UI
complexity. The CLI is the fastest feedback loop, so it forces an ergonomic
engine API surface and then becomes the engine's regression harness for every
later phase — a small suite of example workflows that runs in CI on every engine
change. It is also the canonical reference implementation for how a surface
consumes the RunEvent stream,
the config resolution order, and the
keychain secret model.
- A published, installable
relaviumCLI (npm i -g relavium), bundled to a single ESM bundle withtsup. run / list / logs / gate(pluscreate / import / export / statusand theproviderkey commands) all working against a real local workflow.- An
inkstreaming TUI that renders every canonical run event live (per-node status rings, the active node's token stream, a running cost/duration summary). - A
--jsonnon-interactive mode emitting oneRunEventper line (NDJSON), with deterministic, documented exit codes for CI. - Interactive human-gate approval, and the non-interactive
relavium gateresume path, both driving the same engineresume()contract the other surfaces use. - Durable local run history via
@relavium/dbsolist,logs, andstatusread persisted state. - The CLI wired into CI as the engine regression harness.
- A
commander.jsentry exposing the command surface in ../../reference/cli/commands.md: the engine- proving core (run,list,logs,gate,status) pluscreate / import / exportand theproviderkey subcommands. - Two-level config resolution (
~/.relavium/global → project.relavium/) with CLI flag / env override, exactly per ../../reference/contracts/config-spec.md. - Provider/key commands that store API keys in the OS keychain via
@napi-rs/keyring(ADR-0019 — not the archivedkeytar) under theservice=relavium,account={providerId}:{keyId}naming from ../../reference/desktop/keychain-and-secrets.md, with the headlesssecrets.enc/ env-var fallback for CI. - An
ink(React-for-terminals) TUI rendering the colon-namespaced run events live;@clack/promptswizards forcreateand the human-gate prompt. - A
--jsonCI / non-interactive mode that is a renderer over the same event bus, not a separate code path. - Local SQLite run history wired through
@relavium/db(runs,step_executions,run_events,run_costs) solist/logs/statusread durable state. - A CLI-driven engine regression harness: a small set of example workflows run
with
--jsonand asserted on in CI on every engine change. - The MCP client (inbound direction of
mcp-integration.md): connect/spawn the
[[mcp_servers]]registrations resolved by 2.B and surface their tools through the engine'sToolRegistry, per ADR-0034 (2.R — off the M3 critical path).
- Any graphical canvas or
packages/uiwork — that is the desktop phase (phase-3-desktop.md). - Accounts and OAuth (
relavium auth login, device flow) — Product Phase 2, first introduced with managed inference (phase-5-managed-inference.md). Cloud execution and remote run dispatch are Product Phase 2 cloud (phase-6-cloud-execution-portal.md). - Scheduled / webhook triggers; only
manual(and the engine'sfile_change) triggers exist in Phase 1, andfile_changeis exercised by later surfaces. - SQLCipher-encrypted history and the Tauri keychain plugin — those are desktop
concerns; the CLI uses
@napi-rs/keyringand an in-process engine bus, not Tauri IPC. - Any engine behavior change made as a CLI workaround. Gaps found here are folded back into Phase 1 packages and re-tested (see Risks).
The workstreams are ordered for execution. Critical-path workstreams (2.A,
2.B, 2.D, 2.F, 2.K) gate the global milestone M3. Earlier workstreams
unblock later ones; build them in this order. (2.R is the one exception to the
documentation order: it appears after the chat workstreams below but is
scheduled early — implementation needs only 2.B + 2.C, while its end-to-end
acceptance fixture additionally needs 2.F's relavium run to exist.)
flowchart LR
A["2.A skeleton"] --> B["2.B config"]
B --> C["2.C provider/keys"]
C --> D["2.D run wiring"]
D --> E["2.E ink TUI"]
D --> F["2.F --json / CI mode"]
E --> G["2.G human gate"]
F --> G
D --> H["2.H db + history"]
H --> G
H --> I["2.I list/logs/status"]
A --> J["2.J create/import/export"]
D --> K["2.K regression harness"]
F --> K
G --> K
K --> L["2.L packaging & publish"]
B --> R["2.R MCP client"]
C --> R
Stand up apps/cli with the commander.js program, the binary entry point, and
the cross-cutting process contract (output-mode detection, exit codes, global
flags) every command inherits.
Tasks:
- Scaffold
apps/cliin the Turborepo withtsupESM bundling and thebinfield that produces therelaviumexecutable. - Define the
commanderprogram with the full subcommand surface from ../../reference/cli/commands.md:run,list,create,import,export,logs,status,gate,provider(and reservedinit/agentstubs that error cleanly as planned). - Add global flags resolved once at startup:
--json,--no-color,--cwd,--config,--verbose/--quiet. - Implement output-mode auto-detection: interactive TUI when a TTY is
attached; non-interactive line-buffered output when no TTY,
--json, orCI=true(per the Output modes table in the command reference). - Implement the deterministic exit-code map as a single shared helper:
0success,1workflow failed,2invalid invocation / not found / schema error, exit3for gate-paused (a non-interactive run suspended at a human gate), and exit4for a chat session ended by the user (/exitin an interactiverelavium chat— implemented in 2.M). The canonical home for these values is commands.md; use them consistently here. - Centralize error rendering: human-readable to a TTY, structured
RunEvent-shaped error object to stdout under--json; never leak a stack trace as the primary output. - Keep each command's core framework-free: the logic lives in a plain module
(parsed args in, typed result out) that imports neither
commandernorink; the CLI layer is thin wiring around it. This is what makes commands unit-testable without a TTY and keeps the TUI and--jsonpaths renderers over one core.
Acceptance: relavium --help and every subcommand's --help print the
documented surface; relavium <bogus> exits 2; output mode is correctly
selected across a TTY, a piped stdout, and CI=true; the exit-code helper is unit
tested for codes 0–3 here (the chat /exit code 4 is exercised in 2.M).
Implement the two-level configuration loader the rest of the CLI depends on, with the exact precedence in config-spec.md.
Tasks:
- Locate and lazily create
~/.relavium/on first run (config.toml,history.db,tmp/); never write secrets into any config file. - Discover the project root by walking up for a
.relavium/directory from--cwd(defaultprocess.cwd()); loadproject.toml, optionalworkspace.toml, and theworkflows/+agents/YAML catalog. - Implement the merge with last-writer-wins precedence: global → workspace → project → per-invocation (CLI flag / env var), matching the resolution diagram.
- Validate each config layer against the
@relavium/sharedconfig Zod schema; surface a precise, file-attributed error (exit2) on a malformed layer. - Expose a resolved-config object (default model,
fs_scope, variables, MCP registrations) consumed byrun,provider, and the wizards. - Honor
.relaviumignoresemantics for reads only (the CLI never commits or ignores on the user's behalf).
Acceptance: a setting defined in project.toml overrides config.toml; a
--input/flag/env value overrides both; a malformed TOML layer fails with a
file-attributed error and exit 2; a project with no .relavium/ is reported
clearly; no secret is ever read from or written to a config file.
Add the relavium provider subcommands so a user can register providers and store
API keys in the OS keychain — the CLI's secret seam, equivalent to the desktop
keychain and VS Code SecretStorage.
Tasks:
- Implement
provider list,provider add,provider set-key,provider remove-key,provider test(a minimal live ping through@relavium/llm). - Store each key with
@napi-rs/keyringunderservice=relavium,account={providerId}:{keyId}(one entry per key so keys rotate/revoke independently), per keychain-and-secrets.md. - Persist only the non-secret provider row (
name,base_url,api_key_keychain_ref, …) to@relavium/db'sllm_providerstable — never the key value. - Implement the headless fallback chain for CI / no-keychain hosts: env var
(e.g.
RELAVIUM_<PROVIDER>_API_KEY) → hard error. No silent plaintext fallback. (The opt-in~/.relavium/secrets.encencrypted-file fallback is deferred past v1.0 — it requires a proper KDF, e.g. Argon2id over a user passphrase, not a hand-rolled scheme; the canonical keychain-and-secrets.md §Encrypted-file fallback governs. v1.0 is keychain + env var only.) - On display, show only a key hint (last 4 chars); never echo a full key to
stdout, logs, or
--jsonoutput. - Resolve keys at LLM-call time through the engine's provider seam so a key is never held longer than a request needs it.
Acceptance: provider set-key writes to the OS keychain (verified out-of-band)
and the DB row holds only a reference; provider test succeeds against a real key
and fails cleanly with a bad one; on a host with no keychain, an env var or
secrets.enc resolves the key and absence yields a clear error; no command path
ever prints a full key.
Wire relavium run to the engine: resolve the workflow, validate inputs, call
WorkflowEngine.start(...), and consume the RunHandle.events stream. This is
the central integration with Phase 1 and the heart of M3.
Tasks:
- Resolve the
<workflow>argument as an explicit.relavium.yamlpath or as a workflow id/slug discoverable under the project.relavium/workflows/. - Parse repeatable
--input k=vinto typed inputs and validate them against the workflow'sinputsschema before starting (fail fast with exit2). - Construct the engine with the resolved config (default model,
fs_scope, MCP servers, the keychain-backed provider seam) and callWorkflowEngine.start(workflowId, input). - Consume the in-process
RunEventBus/RunHandle.eventsasync iterator, routing every event bynodeIdto whichever renderer (2.ETUI or2.FJSON) is active. - Forward
AbortSignalon SIGINT so Ctrl-C cancels the run cleanly and emits arun:cancelledevent before exiting. - Map the terminal event to the process exit code:
run:completed→0,run:failed→1,human_gate:pausedin non-interactive mode→3(the gate-paused code; canonical home commands.md), validation failure→2. - Treat any engine API friction as a Phase 1 amendment: file it back against
@relavium/core/@relavium/llm, do not paper over it in the CLI.
Acceptance: a real 3-node sequential workflow runs end-to-end via
relavium run, streaming every canonical event in order; --input values reach
the workflow; Ctrl-C cancels with run:cancelled and a non-zero exit; the
terminal-event→exit-code mapping holds for success (0), failure (1), and
gate-paused (3).
Render the live run in the terminal with ink, consuming the same event bus as
the JSON path so the two never diverge.
Tasks:
- Build an
inkapp that subscribes to the run event stream and maintains a per-nodeIdstatus map (pending / running / completed / failed) shown as a list of status rings/spinners. - Stream
agent:tokenfor the active node into a live-updating output region; renderagent:tool_call/agent:tool_resultas compact, secret-free lines (tool input is already sanitized by the engine). - Maintain a running cost/duration footer fed by
cost:updated(cumulativeCostMicrocents) and nodedurationMs, ending in a final summary panel. - Throttle/batch high-frequency token events to keep the render smooth without
dropping events; use
sequenceNumberto detect a gap and resync from durable state if one occurs. - Degrade gracefully under
--no-colorand narrow terminals; never render the TUI when no TTY is attached (defer to2.F).
Acceptance: running the 3-node workflow in a real terminal shows live per-node
status transitions, streaming tokens on the active node, and a running cost that
matches the final summary; --no-color produces plain output; no event is dropped
under a high token rate (verified against the JSON stream's event count).
As-built reconcile (PR for 2.E). Two task items were narrowed, both deliberately: (1)
sequenceNumbergap handling is detect-and-warn, not resync. The CLI consumes the in-process no-dropRunEventBus, so a gap (or a backward/duplicate seq) signals a defect and surfaces a TUI warning; a true "resync from durable state" needs the history read API and lands with 2.I (same reconcile-the-spec discipline as 2.F/ADR-0049). (2)--no-colorkeeps the TUI and suppresses ANSI color/dim (it does not swap to the plain renderer — that is the no-TTY/CI=truefallback), which satisfies "plain output". Known limitation: the live token region shows the single active node's stream; concurrent agent nodes in a fan-out share that one region (no data loss — the node list and final summary stay correct), so per-node token buffers are a possible later enhancement.
Implement the machine-readable path: one RunEvent per line (NDJSON), a stable
envelope, and deterministic exit codes — the contract CI jobs assert on.
Tasks:
- Implement a JSON renderer that serializes each
RunEventfrom the bus to a single line on stdout (NDJSON), preservingtype,runId,timestamp, andsequenceNumberexactly per sse-event-schema.md. - Engage NDJSON only under
--json(the explicit machine opt-in, per ADR-0049); no-TTY /CI=truedisable the interactive TUI but do not by themselves switch stdout to NDJSON (a non-interactive run without--jsonuses the plain line-per-event renderer). The JSON renderer shares the same event bus as the TUI (renderer, not a fork). - Use the terminal
run:completedevent as the final structured result line — it already carries outputs + totals, so do not emit a separate summary line (it would break the every-line-is-a-RunEvent invariant; ADR-0049 §2). Set the exit code from the terminal event; in non-interactive mode arun:paused(the aggregate suspension event — a human/approval/budget gate) exits with code3(the gate-paused code; canonical home commands.md) rather than blocking. - Keep stdout pure NDJSON (events/result only); send human-oriented diagnostics to stderr so a pipe consumer never has to filter noise.
- Document the stable envelope and exit-code table additions in commands.md and the run-a-workflow-in-ci tutorial.
Acceptance: relavium run … --json with no TTY emits valid NDJSON where every
line is a schema-valid RunEvent in sequenceNumber order; stdout contains no
non-JSON bytes; a successful run exits 0, a failed run 1, a gate-paused run 3;
the same run under the TUI produces the identical event set.
Implement both gate surfaces over the engine's single suspend/resume contract: an
inline interactive prompt during run, and the out-of-band relavium gate <runId>
resume command.
Tasks:
- On
human_gate:pausedin interactive mode, render a@clack/promptscard from the event (gateTypeapproval/input/review,message,expiresAt?) and collect approve / reject (+ comment) or the requested input. - Build the
GateDecision(decision,decidedBy,payload?,comment?) and callengine.resume(runId, gateId, decision); continue rendering onhuman_gate:resumed. - Implement
relavium gate <runId> --approve | --reject --comment … | --input '…'for resuming a run that paused (e.g. in CI) by loading the persisted gate state from@relavium/dband calling the sameresume()path. - Make resume idempotent: re-delivering the same decision must not advance the run twice (rely on the checkpoint/gate-state idempotency from the engine).
- Surface the node's
timeout_actionoutcome (a timed-out gate resolves withdecidedBy: 'timeout'); render it consistently in both modes.
Acceptance: an interactive run pauses at a gate, prompts, and resumes on
approval to completion; a non-interactive run exits with the gate-paused code 3
and is later resumed by relavium gate <runId> --approve; a doubled decision does not
double-advance; reject and --input paths both work end-to-end.
Wire durable SQLite history so runs persist their events, steps, and costs for later inspection — the same tables the desktop replays.
Tasks:
- Depend on
@relavium/dband open the CLI's local history database (~/.relavium/history.db), running migrations on first use. - Persist a
runsrow at start (withworkflow_definition_snapshotandtrigger_type='manual'),step_executionsper node attempt, the append-onlyrun_eventslog (with monotonicseq), andrun_costsper node. - Update
runs.status, token totals, andtotal_cost_microcentson terminal events; store cost as integer micro-cents (never floats). - Ensure persistence is a passive consumer of the event bus so writing history can
never alter run behavior or ordering; sanitize before write (no secrets in
messages/run_events, consistent with the engine's guarantees).
Acceptance: after a run, history.db contains the run, its ordered
run_events, per-node step_executions, and run_costs whose totals equal the
run's reported cost; a gate-paused run persists enough state for relavium gate
to resume it in a fresh process.
Implement the read commands against persisted state so users can browse the catalog and inspect past and active runs.
Tasks:
relavium list: list workflows discovered under.relavium/grouped by tag with last-run status (using theROW_NUMBER() OVER (PARTITION BY workflow_id …)pattern, since SQLite has noDISTINCT ON); add a flag to list agents.relavium logs <runId>: print the persistedrun_eventsstream for a past run inseqorder; under the global--jsonflag, emit rawRunEventJSON (the same data the desktop run-detail drawer replays).relavium status: show currently active/paused runs and their per-node status fromruns+step_executions.relavium gate list [<runId>]: list the pending human gates (all active runs, or one run) with their gate type + node id, so an operator can pick thegateIdto pass torelavium gate <runId> --gate <gateId>— the multi-gate discovery surface the 2.Ggatecommand's--gaterequirement points at (canonical: commands.md).- Make these honor
--jsonfor scriptable output and the documented exit codes (2for an unknownrunId).
Acceptance: list shows discovered workflows with correct last-run status;
logs <runId> reproduces the run's event stream in order (and as raw RunEvent
NDJSON under --json); status reflects a paused run while it awaits a gate;
gate list lists the pending human gates across paused runs (and of one run when
given a <runId>), so an operator can pick the gateId; an unknown runId
exits 2.
Round out the authoring commands so workflows/agents can be scaffolded and moved in and out of a project — the "workflow file is the invite" distribution path.
Tasks:
relavium create: a@clack/promptswizard that writes a new.relavium.yamlor.agent.yaml(name, model, system prompt, tools) as plain, git-ready YAML validated against the@relavium/sharedschema before write.relavium import <path>: copy an external YAML into the project.relavium/, validating schema and slug uniqueness.relavium export <id>: write a portable copy with all secret references stripped/placeholdered per keychain-and-secrets.md (safe to share via PR).- Keep these surface-agnostic: they only read/write git-native YAML, never the keychain or run state.
Acceptance: create produces a schema-valid YAML file that subsequently
runs; import rejects a malformed or slug-colliding file with exit 2; export
output contains no secret material and re-imports cleanly elsewhere.
As-built reconcile (PR #58). Three details settled during build, all maintainer-approved: (1)
createscaffolds either an agent or a minimal single-agent workflow (input → agent → outputwrapping one inline agent), validated against the kind-appropriate@relavium/sharedschema before write — the wizard needs an interactive terminal on both ends (TTY stdout + TTY stdin), failing loud (exit2) under--json/ a pipe. (2) Share-safety is a re-serialize from the validated AST, not a literal "strip secret references": agent/workflow YAML carries no provider key by construction (no schema field holds a key value), MCPenvsecrets are{{secrets.*}}placeholders by convention, andexport/importre-serialize faithfully (canonical form, comments dropped where a stray secret might hide) — the corrected framing is canonical in keychain-and-secrets.md. (3) Ids are unique project-globally (across both catalogs):create/importshare oneassertSlugAvailableguard — a same-kind clash needs--force, a cross-kind clash is always rejected — so a bare<id>stays unambiguous forexport <id>/run.
Adopt the CLI as the engine's canonical integration-test harness so every engine
change is exercised end-to-end. Completes M3 with 2.D + 2.F.
Tasks:
- Add a small fixture suite of example workflows under the repo (sequential, fan-out/aggregate, conditional, and a human-gate workflow) with recorded LLM fixtures so runs are deterministic and offline on PRs.
- Add a CI job that runs each fixture with
relavium run … --jsonand asserts on the NDJSON stream (event types and order) and the exit code. - Add a gate-resume scenario: run to a gate, assert the gate-paused exit (
3), thenrelavium gate … --approveand assert completion. - Wire the harness to run on every change to
@relavium/core/@relavium/llm/apps/cli; keep a nightly variant that can run against live providers. - Document the harness so later phases extend it rather than re-inventing one.
Acceptance: a green CI run executes every fixture workflow with no TTY,
asserting the NDJSON event stream and exit codes, including a gate-pause-then-resume
scenario; the job is triggered by engine-package changes and is the agreed
regression gate for Phases 3–6. (See First cut + deferred below for what the first
cut realizes — the harness rides the required test task rather than a new job, and the
gate-resume half is deferred to 2.G.)
First cut + deferred (scope-split, maintainer-approved 2026-06-22). The harness lands as an
in-process vitest e2e suite (apps/cli/src/harness/) that drives the real relavium run … --json
path over five committed non-agent fixtures (sequential, fan-out/aggregate, conditional,
human-gate, and a failure case → run:failed) — together covering every authorable non-agent node
type and all three run exit codes (0/1/3), fully deterministic + offline, run inside the
required CI test gate on every PR;
its fixture + scenario format is documented in
reference/cli/regression-harness.md. Two parts are
deferred because they depend on not-yet-built workstreams:
- Gate-resume scenario (
relavium gate --approve→ completion) → ✅ landed with 2.G (PR #47): the harness now runs the human-gate fixture to exit3against a durable (file) db, then resumes it from a fresh connection torun:completed(gap-free seq across the boundary). The run-to-gate → exit3half shipped with the §2.K first cut. - Agent fixtures via recorded-LLM replay + the nightly live-provider lane → the offline first cut is
non-agent; an agent fixture drives a recorded
fetchcassette (@relavium/llmconformance/replay.ts) through the injectableProviderResolver, and the reservedlive-api(on: schedule) job in.github/workflows/ci.ymlis the nightly variant that re-records + diffs them.
Make the CLI installable and verify the published artifact behaves like local dev. Behind ADR-0051 (the bundle boundary — an engine-inlined ESM bundle that externalizes every third-party dependency).
Tasks:
- Finalize the
tsupESM bundle andpackage.json(bin,engines, files allowlist) sonpm i -g relaviumyields a workingrelaviumcommand. - Smoke-test the global install on macOS / Linux / Windows:
relavium --help,providerkeychain round-trip, and a fixturerun --json. - Document install + CI usage in commands.md and the CI tutorial; align the release flow with ../../runbooks/release-a-surface.md.
Acceptance: a clean npm i -g relavium on each OS produces a working binary
that runs a fixture workflow with --json and the correct exit code; install/CI
docs match the shipped flags.
The interactive agent entry point on the CLI (ADR-0024, chat-session.md). Non-critical to M3 — these ride behind the regression-harness spine.
- 2.M —
relavium chatREPL. Aninkmulti-turn terminal chat overAgentSession: streaming tokens, tool-call annotations, FS-scope tier + command allowlist honored./exitends the session with exit code 4. - 2.N —
relavium chat-resume <sessionId>. Reload + continue a persisted session fromhistory.db. - 2.O —
relavium chat-list. List session history (id, agent, title, last activity). - 2.P —
relavium chat-export <sessionId>. Export a session to a.relavium.yamlscaffold (ADR-0026). - 2.Q —
relavium chat --json+relavium agent run. A deterministic--jsonsession:*stream (CI-friendly); and a one-shotrelavium agent run <agent> [--json] [--fixture …]on the sameAgentSessioninfra (--inputreserved/rejected — see the status note). (Therelavium gate listmulti-gate discovery command lands with the read commands in 2.I, per commands.md.)
Acceptance: an interactive relavium chat streams a multi-turn conversation with a tool call, persists, and resumes; chat --json emits a deterministic session:* stream; agent run invokes a single agent headlessly; chat /exit returns exit code 4.
Status. 2.M (
relavium chat) is ✅ Done (PR #54, 2026-06-26) — no new ADR (covered by ADR-0024, ADR-0047, ADR-0028, ADR-0050, ADR-0029). Shipped: the interactive multi-turn REPL overAgentSession(ONE framework-free command core driving both aninkTTY app and a plain non-TTY line loop), a built-in default agent over[chat].default_modelfor a zero-config first run, durable per-turnhistory.dbpersistence that round-trips viareconstructSessionState, the ADR-0028 cost cap, and display-boundary sanitization of terminal control sequences;/exit//cancel/ an input EOF / raw-mode Ctrl-C end the session with exit code 4.read_mediainput access (D12) — which 2.S had pointed here — was split into a dedicated, security-reviewed follow-up (maintainer-approved); the REPL shipped without it (see ../deferred-tasks.md). 2.N–2.Q are ✅ Done (PR #55, no new ADR):chat-resume(2.N — reload + continue a persisted session over a shared REPL),chat-list(2.O — over a new additiveSessionStore.listSessionsread seam),chat-export+ the in-REPL/export(2.P — session→.relavium.yamlscaffold, ADR-0026), andchat --json(a headlessSessionEventNDJSON driver) + the one-shotagent run(with a minimal in-house--fixturecassette for deterministic offline replay) (2.Q) — completing the agent-first CLI lane.agent run --inputis reserved/rejected until session{{ctx.*}}prompt interpolation lands (a tracked engine follow-up).
Implement the inbound half of mcp-integration.md
— agents consuming MCP tools — on the official TypeScript SDK per
ADR-0034. Scheduled early (implementation
needs only 2.B config + 2.C keys; the end-to-end acceptance below additionally requires 2.F's
relavium run) but off the M3 critical path; the workflow-as-MCP-server (outbound) direction
is deliberately not in this slot.
✅ Shipped. The
@relavium/mcppackage fences@modelcontextprotocol/sdk+node:child_processbehind four SDK adapters (stdio/http/sse/websocket); the dependency-free JSON-Schema→Zod compiler, the fail-loud concurrent connect-all manager, and themcp_{server}_{tool}namespacing landed PR #56. The CLI host wiring (chat/run/agent run, with per-agent grant union + cross-agent dedup), the network transports behind the SSRF pre-connect floor + the per-serverallow_local_endpointopt-in (ADR-0053), named secrets via the isolatedmcp-secret:*keychain namespace (ADR-0052 §6), the by-namerefregistration form, and the deterministic real-spawn e2e landed PR #57. Residual hardening (connect-by-validated-IP dialer, network header-auth, tool-list caching, mid-call abort propagation) is tracked in deferred-tasks.md. The stdio spawn is no longer ungated: consent before a local spawn landed in 2.6.5CR-16(ADR-0084), which also replaced the "import-trust" framing — the gate covers every stdio server, not an imported one. Only thenpxversion/integrity pinning half remains deferred.
Tasks:
- Add the SDK dependency (version pinned in the pnpm
catalog:, recorded in tech-stack.md) per ADR-0034; SDK types stay confined to the MCP integration layer — discovered tools surface to theToolRegistryonly as Relavium/Zod tool definitions (the same confinement discipline as provider SDKs in@relavium/llm). - Resolve
[[mcp_servers]]registrations through the 2.B config loader (global → project merge per config-spec.md); spawn stdio servers / connect SSE/WebSocket servers, runtools/listdiscovery, and register tools under themcp_{server}_{tool}namespace per mcp-integration.md. - Inject server credentials from the keychain-backed secret seam (2.C) into the server
envat spawn time; construct the child environment explicitly (declaredenv+ a minimal base), never a blanket copy of the host environment; no secret in YAML, logs, or--jsonoutput. - Validate every MCP
urlthrough the one shared SSRF primitive (same range-block as provider base URLs andhttp_request, ADR-0029); honor the explicit local-endpoint opt-in. - Enforce tool policy: MCP tools enter an agent's surface only under the narrow-only rules of
ADR-0029; honor a per-server
tools_allowlist; schema-validate every call against the server-reported JSON Schema before dispatch.
Acceptance: a fixture agent declaring a stdio MCP server completes a real tool round-trip via
relavium run (and relavium chat), with the tool visible under its namespaced id; credentials
reach the server only via its spawn-time env; a private-range MCP url is rejected without the
explicit opt-in; the import-zone check confirms no SDK type leaks past the integration layer.
Status: ✅ Done (PR #52, 2026-06-25) — behind ADR-0042–ADR-0046 (no new ADR). Shipped: the
model_catalogreader →resolveMediaSurfacerouting + the D15 catalog load-check shared byrun/gate, the content-addressedMediaStorede-inline, the SSRF-validatedEgressCapability.fetchegress (with a dedicated security review), the containment-checkedsave_towrite port, durable fail-cost on the terminal events (ADR-0045 §5), the produced-media render surface, and the best-effort run-end host media GC (ADR-0042 §4: grace reclaim + CAS-orphan sweep + clean-terminal reclaim-retry).read_mediainput access (D12) was deferred to 2.M, then split into a dedicated, security-reviewed follow-up (maintainer-approved) — the 2.M chat REPL shipped without it; the deferred D8/D15/D17 mechanism half is discharged for the CLI surface (see ../deferred-tasks.md). The first additive lane.
The surface half of the multimodal sub-spine. The engine-pure media policy landed
in Phase 1 (1.AF/1.AG: the MediaStore/deInlineMedia choke point, read_media +
scope-set authz, the byte-delivery Range gate, the output_modalities load-check,
the per-modality cost governor, the save_to write port, the inline media-out path,
and the engine-owned async media-job LRO); 1.AH then landed the four generative
media-output adapters behind the @relavium/llm seam. None of it is reachable
end-to-end until a host wires it. The CLI is the first surface to do so, so this
is the canonical home for the host-wiring task list — desktop (phase-3-desktop.md)
and VS Code (phase-4-vscode.md) point here for the shared
obligations and add only their surface-specific deltas (the desktop Rust de-inline +
Rust CAS per ADR-0032).
Each task is the deferred mechanism/wiring half of a 1.AF/1.AG decision-point;
the canonical pointers are the media ADRs (ADR-0042–ADR-0046)
and the D-series entries in ../deferred-tasks.md (§1.AF P4 +
Multimodal forward-obligations).
Off the M3 critical path — built with the CLI but designed to fit
desktop/VS Code: each port lives behind an injectable seam (AgentRunnerDeps,
MediaReadAccess, the write port) so the desktop's Rust CAS and the VS Code host can
reuse it. The warning: do not let a CLI-shaped shortcut leak into a port the other
surfaces inherit (treat any such friction as a Phase-1 amendment, per Risks). Media
runs at 0 cost until verified generative pricing rows land (never fabricate a
rate); multi-image count > 1 and the OpenAI Responses-API image-gen surface stay
deferred (each needs a future seam amendment — see
deferred-tasks.md).
Tasks:
resolveMediaSurface(AgentRunnerDeps) — the top routing gate. Wire the host lookup that readsmodel_catalog.media_surfaceand suppliesresolveMediaSurface?(model) → MediaSurface, the discriminator that routes a generative-surface model togenerateMedia/pollMediaJob(default'chat'). Until wired, every model routes inline and no generative model is runtime-reachable. (ADR-0045 §1; deferred-tasks.md 1.AG Section C → 1.AH.)- The
read_mediahostMediaReadAccessfactory. Build the host factory that bridgesMediaReferenceStore.describe()+MediaStore.readRange()into theMediaReadAccessthe tool needs; it must return a handle-bearing result (durable form, resolved on egress by the seam), not inline base64, so the next LLM call'scontainsInlineMediaBytesrefine does not reject the turn — and narrowreadRangeto that chosen form (not the wideMediaSource) and threadAbortSignalLikeintodescribe/readRange. (ADR-0044 §1; deferred-tasks.md D12.) - Session-kind
media_referencespopulation (D12 authz data). Writesession(and, when a shared-asset consumer lands,workspace)media_referencesrows at the node/session input-transfer boundary sodescribe().allowedScopesis non-empty; today the only writer emitsrunrefs and every read denies. (ADR-0044 §1; deferred-tasks.md D12.) - Thread
ctx.mediaRead+requestingScopeintoToolDispatchContext. The AgentRunner + AgentSession build the dispatch context without these, soread_mediacurrently throwsToolUnavailableError(fail-closed). Inject theMediaReadAccessimpl and the requesting scope so the tool resolves in the engine path. (ADR-0044 §1; deferred-tasks.md D12.) validateWorkflowWithCatalogpost-parse (the D15 load-check). Call the exported, tested load-check from the CLI load path with the DBmodel_catalogso authoredoutput_modalitiesare validated at parse time, not only by the runtime FallbackChain pre-skip. (ADR-0045; deferred-tasks.md D15.)[defaults].media_cost_estimate→mediaCostEstimate(the D17 cost governor). Read the config key and thread it intocreateAgentNodeExecutorso the pre-egress governor uses the configured per-modality estimate instead of the built-inDEFAULT_MEDIA_UNIT_ESTIMATE. (ADR-0044 §3; deferred-tasks.md D17.)resolveForEgress(the D8 FallbackChain resolve-before-egress). Inject the re-materialization hook on the AgentRunner so a durable handle in a transcript message is resolved before egress; without it the D7/D8 failover re-materialization is inert (a handle is sent unchanged). (ADR-0043; deferred-tasks.md D8.)createFilesystemMediaWrite— thesave_towrite port. Implement the host filesystem write port withrealpath+commonpathfail-closed containment (no symlink escape) so an output node'ssave_tolands bytes in the workspace. (ADR-0044 §2.)save_torun.idnamespace. Scope the written artifact path under the run id namespace (so concurrent/repeat runs do not collide), and surface the load-timerun.id-only restriction so a non-run.idsave_toref errors at load, not runtime. (ADR-0044 §2; deferred-tasks.md §1.AF-P4 save_to.)- The
EgressCapability.fetchSSRF mechanism (DNS-resolve + connect-by-IP + redirect re-validate) — SECURITY-CRITICAL. Implement the host-side runtime half of the SSRF defense: resolve the hostname, validate the IP against the shared range block, pin the connection to that validated IP, and re-validate on every redirect hop. The shared 1.AE primitive covers only construction-time policy; this is the mechanism that catches DNS rebinding / a public hostname resolving to a private IP. Flag for explicit security review. (ADR-0043; deferred-tasks.md §Multimodal — host-side SSRF enforcement inEgressCapability.fetch.) - Render a produced media handle on
node:completed(CLI surface). Render the produced media handle (never inline bytes) in the TUI and--jsonpaths when a node emits media output — the CLI's leaf of the cross-surface "each surface renders a produced media handle" acceptance. (ADR-0046; deferred-tasks.md 1.AG.) [chat].max_turns→SessionDeps.maxTurns. Read the config turn cap and thread it into theAgentSessiondeps so a media (or any) chat turn budget is host-configurable. (config-spec.md; 2.M–2.Q.)- Host media GC + durable fail-cost. Run the host media garbage collection — the
CAS-orphan sweep (row-less
save_tobytes from a crash window) + the clean-terminal reclaim retry keyed on terminal run events — and snapshot durable realized cost ontonode:failed/run:cancelledfor a billed-but-failed paid media job (the livecost:updatedstream is the only carrier today). (ADR-0042 §4, ADR-0045 §5; deferred-tasks.md §1.AF-P4 GC + review M1.)
Acceptance: a fixture workflow with a generative media-output node runs end-to-end
via relavium run, routed by a host resolveMediaSurface to generateMedia /
pollMediaJob; read_media resolves a scoped handle (and denies an out-of-scope one)
with no inline bytes crossing into the next turn; save_to writes a containment-checked
artifact under the run.id namespace and rejects an escape attempt; the SSRF
mechanism rejects a hostname that resolves to a private IP (and re-validates on
redirect) — covered by a dedicated security review; the load-check rejects an
unsupported authored output_modalities; media cost folds at 0 until a verified
rate lands; and the CLI renders the produced media handle in both the TUI and --json
streams. Built behind injectable ports so desktop (§3.B) and VS Code (§4.N) reuse them.
| In-phase milestone | Completed by | Global milestone |
|---|---|---|
| CLI scaffold + config + keys resolve (engine can be constructed) | 2.A, 2.B, 2.C | — |
run drives the engine end-to-end with the live TUI |
2.D, 2.E | (toward M3) |
Stable --json CI mode + deterministic exit codes |
2.F | M3 |
Human-gate pause/resume across interactive and gate command |
2.G, 2.H | — |
Durable history powers list / logs / status |
2.H, 2.I | — |
Authoring lifecycle (create/import/export) ✅ (PR #58, 2026-06-28) — the agent-or-minimal-workflow create wizard, schema-validated import + project-global id uniqueness, and the re-serialize-from-AST share-safe export; the last in-phase workstream — Phase 2 complete |
2.J | — |
| CLI adopted as the engine regression harness | 2.D, 2.F, 2.K | M3 |
| Published, installable binary verified on all OSes | 2.L | — |
Agent-first CLI — relavium chat + session commands (resume / list / export / agent run / gate list): the first user-facing AgentSession surface, a committed build-phase-2 deliverable (off the M3 critical path and the Phase-3 go/no-go, completed in-phase — the agent-first headline is demonstrable here) |
2.M, 2.N, 2.O, 2.P, 2.Q | 2.M ✅ (PR #54); 2.N–2.Q ✅ (PR #55, 2026-06-26) |
MCP client live ✅ (PR #56 + #57, 2026-06-26/27) — a fixture agent completes a real stdio MCP tool round-trip behind the ToolRegistry (the real-spawn e2e), plus the http/sse/websocket network transports behind the SSRF floor + allow_local_endpoint, named secrets (mcp-secret:*), and the by-name ref form, per ADR-0034/ADR-0052/ADR-0053 (off the M3 critical path) |
2.R | — |
Media host-wiring ✅ (PR #52, 2026-06-25) — a generative media-output fixture runs end-to-end on the CLI (host resolveMediaSurface routing, containment-checked save_to, the EgressCapability.fetch SSRF mechanism; read_media input access split into a dedicated follow-up past 2.M), the shared ports designed to fit desktop/VS Code, per the media ADRs (ADR-0042–ADR-0046) (off the M3 critical path) |
2.S | — |
The Work breakdown above is the per-workstream logical DAG (each edge's rationale lives in its workstream prose); this section is the scheduling view — the order to build in, what blocks what, and what runs concurrently. It is the single home for the execution plan; the work-breakdown DAG and the Milestones table are its inputs, not duplicates. Unlike Phase 1 (three packages behind one shared gate), Phase 2 is one app: a short linear spine that reaches the global milestone M3, with run-surface feeders and three additive lanes running concurrently around it.
The status-aware pickup queue — given what's already merged, the order to build everything still open, top-to-bottom, one workstream per PR. Each row's blockers are satisfied by the time you reach it, so there is no need to re-derive "what next" each time. This is the status × plan view; the dependency rationale for every row lives in the Dependency matrix and the from-scratch wave plan in Ordered waves — this table does not restate them, it only sequences what remains.
Status (2026-06-28): ✅ 2.A · 2.B · 2.D · 2.F · 2.K · 2.H · 2.C · 2.E · 2.G · 2.I · 2.L · 2.S · 2.M · 2.N–2.Q · 2.R · 2.J done — every Phase-2 workstream is merged; the in-phase CLI is feature-complete and published as v0.1.1. M3 was reached at 2.K, the spine completed at 2.L (all 7 Phase-3 go/no-go criteria hold), the three additive lanes landed (2.S media PR #52, chat 2.M–2.Q PR #54/#55, 2.R inbound MCP PR #56/#57), and 2.J (the
create/import/exportauthoring lifecycle — the last in-phase lane) landed PR #58. The pickup queue below is now empty. (2.L shipped the published, install-verified binary (PR #49) — so go/no-go #7 holds and every Phase-2 spine/gate PR is done; 2.S then cleared the first additive lane (PR #52), the chat lane (2.M–2.Q) landed (PR #54/#55), and 2.R (the inbound MCP client) landed (PR #56/#57). 2.J (authoring lifecycle) then landed (PR #58), so every Phase-2 workstream is now complete — Phase 3 was already unblocked at 2.L.)
| Next | Lane | Why now | Blockers (all met on arrival) |
|---|---|---|---|
| ✅ 2.J create / import / export | feeder | Done (PR #58) — the last in-phase lane: agent/minimal-workflow create wizard, schema-validated import + project-global id uniqueness, re-serialize-from-AST share-safe export |
2.A ✓ |
| ✅ 2.R MCP client | additive | Done (PR #56, #57) — inbound MCP tools (chat/run/agent run), network transports + SSRF floor, named secrets, real-spawn e2e |
2.B ✓ · 2.C ✓ |
| ✅ 2.M → 2.N–2.Q chat | additive | Done (PR #54, #55) — the agent-first chat family (chat / resume / list / export / --json / agent run) |
2.C ✓ · 2.H ✓ · 2.E ✓ |
| ✅ 2.S media host-wiring | additive | Done (PR #52) — the first additive lane; the lone SSRF security review cleared | 2.D · 2.H ✓ |
- Gate-closing backbone — complete (
2.Llanded, PR #49): with the published, install-verified binary shipped, every exit-criteria/spine PR is done (2.K + 2.H + 2.C + 2.E + 2.G + 2.I + 2.L), so all seven Phase-3 go/no-go criteria hold and Phase 3 may start. 2.S then cleared the first additive lane (PR #52), the chat lane (2.M–2.Q) landed (PR #54/#55), 2.R (the inbound MCP client) landed (PR #56/#57), and 2.J (the authoring lifecycle — the last in-phase lane) landed (PR #58); every Phase-2 workstream is now complete (Phase 3 was already unblocked at 2.L). - 2.K is fully closed (via 2.G). Its deferred gate-resume scenario was exercised once the gate pause/resume surface shipped, which unblocked 2.L — now landed (PR #49).
- The one judgement call — 2.S timing — was honored. It landed as the first additive lane (PR #52,
2026-06-25), never tailed behind chat / MCP / filler. The long pole with the only dedicated security review
(the
EgressCapability.fetchSSRF mechanism) is cleared first, and its injectable ports are now ready for desktop (§3.B) + VS Code (§4.N) reuse.
- Spine —
2.A → 2.B → 2.D → 2.F → 2.K → 2.L(gates M3 and the Phase-3 go/no-go). The engine-proving path: scaffold → config → wirerunto the engine → the deterministic--jsonCI contract → the regression harness (M3) → the published binary. Keep it short and linear; everything else hangs off it. - Run-surface feeders (each opens the moment
2.Dlands, then runs in parallel).2.E(ink TUI) and2.H(durable history) both consume2.D's event bus;2.H → 2.I(list/logs/status);2.G(human gate) needs2.E+2.F(both render the prompt) and2.H(the out-of-bandrelavium gate <runId>resume loads persisted gate state).2.J(create/import/export) needs only2.A— start it early as filler.2.C(provider/keys) feeds live runs and the nightly live lane; it is not on the fixture M3 path (see the insight below). - Additive lane — MCP (
2.R). Scheduled early: its implementation needs only2.B+2.C; only its end-to-end acceptance additionally needs2.F'srelavium run. - Additive lane — agent-first chat (
2.M → 2.N/2.O/2.P/2.Q). The first user-facingAgentSessionsurface.2.M(the REPL) needs2.C(keys) +2.H(session persistence inhistory.db) + the ink/output infra shared with2.E; the rest hang off2.M. - Additive lane — media host-wiring (
2.S). The largest, security-gated lane (theEgressCapability.fetchSSRF mechanism carries a dedicated security review). It needs2.D+2.Hplus the Phase-1 media policy (already landed). **Open it as soon as2.D2.Hexist and run it in parallel** — do not save it for the end (see below).
All three additive lanes are committed Phase-2 deliverables but sit off the M3 critical path and off the 7-item Phase-3 go/no-go (Exit criteria): they complete within the phase, in parallel, and do not block starting Phase 3.
- The M3 spine is short and almost everything parallelizes around it. Wall-clock to M3
is
2.A → 2.B → 2.D → 2.F → 2.K; the feeders and additive lanes overlap it, so the phase is feeder-throughput-bound, not spine-bound. 2.Dis the keystone. It is the first real consumer of the Phase-1 engine, so it is where any engine-API friction surfaces. Treat every gap as a Phase-1 amendment (re-tested in@relavium/core/@relavium/llm), never a CLI workaround (Risks) — a workaround here becomes debt every later surface inherits.- M3 is fixture-based, so
2.Cis off it. The regression harness (2.K) and--jsonmode (2.F) run recorded LLM fixtures — deterministic and offline — so the M3 path needs no real keys.2.Cis a feeder that unlocks liveprovider test/ runs and the nightly live lane, which is why the critical path is{2.A, 2.B, 2.D, 2.F, 2.K}and not…2.C…. - Front-load
2.S, don't tail it. Media host-wiring is the biggest lane and carries the one security-review gate; deferred to the end it becomes the long pole and risks a CLI-shaped shortcut leaking into a port that desktop (§3.B) and VS Code (§4.N) inherit. Open it right after2.D+2.Hand let it run the whole phase.
flowchart LR
subgraph SPINE["Spine — gates M3 + Phase-3 go/no-go"]
direction TB
A["2.A skeleton"] --> B["2.B config"]
B --> D["2.D run → engine"]
D --> F["2.F --json / CI"]:::ms
F --> K["2.K regression harness"]:::ms
K --> L["2.L package & publish"]
end
subgraph FEED["Run-surface feeders (parallel, post-2.D)"]
direction TB
Cc["2.C provider / keys"]
E["2.E ink TUI"]
H["2.H db history"] --> I["2.I list / logs / status"]
G["2.G human gate"]
J["2.J create / import / export"]
end
subgraph ADD["Additive lanes (committed, off M3, parallel)"]
direction TB
R["2.R MCP client (early)"]
M["2.M chat REPL"] --> NQ["2.N–2.Q resume · list · export · json · agent-run"]
S["2.S media host-wiring (large · security-gated)"]
end
A --> J
B --> Cc
Cc -. live runs .-> D
D --> E
D --> H
E --> G
F --> G
H --> G
G --> K
B --> R
Cc --> R
F -. acceptance .-> R
Cc --> M
H --> M
E -. shared ink .-> M
D --> S
H --> S
classDef ms fill:#fde68a,stroke:#b45309;
| Wave | Goal (milestone) | Spine | Feeders (parallel) | Additive lanes (parallel) |
|---|---|---|---|---|
| W1 | scaffold + config | 2.A → 2.B | 2.J opens (needs only 2.A) | — |
| W2 | engine runs end-to-end | 2.D (keystone) | 2.C (keys) | 2.R impl opens (needs 2.B + 2.C) |
| W3 | M3 — CI contract + harness | 2.F → 2.K (M3) | 2.E ‖ 2.H → 2.I; 2.G (needs 2.E, 2.F, 2.H) → feeds 2.K | 2.R accept (at 2.F) · 2.M → 2.N–2.Q · 2.S (opens at 2.D + 2.H) |
| W4 | publish + phase exit | 2.L | — | finish 2.R · chat · 2.S |
◆ = M3 spine (the linear critical path); ⬤ = run-surface feeder (parallel, overlaps the
spine — most are Phase-3 go/no-go items); ◇ = additive lane (committed in-phase, off M3 and
off the go/no-go). The Phase-3 gate column names the go/no-go exit criterion
each workstream satisfies (— = committed but not a go/no-go gate).
| WS | Lane | Depends on | Enables | Phase-3 gate |
|---|---|---|---|---|
| 2.A | ◆ spine | Phase 1 (engine @ M2) | 2.B, 2.J | #1/#4 |
| 2.B | ◆ spine | 2.A | 2.C, 2.D, 2.R | #1/#4 |
| 2.C | ⬤ feeder | 2.B | 2.D (live), 2.R, 2.M | #5 (live keys; off the fixture M3 path) |
| 2.D | ◆ spine | 2.B (2.C for live) | 2.E, 2.F, 2.H, 2.S | #1 — keystone |
| 2.E | ⬤ feeder | 2.D | 2.G, 2.M | #1 |
| 2.F | ◆ spine | 2.D | 2.G, 2.K, 2.R (accept) | #4 |
| 2.G | ⬤ feeder | 2.E, 2.F, 2.H | 2.K | #3 |
| 2.H | ⬤ feeder | 2.D | 2.I, 2.G, 2.M, 2.S | #2 |
| 2.I | ⬤ feeder | 2.H | — | #2 |
| 2.J | ⬤ feeder | 2.A | — | — (committed) |
| 2.K | ◆ spine | 2.D, 2.F, 2.G | 2.L | #4 (M3) |
| 2.L | ◆ spine | 2.K | published binary | #7 |
| 2.M | ◇ chat | 2.C, 2.H, 2.E | 2.N–2.Q | — |
| 2.N–2.Q | ◇ chat | 2.M | — | — |
| 2.R | ◇ MCP | 2.B, 2.C (accept: 2.F) | — | — |
| 2.S | ◇ media | 2.D, 2.H (+ Phase-1 media policy) | desktop §3.B / VS Code §4.N reuse the ports | — |
- Phase 1 complete (phase-1-engine-and-llm.md):
@relavium/coreand@relavium/llmproven from the Node harness, with checkpoint/resume, retry, and provider fallback working (global M2). - Engine API contracts the CLI binds to:
WorkflowEngine.start(workflowId, input), the in-processRunEventBus/RunHandle.eventsiterator, andengine.resume(runId, gateId, GateDecision)(see shared-core-engine.md and execution-model.md). @relavium/dbproviding the local SQLite schema (runs,step_executions,run_events,run_costs,llm_providers) per database-schema.md.- Frozen reference contracts: the
CLI command surface, the
RunEventschema, the config spec, and the keychain/secret model. - Media engine policy (1.AF/1.AG) complete behind the
@relavium/llmseam — theMediaStore/deInlineMediachoke point,read_mediaauthz, thesave_towrite port, the load-check, the cost governor, and the async media-job LRO — per ADR-0042–ADR-0046; the host-wiring tasks (2.S) discharge the deferred mechanism half tracked in deferred-tasks.md (§1.AF P4 + Multimodal forward-obligations). @relavium/sharedZod schemas for workflow/agent/config validation (Phase 0).
All must be true to start Phase 3 (desktop):
- A 3-node sequential workflow runs end-to-end via
relavium run, with the liveinkTUI reflecting every canonical run event in order (2.D, 2.E). listandlogsread durable local run history;statusreflects active/paused runs; an unknownrunIdexits2(2.H, 2.I).- A human gate blocks the run, prompts interactively, and resumes on approval; the
non-interactive path exits with the gate-paused code
3and resumes viarelavium gate <runId> --approve, idempotently (2.G). --jsonmode emits a stable, documented NDJSONRunEventstream that a green, no-TTY CI job asserts on, with deterministic exit codes; the regression harness runs on engine changes (2.F, 2.K).- Provider keys are stored only in the OS keychain (or the documented headless
fallback); no full key ever appears in stdout, logs,
--json, or a config file (2.C). - The engine API needed no breaking change to support the CLI ergonomically — or any change was folded back into Phase 1 packages and re-tested (cross-phase).
npm i -g relaviumyields a working binary verified on macOS, Linux, and Windows (2.L).
| Risk | Impact | Mitigation |
|---|---|---|
| Engine API gaps surface here (first real consumer). | Could tempt CLI workarounds that mask engine debt. | Treat every gap as a Phase 1 amendment with full re-test (2.D); never paper over it in the CLI. |
TTY (ink) vs CI (--json) paths drift. |
Two behaviors for one run; CI asserts the wrong thing. | Both are renderers over the same RunEventBus; the harness (2.K) asserts the JSON event set equals the TUI's. |
| Human-gate suspend/resume in a terminal is fiddly. | Hung runs or double-applied decisions. | Model it as the same gate event + resume() contract the other surfaces use; rely on checkpoint idempotency (2.G). |
Secret leakage via stdout/logs/--json. |
API key exposure. | Key hints only; keys resolved at call time; sanitized persistence; export re-serializes from the validated AST — no provider key by construction, comments dropped (2.C, 2.H, 2.J). |
| No OS keychain in CI/headless hosts. | Cannot run in pipelines. | Documented env-var → secrets.enc fallback with a hard error on absence, no silent plaintext (2.C). |
| High token volume overwhelms the TUI. | Dropped events / janky render. | Throttle/batch with sequenceNumber gap-detection + resync; verify event count vs JSON stream (2.E). |
| Cross-OS install differences. | "Works on my machine" binary. | Multi-OS smoke tests of the published artifact before sign-off (2.L). |