From dba91e06d5a4a59c04dfe8c1430b4ab46e8d3209 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 9 Apr 2026 10:28:17 +0300 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20monorepo=20subfolder=20support=20?= =?UTF-8?q?=E2=80=94=20never=20run=20git=20init=20(#939)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When squad init is run in a subfolder of a monorepo, it previously ran git init to create a nested repo boundary. This broke monorepo workflows by creating git-in-git. Changes: - Never run git init in init command - Detect monorepo (cwd != git root) and split file placement: - .github/agents/squad.agent.md → git root (Copilot needs it there) - .squad/ → cwd subfolder (team state lives here) - Add gentFileRoot option to InitOptions for explicit control - Update team root resolution: check CWD first for .squad/, then fall back to git root (enables multiple squads per monorepo) Closes #939 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- packages/squad-cli/src/cli/core/init.ts | 28 +++++++++++-------- .../templates/squad.agent.md.template | 8 ++++-- packages/squad-sdk/src/config/init.ts | 7 ++++- .../templates/squad.agent.md.template | 8 ++++-- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/packages/squad-cli/src/cli/core/init.ts b/packages/squad-cli/src/cli/core/init.ts index c0619dd21..effb8130b 100644 --- a/packages/squad-cli/src/cli/core/init.ts +++ b/packages/squad-cli/src/cli/core/init.ts @@ -123,10 +123,12 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi // Detect project type const projectType = detectProjectType(dest); - // ── Parent git repo detection ───────────────────────────────────── + // ── Monorepo / subfolder detection ─────────────────────────────── // Copilot resolves .github/agents/ relative to the git root. - // If CWD is inside a parent git repo, the agent file will be - // invisible to copilot because the git root points elsewhere. + // If CWD is a subfolder of a larger repo (monorepo), we place + // squad.agent.md at the git root and .squad/ in the subfolder. + // Never run `git init` — it creates broken nested repos (#939). + let agentFileRoot = dest; // default: place agent file relative to dest try { const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd: dest, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], @@ -135,18 +137,19 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi const normalGitRoot = path.resolve(gitRoot); if (normalDest.toLowerCase() !== normalGitRoot.toLowerCase()) { console.log(); - console.log(`${YELLOW}${BOLD}⚠ Parent git repo detected${RESET}`); - console.log(`${YELLOW} Git root: ${normalGitRoot}${RESET}`); - console.log(`${YELLOW} You're in: ${normalDest}${RESET}`); + console.log(`${CYAN}${BOLD}📦 Monorepo detected${RESET}`); + console.log(`${DIM} Git root: ${normalGitRoot}${RESET}`); + console.log(`${DIM} You're in: ${normalDest}${RESET}`); console.log(); - console.log(`${DIM}Copilot resolves .github/agents/ from the git root, not from here.${RESET}`); - console.log(`${DIM}The Squad agent won't be visible to copilot in this folder.${RESET}`); - console.log(`${DIM}Consider running squad init from the git root instead.${RESET}`); + console.log(`${DIM}squad.agent.md → git root (.github/agents/)${RESET}`); + console.log(`${DIM}.squad/ → here (${path.basename(normalDest)}/)${RESET}`); console.log(); + // Place the agent file at the git root so Copilot can find it. + // Team state (.squad/) stays in cwd — resolved via cwd at runtime. + agentFileRoot = normalGitRoot; } } catch { - // No git available or not in a git repo — that's fine, continue normally. - // Copilot will fall back to CWD for .github/agents/ discovery. + // No git available or not in a git repo — continue normally. } // Detect squad directory @@ -205,6 +208,7 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi // Build SDK options const initOptions: InitOptions = { teamRoot: dest, + agentFileRoot, projectName: path.basename(dest) || 'my-project', agents: [ { @@ -255,7 +259,7 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi process.off('SIGINT', sigintHandler); // Ensure version is fully stamped in squad.agent.md - const agentPath = path.join(dest, '.github', 'agents', 'squad.agent.md'); + const agentPath = path.join(agentFileRoot, '.github', 'agents', 'squad.agent.md'); if (storage.existsSync(agentPath)) { stampVersion(agentPath, version); } diff --git a/packages/squad-cli/templates/squad.agent.md.template b/packages/squad-cli/templates/squad.agent.md.template index c1d0a34e7..01e18dfad 100644 --- a/packages/squad-cli/templates/squad.agent.md.template +++ b/packages/squad-cli/templates/squad.agent.md.template @@ -625,15 +625,17 @@ Squad and all spawned agents may be running inside a **git worktree** rather tha **How the Coordinator resolves the team root (on every session start):** -1. Run `git rev-parse --show-toplevel` to get the current worktree root. -2. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet). +1. **Check CWD first** — does `.squad/` exist in the current working directory? + - **Yes** → Team root = CWD. This handles monorepos where `.squad/` lives in a subfolder. +2. If not, run `git rev-parse --show-toplevel` to get the current worktree root. +3. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet). - **Yes** → use **worktree-local** strategy. Team root = current worktree root. - **No** → use **main-checkout** strategy. Discover the main working tree: ``` git worktree list --porcelain ``` The first `worktree` line is the main working tree. Team root = that path. -3. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*). +4. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*). **Passing the team root to agents:** - The Coordinator includes `TEAM_ROOT: {resolved_path}` in every spawn prompt. diff --git a/packages/squad-sdk/src/config/init.ts b/packages/squad-sdk/src/config/init.ts index 663fb74c1..9416d2a3b 100644 --- a/packages/squad-sdk/src/config/init.ts +++ b/packages/squad-sdk/src/config/init.ts @@ -136,6 +136,11 @@ export interface InitOptions { streams?: SubSquadDefinition[]; /** If true, use built-in base roles with useRole() in SDK config (default: false) */ roles?: boolean; + /** Root directory for the .github/agents/squad.agent.md file. + * Defaults to teamRoot. In monorepos, this should be the git root + * so Copilot can discover the agent file, while teamRoot stays in + * the subfolder where .squad/ lives. */ + agentFileRoot?: string; /** ADO work item configuration — used when platform is azure-devops */ adoConfig?: { defaultWorkItemType?: string; @@ -1062,7 +1067,7 @@ ${projectDescription ? `- **Description:** ${projectDescription}\n` : ''}- **Cre // Create .github/agents/squad.agent.md // ------------------------------------------------------------------------- - const agentFile = join(teamRoot, '.github', 'agents', 'squad.agent.md'); + const agentFile = join(options.agentFileRoot ?? teamRoot, '.github', 'agents', 'squad.agent.md'); if (!storage.existsSync(agentFile) || !skipExisting) { if (templatesDir && storage.existsSync(join(templatesDir, 'squad.agent.md.template'))) { let agentContent = storage.readSync(join(templatesDir, 'squad.agent.md.template')) ?? ''; diff --git a/packages/squad-sdk/templates/squad.agent.md.template b/packages/squad-sdk/templates/squad.agent.md.template index c1d0a34e7..01e18dfad 100644 --- a/packages/squad-sdk/templates/squad.agent.md.template +++ b/packages/squad-sdk/templates/squad.agent.md.template @@ -625,15 +625,17 @@ Squad and all spawned agents may be running inside a **git worktree** rather tha **How the Coordinator resolves the team root (on every session start):** -1. Run `git rev-parse --show-toplevel` to get the current worktree root. -2. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet). +1. **Check CWD first** — does `.squad/` exist in the current working directory? + - **Yes** → Team root = CWD. This handles monorepos where `.squad/` lives in a subfolder. +2. If not, run `git rev-parse --show-toplevel` to get the current worktree root. +3. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet). - **Yes** → use **worktree-local** strategy. Team root = current worktree root. - **No** → use **main-checkout** strategy. Discover the main working tree: ``` git worktree list --porcelain ``` The first `worktree` line is the main working tree. Team root = that path. -3. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*). +4. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*). **Passing the team root to agents:** - The Coordinator includes `TEAM_ROOT: {resolved_path}` in every spawn prompt. From 0017b07ff79254c75594e49bbeaba47c95b18894 Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 9 Apr 2026 10:44:06 +0300 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20address=20PR=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20template=20sync,=20tests,=20code=20reuse?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sync canonical template (.squad-templates/) and run sync-templates.mjs - Fix toRelativePath() to use path.relative() for cross-root paths - Reuse detectParentGitRepo() instead of duplicating git-root logic - Add monorepo regression test (no nested .git, agent at root, .squad in subfolder) - Add .changeset for release tracking Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/monorepo-subfolder-support.md | 8 ++++++ .github/agents/squad.agent.md | 8 +++--- .squad-templates/squad.agent.md | 8 +++--- packages/squad-cli/src/cli/core/init.ts | 34 +++++++++--------------- packages/squad-sdk/src/config/init.ts | 15 ++++------- templates/squad.agent.md.template | 8 +++--- test/init-scaffolding.test.ts | 25 +++++++++++++++++ 7 files changed, 66 insertions(+), 40 deletions(-) create mode 100644 .changeset/monorepo-subfolder-support.md diff --git a/.changeset/monorepo-subfolder-support.md b/.changeset/monorepo-subfolder-support.md new file mode 100644 index 000000000..83eefed28 --- /dev/null +++ b/.changeset/monorepo-subfolder-support.md @@ -0,0 +1,8 @@ +--- +"@bradygaster/squad-cli": patch +"@bradygaster/squad-sdk": patch +--- + +fix: monorepo subfolder support — never run git init (#939) + +When `squad init` runs in a subfolder of a monorepo, place `.github/agents/squad.agent.md` at the git root and `.squad/` in the subfolder. Never run `git init`. diff --git a/.github/agents/squad.agent.md b/.github/agents/squad.agent.md index c1d0a34e7..01e18dfad 100644 --- a/.github/agents/squad.agent.md +++ b/.github/agents/squad.agent.md @@ -625,15 +625,17 @@ Squad and all spawned agents may be running inside a **git worktree** rather tha **How the Coordinator resolves the team root (on every session start):** -1. Run `git rev-parse --show-toplevel` to get the current worktree root. -2. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet). +1. **Check CWD first** — does `.squad/` exist in the current working directory? + - **Yes** → Team root = CWD. This handles monorepos where `.squad/` lives in a subfolder. +2. If not, run `git rev-parse --show-toplevel` to get the current worktree root. +3. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet). - **Yes** → use **worktree-local** strategy. Team root = current worktree root. - **No** → use **main-checkout** strategy. Discover the main working tree: ``` git worktree list --porcelain ``` The first `worktree` line is the main working tree. Team root = that path. -3. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*). +4. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*). **Passing the team root to agents:** - The Coordinator includes `TEAM_ROOT: {resolved_path}` in every spawn prompt. diff --git a/.squad-templates/squad.agent.md b/.squad-templates/squad.agent.md index c1d0a34e7..01e18dfad 100644 --- a/.squad-templates/squad.agent.md +++ b/.squad-templates/squad.agent.md @@ -625,15 +625,17 @@ Squad and all spawned agents may be running inside a **git worktree** rather tha **How the Coordinator resolves the team root (on every session start):** -1. Run `git rev-parse --show-toplevel` to get the current worktree root. -2. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet). +1. **Check CWD first** — does `.squad/` exist in the current working directory? + - **Yes** → Team root = CWD. This handles monorepos where `.squad/` lives in a subfolder. +2. If not, run `git rev-parse --show-toplevel` to get the current worktree root. +3. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet). - **Yes** → use **worktree-local** strategy. Team root = current worktree root. - **No** → use **main-checkout** strategy. Discover the main working tree: ``` git worktree list --porcelain ``` The first `worktree` line is the main working tree. Team root = that path. -3. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*). +4. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*). **Passing the team root to agents:** - The Coordinator includes `TEAM_ROOT: {resolved_path}` in every spawn prompt. diff --git a/packages/squad-cli/src/cli/core/init.ts b/packages/squad-cli/src/cli/core/init.ts index effb8130b..abaefb19a 100644 --- a/packages/squad-cli/src/cli/core/init.ts +++ b/packages/squad-cli/src/cli/core/init.ts @@ -129,27 +129,19 @@ export async function runInit(dest: string, options: RunInitOptions = {}): Promi // squad.agent.md at the git root and .squad/ in the subfolder. // Never run `git init` — it creates broken nested repos (#939). let agentFileRoot = dest; // default: place agent file relative to dest - try { - const gitRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], { - cwd: dest, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], - }).trim().replace(/\//g, path.sep); - const normalDest = path.resolve(dest); - const normalGitRoot = path.resolve(gitRoot); - if (normalDest.toLowerCase() !== normalGitRoot.toLowerCase()) { - console.log(); - console.log(`${CYAN}${BOLD}📦 Monorepo detected${RESET}`); - console.log(`${DIM} Git root: ${normalGitRoot}${RESET}`); - console.log(`${DIM} You're in: ${normalDest}${RESET}`); - console.log(); - console.log(`${DIM}squad.agent.md → git root (.github/agents/)${RESET}`); - console.log(`${DIM}.squad/ → here (${path.basename(normalDest)}/)${RESET}`); - console.log(); - // Place the agent file at the git root so Copilot can find it. - // Team state (.squad/) stays in cwd — resolved via cwd at runtime. - agentFileRoot = normalGitRoot; - } - } catch { - // No git available or not in a git repo — continue normally. + const parentGitRoot = detectParentGitRepo(dest); + if (parentGitRoot) { + console.log(); + console.log(`${CYAN}${BOLD}📦 Monorepo detected${RESET}`); + console.log(`${DIM} Git root: ${parentGitRoot}${RESET}`); + console.log(`${DIM} You're in: ${path.resolve(dest)}${RESET}`); + console.log(); + console.log(`${DIM}squad.agent.md → git root (.github/agents/)${RESET}`); + console.log(`${DIM}.squad/ → here (${path.basename(path.resolve(dest))}/)${RESET}`); + console.log(); + // Place the agent file at the git root so Copilot can find it. + // Team state (.squad/) stays in cwd — resolved via cwd at runtime. + agentFileRoot = parentGitRoot; } // Detect squad directory diff --git a/packages/squad-sdk/src/config/init.ts b/packages/squad-sdk/src/config/init.ts index 9416d2a3b..ec2983a72 100644 --- a/packages/squad-sdk/src/config/init.ts +++ b/packages/squad-sdk/src/config/init.ts @@ -8,7 +8,7 @@ * @module config/init */ -import { join, dirname } from 'path'; +import { join, dirname, relative as pathRelative } from 'path'; import { fileURLToPath } from 'url'; import type { StorageProvider } from '../storage/index.js'; import { FSStorageProvider } from '../storage/index.js'; @@ -674,15 +674,10 @@ export async function initSquad(options: InitOptions, storage: StorageProvider = // Helper to convert absolute path to relative const toRelativePath = (absolutePath: string): string => { - // Use path separator-agnostic approach - if (absolutePath.startsWith(teamRoot)) { - const relative = absolutePath.slice(teamRoot.length); - // Remove leading separator if present - return relative.startsWith('/') || relative.startsWith('\\') - ? relative.slice(1) - : relative; - } - return absolutePath; + // Use path.relative for correct cross-root handling (monorepo: agentFileRoot != teamRoot) + const rel = pathRelative(teamRoot, absolutePath); + // path.relative returns '' for same path, use '.' instead + return rel || '.'; }; // Helper to write file (respects skipExisting) diff --git a/templates/squad.agent.md.template b/templates/squad.agent.md.template index c1d0a34e7..01e18dfad 100644 --- a/templates/squad.agent.md.template +++ b/templates/squad.agent.md.template @@ -625,15 +625,17 @@ Squad and all spawned agents may be running inside a **git worktree** rather tha **How the Coordinator resolves the team root (on every session start):** -1. Run `git rev-parse --show-toplevel` to get the current worktree root. -2. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet). +1. **Check CWD first** — does `.squad/` exist in the current working directory? + - **Yes** → Team root = CWD. This handles monorepos where `.squad/` lives in a subfolder. +2. If not, run `git rev-parse --show-toplevel` to get the current worktree root. +3. Check if `.squad/` exists at that root (fall back to `.ai-team/` for repos that haven't migrated yet). - **Yes** → use **worktree-local** strategy. Team root = current worktree root. - **No** → use **main-checkout** strategy. Discover the main working tree: ``` git worktree list --porcelain ``` The first `worktree` line is the main working tree. Team root = that path. -3. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*). +4. The user may override the strategy at any time (e.g., *"use main checkout for team state"* or *"keep team state in this worktree"*). **Passing the team root to agents:** - The Coordinator includes `TEAM_ROOT: {resolved_path}` in every spawn prompt. diff --git a/test/init-scaffolding.test.ts b/test/init-scaffolding.test.ts index c73411488..03da202db 100644 --- a/test/init-scaffolding.test.ts +++ b/test/init-scaffolding.test.ts @@ -223,6 +223,31 @@ describe('no-remote resilience (#579)', () => { await expect(initSquad(sdkOptions(TEST_ROOT))).resolves.toBeDefined(); expect(existsSync(join(TEST_ROOT, '.squad', 'casting', 'registry.json'))).toBe(true); }); + + it('monorepo subfolder: no nested git init, agent at root, .squad in subfolder (#939)', async () => { + // Set up a monorepo with a subfolder + gitInit(TEST_ROOT); + const subfolder = join(TEST_ROOT, 'team-alpha'); + await mkdir(subfolder, { recursive: true }); + + // Run SDK init with agentFileRoot pointing to the git root + const result = await initSquad({ + ...sdkOptions(subfolder), + agentFileRoot: TEST_ROOT, + }); + + // 1. No nested .git/ in subfolder + expect(existsSync(join(subfolder, '.git'))).toBe(false); + // 2. .squad/ created in subfolder + expect(existsSync(join(subfolder, '.squad'))).toBe(true); + expect(existsSync(join(subfolder, '.squad', 'casting', 'registry.json'))).toBe(true); + // 3. squad.agent.md created at monorepo root + expect(existsSync(join(TEST_ROOT, '.github', 'agents', 'squad.agent.md'))).toBe(true); + // 4. squad.agent.md NOT in subfolder + expect(existsSync(join(subfolder, '.github', 'agents', 'squad.agent.md'))).toBe(false); + // 5. createdFiles should include relative path with .. + expect(result.createdFiles.some(f => f.includes('squad.agent.md'))).toBe(true); + }); }); // ─── Doctor validation after init ────────────────────────────────────── From efe893bfb9b963bae81cf7c250bf25dbcf15edcf Mon Sep 17 00:00:00 2001 From: Copilot Date: Thu, 9 Apr 2026 12:39:29 +0300 Subject: [PATCH 3/4] fix: skip workflows in monorepo-subfolder mode (#939) GitHub Actions only reads workflows from /.github/workflows/. In monorepo mode, placing workflows in a subfolder has no effect. Additionally, multiple squads would conflict on the same workflow files. - Skip workflow placement when agentFileRoot != teamRoot - Emit warning in InitResult.warnings explaining why - Add workflow skip + warning assertions to monorepo test Found by Q (Devil's Advocate reviewer) during E2E test review. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/agents/squad.agent.md | 4 ++-- package.json | 2 +- packages/squad-cli/package.json | 2 +- packages/squad-sdk/package.json | 2 +- packages/squad-sdk/src/config/init.ts | 9 +++++++++ test/init-scaffolding.test.ts | 6 ++++++ 6 files changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/agents/squad.agent.md b/.github/agents/squad.agent.md index 01e18dfad..9dadbe7f1 100644 --- a/.github/agents/squad.agent.md +++ b/.github/agents/squad.agent.md @@ -3,14 +3,14 @@ name: Squad description: "Your AI team. Describe what you're building, get a team of specialists that live in your repo." --- - + You are **Squad (Coordinator)** — the orchestrator for this project's AI team. ### Coordinator Identity - **Name:** Squad (Coordinator) -- **Version:** 0.0.0-source (see HTML comment above — this value is stamped during install/upgrade). Include it as `Squad v{version}` in your first response of each session (e.g., in the acknowledgment or greeting). +- **Version:** 0.9.1-build.3 (see HTML comment above — this value is stamped during install/upgrade). Include it as `Squad v0.9.1-build.3` in your first response of each session (e.g., in the acknowledgment or greeting). - **Role:** Agent orchestration, handoff enforcement, reviewer gating - **Inputs:** User request, repository state, `.squad/decisions.md` - **Outputs owned:** Final assembled artifacts, orchestration log (via Scribe) diff --git a/package.json b/package.json index c01bc0a6d..f3ba74257 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad", - "version": "0.9.1", + "version": "0.9.1-build.3", "private": true, "description": "Squad — Programmable multi-agent runtime for GitHub Copilot, built on @github/copilot-sdk", "type": "module", diff --git a/packages/squad-cli/package.json b/packages/squad-cli/package.json index e84fa7aa5..82ae944f6 100644 --- a/packages/squad-cli/package.json +++ b/packages/squad-cli/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad-cli", - "version": "0.9.1", + "version": "0.9.1-build.3", "description": "Squad CLI — Command-line interface for the Squad multi-agent runtime", "type": "module", "bin": { diff --git a/packages/squad-sdk/package.json b/packages/squad-sdk/package.json index 3e9e3e184..fb9fed9f1 100644 --- a/packages/squad-sdk/package.json +++ b/packages/squad-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad-sdk", - "version": "0.9.1", + "version": "0.9.1-build.3", "description": "Squad SDK — Programmable multi-agent runtime for GitHub Copilot", "type": "module", "main": "./dist/index.js", diff --git a/packages/squad-sdk/src/config/init.ts b/packages/squad-sdk/src/config/init.ts index ec2983a72..71add7f86 100644 --- a/packages/squad-sdk/src/config/init.ts +++ b/packages/squad-sdk/src/config/init.ts @@ -1110,6 +1110,14 @@ ${projectDescription ? `- **Description:** ${projectDescription}\n` : ''}- **Cre // ------------------------------------------------------------------------- if (includeWorkflows && isGitHub && templatesDir && storage.existsSync(join(templatesDir, 'workflows'))) { + // In monorepo mode (agentFileRoot != teamRoot), skip workflow placement. + // GitHub Actions only reads from /.github/workflows/ — placing + // workflows in a subfolder has no effect. Multiple squads in one monorepo + // would also conflict on the same workflow files. (#939) + const isMonorepoSubfolder = options.agentFileRoot && options.agentFileRoot !== teamRoot; + if (isMonorepoSubfolder) { + warnings.push('Skipped GitHub Actions workflows in monorepo-subfolder mode — workflows must be at the git root. Set up workflows manually or use a single shared workflow for all squads.'); + } else { const workflowsSrc = join(templatesDir, 'workflows'); const workflowsDest = join(teamRoot, '.github', 'workflows'); @@ -1128,6 +1136,7 @@ ${projectDescription ? `- **Description:** ${projectDescription}\n` : ''}- **Cre } } } + } } // ------------------------------------------------------------------------- diff --git a/test/init-scaffolding.test.ts b/test/init-scaffolding.test.ts index 03da202db..05988e314 100644 --- a/test/init-scaffolding.test.ts +++ b/test/init-scaffolding.test.ts @@ -231,9 +231,11 @@ describe('no-remote resilience (#579)', () => { await mkdir(subfolder, { recursive: true }); // Run SDK init with agentFileRoot pointing to the git root + // Enable workflows to test monorepo skip behavior const result = await initSquad({ ...sdkOptions(subfolder), agentFileRoot: TEST_ROOT, + includeWorkflows: true, }); // 1. No nested .git/ in subfolder @@ -247,6 +249,10 @@ describe('no-remote resilience (#579)', () => { expect(existsSync(join(subfolder, '.github', 'agents', 'squad.agent.md'))).toBe(false); // 5. createdFiles should include relative path with .. expect(result.createdFiles.some(f => f.includes('squad.agent.md'))).toBe(true); + // 6. Workflows NOT placed in subfolder (GitHub Actions ignores them there) + expect(existsSync(join(subfolder, '.github', 'workflows'))).toBe(false); + // 7. Warning emitted about skipped workflows + expect(result.warnings?.some(w => w.includes('monorepo-subfolder'))).toBe(true); }); }); From 2a60095e610ff90c2419131df657f460cf12ade4 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 14 Apr 2026 09:19:20 +0300 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20address=20review=20comments=20?= =?UTF-8?q?=E2=80=94=20revert=20build=20versions,=20path=20normalization,?= =?UTF-8?q?=20CLI=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert all package.json versions from 0.9.1-build.3 to 0.9.1 (build versions are local-only, changesets drive real bumps) - Revert .github/agents/squad.agent.md to canonical template content - Fix isMonorepoSubfolder: use path.resolve() + case-insensitive compare - Add CLI-level runInit() monorepo test (verifies no .git in subfolder) - Import path.resolve in SDK init.ts Addresses review comments on #944. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/agents/squad.agent.md | 4 ++-- package.json | 2 +- packages/squad-cli/package.json | 2 +- packages/squad-sdk/package.json | 2 +- packages/squad-sdk/src/config/init.ts | 5 +++-- test/init-scaffolding.test.ts | 19 +++++++++++++++++++ 6 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.github/agents/squad.agent.md b/.github/agents/squad.agent.md index 9dadbe7f1..01e18dfad 100644 --- a/.github/agents/squad.agent.md +++ b/.github/agents/squad.agent.md @@ -3,14 +3,14 @@ name: Squad description: "Your AI team. Describe what you're building, get a team of specialists that live in your repo." --- - + You are **Squad (Coordinator)** — the orchestrator for this project's AI team. ### Coordinator Identity - **Name:** Squad (Coordinator) -- **Version:** 0.9.1-build.3 (see HTML comment above — this value is stamped during install/upgrade). Include it as `Squad v0.9.1-build.3` in your first response of each session (e.g., in the acknowledgment or greeting). +- **Version:** 0.0.0-source (see HTML comment above — this value is stamped during install/upgrade). Include it as `Squad v{version}` in your first response of each session (e.g., in the acknowledgment or greeting). - **Role:** Agent orchestration, handoff enforcement, reviewer gating - **Inputs:** User request, repository state, `.squad/decisions.md` - **Outputs owned:** Final assembled artifacts, orchestration log (via Scribe) diff --git a/package.json b/package.json index f3ba74257..c01bc0a6d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad", - "version": "0.9.1-build.3", + "version": "0.9.1", "private": true, "description": "Squad — Programmable multi-agent runtime for GitHub Copilot, built on @github/copilot-sdk", "type": "module", diff --git a/packages/squad-cli/package.json b/packages/squad-cli/package.json index 82ae944f6..e84fa7aa5 100644 --- a/packages/squad-cli/package.json +++ b/packages/squad-cli/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad-cli", - "version": "0.9.1-build.3", + "version": "0.9.1", "description": "Squad CLI — Command-line interface for the Squad multi-agent runtime", "type": "module", "bin": { diff --git a/packages/squad-sdk/package.json b/packages/squad-sdk/package.json index fb9fed9f1..3e9e3e184 100644 --- a/packages/squad-sdk/package.json +++ b/packages/squad-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@bradygaster/squad-sdk", - "version": "0.9.1-build.3", + "version": "0.9.1", "description": "Squad SDK — Programmable multi-agent runtime for GitHub Copilot", "type": "module", "main": "./dist/index.js", diff --git a/packages/squad-sdk/src/config/init.ts b/packages/squad-sdk/src/config/init.ts index 71add7f86..1533a3421 100644 --- a/packages/squad-sdk/src/config/init.ts +++ b/packages/squad-sdk/src/config/init.ts @@ -8,7 +8,7 @@ * @module config/init */ -import { join, dirname, relative as pathRelative } from 'path'; +import { join, dirname, relative as pathRelative, resolve as pathResolve } from 'path'; import { fileURLToPath } from 'url'; import type { StorageProvider } from '../storage/index.js'; import { FSStorageProvider } from '../storage/index.js'; @@ -1114,7 +1114,8 @@ ${projectDescription ? `- **Description:** ${projectDescription}\n` : ''}- **Cre // GitHub Actions only reads from /.github/workflows/ — placing // workflows in a subfolder has no effect. Multiple squads in one monorepo // would also conflict on the same workflow files. (#939) - const isMonorepoSubfolder = options.agentFileRoot && options.agentFileRoot !== teamRoot; + const isMonorepoSubfolder = !!options.agentFileRoot && + pathResolve(options.agentFileRoot).toLowerCase() !== pathResolve(teamRoot).toLowerCase(); if (isMonorepoSubfolder) { warnings.push('Skipped GitHub Actions workflows in monorepo-subfolder mode — workflows must be at the git root. Set up workflows manually or use a single shared workflow for all squads.'); } else { diff --git a/test/init-scaffolding.test.ts b/test/init-scaffolding.test.ts index 05988e314..3b180a812 100644 --- a/test/init-scaffolding.test.ts +++ b/test/init-scaffolding.test.ts @@ -218,6 +218,25 @@ describe('no-remote resilience (#579)', () => { expect(existsSync(join(TEST_ROOT, '.github', 'agents', 'squad.agent.md'))).toBe(true); }); + it('runInit CLI: monorepo subfolder — no nested .git, agent at git root (#939)', async () => { + // Set up a monorepo: git init at TEST_ROOT, run from a subfolder + gitInit(TEST_ROOT); + const subfolder = join(TEST_ROOT, 'services', 'api'); + await mkdir(subfolder, { recursive: true }); + + // runInit from the subfolder — CLI should detect the parent git repo + await expect(runInit(subfolder)).resolves.toBeUndefined(); + + // 1. No nested .git/ in subfolder (the original bug) + expect(existsSync(join(subfolder, '.git'))).toBe(false); + // 2. .squad/ created in the subfolder + expect(existsSync(join(subfolder, '.squad'))).toBe(true); + expect(existsSync(join(subfolder, '.squad', 'casting', 'registry.json'))).toBe(true); + // 3. squad.agent.md placed at the git root, not the subfolder + expect(existsSync(join(TEST_ROOT, '.github', 'agents', 'squad.agent.md'))).toBe(true); + expect(existsSync(join(subfolder, '.github', 'agents', 'squad.agent.md'))).toBe(false); + }); + it('initSquad succeeds when git is not initialized at all', async () => { // TEST_ROOT is a plain directory — no git init await expect(initSquad(sdkOptions(TEST_ROOT))).resolves.toBeDefined();