.maestro/playbooks/2026-02-25-CM-Issues-PRs/2026-02-25-Branch-Memory/BRANCH-MEMORY-02.md
This phase creates the core utility that powers branch isolation. Given a set of commit SHAs from stored observations, it uses git merge-base --is-ancestor to determine which are ancestors of the current HEAD. This enables the "like how git works" visibility model — observations from merged branches become visible automatically, while sibling branch work stays invisible. Both the context builder (Phase 03) and search system (Phase 04) depend on this utility.
Create git ancestry resolution utility:
src/services/integrations/git-ancestry.tsasync function getCurrentHead(cwd: string): Promise<string | null> — runs git rev-parse HEAD, returns full 40-char SHA or null on failure. Use the same spawn pattern established in src/services/integrations/git-branch.ts from Phase 01async function resolveAncestorCommits(currentHead: string, candidateCommitShas: string[], cwd: string): Promise<string[]>git merge-base --is-ancestor <candidate> <currentHead> — this command exits with code 0 if the candidate IS an ancestor, non-zero if notPromise.all to run checks concurrently for performance (each git merge-base call is fast and independent)candidateCommitShas that are ancestors of currentHeadgit merge-base fails for a specific SHA (e.g., SHA no longer exists after garbage collection), exclude that SHA from results rather than failing the entire batchcandidateCommitShas is empty, return empty array immediately (no git calls needed)Create observation commit SHA query helper:
src/services/sqlite/observations/get.ts, add a new exported function: getUniqueCommitShasForProject(db: Database, project: string): string[]SELECT DISTINCT commit_sha FROM observations WHERE project = ? AND commit_sha IS NOT NULLCreate a combined branch resolution function:
src/services/integrations/git-ancestry.ts, add: async function resolveVisibleCommitShas(candidateCommitShas: string[], cwd: string): Promise<string[] | null>getCurrentHead(cwd) — if null (not a git repo), return null (the null convention means "no filtering, show everything")resolveAncestorCommits(currentHead, candidateCommitShas, cwd) to filternull = unfiltered convention lets callers distinguish "not in a git repo" (show all) from "in a git repo but no ancestors found" (show nothing from branches)Write tests for the ancestry resolution utility:
tests/git-ancestry.test.tsgetCurrentHead: should return a 40-character hex string when run in this repo's directoryresolveAncestorCommits with this repo: the current HEAD's own SHA should be considered an ancestor of itself (git merge-base --is-ancestor returns 0 for same commit). Find an old commit SHA from git log to verify it's an ancestor of HEAD'0000000000000000000000000000000000000000') — should be excluded gracefully, not throwresolveVisibleCommitShas with null-safety: call with a non-git directory like /tmp — should return nullRun tests and fix any failures:
tests/git-ancestry.test.ts