Cherry-picked from #1468 by 0xDevNinja and extended with the
hostname-fold migration that codex review surfaced.
Pre-fix `deriveCodeSourceId` hashed the absolute repo path alone, so two
machines with identical home-dir layouts (chezmoi-managed dotfiles,
ansible-provisioned VMs) derived the same id and clobbered each other's
`local_path` in a federated brain. Last-writer-wins, with cryptic "Not a
git repository" errors on the loser.
Hash key is now `\${hostname}::\${path}`. Conductor worktrees on a single
host stay distinct (path entropy unchanged within a host); cross-machine
federations stop colliding.
Migration (D1=B + codex refinements): every existing user has a
pre-#1468 path-only-hash source id in their brain that no longer matches
what `deriveCodeSourceId` produces. Without migration, the next sync
registers a fresh source and orphans the old one. This commit adds:
- \`derivePathOnlyHashLegacyId\` — separate helper for the pre-#1468 form.
Distinct from \`deriveLegacyCodeSourceId\` (pre-pathhash v1.x form);
both probes run.
- \`planHostnameFoldMigration\` — feature-checks \`gbrain sources rename
<old> <new>\` (exact argument shape, not just \`--help\`), gates on
path-drift (skip migration if old source's \`local_path\` differs from
current repo root), and falls back to register-new + sync-OK +
remove-old when rename is unsupported. As of gbrain 0.35.0.0 the
rename subcommand does not exist, so users go through the cleanup
path; the rename path stays dormant until gbrain ships it.
- \`removeOrphanedSource\` — called only AFTER new-source sync verifies
page_count > 0. Closes the data-loss window codex flagged where
"register new, remove old before sync" can wipe pages if sync fails.
- \`sourceLocalPath\` — looks up a source's \`local_path\` from
\`gbrain sources list --json\` for the drift gate.
- Helpers accept an optional \`env\` parameter so tests can inject a
gbrain shim via PATH without process-wide PATH mutation (Bun's
spawnSync doesn't pick up runtime PATH changes). Pre-positions for
commit 4's centralized gbrain-exec helper.
- \`if (import.meta.main)\` guard around \`main()\` so the helpers can be
imported for in-process unit tests.
Tests cover: pure derivation, ids-match degenerate case, no-legacy
short-circuit, path-drift skip path, rename path with shim, cleanup
fallback when rename unsupported, cleanup fallback when rename call
itself fails, source-lookup happy/missing/error paths.
\`GSTACK_HOSTNAME\` env var is a test-only knob; production uses
\`os.hostname()\`.
Fixes#1414
Co-Authored-By: Claude <noreply@anthropic.com>