#3801·nanoclaw

update-nanoclaw validate: channel refresh overwrites files that local patch skills modified

Author: foxskyCreated Sep 14, 2026Updated Sep 14, 2026

What happens

At main 3f9ed607, update-nanoclaw validate refreshes detected installed channels and providers before the host build and tests. After a successful refresh, it commits any resulting changes in the staging branch as chore: refresh installed skill payloads (scripts/update/transaction.ts:330–341).

For WhatsApp, the staged .claude/skills/add-whatsapp/SKILL.md:79–84 copies src/channels/whatsapp.ts and the other listed payload files from the selected remote's channels branch. Refresh mode always applies that copy, replacing the destination's contents. A committed local edit absent from the selected registry payload is therefore removed unless a later refresh step restores it.

Normal install mode skips a copy fence when all its destinations exist. If any destination is missing, it recopies every file in that fence and can also overwrite existing edits. Overwriting is not exclusive to refresh; refresh disables the all-destinations-present skip (scripts/skill-apply.ts:137–146,651–667).

The narrower gap

docs/skills-model.md:11 says:

Every customization is a skill: not an edit buried in the core, but a skill that adds the change on top.

The document describes patch skills and requires tests for their edits (lines 41–48). It also describes ordinary upgrades as pulling upstream and fixing test failures, and recipe rebuilds as applying every skill afresh (lines 105–129). It does not promise automatic replay of every patch skill during every refresh.

The current refresh helper discovers channel/provider installations through their barrel imports and applies their corresponding add-<name>/SKILL.md. It does not discover separate patch skills or execute a fork recipe (scripts/update-skills.ts:78–104,129–183). Merely retaining a patch skill and recipe in the fork does not make validation reapply the patch after copying its target file.

A retained test that detects the lost change will fail validation and block cutover. Without such a check, this refresh path has no separate check for lost local edits. Patch skills are required to ship integration tests; merely having tests does not establish that they cover this loss or survive the payload copy.

Reproduction

Prerequisites: use a controller that loads successfully (apply the extraction fix in #3800 if using the documented bootstrap), a clean fork on a named branch with WhatsApp installed, and a reachable configured remote carrying the required channels payload.

  1. Make and commit a change to src/channels/whatsapp.ts that is absent from the selected registry payload. Retain its patch skill and recipe in the fork, but do not add a refresh hook to add-whatsapp.
  2. Run the documented prepare --project-root ... --upstream-ref ... --strategy merge, and record its id and stageRoot. Resolve any staging merge conflicts before continuing. The staged adapter still contains the committed local change.
  3. Record the staging HEAD, then run the staged controller's validate --project-root ... --id ....
  4. Once skill refresh succeeds, inspect the staging history:
bash
git -C "$stageRoot" log --oneline
git -C "$stageRoot" show <refresh-commit> -- src/channels/whatsapp.ts
git -C "$stageRoot" diff <pre-refresh-commit> HEAD -- src/channels/whatsapp.ts

The refresh commit removes the local change. git diff -- src/channels/whatsapp.ts in the stage is empty after that commit, provided no later step edits the file. The live checkout still has the original patch until cutover. A subsequent test failure does not undo the staging refresh commit.

This sequence was reproduced offline with the upstream transaction/refresh/apply code, the upstream WhatsApp skill definition, local Git remotes, and a synthetic adapter payload. Dependency installation and build commands were stubbed. A patch assertion at the test step failed without a hook and passed with one; this was not a full WhatsApp installation or full project test run.

Existing mechanisms and requested improvement

nc:run effect:refresh is already documented in docs/skill-directives.md:69 and covered by scripts/skill-apply.test.ts:125–151. An idempotent command placed after the copy in the staged add-whatsapp/SKILL.md can reapply local patches before validation:

nc
bash scripts/local/whatsapp/refresh.sh

This is an example hook; the applier must be supplied by the fork. The current integration point requires editing the channel skill definition. Recipe rebuilds and /migrate-nanoclaw also provide explicit customization-reapplication workflows, and the registry remote can be selected with NANOCLAW_REGISTRY_REMOTE. Those mechanisms are distinct from automatic replay of separate patch skills during validate.

/update-skills reads the existing channel skill definition and refreshes its declared payloads; it does not replace add-whatsapp/SKILL.md itself. The Git integration in prepare can update that definition, but an ordinary non-conflicting merge preserves a locally added hook. An unrelated upstream edit to the definition does not by itself remove the local section.

Please provide a documented way to compose separate local patch skills after a channel payload refresh without modifying that channel's upstream skill definition—for example, an explicit local recipe or post-refresh entry point that validation executes before tests.