@polymorphism-tech/morph-spec 4.10.5 → 4.10.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -0
- package/README.md +3 -3
- package/bin/morph-spec.js +90 -1
- package/bin/task-manager.js +59 -6
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/agents/backend/hangfire-orchestrator.md +1 -1
- package/framework/agents/backend/ms-agent-expert.md +2 -1
- package/framework/agents/integrations/asaas-financial.md +1 -1
- package/framework/agents/integrations/clerk-auth.md +1 -2
- package/framework/agents/integrations/hangfire-integration.md +1 -1
- package/framework/agents/integrations/resend-email.md +1 -1
- package/framework/agents.json +345 -708
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +10 -5
- package/framework/skills/README.md +0 -1
- package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +42 -13
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +16 -12
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/commands/agents/agent-metrics.js +174 -0
- package/src/commands/agents/audit-dispatch.js +82 -0
- package/src/commands/agents/detect-agents.js +275 -0
- package/src/commands/agents/dispatch-agents.js +41 -135
- package/src/commands/agents/index.js +9 -0
- package/src/commands/agents/manage-agents.js +352 -0
- package/src/commands/project/doctor.js +61 -2
- package/src/commands/project/init.js +10 -2
- package/src/commands/project/update.js +12 -25
- package/src/commands/state/advance-phase.js +11 -3
- package/src/core/state/state-manager.js +81 -0
- package/src/lib/agents/agents-manager.js +349 -0
- package/src/lib/agents/metrics-calculator.js +125 -0
- package/src/lib/agents/output-collector.js +75 -0
- package/src/lib/generators/context-generator.js +8 -7
- package/src/lib/tasks/task-parser.js +94 -3
- package/src/lib/validators/checkpoint-persistence.js +39 -0
- package/src/lib/validators/content/content-validator.js +9 -1
- package/src/lib/validators/spec-validator.js +2 -2
- package/src/lib/validators/validation-runner.js +20 -0
- package/src/scripts/setup-infra.js +1 -17
- package/src/utils/agent-id-mapper.js +11 -0
- package/src/utils/agents-installer.js +40 -8
- package/src/utils/claude-settings-manager.js +1 -1
- package/src/utils/hooks-installer.js +0 -245
- package/src/utils/tier-helpers.js +24 -0
- package/framework/agents/architecture/po-pm-advisor.md +0 -203
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +0 -68
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* detect-agents — Detect relevant agents for a feature based on keywords
|
|
3
|
+
*
|
|
4
|
+
* Supports single string input (legacy) and multi-source TextSource[] input
|
|
5
|
+
* with weighted keyword scoring. Also provides refreshAgents() for additive
|
|
6
|
+
* agent detection at phase boundaries.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* morph-spec detect-agents <feature> [--proposal <path>] [--refresh] [--json] [--table]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { join, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
import { collectPhaseOutputs } from '../../lib/agents/output-collector.js';
|
|
17
|
+
import { loadAgentsFile } from '../../lib/agents/agents-manager.js';
|
|
18
|
+
import { loadState, saveState, addAgent } from '../../core/state/state-manager.js';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
/** Default path to agents.json in the installed package */
|
|
23
|
+
const AGENTS_JSON_PATH = join(__dirname, '../../../framework/agents.json');
|
|
24
|
+
|
|
25
|
+
/** Match weight constants */
|
|
26
|
+
const MATCH_WEIGHTS = {
|
|
27
|
+
heading: 3,
|
|
28
|
+
body: 1,
|
|
29
|
+
domain: 2,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Detect agents relevant to a feature.
|
|
34
|
+
*
|
|
35
|
+
* @param {string|Array<{source: string, content: string, weight: number}>} sourcesOrText - Text sources or legacy string
|
|
36
|
+
* @param {Object} agentsJson - Parsed agents.json content
|
|
37
|
+
* @param {Object} [opts] - Options
|
|
38
|
+
* @param {string[]} [opts.excludeIds] - Agent IDs to exclude from detection
|
|
39
|
+
* @returns {{ detected: Array<{id: string, reason: string, keywords_matched: string[], score: number, sources: Array}>, skipped: Array<{id: string, reason: string}> }}
|
|
40
|
+
*/
|
|
41
|
+
export function detectAgents(sourcesOrText, agentsJson, opts = {}) {
|
|
42
|
+
// Backward compatibility: wrap string as single source
|
|
43
|
+
const sources = typeof sourcesOrText === 'string'
|
|
44
|
+
? [{ source: 'proposal.md', content: sourcesOrText, weight: 1.0 }]
|
|
45
|
+
: sourcesOrText;
|
|
46
|
+
|
|
47
|
+
const allAgents = agentsJson.agents || {};
|
|
48
|
+
const excludeIds = new Set(opts.excludeIds || []);
|
|
49
|
+
|
|
50
|
+
const detected = [];
|
|
51
|
+
const skipped = [];
|
|
52
|
+
|
|
53
|
+
for (const [agentId, agentData] of Object.entries(allAgents)) {
|
|
54
|
+
if (excludeIds.has(agentId)) continue;
|
|
55
|
+
if (!agentData || typeof agentData !== 'object' || !agentData.tier) continue;
|
|
56
|
+
|
|
57
|
+
// Always-active agents (active_phases includes '*')
|
|
58
|
+
if (Array.isArray(agentData.active_phases) && agentData.active_phases.includes('*')) {
|
|
59
|
+
detected.push({
|
|
60
|
+
id: agentId,
|
|
61
|
+
reason: 'active_phases: *',
|
|
62
|
+
keywords_matched: [],
|
|
63
|
+
score: 0,
|
|
64
|
+
sources: [],
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Score across all sources
|
|
70
|
+
const keywords = agentData.keywords || [];
|
|
71
|
+
const domains = agentData.domains || [];
|
|
72
|
+
let totalScore = 0;
|
|
73
|
+
const allMatched = new Set();
|
|
74
|
+
const sourceBreakdown = [];
|
|
75
|
+
|
|
76
|
+
for (const src of sources) {
|
|
77
|
+
const textLower = src.content.toLowerCase();
|
|
78
|
+
const lines = src.content.split('\n');
|
|
79
|
+
let srcScore = 0;
|
|
80
|
+
const srcKeywords = [];
|
|
81
|
+
|
|
82
|
+
// Check keywords
|
|
83
|
+
for (const kw of keywords) {
|
|
84
|
+
const kwLower = kw.toLowerCase();
|
|
85
|
+
if (!textLower.includes(kwLower)) continue;
|
|
86
|
+
|
|
87
|
+
allMatched.add(kw);
|
|
88
|
+
srcKeywords.push(kw);
|
|
89
|
+
|
|
90
|
+
// Check if keyword appears in a heading line
|
|
91
|
+
const inHeading = lines.some(line =>
|
|
92
|
+
/^#{1,6}\s/.test(line) && line.toLowerCase().includes(kwLower)
|
|
93
|
+
);
|
|
94
|
+
srcScore += (inHeading ? MATCH_WEIGHTS.heading : MATCH_WEIGHTS.body) * src.weight;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check domains
|
|
98
|
+
for (const domain of domains) {
|
|
99
|
+
const domLower = domain.toLowerCase();
|
|
100
|
+
if (!textLower.includes(domLower)) continue;
|
|
101
|
+
|
|
102
|
+
allMatched.add(domain);
|
|
103
|
+
srcKeywords.push(domain);
|
|
104
|
+
srcScore += MATCH_WEIGHTS.domain * src.weight;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (srcScore > 0) {
|
|
108
|
+
sourceBreakdown.push({
|
|
109
|
+
source: src.source,
|
|
110
|
+
keywords: srcKeywords,
|
|
111
|
+
score: srcScore,
|
|
112
|
+
});
|
|
113
|
+
totalScore += srcScore;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (totalScore > 0) {
|
|
118
|
+
detected.push({
|
|
119
|
+
id: agentId,
|
|
120
|
+
reason: 'keyword',
|
|
121
|
+
keywords_matched: [...allMatched],
|
|
122
|
+
score: totalScore,
|
|
123
|
+
sources: sourceBreakdown,
|
|
124
|
+
});
|
|
125
|
+
} else {
|
|
126
|
+
skipped.push({
|
|
127
|
+
id: agentId,
|
|
128
|
+
reason: 'no_keyword_match',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { detected, skipped };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Refresh agent detection using all available phase outputs.
|
|
138
|
+
* Additive only — never removes existing agents.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} projectRoot - Absolute path
|
|
141
|
+
* @param {string} featureName - Feature name
|
|
142
|
+
* @returns {Promise<{added: Array, unchanged: number, total: number}>}
|
|
143
|
+
*/
|
|
144
|
+
export async function refreshAgents(projectRoot, featureName) {
|
|
145
|
+
const state = loadState(false);
|
|
146
|
+
const feature = state?.features?.[featureName];
|
|
147
|
+
if (!feature) {
|
|
148
|
+
return { added: [], unchanged: 0, total: 0 };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const currentAgents = new Set(feature.activeAgents || []);
|
|
152
|
+
|
|
153
|
+
const sources = collectPhaseOutputs(projectRoot, featureName);
|
|
154
|
+
if (sources.length === 0) {
|
|
155
|
+
return { added: [], unchanged: currentAgents.size, total: currentAgents.size };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let agentsJson;
|
|
159
|
+
try {
|
|
160
|
+
agentsJson = loadAgentsFile(AGENTS_JSON_PATH);
|
|
161
|
+
} catch {
|
|
162
|
+
return { added: [], unchanged: currentAgents.size, total: currentAgents.size };
|
|
163
|
+
}
|
|
164
|
+
const { detected } = detectAgents(sources, agentsJson);
|
|
165
|
+
|
|
166
|
+
const added = detected.filter(a => !currentAgents.has(a.id));
|
|
167
|
+
|
|
168
|
+
// Additive: add new agents to state
|
|
169
|
+
for (const agent of added) {
|
|
170
|
+
await addAgent(featureName, agent.id);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
added,
|
|
175
|
+
unchanged: currentAgents.size,
|
|
176
|
+
total: currentAgents.size + added.length,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Display detection diff after refresh.
|
|
182
|
+
*
|
|
183
|
+
* @param {{added: Array, unchanged: number, total: number}} result
|
|
184
|
+
* @param {string} fromPhase
|
|
185
|
+
* @param {string} toPhase
|
|
186
|
+
*/
|
|
187
|
+
export function displayDetectionDiff(result, fromPhase, toPhase) {
|
|
188
|
+
console.log(chalk.cyan(`\nAgent Detection Refresh (${fromPhase} \u2192 ${toPhase}):`));
|
|
189
|
+
|
|
190
|
+
for (const agent of result.added) {
|
|
191
|
+
const kws = agent.keywords_matched.join('", "');
|
|
192
|
+
console.log(chalk.green(` + ${agent.id.padEnd(24)} (keyword: "${kws}" | score: ${agent.score})`));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
console.log(chalk.gray(` = ${result.unchanged} agents unchanged`));
|
|
196
|
+
console.log(chalk.white(` Total: ${result.total} active agents\n`));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* CLI command: morph-spec detect-agents <feature>
|
|
201
|
+
*/
|
|
202
|
+
export async function detectAgentsCommand(featureName, options = {}) {
|
|
203
|
+
if (!featureName) {
|
|
204
|
+
console.error(chalk.red('Usage: morph-spec detect-agents <feature> [--proposal <path>] [--refresh] [--json] [--table]'));
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// --refresh mode: use all phase outputs and mutate state
|
|
209
|
+
if (options.refresh) {
|
|
210
|
+
const result = await refreshAgents(process.cwd(), featureName);
|
|
211
|
+
if (options.json) {
|
|
212
|
+
console.log(JSON.stringify(result, null, 2));
|
|
213
|
+
} else {
|
|
214
|
+
displayDetectionDiff(result, 'manual', 'refresh');
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Resolve proposal path
|
|
220
|
+
const proposalPath = options.proposal
|
|
221
|
+
|| join(process.cwd(), `.morph/features/${featureName}/0-proposal/proposal.md`);
|
|
222
|
+
|
|
223
|
+
if (!existsSync(proposalPath)) {
|
|
224
|
+
console.error(chalk.red(`Proposal not found: ${proposalPath}`));
|
|
225
|
+
console.error(chalk.gray('Create the proposal first or use --proposal <path> to specify a custom path.'));
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Load agents.json
|
|
230
|
+
const agentsPath = options.agentsJson || AGENTS_JSON_PATH;
|
|
231
|
+
let agentsJson;
|
|
232
|
+
try {
|
|
233
|
+
agentsJson = loadAgentsFile(agentsPath);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
console.error(chalk.red(err.message));
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const proposalText = readFileSync(proposalPath, 'utf8');
|
|
240
|
+
|
|
241
|
+
const result = detectAgents(proposalText, agentsJson, {
|
|
242
|
+
excludeIds: options.exclude ? options.exclude.split(',') : [],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const output = {
|
|
246
|
+
feature: featureName,
|
|
247
|
+
...result,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
if (options.table) {
|
|
251
|
+
// Human-readable table
|
|
252
|
+
console.log(chalk.bold(`\n Detected Agents \u2014 ${featureName}`));
|
|
253
|
+
console.log(' ' + '\u2500'.repeat(62));
|
|
254
|
+
|
|
255
|
+
if (result.detected.length > 0) {
|
|
256
|
+
console.log(` ${'ID'.padEnd(28)} ${'Reason'.padEnd(16)} Keywords`);
|
|
257
|
+
console.log(' ' + '\u2500'.repeat(62));
|
|
258
|
+
for (const agent of result.detected) {
|
|
259
|
+
const reason = agent.reason === 'active_phases: *' ? 'always' : agent.reason;
|
|
260
|
+
const kw = agent.keywords_matched.length > 0 ? agent.keywords_matched.join(', ') : '\u2014';
|
|
261
|
+
console.log(
|
|
262
|
+
` ${agent.id.padEnd(28)} ${chalk.cyan(reason.padEnd(16))} ${kw}`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
} else {
|
|
266
|
+
console.log(chalk.gray(' No agents detected.'));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
console.log(`\n Skipped: ${result.skipped.length} agents (no keyword match)`);
|
|
270
|
+
console.log();
|
|
271
|
+
} else {
|
|
272
|
+
// JSON output (default)
|
|
273
|
+
console.log(JSON.stringify(output, null, 2));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
@@ -21,6 +21,9 @@ import { join, dirname } from 'path';
|
|
|
21
21
|
import { fileURLToPath } from 'url';
|
|
22
22
|
import { loadState, stateExists } from '../../core/state/state-manager.js';
|
|
23
23
|
import { buildAgentBriefing, buildValidatorPrompt } from '../../lib/standards/digest-builder.js';
|
|
24
|
+
import { agentIdToSubagentType } from '../../utils/agent-id-mapper.js';
|
|
25
|
+
import { parseAndGroupTasks } from '../../lib/tasks/task-parser.js';
|
|
26
|
+
export { parseAndGroupTasks } from '../../lib/tasks/task-parser.js';
|
|
24
27
|
import chalk from 'chalk';
|
|
25
28
|
|
|
26
29
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -34,62 +37,6 @@ const PHASES_JSON_PATH = join(__dirname, '../../../framework/phases.json');
|
|
|
34
37
|
/** Phases where agent dispatch is meaningful */
|
|
35
38
|
const DISPATCHABLE_PHASES = ['design', 'tasks', 'implement'];
|
|
36
39
|
|
|
37
|
-
/**
|
|
38
|
-
* Domain → parallel group mapping.
|
|
39
|
-
* Agents inherit group from their first matching domain.
|
|
40
|
-
*/
|
|
41
|
-
const DOMAIN_TO_GROUP = {
|
|
42
|
-
'backend': 'backend',
|
|
43
|
-
'backend-data': 'backend',
|
|
44
|
-
'backend-jobs': 'backend',
|
|
45
|
-
'backend-events': 'backend',
|
|
46
|
-
'frontend': 'frontend',
|
|
47
|
-
'frontend-blazor': 'frontend',
|
|
48
|
-
'frontend-nextjs': 'frontend',
|
|
49
|
-
'frontend-css': 'frontend',
|
|
50
|
-
'frontend-design': 'frontend',
|
|
51
|
-
'infrastructure': 'infra',
|
|
52
|
-
'cloud-azure': 'infra',
|
|
53
|
-
'devops': 'infra',
|
|
54
|
-
'domain': 'domain-analysis',
|
|
55
|
-
'domain-modeling': 'domain-analysis',
|
|
56
|
-
'architecture': 'domain-analysis',
|
|
57
|
-
'ai-orchestration': 'domain-analysis',
|
|
58
|
-
'testing': 'tests',
|
|
59
|
-
'qa': 'tests',
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
/** Agent-level group overrides (higher priority than domain mapping) */
|
|
63
|
-
const AGENT_GROUP_OVERRIDES = {
|
|
64
|
-
'domain-architect': 'domain-analysis',
|
|
65
|
-
'standards-architect': 'domain-analysis',
|
|
66
|
-
'ai-system-architect': 'domain-analysis',
|
|
67
|
-
'popm-advisor': 'domain-analysis',
|
|
68
|
-
'dotnet-senior': 'backend',
|
|
69
|
-
'infra-architect': 'infra',
|
|
70
|
-
'ui-designer': 'frontend',
|
|
71
|
-
'testing-specialist': 'tests',
|
|
72
|
-
'code-analyzer': 'tests',
|
|
73
|
-
'load-testing-expert': 'tests',
|
|
74
|
-
'documentation-specialist': 'docs',
|
|
75
|
-
'troubleshooting-expert': 'support',
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Task category → parallel group mapping for implement-phase task splitting.
|
|
80
|
-
* Matches task categories defined in phase-tasks SKILL.md.
|
|
81
|
-
*/
|
|
82
|
-
const TASK_CATEGORY_TO_GROUP = {
|
|
83
|
-
'domain': 'backend',
|
|
84
|
-
'domain-bc': 'backend',
|
|
85
|
-
'infrastructure': 'backend',
|
|
86
|
-
'application': 'backend',
|
|
87
|
-
'presentation': 'frontend',
|
|
88
|
-
'tests': 'tests',
|
|
89
|
-
'infra': 'infra',
|
|
90
|
-
'docs': 'docs',
|
|
91
|
-
};
|
|
92
|
-
|
|
93
40
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
94
41
|
// Required Skills Injection
|
|
95
42
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -175,34 +122,24 @@ export function buildMcpBlock(phase, phasesData, opts = {}) {
|
|
|
175
122
|
|
|
176
123
|
/**
|
|
177
124
|
* Determine the parallel execution group for an agent.
|
|
178
|
-
*
|
|
125
|
+
* Reads from the `group` field in agents.json v4; falls back to 'backend'.
|
|
179
126
|
*
|
|
180
127
|
* @param {string} agentId
|
|
181
128
|
* @param {Object} agentData
|
|
182
129
|
* @returns {string} group name
|
|
183
130
|
*/
|
|
184
131
|
function getAgentGroup(agentId, agentData) {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const domains = agentData.domains || [];
|
|
188
|
-
for (const domain of domains) {
|
|
189
|
-
if (DOMAIN_TO_GROUP[domain]) return DOMAIN_TO_GROUP[domain];
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Tier-2 domain leaders default to backend if no domain matched
|
|
193
|
-
if (agentData.tier === 2) return 'backend';
|
|
194
|
-
return 'backend';
|
|
132
|
+
return agentData.group || 'backend';
|
|
195
133
|
}
|
|
196
134
|
|
|
197
135
|
/**
|
|
198
136
|
* Check whether an agent should be dispatched for the given phase.
|
|
199
137
|
*
|
|
200
|
-
* Rules
|
|
201
|
-
* 1. Tier-4 validators → never dispatch (
|
|
202
|
-
* 2.
|
|
203
|
-
* 3.
|
|
204
|
-
* 4.
|
|
205
|
-
* 5. Default: specialists without active_phases → implement only
|
|
138
|
+
* Rules:
|
|
139
|
+
* 1. Tier-4 validators → never dispatch (validate mode bypasses this)
|
|
140
|
+
* 2. `active_phases` includes '*' → all dispatchable phases
|
|
141
|
+
* 3. `active_phases` includes the phase → dispatch
|
|
142
|
+
* 4. Default (no active_phases): implement only
|
|
206
143
|
*
|
|
207
144
|
* @param {string} agentId
|
|
208
145
|
* @param {Object} agentData
|
|
@@ -210,72 +147,16 @@ function getAgentGroup(agentId, agentData) {
|
|
|
210
147
|
* @returns {boolean}
|
|
211
148
|
*/
|
|
212
149
|
function isRelevantForPhase(agentId, agentData, phase) {
|
|
213
|
-
// Tier-4 validators have no implementation tasks (validate mode bypasses this)
|
|
214
150
|
if (agentData.tier === 4) return false;
|
|
215
151
|
|
|
216
|
-
|
|
217
|
-
if (Array.isArray(
|
|
218
|
-
return
|
|
152
|
+
const phases = agentData.active_phases;
|
|
153
|
+
if (Array.isArray(phases)) {
|
|
154
|
+
return phases.includes('*') || phases.includes(phase);
|
|
219
155
|
}
|
|
220
156
|
|
|
221
|
-
// active_phases nested in relationships
|
|
222
|
-
const relPhases = agentData.relationships?.active_phases;
|
|
223
|
-
if (Array.isArray(relPhases)) {
|
|
224
|
-
return relPhases.includes(phase);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
// always_active agents: dispatch for all recognised phases
|
|
228
|
-
if (agentData.always_active && DISPATCHABLE_PHASES.includes(phase)) return true;
|
|
229
|
-
|
|
230
|
-
// Specialists without active_phases: implement phase only
|
|
231
157
|
return phase === 'implement';
|
|
232
158
|
}
|
|
233
159
|
|
|
234
|
-
/**
|
|
235
|
-
* Parse tasks.md and return a map of { parallelGroup → taskId[] }.
|
|
236
|
-
* Gracefully returns {} if the file doesn't exist or is unparseable.
|
|
237
|
-
*
|
|
238
|
-
* @param {string} tasksPath - Absolute path to tasks.md
|
|
239
|
-
* @returns {Object}
|
|
240
|
-
*/
|
|
241
|
-
export function parseAndGroupTasks(tasksPath) {
|
|
242
|
-
if (!existsSync(tasksPath)) return {};
|
|
243
|
-
|
|
244
|
-
try {
|
|
245
|
-
const content = readFileSync(tasksPath, 'utf8');
|
|
246
|
-
const groups = {};
|
|
247
|
-
|
|
248
|
-
// Match task headings: ### T001 — Title or ### T001: Title
|
|
249
|
-
const taskHeadingRe = /^###\s+(T\d{3,})\s*[—–:\-]\s*(.+)$/gm;
|
|
250
|
-
// Match category line within a task block: **category**: `domain` or - category: application
|
|
251
|
-
const categoryRe = /(?:\*\*category\*\*\s*:\s*`?([a-z-]+)`?|category:\s*([a-z-]+))/i;
|
|
252
|
-
|
|
253
|
-
let headingMatch;
|
|
254
|
-
const taskPositions = [];
|
|
255
|
-
|
|
256
|
-
while ((headingMatch = taskHeadingRe.exec(content)) !== null) {
|
|
257
|
-
taskPositions.push({ id: headingMatch[1], pos: headingMatch.index });
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
for (let i = 0; i < taskPositions.length; i++) {
|
|
261
|
-
const { id, pos } = taskPositions[i];
|
|
262
|
-
const end = i + 1 < taskPositions.length ? taskPositions[i + 1].pos : content.length;
|
|
263
|
-
const block = content.slice(pos, end);
|
|
264
|
-
|
|
265
|
-
const catMatch = categoryRe.exec(block);
|
|
266
|
-
const category = (catMatch?.[1] || catMatch?.[2] || 'application').toLowerCase();
|
|
267
|
-
const group = TASK_CATEGORY_TO_GROUP[category] || 'backend';
|
|
268
|
-
|
|
269
|
-
if (!groups[group]) groups[group] = [];
|
|
270
|
-
groups[group].push(id);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return groups;
|
|
274
|
-
} catch {
|
|
275
|
-
return {};
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
160
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
280
161
|
// Core Logic
|
|
281
162
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -345,16 +226,15 @@ export async function buildDispatchConfig(projectPath, featureName, phase, opts
|
|
|
345
226
|
const activeAgentIds = new Set(feature.activeAgents || []);
|
|
346
227
|
|
|
347
228
|
// Collect dispatchable agents
|
|
348
|
-
// Process all agents: active ones from state + always_active ones not yet in state
|
|
349
229
|
const isValidateMode = opts.mode === 'validate';
|
|
350
230
|
const processed = new Set();
|
|
351
231
|
const dispatchableAgents = [];
|
|
352
232
|
|
|
353
233
|
const candidateIds = [
|
|
354
234
|
...activeAgentIds,
|
|
355
|
-
// Add
|
|
235
|
+
// Add always-active agents (active_phases includes '*')
|
|
356
236
|
...Object.entries(allAgents)
|
|
357
|
-
.filter(([id, data]) =>
|
|
237
|
+
.filter(([id, data]) => Array.isArray(data.active_phases) && data.active_phases.includes('*'))
|
|
358
238
|
.map(([id]) => id),
|
|
359
239
|
// In validate mode, also include tier-4 validators
|
|
360
240
|
...(isValidateMode
|
|
@@ -402,6 +282,7 @@ export async function buildDispatchConfig(projectPath, featureName, phase, opts
|
|
|
402
282
|
|
|
403
283
|
dispatchableAgents.push({
|
|
404
284
|
id: agentId,
|
|
285
|
+
subagentType: agentIdToSubagentType(agentId, agentData.tier),
|
|
405
286
|
title: agentData.title,
|
|
406
287
|
tier: agentData.tier,
|
|
407
288
|
icon: agentData.teammate?.icon || '🤖',
|
|
@@ -452,6 +333,7 @@ export async function buildDispatchConfig(projectPath, featureName, phase, opts
|
|
|
452
333
|
const groupAgent = dispatchableAgents.find(a => a.parallelGroup === group);
|
|
453
334
|
taskGroups[group] = {
|
|
454
335
|
agentId: groupAgent?.id ?? null,
|
|
336
|
+
subagentType: groupAgent?.subagentType ?? null,
|
|
455
337
|
description: `${group} implementation tasks for feature '${featureName}'`,
|
|
456
338
|
tasks: taskIds,
|
|
457
339
|
canRunParallel: true,
|
|
@@ -467,6 +349,7 @@ export async function buildDispatchConfig(projectPath, featureName, phase, opts
|
|
|
467
349
|
|
|
468
350
|
taskGroups[agent.parallelGroup] = {
|
|
469
351
|
agentId: agent.id,
|
|
352
|
+
subagentType: agent.subagentType,
|
|
470
353
|
description: agent.role,
|
|
471
354
|
tasks: [],
|
|
472
355
|
canRunParallel: true,
|
|
@@ -475,6 +358,19 @@ export async function buildDispatchConfig(projectPath, featureName, phase, opts
|
|
|
475
358
|
}
|
|
476
359
|
}
|
|
477
360
|
|
|
361
|
+
// ── Pre-flight: check agent files exist ──────────────────────────────────
|
|
362
|
+
const warnings = [];
|
|
363
|
+
for (const agent of dispatchableAgents) {
|
|
364
|
+
const agentFile = join(projectPath, '.claude', 'agents', `${agent.subagentType}.md`);
|
|
365
|
+
if (!existsSync(agentFile)) {
|
|
366
|
+
warnings.push({
|
|
367
|
+
agentId: agent.id,
|
|
368
|
+
subagentType: agent.subagentType,
|
|
369
|
+
message: `File .claude/agents/${agent.subagentType}.md not found`,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
478
374
|
return {
|
|
479
375
|
feature: featureName,
|
|
480
376
|
phase,
|
|
@@ -487,6 +383,7 @@ export async function buildDispatchConfig(projectPath, featureName, phase, opts
|
|
|
487
383
|
: `No dispatchable agents found for phase '${phase}'`,
|
|
488
384
|
agents: dispatchableAgents,
|
|
489
385
|
taskGroups,
|
|
386
|
+
warnings,
|
|
490
387
|
};
|
|
491
388
|
}
|
|
492
389
|
|
|
@@ -563,6 +460,15 @@ export async function dispatchAgentsCommand(featureName, phase, options = {}) {
|
|
|
563
460
|
// JSON (default — consumed by phase skills / LLM)
|
|
564
461
|
console.log(JSON.stringify(config, null, 2));
|
|
565
462
|
}
|
|
463
|
+
|
|
464
|
+
// Pre-flight mode: exit with code 1 if warnings exist
|
|
465
|
+
if (options.preflight && config.warnings?.length > 0) {
|
|
466
|
+
console.error(chalk.yellow(`\n ⚠ Pre-flight: ${config.warnings.length} missing agent file(s):`));
|
|
467
|
+
for (const w of config.warnings) {
|
|
468
|
+
console.error(chalk.yellow(` - ${w.agentId}: ${w.message}`));
|
|
469
|
+
}
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
566
472
|
} catch (err) {
|
|
567
473
|
console.error(chalk.red(`Error: ${err.message}`));
|
|
568
474
|
if (options.verbose) console.error(err.stack);
|
|
@@ -2,3 +2,12 @@
|
|
|
2
2
|
* Agent Commands
|
|
3
3
|
*/
|
|
4
4
|
export { dispatchAgentsCommand, buildDispatchConfig } from './dispatch-agents.js';
|
|
5
|
+
export { auditDispatchCommand } from './audit-dispatch.js';
|
|
6
|
+
export { detectAgentsCommand, detectAgents } from './detect-agents.js';
|
|
7
|
+
export {
|
|
8
|
+
agentListCommand,
|
|
9
|
+
agentShowCommand,
|
|
10
|
+
agentAddCommand,
|
|
11
|
+
agentEditCommand,
|
|
12
|
+
agentRemoveCommand,
|
|
13
|
+
} from './manage-agents.js';
|