From fd20373cf4ecab79f4158ee00ca86a254f3fadcd Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 11 May 2026 15:27:35 -0400 Subject: [PATCH] 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 --- .changeset/new-project-agent-diagnostics.md | 5 ++ get-shit-done/workflows/new-project.md | 18 +++++-- sdk/src/query/init-complex.test.ts | 40 +++++++++++++++ sdk/src/query/init-complex.ts | 51 ++++++++++++++++++- ...bug-2419-project-researcher-agent.test.cjs | 12 +++++ 5 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 .changeset/new-project-agent-diagnostics.md diff --git a/.changeset/new-project-agent-diagnostics.md b/.changeset/new-project-agent-diagnostics.md new file mode 100644 index 000000000..20af7daa4 --- /dev/null +++ b/.changeset/new-project-agent-diagnostics.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3390 +--- +**`/gsd-new-project` now reports required-agent registration separately from configured agent skill payloads** - missing-agent warnings identify the runtime agents directory and whether prompt skill payloads are available. diff --git a/get-shit-done/workflows/new-project.md b/get-shit-done/workflows/new-project.md index ea62a165a..4356c66d6 100644 --- a/get-shit-done/workflows/new-project.md +++ b/get-shit-done/workflows/new-project.md @@ -64,15 +64,27 @@ AGENT_SKILLS_SYNTHESIZER=$(gsd-sdk query agent-skills gsd-research-synthesizer) AGENT_SKILLS_ROADMAPPER=$(gsd-sdk query agent-skills gsd-roadmapper) ``` -Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `project_exists`, `has_codebase_map`, `planning_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `needs_codebase_map`, `has_git`, `project_path`, `agents_installed`, `missing_agents`. +Parse JSON for: `researcher_model`, `synthesizer_model`, `roadmapper_model`, `commit_docs`, `project_exists`, `has_codebase_map`, `planning_exists`, `has_existing_code`, `has_package_file`, `is_brownfield`, `needs_codebase_map`, `has_git`, `project_path`, `agents_installed`, `missing_agents`, `agent_runtime`, `agents_dir`, `required_agents`, `required_agents_installed`, `missing_required_agents`, `agent_skill_payloads_available`, `agent_skill_payload_agents`. **If `agents_installed` is false:** Display a warning before proceeding: -``` +```text ⚠ GSD agents not installed. The following agents are missing from your agents directory: {missing_agents joined with newline} +Runtime checked: {agent_runtime} +Agents directory checked: {agents_dir} +Required new-project agents missing: + {missing_required_agents joined with newline, or "none"} + +Agent skill payloads available: {agent_skill_payloads_available} +Agent skill payload agents: + {agent_skill_payload_agents joined with newline, or "none"} + +Skill payloads only provide prompt context. Named subagent spawns still require agent +definitions to be installed for this runtime. + Subagent spawns (gsd-project-researcher, gsd-research-synthesizer, gsd-roadmapper) will fail -with "agent type not found". Run the installer with --global to make agents available: +with "agent type not found" if `required_agents_installed` is false. Run the installer with --global to make agents available: npx get-shit-done-cc@latest --global diff --git a/sdk/src/query/init-complex.test.ts b/sdk/src/query/init-complex.test.ts index 9aca816ba..925151710 100644 --- a/sdk/src/query/init-complex.test.ts +++ b/sdk/src/query/init-complex.test.ts @@ -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; 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; + + 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', () => { diff --git a/sdk/src/query/init-complex.ts b/sdk/src/query/init-complex.ts index 2e29d4251..a9e66e43a 100644 --- a/sdk/src/query/init-complex.ts +++ b/sdk/src/query/init-complex.ts @@ -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 { + 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 = { 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) }; diff --git a/tests/bug-2419-project-researcher-agent.test.cjs b/tests/bug-2419-project-researcher-agent.test.cjs index 516369297..38ac1bcb7 100644 --- a/tests/bug-2419-project-researcher-agent.test.cjs +++ b/tests/bug-2419-project-researcher-agent.test.cjs @@ -58,6 +58,18 @@ describe('gsd-project-researcher agent registration (#2419)', () => { ); }); + test('new-project.md reports required-agent and skill-payload diagnostics separately', () => { + const content = fs.readFileSync(NEW_PROJECT_PATH, 'utf-8'); + assert.ok(content.includes('required_agents_installed'), + 'new-project.md must parse required_agents_installed from init JSON'); + assert.ok(content.includes('missing_required_agents'), + 'new-project.md must report missing required new-project agents separately'); + assert.ok(content.includes('agent_skill_payloads_available'), + 'new-project.md must distinguish skill payload availability from agent definitions'); + assert.ok(content.includes('agents_dir'), + 'new-project.md must show which agents directory was checked'); + }); + test('new-milestone.md parses agents_installed from init JSON', () => { const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8'); assert.ok(