QA on the VM showed two leaks in the effect:step tee: the host logger's
INFO lines (it always emits ANSI and defaults to info) landed on the
wizard screen, and the child's clack card drew colorless because
picocolors disables ANSI on a piped stdout, clashing with the wizard
theme.
hostExecStream now spawns steps with LOG_LEVEL=warn (operator-set level
wins; warnings/errors still pass) and FORCE_COLOR=1 when the operator's
terminal is a real TTY.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HB8rQxJN7itcFhNYVDntpm
SSF-002 — the telegram pairing card renders via clack's static note/log
primitives instead of bare console.log. hostExecStream tees non-status
lines verbatim, and static clack output is just lines — only
interactive/animated widgets need the TTY the piped child doesn't have.
SSF-003 — an either/or nc:prompt (validate:^(a|b)$) renders as an
arrow-key p.select; the options come from the validate regex itself
(literalChoices), so no grammar addition and zero skill edits. Prefixes
and format unions keep the text prompt. The `?` help-escape doesn't
apply to selects — every choice is valid and self-describing.
SSF-004 — a bounced step no longer dumps a raw stacktrace or the skill's
reference prose into the wizard. hostExec recomposes failures as
`exit <code>: <first stderr line>` (full stderr kept below for the
agentTask reason an agent fixes from); run-channel-skill shows one line
per bounce and writes the full text to a logs/setup-steps raw log, which
fail() forwards to the Claude handoff.
The directive engine (scripts/skill-apply.ts, skill-directives.ts) is
untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HB8rQxJN7itcFhNYVDntpm
Conflict resolutions:
- 9 SKILL.mds we rewrote in the nc: format: kept ours, then bumped every
@chat-adapter pin 4.26.0 -> 4.29.0 to match the merged chat core (the
lockstep lint enforces equality; pin-test expectation updated).
- 14 setup/add-*.sh, install-*.sh + setup/channels/{slack,signal,whatsapp}.ts:
stay deleted — replaced by the directive engine + runChannelSkill. Upstream's
Slack Socket Mode (cf8478ff, setup-side) will be ported into add-slack's
SKILL.md as a when:-guarded mode in a follow-up commit.
- src/cli/resources/groups.ts: fused create — upstream's --template branch
first, else our idempotent-on-folder create with ensureContainerConfig.
- src/cli/registry.ts + dispatch.test.ts: both sides' additions kept
(hostOnly field/doc + upstream's resource doc and approval-context fixture).
Known follow-ups (next commits): remove the now-dead nc:env-sync directive
(upstream removed the data/env/env mirror writers; the reader died pre-fork),
port Slack Socket Mode into the SKILL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two steps under one heading share a heading-derived caption (add-telegram's
build + test both render "5. Build and validate"), which reads as a
stuttered duplicate in the wizard. The driver now pre-computes labelOrdinals
from the parsed document (same pre-parse the gate policy uses) and suffixes
" (i/n)" when a caption repeats — keyed by the directive line the step
events already carry. Pure driver-side presentation; no engine change, no
event-payload change. Solo captions stay unsuffixed; counting is static, so
a runtime-skipped sibling can leave a cosmetic gap in the sequence.
Live stutter observed in the telegram wizard smoke run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two bugs the first live wizard run surfaced:
- channels-remote.sh's unanchored `*qwibitai/nanoclaw*` glob matched sibling
channel repos (qwibitai/nanoclaw-discord sorts first on multi-remote
machines) and resolved the from-branch copy to a remote with no channels
ref, failing every from-branch fetch. The pattern now anchors the repo-name
tail (nanoclaw / nanoclaw.git only).
- Once the run-health gate latched `blocked`, later nc:operator blocks still
rendered — walking the human through "a pairing code is about to appear"
and a readiness confirm for a step the engine had already gated. Blocked
runs now skip operator directives: no event (so no URL offer / readiness
confirm), no operatorMessages entry, recorded in skipped. Spec §2 updated.
Suite 722 passed | 1 skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
scripts/skill-policy.ts: UI-free gatePolicy(md) (§5.1 rules 1-5: guard-
compatibility, operator-chain termination, prompt/end-of-doc barriers,
confirm flavor) + extractOfferUrl(text) (§5.2 placeholder exclusion; slack's
<your-public-host> is the normative negative), unit-tested against the full
parity table over the real in-tree channel skills.
setup/lib/skill-driver.ts: clackPrompter/spinnerReporter dissolve into
clackResolveInput (help-escape intact) + a default onEvent policy handler
(spinner branch + operator note → URL offer → natural-barrier confirm,
decline = proceed); new TTY-gated confirm/openUrl seams on RunSkillOptions;
an injected onEvent replaces the default policy entirely; reuseFromEnv
pre-filters offers through the prompt's normalize/validate/flags (§5.4);
promptValidator loses min/error and derives its message from the question
prose (§5.3). run-channel-skill renames the overrides and passes the new
seams through; providers/install drops its defer-all stub Prompter. No
in-tree caller passes prompter/reporter anymore; normalizeValue is exported
for the reuse pre-filter. Suite: 723 passed | 1 skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add backGate(label) to setup/lib/back-nav.ts: a brightSelect ["Yes,
connect <label>", "← Back to channel selection"] wrapped in ensureAnswer
(Esc/Ctrl-C → exit 0), returning the existing BACK_TO_CHANNEL_SELECTION
sentinel on back. runChannelSkill grows an opt-in offerBack flag that
runs the gate at the very top — before resolveAgentName/role, the skill
run, and the wire (covers Teams/deferWire too). All auto.ts channel
dispatch sites opt in. Kept opt-in so headless callers and existing
tests are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
clackPrompter now wraps each prompt's validate with the tested
validateWithHelpEscape helper so a lone "?" short-circuits format checks,
then intercepts it post-prompt: it calls offerClaudeHandoff with the run's
channel + step context and the prompt question, then re-asks the same
prompt. Recursion is operator-bounded. Gated on a TTY, so a headless /
non-TTY run is a no-op (no interactive child spawned).
The channel + step label are plumbed through RunSkillOptions → runSkill →
clackPrompter, and run-channel-skill threads the channel it is wiring so
the handoff has the right context. An injected prompter is unaffected (the
injector owns its I/O).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a trailing-optional Prompter.open?(url) (wired in clackPrompter to
setup/lib/browser.ts openUrl, headless-safe). Extend the nc:operator handler
with two pure-polish attrs: open:<url> deep-links the operator to the page the
steps describe (after rendering, {{vars}} substituted), and a bare gate flag
turns the block into a human BARRIER — a confirm the engine waits on before the
following side-effecting directives run. Both degrade invisibly: a stripped
fence leaves the same prose, and a prompter without open/confirm skips them.
Grammar/validator awareness in skill-directives (open: var-ref check + empty-URL
flag). Applied: add-teams gates the operator blocks before the manifest build +
restart (fixes the manifest-builds-before-the-Azure-app-exists hazard) and opens
the Azure portal; add-telegram opens the t.me bot deep-link; add-discord opens
the OAuth invite URL (pre-filled with the captured application_id).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Widen Prompter.ask with a trailing-optional PromptOpts (flags/min/error/
normalize); existing async ask(name) fakes are untouched. clackPrompter builds
the regex with opts.flags, enforces min-length, surfaces opts.error, and sets
clearOnError on the secret prompt — via an exported, TTY-free promptValidator.
normalize (trim | rstrip-slash | lower) binds DETERMINISTICALLY in the engine,
so an inputs value and a typed answer land identically. The grammar + validator
learn flags/min/normalize/error/reuse on nc:prompt. reuseFromEnv gains a second
pass over reuse:<ENV_KEY> so a credential a helper script owns (imessage Photon
IMESSAGE_SERVER_URL/API_KEY, written by effect:external) regains the masked
reuse offer. Applied: add-teams app_password min:20, public_url
normalize:rstrip-slash; add-imessage server_url flags:i + reuse on the Photon
prompts. SKILLs stay lint-clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a channel skill doesn't fully apply, surface the prose beside the
directive that bounced as the operator's failure hint instead of a generic
"couldn't finish" message.
- skill-apply.ts: AgentTask gains hint? (default = trimmed prose; optional
on-fail:<token> attr narrows it to the prose LINE diagnosing the failure,
falling back to full prose when the token has no matching line so a stripped
fence never leaks a bare token). New exported firstFailureHint(res) returns the
first bounce's heading as a concise headline + its hint.
- run-channel-skill.ts: the !fullyApplied path threads firstFailureHint into
fail(`<channel>-install`, headline, hint) so runner.ts dims the hint and
forwards it to offerClaudeOnFailure. fail is now injectable for tests.
- claude-handoff.ts: repoint buildHandoffPrompt's stale setup/channels/<ch>.ts
reference to the channel's SKILL.md (the bespoke files are deleted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The apply engine now brackets each real mutation (applyOne) with a
StepReporter (stepStart/stepEnd, balanced even on the failure path) and
derives a human spinner caption per step via stepLabel: the nearest
heading for slow/effectful kinds (dep, run, branch-fetch copy), null for
instant kinds (local copy, env-set, json-merge, env-sync) and for the
self-rendering effect:step so a spinner never clobbers a QR/pairing card.
A label:<word> fence attr overrides; it's stripped with the fence, so it
degrades invisibly to prose.
runner.ts exposes its spinner machinery as startSpinner (runUnderSpinner
now delegates to it, behavior unchanged). skill-driver.ts builds a
TTY-gated spinnerReporter from it and passes it into applySkill by
default, so piped/CI/test runs stay silent and unchanged. run-channel-skill
threads an optional reporter override and keeps the connected_as line.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two generic engine primitives, derived from how the bespoke setup flows
already work, so the remaining channels can move to SKILL.md:
- `when:<var>=<value>` — a guard on any directive, evaluated against an
earlier prompt/capture var. Unmet (incl. unresolved) skips the directive;
a guarded prompt is skipped, never deferred, so a fully-programmatic apply
still completes across mutually-exclusive branches (e.g. imessage's
local vs remote mode). Lint requires the guard var be defined earlier.
- `nc:run effect:step` + `capture:<var>=<FIELD>,…` — directive access to the
existing spawnStep/StatusStream mechanism: runs a long-running,
operator-interactive step (a pairing code, a QR device-link), tees its
`=== NANOCLAW SETUP ===` blocks to the operator live, and binds the
terminal block's named fields into vars. Degrades to an agent when no
streaming exec is wired. New seam: ApplyOptions.execStream; the driver
provides hostExecStream. One primitive covers telegram/whatsapp/signal.
Full suite 624 passed | 1 skipped; the six already-converted skills still
lint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Closes the last two gaps to near-parity with the old bespoke setup.
Identity display ("Connected to slack as @bot in Acme"):
- add-slack's token check now captures the identity — auth.test piped through
jq into `capture:connected_as` (still fails on a bad token). runChannelSkill
shows res.vars.connected_as after the skill runs.
Reuse-existing credentials:
- Prompter gains `confirm`; clackPrompter implements it via p.confirm.
- runSkill gains a `reuse` option: before prompting, it maps each prompt var to
its ENV_KEY from the skill's own `env-set` directives, checks `.env`, and for
any already set offers "Found an existing SLACK_BOT_TOKEN (xoxb-…). Use it?".
Accepted values become inputs, so the prompt is skipped. runChannelSkill opts
in (reuse: true). Generic — works for any skill, no per-channel logic.
Tests: reuse accept/decline; identity capture flows through the adapter. 614 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restores the old setup's credential format checks. A prompt can carry
`validate:<regex>`; the clack prompter re-asks until the answer matches (a bad
paste is caught at entry, not later). `inputs` bypass it (programmatic apply is
trusted). Lint rejects a non-compiling regex.
add-slack prompts now validate: bot_token `^xoxb-`, signing_secret
`^[a-fA-F0-9]{16,}$`, owner_handle `^U[A-Z0-9]{8,}$`.
612 pass. Remaining setup polish: "Connected as @bot" identity display and
reuse-existing-credential prompts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
auto.ts now applies add-slack via the thin driver instead of the bespoke
runSlackChannel. The whole connect+wire procedure lives in the SKILL.md; the
host side is just the driver plus a small adapter that ensures the wire-target
agent group.
- runChannelSkill(channel, displayName): ensures the `dm-with-<name>` agent
group exists over ncl (idempotent — a 2nd DM channel reuses it), then runs
`.claude/skills/add-<channel>` with that folder pre-supplied as agent_folder.
Reports the outcome via fullyApplied. All I/O injectable for tests.
- `ncl groups create` → custom op: creates the agent group AND its container
config (a working group needs it; generic create made only a bare row),
idempotent on folder. The minimal wire-target slice of the deferred #3.
- skill-driver: resolveRemote is now an option (so the adapter/tests can inject).
- Deleted setup/channels/slack.ts — the first bespoke channel flow retired.
provider-contract test now points at run-channel-skill.ts.
Only slack is migrated; the other channels keep run<Channel>Channel until their
SKILL.md wire sections are converted. Tests: groups-create scaffold+idempotent;
the adapter drives the real add-slack with injected I/O (ensures group → wires
to it). 610 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The whole connect+wire procedure now lives in the SKILL.md (operator
walkthroughs, prompts, restart, ncl wiring), so the host-side driver is just:
render nc:operator via clack notes, ask nc:prompt via clack text/password, run
the engine in document order. One function replaces the bespoke per-channel
setup/channels/<channel>.ts flows.
setup/lib/skill-driver.ts:
- clackPrompter(): ask (password for secrets / text; cancel defers) + tell (note).
- hostExec(root): returns stdout (so `run capture:<var>` binds it) and puts the
project bin/ on PATH so a bare `ncl …` resolves to bin/ncl.
- runSkill(skillDir, opts): applySkill wired with the clack prompter, host exec,
fork-aware channels remote; accepts `inputs` (fully programmatic) and
`skipEffects` (e.g. ['restart'] when the caller restarts once). Returns the
ApplyResult; fullyApplied(res) is the success check.
- CLI entry: `tsx setup/lib/skill-driver.ts <skillDir>` applies interactively.
Tests: drives prompt/operator/wire through an injected prompter; runs fully from
inputs; hostExec PATH-resolves a bare command and returns stdout. typecheck +
608 host tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The install→restart→wire "phases" aren't a driver concern; they're directives in
document order. Restart is a run, placed between the install/credentials and the
wiring, and the engine already runs directives top-to-bottom — so no phase
orchestration is needed.
- effect:restart: a run whose body restarts the service and waits for the ncl
socket so a following wiring directive doesn't race it. New helper
setup/lib/restart.sh (platform-aware restart + best-effort socket wait; the
engine execs each body line separately, so one self-contained script).
- ApplyOptions.skipEffects: run effects the CALLER owns and performs itself are
skipped (not executed). A standalone /add-slack runs the restart; a headless
rebuild or a setup that restarts once at the end passes ['restart'] and it's
skipped. The declarative form of applyProviderSkill's isFlowOwnedCommand.
add-slack now carries the restart between Credentials and the wire, so the whole
flow is one self-contained ordered directive sequence. Dry-run: standalone runs
the restart, a flow caller skips it — both fully apply, both wire. 604 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`claude setup-token` runs under script(1) so the browser OAuth flow keeps a
TTY while we capture the printed token. On terminals that wrap long lines
(e.g. sbx), the token lands split across lines with padding spaces, and the
old parser — which stripped only ANSI codes and newlines — matched just the
first fragment and failed the trailing `AA` check. Login succeeded; only our
parse of the human-oriented output failed (`No sk-ant-oat…AA token found`).
Add setup/lib/captured-token.ts: normalize the capture (strip ANSI/control
bytes and all whitespace, un-wrapping the token) then extract. The TS caller
(claude-assist.ts) and the bash registration script now share it, so the
normalization rules can't drift. Placeholder lines like
`export CLAUDE_CODE_OAUTH_TOKEN=<token>` are ignored.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Builds on the structured-skill engine (slack was the first conversion). Adds the
two directives the rest of the family needed, converts the 12 remaining skills,
and routes their setup flows through the engine — deleting the hand-maintained
shell scripts that had drifted from the skills.
Engine (scripts/):
- json-merge: merge a keyed JSON object into an array file (container/cli-tools.json),
idempotent + journal-removable. add-codex uses it for its @openai/codex entry.
- append at:<marker>: insert before a `// <<< <marker>` line instead of EOF.
- setup/index.ts: a dormant `nanoclaw:setup-steps` marker in the STEPS map.
Conversions (.claude/skills/): discord, telegram, teams, imessage, linear, github,
webex, resend, matrix, gchat, whatsapp-cloud, codex — each aligned with its
now-deleted setup script (versions, copied-file lists). @chat-adapter/* pins match
our chat core (4.26.0); the lint enforces it.
Setup integration (setup/): the discord/telegram/teams/imessage channel flows and
the codex provider now apply their skill in-process via applySkill (secrets via the
Prompter, fork-aware remote resolution), mirroring slack. Deleted 5 add-*.sh + 9
install-*.sh drifted duplicates; rewired the claude-assist diagnostics map.
Channel remove no longer tears down the DB: wechat/emacs REMOVE.md stop deleting
messaging_groups/sessions/wirings. Those are user runtime data the skill never
created, so remove must not touch them — and orphan rows are inert (adapters start
from the registry, not the DB).
Verified: all 12 skills lint clean; 168/168 setup+scripts tests pass; no deps
installed by the conversion and no core barrels applied.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduces the optionally-deterministic skill format. Official skills carry
`nc:` directive fences (copy/append/dep/run/prompt/env-set/env-sync) embedded in
prose, so one SKILL.md is both agent-readable and machine-appliable. Robustness
lives in the whole system — graceful degradation to an agent, plus lint + tests —
not in the syntax, so the directives stay minimal and readable.
scripts/skill-directives.ts — parser + linter. Extracts nc: directives; flags
unpinned deps, undefined {{var}} references, and a @chat-adapter/* pin that
doesn't match our lockfile's `chat` core (the drift that put add-slack on the
wrong version).
scripts/skill-apply.ts — the application engine. Plan (idempotency, prompt
resolution, no writes) → mutate (copy/append/env-set, journaled) → run
(dep/build/test). Remove is the journal played back (no hand-written
REMOVE.md). Anything the engine can't do bounces to an AGENT with its prose —
never the human, never a hard abort. A Prompter abstraction lets one engine
serve both interactive setup and headless rebuilds; fork-aware remote
resolution replaces a hardcoded `origin`.
.claude/skills/add-slack/SKILL.md — converted to the format; `prompt` split from
`env-set` so a captured secret can feed env, ncl, or the vault; pinned 4.26.0
to match our chat core.
setup/channels/slack.ts — the Slack setup flow now applies the skill through the
engine in-process (secrets via the Prompter, never argv/disk), deleting the
hand-maintained setup/add-slack.sh + install-slack.sh, which had drifted from
the skill (they pinned 4.26.0 vs the skill's stale 4.27.0). One source of truth.
Verified end-to-end in an isolated worktree: apply copies the adapter + its
registration test, installs 4.26.0, builds clean against chat@4.26.0, and the
registration test passes. 19 unit tests for the parser + engine.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Make the agent provider a first-class, operator-chosen property instead of a
Claude-only assumption. Trunk gains the seams; the actual non-default payloads
(Codex first) install from the `providers` branch.
Setup
- A provider registry feeds a hard-wired setup picker (Claude | Codex). Picking
a non-default provider installs its payload (setup/add-codex.sh, channel-style),
runs a vault-only auth walkthrough (--step provider-auth), and records the pick
on the first agent before its first spawn.
- Picking Claude changes nothing — default installs are byte-for-byte unaffected.
Provider as a DB property
- Provider lives on container_configs.provider (materialized to container.json,
read by resolveProviderName). Creation stays provider-agnostic; the picked
provider is applied via the picked-provider seam. The deprecated
agent_groups.agent_provider path is not used.
Switching + memory
- Switch a live group with `ncl groups config update --provider` + restart.
- Memory never migrates at runtime — each provider keeps its own store. The
/migrate-memory skill carries a group's memory across a switch in either
direction (flat CLAUDE.local.md <-> memory/ scaffold). group-init seeds an
imported-agent-memory note for non-default providers; the runner's memory
definition reads it first turn. See docs/provider-migration.md.
No install-wide default, no runtime provider guard — switching is operator-by-
convention, consistent with the no-install-gating posture.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Injects credentials as request-time stubs so no credential is ever written
into a container or to disk. Gateway and CLI versions move to versions.json
(machine-checkable pins); breaking upgrades are documented in
docs/onecli-upgrades.md as an agent-executable runbook (detect / why / fix /
verify / rollback), and the update flow follows linked docs and diffs the
pins.
BREAKING: requires a gateway upgrade; the doc carries the steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Interactive setup handoffs (mid-flow `?` escape and on-failure) spawned
claude with all context in --append-system-prompt and no user message,
so Claude sat at an empty REPL until the user re-explained themselves.
Move the context into a positional prompt that auto-submits as the
first user message: Claude starts orienting immediately, the context
stays visible in the transcript, and it survives --resume.
Also:
- Share one session across all handoffs in a setup run: pin a
generated UUID via --session-id on the first spawn, --resume it on
later ones (stdio is inherited, so Claude's own id is never visible).
- Switch --permission-mode from acceptEdits to auto.
- Dedupe the two spawn blocks into spawnInteractiveClaude().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- .env backup and removal are now one atomic action: a failed backup
throws into executePlan's catch and the deletion never runs (the bash
original's set -e gave the same guarantee; the port had lost it)
- containers are re-listed by install label at removal time instead of
removed from scan-time ids — the live host can spawn containers during
the confirm phase
- uninstall telemetry no longer creates data/install-id (persistId:false
on emit), so --dry-run truly changes nothing and the already-clean
exit can fire
- runtime-tail failure notes are printed before the Done line instead
of being discarded
- uninstall.sh translates the old short flags (-n/-y) instead of
silently dropping them (-n used to fall through to a real interactive
uninstall)
- nanoclaw.sh gates the TS uninstaller on node (tsx's interpreter), not
pnpm, which the direct-exec path never uses
- detectExistingInstall also checks the system-level systemd unit
- a delete-onecli-agent spawn failure now notes the manual command
instead of claiming the agent was already gone
- setupLog.userInput is skipped when logs/ is absent so the uninstall
doesn't recreate it
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replaces the standalone bash uninstall.sh with a TypeScript flow inside the
setup driver (setup/uninstall/): scan (slug-scoped inventory), plan (pure
ordered removal actions), remove (per-action executor that absorbs failures
into notes), and flow (clack UI). uninstall.sh is now a 3-line pointer that
execs nanoclaw.sh --uninstall.
- nanoclaw.sh --uninstall short-circuits before diagnostics/bootstrap; with
no node_modules it prints manual cleanup commands and exits 1
- setup:auto routes --uninstall before initProgressionLog so an uninstall
never resets logs/setup.log
- fresh setup runs detect an existing install (service registration or
data/v2.db) and offer keep-and-continue (default) or uninstall-and-exit;
suppressed on fail()-retry and sg re-exec resumes
- self-deletion safety: static imports only, dist/ + node_modules/ removed
dead last, nothing but console.log after the runtime tail
- --yes never deletes orphan ag-* vault agents; their manual delete
commands (by vault uuid) are printed instead
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sweep of outbound strings, doc URLs, comments, and clone instructions
that were missed in the original org rename. One both-match case in
setup/lib/channels-remote.sh (URL detection) accepts either name so
existing forks with a `qwibitai` remote continue to resolve cleanly;
everywhere else is a straight rename.
Historical mentions left intact:
- CHANGELOG.md (v2.0.0 entry, frozen history)
- .claude/skills/add-gmail-tool/SKILL.md (pre-v2 qwibitai skill — historical)
- repo-tokens/badge.svg (auto-regenerated by update-tokens.yml)
Failures now launch an interactive Claude session instead of the
non-interactive assist (REASON/COMMAND parser). The user debugs
with full terminal access and types /exit to return to setup.
The original assist mode is available via --assist-mode flag or
NANOCLAW_SETUP_ASSIST_MODE=1 env var.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Picking the wrong messaging channel during setup left users with no way
to bail out — they had to either complete the chosen flow or kill setup
and start over. This adds a Back option to the first prompt of three
channel sub-flows that share the same simple shape (one leading
brightSelect that's easy to extend).
Mechanics:
- New `setup/lib/back-nav.ts` exports a BACK_TO_CHANNEL_SELECTION
sentinel and ChannelFlowResult type.
- `setup/auto.ts` wraps the channel dispatch in a while-loop; channels
return BACK_TO_CHANNEL_SELECTION to bounce back to the chooser
without restarting setup. Channels not yet wired return void and the
loop exits after one pass, so the change is backwards compatible.
- Discord, WhatsApp, iMessage each add a `← Back to channel selection`
option to their first prompt.
Telegram, Slack, Teams, and Signal will follow as separate PRs — they
each need a slightly different shape (extra prompt insertions, gating
inside multi-step flows, etc.) and are easier to review independently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
setup/lib/windowed-runner.ts was the one place on main still printing
elapsed time as raw seconds (`(170s)`) instead of using the
minute-aware `fmtDuration` helper from #2108. Two spots — the live
spinner suffix that ticks during the build, and the
success/error completion suffix — both now go through `fmtDuration`,
so anything past 60 seconds renders as `Xm Ys` (e.g. `2m 50s`) like
the rest of the setup flow.
The miss happened because a separate PR (closed) was supposed to
remove the timer entirely from this file, so #2108 deliberately
skipped it. With that other PR closed, applying `fmtDuration` here
is the consistent fix.
Pure formatting change. The helper itself is unchanged from #2108;
behavior under 60s is identical (`Xs`); behavior past 60s now
matches everywhere else.
- verify: remove the CLI ping; cli-agent step earlier in setup already
proved the round-trip works, and the test agent gets cleaned up before
verify runs — so the ping was guaranteed to fail on installs that wired
a messaging app instead of staying CLI-only. Status now collapses to
service-running ∧ credentials ∧ ≥1 wired group.
- agent-ping: catch Claude Code's "Please run /login" / "Not logged in" /
"Invalid API key" banners so a successfully-spawned agent that has no
credentials no longer reports as 'ok'.
- auth paste: validate the full sk-ant-oat…AA shape; when the cleaned
input is under 90 chars, surface a truncation-specific hint pointing at
terminal wrap as the likely cause. Strip internal whitespace at both
validate and assignment so multi-line pastes that survive clack also
go through cleanly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On GUI devices the URL was previously rendered dim inside the
instructional `note(...)` card, then `confirmThenOpen` printed
its prompt below: read the card, see the URL, then a separate
"Press Enter to open the X" prompt with no link near it. Two
visual moments for what's really one decision.
This PR pulls the URL out of the card on GUI devices and
relocates it directly under the action line of the confirm
prompt, separated only by a dim "If browser does not appear,
please visit: <url>" line:
│
◆ Press Enter to open the Developer Portal
│ If browser does not appear, please visit: … (dim)
│ ● Yes / ○ No
│
Action and fallback live as one prompt block — the user sees
both at the same time, no need to scroll back up to grab the
URL if the auto-open misses.
Headless behavior is unchanged: `formatNoteLink` still emits
"Get started: <url>" inside the card on headless devices (per
#2146), and `confirmThenOpen` still no-ops on headless (per
#2145). The only thing that changed for headless is the leading
`\n` in the helper output, which acts as a visual separator from
the steps above.
Five call sites adjusted (Discord ×3, Slack ×1, Telegram ×1) to
use `.filter((line) => line !== null)` so the now-nullable
`formatNoteLink` cleanly drops out of GUI-rendered cards.
When a card's auto-open is gated on `confirmThenOpen`, the URL also
appears inside the surrounding `note(...)` as a copy-paste fallback —
rendered dim because on a GUI device the auto-open is doing the
heavy lifting and the printed URL is just an incidental backup.
On headless devices the auto-open doesn't run (per #2145), so the
URL inside the note is the user's *only* path forward. A dim URL
reads as "incidental reference" exactly when it should be reading
as "this is the action."
Adds `formatNoteLink(url)` to setup/lib/browser.ts:
- GUI device → `k.dim(url)` (unchanged from today)
- Headless device → `Get started: <url>` at full strength
Replaces five call sites (Discord ×3, Slack ×1, Telegram ×1).
Single helper, atomic switch via the same `isHeadless()` plumbing
introduced in #2145, so the headless behavior across all five
flows stays in sync.
Wires the existing `isHeadless()` from setup/platform.ts into
`confirmThenOpen`. When the helper detects a headless device
(Linux without `DISPLAY`/`WAYLAND_DISPLAY`), both the
"Press Enter to open your browser" prompt and the actual
`openUrl(...)` call are skipped — there's no browser to launch
and the user can't usefully press Enter to summon one.
Why this is enough — the surrounding flow already supports the
headless path implicitly:
- Every `confirmThenOpen` call site sits beneath a `note(...)`
that prints the URL and the steps the user needs to take.
The URL is already visible to copy-paste onto another
device.
- Every site is followed by an explicit confirmation prompt
("Got your bot token?", "Done with the X?", etc.) that
naturally serves as the headless user's "I finished the
thing on my other device" signal.
So the headless branch becomes: read the note, do the thing,
answer the next prompt — without a useless "Press Enter to
open your browser" detour in between.
Coverage rationale (~95% accurate for the cases that actually
cause user confusion today):
- Linux + no `DISPLAY`/`WAYLAND_DISPLAY` → headless. Catches:
• Raspberry Pi headless installs
• Bare-metal Linux servers
• SSH'd into Linux without X11 forwarding
• CI environments on Linux
• Linux containers (which have no display)
- macOS → never headless. Even SSH'd Macs can usually still
open URLs through the local user's session, so treating
them as GUI-capable is the right default.
- Windows → never headless (effectively always GUI in
practice).
The remaining ~5% are edge cases (someone manually unset
`DISPLAY` on a desktop Linux session, etc.) that almost never
happen accidentally and recover gracefully — the URL is still
visible in the surrounding note.
Six call sites in channel adapters (Discord ×3, Slack ×1,
Telegram ×1, Teams ×1) all change behavior atomically through
the single helper. No per-site copy changes needed; consistency
is enforced by the central wiring.
Adds a `fmtDuration(ms)` helper in `setup/lib/theme.ts` that returns
`47s` under a minute and `1m 34s` from 60s onward, then routes every
elapsed-time spinner suffix in the setup flow through it. Replaces
the inline `Math.round((Date.now() - start) / 1000)` + `(${elapsed}s)`
pattern at every site.
Format is consistent past 60s — `1m 0s` over `1m` — so the live
spinner doesn't change shape at every whole-minute crossing.
Sites updated: setup/auto.ts, setup/lib/{runner,tz-from-claude,
claude-assist}.ts, and setup/channels/{signal,whatsapp,telegram,
discord,slack}.ts. Pre-allocated suffix budgets in `fitToWidth`
calls bumped from `' (999s)'` to `' (99m 59s)'` so long-running
steps don't blow past the reserved width.
Adds an `accentGreen` helper (#3fba50) with the same TTY/NO_COLOR/
truecolor gating as the rest of the palette, then wraps the word
"you" in the "What should your assistant call you?" prompt so the
operator parses at a glance who the question is about — the user,
not the assistant. The mirror prompt that asks for the assistant's
name ("What should your assistant be called?") is left for a
follow-up.
Customize `brightSelect`'s render function so the focused option's
label paints in brand cyan during selection and the submitted answer
paints in dim cyan after the user moves on. Inactive options keep
their default rendering — only the cursor and submitted state pick
up the color, matching the body-text emphasis added in #2101.
Also migrate the one remaining `p.select` call site (the "What next?"
prompt after the first chat) to `brightSelect` so every menu in the
setup flow goes through the same render path. The shape of the call
matches what `brightSelect` already supports — message + options
with value/label/hint — so no feature is lost in the swap.
Reuses `brandBody` from #2101 for the cyan, so the prompt highlight
and the body prose share one definition of the brand body color.
Adds a `brandBody` helper in setup/lib/theme.ts that wraps prose in
brand cyan (#2BB7CE), with the same TTY/NO_COLOR/truecolor gating used
by `brand`/`brandBold`/`brandChip`. The helper splits multi-line input
and colors each line independently so the SGR sequence doesn't bleed
across clack's gutter prefix.
Routing:
- `note()` (the un-dim card wrapper from #2095) now passes
`brandBody` as its `format` callback, so card bodies render
cyan line-by-line.
- Every prose `p.log.{message,info,success,step,warn}` call in the
setup flow wraps its body argument in `brandBody`. Calls whose
body is explicitly `k.dim(...)` (failure transcript tails, log
paths, claude-assist response previews) are left alone — those
are the "preview/debug" cases the dim-policy comment in
theme.ts already carves out.
- Spinner-finish lines in windowed-runner / claude-assist color
only the message portion; the `(5s)` elapsed suffix stays dim.
Brand cyan accents (chips, wordmark, inline emphasis) are unchanged.
This PR only adds the body color.
A follow-up will add OSC 11 dark/light detection so light-mode
terminals get a brand blue (#2b6fdc) variant — opt-in upgrade with
no regression for the dark-mode default.
When pasting an invalid token, the old value stayed in the input
field. Pasting a new token appended to the old one instead of
replacing it, causing repeated validation failures.
Add clearOnError: true to all 8 password prompts across setup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Clack's `p.note` defaults to `format: e => styleText("dim", e)`, which
fades note bodies regardless of the project's stated readability stance
(see comment on `dimWrap` in setup/lib/theme.ts: "prose renders at the
terminal's regular weight"). The dim styling makes body copy hard to
read on dark terminals and visibly washes out brand-colored segments
embedded in cards (e.g. the chip + bold heading rows).
Add a `note()` helper in setup/lib/theme.ts that wraps `p.note` with a
pass-through formatter, and route every setup-flow `p.note` call site
through it: setup/auto.ts, every setup/channels/*.ts adapter, and the
two setup/lib/claude-* helpers.
Pre-styled segments (brandBold, brandChip, formatPairingCard,
formatCodeCard) now render at full strength instead of being faded
alongside surrounding prose.
Use script(1) to capture PTY output and extract OAuth token when
browser-based auth isn't available, with fallback code-paste flow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When setup fails and claude-assist kicks in, instead of silently
skipping when the CLI is missing or unauthenticated, interactively
offer to install it (via install-claude.sh) and sign in (via
claude setup-token) so the user can get diagnostic help immediately.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original approach passed ANTHROPIC_AUTH_TOKEN into the container
as an env var and disabled the proxy for the custom host (NO_PROXY) —
which works, but bypasses OneCLI entirely for that credential. The
container holds the raw secret, the gateway loses audit/rotation, and
we lose the rest of the vault's protections for this cohort.
OneCLI-native version: store the token as a generic secret with header
injection (--header-name Authorization --value-format 'Bearer {value}'
+ host-pattern matching the base URL hostname). The container only
needs ANTHROPIC_BASE_URL plus a placeholder ANTHROPIC_AUTH_TOKEN — the
proxy rewrites the Authorization header on the wire.
setup/lib/setup-config.ts — adds --anthropic-auth-token alongside the
existing --anthropic-base-url.
setup/auto.ts — runAuthStep short-circuits the auth-method prompt when
both NANOCLAW_ANTHROPIC_BASE_URL and NANOCLAW_ANTHROPIC_AUTH_TOKEN are
set: creates the OneCLI generic secret, writes ANTHROPIC_BASE_URL to
.env (so the runtime reads it), and appends `import './claude.js';` to
src/providers/index.ts (so the provider only registers when the user
has configured a custom endpoint — no branching for everyone else).
src/providers/claude.ts — drops ANTHROPIC_AUTH_TOKEN/NO_PROXY
passthrough. Reads ANTHROPIC_BASE_URL from .env, sets a placeholder
ANTHROPIC_AUTH_TOKEN in container env so the SDK includes an
Authorization header for OneCLI to overwrite.
src/providers/index.ts — removes the unconditional import; setup
appends it on demand.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>