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>
The engine now declares and emits only: inputs/resolveInput acquire,
onEvent renders. Removed the legacy Prompter (ask/tell/confirm/open),
PromptOpts, StepReporter, ApplyOptions.prompter/reporter, and engine
handling of open:/gate (driver policy now owns both), label: (labels
are heading-derived only), on-fail:+AgentTask.hint (hint is always the
prose — firstFailureHint reads it directly), and the min:/error:
plumbing (flags/normalize live on InputMeta). Engine tests for the
removed syntax deleted; semantic siblings kept (heading labels,
prose-hint default, operator-body {{var}} deferral — plus a new
no-event-fired assertion for the deferred block).
Spec: docs/skill-engine-seam.md §8 step 3, §9.
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>
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 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>
Extend the stdout-capture branch (effect:fetch/external) so a
`capture:a=.x,b=.owner.id` spec parses the command stdout as JSON and binds
each var to its jq-style dot-path — one API call resolves several values. A
bare `capture:var` (no =) still binds stdout as-is. effect:step's terminal-
block field capture is untouched (distinguished by effect). A `validate:<re>`
on a run capture shape-guards each bound value; a mismatch (or unparseable
JSON) throws → bounces to an agent (a command's output has no human to
re-prompt). The linter validates the run-capture regex too.
Rewrite add-discord Resolve to derive application_id + public_key +
owner_handle from one `GET /oauth2/applications/@me` call, removing the two
hand-pastes and the shape-only prompt validation. Resolved owner_handle /
platform_id stay byte-identical to the prior hand-entered flow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add an `effect:check` branch to the run-case in the apply engine: it runs the
body as a shell predicate, mutating nothing (no journal, no capture). A non-zero
exit bounces to an agent and latches the run-health `blocked` flag, so a failed
precondition gates the dangerous side effects (restart, pairing/QR step, wire)
that follow; an unresolved {{var}} defers. Document the new effect in the
directive grammar.
Author the four lost safety preconditions as one-line nc:run effect:check blocks
beside their prose: add-imessage (non-Mac local hard-fail), add-whatsapp (empty
captured bot_phone guard), add-teams (Azure app_id set before manifest build),
add-signal (signal-cli presence probe before linking).
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>
Once any directive bounces to an agent, the skill is no longer in a
known-good state, so the engine must not fire a dangerous side effect on
its own — a live restart, an interactive pairing/QR step, or a wire after
an upstream failure just wastes the operator's time (a doomed QR, a
restart that loads a bad credential). `blocked` latches on the first
bounce and converts any later effect:restart / effect:step / effect:wire
into its own bounce, so the agent finishes it from the prose after fixing
the failure. A deferred prompt (headless rebuild, no answer) never
bounces, so `blocked` stays false and a later restart remains runnable.
Reorder add-slack so auth.test / conversations.open validate the
credential ABOVE effect:restart — fast-fail a bad token while it's still
cheap, matching the old bespoke-flow position.
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>
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>
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>
ApplyOptions gains `inputs` (prompt var → value), checked before the prompter.
Pass every answer and the whole skill runs through with no human interaction and
no prompter; the prompter (now optional) only fills prompts `inputs` doesn't
cover. A prompt with neither still defers (headless rebuild, unchanged).
- ApplyResult gains `operatorMessages`: every `nc:operator` body is collected
(and still rendered live when an interactive prompter is present), so a
programmatic caller relays/outputs the human steps instead of them blocking.
- `fullyApplied(res)` — the check a programmatic caller makes: nothing deferred
for a missing input, nothing bounced to an agent.
Proof: the real add-slack skill applied from `inputs` alone (no prompter) →
fullyApplied true, 0 deferred / 0 agentTasks, the captured DM channel flows into
all 5 ncl commands, and both operator walkthroughs are collected for relay.
Tests: run-through-from-inputs, missing-input-defers, inputs-win-over-prompter.
604 pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The SKILL.md is addressed to the coding agent (the interpreter), so its prose is
agent-facing by default. The parts meant for the HUMAN operator — clicking
through the Slack UI — were undifferentiated prose, so an agent couldn't tell
"relay this to the user" from "do this yourself", and the engine couldn't render
them as human steps.
nc:operator is the output twin of nc:prompt (prompt: ask the operator for input;
operator: give the operator instructions). Lead it with agent-facing prose like
"Tell the user:" so the agent relays it. The engine renders the body via the
Prompter's new optional tell() (a clack note in setup, a channel message when an
agent relays; absent in a headless rebuild → skipped). {{vars}} substitute in, so
a resolved value can be shown to the operator.
The content model is now: agent prose (unmarked, the degrade floor) / execution
directives / operator I/O (prompt in, operator out). add-slack's app-creation and
event-delivery walkthroughs are now nc:operator blocks; strip the fences and each
reads "Tell the user: <steps>" — natural, never narrating the engine.
Dry-run on the real skill: the two operator blocks route to tell() (the human),
the ncl commands to exec() (the agent) — cleanly separated. 600 tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Output-capture is the twin of prompt: prompt binds human input into {{var}},
`run capture:<var>` binds a command's stdout. With it, the two steps that made
slack.ts "resist" being a SKILL.md — validate the token, resolve the DM channel —
are ordinary directives, so nothing in the bespoke flow is left unexpressible.
Engine:
- `exec` now returns the command's stdout; a `run capture:<var>` binds the
trimmed stdout into the vars map (a single-command run; last wins). Composes
with substitution: a captured value flows into later {{refs}}. The journal
still records the original (placeholder) command — secrets/values don't leak.
- Linter treats `run capture:<var>` as defining the var, so a downstream
{{var}} doesn't false-flag; doc + error message updated.
add-slack: the full procedure setup/channels/slack.ts ran is now in the SKILL.md
Wire section — prompt the member id + agent folder, `run effect:fetch` to
validate the bot token (auth.test) and resolve the DM channel
(conversations.open → `capture:dm_channel`), then `run effect:wire` to
users create → roles grant → messaging-groups create → wirings create →
messaging-groups send welcome. Dry-run against the real skill: the captured
`D0SLACK999` flows into every ncl command; zero deferred, zero agentTasks —
fully deterministic, the secret token never journaled.
Tests: capture binding + downstream substitution + lint-accepts-captured-var;
add-slack template guard updated to the complete install→credentials→wire flow
incl. the capture step. Full suite 597 passed | 1 skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wiring a channel is just "collect input + call ncl", so it needs no new
directive — only that nc:run interpolates prompted {{vars}} and that the ncl
wiring verbs are idempotent + natural-key addressable.
Engine:
- nc:run now substitutes prompted {{vars}} into commands (same VAR_REF as
env-set), so a run can `ncl … {{owner_email}}`. The journal records the
ORIGINAL command (placeholders intact) so a substituted secret never lands in
the journal/remove replay. An unanswered prompt → unresolved → deferred
(degrade), never a crash. New documented effect:wire (no undo — the rows it
creates are user runtime data, not reversed on remove).
ncl:
- crud gains optional `naturalKey`: generic create returns an existing row
instead of a UNIQUE violation, making re-applied wiring idempotent (users).
- `messaging-groups create` → custom op: defaults the NOT NULL `instance` to
channel_type and is idempotent on (channel_type, platform_id, instance).
- `messaging-groups send` → new op: injects an inbound (the cli admin-transport
InboundEvent) to wake the wired agent — the welcome, as a plain ncl call.
- `wirings create` → custom op: resolves natural keys (channel_type+platform_id
→ messaging group; agent-group folder → agent group) and is idempotent on the
pair. Direct --messaging-group-id / --agent-group-id still work.
Skills (proof, one per archetype):
- add-resend: owner-bootstrap wiring is now nc:prompt + nc:run effect:wire
(users create → roles grant → messaging-groups create → wirings create →
messaging-groups send welcome), replacing the init-first-agent shell block.
- add-linear: pre-wire is now nc:prompt + nc:run effect:wire, reusing the
{{linear_team_key}} collected in Credentials — also retires the raw-SQL
INSERTs (anti-pattern #4, and they referenced a stale column set).
Tests: nc:run substitution (interpolate / journal-original / defer-on-unanswered);
ncl verb idempotency + natural-key resolution via host-caller dispatch. Full
suite 594 passed | 1 skipped.
Co-Authored-By: Claude Opus 4.8 <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>