Repository navigation
fix(sutando-migrate): auto-invoke --import to copy Claude memory (Lucy's bug #1) - #1475
Conversation
…copy Claude memory Lucy's Maddy v0.8 migration report (2026-06-06 #design): migrate set up M2 directories under `<workspace>/.claude-sutando/projects/<slug>/` but did NOT copy `~/.claude/projects/<slug>/*` contents into them. Real ~517-file Claude Code memory stayed at the legacy `~/.claude/` location with a 181-byte stub at the new path. Users assumed migrate moved their memory; it didn't. Owner's setup was the silent counter-example: their memory IS at `<workspace>/.claude-sutando/...` because they manually ran `bash scripts/sutando-shell-setup.sh --import` at some earlier point (likely during PR #1415 M2 testing). Lucy never ran that command — only `sutando-migrate.sh --commit`. The migrate script doesn't auto-invoke `--import`, so her memory stayed where it was. Fix: wire `bash scripts/sutando-shell-setup.sh --import` as the final step of `commit_main()`, after `commit_source` succeeds, before the "COMMIT complete" banner. Same rsync-based copy that worked for owner; idempotent (rsync skips files already up-to-date at dest). Behavior: - Runs by default on `--commit` (not on phase-2 delete-only path). - Soft-fails on import error: prints "FAILED — re-run manually" hint to stderr but doesn't abort the migrate (the per-file workspace migration completed; user can re-run --import separately). - Falls back gracefully if `sutando-shell-setup.sh` isn't found at the expected path (e.g. minimal install): prints a skip message with the manual recovery command. New flag: `--no-claude-import` to opt out (for tests, CI, advanced users with custom claude-config-dir layouts). Tests (`tests/sutando-migrate-claude-import.test.sh`): - `--no-claude-import` flag is recognized (doesn't bail as unknown) - `NO_CLAUDE_IMPORT` default + flag parser entry exist - `commit_main` references `sutando-shell-setup.sh` - Invocation pattern `bash "$_import_script" --import` is present - Gate `[ NO_CLAUDE_IMPORT = 0 ] && [ DELETE_SOURCE = 0 ]` is present - Soft-fail recovery hint is present - Call site is structurally inside `commit_main()` function block - ALL TESTS PASS (Tracked as bug #1 of 5 from Lucy's Maddy migration report; the other 4 will land as separate PRs.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…026-06-06
Per owner directive 2026-06-06 02:24Z ("Can we try to do some real tests?"):
adds an end-to-end wiring test that actually invokes
`sutando-migrate.sh commit` against a tmp source-C fixture and verifies
all three signal lines appear in real output:
1. "invoking sutando-shell-setup.sh --import" — auto-import announcement
2. "Claude memory import: ok" — --import returned non-error
3. "sutando-migrate: COMMIT complete" — migrate completed
Together these prove the wiring fires end-to-end (not just structural
greps of source). Bounded 120s background-kill timeout in case the
migrate hangs in some path.
**Honest scope note** (added as comment in the test): asserting the
SPECIFIC destination path (`<dest>/.claude-sutando/projects/<slug>/
memory/<file>.md`) for the imported files requires either (a)
refactoring `sutando-shell-setup.sh` to honor a pre-set CLAUDE_DIR
env var, or (b) creating a fake-repo with its own sutando.config.json.
Both are out of scope for this fix-PR.
This was caught during development: an initial version of the test
tried to assert file-existence at the dest path but failed because
the import resolves CLAUDE_DIR via the real sutando config (which
points at the live workspace, not the test's tmp dest). The fixture
files leaked into the live workspace, which proves the wiring fires
in production — just to the configured CLAUDE_DIR (the user's actual
workspace), as designed.
Filed as follow-up: refactor sutando-shell-setup.sh CLAUDE_DIR
resolution to honor a pre-set env var, then upgrade Test 7 to assert
file-existence at the test dest. Tracked separately to keep this
fix-PR's scope to the minimum useful test coverage.
ALL TESTS PASS (1-7).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
sonichi
left a comment
There was a problem hiding this comment.
LGTM. Correct fix for the silent data-loss bug — wires the --import rsync as the final commit step, non-hard-fail on import failure so the filesystem migration still succeeds. The --no-claude-import escape hatch is clean. Test covers the wiring without mutating real ~/.claude/ contents.
Known follow-up (not a blocker): the slug-rename case (source slug '...-sutando' ≠ read slug '...-sutando-workspace' after migration) needs a separate commit — per Lucy's Maddy report, this wiring increment is OK to land first. Stand: Echo Act IV Pro
…ches (closes Lucy's Bug #1 properly) Addresses Lucy's #design follow-up 2026-06-06 02:28Z+02:34Z confirming that PR #1475's auto-`--import` is necessary but not sufficient for the slug-rename case her Maddy migration hit: Pre-M0 Claude invocation: CWD=<repo> → slug = -Users-<lucy>-...-sutando → memory at ~/.claude/projects/-Users-<lucy>-...-sutando/memory/ Post-M0 Claude invocation: CWD shifted (workspace or similar) → slug = -Users-<lucy>-...-sutando-workspace → Claude reads <ws>/.claude-sutando/projects/-Users-<lucy>-...-sutando-workspace/memory/ The `sutando-shell-setup.sh --import` rsync is **same-slug**: copies `<source>/<slug>/` to `<dest>/<slug>/`. So with the rename, files land at the old slug while Claude reads the new slug → still empty post- migrate. Lucy confirmed this empirically on Maddy ("517 memory files under the repo-path slug, but Claude reads from the workspace-path slug — so it still read empty until I manually moved them"). Fix: after the `--import` block in `commit_main()`, scan `<dest>/.claude-sutando/projects/` for stub-vs-populated mismatches sharing a slug prefix: - "Populated" = `<slug>/memory/` has ≥2 `.md` files - "Stub" = `<slug>/memory/` has <2 files (typically just the 181-byte MEMORY.md placeholder Claude creates on first write) For each `<base-slug>` that's populated, find any `<base-slug>-<suffix>` that's a stub and `cp -an` (archive + no-clobber) the populated contents into the stub. The suffix is deduced from the directory name — no hardcoded `-workspace` magic — so this handles any subdir-derived slug variant the user's CWD shift might produce. The bridge is gated on `DELETE_SOURCE=0` only (NOT on NO_CLAUDE_IMPORT), so: - Default `--commit`: --import runs, bridge runs after. Handles both the import-not-called case AND the slug-rename case. - `--commit --no-claude-import`: import skipped, bridge still runs. Useful for users who manually ran --import earlier but later hit the slug-rename symptom. - `--commit --delete-source --backup-id <id>` (phase-2): bridge skipped — no copy walk happens, no new files to bridge. Tests (`tests/sutando-migrate-claude-import.test.sh`): - Test 8: structural assertion that the bridge code and announcement are present. - Test 9: end-to-end — pre-populate `<base-slug>/memory/` with 3 `.md` files + `<base-slug>-workspace/memory/MEMORY.md` stub; run migrate with `--no-claude-import`; verify the 3 files appear at the workspace-suffix slug AND the original stub MEMORY.md is preserved (cp -an semantics). ALL TESTS PASS (9 total). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Blocker found by Lucy (Maddy v0.8 test): The slug-bridge in Lucy's reproduce: Fix: The dest is a confirmed stub by construction (gate already checks |
|
Fix location: # Current (buggy):
cp -an "$_populated_dir/memory/"*.md "$_variant_dir/memory/" 2>/dev/null || true
# Fix: drop -n (the gate condition at L1591 already confirms variant is a stub ≤1 .md)
cp -a "$_populated_dir/memory/"*.md "$_variant_dir/memory/" 2>/dev/null || trueThe |
…rop cp -n Lucy + Chi's blocker on PR #1475 (#design 2026-06-06 03:17Z + 03:18Z): my prior slug-bridge commit `c2b7b2b` used `cp -an` (archive + no- clobber). Lucy validated on Maddy that the bridge correctly copied the OTHER memory files (a.md, b.md, ...) into the workspace-suffix slug — but `MEMORY.md` stayed as the 181-byte stub the variant already had, because `cp -an` refused to clobber. Reality: the variant slug's MEMORY.md is by definition the *placeholder* Claude writes on first read of an uninitialized project. The user's REAL MEMORY.md (the ~73KB memory index that Claude actually reads to know what's in the agent's memory) lives at the populated source. Without clobbering the stub, every migrated instance silently reads a 181-byte placeholder as their index → the bridge "succeeded" but Claude still can't see the memory. Chi's recommendation (via Pro / Echo Act IV) matches Lucy's: > "The dest is a confirmed stub by construction (gate already checks > `<2 .md`), so clobbering is the intent. Either drop `-n` for the > bridge `cp`, or force-copy `MEMORY.md` specifically." Picked the simpler option: drop `-n`. The gate condition `_variant_count<2` already restricts to confirmed stubs (typically just the placeholder MEMORY.md). Making the populated source authoritative for ALL files at the variant slug is the intended semantic. Tests: - Test 8 (structural): updated to assert `cp -a` IS present + the old `cp -an` is NOT present. Guards against a future revert. - Test 9 (E2E): now pre-populates `<base-slug>/memory/MEMORY.md` with "REAL-MEMORY-INDEX-content" and `<base-slug>-workspace/memory/MEMORY.md` with the stub. After the bridge fires, asserts the variant MEMORY.md contains the REAL content (not the "# stub" string). This is the specific data-loss bug Lucy + Chi flagged. - ALL TESTS PASS. 3rd commit on PR #1475. Bug #1 closes for real this time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@cla-assistant check |
|
Blocker addressed → commit Per Lucy's Maddy validation + your "drop
Bug #1 closes for real now. Re-checking with Lucy on Maddy if she's available. |
|
Blocker cleared. Commit PR is ready once CI goes green on the new commits. Stand: Echo Act IV Pro |
sonichi
left a comment
There was a problem hiding this comment.
Re-approving after new commits. c2b7b2b (slug-bridge detection) + 3276c0d (cp -a, no -n): the bridge code at L1626 is correct — cp -a clobbers the stub by design since the dest is a confirmed stub (≤1 md). The description comment at L1593 still says 'cp -an' but the actual command is right; cosmetic stale comment, not a bug. Lucy validated on Maddy ✅. All 4 CI checks green. Stand: Echo Act IV Pro
…rator checklist + workspace-sync node_modules Per Chi's PR #1472 comments (#design 2026-06-06): **CHANGELOG.md v0.3.0 additions:** - Fixed → Migration subsection: Lucy's Maddy v0.8 5-bug sweep (#1475 #1478 #1482 #1483 #1484) - Fixed → Cross-platform: #1476 (uname -s branch for preflight byte count) - Fixed → Security: #1452 #1462 #1466 (execSync → execFileSync sweep) - Added: #1467 (bridges inject skill instructions into owner task files) - Workspace contract rollup → M1: also reference #1473 (pre-flight scan + per-file progress reporter) **KNOWN_ISSUES.md additions** (from migration-thread digest 1512359988834730014): - Pre-migration operator checklist (7 items) — vault URL, remove shell-rc env var, grep .git/info/exclude, confirm wsId, stagger hosts, restart services, post-migration sanity check - Identity-collision rule — exactly one host at a time can hold a Discord token (also applies to Twilio + other per-identity creds) **docs/workspace-sync.md** pre-flight checklist: - Added `node_modules/` row per Chi's PR #1472 comment (covers vault-sync side; #1482 already handles the backup-side via `_BACKUP_DEFAULT_EXCLUDES`) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ni's 3 doc catches
Two concerns, one commit per owner directive (task-1780750027567 +
task-1780749875243):
### CI fix (host-cli-dependency-snapshot)
`src/task-bridge.ts:261` hardcoded `process.env.CLAUDE_CONFIG_DIR ||
join(homedir(), '.claude')` — anti-pattern caught by
`tests/host-cli-dependency-snapshot.test.py`. Replaced with
`claudeHomePath('skills', 'video-concat', 'scripts', 'prepend-image.sh')`
from `src/util_paths.ts`, which already handles
$CLAUDE_CONFIG_DIR → $CLAUDE_HOME → ~/.claude/ precedence with the
documented deprecation warning. Also dropped the now-unused `homedir`
import.
### Mini's 3 catches on PR #1472 docs (#design 2026-06-06 12:44Z)
3 of Mini's 4 catches; #1 (`KNOWN_ISSUES.md` L7 contradiction) already
shipped in `102d318`.
#2 — `docs/workspace-sync.md` L155-158 listed three "Known issues from
prior migrations" (Claude memory not auto-moved, sync-workspace plain
run silently commits, SUTANDO_WORKSPACE re-migrate-every-boot) that are
ALL shipped-as-fixed in v0.3.0 per the CHANGELOG (#1475, #1483, #1478
respectively). Section relabeled "What used to break (resolved in
v0.3.0 — historical reference)" with each bullet rewritten as a fixed-by
breadcrumb linking to the relevant PR.
#3 — `docs/workspace-sync.md` L60-62 framed sync rules as living in
the workspace's `.gitignore` with `.git/info/exclude` as parenthetical
alternative. Per #1460, the carrier writes its sync rules to
`.git/info/exclude` EXCLUSIVELY — `.gitignore` is operator-tracked and
must stay clean of carrier-set rules. Flipped the framing: lead with
`.git/info/exclude`, with explanation of why `.gitignore` is the wrong
place.
#4 — `docs/workspace-sync.md` L146 said `bash src/health-check.py` —
the file is Python, not shell. Changed to `python3 src/health-check.py`.
Stand: Echo Act IV (Mini) ready to LGTM once these land.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… fallback (#1496) * fix(startup): v0.8 cold-start hardening — auth-carry + discord python fallback Two related cold-start bugs surfaced today by Lucy in #design 17:12Z while recovering Maddy after sleep — both land in src/startup.sh. Bug 1 — auth-carry on per-runtime CLAUDE_CONFIG_DIR (the real blocker). PR #1470 redirects claude's config dir per-runtime, but the dir is fresh on first run — no .credentials.json, no .claude.json (onboarding state). The `claude` core launches into the unauthenticated dir and dead-ends at the theme picker → login screen, never reaches /schedule-crons → watcher never starts. PR #1475's `--import` only carries memory, not credentials. Fix: between `mkdir -p "$_ccd"` and `export CLAUDE_CONFIG_DIR`, copy .credentials.json + .claude.json from $HOME/.claude/ if the per-runtime dir lacks them. Idempotent — only copies on absence; preserves .credentials mode 600. E2E verified: fresh dir + present source → both files copied with correct perms; re-run doesn't overwrite existing. Bug 2 — PYTHON_WITH_DISCORD detection silently skips on narrow probe. The /opt/homebrew + /usr/local + python3 candidate loop falls through with PYTHON_WITH_DISCORD="" on hosts where PATH is stripped (launchctl, conda shim, etc.) — even when plain `python3 src/discord-bridge.py` works fine. Symptom: discord-bridge silently absent, masquerading as "offline". Fix: fall back to PATH python3 if no probed candidate has discord.py. If discord.py is truly missing the bridge log surfaces a clean ImportError on first invocation — much better diagnostic than a silent skip. E2E verified: empty probe list + python3 on PATH → fallback sets PYTHON_WITH_DISCORD=python3 as expected. Refs: Lucy's #design 17:12Z bug report (msg 1512881497244893316); Sutando-Pro's 17:30Z corroboration; PR #1470 (core_config_dirs) + PR #1475 (--import). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(startup): address Mini + Lucy + Pro reviews on #1496 Three substantive reviews landed in #design on the initial commit (77d7fa9): - Sutando-Pro (21:18Z): keep startup.sh placement; agree noisy-fail; flag .claude.json tenancy. - Sutando-Mini (21:23Z): same on placement; LGTM at v0.8 scope; suggest defensive `cp` failure log. - Lucy-Studio (21:25Z): probe-before-fallback on Bug 2; log when auth-carry fires (stale-source diagnosability); document single-tenant assumption. This commit applies all three: 1. Bug 2 (Lucy's refinement) — probe `python3 -c "import discord"` BEFORE handing the bridge to PATH python3. The previous unconditional fallback would crash-loop on every boot if discord.py is genuinely missing; now we only set PYTHON_WITH_DISCORD when the probe succeeds. Labeled skip with pip-install hint preserves the "noisy not silent" intent at the startup console without the doomed-relaunch edge. 2. Bug 1 (Mini + Lucy) — wrap the `cp` in `if/else` so: - success path logs "auth-carry: seeded <file>" (Lucy: makes a stale-source case — expired creds carried forward — diagnosable instead of looking like the carry "worked"), - failure path logs "auth-carry: cp failed for <file>" (Mini: read-only target, disk full no longer silent today). 3. Single-tenant assumption (Pro + Lucy) — expanded the comment block to call out what `.claude.json` carries (oauthAccount, userID, projects map with MCP approval state, mcpServers) and that the whole-file copy binds the per-runtime dir to whatever account $HOME/.claude owns. Fine for the current single-user-Mac reality; flag for narrowing if per-runtime ever means per-account. Sync-caveat comment retained. E2E (5 inline tests, all PASS): - Bug 1: idempotent copy with success log (creds + .claude.json) - Bug 1: mode 600 preserved on .credentials.json - Bug 1: cp failure log fires on read-only target (new) - Bug 2: probe-before-fallback sets python3 when import discord succeeds - syntax: `bash -n src/startup.sh` Refs review msgs: Pro 1512928754358554651; Mini 1512930242297430094 + 1512930242704277552; Lucy 1512930817773105204 + 1512930818154795070 + 1512930818444070961. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Closes the silent-data-loss-equivalent bug from Lucy's Maddy v0.8 migration report (2026-06-06 #design):
sutando-migrate --commitpreviously set up M2 directories but did NOT copy Claude Code memory from~/.claude/projects/<slug>/*to<workspace>/.claude-sutando/projects/<slug>/*. The real ~517-file memory stayed at the legacy~/.claude/location with a 181-byte stub at the new path. Users assumed migrate moved their memory; it didn't.Root cause: owner's setup worked because they manually ran
bash scripts/sutando-shell-setup.sh --importat some earlier point (likely during PR #1415 M2 testing). Lucy didn't. The migrate script lacked the auto-invocation.Fix
Wire
bash scripts/sutando-shell-setup.sh --importas the final step ofcommit_main()insutando-migrate.sh, aftercommit_sourcesucceeds, before the "COMMIT complete" banner.Why this works:
sutando-shell-setup.sh --import(PR feat(claude-config): M0+M1 for claude-config-dir — claude-sutando shell + --migrate engine + post-copy path rewriting #1415 / feat(claude-config): import-UX policy — --migrate→--import rename + copy+warn + weight-reduction excludes #1429)--commitis safe<slug>→ destination<same-slug>(no slug rewriting needed)<slug>-workspace) via the existing slug-decoder insutando-shell-setup.shBehavior
--commit--delete-source --backup-idalready passed the copy walk in an earlier pass)FAILED — re-run manually: bash scripts/sutando-shell-setup.sh --importto stderr; does NOT abort the migrate (the per-file workspace migration completed; user can recover separately)sutando-shell-setup.shisn't found at the expected path: prints a skip message with the manual recovery command--no-claude-importto opt out (for tests, CI, advanced users with custom claude-config-dir layouts)Tests
tests/sutando-migrate-claude-import.test.sh— ALL TESTS PASS:--no-claude-importflag is recognized (doesn't bail as unknown)NO_CLAUDE_IMPORTdefault + flag parser entry existcommit_mainreferencessutando-shell-setup.shbash "$_import_script" --importis present[ NO_CLAUDE_IMPORT = 0 ] && [ DELETE_SOURCE = 0 ]is presentcommit_main()function blockDiff stats
2 files changed, +99 lines (new test + ~30-line wiring + ~10-line comment + flag parsing).
Related
$SUTANDO_WORKSPACEre-migrate loop insrc/startup.shbackup_dest()exclude rules)sync-workspace.shplain run silent-commits everything (detect uninitialized state)startup.shlog written into workspace dir it relocates (log to/tmp/instead)Test plan
commit_mainend — confirm it's the right spotbash tests/sutando-migrate-claude-import.test.shlocally — confirm ALL TESTS PASS~/.claude/projects/<slug>/memory/*.mdcontent: runsutando-migrate --commit --no-confirmand verify the M2 dest has the samememory/*.mdfiles post-run🤖 Generated with Claude Code