@polymorphism-tech/morph-spec 4.8.19 → 4.10.0
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 +21 -0
- package/README.md +2 -2
- package/bin/morph-spec.js +44 -55
- package/bin/task-manager.js +133 -20
- package/bin/validate.js +67 -33
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +201 -203
- package/docs/QUICKSTART.md +2 -2
- package/framework/CLAUDE.md +99 -77
- package/framework/agents.json +734 -182
- package/framework/commands/commit.md +166 -0
- package/framework/commands/morph-apply.md +13 -2
- package/framework/commands/morph-archive.md +8 -2
- package/framework/commands/morph-infra.md +6 -0
- package/framework/commands/morph-preflight.md +6 -0
- package/framework/commands/morph-proposal.md +56 -7
- package/framework/commands/morph-status.md +6 -0
- package/framework/commands/morph-troubleshoot.md +6 -0
- package/framework/hooks/claude-code/notification/approval-reminder.js +3 -2
- package/framework/hooks/claude-code/post-tool-use/context-refresh.js +1 -1
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +155 -32
- package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +78 -0
- 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 +4 -3
- 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 +124 -2
- package/framework/hooks/claude-code/session-start/post-compact-restore.js +41 -0
- package/framework/hooks/claude-code/statusline.py +76 -30
- 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/claude-code/user-prompt/set-terminal-title.js +14 -6
- package/framework/hooks/shared/activity-logger.js +0 -24
- 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 +12 -5
- package/framework/hooks/shared/skill-reminder-helpers.js +79 -0
- package/framework/hooks/shared/stale-task-reset.js +57 -0
- package/framework/hooks/shared/state-reader.js +29 -5
- package/framework/hooks/shared/worktree-helpers.js +53 -0
- package/framework/phases.json +69 -14
- package/framework/rules/morph-workflow.md +88 -86
- package/framework/skills/level-0-meta/mcp-registry.json +86 -51
- package/framework/skills/level-0-meta/{brainstorming → morph-brainstorming}/SKILL.md +14 -17
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
- package/framework/skills/level-0-meta/{code-review → morph-code-review}/SKILL.md +2 -2
- package/framework/skills/level-0-meta/{code-review-nextjs → morph-code-review-nextjs}/SKILL.md +163 -163
- package/framework/skills/level-0-meta/{frontend-review → morph-frontend-review}/SKILL.md +9 -9
- package/framework/skills/level-0-meta/morph-init/SKILL.md +77 -12
- package/framework/skills/level-0-meta/{post-implementation → morph-post-implementation}/SKILL.md +62 -15
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +5 -5
- package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +1 -1
- package/framework/skills/level-0-meta/{simulation-checklist → morph-simulation-checklist}/SKILL.md +1 -1
- package/framework/skills/level-0-meta/{terminal-title → morph-terminal-title}/SKILL.md +2 -2
- package/framework/skills/level-0-meta/{tool-usage-guide → morph-tool-usage-guide}/SKILL.md +3 -4
- package/framework/skills/level-0-meta/{tool-usage-guide → morph-tool-usage-guide}/references/tools-per-phase.md +7 -7
- package/framework/skills/level-0-meta/{verification-before-completion → morph-verification-before-completion}/SKILL.md +2 -2
- 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 +238 -0
- package/framework/skills/level-1-workflows/{phase-codebase-analysis → morph-phase-codebase-analysis}/SKILL.md +3 -3
- package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +507 -0
- package/framework/skills/level-1-workflows/{phase-implement → morph-phase-implement}/SKILL.md +168 -27
- 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 +254 -0
- package/framework/skills/level-1-workflows/{phase-setup → morph-phase-setup}/SKILL.md +50 -3
- package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/SKILL.md +48 -11
- package/framework/skills/level-1-workflows/{phase-tasks → morph-phase-tasks}/scripts/validate-tasks.mjs +3 -3
- package/framework/skills/level-1-workflows/{phase-uiux → morph-phase-uiux}/SKILL.md +46 -11
- package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +97 -0
- package/framework/standards/STANDARDS.json +640 -88
- package/framework/standards/infrastructure/vercel/vercel-database.md +106 -0
- package/framework/standards/integration/mcp/mcp-tools.md +25 -7
- package/framework/templates/REGISTRY.json +1825 -1909
- package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
- package/framework/templates/docs/onboarding.md +3 -7
- package/package.json +2 -7
- package/src/commands/agents/dispatch-agents.js +104 -6
- package/src/commands/mcp/mcp-setup.js +39 -2
- package/src/commands/phase/phase-reset.js +74 -0
- package/src/commands/project/doctor.js +34 -51
- package/src/commands/project/init.js +1 -1
- package/src/commands/project/status.js +2 -2
- package/src/commands/project/update.js +381 -365
- package/src/commands/project/worktree.js +154 -0
- package/src/commands/scope/escalate.js +215 -0
- package/src/commands/state/advance-phase.js +132 -68
- package/src/commands/state/approve.js +2 -2
- package/src/commands/state/index.js +7 -8
- package/src/commands/state/phase-runner.js +1 -1
- package/src/commands/state/state.js +61 -6
- package/src/commands/task/expand.js +100 -0
- package/src/commands/tasks/task.js +78 -99
- package/src/commands/templates/template-render.js +93 -173
- package/src/commands/trust/trust.js +26 -21
- package/src/core/paths/output-schema.js +19 -3
- package/src/core/state/phase-state-machine.js +7 -4
- package/src/core/state/state-manager.js +32 -57
- package/src/core/workflows/workflow-detector.js +9 -87
- 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 +336 -0
- package/src/lib/scope/impact-analyzer.js +106 -0
- package/src/lib/stack/stack-profile.js +88 -0
- package/src/lib/tasks/task-classifier.js +16 -0
- package/src/lib/tasks/task-parser.js +1 -1
- package/src/lib/tasks/test-runner.js +77 -0
- package/src/lib/trust/trust-manager.js +32 -144
- package/src/lib/validators/shared/emit-validator-dispatch.js +64 -0
- package/src/lib/validators/spec-validator.js +58 -4
- package/src/lib/validators/validation-runner.js +23 -11
- package/src/scripts/setup-infra.js +255 -224
- package/src/utils/agents-installer.js +34 -14
- package/src/utils/banner.js +1 -1
- package/src/utils/claude-settings-manager.js +1 -1
- package/src/utils/file-copier.js +1 -1
- package/src/utils/hooks-installer.js +272 -8
- package/framework/hooks/dev/check-sync-health.js +0 -117
- package/framework/hooks/dev/guard-version-numbers.js +0 -57
- package/framework/hooks/dev/sync-standards-registry.js +0 -60
- package/framework/hooks/dev/sync-template-registry.js +0 -60
- package/framework/hooks/dev/validate-skill-format.js +0 -70
- package/framework/hooks/dev/validate-standard-format.js +0 -73
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +0 -190
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +0 -366
- package/framework/templates/meta-prompts/hops/hop-retry.md +0 -78
- package/framework/templates/meta-prompts/hops/hop-validation.md +0 -97
- package/framework/templates/meta-prompts/hops/hop-wrapper.md +0 -36
- package/framework/workflows/configs/design-impl.json +0 -49
- package/framework/workflows/configs/express.json +0 -45
- package/framework/workflows/configs/fast-track.json +0 -42
- package/framework/workflows/configs/full-morph.json +0 -79
- package/framework/workflows/configs/fusion.json +0 -39
- package/framework/workflows/configs/long-running.json +0 -33
- package/framework/workflows/configs/spec-only.json +0 -43
- package/framework/workflows/configs/ui-refresh.json +0 -49
- package/framework/workflows/configs/zero-touch.json +0 -82
- package/src/commands/project/index.js +0 -8
- package/src/commands/project/monitor.js +0 -295
- package/src/commands/project/tutorial.js +0 -115
- package/src/commands/state/validate-phase.js +0 -238
- package/src/commands/templates/generate-contracts.js +0 -445
- package/src/core/index.js +0 -10
- package/src/core/orchestrator.js +0 -171
- package/src/core/registry/command-registry.js +0 -28
- package/src/core/registry/index.js +0 -8
- package/src/core/registry/validator-registry.js +0 -204
- 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/templates/template-validator.js +0 -296
- package/src/core/workflows/index.js +0 -7
- package/src/generator/config-generator.js +0 -206
- package/src/generator/templates/config.json.template +0 -40
- package/src/generator/templates/project.md.template +0 -67
- package/src/lib/agents/micro-agent-factory.js +0 -161
- package/src/lib/analysis/complexity-analyzer.js +0 -441
- package/src/lib/analysis/index.js +0 -7
- package/src/lib/analytics/analytics-engine.js +0 -345
- package/src/lib/checkpoints/checkpoint-hooks.js +0 -298
- package/src/lib/checkpoints/index.js +0 -7
- package/src/lib/context/context-bundler.js +0 -241
- package/src/lib/context/context-optimizer.js +0 -212
- package/src/lib/context/context-tracker.js +0 -273
- package/src/lib/context/core-four-tracker.js +0 -201
- package/src/lib/context/mcp-optimizer.js +0 -200
- 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/execution/fusion-executor.js +0 -304
- package/src/lib/execution/parallel-executor.js +0 -270
- package/src/lib/hooks/stop-hook-executor.js +0 -286
- package/src/lib/hops/hop-composer.js +0 -221
- 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/lib/phase-chain/eligibility-checker.js +0 -243
- package/src/lib/threads/thread-coordinator.js +0 -238
- package/src/lib/threads/thread-manager.js +0 -317
- package/src/lib/tracking/artifact-trail.js +0 -202
- package/src/sanitizer/context-sanitizer.js +0 -221
- package/src/sanitizer/patterns.js +0 -163
- package/src/scanner/project-scanner.js +0 -242
- package/src/ui/diff-display.js +0 -91
- package/src/ui/interactive-wizard.js +0 -96
- package/src/ui/user-review.js +0 -211
- package/src/ui/wizard-questions.js +0 -188
- package/src/utils/color-utils.js +0 -70
- package/src/utils/process-handler.js +0 -97
- 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-0-meta/{terminal-title → morph-terminal-title}/scripts/set_title.sh +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 };
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase Validator — centralized phase validation and eligibility checking.
|
|
3
|
+
*
|
|
4
|
+
* Exports: PHASES, validatePhase, computePassRate, checkPhaseEligibility
|
|
5
|
+
*
|
|
6
|
+
* Callers:
|
|
7
|
+
* - src/commands/state/advance-phase.js
|
|
8
|
+
* - src/commands/state/phase-runner.js
|
|
9
|
+
* - src/commands/project/status.js
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from 'fs';
|
|
13
|
+
import path, { join } from 'path';
|
|
14
|
+
import { loadState, derivePhase, getFeature } from '../../core/state/state-manager.js';
|
|
15
|
+
import { getWorkflowConfig } from '../../core/workflows/workflow-detector.js';
|
|
16
|
+
import { shouldAutoApprove } from '../trust/trust-manager.js';
|
|
17
|
+
|
|
18
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
// Phase definitions (from validate-phase.js)
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
export const PHASES = {
|
|
23
|
+
'proposal': {
|
|
24
|
+
order: 0,
|
|
25
|
+
name: 'FASE 0: PROPOSAL',
|
|
26
|
+
requiredOutputs: [],
|
|
27
|
+
description: 'Initial proposal and agent detection'
|
|
28
|
+
},
|
|
29
|
+
'setup': {
|
|
30
|
+
order: 1,
|
|
31
|
+
name: 'FASE 1: SETUP',
|
|
32
|
+
requiredOutputs: ['proposal.md'],
|
|
33
|
+
description: 'Load context and standards'
|
|
34
|
+
},
|
|
35
|
+
'uiux': {
|
|
36
|
+
order: 2,
|
|
37
|
+
name: 'FASE 1.5: UI/UX DESIGN',
|
|
38
|
+
requiredOutputs: ['proposal.md'],
|
|
39
|
+
optionalOutputs: ['ui-design-system.md', 'ui-mockups.md', 'ui-components.md', 'ui-flows.md'],
|
|
40
|
+
description: 'UI/UX design (conditional - only if frontend)',
|
|
41
|
+
optional: true
|
|
42
|
+
},
|
|
43
|
+
'design': {
|
|
44
|
+
order: 3,
|
|
45
|
+
name: 'FASE 2: DESIGN',
|
|
46
|
+
requiredOutputs: ['proposal.md'],
|
|
47
|
+
description: 'Technical specification and contracts'
|
|
48
|
+
},
|
|
49
|
+
'clarify': {
|
|
50
|
+
order: 4,
|
|
51
|
+
name: 'FASE 3: CLARIFY',
|
|
52
|
+
requiredOutputs: ['proposal.md', 'spec.md'],
|
|
53
|
+
description: 'Clarify ambiguities and edge cases'
|
|
54
|
+
},
|
|
55
|
+
'plan': {
|
|
56
|
+
order: 5,
|
|
57
|
+
name: 'FASE 4: PLAN',
|
|
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'],
|
|
65
|
+
description: 'Break down into executable tasks'
|
|
66
|
+
},
|
|
67
|
+
'implement': {
|
|
68
|
+
order: 7,
|
|
69
|
+
name: 'FASE 6: IMPLEMENT',
|
|
70
|
+
requiredOutputs: ['proposal.md', 'spec.md', 'tasks.md'],
|
|
71
|
+
description: 'Execute tasks and implement code'
|
|
72
|
+
},
|
|
73
|
+
'sync': {
|
|
74
|
+
order: 8,
|
|
75
|
+
name: 'FASE 7: SYNC',
|
|
76
|
+
requiredOutputs: ['proposal.md', 'spec.md', 'decisions.md', 'recap.md'],
|
|
77
|
+
description: 'Sync decisions to project standards',
|
|
78
|
+
optional: true
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Map output filenames to their phase subfolders
|
|
83
|
+
const OUTPUT_PHASE_MAP = {
|
|
84
|
+
'proposal.md': '0-proposal',
|
|
85
|
+
'schema-analysis.md': '1-design',
|
|
86
|
+
'spec.md': '1-design',
|
|
87
|
+
'clarifications.md': '1-design',
|
|
88
|
+
'contracts.cs': '1-design',
|
|
89
|
+
'decisions.md': '1-design',
|
|
90
|
+
'ui-design-system.md': '2-ui',
|
|
91
|
+
'ui-mockups.md': '2-ui',
|
|
92
|
+
'ui-components.md': '2-ui',
|
|
93
|
+
'ui-flows.md': '2-ui',
|
|
94
|
+
'plan.md': '3-plan',
|
|
95
|
+
'tasks.md': '4-tasks',
|
|
96
|
+
'recap.md': '5-implement'
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function checkOutput(featurePath, outputFile) {
|
|
100
|
+
const phaseDir = OUTPUT_PHASE_MAP[outputFile] || '';
|
|
101
|
+
const filePath = path.join(featurePath, phaseDir, outputFile);
|
|
102
|
+
return fs.existsSync(filePath);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate phase prerequisites for a feature.
|
|
107
|
+
* Used by advance-phase.js before advancing.
|
|
108
|
+
*/
|
|
109
|
+
export function validatePhase(featureName, targetPhase) {
|
|
110
|
+
const featurePath = path.join(process.cwd(), '.morph/features', featureName);
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(featurePath)) {
|
|
113
|
+
return {
|
|
114
|
+
valid: false,
|
|
115
|
+
error: `Feature directory not found: ${featurePath}`,
|
|
116
|
+
suggestion: `Run 'morph-spec state init ${featureName}' to start`,
|
|
117
|
+
missingOutputs: [],
|
|
118
|
+
phase: null
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const phaseDefinition = PHASES[targetPhase];
|
|
123
|
+
if (!phaseDefinition) {
|
|
124
|
+
return {
|
|
125
|
+
valid: false,
|
|
126
|
+
error: `Unknown phase: ${targetPhase}`,
|
|
127
|
+
validPhases: Object.keys(PHASES),
|
|
128
|
+
missingOutputs: [],
|
|
129
|
+
phase: null
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const missingOutputs = [];
|
|
134
|
+
for (const output of phaseDefinition.requiredOutputs) {
|
|
135
|
+
if (!checkOutput(featurePath, output)) {
|
|
136
|
+
missingOutputs.push(output);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Special validation: implement phase requires tasks.md to have actual tasks
|
|
141
|
+
if (targetPhase === 'implement') {
|
|
142
|
+
const tasksFilePath = path.join(featurePath, '4-tasks', 'tasks.md');
|
|
143
|
+
if (fs.existsSync(tasksFilePath)) {
|
|
144
|
+
const content = fs.readFileSync(tasksFilePath, 'utf-8');
|
|
145
|
+
const taskCount = (content.match(/^###\s+T\d+/gm) || []).length;
|
|
146
|
+
if (taskCount === 0) {
|
|
147
|
+
missingOutputs.push(`tasks.md exists but has no task entries — add ### T001: Task title entries`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Warn if skipping phases
|
|
153
|
+
let stateWarning = null;
|
|
154
|
+
try {
|
|
155
|
+
const feature = getFeature(featureName);
|
|
156
|
+
if (feature) {
|
|
157
|
+
const currentPhaseOrder = PHASES[feature.phase]?.order ?? -1;
|
|
158
|
+
const targetPhaseOrder = phaseDefinition.order;
|
|
159
|
+
if (targetPhaseOrder > currentPhaseOrder + 1) {
|
|
160
|
+
stateWarning = `Skipping phases: current is '${feature.phase}' (order ${currentPhaseOrder}), target is '${targetPhase}' (order ${targetPhaseOrder})`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// State file may not exist
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
valid: missingOutputs.length === 0,
|
|
169
|
+
error: null,
|
|
170
|
+
missingOutputs,
|
|
171
|
+
phase: phaseDefinition,
|
|
172
|
+
featurePath,
|
|
173
|
+
stateWarning
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
178
|
+
// Phase eligibility
|
|
179
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
const PHASE_ORDER = ['proposal', 'setup', 'uiux', 'design', 'clarify', 'plan', 'tasks', 'implement', 'sync'];
|
|
182
|
+
|
|
183
|
+
function getNextPhase(currentPhase) {
|
|
184
|
+
const idx = PHASE_ORDER.indexOf(currentPhase);
|
|
185
|
+
if (idx === -1 || idx >= PHASE_ORDER.length - 1) return null;
|
|
186
|
+
return PHASE_ORDER[idx + 1];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Compute overall validation pass rate from validationHistory.
|
|
191
|
+
* Returns null when no history exists.
|
|
192
|
+
*/
|
|
193
|
+
export function computePassRate(validationHistory) {
|
|
194
|
+
if (!validationHistory || Object.keys(validationHistory).length === 0) return null;
|
|
195
|
+
const entries = Object.values(validationHistory);
|
|
196
|
+
const passed = entries.filter(e => e.status === 'passed').length;
|
|
197
|
+
return entries.length > 0 ? passed / entries.length : null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Map output type names to file paths for eligibility checks
|
|
201
|
+
const OUTPUT_PATH_MAP = {
|
|
202
|
+
'proposal': '0-proposal/proposal.md',
|
|
203
|
+
'spec': '1-design/spec.md',
|
|
204
|
+
'contracts': '1-design/contracts.cs',
|
|
205
|
+
'tasks': '4-tasks/tasks.md',
|
|
206
|
+
'schemaAnalysis': '1-design/schema-analysis.md',
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
function getMissingRequiredOutputs(featureName, phase, projectPath) {
|
|
210
|
+
const phaseDef = PHASES[phase];
|
|
211
|
+
if (!phaseDef?.requiredOutputs) return [];
|
|
212
|
+
|
|
213
|
+
const missing = [];
|
|
214
|
+
const featureBase = join(projectPath, '.morph', 'features', featureName);
|
|
215
|
+
|
|
216
|
+
for (const output of phaseDef.requiredOutputs) {
|
|
217
|
+
const relPath = OUTPUT_PATH_MAP[output];
|
|
218
|
+
if (relPath && !fs.existsSync(join(featureBase, relPath))) {
|
|
219
|
+
missing.push(output);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return missing;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getBlockedTasks(feature) {
|
|
227
|
+
const history = feature.validationHistory || {};
|
|
228
|
+
return Object.entries(history)
|
|
229
|
+
.filter(([, entry]) => entry.status === 'blocked')
|
|
230
|
+
.map(([taskId]) => taskId);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check whether a feature is eligible to auto-advance to the next phase.
|
|
235
|
+
* Used by phase-runner.js.
|
|
236
|
+
*/
|
|
237
|
+
export function checkPhaseEligibility(featureName, opts = {}) {
|
|
238
|
+
const projectPath = opts.projectPath || process.cwd();
|
|
239
|
+
|
|
240
|
+
const state = loadState(false);
|
|
241
|
+
if (!state) {
|
|
242
|
+
return {
|
|
243
|
+
eligible: false,
|
|
244
|
+
blockers: [{ type: 'state_error', items: ['state.json not found'] }],
|
|
245
|
+
currentPhase: 'unknown',
|
|
246
|
+
nextPhase: null,
|
|
247
|
+
passRate: null,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const feature = state.features?.[featureName];
|
|
252
|
+
if (!feature) {
|
|
253
|
+
return {
|
|
254
|
+
eligible: false,
|
|
255
|
+
blockers: [{ type: 'state_error', items: [`Feature '${featureName}' not found in state.json`] }],
|
|
256
|
+
currentPhase: 'unknown',
|
|
257
|
+
nextPhase: null,
|
|
258
|
+
passRate: null,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const featureFolderPath = join(projectPath, '.morph', 'features', featureName);
|
|
263
|
+
const currentPhase = feature.phase || derivePhase(featureFolderPath);
|
|
264
|
+
const nextPhase = getNextPhase(currentPhase);
|
|
265
|
+
|
|
266
|
+
if (!nextPhase) {
|
|
267
|
+
return {
|
|
268
|
+
eligible: false,
|
|
269
|
+
blockers: [{ type: 'state_error', items: ['Feature is at the final phase'] }],
|
|
270
|
+
currentPhase,
|
|
271
|
+
nextPhase: null,
|
|
272
|
+
passRate: null,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
let workflowConfig = null;
|
|
277
|
+
if (feature.workflow && feature.workflow !== 'auto') {
|
|
278
|
+
try {
|
|
279
|
+
workflowConfig = getWorkflowConfig(feature.workflow);
|
|
280
|
+
} catch {
|
|
281
|
+
// Non-blocking: fall through to defaults
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const minPassRate = workflowConfig?.phaseChain?.pauseOn
|
|
286
|
+
? 0.80
|
|
287
|
+
: workflowConfig?.eligibility?.minPassRate ?? 0.80;
|
|
288
|
+
|
|
289
|
+
const blockers = [];
|
|
290
|
+
|
|
291
|
+
const missingOutputs = getMissingRequiredOutputs(featureName, currentPhase, projectPath);
|
|
292
|
+
if (missingOutputs.length > 0) {
|
|
293
|
+
blockers.push({ type: 'missing_outputs', items: missingOutputs });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const blockedTasks = getBlockedTasks(feature);
|
|
297
|
+
if (blockedTasks.length > 0) {
|
|
298
|
+
blockers.push({ type: 'blocked_tasks', items: blockedTasks });
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const passRate = computePassRate(feature.validationHistory);
|
|
302
|
+
if (passRate !== null && passRate < minPassRate) {
|
|
303
|
+
blockers.push({
|
|
304
|
+
type: 'low_pass_rate',
|
|
305
|
+
current: passRate,
|
|
306
|
+
required: minPassRate,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const requiredGateMap = { design: 'design', tasks: 'tasks', uiux: 'uiux' };
|
|
312
|
+
const gate = requiredGateMap[currentPhase];
|
|
313
|
+
if (gate) {
|
|
314
|
+
const trustResult = shouldAutoApprove(featureName, gate);
|
|
315
|
+
if (!trustResult.autoApprove) {
|
|
316
|
+
const gateStatus = feature.approvalGates?.[gate];
|
|
317
|
+
if (!gateStatus?.approved) {
|
|
318
|
+
blockers.push({
|
|
319
|
+
type: 'trust_too_low',
|
|
320
|
+
items: [`Gate '${gate}' requires trust level ${trustResult.level || 'medium+'}`],
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
// Trust manager unavailable — non-blocking
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return {
|
|
330
|
+
eligible: blockers.length === 0,
|
|
331
|
+
blockers,
|
|
332
|
+
currentPhase,
|
|
333
|
+
nextPhase,
|
|
334
|
+
passRate,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
@@ -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,88 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
/** @typedef {'nextjs'|'blazor'|'dotnet'|'nodejs-cli'|'unknown'} StackName */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {Object} StackProfile
|
|
8
|
+
* @property {StackName} stack
|
|
9
|
+
* @property {boolean} isDotnet
|
|
10
|
+
* @property {boolean} isNextjs
|
|
11
|
+
* @property {string[]} defaultValidators
|
|
12
|
+
* @property {string[]} checkpointValidators
|
|
13
|
+
* @property {'contracts'|'contractsTs'} contractsOutputType
|
|
14
|
+
* @property {'contracts.cs'|'contracts.ts'} contractsFilename
|
|
15
|
+
* @property {'csharp'|'typescript'} contractsLanguage
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const DOTNET_STACKS = new Set(['dotnet', 'blazor', 'aspnet', 'dotnet-blazor']);
|
|
19
|
+
const NEXTJS_STACKS = new Set(['nextjs', 'next', 'next.js']);
|
|
20
|
+
const NO_VALIDATOR_STACKS = new Set(['nodejs-cli', 'node-cli', 'cli']);
|
|
21
|
+
|
|
22
|
+
const PROFILES = {
|
|
23
|
+
dotnet: {
|
|
24
|
+
isDotnet: true, isNextjs: false,
|
|
25
|
+
defaultValidators: ['architecture', 'packages'],
|
|
26
|
+
checkpointValidators: ['architecture', 'packages', 'design-system', 'security'],
|
|
27
|
+
contractsOutputType: 'contracts',
|
|
28
|
+
contractsFilename: 'contracts.cs',
|
|
29
|
+
contractsLanguage: 'csharp',
|
|
30
|
+
},
|
|
31
|
+
nextjs: {
|
|
32
|
+
isDotnet: false, isNextjs: true,
|
|
33
|
+
defaultValidators: ['nextjs-component'],
|
|
34
|
+
checkpointValidators: ['nextjs-component', 'css'],
|
|
35
|
+
contractsOutputType: 'contractsTs',
|
|
36
|
+
contractsFilename: 'contracts.ts',
|
|
37
|
+
contractsLanguage: 'typescript',
|
|
38
|
+
},
|
|
39
|
+
empty: {
|
|
40
|
+
isDotnet: false, isNextjs: false,
|
|
41
|
+
defaultValidators: [],
|
|
42
|
+
checkpointValidators: [],
|
|
43
|
+
contractsOutputType: 'contracts',
|
|
44
|
+
contractsFilename: 'contracts.cs',
|
|
45
|
+
contractsLanguage: 'csharp',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Read project stack from .morph/config/config.json.
|
|
51
|
+
* @param {string} cwd - Project root
|
|
52
|
+
* @returns {string|null}
|
|
53
|
+
*/
|
|
54
|
+
function readStack(cwd) {
|
|
55
|
+
const configPath = join(cwd, '.morph', 'config', 'config.json');
|
|
56
|
+
if (!existsSync(configPath)) return null;
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(readFileSync(configPath, 'utf8')).project?.stack ?? null;
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Get the StackProfile for the current project.
|
|
66
|
+
* @param {string} [cwd] - Project root. Defaults to process.cwd().
|
|
67
|
+
* @param {string} [stackOverride] - Skip config read and use this stack directly.
|
|
68
|
+
* @returns {StackProfile}
|
|
69
|
+
*/
|
|
70
|
+
export function getStackProfile(cwd, stackOverride) {
|
|
71
|
+
const root = cwd ?? process.cwd();
|
|
72
|
+
const raw = stackOverride ?? readStack(root);
|
|
73
|
+
const stack = raw?.toLowerCase() ?? 'unknown';
|
|
74
|
+
|
|
75
|
+
let profile;
|
|
76
|
+
if (NEXTJS_STACKS.has(stack)) {
|
|
77
|
+
profile = PROFILES.nextjs;
|
|
78
|
+
} else if (NO_VALIDATOR_STACKS.has(stack)) {
|
|
79
|
+
profile = PROFILES.empty;
|
|
80
|
+
} else if (DOTNET_STACKS.has(stack) || stack === 'unknown') {
|
|
81
|
+
profile = PROFILES.dotnet;
|
|
82
|
+
} else {
|
|
83
|
+
// Unknown non-.NET stack → empty validators, dotnet contracts (backward compat)
|
|
84
|
+
profile = PROFILES.empty;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return { stack, ...profile };
|
|
88
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Task classifier — identifies test/validation tasks by title.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const TEST_TASK_PATTERN = /\b(tests?|testing|specs?|coverage|e2e|unit|integration|integra[cç][aã]o|valida[cç][aã]o|cobertura|testes?)\b/i;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns true if the task title indicates a testing or validation task.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} taskTitle
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*/
|
|
13
|
+
export function isTestTask(taskTitle) {
|
|
14
|
+
if (!taskTitle) return false;
|
|
15
|
+
return TEST_TASK_PATTERN.test(taskTitle);
|
|
16
|
+
}
|
|
@@ -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');
|