@polymorphism-tech/morph-spec 4.10.6 → 4.10.8
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/LICENSE +71 -71
- package/README.md +379 -379
- package/bin/morph-spec.js +89 -0
- package/bin/task-manager.js +59 -6
- package/claude-plugin.json +14 -14
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +206 -206
- package/framework/agents/README.md +14 -14
- package/framework/agents/architecture/standards-architect.md +159 -159
- package/framework/agents/frontend/blazor-builder.md +13 -0
- package/framework/agents/frontend/css-specialist.md +87 -0
- package/framework/agents/frontend/nextjs-expert.md +13 -0
- package/framework/agents/frontend/nextjs-ui-specialist.md +218 -0
- package/framework/agents/frontend/ui-ux-designer.md +80 -10
- package/framework/agents/infrastructure/azure-architect.md +149 -149
- package/framework/agents/infrastructure/infra-architect.md +48 -48
- package/framework/agents.json +2268 -2567
- package/framework/hooks/README.md +204 -204
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +10 -5
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +10 -24
- package/framework/hooks/shared/phase-utils.js +139 -139
- package/framework/hooks/shared/worktree-helpers.js +30 -8
- package/framework/rules/csharp-standards.md +13 -13
- package/framework/rules/frontend-standards.md +13 -13
- package/framework/rules/infrastructure-standards.md +16 -16
- package/framework/rules/testing-standards.md +14 -14
- package/framework/skills/README.md +0 -1
- package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +12 -0
- package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +254 -254
- package/framework/skills/level-0-meta/morph-init/SKILL.md +88 -3
- package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +247 -247
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +270 -270
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +515 -499
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +508 -471
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +12 -1
- package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +272 -258
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +317 -311
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +424 -323
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
- package/framework/standards/STANDARDS.json +2040 -2040
- package/framework/standards/frontend/nextjs/motion-patterns.md +321 -0
- package/framework/standards/frontend/nextjs/nextjs-patterns.md +17 -17
- package/framework/templates/README.md +814 -814
- package/framework/templates/code/dotnet/contracts/Commands.cs +74 -74
- package/framework/templates/code/dotnet/contracts/Entities.cs +25 -25
- package/framework/templates/code/dotnet/contracts/Queries.cs +74 -74
- package/framework/templates/code/dotnet/contracts/README.md +74 -74
- package/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -173
- package/framework/templates/code/dotnet/frontend/component.razor +239 -239
- package/framework/templates/code/dotnet/jobs/agent.cs +163 -163
- package/framework/templates/code/dotnet/jobs/job.cs +171 -171
- package/framework/templates/code/dotnet/test.cs +239 -239
- package/framework/templates/code/sql/rls-policy.sql +57 -57
- package/framework/templates/code/sql/supabase-migration.sql +100 -100
- package/framework/templates/code/sql/supabase-migration.template.sql +113 -113
- package/framework/templates/code/typescript/contracts.ts +168 -168
- package/framework/templates/context/CONTEXT.md +181 -181
- package/framework/templates/context/design-system.md +211 -0
- package/framework/templates/docs/proposal.md +182 -182
- package/framework/templates/docs/user-stories.md +34 -34
- package/framework/templates/examples/design-system-examples.md +357 -357
- package/framework/templates/examples/spec-examples.md +90 -90
- package/framework/templates/feature/decisions.md +187 -187
- package/framework/templates/feature/recap.md +146 -146
- package/framework/templates/feature/tasks.md +199 -199
- package/framework/templates/infrastructure/azure/README.md +286 -286
- package/framework/templates/infrastructure/azure/deploy-checklist.md +426 -426
- package/framework/templates/infrastructure/azure/parameters.dev.json +29 -29
- package/framework/templates/infrastructure/azure/parameters.prod.json +29 -29
- package/framework/templates/infrastructure/azure/parameters.staging.json +29 -29
- package/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -67
- package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
- package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
- package/framework/templates/infrastructure/docker/easypanel.template.json +54 -54
- package/package.json +82 -82
- 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 +13 -4
- package/src/commands/agents/manage-agents.js +352 -0
- package/src/commands/project/doctor.js +59 -0
- package/src/commands/project/install-plugin-cmd.js +28 -28
- package/src/commands/project/setup-infra-cmd.js +12 -12
- package/src/commands/project/update.js +1 -17
- package/src/commands/project/worktree.js +58 -2
- package/src/commands/state/advance-phase.js +11 -3
- package/src/commands/tasks/index.js +4 -4
- package/src/commands/templates/index.js +5 -5
- package/src/commands/utils/index.js +3 -3
- package/src/commands/validation/index.js +4 -4
- package/src/core/state/phase-state-machine.js +217 -217
- 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/generators/design-system-generator.js +298 -298
- package/src/lib/generators/index.js +10 -10
- package/src/lib/generators/metadata-extractor.js +387 -387
- package/src/lib/stack-filter.js +65 -65
- package/src/lib/standards/index.js +7 -7
- package/src/lib/standards/standards-context-injector.js +302 -302
- package/src/lib/tasks/task-parser.js +94 -3
- package/src/lib/validators/architecture/architecture-validator.js +387 -387
- package/src/lib/validators/architecture/index.js +7 -7
- package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +316 -316
- package/src/lib/validators/blazor/blazor-state-validator.js +279 -279
- package/src/lib/validators/blazor/blazor-validator.js +369 -369
- package/src/lib/validators/blazor/index.js +9 -9
- package/src/lib/validators/checkpoint-persistence.js +39 -0
- package/src/lib/validators/content/content-validator.js +287 -279
- package/src/lib/validators/content/index.js +7 -7
- package/src/lib/validators/contracts/index.js +7 -7
- package/src/lib/validators/css/css-validator.js +352 -352
- package/src/lib/validators/css/index.js +7 -7
- package/src/lib/validators/design-system/design-system-validator.js +231 -231
- package/src/lib/validators/design-system/index.js +7 -7
- package/src/lib/validators/nextjs/index.js +6 -6
- package/src/lib/validators/nextjs/next-component-validator.js +183 -183
- package/src/lib/validators/packages/index.js +7 -7
- package/src/lib/validators/packages/package-validator.js +360 -360
- package/src/lib/validators/shared/index.js +12 -12
- package/src/lib/validators/shared/issue-counter.js +18 -18
- package/src/lib/validators/shared/result-formatter.js +124 -124
- package/src/lib/validators/spec-validator.js +2 -2
- package/src/lib/validators/ui/index.js +7 -7
- package/src/lib/validators/ui/ui-contrast-validator.js +422 -422
- package/src/lib/validators/validation-runner.js +20 -0
- package/src/scripts/global-install.js +58 -58
- package/src/scripts/install-plugin.js +126 -126
- 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-md-injector.js +90 -90
- package/src/utils/claude-settings-manager.js +1 -1
- package/src/utils/file-copier.js +200 -196
- package/src/utils/hooks-installer.js +556 -739
- package/src/utils/logger.js +32 -32
- package/src/utils/skills-installer.js +178 -178
- package/src/utils/tier-helpers.js +24 -0
- package/src/utils/version-checker.js +175 -175
- package/framework/agents/architecture/po-pm-advisor.md +0 -203
- package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +0 -68
- package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
- package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
- package/framework/templates/.idea/morph-templates.xml +0 -92
- package/framework/templates/.vscode/morph-templates.code-snippets +0 -186
package/package.json
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@polymorphism-tech/morph-spec",
|
|
3
|
-
"version": "4.10.
|
|
4
|
-
"description": "MORPH-SPEC: AI-First development framework with validation pipeline and multi-stack support",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"claude-code",
|
|
7
|
-
"claude",
|
|
8
|
-
"ai-coding",
|
|
9
|
-
"ai-first",
|
|
10
|
-
"spec-driven",
|
|
11
|
-
"dotnet",
|
|
12
|
-
"dotnet10",
|
|
13
|
-
"blazor",
|
|
14
|
-
"agent-framework",
|
|
15
|
-
"fluent-ui",
|
|
16
|
-
"framework",
|
|
17
|
-
"developer-tools",
|
|
18
|
-
"morph",
|
|
19
|
-
"polymorphism",
|
|
20
|
-
"micro-saas"
|
|
21
|
-
],
|
|
22
|
-
"main": "src/index.js",
|
|
23
|
-
"bin": {
|
|
24
|
-
"morph-spec": "bin/morph-spec.js"
|
|
25
|
-
},
|
|
26
|
-
"files": [
|
|
27
|
-
"bin/",
|
|
28
|
-
"src/",
|
|
29
|
-
"framework/",
|
|
30
|
-
"claude-plugin.json",
|
|
31
|
-
"docs/QUICKSTART.md",
|
|
32
|
-
"docs/CHEATSHEET.md",
|
|
33
|
-
"README.md",
|
|
34
|
-
"LICENSE",
|
|
35
|
-
"CLAUDE.md"
|
|
36
|
-
],
|
|
37
|
-
"type": "module",
|
|
38
|
-
"engines": {
|
|
39
|
-
"node": ">=18.0.0"
|
|
40
|
-
},
|
|
41
|
-
"scripts": {
|
|
42
|
-
"start": "node bin/morph-spec.js",
|
|
43
|
-
"generate": "node scripts/generate-refs.js",
|
|
44
|
-
"test": "node --test --test-concurrency=1",
|
|
45
|
-
"test:coverage": "c8 --reporter=text --reporter=html --reporter=lcov node --test --test-concurrency=1",
|
|
46
|
-
"test:coverage:summary": "c8 --reporter=text-summary node --test --test-concurrency=1",
|
|
47
|
-
"docs": "jsdoc -c jsdoc.json",
|
|
48
|
-
"docs:watch": "jsdoc -c jsdoc.json --watch",
|
|
49
|
-
"docs:serve": "npx http-server docs/api -p 8080 -o",
|
|
50
|
-
"release": "node scripts/release.js",
|
|
51
|
-
"postinstall": "node src/scripts/global-install.js"
|
|
52
|
-
},
|
|
53
|
-
"dependencies": {
|
|
54
|
-
"chalk": "^5.3.0",
|
|
55
|
-
"commander": "^12.0.0",
|
|
56
|
-
"fs-extra": "^11.2.0",
|
|
57
|
-
"glob": "^13.0.6",
|
|
58
|
-
"handlebars": "^4.7.8",
|
|
59
|
-
"inquirer": "^9.2.0",
|
|
60
|
-
"minimatch": "^9.0.5",
|
|
61
|
-
"ora": "^8.0.0"
|
|
62
|
-
},
|
|
63
|
-
"repository": {
|
|
64
|
-
"type": "git",
|
|
65
|
-
"url": "git+https://github.com/lucasPolymorphism/morph-spec-framework.git"
|
|
66
|
-
},
|
|
67
|
-
"author": "Polymorphism Tech <contact@polymorphism.com.br>",
|
|
68
|
-
"license": "SEE LICENSE IN LICENSE",
|
|
69
|
-
"homepage": "https://polymorphism.com.br/morph-spec",
|
|
70
|
-
"bugs": {
|
|
71
|
-
"email": "support@polymorphism.com.br"
|
|
72
|
-
},
|
|
73
|
-
"private": false,
|
|
74
|
-
"publishConfig": {
|
|
75
|
-
"access": "public"
|
|
76
|
-
},
|
|
77
|
-
"devDependencies": {
|
|
78
|
-
"c8": "^11.0.0",
|
|
79
|
-
"docdash": "^2.0.2",
|
|
80
|
-
"jsdoc": "^4.0.5"
|
|
81
|
-
}
|
|
82
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@polymorphism-tech/morph-spec",
|
|
3
|
+
"version": "4.10.8",
|
|
4
|
+
"description": "MORPH-SPEC: AI-First development framework with validation pipeline and multi-stack support",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"claude",
|
|
8
|
+
"ai-coding",
|
|
9
|
+
"ai-first",
|
|
10
|
+
"spec-driven",
|
|
11
|
+
"dotnet",
|
|
12
|
+
"dotnet10",
|
|
13
|
+
"blazor",
|
|
14
|
+
"agent-framework",
|
|
15
|
+
"fluent-ui",
|
|
16
|
+
"framework",
|
|
17
|
+
"developer-tools",
|
|
18
|
+
"morph",
|
|
19
|
+
"polymorphism",
|
|
20
|
+
"micro-saas"
|
|
21
|
+
],
|
|
22
|
+
"main": "src/index.js",
|
|
23
|
+
"bin": {
|
|
24
|
+
"morph-spec": "bin/morph-spec.js"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"bin/",
|
|
28
|
+
"src/",
|
|
29
|
+
"framework/",
|
|
30
|
+
"claude-plugin.json",
|
|
31
|
+
"docs/QUICKSTART.md",
|
|
32
|
+
"docs/CHEATSHEET.md",
|
|
33
|
+
"README.md",
|
|
34
|
+
"LICENSE",
|
|
35
|
+
"CLAUDE.md"
|
|
36
|
+
],
|
|
37
|
+
"type": "module",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18.0.0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"start": "node bin/morph-spec.js",
|
|
43
|
+
"generate": "node scripts/generate-refs.js",
|
|
44
|
+
"test": "node --test --test-concurrency=1",
|
|
45
|
+
"test:coverage": "c8 --reporter=text --reporter=html --reporter=lcov node --test --test-concurrency=1",
|
|
46
|
+
"test:coverage:summary": "c8 --reporter=text-summary node --test --test-concurrency=1",
|
|
47
|
+
"docs": "jsdoc -c jsdoc.json",
|
|
48
|
+
"docs:watch": "jsdoc -c jsdoc.json --watch",
|
|
49
|
+
"docs:serve": "npx http-server docs/api -p 8080 -o",
|
|
50
|
+
"release": "node scripts/release.js",
|
|
51
|
+
"postinstall": "node src/scripts/global-install.js"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"chalk": "^5.3.0",
|
|
55
|
+
"commander": "^12.0.0",
|
|
56
|
+
"fs-extra": "^11.2.0",
|
|
57
|
+
"glob": "^13.0.6",
|
|
58
|
+
"handlebars": "^4.7.8",
|
|
59
|
+
"inquirer": "^9.2.0",
|
|
60
|
+
"minimatch": "^9.0.5",
|
|
61
|
+
"ora": "^8.0.0"
|
|
62
|
+
},
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/lucasPolymorphism/morph-spec-framework.git"
|
|
66
|
+
},
|
|
67
|
+
"author": "Polymorphism Tech <contact@polymorphism.com.br>",
|
|
68
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
69
|
+
"homepage": "https://polymorphism.com.br/morph-spec",
|
|
70
|
+
"bugs": {
|
|
71
|
+
"email": "support@polymorphism.com.br"
|
|
72
|
+
},
|
|
73
|
+
"private": false,
|
|
74
|
+
"publishConfig": {
|
|
75
|
+
"access": "public"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"c8": "^11.0.0",
|
|
79
|
+
"docdash": "^2.0.2",
|
|
80
|
+
"jsdoc": "^4.0.5"
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-metrics — Display agent performance metrics for a feature
|
|
3
|
+
*
|
|
4
|
+
* Reads dispatchLog from state.json, computes per-agent metrics
|
|
5
|
+
* (dispatch count, task coverage, completion rate, avg duration),
|
|
6
|
+
* and renders a table or JSON output.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* morph-spec agent-metrics <feature>
|
|
10
|
+
* morph-spec agent-metrics <feature> --json
|
|
11
|
+
* morph-spec agent-metrics --all
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { loadState, stateExists } from '../../core/state/state-manager.js';
|
|
15
|
+
import { calculateMetrics, calculateAggregatedMetrics, formatDuration } from '../../lib/agents/metrics-calculator.js';
|
|
16
|
+
import chalk from 'chalk';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* CLI command: morph-spec agent-metrics [feature]
|
|
20
|
+
*
|
|
21
|
+
* @param {string} [featureName] - Feature name (required unless --all)
|
|
22
|
+
* @param {Object} [options] - CLI options
|
|
23
|
+
* @param {boolean} [options.json] - Output as JSON instead of table
|
|
24
|
+
* @param {boolean} [options.all] - Aggregate metrics across all features
|
|
25
|
+
*/
|
|
26
|
+
export async function agentMetricsCommand(featureName, options = {}) {
|
|
27
|
+
if (!options.all && !featureName) {
|
|
28
|
+
console.error(chalk.red('Usage: morph-spec agent-metrics <feature> [--json] [--all]'));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!stateExists()) {
|
|
33
|
+
console.error(chalk.red('No state.json found.'));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const state = loadState();
|
|
38
|
+
|
|
39
|
+
if (options.all) {
|
|
40
|
+
return renderAggregated(state.features || {}, options);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const feature = state.features?.[featureName];
|
|
44
|
+
if (!feature) {
|
|
45
|
+
console.error(chalk.red(`Feature '${featureName}' not found.`));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const log = feature.dispatchLog || [];
|
|
50
|
+
if (log.length === 0) {
|
|
51
|
+
console.log(chalk.gray('\n No dispatch events recorded.\n'));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const result = calculateMetrics(log);
|
|
56
|
+
result.feature = featureName;
|
|
57
|
+
|
|
58
|
+
if (options.json) {
|
|
59
|
+
console.log(JSON.stringify(result, null, 2));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
renderTable(featureName, result);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Render per-feature metrics table.
|
|
68
|
+
*/
|
|
69
|
+
function renderTable(featureName, result) {
|
|
70
|
+
const { agents, summary } = result;
|
|
71
|
+
|
|
72
|
+
console.log(chalk.bold(`\n Agent Performance \u2014 ${featureName}`));
|
|
73
|
+
console.log(' ' + '\u2500'.repeat(70));
|
|
74
|
+
|
|
75
|
+
// Header
|
|
76
|
+
console.log(
|
|
77
|
+
' ' +
|
|
78
|
+
pad('Agent', 28) +
|
|
79
|
+
pad('Dispatches', 12) +
|
|
80
|
+
pad('Tasks', 8) +
|
|
81
|
+
pad('Completed', 12) +
|
|
82
|
+
pad('Avg Duration', 14)
|
|
83
|
+
);
|
|
84
|
+
console.log(' ' + '\u2500'.repeat(70));
|
|
85
|
+
|
|
86
|
+
for (const agent of agents) {
|
|
87
|
+
const tasks = agent.tasksAssigned > 0 ? String(agent.tasksAssigned) : '\u2014';
|
|
88
|
+
const completed = agent.tasksAssigned > 0
|
|
89
|
+
? `${agent.tasksCompleted}/${agent.tasksAssigned}`
|
|
90
|
+
: '\u2014';
|
|
91
|
+
const duration = formatDuration(agent.avgDurationMs);
|
|
92
|
+
|
|
93
|
+
console.log(
|
|
94
|
+
' ' +
|
|
95
|
+
pad(agent.id, 28) +
|
|
96
|
+
pad(String(agent.dispatches), 12) +
|
|
97
|
+
pad(tasks, 8) +
|
|
98
|
+
pad(completed, 12) +
|
|
99
|
+
pad(duration, 14)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
console.log();
|
|
104
|
+
const rate = summary.totalTasks > 0
|
|
105
|
+
? `${Math.round(summary.completionRate * 100)}%`
|
|
106
|
+
: '0%';
|
|
107
|
+
console.log(` Summary: ${summary.completedTasks} tasks completed of ${summary.totalTasks} (${rate}), avg ${formatDuration(summary.avgTaskDurationMs)}/task`);
|
|
108
|
+
console.log(` Dispatches: ${summary.totalDispatches} total`);
|
|
109
|
+
console.log();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Render aggregated metrics across all features.
|
|
114
|
+
*/
|
|
115
|
+
function renderAggregated(features, options) {
|
|
116
|
+
const featureNames = Object.keys(features);
|
|
117
|
+
if (featureNames.length === 0) {
|
|
118
|
+
console.log(chalk.gray('\n No features found.\n'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const result = calculateAggregatedMetrics(features);
|
|
123
|
+
|
|
124
|
+
if (options.json) {
|
|
125
|
+
console.log(JSON.stringify(result, null, 2));
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(chalk.bold(`\n Agent Performance \u2014 All Features (${result.featureCount})`));
|
|
130
|
+
console.log(' ' + '\u2500'.repeat(70));
|
|
131
|
+
|
|
132
|
+
console.log(
|
|
133
|
+
' ' +
|
|
134
|
+
pad('Agent', 28) +
|
|
135
|
+
pad('Dispatches', 12) +
|
|
136
|
+
pad('Tasks', 8) +
|
|
137
|
+
pad('Completed', 12) +
|
|
138
|
+
pad('Avg Duration', 14)
|
|
139
|
+
);
|
|
140
|
+
console.log(' ' + '\u2500'.repeat(70));
|
|
141
|
+
|
|
142
|
+
for (const agent of result.agents) {
|
|
143
|
+
const tasks = agent.tasksAssigned > 0 ? String(agent.tasksAssigned) : '\u2014';
|
|
144
|
+
const completed = agent.tasksAssigned > 0
|
|
145
|
+
? `${agent.tasksCompleted}/${agent.tasksAssigned}`
|
|
146
|
+
: '\u2014';
|
|
147
|
+
const duration = formatDuration(agent.avgDurationMs);
|
|
148
|
+
|
|
149
|
+
console.log(
|
|
150
|
+
' ' +
|
|
151
|
+
pad(agent.id, 28) +
|
|
152
|
+
pad(String(agent.dispatches), 12) +
|
|
153
|
+
pad(tasks, 8) +
|
|
154
|
+
pad(completed, 12) +
|
|
155
|
+
pad(duration, 14)
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log();
|
|
160
|
+
const { summary } = result;
|
|
161
|
+
const rate = summary.totalTasks > 0
|
|
162
|
+
? `${Math.round(summary.completionRate * 100)}%`
|
|
163
|
+
: '0%';
|
|
164
|
+
console.log(` Summary: ${summary.completedTasks} tasks completed of ${summary.totalTasks} (${rate}), avg ${formatDuration(summary.avgTaskDurationMs)}/task`);
|
|
165
|
+
console.log(` Dispatches: ${summary.totalDispatches} total across ${result.featureCount} features`);
|
|
166
|
+
console.log();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Right-pad a string to a given width.
|
|
171
|
+
*/
|
|
172
|
+
function pad(str, width) {
|
|
173
|
+
return str.length >= width ? str : str + ' '.repeat(width - str.length);
|
|
174
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* audit-dispatch — Display dispatch audit log for a feature
|
|
3
|
+
*
|
|
4
|
+
* Reads dispatchLog from state.json and presents a summary:
|
|
5
|
+
* - Total dispatches
|
|
6
|
+
* - Per-dispatch: timestamp, phase, mode, agents with subagentTypes
|
|
7
|
+
* - Agent coverage: which agents handled which tasks
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* morph-spec audit-dispatch <feature>
|
|
11
|
+
* morph-spec audit-dispatch <feature> --json
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { loadState, stateExists } from '../../core/state/state-manager.js';
|
|
15
|
+
import chalk from 'chalk';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* CLI command: morph-spec audit-dispatch <feature>
|
|
19
|
+
*
|
|
20
|
+
* @param {string} featureName - Feature name
|
|
21
|
+
* @param {Object} [options] - CLI options
|
|
22
|
+
* @param {boolean} [options.json] - Output as JSON instead of table
|
|
23
|
+
*/
|
|
24
|
+
export async function auditDispatchCommand(featureName, options = {}) {
|
|
25
|
+
if (!featureName) {
|
|
26
|
+
console.error(chalk.red('Usage: morph-spec audit-dispatch <feature>'));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!stateExists()) {
|
|
31
|
+
console.error(chalk.red('No state.json found.'));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const state = loadState();
|
|
36
|
+
const feature = state.features?.[featureName];
|
|
37
|
+
if (!feature) {
|
|
38
|
+
console.error(chalk.red(`Feature '${featureName}' not found.`));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const log = feature.dispatchLog || [];
|
|
43
|
+
|
|
44
|
+
if (options.json) {
|
|
45
|
+
console.log(JSON.stringify(log, null, 2));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log(chalk.bold(`\n Dispatch Audit — ${featureName}`));
|
|
50
|
+
console.log(' ' + '─'.repeat(50));
|
|
51
|
+
console.log(` Total dispatches: ${log.length}`);
|
|
52
|
+
|
|
53
|
+
if (log.length === 0) {
|
|
54
|
+
console.log(chalk.gray('\n No dispatch events recorded.\n'));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (let i = 0; i < log.length; i++) {
|
|
59
|
+
const entry = log[i];
|
|
60
|
+
console.log(`\n #${i + 1}: ${entry.timestamp} — ${entry.phase} (${entry.mode})`);
|
|
61
|
+
for (const agent of entry.agents || []) {
|
|
62
|
+
const taskCount = agent.tasks?.length || 0;
|
|
63
|
+
console.log(` ${agent.id} → ${agent.subagentType} (${agent.group}) — ${taskCount} tasks`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Agent coverage summary
|
|
68
|
+
const coverage = {};
|
|
69
|
+
for (const entry of log) {
|
|
70
|
+
for (const agent of entry.agents || []) {
|
|
71
|
+
if (!coverage[agent.id]) coverage[agent.id] = { tasks: 0, dispatches: 0 };
|
|
72
|
+
coverage[agent.id].tasks += agent.tasks?.length || 0;
|
|
73
|
+
coverage[agent.id].dispatches += 1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(chalk.bold('\n Agent Coverage:'));
|
|
78
|
+
for (const [id, data] of Object.entries(coverage)) {
|
|
79
|
+
console.log(` ${id}: ${data.tasks} tasks across ${data.dispatches} dispatches`);
|
|
80
|
+
}
|
|
81
|
+
console.log();
|
|
82
|
+
}
|
|
@@ -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
|
+
}
|