From 61b9a18aef014e4011272c336605f16435ab391e Mon Sep 17 00:00:00 2001 From: Jorge Vidaurre Date: Sat, 28 Mar 2026 13:29:54 -0300 Subject: [PATCH] fix(obs): resolve squads dir from git root for worktree support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit findSquadsDir() now uses a 3-stage fallback: 1. Standard 5-level ancestor walk from process.cwd() (unchanged happy path) 2. git rev-parse --show-toplevel — handles CWD deep inside a subdirectory 3. git rev-parse --git-common-dir — resolves main repo root and checks sibling directories (e.g. hq/ next to squads-cli/ or worktrees/) No new npm dependencies. spawnSync from child_process (built-in). Closes #652 --- src/lib/squad-parser.ts | 75 +++++++++++++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/src/lib/squad-parser.ts b/src/lib/squad-parser.ts index 2d9112b..efbed52 100644 --- a/src/lib/squad-parser.ts +++ b/src/lib/squad-parser.ts @@ -1,5 +1,6 @@ import { readFileSync, existsSync, readdirSync, writeFileSync } from 'fs'; import { join, basename, dirname } from 'path'; +import { spawnSync } from 'child_process'; import matter from 'gray-matter'; import { resolveMcpConfig, type McpResolution } from './mcp-config.js'; @@ -175,23 +176,77 @@ export interface ExecutionContext extends SquadContext { } /** - * Find the .agents/squads directory by searching current directory and parents. - * Searches up to 5 parent directories. - * @returns Path to squads directory or null if not found + * Run `git rev-parse` with a given flag in a given directory. + * Returns stdout trimmed, or null on error. */ -export function findSquadsDir(): string | null { - // Look for .agents/squads in current directory or parent directories - let dir = process.cwd(); +function gitRevParse(flag: string, cwd: string): string | null { + const result = spawnSync('git', ['rev-parse', flag], { + cwd, + encoding: 'utf-8', + stdio: ['ignore', 'pipe', 'ignore'], + }); + if (result.status !== 0 || !result.stdout) return null; + return result.stdout.trim(); +} - for (let i = 0; i < 5; i++) { +/** + * Walk up from `startDir` (up to `maxLevels` times) looking for .agents/squads. + * Returns the squads path on first hit, or null. + */ +function walkUpForSquadsDir(startDir: string, maxLevels: number): string | null { + let dir = startDir; + for (let i = 0; i < maxLevels; i++) { const squadsPath = join(dir, '.agents', 'squads'); - if (existsSync(squadsPath)) { - return squadsPath; - } + if (existsSync(squadsPath)) return squadsPath; const parent = join(dir, '..'); if (parent === dir) break; dir = parent; } + return null; +} + +/** + * Find the .agents/squads directory by searching current directory and parents. + * + * Search order: + * 1. Walk up to 5 levels from process.cwd() (handles normal project layouts). + * 2. If that fails and we are inside a git worktree or subdirectory, get the + * git toplevel via `git rev-parse --show-toplevel` and walk up from there. + * 3. Also check the parent of the git toplevel so that sibling layouts like + * `agents-squads/hq/` are found when CWD is `agents-squads/.worktrees/xxx/`. + * + * @returns Path to squads directory or null if not found + */ +export function findSquadsDir(): string | null { + const cwd = process.cwd(); + + // 1. Standard ancestor walk from CWD. + const fromCwd = walkUpForSquadsDir(cwd, 5); + if (fromCwd) return fromCwd; + + // 2. Git-aware fallback: get the worktree's toplevel checkout directory. + const gitToplevel = gitRevParse('--show-toplevel', cwd); + if (gitToplevel && gitToplevel !== cwd) { + // Walk up from the git toplevel (handles CWD being deep inside a worktree). + const fromGitRoot = walkUpForSquadsDir(gitToplevel, 5); + if (fromGitRoot) return fromGitRoot; + } + + // 3. For git worktrees the common .git dir lives in the main repo. Use + // --git-common-dir to find the main repo root and look for siblings. + const gitCommonDir = gitRevParse('--git-common-dir', cwd); + if (gitCommonDir) { + // --git-common-dir returns the path to the common .git dir. + // Its parent is the main repo root; walk up from there. + const mainRepoRoot = join(gitCommonDir, '..'); + const fromMainRoot = walkUpForSquadsDir(mainRepoRoot, 5); + if (fromMainRoot) return fromMainRoot; + + // Also check siblings of the main repo root (e.g. hq/ lives next to squads-cli/). + const orgRoot = join(mainRepoRoot, '..'); + const fromOrgRoot = walkUpForSquadsDir(orgRoot, 3); + if (fromOrgRoot) return fromOrgRoot; + } return null; }