Fix(workflow): expose new-project agent diagnostics (#3390)

* fix: expose new-project agent diagnostics

* chore: add changeset for new-project agent diagnostics

* docs: tag new-project warning fence
This commit is contained in:
Tom Boucher
2026-05-11 15:27:35 -04:00
committed by GitHub
parent 543a8569e4
commit fd20373cf4
5 changed files with 122 additions and 4 deletions

View File

@@ -12,8 +12,10 @@ import { tmpdir } from 'node:os';
import { initNewProject, initProgress, initManager } from './init-complex.js';
let tmpDir: string;
let previousGsdAgentsDir: string | undefined;
beforeEach(async () => {
previousGsdAgentsDir = process.env.GSD_AGENTS_DIR;
tmpDir = await mkdtemp(join(tmpdir(), 'gsd-init-complex-'));
// Create minimal .planning structure
@@ -83,6 +85,8 @@ beforeEach(async () => {
});
afterEach(async () => {
if (previousGsdAgentsDir === undefined) delete process.env.GSD_AGENTS_DIR;
else process.env.GSD_AGENTS_DIR = previousGsdAgentsDir;
await rm(tmpDir, { recursive: true, force: true });
});
@@ -119,6 +123,42 @@ describe('initNewProject', () => {
const data = result.data as Record<string, unknown>;
expect(data.planning_exists).toBe(true);
});
it('separates required agent registration from skill payload availability (#3388)', async () => {
const emptyAgentsDir = join(tmpDir, 'empty-agents');
await mkdir(emptyAgentsDir, { recursive: true });
process.env.GSD_AGENTS_DIR = emptyAgentsDir;
const requiredAgents = [
'gsd-project-researcher',
'gsd-research-synthesizer',
'gsd-roadmapper',
];
for (const agent of requiredAgents) {
await mkdir(join(tmpDir, '.claude', 'skills', agent), { recursive: true });
await writeFile(join(tmpDir, '.claude', 'skills', agent, 'SKILL.md'), `# ${agent}\n`);
}
await writeFile(join(tmpDir, '.planning', 'config.json'), JSON.stringify({
model_profile: 'balanced',
commit_docs: false,
agent_skills: {
'gsd-project-researcher': ['.claude/skills/gsd-project-researcher'],
'gsd-research-synthesizer': ['.claude/skills/gsd-research-synthesizer'],
'gsd-roadmapper': ['.claude/skills/gsd-roadmapper'],
},
workflow: { research: true, plan_check: true, verifier: true, nyquist_validation: true },
}));
const result = await initNewProject([], tmpDir);
const data = result.data as Record<string, unknown>;
expect(data.agents_installed).toBe(false);
expect(data.required_agents).toEqual(requiredAgents);
expect(data.required_agents_installed).toBe(false);
expect(data.missing_required_agents).toEqual(requiredAgents);
expect(data.agent_skill_payloads_available).toBe(true);
expect(data.agent_skill_payload_agents).toEqual(requiredAgents);
});
});
describe('initProgress', () => {

View File

@@ -25,13 +25,21 @@ import { homedir } from 'node:os';
import { loadConfig } from '../config.js';
import { resolveModel } from './config-query.js';
import { planningPaths, normalizePhaseName, phaseTokenMatches, toPosixPath } from './helpers.js';
import {
detectRuntime,
planningPaths,
normalizePhaseName,
phaseTokenMatches,
resolveAgentsDir,
toPosixPath,
} from './helpers.js';
import {
getMilestoneInfo,
extractCurrentMilestone,
extractNextMilestoneSection,
extractPhasesFromSection,
} from './roadmap.js';
import { agentSkills } from './skills.js';
import { withProjectRoot } from './init.js';
import type { QueryHandler } from './utils.js';
@@ -53,6 +61,31 @@ function pathExists(base: string, relPath: string): boolean {
return existsSync(join(base, relPath));
}
const NEW_PROJECT_REQUIRED_AGENTS = [
'gsd-project-researcher',
'gsd-research-synthesizer',
'gsd-roadmapper',
];
function hasAgentDefinition(agentsDir: string, agent: string): boolean {
return existsSync(join(agentsDir, `${agent}.md`)) ||
existsSync(join(agentsDir, `${agent}.agent.md`));
}
async function resolveAgentSkillPayloadAgents(
requiredAgents: string[],
projectDir: string,
): Promise<string[]> {
const available: string[] = [];
for (const agent of requiredAgents) {
const result = await agentSkills([agent], projectDir);
if (typeof result.data === 'string' && result.data.trim() !== '') {
available.push(agent);
}
}
return available;
}
/**
* Extract ROADMAP checkbox states: `- [x] Phase N` → true, `- [ ] Phase N` → false.
* Shared by initProgress and initManager so both treat ROADMAP as the
@@ -190,6 +223,15 @@ export const initNewProject: QueryHandler = async (_args, projectDir, workstream
getModelAlias('gsd-research-synthesizer', projectDir),
getModelAlias('gsd-roadmapper', projectDir),
]);
const runtime = detectRuntime(config as { runtime?: unknown });
const agentsDir = resolveAgentsDir(runtime);
const missingRequiredAgents = NEW_PROJECT_REQUIRED_AGENTS.filter(
agent => !hasAgentDefinition(agentsDir, agent),
);
const agentSkillPayloadAgents = await resolveAgentSkillPayloadAgents(
NEW_PROJECT_REQUIRED_AGENTS,
projectDir,
);
const result: Record<string, unknown> = {
researcher_model: researcherModel,
@@ -215,6 +257,13 @@ export const initNewProject: QueryHandler = async (_args, projectDir, workstream
exa_search_available: hasExaSearch,
project_path: '.planning/PROJECT.md',
agent_runtime: runtime,
agents_dir: agentsDir,
required_agents: NEW_PROJECT_REQUIRED_AGENTS,
required_agents_installed: missingRequiredAgents.length === 0,
missing_required_agents: missingRequiredAgents,
agent_skill_payloads_available: agentSkillPayloadAgents.length === NEW_PROJECT_REQUIRED_AGENTS.length,
agent_skill_payload_agents: agentSkillPayloadAgents,
};
return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };