@polymorphism-tech/morph-spec 4.22.0 → 6.0.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 (211) hide show
  1. package/CLAUDE.md +57 -71
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +224 -491
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +55 -38
  8. package/framework/MORPH.md +244 -0
  9. package/framework/agents.json +127 -1198
  10. package/framework/commands/morph-apply.md +56 -240
  11. package/framework/commands/morph-archive.md +48 -85
  12. package/framework/commands/morph-preflight.md +7 -7
  13. package/framework/commands/morph-proposal.md +35 -191
  14. package/framework/commands/morph-status.md +19 -50
  15. package/framework/commands/morph-troubleshoot.md +2 -2
  16. package/framework/evals/rubrics/README.md +44 -0
  17. package/framework/evals/rubrics/architecture.md +46 -0
  18. package/framework/evals/rubrics/code-quality.md +50 -0
  19. package/framework/evals/rubrics/contracts.md +47 -0
  20. package/framework/evals/rubrics/test-coverage.md +50 -0
  21. package/framework/evals/scenarios/dotnet-crud.yaml +42 -0
  22. package/framework/evals/scenarios/nextjs-auth.yaml +38 -0
  23. package/framework/evals/suites/standard.yaml +5 -0
  24. package/framework/evals/trace-schema.md +67 -0
  25. package/framework/hooks/claude-code/core/README.md +3 -0
  26. package/framework/hooks/claude-code/core/denylist.json +14 -0
  27. package/framework/hooks/claude-code/{pre-tool-use → core}/enforce-phase-writes.js +5 -5
  28. package/framework/hooks/claude-code/core/post-edit-typecheck.js +69 -0
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +29 -0
  30. package/framework/hooks/claude-code/{pre-tool-use → core}/protect-spec-files.js +4 -6
  31. package/framework/hooks/claude-code/{stop → core}/validate-completion.js +13 -82
  32. package/framework/hooks/shared/phase-utils.js +20 -31
  33. package/framework/hooks/shared/state-reader.js +4 -5
  34. package/framework/{skills/level-0-meta/mcp-registry.json → mcp/registry.json} +254 -243
  35. package/framework/phases.json +41 -115
  36. package/framework/rubrics/eval-rubric.json +66 -0
  37. package/framework/skills/README.md +39 -64
  38. package/framework/skills/core/README.md +3 -0
  39. package/framework/skills/core/morph-brainstorming/SKILL.md +102 -0
  40. package/framework/skills/core/morph-eval/SKILL.md +57 -0
  41. package/framework/skills/core/morph-implement/SKILL.md +83 -0
  42. package/framework/skills/core/morph-plan/SKILL.md +64 -0
  43. package/framework/skills/core/morph-review/SKILL.md +62 -0
  44. package/framework/skills/core/morph-stack-scan/SKILL.md +109 -0
  45. package/framework/skills/core/morph-standards/SKILL.md +49 -0
  46. package/framework/templates/CLAUDE.md.core.guide.md +29 -0
  47. package/framework/templates/CLAUDE.md.core.template +22 -0
  48. package/framework/templates/REGISTRY.json +4 -4
  49. package/framework/workflows/configs/fast-track.json +5 -10
  50. package/framework/workflows/configs/full.json +4 -8
  51. package/framework/workflows/configs/quick.json +32 -0
  52. package/framework/workflows/configs/standard.json +57 -64
  53. package/package.json +7 -6
  54. package/src/commands/mcp/mcp-setup.js +58 -31
  55. package/src/commands/project/doctor.js +69 -70
  56. package/src/commands/project/init.js +26 -5
  57. package/src/commands/project/status.js +18 -19
  58. package/src/commands/state/approve.js +1 -1
  59. package/src/commands/validation/validate-feature.js +18 -112
  60. package/src/core/paths/output-schema.js +133 -152
  61. package/src/core/state/phase-state-machine.js +10 -24
  62. package/src/core/state/state-manager.js +17 -17
  63. package/src/core/workflows/workflow-detector.js +214 -31
  64. package/src/core/workflows/workflow-router.js +204 -0
  65. package/src/dashboard/app/pages/AgentMap.jsx +32 -33
  66. package/src/dashboard/app/pages/LiveState.jsx +1 -1
  67. package/src/dashboard/app/pages/PhasePipeline.jsx +2 -4
  68. package/src/dashboard/app/pages/SkillsLibrary.jsx +2 -3
  69. package/src/dashboard/app/pages/Workflows.jsx +2 -2
  70. package/src/dashboard/scanner/agents-scanner.js +30 -35
  71. package/src/lib/agents/metrics-aggregator.js +1 -1
  72. package/src/lib/agents/output-collector.js +7 -9
  73. package/src/lib/context/context-builder.js +289 -0
  74. package/src/lib/detectors/claude-config-detector.js +19 -3
  75. package/src/lib/generators/context-generator.js +20 -42
  76. package/src/lib/generators/index.js +1 -0
  77. package/src/lib/generators/mandate-generator.js +512 -0
  78. package/src/lib/generators/metadata-extractor.js +1 -1
  79. package/src/lib/generators/recap-generator.js +3 -4
  80. package/src/lib/installers/mcp-installer.js +300 -92
  81. package/src/lib/phase-chain/phase-validator.js +36 -40
  82. package/src/lib/stack/stack-profile.js +74 -91
  83. package/src/lib/tasks/task-parser.js +1 -1
  84. package/src/lib/tasks/wave-resolver.js +353 -0
  85. package/src/lib/validators/checkpoint-persistence.js +2 -2
  86. package/src/lib/validators/content/cross-phase-validator.js +281 -0
  87. package/src/lib/validators/content/index.js +1 -0
  88. package/src/lib/validators/shared/emit-validator-dispatch.js +6 -56
  89. package/src/scripts/global-install.js +2 -27
  90. package/src/utils/agents-installer.js +224 -447
  91. package/src/utils/claude-settings-manager.js +57 -15
  92. package/src/utils/file-copier.js +1 -0
  93. package/src/utils/hooks-installer.js +15 -117
  94. package/src/utils/skills-installer.js +12 -18
  95. package/bin/detect-agents.js +0 -226
  96. package/bin/task-manager.js +0 -1036
  97. package/framework/agents/README.md +0 -13
  98. package/framework/agents/ai-agents/ai-system-architect.md +0 -194
  99. package/framework/agents/ai-agents/vector-search-expert.md +0 -247
  100. package/framework/agents/architecture/architecture-expert.md +0 -98
  101. package/framework/agents/architecture/morph-spec-architect.md +0 -66
  102. package/framework/agents/architecture/packages-validator.md +0 -89
  103. package/framework/agents/architecture/security-expert.md +0 -79
  104. package/framework/agents/architecture/seo-growth-hacker.md +0 -168
  105. package/framework/agents/architecture/standards-architect.md +0 -153
  106. package/framework/agents/backend/dotnet-senior.md +0 -163
  107. package/framework/agents/backend/ef-modeler.md +0 -119
  108. package/framework/agents/backend/hangfire-orchestrator.md +0 -133
  109. package/framework/agents/backend/ms-agent-expert.md +0 -55
  110. package/framework/agents/backend/neon-expert.md +0 -379
  111. package/framework/agents/frontend/css-specialist.md +0 -89
  112. package/framework/agents/frontend/design-system-validator.md +0 -100
  113. package/framework/agents/frontend/gsap-animator.md +0 -131
  114. package/framework/agents/frontend/nextjs-component-validator.md +0 -97
  115. package/framework/agents/frontend/nextjs-expert.md +0 -201
  116. package/framework/agents/frontend/ui-designer.md +0 -259
  117. package/framework/agents/infrastructure/infra-architect.md +0 -215
  118. package/framework/agents/integrations/asaas-financial.md +0 -138
  119. package/framework/agents/integrations/neon-auth.md +0 -151
  120. package/framework/agents/integrations/resend-email.md +0 -127
  121. package/framework/hooks/claude-code/post-tool-use/agent-complete.js +0 -122
  122. package/framework/hooks/claude-code/post-tool-use/dispatch.js +0 -363
  123. package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +0 -78
  124. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +0 -194
  125. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +0 -199
  126. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +0 -55
  127. package/framework/hooks/claude-code/session-start/inject-morph-context.js +0 -323
  128. package/framework/hooks/claude-code/session-start/post-compact-restore.js +0 -41
  129. package/framework/hooks/claude-code/session-start/resume-context.js +0 -77
  130. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +0 -90
  131. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +0 -114
  132. package/framework/hooks/shared/compact-restore.js +0 -123
  133. package/framework/hooks/shared/dispatch-helpers.js +0 -147
  134. package/framework/hooks/shared/skill-reminder-helpers.js +0 -79
  135. package/framework/hooks/shared/stale-task-reset.js +0 -57
  136. package/framework/hooks/shared/worktree-helpers.js +0 -75
  137. package/framework/skills/level-0-meta/README.md +0 -7
  138. package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +0 -472
  139. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +0 -174
  140. package/framework/skills/level-0-meta/morph-brainstorming/references/proposal-example.md +0 -138
  141. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +0 -145
  142. package/framework/skills/level-0-meta/morph-code-review/SKILL.md +0 -250
  143. package/framework/skills/level-0-meta/morph-code-review/references/review-example.md +0 -164
  144. package/framework/skills/level-0-meta/morph-code-review/references/review-guidelines.md +0 -100
  145. package/framework/skills/level-0-meta/morph-code-review/scripts/scan-csharp.mjs +0 -151
  146. package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +0 -165
  147. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +0 -254
  148. package/framework/skills/level-0-meta/morph-code-review-nextjs/scripts/scan-nextjs.mjs +0 -189
  149. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +0 -344
  150. package/framework/skills/level-0-meta/morph-frontend-review/scripts/scan-accessibility.mjs +0 -376
  151. package/framework/skills/level-0-meta/morph-gemini-image/SKILL.md +0 -290
  152. package/framework/skills/level-0-meta/morph-init/SKILL.md +0 -537
  153. package/framework/skills/level-0-meta/morph-parallel-dispatch/SKILL.md +0 -225
  154. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +0 -401
  155. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-dev-server.mjs +0 -153
  156. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-stack.mjs +0 -234
  157. package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +0 -106
  158. package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +0 -243
  159. package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +0 -169
  160. package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +0 -148
  161. package/framework/skills/level-0-meta/morph-verification-before-completion/scripts/check-phase-outputs.mjs +0 -110
  162. package/framework/skills/level-1-workflows/README.md +0 -7
  163. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +0 -269
  164. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +0 -453
  165. package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +0 -44
  166. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-authoring-guide.md +0 -55
  167. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-example.md +0 -253
  168. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
  169. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +0 -634
  170. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +0 -50
  171. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +0 -54
  172. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +0 -47
  173. package/framework/skills/level-1-workflows/morph-phase-implement/references/recap-example.md +0 -132
  174. package/framework/skills/level-1-workflows/morph-phase-implement/references/vsa-implementation-guide.md +0 -92
  175. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +0 -257
  176. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
  177. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +0 -291
  178. package/framework/skills/level-1-workflows/morph-phase-tasks/references/task-planning-patterns.md +0 -143
  179. package/framework/skills/level-1-workflows/morph-phase-tasks/references/tasks-example.md +0 -404
  180. package/framework/skills/level-1-workflows/morph-phase-tasks/scripts/validate-tasks.mjs +0 -112
  181. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +0 -995
  182. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +0 -147
  183. package/framework/squad-templates/backend-only.json +0 -47
  184. package/framework/squad-templates/frontend-only.json +0 -56
  185. package/framework/squad-templates/full-stack.json +0 -74
  186. package/src/commands/agents/agent-metrics.js +0 -174
  187. package/src/commands/agents/audit-dispatch.js +0 -82
  188. package/src/commands/agents/detect-agents.js +0 -275
  189. package/src/commands/agents/dispatch-agents.js +0 -461
  190. package/src/commands/agents/index.js +0 -13
  191. package/src/commands/agents/manage-agents.js +0 -352
  192. package/src/commands/agents/project-metrics.js +0 -74
  193. package/src/commands/phase/phase-reset.js +0 -74
  194. package/src/commands/scope/escalate.js +0 -217
  195. package/src/commands/state/advance-phase.js +0 -600
  196. package/src/commands/state/phase-runner.js +0 -215
  197. package/src/commands/state/state.js +0 -444
  198. package/src/commands/task/expand.js +0 -100
  199. package/src/commands/tasks/index.js +0 -4
  200. package/src/commands/tasks/task.js +0 -105
  201. package/src/commands/templates/index.js +0 -5
  202. package/src/commands/templates/template-render.js +0 -93
  203. package/src/commands/trust/trust.js +0 -118
  204. package/src/lib/agents/agents-manager.js +0 -349
  205. package/src/lib/build/build-runner.js +0 -99
  206. package/src/lib/checkpoints/checkpoint-hooks.js +0 -111
  207. package/src/lib/contracts/sprint-contract-generator.js +0 -137
  208. package/src/lib/scope/impact-analyzer.js +0 -106
  209. package/src/lib/trust/trust-manager.js +0 -157
  210. package/src/utils/tier-helpers.js +0 -24
  211. /package/framework/hooks/claude-code/{post-tool-use → core}/loop-detect.js +0 -0
@@ -1,1036 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- /**
4
- * MORPH-SPEC Task Manager (Simplified — Native-First)
5
- *
6
- * Validation gates for task completion, checkpoints, and state persistence.
7
- * Progress tracking and dependency management are handled by Claude Code native tasks.
8
- */
9
-
10
- import { fileURLToPath } from 'url';
11
- import { join, dirname } from 'path';
12
- import { access } from 'fs/promises';
13
- import { execSync } from 'child_process';
14
- import chalk from 'chalk';
15
-
16
- import { loadState, saveState, resolveAgentForTask, derivePhase, withStateLock } from '../src/core/state/state-manager.js';
17
- import { ensureTaskList, syncCounters } from '../src/lib/tasks/task-parser.js';
18
- import { isTestTask, isInfrastructureTask } from '../src/lib/tasks/task-classifier.js';
19
- import { runTestSuite } from '../src/lib/tasks/test-runner.js';
20
-
21
- const __filename = fileURLToPath(import.meta.url);
22
- const __dirname = dirname(__filename);
23
-
24
- // ============================================================================
25
- // Breaking-Change Detection
26
- // ============================================================================
27
-
28
- /**
29
- * Detect potentially broken consumers of recently removed exports.
30
- *
31
- * Strategy:
32
- * 1. Parse `git diff HEAD` (staged + unstaged) for removed export declarations.
33
- * 2. Use `git grep` to find files that still reference each removed symbol.
34
- * 3. Return warnings (non-blocking) so Claude Code can review before marking done.
35
- *
36
- * Returns null when git is unavailable, not in a repo, or no removed exports found.
37
- * Returns [] when removed exports exist but no consumers found.
38
- * Returns [{export, consumers[]}] when consumers are found.
39
- *
40
- * @returns {Promise<Array<{export: string, consumers: string[]}>|null>}
41
- */
42
- export async function detectBreakingChanges(opts = {}) {
43
- try {
44
- const exec = opts._execSync ?? execSync;
45
- const cwd = opts._cwd ?? process.cwd();
46
- const execOpts = { cwd, stdio: ['pipe', 'pipe', 'pipe'], maxBuffer: 5 * 1024 * 1024 };
47
-
48
- // Collect diff from staged + unstaged changes in source files
49
- let diff = '';
50
- const diffPatterns = ['*.ts', '*.tsx', '*.js', '*.jsx', '*.cs'];
51
- for (const flag of ['--cached', '']) {
52
- try {
53
- const args = ['git', 'diff', '--unified=0', flag, '--', ...diffPatterns].filter(Boolean);
54
- diff += exec(args.join(' '), execOpts).toString();
55
- } catch {
56
- // git not available or no changes — continue
57
- }
58
- }
59
-
60
- if (!diff.trim()) return null;
61
-
62
- // Extract source files touched by the diff
63
- const sourceFiles = new Set();
64
- const diffHeaderRe = /^diff --git a\/(.+?) b\//gm;
65
- let m;
66
- while ((m = diffHeaderRe.exec(diff)) !== null) sourceFiles.add(m[1]);
67
-
68
- // Extract removed export symbol names (lines starting with -)
69
- const removedExports = new Set();
70
- const exportRe = /^-\s*export\s+(?:(?:default\s+)?(?:async\s+)?function|const|let|class|interface|type|enum)\s+(\w+)/gm;
71
- let match;
72
- while ((match = exportRe.exec(diff)) !== null) {
73
- removedExports.add(match[1]);
74
- }
75
-
76
- if (removedExports.size === 0) return null;
77
-
78
- // For each removed symbol, check if any tracked file still imports/uses it
79
- const warnings = [];
80
- const defaultExcludes = [':!.morph/archive/', ':!.morph/framework/', ':!node_modules/'];
81
- const pathspecs = [...diffPatterns, ...defaultExcludes];
82
-
83
- for (const sym of removedExports) {
84
- try {
85
- const grepCmd = ['git', 'grep', '-l', `"${sym}"`, '--', ...pathspecs].join(' ');
86
- const result = exec(grepCmd, execOpts).toString().trim();
87
- if (result) {
88
- const consumers = result.split('\n').filter(Boolean).filter(f => !sourceFiles.has(f));
89
- if (consumers.length > 0) {
90
- warnings.push({ export: sym, consumers });
91
- }
92
- }
93
- } catch {
94
- // No match found — safe
95
- }
96
- }
97
-
98
- return warnings;
99
- } catch {
100
- return null; // Non-blocking: any unexpected error → skip detection
101
- }
102
- }
103
-
104
- // ============================================================================
105
- // TaskManager
106
- // ============================================================================
107
-
108
- class TaskManager {
109
- /**
110
- * Complete one or more tasks (runs validation first)
111
- * @param {string} featureName - Feature name
112
- * @param {string[]} taskIds - Task IDs to complete
113
- * @param {Object} options - Options
114
- * @param {boolean} options.skipValidation - Skip code validation (budget-limited)
115
- * @param {string} [options.reason] - Reason for skipping validation (required with skipValidation)
116
- * @param {string} [options.note] - Deviation note explaining why task was done differently than planned
117
- * @param {boolean} options.dryRun - Dry run (no state changes)
118
- * @param {Function} [options._runTestSuite] - Inject a mock runTestSuite for testing
119
- */
120
- async completeTasks(featureName, taskIds, options = {}) {
121
- const state = loadState();
122
- const feature = state.features[featureName];
123
-
124
- if (!feature) {
125
- throw new Error(`Feature '${featureName}' not found in state.json`);
126
- }
127
-
128
- const taskList = await ensureTaskList(feature, featureName);
129
-
130
- if (taskList.length === 0) {
131
- const tasksPath = join(process.cwd(), `.morph/features/${featureName}/4-tasks/tasks.md`);
132
- const tasksExist = await access(tasksPath).then(() => true).catch(() => false);
133
- if (!tasksExist) {
134
- throw new Error(`No tasks found for '${featureName}' — tasks.md not generated yet.\n Complete the tasks phase first: run /phase-tasks`);
135
- }
136
- throw new Error(`tasks.md found but no tasks could be parsed for '${featureName}'.\n Ensure tasks use the format: ### T001 — Title or ### T001: Title`);
137
- }
138
-
139
- // Persist initial taskList so counters are visible in `state get` even if
140
- // validation fails later and the completion save never runs.
141
- syncCounters(feature);
142
- feature.updatedAt = new Date().toISOString();
143
- saveState(state);
144
-
145
- const results = [];
146
- const tasksToComplete = [];
147
-
148
- for (const taskId of taskIds) {
149
- const task = taskList.find(t => t.id === taskId);
150
-
151
- if (!task) {
152
- console.error(chalk.red(`❌ Task ${taskId} not found (available: ${taskList.map(t => t.id).join(', ')})`));
153
- continue;
154
- }
155
-
156
- if (task.status === 'completed') {
157
- console.log(chalk.yellow(`⚠️ Task ${taskId} already completed`));
158
- continue;
159
- }
160
-
161
- // Auto-start if not already in_progress (BUG-007)
162
- if (task.status !== 'in_progress') {
163
- task.status = 'in_progress';
164
- task.startedAt = new Date().toISOString();
165
- const agentId = resolveAgentForTask(featureName, task.id);
166
- if (agentId) task.assignedAgent = agentId;
167
- console.log(chalk.gray(` ▶ Auto-started ${task.id} (was ${task.status || 'pending'})`));
168
- }
169
-
170
- tasksToComplete.push(task);
171
- }
172
-
173
- // ─── Test Task Guard ────────────────────────────────────────────────────────
174
- // When any of the tasks being completed is a test/validation task (e.g.
175
- // "Testes e Validação"), run the project's test suite BEFORE code validation.
176
- // The guard is skipped when --skip-validation or --dry-run is active.
177
- runTestGuard(tasksToComplete, process.cwd(), options);
178
-
179
- // ─── Skip-Validation Budget Enforcement ───────────────────────────────────
180
- if (options.skipValidation && tasksToComplete.length > 0 && !options.dryRun) {
181
- // Require --reason with minimum 5 characters
182
- if (!options.reason || options.reason.trim().length < 5) {
183
- console.error(chalk.red('❌ --skip-validation requires --reason <text> (minimum 5 characters)'));
184
- console.error(chalk.gray(' Example: morph-spec task done feat T001 --skip-validation --reason "CSS-only change, no logic"'));
185
- process.exit(1);
186
- }
187
-
188
- // Check budget
189
- const budget = computeSkipBudget(feature);
190
- const used = (feature.skipValidationLog || []).length;
191
- if (used >= budget) {
192
- console.error(chalk.red(`❌ Skip-validation budget exhausted (${used}/${budget})`));
193
- console.error(chalk.gray(' All remaining tasks must pass Tier-4 validation.'));
194
- console.error(chalk.gray(' Budget formula: min(5, ceil(totalTasks × 0.20))'));
195
- process.exit(1);
196
- }
197
-
198
- console.log(chalk.yellow(`⚠ --skip-validation: using budget (${used + tasksToComplete.length}/${budget})`));
199
- }
200
-
201
- // Run validation BEFORE marking tasks as complete
202
- const allInfra = tasksToComplete.every(t => isInfrastructureTask(t.title));
203
- if (tasksToComplete.length > 0 && !options.skipValidation) {
204
- const validationOpts = { mode: 'light' };
205
- if (allInfra) validationOpts.skipContracts = true;
206
- const validationResult = await this.runValidation(featureName, validationOpts);
207
- if (options.dryRun) {
208
- console.log(chalk.cyan('\n ℹ️ Dry-run — tasks NOT marked as complete'));
209
- return [];
210
- }
211
- // Score-based retry decision (falls back to pass/fail if scores unavailable)
212
- const scoreAction = validationResult.scores?.action;
213
- const shouldFail = scoreAction ? scoreAction !== 'approve' : !validationResult.passed;
214
-
215
- if (shouldFail) {
216
- // Reload state to ensure we have the latest (may have been modified by validation)
217
- const currentState = loadState();
218
- const currentFeature = currentState.features[featureName];
219
- if (currentFeature) {
220
- // Score-based escalation: 'escalate' action blocks immediately
221
- const forceEscalate = scoreAction === 'escalate';
222
-
223
- for (const task of tasksToComplete) {
224
- this.persistValidationHistory(currentFeature, task.id, validationResult, forceEscalate);
225
- }
226
- saveState(currentState);
227
-
228
- // Check escalation status
229
- const blockedTasks = tasksToComplete.filter(t => {
230
- const hist = currentFeature.validationHistory?.[t.id];
231
- return hist?.status === 'blocked';
232
- });
233
- if (blockedTasks.length > 0) {
234
- const scoreInfo = validationResult.scores ? ` (score: ${validationResult.scores.score}/10)` : '';
235
- console.error(chalk.red(`\n⛔ ESCALATION — Task(s) ${blockedTasks.map(t => t.id).join(', ')}${scoreInfo}`));
236
- if (forceEscalate) {
237
- console.error(chalk.red(' Score below retry threshold — immediate escalation'));
238
- } else {
239
- console.error(chalk.red(' Failed 3 times — human review required'));
240
- }
241
- console.error(chalk.red(' Mark resolved with: morph-spec state set <feature> validationHistory.<taskId>.status passed'));
242
- } else {
243
- const attempt = currentFeature.validationHistory?.[tasksToComplete[0]?.id]?.attempt || 1;
244
- const scoreInfo = validationResult.scores ? ` | score: ${validationResult.scores.score}/10` : '';
245
- console.error(chalk.red(`\n❌ Validation failed (attempt ${attempt}/3${scoreInfo}) — tasks NOT marked as complete`));
246
- console.log(chalk.gray(' Fix the issues above, then run task done again'));
247
- if (attempt >= 2) {
248
- console.log(chalk.yellow(` ⚠️ Next failure will escalate to human review`));
249
- }
250
- }
251
- } else {
252
- console.error(chalk.red('\n❌ Validation failed — tasks NOT marked as complete'));
253
- console.log(chalk.gray(' Fix the issues above, then run task done again'));
254
- }
255
- console.log(chalk.gray(' Or use --skip-validation to bypass (not recommended)\n'));
256
- process.exit(1);
257
- }
258
-
259
- // Validation passed — always record passed status in validationHistory.
260
- // Update in-place on `feature` (same object as state.features[featureName]) so
261
- // the single saveState(state) at the end of the function persists everything
262
- // atomically. A separate saveState here would trigger a second renameSync on
263
- // Windows, risking EPERM and leaving state.json without the task completions
264
- // (the count bug), and would also be clobbered by the final save anyway.
265
- if (!feature.validationHistory) feature.validationHistory = {};
266
- for (const task of tasksToComplete) {
267
- feature.validationHistory[task.id] = {
268
- ...(feature.validationHistory[task.id] || {}),
269
- status: 'passed',
270
- updatedAt: new Date().toISOString(),
271
- };
272
- }
273
- } else if (options.dryRun) {
274
- console.log(chalk.cyan('\n ℹ️ Dry-run — tasks NOT marked as complete (validation skipped)'));
275
- return [];
276
- } else if (shouldLogSkipValidationWarning(tasksToComplete, options.skipValidation, options.dryRun)) {
277
- // Record skip in state: skipValidationLog + validationHistory
278
- if (!feature.skipValidationLog) feature.skipValidationLog = [];
279
- if (!feature.validationHistory) feature.validationHistory = {};
280
- for (const task of tasksToComplete) {
281
- feature.skipValidationLog.push({
282
- taskId: task.id,
283
- reason: options.reason,
284
- timestamp: new Date().toISOString(),
285
- });
286
- feature.validationHistory[task.id] = {
287
- ...(feature.validationHistory[task.id] || {}),
288
- attempt: 0,
289
- status: 'skipped',
290
- reason: options.reason,
291
- updatedAt: new Date().toISOString(),
292
- };
293
- }
294
-
295
- console.log(chalk.yellow('\n⚠ --skip-validation: Tier-4 code validators bypassed — tasks will be marked complete without validation'));
296
- console.log(chalk.gray(' Re-run without --skip-validation before committing.\n'));
297
- }
298
-
299
- // Breaking change detection (non-blocking warning)
300
- if (tasksToComplete.length > 0 && !allInfra) {
301
- const _detectBreaking = options._detectBreakingChanges ?? detectBreakingChanges;
302
- const breakingChanges = await _detectBreaking();
303
- if (breakingChanges && breakingChanges.length > 0) {
304
- console.log(chalk.yellow('\n⚠️ BREAKING CHANGE DETECTION:'));
305
- console.log(chalk.yellow(' Removed exports with active consumers found — review before completing:\n'));
306
- for (const { export: sym, consumers } of breakingChanges) {
307
- console.log(chalk.yellow(` • "${sym}" removed — used by:`));
308
- consumers.slice(0, 5).forEach(f => console.log(chalk.gray(` - ${f}`)));
309
- if (consumers.length > 5) {
310
- console.log(chalk.gray(` … and ${consumers.length - 5} more files`));
311
- }
312
- }
313
- console.log(chalk.yellow('\n Fix broken imports before the smoke test, or use --skip-validation to bypass.\n'));
314
- }
315
- }
316
-
317
- // Mark tasks as completed with agent correlation
318
- for (const task of tasksToComplete) {
319
- task.status = 'completed';
320
- task.completedAt = new Date().toISOString();
321
-
322
- // Resolve agent from dispatch (falls back to 'claude')
323
- const agentId = resolveAgentForTask(featureName, task.id);
324
- task.completedBy = agentId || 'claude';
325
-
326
- // Add deviation note if provided
327
- if (options.note) {
328
- if (!task.notes) task.notes = [];
329
- task.notes.push({
330
- text: options.note,
331
- type: 'deviation',
332
- addedAt: new Date().toISOString(),
333
- addedBy: agentId || 'claude'
334
- });
335
- }
336
-
337
- results.push(task);
338
-
339
- console.log(chalk.green(`✅ Task ${task.id} completed!`));
340
- if (options.note) {
341
- console.log(chalk.gray(` Note: ${options.note}`));
342
- }
343
-
344
- // Register checkpoint if task has one
345
- if (task.checkpoint) {
346
- this.registerCheckpoint(feature, task);
347
- }
348
- }
349
-
350
- // Sync v3 counters
351
- syncCounters(feature);
352
-
353
- // Auto-checkpoint every 3 tasks (at exact multiples of 3, not after every task).
354
- // Uses feature._lastAutoCheckpointNum to track which threshold already fired.
355
- const activeTasks = taskList.filter(t => t.status !== 'expanded');
356
- const completedCount = activeTasks.filter(t => t.status === 'completed').length;
357
- if (completedCount > 0 && completedCount % 3 === 0) {
358
- const checkpointNum = Math.floor(completedCount / 3);
359
- const lastAutoNum = feature._lastAutoCheckpointNum || 0;
360
- if (checkpointNum > lastAutoNum) {
361
- const recentCompleted = this.getRecentCompleted(taskList, 3);
362
- await this.autoCheckpoint(feature, recentCompleted, featureName);
363
- feature._lastAutoCheckpointNum = checkpointNum;
364
- }
365
- }
366
-
367
- // Persist state (atomic write via state-manager)
368
- feature.updatedAt = new Date().toISOString();
369
- saveState(state);
370
-
371
- // Run TaskCompleted agent-teams hook for each completed task (non-blocking)
372
- for (const task of results) {
373
- try {
374
- const { executeHook, formatHookResults } = await import('../src/lib/hooks/hook-executor.js');
375
- const hookResult = await executeHook(process.cwd(), featureName, 'TaskCompleted', {
376
- taskId: task.id
377
- });
378
- if (!hookResult.passed && hookResult.errors.length > 0) {
379
- console.log(formatHookResults(hookResult, 'TaskCompleted'));
380
- }
381
- } catch {
382
- // Hook executor unavailable — non-blocking
383
- }
384
- }
385
-
386
- // Auto-generate metadata.json (for quick LLM access)
387
- await this.generateMetadata(featureName, feature);
388
-
389
- // Emit VALIDATION DISPATCH for PostToolUse hook (strategy 1 parsing).
390
- // Skipped when --skip-validation is active (no validators needed).
391
- if (!options.skipValidation && results.length > 0) {
392
- try {
393
- const { emitValidatorDispatch } = await import('../src/lib/validators/shared/emit-validator-dispatch.js');
394
- const phase = feature.phase || 'implement';
395
- await emitValidatorDispatch(featureName, phase, process.cwd());
396
- } catch {
397
- // Non-blocking
398
- }
399
- }
400
-
401
- return results;
402
- }
403
-
404
- /**
405
- * Run validation for a feature using the ValidationRunner (ESM dynamic import).
406
- * Returns a structured result with per-validator breakdown for validationHistory.
407
- *
408
- * @param {string} featureName - Feature name
409
- * @returns {{ passed: boolean, validators: Object, passRate: number }}
410
- */
411
- async runValidation(featureName, opts = {}) {
412
- try {
413
- console.log(chalk.cyan('\n🔍 Running code validation...'));
414
-
415
- const { runValidation, formatValidationResults } = await import('../src/lib/validators/validation-runner.js');
416
- const result = await runValidation('.', featureName, { verbose: true, ...opts });
417
-
418
- formatValidationResults(result);
419
-
420
- // Build structured validators map from result
421
- const validators = {};
422
- if (result.results && typeof result.results === 'object') {
423
- for (const [name, vResult] of Object.entries(result.results)) {
424
- const issues = (vResult.errors || []).map(e => ({
425
- message: typeof e === 'string' ? e : (e.message || String(e)),
426
- file: e.file,
427
- line: e.line,
428
- rule: e.rule,
429
- }));
430
- validators[name] = {
431
- passed: vResult.passed ?? (issues.length === 0),
432
- issues,
433
- };
434
- }
435
- } else if (!result.passed && result.errors?.length > 0) {
436
- // Flat format fallback — wrap as single 'validation' validator
437
- validators['validation'] = {
438
- passed: false,
439
- issues: result.errors.map(e => ({
440
- message: typeof e === 'string' ? e : (e.message || String(e)),
441
- file: e.file,
442
- line: e.line,
443
- rule: e.rule || 'validation',
444
- })),
445
- };
446
- }
447
-
448
- const validatorEntries = Object.values(validators);
449
- const passRate = validatorEntries.length > 0
450
- ? validatorEntries.filter(v => v.passed).length / validatorEntries.length
451
- : (result.passed ? 1.0 : 0.0);
452
-
453
- return { passed: result.passed, validators, passRate, scores: result.scores || null };
454
- } catch (error) {
455
- // If validation runner fails to load, warn but don't block task completion.
456
- // This is fail-open by design: a broken validator shouldn't block commits.
457
- // Common cause: missing optional deps.
458
- console.log(chalk.yellow(`\n⚠️ Validation skipped (${error.message})`));
459
- console.log(chalk.gray(` Run manually: morph-spec validate-feature ${featureName}`));
460
- return { passed: true, validators: {}, passRate: 1.0, scores: null };
461
- }
462
- }
463
-
464
- /**
465
- * Persist a validation result to feature.validationHistory[taskId].
466
- * Increments attempt counter and sets status to 'failed' or 'blocked' (attempt >= 3).
467
- *
468
- * @param {Object} feature - Mutable feature state object
469
- * @param {string} taskId - Task ID
470
- * @param {{ passed: boolean, validators: Object, passRate: number }} validationResult
471
- */
472
- persistValidationHistory(feature, taskId, validationResult, forceEscalate = false) {
473
- if (!feature.validationHistory) {
474
- feature.validationHistory = {};
475
- }
476
- const existing = feature.validationHistory[taskId] || { attempt: 0 };
477
- const attempt = (existing.attempt || 0) + 1;
478
- const status = forceEscalate ? 'blocked' : (attempt >= 3 ? 'blocked' : 'failed');
479
-
480
- const entry = {
481
- attempt,
482
- validators: validationResult.validators || {},
483
- passRate: validationResult.passRate || 0,
484
- status,
485
- updatedAt: new Date().toISOString(),
486
- };
487
-
488
- // Attach score data when available
489
- if (validationResult.scores) {
490
- entry.scores = {
491
- score: validationResult.scores.score,
492
- action: validationResult.scores.action,
493
- breakdown: validationResult.scores.breakdown,
494
- };
495
- }
496
-
497
- feature.validationHistory[taskId] = entry;
498
- }
499
-
500
- /**
501
- * Register checkpoint (from task flag)
502
- */
503
- registerCheckpoint(feature, task) {
504
- const checkpoint = {
505
- id: task.checkpoint,
506
- timestamp: new Date().toISOString(),
507
- tasksCompleted: [task.id],
508
- note: `Checkpoint: ${task.title}`,
509
- passed: true // reaching a named checkpoint means the task completed successfully
510
- };
511
-
512
- feature.checkpoints = feature.checkpoints || [];
513
- feature.checkpoints.push(checkpoint);
514
-
515
- console.log(chalk.magenta(`\n🎯 ${task.checkpoint} reached!`));
516
- }
517
-
518
- /**
519
- * Auto-checkpoint every 3 tasks (includes validation summary)
520
- */
521
- async autoCheckpoint(feature, tasks, featureName) {
522
- const tasksCompleted = feature.tasks?.completed || tasks.length;
523
- const checkpointNum = Math.floor(tasksCompleted / 3);
524
-
525
- console.log(chalk.magenta(`\n🔍 CHECKPOINT ${checkpointNum} - Running automated validation...`));
526
-
527
- let checkpointResult = null;
528
- let validationNote = '';
529
-
530
- try {
531
- const { runCheckpointHooks, shouldRunCheckpoint } = await import('../src/lib/checkpoints/checkpoint-hooks.js');
532
-
533
- if (shouldRunCheckpoint(tasksCompleted, 3)) {
534
- checkpointResult = await runCheckpointHooks(featureName, checkpointNum);
535
-
536
- if (checkpointResult.passed) {
537
- validationNote = ' | ✓ All validations passed';
538
- } else {
539
- validationNote = ` | ✗ ${checkpointResult.summary.errors} errors, ${checkpointResult.summary.warnings} warnings`;
540
-
541
- const config = await this.loadCheckpointConfig();
542
- if (config.checkpoints?.onFailure?.blockProgress && checkpointResult.summary.errors > 0) {
543
- console.log(chalk.red('\n❌ Checkpoint FAILED - Fix violations before proceeding'));
544
- throw new Error('Checkpoint validation failed');
545
- }
546
- }
547
- }
548
- } catch (error) {
549
- if (error.message === 'Checkpoint validation failed') {
550
- throw error;
551
- }
552
- // Fallback to old validation if checkpoint-hooks not available
553
- console.log(chalk.yellow('⚠️ Checkpoint hooks not available, using legacy validation'));
554
- try {
555
- const { runValidation } = await import('../src/lib/validators/validation-runner.js');
556
- const result = await runValidation('.', featureName, { verbose: false, mode: 'full' });
557
- validationNote = result.passed
558
- ? ' | Validation: PASSED'
559
- : ` | Validation: ${result.errors} errors, ${result.warnings} warnings`;
560
-
561
- // Persist checkpoint validation results to disk
562
- try {
563
- const { persistCheckpointValidation } = await import('../src/lib/validators/checkpoint-persistence.js');
564
- persistCheckpointValidation(process.cwd(), featureName, {
565
- checkpointNum,
566
- timestamp: new Date().toISOString(),
567
- passed: result.passed,
568
- validators: result.results || {},
569
- summary: result.summary || '',
570
- });
571
- } catch {
572
- // Non-blocking
573
- }
574
- } catch {
575
- // No validation available
576
- }
577
- }
578
-
579
- const checkpoint = {
580
- id: `CHECKPOINT_AUTO_${Date.now()}`,
581
- timestamp: new Date().toISOString(),
582
- tasksCompleted: tasks.map(t => t.id),
583
- note: `Auto-checkpoint: ${tasks.length} tasks completed${validationNote}`,
584
- passed: checkpointResult?.passed ?? true, // null checkpointResult means no validator ran → assume passed
585
- validationResults: checkpointResult?.results || null
586
- };
587
-
588
- feature.checkpoints = feature.checkpoints || [];
589
- feature.checkpoints.push(checkpoint);
590
-
591
- if (checkpointResult?.passed) {
592
- console.log(chalk.green(`\n✅ Checkpoint ${checkpointNum} PASSED`));
593
- } else if (checkpointResult) {
594
- console.log(chalk.yellow(`\n⚠️ Checkpoint ${checkpointNum} completed with warnings`));
595
- } else {
596
- console.log(chalk.magenta(`\n🎉 Auto-checkpoint ${checkpointNum} reached!`));
597
- }
598
- }
599
-
600
- /**
601
- * Load checkpoint configuration from llm-interaction.json
602
- */
603
- async loadCheckpointConfig() {
604
- try {
605
- const { readFile } = await import('fs/promises');
606
- const configPath = join(process.cwd(), '.morph/config/llm-interaction.json');
607
- const content = await readFile(configPath, 'utf-8');
608
- return JSON.parse(content);
609
- } catch {
610
- return {
611
- checkpoints: {
612
- frequency: 3,
613
- autoValidate: true,
614
- onFailure: { blockProgress: false }
615
- }
616
- };
617
- }
618
- }
619
-
620
- /**
621
- * Auto-generate metadata.json after task completion
622
- */
623
- async generateMetadata(featureName, feature) {
624
- try {
625
- const config = await this.loadCheckpointConfig();
626
-
627
- if (!config.metadata?.autoGenerate) {
628
- return;
629
- }
630
-
631
- const { extractFeatureMetadata } = await import('../src/lib/generators/metadata-extractor.js');
632
- const metadata = extractFeatureMetadata(feature);
633
-
634
- const outputPath = join(process.cwd(), `.morph/features/${featureName}/metadata.json`);
635
-
636
- const { mkdir, writeFile } = await import('fs/promises');
637
- await mkdir(dirname(outputPath), { recursive: true });
638
- await writeFile(outputPath, JSON.stringify(metadata, null, 2), 'utf-8');
639
-
640
- console.log(chalk.gray(` 📊 Metadata updated: .morph/features/${featureName}/metadata.json`));
641
- } catch (error) {
642
- console.log(chalk.yellow(` ⚠️ Metadata generation failed: ${error.message}`));
643
- }
644
- }
645
-
646
- /**
647
- * Get recently completed tasks (last N)
648
- */
649
- getRecentCompleted(tasks, count) {
650
- const completed = tasks
651
- .filter(t => t.status === 'completed' && t.completedAt)
652
- .sort((a, b) => new Date(b.completedAt) - new Date(a.completedAt));
653
-
654
- return completed.slice(0, count);
655
- }
656
-
657
- /**
658
- * Start a task (mark as in_progress).
659
- * Primarily serves as dispatch enforcement gate during implement phase.
660
- */
661
- async startTask(featureName, taskId) {
662
- const state = loadState();
663
- const feature = state.features[featureName];
664
-
665
- if (!feature) {
666
- throw new Error(`Feature '${featureName}' not found`);
667
- }
668
-
669
- // Dispatch enforcement gate: block task start in implement phase
670
- // unless dispatch-agents has been run (recorded in dispatchLog)
671
- const featurePath = join(process.cwd(), '.morph', 'features', featureName);
672
- const currentPhase = feature.phase || derivePhase(featurePath);
673
- if (currentPhase === 'implement') {
674
- const hasImplementDispatch = Array.isArray(feature.dispatchLog) &&
675
- feature.dispatchLog.some(entry => entry.phase === 'implement');
676
- if (!hasImplementDispatch) {
677
- console.error(chalk.red('\n✗ Cannot start task — agent dispatch not recorded for implement phase'));
678
- console.error(chalk.yellow(
679
- `\n Run this command first:\n` +
680
- ` morph-spec dispatch-agents ${featureName} implement\n\n` +
681
- ` This configures typed specialist agents with enriched prompts,\n` +
682
- ` task assignments, and quality standards for parallel execution.\n`
683
- ));
684
- process.exit(1);
685
- }
686
- }
687
-
688
- const taskList = await ensureTaskList(feature, featureName);
689
-
690
- if (taskList.length === 0) {
691
- const tasksPath = join(process.cwd(), `.morph/features/${featureName}/4-tasks/tasks.md`);
692
- const tasksExist = await access(tasksPath).then(() => true).catch(() => false);
693
- if (!tasksExist) {
694
- throw new Error(`No tasks found for '${featureName}' — tasks.md not generated yet.\n Complete the tasks phase first: run /phase-tasks`);
695
- }
696
- throw new Error(`tasks.md found but no tasks could be parsed for '${featureName}'.\n Ensure tasks use the format: ### T001 — Title or ### T001: Title`);
697
- }
698
-
699
- const task = taskList.find(t => t.id === taskId);
700
-
701
- if (!task) {
702
- throw new Error(`Task ${taskId} not found in '${featureName}' (${taskList.length} tasks available: ${taskList.map(t => t.id).join(', ')})`);
703
- }
704
-
705
- if (task.status === 'completed') {
706
- console.log(chalk.yellow(`⚠️ Task ${taskId} already completed`));
707
- return;
708
- }
709
-
710
- task.status = 'in_progress';
711
- task.startedAt = new Date().toISOString();
712
-
713
- // Agent correlation: resolve which agent is assigned this task from the latest dispatch
714
- const agentId = resolveAgentForTask(featureName, taskId);
715
- if (agentId) {
716
- task.assignedAgent = agentId;
717
- }
718
-
719
- syncCounters(feature);
720
- feature.updatedAt = new Date().toISOString();
721
-
722
- saveState(state);
723
-
724
- console.log(chalk.blue(`▶️ Task ${taskId} started: ${task.title}`));
725
- }
726
-
727
- /**
728
- * Bulk-complete tasks with a single validation pass.
729
- * @param {string} featureName
730
- * @param {Object} opts
731
- * @param {boolean} [opts.all] - Complete all pending tasks
732
- * @param {string} [opts.from] - Start of range (e.g. T001)
733
- * @param {string} [opts.to] - End of range (e.g. T053)
734
- * @param {string} [opts.range] - Compact range string (e.g. T001..T082)
735
- * @param {boolean} [opts.skipValidation]
736
- * @param {boolean} [opts.dryRun]
737
- */
738
- async bulkCompleteTasks(featureName, opts = {}) {
739
- const state = loadState();
740
- const feature = state.features[featureName];
741
- if (!feature) throw new Error(`Feature '${featureName}' not found in state.json`);
742
-
743
- const taskList = await ensureTaskList(feature, featureName);
744
- if (taskList.length === 0) throw new Error(`No tasks found for '${featureName}'`);
745
-
746
- // Resolve task IDs from range options
747
- let targetIds;
748
- if (opts.all) {
749
- targetIds = taskList.filter(t => t.status !== 'completed' && t.status !== 'expanded').map(t => t.id);
750
- } else if (opts.from && opts.to) {
751
- targetIds = this.expandRange(taskList, opts.from, opts.to);
752
- } else if (opts.range && opts.range.includes('..')) {
753
- const [from, to] = opts.range.split('..');
754
- targetIds = this.expandRange(taskList, from.trim(), to.trim());
755
- } else {
756
- throw new Error('bulk-done requires --all, --from/--to, or a T001..T082 range argument');
757
- }
758
-
759
- console.log(chalk.cyan(`\n📦 Bulk-done: ${targetIds.length} task(s) targeted`));
760
- await this.completeTasks(featureName, targetIds, { skipValidation: opts.skipValidation, reason: opts.reason, note: opts.note, dryRun: opts.dryRun });
761
- }
762
-
763
- /**
764
- * Expand a task range like T001..T053 using zero-padded numeric IDs.
765
- */
766
- expandRange(taskList, from, to) {
767
- const numOf = id => parseInt(id.replace(/\D/g, ''), 10);
768
- const prefix = from.replace(/\d+$/, '');
769
- const padLen = from.replace(prefix, '').length;
770
- const fromNum = numOf(from);
771
- const toNum = numOf(to);
772
- const ids = [];
773
- for (let n = fromNum; n <= toNum; n++) {
774
- ids.push(prefix + String(n).padStart(padLen, '0'));
775
- }
776
- return ids.filter(id => taskList.some(t => t.id === id));
777
- }
778
-
779
- /**
780
- * Add a note to a task (deviation, context, or blocker-resolved)
781
- * @param {string} featureName - Feature name
782
- * @param {string} taskId - Task ID
783
- * @param {string} text - Note text
784
- * @param {string} [type='deviation'] - Note type: deviation | context | blocker-resolved
785
- */
786
- async annotateTask(featureName, taskId, text, type = 'deviation', markDone = false) {
787
- const validTypes = ['deviation', 'context', 'blocker-resolved'];
788
- if (!validTypes.includes(type)) {
789
- throw new Error(`Invalid note type '${type}'. Valid types: ${validTypes.join(', ')}`);
790
- }
791
-
792
- const state = loadState();
793
- const feature = state.features[featureName];
794
- if (!feature) throw new Error(`Feature '${featureName}' not found`);
795
-
796
- const taskList = await ensureTaskList(feature, featureName);
797
- const task = taskList.find(t => t.id === taskId);
798
- if (!task) throw new Error(`Task ${taskId} not found in '${featureName}'`);
799
-
800
- if (!task.notes) task.notes = [];
801
- task.notes.push({
802
- text,
803
- type,
804
- addedAt: new Date().toISOString(),
805
- addedBy: 'user'
806
- });
807
-
808
- if (markDone && task.status !== 'completed') {
809
- if (task.status !== 'in_progress') {
810
- task.status = 'in_progress';
811
- task.startedAt = new Date().toISOString();
812
- }
813
- task.status = 'completed';
814
- task.completedAt = new Date().toISOString();
815
- syncCounters(feature);
816
- }
817
-
818
- feature.updatedAt = new Date().toISOString();
819
- saveState(state);
820
-
821
- console.log(chalk.green(`Added ${type} note to ${taskId}`));
822
- console.log(chalk.gray(` "${text}"`));
823
- if (markDone && task.status === 'completed') {
824
- console.log(chalk.green(`✅ Task ${taskId} marked as completed`));
825
- }
826
- }
827
- }
828
-
829
- // ============================================================================
830
- // CLI entry point
831
- // ============================================================================
832
-
833
- async function main() {
834
- const args = process.argv.slice(2);
835
- const command = args[0];
836
-
837
- const manager = new TaskManager();
838
-
839
- try {
840
- switch (command) {
841
- case 'done':
842
- case 'complete': {
843
- const skipValidation = args.includes('--skip-validation');
844
- const skipBuild = args.includes('--skip-build');
845
- const dryRun = args.includes('--dry-run');
846
- const reasonIdx = args.indexOf('--reason');
847
- const reason = reasonIdx !== -1 ? args[reasonIdx + 1] : null;
848
- const noteIdx = args.indexOf('--note');
849
- const note = noteIdx !== -1 ? args[noteIdx + 1] : null;
850
- const filteredArgs = args.filter((a, i) =>
851
- !['--skip-validation', '--skip-build', '--dry-run', '--reason', '--note'].includes(a) &&
852
- !(i > 0 && args[i - 1] === '--reason') &&
853
- !(i > 0 && args[i - 1] === '--note'));
854
- const featureName = filteredArgs[1];
855
- const taskIds = filteredArgs.slice(2);
856
-
857
- if (!featureName || taskIds.length === 0) {
858
- console.error(chalk.red('Usage: morph-spec task done <feature> <task-id> [task-id...] [--note <text>] [--skip-validation] [--skip-build] [--reason <text>] [--dry-run]'));
859
- process.exit(1);
860
- }
861
-
862
- await withStateLock(() => manager.completeTasks(featureName, taskIds, { skipValidation, skipBuild, reason, note, dryRun }));
863
- break;
864
- }
865
-
866
- case 'bulk-done': {
867
- const skipValidation = args.includes('--skip-validation');
868
- const dryRun = args.includes('--dry-run');
869
- const allFlag = args.includes('--all');
870
- const fromIdx = args.indexOf('--from');
871
- const toIdx = args.indexOf('--to');
872
- const reasonIdx = args.indexOf('--reason');
873
- const reason = reasonIdx !== -1 ? args[reasonIdx + 1] : null;
874
- const noteIdx = args.indexOf('--note');
875
- const note = noteIdx !== -1 ? args[noteIdx + 1] : null;
876
- const filteredArgs = args.filter((a, i) => !['--skip-validation', '--dry-run', '--all', '--reason', '--note'].includes(a)
877
- && !a.startsWith('--from') && !a.startsWith('--to')
878
- && !(i > 0 && args[i - 1] === '--reason')
879
- && !(i > 0 && args[i - 1] === '--note'));
880
- const featureName = filteredArgs[1];
881
- const rangeArg = filteredArgs[2]; // e.g. T001..T082
882
-
883
- if (!featureName) {
884
- console.error(chalk.red('Usage: morph-spec task bulk-done <feature> [--all | --from T001 --to T053 | T001..T082] [--note <text>] [--skip-validation] [--reason <text>] [--dry-run]'));
885
- process.exit(1);
886
- }
887
-
888
- const fromId = fromIdx !== -1 ? args[fromIdx + 1] : null;
889
- const toId = toIdx !== -1 ? args[toIdx + 1] : null;
890
-
891
- await withStateLock(() => manager.bulkCompleteTasks(featureName, { all: allFlag, from: fromId, to: toId, range: rangeArg, skipValidation, reason, note, dryRun }));
892
- break;
893
- }
894
-
895
- case 'start': {
896
- const featureName = args[1];
897
- const taskId = args[2];
898
-
899
- if (!featureName || !taskId) {
900
- console.error(chalk.red('Usage: morph-spec task start <feature> <task-id>'));
901
- process.exit(1);
902
- }
903
-
904
- await withStateLock(() => manager.startTask(featureName, taskId));
905
-
906
- // Generate sprint contract for evaluator
907
- try {
908
- const { generateSprintContract } = await import('../src/lib/contracts/sprint-contract-generator.js');
909
- const contract = generateSprintContract(process.cwd(), featureName, taskId);
910
- if (contract) {
911
- console.log(chalk.gray(` Sprint contract: .morph/features/${featureName}/5-implement/contracts/${taskId}-contract.yaml`));
912
- }
913
- } catch {
914
- // Non-blocking — contract generation is optional
915
- }
916
-
917
- break;
918
- }
919
-
920
- case 'annotate': {
921
- const featureName = args[1];
922
- const taskId = args[2];
923
- const text = args[3];
924
- const typeIdx = args.indexOf('--type');
925
- const type = typeIdx !== -1 ? args[typeIdx + 1] : 'deviation';
926
- const markDone = args.includes('--mark-done');
927
-
928
- if (!featureName || !taskId || !text) {
929
- console.error(chalk.red('Usage: morph-spec task annotate <feature> <task-id> <text> [--type deviation|context|blocker-resolved] [--mark-done]'));
930
- process.exit(1);
931
- }
932
-
933
- await manager.annotateTask(featureName, taskId, text, type, markDone);
934
- break;
935
- }
936
-
937
- default:
938
- console.error(chalk.red(`Unknown command: ${command}`));
939
- console.log(chalk.gray('\nAvailable commands:'));
940
- console.log(chalk.gray(' done <feature> <task-id...> - Mark tasks as completed (validation gate)'));
941
- console.log(chalk.gray(' bulk-done <feature> --all - Mark all pending tasks as completed'));
942
- console.log(chalk.gray(' bulk-done <feature> --from T001 --to T053 - Mark range as completed'));
943
- console.log(chalk.gray(' bulk-done <feature> T001..T082 - Mark range as completed'));
944
- console.log(chalk.gray(' start <feature> <task-id> - Start a task (dispatch gate + state tracking)'));
945
- console.log(chalk.gray(' annotate <feature> <task-id> <text> - Add a note to a task'));
946
- console.log(chalk.gray('\nFlags: --skip-validation --reason <text>, --note <text>, --dry-run'));
947
- process.exit(1);
948
- }
949
- } catch (error) {
950
- console.error(chalk.red(`\n❌ Error: ${error.message}`));
951
- process.exit(1);
952
- }
953
- }
954
-
955
- // ESM equivalent of `if (require.main === module)`
956
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
957
- main();
958
- }
959
-
960
- /**
961
- * Pure predicate: should a --skip-validation bypass warning be logged?
962
- *
963
- * @param {Array} tasksToComplete - Tasks that will be marked complete
964
- * @param {boolean} skipValidation - Whether --skip-validation flag is active
965
- * @param {boolean} dryRun - Whether --dry-run is active (has its own message)
966
- * @returns {boolean}
967
- */
968
- export function shouldLogSkipValidationWarning(tasksToComplete, skipValidation, dryRun) {
969
- return tasksToComplete.length > 0 && !!skipValidation && !dryRun;
970
- }
971
-
972
- /**
973
- * Compute the skip-validation budget for a feature.
974
- * Formula: clamp(ceil(totalTasks * 0.20), 1, 5)
975
- *
976
- * @param {Object} feature - Feature state object
977
- * @returns {number} Maximum allowed skip-validation uses
978
- */
979
- export function computeSkipBudget(feature) {
980
- const total = feature.tasks?.total || 0;
981
- if (total === 0) return 1;
982
- const isMigration = feature.workflow === 'migration' || feature.workflow === 'refactor'
983
- || (feature.tags || []).some(t => ['migration', 'refactor'].includes(t));
984
- const cap = isMigration ? 20 : 5;
985
- const rate = isMigration ? 0.40 : 0.20;
986
- return Math.max(1, Math.min(cap, Math.ceil(total * rate)));
987
- }
988
-
989
- /**
990
- * Test Task Guard — run project test suite before completing test-keyword tasks.
991
- *
992
- * Extracted as a pure, injectable function so it can be unit-tested without
993
- * spawning a full TaskManager or writing state to disk.
994
- *
995
- * @param {Array} tasksToComplete - Tasks about to be marked complete
996
- * @param {string} projectPath - Project root (process.cwd() in production)
997
- * @param {object} [opts]
998
- * @param {boolean} [opts.skipValidation] - Skip the guard entirely
999
- * @param {boolean} [opts.dryRun] - Skip the guard entirely
1000
- * @param {Function} [opts._runTestSuite] - Override runTestSuite for testing
1001
- * @param {Function} [opts._exit] - Override process.exit for testing
1002
- * @returns {void} Calls _exit/process.exit(1) on failure; returns normally otherwise
1003
- */
1004
- export function runTestGuard(tasksToComplete, projectPath, opts = {}) {
1005
- if (!tasksToComplete.length || opts.skipValidation || opts.skipBuild || opts.dryRun) return;
1006
-
1007
- const _runTS = opts._runTestSuite ?? runTestSuite;
1008
- const hasTestTask = tasksToComplete.some(t => isTestTask(t.title));
1009
- if (!hasTestTask) return;
1010
-
1011
- const suiteResult = _runTS(projectPath);
1012
-
1013
- if (suiteResult.passed === false && suiteResult.buildFailed) {
1014
- console.log(chalk.yellow('\n⚠️ Test build failed — skipping test guard'));
1015
- console.log(chalk.gray(' Ensure the project builds before running tests'));
1016
- if (suiteResult.output) {
1017
- console.log(chalk.gray(suiteResult.output.slice(-500)));
1018
- }
1019
- } else if (suiteResult.passed === false) {
1020
- console.error(chalk.red('\n❌ Test suite FAILED — tasks NOT marked as complete'));
1021
- if (suiteResult.output) {
1022
- console.error(chalk.gray(suiteResult.output.slice(-2000)));
1023
- }
1024
- console.log(chalk.gray(' Fix failing tests, then run task done again'));
1025
- console.log(chalk.gray(' Or use --skip-validation to bypass (not recommended)\n'));
1026
- (opts._exit ?? process.exit)(1);
1027
- } else if (suiteResult.skipped) {
1028
- console.log(chalk.yellow('\n⚠️ Test suite skipped — no testCommand configured'));
1029
- console.log(chalk.gray(` Reason: ${suiteResult.reason}`));
1030
- console.log(chalk.gray(' Set project.testCommand in .morph/config/config.json to enable auto-run\n'));
1031
- } else {
1032
- console.log(chalk.green('\n✅ Test suite passed'));
1033
- }
1034
- }
1035
-
1036
- export default TaskManager;