Documents and implements the output contract from docs/setup-flow.md:
Level 1: clack UI — branded, concise, product content
Level 2: logs/setup.log — append-only, linear, structured entries for
humans + AI agents reviewing a run
Level 3: logs/setup-steps/NN-name.log — full raw stdout+stderr per step
Every scripted sub-step, including bootstrap, emits at all three levels.
Bootstrap now runs under a bash-side clack-alike spinner with live elapsed
time; its apt/pnpm output is captured to 01-bootstrap.log and summarised
as a progression entry. setup.sh's legacy log() routes to the raw log
instead of contaminating the progression log.
Telegram install becomes fully branded: setup/auto.ts owns the BotFather
instructions (clack note), token paste (clack password with format
validation), and getMe check (clack spinner). add-telegram.sh drops to a
non-interactive installer that reads TELEGRAM_BOT_TOKEN from env, logs to
stderr, and emits a single ADD_TELEGRAM status block on stdout.
The Anthropic credential flow is the one intentional break — register-
claude-token.sh still inherits the TTY for claude setup-token's browser
dance; it logs as an 'interactive' progression entry with the method.
setup/logs.ts centralises the level 2/3 formatting: reset, header, step,
userInput, complete, abort, stepRawLog. User answers (display name, agent
name, channel choice, telegram_token preview) log as their own entries so
the setup path is reconstructable from the progression log alone.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
9.4 KiB
Setup flow
This document is the contract for NanoClaw's end-to-end scripted setup
(bash nanoclaw.sh → pnpm run setup:auto). Read it before adding a new
step, fixing a regression, or changing how output is rendered.
The three output levels
Every setup step produces output at three distinct levels. They have different audiences, go to different places, and are formatted differently. Don't conflate them.
| Level | Audience | Destination | Format |
|---|---|---|---|
| 1. User-facing | The operator running setup | Terminal (via clack) | Branded, concise, informational — "product content" |
| 2. Progression | Future debuggers, AI agents reviewing a failed run, release support | logs/setup.log (one file, append-only) |
Structured per-step blocks, linear chronology, human + machine readable |
| 3. Raw | Whoever is deep-debugging a specific step | logs/setup-steps/NN-step-name.log (one file per step) |
Full raw child stdout + stderr, verbatim |
Think of it as: the user sees a summary, the progression log is an index with key facts, the raw logs are the evidence.
Level 1: user-facing (clack)
Rendered by setup/auto.ts via @clack/prompts. This is our product
surface for setup — every line should read as if we designed it for a
stranger on day one.
- Clack spinners for in-progress work. Show elapsed time.
p.log.success/p.log.step/p.log.warnfor permanent status markers.p.notefor multi-line information (pairing code, next steps).p.text/p.select/p.passwordfor prompts.- Brand palette:
brand()/brandBold()/brandChip()helpers insetup/auto.ts. Truecolor when the terminal supports it, 16-color cyan fallback otherwise, plain text when piped /NO_COLOR.
Rules:
- No discontinuity. Every sub-step belongs to the same visual flow. The only exception is Anthropic credential registration (see below).
- No raw child output. Never
stdio: 'inherit'a child whose output wasn't written by us. Capture it and show it on failure only. - No debug-style prefixes (
[add-telegram] …,INFO …, timestamps). Those belong in levels 2 and 3. - No emoji unless the clack glyph requires it.
Level 2: progression log
logs/setup.log — one file per setup run, append-only, cumulative across
a multi-run install (if a run fails midway and is re-attempted, the new
entries append). It's the thing you'd ask an operator to paste when they
report a setup bug, and the thing an AI agent would read to understand
what happened.
Entry format:
=== [2026-04-22T22:14:12Z] bootstrap [45.1s] → success ===
platform: linux
is_wsl: false
node_version: 22.22.2
deps_ok: true
native_ok: true
raw: logs/setup-steps/01-bootstrap.log
=== [2026-04-22T22:14:57Z] environment [2.3s] → success ===
docker: running
apple_container: not_found
raw: logs/setup-steps/02-environment.log
=== [2026-04-22T22:15:00Z] container [92.4s] → success ===
runtime: docker
image: nanoclaw-agent:latest
build_ok: true
raw: logs/setup-steps/03-container.log
Design constraints:
-
Start-time timestamp (UTC, ISO-8601) on the opening line so a
grepgives you the sequence. -
Duration in seconds with one decimal — fast steps read as "0.5s", not "0ms".
-
Status is one of:
success,skipped,failed,aborted. -
Fields are step-specific but must be short scalar values. No JSON, no multi-line. If a value is long, put it in the raw log and reference it.
-
Always emit a
raw:pointer, even on success — makes debugging the second failure easier. -
User choices are their own entries, not nested inside a step:
=== [2026-04-22T22:17:44Z] user-input → display_name === value: gav === [2026-04-22T22:17:51Z] user-input → channel_choice === value: telegramThese matter because the path through the setup flow depends on them.
The log opens with a header block identifying the run, and closes with a completion block:
## 2026-04-22T22:14:12Z · setup:auto started
user: exedev
cwd: /home/exedev/nanoclaw
branch: branded-setup
commit: 6e0d742
… (step entries) …
## 2026-04-22T22:18:54Z · completed (total 4m42s)
On failure the completion block names the failing step and its error:
## 2026-04-22T22:16:40Z · aborted at container (err=cache_miss)
Level 3: raw per-step logs
logs/setup-steps/NN-step-name.log — one file per step, numbered in
execution order (zero-padded 2-digit prefix for natural sorting). Full
verbatim stdout + stderr from the child process. Truncated and rewritten
on each run (not appended).
Contents are whatever the step emits: apt output, docker build layers,
pnpm install spam, curl bodies, etc. This is the evidence plane —
"what did the shell actually see?" Nothing is filtered.
Contract for a new step
When you add a step (either a TS step in setup/<name>.ts or a bash
installer invoked from auto.ts), it must:
-
Receive a raw-log path from the caller. Write all stdout + stderr there. Don't write to the terminal directly.
-
Emit a single terminal status block at the end, containing
STATUS: success|skipped|failedand any step-specific fields:=== NANOCLAW SETUP: STEP_NAME === STATUS: success KEY: value KEY: value === END ===Field names are
UPPER_SNAKE_CASE. Values are short scalars. -
If it's a long-running step, optionally emit sub-status blocks mid-stream.
auto.tsparses them live and can render intermediate UI (aspair-telegramdoes withPAIR_TELEGRAM_CODE/PAIR_TELEGRAM_ATTEMPT). -
Exit non-zero on hard failure so
auto.tscan distinguish "step ran to completion and reported failed" from "step crashed".
The driver handles the rest: spinner in level 1, structured append to level 2, raw capture to level 3.
The Anthropic exception
Anthropic credential registration (setup/register-claude-token.sh) is
the one permitted break in the visual flow. Why:
claude setup-tokenopens a browser, runs its own OAuth prompt, and prints the token. It owns the TTY viascript(1).- We don't want to re-implement the OAuth device flow ourselves.
- We don't want to intercept / mirror the token (it appears in the user's terminal already — mirroring it adds attack surface).
So during this step:
- The clack flow explicitly pauses (a
p.log.stepmarker says "this part is interactive, you're handing off to Anthropic"). - The child inherits stdio fully.
- When control returns, clack resumes on the next line with a success marker.
The level-2 log still gets an entry (auth [interactive] → success
with the method — subscription / oauth-token / api-key). Level-3 captures
are optional here; mirroring script -q output is tricky and the risk of
leaking the token to disk outweighs the debugging value.
File reference
| File | Role |
|---|---|
nanoclaw.sh |
Top-level wrapper. Phase 1 (bootstrap) and phase 2 (setup:auto) orchestration. Writes bootstrap's raw log + progression entry. |
setup.sh |
Phase 1 bootstrap: Node, pnpm, native-module verify. Emits its own BOOTSTRAP status block (historically printed to stdout; now goes to the bootstrap raw log). |
setup/auto.ts |
Phase 2 driver. Orchestrates the clack UI, step execution, user prompts, and writes to all three log levels for every step it spawns. |
setup/logs.ts |
The logging primitives (logStep, logUserInput, logComplete, stepRawLog, initSetupLog). Single source of truth for level 2/3 formatting and file paths. |
setup/<step>.ts |
Individual step implementations. Must emit one terminal status block; must not write directly to the terminal. |
setup/register-claude-token.sh |
The Anthropic exception. Inherits stdio, prints its own UI, returns a status to the driver. |
setup/add-telegram.sh |
Non-interactive adapter installer. Reads TELEGRAM_BOT_TOKEN from env; never prompts. User-facing bits live in auto.ts. |
setup/pair-telegram.ts |
Emits PAIR_TELEGRAM_CODE / PAIR_TELEGRAM_ATTEMPT / PAIR_TELEGRAM status blocks. Never prints UI. The driver renders it via clack notes. |
Common pitfalls
- Printing debug output from inside a step. Tempting during development; forbidden in checked-in code. All runtime messaging goes through status blocks (level 2) or raw log writes (level 3).
- Adding a
console.logthat "just this once" goes to the terminal. It breaks the clack flow — the spinner line gets torn. Uselog.info/log.errorfromsrc/log.ts(writes to the raw log) instead. stdio: 'inherit'for a non-exception child. See Anthropic above. Anything else needspipe+ explicit capture.- Tee-ing to stderr. Clack's spinner owns the terminal during a step. Even stderr writes tear the frame. Pipe everything, then choose what to surface.
- UTF-8 in bash
$VAR…positions. Bash's lexer can pull the first byte of a multi-byte character into the variable name and tripset -u. Always brace:${VAR}….
Future work (not yet implemented)
- Progression log rotation. Today's implementation truncates on each
run. Future: roll prior runs to
logs/setup.log.1,.2, etc. - Raw log rotation for multi-run installs. Currently each run overwrites. Fine for now; revisit if support needs to compare successive attempts.
- Structured output from
register-claude-token.sh. The interactive step emits no machine-readable status today. Future could add a post-interaction status block with the method used.