Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions bin/gstack-brain-sync
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
}

Expand Down
45 changes: 45 additions & 0 deletions test/brain-sync.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down