diff --git a/README.md b/README.md index 36fb92ae..4c258ab8 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,31 @@ machine unless you choose it. - **Per-node cost waterfall** — token and dollar attribution per node, per model. - **Local-first, zero-install posture** — BYOK, OS keychain, no sign-up in Phase 1. +## Getting started + +The CLI is the first usable surface. It ships as a single npm binary — `npm install -g relavium` +(the public npm publish is the final maintainer step of the **v0.1.1** release; until it lands, build from +source per [local dev setup](docs/runbooks/local-dev-setup.md)). Then **start as an agent → ship the workflow +→ own every run**: + +```bash +# 1. Point Relavium at a provider — your key goes to the OS keychain, never a file +relavium provider add anthropic +echo "$ANTHROPIC_API_KEY" | relavium provider set-key anthropic # the key is read from stdin, never argv + +# 2. Start as an agent — a multi-turn session in your terminal +relavium chat +# …converse until a flow proves itself, then run /export inside the REPL +# to ship the session to a git-committable .relavium.yaml + +# 3. Own every run — execute the workflow and stream every event (CI-friendly with --json) +relavium run ./my-workflow.relavium.yaml --json +``` + +Prefer to author directly? `relavium create` scaffolds an agent or a minimal single-agent workflow, and +`relavium import` / `relavium export` move them between projects. The full surface is the +[CLI command reference](docs/reference/cli/commands.md). + ## Architecture ```mermaid @@ -92,7 +117,7 @@ One engine, three modes behind the one `LLMProvider` seam: **Phase 1 — Engine and LLM is complete** (2026-06-21): the engine runs end-to-end on local-first BYOK — workflow parsing, DAG execution, live streaming, checkpoint/resume, multi-provider failover, cost governance, and multimodal media I/O. **Phase 2 (the CLI) is -underway** — the CLI skeleton, config resolution, `relavium run` (wired to the engine), its +feature-complete** — the CLI skeleton, config resolution, `relavium run` (wired to the engine), its `--json` CI machine-output contract, the engine regression harness, durable local run history, the provider/key commands (API keys in the OS keychain), the live `ink` streaming TUI, the human-gate prompt + out-of-band `relavium gate` resume, the read commands (`list` / `logs` / `status` / `gate list`) @@ -104,7 +129,10 @@ go/no-go exit criteria now hold). The first additive lanes have since landed too one-shot `agent run` with deterministic offline `--fixture` replay (the first user-facing `AgentSession` surface); and the **inbound MCP client** — agents consume external MCP servers' tools over stdio + the `http`/`sse`/`websocket` network transports (behind an SSRF floor and isolated keychain-resolved secrets), -proven by a real-spawn end-to-end test. For live status and the full roadmap, see +proven by a real-spawn end-to-end test; and the **YAML-authoring lifecycle** — `relavium create` (a wizard +scaffolding an agent or a minimal single-agent workflow), `import`, and a share-safe `export` (re-serialized +from the validated AST, no provider key by construction). With every in-phase workstream merged, the CLI is +cut as **v0.1.1** (the public npm publish is the pending final maintainer step). For live status and the full roadmap, see [docs/roadmap/current.md](docs/roadmap/current.md) and the [roadmap](docs/roadmap/README.md). diff --git a/apps/cli/CHANGELOG.md b/apps/cli/CHANGELOG.md new file mode 100644 index 00000000..d693dc11 --- /dev/null +++ b/apps/cli/CHANGELOG.md @@ -0,0 +1,76 @@ +# Changelog + +All notable changes to the `relavium` CLI are documented here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this project is pre-1.0, so +minor and patch bumps both land additively under a `0.1.x` line. The publish flow is +[release-a-surface.md](../../docs/runbooks/release-a-surface.md) (tag `v` → +the `Release CLI` workflow: pack → cross-OS smoke → publish). + +## [0.1.1] — 2026-06-28 + +Everything merged since the `v0.1.0` spine — the three additive lanes and the authoring +lifecycle — with the in-phase CLI now feature-complete (Phase 2 workstreams 2.A–2.S). + +### Added + +- **Authoring lifecycle (`create` / `import` / `export`)** (2.J, PR #58). `relavium create` + is a `@clack/prompts` wizard that scaffolds either an **agent** (`.agent.yaml`) or a + **minimal single-agent workflow** (`input → agent → output`, `.relavium.yaml`), validated + against the kind-appropriate `@relavium/shared` schema before write. `relavium import ` + copies an external workflow/agent into the project after schema validation; `relavium + export ` writes a portable, share-safe copy **re-serialized from the validated AST** + (canonical, comment-free; no provider key by construction). Ids are unique **project-globally** + across both catalogs (a same-kind clash needs `--force`; a cross-kind clash is always + rejected), so a bare `` stays unambiguous for `export` / `run`. +- **Agent-first chat family** (2.M–2.Q, PR #54/#55). `relavium chat` — an interactive + multi-turn REPL over `AgentSession` with streaming tokens, tool-call annotations, and the + FS-scope tier + command allowlist honored (`/exit` returns exit code `4`); `relavium + chat-resume `, `relavium chat-list`, and `relavium chat-export` (+ the in-REPL + `/export`) to ship a session to a `.relavium.yaml` scaffold; a headless `relavium chat --json` + `SessionEvent` stream; and a one-shot `relavium agent run ` with a `--fixture` cassette + for deterministic offline replay. +- **Inbound MCP client** (2.R, PR #56/#57). Agents consume external MCP servers' tools across + `chat`, `run`, and `agent run` via the SDK-fenced `@relavium/mcp` package — `stdio` plus the + `http` / `sse` / `websocket` network transports behind an SSRF pre-connect floor + a per-server + `allow_local_endpoint` opt-in, with server credentials resolved from an isolated `mcp-secret:*` + keychain namespace and injected only into the spawn-time `env`. Discovered tools surface under + the `mcp_{server}_{tool}` namespace; a real-spawn end-to-end test proves the round-trip. +- **Media host-wiring** (2.S, PR #52). A generative media-output fixture runs end-to-end on the + CLI — host `resolveMediaSurface` routing, content-addressed `MediaStore` de-inline to a + `media://` handle, the SSRF-validated `EgressCapability.fetch` egress, a containment-checked + `save_to` write port, and the produced-media render surface in both the TUI and `--json` paths. + +### Changed + +- `relavium export` / `import` re-serialize from the validated AST: the canonical form drops + authored comments and emits cwd-relative paths in both human and `--json` output (no absolute + filesystem path in any message or record). +- The `create` wizard requires an interactive terminal on **both** ends (TTY stdout + TTY stdin); + it fails loud (exit `2`) under `--json` or a piped stream rather than hanging. + +## [0.1.0] — 2026-06-24 + +The first published CLI — the engine-proving spine and the run/read surface. + +### Added + +- The `commander.js` CLI skeleton + process contract (output-mode detection, the deterministic + exit-code map, global flags) (2.A) and two-level config resolution (`~/.relavium/` → project + `.relavium/`) (2.B). +- `relavium run` wired to `@relavium/core` — workflow resolution, typed `--input` coercion, the + live event stream, SIGINT→cancel, and the terminal-event→exit-code mapping (2.D). +- The `--json` CI machine-output contract: one `RunEvent` per line (NDJSON), pure stdout, + diagnostics on stderr, deterministic exit codes (2.F). +- The `ink` streaming TUI — live per-node status, the active node's token stream, and a running + cost/duration footer over the same event bus as `--json` (2.E). +- The interactive human-gate prompt + the out-of-band `relavium gate ` cross-process + resume (2.G). +- Durable local run history via `@relavium/db`, powering the read commands `list` / `logs` / + `status` / `gate list` (2.H, 2.I). +- `relavium provider` commands storing API keys in the OS keychain via `@napi-rs/keyring`, with a + `RELAVIUM__API_KEY` env-var fallback for headless/CI hosts (2.C). +- The engine regression harness adopted as the CI gate (2.K), and packaging + cross-OS install + verification — the `tsup` engine-inlined ESM bundle published as `npm i -g relavium` (2.L). + +[0.1.1]: https://github.com/HodeTech/Relavium/releases/tag/v0.1.1 +[0.1.0]: https://github.com/HodeTech/Relavium/releases/tag/v0.1.0 diff --git a/apps/cli/README.md b/apps/cli/README.md index 466de519..b48cda7d 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -1,11 +1,17 @@ # relavium -> Run agent workflows from your terminal — a product of [HodeTech](https://github.com/HodeTech). +![Node](https://img.shields.io/badge/node-%E2%89%A5%2020.12-339933?logo=nodedotjs&logoColor=white) +![Local-first](https://img.shields.io/badge/local--first-BYOK-0aa) +![License](https://img.shields.io/badge/license-proprietary-555) -`relavium` is the command-line surface of the [Relavium](https://github.com/HodeTech/Relavium) -local-first AI agent platform. It runs git-committable `.relavium.yaml` workflows on the same -pure-TypeScript engine as the desktop and VS Code surfaces — every step debuggable, every token and -dollar tracked, nothing leaving your machine unless you choose it. +> **Start as an agent. Ship the workflow. Own every run.** + +`relavium` is the command-line surface of **[Relavium](https://github.com/HodeTech/Relavium)** — a +local-first, multi-model AI agent platform from **[HodeTech](https://github.com/HodeTech)**. Begin in a +conversational **agent session** in your terminal, graduate it into a git-committable, multi-agent +`.relavium.yaml` **workflow**, and run it anywhere — all on the same pure-TypeScript engine that powers +the Relavium desktop and VS Code surfaces. Every step is debuggable, every token and dollar tracked, and +nothing leaves your machine unless you choose it. ## Install @@ -13,42 +19,118 @@ dollar tracked, nothing leaving your machine unless you choose it. npm install -g relavium ``` -Requires **Node.js ≥ 20.12**. The package ships an engine-inlined bundle and installs prebuilt native -binaries, so no C/C++ toolchain is needed. +Requires **Node.js ≥ 20.12**. The package is an engine-inlined bundle that installs prebuilt native +binaries — no C/C++ toolchain, no Python sidecar. ## Quick start ```bash -# run a workflow, streaming live progress in the terminal -relavium run ./workflows/code-review.relavium.yaml --input file=./src/index.ts +# 1 · Point Relavium at a provider — your key goes to the OS keychain, never a file or argv +relavium provider add anthropic +echo "$ANTHROPIC_API_KEY" | relavium provider set-key anthropic -# CI / scripting: a stable NDJSON RunEvent stream, deterministic exit codes -relavium run ./workflows/code-review.relavium.yaml --input file=src/index.ts --json +# 2 · Start as an agent — a streaming, multi-turn session in your terminal +relavium chat +# …converse until a flow proves itself, then run /export inside the REPL +# to ship the session to a git-committable .relavium.yaml -# store a provider key in the OS keychain (read from stdin, never argv) -echo "$ANTHROPIC_API_KEY" | relavium provider set-key anthropic +# 3 · Own every run — execute the workflow, streaming live (or --json for CI) +relavium run ./my-workflow.relavium.yaml --input file=./src/index.ts +relavium run ./my-workflow.relavium.yaml --json ``` +Prefer authoring directly? `relavium create` scaffolds an agent or a minimal single-agent workflow from +an interactive wizard. + +## Why relavium + +- **One engine, every surface.** The terminal, desktop, and VS Code run the _identical_ engine — a + workflow behaves the same on your laptop and in CI. +- **A chat-to-workflow continuum.** Sessions are persistent and resumable; one command — `/export` — + turns a proven conversation into a reviewed, committed workflow. +- **Multi-model with fallback chains.** Route across Anthropic, OpenAI / DeepSeek, and Gemini behind one + seam; a run survives a provider outage by failing over (`[claude → gpt-4o → gemini]`). +- **Local-first, keys in your OS keychain.** BYOK, no account, no telemetry — keys never touch a file, a + log, an argv, or `--json` output. +- **Live _and_ scriptable.** A rich streaming TUI on a TTY; a stable NDJSON `RunEvent` stream with + deterministic exit codes under `--json` for CI. +- **Extensible and multimodal.** Agents consume external **MCP** tools (stdio + `http` / `sse` / + `websocket`, behind an SSRF floor), and workflows generate image / audio / video **output** through the + same engine (media input via `read_media` is a tracked follow-up). + ## Commands -| Command | Purpose | -|---|---| -| `relavium run [--input k=v]` | Execute a workflow; streams progress (or `--json` NDJSON). | -| `relavium list [--agents]` | List discovered workflows (or agents) with last-run status. | -| `relavium logs ` | Replay a past run's event stream. | -| `relavium status` | Show active/paused runs and their per-node status. | -| `relavium gate --approve\|--reject\|--input …` | Resolve a pending human gate. | -| `relavium gate list []` | List pending human gates. | -| `relavium provider ` | Manage providers + API keys (OS keychain). | +### Agent sessions + +| Command | Purpose | +| --------------------------------------------- | ----------------------------------------------------------- | +| `relavium chat [--agent ]` | Start an interactive multi-turn agent session (the REPL). | +| `relavium chat-resume ` | Reload and continue a persisted session. | +| `relavium chat-list` | List past sessions (id, agent, last activity). | +| `relavium chat-export ` | Export a session to a `.relavium.yaml` workflow scaffold. | +| `relavium agent run [--fixture

]` | Run a single agent one-shot (prompt on **stdin**); `--fixture` replays a cassette (offline, CI-ready). | + +### Workflows & authoring + +| Command | Purpose | +| ------------------------------------ | ----------------------------------------------------------------- | +| `relavium run [--input k=v]` | Execute a workflow — live TUI, or `--json` NDJSON for CI. | +| `relavium create` | Scaffold a new agent or a minimal workflow from a wizard. | +| `relavium import ` | Import an external `.relavium.yaml` / `.agent.yaml` into the project. | +| `relavium export ` | Write a portable, share-safe copy (no secret material). | + +### History & human gates + +| Command | Purpose | +| ---------------------------------------------------- | ---------------------------------------------------- | +| `relavium list [--agents]` | List discovered workflows (or agents) + last-run status. | +| `relavium logs ` | Replay a past run's event stream (raw under `--json`). | +| `relavium status` | Show active / paused runs and their per-node status. | +| `relavium gate --approve\|--reject\|--input …` | Resolve a pending human gate. | +| `relavium gate list []` | List pending human gates across runs. | + +### Providers & keys + +| Command | Purpose | +| -------------------------------- | ---------------------------------------------------------- | +| `relavium provider list` | Registered providers and whether a key is set. | +| `relavium provider add ` | Register a provider. | +| `relavium provider set-key ` | Store a key in the OS keychain (read from **stdin**). | +| `relavium provider remove-key ` | Delete a key from the keychain. | +| `relavium provider test ` | Verify a key with a minimal live request. | + +The global flags `--json`, `--cwd`, `--config`, `--no-color`, `--verbose` (`-v`), and `--quiet` (`-q`) +apply throughout. Run `relavium --help` or `relavium --help` for the full surface. + +## Exit codes + +Deterministic, for CI: + +| Code | Meaning | +| ---- | --------------------------------------------------------------------- | +| `0` | Completed successfully | +| `1` | Workflow failed (a node errored and exhausted retries / fallbacks) | +| `2` | Invalid invocation (bad arguments, not found, schema error) | +| `3` | Run paused at a human gate (non-interactive) — resume with `relavium gate` | +| `4` | A `relavium chat` / `chat-resume` session ended (`/exit`, `/cancel`, or input EOF) | + +For a workflow run or agent session, `--json` makes stdout a pure NDJSON stream — `RunEvent`s for `run` / +`gate`, `SessionEvent`s for `chat` / `agent run` — with all diagnostics on stderr. (`--help`, `--version`, +and a bare invocation stay human-readable on stdout.) + +## Keys & configuration -**Exit codes** (CI-friendly): `0` completed · `1` failed · `2` invalid invocation · `3` paused at a -human gate. Provider keys resolve from the OS keychain → `RELAVIUM__API_KEY` env var → error. +Provider keys resolve **OS keychain → `RELAVIUM__API_KEY` env var → error** — no plaintext +fallback, and only a 4-character hint is ever displayed. Configuration layers from `~/.relavium/` +(global) and a project `.relavium/`, with CLI flags and env vars overriding. Run history persists locally +to `~/.relavium/history.db`. ## Documentation -The full command reference, the `--json` machine contract, and the CI guide live in the -[Relavium docs](https://github.com/HodeTech/Relavium/tree/main/docs/reference/cli/commands.md). +The full command reference, the `--json` machine-output contract, and the CI guide live in the Relavium +docs: **[docs/reference/cli/commands.md](https://github.com/HodeTech/Relavium/blob/main/docs/reference/cli/commands.md)**. ## License -Proprietary — © HodeTech, all rights reserved. See [LICENSE](https://github.com/HodeTech/Relavium/blob/main/LICENSE). +Proprietary — © HodeTech, all rights reserved. Not open source; no rights are granted except as expressly +stated. See **[LICENSE](https://github.com/HodeTech/Relavium/blob/main/LICENSE)**. diff --git a/apps/cli/package.json b/apps/cli/package.json index e0307fe1..c9746c9f 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,7 +1,7 @@ { "name": "relavium", - "version": "0.1.0", - "description": "Relavium CLI (`relavium`) — run agent workflows from the terminal; the engine's first real consumer.", + "version": "0.1.1", + "description": "The local-first Relavium CLI — run multi-model AI agent sessions and git-committable workflows from your terminal.", "license": "SEE LICENSE IN LICENSE", "type": "module", "bin": { @@ -9,15 +9,24 @@ }, "files": [ "dist", - "drizzle" + "drizzle", + "CHANGELOG.md" ], "keywords": [ "relavium", "ai", "agent", + "ai-agent", + "multi-agent", "workflow", "cli", - "llm" + "llm", + "local-first", + "mcp", + "anthropic", + "openai", + "gemini", + "deepseek" ], "repository": { "type": "git", diff --git a/docs/decisions/0024-agent-first-entry-point-agentsession.md b/docs/decisions/0024-agent-first-entry-point-agentsession.md index ab675069..98469e11 100644 --- a/docs/decisions/0024-agent-first-entry-point-agentsession.md +++ b/docs/decisions/0024-agent-first-entry-point-agentsession.md @@ -2,7 +2,13 @@ - **Status**: Accepted - **Date**: 2026-06-05 -- **Related**: [0003-pure-ts-engine-not-langgraph-python.md](0003-pure-ts-engine-not-langgraph-python.md), [0005-sqlite-drizzle-local-postgres-cloud.md](0005-sqlite-drizzle-local-postgres-cloud.md), [0008-local-first-phase-1-cloud-phase-2.md](0008-local-first-phase-1-cloud-phase-2.md), [0009-git-native-workflow-yaml.md](0009-git-native-workflow-yaml.md), [0011-internal-llm-abstraction.md](0011-internal-llm-abstraction.md), [0018-desktop-execution-and-rust-egress.md](0018-desktop-execution-and-rust-egress.md), [0025-agent-surface-refines-desktop-scope.md](0025-agent-surface-refines-desktop-scope.md), [0026-session-export-to-workflow.md](0026-session-export-to-workflow.md), [../reference/contracts/agent-session-spec.md](../reference/contracts/agent-session-spec.md), [../reference/contracts/sse-event-schema.md](../reference/contracts/sse-event-schema.md), [../reference/desktop/database-schema.md](../reference/desktop/database-schema.md) +- **Related**: [0003-pure-ts-engine-not-langgraph-python.md](0003-pure-ts-engine-not-langgraph-python.md), [0005-sqlite-drizzle-local-postgres-cloud.md](0005-sqlite-drizzle-local-postgres-cloud.md), [0008-local-first-phase-1-cloud-phase-2.md](0008-local-first-phase-1-cloud-phase-2.md), [0009-git-native-workflow-yaml.md](0009-git-native-workflow-yaml.md), [0011-internal-llm-abstraction.md](0011-internal-llm-abstraction.md), [0018-desktop-execution-and-rust-egress.md](0018-desktop-execution-and-rust-egress.md), [0025-agent-surface-refines-desktop-scope.md](0025-agent-surface-refines-desktop-scope.md), [0026-session-export-to-workflow.md](0026-session-export-to-workflow.md), [../reference/contracts/agent-session-spec.md](../reference/contracts/agent-session-spec.md), [../reference/contracts/sse-event-schema.md](../reference/contracts/sse-event-schema.md), [../reference/desktop/database-schema.md](../reference/desktop/database-schema.md), [0050-cli-history-db-at-rest-posture.md](0050-cli-history-db-at-rest-posture.md) + +> Amended 2026-06-28: the `history.db` is **not** encrypted at rest on the **CLI** surface — it is +> guarded by `0700`/`0600` OS file permissions with API keys in the keychain only (see +> [ADR-0050](0050-cli-history-db-at-rest-posture.md)). Only the **desktop** surface uses a SQLCipher-encrypted +> store. The Context below originally said "encrypted" without that surface distinction; read it as +> surface-specific. ## Context @@ -28,8 +34,9 @@ engine before any engine code exists — the cheapest possible moment. the event bus — rather than a parallel implementation; - emits on a **`session:*` event namespace**, disjoint from the workflow `run:*` namespace, on the same bus (the canonical event home is [sse-event-schema.md](../reference/contracts/sse-event-schema.md)); -- **auto-persists and is resumable** — sessions and their messages live in the existing encrypted - `history.db` (new `agent_sessions` + `session_messages` tables; see +- **auto-persists and is resumable** — sessions and their messages live in the existing + `history.db` (CLI: unencrypted at rest, `0700`/`0600`-guarded per [ADR-0050](0050-cli-history-db-at-rest-posture.md); + desktop: SQLCipher-encrypted) (new `agent_sessions` + `session_messages` tables; see [database-schema.md](../reference/desktop/database-schema.md)); - binds **one agent (and its `fallback_chain`) per session** in Phase 1 — no mid-session agent switching; multi-agent orchestration remains a workflow concern; diff --git a/docs/decisions/0054-cli-bare-invocation-interactive-home.md b/docs/decisions/0054-cli-bare-invocation-interactive-home.md new file mode 100644 index 00000000..3323ca2f --- /dev/null +++ b/docs/decisions/0054-cli-bare-invocation-interactive-home.md @@ -0,0 +1,66 @@ +# ADR-0054: Bare `relavium` invocation opens an interactive Home (TTY only), preserving the meta-op contract + +- **Status**: Proposed +- **Date**: 2026-06-28 +- **Related**: [ADR-0049](0049-cli-machine-output-contract.md), [ADR-0047](0047-cli-framework-commander-ink-clack.md), [ADR-0024](0024-agent-first-entry-point-agentsession.md), [ADR-0007](0007-desktop-is-not-an-ide.md), [ADR-0025](0025-agent-surface-refines-desktop-scope.md), [phase-2.5-cli-consolidation.md](../roadmap/phases/phase-2.5-cli-consolidation.md) (2.5.B), [product-constraints.md](../product-constraints.md), [architectural-principles.md](../standards/architectural-principles.md) + +> **Draft.** Proposed alongside the Phase 2.5 plan; to be reviewed and finalized (→ Accepted) when workstream 2.5.B begins. + +## Context + +Today a bare `relavium` (no subcommand) prints `program.helpInformation()` and exits `0` +(`apps/cli/src/run.ts`), and `commander` deliberately has **no default action** +(`apps/cli/src/program.ts`) — a default action would turn an unknown subcommand into a positional +argument and swallow the "unknown command" error. Every terminal-native agent CLI we surveyed +(Claude Code, Codex, opencode, Google's Antigravity `agy`) instead opens an interactive session on the +bare command; the project's agent-first stance ([ADR-0024](0024-agent-first-entry-point-agentsession.md)) +wants the same "type the name → start talking" entry. But the CLI is also a CI/automation tool: the +`--json` NDJSON stream and the meta-op behaviour (help, version) are a binding contract +([ADR-0049](0049-cli-machine-output-contract.md)), and breaking it would strand scripts and pipelines. +Getting this wrong either leaves the CLI feeling second-class (today) or breaks CI silently. + +## Decision + +**We will open a branded, conversation-first Home from the bare `relavium` invocation, but only when +the process is genuinely interactive.** The gate is `stdoutIsTty && stdinIsTty && global.json !== true +&& !isCiEnv(io.env)`; otherwise the current `helpInformation()` + exit `0` meta-op is preserved. +The primary control is `stdoutIsTty && stdinIsTty`; the CI guard reuses the **existing `isCiEnv` helper** +(`apps/cli/src/process/output-mode.ts`) — which treats `CI=true`/`CI=1`/any truthy `CI` as CI — rather +than a bare `env.CI !== 'true'` test, so a CI runner that sets `CI=1` (some Drone/Woodpecker/custom setups) +or allocates a pseudo-TTY cannot accidentally open an interactive Home and stall the pipeline. (Earlier +text used `env.CI !== 'true'`, which would miss `CI=1`.) The remainder reads as preserved +unchanged. The gate lives in the existing bare-invocation branch of `run.ts` — **not** as a `commander` +default action, so the no-default-action decision and the unknown-command semantics stand. The gate +**reuses** the existing `stdinIsTty` field on the `io` seam (already wired for the `create` wizard) — no +new IO surface, just a new TTY-gate condition. The Home is a long-lived process mode whose own exit code +is `0`; a chat launched from within it ends with the chat exit code `4`, which the Home loop **consumes** +(never leaks). The Home's contract is canonically homed in a new `docs/reference/cli/home.md` (authored in +this phase, 2.5.B). + +Considered a `commander` default action (rejected: it swallows unknown-command errors — the exact +reason `program.ts` avoids one); a separate `relavium home` subcommand (rejected: it does not meet the +"type `relavium` → Home" goal — the bare invocation is the requirement); and always opening the Home +regardless of TTY (rejected: it breaks pipes, CI, and the `--json` contract). Chose the TTY-gated +bare-branch because it adds the first-class entry while keeping every non-interactive path byte-for-byte +compatible. + +The Home applies the **no-IDE-shell principle** of [ADR-0007](0007-desktop-is-not-an-ide.md) (which ADR-0007 +frames for desktop, generalized by [ADR-0025](0025-agent-surface-refines-desktop-scope.md)) to the CLI **by +analogy**: management + chat only, a **read-only display** status strip with the interactive prompt below it +— no file tree, editor, or embedded terminal. + +## Consequences + +### Positive + +- A first-class "type `relavium` → start" entry, matching user expectation and the agent-first stance. +- Fully backward-compatible: `--json`, pipes, `CI=true`, and every subcommand behave exactly as before. +- The extension point is one isolated branch in `run.ts`; the engine and `commander` wiring are untouched. + +### Negative + +- A new long-lived interactive process mode with its own lifecycle (the `stdinIsTty` seam already exists) — + mitigated by a single ink tree (one `useInput` owner) and a single SIGINT/SIGTERM lifecycle covering + Home, the in-Home chat, and MCP teardown. +- The Home's exit-code semantics (Home `0`, consumed chat `4`) add a small mapping the regression + harness must assert; covered by the Phase 2.5 backward-compatibility exit criterion. diff --git a/docs/decisions/0055-cli-host-capability-seam-tool-environment-factory.md b/docs/decisions/0055-cli-host-capability-seam-tool-environment-factory.md new file mode 100644 index 00000000..35d4f03b --- /dev/null +++ b/docs/decisions/0055-cli-host-capability-seam-tool-environment-factory.md @@ -0,0 +1,86 @@ +# ADR-0055: Shared CLI tool-environment factory — `ToolHost`, `ToolPolicy`, and dispatch context as separate channels + +- **Status**: Proposed +- **Date**: 2026-06-28 +- **Related**: [ADR-0029](0029-tool-policy-hardening.md), [ADR-0037](0037-engine-tool-execution-boundary.md), [ADR-0043](0043-media-egress-failover-rematerialization-ssrf.md), [ADR-0053](0053-mcp-network-transport-egress-security.md), [ADR-0052](0052-inbound-mcp-client-package-lifecycle-registration.md), [phase-2.5-cli-consolidation.md](../roadmap/phases/phase-2.5-cli-consolidation.md) (2.5.A), [architectural-principles.md](../standards/architectural-principles.md) + +> **Draft.** Proposed alongside the Phase 2.5 plan; to be reviewed and finalized (→ Accepted) when workstream 2.5.A begins. **Security review is mandatory before Accept.** + +## Context + +The built-in chat agent advertises `read_file` / `list_directory` / `git_status` +(`apps/cli/src/chat/default-agent.ts`) and its system prompt tells the model to "say so plainly" when a +tool is unavailable — a designed graceful path. But the CLI wires a **fail-closed** `ToolHost` (no `fs`, +no `process`, no `egress`) on both the chat path (`apps/cli/src/chat/session-host.ts`) and the +workflow-run path (`apps/cli/src/engine/build-engine.ts`). So the first tool call throws +`capability_unavailable`, which the turn core maps to a bare `internal`, and the user sees an opaque +`error: internal`. Two further problems compound it: the two paths build the host **differently** — the +chat path *merges* the MCP arm onto a base host, the run path *replaces* the host with `{ mcp }` — which +is harmless today but silently drops sibling arms (`fs`/`process`) once they are added; and the three +concepts a tool dispatch needs are distinct types that a naive factory signature would conflate — the +`ToolHost` capability arms, the `ToolPolicy` allowlists ([ADR-0029](0029-tool-policy-hardening.md)), and +the per-dispatch `ToolDispatchContext` (`fsScope`). + +## Decision + +**We will add one shared factory `assembleToolEnv(mode, fsScopeTier)` that returns `{ host: ToolHost, +policy: ToolPolicy }`, wired into both the chat and run paths, with `fsScope` flowing through the +dispatch context — keeping the three concepts in three channels.** The factory always uses conditional +spread (`exactOptionalPropertyTypes`-clean) so the MCP arm is a true **merge** on both paths, deleting +the two divergent inline host expressions. The default chat profile wires **`fs` read-only and the +`process` arm** — `read_file` / `list_directory` use `fs`, but `git_status` spawns `git` through +`requireProcess`, so an `fs`-only host would still fail it with `capability_unavailable` (the exact root +cause). `run_command` is kept unadvertised (the mode advertise-filter, +[ADR-0057](0057-cli-chat-modes-and-per-tool-approval.md)) and denied by an empty `ToolPolicy.allowedCommands`, +so in the default profile the `process` arm only ever serves the pre-approved `git_status` — which is +**not** subject to `allowedCommands` (it exposes no model-controlled `command` to `policyTarget`, so +`enforcePolicy`'s command-allowlist arm never fires for it; an empty allowlist therefore blocks +`run_command` only, never `git_status`); `fs`-read-write and `egress` are wired only for the higher tiers. The `egress` arm reuses the existing SSRF-validated +`EgressCapability` mechanism ([ADR-0043](0043-media-egress-failover-rematerialization-ssrf.md) +/ [ADR-0053](0053-mcp-network-transport-egress-security.md)) — the one shared primitive, never +re-implemented. + +**Responsibility boundary with [ADR-0057](0057-cli-chat-modes-and-per-tool-approval.md):** this ADR owns +*which capability arms are physically wired* — the `host`/`policy` shape, mode-independent; the `mode` +parameter drives only the `ToolPolicy` allowlists and the dispatch context, never the host-arm selection, +and that mapping lives in `apps/cli`, not the engine. ADR-0057 owns *whether a present capability may be +used in a given mode*. The tool dispatch boundary is unchanged +([ADR-0037](0037-engine-tool-execution-boundary.md)); the engine stays pure. Spec lives in +[tool-registry.md](../reference/shared-core/tool-registry.md). + +This decision also covers the two pure engine amendments that complete the error surface (the phase doc's +EA1/EA2): **EA1** maps the dispatch-layer `capability_unavailable` to a new portable `tool_unavailable` +`ErrorCode` (`@relavium/shared` `ERROR_CODES`; `codeForToolError` in `agent-turn.ts` is the single change +point) instead of `internal`, so a missing capability surfaces with the tool name; and **EA2** carries the +real accumulated usage on a failed turn (a `usage` field on `AgentTurnError`), touching only the two +provider-engaged branches in `agent-session.ts`. Both are recorded by this ADR. + +Considered the conflated signature `createCliToolHost({ fsScope, allowedCommands, egress })` (rejected: +`fsScope` is dispatch-context, `allowedCommands` is `ToolPolicy` — three types crammed into one); wiring +only `fs` for chat (rejected: leaves the run path and the `process`/`egress` tiers unwired — the same +gap, just narrower); and keeping the host fail-closed and merely filtering the advertised tool set +(rejected: the advertised tools must actually **work** for a first-class chat — the advertise-filter is +the safety complement, not a substitute for wiring the capability). + +## Consequences + +### Positive + +- The capability-gap root cause is closed at its source: the chat agent's tools work, and the + merge-vs-replace asymmetry is fixed once, in one place. +- One security-reviewed seam instead of two drifting inline host expressions; desktop/VS Code can reuse + the same factory shape. + +### Negative + +- A new security surface — `fs`-write, `process`, and `egress` capabilities now exist in the CLI host — + requiring a dedicated security review. The posture is **default-deny by policy, not by capability + absence**: the host can write, but the mode/approval layer must permit it. ("Read-only by default" means + the *chat default profile* wires only the read tiers; the higher tiers, once wired, are gated.) +- The write capability is physically present even in `ask` mode, since the mode system is a policy layer + rather than a per-mode rebind — a deliberate defense-in-depth trade-off. It is mitigated by + [ADR-0057](0057-cli-chat-modes-and-per-tool-approval.md)'s **fail-closed** `confirmAction` floor: whenever + a write/process/egress arm is wired, a write-class dispatch *requires* an approval decision, and an absent + hook **denies** — so even if the advertise-filter is bypassed, `ask` mode cannot write. (Crucially, + `enforcePolicy` alone is **inert** for `write_file` — its `FS_POLICY` triggers none of `enforcePolicy`'s + arms — so the floor is `confirmAction`, not `enforcePolicy`.) Protected paths apply in every mode. diff --git a/docs/decisions/0056-cli-in-app-slash-command-system-and-manifest.md b/docs/decisions/0056-cli-in-app-slash-command-system-and-manifest.md new file mode 100644 index 00000000..d1781fc7 --- /dev/null +++ b/docs/decisions/0056-cli-in-app-slash-command-system-and-manifest.md @@ -0,0 +1,53 @@ +# ADR-0056: In-app slash command system driven by a single command manifest + +- **Status**: Proposed +- **Date**: 2026-06-28 +- **Related**: [ADR-0049](0049-cli-machine-output-contract.md), [ADR-0047](0047-cli-framework-commander-ink-clack.md), [ADR-0054](0054-cli-bare-invocation-interactive-home.md), [ADR-0057](0057-cli-chat-modes-and-per-tool-approval.md), [phase-2.5-cli-consolidation.md](../roadmap/phases/phase-2.5-cli-consolidation.md) (2.5.C), [architectural-principles.md](../standards/architectural-principles.md) + +> **Draft.** Proposed alongside the Phase 2.5 plan; to be reviewed and finalized (→ Accepted) when workstream 2.5.C begins. + +## Context + +The interactive surface today exposes only three slash commands (`/exit`, `/cancel`, `/export`), all +REPL-only, while the shell-level command grammar is inconsistent (`chat-resume` vs `agent run` vs `gate +list` vs `provider set-key` are four different shapes) and there is no machine-readable command +discovery. The Home ([ADR-0054](0054-cli-bare-invocation-interactive-home.md)) needs a discoverable +palette, and the per-command wiring is currently buried inside the `register*` action bodies +(`apps/cli/src/commands/specs.ts`), not reusable by a palette or a slash handler. Surveyed competitors +sprawl to 40–100 slash commands with heavy alias inflation (`/cost`=`/stats`=`/usage`), which hurts +discoverability — an anti-pattern to avoid. + +## Decision + +**We will drive `commander`, the in-app palette, and the in-REPL slash commands from one shared dispatch +table generated from a single command manifest**, defined as a Zod schema (canonically homed in +`docs/reference/cli/home.md`): per-entry `{ id, label, description, canonical, args?, effect, modeScope?, +requiresProvider? }` — where `effect` is `read | write | destructive` (destructive entries are marked and +approval-gated), `description` feeds the `--help --json` text, `args` describes subcommand arguments, and +`modeScope?` is an optional list of chat modes a command is available in (omit = all modes; used to filter +the palette in, e.g., `ask` mode). The set is deliberately **small, canonical, and alias-free**. The same +manifest also feeds `relavium --help --json`, realizing the long-noted "agent-readable command surface". The per-command +dependency wiring is extracted from the `register*` bodies into a shared dispatch module that a +`commander` action, the palette, and a slash command all call, so the three surfaces can never diverge. +The taxonomy (which slash maps to which subcommand, and how the non-interactive equivalent is preserved +for CI/scripting) is canonically homed in `docs/reference/cli/home.md` (authored in 2.5.B). + +Considered keeping slash separate from `commander` (rejected: the two surfaces drift); allowing aliases +for ergonomics (rejected: the competitor anti-pattern — cognitive load and inconsistent muscle memory); +and shipping a user-defined plugin/TOML command system now (rejected: Phase 3 — the manifest is the +foundation it will build on, not a Phase 2.5 deliverable). + +## Consequences + +### Positive + +- One discoverable, consistent, machine-readable command surface; the palette, slash help, and + `--help --json` all derive from one source, so they cannot disagree. +- DRY wiring: a command's dependencies are assembled once and shared by every surface. + +### Negative + +- A one-time refactor of the `specs.ts` action wiring into the shared dispatch module — contained to + `apps/cli`, with the existing command tests as the safety net. +- The command manifest is a new contract to maintain; mitigated by making it the *only* source for the + three surfaces (drift is structurally impossible). diff --git a/docs/decisions/0057-cli-chat-modes-and-per-tool-approval.md b/docs/decisions/0057-cli-chat-modes-and-per-tool-approval.md new file mode 100644 index 00000000..ab2cdc28 --- /dev/null +++ b/docs/decisions/0057-cli-chat-modes-and-per-tool-approval.md @@ -0,0 +1,87 @@ +# ADR-0057: Reseat-less chat modes and per-tool approval (with mid-turn abort) + +- **Status**: Proposed +- **Date**: 2026-06-28 +- **Related**: [ADR-0024](0024-agent-first-entry-point-agentsession.md), [ADR-0029](0029-tool-policy-hardening.md), [ADR-0028](0028-workflow-resource-governance.md), [ADR-0037](0037-engine-tool-execution-boundary.md), [ADR-0055](0055-cli-host-capability-seam-tool-environment-factory.md), [ADR-0059](0059-cli-mid-session-model-reseat.md), [phase-2.5-cli-consolidation.md](../roadmap/phases/phase-2.5-cli-consolidation.md) (2.5.E), [architectural-principles.md](../standards/architectural-principles.md) + +> **Draft.** Proposed alongside the Phase 2.5 plan; to be reviewed and finalized (→ Accepted) when workstream 2.5.E begins. **Security review is mandatory before Accept.** + +## Context + +We want Claude-Code-style chat modes (ask / plan / accept-edits / auto) and per-tool approval for writes +and shell. Two facts about the current code shape the decision. First, a session binds one agent and one +model for its lifetime, with the fallback plan memoized ([ADR-0024](0024-agent-first-entry-point-agentsession.md)), +and the cross-turn transcript plus `reconstructSessionState` are **text-only** — so applying each mode +change by "reseating" the session (reconstruct + a new instance) would silently delete that session's +accumulated tool context (which files were read, tool results), a data-loss trap on a frequent keystroke. +Second, there is **no** per-tool approval infrastructure: the workflow human-gate is node-level, the +`gateApproved` flag is a one-way static deny that is always `false` on the chat path, `agent:tool_call` +is emitted *after* dispatch, and `ToolPolicy` carries no approval concept. Getting this wrong either +loses context on every `Shift+Tab` or ships an unsafe "approve everything" escape hatch. + +## Decision + +**We will make chat modes a policy layer on the same session instance — no reseat — and build per-tool +approval as a new, bounded engine vertical.** + +- **Reseat-less modes.** The `ToolHost` is bound full-capability for the session lifetime + ([ADR-0055](0055-cli-host-capability-seam-tool-environment-factory.md)); a mutable `#mode` (snapshotted + per turn) controls only (a) the model-advertised tool subset (a per-turn `buildLlmTools` filter) and + (b) the per-dispatch approval policy. Mode changes flip a flag — lossless, cheap, and two-layer safe: + the advertise-filter (best-effort — it keeps a write tool out of the model's reach) plus the + **mode-aware `confirmAction` approval policy** (authoritative — if the model emits a `write_file` call in + `ask` mode anyway, the approval policy denies it by mode). The existing `enforcePolicy` gate is + mode-agnostic (it only enforces `requiresGateApproval` / `allowedCommands`) and is the floor beneath + both. The mode policy is read from the same per-turn snapshot as the advertise-filter, so a mid-turn + `/mode` change applies on the next turn. `Shift+Tab` cycles `ask → plan → accept-edits`; `auto` is + explicit-only (`/mode auto`); there is **no one-key bypass valve** + ([ADR-0029](0029-tool-policy-hardening.md)). `.git/`, `.relavium/`, and shell rc files are never + auto-written in any mode. Switching the **model** mid-session does require a reseat — the separate + concern of [ADR-0059](0059-cli-mid-session-model-reseat.md). +- **Per-tool approval (new vertical), fail-closed.** A registry pre-dispatch `confirmAction` hook + (host-injected — the same dependency-inversion pattern as `ToolHost`, so it does not violate + [ADR-0037](0037-engine-tool-execution-boundary.md)'s tool-execution boundary: the engine defines the hook + interface, the host supplies the implementation). It runs **between `enforcePolicy` and the side-effect** + in `packages/core/src/tools/registry.ts`. **Critically:** `enforcePolicy` is mode-agnostic and **inert + for `write_file`** (its `FS_POLICY` sets `requiresGateApproval:false` and is not a process tool, so none + of `enforcePolicy`'s three arms — gate / command-allowlist / domain-allowlist — apply). So `confirmAction` + is the **authoritative** mode gate, and it must be **fail-closed**: when the host has wired a write / + process / egress arm ([ADR-0055](0055-cli-host-capability-seam-tool-environment-factory.md)), a + write-/exec-/egress-class tool dispatch **requires** a `confirmAction` decision — if no hook is supplied, + the dispatch is **denied**, never allowed. The advertise-filter is best-effort; this floor is the + guarantee, so a wiring bug cannot let `ask` mode write. Plus: a session-stream `agent:approval_requested` + event (kept in the existing `agent:*` namespace, alongside `agent:tool_call`); an `AgentSession` + pause/resume state (which today does not exist) — and an `Esc` **mid-turn abort** that aborts the + in-flight turn and keeps the session alive by emitting **one** `session:turn_completed` (with an abort + stop-reason), rolling back the pending user message, and returning `#status` to `idle` (the engine has no + `aborted` status and this is **not** `cancel()`, which is terminal); a REPL `[approve]/[reject]/[comment]` + intercept that bypasses the in-flight key-swallow gate (so the prompt cannot deadlock); a typed + `ToolDeniedByUserError` carrying the **existing** `tool_denied` `ErrorCode` (already non-retryable — it is + absent from `RETRYABLE_ERROR_CODES` in `@relavium/shared/constants`, so a user deny is final, not a retried + execution error); and a session-scoped, **in-memory** once/always cache (instance-scoped — **not** + persisted across resume, so a reseat or `chat-resume` re-prompts; semantics: **once** = this specific + invocation (tool + args), **always** = this tool id for the remainder of this session instance). The + `gateApproved` flag is **not** reused (it is a one-way static deny). The session-level budget pause/resume + deferred from Phase 2 ([ADR-0028](0028-workflow-resource-governance.md)) rides the same machine. + +Considered reseating per mode change (rejected: silent tool-context loss + instance churn); binding a +read-only host and reseating to add write capability (rejected: same loss); reusing the node-level +workflow gate for per-tool approval (rejected: wrong granularity — it pauses a DAG vertex, not a tool +call); and a single bypass flag like the four surveyed competitors (rejected: secure-by-default). + +## Consequences + +### Positive + +- Instant, lossless mode switching; real per-tool approval with once/always memory; a true mid-turn abort + that keeps the session alive (today `/cancel` kills it). +- No one-key bypass valve — the one structural differentiator from every surveyed terminal agent CLI. +- The pause/resume machine is shared by per-tool approval, mid-turn abort, and the deferred budget pause. + +### Negative + +- The `ToolHost` is full-capability even in `ask` mode, so defense-in-depth rests one layer more on the + policy layer than on capability absence — mitigated by the advertise-filter, the `enforcePolicy` gate, + protected paths, and this ADR's mandatory security review. +- This is the most complex workstream: a new shared event, a new session state, and a REPL intercept — + isolated in 2.5.E behind this ADR so the rest of Phase 2.5 does not depend on it. diff --git a/docs/decisions/0058-relavium-authoring-package-and-conversational-authoring.md b/docs/decisions/0058-relavium-authoring-package-and-conversational-authoring.md new file mode 100644 index 00000000..d21998a1 --- /dev/null +++ b/docs/decisions/0058-relavium-authoring-package-and-conversational-authoring.md @@ -0,0 +1,80 @@ +# ADR-0058: `@relavium/authoring` package and the conversational-authoring pre-flight contract + +- **Status**: Proposed +- **Date**: 2026-06-28 +- **Related**: [ADR-0026](0026-session-export-to-workflow.md), [ADR-0055](0055-cli-host-capability-seam-tool-environment-factory.md), [ADR-0057](0057-cli-chat-modes-and-per-tool-approval.md), [phase-2.6-conversational-authoring.md](../roadmap/phases/phase-2.6-conversational-authoring.md) (2.6.A/2.6.B), [product-constraints.md](../product-constraints.md), [architectural-principles.md](../standards/architectural-principles.md), [CLAUDE.md](../../CLAUDE.md) (#8 one-canonical-home) + +> **Draft.** Proposed alongside the Phase 2.6 plan; to be reviewed and finalized (→ Accepted) when workstream 2.6.A begins. **Security review of the write surface is mandatory before Accept.** + +## Context + +Phase 2 (workstream 2.J) landed an authoring core in `apps/cli/src/authoring/authoring.ts`: it wraps +`parseWorkflow` / `serializeWorkflow` / `parseAgent` / `buildAuthored` / `detectAndParse` from +`@relavium/core` and drives the `create` / `import` / `export` commands. We now want a `relavium chat` +conversation to **author** a standards-valid `.relavium.yaml` from a free-text request (the terminal +realization of *"Start as an agent. Ship the workflow."*), and the maintainer has decided the authoring +core should be reusable beyond the CLI — the desktop ([phase-3-desktop.md](../roadmap/phases/phase-3-desktop.md)) +and VS Code ([phase-4-vscode.md](../roadmap/phases/phase-4-vscode.md)) surfaces should consume the same +core. Two constraints frame it: the existing `create`/`import`/`export` pre-flight is **parse-only** (it +does not run the catalog-aware validator — only the run path does), so a wizard could accept a +model/modality the run path rejects; and the Relavium **product** agent (`relavium chat`) never reads +`.claude/`, so the repo-development skills there cannot serve as its authoring knowledge. + +## Decision + +**We will promote the in-tree authoring core to a new shared package `@relavium/authoring`, expose a +single catalog-aware `validateAuthoredWorkflow` pre-flight used by every authoring path, and drive a +conversational authoring agent from a product-side knowledge pack derived from the canonical specs.** + +- **Package promotion is an extract-and-decouple, not a move.** The in-tree core + (`apps/cli/src/authoring/authoring.ts`) currently imports three **`apps/cli` internal** modules — + `findProjectConfigDir` (`../config/paths`), `CliError` (`../process/errors`, exit-code-coupled), and + `discoverCatalog` (`../workflows/catalog`). A new `packages/authoring` must **not** import from + `apps/cli` (a forbidden `packages → apps` back-edge that fails `pnpm turbo build`), so promotion **decouples** + those three: (a) replace `CliError` throws with a platform-free `Result`/typed error the CLI maps to its + own exit codes at the boundary; (b) keep catalog **discovery** CLI-side and pass the catalog **in** — + `validateAuthoredWorkflow(yaml, catalog)` already takes it as a parameter; (c) keep `findProjectConfigDir` + CLI-side (path discovery is a surface concern). The resulting package is pure TS / platform-free (engine + purity holds) and depends **only** on `@relavium/core` + `@relavium/shared` — a one-direction edge + (`authoring → core`; `core`/`shared` never import `authoring`). It adds **no new third-party dependency**; + the add-package ADR records the multi-surface decision (architectural-principles §9). An import-zone lint + fence (the Phase-0 seam-fence pattern) enforces the `packages/authoring → apps/cli` ban. +- **One pre-flight.** Expose `validateAuthoredWorkflow(yaml, catalog)` = `parseWorkflow` + + `validateWorkflowWithCatalog`, and back-port it into `create`/`import`/`export` so wizard-authored and + conversationally-authored artifacts share one front end — a `create` can never accept what the run path + rejects. The self-correct loop (model → YAML → pre-flight → field-named, secret-free error → fix) + reuses the same `detectAndParse`. This is the sibling of session export + ([ADR-0026](0026-session-export-to-workflow.md)): export replays a transcript; authoring generates. +- **Knowledge, derived not restated.** The conversational agent's knowledge is a product-side pack + **derived** from [node-types.md](../reference/shared-core/node-types.md), + [workflow-yaml-spec.md](../reference/contracts/workflow-yaml-spec.md), + [agent-yaml-spec.md](../reference/contracts/agent-yaml-spec.md), and the Zod schemas, plus one minimal + valid example per node type — never a second copy ([CLAUDE.md](../../CLAUDE.md) #8). It does **not** live + under `.claude/skills/`. Artifacts are written only under accept-edits/auto + ([ADR-0057](0057-cli-chat-modes-and-per-tool-approval.md)) with the scope-tiered host + ([ADR-0055](0055-cli-host-capability-seam-tool-environment-factory.md)). + +Considered building a new authoring package from scratch (rejected: the core already exists from 2.J — +promote, do not duplicate); keeping authoring CLI-only (rejected: the maintainer wants desktop/VS Code to +consume it); restating the schema in the knowledge pack (rejected: one-canonical-home — derive instead); +and placing the knowledge under `.claude/skills/` (rejected: the product agent never reads `.claude/`). + +## Consequences + +### Positive + +- One authoring core, reusable by every surface; one catalog-aware pre-flight shared by wizard and + conversational authoring; no parse-vs-catalog drift between `create` and `run`. +- The conversational author reuses the proven validator as its self-correct oracle, lowering cost and + guaranteeing run-validity. + +### Negative + +- A new workspace package **plus an extract-and-decouple refactor** (three `apps/cli` couplings — + `CliError`, `discoverCatalog`, `findProjectConfigDir` — must be cut), not a free move; the CLI commands + re-wire to the package and the boundary maps the package's typed errors to exit codes. The `create` / + `import` / `export` behaviour must round-trip unchanged (regression-tested), and an import-zone lint + fence guards the `packages/authoring → apps/cli` ban. +- A knowledge-derivation discipline with a no-restate check to prevent drift from the canonical specs. +- A new write surface (the model writing files) — gated by accept-edits approval, the scope-tier host, + and the existing secret-taint gate in `parseWorkflow`, plus a mandatory security review. diff --git a/docs/decisions/0059-cli-mid-session-model-reseat.md b/docs/decisions/0059-cli-mid-session-model-reseat.md new file mode 100644 index 00000000..60a47752 --- /dev/null +++ b/docs/decisions/0059-cli-mid-session-model-reseat.md @@ -0,0 +1,71 @@ +# ADR-0059: Mid-session model switching via host-side reseat (refines ADR-0024) + +- **Status**: Proposed +- **Date**: 2026-06-28 +- **Related**: [ADR-0024](0024-agent-first-entry-point-agentsession.md), [ADR-0026](0026-session-export-to-workflow.md), [ADR-0057](0057-cli-chat-modes-and-per-tool-approval.md), [phase-2.6-conversational-authoring.md](../roadmap/phases/phase-2.6-conversational-authoring.md) (2.6.C), [architectural-principles.md](../standards/architectural-principles.md) + +> **Draft.** Proposed alongside the Phase 2.6 plan; to be reviewed and finalized (→ Accepted) when workstream 2.6.C begins. + +## Context + +An `AgentSession` binds one agent and one model for its lifetime — multi-agent/model orchestration is a +workflow concern, and the fallback plan is memoized ([ADR-0024](0024-agent-first-entry-point-agentsession.md)). +But users want `/models` to switch the bound model **mid-chat** (e.g. start on a cheap model, escalate to +a stronger one for a hard turn). This is distinct from a **mode** change, which stays on the same +instance with no reseat ([ADR-0057](0057-cli-chat-modes-and-per-tool-approval.md)); a model change alters +the provider, the pricing, and the memoized plan, so it cannot be a flag flip. The risk is either +overstating what carries across the switch (the transcript is text-only) or churning the engine to +support in-place rebind. + +## Decision + +**We will switch models mid-session via a host-side "reseat" that reuses the existing `chat-resume` +machinery — no engine change — and explicitly disclose what it does not carry.** A mid-chat `/models` +reconstructs the transcript (`reconstructSessionState`) and starts a new `AgentSession.resume` bound to +the new model/provider, carrying the cumulative cost and turn count. The new instance memoizes a **fresh** +`#plan` for the chosen model (the old plan is discarded with the old instance): the picked model becomes +the primary, and the chain is built **exactly as `AgentSession.build()` builds one for a fresh session +bound to that model with no explicit `fallback_chain`** — i.e. the same default-plan helper, so a switch to +`claude-opus-4-8` yields the identical chain to starting a new session on it (a single-model chain if the +provider has no default). The original agent's `fallback_chain` is **not** carried across a model switch +(that chain belonged to the original model). Each +`AgentSession` instance still has exactly one model — so this **refines** (clarifies) ADR-0024's +one-model-per-lifetime rule rather than reversing it. We persist a per-message `modelId` with the model +that actually produced each message (failover-aware) — the `session_messages` schema already has the +(nullable) column; only the CLI persister wiring is missing — and show a per-model cost breakdown. Rows +written before this wiring (and any failover edge that leaves it unset) carry a **null** `modelId`; the +breakdown assigns those to an explicit `unknown` (pre-attribution) bucket rather than dropping them, so +legacy history is never silently lost from the totals. The reseat carries +the **text-only** transcript, so we surface an explicit notice — shared with the `chat-resume` family — +that prior tool calls and file contents are **not** carried to the new model; full-fidelity tool-context +(a persister + schema extension) is deferred to Phase 3. + +Considered an in-place model rebind on the same instance (rejected: it fights the `#plan` memoization and +ADR-0024's lifetime invariant; reseat reuses proven, tested machinery with zero engine change); claiming +full context carries (rejected: false — the cross-turn transcript is text-only); and starting a fresh +session that drops history (rejected: poor UX — the user expects to continue). + +## Consequences + +### Positive + +- Mid-chat model switching reusing the proven resume path; ADR-0024's invariant is honored, not broken; + per-message model attribution makes the cost breakdown accurate. + +### Negative + +- Tool context (read files, tool results) is **not** carried across the switch — disclosed by an explicit + notice; full fidelity is Phase 3. +- Each switch reconstructs the transcript (an `O(n)` cost on a long session) — acceptable for an + interactive, user-initiated action; verified by the 2.6.C harness (a 200-message session reseats in well + under the interactive budget). +- The per-tool approval once/always cache ([ADR-0057](0057-cli-chat-modes-and-per-tool-approval.md)) is + instance-scoped, so a reseat **re-prompts** for previously-approved tools — mildly friction in an + author → escalate-model → run flow; accepted as the safe default (a new model is a new trust context). + +### Neutral + +- This **refines, not reverses**, [ADR-0024](0024-agent-first-entry-point-agentsession.md): each + `AgentSession` instance still binds exactly one model for its lifetime; mid-session model switching is a + host-side reseat (a new instance), not an in-place rebind. When this ADR is Accepted, ADR-0024 gets a + dated `> Amended` note + a Related forward-link (documentation-style §7). diff --git a/docs/decisions/0060-session-ctx-prompt-interpolation.md b/docs/decisions/0060-session-ctx-prompt-interpolation.md new file mode 100644 index 00000000..3f26cafa --- /dev/null +++ b/docs/decisions/0060-session-ctx-prompt-interpolation.md @@ -0,0 +1,68 @@ +# ADR-0060: Session `{{ctx.*}}` prompt interpolation + +- **Status**: Proposed +- **Date**: 2026-06-28 +- **Related**: [ADR-0024](0024-agent-first-entry-point-agentsession.md), [ADR-0027](0027-expression-sandbox.md), [ADR-0058](0058-relavium-authoring-package-and-conversational-authoring.md), [ADR-0059](0059-cli-mid-session-model-reseat.md), [phase-2.6-conversational-authoring.md](../roadmap/phases/phase-2.6-conversational-authoring.md) (2.6.D), [architectural-principles.md](../standards/architectural-principles.md) + +> **Draft.** Proposed alongside the Phase 2.6 plan; to be reviewed and finalized (→ Accepted) when workstream 2.6.D begins. **Security review of the session-prompt taint path is mandatory before Accept.** + +## Context + +The session agent's `system_prompt` is passed to the turn core **verbatim** — there is no template +resolution — and `relavium agent run --input k=v` is consequently reserved/rejected until session-scoped +variable interpolation exists (a tracked Phase-2 engine follow-up). The conversational authoring agent +([ADR-0058](0058-relavium-authoring-package-and-conversational-authoring.md)) and the reseat path both +want session context in the prompt, so the deferral now blocks Phase 2.6. The workflow path already has a +template/interpolation engine for node prompts; the question is whether the session reuses it or a +surface-side substitute is introduced. + +## Decision + +**We will resolve `{{ctx.*}}` placeholders in the session `system_prompt` against session-scoped +variables inside `AgentSession` (a pure engine amendment), reusing the workflow interpolation +mechanism (`resolveTemplate`), and unblock `agent run --input`.** The interpolation is the engine's +single mechanism — we cite the [workflow-yaml-spec.md](../reference/contracts/workflow-yaml-spec.md) +interpolation contract rather than re-implementing or restating it; this is template substitution, **not** +expression evaluation ([ADR-0027](0027-expression-sandbox.md) keeps that boundary). + +**Untrusted-content invariant (security) — a value-provenance rule, not just a `caps={}` rule.** Two +sources of untrust must be kept out of the `system` position. (1) **Resolver source:** `resolveTemplate` +accepts an optional `ResolverCapabilities`; its `read_file`-style resolver would pull file content, which +the resolver-taint rules require to land **only** in `user`/`tool` positions, never `system`. Session-prompt +interpolation therefore calls `resolveTemplate` with **no `read_file` capability** (`caps = {}`) and the +**`ctx.*` namespace only**. (2) **Value provenance:** the `ctx.*` **values** themselves are not all trusted — +`agent run --input k=v` values are user-/pipeline-controlled (e.g. `--input task="$(curl evil)"`), so they +are **untrusted by provenance** even though their source is not `read_file`. The safe property is therefore +that **only trusted-literal session variables interpolate into the `system` prompt**; `--input`-derived +values are tagged untrusted and resolve **only in `user`-position** turns, never `system`. (`SessionContext` +gains a per-variable provenance/taint marker — today it is a flat record with none — so the resolver can +enforce this.) The marker is **sticky/transitive**: any `ctx.*` value copied from, merged with, or derived +from an untrusted value inherits the untrusted provenance (the most-untrusted source wins), so taint cannot +be laundered by relabeling an `--input`-derived value into a "trusted-literal" key — only variables that +originate as trusted literals (config / agent definition) are ever `system`-eligible. Secret-taint +discipline also applies: `{{ctx.*}}` carries plaintext variables only, never +secrets (those stay in the keychain and never enter a prompt). **A security review of the session-prompt +taint path is mandatory before Accept.** + +Considered keeping the prompt verbatim and leaving `--input` rejected (rejected: it blocks both +conversational authoring context and the advertised `agent run --input`); and a CLI-side string replace +before the prompt reaches the engine (rejected: the engine owns interpolation — a surface-side hack would +diverge from the workflow path and break the one-mechanism / engine-purity principle). + +## Consequences + +### Positive + +- `agent run --input` is unblocked; the session prompt can carry context for authoring and reseat; one + interpolation mechanism across the workflow and session paths. + +### Negative + +- A pure engine amendment to `AgentSession` (recorded by this ADR) **plus a per-variable provenance/taint + marker on `SessionContext`** (today a flat record) so untrusted `--input`-derived values cannot reach the + `system` position — a real prompt-injection surface if missed. Mitigated by the value-provenance rule + above, the existing secret-taint discipline, and a mandatory security review. +- If an operator pipes untrusted external data into `--input` (shell substitution, a CI input), those values + still reach a `user`-position prompt — no automatic content sanitization is applied; operators piping + untrusted values are responsible for sanitizing them (the engine guarantees only that such values never + reach the `system` position). diff --git a/docs/decisions/README.md b/docs/decisions/README.md index 7945ba7e..eb480c7d 100644 --- a/docs/decisions/README.md +++ b/docs/decisions/README.md @@ -97,6 +97,13 @@ flowchart TD | 0051 | [CLI distribution — an engine-inlined ESM bundle that externalizes every third-party dependency (finalizes 0047's bundle boundary)](0051-cli-distribution-thin-bundle-private-engine.md) | Accepted | 2026-06-24 | | 0052 | [Inbound MCP client — the `@relavium/mcp` package boundary, host-injected connection lifecycle, host-side tool registration, a dependency-free schema→validator compiler, and the agent↔config reference linkage (implements 0034)](0052-inbound-mcp-client-package-lifecycle-registration.md) | Accepted | 2026-06-26 | | 0053 | [MCP network-transport (`sse`/`websocket`) egress security — SSRF enforcement on the one shared primitive, and the explicit per-server local-endpoint opt-in](0053-mcp-network-transport-egress-security.md) | Accepted | 2026-06-26 | +| 0054 | [Bare `relavium` invocation opens an interactive Home (TTY only), preserving the meta-op contract](0054-cli-bare-invocation-interactive-home.md) | Proposed | 2026-06-28 | +| 0055 | [Shared CLI tool-environment factory — `ToolHost`, `ToolPolicy`, and dispatch context as separate channels](0055-cli-host-capability-seam-tool-environment-factory.md) | Proposed | 2026-06-28 | +| 0056 | [In-app slash command system driven by a single command manifest](0056-cli-in-app-slash-command-system-and-manifest.md) | Proposed | 2026-06-28 | +| 0057 | [Reseat-less chat modes and per-tool approval (with mid-turn abort)](0057-cli-chat-modes-and-per-tool-approval.md) | Proposed | 2026-06-28 | +| 0058 | [`@relavium/authoring` package and the conversational-authoring pre-flight contract](0058-relavium-authoring-package-and-conversational-authoring.md) | Proposed | 2026-06-28 | +| 0059 | [Mid-session model switching via host-side reseat (refines ADR-0024)](0059-cli-mid-session-model-reseat.md) | Proposed | 2026-06-28 | +| 0060 | [Session `{{ctx.*}}` prompt interpolation](0060-session-ctx-prompt-interpolation.md) | Proposed | 2026-06-28 | ## Creating a new ADR diff --git a/docs/roadmap/README.md b/docs/roadmap/README.md index 9fb6cb35..2292540a 100644 --- a/docs/roadmap/README.md +++ b/docs/roadmap/README.md @@ -88,9 +88,11 @@ flowchart LR P0["Phase 0
foundations
(shared schemas + CI)"] P1E["Phase 1
engine + llm
CRITICAL PATH"] P2["Phase 2
CLI
(first engine consumer)"] + P25["Phase 2.5
CLI consolidation
(Home + modes + root-cause)"] + P26["Phase 2.6
conversational authoring
(@relavium/authoring)"] P3["Phase 3
desktop
(Tauri agent center)"] P4["Phase 4
VS Code
(standalone ext)"] - P0 --> P1E --> P2 --> P3 --> P4 + P0 --> P1E --> P2 --> P25 --> P26 --> P3 --> P4 end subgraph P2C["Product Phase 2 — managed inference + cloud (optional, additive)"] direction LR @@ -124,7 +126,9 @@ contract and a green CI gate. | **0** | Stand up the Turborepo + pnpm monorepo, land `packages/shared` (the Zod schemas + inferred types that are the single source of truth), and wire the tooling/CI/docs spine every later phase builds on. | — (build-order step 1) | Clean `pnpm install` + green `pnpm turbo run lint typecheck test` across all workspaces in CI; `@relavium/shared` round-trips the reference YAML with no drift; the `RunEvent` union matches the canonical colon-namespaced schema (type-level + runtime test); seam lint fence live. | [phases/phase-0-foundations.md](phases/phase-0-foundations.md) | | **1** | Build the two engine packages everything depends on — `@relavium/llm` (the provider-agnostic `LLMProvider` seam, 3 adapters, fallback runner, cost) and `@relavium/core` (YAML→DAG, runner, checkpoint/resume, retry) — proven end-to-end from a Node harness. **The critical path.** | Phase 0 | A Node harness runs a 3-node workflow end-to-end with live streaming, canonical events, checkpoint/resume, retry, and provider fallback; all 3 adapters pass conformance (fixtures on PR, live nightly); no vendor type crosses the seam; zero platform imports. | [phases/phase-1-engine-and-llm.md](phases/phase-1-engine-and-llm.md) | | **2** | Ship the `relavium` CLI as the **first real engine consumer** and the engine's **regression harness** — `run / list / logs / gate / status`, an `ink` TUI, and a deterministic `--json` CI mode. | Phase 1 | A 3-node workflow runs via `relavium run` with the live TUI; durable history powers `list`/`logs`/`status`; human-gate pause/resume works interactively and via `relavium gate`; `--json` NDJSON is asserted by a green no-TTY CI regression harness; keys live only in the keychain (or documented fallback); `npm i -g relavium` verified on macOS/Linux/Windows. | [phases/phase-2-cli.md](phases/phase-2-cli.md) | -| **3** | Ship the Tauri v2 desktop **agent-management center**: ReactFlow canvas, live run monitoring, local SQLite history, OS keychain — offline, no account. **NOT an IDE** ([ADR-0007](../decisions/0007-desktop-is-not-an-ide.md)). | Phase 2 | Signed builds on macOS/Windows/Linux launch to a working agent center; build + run a 3-node workflow on the canvas with live streaming and per-node cost — all offline; SQLite history supports trace/Gantt/replay/retry-from-node; gates pause/resume; no secret crosses into the WebView; ADR-0010 perf gate passes; per-platform Playwright e2e green. | [phases/phase-3-desktop.md](phases/phase-3-desktop.md) | +| **2.5** | Consolidate the CLI into a conversation-first **Home** at the bare invocation, with a slash palette, reseat-less chat modes + per-tool approval, onboarding, and reasoning rendering — plus the bounded engine amendments and docs-debt Phase 2 deferred. Fixes the capability-gap root cause (advertised-but-unwired tools → bare `internal`). | Phase 2 | `relavium` (TTY) opens Home; `--json`/CI/non-TTY keep help + exit `0` (regression-harness proven); the built-in agent's tools actually work and `capability_unavailable` never surfaces as `internal`; modes + per-tool approval + mid-turn abort ship with no one-key bypass; the required ADRs are Accepted. | [phases/phase-2.5-cli-consolidation.md](phases/phase-2.5-cli-consolidation.md) | +| **2.6** | Promote the authoring core to a shared `@relavium/authoring` package, let a chat **author** standards-valid `.relavium.yaml` (model-generated, pre-flight-validated), switch models mid-session, and reach competitor-parity polish. Realises *"Start as an agent. Ship the workflow."* at the terminal. | Phase 2.5 | `@relavium/authoring` is the shared core (CLI consumes it; `create` runs the catalog-aware pre-flight); a conversational request yields a strict-valid workflow YAML; mid-session `/models` reseat works with the context-loss notice; `{{ctx.*}}` interpolation lands; the required ADRs are Accepted. | [phases/phase-2.6-conversational-authoring.md](phases/phase-2.6-conversational-authoring.md) | +| **3** | Ship the Tauri v2 desktop **agent-management center**: ReactFlow canvas, live run monitoring, local SQLite history, OS keychain — offline, no account. **NOT an IDE** ([ADR-0007](../decisions/0007-desktop-is-not-an-ide.md)). | Phase 2.6 | Signed builds on macOS/Windows/Linux launch to a working agent center; build + run a 3-node workflow on the canvas with live streaming and per-node cost — all offline; SQLite history supports trace/Gantt/replay/retry-from-node; gates pause/resume; no secret crosses into the WebView; ADR-0010 perf gate passes; per-platform Playwright e2e green. | [phases/phase-3-desktop.md](phases/phase-3-desktop.md) | | **4** | Ship the standalone VS Code extension that **bundles `@relavium/core` in-process**: right-click run, sidebar + status-bar monitor, human-gate webview — with **no desktop app required**. Closes Product Phase 1. | Phase 3 | With only the extension installed: right-click a file → pick a workflow → watch streaming in the sidebar/status bar → approve a gate in a webview → run completes; same canonical events + same local SQLite history as CLI/desktop; keys only in `SecretStorage`; import-zone check passes; `relavium.relavium` published and installs on macOS/Windows/Linux. | [phases/phase-4-vscode.md](phases/phase-4-vscode.md) | | **5** | **Product Phase 2 — managed inference (the first Phase-2 deliverable, Option B).** The opt-in `managed` execution mode: the engine stays **local**, only LLM egress is proxied through Relavium's gateway behind a new `ManagedGatewayProvider`, calling providers with Relavium's own keys and selling metered usage. Accounts/device-flow auth, key vault + pools, real-time metering (reserve→settle, UNIQUE `request_id`), quota/budget caps, merchant-of-record billing (prepaid + overage), cheap-default routing, abuse controls, no-prompt-logging, usage dashboard. **BYOK stays first-class.** | All of Phase 1 (0–4), shipped + battle-tested ([ADR-0008](../decisions/0008-local-first-phase-1-cloud-phase-2.md)); **plus R1**: provider-ToS confirmation + merchant-of-record + DPA/KVKK/GDPR ([ADR-0012](../decisions/0012-managed-inference-dual-mode.md)) | R1 gate met; a user logs in via device flow, **explicitly** opts into managed, and a workflow runs in `managed` mode with the engine still local and only egress proxied (no silent mode crossing); `ManagedGatewayProvider` is behind the unchanged seam (engine/types untouched); metering is idempotent (UNIQUE `request_id`) + nightly-reconciled; quota/hard-cap + merchant-of-record billing fire; no prompt logging by default; **BYOK unchanged and first-class.** | [phases/phase-5-managed-inference.md](phases/phase-5-managed-inference.md) | | **6** | **Product Phase 2 — cloud execution + web portal (decoupled from, sequenced after Phase 5).** Optional cloud **execution** (BullMQ/Redis/Postgres) + the web portal (usage/quota/license/enterprise), with a **transparent local→cloud switch** behind the engine's `ExecutionHost` interface, so the *same* `@relavium/core` runs in both modes with no fork; BYOK-cloud mode and team/RBAC/enterprise. | All of Phase 1 (0–4), shipped + battle-tested ([ADR-0008](../decisions/0008-local-first-phase-1-cloud-phase-2.md)); **does not depend on Phase 5** ([ADR-0012](../decisions/0012-managed-inference-dual-mode.md)) | A team runs a workflow in the cloud, shares a run-replay URL, and approves a gate via an email link — same engine, no fork (mode flag via `ExecutionHost`); SSE delivers canonical events with lossless reconnection; multi-tenancy is provably isolated and the four-key-leak security audit passes; quota enforcement fires; transcripts are never synced; **all Phase 1 surfaces still run fully locally with no account, unchanged.** | [phases/phase-6-cloud-execution-portal.md](phases/phase-6-cloud-execution-portal.md) | @@ -140,7 +144,7 @@ to that phase's work breakdown). | **M0 ✅** | **Foundations green** *(achieved 2026-06-04)* — monorepo + tooling + CI are green on a clean checkout; `@relavium/shared` exports the full Zod schema set and round-trips the reference YAML with no drift; the canonical `RunEvent` union (`sequenceNumber`, `cost:updated`) is pinned by a type-level + runtime test; the no-vendor-type seam fence is live; `@relavium/db` is scaffolded. | 1 | Phase 0 · 0.A–0.I *(done, PR #1–#3)* | | **M1 ✅** | **LLM seam proven** *(achieved 2026-06-07, PR #9)* — `@relavium/llm`'s provider-agnostic `LLMProvider` seam with 3 adapters (Anthropic, the OpenAI-compatible adapter serving OpenAI + DeepSeek, Gemini), all passing one shared conformance suite (fixtures on PR, live nightly reserved/pending keys); no vendor SDK type crosses the seam. The `FallbackChain` runner (1.K, PR #13) and the cost tracker (1.B) have since landed, completing 1.m2. (Built **first** per the engine-first order — the engine's `AgentRunner` depends on it.) | 1 | Phase 1 · `@relavium/llm` seam + 3 adapters — conformance green at **1.J** (1.G–1.J; FallbackChain + cost tracker at 1.m2 / 1.B, 1.K) | | **M2 ✅** | **Engine end-to-end (critical-path milestone)** *(achieved 2026-06-16, PR #27)* — `@relavium/core` parses YAML→DAG and runs it (the `WorkflowEngine` + `AgentRunner` over all node types, emitting canonical events through the `RunEventBus`, with checkpoint/resume, retry, the deterministic expression sandbox, and the pre-egress budget governor), with the M1 seam wired in; the 1.U Node harness runs a workflow end-to-end with live streaming, checkpoint + cross-process resume, retry, and a provider fallback. Zero platform-specific imports. | 1 | Phase 1 · `@relavium/core` parser + runner + checkpoint/resume + retry + **expression sandbox (1.AB) + budget governor (1.AC)** + Node harness (1.L–1.U, all landed); the agent-first sub-spine (1.V–1.AA) lands in parallel (1.m5) | -| **M3** | **CLI + engine regression harness** — the `relavium` CLI drives the engine end-to-end with a live `ink` TUI and a deterministic `--json` CI mode; a small fixture suite runs in CI on every engine change as the agreed regression gate for Phases 3–6. | 1 | Phase 2 · 2.D (run wiring), 2.F (`--json` / CI mode), 2.K (regression harness) | +| **M3 ✅** | **CLI + engine regression harness** *(achieved 2026-06-23, PR #43)* — the `relavium` CLI drives the engine end-to-end with a live `ink` TUI and a deterministic `--json` CI mode; a small fixture suite runs in CI on every engine change as the agreed regression gate for Phases 2.5–6. (Phase 2 shipped complete as **v0.1.1**, PR #58; the CLI consolidation/authoring work continues in Phases 2.5–2.6 before M4.) | 1 | Phase 2 · 2.D (run wiring), 2.F (`--json` / CI mode), 2.K (regression harness) | | **M4** | **Desktop agent-management center** — a signed, offline, no-account Tauri v2 app: ReactFlow canvas with all node types, live execution theater, gate overlay, local SQLite history (trace/Gantt/replay/retry-from-node), OS keychain; ADR-0010 perf gate + per-platform e2e green. | 1 | Phase 3 · 3.K (packaging), 3.L (e2e + perf gate), 3.P (sign-off) | | **M5** | **Product Phase 1 complete** — the standalone VS Code extension is shipped to the Marketplace: right-click run, live monitoring, human-gate webview, **no desktop app required**, same engine + events + SQLite history as the other surfaces. | 1 | Phase 4 · 4.E (right-click run), 4.H (gate webview), 4.K (Marketplace publish) | | **M6** | **Managed inference shipped (the first Phase-2 deliverable, Option B)** — the opt-in `managed` execution mode behind the unchanged `LLMProvider` seam: the engine stays **local** and only LLM egress is proxied through Relavium's gateway (`ManagedGatewayProvider`); accounts + device-flow auth, key vault + pools, idempotent real-time metering (reserve→settle, UNIQUE `request_id`) + nightly reconciliation, quota/hard-cap + per-day budget, merchant-of-record billing (prepaid + overage), cheap-default routing, abuse controls + kill switch, no-prompt-logging, usage dashboard. R1 (provider-ToS + merchant-of-record + DPA/KVKK/GDPR) cleared; **BYOK unchanged and first-class, no silent mode crossing.** | **2** | Phase 5 · 5.C (gateway + `ManagedGatewayProvider`), 5.E (metering), 5.K (launch) | diff --git a/docs/roadmap/current.md b/docs/roadmap/current.md index b2604377..eeb85ada 100644 --- a/docs/roadmap/current.md +++ b/docs/roadmap/current.md @@ -2,7 +2,7 @@ > Status: Living -> Last updated: 2026-06-26 +> Last updated: 2026-06-28 - **Related**: [README.md](README.md), [phases/phase-2-cli.md](phases/phase-2-cli.md), [deferred-tasks.md](deferred-tasks.md), [../project-structure.md](../project-structure.md), [../tech-stack.md](../tech-stack.md) @@ -28,14 +28,16 @@ and the [reference specs](../reference/). > **Live maintainer obligations:** (1) mark the CI `ci` job a **required check** in GitHub branch > protection (carried from Phase 0; optionally add `TURBO_TOKEN`/`TURBO_TEAM` secrets for the -> cross-runner remote cache); (2) now that **2.L** has landed (PR #49), add the **`NPM_TOKEN`** repo secret + npm 2FA -> so the tag-triggered `Release CLI` workflow can publish (the actual `npm publish` is maintainer-gated, +> cross-runner remote cache); (2) now that **2.L** has landed (PR #49) and **v0.1.1** has been cut, add the +> **`NPM_TOKEN`** repo secret + npm 2FA so the tag-triggered `Release CLI` workflow can publish — **still pending +> for the v0.1.1 tag** (the actual `npm publish` is maintainer-gated, > [ADR-0051](../decisions/0051-cli-distribution-thin-bundle-private-engine.md) / > [release-a-surface.md](../runbooks/release-a-surface.md)). ## What is active now -**Phase 2 — CLI (milestone M3) is in progress.** The CLI is the first real +**Phase 2 — CLI (milestone M3) is feature-complete** (every in-phase workstream 2.A–2.S merged; published as +**v0.1.1**). The CLI is the first real `@relavium/core` consumer and doubles as the engine's regression harness — validating the engine API ergonomics before the desktop and VS Code surfaces. **Landed:** the CLI skeleton + process contract (**2.A**) and the two-level config-resolution loader (**2.B**), both ✅ Done @@ -109,10 +111,19 @@ e2e** are ✅ **Done (PR #57, 2026-06-27)** — behind [ADR-0034](../decisions/0 Phase-3 go/no-go (capability without gating). Residual MCP hardening — the connect-by-validated-IP dialer, network header-auth, tool-list caching, mid-call abort propagation, and the stdio import-trust gate — is tracked in [deferred-tasks.md](deferred-tasks.md). -**Next pickup:** **2.J** — the last in-phase additive lane; the full status-aware order is the -[Remaining build order](phases/phase-2-cli.md#remaining-build-order) queue. See the +**Also landed — 2.J (the YAML-authoring lifecycle), the last in-phase lane:** `relavium create` (a +`@clack/prompts` wizard scaffolding an agent **or** a minimal single-agent workflow, validated against the +kind-appropriate `@relavium/shared` schema before write, dual-TTY-gated), `relavium import ` (schema- +validated copy-in with **project-global** id uniqueness), and `relavium export ` (a portable copy +**re-serialized from the validated AST** — canonical, comment-free, no provider key by construction), sharing one +`assertSlugAvailable` cross-catalog guard, ✅ **Done (PR #58, 2026-06-28)** — **no new ADR** (covered by +[ADR-0026](../decisions/0026-session-export-to-workflow.md)/[ADR-0047](../decisions/0047-cli-framework-commander-ink-clack.md)). + +**Phase 2 — CLI is feature-complete.** Every in-phase workstream (2.A–2.S) is merged and the published CLI is cut +as **v0.1.1**; M3 was reached at 2.K and the Phase-3 go/no-go held from 2.L. See the [Phase 2 workstreams](phases/phase-2-cli.md) and the -[sequencing plan](phases/phase-2-cli.md#sequencing--parallelization). +[sequencing plan](phases/phase-2-cli.md#sequencing--parallelization). The full status-aware history is the +[Remaining build order](phases/phase-2-cli.md#remaining-build-order) section (its queue is now empty). Carry-over hardening is tracked in [deferred-tasks.md](deferred-tasks.md) — Phase 2 picks items up as it first touches each file. Notable inheritances: 1.AH's host-wiring half diff --git a/docs/roadmap/phases/README.md b/docs/roadmap/phases/README.md index 88996965..ec36bf6c 100644 --- a/docs/roadmap/phases/README.md +++ b/docs/roadmap/phases/README.md @@ -22,6 +22,8 @@ phase-relative, never calendar dates. | 0 | [phase-0-foundations.md](phase-0-foundations.md) | Phase 1 | Turborepo + pnpm monorepo, tooling, CI, and the `@relavium/shared` Zod schemas — the foundation every package builds on. | | 1 | [phase-1-engine-and-llm.md](phase-1-engine-and-llm.md) | Phase 1 | The pure-TS engine (`@relavium/core`) and `@relavium/llm` — YAML→DAG parse, run loop, checkpoint/resume, retry, the provider seam + adapters. The most important phase. | | 2 | [phase-2-cli.md](phase-2-cli.md) | Phase 1 | The terminal CLI (`relavium`) — the engine's first real consumer and its regression harness, proving the engine end-to-end before any UI. | +| 2.5 | [phase-2.5-cli-consolidation.md](phase-2.5-cli-consolidation.md) | Phase 1 | CLI consolidation — a conversation-first **Home** at the bare invocation, a slash palette, reseat-less chat modes with per-tool approval, plus the bounded engine amendments and docs-debt Phase 2 deferred. Fixes the capability-gap root cause. | +| 2.6 | [phase-2.6-conversational-authoring.md](phase-2.6-conversational-authoring.md) | Phase 1 | Conversational authoring — a shared `@relavium/authoring` package, a chat that authors standards-valid `.relavium.yaml`, mid-session model reseat, and competitor-parity polish. | | 3 | [phase-3-desktop.md](phase-3-desktop.md) | Phase 1 | The Tauri v2 desktop **agent-management center** — canvas, run monitoring, keychain, Rust-delegated LLM egress (not an IDE). | | 4 | [phase-4-vscode.md](phase-4-vscode.md) | Phase 1 | The standalone VS Code extension — inline triggering and gate handling, bundling the engine in-process. | | 5 | [phase-5-managed-inference.md](phase-5-managed-inference.md) | **Phase 2** | Managed inference — the revenue beachhead: a thin metered gateway where the engine stays local and only LLM egress is proxied. Ships ahead of cloud execution. | diff --git a/docs/roadmap/phases/phase-2-cli.md b/docs/roadmap/phases/phase-2-cli.md index 288083e7..38078dd8 100644 --- a/docs/roadmap/phases/phase-2-cli.md +++ b/docs/roadmap/phases/phase-2-cli.md @@ -1,6 +1,6 @@ # Phase 2 — CLI -> Status: In progress (Product Phase 1, build phase 2). **2.A** (CLI skeleton + process contract) and **2.B** (config resolution) are ✅ **Done (PR #40, 2026-06-22)**, behind [ADR-0047](../../decisions/0047-cli-framework-commander-ink-clack.md) + [ADR-0048](../../decisions/0048-toml-config-parser.md); **2.D** (`run` → engine, the M3 keystone) is ✅ **Done (PR #41, 2026-06-22)**, and **2.F** (the `--json` CI machine-output contract) is ✅ **Done (PR #42, 2026-06-22)**, behind [ADR-0049](../../decisions/0049-cli-machine-output-contract.md), 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](../../decisions/0050-cli-history-db-at-rest-posture.md); and **2.C** (provider/key commands — OS keychain via `@napi-rs/keyring`) is ✅ **Done (PR #45, 2026-06-23)**, behind [ADR-0019](../../decisions/0019-cli-node-keychain-library.md) + [ADR-0006](../../decisions/0006-os-keychain-for-api-keys.md); and **2.E** (the `ink` streaming TUI) is ✅ **Done (PR #46, 2026-06-24)**, behind [ADR-0047](../../decisions/0047-cli-framework-commander-ink-clack.md); and **2.G** (the interactive human-gate prompt + `relavium gate` cross-process resume) is ✅ **Done (PR #47, 2026-06-24)**, behind [ADR-0047](../../decisions/0047-cli-framework-commander-ink-clack.md) (`@clack/prompts`; no new ADR) — **fully closing 2.K's deferred gate-resume half**; and **2.I** (the read commands `list` / `logs` / `status` / `gate list` over durable history) is ✅ **Done (PR #48, 2026-06-24)** (no new ADR — additive `@relavium/db` read seam + `@relavium/core` `parseAgent`); and **2.L** (packaging, distribution & install verification) is ✅ **Done (PR #49, 2026-06-24)**, behind [ADR-0051](../../decisions/0051-cli-distribution-thin-bundle-private-engine.md) — **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](../../decisions/0042-engine-media-storage-substrate-mediastore-deinline-retention.md)–[ADR-0046](../../decisions/0046-inline-media-out-via-generate-streaming-triad-deferred.md) (no new ADR), **the first additive lane done**; and **2.M** (the `relavium chat` REPL — the first user-facing `AgentSession` surface) is ✅ **Done (PR #54, 2026-06-26)** (no new ADR — covered by [ADR-0024](../../decisions/0024-agent-first-entry-point-agentsession.md)/[ADR-0047](../../decisions/0047-cli-framework-commander-ink-clack.md)/[ADR-0028](../../decisions/0028-workflow-resource-governance.md)/[ADR-0050](../../decisions/0050-cli-history-db-at-rest-posture.md)/[ADR-0029](../../decisions/0029-tool-policy-hardening.md); `read_media` **input** 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-shot `agent run` + `--fixture` cassette) — 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/mcp`** SDK-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 isolated `mcp-secret:*` keychain namespace, the by-name `ref` form, and the real-spawn e2e landed **PR #57, 2026-06-27**, behind [ADR-0034](../../decisions/0034-mcp-client-sdk-dependency.md) + [ADR-0052](../../decisions/0052-inbound-mcp-client-package-lifecycle-registration.md) + [ADR-0053](../../decisions/0053-mcp-network-transport-egress-security.md) — **the first two additive lanes (2.S, 2.R) now done, off the Phase-3 go/no-go**. The status-aware order for everything still open (next pickup: **2.J**) is the [Remaining build order](#remaining-build-order) queue. +> 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](../../decisions/0047-cli-framework-commander-ink-clack.md) + [ADR-0048](../../decisions/0048-toml-config-parser.md); **2.D** (`run` → engine, the M3 keystone) is ✅ **Done (PR #41, 2026-06-22)**, and **2.F** (the `--json` CI machine-output contract) is ✅ **Done (PR #42, 2026-06-22)**, behind [ADR-0049](../../decisions/0049-cli-machine-output-contract.md), 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](../../decisions/0050-cli-history-db-at-rest-posture.md); and **2.C** (provider/key commands — OS keychain via `@napi-rs/keyring`) is ✅ **Done (PR #45, 2026-06-23)**, behind [ADR-0019](../../decisions/0019-cli-node-keychain-library.md) + [ADR-0006](../../decisions/0006-os-keychain-for-api-keys.md); and **2.E** (the `ink` streaming TUI) is ✅ **Done (PR #46, 2026-06-24)**, behind [ADR-0047](../../decisions/0047-cli-framework-commander-ink-clack.md); and **2.G** (the interactive human-gate prompt + `relavium gate` cross-process resume) is ✅ **Done (PR #47, 2026-06-24)**, behind [ADR-0047](../../decisions/0047-cli-framework-commander-ink-clack.md) (`@clack/prompts`; no new ADR) — **fully closing 2.K's deferred gate-resume half**; and **2.I** (the read commands `list` / `logs` / `status` / `gate list` over durable history) is ✅ **Done (PR #48, 2026-06-24)** (no new ADR — additive `@relavium/db` read seam + `@relavium/core` `parseAgent`); and **2.L** (packaging, distribution & install verification) is ✅ **Done (PR #49, 2026-06-24)**, behind [ADR-0051](../../decisions/0051-cli-distribution-thin-bundle-private-engine.md) — **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](../../decisions/0042-engine-media-storage-substrate-mediastore-deinline-retention.md)–[ADR-0046](../../decisions/0046-inline-media-out-via-generate-streaming-triad-deferred.md) (no new ADR), **the first additive lane done**; and **2.M** (the `relavium chat` REPL — the first user-facing `AgentSession` surface) is ✅ **Done (PR #54, 2026-06-26)** (no new ADR — covered by [ADR-0024](../../decisions/0024-agent-first-entry-point-agentsession.md)/[ADR-0047](../../decisions/0047-cli-framework-commander-ink-clack.md)/[ADR-0028](../../decisions/0028-workflow-resource-governance.md)/[ADR-0050](../../decisions/0050-cli-history-db-at-rest-posture.md)/[ADR-0029](../../decisions/0029-tool-policy-hardening.md); `read_media` **input** 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-shot `agent run` + `--fixture` cassette) — 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/mcp`** SDK-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 isolated `mcp-secret:*` keychain namespace, the by-name `ref` form, and the real-spawn e2e landed **PR #57, 2026-06-27**, behind [ADR-0034](../../decisions/0034-mcp-client-sdk-dependency.md) + [ADR-0052](../../decisions/0052-inbound-mcp-client-package-lifecycle-registration.md) + [ADR-0053](../../decisions/0053-mcp-network-transport-egress-security.md) — **the first two additive lanes (2.S, 2.R) now done, off the Phase-3 go/no-go**; and **2.J** (the `create` / `import` / `export` YAML-authoring lifecycle — the last in-phase lane) is ✅ **Done (PR #58, 2026-06-28)** (no new ADR — covered by [ADR-0026](../../decisions/0026-session-export-to-workflow.md)/[ADR-0047](../../decisions/0047-cli-framework-commander-ink-clack.md)). **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](#remaining-build-order) section. - **Related**: [../README.md](../README.md), [phase-1-engine-and-llm.md](phase-1-engine-and-llm.md), [phase-3-desktop.md](phase-3-desktop.md), [../../reference/cli/commands.md](../../reference/cli/commands.md), [../../reference/contracts/config-spec.md](../../reference/contracts/config-spec.md), [../../reference/desktop/keychain-and-secrets.md](../../reference/desktop/keychain-and-secrets.md), [../../reference/contracts/sse-event-schema.md](../../reference/contracts/sse-event-schema.md), [../../reference/desktop/database-schema.md](../../reference/desktop/database-schema.md), [../../architecture/execution-model.md](../../architecture/execution-model.md), [../../architecture/shared-core-engine.md](../../architecture/shared-core-engine.md) @@ -385,7 +385,7 @@ NDJSON under `--json`); `status` reflects a paused run while it awaits a gate; given a ``), so an operator can pick the `gateId`; an unknown `runId` exits `2`. -### 2.J — `create` / `import` / `export` (YAML lifecycle) +### 2.J — `create` / `import` / `export` (YAML lifecycle) — ✅ **Done (PR #58)** 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. @@ -407,6 +407,22 @@ in and out of a project — the "workflow file is the invite" distribution path. `run`s; `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) **`create` scaffolds either an agent or a *minimal +> single-agent workflow*** (`input → agent → output` wrapping one inline agent), +> validated against the kind-appropriate `@relavium/shared` schema *before* write — +> the wizard needs an interactive terminal on **both** ends (TTY stdout + TTY stdin), +> failing loud (exit `2`) 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), MCP `env` secrets are `{{secrets.*}}` placeholders *by convention*, and +> `export`/`import` re-serialize faithfully (canonical form, comments dropped where a +> stray secret might hide) — the corrected framing is canonical in +> [keychain-and-secrets.md](../../reference/desktop/keychain-and-secrets.md). (3) **Ids +> are unique project-globally** (across both catalogs): `create`/`import` share one +> `assertSlugAvailable` guard — a same-kind clash needs `--force`, a cross-kind clash +> is always rejected — so a bare `` stays unambiguous for `export ` / `run`. + ### 2.K — Engine regression harness in CI — ✅ **Done (PR #43)** Adopt the CLI as the engine's canonical integration-test harness so every engine @@ -652,7 +668,7 @@ streams. Built behind injectable ports so desktop (§3.B) and VS Code (§4.N) re | 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`) | 2.J | — | +| **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)** | @@ -679,15 +695,15 @@ This is the status × plan view; the dependency rationale for every row lives in [Ordered waves](#ordered-waves-each-wave-is-internally-parallel-waves-gate-left-to-right) — this table does not restate them, it only sequences what remains. -> **Status (2026-06-27):** ✅ **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** done — **M3 reached, Phase-2 spine complete, all 7 Phase-3 go/no-go exit criteria hold (Phase 3 may start)**; **2.S** (media host-wiring) landed PR #52, the agent-first **chat** lane (2.M–2.Q) landed PR #54/#55, and the **inbound MCP client** (2.R) landed PR #56/#57 · next pickup: **2.J** (the last in-phase additive lane). +> **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`/`export` authoring 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). Only **2.J** remains in-phase — -> it does not block Phase 3.) +> 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) | |---|---|---|---| -| **1. 2.J** create / import / export | additive | cheap filler — the last in-phase lane; drop into any low-energy slot | 2.A ✓ | +| ✅ 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 ✓ | @@ -695,8 +711,9 @@ this table does not restate them, it only sequences what remains. - **Gate-closing backbone — complete (`2.L` landed, 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), and **2.R** (the inbound MCP client) landed (PR #56/#57); only - **2.J** remains in-phase and it does **not** block starting Phase 3. + 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, @@ -886,7 +903,7 @@ All must be true to start Phase 3 (desktop): | 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 strips references (2.C, 2.H, 2.J). | +| 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). | diff --git a/docs/roadmap/phases/phase-2.5-cli-consolidation.md b/docs/roadmap/phases/phase-2.5-cli-consolidation.md new file mode 100644 index 00000000..0d98855c --- /dev/null +++ b/docs/roadmap/phases/phase-2.5-cli-consolidation.md @@ -0,0 +1,412 @@ +# Phase 2.5 — CLI Consolidation and Conversational Home + +> Status: Planned. Spine: 2.5.A (tool-env + root-cause) → 2.5.B (Home) → 2.5.C (slash) → 2.5.E +> (modes + per-tool approval). Experience arm (off the spine, depends on B/C): 2.5.D / F / G. Additive +> lanes (no dependency chain): 2.5.H / I / J. + +- **Related**: [../README.md](../README.md), [phase-2-cli.md](phase-2-cli.md), [phase-2.6-conversational-authoring.md](phase-2.6-conversational-authoring.md), [phase-3-desktop.md](phase-3-desktop.md), [../../reference/cli/commands.md](../../reference/cli/commands.md), [../../reference/cli/chat-session.md](../../reference/cli/chat-session.md), [../../reference/cli/regression-harness.md](../../reference/cli/regression-harness.md), [../../decisions/README.md](../../decisions/README.md) (ADR-0054–0057) + +A consolidation phase between Phase 2 (CLI) and Phase 3 (desktop). It is **not only a CLI +surface phase**: it also discharges a set of bounded, pure engine amendments, a reasoning-render +lane, a regression-harness extension, and a documentation-debt cleanup that accumulated across +Phase 2. Phase 2.6 ([phase-2.6-conversational-authoring.md](phase-2.6-conversational-authoring.md)) +carries the heavier authoring and parity work that depends on this spine. + +## Goal + +Turn the bare `relavium` invocation into a branded, conversation-first **Home**, give it a +first-class slash-command palette and modern chat ergonomics, and **structurally fix the +capability-gap root cause** — all without breaking the `--json` / CI / non-TTY contract +([ADR-0049](../../decisions/0049-cli-machine-output-contract.md)) or any existing subcommand. +Along the way, close the bounded engine amendments and docs-debt that Phase 2 deferred. + +## Outcomes (Definition of Done) + +- `relavium` in a TTY opens a branded Home; under `--json` / `CI=true` / non-TTY it keeps the + current `helpInformation()` + exit `0` behaviour (regression-harness proven). +- The built-in chat agent's advertised tools (`read_file` / `list_directory` / `git_status`) + **actually work** (read-only host wired); `capability_unavailable` **never** surfaces as a bare + `internal`; a failed turn reports **real** token usage. +- A `/` palette + `/help` + `/doctor` + `/workflows` + `/clear`; chat modes + (ask / plan / accept-edits / auto) with **per-tool approval** and **mid-turn abort**, all on the + same session instance (no reseat for mode changes); `Ctrl+J` multiline, history recall, `@`-mention, + `!`-shell, bracketed paste. +- An onboarding wizard (hidden-prompt key → keychain) and `/models` (Home) over a connected-provider + model catalog; reasoning is visible in the TUI with live-turn latency feedback. +- The regression harness covers `Home → chat → resume → export`; the docs-debt is closed. + +## Scope + +### In scope + +- The bare-invocation Home, the in-app slash system, chat input ergonomics, the reseat-less mode + system with per-tool approval and mid-turn abort, onboarding, the Home-side `/models` picker, + reasoning rendering, the regression-harness extension, and docs reconciliation. +- **Bounded, pure engine amendments** (each behind a new ADR; the engine's architecture and the + platform boundary do **not** change — [CLAUDE.md](../../../CLAUDE.md) #5 holds): a `tool_unavailable` + error code, real-usage reporting on failed turns, a registry pre-dispatch approval hook, an + `AgentSession` pause/resume + mid-turn-abort state, an `agent:approval_requested` event, and a + reasoning host-emit event. +- The shared `read-only`/`read-write` tool-environment factory wired into **both** the chat path + and the workflow-run path (the host-side SSRF egress arm reuses the Phase-2 `EgressCapability`). + +### Explicitly out of scope (→ Phase 2.6 or Phase 3) + +- Conversational (model-generated) authoring, the `@relavium/authoring` package promotion, mid-session + **model** reseat, session `{{ctx.*}}` interpolation, and competitor-parity polish — all + [phase-2.6-conversational-authoring.md](phase-2.6-conversational-authoring.md). +- `/compact` model-summarised compaction (the engine has no summarisation primitive — Phase 2.5 ships + only deterministic `/trim`), `read_media` input (D12), full-fidelity reseat tool-context, in-app + scrollback/pager, a **type-ahead message queue while a turn runs** (the in-flight key-swallow is handled + for approval input in 2.5.E, but queuing the *next* message is deferred), live provider `/v1/models` + fetch, and a multi-pane dashboard — Phase 3 / later (tracked in [../deferred-tasks.md](../deferred-tasks.md)). + +## Work breakdown + +### 2.5.A — Shared tool-environment factory and capability-gap root-cause fix + +The spine. The built-in chat agent advertises `read_file` / `list_directory` / `git_status` +(`apps/cli/src/chat/default-agent.ts`) and its system prompt already tells the model to *"say so +plainly"* when a tool is unavailable — a **designed** graceful path. But the CLI wires a fail-closed +`ToolHost` on both the chat path (`apps/cli/src/chat/session-host.ts`) and the workflow-run path +(`apps/cli/src/engine/build-engine.ts`), so the first tool call throws `capability_unavailable`, +which `codeForToolError` (`packages/core/src/engine/agent-turn.ts`) maps to a bare `internal`. This +workstream restores and completes that path with a single shared factory. + +**Tasks:** + +- Add a shared `assembleToolEnv(mode, fsScopeTier)` factory returning `{ host: ToolHost, policy: + ToolPolicy }` — keeping the three concepts separate: `ToolHost` capability arms + (`fs`/`process`/`egress`/`os`/`mcp`/`outputStore`, [tool-registry.md](../../reference/shared-core/tool-registry.md)), + `ToolPolicy` allowlists (`allowedCommands`/`allowedDomains`, + [workflow-yaml-spec.md](../../reference/contracts/workflow-yaml-spec.md)), and the + `ToolDispatchContext` (`fsScope`). Wire it into **both** `session-host.ts` and `build-engine.ts`, + deleting the two inline host expressions; always use conditional spread (`exactOptionalPropertyTypes`) + so the existing MCP-merge stays a true **merge**, not a replace (the run path currently *replaces* + — a latent bug once a sibling arm is added). +- Wire the host-side `fs` (read-only by default) **and the `process` arm** so the chat agent's three + tools work — `read_file` / `list_directory` use `fs`, but `git_status` spawns `git` through + `requireProcess`, so `fs` alone would still fail it (the same root cause). `run_command` stays + unadvertised (advertise-filter) and denied by an empty `allowedCommands`, so the `process` arm only + serves the pre-approved `git_status` in the default profile. Add the `fs`-read-write and `egress` + tiers for the higher modes (egress reuses the Phase-2 SSRF-validated `EgressCapability.fetch`), with a + dedicated security review. +- **Engine amendment (EA1):** map the existing dispatch-layer `capability_unavailable` + (`ToolUnavailableError`) to a new portable `ErrorCode` (proposed `tool_unavailable`) in `constants.ts` + instead of `internal` — `codeForToolError` (`agent-turn.ts`) is the single change point — so a missing + capability is surfaced with the tool name and an actionable message. +- **Engine amendment (EA2):** report **real** accumulated usage on a failed turn. The usage source is the + turn-core tracker; EA2 adds a `usage` field to `AgentTurnError` (a new public surface) which the catch + then emits. Touch **only** the two locations where a provider actually engaged (the `AgentTurnError` + and unclassified branches in `agent-session.ts`; the unclassified branch still re-raises); the turn-cap + and budget-pause branches are zero **by design** and must not be changed. +- Filter the model-advertised tool set by the wired capabilities (advertise-filter): an unwired tool + is never offered, so the model cannot call it and the designed "say so plainly" path applies. + +**Acceptance:** one shared factory feeds both paths; the built-in agent's `read_file` works in a TTY +chat; an unwired tool is not advertised; a capability gap surfaces as a named, actionable +`tool_unavailable` (never `internal`); a failed turn reports real usage; an MCP-plus-`fs` run keeps +both arms (merge, not replace). A security review of the host capability seam passes. **Required ADR: +host-capability seam.** + +### 2.5.B — Bare-invocation Home (single ink tree, HomeStore, bracketed paste) + +Today the bare invocation prints help and exits `0` (`apps/cli/src/run.ts`); `commander` deliberately +has no default action (`apps/cli/src/program.ts`). This workstream adds a branded Home at that one +extension point — a read-only management strip (recent sessions / runs / agents over the durable +`history.db`) above a live prompt, where typing drops straight into chat. + +**Tasks:** + +- Gate the bare-invocation branch in `run.ts`: open the Home only when `stdoutIsTty && stdinIsTty && + global.json !== true && !isCiEnv(io.env)`; otherwise keep `helpInformation()` + exit `0`. Use the + **existing** `isCiEnv` helper (`apps/cli/src/process/output-mode.ts`, which already treats `CI=1`/any + truthy `CI` as CI — not a bare `env.CI !== 'true'` that would miss `CI=1`) and the **existing** + `stdinIsTty` field on the `io` seam (`apps/cli/src/process/io.ts` — already wired for the `create` + wizard), so this adds only a TTY-gate branch, no new IO surface. Do **not** add a `commander` default + action. +- Render Home and Chat as **one ink tree with conditional rendering** (a single `useInput` owner) — + not two mounted apps — so the raw-mode owner never conflicts; trigger the async `buildChatSession` + **after** the ink mount with an explicit loading state, and route a build failure back to Home. +- Add a `HomeStore` that reads the durable `history.db` (recent sessions/runs/agents) with an + "Attention required" section (pending gates / failed runs first, most-recent within a group) above the + neutral "Continue" list; below **80×24**, degrade by rendering a single "Terminal too small (WxH) — + resize to at least 80×24" message and suspending the Home render until a terminal **resize** arrives — + listen on `process.stdout`'s cross-platform `'resize'` event (backed by `SIGWINCH` on POSIX) rather than a + bare `SIGWINCH` binding, which is unreliable on Windows — never a broken/garbled TUI. The `history.db` aggregation must stay fast at scale (see the 2.5.I performance + budget) — index the read query, do not full-scan. +- **Author `docs/reference/cli/home.md`** as the canonical contract for the Home surface — an exit-criterion + deliverable, not a parenthetical: the TTY/CI gate, the command/slash taxonomy + manifest shape, `@`-mention + semantics, the mode keymap, the footer hint-bar layout, and the min-terminal-size degrade. +- Wire a single signal lifecycle (SIGINT/SIGTERM) covering Home, the in-Home chat, and MCP teardown + (`closeMcp`): a **clean** Home exit (e.g. `/exit`) returns `0`, while a **signal-driven** termination runs + teardown then exits with the conventional `128+signo` (`130` SIGINT / `143` SIGTERM) so shell pipelines + can still detect the interruption; a chat's exit-code-`4` is consumed by the Home loop, never leaked. +- Add **bracketed paste** (DECSET 2004) to the chat input so a pasted multi-line block is taken + literally instead of submitting early. +- Derive a session title from the first user message (first ~40 chars) so the Home list is readable + (an LLM-summarised title is Phase 3). + +**Acceptance:** `relavium` in a TTY opens Home; `--json` / piped / CI (`CI=true` **and** `CI=1`) keep help ++ exit `0`; Home → chat → Home transitions never corrupt the terminal; a slow/failed `buildChatSession` +shows a loading state and recovers to Home; pasting a multi-line YAML block produces one message; below +80×24 a resize prompt shows instead of a broken TUI; `docs/reference/cli/home.md` is published. **Required +ADR: bare-invocation interactive-entry contract.** + +### 2.5.C — In-app slash registry, command palette, `/help`, `/doctor`, `/workflows` + +A single shared dispatch table behind `commander`, the palette, and the in-REPL slash commands — a +small, canonical, **alias-free** set (avoiding the 40–100-command sprawl of other agent CLIs). + +**Tasks:** + +- Extract the per-command wiring currently inside the `register*` bodies (`apps/cli/src/commands/specs.ts`) + into a shared dispatch module a `commander` action, the palette, and a slash command all call. +- Implement a filterable `/` palette and the Home/chat slash taxonomy (canonically homed in + `docs/reference/cli/home.md`, authored in 2.5.B); a command manifest + (`{ id, label, canonical, effect, modeScope }`) is the single source for the palette, slash help, + and `relavium --help --json`. +- `/doctor` — a staged health check (fast: keychain / config / wired tool capabilities; `--deep`: + provider-key validation + MCP connectivity), rendered incrementally as each check settles. +- `/workflows` — the disk-discovery catalog (`apps/cli/src/workflows/catalog.ts`); `/help` — the + palette; an unknown slash prints a sanitized, secret-free hint. +- **Discoverability:** a `/shortcuts` command (derived from the manifest — zero extra source) and a + persistent footer hint-bar surfacing the 2–3 most relevant keys per context (so the mode/`@`/`!`/`Esc` + ergonomics of 2.5.D/E are findable, not hidden); `/cost` — the session-cumulative spend (an + `effect: read` manifest entry; the per-model breakdown is 2.6.C). + +**Acceptance:** every existing subcommand also reachable through the palette/slash with no behaviour +change; `/doctor` reports the real health (and would have explained the original root-cause symptom); +`/workflows` lists discovered workflows; `/shortcuts` and the footer hint-bar make the keymap discoverable; +the palette filters; an unknown slash is safe. **Required ADR: in-app slash command system + command +manifest.** + +### 2.5.D — Chat input ergonomics + +Today the ink editor is single-line only, with no history or cursor movement +(`apps/cli/src/render/tui/chat-ink.tsx`). REPL-only; no engine/seam change. + +**Tasks:** `Ctrl+J` newline (canonical; `Shift+Enter` optional, never relied on); `↑/↓` history + +`Ctrl+R` reverse search; readline cursor/word motions; `@`-mention (Tab-completion, `.gitignore`/ +`.relavium`-respecting, binary detection, token-limit warning) to inject file context explicitly; +`!`-shell bounded by `ToolPolicy.allowedCommands` (off in ask/plan, gated in accept-edits). **Esc +per-turn abort is NOT here** — it requires an engine state and lives in 2.5.E (EA7). + +**Acceptance:** multi-line input, history recall/search, and `@`/`!` work; the raw-mode owner is +preserved; zero engine/seam change. + +### 2.5.E — Chat modes (reseat-less) + per-tool approval + mid-turn abort + +The capability workstream. Claude-Code-style modes — but a mode is a **policy layer on the same +session instance**, not a reseat: the `ToolHost` is bound full-capability for the session lifetime, +and the mode controls only (a) the model-advertised tool subset (a per-turn `buildLlmTools` filter) +and (b) the per-dispatch approval policy. This is lossless (no tool-context loss), cheap (no new +instance), and two-layer safe: the advertise-filter (best-effort) plus the **fail-closed `confirmAction` +approval policy** (authoritative — `enforcePolicy` is mode-agnostic and inert for `write_file`). + +**Tasks:** + +- Add a mutable `#mode` to `AgentSession`, snapshotted per turn; the four modes + (ask / plan / accept-edits / auto) map to an advertised-tool subset + an approval policy. + `Shift+Tab` cycles `ask → plan → accept-edits`; `auto` is explicit-only (`/mode auto`); **no + one-key bypass valve** ([ADR-0029](../../decisions/0029-tool-policy-hardening.md)). +- **Protected paths:** `.git/`, `.relavium/`, shell rc files are never auto-written in any mode. +- **Per-tool approval (new vertical — not the workflow gate), fail-closed:** a registry pre-dispatch hook + (**EA3** — `confirmAction?`, host-injected like `ToolHost` so the engine boundary (ADR-0037) holds; runs + between the `enforcePolicy` check and the side-effect in `packages/core/src/tools/registry.ts`). **Note:** + `enforcePolicy` is **inert for `write_file`** (its `FS_POLICY` has `requiresGateApproval:false` and no + command/domain allowlist applies), so `confirmAction` is the **authoritative** mode gate and must be + **fail-closed** — when a write/process/egress arm is wired, a write-/exec-/egress-class dispatch **requires** + a decision; **absent hook ⇒ deny** (so an advertise-filter or wiring bug can never let `ask` mode write). + Plus: an `agent:approval_requested` event (**EA5**), an `AgentSession` pause/resume state (**EA4**), a REPL + `[approve]/[reject]/[comment]` intercept that bypasses the in-flight key-swallow gate (no deadlock), a + typed `ToolDeniedByUserError` (the existing `tool_denied` `ErrorCode`, already non-retryable), and a + session-scoped, **in-memory** once/always cache (not persisted across resume; **once** = this invocation + (tool+args), **always** = this tool id for this session instance). The existing `gateApproved` flag is a + one-way static deny and is **not** reused. +- **Mid-turn abort (EA7):** an `Esc` that aborts the in-flight turn but keeps the session alive (today + `cancel()` is terminal). It emits **one** `session:turn_completed` (abort stop-reason), rolls back the + pending user message, and returns `#status` to `idle` — there is **no** new `aborted` status and it is + **not** `cancel()`/`session:cancelled` (which kills the session). Shares the EA4 pause/resume + `AbortSignal` + machinery; the session-level budget pause/resume deferred from Phase 2 rides the same machine. + +**Acceptance:** `Shift+Tab` switches modes instantly with **no** tool-context loss; ask mode advertises +read-only tools; accept-edits prompts before each write with `[a]/[r]/[c]` and an once/always memory; +a rejection is a clean `tool_denied`, not a retry; `Esc` aborts a turn and the session continues; auto +is sandbox-bounded with protected paths honoured. A security review of the reseat-less mode model +(defense-in-depth trade-off) passes. **Required ADR: per-tool approval + reseat-less chat mode system +(incl. mid-turn abort).** + +### 2.5.F — `/clear` and `/trim` + +`/clear` starts a new conversation (the old session stays persisted and resumable). `/trim` is a +**deterministic** history trim that finally consumes the dead `max_messages` config field +(`packages/shared/src/config.ts` — plumbed but never read). `/compact` (model-summarised) is **Phase +3** (the engine has no summarisation primitive); a no-op stub is forbidden — instead the slash registry +carries a **recognized-but-deferred** entry so `/compact` prints *"not yet available — use `/trim` for a +deterministic trim; model-summarised compaction is planned"*, distinct from the generic unknown-slash hint. + +**Acceptance:** `/clear` opens a fresh session, the prior one resumable; `/trim` bounds history by +`max_messages` with no LLM call; `max_messages` is no longer dead. + +### 2.5.G — Onboarding wizard and `/models` (Home model catalog) + +**Tasks:** a `@clack` first-run wizard from the key-less Home (provider → **hidden** stdin key → +keychain, with a write-failure fallback and an env-key import offer) — reusing the two existing +ink↔clack custody patterns (the gate prompter and the 2.J create wizard); a shared `modelCatalog` +helper deriving available models from the `@relavium/llm` pricing registry filtered to +connected providers, with a staleness/deprecation guard; `/models` in Home sets the next session's +model (writing `[chat].default_model`); `provider list` shows verification state; the `[chat].max_turns` +surface wiring deferred from Phase 2 lands here. + +**Acceptance:** a key-less first run reaches a working chat via the wizard (key in the keychain, never +on disk); `/models` lists connected-provider models and sets the default; a deprecated catalog entry is +flagged. (Mid-chat model switch via reseat is Phase 2.6.) + +### 2.5.H — Reasoning rendering and live-turn feedback + +The `@relavium/llm` seam already carries reasoning chunks (`StreamChunk` reasoning deltas, folded in +`packages/core/src/engine/agent-turn.ts`); the gap is purely the host-emit + TUI render. Additive. + +**Tasks:** **engine amendment (EA6):** emit reasoning over a new `agent:reasoning` event (today `foldChunk` +emits `agent:token` on a `text_delta` chunk and only **accumulates** reasoning deltas with no event of +their own) — the `@relavium/llm` seam is untouched (it already carries the reasoning chunks); EA6 is a new +event in the shared event union (canonical home [sse-event-schema.md](../../reference/contracts/sse-event-schema.md), +amending the [ADR-0036](../../decisions/0036-run-loop-substrate-event-bus-and-execution-host.md) event +substrate — **not** ADR-0030, which is the seam shape). Plus a collapsible "thinking" panel in the +TUI (default collapsed, toggle); live-turn latency feedback ("thinking… {elapsed}s · Esc to stop", +the turn-start timestamp already exists in the view-model); a visible elision marker when the live +buffer truncates (today a silent loss); per-attempt model attribution for the token line (the Phase-2 +follow-up) lands with EA2's accuracy surface. +- **Actionable error taxonomy:** extend the error surface beyond the capability gap (2.5.A). Two classes + the engine **already** marks retryable in `RETRYABLE_ERROR_CODES` (`@relavium/shared/constants.ts`) but + the chat renders opaquely — `provider_rate_limit` (`429` → backoff + visible failover) and + `provider_unavailable` (→ visible failover); plus three classes the engine does **not** map as retryable + and the chat must surface actionably — context-overflow (a `bad_request`/fatal → suggest `/trim`), + keychain-locked mid-session, and MCP-server timeout. Each renders a one-line recovery hint and makes + explicit that **the session survives** (the "say so plainly" philosophy, extended from tools to + transport/quota). + +**Acceptance:** reasoning is visible and toggleable; the user sees elapsed time and the abort hint; +truncation is visible; each operational error class renders an actionable recovery hint with the session +intact; the seam is not modified. + +### 2.5.I — Regression harness and concurrency hardening + +**Tasks:** extend the regression harness ([regression-harness.md](../../reference/cli/regression-harness.md)) +to cover `Home → chat → resume → export` with an agent/chat replay cassette (the recorded-LLM replay +infrastructure is the prerequisite, and the deferred multimodal conformance fixtures reuse the same +mechanism); a concurrent-writer e2e (`relavium chat` + `relavium run` against the shared `history.db`), +evaluating `BEGIN IMMEDIATE` + `SQLITE_BUSY` retry (WAL + `busy_timeout` already exist) and wrapping +`loadFull` in a single read transaction (torn-read guard); cross-platform ink/raw-mode verification on +Windows. + +- **Performance budgets:** assert measurable targets so "first-class" is verifiable — Home cold-open + ≤ a defined budget at 1000 sessions (the indexed `history.db` query, not a full scan), a first-token + latency feedback within the live-turn render, and the 80×24 minimum-terminal-size degrade. Capture them + as harness checks, not prose. + +**Acceptance:** the new e2e chain is green in CI; a concurrent chat+run does not corrupt history; the +performance budgets (Home cold-open at 1000 sessions; 80×24 minimum) hold; the harness gates the +backward-compatibility exit criterion. + +### 2.5.J — Documentation reconciliation and dead-code cleanup + +**Tasks:** correct the surface-blind "encrypted history" wording (`docs/uvp.md`, `docs/vision.md`, +`docs/tutorials/cli/start-a-chat-session.md`) to the accurate CLI posture — **unencrypted**, protected +by `0700`/`0600` + keychain (no credentials at rest), per +[ADR-0050](../../decisions/0050-cli-history-db-at-rest-posture.md) (the canonical references are already +correct); reconcile the roadmap status surfaces (`docs/roadmap/current.md`, +[phase-2-cli.md](phase-2-cli.md), [CLAUDE.md](../../../CLAUDE.md)) now that **2.R and 2.J have both +landed**, and complete the central roadmap narrative for 2.5/2.6 ([../README.md](../README.md) — the phase +index and the dependency graph already include them; reconcile the milestone-spine prose); handle the +`NO_COLOR` / `FORCE_COLOR` env standards (today only the `--no-color` flag is honoured). + +**Acceptance:** no tracked doc claims the CLI `history.db` is encrypted; the roadmap reflects the landed +state; `NO_COLOR` is honoured. + +## Milestones + +| In-phase | Completed by | Outcome | +|----------|--------------|---------| +| M2.5-1 Secure base | 2.5.A | Root-cause closed (capability gap + merge asymmetry); host seam reviewed | +| M2.5-2 Home + entry + onboarding | 2.5.B + 2.5.C + 2.5.D + 2.5.F + 2.5.G | First-class entry + ergonomics + onboarding | +| M2.5-3 Modes + observability | 2.5.E + 2.5.H | Safe reseat-less mode system + per-tool approval + reasoning | +| M2.5-4 Consolidation | 2.5.I + 2.5.J | Harness + concurrency + docs-debt | + +## Sequencing & parallelization + +**2.5.A is absolute-first** — without the wired tool-environment the Home re-creates the same root-cause +("inspect this doc" → fails). Then two parallel arms: (i) experience: 2.5.B → 2.5.C → 2.5.D / 2.5.F / +2.5.G; (ii) capability: 2.5.E. The additive lanes (2.5.H / I / J) are not on the spine and run in +parallel at any point. + +## Dependencies + +- **Phase 2** complete (chat REPL, history, providers, MCP, the 2.J authoring core that 2.5.A's tool + host and Phase 2.6 build on). +- The pure engine amendments EA1–EA7 are filed back against `@relavium/core` / `@relavium/shared` as + Phase-1 amendments, each behind the ADR mapped in the [Engine amendments appendix](#engine-amendments-appendix-ea1ea7) + (EA1/EA2 → ADR-0055; EA3/EA4/EA5/EA7 → ADR-0057; EA6 amends ADR-0036); the engine architecture and the + platform boundary are unchanged. + +## Exit criteria (go / no-go → Phase 2.6) + +1. `relavium` opens Home in a TTY; the `--json` / CI / non-TTY backward-compatibility is proven by the + extended regression harness. +2. The capability-gap root cause is closed (`tool_unavailable`, real usage, advertise-filter, merge + asymmetry); the host-capability and mode-system security reviews pass. +3. All existing subcommands and the `--json` contract ([ADR-0049](../../decisions/0049-cli-machine-output-contract.md)) + are unbroken. +4. Modes, per-tool approval, mid-turn abort, reasoning rendering, onboarding, and `/models` (Home) ship; + the docs-debt is closed. +5. The required ADRs are Accepted; `docs/reference/cli/home.md` is the canonical Home reference. + +## Required ADRs + +Drafted as **Proposed** alongside this plan; each is reviewed and finalized (→ Accepted) when its +workstream begins. + +1. [ADR-0054](../../decisions/0054-cli-bare-invocation-interactive-home.md) — bare-invocation interactive + Home (TTY only), preserving the meta-op contract (2.5.B). +2. [ADR-0055](../../decisions/0055-cli-host-capability-seam-tool-environment-factory.md) — shared + tool-environment factory (`ToolHost` / `ToolPolicy` / ctx as separate channels); a new security surface + (2.5.A). +3. [ADR-0056](../../decisions/0056-cli-in-app-slash-command-system-and-manifest.md) — in-app slash command + system + command manifest (2.5.C). +4. [ADR-0057](../../decisions/0057-cli-chat-modes-and-per-tool-approval.md) — reseat-less chat modes + + per-tool approval + mid-turn abort (2.5.E). + +EA6 (the new `agent:reasoning` event) is an additive event in the shared event union; it does not need a new +top-level ADR — it **amends** [ADR-0036](../../decisions/0036-run-loop-substrate-event-bus-and-execution-host.md) +(the event substrate) with a dated note and updates +[sse-event-schema.md](../../reference/contracts/sse-event-schema.md) when 2.5.H lands. + +## Engine amendments appendix (EA1–EA7) + +Each amendment is pure and platform-free (the engine architecture and the `LLMProvider` seam are unchanged); +each ships behind the ADR below and updates its canonical-home spec + the drift-pin test where it touches a +shared contract. + +| EA | What | Where | ADR / canonical home | +|----|------|-------|----------------------| +| EA1 | `tool_unavailable` portable `ErrorCode` + `codeForToolError` remap (was `→ internal`) | `@relavium/shared/constants.ts` (`ERROR_CODES`) + `agent-turn.ts` | ADR-0055; `sse-event-schema.md` (error code) + drift-pin | +| EA2 | real usage on a failed turn — `usage` field on `AgentTurnError`, emitted (only the provider-engaged branches) | `agent-turn.ts` + `agent-session.ts` | ADR-0055 | +| EA3 | pre-dispatch, **fail-closed** `confirmAction` hook (host-injected, ADR-0037-clean) | `tools/registry.ts` + `tools/types.ts` (`ToolDispatchContext`) | ADR-0057 | +| EA4 | `AgentSession` pause/resume state (also carries EA7 + the deferred budget pause) | `agent-session.ts` | ADR-0057; `agent-session-spec.md` | +| EA5 | `agent:approval_requested` stream event (in the `agent:*` namespace) | shared event union | ADR-0057; `sse-event-schema.md` + drift-pin | +| EA6 | `agent:reasoning` stream event (host-emit; seam already carries reasoning) | `agent-turn.ts` + shared event union | **amends ADR-0036**; `sse-event-schema.md` + drift-pin | +| EA7 | mid-turn abort (`Esc` → one `session:turn_completed`/abort → `idle`; no new status) | `agent-session.ts` | ADR-0057; `agent-session-spec.md` | + +## Risks & mitigations + +| Risk | Mitigation | +|------|------------| +| 2.5.A skipped → Home is an empty promise | Spine strict; until then Home shows a read-only badge and `/tools`/`/doctor` stay honest | +| Scope drifts toward an IDE (Home + agent panel) | Home is management + chat only; **no** file tree / editor / terminal; the strip is read-only ([ADR-0007](../../decisions/0007-desktop-is-not-an-ide.md) boundary) | +| Engine-purity leak | All Home/ink/palette code stays in `apps/cli`; Home is a consumer of existing seams; engine amendments are pure ([CLAUDE.md](../../../CLAUDE.md) #5) | +| Per-tool approval scope explosion | Isolated in 2.5.E behind its own ADR; 2.5.D / F are independent of it | +| Reseat-less mode weakens defense-in-depth | `ask` keeps fs-write physically bound but advertise-filtered + guarded by the **fail-closed `confirmAction` floor** (absent hook ⇒ deny; `enforcePolicy` is inert for `write_file`) + protected-paths; called out in the mode-system ADR + security review | +| Two raw-mode owners (Home↔Chat / ink↔clack) collide | Single ink tree conditional render; clack custody handoff (existing gate pattern) | + +Part of [roadmap/](../README.md). Carry-over hardening lives in [../deferred-tasks.md](../deferred-tasks.md). diff --git a/docs/roadmap/phases/phase-2.6-conversational-authoring.md b/docs/roadmap/phases/phase-2.6-conversational-authoring.md new file mode 100644 index 00000000..cdee0026 --- /dev/null +++ b/docs/roadmap/phases/phase-2.6-conversational-authoring.md @@ -0,0 +1,216 @@ +# Phase 2.6 — Conversational Authoring and Parity + +> Status: Planned. Depends on the Phase 2.5 spine (the wired tool-environment and the per-tool +> approval / mode system). Spine: 2.6.A (`@relavium/authoring` package) → 2.6.B (conversational +> authoring agent). Additive: 2.6.C / D / E. + +- **Related**: [../README.md](../README.md), [phase-2.5-cli-consolidation.md](phase-2.5-cli-consolidation.md), [phase-2-cli.md](phase-2-cli.md), [phase-3-desktop.md](phase-3-desktop.md), [phase-4-vscode.md](phase-4-vscode.md), [../../reference/contracts/workflow-yaml-spec.md](../../reference/contracts/workflow-yaml-spec.md), [../../reference/contracts/agent-yaml-spec.md](../../reference/contracts/agent-yaml-spec.md), [../../reference/shared-core/node-types.md](../../reference/shared-core/node-types.md), [../../decisions/README.md](../../decisions/README.md) (ADR-0058–0060) + +The second half of the consolidation work, split out from +[phase-2.5-cli-consolidation.md](phase-2.5-cli-consolidation.md) because it is ADR-heavy and depends on +that spine. It realises the product's tagline — *"Start as an agent. Ship the workflow."* — at the +terminal: a conversation can now **author** a standards-valid Relavium workflow/agent, switch models +mid-session, and reach competitor-parity ergonomics. + +## Goal + +Let a `relavium chat` conversation produce a **standards-valid** `.relavium.yaml` from a free-text +request, promote the existing authoring core into a shareable `@relavium/authoring` package so every +surface can consume it, enable mid-session model switching, and close the remaining parity polish — all +on top of the Phase 2.5 tool-environment and approval system. + +## Outcomes (Definition of Done) + +- A `@relavium/authoring` package (`@relavium/authoring`) wraps parse / validate / serialize / scaffold + for workflows and agents; the CLI's `create` / `import` / `export` consume it; desktop and VS Code can + too. +- Every authored artifact passes a single `validateAuthoredWorkflow` pre-flight (parse **and** catalog + validation); a failure returns a field-named, secret-free error the model self-corrects against. +- A conversational authoring agent turns a free-text request into a strict-valid `.relavium.yaml`, writes + it only under accept-edits/auto with a scope-tiered host, and offers to `/run` it. +- `/models` switches the bound model **mid-session** (a host-side reseat), carrying cost/turn and + persisting per-message model attribution — with an explicit tool-context-loss notice. +- Session `{{ctx.*}}` prompt interpolation lands, unblocking `agent run --input`; parity polish + (`/rewind`, `/fork`, advanced `@`-injection, markdown render, `/theme`) ships. + +## Scope + +### In scope + +- The `@relavium/authoring` package promotion + catalog-aware pre-flight back-port; the conversational + authoring agent + its product-side knowledge pack; mid-session model reseat; session `{{ctx.*}}` + interpolation; and the parity/polish lane. + +### Explicitly out of scope (→ Phase 3 / later) + +- `/compact` model-summarised compaction (no engine summarisation primitive); full-fidelity reseat + tool-context (the persister/schema extension, 1.X/1.Z); `read_media` input (D12); in-app + scrollback/pager; live provider `/v1/models` fetch; a multi-pane dashboard. Tracked in + [../deferred-tasks.md](../deferred-tasks.md). + +## Work breakdown + +### 2.6.A — `@relavium/authoring` package promotion + catalog-aware pre-flight + +The authoring core already exists in-tree (`apps/cli/src/authoring/authoring.ts`, landed with 2.J): +it wraps `parseWorkflow` / `serializeWorkflow` / `parseAgent` / `buildAuthored` / `detectAndParse` from +`@relavium/core` and drives the `create` / `import` / `export` commands. **Decision (maintainer):** +promote it to a shared `@relavium/authoring` package so desktop ([phase-3-desktop.md](phase-3-desktop.md)) +and VS Code ([phase-4-vscode.md](phase-4-vscode.md)) can consume the same authoring core, not just the CLI. + +**Tasks:** + +- Scaffold `packages/authoring` (`@relavium/authoring`) — pure TS, platform-free (engine-purity holds) + — and **extract-and-decouple** the existing `apps/cli/src/authoring/` core into it (it is **not** a free + move: the core imports `CliError`, `discoverCatalog`, and `findProjectConfigDir` from `apps/cli`, which + a package may not import — a forbidden `packages → apps` back-edge). Cut those three: replace `CliError` + with a platform-free typed error the CLI maps to exit codes at the boundary; keep catalog **discovery** + and `findProjectConfigDir` CLI-side and pass the catalog **in**. Add an import-zone lint fence (Phase-0 + seam-fence pattern) banning `packages/authoring → apps/cli`. Follow the add-package procedure + ([.claude/skills/add-package/SKILL.md](../../../.claude/skills/add-package/SKILL.md)). +- Expose a single `validateAuthoredWorkflow(yaml, catalog)` = `parseWorkflow` **+** + `validateWorkflowWithCatalog` pre-flight. The existing `create` / `import` / `export` pre-flight is + **parse-only** (it does not call the catalog validator — only the run path does); **back-port** the + catalog-aware pre-flight so wizard-authored and conversationally-authored artifacts share one front + end and `create` can never accept a model/modality the run path rejects. +- Add direct unit tests for the authoring core (`detectAndParse` / `buildAuthored` / + `validateAuthoredWorkflow`) — today only the command wrappers are tested. + +**Acceptance:** `@relavium/authoring` builds and imports **only** `@relavium/core` + `@relavium/shared` +(lint-fence enforced — no `apps/cli` back-edge); the CLI consumes it with `create` / `import` / `export` +round-tripping **unchanged** (regression-tested); `create` runs the same catalog-aware pre-flight the run +path uses; the core is directly unit-tested. **Required ADR: `@relavium/authoring` package + +conversational-authoring pre-flight contract.** + +### 2.6.B — Conversational workflow/agent authoring agent + +A `relavium chat` request such as *"define a workflow with these agents…"* produces a strict-valid +`.relavium.yaml`. This is the sibling of `chat-export` (which **replays** a transcript into a workflow); +here the model **generates** the artifact. + +**Tasks:** + +- Add an authoring agent (an `--agent` profile or a `/author` mode) whose system prompt references a + **product-side knowledge pack** — a model-readable cheat-sheet **derived from** the canonical specs + ([node-types.md](../../reference/shared-core/node-types.md), + [workflow-yaml-spec.md](../../reference/contracts/workflow-yaml-spec.md), + [agent-yaml-spec.md](../../reference/contracts/agent-yaml-spec.md)) and the Zod schemas, plus a minimal + valid example per node type. The knowledge is **derived, never restated** ([CLAUDE.md](../../../CLAUDE.md) + #8); a check proves no schema is duplicated. It **must not** live under `.claude/skills/` — those are + repo-development (Claude Code) procedures and the Relavium **product** agent never reads `.claude/`. +- The self-correct loop reuses the same `detectAndParse` / `validateAuthoredWorkflow` pre-flight: model → + YAML → pre-flight → field-named, secret-free error → model fixes (improve `AgentParseError` to carry + line/col, a Phase-2 follow-up). +- The artifact is written only under accept-edits/auto with the scope-tiered host (2.5.A/2.5.E), then the + Home offers *"Run it now with `/run`?"* — closing the author → run loop on one screen. +- **Discoverability of the UVP (proactive, opt-out):** because *"Start as an agent. Ship the workflow."* + is the product's promise, a chat that has run several tool turns or produced a reusable plan surfaces a + quiet, dismissible hint — *"turn this session into a workflow with `/export`"* — so the tagline is + discovered, not buried in a slash command. Opt-out via config; never interrupts a turn. + +**Acceptance:** a free-text request yields a strict-valid `.relavium.yaml` that passes the same pre-flight +as `relavium run`; an invalid draft is corrected via the secret-free error loop; the file is written only +with approval; the proactive `/export` hint appears (and is dismissible); a knowledge-restate check passes. +A security review of the write surface + the authored artifact's secret-taint gate passes. **Required ADR: +shared with 2.6.A.** + +### 2.6.C — Mid-session model reseat (`/models` mid-chat) + +A session binds one model for its lifetime ([ADR-0024](../../decisions/0024-agent-first-entry-point-agentsession.md); +the fallback plan is memoized). Switching the **model** mid-chat is therefore a host-side **reseat**: +reconstruct the transcript (`reconstructSessionState`) and start a new `AgentSession.resume` bound to the +new model/provider, carrying cost/turn. (Mode changes do **not** reseat — that is the reseat-less mode +system in 2.5.E.) + +**Tasks:** mid-chat `/models` performs the reseat; persist per-message `modelId` (the `session_messages` +schema already has the column — only the CLI persister wiring is missing); surface an explicit, shared +notice on the `chat-resume` family that **prior tool calls and file contents are not carried to the new +model** (the transcript is text-only; full-fidelity tool-context is Phase 3); show a per-model cost +breakdown. + +**Acceptance:** `/models` mid-chat continues the conversation on a new model from the next turn; cost/turn +carry; per-message model attribution persists; the context-loss notice is shown; "carries full context" is +never claimed. When [ADR-0059](../../decisions/0059-cli-mid-session-model-reseat.md) flips to Accepted, its +refinement of [ADR-0024](../../decisions/0024-agent-first-entry-point-agentsession.md) is recorded **in +place** on ADR-0024 with a dated `> Amended …` note + a Related forward-link (documentation-style §7), since +it refines without reversing. **Required ADR: mid-session reseat (model-only) — refines ADR-0024.** + +### 2.6.D — Session `{{ctx.*}}` prompt interpolation + +The agent system prompt is passed verbatim today (no template resolution), and `agent run --input` is +reserved/rejected until this lands. The conversational authoring agent and reseat both want session +context, so the Phase-2 deferral is pulled forward. + +**Tasks:** resolve `{{ctx.*}}` in the session system prompt against session-scoped variables (an engine +follow-up); unblock `agent run --input k=v`. + +**Acceptance:** `{{ctx.*}}` resolves in a session prompt; `agent run --input` is accepted and reaches the +prompt. **Required ADR: session `{{ctx.*}}` interpolation** (engine amendment). + +### 2.6.E — Parity and polish + +The competitor-parity ergonomics that are valuable but not on the spine: `/rewind` + `/fork` (the engine +already has per-node-boundary checkpoints to build on); advanced `@`-injection (glob/directory, respecting +ignore files); basic markdown + code-block rendering with a table layout (syntax highlighting is Phase 3; +a markdown dependency, if chosen, needs an ADR); `/theme` with at least one **high-contrast** and one +**colorblind-safe** theme (and semantic markers — `✓`/`✗`/`⏸` — that survive `--no-color`/`NO_COLOR`, so +the experience is not colour-dependent, degrading to ASCII equivalents — `[v]`/`[x]`/`[||]` — when the +terminal lacks Unicode support so the glyphs never render as broken boxes). Note any screen-reader limitations of the raw-mode TUI as a +documented constraint with a non-TTY fallback. + +**Acceptance:** `/rewind`/`/fork` work over the checkpoint substrate; `@`-injection handles globs/dirs; +markdown/code-blocks render; `/theme` switches themes including the high-contrast and colorblind-safe +options; the colour-free path stays legible via semantic markers. + +## Milestones + +| In-phase | Completed by | Outcome | +|----------|--------------|---------| +| M2.6-1 Authoring core shared | 2.6.A | `@relavium/authoring` + catalog-aware pre-flight | +| M2.6-2 Conversational authoring | 2.6.B + 2.6.D | "define a workflow…" produces valid YAML; `{{ctx.*}}` lands | +| M2.6-3 Reseat + parity | 2.6.C + 2.6.E | Mid-session model switch + parity polish | + +## Sequencing & parallelization + +2.6.A first (the package + shared pre-flight). Then 2.6.B (conversational authoring, depends on A + the +2.5.E approval/write path) in parallel with 2.6.C (reseat) and 2.6.D ({{ctx.*}}); 2.6.E is additive +polish throughout. + +## Dependencies + +- **Phase 2.5** complete — specifically 2.5.A (the wired write-capable tool-environment) and 2.5.E + (accept-edits per-tool approval), which gate the authoring write surface. +- **2.J** (the in-tree authoring core that 2.6.A promotes) — landed. + +## Exit criteria (go / no-go) + +1. `@relavium/authoring` is the shared authoring core; the CLI consumes it; `create` runs the catalog-aware + pre-flight. +2. A conversational request produces a strict-valid `.relavium.yaml` (pre-flight-proven, security-reviewed); + the knowledge pack is derived, not restated. +3. Mid-session `/models` reseat works with the context-loss notice and per-message attribution. +4. `{{ctx.*}}` interpolation lands and `agent run --input` is unblocked. +5. The required ADRs are Accepted. + +## Required ADRs + +Drafted as **Proposed** alongside this plan; each is reviewed and finalized (→ Accepted) when its +workstream begins. + +1. [ADR-0058](../../decisions/0058-relavium-authoring-package-and-conversational-authoring.md) — + `@relavium/authoring` package + conversational-authoring pre-flight contract (2.6.A/2.6.B). +2. [ADR-0059](../../decisions/0059-cli-mid-session-model-reseat.md) — mid-session model reseat; + refines [ADR-0024](../../decisions/0024-agent-first-entry-point-agentsession.md) (2.6.C). +3. [ADR-0060](../../decisions/0060-session-ctx-prompt-interpolation.md) — session `{{ctx.*}}` + interpolation, unblocking `agent run --input` (2.6.D). + +## Risks & mitigations + +| Risk | Mitigation | +|------|------------| +| Authoring knowledge drifts from the specs (restate) | Derive from the Zod schemas / reference docs; a no-duplication check is an acceptance gate ([CLAUDE.md](../../../CLAUDE.md) #8) | +| Authored YAML smuggles secrets | The authored artifact passes the existing `parseWorkflow` secret-taint gate; the write surface is security-reviewed | +| Reseat sells a false promise | An explicit context-loss notice; full-fidelity tool-context is Phase 3 | +| Package promotion over-engineers a CLI-only need | The maintainer decision is multi-surface (desktop/VS Code also consume); the add-package ADR records the rationale | + +Part of [roadmap/](../README.md). Carry-over hardening lives in [../deferred-tasks.md](../deferred-tasks.md).