feat(cli): 2.D — relavium run wired to the @relavium/core engine#41
Conversation
PR #40 merged: build-phase-2 workstreams 2.A (CLI skeleton + process contract) and 2.B (config resolution loader) are complete. - phase-2-cli.md: Status → In progress; §2.A and §2.B headings → ✅ Done (PR #40). - current.md: "What is active now" reflects 2.A/2.B landed + the next spine step (2.D → engine). - README.md / CLAUDE.md: high-level status refreshed (Phase 2 underway / in progress), still pointing at current.md as the canonical live-status home. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expose a single public factory that maps each ProviderId to its keyless @relavium/llm adapter (DeepSeek via the OpenAI-compatible adapter with its provider id). A host wires this into AgentRunnerDeps.resolveProvider and injects the API key per call via keyFor, so the provider->adapter mapping lives in the seam package rather than being re-derived in every surface (CLI, desktop, VS Code). No vendor SDK type crosses the seam -- the return is Record<ProviderId, LlmProvider>, and the seam fence stays airtight. First consumed by the CLI's `relavium run` (workstream 2.D). Refs: ADR-0011, ADR-0038 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The M3 keystone -- the first real consumer of the Phase-1 engine, and the first
consumption of the 2.B config loader and the @relavium/llm provider seam.
`relavium run <workflow>` now resolves the workflow (explicit path or an id/slug
under .relavium/workflows/), coerces and validates --input against the declared
inputs, builds the engine, drives its RunHandle.events stream through a renderer,
forwards SIGINT as a cooperative cancel, and maps the terminal event to a
deterministic exit code (0 completed / 1 failed|cancelled / 2 invalid invocation
/ 3 gate-paused).
Wiring (all injectable for tests + the 2.K harness):
- engine/host.ts: a real node-backed ExecutionHost (ISO wall clock, UUID ids per
ADR-0022, setTimeout timers, AbortController) over the in-memory run store
(durable history -> 2.H).
- engine/providers.ts: host-injected provider resolution (ADR-0038) over
@relavium/llm defaultProviders(); keys read per-call from
RELAVIUM_<PROVIDER>_API_KEY (OS keychain -> 2.C), never logged or stored.
- engine/build-engine.ts: the standard node executor + expression sandbox and a
fail-closed ToolHost ({}) -- capability wiring (fs/process/egress) deferred to
a security-reviewed workstream (deferred-tasks).
- render/renderer.ts: a RunRenderer seam with minimal plain + NDJSON renderers
(rich ink TUI -> 2.E, finalized --json envelope -> 2.F).
- commands/run.ts is framework-free; commands/specs.ts registers the real action.
Tests: real-engine e2e (input->transform->output to run:completed exit 0; a
runtime sandbox error to run:failed exit 1), input coercion/validation, workflow
resolution, and both renderers.
Docs: commands.md gains a `relavium run` implementation-status note; the
secret-in-prompt_template parse-rejection obligation assigned to 2.D was verified
already-satisfied by the 1.L2 taint analysis (collect.ts/analyze.ts/parser.ts +
analyze.test.ts) -- marked resolved; the fail-closed ToolHost recorded as a
security follow-up in deferred-tasks.
Refs: ADR-0036, ADR-0038, ADR-0022, ADR-0047
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three independent reviews of the 2.D `relavium run` work returned merge-ready with a few low/medium findings; an adversarial verify pass surfaced one more. This resolves them all: - Provider-key pre-flight (review L1): `relavium run` now pre-flights provider keys so a missing key is a clean exit 2 with the RELAVIUM_<PROVIDER>_API_KEY hint, rather than a swallowed mid-run failure (exit 1). Scoped to each referenced inline agent's PRIMARY provider only: `auth` is not in RETRYABLE_KINDS, so a missing primary key is fatal at the first attempt (the FallbackChain never fails over from it), making it guaranteed-needed; a fallback_chain or $ref agent's key is conditional and still surfaces at runtime, so the pre-flight is a strict subset that never false-fails a valid run. One resolver (reading the io.env seam) is shared by the pre-flight and the engine. The key value is only presence-checked -- never logged, stored, or rendered. - Exit-3 + cancellation coverage (review M1): added an e2e human_gate -> exit 3 test, a deterministic SIGINT -> run:cancelled -> exit 1 test (event-driven coordination + a generous ceiling, no real-timer polling), and a 25-run SIGINT-listener-hygiene test for the 2.K harness. Hardened run.ts so the SIGINT registration is adjacent to the try/finally that removes it (no leak window). - resolve.ts robustness (review L2): mirror the 2.B config loader's statSync-first discipline -- an existing-but-unreadable file (EACCES), a non-regular file, or one over a 2 MiB cap is now a clean exit 2, never silently mis-reported as "not found"; the size cap is enforced before the file is read. - Input coercion (review): reject an empty/whitespace `--input n=` for a number input instead of silently coercing it to 0; `n=0` stays valid. Docs: commands.md `relavium run` notes updated for the key pre-flight and SIGINT. Refs: ADR-0038 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewer's GuideWires Sequence diagram for relavium run wiring to the core enginesequenceDiagram
actor User
participant CliRun as run
participant RunCmd as runCommand
participant ProvRes as createProviderResolver
participant Engine as WorkflowEngine
participant Renderer as RunRenderer
User->>CliRun: run(argv, io)
CliRun->>CliRun: extractGlobalOptions
CliRun->>CliRun: resolveGlobalOptions
CliRun->>CliRun: buildProgram(io, context)
CliRun->>RunCmd: runCommand({workflow, input}, deps)
RunCmd->>RunCmd: loadResolvedConfig
RunCmd->>RunCmd: resolveWorkflowSource
RunCmd->>RunCmd: parseWorkflow
RunCmd->>RunCmd: parseInputArgs
RunCmd->>RunCmd: resolveInputs
RunCmd->>ProvRes: createProviderResolver
RunCmd->>RunCmd: neededProviderIds
loop primary providers
RunCmd->>ProvRes: keyFor
end
RunCmd->>Engine: buildEngine({ providers })
RunCmd->>Engine: start({ workflow, inputs })
activate Engine
RunCmd->>Renderer: createJsonRenderer / createPlainRenderer
loop handle.events
Engine-->>RunCmd: RunEvent
RunCmd->>Renderer: onEvent(event)
RunCmd->>RunCmd: nextOutcome
end
deactivate Engine
User-->>CliRun: Ctrl-C (SIGINT)
CliRun->>RunCmd: SIGINT handler
RunCmd->>Engine: handle.cancel
RunCmd-->>CliRun: ExitCode (0/1/2/3)
CliRun-->>User: process exit code
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughImplements the Changesrelavium run command (workstream 2.D)
Sequence Diagram(s)sequenceDiagram
participant User
participant run as run() entry
participant runCommand
participant resolveWorkflowSource
participant resolveInputs
participant neededProviderIds
participant createProviderResolver
participant buildEngine
participant WorkflowEngine
participant RunRenderer
User->>run: argv [run, workflow.yaml, --input k=v]
run->>run: resolveGlobalOptions
run->>runCommand: RunCommandArgs, RunCommandDeps
runCommand->>resolveWorkflowSource: workflowArg, {cwd, projectConfigDir}
resolveWorkflowSource-->>runCommand: {path, yaml}
runCommand->>resolveInputs: WorkflowDefinition, raw inputs
resolveInputs-->>runCommand: typed Record
runCommand->>neededProviderIds: WorkflowDefinition
neededProviderIds-->>runCommand: ProviderId[]
runCommand->>createProviderResolver: keyFor pre-flight per provider
createProviderResolver-->>runCommand: API key validated
runCommand->>buildEngine: {host?, providers?}
buildEngine-->>runCommand: WorkflowEngine
runCommand->>WorkflowEngine: start(def, inputs)
loop engine event stream
WorkflowEngine-->>runCommand: RunEvent
runCommand->>RunRenderer: onEvent(RunEvent)
RunRenderer-->>User: plain text or NDJSON line
end
runCommand-->>run: ExitCode
run-->>User: process exit
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- The workflow size cap in
resolveWorkflowSourceis hard-coded asMAX_WORKFLOW_BYTESmirroring the core parser’sMAX_SOURCE_CHARS; consider centralizing this limit (or importing from a shared module) so future changes to the parser’s cap don’t silently desync the CLI’s pre-read check.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The workflow size cap in `resolveWorkflowSource` is hard-coded as `MAX_WORKFLOW_BYTES` mirroring the core parser’s `MAX_SOURCE_CHARS`; consider centralizing this limit (or importing from a shared module) so future changes to the parser’s cap don’t silently desync the CLI’s pre-read check.
## Individual Comments
### Comment 1
<location path="apps/cli/src/commands/inputs.ts" line_range="74-80" />
<code_context>
+ return parsed;
+ }
+ case 'boolean': {
+ if (value === 'true' || value === '1') {
+ return true;
+ }
+ if (value === 'false' || value === '0') {
+ return false;
+ }
+ throw new CliError('invalid_invocation', `input '${name}' must be a boolean (true/false).`);
+ }
+ default:
</code_context>
<issue_to_address>
**nitpick:** Align the boolean error message with the accepted values (true/false and 1/0), or narrow the parser.
Right now the parser accepts both `true`/`false` and `1`/`0`, but the error text only mentions `true/false`. Please either update the message to match the accepted inputs or remove the numeric aliases so the diagnostics stay accurate.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Code Review
This pull request implements the core 'run' command ('relavium run') for the CLI (Workstream 2.D), integrating it with @relavium/core and @relavium/llm. Key additions include workflow path/ID resolution, input parsing and coercion, pre-flight provider key validation, a Node-backed execution host, event rendering (plain text and NDJSON), and cooperative cancellation via SIGINT. The feedback suggests a minor refactoring of the 'sleep' helper in 'build-engine.ts' to use a more concise, idiomatic one-liner promise syntax.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| sleep: (ms) => | ||
| new Promise((resolveSleep) => { | ||
| setTimeout(resolveSleep, ms); | ||
| }), |
The pre-parse character cap was a parser-local const. Export it so a host -- the CLI's resolveWorkflowSource pre-read byte guard -- can apply the SAME limit rather than hard-coding a copy that could silently desync from this authoritative cap. Refs: PR #41 review Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- resolve.ts: reuse @relavium/core's exported MAX_SOURCE_CHARS as the pre-read byte
ceiling instead of a hard-coded copy, so the CLI guard never desyncs from the
parser's authoritative cap (review: centralize the limit).
- inputs.ts: the boolean coercion accepts true/false AND 1/0, so the error message
now matches ("must be a boolean (true/false or 1/0)").
- build-engine.ts: collapse AgentRunnerDeps.sleep to the idiomatic one-liner.
- run.test.ts: throw TypeError (not Error) from the SIGINT-handler type-check guard
(Sonar — a failed type check should raise TypeError).
Refs: PR #41 review
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A final multi-dimensional review (9 dimensions, every finding adversarially verified) returned 0 blockers/high/medium; this folds in the confirmed low/nit findings: - inputs: reject a repeated `--input k=…` key (fail-fast, not silent last-wins); reject JS radix literals for a number input (`--input n=0x10` no longer silently becomes 16) while still accepting decimal/negative/scientific forms. - run: the run:paused→exit-3 mapping is correct for 2.D (a human gate is the only run:paused source — no media host is wired); tightened the comment and recorded the media-only-park revisit as a 2.S follow-up in deferred-tasks. - providers/commands.md: stop mis-attributing the RELAVIUM_<PROVIDER>_API_KEY name to config-spec.md (it homes the precedence layer, not the env-var name). - tests: add the symmetric pre-flight PASS case (a present key builds the engine, proving no false-fail), a non-anthropic (gemini) missing-key case, and the gate-terminal NDJSON line under --json; harden the SIGINT test to identify run.ts's handler by set-delta (not .at(-1)) and correct a once-listener comment; annotate the providers.test parse() return type. Toolchain green (lint/typecheck/test 110, build, format, seam fence, engine-deps); Leakwatch clean on the new code. Refs: PR #41 review Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ES2022 modernization — replace Object.prototype.hasOwnProperty.call(out, key) with the clearer Object.hasOwn(out, key) (Sonar es2022). Refs: PR #41 review Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
PR #41 merged `relavium run` wired to @relavium/core (2.D — the M3 keystone) plus the `defaultProviders()` seam registry and the exported `MAX_SOURCE_CHARS` cap. Flip the status surfaces now that it's on main (roadmap-done-after-merge): - phase-2-cli.md: 2.D heading → ✅ Done (PR #41); status line advances the spine pointer to 2.F and notes the 2.E (ink TUI) + 2.H (durable history) run-surface feeders now open. - current.md: record 2.D Done (PR #41); "Next on the spine" → 2.F then 2.K (M3). - CLAUDE.md / README.md: status paragraphs note `relavium run` has landed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>



What & why
Phase-2 workstream 2.D — the M3 keystone:
relavium runis wired to the Phase-1@relavium/coreengine. This is the first real engine consumer and the first consumption of the 2.B config loader and the@relavium/llmprovider seam, so it doubles as the engine's integration harness.relavium run <workflow>now: resolves the workflow (explicit path or an id/slug under.relavium/workflows/), coerces + validates--inputagainst the declared inputs, builds the engine, drivesRunHandle.eventsthrough a renderer, forwardsSIGINTas a cooperative cancel, and maps the terminal event to a deterministic exit code (0completed ·1failed/cancelled ·2invalid invocation ·3gate-paused).Refs: ADR-0036 (run-loop substrate), ADR-0038 (host-injected provider resolution), ADR-0022 (run UUID), ADR-0047 (CLI framework).
Commits
feat(llm): adddefaultProviders()— a keyless adapter registry centralizing the provider→adapter mapping (incl. DeepSeek via the OpenAI-compatible adapter) in the seam package. Public-API addition to@relavium/llm(realizes ADR-0011/0038; no new decision). No vendor type crosses the seam.feat(cli): therelavium runcore + node-backedExecutionHost, host-injected provider resolution, fail-closedToolHost, aRunRendererseam (minimal plain + NDJSON), and the engine wiring.fix(cli): review + adversarial-verify follow-up (see below).docs(roadmap): mark 2.A + 2.B Done (PR feat(cli): Phase 2 — 2.A CLI skeleton + 2.B config resolution (ADR-0047/0048) #40).Scope decisions (deferred, tracked)
RELAVIUM_<PROVIDER>_API_KEYenv keys now → OS keychain is 2.C.inkTUI is 2.E, the finalized--jsonenvelope is 2.F.ToolHost({}) — every capability-backed built-in tool is cleanly unavailable (typed failure), never an insecure stub. Capability wiring (fs/process/egress) is deferred to a security-reviewed workstream (recorded indeferred-tasks.md).human_gateexits3now; the interactive prompt +relavium gateresume are 2.G/2.H.Review → adversarial-verify follow-up (folded into
fix(cli))Three independent reviews returned merge-ready, no blockers; a follow-up adversarial-verify pass caught two more issues. All resolved:
RELAVIUM_<PROVIDER>_API_KEYhint before start, not a swallowed mid-run exit 1. Scoped to each inline agent's primary provider only:auth ∉ RETRYABLE_KINDS, so a missing primary key is fatal at attempt 1 (no failover) → guaranteed-needed; afallback_chain/$refkey is conditional and defers to runtime, so the pre-flight is a strict subset that never false-fails a valid run.human_gate→exit 3, a deterministicSIGINT→run:cancelled→exit 1 test (event-driven, no real-timer polling), and a 25-run SIGINT-listener-hygiene test for the 2.K harness; hardened the listener registration to be leak-free.resolve.tsrobustness — mirrors the 2.BstatSync-first discipline: EACCES / non-regular-file / over-2-MiB is a clean exit 2, never silently "not found"; size cap enforced before read.--input n=(empty) now errors instead of silently coercing to0;n=0stays valid.The verify pass independently re-confirmed: the seam holds (
agent.provider ≡ ProviderId, no cast), no secret is logged/stored/rendered (key presence-checked then discarded), engine purity, no new runtime dependency, and the exit-code mappings — via mutation testing + worker probing.Already-satisfied obligation (verified, not re-implemented)
The maintainer-decided "reject a
secret-typed input in an agentprompt_templateat parse" obligation assigned to 2.D was found already enforced by the 1.L2 taint analysis (collect.ts→analyzeSecretTaint→WorkflowSecretLeakError), with tests for the direct, transitive-via-context, and$refcases. 2.D maderelavium runthe first live consumer and confirmed it end-to-end. Marked resolved indeferred-tasks.md; no new code.Toolchain
pnpm run cigreen end-to-end: lint, typecheck (+ tools), 104 CLI tests (full monorepo green), build,format:check, the@relavium/llmseam fence, and engine-deps. Leakwatch clean on the new code (one pre-existing 2.B test false-positive, untouched here).🤖 Generated with Claude Code
Summary by Sourcery
Wire the
relavium runCLI command to the@relavium/coreengine with minimal plain and NDJSON renderers, deterministic exit codes, and SIGINT-driven cancellation, and introduce a keyless provider registry in@relavium/llmalongside updated roadmap and reference docs.New Features:
relavium runcommand that resolves workflows by path or id, validates and coerces--inputvalues, executes workflows via the core engine, and maps terminal outcomes to exit codes 0/1/2/3.RunRendererseam for lifecycle event streaming.@relavium/llminto the engine.ExecutionHostand fail-closedToolHostwiring for the CLI engine, using an in-memory run store and checkpointer.Enhancements:
runto set exit codes while keeping other commands as stubs.runboundary so command outcomes drive the process exit code via a shared result holder.--inputhandling with stricter parsing and type coercion semantics that reject unknown inputs, invalid values, and missing required inputs.Documentation:
relavium runwith details on input coercion, SIGINT cancellation, provider key pre-flight, gate-paused semantics, and current implementation status.ToolHostand its planned security-reviewed capability wiring.Tests:
relavium runcommand covering happy-path execution, NDJSON output, failure mapping, gate pauses, SIGINT-driven cancellation, and SIGINT-listener hygiene.runis implemented.Summary by CodeRabbit
Release Notes
relavium runwith repeatable--input k=v(duplicate/unknown/missing/invalid-value detection), plain output, and--jsonNDJSON rendering.relavium runcommand reference for inputs, SIGINT, and human-gate behavior.defaultProvidersandMAX_SOURCE_CHARSfor external use.