{"id":"migrate-from-v1","name":"migrate-from-v1","summary":"NanoClaw v1のインストールをv2に移行し終えてください。「バッシュ migrate-v2.sh」が完了したら走りましょう。","body":"# Finish v1 → v2 migration\n\n`bash migrate-v2.sh` already ran the deterministic migration. It handled:\n\n- .env keys merged\n- v2 DB seeded (agent_groups, messaging_groups, wiring)\n- Group folders copied (v1 CLAUDE.md → v2 CLAUDE.local.md)\n- Session data copied with conversation continuity (incl. Claude Code memory + JSONL transcripts)\n- Scheduled tasks ported\n- Channel code installed and auth state copied (incl. WhatsApp Baileys keystore)\n- WhatsApp LIDs resolved from `store/auth` and aliased into `messaging_groups`\n- Container skills copied\n- Container image built\n\nYour job is the parts that need human judgment: triage failed steps, seed the owner, run the shared-memory migration, reconcile configs, and port fork customizations.\n\nRead `logs/setup-migration/handoff.json` first — it has `overall_status`, per-step results in `steps`, and a `followups` list.\n\n## Preflight: was the script run?\n\nBefore anything else, check that `logs/setup-migration/handoff.json` exists. If it doesn't, the user is invoking this skill before `migrate-v2.sh` ran. Stop and tell them, verbatim:\n\n> This skill finishes a migration that `migrate-v2.sh` started. Run that first, in your terminal — not from inside Claude:\n>\n> ```bash\n> bash migrate-v2.sh\n> ```\n>\n> It needs interactive prompts (channel selection, service switchover) and runs Node/pnpm bootstrap, Docker, OneCLI setup, and a container build that don't fit inside a Claude session. When it finishes, it'll hand control back to Claude automatically — at which point this skill picks up.\n\nDo not attempt to run the script yourself, simulate its effects, or pick up the migration mid-stream. The deterministic side has dependencies on a real interactive shell.\n\nOnce `handoff.json` exists, proceed to Phase 0.\n\n## Phase 0: Get v2 routing real messages\n\nBefore any deeper migration work, prove v2 actually answers messages on the user's real channels. v1 is paused, not touched — flipping back is a service restart.\n\n### 0a — Fix blockers only\n\nWalk `handoff.steps`. Fix only the failures that would stop the bot from routing one message; defer the rest to its later phase.\n\n### 0b — Smoke test, then continue\n\nTell the user the switch is non-destructive (v1 is paused, not modified; reverting is one command). Help them stop v1's service unit and start v2's, tail the host log for a clean boot, and have them send a real test message. Use `AskUserQuestion` to confirm the bot responded.\n\nIf yes, continue to Phase 1. If no, diagnose from `logs/nanoclaw.log` and re-test — don't proceed to deeper work on a broken router.\n\n### Deferred failures\n\nRe-visit anything you skipped in 0a before declaring the migration done. Most surface naturally in later phases (`1c-groups` ↔ Phase 2, `1e-tasks` ↔ task verification).\n\n## Phase 1: Owner and access\n\nv2 auto-creates a `users` row for every sender it sees (via `extractAndUpsertUser` in `src/modules/permissions/index.ts`). By the time this skill runs, the owner's row likely already exists — it just needs the `owner` role granted.\n\n**User ID format**: always `<channel_type>:<platform_handle>`. Each channel populates this differently:\n- **Telegram**: `telegram:<numeric_user_id>` (e.g. `telegram:6037840640`)\n- **Discord**: `discord:<snowflake_user_id>` (e.g. `discord:123456789012345678`)\n- **WhatsApp**: `whatsapp:<phone>@s.whatsapp.net` (e.g. `whatsapp:14155551234@s.whatsapp.net`)\n- **Slack**: `slack:<user_id>` (e.g. `slack:U04ABCDEF`)\n- **Others**: `<channel_type>:<platform_id>`\n\n**Steps:**\n\n1. Query `users` table: `SELECT id, kind, display_name FROM users`.\n2. If exactly one user exists, confirm: `AskUserQuestion`: \"Is `<display_name>` (`<id>`) you?\" — Yes / No, let me type it.\n3. If multiple users exist, present them as options in `AskUserQuestion`.\n4. If no users exist yet (service hasn't received a message), ask the user to send a test message first, then re-query.\n5. Once confirmed, check `user_roles` via `getUserRoles(userId)`. If an `owner` row already exists, skip. Otherwise grant it with `grantRole`. `grantRole` inserts a new row per call, so the `getUserRoles` check keeps this re-runnable.\n\nUse the DB helpers in `src/modules/permissions/db/user-roles.ts` (`getUserRoles`, `grantRole`). Init the DB first, then call the helpers:\n\n```ts\nimport { closeDb, initDb } from '../src/db/connection.js';\nimport { runMigrations } from '../src/db/migrations/index.js';\nimport { CENTRAL_DB_PATH } from '../src/config.js';\nimport { getUserRoles, grantRole } from '../src/modules/permissions/db/user-roles.js';\n\nconst db = await initDb(CENTRAL_DB_PATH);\ntry {\n  await runMigrations(db); // idempotent\n\n  const userId = '<user_id>';\n  if (!(await getUserRoles(userId)).some((r) => r.role === 'owner')) {\n    await grantRole({\n      user_id: userId,\n      role: 'owner',\n      agent_group_id: null, // owner role must be global\n      granted_by: null,\n      granted_at: new Date().toISOString(),\n    });\n  }\n} finally {\n  await closeDb();\n}\n```\n\n### Access policy\n\nAfter seeding the owner, discuss the access policy. v2's `messaging_groups.unknown_sender_policy` controls who can interact with the bot. `migrate-v2.sh` set it to `public` so the bot would respond during the switchover test, but the user may want to tighten it.\n\nPresent the options via `AskUserQuestion`:\n\n1. **Public** (`public`, current) — anyone can message the bot. Good for personal DM bots.\n2. **Known users only** (`strict`) — only users the access gate accepts (owner, admin, or `agent_group_members`) can trigger the bot. Others are silently dropped.\n3. **Approval required** (`request_approval`) — unknown senders trigger an approval request to the owner. Good for group chats where you want to vet new members.\n\nThe `unknown_sender_policy` column accepts exactly these three values; use the parenthesized value for `<chosen_policy>` below.\n\nIf the user picks option 2 or 3, seed the known users from v1's message history. The v1 database is at `<handoff.v1_path>/store/messages.db`. It has a `messages` table with `sender` and `sender_name` columns. For each group:\n\n```sql\n-- v1: unique senders per chat (excluding bot messages)\nSELECT DISTINCT sender, sender_name\nFROM messages\nWHERE chat_jid = '<v1_jid>' AND is_from_me = 0 AND sender IS NOT NULL\n```\n\nThe `sender` value is a platform handle (e.g. `6037840640` for Telegram). Build the v2 user ID by inferring the channel type from the chat JID prefix (use `parseJid` from `setup/migrate-v2/shared.ts`) and combining: `<channel_type>:<sender>`.\n\nFor each sender:\n1. Upsert into `users(id, kind, display_name)` if not already present.\n2. Insert into `agent_group_members(user_id, agent_group_id)` for each agent group wired to that messaging group.\n\nShow the user the list of senders being imported and let them deselect any they don't want.\n\nThen update the messaging groups:\n```sql\nUPDATE messaging_groups SET unknown_sender_policy = '<chosen_policy>'\nWHERE id IN (SELECT id FROM messaging_groups WHERE channel_type IN (<migrated_channels>))\n```\n\n## Phase 2: Migrate legacy memory\n\nRun `/migrate-memory` for the imported groups. It quiesces each group, moves the\nv1 `CLAUDE.local.md` into the shared `memory/` tree without reading it during\nstaging, then has the invoking coding harness distill standing identity into\n`instructions.prepend.md` and durable facts into Core Memory or focused linked\nfiles before the NanoClaw group runs again.\n\nDo not duplicate that migration logic here. Record each group's result in the\nhandoff before continuing.\n\n## Phase 3: Container config\n\n`migrate-v2.sh` writes `container.json` directly from v1's `container_config` (the `additionalMounts` shape is identical). If the v1 config was unparseable, it falls back to a `.v1-container-config.json` sidecar.\n\nFor each group, check:\n\n1. If `container.json` exists, read it and verify the `additionalMounts` host paths are still valid on this machine. Flag any that don't exist.\n2. If `.v1-container-config.json` exists (parse failure fallback), read it, discuss with the user, and write a proper `container.json`. Then delete the sidecar.\n3. Check for `env` or `packages` fields — `env` may overlap with OneCLI vault, `packages` (apt/npm) are portable.\n\n## Phase 4: Fork customizations\n\nCheck whether the user's v1 install was a customized fork.\n\n```bash\ncd <v1_path>\ngit remote -v\ngit log --oneline <upstream>/main..HEAD 2>/dev/null\n```\n\nIf no commits ahead of upstream: stock v1, skip this phase.\n\nIf there are commits:\n\n1. Show the commit list to the user.\n2. `AskUserQuestion`: \"How do you want to handle your v1 customizations?\"\n   - **Copy portable items** (recommended) — copy `container/skills/*`, `.claude/skills/*`, `docs/*`. Grep each copied file for v1-only references that won't resolve in v2 and flag them to the user: workspace paths (`/workspace/group/`, `/workspace/project/`, `/workspace/ipc/`, `/workspace/extra/`), the v1 IPC mechanism, `registered_groups` / `is_main`, the v1 sender allowlist, and `store/messages.db`.\n   - **Full walkthrough** — go commit by commit, decide together.\n   - **Reference only** — stash to `docs/v1-fork-reference/` for later.\n3. Source code (`src/*`, `container/agent-runner/src/*`) is NOT portable — v2's architecture is fundamentally different. Stash to `docs/v1-fork-reference/` with a README explaining what each file did. Don't translate.\n\n## Principles\n\n- **v1 checkout is read-only.** Never modify files under `handoff.v1_path`.\n- **Show before writing.** Show diffs or proposed content before modifying standing instructions, memory, or container.json.\n- **Mask credentials** when displaying (first 4 + `...` + last 4 characters).\n- **`handoff.json` is the recovery point.** If context gets compacted, re-read it and `git status` to recover state.\n\n## Setup steps you can run\n\nThe setup flow at `setup/index.ts` has individual steps you can invoke if something is missing or failed:\n\n```bash\npnpm exec tsx setup/index.ts --step <name>\n```\n\n| Step | When to use |\n|------|-------------|\n| `onecli` | OneCLI not installed or not healthy |\n| `auth` | No Anthropic credential in vault |\n| `container` | Container image needs rebuild |\n| `service` | Service not installed or not running |\n| `mounts` | Mount allowlist missing |\n| `verify` | End-to-end health check (run after everything else) |\n| `environment` | System check (Node, dirs) |\n\n## When done\n\n1. Run the verify step to confirm everything works:\n   ```bash\n   pnpm exec tsx setup/index.ts --step verify\n   ```\n2. Delete `logs/setup-migration/handoff.json` — offer to save as `docs/migration-<date>.md` first.\n3. Restart the service if running so changes take effect. The v2 service label is install-specific (`nanoclaw-v2-<slug>` / `com.nanoclaw-v2-<slug>`), so derive it from `src/install-slug.ts` rather than guessing:\n   ```bash\n   # Linux\n   UNIT=$(pnpm exec tsx -e \"import{getSystemdUnit}from'./src/install-slug.js';console.log(getSystemdUnit())\")\n   systemctl --user restart \"$UNIT\"\n   # macOS\n   LABEL=$(pnpm exec tsx -e \"import{getLaunchdLabel}from'./src/install-slug.js';console.log(getLaunchdLabel())\")\n   launchctl kickstart -k \"gui/$(id -u)/$LABEL\"\n   ```","author":"@nanocoai","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/migrate-from-v1","license":"MIT","category":"devops","lang":"en","tokens":2837,"stars":0,"calls30d":1,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":[]}}