Skip to content

19. CLI

Context and Problem Statement

Pulse is LychD's one host-side operator language: inscription, binding, bounded lifecycle control, observation, work admission, and retirement. It is a small local adapter over owned services, neither a daemon nor a catalogue of framework organs. Help owns current spelling; source, focused tests, and State own delivery.

Requirements

  • The root grammar is closed: internal frameworks and extensions add no public root command.
  • Commands name operator intent rather than Litestar, systemd, Podman, database, queue, or worker topology. Click validates and renders; typed services retain authority.
  • One source-owned target vocabulary supplies observation, with a safely actuable subset for lifecycle. A future extension-operation namespace must remain beneath a single host-owned verb.
  • A mutating --dry-run previews the same typed plan or request later admitted for effect.
  • Help and host-local rites do not need ASGI, Postgres, or a Vessel. Effects revalidate exact ownership while serialized, and only documented machine projections are stable.

Considered Options

Option Decision Why
One command per organ Rejected It leaks topology, expands without bound, and fragments safety and output contracts.
Extension-owned Click callbacks Rejected It allows namespace collisions and unreviewed host execution without common admission.
Closed verbs with typed targets and operations Selected It preserves one public language, authority boundary, and safe extension contribution point.

Decision Outcome

The installed command is lychd. PulseGroup owns the closed grammar and the exact st alias for status; lychd --help is its executable inventory. No lych alias is promised. Generated services and development machinery have hidden entrypoints, but hidden registration is not public compatibility. The root verbs are init, bind, start, stop, status/st, logs, and del; no run-operation, snapshot, restore, selector shorthand, recovery, promotion, or consent spelling is reserved by the grammar.

Inscribe, then bind

lychd init inspects the host, plans canonical XDG layout, and establishes the lifecycle receipt and root authority used later; it does not bind declarations or start services. Its --dry-run renders the plan without LychD-managed mutation. Probes are bounded, though an inspected tool may write its own metadata; therefore this is not a bit-for-bit host no-effect claim. Missing runtime capabilities remain for bind; only an unsafe or impossible inscription blocks init.

Effectful init rejects effective UID 0 before Settings, host inspection, locks, or filesystem effects; locks and requires the previewed plan unchanged; creates only planned paths, journals confirmed progress, verifies convergence; and records exact device, inode, and mount authority for dedicated Codex, Crypt, and Forge roots. It adopts an existing dedicated root only through that verified receipt. Shared XDG parents, source checkouts, foreign mounts, and the mounted Phylactery are never recursively adopted. Dry-run is available under effective root because it does not grant LychD mutation authority.

lychd bind compiles one immutable snapshot of Settings, extensions, Runes, runtime declarations, secrets, and host foundation into the exact Scribe-owned fileset. Its dry run validates and renders without LychD-managed mutation. Effectful bind reacquires the lifecycle lock, repeats preflight, and requires trusted executables, effective generator, sites, desired and observed files, Settings generation, and secret generation to remain equal before commit. Missing or unsafe sites block; bind does not create the sites that init owns.

Apply provisions only authorized absent core secrets, commits the exact fileset, and reloads the user manager once. A late failure reports confirmed progress rather than claiming no effect; native cancellation survives cleanup classification. --uncaged changes the compiled fileset, not silent unit enablement. Planned binding truth belongs here; current host inventory belongs to status.

Start, stop, observe, and read

For lychd start [TARGET] and lychd stop [TARGET], omission means the source-owned system target. Direct control accepts only the help-advertised lifecycle subset and exact Scribe-owned user units. Before an effect it holds the lifecycle lock and revalidates binding generation, unit identity/state, and Vessel authority, refusing unknown, split, or ambiguous state. An active Vessel must be controlled through its authenticated lifecycle port. That port is not in the production composition, so both commands refuse rather than bypass the Vessel. Direct control neither runs migrations nor evaluates application readiness nor executes a general dependency graph; graceful admission closure, lease-aware draining, and force semantics are likewise not delivered.

lychd status [TARGET] and exact st inspect bounded local evidence. The default is system, and fixed choices are published by help. --json is the stable machine result; Rich is for humans. It inventories local ownership, exact user-unit state, declarations, storage, configuration, bindings, workers, and locally visible runs. It does not attest HTTP/database health, migration currency, queue readiness, durable run health, model warmth, or extension status; silence is not proof.

lychd logs [TARGET] maps a target to exact owned units and takes one bounded journald tail; Click validates and help advertises the line bound. It has no follow mode or joined container, run-event, or Oculus evidence view.

Admit work without ceding the language

A future host-owned run-operation verb may become the sole extension execution namespace. Registration would contribute inert, typed identity, inputs, authority/scope, mutation and consent characteristics, and progress/result shape; it must never grant an extension-owned Click callback or arbitrary host execution. No run-operation catalogue, schema help, injected client seam, or Ward-authenticated CLI admission route is delivered today.

Delete only what the body can name

lychd del is init's confirmed destructive counterpart. It removes only authority proven by receipt and live revalidation: familiar paths, units, object names, and mounts are not ownership. Planning is staged and fingerprinted. Dry-run makes the same authority and blocker decisions without deletion; an effect requires confirmation unless explicitly supplied, recomputes and revalidates under the lifecycle lock, and supplies the displayed fingerprint to execution. Any blocker suppresses every deletion effect.

The executor stops exact owned units, clears exact Scribe bindings, traverses receipted dedicated roots without following symlinks or crossing mount authority, quarantines and re-attests names before irreversible removal, and retains typed recovery evidence when restoration fails to converge. It preserves unreceipted Podman objects, secrets, package installations, source checkouts, and storage; it never calls sudo. Privileged Btrfs is a handoff: copyable arguments appear only after the unprivileged plan is executable and mounted or receipt-backed unmounted subvolume identity is attested, then execution waits for reconciliation. A blocked dry run or incomplete effect exits 2.

Shared target, recovery, and adapter laws

The delivered target source is fixed OperatorTarget enum/resolver, not a runtime extension registry. status and logs accept its whole observable set; start/stop only its explicit lifecycle subset. A target resolves to visible resources and exact ownership, never arbitrary units, containers, paths, or tables. Adding one means source, authority mapping, help, tests, and delivery evidence. Observation never confers actuation; dynamic targets and extension status are Designed.

Whole-body capture and restoration remain Designed. A future inventory may project through status, and mutation may use a typed operation or lifecycle target only after freeze, durability, confirmation, and reconciliation law exists.

Importing or rendering the root does not construct Litestar, connect Postgres, start workers, initialize models, or load the Altar. Host planning and observation call bounded services directly; run help loads inert metadata lazily. ASGI, database, and Reactor consumers are hidden process entrypoints, not alternative operator surfaces. A handler validates input, forms a typed request, delegates, renders deterministic human or documented machine output, and translates typed refusal. It does not reimplement lifecycle, authorization, persistence, deletion, or orchestration.

Read-only status and logs do not acquire lifecycle authority. Init, bind, direct control, and deletion serialize under the lifecycle lock and revalidate the authority they consume. An active Vessel is preferred authority; direct user-manager control serves only safe bootstrap or inactive Vessel state. Recovery does not authorize an emergency root or raw substrate argument.

Normal dispatch installs structured logging before execution; effectful root rejection in init comes earlier. Results go stdout; logs and diagnostics stderr. status --json alone is a stable machine projection. Status 0 means help, successful read/effect, or unblocked dry run; 1 means ordinary refusal, unavailable run transport, effectful root init, or concise ritual failure; 2 means Click usage, blocked del --dry-run, or partial deletion. Confirmation abort and native cancellation remain nonzero. Ritual-wrapped bootstrap failure emits one cli_command_failed event; ordinary CLI errors do not disclose a traceback.

Consequences

Positive

Operators gain one small language bounded by ownership proof; extensions describe work without acquiring roots or host callbacks; host recovery probes survive application-stack failure.

Negative

New targets and operations need typed evidence; human output stays non-API, while several verbs deliberately expose refusal or a narrower surface until authenticated runtime ports arrive.