From d44ef47197081a4dd04f5929673d20b57fb54bf3 Mon Sep 17 00:00:00 2001 From: Alex Putici <222088397+alexputici@users.noreply.github.com> Date: Thu, 14 May 2026 23:21:49 -0600 Subject: [PATCH] fix: gstack-brain-sync advances the artifacts worktree after push MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without this, the worktree at ~/.gstack-brain-worktree stays at the init commit forever while the parent repo's main branch accumulates artifact commits. gbrain indexes from the worktree path, so users see `gbrain sources list` reporting 0 pages and recent CEO plans / test plans / reviews never become searchable locally. Reproduction: enable artifacts sync, run a few /autoplan or /ship sessions to produce committed artifacts, then `gbrain query` for a filename you just created — no results. Inspection shows `git -C ~/.gstack worktree list` has the worktree pinned at the init SHA while `git -C ~/.gstack rev-parse main` has moved ahead. The fix adds an advance_brain_worktree_to_head helper and calls it on both successful push paths in subcmd_once (normal + rebased retry). It's a no-op when the worktree doesn't exist or the parent HEAD can't be read, so installs that haven't run gstack-gbrain-source-wireup yet are unaffected. Regression test runs --once against a real detached worktree and asserts the worktree HEAD matches the parent HEAD afterwards, plus that the synced file actually appears at the worktree path. Verified the test fails without the fix (worktree stuck at init SHA). Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/gstack-brain-sync | 17 ++++++++++++++++ test/brain-sync.test.ts | 45 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/bin/gstack-brain-sync b/bin/gstack-brain-sync index 939aa0d654..a5e594eaa4 100755 --- a/bin/gstack-brain-sync +++ b/bin/gstack-brain-sync @@ -43,6 +43,21 @@ remote_auth_hint() { esac } +# Advance the artifacts worktree (the path gbrain indexes) to the parent +# repo's current HEAD. Best-effort: failures are non-fatal because the push +# has already succeeded, so the local index can catch up on the next run. +# Without this, the worktree stays at its init commit and gbrain reports +# 0 pages while artifacts pile up in main. +advance_brain_worktree_to_head() { + local worktree="${GSTACK_BRAIN_WORKTREE:-$HOME/.gstack-brain-worktree}" + if [ ! -d "$worktree/.git" ] && [ ! -f "$worktree/.git" ]; then + return 0 + fi + local sha + sha=$(git -C "$GSTACK_HOME" rev-parse HEAD 2>/dev/null) || return 0 + ( cd "$worktree" && git checkout --detach "$sha" >/dev/null 2>&1 ) || true +} + write_status() { # args: status_code message [extra_json_blob] local code="$1" @@ -306,6 +321,7 @@ subcmd_once() { : > "$QUEUE" date -u +%Y-%m-%dT%H:%M:%SZ > "$LAST_PUSH_FILE" write_status "ok" "pushed $n file(s) after rebase" + advance_brain_worktree_to_head exit 0 fi fi @@ -319,6 +335,7 @@ subcmd_once() { : > "$QUEUE" date -u +%Y-%m-%dT%H:%M:%SZ > "$LAST_PUSH_FILE" write_status "ok" "pushed $n file(s)" + advance_brain_worktree_to_head exit 0 } diff --git a/test/brain-sync.test.ts b/test/brain-sync.test.ts index 2e7c121d2f..20dc112ca7 100644 --- a/test/brain-sync.test.ts +++ b/test/brain-sync.test.ts @@ -255,6 +255,51 @@ describe('init + sync + restore round-trip', () => { expect(log.stdout).toMatch(/sync: 1 file/); }); + test('--once advances the brain worktree HEAD to match parent after push', () => { + // The brain worktree is what gbrain indexes from. If --once commits + + // pushes but leaves the detached worktree behind, gbrain reports + // 0 pages and recent artifacts never become searchable. + run(['gstack-artifacts-init', '--remote', bareRemote]); + run(['gstack-config', 'set', 'artifacts_sync_mode', 'full']); + + // Set up a detached worktree at the init commit, mirroring what + // gstack-gbrain-source-wireup does in real installs. + const worktree = fs.mkdtempSync(path.join(os.tmpdir(), 'brain-worktree-')); + fs.rmSync(worktree, { recursive: true, force: true }); + const initSha = git(['rev-parse', 'HEAD'], tmpHome).stdout.trim(); + const addRes = spawnSync( + 'git', + ['-C', tmpHome, 'worktree', 'add', '--detach', worktree, initSha], + { encoding: 'utf-8' }, + ); + expect(addRes.status).toBe(0); + + // Enqueue + sync. brain-sync should commit, push, AND advance the + // worktree's detached HEAD to the new commit. + fs.mkdirSync(path.join(tmpHome, 'projects', 'p'), { recursive: true }); + fs.writeFileSync( + path.join(tmpHome, 'projects/p/learnings.jsonl'), + '{"skill":"x","insight":"y","ts":"2026-04-22T10:00:00Z"}\n', + ); + run(['gstack-brain-enqueue', 'projects/p/learnings.jsonl']); + const r = run(['gstack-brain-sync', '--once'], { + env: { GSTACK_BRAIN_WORKTREE: worktree }, + }); + expect(r.status).toBe(0); + + const parentSha = git(['rev-parse', 'HEAD'], tmpHome).stdout.trim(); + const worktreeSha = git(['rev-parse', 'HEAD'], worktree).stdout.trim(); + expect(parentSha).not.toBe(initSha); // sanity: commit actually happened + expect(worktreeSha).toBe(parentSha); // the actual regression check + expect( + fs.existsSync(path.join(worktree, 'projects/p/learnings.jsonl')), + ).toBe(true); + + // Cleanup. + spawnSync('git', ['-C', tmpHome, 'worktree', 'remove', '--force', worktree]); + fs.rmSync(worktree, { recursive: true, force: true }); + }); + test('restore round-trip: writes on machine A visible on machine B', () => { // Machine A. run(['gstack-artifacts-init', '--remote', bareRemote]);