@polymorphism-tech/morph-spec 4.21.0 → 5.0.1
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 +57 -71
- package/README.md +2 -2
- package/bin/morph-spec.js +224 -491
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +55 -38
- package/framework/MORPH.md +244 -0
- package/framework/agents.json +127 -1183
- package/framework/commands/morph-apply.md +56 -240
- package/framework/commands/morph-archive.md +48 -85
- package/framework/commands/morph-preflight.md +7 -7
- package/framework/commands/morph-proposal.md +35 -191
- package/framework/commands/morph-status.md +19 -50
- package/framework/commands/morph-troubleshoot.md +2 -2
- package/framework/evals/rubrics/README.md +44 -0
- package/framework/evals/rubrics/architecture.md +46 -0
- package/framework/evals/rubrics/code-quality.md +50 -0
- package/framework/evals/rubrics/contracts.md +47 -0
- package/framework/evals/rubrics/test-coverage.md +50 -0
- package/framework/evals/scenarios/dotnet-crud.yaml +42 -0
- package/framework/evals/scenarios/nextjs-auth.yaml +38 -0
- package/framework/evals/suites/standard.yaml +5 -0
- package/framework/evals/trace-schema.md +67 -0
- package/framework/hooks/claude-code/core/README.md +3 -0
- package/framework/hooks/claude-code/core/denylist.json +14 -0
- package/framework/hooks/claude-code/{pre-tool-use → core}/enforce-phase-writes.js +5 -5
- package/framework/hooks/claude-code/core/post-edit-typecheck.js +69 -0
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +29 -0
- package/framework/hooks/claude-code/{pre-tool-use → core}/protect-spec-files.js +4 -6
- package/framework/hooks/claude-code/{stop → core}/validate-completion.js +13 -82
- package/framework/hooks/shared/phase-utils.js +20 -28
- package/framework/hooks/shared/state-reader.js +4 -5
- package/framework/{skills/level-0-meta/mcp-registry.json → mcp/registry.json} +254 -210
- package/framework/phases.json +40 -114
- package/framework/rubrics/eval-rubric.json +66 -0
- package/framework/skills/README.md +39 -64
- package/framework/skills/core/README.md +3 -0
- package/framework/skills/core/morph-brainstorming/SKILL.md +102 -0
- package/framework/skills/core/morph-eval/SKILL.md +57 -0
- package/framework/skills/core/morph-implement/SKILL.md +83 -0
- package/framework/skills/core/morph-plan/SKILL.md +64 -0
- package/framework/skills/core/morph-review/SKILL.md +62 -0
- package/framework/skills/core/morph-stack-scan/SKILL.md +109 -0
- package/framework/skills/core/morph-standards/SKILL.md +49 -0
- package/framework/standards/STANDARDS.json +32 -0
- package/framework/standards/frontend/design-system/ai-image-generation.md +244 -0
- package/framework/standards/integration/mcp/mcp-tools.md +48 -0
- package/framework/templates/CLAUDE.md.core.guide.md +29 -0
- package/framework/templates/CLAUDE.md.core.template +22 -0
- package/framework/templates/REGISTRY.json +4 -4
- package/framework/workflows/configs/fast-track.json +5 -10
- package/framework/workflows/configs/full.json +4 -8
- package/framework/workflows/configs/quick.json +32 -0
- package/framework/workflows/configs/standard.json +57 -64
- package/package.json +7 -6
- package/src/commands/mcp/mcp-setup.js +58 -31
- package/src/commands/project/doctor.js +69 -70
- package/src/commands/project/init.js +26 -5
- package/src/commands/project/status.js +18 -19
- package/src/commands/state/approve.js +1 -1
- package/src/commands/validation/validate-feature.js +18 -112
- package/src/core/paths/output-schema.js +133 -151
- package/src/core/state/phase-state-machine.js +10 -24
- package/src/core/state/state-manager.js +17 -17
- package/src/core/workflows/workflow-detector.js +214 -31
- package/src/core/workflows/workflow-router.js +204 -0
- package/src/dashboard/app/pages/AgentMap.jsx +32 -33
- package/src/dashboard/app/pages/LiveState.jsx +1 -1
- package/src/dashboard/app/pages/PhasePipeline.jsx +2 -4
- package/src/dashboard/app/pages/SkillsLibrary.jsx +2 -3
- package/src/dashboard/app/pages/Workflows.jsx +2 -2
- package/src/dashboard/scanner/agents-scanner.js +30 -35
- package/src/lib/agents/metrics-aggregator.js +1 -1
- package/src/lib/agents/output-collector.js +7 -8
- package/src/lib/context/context-builder.js +289 -0
- package/src/lib/detectors/claude-config-detector.js +19 -3
- package/src/lib/generators/context-generator.js +20 -42
- package/src/lib/generators/index.js +1 -0
- package/src/lib/generators/mandate-generator.js +512 -0
- package/src/lib/generators/metadata-extractor.js +1 -1
- package/src/lib/generators/recap-generator.js +3 -4
- package/src/lib/installers/mcp-installer.js +300 -92
- package/src/lib/phase-chain/phase-validator.js +36 -40
- package/src/lib/stack/stack-profile.js +74 -91
- package/src/lib/tasks/task-parser.js +1 -1
- package/src/lib/tasks/wave-resolver.js +353 -0
- package/src/lib/validators/checkpoint-persistence.js +2 -2
- package/src/lib/validators/content/cross-phase-validator.js +281 -0
- package/src/lib/validators/content/index.js +1 -0
- package/src/lib/validators/shared/emit-validator-dispatch.js +6 -56
- package/src/scripts/global-install.js +2 -27
- package/src/utils/agents-installer.js +224 -447
- package/src/utils/claude-settings-manager.js +57 -15
- package/src/utils/file-copier.js +1 -0
- package/src/utils/hooks-installer.js +15 -117
- package/src/utils/skills-installer.js +12 -18
- package/bin/detect-agents.js +0 -226
- package/bin/task-manager.js +0 -1036
- package/framework/agents/README.md +0 -13
- package/framework/agents/ai-agents/ai-system-architect.md +0 -194
- package/framework/agents/ai-agents/vector-search-expert.md +0 -247
- package/framework/agents/architecture/architecture-expert.md +0 -98
- package/framework/agents/architecture/morph-spec-architect.md +0 -66
- package/framework/agents/architecture/packages-validator.md +0 -89
- package/framework/agents/architecture/security-expert.md +0 -79
- package/framework/agents/architecture/seo-growth-hacker.md +0 -168
- package/framework/agents/architecture/standards-architect.md +0 -153
- package/framework/agents/backend/dotnet-senior.md +0 -163
- package/framework/agents/backend/ef-modeler.md +0 -119
- package/framework/agents/backend/hangfire-orchestrator.md +0 -133
- package/framework/agents/backend/ms-agent-expert.md +0 -55
- package/framework/agents/backend/neon-expert.md +0 -379
- package/framework/agents/frontend/css-specialist.md +0 -89
- package/framework/agents/frontend/design-system-validator.md +0 -100
- package/framework/agents/frontend/gsap-animator.md +0 -131
- package/framework/agents/frontend/nextjs-component-validator.md +0 -97
- package/framework/agents/frontend/nextjs-expert.md +0 -201
- package/framework/agents/frontend/ui-designer.md +0 -259
- package/framework/agents/infrastructure/infra-architect.md +0 -215
- package/framework/agents/integrations/asaas-financial.md +0 -138
- package/framework/agents/integrations/neon-auth.md +0 -151
- package/framework/agents/integrations/resend-email.md +0 -127
- package/framework/hooks/claude-code/post-tool-use/agent-complete.js +0 -122
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +0 -363
- package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +0 -78
- package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +0 -194
- package/framework/hooks/claude-code/pre-compact/save-morph-context.js +0 -199
- package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +0 -55
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +0 -323
- package/framework/hooks/claude-code/session-start/post-compact-restore.js +0 -41
- package/framework/hooks/claude-code/session-start/resume-context.js +0 -77
- package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +0 -90
- package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +0 -114
- package/framework/hooks/shared/compact-restore.js +0 -123
- package/framework/hooks/shared/dispatch-helpers.js +0 -147
- package/framework/hooks/shared/skill-reminder-helpers.js +0 -79
- package/framework/hooks/shared/stale-task-reset.js +0 -57
- package/framework/hooks/shared/worktree-helpers.js +0 -75
- package/framework/skills/level-0-meta/README.md +0 -7
- package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +0 -472
- package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +0 -174
- package/framework/skills/level-0-meta/morph-brainstorming/references/proposal-example.md +0 -138
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +0 -145
- package/framework/skills/level-0-meta/morph-code-review/SKILL.md +0 -250
- package/framework/skills/level-0-meta/morph-code-review/references/review-example.md +0 -164
- package/framework/skills/level-0-meta/morph-code-review/references/review-guidelines.md +0 -100
- package/framework/skills/level-0-meta/morph-code-review/scripts/scan-csharp.mjs +0 -151
- package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +0 -165
- package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +0 -254
- package/framework/skills/level-0-meta/morph-code-review-nextjs/scripts/scan-nextjs.mjs +0 -189
- package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +0 -344
- package/framework/skills/level-0-meta/morph-frontend-review/scripts/scan-accessibility.mjs +0 -376
- package/framework/skills/level-0-meta/morph-init/SKILL.md +0 -537
- package/framework/skills/level-0-meta/morph-parallel-dispatch/SKILL.md +0 -225
- package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +0 -382
- package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-dev-server.mjs +0 -153
- package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-stack.mjs +0 -234
- package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +0 -106
- package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +0 -243
- package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +0 -169
- package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +0 -148
- package/framework/skills/level-0-meta/morph-verification-before-completion/scripts/check-phase-outputs.mjs +0 -110
- package/framework/skills/level-1-workflows/README.md +0 -7
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +0 -269
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +0 -453
- package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +0 -44
- package/framework/skills/level-1-workflows/morph-phase-design/references/spec-authoring-guide.md +0 -55
- package/framework/skills/level-1-workflows/morph-phase-design/references/spec-example.md +0 -253
- package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +0 -493
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +0 -50
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +0 -46
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +0 -47
- package/framework/skills/level-1-workflows/morph-phase-implement/references/recap-example.md +0 -132
- package/framework/skills/level-1-workflows/morph-phase-implement/references/vsa-implementation-guide.md +0 -92
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +0 -257
- package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +0 -291
- package/framework/skills/level-1-workflows/morph-phase-tasks/references/task-planning-patterns.md +0 -143
- package/framework/skills/level-1-workflows/morph-phase-tasks/references/tasks-example.md +0 -404
- package/framework/skills/level-1-workflows/morph-phase-tasks/scripts/validate-tasks.mjs +0 -112
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +0 -788
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +0 -147
- package/framework/squad-templates/backend-only.json +0 -47
- package/framework/squad-templates/frontend-only.json +0 -56
- package/framework/squad-templates/full-stack.json +0 -74
- package/src/commands/agents/agent-metrics.js +0 -174
- package/src/commands/agents/audit-dispatch.js +0 -82
- package/src/commands/agents/detect-agents.js +0 -275
- package/src/commands/agents/dispatch-agents.js +0 -461
- package/src/commands/agents/index.js +0 -13
- package/src/commands/agents/manage-agents.js +0 -352
- package/src/commands/agents/project-metrics.js +0 -74
- package/src/commands/phase/phase-reset.js +0 -74
- package/src/commands/scope/escalate.js +0 -217
- package/src/commands/state/advance-phase.js +0 -600
- package/src/commands/state/phase-runner.js +0 -215
- package/src/commands/state/state.js +0 -444
- package/src/commands/task/expand.js +0 -100
- package/src/commands/tasks/index.js +0 -4
- package/src/commands/tasks/task.js +0 -105
- package/src/commands/templates/index.js +0 -5
- package/src/commands/templates/template-render.js +0 -93
- package/src/commands/trust/trust.js +0 -118
- package/src/lib/agents/agents-manager.js +0 -349
- package/src/lib/build/build-runner.js +0 -99
- package/src/lib/checkpoints/checkpoint-hooks.js +0 -111
- package/src/lib/contracts/sprint-contract-generator.js +0 -137
- package/src/lib/scope/impact-analyzer.js +0 -106
- package/src/lib/trust/trust-manager.js +0 -157
- package/src/utils/tier-helpers.js +0 -24
- /package/framework/hooks/claude-code/{post-tool-use → core}/loop-detect.js +0 -0
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* @module workflow-detector
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
13
|
-
import { join, dirname } from 'path';
|
|
12
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
13
|
+
import { join, dirname, basename } from 'path';
|
|
14
14
|
import { fileURLToPath } from 'url';
|
|
15
15
|
import { getPhaseSequence } from '../state/phase-state-machine.js';
|
|
16
16
|
|
|
@@ -221,6 +221,153 @@ function matchComplexity(complexity, criteria) {
|
|
|
221
221
|
return { matches: meetsAll, score };
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
// Directories to skip during codebase scanning
|
|
225
|
+
const SCAN_SKIP_DIRS = new Set([
|
|
226
|
+
'node_modules', '.git', '.morph', 'dist', 'out', '.next',
|
|
227
|
+
'obj', 'bin', '.worktrees', '.vs', '.vscode', 'coverage'
|
|
228
|
+
]);
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Quick codebase scan to estimate change complexity from actual files.
|
|
232
|
+
* Reads only filenames (not content). Capped at 2000 files.
|
|
233
|
+
*
|
|
234
|
+
* @param {string} projectPath - Project root
|
|
235
|
+
* @param {string[]} keywords - Keywords extracted from user request
|
|
236
|
+
* @returns {{ affectedFiles: number, hasSharedInfra: boolean, hasTests: boolean, score: number }}
|
|
237
|
+
*/
|
|
238
|
+
export function quickCodebaseScan(projectPath, keywords) {
|
|
239
|
+
if (!projectPath || projectPath === '.' || !existsSync(projectPath)) {
|
|
240
|
+
return { affectedFiles: 0, hasSharedInfra: false, hasTests: false, score: 0.5 };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const infraPatterns = ['dockerfile', 'docker-compose', '.bicep', '.tf', 'appsettings.json', 'terraform'];
|
|
244
|
+
const testPatterns = ['.test.', '.spec.', '__tests__'];
|
|
245
|
+
const keywordsLower = keywords.map(k => k.toLowerCase());
|
|
246
|
+
|
|
247
|
+
let filesExamined = 0;
|
|
248
|
+
let affectedFiles = 0;
|
|
249
|
+
let hasSharedInfra = false;
|
|
250
|
+
let hasTests = false;
|
|
251
|
+
const MAX_FILES = 2000;
|
|
252
|
+
|
|
253
|
+
function walk(dir) {
|
|
254
|
+
if (filesExamined >= MAX_FILES) return;
|
|
255
|
+
let entries;
|
|
256
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
257
|
+
|
|
258
|
+
for (const entry of entries) {
|
|
259
|
+
if (filesExamined >= MAX_FILES) return;
|
|
260
|
+
if (entry.isDirectory()) {
|
|
261
|
+
if (!SCAN_SKIP_DIRS.has(entry.name)) walk(join(dir, entry.name));
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
filesExamined++;
|
|
265
|
+
const nameLower = entry.name.toLowerCase();
|
|
266
|
+
|
|
267
|
+
if (infraPatterns.some(p => nameLower.includes(p))) hasSharedInfra = true;
|
|
268
|
+
if (testPatterns.some(p => nameLower.includes(p))) hasTests = true;
|
|
269
|
+
if (keywordsLower.some(k => k.length >= 3 && nameLower.includes(k))) affectedFiles++;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
walk(projectPath);
|
|
274
|
+
|
|
275
|
+
// Score: more affected files = higher complexity
|
|
276
|
+
let score = 0.5;
|
|
277
|
+
if (affectedFiles === 0) score = 0.3;
|
|
278
|
+
else if (affectedFiles <= 3) score = 0.4;
|
|
279
|
+
else if (affectedFiles <= 8) score = 0.6;
|
|
280
|
+
else if (affectedFiles <= 15) score = 0.75;
|
|
281
|
+
else score = 0.9;
|
|
282
|
+
|
|
283
|
+
if (hasSharedInfra) score = Math.min(1, score + 0.1);
|
|
284
|
+
|
|
285
|
+
return { affectedFiles, hasSharedInfra, hasTests, score };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Find completed features similar to the current description.
|
|
290
|
+
* Uses Jaccard similarity on tokenized keywords.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} userRequest - Current feature description
|
|
293
|
+
* @param {string} projectPath - Project root (for loading state)
|
|
294
|
+
* @returns {{ similarFeatures: Array<{name: string, workflow: string, similarity: number}>, suggestedWorkflow: string|null, score: number }}
|
|
295
|
+
*/
|
|
296
|
+
export function findSimilarFeatures(userRequest, projectPath) {
|
|
297
|
+
const neutral = { similarFeatures: [], suggestedWorkflow: null, score: 0.5 };
|
|
298
|
+
|
|
299
|
+
let state;
|
|
300
|
+
try {
|
|
301
|
+
const statePath = join(projectPath, '.morph', 'state.json');
|
|
302
|
+
if (!existsSync(statePath)) return neutral;
|
|
303
|
+
state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
304
|
+
} catch { return neutral; }
|
|
305
|
+
|
|
306
|
+
if (!state?.features) return neutral;
|
|
307
|
+
|
|
308
|
+
const requestTokens = new Set(
|
|
309
|
+
userRequest.toLowerCase().split(/[\s\-_.,;:!?]+/).filter(t => t.length >= 3)
|
|
310
|
+
);
|
|
311
|
+
if (requestTokens.size === 0) return neutral;
|
|
312
|
+
|
|
313
|
+
const candidates = [];
|
|
314
|
+
|
|
315
|
+
for (const [name, feature] of Object.entries(state.features)) {
|
|
316
|
+
if (feature.status !== 'completed') continue;
|
|
317
|
+
if (!feature.workflow || feature.workflow === 'auto') continue;
|
|
318
|
+
|
|
319
|
+
// Build token set from feature name + matched keywords
|
|
320
|
+
const featureTokens = new Set(
|
|
321
|
+
name.toLowerCase().split(/[\s\-_.,;:!?]+/).filter(t => t.length >= 3)
|
|
322
|
+
);
|
|
323
|
+
if (feature.workflowDetection?.matchedKeywords) {
|
|
324
|
+
for (const kw of feature.workflowDetection.matchedKeywords) {
|
|
325
|
+
kw.toLowerCase().split(/[\s\-_]+/).filter(t => t.length >= 3).forEach(t => featureTokens.add(t));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Jaccard similarity
|
|
330
|
+
const intersection = [...requestTokens].filter(t => featureTokens.has(t)).length;
|
|
331
|
+
const union = new Set([...requestTokens, ...featureTokens]).size;
|
|
332
|
+
const similarity = union > 0 ? intersection / union : 0;
|
|
333
|
+
|
|
334
|
+
if (similarity > 0.3) {
|
|
335
|
+
candidates.push({ name, workflow: feature.workflow, similarity });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (candidates.length === 0) return neutral;
|
|
340
|
+
|
|
341
|
+
candidates.sort((a, b) => b.similarity - a.similarity);
|
|
342
|
+
const top = candidates.slice(0, 3);
|
|
343
|
+
|
|
344
|
+
// Suggest the workflow used by the most similar feature
|
|
345
|
+
const suggestedWorkflow = top[0].workflow;
|
|
346
|
+
|
|
347
|
+
// Score: higher similarity = more confidence in the suggestion
|
|
348
|
+
const score = Math.min(1, top[0].similarity + 0.2);
|
|
349
|
+
|
|
350
|
+
return { similarFeatures: top, suggestedWorkflow, score };
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Score based on description length and structure.
|
|
355
|
+
* Short descriptions suggest simple tasks, long ones suggest complex.
|
|
356
|
+
*
|
|
357
|
+
* @param {string} userRequest
|
|
358
|
+
* @returns {number} 0-1 score where higher = more complex
|
|
359
|
+
*/
|
|
360
|
+
export function scoreDescriptionLength(userRequest) {
|
|
361
|
+
const len = userRequest.trim().length;
|
|
362
|
+
const sentences = userRequest.split(/[.!?]+/).filter(s => s.trim().length > 0).length;
|
|
363
|
+
|
|
364
|
+
if (len < 30) return 0.2;
|
|
365
|
+
if (len < 80) return 0.35;
|
|
366
|
+
if (len < 150) return 0.5;
|
|
367
|
+
if (len < 300 && sentences <= 3) return 0.65;
|
|
368
|
+
return 0.8;
|
|
369
|
+
}
|
|
370
|
+
|
|
224
371
|
/**
|
|
225
372
|
* Detect workflow for a feature request
|
|
226
373
|
* @param {Object} options - Detection options
|
|
@@ -254,6 +401,14 @@ export async function detectWorkflow(options) {
|
|
|
254
401
|
// Estimate complexity
|
|
255
402
|
const complexity = estimateComplexity(userRequest);
|
|
256
403
|
|
|
404
|
+
// New signals: codebase scan + feature history + description length
|
|
405
|
+
const codebaseScan = quickCodebaseScan(projectPath, complexity.hasInfra
|
|
406
|
+
? [...userRequest.toLowerCase().split(/\s+/).filter(w => w.length >= 3)]
|
|
407
|
+
: userRequest.toLowerCase().split(/\s+/).filter(w => w.length >= 3)
|
|
408
|
+
);
|
|
409
|
+
const featureHistory = findSimilarFeatures(userRequest, projectPath);
|
|
410
|
+
const descriptionScore = scoreDescriptionLength(userRequest);
|
|
411
|
+
|
|
257
412
|
// Score each workflow
|
|
258
413
|
const scores = [];
|
|
259
414
|
|
|
@@ -261,30 +416,53 @@ export async function detectWorkflow(options) {
|
|
|
261
416
|
const keywordMatch = matchKeywords(userRequest, workflow.keywords || []);
|
|
262
417
|
const complexityMatch = matchComplexity(complexity, workflow.criteria || {});
|
|
263
418
|
|
|
264
|
-
//
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
419
|
+
// Enhanced weighted scoring (6 signals)
|
|
420
|
+
const weights = {
|
|
421
|
+
keywords: 0.25,
|
|
422
|
+
complexity: 0.20,
|
|
423
|
+
codebaseAnalysis: 0.20,
|
|
424
|
+
featureHistory: 0.15,
|
|
425
|
+
priority: 0.10,
|
|
426
|
+
descriptionLength: 0.10,
|
|
427
|
+
};
|
|
269
428
|
|
|
270
429
|
// Priority score (lower number = higher priority = higher score)
|
|
271
430
|
const priorityScore = workflow.priority ? (4 - workflow.priority) / 3 : 0.5;
|
|
272
431
|
|
|
273
|
-
//
|
|
274
|
-
let
|
|
275
|
-
if (workflow.criteria?.
|
|
276
|
-
|
|
432
|
+
// Codebase analysis score: align scan results with workflow expectations
|
|
433
|
+
let codebaseScore = codebaseScan.score;
|
|
434
|
+
if (workflow.criteria?.noInfrastructure && codebaseScan.hasSharedInfra) {
|
|
435
|
+
codebaseScore *= 0.5; // Penalize simple workflows when infra detected
|
|
436
|
+
}
|
|
437
|
+
if (workflow.criteria?.minFiles && codebaseScan.affectedFiles >= workflow.criteria.minFiles) {
|
|
438
|
+
codebaseScore = Math.min(1, codebaseScore + 0.2);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Feature history score: boost if similar features used this workflow
|
|
442
|
+
let historyScore = featureHistory.score;
|
|
443
|
+
if (featureHistory.suggestedWorkflow === workflow.id) {
|
|
444
|
+
historyScore = Math.min(1, historyScore + 0.3);
|
|
445
|
+
} else if (featureHistory.suggestedWorkflow && featureHistory.suggestedWorkflow !== workflow.id) {
|
|
446
|
+
historyScore *= 0.6;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Description length score: align with workflow complexity expectations
|
|
450
|
+
let descScore = descriptionScore;
|
|
451
|
+
if (workflow.priority === 1 && descriptionScore > 0.6) {
|
|
452
|
+
descScore *= 0.7; // Long descriptions less likely to be fast-track
|
|
277
453
|
}
|
|
278
|
-
if (workflow.
|
|
279
|
-
|
|
454
|
+
if (workflow.priority === 3 && descriptionScore < 0.4) {
|
|
455
|
+
descScore *= 0.7; // Short descriptions less likely to be full
|
|
280
456
|
}
|
|
281
457
|
|
|
282
458
|
// Calculate total score
|
|
283
459
|
let totalScore =
|
|
284
|
-
(keywordMatch.score *
|
|
285
|
-
(complexityMatch.score *
|
|
286
|
-
(
|
|
287
|
-
(
|
|
460
|
+
(keywordMatch.score * weights.keywords) +
|
|
461
|
+
(complexityMatch.score * weights.complexity) +
|
|
462
|
+
(codebaseScore * weights.codebaseAnalysis) +
|
|
463
|
+
(historyScore * weights.featureHistory) +
|
|
464
|
+
(priorityScore * weights.priority) +
|
|
465
|
+
(descScore * weights.descriptionLength);
|
|
288
466
|
|
|
289
467
|
// Penalty for workflows with no keyword matches
|
|
290
468
|
if (keywordMatch.matched.length === 0) {
|
|
@@ -335,13 +513,26 @@ export async function detectWorkflow(options) {
|
|
|
335
513
|
reason: `${w.workflowName} (${Math.round(w.score * 100)}% match)`
|
|
336
514
|
}));
|
|
337
515
|
|
|
516
|
+
// Warnings for low confidence or ambiguous results
|
|
517
|
+
const warnings = [];
|
|
518
|
+
if (confidence < 0.4) {
|
|
519
|
+
warnings.push('Low confidence — consider specifying workflow explicitly with --workflow');
|
|
520
|
+
}
|
|
521
|
+
if (alternatives.length > 0 && alternatives[0].confidence > confidence * 0.8) {
|
|
522
|
+
warnings.push(`Close alternative: ${alternatives[0].workflowId} (${Math.round(alternatives[0].confidence * 100)}%) — verify recommendation`);
|
|
523
|
+
}
|
|
524
|
+
if (featureHistory.similarFeatures.length > 0 && featureHistory.suggestedWorkflow !== topResult.workflowId) {
|
|
525
|
+
warnings.push(`Similar features used ${featureHistory.suggestedWorkflow} workflow`);
|
|
526
|
+
}
|
|
527
|
+
|
|
338
528
|
return {
|
|
339
529
|
workflowId: topResult.workflowId,
|
|
340
530
|
confidence,
|
|
341
531
|
matchedKeywords: topResult.keywordMatch,
|
|
342
532
|
estimatedComplexity: complexity,
|
|
343
533
|
reasoning: reasoning.join(' + '),
|
|
344
|
-
alternativeWorkflows: alternatives
|
|
534
|
+
alternativeWorkflows: alternatives,
|
|
535
|
+
warnings
|
|
345
536
|
};
|
|
346
537
|
}
|
|
347
538
|
|
|
@@ -387,17 +578,9 @@ export function listWorkflows(projectPath = '.') {
|
|
|
387
578
|
* @param {string} feature
|
|
388
579
|
* @returns {Promise<boolean>}
|
|
389
580
|
*/
|
|
390
|
-
export async function checkZeroTouchEligibility(feature) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
if (!getTrust) return false;
|
|
397
|
-
|
|
398
|
-
const trust = getTrust(feature);
|
|
399
|
-
return trust.level === 'auto';
|
|
400
|
-
} catch {
|
|
401
|
-
return false;
|
|
402
|
-
}
|
|
581
|
+
export async function checkZeroTouchEligibility(/* feature */) {
|
|
582
|
+
// v5: trust manager removed. The autonomy contract (MORPH.md) replaces it —
|
|
583
|
+
// Claude advances autonomously between gates. Returning false keeps any
|
|
584
|
+
// legacy callers happy without granting unintended privileges.
|
|
585
|
+
return false;
|
|
403
586
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Natural Language Workflow Router
|
|
3
|
+
*
|
|
4
|
+
* Classifies free-text input into morph-spec intents and routes to the
|
|
5
|
+
* appropriate command. Uses pattern matching for intent detection and
|
|
6
|
+
* delegates to detectWorkflow() for proposal classification.
|
|
7
|
+
*
|
|
8
|
+
* @module workflow-router
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { detectWorkflow } from './workflow-detector.js';
|
|
12
|
+
import { readFileSync, existsSync } from 'fs';
|
|
13
|
+
import { join } from 'path';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} RoutedIntent
|
|
17
|
+
* @property {string} intent - 'proposal' | 'status' | 'approve' | 'fast-track'
|
|
18
|
+
* @property {string} command - The morph-spec command to execute
|
|
19
|
+
* @property {string[]} args - Arguments for the command
|
|
20
|
+
* @property {number} confidence - 0-1
|
|
21
|
+
* @property {string} explanation - Human-readable reason for routing decision
|
|
22
|
+
* @property {Object} [workflowDetection] - If intent is proposal/fast-track, includes detection result
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const INTENT_PATTERNS = [
|
|
26
|
+
// Status queries (highest priority)
|
|
27
|
+
{ pattern: /^(status|progress|where\s+am\s+i)\b/i, intent: 'status', confidence: 0.9 },
|
|
28
|
+
{ pattern: /what('s|\s+is)\s+(next|done|left|remaining)/i, intent: 'status', confidence: 0.85 },
|
|
29
|
+
{ pattern: /what\s+tasks/i, intent: 'status', confidence: 0.85 },
|
|
30
|
+
{ pattern: /tasks?\s+(left|remaining|pending|todo)/i, intent: 'status', confidence: 0.85 },
|
|
31
|
+
{ pattern: /^show\s+(me\s+)?status/i, intent: 'status', confidence: 0.9 },
|
|
32
|
+
|
|
33
|
+
// Approval actions
|
|
34
|
+
{ pattern: /^(approve|lgtm|looks?\s+good|ship\s+it)\b/i, intent: 'approve', confidence: 0.85 },
|
|
35
|
+
|
|
36
|
+
// Explicit simple/fix intents → fast-track proposal
|
|
37
|
+
{ pattern: /^(fix|typo|rename|toggle|bump|config)\b/i, intent: 'fast-track', confidence: 0.75 },
|
|
38
|
+
{ pattern: /\b(typo|spelling|label|icon|badge|tooltip|css\s+fix)\b/i, intent: 'fast-track', confidence: 0.7 },
|
|
39
|
+
|
|
40
|
+
// Everything else → proposal (further classified by detectWorkflow)
|
|
41
|
+
{ pattern: /.+/, intent: 'proposal', confidence: 0.5 },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Route natural language to the best morph-spec command.
|
|
46
|
+
*
|
|
47
|
+
* @param {string} input - User's natural language description
|
|
48
|
+
* @param {Object} [options]
|
|
49
|
+
* @param {string} [options.projectPath='.'] - Project path for codebase analysis
|
|
50
|
+
* @param {string} [options.activeFeature] - Currently active feature name (for status/approve)
|
|
51
|
+
* @returns {Promise<RoutedIntent>}
|
|
52
|
+
*/
|
|
53
|
+
export async function routeIntent(input, options = {}) {
|
|
54
|
+
const { projectPath = '.' } = options;
|
|
55
|
+
const trimmed = (input || '').trim();
|
|
56
|
+
|
|
57
|
+
if (!trimmed) {
|
|
58
|
+
return {
|
|
59
|
+
intent: 'proposal',
|
|
60
|
+
command: 'morph-spec',
|
|
61
|
+
args: ['--help'],
|
|
62
|
+
confidence: 0,
|
|
63
|
+
explanation: 'Empty input — showing help'
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Match against patterns in order
|
|
68
|
+
let matchedIntent = null;
|
|
69
|
+
for (const { pattern, intent, confidence } of INTENT_PATTERNS) {
|
|
70
|
+
if (pattern.test(trimmed)) {
|
|
71
|
+
matchedIntent = { intent, confidence };
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const activeFeature = options.activeFeature || findActiveFeature(projectPath);
|
|
77
|
+
|
|
78
|
+
// Route based on intent
|
|
79
|
+
switch (matchedIntent.intent) {
|
|
80
|
+
case 'status': {
|
|
81
|
+
const feature = activeFeature || '<feature>';
|
|
82
|
+
return {
|
|
83
|
+
intent: 'status',
|
|
84
|
+
command: 'morph-spec',
|
|
85
|
+
args: ['status', feature],
|
|
86
|
+
confidence: activeFeature ? matchedIntent.confidence : matchedIntent.confidence * 0.6,
|
|
87
|
+
explanation: activeFeature
|
|
88
|
+
? `Status query for active feature "${feature}"`
|
|
89
|
+
: 'Status query — no active feature found'
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
case 'approve': {
|
|
94
|
+
const feature = activeFeature || '<feature>';
|
|
95
|
+
return {
|
|
96
|
+
intent: 'approve',
|
|
97
|
+
command: 'morph-spec',
|
|
98
|
+
args: ['approve', feature],
|
|
99
|
+
confidence: activeFeature ? matchedIntent.confidence : matchedIntent.confidence * 0.5,
|
|
100
|
+
explanation: activeFeature
|
|
101
|
+
? `Approve active feature "${feature}"`
|
|
102
|
+
: 'Approve — no active feature found'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
case 'fast-track': {
|
|
107
|
+
// Validate with detectWorkflow — regex might be too eager
|
|
108
|
+
const detection = await detectWorkflow({
|
|
109
|
+
userRequest: trimmed,
|
|
110
|
+
projectPath,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// If detectWorkflow disagrees, defer to it
|
|
114
|
+
if (detection.workflowId !== 'fast-track' && detection.confidence > matchedIntent.confidence) {
|
|
115
|
+
const slug = slugify(trimmed);
|
|
116
|
+
return {
|
|
117
|
+
intent: 'proposal',
|
|
118
|
+
command: 'morph-spec',
|
|
119
|
+
args: ['state', 'init', slug, '--user-request', trimmed],
|
|
120
|
+
confidence: detection.confidence,
|
|
121
|
+
explanation: `Detected as ${detection.workflowId} (${detection.reasoning})`,
|
|
122
|
+
workflowDetection: detection
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const slug = slugify(trimmed);
|
|
127
|
+
return {
|
|
128
|
+
intent: 'fast-track',
|
|
129
|
+
command: 'morph-spec',
|
|
130
|
+
args: ['state', 'init', slug, '--workflow', 'fast-track', '--user-request', trimmed],
|
|
131
|
+
confidence: Math.max(matchedIntent.confidence, detection.confidence),
|
|
132
|
+
explanation: `Simple task → fast-track workflow (${detection.reasoning})`,
|
|
133
|
+
workflowDetection: detection
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case 'proposal':
|
|
138
|
+
default: {
|
|
139
|
+
const detection = await detectWorkflow({
|
|
140
|
+
userRequest: trimmed,
|
|
141
|
+
projectPath,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const slug = slugify(trimmed);
|
|
145
|
+
const args = ['state', 'init', slug, '--user-request', trimmed];
|
|
146
|
+
if (detection.workflowId !== 'auto') {
|
|
147
|
+
args.push('--workflow', detection.workflowId);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
intent: 'proposal',
|
|
152
|
+
command: 'morph-spec',
|
|
153
|
+
args,
|
|
154
|
+
confidence: detection.confidence,
|
|
155
|
+
explanation: `New feature → ${detection.workflowId} workflow (${detection.reasoning})`,
|
|
156
|
+
workflowDetection: detection
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Find the currently active feature from state.json.
|
|
164
|
+
* Returns the most recently updated feature.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} [projectPath='.']
|
|
167
|
+
* @returns {string|null} Feature name or null
|
|
168
|
+
*/
|
|
169
|
+
export function findActiveFeature(projectPath = '.') {
|
|
170
|
+
try {
|
|
171
|
+
const statePath = join(projectPath, '.morph', 'state.json');
|
|
172
|
+
if (!existsSync(statePath)) return null;
|
|
173
|
+
|
|
174
|
+
const state = JSON.parse(readFileSync(statePath, 'utf8'));
|
|
175
|
+
if (!state?.features) return null;
|
|
176
|
+
|
|
177
|
+
const entries = Object.entries(state.features)
|
|
178
|
+
.sort((a, b) => {
|
|
179
|
+
const dateA = a[1].updatedAt || a[1].createdAt || '';
|
|
180
|
+
const dateB = b[1].updatedAt || b[1].createdAt || '';
|
|
181
|
+
return dateB.localeCompare(dateA);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return entries.length > 0 ? entries[0][0] : null;
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Convert a description into a feature slug.
|
|
192
|
+
* @param {string} text
|
|
193
|
+
* @returns {string}
|
|
194
|
+
*/
|
|
195
|
+
function slugify(text) {
|
|
196
|
+
return text
|
|
197
|
+
.toLowerCase()
|
|
198
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
199
|
+
.trim()
|
|
200
|
+
.replace(/\s+/g, '-')
|
|
201
|
+
.replace(/-+/g, '-')
|
|
202
|
+
.slice(0, 50)
|
|
203
|
+
.replace(/-$/, '');
|
|
204
|
+
}
|
|
@@ -7,18 +7,22 @@ import DetailPanel from '../components/DetailPanel.jsx';
|
|
|
7
7
|
import SearchFilter from '../components/SearchFilter.jsx';
|
|
8
8
|
import { useDetailPanel } from '../hooks/useDetailPanel.js';
|
|
9
9
|
|
|
10
|
-
//
|
|
11
|
-
const
|
|
12
|
-
user:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
// Domain persona color palette (v5 — flat, no tiers)
|
|
11
|
+
const PERSONA_COLORS = {
|
|
12
|
+
user: { bg: '#78350f', border: '#f59e0b', label: 'User' },
|
|
13
|
+
'dotnet-senior':{ bg: '#991b1b', border: '#ef4444', label: 'Backend' },
|
|
14
|
+
'ef-modeler': { bg: '#7c2d12', border: '#fb923c', label: 'Data' },
|
|
15
|
+
'maf-expert': { bg: '#1e3a5f', border: '#38bdf8', label: 'Agents' },
|
|
16
|
+
'nextjs-expert':{ bg: '#064e3b', border: '#34d399', label: 'Frontend' },
|
|
17
|
+
'ui-designer': { bg: '#4a1d96', border: '#a78bfa', label: 'Design' },
|
|
18
|
+
'infra-engineer':{ bg: '#1e3a5f', border: '#60a5fa', label: 'Infra' },
|
|
19
|
+
'evaluator': { bg: '#374151', border: '#9ca3af', label: 'Eval' },
|
|
17
20
|
};
|
|
21
|
+
const FALLBACK_PERSONA = { bg: '#1e40af', border: '#3b82f6', label: 'Persona' };
|
|
18
22
|
|
|
19
23
|
// Custom AgentNode component
|
|
20
24
|
function AgentNode({ data }) {
|
|
21
|
-
const colors = data.isUser ?
|
|
25
|
+
const colors = data.isUser ? PERSONA_COLORS.user : (PERSONA_COLORS[data.personaId] || FALLBACK_PERSONA);
|
|
22
26
|
return (
|
|
23
27
|
<div
|
|
24
28
|
className="rounded-lg px-3 py-2 min-w-[130px] max-w-[160px] cursor-pointer select-none"
|
|
@@ -29,7 +33,7 @@ function AgentNode({ data }) {
|
|
|
29
33
|
{data.label}
|
|
30
34
|
</div>
|
|
31
35
|
<div className="text-gray-300 text-[10px] mt-0.5 truncate">
|
|
32
|
-
{data.isUser ? 'Principal' : `${colors.label} · ${data.
|
|
36
|
+
{data.isUser ? 'Principal' : `${colors.label} · ${data.domains?.[0] || ''}`}
|
|
33
37
|
</div>
|
|
34
38
|
<Handle type="source" position={Position.Bottom} style={{ background: colors.border, width: 8, height: 8 }} />
|
|
35
39
|
</div>
|
|
@@ -58,22 +62,19 @@ function buildLayout(nodes, edges) {
|
|
|
58
62
|
// AgentDetail shown inside the panel
|
|
59
63
|
function AgentDetail({ agent }) {
|
|
60
64
|
const [showPrompt, setShowPrompt] = useState(false);
|
|
61
|
-
const
|
|
65
|
+
const personaColors = PERSONA_COLORS[agent.id] || FALLBACK_PERSONA;
|
|
62
66
|
|
|
63
67
|
return (
|
|
64
68
|
<div className="text-sm text-gray-200 space-y-4">
|
|
65
69
|
{/* Header */}
|
|
66
70
|
<div>
|
|
67
71
|
<div className="flex items-center gap-2 mb-1">
|
|
68
|
-
<span className="px-2 py-0.5 rounded text-xs font-bold" style={{ background:
|
|
69
|
-
|
|
72
|
+
<span className="px-2 py-0.5 rounded text-xs font-bold" style={{ background: personaColors.bg, color: personaColors.border, border: `1px solid ${personaColors.border}` }}>
|
|
73
|
+
{personaColors.label}
|
|
70
74
|
</span>
|
|
71
|
-
<span className="text-gray-400 text-xs">{agent.
|
|
75
|
+
<span className="text-gray-400 text-xs">{agent.domains?.[0] || ''}</span>
|
|
72
76
|
</div>
|
|
73
77
|
<p className="text-gray-400 text-xs">ID: <span className="text-gray-300 font-mono">{agent.id}</span></p>
|
|
74
|
-
{agent.reportsTo && (
|
|
75
|
-
<p className="text-gray-400 text-xs mt-0.5">Reports to: <span className="text-blue-300 font-mono">{agent.reportsTo}</span></p>
|
|
76
|
-
)}
|
|
77
78
|
</div>
|
|
78
79
|
|
|
79
80
|
{/* Active Phases */}
|
|
@@ -173,15 +174,15 @@ function AgentDetail({ agent }) {
|
|
|
173
174
|
export default function AgentMap() {
|
|
174
175
|
const { agents } = morphData;
|
|
175
176
|
const [search, setSearch] = useState('');
|
|
176
|
-
const [
|
|
177
|
+
const [domainFilter, setDomainFilter] = useState('');
|
|
177
178
|
const [groupFilter, setGroupFilter] = useState('');
|
|
178
179
|
const [phaseFilter, setPhaseFilter] = useState('');
|
|
179
180
|
const [stackFilter, setStackFilter] = useState('');
|
|
180
181
|
const { panel, openPanel, closePanel } = useDetailPanel();
|
|
181
182
|
|
|
182
183
|
// Derive unique groups, tiers, phases, and stacks for filters
|
|
183
|
-
const allGroups = useMemo(() => [...new Set(agents.list.map((a) => a.
|
|
184
|
-
const
|
|
184
|
+
const allGroups = useMemo(() => [...new Set(agents.list.map((a) => a.domains?.[0]).filter(Boolean))].sort(), [agents.list]);
|
|
185
|
+
const allDomains = useMemo(() => [...new Set(agents.list.flatMap((a) => a.domains || []))].sort(), [agents.list]);
|
|
185
186
|
const allPhases = useMemo(() => {
|
|
186
187
|
const phases = new Set();
|
|
187
188
|
for (const a of agents.list) {
|
|
@@ -208,17 +209,17 @@ export default function AgentMap() {
|
|
|
208
209
|
agents.list
|
|
209
210
|
.filter((a) => {
|
|
210
211
|
const matchesSearch = !q || a.title.toLowerCase().includes(q) || a.id.toLowerCase().includes(q) || a.keywords?.some((k) => k.toLowerCase().includes(q));
|
|
211
|
-
const
|
|
212
|
-
const matchesGroup = !groupFilter || a.
|
|
212
|
+
const matchesDomain = !domainFilter || (a.domains || []).includes(domainFilter);
|
|
213
|
+
const matchesGroup = !groupFilter || (a.domains || []).includes(groupFilter);
|
|
213
214
|
const matchesPhase = !phaseFilter || (a.activePhases || []).includes('*') || (a.activePhases || []).includes(phaseFilter);
|
|
214
215
|
const matchesStack = !stackFilter || (a.stacks || []).includes(stackFilter);
|
|
215
|
-
return matchesSearch &&
|
|
216
|
+
return matchesSearch && matchesDomain && matchesGroup && matchesPhase && matchesStack;
|
|
216
217
|
})
|
|
217
218
|
.map((a) => a.id)
|
|
218
219
|
);
|
|
219
220
|
}, [agents.list, search, tierFilter, groupFilter, phaseFilter, stackFilter]);
|
|
220
221
|
|
|
221
|
-
const hasFilter = search ||
|
|
222
|
+
const hasFilter = search || domainFilter || groupFilter || phaseFilter || stackFilter;
|
|
222
223
|
|
|
223
224
|
// Build React Flow nodes and edges
|
|
224
225
|
const { nodes: layoutNodes, edges } = useMemo(() => {
|
|
@@ -236,15 +237,15 @@ export default function AgentMap() {
|
|
|
236
237
|
// Agent nodes
|
|
237
238
|
for (const agent of agents.list) {
|
|
238
239
|
const dimmed = hasFilter && !matchedIds.has(agent.id);
|
|
239
|
-
const
|
|
240
|
+
const personaColors = PERSONA_COLORS[agent.id] || FALLBACK_PERSONA;
|
|
240
241
|
rawNodes.push({
|
|
241
242
|
id: agent.id,
|
|
242
243
|
type: 'agentNode',
|
|
243
244
|
data: {
|
|
244
245
|
label: agent.title,
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
color:
|
|
246
|
+
personaId: agent.id,
|
|
247
|
+
domains: agent.domains,
|
|
248
|
+
color: personaColors.border,
|
|
248
249
|
agentRef: agent,
|
|
249
250
|
},
|
|
250
251
|
style: dimmed ? { opacity: 0.2 } : {},
|
|
@@ -252,17 +253,15 @@ export default function AgentMap() {
|
|
|
252
253
|
});
|
|
253
254
|
}
|
|
254
255
|
|
|
255
|
-
//
|
|
256
|
+
// user → agent edges (flat — no hierarchy in v5)
|
|
256
257
|
for (const agent of agents.list) {
|
|
257
|
-
const
|
|
258
|
-
if (!source) continue;
|
|
259
|
-
const tierColors = TIER_COLORS[agent.tier] || TIER_COLORS[4];
|
|
258
|
+
const personaColors = PERSONA_COLORS[agent.id] || FALLBACK_PERSONA;
|
|
260
259
|
rawEdges.push({
|
|
261
260
|
id: `rt-${agent.id}`,
|
|
262
|
-
source,
|
|
261
|
+
source: '__user__',
|
|
263
262
|
target: agent.id,
|
|
264
263
|
type: 'smoothstep',
|
|
265
|
-
style: { stroke:
|
|
264
|
+
style: { stroke: personaColors.border, strokeWidth: 1.5 },
|
|
266
265
|
animated: false,
|
|
267
266
|
});
|
|
268
267
|
}
|
|
@@ -2,7 +2,7 @@ import morphData from 'virtual:morph-data';
|
|
|
2
2
|
|
|
3
3
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
|
-
const PHASE_ORDER = ['proposal', 'uiux', '
|
|
5
|
+
const PHASE_ORDER = ['proposal', 'uiux', 'plan', 'implement', 'review'];
|
|
6
6
|
|
|
7
7
|
const STATUS_BADGE = {
|
|
8
8
|
in_progress: { label: 'In Progress', bg: 'bg-blue-900/60', border: 'border-blue-600', text: 'text-blue-300' },
|