@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.
Files changed (137) hide show
  1. package/CLAUDE.md +21 -0
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +15 -56
  4. package/bin/task-manager.js +115 -14
  5. package/bin/validate.js +67 -33
  6. package/claude-plugin.json +1 -1
  7. package/docs/CHEATSHEET.md +201 -203
  8. package/docs/QUICKSTART.md +2 -2
  9. package/framework/CLAUDE.md +21 -0
  10. package/framework/agents.json +698 -176
  11. package/framework/hooks/claude-code/post-tool-use/context-refresh.js +1 -1
  12. package/framework/hooks/claude-code/post-tool-use/dispatch.js +2 -2
  13. package/framework/hooks/claude-code/post-tool-use/skill-reminder.js +155 -0
  14. package/framework/hooks/claude-code/pre-tool-use/protect-spec-files.js +1 -1
  15. package/framework/hooks/claude-code/session-start/inject-morph-context.js +71 -2
  16. package/framework/hooks/claude-code/statusline.py +76 -30
  17. package/framework/hooks/claude-code/user-prompt/set-terminal-title.js +14 -6
  18. package/framework/hooks/shared/activity-logger.js +0 -24
  19. package/framework/hooks/shared/phase-utils.js +3 -0
  20. package/framework/hooks/shared/skill-reminder-helpers.js +79 -0
  21. package/framework/hooks/shared/stale-task-reset.js +57 -0
  22. package/framework/hooks/shared/state-reader.js +2 -2
  23. package/framework/hooks/shared/worktree-helpers.js +53 -0
  24. package/framework/phases.json +40 -8
  25. package/framework/skills/level-0-meta/brainstorming/SKILL.md +1 -1
  26. package/framework/skills/level-0-meta/code-review/SKILL.md +1 -1
  27. package/framework/skills/level-0-meta/code-review-nextjs/SKILL.md +163 -163
  28. package/framework/skills/level-0-meta/frontend-review/SKILL.md +5 -5
  29. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +2 -2
  30. package/framework/skills/level-0-meta/morph-init/SKILL.md +5 -5
  31. package/framework/skills/level-0-meta/morph-replicate/SKILL.md +4 -4
  32. package/framework/skills/level-0-meta/morph-replicate/references/blazor-html-mapping.md +1 -1
  33. package/framework/skills/level-0-meta/post-implementation/SKILL.md +59 -12
  34. package/framework/skills/level-0-meta/simulation-checklist/SKILL.md +1 -1
  35. package/framework/skills/level-0-meta/terminal-title/SKILL.md +1 -1
  36. package/framework/skills/level-0-meta/tool-usage-guide/SKILL.md +1 -1
  37. package/framework/skills/level-0-meta/tool-usage-guide/references/tools-per-phase.md +6 -5
  38. package/framework/skills/level-0-meta/verification-before-completion/SKILL.md +1 -1
  39. package/framework/skills/level-1-workflows/phase-clarify/SKILL.md +215 -189
  40. package/framework/skills/level-1-workflows/phase-codebase-analysis/SKILL.md +251 -251
  41. package/framework/skills/level-1-workflows/phase-design/SKILL.md +382 -365
  42. package/framework/skills/level-1-workflows/phase-implement/SKILL.md +492 -450
  43. package/framework/skills/level-1-workflows/phase-setup/SKILL.md +194 -190
  44. package/framework/skills/level-1-workflows/phase-tasks/SKILL.md +270 -270
  45. package/framework/skills/level-1-workflows/phase-uiux/SKILL.md +285 -285
  46. package/framework/standards/STANDARDS.json +640 -88
  47. package/framework/standards/infrastructure/vercel/vercel-database.md +106 -0
  48. package/framework/templates/REGISTRY.json +1825 -1909
  49. package/framework/templates/context/CONTEXT-FEATURE.md +276 -276
  50. package/framework/templates/docs/onboarding.md +1 -5
  51. package/package.json +2 -6
  52. package/src/commands/agents/dispatch-agents.js +55 -4
  53. package/src/commands/project/doctor.js +16 -47
  54. package/src/commands/project/init.js +1 -1
  55. package/src/commands/project/status.js +2 -2
  56. package/src/commands/project/update.js +381 -365
  57. package/src/commands/project/worktree.js +154 -0
  58. package/src/commands/state/advance-phase.js +120 -30
  59. package/src/commands/state/approve.js +2 -2
  60. package/src/commands/state/index.js +7 -8
  61. package/src/commands/state/phase-runner.js +1 -1
  62. package/src/commands/state/state.js +61 -6
  63. package/src/commands/tasks/task.js +78 -99
  64. package/src/commands/templates/template-render.js +93 -173
  65. package/src/commands/trust/trust.js +26 -21
  66. package/src/core/paths/output-schema.js +15 -0
  67. package/src/core/state/state-manager.js +28 -54
  68. package/src/core/workflows/workflow-detector.js +9 -87
  69. package/src/lib/phase-chain/phase-validator.js +330 -0
  70. package/src/lib/stack/stack-profile.js +88 -0
  71. package/src/lib/tasks/task-classifier.js +16 -0
  72. package/src/lib/tasks/test-runner.js +77 -0
  73. package/src/lib/trust/trust-manager.js +32 -144
  74. package/src/lib/validators/spec-validator.js +58 -4
  75. package/src/lib/validators/validation-runner.js +23 -11
  76. package/src/scripts/setup-infra.js +240 -224
  77. package/src/utils/agents-installer.js +2 -2
  78. package/src/utils/banner.js +1 -1
  79. package/src/utils/claude-settings-manager.js +1 -1
  80. package/src/utils/file-copier.js +1 -0
  81. package/src/utils/hooks-installer.js +258 -8
  82. package/framework/hooks/dev/check-sync-health.js +0 -117
  83. package/framework/hooks/dev/guard-version-numbers.js +0 -57
  84. package/framework/hooks/dev/sync-standards-registry.js +0 -60
  85. package/framework/hooks/dev/sync-template-registry.js +0 -60
  86. package/framework/hooks/dev/validate-skill-format.js +0 -70
  87. package/framework/hooks/dev/validate-standard-format.js +0 -73
  88. package/framework/templates/meta-prompts/hops/hop-retry.md +0 -78
  89. package/framework/templates/meta-prompts/hops/hop-validation.md +0 -97
  90. package/framework/templates/meta-prompts/hops/hop-wrapper.md +0 -36
  91. package/framework/workflows/configs/design-impl.json +0 -49
  92. package/framework/workflows/configs/express.json +0 -45
  93. package/framework/workflows/configs/fast-track.json +0 -42
  94. package/framework/workflows/configs/full-morph.json +0 -79
  95. package/framework/workflows/configs/fusion.json +0 -39
  96. package/framework/workflows/configs/long-running.json +0 -33
  97. package/framework/workflows/configs/spec-only.json +0 -43
  98. package/framework/workflows/configs/ui-refresh.json +0 -49
  99. package/framework/workflows/configs/zero-touch.json +0 -82
  100. package/src/commands/project/monitor.js +0 -295
  101. package/src/commands/project/tutorial.js +0 -115
  102. package/src/commands/state/validate-phase.js +0 -238
  103. package/src/commands/templates/generate-contracts.js +0 -445
  104. package/src/core/orchestrator.js +0 -171
  105. package/src/core/registry/command-registry.js +0 -28
  106. package/src/core/registry/index.js +0 -8
  107. package/src/core/registry/validator-registry.js +0 -204
  108. package/src/core/templates/template-validator.js +0 -296
  109. package/src/generator/config-generator.js +0 -206
  110. package/src/generator/templates/config.json.template +0 -40
  111. package/src/generator/templates/project.md.template +0 -67
  112. package/src/lib/agents/micro-agent-factory.js +0 -161
  113. package/src/lib/analysis/complexity-analyzer.js +0 -441
  114. package/src/lib/analysis/index.js +0 -7
  115. package/src/lib/analytics/analytics-engine.js +0 -345
  116. package/src/lib/checkpoints/checkpoint-hooks.js +0 -298
  117. package/src/lib/checkpoints/index.js +0 -7
  118. package/src/lib/context/context-bundler.js +0 -241
  119. package/src/lib/context/context-optimizer.js +0 -212
  120. package/src/lib/context/context-tracker.js +0 -273
  121. package/src/lib/context/core-four-tracker.js +0 -201
  122. package/src/lib/context/mcp-optimizer.js +0 -200
  123. package/src/lib/execution/fusion-executor.js +0 -304
  124. package/src/lib/execution/parallel-executor.js +0 -270
  125. package/src/lib/hooks/stop-hook-executor.js +0 -286
  126. package/src/lib/hops/hop-composer.js +0 -221
  127. package/src/lib/phase-chain/eligibility-checker.js +0 -243
  128. package/src/lib/threads/thread-coordinator.js +0 -238
  129. package/src/lib/threads/thread-manager.js +0 -317
  130. package/src/lib/tracking/artifact-trail.js +0 -202
  131. package/src/scanner/project-scanner.js +0 -242
  132. package/src/ui/diff-display.js +0 -91
  133. package/src/ui/interactive-wizard.js +0 -96
  134. package/src/ui/user-review.js +0 -211
  135. package/src/ui/wizard-questions.js +0 -188
  136. package/src/utils/color-utils.js +0 -70
  137. 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 to avoid partial-write corruption
141
- const tmpPath = statePath + '.tmp';
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
- renameSync(tmpPath, statePath);
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 | fast-track | standard | full-morph | design-impl | ui-refresh
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
- threadMetrics: {
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 since v5.0.0)
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 += ` - Standard: proposal, spec, contracts, tasks, decisions, recap\n`;
555
- errorMsg += ` - UI/UX: uiDesignSystem, uiMockups, uiComponents, uiFlows\n`;
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: 'full-morph',
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 full-morph as fallback',
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 (unless it's full-morph fallback)
290
- if (keywordMatch.matched.length === 0 && workflow.id !== 'full-morph') {
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 { getTrustLevel } = await import('../../lib/trust/trust-manager.js').catch(() => ({
471
- getTrustLevel: null
392
+ const { getTrust } = await import('../../lib/trust/trust-manager.js').catch(() => ({
393
+ getTrust: null
472
394
  }));
473
395
 
474
- if (!getTrustLevel) return false;
396
+ if (!getTrust) return false;
475
397
 
476
- const level = getTrustLevel(feature);
477
- return level === 'maximum';
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
+ }