Skip to content

Commit 67d9ed0

Browse files
ashtewariCopilottamirdresherbradygasterCopilot
authored
fix(cli): validate doctor state files against resolved team root in remote mode (#2057)
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> Co-authored-by: Tamir Dresher <tamir.dresher@gmail.com> Co-authored-by: Brady Gaster <41929050+bradygaster@users.noreply.github.com> Co-authored-by: Copilot <Copilot@users.noreply.github.com>
1 parent 059b31e commit 67d9ed0

3 files changed

Lines changed: 44 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bradygaster/squad-cli": patch
3+
---
4+
5+
`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.

‎packages/squad-cli/src/cli/commands/doctor.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { FSStorageProvider, resolveStateBackend, type StateBackendType } from '@
1616
import { readCastingRegistryPair } from '@bradygaster/squad-sdk/casting';
1717
import { resolveSquadPaths, clearResolveSquadCache } from '@bradygaster/squad-sdk/resolution';
1818
import { isMutableStateKey, MUTABLE_STATE_PATHS } from '@bradygaster/squad-sdk/tools';
19-
import { resolveStateDir } from '../core/effective-squad-dir.js';
19+
import { effectiveSquadDir } from '../core/effective-squad-dir.js';
2020

2121
const storage = new FSStorageProvider();
2222

@@ -796,8 +796,11 @@ export async function runDoctor(cwd?: string): Promise<DoctorCheck[]> {
796796

797797
// 5–9 standard files (only if .squad/ exists)
798798
if (isDirectory(squadDir)) {
799-
// Resolve effective state dir for externalized files
800-
const stateDir = resolveStateDir(squadDir);
799+
// Resolve effective state dir for externalized files and remote team
800+
// roots — mirrors the dual-root resolver used by `squad cast`/`squad status`
801+
// so linked (remote-mode) projects are validated against the team root's
802+
// .squad/ instead of the local stub that only holds config.json (#2056).
803+
const { stateDir } = effectiveSquadDir(resolvedCwd);
801804
checks.push(checkTeamMd(stateDir));
802805
checks.push(checkRoutingMd(stateDir));
803806
checks.push(checkAgentsDir(stateDir));

‎test/cli/doctor.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,39 @@ describe('squad doctor', () => {
116116
expect(rootCheck?.status).toBe('fail');
117117
});
118118

119+
it('validates state files against the resolved team root in remote mode (#2056)', async () => {
120+
// Central team root: TEST_ROOT/.squad holds the real team state.
121+
await scaffold(TEST_ROOT);
122+
123+
// Linked project: only .squad/config.json + the agent discovery file,
124+
// pointing back at the central root via a relative teamRoot.
125+
const linkedProject = join(TEST_ROOT, 'linked-project');
126+
await mkdir(join(linkedProject, '.squad'), { recursive: true });
127+
await writeFile(
128+
join(linkedProject, '.squad', 'config.json'),
129+
JSON.stringify({ version: 1, teamRoot: '..', projectKey: null }),
130+
);
131+
await mkdir(join(linkedProject, '.github', 'agents'), { recursive: true });
132+
await writeFile(join(linkedProject, '.github', 'agents', 'squad.agent.md'), '# Squad Agent\n');
133+
134+
const checks = await runDoctor(linkedProject);
135+
136+
const rootCheck = checks.find((c: DoctorCheck) => c.name === 'team root resolves');
137+
expect(rootCheck?.status).toBe('pass');
138+
139+
for (const name of [
140+
'team.md found with ## Members header',
141+
'routing.md found',
142+
'agents/ directory exists',
143+
'casting/registry.json exists',
144+
'decisions.md exists',
145+
]) {
146+
const check = checks.find((c: DoctorCheck) => c.name === name);
147+
expect(check, `expected a check named "${name}"`).toBeDefined();
148+
expect(check?.status, `expected "${name}" to pass, got: ${check?.message}`).toBe('pass');
149+
}
150+
});
151+
119152
// One row per MUTABLE_STATE_PATHS entry: the state tools could write each of
120153
// these to the team repo root before #2107. `scaffold()` already puts
121154
// decisions.md in the team's .squad/, so that row is a merge, not a move.

0 commit comments

Comments
 (0)