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>
Step 1 of the skill-engine seam plan (docs/skill-engine-seam.md §8.1):
- New core interface (§2): InputMeta + ApplyEvent types;
ApplyOptions.resolveInput (replaces Prompter.ask) and
ApplyOptions.onEvent (replaces StepReporter + Prompter.tell). The
engine prefers the new seams when present and falls back to the
legacy prompter/reporter otherwise (transitional — deleted in step 3).
- Awaited-event ordering (§2.3): every onEvent call is awaited before
the next directive; a handler throw is the standard bounce path,
including on operator events (blocked latch cascades).
- Validate-at-bind (§4): normalize-then-validate at the single bind
point for inputs, resolveInput answers, and legacy prompter answers
alike. A mismatch leaves the var unbound and records
`<var>: invalid value (does not match validate:<re>)` in deferred —
never the value (secrets can't leak), never a fallthrough from
invalid inputs to resolveInput.
- stepLabel null semantics re-documented (§2): a step-cost/interactivity
declaration, not spinner advice.
- §4 fixture sweep: Option-A slack inputs (signing_secret, owner_handle)
+ the userId assertion updated to validate-passing values; teams
deferWire app_password to a 20+-char value. Assertion structure
unchanged.
Suite: 699 passed | 1 skipped (was 680 | 1).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nothing has read data/env/env since commit 1a07869 removed the container
mount, yet setup still wrote the full .env (live tokens included) there.
Drop every writer and correct the stale comments/docs that described it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR #2837 added Slack Socket Mode end-to-end ("adapter + guided setup") but
was merged into the `channels` branch, not `main`. As a result the
`setup:auto` Slack flow on main is webhook-only: it always collects a
signing secret and pushes the user toward a public Request URL, with no
Socket Mode option — even though setup/verify.ts already recognizes
SLACK_APP_TOKEN and the channels-branch adapter supports it.
Forward-port the setup-side of #2837 onto current main, re-authored on top
of main's current flow (back-nav, inline agent wiring, operator-role prompt,
welcome DM all preserved):
- setup/channels/slack.ts: add a mode picker (askSlackMode), mode-specific
app-creation steps, collectAppToken() for the xapp- app-level token,
conditional credential collection + env, and a mode-aware post-install
checklist (Socket Mode skips the public-URL guidance).
- setup/add-slack.sh: require/persist either SLACK_APP_TOKEN (Socket Mode)
or SLACK_SIGNING_SECRET (webhook) instead of mandating the signing secret.
Scope is setup-side only: the adapter's socket support already lives on
`channels` (#2837/#2839) and reaches users via /add-slack; the add-slack
SKILL.md doc is handled by #2700.
Add referenceProse(md) to slice the engine-ignored reference sections
(## Alternatives / ## Optional configuration / ## Troubleshooting) verbatim
from the raw markdown, surfaced read-only on ApplyResult.referenceProse;
run-channel-skill shows it beside agentTasks on a bounce (the degrade floor a
human would scroll to). Add lintReferenceFloor(md) — WARN-ONLY (never blocks)
when a credentialed/interactive skill (nc:prompt secret or nc:run effect:step)
ships no ## Troubleshooting.
Restore the dropped agent-facing reference prose in add-signal (Path-A
dedicated-number registration, the 5 daemon env keys + TCP security note, the
~7 dropped Troubleshooting entries — correctly dropping the wiring SQL and the
Java prereq), add-whatsapp (QR-in-browser alternative + headless detection,
ASSISTANT_HAS_OWN_NUMBER, Pairing-code-not-working), and add-teams (Teams-CLI/az
auto path, RSC receive-all manifest block, a new Troubleshooting section). Also
delete the engine-narrating leak paragraph at the top of add-signal.
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>
telegram, signal, whatsapp, imessage, teams now run through the directive
engine via runChannelSkill — the bespoke setup/channels/<ch>.ts flows
(2.6k lines) are deleted. Built on the when:/effect:step primitives:
- imessage — when:mode=local/remote guards + setup/channels/imessage-configure.sh
(effect:external) for the mode-exclusive env writes. Wires normally.
- signal / telegram / whatsapp — effect:step against their trunk step scripts
(signal-auth / pair-telegram / whatsapp-auth), which now render their own
operator display (QR / code card) as plain stdout lines and emit the capture
fields in the terminal block. Wire via the resolved owner_handle + platform_id.
- teams — platform_id is runtime-only, so the SKILL installs + manifests + ends
with an nc:operator handoff; runChannelSkill gains a deferWire path that skips
resolve+wire (mirrors the old finishWithHandoff).
auto.ts dispatches every channel through runChannelSkill; provider-contract
CREATION_FILES drops the deleted flows. Byte-parity for platform_id + user_id
was adversarially re-derived per channel and confirmed identical.
Verified: typecheck clean; full suite 620 passed | 1 skipped; all 5 skills
lint clean; the deferWire teams path is exercised end-to-end (fake exec).
NOT yet integration-tested: the effect:step live streaming handshake against
real Telegram/Signal/WhatsApp accounts (no credentials available) — wiring,
byte-parity, and the step-script tweaks are verified statically only.
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>
Ran a multi-agent workflow over the remaining chat-SDK channels
(discord/teams/imessage/telegram), each converting its SKILL.md to the Option A
resolve pattern with an adversarial verify that the captured platform_id is
byte-identical to what the old bespoke .ts passed init-first-agent.
Outcome:
- discord: CONVERTED + verified. Resolve prompts owner_handle (Discord user id,
validate ^\d{17,20}$), captures connected_as (/users/@me) and platform_id
(POST /users/@me/channels → "discord:@me:" + .id — byte-identical to the old
discord:@me:<dmChannelId>). auto.ts now calls runChannelSkill('discord');
setup/channels/discord.ts deleted. (Minor deviation: application_id/public_key
are prompted-with-validate rather than auto-derived from /oauth2/applications/@me
as the old flow did — values identical, one extra paste standalone.)
- imessage: resolve added (owner_handle = phone/Apple-ID handle; platform_id =
the raw handle, byte-identical — native adapter, no prefix). NOT yet wired: its
credentials branch on local vs remote mode, which a directive can't express
deterministically (needs a shell-conditional run). Bespoke imessage.ts stays.
- teams: does NOT fit — the DM platform_id (teams:<b64 conv>:<b64 url>) only
exists after the first inbound activity; no synchronous API resolves it.
- telegram: does NOT fit — resolve is an interactive pairing handshake
(pair-telegram.ts), not a directive.
613 tests pass.
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>
The connect/wire procedure (owner role + cli_scope=global, messaging-group,
wiring, /welcome) is identical across channels, so it should NOT be re-expressed
in every channel SKILL.md. It stays in scripts/init-first-agent.ts. A channel
SKILL.md owns only the channel-specific part: install + credentials + resolving
the wire inputs (owner_handle + platform_id).
- Engine: ApplyResult gains `vars` — the non-secret resolved values (prompt
answers + capture outputs), so a caller can read what a skill produced.
Secrets are never surfaced.
- add-slack SKILL.md: the ncl wire block is removed. "Connect yourself" becomes
"Resolve your DM channel" — prompt owner_handle, validate auth.test, and
capture platform_id from conversations.open as `slack:<channelId>`.
- runChannelSkill: re-adds the shared polish (agent name default "Nano", role
owner|admin) and routes the wire through init-first-agent with the composed
user-id and the resolved platform_id. No ncl wiring emitted by the adapter.
This restores the two regressions for free (cli_scope=global and the /welcome
system instruction come from init-first-agent) plus role choice and the
agent-name prompt — with the wire in exactly one place. Dry-run on the real
add-slack: res.vars = {owner_handle, platform_id}, secrets excluded. 611 pass.
Remaining polish (next): prompt format-validation, "Connected as @bot" identity
display, 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>
The v2 DB seed queried `is_main` from the v1 `registered_groups` table, but
that column was a later v1 addition — older v1 installs (e.g. 1.1.0) don't have
it, so the migration's `1b-db` step crashes with `no such column: is_main` and
v2.db is never created, cascading into the sessions and tasks steps failing.
`is_main` was selected into the V1Group interface but never read anywhere, so
this just drops it from the SELECT and the interface. The accompanying comment
already states the intent ("Query only the columns we know exist in all v1
installs") — the code now matches it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The prior commit moves `chat` to 4.29.0, but main's own install pins were left
behind — and were inconsistent: the 8 /add-<channel> SKILL.md steps pinned
@chat-adapter/*@4.27.0 while the 12 setup/*.sh scripts pinned @4.26.0. Unify all
to @4.29.0 so `/add-<channel>` (and setup:auto) on a main install fetch an
adapter whose ChatInstance matches the bridge.
20 files, version-string only. Shell scripts pass `bash -n`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The setup preflight unloads *crash-looping* peers but ignores a more common
leftover: a launchd plist (or systemd unit) whose program no longer exists,
left behind when a NanoClaw checkout is deleted without running the
uninstaller. The health probe can't see these because an unloaded/inactive job
doesn't report via `launchctl print` / `systemctl show`, so they accumulate —
the OS keeps retrying a missing binary forever.
Detect a registration as dead when its `dist/index.js` target is absent on
disk, then unload (best-effort) and delete the orphaned config file. Own-label
and still-valid registrations are never touched.
Adds peer-cleanup.test.ts (the file previously had no tests) covering both
platforms: dead target removed, live target kept, own registration spared,
unrecognized config ignored.
Co-Authored-By: Claude Opus 4.8 (1M context) <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>
adfae67 moved the agent's global Node CLIs into container/cli-tools.json so a
skill adds one with a json-merge instead of editing the Dockerfile. The Codex
provider install was left behind — add-codex.sh still awk'd an ARG + RUN into
the Dockerfile and its test guarded that shape.
Migrate add-codex to the seam:
- add-codex.sh appends { name: "@openai/codex", version } to cli-tools.json
(idempotent json-merge); install/idempotency gates read the manifest.
- SKILL.md / REMOVE.md document the manifest append/removal, not Dockerfile edits.
- codex-dockerfile.test.ts -> codex-cli-tools.test.ts, asserting the manifest
entry (skips when the manifest is absent, e.g. the bare providers branch).
Pairs with the providers-branch commit that drops the codex Dockerfile lines,
renames the payload test, and points the setup install-check at the manifest.
Verified end-to-end: full add-codex install into a clean worktree leaves the
Dockerfile codex-free, the manifest correctly appended and idempotent; vitest
cli-tools.test.ts (6) and bun codex-cli-tools.test.ts (2) green; host tsc clean.
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>
Refuse to start unless this install reached the current version through a
sanctioned path (setup / update / migrate). A raw `git pull` that skips
migrations now fails loudly with a self-healing message instead of
silently breaking.
- src/upgrade-state.ts: marker at data/upgrade-state.json, getCodeVersion,
isUpgradeCurrent, enforceUpgradeTripwire (fails closed on missing /
corrupt / mismatched marker)
- src/index.ts: gate wired in at startup step 0.5, before DB init
- scripts/upgrade-state.ts: get/set CLI (also the override / recovery cmd)
- setup/service.ts, /update-nanoclaw, /migrate-nanoclaw: stamp on success;
update/migrate also self-update their own skill first
- CHANGELOG [BREAKING] entry bridges existing installs via the skills'
breaking-change check
- docs/upgrade-recovery.md: clearing the tripwire
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chat-adapter-imessage docs use photon.codes — our setup flow
and skill had the wrong domain.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
signal-cli >= 0.13 emits the account identifier as `number` in JSON
output, not `account`. The skip-if-already-linked path in signal-auth
always returned an empty list, so re-runs of setup unconditionally
tried `signal-cli link`, which fails when the data directory already
exists.
Read `number` first, fall back to `account` for older signal-cli.
Previously, passing --assistant-name <Name> when registering an agent
did a project-wide find-replace of "Andy" → <Name> across every
groups/*/CLAUDE.md file, and overwrote .env's ASSISTANT_NAME.
Two unintended consequences:
- Registering a second agent (e.g. "Homie") clobbered an unrelated
primary agent's CLAUDE.md. Real-world hit when wiring Homie's
Signal group on an install that already had Diddyclaw set up —
groups/diddyclaw/CLAUDE.md ended up with "Homie" references it
shouldn't have had.
- The install-wide .env ASSISTANT_NAME flipped to the most recently-
registered name, becoming the default trigger pattern for any
subsequent group registered without an explicit --assistant-name.
Both were a per-agent operation accidentally exercising project-wide
state. Now only groups/<folder>/CLAUDE.md of the agent being
registered is touched. .env is left alone — it represents the
install-wide default and shouldn't be flipped by per-agent registers.
If the install's primary-default name needs to change, that's an
explicit one-line .env edit, not a side-effect of registration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Without files:read, @chat-adapter/slack cannot download attachments —
Slack returns an HTML login page in place of file bytes and the adapter
throws a NetworkError. Bundles files:write for symmetric outbound
(files.uploadV2).
Closes#2457
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The upstream install script supports ONECLI_VERSION; use it to avoid
pulling an untested gateway release during setup.
Co-Authored-By: Claude Opus 4.6 (1M context) <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)
Rename the CLI binary, socket path, container wrapper, error prefixes,
and all references from `nc` to `ncl`. Add ~/.local/bin symlink during
setup and pnpm script alias.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>