Commit Graph

264 Commits

Author SHA1 Message Date
gavrielc 81fea1d7ec Merge origin/main (185 commits) into feat/structured-skill-format
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>
2026-07-05 08:21:49 +03:00
gavrielc 8a3444a68c feat(setup): ordinal-suffix repeated spinner captions — (1/2), (2/2)
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>
2026-07-04 18:36:40 +03:00
gavrielc 0c020e664d fix(setup): live-smoke fixes — anchor the channels-remote glob; silence operator blocks once blocked
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>
2026-07-04 18:05:59 +03:00
gavrielc 22b294e233 refactor(skills): shared policy module + wizard driver migration — presentation derived from document structure
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>
2026-07-04 16:32:10 +03:00
gavrielc 79060c5407 refactor(skills): core seam (additive) + validate-at-bind — resolveInput/onEvent land beside the legacy prompter/reporter
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>
2026-07-04 16:19:18 +03:00
gavrielc c82f062d57 Remove the dead data/env/env secrets mirror from setup
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>
2026-07-04 16:11:09 +03:00
Rob Stevenson cf8478ffbb fix(setup): offer Slack Socket Mode in the guided setup flow
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.
2026-06-29 14:45:31 +01:00
gavrielc 9c91c00c93 feat(setup): reference-prose floor — restore dropped reference prose + kill engine-narration leak
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>
2026-06-28 00:12:06 +03:00
gavrielc f8214ed1d8 feat(setup): backGate — first-prompt back-to-channel-selection
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>
2026-06-28 00:01:51 +03:00
gavrielc 665b0dca77 feat(setup): help-escape handoff — a lone "?" at any prompt hands off to Claude
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>
2026-06-27 23:55:53 +03:00
gavrielc 46de9ae05a feat(setup): Prompter.open + nc:operator open:/gate — human barrier for manual UI steps
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>
2026-06-27 23:47:13 +03:00
gavrielc d7d104aa0b feat(setup): M3 PromptOpts — flags/min/normalize/error + helper-owned reuse
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>
2026-06-27 23:32:50 +03:00
gavrielc bd0962fd97 feat(setup): bounced-step prose as failure hint — M4 surface the diagnosis
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>
2026-06-27 23:24:12 +03:00
gavrielc 462619d287 feat(setup): apply-lifecycle reporter — per-step driver spinners
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>
2026-06-27 22:59:28 +03:00
gavrielc 3b41171071 feat(setup): convert all 5 remaining channels to the SKILL.md flow; delete bespoke .ts
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>
2026-06-27 21:07:26 +03:00
gavrielc 6f616824d6 feat(skills): when: guards + effect:step streaming run with multi-field capture
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>
2026-06-27 20:27:54 +03:00
gavrielc ab596d31d3 feat(setup): convert discord to the Option A SKILL.md flow; delete discord.ts
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>
2026-06-27 19:22:00 +03:00
gavrielc 564e605900 feat(setup): finish the polish — identity display + reuse-existing credentials
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>
2026-06-27 19:04:45 +03:00
gavrielc be0ca10630 feat(skills): nc:prompt validate:<re> — restore input format validation
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>
2026-06-27 18:43:49 +03:00
gavrielc 0a5ffde133 refactor(setup): Option A — shared wire stays in init-first-agent, no duplication
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>
2026-06-27 18:40:19 +03:00
gavrielc 4c4b2ecbbe feat(setup): wire /setup slack through the SKILL.md driver — delete slack.ts
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>
2026-06-27 17:32:29 +03:00
gavrielc f6c13eb6f3 feat(setup): thin generic skill driver — render + ask + run, no phasing
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>
2026-06-27 17:18:37 +03:00
gavrielc f629ff25fa feat(skills): effect:restart directive — phasing is just directive order
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>
2026-06-27 17:11:35 +03:00
Christophe Benoist 797491d8b3 fix(migrate-v2): don't SELECT is_main from v1 registered_groups
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>
2026-06-25 15:43:19 -04:00
amit-shafnir 9bb69c0e50 Merge pull request #2830 from amit-shafnir/fix/peer-dead-plist-reaper
fix(setup): reap dead peer service registrations whose binary is gone
2026-06-25 11:27:54 +03:00
Gabi Simons 8f2f788b6e chore(deps): bump channel adapter install pins to 4.29.0 (skills + setup)
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>
2026-06-23 13:47:31 +03:00
Amit Shafnir 15292ae76c fix(setup): reap dead peer service registrations whose binary is gone
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>
2026-06-21 22:58:51 +03:00
Amit Shafnir 53ed3b77c9 fix(setup): allow env-selected agent provider 2026-06-18 17:49:13 +03:00
Amit Shafnir 197faaaa14 fix(setup): parse Claude OAuth token from wrapped PTY capture
`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>
2026-06-18 00:20:21 +03:00
gavrielc 59460e9a5c feat(skills): convert all chat-sdk channels + codex to nc: format, wire setup
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>
2026-06-16 21:48:30 +03:00
gavrielc ae48986e42 feat(skills): structured nc: directive format + apply engine, first applied to Slack
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>
2026-06-14 22:42:47 +03:00
gavrielc ac0a799cbf refactor(add-codex): install Codex CLI via cli-tools.json, not the Dockerfile
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>
2026-06-14 21:40:44 +03:00
Omri Maya 13a37def89 feat(providers): operator-driven provider selection, switching, and memory migration
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>
2026-06-14 07:49:39 +03:00
Omri Maya 3f9e89d345 feat(onecli): SDK 2.2.1 — credential-stub mounts + machine-checkable pins
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>
2026-06-12 11:30:11 +03:00
gavrielc 67ccd9e74c fix(setup): auto-submit handoff context as Claude's first prompt
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>
2026-06-11 15:14:48 +03:00
Amit Shafnir d8748e3a45 fix: address uninstaller review findings
- .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>
2026-06-10 15:50:12 +03:00
Amit Shafnir 41a720dd59 feat: port uninstaller to TS, wire nanoclaw.sh --uninstall, detect existing installs in setup
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>
2026-06-10 15:50:12 +03:00
gavrielc e734e5cddd feat(upgrade): startup tripwire + upgrade marker
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>
2026-06-06 13:02:12 +03:00
gavrielc e794223968 Merge pull request #2558 from guyb1/main
fix(setup): correct OneCLI default URL from app to api subdomain
2026-05-22 22:58:45 +03:00
gavrielc 5466109104 Merge pull request #2563 from kky/pr/setup-register-scope
fix(setup-register): scope --assistant-name to the registered group only
2026-05-22 22:50:22 +03:00
gavrielc ea06453bcb fix: correct Photon URL from photon.im to photon.codes
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>
2026-05-22 22:41:28 +03:00
Paul Snyman 3a87953bc9 fix(signal-auth): read 'number' field from signal-cli 0.13+ listAccounts
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.
2026-05-21 13:28:48 -07:00
Claw 6db6919086 fix(setup-register): scope --assistant-name to the registered group only
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>
2026-05-19 17:55:24 -04:00
Guy Ben Aharon 1b29a60358 fix(setup): correct OneCLI default URL from app to api subdomain
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-19 21:10:40 +03:00
madevizslove183 4d81dc4e0e setup: add files:read and files:write to Slack scope checklist
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>
2026-05-13 17:43:15 +02:00
gavrielc c9c5ffadc9 fix(setup): pin OneCLI gateway version to 1.23.0
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>
2026-05-13 01:16:33 +03:00
glifocat bda72a4bf4 chore: rename remaining qwibitai/nanoclaw references to nanocoai/nanoclaw
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)
2026-05-11 08:40:09 +02:00
gavrielc 2d03c94252 Merge pull request #2350 from qwibitai/ncl
feat(cli): add ncl admin CLI
2026-05-08 21:05:29 +03:00
gavrielc 93ec82ce38 Merge pull request #2300 from alipgoldberg/setup/slack-member-id-card
setup: correct Slack member-ID card directions
2026-05-08 20:14:27 +03:00
gavrielc 0855369b79 refactor(cli): rename nc to ncl
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>
2026-05-08 15:56:09 +03:00