Architecture

A framework, not a command wrapper

Every respira command runs through the same six-phase execution cycle. You see the output. Underneath, there is a framework.

New to the CLI? Start at respira.press/cli for install, the command surface, and the agent workflow. This page is for people who want to know what is happening under the hood.

The execution cycle

Every invocation of respira moves through six named phases, in this order, every time:

  1. LoadContext: gather invocation context. Auth, target site, switches, task input.
  2. PreHooks: fire the before_resolve framework hook.
  3. Resolve: select and validate the Tool Chain Function that will run.
  4. Execute: run the selected function. This is where the SDK call happens.
  5. PostHooks: fire filter_result and after_execute framework hooks.
  6. Return: normalize the result, emit the trace if --verbose, and return.

Nothing skips phases. Nothing reorders them. Deterministic by design, traceable with --verbose. If a phase throws, the error is captured into a CycleError with the phase name attached and returned as CycleResult.error. The cycle never re-throws at its boundary.

The hook framework

Five framework-level hook points are live in v0.1:

  • before_resolve: action. Observe the invocation before resolution. Log it, enforce policy.
  • filter_plan: filter. Transform the candidate tool chain function list.
  • before_execute: action. Final gate before the tool runs.
  • filter_result: filter. Transform the tool's output on the way back.
  • after_execute: action. Record, notify, trigger follow-on work.

In v0.1 no callbacks register here. The contracts are frozen. v0.2 adds callback registration and extension manifests on top, without changing anything you see in v0.1. If you are building against respira today, nothing you write will need to change when v0.2 arrives.

Tool Chain Functions

Every command is a typed function with metadata:

interface ToolChainFunction<T> {
  name: string;                    // "read.structure"
  description: string;
  domainTags: readonly string[];   // ["pages", "read", "public", "anonymous"]
  capability: "read" | "write" | "destructive";
  prerequisites: readonly Prerequisite[];
  internalHooks?: readonly HookDeclaration[];
  execute(input: unknown, ctx: CycleContext): Promise<T>;
}

Domain tags document what a function does across four dimensions: builder (elementor, divi, bricks, gutenberg, etc.), domain (pages, posts, media, design-system), capability (read, write, destructive), and access (public, anonymous, connected, licensed). In v0.1 these are metadata; in v0.2 they become SQL filters against the Tool Registry.

File-organization convention

There are 34 public commands, and exactly 34 files under packages/cli/src/commands/. One BaseCommand subclass per file, one ToolChainFunction per file, always co-located. No topic-level grouping, no shared files. This is frozen.

  • The ToolChainFunction is a named export alongside the oclif Command class (which stays the default export). Name pattern: <camelCasePath>Function.
  • Examples: auth/login.ts exports authLoginFunction. read/design-system.ts exports readDesignSystemFunction. write/create-page.ts exports writeCreatePageFunction.
  • The function's name field uses dot-notation mirroring the route (auth.login, read.design-system, write.create-page). Traces, logs, and (v0.2) the extension manifest reader key on this.
  • v0.2 discovers every function by globbing src/commands/**/*.ts and picking up named exports ending in Function. No user file changes when the registry swaps in.

Structured tracing

Pass --verbose on any command and the cycle writes a JSON trace to ~/.respira/traces/{invocationId}.json. Each entry carries a phase, event type, timestamp, duration, and status. The trace is for coding agents that need to understand why something did or did not do what was expected.

$ respira read structure https://example.com --verbose
[respira] trace: ~/.respira/traces/2adcfb73-8a97-4b94-be23-1448f2db1cce.json

Without --verbose, traces accumulate in memory and are discarded at end of command. No stderr noise.

Error handling

The cycle catches phase errors and wraps them in a CycleError with codes like PREREQUISITE_NOT_MET, VALIDATION_FAILED, TOOL_CHAIN_ERROR, PHASE_ERROR. The original error is preserved as cause, so command handlers unwrap it for user display. What you see on the terminal is the familiar RespiraError message and hint. The CycleError context is logged only when --verbose is set.

What ships in v0.1, what ships in v0.2

In v0.1:

  • The execution cycle. Six phases. Deterministic. Traceable.
  • The hook framework contracts. Five named hooks. Frozen shape.
  • Tool Chain Function abstraction for all 34 commands, co-located with each BaseCommand subclass.
  • Null hook registry. The hook-firing code path runs, callbacks are always empty. This is the scaffolding that makes v0.2 purely additive: NullHookRegistry swaps for ManifestBackedHookRegistry, callbacks start registering, no existing caller changes.
  • Structured tracing with --verbose.

In v0.2:

  • Callback registration against framework-level hooks.
  • Extension manifests for third-party contributors.
  • SQLite-backed tool registry with full-text search.
  • Tool chain functions declaring internal hook points.
  • Baseline site inventory for per-site prerequisite validation.
  • Priority resolution when callbacks run in order.

Every v0.2 addition is additive. No breaking changes. No "we renamed this" in the changelog. That is the whole point of landing this scaffolding now.

Why this architecture, why now

The CLI works today. 34 commands, 45-tool catalog, typed SDK, green CI. Publishing as-is would be reasonable. But once extensions and third-party integrations exist, they will register callbacks against phase boundaries that do not currently exist. Retrofitting those boundaries later is breaking work. Landing them now, while nothing has subscribed to them, is cheap and costs nothing downstream.

The part most users care about: none of this changes how you use the CLI. Same commands. Same flags. Same output. The architecture is there because v0.2 is going to need it, and retrofitting it later would break everyone who adopted v0.1.

Next: the hook framework in detail, or browse commands.