@polymorphism-tech/morph-spec 4.8.19 → 4.9.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 +15 -56
- package/bin/task-manager.js +115 -14
- 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 +21 -0
- package/framework/agents.json +698 -176
- package/framework/hooks/claude-code/post-tool-use/context-refresh.js +1 -1
- package/framework/hooks/claude-code/post-tool-use/dispatch.js +2 -2
- package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +155 -0
- package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +1 -1
- package/framework/hooks/claude-code/session-start/inject-morph-context.js +71 -2
- package/framework/hooks/claude-code/statusline.py +76 -30
- 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/phase-utils.js +3 -0
- 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 +2 -2
- package/framework/hooks/shared/worktree-helpers.js +53 -0
- package/framework/phases.json +40 -8
- package/framework/skills/level-0-meta/brainstorming/SKILL.md +1 -1
- package/framework/skills/level-0-meta/code-review/SKILL.md +1 -1
- package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +163 -163
- package/framework/skills/level-0-meta/frontend-review/SKILL.md +5 -5
- package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
- package/framework/skills/level-0-meta/morph-init/SKILL.md +5 -5
- package/framework/skills/level-0-meta/morph-replicate/SKILL.md +4 -4
- package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +1 -1
- package/framework/skills/level-0-meta/post-implementation/SKILL.md +59 -12
- package/framework/skills/level-0-meta/simulation-checklist/SKILL.md +1 -1
- package/framework/skills/level-0-meta/terminal-title/SKILL.md +1 -1
- package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +1 -1
- package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +6 -5
- package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +1 -1
- package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +215 -189
- package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +251 -251
- package/framework/skills/level-1-workflows/phase-design/SKILL.md +382 -365
- package/framework/skills/level-1-workflows/phase-implement/SKILL.md +492 -450
- package/framework/skills/level-1-workflows/phase-setup/SKILL.md +194 -190
- package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +270 -270
- package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +285 -285
- package/framework/standards/STANDARDS.json +640 -88
- package/framework/standards/infrastructure/vercel/vercel-database.md +106 -0
- package/framework/templates/REGISTRY.json +1825 -1909
- package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
- package/framework/templates/docs/onboarding.md +1 -5
- package/package.json +2 -6
- package/src/commands/agents/dispatch-agents.js +55 -4
- package/src/commands/project/doctor.js +16 -47
- 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/state/advance-phase.js +120 -30
- 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/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 +15 -0
- package/src/core/state/state-manager.js +28 -54
- package/src/core/workflows/workflow-detector.js +9 -87
- package/src/lib/phase-chain/phase-validator.js +330 -0
- package/src/lib/stack/stack-profile.js +88 -0
- package/src/lib/tasks/task-classifier.js +16 -0
- package/src/lib/tasks/test-runner.js +77 -0
- package/src/lib/trust/trust-manager.js +32 -144
- package/src/lib/validators/spec-validator.js +58 -4
- package/src/lib/validators/validation-runner.js +23 -11
- package/src/scripts/setup-infra.js +240 -224
- package/src/utils/agents-installer.js +2 -2
- package/src/utils/banner.js +1 -1
- package/src/utils/claude-settings-manager.js +1 -1
- package/src/utils/file-copier.js +1 -0
- package/src/utils/hooks-installer.js +258 -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/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/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/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/templates/template-validator.js +0 -296
- 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/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/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/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
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Used both by CLI commands and internal automation.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from 'fs';
|
|
8
|
+
import { readFileSync, writeFileSync, renameSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
|
9
9
|
import { join, dirname } from 'path';
|
|
10
10
|
import { detectWorkflow } from '../workflows/workflow-detector.js';
|
|
11
11
|
import { getAllOutputPaths, getOutputPath } from '../paths/output-schema.js';
|
|
@@ -137,10 +137,29 @@ export function saveState(state) {
|
|
|
137
137
|
mkdirSync(stateDir, { recursive: true });
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
// Atomic write: write to temp then rename
|
|
141
|
-
|
|
140
|
+
// Atomic write: write to a per-process temp file then rename.
|
|
141
|
+
// Per-process name prevents two concurrent CLI invocations (e.g. `task done && task start`)
|
|
142
|
+
// from clobbering each other's temp file on Windows.
|
|
143
|
+
const tmpPath = `${statePath}.tmp.${process.pid}`;
|
|
142
144
|
writeFileSync(tmpPath, JSON.stringify(state, null, 2), 'utf8');
|
|
143
|
-
|
|
145
|
+
|
|
146
|
+
// Retry rename on EPERM — Windows holds a brief exclusive lock on the target
|
|
147
|
+
// when another process is reading it; a short backoff resolves the contention.
|
|
148
|
+
const MAX_RETRIES = 5;
|
|
149
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
150
|
+
try {
|
|
151
|
+
renameSync(tmpPath, statePath);
|
|
152
|
+
return;
|
|
153
|
+
} catch (err) {
|
|
154
|
+
if (err.code === 'EPERM' && attempt < MAX_RETRIES) {
|
|
155
|
+
// Synchronous sleep: CPU-friendly, works on Node.js main thread (v9.4+)
|
|
156
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50 * (attempt + 1));
|
|
157
|
+
} else {
|
|
158
|
+
try { unlinkSync(tmpPath); } catch { /* ignore cleanup errors */ }
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
144
163
|
}
|
|
145
164
|
|
|
146
165
|
/**
|
|
@@ -241,7 +260,7 @@ async function ensureFeature(featureName, options = {}) {
|
|
|
241
260
|
|
|
242
261
|
state.features[featureName] = {
|
|
243
262
|
status: "draft",
|
|
244
|
-
workflow: workflowId, // auto |
|
|
263
|
+
workflow: workflowId, // auto | standard | nodejs-cli
|
|
245
264
|
workflowDetection,
|
|
246
265
|
createdAt: new Date().toISOString(),
|
|
247
266
|
updatedAt: new Date().toISOString(),
|
|
@@ -259,20 +278,7 @@ async function ensureFeature(featureName, options = {}) {
|
|
|
259
278
|
pending: 0
|
|
260
279
|
},
|
|
261
280
|
checkpoints: [],
|
|
262
|
-
|
|
263
|
-
totalThreads: 0,
|
|
264
|
-
parallelPeak: 0,
|
|
265
|
-
avgDuration: 0,
|
|
266
|
-
checkpointPassRate: 100
|
|
267
|
-
},
|
|
268
|
-
trustConfig: {
|
|
269
|
-
level: 'low',
|
|
270
|
-
history: [],
|
|
271
|
-
autoApprove: {
|
|
272
|
-
design: false,
|
|
273
|
-
tasks: false
|
|
274
|
-
}
|
|
275
|
-
},
|
|
281
|
+
trustConfig: {},
|
|
276
282
|
contextBundles: [],
|
|
277
283
|
fileChanges: []
|
|
278
284
|
};
|
|
@@ -515,20 +521,11 @@ export async function markOutput(featureName, outputType) {
|
|
|
515
521
|
|
|
516
522
|
const normalized = normalizeOutputType(outputType);
|
|
517
523
|
|
|
518
|
-
// Initialize outputs on demand (not pre-stored
|
|
524
|
+
// Initialize outputs on demand (not pre-stored)
|
|
519
525
|
if (!state.features[featureName].outputs) {
|
|
520
526
|
state.features[featureName].outputs = getAllOutputPaths(featureName);
|
|
521
527
|
}
|
|
522
528
|
|
|
523
|
-
// Define all valid output types (both camelCase and kebab-case alternatives)
|
|
524
|
-
const validTypes = [
|
|
525
|
-
'proposal', 'spec', 'contracts', 'tasks', 'decisions', 'recap',
|
|
526
|
-
'uiDesignSystem', 'ui-design-system',
|
|
527
|
-
'uiMockups', 'ui-mockups',
|
|
528
|
-
'uiComponents', 'ui-components',
|
|
529
|
-
'uiFlows', 'ui-flows'
|
|
530
|
-
];
|
|
531
|
-
|
|
532
529
|
if (!state.features[featureName].outputs[normalized]) {
|
|
533
530
|
// Try to find closest match for better error message
|
|
534
531
|
const validCamelCaseTypes = Object.keys(state.features[featureName].outputs);
|
|
@@ -551,9 +548,8 @@ export async function markOutput(featureName, outputType) {
|
|
|
551
548
|
}
|
|
552
549
|
|
|
553
550
|
errorMsg += `\n\nValid types:\n`;
|
|
554
|
-
errorMsg += `
|
|
555
|
-
errorMsg +=
|
|
556
|
-
errorMsg += `\nNote: UI types also accept kebab-case (e.g., 'ui-design-system')`;
|
|
551
|
+
errorMsg += ` ${validCamelCaseTypes.join(', ')}\n`;
|
|
552
|
+
errorMsg += `\nNote: UI types also accept kebab-case (e.g., 'ui-design-system', 'schema-analysis')`;
|
|
557
553
|
|
|
558
554
|
throw new Error(errorMsg);
|
|
559
555
|
}
|
|
@@ -575,31 +571,9 @@ export async function markOutput(featureName, outputType) {
|
|
|
575
571
|
});
|
|
576
572
|
}
|
|
577
573
|
|
|
578
|
-
// If marking tasks output, try to sync task count from state tasks array
|
|
579
|
-
if (normalized === 'tasks') {
|
|
580
|
-
syncTasksCount(state.features[featureName]);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
574
|
saveState(state);
|
|
584
575
|
}
|
|
585
576
|
|
|
586
|
-
/**
|
|
587
|
-
* Sync progress counters from taskList array into feature.progress
|
|
588
|
-
* @param {Object} feature - Feature object
|
|
589
|
-
*/
|
|
590
|
-
function syncTasksCount(feature) {
|
|
591
|
-
const list = feature.taskList;
|
|
592
|
-
if (!Array.isArray(list) || list.length === 0) return;
|
|
593
|
-
const completed = list.filter(t => t.status === 'completed').length;
|
|
594
|
-
feature.progress = {
|
|
595
|
-
total: list.length,
|
|
596
|
-
completed,
|
|
597
|
-
inProgress: list.filter(t => t.status === 'in_progress').length,
|
|
598
|
-
pending: list.filter(t => t.status === 'pending').length,
|
|
599
|
-
percentage: Math.round((completed / list.length) * 100)
|
|
600
|
-
};
|
|
601
|
-
}
|
|
602
|
-
|
|
603
577
|
/**
|
|
604
578
|
* Track a file change for a feature
|
|
605
579
|
* @param {string} featureName - Feature name
|
|
@@ -242,11 +242,11 @@ export async function detectWorkflow(options) {
|
|
|
242
242
|
if (workflows.length === 0) {
|
|
243
243
|
// Fallback if no configs found
|
|
244
244
|
return {
|
|
245
|
-
workflowId: '
|
|
245
|
+
workflowId: 'standard',
|
|
246
246
|
confidence: 0.5,
|
|
247
247
|
matchedKeywords: [],
|
|
248
248
|
estimatedComplexity: { files: 0, lines: 0, components: 0, hasInfra: false },
|
|
249
|
-
reasoning: 'No workflow configs found - using
|
|
249
|
+
reasoning: 'No workflow configs found - using standard as fallback',
|
|
250
250
|
alternativeWorkflows: []
|
|
251
251
|
};
|
|
252
252
|
}
|
|
@@ -286,8 +286,8 @@ export async function detectWorkflow(options) {
|
|
|
286
286
|
(priorityScore * priorityWeight) +
|
|
287
287
|
(contextScore * contextWeight);
|
|
288
288
|
|
|
289
|
-
// Penalty for workflows with no keyword matches
|
|
290
|
-
if (keywordMatch.matched.length === 0
|
|
289
|
+
// Penalty for workflows with no keyword matches
|
|
290
|
+
if (keywordMatch.matched.length === 0) {
|
|
291
291
|
totalScore *= 0.5; // 50% penalty for no keyword match
|
|
292
292
|
}
|
|
293
293
|
|
|
@@ -382,84 +382,6 @@ export function listWorkflows(projectPath = '.') {
|
|
|
382
382
|
}));
|
|
383
383
|
}
|
|
384
384
|
|
|
385
|
-
// ============================================================================
|
|
386
|
-
// Parallel, Fusion, Long-Running, Zero-Touch Detection
|
|
387
|
-
// ============================================================================
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* Detect if parallel execution mode should be used for a feature.
|
|
391
|
-
* Based on dependency graph analysis: if feature has independent task squads,
|
|
392
|
-
* recommend parallel mode.
|
|
393
|
-
*
|
|
394
|
-
* @param {string} feature - Feature name
|
|
395
|
-
* @param {string} [projectPath='.'] - Project path
|
|
396
|
-
* @returns {Object} { useParallel: boolean, reason: string, squads: Array }
|
|
397
|
-
*/
|
|
398
|
-
export async function detectParallelMode(feature, projectPath = '.') {
|
|
399
|
-
try {
|
|
400
|
-
const { getExecutionPlan } = await import('../../lib/threads/thread-coordinator.js').catch(() => {
|
|
401
|
-
// thread-coordinator may not exist yet — graceful fallback
|
|
402
|
-
return { getExecutionPlan: null };
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
if (!getExecutionPlan) {
|
|
406
|
-
return { useParallel: false, reason: 'thread-coordinator not available', squads: [] };
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
const plan = getExecutionPlan(feature);
|
|
410
|
-
|
|
411
|
-
if (!plan.valid) {
|
|
412
|
-
return { useParallel: false, reason: plan.error, squads: [] };
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
const parallelPhases = plan.phases.filter(p => p.canRunParallel);
|
|
416
|
-
const useParallel = parallelPhases.length > 0 && plan.stats.parallelizationRatio > 30;
|
|
417
|
-
|
|
418
|
-
return {
|
|
419
|
-
useParallel,
|
|
420
|
-
reason: useParallel
|
|
421
|
-
? `${plan.stats.parallelizationRatio}% of tasks are parallelizable across ${parallelPhases.length} phases`
|
|
422
|
-
: 'Tasks are mostly sequential — parallel mode would not improve throughput',
|
|
423
|
-
parallelizationRatio: plan.stats.parallelizationRatio,
|
|
424
|
-
parallelPhases: parallelPhases.length,
|
|
425
|
-
squads: parallelPhases.map(p => p.tasks)
|
|
426
|
-
};
|
|
427
|
-
} catch {
|
|
428
|
-
return { useParallel: false, reason: 'Dependency analysis failed', squads: [] };
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
/**
|
|
433
|
-
* Detect if a request needs fusion workflow (uncertainty → run N agents → best-of-N)
|
|
434
|
-
* @param {string} userRequest
|
|
435
|
-
* @returns {boolean}
|
|
436
|
-
*/
|
|
437
|
-
export function detectFusionNeed(userRequest) {
|
|
438
|
-
const fusionKeywords = [
|
|
439
|
-
'uncertain', 'uncertainty', 'critical decision', 'compare approaches',
|
|
440
|
-
'best approach', 'prototype', 'explore options', 'which is better',
|
|
441
|
-
'not sure how', 'multiple solutions', 'benchmark'
|
|
442
|
-
];
|
|
443
|
-
const req = userRequest.toLowerCase();
|
|
444
|
-
return fusionKeywords.some(kw => req.includes(kw));
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
/**
|
|
448
|
-
* Detect if a request needs long-running workflow (L-Thread with stop hooks)
|
|
449
|
-
* @param {string} userRequest
|
|
450
|
-
* @returns {boolean}
|
|
451
|
-
*/
|
|
452
|
-
export function detectLongRunningNeed(userRequest) {
|
|
453
|
-
const longRunningKeywords = [
|
|
454
|
-
'autonomous', 'large scope', 'full system', 'end to end', 'e2e',
|
|
455
|
-
'complete implementation', 'no interruption', 'long running', 'unattended'
|
|
456
|
-
];
|
|
457
|
-
const req = userRequest.toLowerCase();
|
|
458
|
-
const fileCountMatch = req.match(/(\d+)\+?\s+files?/);
|
|
459
|
-
const fileCount = fileCountMatch ? parseInt(fileCountMatch[1]) : 0;
|
|
460
|
-
return longRunningKeywords.some(kw => req.includes(kw)) || fileCount > 10;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
385
|
/**
|
|
464
386
|
* Check zero-touch eligibility for a feature (requires trust-manager)
|
|
465
387
|
* @param {string} feature
|
|
@@ -467,14 +389,14 @@ export function detectLongRunningNeed(userRequest) {
|
|
|
467
389
|
*/
|
|
468
390
|
export async function checkZeroTouchEligibility(feature) {
|
|
469
391
|
try {
|
|
470
|
-
const {
|
|
471
|
-
|
|
392
|
+
const { getTrust } = await import('../../lib/trust/trust-manager.js').catch(() => ({
|
|
393
|
+
getTrust: null
|
|
472
394
|
}));
|
|
473
395
|
|
|
474
|
-
if (!
|
|
396
|
+
if (!getTrust) return false;
|
|
475
397
|
|
|
476
|
-
const
|
|
477
|
-
return level === '
|
|
398
|
+
const trust = getTrust(feature);
|
|
399
|
+
return trust.level === 'auto';
|
|
478
400
|
} catch {
|
|
479
401
|
return false;
|
|
480
402
|
}
|
|
@@ -0,0 +1,330 @@
|
|
|
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 { existsSync } from 'fs';
|
|
14
|
+
import path, { join } from 'path';
|
|
15
|
+
import { loadState, derivePhase, getFeature } from '../../core/state/state-manager.js';
|
|
16
|
+
import { getWorkflowConfig } from '../../core/workflows/workflow-detector.js';
|
|
17
|
+
import { shouldAutoApprove } from '../trust/trust-manager.js';
|
|
18
|
+
|
|
19
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
|
+
// Phase definitions (from validate-phase.js)
|
|
21
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
export const PHASES = {
|
|
24
|
+
'proposal': {
|
|
25
|
+
order: 0,
|
|
26
|
+
name: 'FASE 0: PROPOSAL',
|
|
27
|
+
requiredOutputs: [],
|
|
28
|
+
description: 'Initial proposal and agent detection'
|
|
29
|
+
},
|
|
30
|
+
'setup': {
|
|
31
|
+
order: 1,
|
|
32
|
+
name: 'FASE 1: SETUP',
|
|
33
|
+
requiredOutputs: ['proposal.md'],
|
|
34
|
+
description: 'Load context and standards'
|
|
35
|
+
},
|
|
36
|
+
'uiux': {
|
|
37
|
+
order: 2,
|
|
38
|
+
name: 'FASE 1.5: UI/UX DESIGN',
|
|
39
|
+
requiredOutputs: ['proposal.md'],
|
|
40
|
+
optionalOutputs: ['ui-design-system.md', 'ui-mockups.md', 'ui-components.md', 'ui-flows.md'],
|
|
41
|
+
description: 'UI/UX design (conditional - only if frontend)',
|
|
42
|
+
optional: true
|
|
43
|
+
},
|
|
44
|
+
'design': {
|
|
45
|
+
order: 3,
|
|
46
|
+
name: 'FASE 2: DESIGN',
|
|
47
|
+
requiredOutputs: ['proposal.md'],
|
|
48
|
+
description: 'Technical specification and contracts'
|
|
49
|
+
},
|
|
50
|
+
'clarify': {
|
|
51
|
+
order: 4,
|
|
52
|
+
name: 'FASE 3: CLARIFY',
|
|
53
|
+
requiredOutputs: ['proposal.md', 'spec.md'],
|
|
54
|
+
description: 'Clarify ambiguities and edge cases'
|
|
55
|
+
},
|
|
56
|
+
'tasks': {
|
|
57
|
+
order: 5,
|
|
58
|
+
name: 'FASE 4: TASKS',
|
|
59
|
+
requiredOutputs: ['proposal.md', 'spec.md'],
|
|
60
|
+
description: 'Break down into executable tasks'
|
|
61
|
+
},
|
|
62
|
+
'implement': {
|
|
63
|
+
order: 6,
|
|
64
|
+
name: 'FASE 5: IMPLEMENT',
|
|
65
|
+
requiredOutputs: ['proposal.md', 'spec.md'],
|
|
66
|
+
description: 'Execute tasks and implement code'
|
|
67
|
+
},
|
|
68
|
+
'sync': {
|
|
69
|
+
order: 7,
|
|
70
|
+
name: 'FASE 6: SYNC',
|
|
71
|
+
requiredOutputs: ['proposal.md', 'spec.md', 'decisions.md'],
|
|
72
|
+
description: 'Sync decisions to project standards',
|
|
73
|
+
optional: true
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Map output filenames to their phase subfolders
|
|
78
|
+
const OUTPUT_PHASE_MAP = {
|
|
79
|
+
'proposal.md': '0-proposal',
|
|
80
|
+
'schema-analysis.md': '1-design',
|
|
81
|
+
'spec.md': '1-design',
|
|
82
|
+
'clarifications.md': '1-design',
|
|
83
|
+
'contracts.cs': '1-design',
|
|
84
|
+
'decisions.md': '1-design',
|
|
85
|
+
'ui-design-system.md': '2-ui',
|
|
86
|
+
'ui-mockups.md': '2-ui',
|
|
87
|
+
'ui-components.md': '2-ui',
|
|
88
|
+
'ui-flows.md': '2-ui',
|
|
89
|
+
'tasks.md': '3-tasks',
|
|
90
|
+
'recap.md': '4-implement'
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function checkOutput(featurePath, outputFile) {
|
|
94
|
+
const phaseDir = OUTPUT_PHASE_MAP[outputFile] || '';
|
|
95
|
+
const filePath = path.join(featurePath, phaseDir, outputFile);
|
|
96
|
+
return fs.existsSync(filePath);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate phase prerequisites for a feature.
|
|
101
|
+
* Used by advance-phase.js before advancing.
|
|
102
|
+
*/
|
|
103
|
+
export function validatePhase(featureName, targetPhase) {
|
|
104
|
+
const featurePath = path.join(process.cwd(), '.morph/features', featureName);
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(featurePath)) {
|
|
107
|
+
return {
|
|
108
|
+
valid: false,
|
|
109
|
+
error: `Feature directory not found: ${featurePath}`,
|
|
110
|
+
suggestion: `Run 'morph-spec state init ${featureName}' to start`,
|
|
111
|
+
missingOutputs: [],
|
|
112
|
+
phase: null
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const phaseDefinition = PHASES[targetPhase];
|
|
117
|
+
if (!phaseDefinition) {
|
|
118
|
+
return {
|
|
119
|
+
valid: false,
|
|
120
|
+
error: `Unknown phase: ${targetPhase}`,
|
|
121
|
+
validPhases: Object.keys(PHASES),
|
|
122
|
+
missingOutputs: [],
|
|
123
|
+
phase: null
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const missingOutputs = [];
|
|
128
|
+
for (const output of phaseDefinition.requiredOutputs) {
|
|
129
|
+
if (!checkOutput(featurePath, output)) {
|
|
130
|
+
missingOutputs.push(output);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Special validation: implement phase requires tasks.md to have actual tasks
|
|
135
|
+
if (targetPhase === 'implement') {
|
|
136
|
+
const tasksFilePath = path.join(featurePath, '3-tasks', 'tasks.md');
|
|
137
|
+
if (fs.existsSync(tasksFilePath)) {
|
|
138
|
+
const content = fs.readFileSync(tasksFilePath, 'utf-8');
|
|
139
|
+
const taskCount = (content.match(/^###\s+T\d+/gm) || []).length;
|
|
140
|
+
if (taskCount === 0) {
|
|
141
|
+
missingOutputs.push(`tasks.md exists but has no task entries — add ### T001 — Task title entries`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Warn if skipping phases
|
|
147
|
+
let stateWarning = null;
|
|
148
|
+
try {
|
|
149
|
+
const feature = getFeature(featureName);
|
|
150
|
+
if (feature) {
|
|
151
|
+
const currentPhaseOrder = PHASES[feature.phase]?.order ?? -1;
|
|
152
|
+
const targetPhaseOrder = phaseDefinition.order;
|
|
153
|
+
if (targetPhaseOrder > currentPhaseOrder + 1) {
|
|
154
|
+
stateWarning = `Skipping phases: current is '${feature.phase}' (order ${currentPhaseOrder}), target is '${targetPhase}' (order ${targetPhaseOrder})`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// State file may not exist
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
valid: missingOutputs.length === 0,
|
|
163
|
+
error: null,
|
|
164
|
+
missingOutputs,
|
|
165
|
+
phase: phaseDefinition,
|
|
166
|
+
featurePath,
|
|
167
|
+
stateWarning
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
172
|
+
// Phase eligibility
|
|
173
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
const PHASE_ORDER = ['proposal', 'setup', 'uiux', 'design', 'clarify', 'tasks', 'implement', 'sync'];
|
|
176
|
+
|
|
177
|
+
function getNextPhase(currentPhase) {
|
|
178
|
+
const idx = PHASE_ORDER.indexOf(currentPhase);
|
|
179
|
+
if (idx === -1 || idx >= PHASE_ORDER.length - 1) return null;
|
|
180
|
+
return PHASE_ORDER[idx + 1];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Compute overall validation pass rate from validationHistory.
|
|
185
|
+
* Returns null when no history exists.
|
|
186
|
+
*/
|
|
187
|
+
export function computePassRate(validationHistory) {
|
|
188
|
+
if (!validationHistory || Object.keys(validationHistory).length === 0) return null;
|
|
189
|
+
const entries = Object.values(validationHistory);
|
|
190
|
+
const passed = entries.filter(e => e.status === 'passed').length;
|
|
191
|
+
return entries.length > 0 ? passed / entries.length : null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Map output type names to file paths for eligibility checks
|
|
195
|
+
const OUTPUT_PATH_MAP = {
|
|
196
|
+
'proposal': '0-proposal/proposal.md',
|
|
197
|
+
'spec': '1-design/spec.md',
|
|
198
|
+
'contracts': '1-design/contracts.cs',
|
|
199
|
+
'tasks': '3-tasks/tasks.md',
|
|
200
|
+
'schemaAnalysis': '1-design/schema-analysis.md',
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
function getMissingRequiredOutputs(featureName, phase, projectPath) {
|
|
204
|
+
const phaseDef = PHASES[phase];
|
|
205
|
+
if (!phaseDef?.requiredOutputs) return [];
|
|
206
|
+
|
|
207
|
+
const missing = [];
|
|
208
|
+
const featureBase = join(projectPath, '.morph', 'features', featureName);
|
|
209
|
+
|
|
210
|
+
for (const output of phaseDef.requiredOutputs) {
|
|
211
|
+
const relPath = OUTPUT_PATH_MAP[output];
|
|
212
|
+
if (relPath && !existsSync(join(featureBase, relPath))) {
|
|
213
|
+
missing.push(output);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return missing;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getBlockedTasks(feature) {
|
|
221
|
+
const history = feature.validationHistory || {};
|
|
222
|
+
return Object.entries(history)
|
|
223
|
+
.filter(([, entry]) => entry.status === 'blocked')
|
|
224
|
+
.map(([taskId]) => taskId);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Check whether a feature is eligible to auto-advance to the next phase.
|
|
229
|
+
* Used by phase-runner.js.
|
|
230
|
+
*/
|
|
231
|
+
export function checkPhaseEligibility(featureName, opts = {}) {
|
|
232
|
+
const projectPath = opts.projectPath || process.cwd();
|
|
233
|
+
|
|
234
|
+
const state = loadState(false);
|
|
235
|
+
if (!state) {
|
|
236
|
+
return {
|
|
237
|
+
eligible: false,
|
|
238
|
+
blockers: [{ type: 'state_error', items: ['state.json not found'] }],
|
|
239
|
+
currentPhase: 'unknown',
|
|
240
|
+
nextPhase: null,
|
|
241
|
+
passRate: null,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const feature = state.features?.[featureName];
|
|
246
|
+
if (!feature) {
|
|
247
|
+
return {
|
|
248
|
+
eligible: false,
|
|
249
|
+
blockers: [{ type: 'state_error', items: [`Feature '${featureName}' not found in state.json`] }],
|
|
250
|
+
currentPhase: 'unknown',
|
|
251
|
+
nextPhase: null,
|
|
252
|
+
passRate: null,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const featureFolderPath = join(projectPath, '.morph', 'features', featureName);
|
|
257
|
+
const currentPhase = feature.phase || derivePhase(featureFolderPath);
|
|
258
|
+
const nextPhase = getNextPhase(currentPhase);
|
|
259
|
+
|
|
260
|
+
if (!nextPhase) {
|
|
261
|
+
return {
|
|
262
|
+
eligible: false,
|
|
263
|
+
blockers: [{ type: 'state_error', items: ['Feature is at the final phase'] }],
|
|
264
|
+
currentPhase,
|
|
265
|
+
nextPhase: null,
|
|
266
|
+
passRate: null,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
let workflowConfig = null;
|
|
271
|
+
if (feature.workflow && feature.workflow !== 'auto') {
|
|
272
|
+
try {
|
|
273
|
+
workflowConfig = getWorkflowConfig(feature.workflow);
|
|
274
|
+
} catch {
|
|
275
|
+
// Non-blocking: fall through to defaults
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const minPassRate = workflowConfig?.phaseChain?.pauseOn
|
|
280
|
+
? 0.80
|
|
281
|
+
: workflowConfig?.eligibility?.minPassRate ?? 0.80;
|
|
282
|
+
|
|
283
|
+
const blockers = [];
|
|
284
|
+
|
|
285
|
+
const missingOutputs = getMissingRequiredOutputs(featureName, currentPhase, projectPath);
|
|
286
|
+
if (missingOutputs.length > 0) {
|
|
287
|
+
blockers.push({ type: 'missing_outputs', items: missingOutputs });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const blockedTasks = getBlockedTasks(feature);
|
|
291
|
+
if (blockedTasks.length > 0) {
|
|
292
|
+
blockers.push({ type: 'blocked_tasks', items: blockedTasks });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const passRate = computePassRate(feature.validationHistory);
|
|
296
|
+
if (passRate !== null && passRate < minPassRate) {
|
|
297
|
+
blockers.push({
|
|
298
|
+
type: 'low_pass_rate',
|
|
299
|
+
current: passRate,
|
|
300
|
+
required: minPassRate,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
try {
|
|
305
|
+
const requiredGateMap = { design: 'design', tasks: 'tasks', uiux: 'uiux' };
|
|
306
|
+
const gate = requiredGateMap[currentPhase];
|
|
307
|
+
if (gate) {
|
|
308
|
+
const trustResult = shouldAutoApprove(featureName, gate);
|
|
309
|
+
if (!trustResult.autoApprove) {
|
|
310
|
+
const gateStatus = feature.approvalGates?.[gate];
|
|
311
|
+
if (!gateStatus?.approved) {
|
|
312
|
+
blockers.push({
|
|
313
|
+
type: 'trust_too_low',
|
|
314
|
+
items: [`Gate '${gate}' requires trust level ${trustResult.level || 'medium+'}`],
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} catch {
|
|
320
|
+
// Trust manager unavailable — non-blocking
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
eligible: blockers.length === 0,
|
|
325
|
+
blockers,
|
|
326
|
+
currentPhase,
|
|
327
|
+
nextPhase,
|
|
328
|
+
passRate,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
@@ -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
|
+
}
|