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:
5
.changeset/new-project-agent-diagnostics.md
Normal file
5
.changeset/new-project-agent-diagnostics.md
Normal file
@@ -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.
|
||||||
@@ -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)
|
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:
|
**If `agents_installed` is false:** Display a warning before proceeding:
|
||||||
```
|
```text
|
||||||
⚠ GSD agents not installed. The following agents are missing from your agents directory:
|
⚠ GSD agents not installed. The following agents are missing from your agents directory:
|
||||||
{missing_agents joined with newline}
|
{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
|
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
|
npx get-shit-done-cc@latest --global
|
||||||
|
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ import { tmpdir } from 'node:os';
|
|||||||
import { initNewProject, initProgress, initManager } from './init-complex.js';
|
import { initNewProject, initProgress, initManager } from './init-complex.js';
|
||||||
|
|
||||||
let tmpDir: string;
|
let tmpDir: string;
|
||||||
|
let previousGsdAgentsDir: string | undefined;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
previousGsdAgentsDir = process.env.GSD_AGENTS_DIR;
|
||||||
tmpDir = await mkdtemp(join(tmpdir(), 'gsd-init-complex-'));
|
tmpDir = await mkdtemp(join(tmpdir(), 'gsd-init-complex-'));
|
||||||
|
|
||||||
// Create minimal .planning structure
|
// Create minimal .planning structure
|
||||||
@@ -83,6 +85,8 @@ beforeEach(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(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 });
|
await rm(tmpDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,6 +123,42 @@ describe('initNewProject', () => {
|
|||||||
const data = result.data as Record<string, unknown>;
|
const data = result.data as Record<string, unknown>;
|
||||||
expect(data.planning_exists).toBe(true);
|
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', () => {
|
describe('initProgress', () => {
|
||||||
|
|||||||
@@ -25,13 +25,21 @@ import { homedir } from 'node:os';
|
|||||||
|
|
||||||
import { loadConfig } from '../config.js';
|
import { loadConfig } from '../config.js';
|
||||||
import { resolveModel } from './config-query.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 {
|
import {
|
||||||
getMilestoneInfo,
|
getMilestoneInfo,
|
||||||
extractCurrentMilestone,
|
extractCurrentMilestone,
|
||||||
extractNextMilestoneSection,
|
extractNextMilestoneSection,
|
||||||
extractPhasesFromSection,
|
extractPhasesFromSection,
|
||||||
} from './roadmap.js';
|
} from './roadmap.js';
|
||||||
|
import { agentSkills } from './skills.js';
|
||||||
import { withProjectRoot } from './init.js';
|
import { withProjectRoot } from './init.js';
|
||||||
import type { QueryHandler } from './utils.js';
|
import type { QueryHandler } from './utils.js';
|
||||||
|
|
||||||
@@ -53,6 +61,31 @@ function pathExists(base: string, relPath: string): boolean {
|
|||||||
return existsSync(join(base, relPath));
|
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.
|
* Extract ROADMAP checkbox states: `- [x] Phase N` → true, `- [ ] Phase N` → false.
|
||||||
* Shared by initProgress and initManager so both treat ROADMAP as the
|
* 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-research-synthesizer', projectDir),
|
||||||
getModelAlias('gsd-roadmapper', 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> = {
|
const result: Record<string, unknown> = {
|
||||||
researcher_model: researcherModel,
|
researcher_model: researcherModel,
|
||||||
@@ -215,6 +257,13 @@ export const initNewProject: QueryHandler = async (_args, projectDir, workstream
|
|||||||
exa_search_available: hasExaSearch,
|
exa_search_available: hasExaSearch,
|
||||||
|
|
||||||
project_path: '.planning/PROJECT.md',
|
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>) };
|
return { data: withProjectRoot(projectDir, result, config as Record<string, unknown>) };
|
||||||
|
|||||||
@@ -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', () => {
|
test('new-milestone.md parses agents_installed from init JSON', () => {
|
||||||
const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8');
|
const content = fs.readFileSync(NEW_MILESTONE_PATH, 'utf-8');
|
||||||
assert.ok(
|
assert.ok(
|
||||||
|
|||||||
Reference in New Issue
Block a user