@polymorphism-tech/morph-spec 4.9.0 → 4.10.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/README.md +2 -2
- package/bin/morph-spec.js +30 -0
- package/bin/task-manager.js +34 -22
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +35 -98
- package/framework/agents/backend/api-designer.md +3 -0
- package/framework/agents/backend/dotnet-senior.md +3 -0
- package/framework/agents/backend/ef-modeler.md +2 -0
- package/framework/agents/backend/hangfire-orchestrator.md +2 -0
- package/framework/agents/backend/ms-agent-expert.md +2 -0
- package/framework/agents/frontend/blazor-builder.md +2 -0
- package/framework/agents/frontend/nextjs-expert.md +2 -0
- package/framework/agents/infrastructure/azure-architect.md +2 -0
- package/framework/agents/infrastructure/azure-deploy-specialist.md +2 -0
- package/framework/agents/infrastructure/bicep-architect.md +2 -0
- package/framework/agents/infrastructure/container-specialist.md +2 -0
- package/framework/agents/infrastructure/devops-engineer.md +3 -0
- package/framework/agents/infrastructure/infra-architect.md +3 -0
- package/framework/agents/integrations/asaas-financial.md +2 -0
- package/framework/agents/integrations/azure-identity.md +2 -0
- package/framework/agents/integrations/clerk-auth.md +3 -0
- package/framework/agents/integrations/hangfire-integration.md +2 -0
- package/framework/agents/integrations/resend-email.md +2 -0
- package/framework/agents.json +37 -7
- package/framework/commands/commit.md +166 -0
- package/framework/commands/morph-apply.md +156 -155
- package/framework/commands/morph-archive.md +33 -27
- package/framework/commands/morph-infra.md +83 -77
- package/framework/commands/morph-preflight.md +97 -55
- package/framework/commands/morph-proposal.md +131 -58
- package/framework/commands/morph-status.md +36 -30
- package/framework/commands/morph-troubleshoot.md +68 -59
- package/framework/hooks/claude-code/notification/approval-reminder.js +3 -2
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +154 -31
- package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +7 -84
- package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +8 -17
- package/framework/hooks/claude-code/pre-compact/save-morph-context.js +16 -3
- package/framework/hooks/claude-code/pre-tool-use/enforce-phase-writes.js +4 -3
- package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +3 -2
- package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +60 -0
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +55 -2
- package/framework/hooks/claude-code/session-start/post-compact-restore.js +41 -0
- package/framework/hooks/claude-code/stop/validate-completion.js +2 -15
- package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +23 -5
- package/framework/hooks/shared/compact-restore.js +100 -0
- package/framework/hooks/shared/dispatch-helpers.js +116 -0
- package/framework/hooks/shared/phase-utils.js +9 -5
- package/framework/hooks/shared/state-reader.js +27 -3
- package/framework/phases.json +30 -7
- package/framework/rules/csharp-standards.md +3 -0
- package/framework/rules/frontend-standards.md +2 -0
- package/framework/rules/infrastructure-standards.md +3 -0
- package/framework/rules/morph-workflow.md +143 -86
- package/framework/rules/nextjs-standards.md +2 -0
- package/framework/rules/testing-standards.md +3 -0
- package/framework/skills/level-0-meta/mcp-registry.json +86 -51
- package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +139 -0
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +42 -19
- package/framework/skills/level-0-meta/{code-review → morph-code-review}/SKILL.md +8 -5
- package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/SKILL.md +8 -6
- package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +362 -0
- package/framework/skills/level-0-meta/morph-init/SKILL.md +114 -20
- package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +362 -0
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +95 -87
- package/framework/skills/level-0-meta/{simulation-checklist → morph-simulation-checklist}/SKILL.md +24 -0
- package/framework/skills/level-0-meta/{tool-usage-guide → morph-tool-usage-guide}/SKILL.md +43 -43
- package/framework/skills/level-0-meta/{tool-usage-guide → morph-tool-usage-guide}/references/tools-per-phase.md +1 -2
- package/framework/skills/level-0-meta/{verification-before-completion → morph-verification-before-completion}/SKILL.md +23 -12
- package/framework/skills/level-0-meta/{verification-before-completion → morph-verification-before-completion}/scripts/check-phase-outputs.mjs +2 -2
- package/framework/skills/level-1-workflows/morph-phase-clarify/SKILL.md +247 -0
- package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +270 -0
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +499 -0
- package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +38 -0
- package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +472 -0
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +50 -0
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +45 -0
- package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +47 -0
- package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +246 -0
- package/framework/skills/level-1-workflows/morph-phase-setup/SKILL.md +238 -0
- package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +14 -0
- package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +312 -0
- package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/scripts/validate-tasks.mjs +3 -3
- package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +324 -0
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +146 -0
- package/framework/standards/integration/mcp/mcp-tools.md +25 -7
- package/framework/templates/docs/onboarding.md +2 -2
- package/package.json +3 -4
- package/src/commands/agents/dispatch-agents.js +50 -3
- package/src/commands/mcp/mcp-setup.js +39 -2
- package/src/commands/phase/phase-reset.js +74 -0
- package/src/commands/project/doctor.js +26 -7
- package/src/commands/project/update.js +4 -4
- package/src/commands/scope/escalate.js +215 -0
- package/src/commands/state/advance-phase.js +27 -53
- package/src/commands/state/state.js +1 -1
- package/src/commands/task/expand.js +100 -0
- package/src/core/paths/output-schema.js +4 -3
- package/src/core/state/phase-state-machine.js +7 -4
- package/src/core/state/state-manager.js +4 -3
- package/src/lib/detectors/claude-config-detector.js +93 -347
- package/src/lib/detectors/design-system-detector.js +189 -189
- package/src/lib/detectors/index.js +155 -57
- package/src/lib/generators/context-generator.js +2 -2
- package/src/lib/installers/mcp-installer.js +37 -5
- package/src/lib/phase-chain/phase-validator.js +22 -16
- package/src/lib/scope/impact-analyzer.js +106 -0
- package/src/lib/stack-filter.js +58 -0
- package/src/lib/tasks/task-parser.js +1 -1
- package/src/lib/validators/shared/emit-validator-dispatch.js +64 -0
- package/src/scripts/setup-infra.js +68 -18
- package/src/utils/agents-installer.js +51 -17
- package/src/utils/claude-md-injector.js +90 -0
- package/src/utils/file-copier.js +0 -1
- package/src/utils/hooks-installer.js +16 -5
- package/src/utils/skills-installer.js +67 -7
- package/CLAUDE.md +0 -98
- package/framework/memory/patterns-learned.md +0 -766
- package/framework/skills/level-0-meta/brainstorming/SKILL.md +0 -137
- package/framework/skills/level-0-meta/frontend-review/SKILL.md +0 -359
- package/framework/skills/level-0-meta/post-implementation/SKILL.md +0 -362
- package/framework/skills/level-0-meta/terminal-title/SKILL.md +0 -61
- package/framework/skills/level-0-meta/terminal-title/scripts/set_title.sh +0 -65
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +0 -216
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +0 -252
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +0 -383
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +0 -492
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +0 -195
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +0 -271
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +0 -286
- package/src/commands/project/index.js +0 -8
- package/src/core/index.js +0 -10
- package/src/core/state/index.js +0 -8
- package/src/core/templates/index.js +0 -9
- package/src/core/templates/template-data-sources.js +0 -325
- package/src/core/workflows/index.js +0 -7
- package/src/lib/detectors/config-detector.js +0 -223
- package/src/lib/detectors/standards-generator.js +0 -335
- package/src/lib/detectors/structure-detector.js +0 -275
- package/src/lib/monitor/agent-resolver.js +0 -144
- package/src/lib/monitor/renderer.js +0 -230
- package/src/lib/orchestration/index.js +0 -7
- package/src/lib/orchestration/team-orchestrator.js +0 -404
- package/src/sanitizer/context-sanitizer.js +0 -221
- package/src/sanitizer/patterns.js +0 -163
- package/src/writer/file-writer.js +0 -86
- /package/framework/skills/level-0-meta/{brainstorming → morph-brainstorming}/references/proposal-example.md +0 -0
- /package/framework/skills/level-0-meta/{code-review → morph-code-review}/references/review-example.md +0 -0
- /package/framework/skills/level-0-meta/{code-review → morph-code-review}/references/review-guidelines.md +0 -0
- /package/framework/skills/level-0-meta/{code-review → morph-code-review}/scripts/scan-csharp.mjs +0 -0
- /package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/references/review-example-nextjs.md +0 -0
- /package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/scripts/scan-nextjs.mjs +0 -0
- /package/framework/skills/level-0-meta/{frontend-review → morph-frontend-review}/scripts/scan-accessibility.mjs +0 -0
- /package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/scripts/detect-dev-server.mjs +0 -0
- /package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/scripts/detect-stack.mjs +0 -0
- /package/framework/skills/level-1-workflows/{phase-clarify → morph-phase-clarify}/references/clarifications-example.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/architecture-analysis-guide.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/spec-authoring-guide.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-design → morph-phase-design}/references/spec-example.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/references/recap-example.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/references/vsa-implementation-guide.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/references/task-planning-patterns.md +0 -0
- /package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/references/tasks-example.md +0 -0
|
@@ -12,7 +12,7 @@ import { execSync } from 'child_process';
|
|
|
12
12
|
import { installMcpServers } from '../../utils/claude-settings-manager.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Load the enhanced MCP registry (
|
|
15
|
+
* Load the enhanced MCP registry (v3.0.0)
|
|
16
16
|
* @returns {Object} Registry with install blocks
|
|
17
17
|
*/
|
|
18
18
|
export function loadMcpRegistry() {
|
|
@@ -96,14 +96,26 @@ export function checkPrerequisites(mcpEntry) {
|
|
|
96
96
|
return results;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Check if an MCP config uses remote HTTP transport (no local process).
|
|
101
|
+
* @param {Object} config - MCP server config
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
export function isRemoteMcp(config) {
|
|
105
|
+
return config.type === 'http' || config.url != null;
|
|
106
|
+
}
|
|
107
|
+
|
|
99
108
|
/**
|
|
100
109
|
* Patch MCP config for the current platform.
|
|
101
110
|
* On Windows, npx must be invoked via `cmd /c` to avoid ENOENT errors.
|
|
102
111
|
* Registry already stores cmd/c format; this handles legacy configs that still use npx directly.
|
|
103
|
-
*
|
|
112
|
+
* Remote HTTP MCPs are returned as-is (no local process to patch).
|
|
113
|
+
* @param {Object} config - MCP server config { command, args, env? } or { type: "http", url }
|
|
104
114
|
* @returns {Object} Patched config (new object, original unmodified)
|
|
105
115
|
*/
|
|
106
116
|
export function patchConfigForPlatform(config) {
|
|
117
|
+
// Remote MCPs need no platform patching
|
|
118
|
+
if (isRemoteMcp(config)) return config;
|
|
107
119
|
if (config.command !== 'npx') return config;
|
|
108
120
|
return {
|
|
109
121
|
...config,
|
|
@@ -123,9 +135,12 @@ export async function installAutoMcps(targetPath, mcpsToInstall) {
|
|
|
123
135
|
|
|
124
136
|
for (const [name, entry] of Object.entries(mcpsToInstall)) {
|
|
125
137
|
let config = { ...entry.install.config };
|
|
126
|
-
//
|
|
127
|
-
if (
|
|
128
|
-
|
|
138
|
+
// Remote MCPs don't have env to strip
|
|
139
|
+
if (!isRemoteMcp(config)) {
|
|
140
|
+
// Only include env if it has actual values
|
|
141
|
+
if (config.env && Object.values(config.env).every(v => !v)) {
|
|
142
|
+
delete config.env;
|
|
143
|
+
}
|
|
129
144
|
}
|
|
130
145
|
servers[name] = patchConfigForPlatform(config);
|
|
131
146
|
}
|
|
@@ -156,6 +171,23 @@ export async function installMcpWithCredentials(targetPath, name, mcpEntry, cred
|
|
|
156
171
|
export function generateSetupInstructions(name, mcpEntry) {
|
|
157
172
|
const config = mcpEntry.install.config;
|
|
158
173
|
const credentials = mcpEntry.install.credentials || [];
|
|
174
|
+
const setupGuide = mcpEntry.install.setupGuide;
|
|
175
|
+
|
|
176
|
+
// Remote HTTP MCPs — show claude mcp add command instead of config snippet
|
|
177
|
+
if (isRemoteMcp(config)) {
|
|
178
|
+
const configSnippet = JSON.stringify({ [name]: config }, null, 2);
|
|
179
|
+
const credentialUrls = credentials.map(c => ({
|
|
180
|
+
name: c.name,
|
|
181
|
+
envVar: c.envVar,
|
|
182
|
+
helpUrl: c.helpUrl
|
|
183
|
+
}));
|
|
184
|
+
return {
|
|
185
|
+
configSnippet,
|
|
186
|
+
credentialUrls,
|
|
187
|
+
cliCommand: setupGuide || `claude mcp add --transport http ${name} ${config.url}`,
|
|
188
|
+
isRemote: true
|
|
189
|
+
};
|
|
190
|
+
}
|
|
159
191
|
|
|
160
192
|
// Build the config snippet with placeholder values
|
|
161
193
|
const snippetConfig = { ...config };
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import fs from 'fs';
|
|
13
|
-
import { existsSync } from 'fs';
|
|
14
13
|
import path, { join } from 'path';
|
|
15
14
|
import { loadState, derivePhase, getFeature } from '../../core/state/state-manager.js';
|
|
16
15
|
import { getWorkflowConfig } from '../../core/workflows/workflow-detector.js';
|
|
@@ -53,22 +52,28 @@ export const PHASES = {
|
|
|
53
52
|
requiredOutputs: ['proposal.md', 'spec.md'],
|
|
54
53
|
description: 'Clarify ambiguities and edge cases'
|
|
55
54
|
},
|
|
56
|
-
'
|
|
55
|
+
'plan': {
|
|
57
56
|
order: 5,
|
|
58
|
-
name: 'FASE 4:
|
|
57
|
+
name: 'FASE 4: PLAN',
|
|
59
58
|
requiredOutputs: ['proposal.md', 'spec.md'],
|
|
59
|
+
description: 'Planning phase before task breakdown'
|
|
60
|
+
},
|
|
61
|
+
'tasks': {
|
|
62
|
+
order: 6,
|
|
63
|
+
name: 'FASE 5: TASKS',
|
|
64
|
+
requiredOutputs: ['proposal.md', 'spec.md', 'plan.md'],
|
|
60
65
|
description: 'Break down into executable tasks'
|
|
61
66
|
},
|
|
62
67
|
'implement': {
|
|
63
|
-
order:
|
|
64
|
-
name: 'FASE
|
|
65
|
-
requiredOutputs: ['proposal.md', 'spec.md'],
|
|
68
|
+
order: 7,
|
|
69
|
+
name: 'FASE 6: IMPLEMENT',
|
|
70
|
+
requiredOutputs: ['proposal.md', 'spec.md', 'tasks.md'],
|
|
66
71
|
description: 'Execute tasks and implement code'
|
|
67
72
|
},
|
|
68
73
|
'sync': {
|
|
69
|
-
order:
|
|
70
|
-
name: 'FASE
|
|
71
|
-
requiredOutputs: ['proposal.md', 'spec.md', 'decisions.md'],
|
|
74
|
+
order: 8,
|
|
75
|
+
name: 'FASE 7: SYNC',
|
|
76
|
+
requiredOutputs: ['proposal.md', 'spec.md', 'decisions.md', 'recap.md'],
|
|
72
77
|
description: 'Sync decisions to project standards',
|
|
73
78
|
optional: true
|
|
74
79
|
}
|
|
@@ -86,8 +91,9 @@ const OUTPUT_PHASE_MAP = {
|
|
|
86
91
|
'ui-mockups.md': '2-ui',
|
|
87
92
|
'ui-components.md': '2-ui',
|
|
88
93
|
'ui-flows.md': '2-ui',
|
|
89
|
-
'
|
|
90
|
-
'
|
|
94
|
+
'plan.md': '3-plan',
|
|
95
|
+
'tasks.md': '4-tasks',
|
|
96
|
+
'recap.md': '5-implement'
|
|
91
97
|
};
|
|
92
98
|
|
|
93
99
|
function checkOutput(featurePath, outputFile) {
|
|
@@ -133,12 +139,12 @@ export function validatePhase(featureName, targetPhase) {
|
|
|
133
139
|
|
|
134
140
|
// Special validation: implement phase requires tasks.md to have actual tasks
|
|
135
141
|
if (targetPhase === 'implement') {
|
|
136
|
-
const tasksFilePath = path.join(featurePath, '
|
|
142
|
+
const tasksFilePath = path.join(featurePath, '4-tasks', 'tasks.md');
|
|
137
143
|
if (fs.existsSync(tasksFilePath)) {
|
|
138
144
|
const content = fs.readFileSync(tasksFilePath, 'utf-8');
|
|
139
145
|
const taskCount = (content.match(/^###\s+T\d+/gm) || []).length;
|
|
140
146
|
if (taskCount === 0) {
|
|
141
|
-
missingOutputs.push(`tasks.md exists but has no task entries — add ### T001
|
|
147
|
+
missingOutputs.push(`tasks.md exists but has no task entries — add ### T001: Task title entries`);
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
150
|
}
|
|
@@ -172,7 +178,7 @@ export function validatePhase(featureName, targetPhase) {
|
|
|
172
178
|
// Phase eligibility
|
|
173
179
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
174
180
|
|
|
175
|
-
const PHASE_ORDER = ['proposal', 'setup', 'uiux', 'design', 'clarify', 'tasks', 'implement', 'sync'];
|
|
181
|
+
const PHASE_ORDER = ['proposal', 'setup', 'uiux', 'design', 'clarify', 'plan', 'tasks', 'implement', 'sync'];
|
|
176
182
|
|
|
177
183
|
function getNextPhase(currentPhase) {
|
|
178
184
|
const idx = PHASE_ORDER.indexOf(currentPhase);
|
|
@@ -196,7 +202,7 @@ const OUTPUT_PATH_MAP = {
|
|
|
196
202
|
'proposal': '0-proposal/proposal.md',
|
|
197
203
|
'spec': '1-design/spec.md',
|
|
198
204
|
'contracts': '1-design/contracts.cs',
|
|
199
|
-
'tasks': '
|
|
205
|
+
'tasks': '4-tasks/tasks.md',
|
|
200
206
|
'schemaAnalysis': '1-design/schema-analysis.md',
|
|
201
207
|
};
|
|
202
208
|
|
|
@@ -209,7 +215,7 @@ function getMissingRequiredOutputs(featureName, phase, projectPath) {
|
|
|
209
215
|
|
|
210
216
|
for (const output of phaseDef.requiredOutputs) {
|
|
211
217
|
const relPath = OUTPUT_PATH_MAP[output];
|
|
212
|
-
if (relPath && !existsSync(join(featureBase, relPath))) {
|
|
218
|
+
if (relPath && !fs.existsSync(join(featureBase, relPath))) {
|
|
213
219
|
missing.push(output);
|
|
214
220
|
}
|
|
215
221
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Impact Analyzer for Scope Escalation
|
|
3
|
+
*
|
|
4
|
+
* Analyzes tasks.md + spec.md to determine the impact of a discovered
|
|
5
|
+
* complexity and recommend an escalation action.
|
|
6
|
+
*
|
|
7
|
+
* Pure functions — no I/O, no state mutations.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Classify impact based on affected task count and spec status.
|
|
12
|
+
*
|
|
13
|
+
* @param {{ affectedTasks: string[], specNeedsUpdate: boolean }} input
|
|
14
|
+
* @returns {{ impact: 'low'|'medium'|'high', recommendation: string, targetPhase?: string }}
|
|
15
|
+
*/
|
|
16
|
+
export function classifyImpact({ affectedTasks, specNeedsUpdate }) {
|
|
17
|
+
if (specNeedsUpdate) {
|
|
18
|
+
return { impact: 'high', recommendation: 'regress-design', targetPhase: 'design' };
|
|
19
|
+
}
|
|
20
|
+
if (affectedTasks.length <= 3) {
|
|
21
|
+
return { impact: 'low', recommendation: 'expand' };
|
|
22
|
+
}
|
|
23
|
+
return { impact: 'medium', recommendation: 'regress-tasks', targetPhase: 'tasks' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse task IDs from tasks.md content.
|
|
28
|
+
* @param {string} tasksMd
|
|
29
|
+
* @returns {string[]} Ordered task IDs
|
|
30
|
+
*/
|
|
31
|
+
function parseTaskIds(tasksMd) {
|
|
32
|
+
const ids = [];
|
|
33
|
+
const re = /^###\s+(T\d+)\s*[—–:\-]/gm;
|
|
34
|
+
let match;
|
|
35
|
+
while ((match = re.exec(tasksMd)) !== null) {
|
|
36
|
+
ids.push(match[1]);
|
|
37
|
+
}
|
|
38
|
+
return ids;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Find consecutive task IDs starting from a trigger task.
|
|
43
|
+
* Tasks are "consecutive" if their numeric IDs have no gaps.
|
|
44
|
+
*
|
|
45
|
+
* @param {string[]} allIds - All task IDs in order
|
|
46
|
+
* @param {string} triggerId - The trigger task ID
|
|
47
|
+
* @returns {string[]} Consecutive group including trigger
|
|
48
|
+
*/
|
|
49
|
+
function findConsecutiveGroup(allIds, triggerId) {
|
|
50
|
+
const triggerIdx = allIds.indexOf(triggerId);
|
|
51
|
+
if (triggerIdx === -1) return [triggerId];
|
|
52
|
+
|
|
53
|
+
const numOf = id => parseInt(id.replace(/\D/g, ''), 10);
|
|
54
|
+
|
|
55
|
+
const group = [triggerId];
|
|
56
|
+
|
|
57
|
+
// Look forward
|
|
58
|
+
for (let i = triggerIdx + 1; i < allIds.length; i++) {
|
|
59
|
+
const prevNum = numOf(allIds[i - 1]);
|
|
60
|
+
const currNum = numOf(allIds[i]);
|
|
61
|
+
if (currNum - prevNum <= 1) {
|
|
62
|
+
group.push(allIds[i]);
|
|
63
|
+
} else {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Look backward
|
|
69
|
+
for (let i = triggerIdx - 1; i >= 0; i--) {
|
|
70
|
+
const nextNum = numOf(allIds[i + 1]);
|
|
71
|
+
const currNum = numOf(allIds[i]);
|
|
72
|
+
if (nextNum - currNum <= 1) {
|
|
73
|
+
group.unshift(allIds[i]);
|
|
74
|
+
} else {
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return group;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Analyze the impact of a scope escalation.
|
|
84
|
+
*
|
|
85
|
+
* @param {{ triggerTaskId: string, tasksMd: string, specMd: string, reason: string }} input
|
|
86
|
+
* @returns {{ triggerTask: string, affectedTasks: string[], impact: string, recommendation: string, targetPhase?: string, reason: string }}
|
|
87
|
+
*/
|
|
88
|
+
export function analyzeImpact({ triggerTaskId, tasksMd, specMd, reason }) {
|
|
89
|
+
const allIds = parseTaskIds(tasksMd);
|
|
90
|
+
const affectedTasks = findConsecutiveGroup(allIds, triggerTaskId);
|
|
91
|
+
|
|
92
|
+
// Heuristic: detect phrases that indicate the spec itself is wrong, not just casual mentions.
|
|
93
|
+
// Requires phrases like "spec is wrong", "architecture needs", "contract mismatch", etc.
|
|
94
|
+
// Avoids false positives on casual uses like "the design of the component" or "schema file".
|
|
95
|
+
const specPhrases = /\b(spec\s+(is|was|needs|incorrect|wrong|outdated|assumed|missed)|architecture\s+(needs|is wrong|incorrect|mismatch)|contract\s+(mismatch|wrong|incorrect|needs|outdated)|schema\s+(needs|incorrect|wrong|mismatch|change))\b/i;
|
|
96
|
+
const specNeedsUpdate = specPhrases.test(reason);
|
|
97
|
+
|
|
98
|
+
const classification = classifyImpact({ affectedTasks, specNeedsUpdate });
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
triggerTask: triggerTaskId,
|
|
102
|
+
affectedTasks,
|
|
103
|
+
...classification,
|
|
104
|
+
reason
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse `stacks:` from YAML frontmatter content.
|
|
3
|
+
* Returns ['*'] if no stacks field found (backwards compatible — always install).
|
|
4
|
+
*
|
|
5
|
+
* Supports:
|
|
6
|
+
* stacks:\n - dotnet\n - blazor (YAML list)
|
|
7
|
+
* stacks: [dotnet, blazor] (inline array)
|
|
8
|
+
*
|
|
9
|
+
* @param {string} content - Raw file content with YAML frontmatter
|
|
10
|
+
* @returns {string[]}
|
|
11
|
+
*/
|
|
12
|
+
export function parseStacks(content) {
|
|
13
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
14
|
+
const fmMatch = normalized.match(/^---\n([\s\S]*?)\n---/);
|
|
15
|
+
if (!fmMatch) return ['*'];
|
|
16
|
+
|
|
17
|
+
const fm = fmMatch[1];
|
|
18
|
+
|
|
19
|
+
// Try inline format: stacks: [dotnet, blazor]
|
|
20
|
+
const inlineMatch = fm.match(/^stacks:\s*\[([^\]]*)\]/m);
|
|
21
|
+
if (inlineMatch) {
|
|
22
|
+
const items = inlineMatch[1].split(',').map(s => s.trim()).filter(Boolean);
|
|
23
|
+
return items.length > 0 ? items : ['*'];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Try YAML list format: stacks:\n - dotnet\n - blazor
|
|
27
|
+
const listMatch = fm.match(/^stacks:\s*\n((?:\s+-\s+.+\n?)*)/m);
|
|
28
|
+
if (listMatch) {
|
|
29
|
+
const items = listMatch[1]
|
|
30
|
+
.split('\n')
|
|
31
|
+
.map(line => line.replace(/^\s+-\s+/, '').replace(/["']/g, '').trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
return items.length > 0 ? items : ['*'];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return ['*'];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract project stack tags from config object.
|
|
41
|
+
* Splits `project.stack` by '-' to produce tag array.
|
|
42
|
+
*/
|
|
43
|
+
export function getProjectTags(config) {
|
|
44
|
+
const stack = config?.project?.stack;
|
|
45
|
+
if (!stack || typeof stack !== 'string') return [];
|
|
46
|
+
return stack.split('-').filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Determine if an asset should be installed for the given project tags.
|
|
51
|
+
* - stacks includes '*' → always install
|
|
52
|
+
* - stacks is empty → treat as '*' (backwards compatible)
|
|
53
|
+
* - Otherwise → install if intersection with projectTags is non-empty
|
|
54
|
+
*/
|
|
55
|
+
export function shouldInstall(assetStacks, projectTags) {
|
|
56
|
+
if (assetStacks.length === 0 || assetStacks.includes('*')) return true;
|
|
57
|
+
return assetStacks.some(s => projectTags.includes(s));
|
|
58
|
+
}
|
|
@@ -23,7 +23,7 @@ import { join } from 'path';
|
|
|
23
23
|
* @returns {Promise<Array<{id, title, status, dependencies, files, checkpoint}>>}
|
|
24
24
|
*/
|
|
25
25
|
export async function parseTasksMd(featureName, cwd = process.cwd()) {
|
|
26
|
-
const tasksPath = join(cwd, `.morph/features/${featureName}/
|
|
26
|
+
const tasksPath = join(cwd, `.morph/features/${featureName}/4-tasks/tasks.md`);
|
|
27
27
|
let content = '';
|
|
28
28
|
try {
|
|
29
29
|
content = await readFile(tasksPath, 'utf-8');
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility: emit a VALIDATION DISPATCH block to stdout.
|
|
3
|
+
*
|
|
4
|
+
* Both `advance-phase.js` and `task-manager.js` call this so that the
|
|
5
|
+
* PostToolUse dispatch hook can parse the JSON via strategy 1 (from
|
|
6
|
+
* tool_result) without falling back to a subprocess.
|
|
7
|
+
*
|
|
8
|
+
* @module emit-validator-dispatch
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { readFileSync } from 'fs';
|
|
13
|
+
import { join, dirname } from 'path';
|
|
14
|
+
import { fileURLToPath } from 'url';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Emit a VALIDATION DISPATCH block to stdout for the PostToolUse hook.
|
|
20
|
+
*
|
|
21
|
+
* Non-blocking — fails silently on any error.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} featureName - Feature name
|
|
24
|
+
* @param {string} phase - Current phase
|
|
25
|
+
* @param {string} cwd - Project root
|
|
26
|
+
*/
|
|
27
|
+
export async function emitValidatorDispatch(featureName, phase, cwd) {
|
|
28
|
+
try {
|
|
29
|
+
const { buildDispatchConfig } = await import('../../../commands/agents/dispatch-agents.js');
|
|
30
|
+
const config = await buildDispatchConfig(cwd, featureName, phase, { mode: 'validate' });
|
|
31
|
+
const validators = config.agents?.filter(a => a.tier === 4);
|
|
32
|
+
if (!validators || validators.length === 0) return;
|
|
33
|
+
|
|
34
|
+
const agentsPath = join(__dirname, '..', '..', '..', '..', 'framework', 'agents.json');
|
|
35
|
+
const agentsData = JSON.parse(readFileSync(agentsPath, 'utf8'));
|
|
36
|
+
const allAgents = agentsData.agents || {};
|
|
37
|
+
|
|
38
|
+
const validatorEntries = validators.map(v => {
|
|
39
|
+
const agentData = allAgents[v.id];
|
|
40
|
+
return {
|
|
41
|
+
id: v.id,
|
|
42
|
+
title: v.title,
|
|
43
|
+
severity: agentData?.hook_behavior?.severity || 'error',
|
|
44
|
+
blocksOnFail: agentData?.hook_behavior?.blocks_on_fail ?? true,
|
|
45
|
+
checks: agentData?.hook_behavior?.validates || [],
|
|
46
|
+
taskPrompt: v.taskPrompt,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const dispatch = {
|
|
51
|
+
validationRequired: true,
|
|
52
|
+
phase,
|
|
53
|
+
feature: featureName,
|
|
54
|
+
validators: validatorEntries,
|
|
55
|
+
instruction: 'Dispatch these validators as READ-ONLY subagents before continuing. Each must output { "passed": boolean, "issues": [] }. Blocking validators (blocksOnFail: true) must pass before marking phase complete.',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
console.log(chalk.cyan('\n--- VALIDATION DISPATCH ---'));
|
|
59
|
+
console.log(JSON.stringify(dispatch, null, 2));
|
|
60
|
+
console.log(chalk.cyan('--- END VALIDATION DISPATCH ---\n'));
|
|
61
|
+
} catch {
|
|
62
|
+
// Non-blocking — fail silently
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { join, dirname } from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { execSync } from 'child_process';
|
|
17
|
+
import { readFileSync, readdirSync, copyFileSync, unlinkSync } from 'fs';
|
|
17
18
|
import {
|
|
18
19
|
copyDirectory,
|
|
19
20
|
copyFile,
|
|
@@ -23,6 +24,8 @@ import {
|
|
|
23
24
|
writeFile,
|
|
24
25
|
updateGitignore
|
|
25
26
|
} from '../utils/file-copier.js';
|
|
27
|
+
import { parseStacks, getProjectTags, shouldInstall } from '../lib/stack-filter.js';
|
|
28
|
+
import { injectMorphImport } from '../utils/claude-md-injector.js';
|
|
26
29
|
import { saveProjectMorphVersion, getInstalledCLIVersion } from '../utils/version-checker.js';
|
|
27
30
|
import { installClaudeHooks, installGlobalStatusline, installVSCodeTerminalSettings, installShellIntegration } from '../utils/claude-settings-manager.js';
|
|
28
31
|
import { installSkills } from '../utils/skills-installer.js';
|
|
@@ -47,6 +50,35 @@ async function installRequiredPlugins(log, exec) {
|
|
|
47
50
|
}
|
|
48
51
|
}
|
|
49
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Install only stack-matching rules from rulesSrc to rulesDest.
|
|
55
|
+
* Also removes orphan rules in rulesDest that are no longer in the install set.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} rulesSrc - Source rules directory (framework/rules/)
|
|
58
|
+
* @param {string} rulesDest - Destination rules directory (.claude/rules/)
|
|
59
|
+
* @param {string[]} projectTags - Stack tags from config (e.g., ['dotnet', 'blazor'])
|
|
60
|
+
*/
|
|
61
|
+
export function installRulesForStack(rulesSrc, rulesDest, projectTags) {
|
|
62
|
+
const sourceFiles = readdirSync(rulesSrc).filter(f => f.endsWith('.md'));
|
|
63
|
+
const installed = new Set();
|
|
64
|
+
|
|
65
|
+
for (const file of sourceFiles) {
|
|
66
|
+
const content = readFileSync(join(rulesSrc, file), 'utf-8');
|
|
67
|
+
const stacks = parseStacks(content);
|
|
68
|
+
if (shouldInstall(stacks, projectTags)) {
|
|
69
|
+
copyFileSync(join(rulesSrc, file), join(rulesDest, file));
|
|
70
|
+
installed.add(file);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Cleanup orphans
|
|
75
|
+
for (const file of readdirSync(rulesDest).filter(f => f.endsWith('.md'))) {
|
|
76
|
+
if (!installed.has(file)) {
|
|
77
|
+
unlinkSync(join(rulesDest, file));
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
50
82
|
/**
|
|
51
83
|
* Installs MORPH-SPEC infrastructure into the target project directory.
|
|
52
84
|
* Headless — no prompts, no spinner (suppressed when MORPH_QUIET=1), no stack detection.
|
|
@@ -63,19 +95,12 @@ export async function setupInfra(targetPath, { _exec = execSync } = {}) {
|
|
|
63
95
|
// --- 0. Install required Claude Code plugins ---
|
|
64
96
|
await installRequiredPlugins(log, _exec);
|
|
65
97
|
|
|
66
|
-
// --- 1.
|
|
67
|
-
log('Step 1:
|
|
98
|
+
// --- 1. Inject @import into root CLAUDE.md (preserves user content) ---
|
|
99
|
+
log('Step 1: Setting up CLAUDE.md...');
|
|
68
100
|
const claudeMdSrc = join(FRAMEWORK_DIR, 'CLAUDE.md');
|
|
69
101
|
const claudeMdDest = join(targetPath, 'CLAUDE.md');
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const { readFile } = await import('../utils/file-copier.js');
|
|
73
|
-
const existingContent = await readFile(claudeMdDest);
|
|
74
|
-
if (!existingContent.includes('MORPH-SPEC')) {
|
|
75
|
-
await copyFile(claudeMdDest, `${claudeMdDest}.backup`);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
await copyFile(claudeMdSrc, claudeMdDest);
|
|
102
|
+
const importResult = await injectMorphImport(claudeMdDest);
|
|
103
|
+
log(` ✓ Root CLAUDE.md: ${importResult}`);
|
|
79
104
|
|
|
80
105
|
// --- 2. Create .morph directory structure ---
|
|
81
106
|
log('Step 2: Creating .morph structure...');
|
|
@@ -163,27 +188,37 @@ export async function setupInfra(targetPath, { _exec = execSync } = {}) {
|
|
|
163
188
|
await copyDirectory(commandsSrc, commandsDest);
|
|
164
189
|
}
|
|
165
190
|
|
|
166
|
-
// ---
|
|
191
|
+
// --- Compute project tags for stack-aware installation ---
|
|
192
|
+
let projectTags = [];
|
|
193
|
+
if (await pathExists(configPath)) {
|
|
194
|
+
try {
|
|
195
|
+
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
196
|
+
projectTags = getProjectTags(config);
|
|
197
|
+
} catch { /* ignore malformed config */ }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// --- 10. Copy framework/rules → .claude/rules (filtered by stack) ---
|
|
167
201
|
log('Step 10: Installing path-scoped rules...');
|
|
168
202
|
const rulesSrc = join(FRAMEWORK_DIR, 'rules');
|
|
169
203
|
const rulesDest = join(claudeDest, 'rules');
|
|
170
204
|
if (await pathExists(rulesSrc)) {
|
|
171
|
-
await
|
|
205
|
+
await ensureDir(rulesDest);
|
|
206
|
+
installRulesForStack(rulesSrc, rulesDest, projectTags);
|
|
172
207
|
}
|
|
173
208
|
|
|
174
|
-
// --- 11. installSkills ---
|
|
209
|
+
// --- 11. installSkills (filtered by stack) ---
|
|
175
210
|
log('Step 11: Installing skills...');
|
|
176
|
-
await installSkills(targetPath);
|
|
211
|
+
await installSkills(targetPath, { projectTags });
|
|
177
212
|
|
|
178
213
|
// --- 12. installAgents ---
|
|
179
214
|
log('Step 12: Installing agents...');
|
|
180
215
|
const agentCounts = await installAgents(targetPath, FRAMEWORK_DIR, { projectStack: null });
|
|
181
216
|
|
|
182
|
-
// --- 13. installDomainAgents ---
|
|
217
|
+
// --- 13. installDomainAgents (filtered by stack) ---
|
|
183
218
|
log('Step 13: Installing domain agents...');
|
|
184
|
-
const domainCounts = await installDomainAgents(targetPath, FRAMEWORK_DIR);
|
|
219
|
+
const domainCounts = await installDomainAgents(targetPath, FRAMEWORK_DIR, { projectTags });
|
|
185
220
|
|
|
186
|
-
// --- 14. Copy framework/CLAUDE.md → .claude/CLAUDE.md ---
|
|
221
|
+
// --- 14. Copy framework/CLAUDE.md → .claude/CLAUDE.md (morph-owned, gitignored) ---
|
|
187
222
|
log('Step 14: Installing .claude/CLAUDE.md...');
|
|
188
223
|
const runtimeClaudeMdDest = join(claudeDest, 'CLAUDE.md');
|
|
189
224
|
if (await pathExists(claudeMdSrc)) {
|
|
@@ -228,6 +263,21 @@ export async function setupInfra(targetPath, { _exec = execSync } = {}) {
|
|
|
228
263
|
log('Step 18: Updating .gitignore...');
|
|
229
264
|
await updateGitignore(targetPath);
|
|
230
265
|
|
|
266
|
+
// --- 19. Initialize state.json (only if not exists) ---
|
|
267
|
+
log('Step 19: Initializing state.json...');
|
|
268
|
+
const statePath = join(morphPath, 'state.json');
|
|
269
|
+
if (!(await pathExists(statePath))) {
|
|
270
|
+
const dirName = targetPath.split(/[\/\\]/).pop();
|
|
271
|
+
const now = new Date().toISOString();
|
|
272
|
+
await writeJson(statePath, {
|
|
273
|
+
version: '5.0.0',
|
|
274
|
+
project: { name: dirName, type: 'unknown', createdAt: now, updatedAt: now },
|
|
275
|
+
features: {},
|
|
276
|
+
threads: {},
|
|
277
|
+
metadata: { totalFeatures: 0, completedFeatures: 0, totalTimeSpent: 0, lastUpdated: now }
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
231
281
|
log('setup-infra: complete.');
|
|
232
282
|
|
|
233
283
|
return {
|