Skip to content

feat(cli): Phase 2 — 2.A CLI skeleton + 2.B config resolution (ADR-0047/0048)#40

Merged
cemililik merged 7 commits into
mainfrom
development
Jun 22, 2026
Merged

feat(cli): Phase 2 — 2.A CLI skeleton + 2.B config resolution (ADR-0047/0048)#40
cemililik merged 7 commits into
mainfrom
development

Conversation

@cemililik

@cemililik cemililik commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Builds the foundation of the relavium CLI (apps/cli) — Phase-2 workstreams 2.A (CLI
skeleton + process contract) and 2.B (two-level config resolution) — plus the two gating
ADRs and the phase doc's sequencing plan. The CLI is the engine's first real consumer and its
regression harness; this PR lands the skeleton + config plumbing (no engine call yet — that is
2.D).

Decisions (ADRs, Accepted before implementation)

  • ADR-0047 — CLI framework: commander (2.A) + ink (2.E) + @clack/prompts (2.G/2.J), confined to apps/cli; tsup is the build tool.
  • ADR-0048smol-toml for config files, confined to the apps/cli config loader (mirrors ADR-0035 for YAML); strict Zod (Global/Project schemas, ADR-0033) stays the validation truth.

Both name a concrete rationale over the alternatives; config-spec.md now states TOML 1.0; the monorepo root was renamed relaviumrelavium-monorepo to free the published name for the CLI.

2.A — CLI skeleton & process contract

  • commander program (tsup → single ESM bin + shebang) registering the confirmed pre-chat surface (run/list/logs/status/gate/create/import/export/provider/agent/init) with clean "not-yet-available" stubs.
  • A framework-free process contract (testable with no TTY): position-independent global flags (--json/--no-color/--cwd/--config/--verbose/--quiet, POSIX -- honored), output-mode detection (TTY/--json/CI), the deterministic exit-code map (0–3; 4 at 2.M), a typed CliError family + centralized error rendering (human→stderr, JSON envelope→stdout, never a stack/secret as primary output).

2.B — Two-level config resolution

  • Discovery: ~/.relavium global dir + walk-up discovery of the project .relavium/ (a non-directory .relavium is ignored).
  • Loading: smol-toml parse behind a hardened wrapper — size cap, absent→undefined, every fault → typed file-attributed ConfigError (exit 2) whose detail is a TOML position or a Zod code-derived, value-free reason (never the file's contents).
  • Pure merge: last-writer-wins across global → workspace → project (config-spec.md), MCP merged by name — IO-free and extractable later.

Review

Three independent code reviews ran against 2.A/2.B; all findings addressed in 29d0d08:

  • 2 MEDIUM — POSIX -- end-of-options gap in the extractor; Zod error messages echoing the offending config value (contradicted the loader's hygiene guarantee). Both fixed + regression-tested + empirically verified.
  • 5 LOW — empty =-form values, --json-on-extraction-error, size-cap/unreadable-path/ensureGlobalConfigDir test gaps, the specs.ts comment, the ConfigError-vs-ConfigSyntaxError ADR drift (dated amendment), project-scoped [[mcp_servers]] doc drift.

Verification

  • 57 unit tests pass (process contract + config loader).
  • Green: typecheck · lint · test · build · format · seam-fence · engine-deps · tools-typecheck (full root turbo: 20/20).
  • Strict TS throughout: no any, no unsafe as (in-narrowing for errno/TOML position), typed discriminated errors (error-handling.md), Zod at the boundary, secrets never read/written (strict schema rejects stray keys — tested).
  • apps/cli confinement: no engine-internal or vendor-SDK imports; the engine-deps guard correctly excludes apps.

Notes / deferred (no silent shortcuts)

  • The config loader is built + tested but not yet wired into a commandrun consumes it in 2.D.
  • .relaviumignore is a no-op for config resolution; it lands with its consumer (2.J export).
  • Roadmap status (phase-2-cli.md) is not flipped to Done — that happens after merge.

🤖 Generated with Claude Code

Summary by Sourcery

Introduce the initial relavium CLI application with a framework-agnostic process layer, deterministic exit codes, and documented command surface, along with TOML-based config resolution and associated ADRs.

New Features:

  • Add a new apps/cli package providing the relavium CLI entrypoint, wired through a single ESM binary built with tsup.
  • Define a commander-based CLI program that registers the initial command surface (run, list, logs, status, gate, create, import, export, provider, agent, init) with clean not-yet-implemented stubs.
  • Implement a position-independent global options model (--json, --no-color, --cwd, --config, --verbose, --quiet) and output-mode detection (TTY/JSON/CI) for all commands.
  • Add a two-level configuration loading pipeline that discovers global and project .relavium config, parses TOML config files with smol-toml, validates via shared Zod schemas, and merges layers into a resolved config shape.

Enhancements:

  • Document CLI global options, exit codes, and config semantics in the CLI commands reference and config-spec, including TOML 1.0 and project-scoped MCP server overrides.
  • Add ADR-0047 and ADR-0048 to record the decisions for the CLI framework stack (commander/ink/@clack/prompts) and TOML parsing (smol-toml), confined to the CLI app.
  • Refine the Phase 2 CLI roadmap to describe the execution sequencing, critical path to M3, and parallel workstreams, including updated dependency DAGs and milestones.
  • Rename the monorepo root package to relavium-monorepo and clarify that the published CLI package name is relavium.

Build:

  • Configure tsup and TypeScript settings for the CLI app to build a Node-targeted ESM binary with a shebang and Node typings.
  • Extend the pnpm workspace catalog with CLI-only surface dependencies (commander, smol-toml, tsup) pinned via the catalog and confined to apps/cli.

Documentation:

  • Expand Phase 2 CLI documentation with a detailed sequencing and parallelization plan, dependency matrix, and updated status toward milestone M3.
  • Update CLI and config reference docs to describe TOML 1.0 config files, global options, and project-level MCP server configuration behavior.

Tests:

  • Add unit tests for CLI global option extraction and resolution, output-mode/CI detection, error projection and rendering, the top-level run behavior, and config path discovery, loading, and resolution.

Summary by CodeRabbit

  • New Features
    • Added the CLI entrypoint with structured command registration and deterministic exit codes.
    • Implemented TOML-based config loading/validation (strict schemas, size limits, missing-file handling) with last-writer-wins merging and project-scoped MCP server overrides.
    • Added position-independent global flags (--json, --verbose/--quiet, --cwd, --config, color control) plus automatic output-mode switching (TUI vs plain, CI-aware).
    • Improved JSON and human-readable error output.
  • Documentation
    • Updated CLI reference and configuration docs, including TOML and MCP merge behavior.
  • Tests
    • Extended coverage for flags, config loading/resolution, and error/output rendering.

cemililik and others added 5 commits June 22, 2026 00:55
Record the two new runtime-dependency decisions gating Phase-2 workstreams 2.A/2.B
(CLAUDE.md non-negotiable #2 — no new runtime dependency without an ADR):

- ADR-0047 — the CLI command/UI framework: commander (2.A) + ink (2.E) +
  @clack/prompts (2.G/2.J), confined to apps/cli; tsup is the build tool (toolchain).
- ADR-0048 — smol-toml for config files, confined to the apps/cli config loader
  (mirrors ADR-0035 for YAML); strict Zod (ADR-0033) stays the validation truth.

Also states TOML 1.0 as the config format in config-spec.md (its canonical home) and
indexes both ADRs as Accepted.

Refs: ADR-0047, ADR-0048
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stand up apps/cli (the `relavium` bin) as the first engine consumer (build phase 2):

- A commander program (tsup-bundled single ESM bin + shebang) registering the full
  commands.md surface (run/list/logs/status/gate/create/import/export/provider/
  agent/init) with clean "not-yet-available" stubs until each command's workstream.
- A framework-free process contract, testable with no TTY: position-independent global
  flags (--json/--no-color/--cwd/--config/--verbose/--quiet) extracted before parsing,
  output-mode detection (TTY/--json/CI), the deterministic exit-code map (0-3; 4 at 2.M),
  a typed CliError family + centralized error rendering (human→stderr, JSON envelope→
  stdout, never a stack trace as primary output).
- 31 unit tests (output-mode, errors, options/extraction, run integration). Green across
  typecheck, lint, test, build, format, the seam fence, and engine-deps.

Renames the monorepo root package to `relavium-monorepo` to free the published name
`relavium` for apps/cli; pins commander/smol-toml/tsup in the pnpm catalog; documents the
finalized global options in commands.md.

Refs: ADR-0047, ADR-0048
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the apps/cli config loader the rest of the CLI builds on (config-spec.md):

- Discovery (config/paths.ts): the ~/.relavium global dir + walk-up discovery of the
  project .relavium/ from cwd (a non-directory `.relavium` is ignored).
- Loading (config/load.ts): smol-toml parse (ADR-0048) behind a hardened wrapper — a
  pre-parse size cap, an absent layer → undefined, and every fault normalized to a typed,
  file-attributed ConfigError (exit 2) whose detail is a TOML position or a Zod field path,
  never the file's contents. Strict Zod (Global/Project schemas, ADR-0033) stays the
  validation truth.
- Pure merge (config/resolve.ts): last-writer-wins across global → workspace → project
  (config-spec.md), MCP servers merged by name; framework-free and IO-free, so extractable
  to a shared package later.
- 16 unit tests: merge precedence, discovery walk-up, load/validate, --config override,
  and unknown-key rejection (proving the strict schema accepts no stray/secret keys).

`--config` overrides the global layer (project layers still apply), documented in
commands.md. NB: `.relaviumignore` is a no-op for config resolution and lands with its
consumer (2.J export). Toolchain green: typecheck, lint, test (47), build, format, fence,
engine-deps.

Refs: ADR-0048
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three independent code reviews of apps/cli (2.A/2.B). Fixes:

- [MEDIUM] Honor POSIX `--` in the global-flag extractor: `--` and everything after it are
  passed to commander verbatim, so a literal `--json` can be a command argument; a lone `--`
  is a bare invocation → help, exit 0 (options.ts, run.ts).
- [MEDIUM] Config schema errors no longer echo the offending value: formatZodError derives a
  value-free reason from the Zod issue code + schema-side data (expected type, allowed enum
  options, unknown key names) — never issue.message/issue.received — so the loader's
  "never the file's contents" guarantee now holds on the schema path too (load.ts).
- [LOW] Empty `=`-form values (`--cwd=`, `--config=`) are rejected as invalid_invocation
  rather than silently becoming "" (options.ts).
- [LOW] extractGlobalOptions returns its error instead of throwing, so a `--json` parsed
  before a failing global flag is still honored when rendering that error (options.ts, run.ts).
- [LOW] Tests added for the MAX_CONFIG_BYTES size cap, the unreadable-path (EISDIR) branch,
  the value-non-echo hygiene guard, ensureGlobalConfigDir, and the `--`/empty-= cases.
- [docs] Correct the specs.ts "whole surface" comment (chat family + gate-list/budget-resume
  land with their workstreams); ADR-0048 dated amendment reconciling the implemented
  `ConfigError` name and the value-free schema detail; config-spec.md documents project-scoped
  `[[mcp_servers]]`.

57 tests pass; typecheck / lint / build / format / fence / engine-deps green.

Refs: ADR-0047, ADR-0048
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements the initial relavium CLI skeleton (apps/cli) using commander and tsup, defines a framework-free process contract (global flags, IO seam, error handling, exit codes, output-mode selection), and adds a hardened TOML-based two-level config loader (global + project) with pure merge logic, all backed by ADRs 0047/0048 and updated CLI/config roadmap docs.

Sequence diagram for CLI run flow and process contract

sequenceDiagram
  actor User
  participant NodeProcess as index_ts
  participant Run as run
  participant Options as extractGlobalOptions
  participant Program as buildProgram
  participant Commander as Command_parseAsync
  participant Errors as renderError

  User->>NodeProcess: invoke relavium (process.argv)
  NodeProcess->>Run: run(argv, processIo())

  Run->>Options: extractGlobalOptions(argv)
  Options-->>Run: { raw, rest, error }
  alt extractError exists
    Run->>Errors: renderError(error, renderCtx, io)
    Run-->>NodeProcess: error.exitCode
  else
    Run->>Run: assertNoGlobalOptionConflicts(raw)
    alt no subcommand tokens in rest
      Run-->>NodeProcess: EXIT_CODES.success (print help)
    else
      Run->>Program: buildProgram(io)
      Program-->>Run: Command instance
      Run->>Commander: program.parseAsync(rest)
      alt parse succeeds
        Commander-->>Run: (resolved)
        Run-->>NodeProcess: EXIT_CODES.success
      else CommanderError
        Commander-->>Run: CommanderError
        Run-->>NodeProcess: EXIT_CODES.invalidInvocation or success
      else other error
        Run->>Errors: renderError(err, renderCtx, io)
        Run-->>NodeProcess: toUserFacing(err).exitCode
      end
    end
  end
Loading

File-Level Changes

Change Details Files
Introduce apps/cli package with commander-based program, top-level run boundary, IO seam, exit-code mapping, and stubbed commands wired to a framework-free process contract.
  • Add apps/cli package.json, tsconfig, and tsup config to build a single ESM bin with a shebang and Node-targeted TypeScript setup.
  • Implement src/index.ts as the bin entry that wires real process IO into the CLI run function and handles last-resort fatal errors without leaking stacks by default.
  • Define CliIo abstraction and processIo() to inject stdout/stderr/env/TTY into CLI logic so commands are testable without a real TTY.
  • Implement deterministic exit-code constants and types, mapping high-level outcomes (success, workflow failed, invalid invocation, gate paused, chat ended) to numeric codes.
  • Create CliError discriminated error type with stable codes, exit-code mapping, and toUserFacing projection, plus helpers for safe rendering via render-error.ts.
  • Implement buildProgram using commander with configured output hooks, version flag, exitOverride, and help text documenting global options, while delegating subcommand registration.
  • Add run.ts top-level orchestrator that extracts global flags, builds the program, handles bare invocation, maps CommanderError vs CliError vs unexpected errors to exit codes, and ensures all errors go through centralized rendering.
  • Add commands/specs.ts to declare the confirmed pre-chat command surface and register stub actions that throw typed not_implemented errors with clean messaging.
apps/cli/package.json
apps/cli/tsconfig.json
apps/cli/tsup.config.ts
apps/cli/src/index.ts
apps/cli/src/process/io.ts
apps/cli/src/process/exit-codes.ts
apps/cli/src/process/errors.ts
apps/cli/src/process/render-error.ts
apps/cli/src/program.ts
apps/cli/src/run.ts
apps/cli/src/commands/specs.ts
Implement position-independent global flag extraction, verbosity handling, and output-mode detection including CI/TTY awareness, with tests.
  • Implement extractGlobalOptions to strip global flags (--json, --no-color, --cwd, --config, --verbose/-v, --quiet/-q) from argv before commander, supporting both spaced and --flag=value forms, reporting malformed flags via CliError instead of throwing, and honoring POSIX -- as end-of-options.
  • Add conflict detection between --verbose and --quiet plus resolveGlobalOptions to normalize raw flags into a GlobalOptions structure with resolved cwd, configPath, color, json, and verbosity.
  • Implement detectOutputMode and isCiEnv to choose between TUI and plain/NDJSON modes based on --json, CI environment, and TTY presence, keeping this logic pure and injectable.
  • Add focused unit tests for options extraction, conflict rules, resolution behavior, and output-mode/CI detection edge cases.
apps/cli/src/process/options.ts
apps/cli/src/process/options.test.ts
apps/cli/src/process/output-mode.ts
apps/cli/src/process/output-mode.test.ts
Add TOML-based configuration loading pipeline for global and project configs with hardened error handling and pure resolution merge logic.
  • Introduce ConfigError as a CliError subclass that attributes errors to specific config files and guarantees value-free, user-safe messages for all config failures.
  • Implement loadConfigFile to read, size-cap, parse TOML via smol-toml, and validate against Zod schemas, mapping all failures (missing, unreadable, size violations, TOML syntax, schema invalid) to ConfigError with TOML position or safe Zod issue reasons, never echoing raw values or snippets.
  • Implement loadResolvedConfig to locate the global config file (including overridden --config path), discover project .relavium config directory, load workspace.toml and project.toml if present, and return a merged ResolvedConfig plus discovered project directory.
  • Provide resolveConfig pure function that merges global/workspace/project layers using last-writer-wins semantics, computes derived fields (default model, fs scope, max_tokens_estimate, variables) and merges MCP server registrations by name.
  • Add filesystem path utilities to locate and lazily create ~/.relavium and walk up from cwd to find .relavium directories, designed with injectable home/cwd for testing.
  • Add unit tests (placeholders referenced in the diff) for config load, paths, resolution, and error behavior to exercise size caps, missing files, invalid TOML, schema errors, and MCP merging semantics.
apps/cli/src/config/errors.ts
apps/cli/src/config/load.ts
apps/cli/src/config/resolve.ts
apps/cli/src/config/paths.ts
apps/cli/src/config/load.test.ts
apps/cli/src/config/paths.test.ts
apps/cli/src/config/resolve.test.ts
Wire CLI process behavior to centralized tests that validate help/version behavior, stub command handling, global-flag error rendering, and strict non-leakage of stacks in primary output.
  • Add apps/cli/src/run.test.ts to exercise run() end-to-end using a fake CliIo, covering help and bare invocation behavior, version printing, unknown command exit codes, missing-argument handling, stub command not_implemented messaging and absence of stack traces on stderr, JSON error envelope rendering on stdout respecting --json placement, verbose/quiet conflict behavior, malformed global flag handling under --json, and POSIX -- edge cases.
  • Add apps/cli/src/process/errors.test.ts to validate CliError classification, toUserFacing mapping, and internal-fault handling semantics.
apps/cli/src/run.test.ts
apps/cli/src/process/errors.test.ts
Document and codify CLI and config decisions via new ADRs and reference updates, and adjust roadmap sequencing for Phase 2 CLI.
  • Add ADR-0047 describing the choice of commander, ink, and @clack/prompts for the CLI, their confinement to apps/cli, tsup bundling strategy, and considered alternatives.
  • Add ADR-0048 selecting smol-toml as the TOML parser confined to the CLI config loader, detailing hardening guarantees, confinement rules, and decode-vs-validation responsibilities, including an amendment clarifying ConfigError semantics.
  • Register ADRs 0047 and 0048 in the ADR index and update tech-stack references and dependency governance comments.
  • Extend docs/reference/cli/commands.md with a Global options section documenting position-independent global flags and their behavior, consistent with the process implementation.
  • Clarify docs/reference/contracts/config-spec.md to state TOML 1.0 as the config format, explain decode-and-validate flow, and document project-scoped [[mcp_servers]] merging semantics.
  • Update phase-2-cli.md to reflect Phase 2 unblocking and M3 context, adjust the workstream DAG and dependency graph for regression harness and chat, and add a detailed sequencing/parallelization plan with waves, mermaid diagrams, and a dependency matrix.
  • Rename the monorepo root package to relavium-monorepo and update its description to reserve relavium for the CLI app package.
  • Update pnpm workspace catalog to add commander, smol-toml, and tsup as catalog-pinned dependencies with comments on confinement to apps/cli and cooling-window expectations.
docs/decisions/0047-cli-framework-commander-ink-clack.md
docs/decisions/0048-toml-config-parser.md
docs/decisions/README.md
docs/reference/cli/commands.md
docs/reference/contracts/config-spec.md
docs/roadmap/phases/phase-2-cli.md
package.json
pnpm-workspace.yaml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93b2ee35-f84c-47e5-82a8-13467044e0c6

📥 Commits

Reviewing files that changed from the base of the PR and between 729f107 and f01e4fb.

📒 Files selected for processing (4)
  • apps/cli/src/process/options.test.ts
  • apps/cli/src/program.ts
  • apps/cli/src/run.test.ts
  • apps/cli/src/run.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/cli/src/process/options.test.ts
  • apps/cli/src/run.test.ts

📝 Walkthrough

Walkthrough

Introduces the complete apps/cli package: workspace scaffolding (package.json, tsconfig.json, tsup.config.ts), process primitives (exit codes, typed errors, IO seam, output mode), position-independent global option parsing, TOML config loading with smol-toml and layered merge/resolution, a Commander program builder with stubbed command registrations, and the run()/index.ts entrypoints. Two new ADRs and reference doc updates accompany the code.

Changes

CLI Package Bootstrap

Layer / File(s) Summary
Package scaffolding and workspace config
package.json, pnpm-workspace.yaml, apps/cli/package.json, apps/cli/tsconfig.json, apps/cli/tsup.config.ts
Root package renamed to relavium-monorepo; workspace catalog gains commander, smol-toml, and tsup pins; new CLI package configured as ESM with relavium binary, build/lint/test scripts, and Node-targeted TypeScript typecheck-only config.
Process primitives: exit codes, errors, IO, output mode
apps/cli/src/process/exit-codes.ts, apps/cli/src/process/errors.ts, apps/cli/src/process/io.ts, apps/cli/src/process/output-mode.ts, apps/cli/src/process/errors.test.ts, apps/cli/src/process/output-mode.test.ts
Defines EXIT_CODES (0–4) and derived ExitCode type; CliErrorCode discriminant mapped to exit codes via CliError; isCliError/toUserFacing projection; injectable CliIo interface with processIo() concrete implementation; OutputMode union and detectOutputMode/isCiEnv helpers, all fully tested.
Global option extraction, validation, and resolution
apps/cli/src/process/options.ts, apps/cli/src/process/options.test.ts
Position-independent extractGlobalOptions scans argv for --json, --no-color, --verbose/-v, --quiet/-q, --cwd, --config, respects -- termination, and returns errors without throwing for malformed value flags. assertNoGlobalOptionConflicts and resolveGlobalOptions normalize raw flags into typed GlobalOptions.
Error rendering
apps/cli/src/process/render-error.ts
renderError maps any thrown value through toUserFacing and writes either a JSON error envelope to stdout (--json) or a plain stderr line; appends stack trace to stderr when --verbose is active.
Config filesystem path utilities
apps/cli/src/config/paths.ts, apps/cli/src/config/paths.test.ts
globalConfigDir computes ~/.relavium; ensureGlobalConfigDir idempotently creates it with a tmp/ subdirectory; findProjectConfigDir walks upward from cwd looking for a .relavium/ directory, returning undefined at root.
Config layer merge and resolution
apps/cli/src/config/resolve.ts, apps/cli/src/config/resolve.test.ts
resolveConfig(layers) merges global → workspace → project with last-writer-wins for scalars, spread-merge for variables, and mergeMcpServers for name-keyed override of MCP server registrations.
TOML config load: ConfigError, loadConfigFile, loadResolvedConfig
apps/cli/src/config/errors.ts, apps/cli/src/config/load.ts, apps/cli/src/config/load.test.ts
ConfigError (CliError subclass with filePath) wraps all load failures. loadConfigFile enforces a byte size cap, wraps smol-toml parse errors with position, and converts Zod failures to secret-free path-attributed messages. loadResolvedConfig discovers and merges the global, workspace, and project TOML layers.
Commander program builder and command stubs
apps/cli/src/program.ts, apps/cli/src/commands/specs.ts
buildProgram(io) creates the Commander instance, routes output through CliIo, appends global-options help, and calls registerCommands. Command stubs for run, list, logs, status, gate, create, import, export, provider, agent, init throw CliError('not_implemented') referencing each command's target workstream.
run() entrypoint and index bootstrap
apps/cli/src/run.ts, apps/cli/src/index.ts, apps/cli/src/run.test.ts
run(argv, io) orchestrates global extraction → conflict check → bare-invocation → program.parseAsync and maps all outcomes to deterministic ExitCodes, never rejecting. index.ts wires processIo() into run and adds a last-resort rejection handler respecting RELAVIUM_DEBUG. Integration tests cover help/version, unknown commands, stub behavior, JSON error envelopes, flag conflicts, and -- handling.
ADRs, reference docs, and roadmap
docs/decisions/0047-*.md, docs/decisions/0048-*.md, docs/decisions/README.md, docs/reference/cli/commands.md, docs/reference/contracts/config-spec.md, docs/roadmap/phases/phase-2-cli.md
ADR-0047 records commander/ink/@clack/prompts adoption with confinement rules; ADR-0048 records smol-toml selection and the secret-free ConfigError normalization contract. Reference docs add global-options table and TOML decode/MCP merge semantics. Phase 2 roadmap gains sequencing narrative, ordered-waves diagram, and refreshed dependency matrix.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant index.ts
  participant run
  participant extractGlobalOptions
  participant buildProgram
  participant registerCommands
  participant loadResolvedConfig

  User->>index.ts: executes relavium <args>
  index.ts->>run: run(process.argv, processIo())
  run->>extractGlobalOptions: argv
  extractGlobalOptions-->>run: ExtractedArgv {raw, rest, error?}

  alt extraction error or --verbose+--quiet conflict
    run-->>index.ts: exitCode 2 (invalidInvocation)
  else bare invocation (no subcommand)
    run-->>index.ts: exitCode 0 (help printed)
  else normal subcommand
    run->>buildProgram: io
    buildProgram->>registerCommands: program
    registerCommands-->>buildProgram: stubs registered
    buildProgram-->>run: Command
    run->>loadResolvedConfig: cwd, configPath
    loadResolvedConfig-->>run: ResolvedConfig
    run->>run: program.parseAsync(rest)
    run-->>index.ts: exitCode (0–4)
  end

  index.ts->>index.ts: process.exitCode = exitCode
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 Hop, hop — a new command takes shape!
relavium leaps from scaffold to cape,
With TOML parsed clean, no secrets in sight,
Exit codes mapped, and errors wrapped tight.
Commander stands ready, stubs waiting their turn —
The rabbit has planted; soon fires will burn! 🔥

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main deliverables (Phase 2 workstreams 2.A and 2.B) with specific scope (CLI skeleton, config resolution) and references the governing ADRs (0047/0048), directly matching the PR's objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch development

Comment @coderabbitai help to get the list of available commands and usage tips.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="docs/decisions/0048-toml-config-parser.md" line_range="56-61" />
<code_context>
+[tech-stack.md](../tech-stack.md) for the version (added under the §9a cooling window).**
+
+- The parser is wrapped in a **hardened, defensive loader**, not called raw: a pre-parse
+  source-size cap, and **every** parse throw normalized to a typed, secret-free
+  `ConfigSyntaxError` carrying the file path and the library's line/column when present (never
+  the source text). The strict Zod config schemas ([ADR-0033](0033-strict-config-files-amends-0023.md))
+  remain the **single source of validation truth** on the parsed object; the parser's only job
+  is string → plain data. Relavium config files are **TOML 1.0**
+  ([config-spec.md](../reference/contracts/config-spec.md)); `ConfigSyntaxError` is a CLI-local
+  typed error (the loader is CLI-local) and would be promoted to a shared error only if the
+  loader is later extracted.
</code_context>
<issue_to_address>
**issue:** Error type name is inconsistent within the ADR (`ConfigError` vs `ConfigSyntaxError`).

The amendment block says the implemented error type is `ConfigError`, but this section still refers to `ConfigSyntaxError` (for normalized parse errors and as a CLI-local typed error). Please align these references to `ConfigError` and update the surrounding wording as needed to reflect the broader config/load scope rather than syntax-only.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread docs/decisions/0048-toml-config-parser.md Outdated

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request scaffolds the Relavium CLI (apps/cli), implementing command registration with commander and a two-level TOML configuration loader using smol-toml and Zod. It also includes comprehensive unit tests, error handling, and updated documentation (ADRs 0047 and 0048). Feedback on the configuration loader suggests checking file sizes using statSync before reading them into memory to prevent potential out-of-memory issues with extremely large files.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread apps/cli/src/config/load.ts Outdated
@@ -0,0 +1,152 @@
import { readFileSync } from 'node:fs';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Import statSync from node:fs to enable checking the file size before reading the file into memory.

Suggested change
import { readFileSync } from 'node:fs';
import { readFileSync, statSync } from 'node:fs';

Comment on lines +24 to +37
export function loadConfigFile<T>(filePath: string, schema: ZodType<T>): T | undefined {
let text: string;
try {
text = readFileSync(filePath, 'utf8');
} catch (err) {
if (errnoCode(err) === 'ENOENT') {
return undefined;
}
throw new ConfigError(filePath, 'could not be read.', { cause: err });
}

if (Buffer.byteLength(text, 'utf8') > MAX_CONFIG_BYTES) {
throw new ConfigError(filePath, `exceeds the ${MAX_CONFIG_BYTES}-byte config size limit.`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

To prevent potential out-of-memory (OOM) errors or performance issues when encountering extremely large files, check the file size using statSync before reading the entire file into memory with readFileSync.

export function loadConfigFile<T>(filePath: string, schema: ZodType<T>): T | undefined {
  try {
    const stat = statSync(filePath);
    if (stat.size > MAX_CONFIG_BYTES) {
      throw new ConfigError(filePath, 'exceeds the ' + MAX_CONFIG_BYTES + '-byte config size limit.');
    }
  } catch (err) {
    if (errnoCode(err) === 'ENOENT') {
      return undefined;
    }
    throw new ConfigError(filePath, 'could not be read.', { cause: err });
  }

  let text: string;
  try {
    text = readFileSync(filePath, 'utf8');
  } catch (err) {
    throw new ConfigError(filePath, 'could not be read.', { cause: err });
  }

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
apps/cli/src/index.ts (1)

17-17: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Use the shared exit-code constant instead of a numeric literal.

Line 17 hardcodes 1 in the fallback path. Prefer the centralized constant from process/exit-codes.ts to keep this path aligned with the deterministic exit-code contract if values evolve.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/cli/src/index.ts` at line 17, Replace the hardcoded numeric literal `1`
in the `process.exitCode = 1` statement with the appropriate exit-code constant
from the `process/exit-codes.ts` module. First, import the relevant exit-code
constant at the top of the file, then substitute the literal value with that
constant to ensure consistency with the centralized exit-code definitions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/cli/src/process/options.ts`:
- Around line 73-76: The validation check for --cwd and --config options in the
token parsing logic does not reject empty string values passed with quotes (like
--cwd ""). Add an additional condition to the existing validation that checks if
the value is empty or contains only whitespace in addition to the existing
checks for undefined and options starting with a dash. This will ensure that
quoted-empty arguments are properly rejected and do not propagate downstream as
invalid paths or configurations.

In `@apps/cli/src/run.test.ts`:
- Line 68: The code at line 68 uses an unsafe TypeScript `as` cast when parsing
JSON, which doesn't verify the actual runtime shape of the data. Replace the
unsafe type assertion on the JSON.parse() result with a proper type guard
function. Create a type guard function that validates the parsed object has the
expected properties (type and code as strings) using the `is` keyword, then use
that function to check the parsed result before assignment. Apply this same fix
to line 82 as well, ensuring both locations use consistent runtime shape
validation instead of type assertions.

In `@docs/decisions/0048-toml-config-parser.md`:
- Around line 55-63: In the decision document section describing the hardened,
defensive loader (around the paragraph mentioning parse throw normalization),
replace all references to `ConfigSyntaxError` with `ConfigError` to align with
the amendment above and the actual loader implementation. The terminology should
be consistent across the document, as `ConfigError` is the typed error used for
all failure modes including unreadable files, oversized files, TOML parse
failures, and schema validation failures, not just syntax errors.

---

Nitpick comments:
In `@apps/cli/src/index.ts`:
- Line 17: Replace the hardcoded numeric literal `1` in the `process.exitCode =
1` statement with the appropriate exit-code constant from the
`process/exit-codes.ts` module. First, import the relevant exit-code constant at
the top of the file, then substitute the literal value with that constant to
ensure consistency with the centralized exit-code definitions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7bd9fd0a-b1a3-445a-93af-f8eaa4666b9c

📥 Commits

Reviewing files that changed from the base of the PR and between ffb6338 and 29d0d08.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (32)
  • apps/cli/package.json
  • apps/cli/src/commands/specs.ts
  • apps/cli/src/config/errors.ts
  • apps/cli/src/config/load.test.ts
  • apps/cli/src/config/load.ts
  • apps/cli/src/config/paths.test.ts
  • apps/cli/src/config/paths.ts
  • apps/cli/src/config/resolve.test.ts
  • apps/cli/src/config/resolve.ts
  • apps/cli/src/index.ts
  • apps/cli/src/process/errors.test.ts
  • apps/cli/src/process/errors.ts
  • apps/cli/src/process/exit-codes.ts
  • apps/cli/src/process/io.ts
  • apps/cli/src/process/options.test.ts
  • apps/cli/src/process/options.ts
  • apps/cli/src/process/output-mode.test.ts
  • apps/cli/src/process/output-mode.ts
  • apps/cli/src/process/render-error.ts
  • apps/cli/src/program.ts
  • apps/cli/src/run.test.ts
  • apps/cli/src/run.ts
  • apps/cli/tsconfig.json
  • apps/cli/tsup.config.ts
  • docs/decisions/0047-cli-framework-commander-ink-clack.md
  • docs/decisions/0048-toml-config-parser.md
  • docs/decisions/README.md
  • docs/reference/cli/commands.md
  • docs/reference/contracts/config-spec.md
  • docs/roadmap/phases/phase-2-cli.md
  • package.json
  • pnpm-workspace.yaml

Comment thread apps/cli/src/process/options.ts Outdated
Comment thread apps/cli/src/run.test.ts Outdated
Comment thread docs/decisions/0048-toml-config-parser.md Outdated
cemililik and others added 2 commits June 22, 2026 07:03
- options.ts: reject empty/whitespace-only spaced value flags (`--cwd ""`), not just the
  `=`-form; extract the verbosity nested ternary; refactor the extractor into a BOOLEAN_FLAGS
  table + `takeValueFlag` helper to drop cognitive complexity below the threshold.
- load.ts: stat the file size with `statSync` BEFORE `readFileSync` (OOM hardening) and reject
  a non-regular-file path; drop the post-read byteLength check.
- index.ts: top-level await instead of a promise chain; map the fatal-guard exit to
  `EXIT_CODES.workflowFailed` rather than a literal `1`.
- run.test.ts: replace the unsafe `as` JSON casts with an `isErrorEnvelope` type guard;
  simplify the no-stack-leak assertion to a non-backtracking regex.
- resolve.test.ts: explicit sort comparator.
- ADR-0048: rename the loader bullet's `ConfigSyntaxError` → `ConfigError` (consistent with the
  amendment + implementation; reflects the stat-before-read change).

57 tests pass; typecheck / lint / build / format / fence / engine-deps green.

Refs: ADR-0048
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…5852 hotspot

Final review pass on PR #40:

- Consistency fix (§2.A error contract): a commander parse error (e.g. `--json bogus`) now
  renders the structured JSON envelope on stdout under --json — commander's human stderr is
  suppressed (buildProgram `suppressErrorOutput`), the envelope is emitted in run.ts, and its
  redundant `error:` prefix is stripped. In human mode commander still writes to stderr.
- Sonar S5852 (slow-regex security hotspot): de-regex apps/cli to remove every
  repetition-bearing regex — the `error:`-prefix strip is now `startsWith`+`slice`+`trimStart`,
  the no-stack-leak assertion splits lines + `startsWith('at ')`, and the literal-match test
  regexes became `toContain`/`startsWith`/string `toThrowError`. No regex remains in
  apps/cli/src, so there is no S5852 surface to review.
- isErrorEnvelope type guard now validates `message` too (it is asserted on).

58 tests pass; typecheck / lint / build / format / fence / engine-deps green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cemililik
cemililik merged commit 894ecbe into main Jun 22, 2026
9 checks passed
@sonarqubecloud

Copy link
Copy Markdown

cemililik added a commit that referenced this pull request Jun 22, 2026
PR #40 merged: build-phase-2 workstreams 2.A (CLI skeleton + process contract) and 2.B
(config resolution loader) are complete.

- phase-2-cli.md: Status → In progress; §2.A and §2.B headings → ✅ Done (PR #40).
- current.md: "What is active now" reflects 2.A/2.B landed + the next spine step (2.D → engine).
- README.md / CLAUDE.md: high-level status refreshed (Phase 2 underway / in progress), still
  pointing at current.md as the canonical live-status home.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant