CLI Architecture
The holon CLI binary is implemented by the holon-cli crate and exercises the full Holon runtime protocol surface. It shares the same holon-runtime-core::Runtime contract as the desktop application.
Architecture
holon CLI (clap)
↓ direct fn call, no transport, no serialization
holon-runtime-core::Runtime::open(db_path)
↓ synchronous rusqlite
SQLite (runtime/.data/holon.db)
Most CLI subcommands open the runtime directly with no subprocess or transport boundary. The explicit exceptions are transport stdio and transport tcp-readonly, which expose the bounded JSONL transport surface over stdin/stdout or one loopback TCP connection.
Key Design Decisions
Direct runtime access. The CLI opens Runtime::open(db_path) directly for most subcommands. This means CLI operations are synchronous, single-user, and share no transport layer with the desktop. The same SQLite database can be inspected by both the CLI and desktop, but only one process should hold the database open at a time.
--db flag. All subcommands accept --db <path> to specify the database path. Default: runtime/.data/holon.db. This is surfaced as a global clap argument.
--json flag. All subcommands accept --json for machine-readable JSON output. When set, responses are serialized as JSON lines with a status envelope instead of human-readable tables.
initialize required before stateful commands. The initialize subcommand seeds default skill sources and marks the database as initialized. status works before initialization; all other CRUD subcommands require it.
Subcommand Groups
| Group | Subcommands | Purpose |
|---|---|---|
| Initialize | initialize | Seed default skill sources, mark database ready |
| Inspect | status | Read runtime status: queued, active, pending, backpressure, lag, degraded |
| Goal lifecycle | goal {create,read,set-status,list} | CRUD goals with optional workspace binding and auto-end-to-end mode |
| Turn management | turn {start,set-status,list} | Attach user turns to goals, manage turn state |
| Telemetry | telemetry export, telemetry analyze, telemetry eval | Export local redacted JSONL telemetry, derive offline scorecards and draft-only profile proposals, and run deterministic local harness telemetry regression scenarios |
| Harness profiles | harness-profile {create,read,list,resolve} | Persist and inspect inert draft-only harness profiles, then resolve a read-only layered inert profile view across explicit scope refs |
| Harness profile proposals | harness-profile-proposal {create,read,list} | Persist and inspect inert draft-only harness profile proposals across system, user, workspace, task-family, session, or goal scope refs |
| Workspace | workspace {create,read,list} | Persist named workspaces |
| Planning | plan {create,read,set-status,list,create-revision,compare} | Create and revise bounded plans, compare parent-child revisions |
| Plan review | plan-review {create,list} | Persist durable review records with decision and version tracking |
| Plan approval | plan-approval-request {create,list,evaluate} | Create approval requests, recompute from reviews, set plan status |
| Execution approval | execution-approval {decide} | Decide workflow step execution approvals after request-step-approval creates them |
| Plan steps | plan-step {create,read,set-status,list,set-capability-reference} | Step-level state and inert capability binding references |
| Questions | question {create,read,answer,defer,list} | Track open questions as explicit state records |
| Assumptions | assumption {create,read,set-status,list} | Track and resolve assumptions explicitly |
| Run management | run {create,cancel,check-eligibility,execute-safe,list} | Lifecycle: create queued runs, cancel, eligibility gating, safe execution |
| Run events | run-event {create,list} | Append tiered run events (critical or progress), poll with cursor windows |
| Evidence | evidence {create,list} | Persist observation evidence records with run linkage |
| Workspace observation | workspace-observation {read,set-enabled,start-session,stop-session,renew-lease,capture-create,capture-read,capture-list,capture-materialize} | Persist governed observation state and materialize observation captures into desktop target or surface state |
| Skill sources | skill-source {register,list} | Register inert skill source descriptors with optional discovery input |
| Skill definitions | skill-definition {create,list} | Create and list skill definition records (including Elegy v2 imports) |
| Capability bindings | capability-binding {create,list} | Bind capabilities to skill definitions with dry-run support flags |
| Capability catalog | capability-catalog {list,read} | Read-only catalog merging built-in stubs with persisted definitions |
| Adapter state | target-descriptor {create,read,list}, surface-map {create,read,list}, bridge-binding {create,read,list}, bridge-session {create,read,list,set-status}, bridge readiness | Persist governed target identity, observational surface-map artifacts, bridge bindings, bridge sessions, and computed bridge readiness |
| Workflow definitions | workflow-definition {create,read,list} | Persist and inspect normalized workflow definitions |
| Workflow runs | workflow-run {create,read,list,ready-set,claim-step,set-step-status,events,evidence,execute-safe,step-readiness,request-step-approval,resume-step-after-approval,execute-ready,complete-human-gate,wake-wait-timer,deliver-wait-event} | Deterministic workflow run control, waiting, approvals, execution, and evidence |
| Console | console [--once <prompt>] | Non-blocking console scaffold; auto-initializes |
| Transport | transport stdio, transport tcp-readonly | Start a newline-delimited JSON transport session over stdin/stdout or a bounded TCP listener that stays loopback-only unless non-loopback serving is explicitly enabled |
| Runtime tools | runtime-tool {discover,call} | Inspect context-aware runtime discovery output and execute the bounded read-only runtime-tool/call slice |
| Rust code tools | rust-code-tool {validate,create-frozen-tool-proposal,build,test,read,list} | Validate, persist, build, test, and inspect managed compiled Rust CLI frozen tools |
Transport Subcommands
The transport stdio subcommand starts a synchronous local stdio transport using holon-transport::StdioTransport. It reads newline-delimited JSON frames from stdin and writes responses to stdout.
The transport tcp-readonly subcommand starts a synchronous TCP JSONL listener using holon-transport::TcpReadonlyTransport. It binds 127.0.0.1:0 by default, stays loopback-only unless --allow-non-loopback is supplied explicitly, serves one authenticated remote client connection, then exits. Operators may widen that to a bounded sequential count with --max-sessions <N>; the listener accepts exactly N TCP connections one after another, creates a fresh authenticated transport session for each connection, then exits cleanly. The public CLI path still rejects non-loopback bind addresses unless --allow-non-loopback is set, rejects --max-sessions 0, and requires a bearer token to be supplied through --remote-token-env <ENV_VAR> before binding. Human output prints the bound address to stderr and clearly marks explicit non-loopback serving when enabled; --json prints a startup envelope with the bound address, transport kind, networkScope, maxSessions, and read-only or auth requirements, then a final shutdown envelope after serving ends with servedSessions, response or error totals, shutdownReason, and bounded diagnostic counters. Neither mode prints the configured token.
Each stdio connection initializes independently and allows transport/status plus runtime/status before initialization. Each TCP connection also initializes independently, but the TCP pre-auth gate continues to block runtime/status until a valid remote runtime/initialize; only transport/status, transport/close, and runtime/initialize are available before TCP remote initialization. transport/status remains per-connection only and now reports local diagnostic counters for that one connection, such as blocked pre-initialize runtime reads, remote auth failures, remote forbidden requests, oversized frames, malformed or invalid requests, and accepted authenticated remote sessions.
When a client initializes with ClientKind::Remote, the transport requires remoteAuth: { kind: "bearer", token: string } on runtime/initialize and rejects missing or invalid credentials with typed remote_auth_required or remote_auth_invalid errors before runtime dispatch. An authenticated remote client is still intentionally read-only and may call only runtime/initialize, runtime/status, transport/status, transport/close, goal/read, turn/list, session-artifact/list, session-event/list, model-routing/list, and child-lane/list as the current remote-read-continuity scope. All mutation or execution methods still fail closed with typed remote_forbidden transport errors.
The TCP listener is still a bounded proof surface. Even in explicit non-loopback mode it remains plain TCP bearer auth without TLS, is not a production internet-facing server, and still does not add concurrent multi-client serving, WebSocket transport, streaming, remote writes, remote provider execution, cancellation, user identity provider integration, OAuth flow, token issuance or rotation, token persistence, or broader multi-client coordination in the CLI transport path. These new diagnostics are local operator visibility only, not a telemetry exporter or broader remote inspection surface.
Telemetry Export
The telemetry export subcommand is a local, opt-in JSONL export surface for redacted harness telemetry derived from existing durable runtime records. It can export either one goal-backed session scope or one specific turn when --goal-id and --turn-id are provided together. The export is local only: it writes JSONL to stdout by default or to an explicit --output <path> file, and it does not create background collection, remote upload, or a new transport authority surface.
The Phase 9A export floor includes persisted turns, durable session events, model-routing records, child-lane records, and retained session-artifact metadata. It omits prompt text, assistant reply text, artifact bodies, and raw tool arguments or results by default, replacing them with redaction metadata such as counts, status, lifecycle fields, and text lengths. Transport listener startup or shutdown diagnostics from Phase 8F remain intentionally outside this export floor because they are operator-visible local summaries rather than durable runtime records.
Telemetry Eval
The telemetry eval --scenario <id|all> subcommand is a local deterministic regression surface for harness telemetry semantics. Each named scenario seeds one in-memory runtime with synthetic durable turns, session events, routing records, child lanes, outcomes, and retained artifact metadata, exports the same Phase 9A telemetry floor, and checks that the expected statuses, outcome kinds, record presence, and redaction metadata are present.
These evals do not call providers, do not use the network, do not write to the configured runtime database, and do not measure model answer quality. They validate host-owned harness state and telemetry export semantics only.
Telemetry Analyze
The telemetry analyze --goal-id <id> [--turn-id <id>] --json subcommand is a local, read-only offline analysis surface built on top of the same redacted telemetry export floor as telemetry export. It normalizes exported records into harness analytics events, emits a typed metric catalog plus metric values, computes a deterministic scorecard, emits analyzer-local cluster summaries for coverage gaps, failure patterns, and friction patterns, and may emit draft-only HarnessProfileProposal records for safe findings such as incomplete measurement coverage, low validation-evidence coverage, low tool-call success rate, high child-lane failure rate, high routing fallback usage, or high clarification friction.
This command does not activate profiles, persist profile changes, change routing, modify prompts, or add any new runtime collection surface. Missing numeric measurements remain explicit recorded, not-recorded, unavailable, or redacted states rather than defaulting to zero. Proposal-capable metrics now carry minimum sample-size and behavior-influence metadata, including measurement-coverage findings, and proposal emission is gated on those thresholds instead of firing on tiny samples.
When analyzer proposals are emitted, they stay draft-only and target a real resolved profile scope when one can be determined safely from current runtime truth. In this slice the analyzer uses read-only resolution over system, truthfully derivable workspace, and goal, and otherwise falls back to a goal-scoped inert target without persisting anything. Cluster summaries are analyzer output only; they are not persisted and do not activate runtime behavior.
Harness Profile Proposals
The harness-profile-proposal command group is the durable persistence surface for inert draft-only HarnessProfileProposal records. create reads one proposal body from a JSON file and binds it to exactly one explicit scope selector: --system, --user-key, --workspace-id, --task-family, --session-id, or --goal-id. read loads one persisted record by runtime-owned record UUID, and list returns all persisted records for one exact scope ref.
This surface is intentionally narrow in the current phase. It persists typed proposal records only, keeps proposal status at draft, requires any persisted targetScopeRef to match the selected scope_ref exactly, performs no lifecycle mutation, does not auto-persist from telemetry analyze, and does not activate any profile or runtime behavior.
Harness Profiles
The harness-profile command group is the durable persistence surface for inert draft-only HarnessProfile records. create reads one profile body from a JSON file and binds it to exactly one explicit scope selector: --system, --user-key, --workspace-id, --task-family, --session-id, or --goal-id. read loads one persisted record by runtime-owned record UUID, list returns all persisted records for one exact scope ref, and resolve returns a read-only layered view from explicit selectors.
This surface is still inert in the current phase. Persisted profiles remain draft-only, their embedded scope must match the selected scope_ref, and resolve does not activate or apply runtime behavior. Instead it returns a dedicated resolved profile DTO plus contributing persisted records. Resolution stays explicit for system, user, workspace, task-family, session, and goal; the only derived layer in this slice is workspace when a supplied goal-backed session or goal record already truthfully exposes workspace_id. For each participating layer, the latest persisted profile record wins within that layer, and later layers override earlier ones in deterministic order: system -> user -> workspace -> task-family -> session -> goal. telemetry analyze remains read-only and does not persist profiles or proposals automatically.
Relationship to Desktop
Both the CLI and the desktop app share:
holon-protocol— typed DTOs, enums, request/response shapesholon-store— SQLite schema, migrations, persistenceholon-runtime-core— theRuntimefacade and in-process dispatcher
The desktop wraps the runtime behind Tauri commands with mutex-guarded access (Arc<tokio::sync::Mutex<Runtime>>). The CLI opens it directly with no concurrency guard. Both operate on the same protocol contract.
The workflow-run surface now includes two distinct waiting controls:
wake-wait-timerresumes a pausedWaitTimerstep once its due time has elapsed.deliver-wait-eventresumes a pausedWaitForEventstep with an explicitly supplied event envelope.
This split is intentional. Event waits are not treated as disguised timers, and a config-less WaitForEvent step still requires an explicit delivered event rather than host-driven auto-resume.
Bridge Surface
The CLI now exposes the additive bridge contract slice used by the runtime to bind governed target identity to lane-specific authority and readiness state:
bridge-binding create|read|listpersists a workspace-scoped binding between a target descriptor and a declared bridge key with supported lanes, preferred lane, and evidence refs.bridge-session create|read|list|set-statuspersists lane-specific authority handles as opaqueauthority_ref_kindplusauthority_refvalues rather than raw secret material.bridge readinesscomputes the current bridge state from the stored binding, target descriptor, newest surface map, and persisted sessions.
This CLI surface is additive. It does not change the existing workflow live API lane requirement set and does not make bridge records mandatory for older target-descriptor plus surface-map flows.
Runtime Tool Surface
The CLI now exposes the additive runtime tool surface used by the desktop discovery-first chat slice:
runtime-tool discoverreadsruntime-capability/discoverwith optional--limitand preserves context-aware availability, schemas, projection metadata, and blocked reasons.runtime-tool callinvokesruntime-tool/call, which currently stays bounded to built-in read-only runtime tools and still fails closed when provider tool calling is unsupported orwire_apiis missing.
This surface is intentionally narrower than a general executor. In the current implemented slice, runtime-tool/call supports only the guarded built-ins behind the runtime allowlist and keeps the runtime as the final authority on blocked or unsupported calls.
Observation Materialization
workspace-observation capture-materialize <capture_id> exercises the RM-032 observation materialization flow. The CLI forwards workspace-observation-capture/materialize-target-surface directly into the runtime and can optionally accept --target-key, --surface-map-key, and --no-bridge.
This flow is observation-only. It materializes or reuses a desktop target descriptor, desktop surface map, and optional desktop-semantic bridge binding from one persisted foregroundWindowPolling capture. Reuse is validated for workspace, target linkage, desktop target kind, desktop surface kind, and desktop-semantic bridge lanes. The runtime persists capture links atomically only after all requested materialization steps succeed.