From 40b65521c0b94b7e03bcd79d7cf50101a7798b59 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sun, 5 Jul 2026 08:27:40 +0300 Subject: [PATCH] =?UTF-8?q?refactor(skills):=20retire=20nc:env-sync=20?= =?UTF-8?q?=E2=80=94=20the=20data/env/env=20mirror=20is=20dead?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/skills/add-deltachat/SKILL.md | 1 - .claude/skills/add-discord/SKILL.md | 6 +----- .claude/skills/add-gchat/SKILL.md | 5 +---- .claude/skills/add-github/SKILL.md | 5 +---- .claude/skills/add-imessage/SKILL.md | 6 ------ .claude/skills/add-linear/SKILL.md | 3 --- .claude/skills/add-matrix/SKILL.md | 5 ----- .claude/skills/add-resend/SKILL.md | 5 +---- .claude/skills/add-signal/SKILL.md | 3 --- .claude/skills/add-slack/SKILL.md | 3 --- .claude/skills/add-teams/SKILL.md | 7 ++----- .claude/skills/add-telegram/SKILL.md | 3 --- .claude/skills/add-webex/SKILL.md | 5 +---- .claude/skills/add-wechat/SKILL.md | 1 - .claude/skills/add-whatsapp-cloud/SKILL.md | 5 +---- .claude/skills/add-whatsapp/SKILL.md | 3 --- docs/skill-engine-seam.md | 2 +- scripts/skill-apply.ts | 6 ------ scripts/skill-directives.test.ts | 10 +++++++++- scripts/skill-directives.ts | 11 ++++++++--- scripts/skill-policy.ts | 2 +- 21 files changed, 27 insertions(+), 70 deletions(-) diff --git a/.claude/skills/add-deltachat/SKILL.md b/.claude/skills/add-deltachat/SKILL.md index 32b8927d0..857f99254 100644 --- a/.claude/skills/add-deltachat/SKILL.md +++ b/.claude/skills/add-deltachat/SKILL.md @@ -89,7 +89,6 @@ DC_SMTP_SECURITY=2 # 2=STARTTLS (default), 1=SSL/TLS, 3=plain Security settings are applied on every startup, so changing them in `.env` and restarting takes effect without wiping the account. -Sync to container: `mkdir -p data/env && cp .env data/env/env` ### Optional settings diff --git a/.claude/skills/add-discord/SKILL.md b/.claude/skills/add-discord/SKILL.md index d91b4d995..a09816139 100644 --- a/.claude/skills/add-discord/SKILL.md +++ b/.claude/skills/add-discord/SKILL.md @@ -96,17 +96,13 @@ curl -sf https://discord.com/api/v10/oauth2/applications/@me -H "Authorization: Store the token and the two derived credentials — the adapter reads them from `.env` and fails to start without `DISCORD_PUBLIC_KEY` and `DISCORD_APPLICATION_ID` -(set-if-absent, so a value you've already filled in is never overwritten) — and -sync them to the container: +(set-if-absent, so a value you've already filled in is never overwritten): ```nc:env-set DISCORD_BOT_TOKEN={{bot_token}} DISCORD_APPLICATION_ID={{application_id}} DISCORD_PUBLIC_KEY={{public_key}} ``` -```nc:env-sync -``` - ## Restart Restart the service so it loads the Discord adapter and the credentials you just diff --git a/.claude/skills/add-gchat/SKILL.md b/.claude/skills/add-gchat/SKILL.md index f1f4fb235..003c24c8b 100644 --- a/.claude/skills/add-gchat/SKILL.md +++ b/.claude/skills/add-gchat/SKILL.md @@ -86,7 +86,7 @@ Capture the service account JSON, then write it. `prompt` only *asks* and binds the answer to a name; a separate directive consumes it — so the same prompt could feed `ncl` or the OneCLI vault instead of `.env` by swapping only the consumer. Here it goes to `.env` (set-if-absent — a value you've already filled -in is never overwritten) as a single-line string, then syncs to the container: +in is never overwritten) as a single-line string: ```nc:prompt gchat_credentials secret Paste the service account JSON as a single line — the key file you downloaded, e.g. `{"type":"service_account","project_id":"...","private_key":"...","client_email":"..."}`. @@ -94,9 +94,6 @@ Paste the service account JSON as a single line — the key file you downloaded, ```nc:env-set GCHAT_CREDENTIALS={{gchat_credentials}} ``` -```nc:env-sync -``` - ### Webhook server The Chat SDK bridge automatically starts a shared webhook server on port 3000 diff --git a/.claude/skills/add-github/SKILL.md b/.claude/skills/add-github/SKILL.md index 154ab3c20..aefb9bbc4 100644 --- a/.claude/skills/add-github/SKILL.md +++ b/.claude/skills/add-github/SKILL.md @@ -96,7 +96,7 @@ Capture the three values, then write them. `prompt` only *asks* and binds the answer to a name; a separate directive consumes it — so the same prompts could feed `ncl` or the OneCLI vault instead of `.env` by swapping only the consumer. Here they go to `.env` (set-if-absent — a value you've already filled in is -never overwritten) and sync to the container: +never overwritten): ```nc:prompt github_token secret Paste the Fine-grained Personal Access Token for the bot account — starts with `github_pat_`. @@ -112,9 +112,6 @@ GITHUB_TOKEN={{github_token}} GITHUB_WEBHOOK_SECRET={{webhook_secret}} GITHUB_BOT_USERNAME={{bot_username}} ``` -```nc:env-sync -``` - `GITHUB_BOT_USERNAME` must match the bot account's GitHub username exactly. This is used for @-mention detection — the agent responds when someone writes `@your-bot-username` in a PR or issue comment. ## Wiring diff --git a/.claude/skills/add-imessage/SKILL.md b/.claude/skills/add-imessage/SKILL.md index 6245da302..c6faeaf6b 100644 --- a/.claude/skills/add-imessage/SKILL.md +++ b/.claude/skills/add-imessage/SKILL.md @@ -159,12 +159,6 @@ bash setup/channels/imessage-configure.sh local bash setup/channels/imessage-configure.sh remote "{{server_url}}" "{{api_key}}" ``` -Once the keys for your mode are written, sync `.env` to the container (the host -mounts `data/env/env`): - -```nc:env-sync -``` - ## Restart Restart the service so it loads the iMessage adapter and the credentials you diff --git a/.claude/skills/add-linear/SKILL.md b/.claude/skills/add-linear/SKILL.md index e4b7471d8..1272db2cc 100644 --- a/.claude/skills/add-linear/SKILL.md +++ b/.claude/skills/add-linear/SKILL.md @@ -135,9 +135,6 @@ LINEAR_WEBHOOK_SECRET={{linear_webhook_secret}} LINEAR_TEAM_KEY={{linear_team_key}} LINEAR_BOT_USERNAME={{linear_bot_username}} ``` -```nc:env-sync -``` - If you went the Personal API key route, add this line to `.env` instead of the OAuth pair (agent posts as you, your own comments are filtered): diff --git a/.claude/skills/add-matrix/SKILL.md b/.claude/skills/add-matrix/SKILL.md index 442547fa2..2363ec662 100644 --- a/.claude/skills/add-matrix/SKILL.md +++ b/.claude/skills/add-matrix/SKILL.md @@ -180,11 +180,6 @@ Option B only — the access token from Element Settings > Help & About, or from MATRIX_ACCESS_TOKEN={{access_token}} ``` -Then sync `.env` into the container: - -```nc:env-sync -``` - ## Next Steps If you're in the middle of `/setup`, return to the setup flow now. diff --git a/.claude/skills/add-resend/SKILL.md b/.claude/skills/add-resend/SKILL.md index 94ff1e28b..503fc5051 100644 --- a/.claude/skills/add-resend/SKILL.md +++ b/.claude/skills/add-resend/SKILL.md @@ -84,7 +84,7 @@ Capture the secrets, then write them. `prompt` only *asks* and binds the answer to a name; a separate directive consumes it — so the same prompts could feed `ncl` or the OneCLI vault instead of `.env` by swapping only the consumer. Here they go to `.env` (set-if-absent — a value you've already filled in is never -overwritten) and sync to the container: +overwritten): ```nc:prompt api_key secret Paste the Resend API key — API Keys, starts with `re_`. @@ -104,9 +104,6 @@ RESEND_FROM_ADDRESS={{from_address}} RESEND_FROM_NAME={{from_name}} RESEND_WEBHOOK_SECRET={{webhook_secret}} ``` -```nc:env-sync -``` - ## Connect yourself Because email is direct-addressable, the bot can write to you first — so wire diff --git a/.claude/skills/add-signal/SKILL.md b/.claude/skills/add-signal/SKILL.md index 0e7a6d1bc..c8baebcdf 100644 --- a/.claude/skills/add-signal/SKILL.md +++ b/.claude/skills/add-signal/SKILL.md @@ -118,9 +118,6 @@ it into the container env: ```nc:env-set SIGNAL_ACCOUNT={{platform_id}} ``` -```nc:env-sync -``` - ## Restart Restart the service so it loads the Signal adapter and binds the account you just diff --git a/.claude/skills/add-slack/SKILL.md b/.claude/skills/add-slack/SKILL.md index 314b8f4dd..77092f534 100644 --- a/.claude/skills/add-slack/SKILL.md +++ b/.claude/skills/add-slack/SKILL.md @@ -87,9 +87,6 @@ Paste the Signing Secret — Basic Information. SLACK_BOT_TOKEN={{bot_token}} SLACK_SIGNING_SECRET={{signing_secret}} ``` -```nc:env-sync -``` - The bridge serves the webhook on port 3000 at `/webhook/slack` automatically; to receive replies, that port must be reachable from the internet and registered with Slack. Tell the user: diff --git a/.claude/skills/add-teams/SKILL.md b/.claude/skills/add-teams/SKILL.md index d0e449091..eba5360e6 100644 --- a/.claude/skills/add-teams/SKILL.md +++ b/.claude/skills/add-teams/SKILL.md @@ -136,7 +136,7 @@ Paste the client secret Value — Certificates & secrets (shown only once, at le ### Store the credentials The adapter reads these from `.env` (set-if-absent, so a value you've already -filled in is never overwritten) and syncs them to the container. +filled in is never overwritten). `TEAMS_APP_TENANT_ID` is written only for a Single Tenant app; Multi Tenant doesn't need it. @@ -148,9 +148,6 @@ TEAMS_APP_TYPE={{app_type}} ```nc:env-set when:app_type=SingleTenant TEAMS_APP_TENANT_ID={{app_tenant_id}} ``` -```nc:env-sync -``` - ### Create the Azure Bot resource Tell the user: @@ -339,7 +336,7 @@ Business / EDU / developer tenant. 1. The tunnel is up and the messaging endpoint matches it: Azure Bot → **Configuration** → **Messaging endpoint** must be `https:///api/webhooks/teams`, and your tunnel (e.g. `ngrok http 3000`) must be forwarding to this machine's port 3000. 2. The adapter started: `grep -i teams logs/nanoclaw.log | tail`. -3. The credentials are in `.env` and synced to `data/env/env` (re-run the env-sync step if not). +3. The credentials are in `.env` (`TEAMS_APP_ID`, `TEAMS_APP_PASSWORD`, `TEAMS_APP_TYPE`). ### `Unauthorized` / 401 from Azure Bot Service diff --git a/.claude/skills/add-telegram/SKILL.md b/.claude/skills/add-telegram/SKILL.md index 0240c4a29..63e65b15d 100644 --- a/.claude/skills/add-telegram/SKILL.md +++ b/.claude/skills/add-telegram/SKILL.md @@ -102,9 +102,6 @@ Paste the bot token from BotFather (looks like `123456:ABC-DEF...`). ```nc:env-set TELEGRAM_BOT_TOKEN={{bot_token}} ``` -```nc:env-sync -``` - Confirm the token works and capture the bot's handle — `getMe` returns the bot account and fails here if the token is bad. You'll use the handle to open the right chat just before pairing: diff --git a/.claude/skills/add-webex/SKILL.md b/.claude/skills/add-webex/SKILL.md index 57ebf5385..e0b56b3e8 100644 --- a/.claude/skills/add-webex/SKILL.md +++ b/.claude/skills/add-webex/SKILL.md @@ -85,7 +85,7 @@ Capture the two values, then write them. `prompt` only *asks* and binds the answer to a name; a separate directive consumes it — so the same prompts could feed `ncl` or the OneCLI vault instead of `.env` by swapping only the consumer. Here they go to `.env` (set-if-absent — a value you've already filled in is -never overwritten) and sync to the container: +never overwritten): ```nc:prompt bot_token secret Paste the Bot Access Token — from the Webex bot you created. @@ -97,9 +97,6 @@ Paste the webhook secret you set for signature verification. WEBEX_BOT_TOKEN={{bot_token}} WEBEX_WEBHOOK_SECRET={{webhook_secret}} ``` -```nc:env-sync -``` - ### Webhook server The Chat SDK bridge automatically starts a shared webhook server on port 3000 diff --git a/.claude/skills/add-wechat/SKILL.md b/.claude/skills/add-wechat/SKILL.md index 80a29a5da..03947241c 100644 --- a/.claude/skills/add-wechat/SKILL.md +++ b/.claude/skills/add-wechat/SKILL.md @@ -85,7 +85,6 @@ Add to `.env`: WECHAT_ENABLED=true ``` -Sync to container: `mkdir -p data/env && cp .env data/env/env` ### 2. Start the service and scan the QR diff --git a/.claude/skills/add-whatsapp-cloud/SKILL.md b/.claude/skills/add-whatsapp-cloud/SKILL.md index 4fc64a94a..4f77d8d0e 100644 --- a/.claude/skills/add-whatsapp-cloud/SKILL.md +++ b/.claude/skills/add-whatsapp-cloud/SKILL.md @@ -88,7 +88,7 @@ Capture the four values, then write them. `prompt` only *asks* and binds the answer to a name; a separate directive consumes it — so the same prompts could feed `ncl` or the OneCLI vault instead of `.env` by swapping only the consumer. Here they go to `.env` (set-if-absent — a value you've already filled in is -never overwritten) and sync to the container: +never overwritten): ```nc:prompt access_token secret Paste the System User access token — WhatsApp > API Setup, with `whatsapp_business_messaging` permission. @@ -108,9 +108,6 @@ WHATSAPP_PHONE_NUMBER_ID={{phone_number_id}} WHATSAPP_APP_SECRET={{app_secret}} WHATSAPP_VERIFY_TOKEN={{verify_token}} ``` -```nc:env-sync -``` - ### Webhook server The Chat SDK bridge automatically starts a shared webhook server on port 3000 diff --git a/.claude/skills/add-whatsapp/SKILL.md b/.claude/skills/add-whatsapp/SKILL.md index c7fef6fd2..8f5de64ef 100644 --- a/.claude/skills/add-whatsapp/SKILL.md +++ b/.claude/skills/add-whatsapp/SKILL.md @@ -169,9 +169,6 @@ shouldn't be prefixed with its name. Record that (skipped for a shared number): ```nc:env-set when:number_kind=dedicated ASSISTANT_HAS_OWN_NUMBER=true ``` -```nc:env-sync when:number_kind=dedicated -``` - Resolve the conversation address as the WhatsApp JID for the number you chat from — the linked number for a shared account, or the dedicated number you just gave. Run the one matching the choice above: diff --git a/docs/skill-engine-seam.md b/docs/skill-engine-seam.md index cb3089223..7676ae7aa 100644 --- a/docs/skill-engine-seam.md +++ b/docs/skill-engine-seam.md @@ -315,7 +315,7 @@ For each `nc:operator` directive at line L, `gatePolicy` computes and double-confirm — the exact bug the teams parity table below forbids.) 3. Next compatible directive is a `prompt` → **no confirm** (the prompt is the barrier). 4. No such directive (end of document) → **no confirm** (a final handoff block, e.g. teams `:228`). -5. Anything else (`run`, `copy`, `dep`, `append`, `env-set`, `env-sync`, `json-merge`) → +5. Anything else (`run`, `copy`, `dep`, `append`, `env-set`, `json-merge`) → **confirm** after rendering. Confirm wording is derived from the barrier's *flavor* — the next compatible directive's effect: `effect:step` → readiness phrasing (`"Ready? The next step starts immediately."` — the block diff --git a/scripts/skill-apply.ts b/scripts/skill-apply.ts index c8b8d83ea..91b414e97 100644 --- a/scripts/skill-apply.ts +++ b/scripts/skill-apply.ts @@ -142,8 +142,6 @@ function selfStatus(d: Directive, root: string): { status: StepStatus; detail: s ? { status: 'apply', detail: `set ${missing.join(', ')} in .env` } : { status: 'skip', detail: `${keys.join(', ')} already set` }; } - case 'env-sync': - return { status: 'apply', detail: 'sync .env → data/env/env' }; case 'json-merge': { const into = String(d.attrs.into ?? ''); const key = String(d.attrs.key ?? ''); @@ -668,10 +666,6 @@ async function applyOne( } break; } - case 'env-sync': - mkdirSync(join(root, 'data/env'), { recursive: true }); - copyFileSync(join(root, '.env'), join(root, 'data/env/env')); - break; case 'json-merge': { const into = String(d.attrs.into); const key = String(d.attrs.key); diff --git a/scripts/skill-directives.test.ts b/scripts/skill-directives.test.ts index a23be9ae3..c27480b6e 100644 --- a/scripts/skill-directives.test.ts +++ b/scripts/skill-directives.test.ts @@ -19,7 +19,6 @@ describe('skill-directives parser, on the converted add-slack', () => { 'prompt', // credentials: capture bot token 'prompt', // credentials: capture signing secret 'env-set', // credentials: write captured values to .env - 'env-sync', // credentials: sync to container 'operator', // credentials: event-delivery walkthrough 'prompt', // resolve: owner member id (owner_handle) 'run', // resolve: validate token (auth.test) — fast-fail before the restart @@ -187,6 +186,15 @@ describe('append at: attribute', () => { }); }); +describe('retired directives', () => { + it('flags nc:env-sync with a targeted retirement error, not a generic unknown', () => { + const probs = validate(parseDirectives(['```nc:env-sync', '```'].join('\n'))); + expect(probs).toHaveLength(1); + expect(probs[0].message).toMatch(/retired/); + expect(probs[0].message).toMatch(/data\/env\/env/); + }); +}); + describe('when: guard + multi-field capture', () => { it('parses when: into attrs and lints a guard whose var an earlier prompt defined', () => { const md = ['```nc:prompt mode', 'local or remote', '```', '```nc:prompt server_url when:mode=remote', 'url', '```'].join('\n'); diff --git a/scripts/skill-directives.ts b/scripts/skill-directives.ts index c85c085ef..e1fbc4320 100644 --- a/scripts/skill-directives.ts +++ b/scripts/skill-directives.ts @@ -68,7 +68,6 @@ // pauses for confirmation before the next side effect is derived from // document structure (scripts/skill-policy.ts), never authored here. // env-set body: `KEY=value` ({{var}} allowed) set-if-absent -// env-sync (no body) `.env` → data/env/env idempotent copy // json-merge into: key: body: a JSON object push-if-absent // // `append` without `at:` adds to EOF; with `at:` it inserts before the @@ -113,7 +112,12 @@ export interface Problem { const FENCE = /^```(\S.*)?$/; const EXACT_SEMVER = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/; const VAR_REF = /\{\{\s*([A-Za-z_][A-Za-z0-9_]*)\s*\}\}/g; -const KNOWN = new Set(['copy', 'append', 'dep', 'run', 'prompt', 'operator', 'env-set', 'env-sync', 'json-merge']); +const KNOWN = new Set(['copy', 'append', 'dep', 'run', 'prompt', 'operator', 'env-set', 'json-merge']); +// Retired directives get a targeted lint error (not just "unknown") so an +// author knows the removal was deliberate and what to do instead. +const RETIRED: Record = { + 'env-sync': 'nc:env-sync was retired — nothing reads the data/env/env mirror (and it copied live tokens); delete the fence, the adapter reads .env directly', +}; const PROMPT_FLAGS = new Set(['secret']); export function parseDirectives(markdown: string): Directive[] { @@ -203,7 +207,8 @@ export function validate(directives: Directive[], ctx?: { chatVersion?: string } const defined = new Set(); const flag = (d: Directive, message: string) => problems.push({ line: d.line, kind: d.kind, message }); for (const d of directives) { - if (!KNOWN.has(d.kind)) flag(d, `unknown directive nc:${d.kind}`); + if (RETIRED[d.kind]) flag(d, RETIRED[d.kind]); + else if (!KNOWN.has(d.kind)) flag(d, `unknown directive nc:${d.kind}`); switch (d.kind) { case 'dep': for (const spec of d.body) { diff --git a/scripts/skill-policy.ts b/scripts/skill-policy.ts index 85b4c58b0..f0de24dcc 100644 --- a/scripts/skill-policy.ts +++ b/scripts/skill-policy.ts @@ -65,7 +65,7 @@ function guardOf(d: Directive): { v: string; value: string } | undefined { * 3. Next compatible directive is a `prompt` → no confirm (the prompt is the * barrier — the human can't paste a token before doing the steps). * 4. No such directive (end of document) → no confirm (a final handoff block). - * 5. Anything else (`run`, `copy`, `dep`, `append`, `env-set`, `env-sync`, + * 5. Anything else (`run`, `copy`, `dep`, `append`, `env-set`, * `json-merge`) → confirm, with the flavor derived from that barrier * directive's effect (`effect:step` → readiness, else completed). *