Skip to content

Operating Vita

This page is the operator's manual: how to stand Vita up, keep it running, verify changes before they ship, and extend it. Where other pages describe what a feature does, this one covers the surfaces a reimplementer touches first β€” the unified service manager, the verification gates, the flow orchestrator, the skill system, the agent launcher, the meta-status APIs, and the encrypted vault.

Everything routes through one entry point: the ./run script at the repo root. There are no Vita systemd units β€” ./run is the single source of truth for process lifecycle. Alerts about a dead service recommend ./run <svc> restart, never systemctl.


The ./run service manager

run is the repository's large Bash control plane (/home/zack/work/vita/run). It defines a service registry with a unified lifecycle interface, bundles full-stack dev commands, and exposes dev tooling (verification gates, type checks, generated contracts, lint, and metrics).

The script pins all ports at the top, so they are unambiguous:

API_PORT=33800        # internal API + scheduler
PUBLIC_API_PORT=33802 # read-only API for public tunnels, no scheduler
WEB_PORT=33801        # Vite dev server
STEPWISE_PORT=8341    # Stepwise flow server
DOCS_PORT=47447       # built MkDocs site

The unified service interface

Every registered service speaks the same five-verb interface:

./run <service> [start|stop|restart|status|log]

The registry (SERVICES_LIST in run) is the canonical list of services:

Service What it is Default URL
api Internal API server plus the scheduler loop engine http://localhost:33800
public-api Read-only API for public tunnels β€” no scheduler http://localhost:33802
web Vite dev server (web UI) http://localhost:33801
telegram Telegram bot β€”
claude-daemon Claude daemon β€”
cadence-daemon Cadence daemon β€”
stepwise Stepwise flow server http://localhost:8341
service-monitor Host-side health monitor (see below) β€”
podcast-tunnel ngrok tunnel β€” public podcast feed β€”
reports-tunnel ngrok tunnel β€” public reports site β€”
rss-tunnel ngrok tunnel β€” public RSS feed β€”
eu-tunnel ngrok tunnel β€” EU region β€”
docs MkDocs build + UTF-8 static server + docs tunnel https://vita-docs.ham.xyz
bp-demo Bikepacking partner-embed sandbox and tunnel https://bp-test.ham.xyz

Note: The split between api (scheduler-bearing) and public-api (read-only, scheduler-less) is deliberate: only one process runs the scheduler/loop engine so jobs never double-fire, while the public tunnels are served by a process that can be exposed safely.

How dispatch works: at the bottom of run, if the first argument matches a service name, it calls service_dispatch <svc> <action>. Dispatch resolves three convention-named Bash functions per service (dashes map to underscores) and calls the right one:

start_fn="start_${svc//-/_}"     # e.g. start_service_monitor
stop_fn="stop_${svc//-/_}"
status_fn="svc_status_${svc//-/_}"
  • start β€” idempotent: most start functions check a PID file (.logs/<svc>.pid) or pgrep first and adopt or skip an already-running process.
  • stop β€” terminates by PID file, cleans up sockets.
  • restart β€” stop, sleep 1, start.
  • status β€” prints one status line (HTTP health check where applicable).
  • log β€” tail -f the service's log (paths resolved by _svc_log_path; most live under .logs/, daemons under ~/.local/run/vita/, stepwise under .stepwise/logs/server.log).

To register a new service you add its name to SERVICES_LIST, define start_<name> / stop_<name> / svc_status_<name>, and add a log path to _svc_log_path().

Full-stack commands

./run dev [--profile local-safe|local-daemons|full]   # start the whole stack
./run stop                                             # stop everything
./run restart [--profile ...]                          # stop then start
./run status                                           # status of all services

dev/restart parse a profile that gates which optional services come up:

Profile API + public-api + web + telegram + stepwise Daemons (claude, cadence) Tunnels
local-safe yes no no
local-daemons yes yes no
full (default) yes yes yes

Individual toggles also exist: --with-daemons, --with-tunnels, --no-streamdock-refresh. The API start path waits up to 30s for GET /api/v1/health to return before declaring success.

Dev tooling commands

./run typecheck             # mypy regression gate (api/src) + tsc (web/)
./run test                  # API tests only (api/tests)
./run verify-fast           # fast verification gate (see below)
./run verify-standard       # fast gate + full API + full root tests
./run verify-full           # alias for test-all (the full local validation)
./run test-all              # API + root + web build + iOS subset
./run health [full]         # codebase hygiene checks
./run skills-lint           # validate SKILL.md frontmatter
./run architecture-metrics  # emit router/task/data-dir/skill counts
./run gen-types             # generate TS types from the OpenAPI schema
./run gen-capabilities      # regenerate capability Markdown + JSON contracts
./run capabilities <cmd>    # list/spec/call through the shared capability layer

Verification gates

Before any non-trivial edit, run a gate. They are layered β€” each tier subsumes the one below it, trading speed for coverage.

Gate What it runs Use when
verify-fast codebase health summary, the unsafe-writes gate, web typecheck, a board-schema API smoke test, and a curated set of critical root smoke tests (session coordination, sentinel registry/loop/API, parallel sessions, aloop contract, inference) before most edits
verify-standard verify-fast + the full API test suite (api/tests) + the full root test suite (tests/) before something risky
verify-full / test-all API tests, root tests, web build (type + bundle check), and an iOS vitaTests subset via xcodebuild when available before a release-grade change

Tip: The unsafe-writes gate (scripts/codebase/health.py --gate-unsafe-writes) fails the build if new code writes files outside the data-integrity helpers without being on the allowlist (scripts/codebase/unsafe_writes_allowlist.json). This is how the system enforces that all durable writes go through atomic_write() / atomic_append() rather than raw open(...).write().

The fast gate's curated test list lives inline in run (the fast_tests array) β€” these are the tests that catch the highest-blast-radius regressions (scheduler, sessions, sentinel) cheaply.

Health checks

./run health          # summary mode
./run health full     # full codebase hygiene report

health runs scripts/codebase/health.py, which spans many checks (skill frontmatter validity, unsafe-write detection, and more). It is also the gate that verify-fast invokes first, so a broken-hygiene tree fails fast.


Architecture metrics

./run architecture-metrics --summary
# routers=77 tasks=71 data_dirs=119 skills=57

scripts/codebase/architecture_metrics.py computes the system-wide counts that the docs and dashboards cite, so they are never hand-maintained:

Metric How it's counted
routers FastAPI routers under api/src/routers/
tasks LoopRegistration( literals in the loop engine β€” the canonical scheduler-task count
data_dirs top-level directories under data/
skills .claude/skills/*/ directories with a valid SKILL.md

Warning: Never hardcode these numbers in docs or code β€” they drift. Read them from architecture-metrics instead. (Snapshot verified 2026-07-15: routers=77, tasks=71, data-dirs=119, skills=57.)


Documentation publishing

The public documentation is a registered service:

./run docs restart
./run docs status
./run docs log

start_docs() runs three stages in order:

  1. uv run mkdocs build --clean renders docs/pages/ using mkdocs.yml;
  2. scripts/docs/gen_llms.py adds raw Markdown, llms.txt, llms-full.txt, and the generated capability contracts under /api/capabilities/; and
  3. scripts/docs/serve.py serves site/ on port 47447 with explicit UTF-8 types for text, Markdown, and JSON before the ngrok tunnel exposes vita-docs.ham.xyz.

Narrative pages are hand-authored. The files under docs/api/capabilities/ are generated from the live capability registry and must be refreshed with ./run gen-capabilities, not edited manually. The public Capability Layer page explains the design; the generated files are the exact operation contracts.


Stepwise flow orchestration

Stepwise is the multi-step workflow engine β€” a standalone CLI (stepwise, installed on PATH at ~/.local/bin/stepwise, never wrapped in uv run or python -m). A flow is a FLOW.yaml describing a pipeline of agent and script steps. Vita ships ~60 flows under flows/ β€” software-dev kits (plan / implement variants), research, council/tribunal deliberation, podcast pipelines, the executive loop, the capacity dispatch family, code review, docs audit, and more.

Note: Stepwise itself is a separate repository (~/work/stepwise), but it is always driven from ~/work/vita β€” flows live in vita/flows/, the job DB and runtime state live in vita/.stepwise/. Never cd to the stepwise repo to run a flow.

Running flows

stepwise agent-help                                      # source-of-truth flow + CLI reference
stepwise run flows/<flow> --name "verb: noun" --wait     # run a flow
./run stepwise [start|stop|restart|status|log]           # manage the flow server

Operating conventions (enforced project-wide):

  • Always run from ~/work/vita.
  • Always pass --name so jobs get human-readable labels in the DB.
  • Always use --wait together with a backgrounded bash call. --wait blocks until completion and prints a JSON result; running it backgrounded means you get notified on completion/failure without polling. Synchronous mode (no --wait) spams the terminal; --async then forgetting to check strands the job.
  • Never run two stepwise run calls for the same flow β€” that spawns duplicate agents and burns tokens.

Kits group related flows; stepwise agent-help <kit> shows the flows inside one. Run agent-help once per session β€” it is the always-current reference for flows, their inputs/outputs, and the CLI, so you never need to read raw FLOW.yaml files.

State, the DB, and recovery

Runtime state lives entirely under .stepwise/:

Path Holds
.stepwise/stepwise.db the job database (jobs, steps, schedule ticks, step runs)
.stepwise/jobs/, .stepwise/step-io/ per-job working files and step I/O
.stepwise/logs/server.log the flow server log
.stepwise/snapshots/ the integrity-gated snapshot ring (last 10 kept)
.stepwise/registry/, .stepwise/templates/ flow registry + templates

Stepwise has a history of a DB-corruption outage class (btrfs copy-on-write + WAL mmap producing "database disk image is malformed"). Two antibodies were added:

  1. A snapshot ring. scripts/stepwise_snapshot_ring.py runs at flow boot (nightly sleep + executive-loop boots), doing VACUUM INTO .stepwise/snapshots/stepwise-*.db, keeping the last 10, integrity-checked.
  2. A recovery command. ./run stepwise recover stops the server, quarantines the corrupt DB, restores the newest snapshot that passes PRAGMA integrity_check, and restarts.
./run stepwise recover --dry-run   # preview: live-db integrity, chosen snapshot, quarantine name
./run stepwise recover             # do it
./run stepwise recover --force     # override safety guards (e.g. live DB looks healthy)

The stepwise start path also pre-checks DB integrity and warns if .stepwise/ is not marked NoCOW (+C) on btrfs.

See Research and Deliberation for flows built on this engine.


The skill system

A skill is the unit of agent capability and slash command. Each lives in .claude/skills/<name>/ as a SKILL.md with YAML frontmatter, optionally plus supporting scripts and references. Use ./run architecture-metrics --summary for the live count (57 valid skills in the 2026-07-15 snapshot).

Minimal frontmatter:

---
name: my-skill
description: One line that tells the agent when to auto-apply this skill.
---

# My Skill
...instructions, references, examples...

skills-lint

./run skills-lint            # human-readable
./run skills-lint --json     # machine-readable {issues, count}

scripts/skills/lint.py delegates to check_skill_frontmatter() in scripts/codebase/health.py, which enforces:

Rule Failure
frontmatter is present "missing opening/closing frontmatter delimiter"
name and description keys exist "missing required keys"
name equals the folder name "name 'X' does not match folder 'Y'"
name is unique "duplicate skill name"

The same check runs inside ./run health, so broken skill metadata fails the hygiene gate too. The exit code is non-zero when any issue is found, which makes skills-lint usable as a CI gate.

Tip: Because the name must match the folder and be unique, the skill registry is collision-free by construction β€” the slash command /my-skill maps deterministically to one directory.

High-stakes builds with oneshot

The opt-in oneshot skill is Vita's expensive, evidence-first build protocol for work explicitly framed as graded, high-blast-radius, or β€œkitchen sink.” It was reorganized in July 2026 around four phases:

  1. plan β€” define success at the appropriate altitude and enumerate a keep/change/drop scope map;
  2. work β€” implement the settled plan using existing patterns;
  3. verify β€” fresh-context adversarial code review, real interface exercise, screenshot review, and a completeness critic, looping back on real findings;
  4. present β€” build a reviewable evidence surface and a cold-reader handoff.

The main agent remains the judge and delegates exploration and independent verification. UI work is not complete without exercising the real running surface; non-UI work uses the strongest primary evidence available. Evidence is routed by audience: RWGPS work uses an org-visible Cadence plat, while Vita and other personal work use a Vita canvas. The skill follows each repository's actual version-control conventionβ€”Vita itself remains on local master.


The agent launcher

./run agent launches a named Claude-Code agent identity β€” a different persona/config/toolset than the default session. Identities are JSON files in agents/ (developer, planner, ideator, telegram, brief-developer, user-terminal, with base.json as the shared parent and _schema.json validating them).

./run agent --list           # list launchable agents with descriptions
./run agent --list-all       # include non-launchable agents
./run agent <name>           # launch an agent identity
./run agent --validate       # validate all agent configs
./run agent --show <name>    # print resolved config without launching
./run agent --reset          # clear lock + restore default symlink

Internally (scripts/agents/launcher.py) a launch:

  1. Ensures the symlink structure exists β€” on first run it renames the real .claude/ directory to .claude.default/ and replaces .claude with a symlink pointing at it.
  2. Acquires a lock (.claude.lock) so two agents can't claim the workspace at once.
  3. Resolves the agent's config chain (base.json β†’ agent) and merges it.
  4. Generates the agent's context directory atomically.
  5. Switches the .claude symlink to point at the chosen agent's dir (relative path), then execs claude.
  6. On exit (or --reset), restores the .claude symlink back to .claude.default.

This symlink-switching is the isolation mechanism: each agent gets its own .claude contents (settings, skills, context) for the duration of the run without touching the others.

Note: These launcher agents are distinct from the board personas in agents/board/ (Aurelius, Dalio, da Vinci, Jobs, Musk, Rogers, Roosevelt) β€” those are advisory voices consumed by the Board flows, not launchable Claude-Code sessions.


Meta-status APIs

Two read endpoints give an at-a-glance view of the running system.

System-kernel overview

GET /api/v1/system-kernel/overview

api/src/routers/system_kernel.py returns a single snapshot spanning three subsystems:

Section Source Contents
project_registry scripts/projects/registry.py project counts + a few highlighted slugs
loop_engine api/src/loop_engine (group_counts, list_loop_registrations) scheduler group counts + highlighted loops (e.g. sentinel, vdb_daily)
session_kernel api/src/session_kernel.py the currently active voice session, if any

This is the consolidated meta-status surface: storage, loops, and sessions in one call. It rides on the loop-engine refactor that made LoopRegistration the canonical scheduler-task source β€” see Scheduling and Executive.

Service monitor

A host-side background service (scripts/service_monitor/host_runner.py, started by ./run dev) health-checks Vita services every 5 minutes and persists results:

File Holds
data/services/monitor_state.json current per-service state
data/services/monitor_history.jsonl append-only check history

It is exposed via api/src/routers/service_monitor.py:

GET  /api/v1/services/monitors   # all monitors + current state
GET  /api/v1/services/state      # current state snapshot
POST /api/v1/services/check      # trigger a check

It runs from the host (not Docker) so it can do socket/process checks containers can't. Manage it like any other service: ./run service-monitor [start|stop|status|log]. When it reports a dead service, the remediation is always ./run <svc> restart.


The encrypted vault

data/vault/ stores sensitive personal data that should never exist in plaintext on disk. There are no helper scripts β€” it is operated with raw openssl per data/vault/README.md.

Layout:

File Role
manifest.enc encrypted index of all entries (descriptions, tags, metadata)
<uuid>.enc one encrypted entry per file, named by UUID

Encryption: AES-256-CBC with PBKDF2 key derivation (100,000 iterations) via openssl enc. The passphrase exists only in the operator's memory and is supplied per session β€” never stored on disk.

# Decrypt the manifest to see what's in the vault (in-memory only)
openssl enc -d -aes-256-cbc -salt -pbkdf2 -iter 100000 \
  -in data/vault/manifest.enc -pass pass:"PLACEHOLDER_PASSPHRASE"

# Decrypt one entry
openssl enc -d -aes-256-cbc -salt -pbkdf2 -iter 100000 \
  -in data/vault/<ENTRY_UUID>.enc -pass pass:"PLACEHOLDER_PASSPHRASE"

Warning: Never write decrypted content to disk β€” decrypt into memory/context only. When adding an entry, write plaintext to /tmp, encrypt to data/vault/<uuid>.enc, update the manifest, then shred -u the plaintext.

Threat model: an attacker with machine access sees only N opaque .enc files and the README β€” no content, no categories, no metadata, because the manifest itself is encrypted. The README explicitly forbids speculating about contents from the existence of entries; this page documents the mechanism only.


File Locations

Path What it is
run the unified service manager + dev tooling entry point
scripts/codebase/health.py codebase hygiene checks, skill frontmatter, unsafe-writes gate
scripts/codebase/architecture_metrics.py system-wide router/task/data-dir/skill counts
scripts/skills/lint.py skills-lint entry point
.claude/skills/<name>/SKILL.md a skill definition
scripts/agents/launcher.py agent launcher (symlink-switching, locking)
agents/*.json, agents/board/*.json agent identities and board personas
flows/<flow>/FLOW.yaml a Stepwise flow definition
.stepwise/ Stepwise job DB, snapshots, logs, runtime state
scripts/stepwise_recover.py, scripts/stepwise_snapshot_ring.py recovery + snapshot ring
api/src/routers/system_kernel.py /api/v1/system-kernel/overview
scripts/service_monitor/host_runner.py, api/src/routers/service_monitor.py service monitor + its API
data/vault/, data/vault/README.md encrypted personal data + its operating manual
.logs/ per-service PID files and logs

Where to go next