From d71554e0bebf680a5e8c32dccc9ed7c256f5e61e Mon Sep 17 00:00:00 2001 From: Ash Date: Fri, 18 Sep 2026 19:46:59 -0400 Subject: [PATCH] fix(cli): validate doctor state files against resolved team root in remote mode squad doctor detected the remote teamRoot correctly but then ran the team.md/routing.md/agents/casting/decisions checks against the local .squad/ stub instead of the resolved team root's .squad/, producing false 'file not found' failures for linked projects even though squad status and squad cast resolved the team fine. runDoctor() now uses effectiveSquadDir() (the same dual-root resolver already used by build/copilot/export/loop/plugin/watch) to compute the state dir, so it follows the remote teamRoot the same way the rest of the CLI does. Fixes #2056. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .changeset/2056-doctor-remote-state-dir.md | 5 +++ packages/squad-cli/src/cli/commands/doctor.ts | 9 +++-- test/cli/doctor.test.ts | 33 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 .changeset/2056-doctor-remote-state-dir.md diff --git a/.changeset/2056-doctor-remote-state-dir.md b/.changeset/2056-doctor-remote-state-dir.md new file mode 100644 index 000000000..b51d87519 --- /dev/null +++ b/.changeset/2056-doctor-remote-state-dir.md @@ -0,0 +1,5 @@ +--- +"@bradygaster/squad-cli": patch +--- + +`squad doctor` now validates team state files (`team.md`, `routing.md`, `agents/`, `casting/registry.json`, `decisions.md`) against the resolved team root in remote (linked) mode instead of the local `.squad/` stub, which only holds `config.json`. Previously this caused false "file not found" failures for correctly configured `squad link` setups even though `squad status` and `squad cast` resolved the team fine. Fixes #2056. diff --git a/packages/squad-cli/src/cli/commands/doctor.ts b/packages/squad-cli/src/cli/commands/doctor.ts index 3e0b0bbde..3c0574af9 100644 --- a/packages/squad-cli/src/cli/commands/doctor.ts +++ b/packages/squad-cli/src/cli/commands/doctor.ts @@ -13,7 +13,7 @@ import path from 'node:path'; import { execFile, execFileSync } from 'node:child_process'; import { FSStorageProvider, resolveStateBackend, type StateBackendType } from '@bradygaster/squad-sdk'; -import { resolveStateDir } from '../core/effective-squad-dir.js'; +import { effectiveSquadDir } from '../core/effective-squad-dir.js'; const storage = new FSStorageProvider(); @@ -715,8 +715,11 @@ export async function runDoctor(cwd?: string): Promise { // 5–9 standard files (only if .squad/ exists) if (isDirectory(squadDir)) { - // Resolve effective state dir for externalized files - const stateDir = resolveStateDir(squadDir); + // Resolve effective state dir for externalized files and remote team + // roots — mirrors the dual-root resolver used by `squad cast`/`squad status` + // so linked (remote-mode) projects are validated against the team root's + // .squad/ instead of the local stub that only holds config.json (#2056). + const { stateDir } = effectiveSquadDir(resolvedCwd); checks.push(checkTeamMd(stateDir)); checks.push(checkRoutingMd(stateDir)); checks.push(checkAgentsDir(stateDir)); diff --git a/test/cli/doctor.test.ts b/test/cli/doctor.test.ts index 65e4b7a4b..a00769a9d 100644 --- a/test/cli/doctor.test.ts +++ b/test/cli/doctor.test.ts @@ -89,6 +89,39 @@ describe('squad doctor', () => { expect(rootCheck?.status).toBe('fail'); }); + it('validates state files against the resolved team root in remote mode (#2056)', async () => { + // Central team root: TEST_ROOT/.squad holds the real team state. + await scaffold(TEST_ROOT); + + // Linked project: only .squad/config.json + the agent discovery file, + // pointing back at the central root via a relative teamRoot. + const linkedProject = join(TEST_ROOT, 'linked-project'); + await mkdir(join(linkedProject, '.squad'), { recursive: true }); + await writeFile( + join(linkedProject, '.squad', 'config.json'), + JSON.stringify({ version: 1, teamRoot: '..', projectKey: null }), + ); + await mkdir(join(linkedProject, '.github', 'agents'), { recursive: true }); + await writeFile(join(linkedProject, '.github', 'agents', 'squad.agent.md'), '# Squad Agent\n'); + + const checks = await runDoctor(linkedProject); + + const rootCheck = checks.find((c: DoctorCheck) => c.name === 'team root resolves'); + expect(rootCheck?.status).toBe('pass'); + + for (const name of [ + 'team.md found with ## Members header', + 'routing.md found', + 'agents/ directory exists', + 'casting/registry.json exists', + 'decisions.md exists', + ]) { + const check = checks.find((c: DoctorCheck) => c.name === name); + expect(check, `expected a check named "${name}"`).toBeDefined(); + expect(check?.status, `expected "${name}" to pass, got: ${check?.message}`).toBe('pass'); + } + }); + it('detects hub mode from squad-hub.json', async () => { await writeFile(join(TEST_ROOT, 'squad-hub.json'), JSON.stringify({ squads: [] })); await mkdir(join(TEST_ROOT, '.squad'), { recursive: true });