Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/2056-doctor-remote-state-dir.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 6 additions & 3 deletions packages/squad-cli/src/cli/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { FSStorageProvider, resolveStateBackend, type StateBackendType } from '@
import { readCastingRegistryPair } from '@bradygaster/squad-sdk/casting';
import { resolveSquadPaths, clearResolveSquadCache } from '@bradygaster/squad-sdk/resolution';
import { isMutableStateKey, MUTABLE_STATE_PATHS } from '@bradygaster/squad-sdk/tools';
import { resolveStateDir } from '../core/effective-squad-dir.js';
import { effectiveSquadDir } from '../core/effective-squad-dir.js';

const storage = new FSStorageProvider();

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

// 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));
Expand Down
33 changes: 33 additions & 0 deletions test/cli/doctor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,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');
}
});

// One row per MUTABLE_STATE_PATHS entry: the state tools could write each of
// these to the team repo root before #2107. `scaffold()` already puts
// decisions.md in the team's .squad/, so that row is a merge, not a move.
Expand Down
Loading