Fuses 73 upstream commits into the structured-skill-format branch:
- src/cli: main's declaration-aware CRUD (resolveDefaults/postCreate/
postCommit, deep help, guard seam) fused with our idempotent natural-key
creates and hostOnly ops; hostOnly enforcement moved into commandDecide.
- add-whatsapp: main's number-ownership safety flow (ban-risk warning,
shared-mode interception, ASSISTANT_HAS_OWN_NUMBER/ASSISTANT_NAME
replace-writes, @-name engage pattern) ported into the structured skill;
orphaned setup/channels/whatsapp tests rewritten against the surviving
homes of their pure functions.
- add-slack/add-whatsapp: formatting container skills now copied from the
channels branch (trunk stopped shipping them); engine mkdirs copy dest
parents so fresh installs don't ENOENT; REMOVE.md cleans up the payload
and drops the retired data/env mirror sync.
- add-linear keeps explicit engage flags (Linear declares mentions:never —
dropping them would create a dead mention-mode wiring); channel-defaults
guidance ported as prose. add-signal regains ncl-based wiring guidance.
- gcal/gmail tool skills keep our ncl mount verbs (upstream's ISO-timestamp
SQL fix is subsumed by the DB layer's toISOString stamps).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- wirings preUpdate: treat legacy pattern rows with NULL engage_pattern
as match-all so unrelated updates no longer reject; still rejects when
the pattern fields themselves change invalidly (+ test)
- resolveWiringDefaults: drop a declared \b adjacent to a non-word char
in the {name} substitution so names like 'Andy (backup)' still engage
(+ tests; mirrors selfChatEngagePattern)
- wireApprovedChannel returns whether the wiring was created; the
free-text interceptor now reports failure instead of a false success
- ncl create hooks log a one-line notice when a channel has no declared
defaults and legacy static defaults apply
- stale comments/docs reconciled: chat-sdk-bridge DM thread-policy note,
seed-discord DISCORD_DEFAULTS sync note, user-dm deliberate 'strict'
rationale, manage-channels/update-nanoclaw/init-first-agent SKILL.md
policy wording
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJkDAnLGWJUqjgJNXpyqGZ
- setup/register.ts: import the channels barrel (registration-only, no
factory invocation), require --channel explicitly, resolve wiring engage
defaults and unknown_sender_policy through the declaration helpers gated
on hasDeclaredChannelDefaults (legacy heuristic/'strict' for undeclared
adapters), add --is-group / --engage-mode / --unknown-sender-policy flags
with --trigger kept as the explicit pattern override
- scripts/init-first-agent.ts: same barrel import + helper gating for the
DM wiring and messaging-group policy; new optional --engage-pattern flag;
sender_scope/'drop' hardcodes kept as deliberate owner-bootstrap choices
- scripts/init-cli-agent.ts: mg + wiring values now come from the cli
adapter's declaration (value-identical to the old hardcodes)
- move validateEngageAgainstChannel + EngageValues from
src/cli/resources/wirings.ts to src/channels/channel-defaults.ts so ncl
and the setup register step share one mention/pattern/sticky validator
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PJkDAnLGWJUqjgJNXpyqGZ
Storage: every JS-side datetime('now') write becomes new Date().toISOString()
(messages_out all writers, processing_ack all stamps, delivered, retry
backoff, roles/members/destinations, skill SQL via strftime ISO-Z). Dormant
deliver_after raw string compares wrapped in datetime().
Display: ncl human output renders ISO instants as local "YYYY-MM-DD HH:mm"
(host + container renderers; --json stays ISO; the stamp shape round-trips
through parseZonedToUtc). Task run logs, conversation archive filenames,
clidash, the dashboard pusher, and the uninstall backup stamp all render
local; instruction texts updated in lockstep. CLAUDE.md documents the
convention.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2YZQDTw9TQrH3m8NBtVAW
The identity note ("stop here (Ctrl-C) if that's not you") becomes a real
decision: an operator note names the detected account ({{owner_upn}}), then
nc:prompt wire_owner (^(yes|no)$ — renders as a select under the driver's
enum-prompt rendering) gates the whole DM-open chain via when:wire_owner=yes.
wire_owner is only prompted on fresh creates, and an unbound var fails any
when: guard, so drop-through re-runs skip the chain exactly as before.
On no: a note explains how to wire the desired Teams user afterwards (DM-first
path needs no IDs; proactive wiring needs the person's Entra object ID and
where to find it), the wire inputs stay unresolved, and wireIfResolved falls
through to the deferred ending. Install/create/env still complete either way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Same move as Slack/Discord: after the app install, open the bot<->owner 1:1
proactively with the bot credentials. The owner is the Teams-CLI account
that created the bot (teams status --json -> username + AAD object id); the
conversation-members fetch converts that to the 29: id inbound senders carry
and doubles as an identity cross-check; the platform id is composed exactly
as the adapter encodes thread ids. An operator note names who gets wired.
runChannelSkill gains wireIfResolved: a fresh create wires + welcomes like
any channel, a drop-through re-run resolves nothing and keeps the deferred
ending. Logout is now the operator's choice (yes/no prompt) instead of
automatic, and runs after the identity resolve.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 4-line operator block duplicated the wizard's ending note and was the
'too much for the average joe' feedback. Setup now says it once, at the very
end; the SKILL.md keeps a one-line prose version for agents applying the
skill outside the wizard. The note gains 'with your coding agent'.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One sign-in and one create command replace the ~7-step Azure portal walk:
- have_creds probe runs FIRST (either credential key in .env skips every
creation step — prompts included — so re-runs drop straight to Restart;
either-key semantics prevent a partial .env from pairing a new app with
stale keys, the guaranteed-401 mismatch).
- teams login runs as nc:run effect:step (browser locally, device code over
SSH; body emits the terminal status block the streaming exec requires).
- teams app create --json is one effect:external with a five-var JSON
capture (CLIENT_ID/CLIENT_SECRET/TENANT_ID + teamsAppId + installLink),
validate:^.+$ so a shape drift bounces instead of writing empty creds.
Single-tenant (--sign-in-audience myOrg) is the default pairing; the CLI
registers a Teams-managed bot — no Azure subscription, no manifest zip,
no manual sideload.
- Webhook endpoint corrected everywhere: /webhook/teams (was
/api/webhooks/teams, which silently receives nothing).
- Portal walk + multi-tenant variant moved to prose Alternatives; RSC via
teams app rsc add (CLI path) or manifest --rsc re-sideload (manual path).
- REMOVE.md: teams logout before CLI uninstall (MSAL cache outlives the
package), Entra deletion required on both paths (it revokes the secret),
drop the retired data/env mirror step.
- Parity tests rewritten: drop-through fixture (creds present → no create,
no prompts, fail() spy), runSkill-level fresh-create fixture (injected
execStream; proves capture→env-set chain + substituted install-link URL
offer), probe semantics executed from the shipped document, policy gate
inventory updated (3 operators, 1 barrier).
- claude-assist: point teams handoff file lists at files that exist on
this branch.
Known deferrals (engine-owned, flagged for the SSF branch owner):
reuseFromEnv offers capture-var env keys on re-runs (needs a one-line
promptShape guard); effect:external is not run-health gated; captures
cannot be marked secret. docs/skill-engine-seam.md line anchors into the
old teams SKILL.md are stale (pre-existing anchoring style).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream added Socket Mode to the guided setup (cf8478ff) in the bespoke
setup/channels/slack.ts this branch deleted. The port expresses it the way
the engine was built for: a `connection` prompt (socket|webhook) with
when:-guarded branches —
- socket: app-level token walkthrough (connections:write scope, Socket Mode
toggle), `nc:prompt app_token` (xapp-, reuse:SLACK_APP_TOKEN),
SLACK_APP_TOKEN env-set. The adapter enables Socket Mode purely from the
token's presence, so no public URL and no signing secret.
- webhook: the existing signing-secret + Event Subscriptions path, now
guard-scoped to its branch.
Gate-policy parity holds: all three operator blocks remain prompt-barriered
(no confirms), matching the §5.1 rules; the placeholder-URL negative fixture
moves to operator index 2. Structure-tracking tests updated (directive
order, operator/prompt/env-set inventories, Option-A inputs gain
connection:webhook).
Suite 827 passed | 1 skipped; add-slack lint exit 0.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upstream removed every setup-side writer of data/env/env (c82f062d) because
nothing has read it since the container mount was dropped — before this
branch even forked. Our engine codified the dead pattern as a directive:
every apply copied the full .env (live tokens included) into a file nothing
consumes.
- engine: env-sync handler removed from selfStatus + applyOne
- grammar: dropped from KNOWN; a new RETIRED table gives a targeted lint
error ("delete the fence, the adapter reads .env directly") instead of a
generic unknown-directive message
- skills: fence stripped from the 14 converted skills; orphaned "sync to the
container" prose cleaned (incl. add-deltachat/add-wechat old-format prose
upstream's sweep missed); teams troubleshooting entry rewritten
- policy/spec doc lists updated
Suite 827 passed | 1 skipped; all nc: skills lint clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 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>
Drop min:/error:/open:/gate validation and their grammar doc from
scripts/skill-directives.ts; add hard lint errors for all six removed
attrs (operator open:/gate, prompt min:/error:, any-directive
label:/on-fail:) so stale authorship fails loudly instead of silently
no-oping, each pointing at its replacement. Add the warn-only
lintGateAmbiguity check (unguarded operator followed by when:-guarded
directives spanning more than one branch value — spec §3/§5.1 open
question 1), wired into the CLI warnings alongside the reference floor.
Prompt/operator grammar doc rewritten for validate-at-bind and
structure-derived gating; the dead open: {{var}} ref check removed.
Directive tests reworked: min: dropped from the attrs fixture, the
open/gate supported-syntax suite re-added inverted, gate-ambiguity
fixtures incl. an all-channel-skills negative sweep.
Spec: docs/skill-engine-seam.md §8 step 5.
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>
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>
These are leftovers from the April-2026 inbound/outbound session-DB
split, marked @deprecated "kept temporarily for test compatibility":
sessionDbPath, openSessionDb, writeSystemResponse (session-manager),
getStuckProcessingIds (session-db, superseded by getProcessingClaims),
getSessionDb (container connection + barrel re-export), and the dead
registerResponseHandler/onShutdown re-exports from index.ts. The only
real callers are two dev scripts, repointed to inboundDbPath /
getInboundDb.
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>
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>
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>
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>
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>
When init-first-agent creates an agent group for an owner, set
cli_scope to 'global' so the owner's personal agent has full ncl
access. All other agent groups remain 'group'-scoped by default.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
The first-keyword check (`WITH` → SELECT path) was wrong for CTEs that
precede mutations (e.g. `WITH stale AS (...) DELETE FROM t WHERE ...`).
These would be routed through `db.prepare().all()` instead of executing
the mutation.
Use better-sqlite3's `stmt.reader` property, which asks SQLite's own
parser whether the statement returns data. Single mutations go through
`stmt.run()`; compound statements (which `prepare()` rejects) fall back
to `db.exec()`.
Add a regression test for WITH...DELETE.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Setup deliberately avoids the sqlite3 CLI (`setup/verify.ts:5` calls
this out: "Uses better-sqlite3 directly (no sqlite3 CLI)") and never
installs or probes for the binary. Despite that, 13 skills shelled out
to `sqlite3 ...` directly, breaking on hosts where the CLI isn't
preinstalled — the same root cause as #2191 but spread across the
skill surface.
Add `scripts/q.ts`, a ~30-LOC wrapper over the `better-sqlite3` dep
that setup already installs and verifies. Default output matches
`sqlite3 -list` (pipe-separated, no header) so existing skill text
reads identically — only the binary changes. SELECT/WITH queries go
through `db.prepare().all()`; everything else (INSERT/UPDATE/DELETE,
including compound statements) goes through `db.exec()`.
Migrate every in-tree caller:
- 17 hardcoded invocations across 8 SKILL.md files (init-first-agent,
add-deltachat, add-signal, add-emacs, add-whatsapp, add-ollama-provider,
debug, add-parallel) plus add-deltachat/VERIFY.md.
- `manage-channels/SKILL.md` shows canonical SQL but never prescribed
a tool, so the assistant defaulted to `sqlite3` and silently failed.
Add a one-line wrapper hint above the SQL block.
- `migrate-v2.sh` schema/count probes (was the original #2191 case).
Replace `.tables` with `SELECT name FROM sqlite_master`.
- Document the wrapper convention in root `CLAUDE.md` under "Central DB".
Add `scripts/q.test.ts` with 6 vitest cases covering both modes,
NULL rendering, empty-result, compound mutations, and arg validation.
Wire `scripts/**/*.test.ts` into `vitest.config.ts`.
Out of scope (flagged for follow-up):
- `debug` and `add-parallel` still reference the v1-only path
`store/messages.db`. Routing through the wrapper now produces a
cleaner "no such file" error, but the surrounding sections are
v1-era throughout — a v1-content cleanup is its own PR.
- `cleanup-sessions.sh` is being addressed in #1889 (different style,
hard-fail rather than wrap); left untouched here to avoid stepping
on that author's work.
Closes#2191.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Scratch agent uses fixed folder `_ping-test` so it can never collide
with a real agent on re-runs
- Added --folder flag to init-cli-agent.ts and cli-agent step wrapper
- Delete always targets `_ping-test` exactly — no re-derivation needed
- Removed normalizeName coupling and FOLDER status field (no longer needed)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- delete-cli-agent.ts discovers tables with agent_group_id dynamically
instead of hardcoding a list
- cli-agent step emits FOLDER in its status block so setup/auto.ts
reads it from the step result instead of re-deriving via normalizeName
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "Terminal Agent" created for the connection test is now silently
deleted after a successful ping. If the user chooses to chat, a new
agent is auto-created as "{name}'s Terminal" — no name prompt needed.
Condensed the three-line ping section into a single "Connection verified."
status line.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
setup/register.ts had two bugs that prevented new channels from being
registered via `/manage-channels`:
1. createMessagingGroupAgent was called with the legacy field names
`trigger_rules` and `response_scope`. The SQL INSERT expects
`engage_mode` / `engage_pattern` / `sender_scope` / `ignored_message_policy`
(migration 010). Every register call failed with
`RangeError: Missing named parameter "engage_mode"` after the agent
and messaging group were partially created — leaving an orphaned pair.
Now mirrors scripts/init-first-agent.ts:wireIfMissing:
- Groups (is_group=1) default to engage_mode='mention' (bot only
responds when addressed).
- DMs (is_group=0) default to engage_mode='pattern' with '.' (respond
to every message).
- An explicit --trigger overrides the pattern regex.
2. The "normalize platform_id" block unconditionally prefixed
"<channel>:" even for native IDs like WhatsApp JIDs
("120363408974444974@g.us"), iMessage emails ("user@example.com"),
or Signal phones ("+15551234567") / Signal groups ("group:abc"). But
the router (src/router.ts:158) looks up messaging_groups by the raw
event.platformId from the adapter, which for these native adapters
never has a prefix. So the prefixed row was never matched — the
message was silently dropped with no "Message routed" log.
Extracted scripts/init-first-agent.ts:namespacedPlatformId into
src/platform-id.ts so both setup paths use the same heuristic (skip
the prefix for IDs containing '@', starting with '+', or starting
with 'group:'). Prevents future drift between the two paths.
Tested by: re-running `setup/index.ts --step register` for a WhatsApp
group JID, confirming the row is created with correct engage fields
and matching platform_id, then sending a test message and observing
"Message routed" with the right agent group.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Native Signal adapter using signal-cli TCP JSON-RPC daemon. No Chat SDK
bridge or npm dependencies — uses only Node.js builtins.
Features:
- DM and group message support
- Voice message detection (placeholder text; transcription via
/add-voice-transcription skill)
- Typing indicators (DMs only)
- Mention detection via text match
- Managed daemon lifecycle (auto-start/stop signal-cli)
- Echo suppression for outbound messages
Also fixes init-first-agent.ts to skip channel-prefixing for phone
numbers (+...) and Signal group IDs (group:...), which are native
platform IDs that adapters send without a channel prefix.
Install via /add-signal skill. Uses /init-first-agent for channel wiring.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The concurrent poll in processQuery filtered out messages with
mismatched thread_ids, causing a deadlock when the initial batch
(e.g. a host-generated welcome trigger with null thread_id) completed
but follow-ups arrived with a different thread_id (e.g. a Discord DM).
The query stayed open waiting for matching-thread pushes that never
came, blocking the poll loop indefinitely.
Thread routing is the router's concern — per-thread sessions already
isolate threads into separate containers; shared sessions intentionally
merge everything. Removed the filter.
Also fixed processing_ack: a result event (with or without text) means
the turn is done, but markCompleted only ran when event.text was truthy.
When the agent responded via MCP send_message (empty result text), the
initial batch stayed in 'processing' for the query's lifetime, creating
false stuck signals in the host sweep. Now marks completed on any result
event.
Belt-and-suspenders: init-first-agent welcome trigger now sets threadId
to the DM platform_id instead of null.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the per-group "written once at init, owned by the group" CLAUDE.md
with a host-regenerated entry point that imports:
- a shared base (`container/CLAUDE.md` mounted RO at `/app/CLAUDE.md`)
- optional per-skill fragments (skills that ship `instructions.md`)
- optional per-MCP-server fragments (inline `instructions` field in
`container.json`)
- per-group agent memory (`CLAUDE.local.md`, auto-loaded by Claude Code)
Principle: RW = per-group memory, RO = shared content. Source/skills/base
are shared; personality, config, working files, and Claude state stay
per-group.
Key changes:
- New `src/claude-md-compose.ts` — per-spawn composition +
`migrateGroupsToClaudeLocal()` one-time cutover.
- New `container/CLAUDE.md` — shared base, seeded verbatim from the
former `groups/global/CLAUDE.md`.
- `src/container-runner.ts` — swap `/workspace/global` mount for RO
`/app/CLAUDE.md`; call `composeGroupClaudeMd()` after
`initGroupFilesystem()`.
- `src/group-init.ts` — drop `.claude-global.md` symlink + initial
`CLAUDE.md` write; seed `CLAUDE.local.md` from `opts.instructions`.
- `src/index.ts` — call `migrateGroupsToClaudeLocal()` at startup.
- `src/container-config.ts` — add optional `instructions` field to
`McpServerConfig` (inline per-MCP guidance fragment).
- `container/Dockerfile` — drop dead `/workspace/global` mkdir.
- Remove obsolete `scripts/migrate-group-claude-md.ts`.
Migration (runs once at host startup, idempotent):
- Delete `.claude-global.md` symlinks in each group.
- Rename each `groups/<folder>/CLAUDE.md` → `CLAUDE.local.md`
(preserves existing per-group content as memory).
- Delete `groups/global/` directory.
Design docs: `docs/claude-md-composition.md` and `docs/shared-source.md`
(the latter is the sibling design discussion this refactor builds on).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>