@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.
Files changed (149) hide show
  1. package/CLAUDE.md +3 -0
  2. package/LICENSE +71 -71
  3. package/README.md +379 -379
  4. package/bin/morph-spec.js +89 -0
  5. package/bin/task-manager.js +59 -6
  6. package/claude-plugin.json +14 -14
  7. package/docs/CHEATSHEET.md +1 -1
  8. package/docs/QUICKSTART.md +206 -206
  9. package/framework/agents/README.md +14 -14
  10. package/framework/agents/architecture/standards-architect.md +159 -159
  11. package/framework/agents/frontend/blazor-builder.md +13 -0
  12. package/framework/agents/frontend/css-specialist.md +87 -0
  13. package/framework/agents/frontend/nextjs-expert.md +13 -0
  14. package/framework/agents/frontend/nextjs-ui-specialist.md +218 -0
  15. package/framework/agents/frontend/ui-ux-designer.md +80 -10
  16. package/framework/agents/infrastructure/azure-architect.md +149 -149
  17. package/framework/agents/infrastructure/infra-architect.md +48 -48
  18. package/framework/agents.json +2268 -2567
  19. package/framework/hooks/README.md +204 -204
  20. package/framework/hooks/claude-code/post-tool-use/dispatch.js +10 -5
  21. package/framework/hooks/claude-code/session-start/inject-morph-context.js +10 -24
  22. package/framework/hooks/shared/phase-utils.js +139 -139
  23. package/framework/hooks/shared/worktree-helpers.js +30 -8
  24. package/framework/rules/csharp-standards.md +13 -13
  25. package/framework/rules/frontend-standards.md +13 -13
  26. package/framework/rules/infrastructure-standards.md +16 -16
  27. package/framework/rules/testing-standards.md +14 -14
  28. package/framework/skills/README.md +0 -1
  29. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +12 -0
  30. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +254 -254
  31. package/framework/skills/level-0-meta/morph-init/SKILL.md +88 -3
  32. package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +247 -247
  33. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +270 -270
  34. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +515 -499
  35. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +508 -471
  36. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +12 -1
  37. package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +272 -258
  38. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +317 -311
  39. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +424 -323
  40. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +1 -1
  41. package/framework/standards/STANDARDS.json +2040 -2040
  42. package/framework/standards/frontend/nextjs/motion-patterns.md +321 -0
  43. package/framework/standards/frontend/nextjs/nextjs-patterns.md +17 -17
  44. package/framework/templates/README.md +814 -814
  45. package/framework/templates/code/dotnet/contracts/Commands.cs +74 -74
  46. package/framework/templates/code/dotnet/contracts/Entities.cs +25 -25
  47. package/framework/templates/code/dotnet/contracts/Queries.cs +74 -74
  48. package/framework/templates/code/dotnet/contracts/README.md +74 -74
  49. package/framework/templates/code/dotnet/contracts/api-contracts.cs +173 -173
  50. package/framework/templates/code/dotnet/frontend/component.razor +239 -239
  51. package/framework/templates/code/dotnet/jobs/agent.cs +163 -163
  52. package/framework/templates/code/dotnet/jobs/job.cs +171 -171
  53. package/framework/templates/code/dotnet/test.cs +239 -239
  54. package/framework/templates/code/sql/rls-policy.sql +57 -57
  55. package/framework/templates/code/sql/supabase-migration.sql +100 -100
  56. package/framework/templates/code/sql/supabase-migration.template.sql +113 -113
  57. package/framework/templates/code/typescript/contracts.ts +168 -168
  58. package/framework/templates/context/CONTEXT.md +181 -181
  59. package/framework/templates/context/design-system.md +211 -0
  60. package/framework/templates/docs/proposal.md +182 -182
  61. package/framework/templates/docs/user-stories.md +34 -34
  62. package/framework/templates/examples/design-system-examples.md +357 -357
  63. package/framework/templates/examples/spec-examples.md +90 -90
  64. package/framework/templates/feature/decisions.md +187 -187
  65. package/framework/templates/feature/recap.md +146 -146
  66. package/framework/templates/feature/tasks.md +199 -199
  67. package/framework/templates/infrastructure/azure/README.md +286 -286
  68. package/framework/templates/infrastructure/azure/deploy-checklist.md +426 -426
  69. package/framework/templates/infrastructure/azure/parameters.dev.json +29 -29
  70. package/framework/templates/infrastructure/azure/parameters.prod.json +29 -29
  71. package/framework/templates/infrastructure/azure/parameters.staging.json +29 -29
  72. package/framework/templates/infrastructure/docker/docker-compose.template.yml +67 -67
  73. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  74. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  75. package/framework/templates/infrastructure/docker/easypanel.template.json +54 -54
  76. package/package.json +82 -82
  77. package/src/commands/agents/agent-metrics.js +174 -0
  78. package/src/commands/agents/audit-dispatch.js +82 -0
  79. package/src/commands/agents/detect-agents.js +275 -0
  80. package/src/commands/agents/dispatch-agents.js +41 -135
  81. package/src/commands/agents/index.js +13 -4
  82. package/src/commands/agents/manage-agents.js +352 -0
  83. package/src/commands/project/doctor.js +59 -0
  84. package/src/commands/project/install-plugin-cmd.js +28 -28
  85. package/src/commands/project/setup-infra-cmd.js +12 -12
  86. package/src/commands/project/update.js +1 -17
  87. package/src/commands/project/worktree.js +58 -2
  88. package/src/commands/state/advance-phase.js +11 -3
  89. package/src/commands/tasks/index.js +4 -4
  90. package/src/commands/templates/index.js +5 -5
  91. package/src/commands/utils/index.js +3 -3
  92. package/src/commands/validation/index.js +4 -4
  93. package/src/core/state/phase-state-machine.js +217 -217
  94. package/src/core/state/state-manager.js +81 -0
  95. package/src/lib/agents/agents-manager.js +349 -0
  96. package/src/lib/agents/metrics-calculator.js +125 -0
  97. package/src/lib/agents/output-collector.js +75 -0
  98. package/src/lib/generators/context-generator.js +8 -7
  99. package/src/lib/generators/design-system-generator.js +298 -298
  100. package/src/lib/generators/index.js +10 -10
  101. package/src/lib/generators/metadata-extractor.js +387 -387
  102. package/src/lib/stack-filter.js +65 -65
  103. package/src/lib/standards/index.js +7 -7
  104. package/src/lib/standards/standards-context-injector.js +302 -302
  105. package/src/lib/tasks/task-parser.js +94 -3
  106. package/src/lib/validators/architecture/architecture-validator.js +387 -387
  107. package/src/lib/validators/architecture/index.js +7 -7
  108. package/src/lib/validators/blazor/blazor-concurrency-analyzer.js +316 -316
  109. package/src/lib/validators/blazor/blazor-state-validator.js +279 -279
  110. package/src/lib/validators/blazor/blazor-validator.js +369 -369
  111. package/src/lib/validators/blazor/index.js +9 -9
  112. package/src/lib/validators/checkpoint-persistence.js +39 -0
  113. package/src/lib/validators/content/content-validator.js +287 -279
  114. package/src/lib/validators/content/index.js +7 -7
  115. package/src/lib/validators/contracts/index.js +7 -7
  116. package/src/lib/validators/css/css-validator.js +352 -352
  117. package/src/lib/validators/css/index.js +7 -7
  118. package/src/lib/validators/design-system/design-system-validator.js +231 -231
  119. package/src/lib/validators/design-system/index.js +7 -7
  120. package/src/lib/validators/nextjs/index.js +6 -6
  121. package/src/lib/validators/nextjs/next-component-validator.js +183 -183
  122. package/src/lib/validators/packages/index.js +7 -7
  123. package/src/lib/validators/packages/package-validator.js +360 -360
  124. package/src/lib/validators/shared/index.js +12 -12
  125. package/src/lib/validators/shared/issue-counter.js +18 -18
  126. package/src/lib/validators/shared/result-formatter.js +124 -124
  127. package/src/lib/validators/spec-validator.js +2 -2
  128. package/src/lib/validators/ui/index.js +7 -7
  129. package/src/lib/validators/ui/ui-contrast-validator.js +422 -422
  130. package/src/lib/validators/validation-runner.js +20 -0
  131. package/src/scripts/global-install.js +58 -58
  132. package/src/scripts/install-plugin.js +126 -126
  133. package/src/scripts/setup-infra.js +1 -17
  134. package/src/utils/agent-id-mapper.js +11 -0
  135. package/src/utils/agents-installer.js +40 -8
  136. package/src/utils/claude-md-injector.js +90 -90
  137. package/src/utils/claude-settings-manager.js +1 -1
  138. package/src/utils/file-copier.js +200 -196
  139. package/src/utils/hooks-installer.js +556 -739
  140. package/src/utils/logger.js +32 -32
  141. package/src/utils/skills-installer.js +178 -178
  142. package/src/utils/tier-helpers.js +24 -0
  143. package/src/utils/version-checker.js +175 -175
  144. package/framework/agents/architecture/po-pm-advisor.md +0 -203
  145. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +0 -68
  146. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
  147. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
  148. package/framework/templates/.idea/morph-templates.xml +0 -92
  149. 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.6",
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
+ }