@polymorphism-tech/morph-spec 4.21.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) 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 -1183
  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 -28
  33. package/framework/hooks/shared/state-reader.js +4 -5
  34. package/framework/{skills/level-0-meta/mcp-registry.json → mcp/registry.json} +254 -210
  35. package/framework/phases.json +40 -114
  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/standards/STANDARDS.json +32 -0
  47. package/framework/standards/frontend/design-system/ai-image-generation.md +244 -0
  48. package/framework/standards/integration/mcp/mcp-tools.md +48 -0
  49. package/framework/templates/CLAUDE.md.core.guide.md +29 -0
  50. package/framework/templates/CLAUDE.md.core.template +22 -0
  51. package/framework/templates/REGISTRY.json +4 -4
  52. package/framework/workflows/configs/fast-track.json +5 -10
  53. package/framework/workflows/configs/full.json +4 -8
  54. package/framework/workflows/configs/quick.json +32 -0
  55. package/framework/workflows/configs/standard.json +57 -64
  56. package/package.json +7 -6
  57. package/src/commands/mcp/mcp-setup.js +58 -31
  58. package/src/commands/project/doctor.js +69 -70
  59. package/src/commands/project/init.js +26 -5
  60. package/src/commands/project/status.js +18 -19
  61. package/src/commands/state/approve.js +1 -1
  62. package/src/commands/validation/validate-feature.js +18 -112
  63. package/src/core/paths/output-schema.js +133 -151
  64. package/src/core/state/phase-state-machine.js +10 -24
  65. package/src/core/state/state-manager.js +17 -17
  66. package/src/core/workflows/workflow-detector.js +214 -31
  67. package/src/core/workflows/workflow-router.js +204 -0
  68. package/src/dashboard/app/pages/AgentMap.jsx +32 -33
  69. package/src/dashboard/app/pages/LiveState.jsx +1 -1
  70. package/src/dashboard/app/pages/PhasePipeline.jsx +2 -4
  71. package/src/dashboard/app/pages/SkillsLibrary.jsx +2 -3
  72. package/src/dashboard/app/pages/Workflows.jsx +2 -2
  73. package/src/dashboard/scanner/agents-scanner.js +30 -35
  74. package/src/lib/agents/metrics-aggregator.js +1 -1
  75. package/src/lib/agents/output-collector.js +7 -8
  76. package/src/lib/context/context-builder.js +289 -0
  77. package/src/lib/detectors/claude-config-detector.js +19 -3
  78. package/src/lib/generators/context-generator.js +20 -42
  79. package/src/lib/generators/index.js +1 -0
  80. package/src/lib/generators/mandate-generator.js +512 -0
  81. package/src/lib/generators/metadata-extractor.js +1 -1
  82. package/src/lib/generators/recap-generator.js +3 -4
  83. package/src/lib/installers/mcp-installer.js +300 -92
  84. package/src/lib/phase-chain/phase-validator.js +36 -40
  85. package/src/lib/stack/stack-profile.js +74 -91
  86. package/src/lib/tasks/task-parser.js +1 -1
  87. package/src/lib/tasks/wave-resolver.js +353 -0
  88. package/src/lib/validators/checkpoint-persistence.js +2 -2
  89. package/src/lib/validators/content/cross-phase-validator.js +281 -0
  90. package/src/lib/validators/content/index.js +1 -0
  91. package/src/lib/validators/shared/emit-validator-dispatch.js +6 -56
  92. package/src/scripts/global-install.js +2 -27
  93. package/src/utils/agents-installer.js +224 -447
  94. package/src/utils/claude-settings-manager.js +57 -15
  95. package/src/utils/file-copier.js +1 -0
  96. package/src/utils/hooks-installer.js +15 -117
  97. package/src/utils/skills-installer.js +12 -18
  98. package/bin/detect-agents.js +0 -226
  99. package/bin/task-manager.js +0 -1036
  100. package/framework/agents/README.md +0 -13
  101. package/framework/agents/ai-agents/ai-system-architect.md +0 -194
  102. package/framework/agents/ai-agents/vector-search-expert.md +0 -247
  103. package/framework/agents/architecture/architecture-expert.md +0 -98
  104. package/framework/agents/architecture/morph-spec-architect.md +0 -66
  105. package/framework/agents/architecture/packages-validator.md +0 -89
  106. package/framework/agents/architecture/security-expert.md +0 -79
  107. package/framework/agents/architecture/seo-growth-hacker.md +0 -168
  108. package/framework/agents/architecture/standards-architect.md +0 -153
  109. package/framework/agents/backend/dotnet-senior.md +0 -163
  110. package/framework/agents/backend/ef-modeler.md +0 -119
  111. package/framework/agents/backend/hangfire-orchestrator.md +0 -133
  112. package/framework/agents/backend/ms-agent-expert.md +0 -55
  113. package/framework/agents/backend/neon-expert.md +0 -379
  114. package/framework/agents/frontend/css-specialist.md +0 -89
  115. package/framework/agents/frontend/design-system-validator.md +0 -100
  116. package/framework/agents/frontend/gsap-animator.md +0 -131
  117. package/framework/agents/frontend/nextjs-component-validator.md +0 -97
  118. package/framework/agents/frontend/nextjs-expert.md +0 -201
  119. package/framework/agents/frontend/ui-designer.md +0 -259
  120. package/framework/agents/infrastructure/infra-architect.md +0 -215
  121. package/framework/agents/integrations/asaas-financial.md +0 -138
  122. package/framework/agents/integrations/neon-auth.md +0 -151
  123. package/framework/agents/integrations/resend-email.md +0 -127
  124. package/framework/hooks/claude-code/post-tool-use/agent-complete.js +0 -122
  125. package/framework/hooks/claude-code/post-tool-use/dispatch.js +0 -363
  126. package/framework/hooks/claude-code/post-tool-use/handle-tool-failure.js +0 -78
  127. package/framework/hooks/claude-code/post-tool-use/validator-feedback.js +0 -194
  128. package/framework/hooks/claude-code/pre-compact/save-morph-context.js +0 -199
  129. package/framework/hooks/claude-code/pre-tool-use/task-tracking-guard.js +0 -55
  130. package/framework/hooks/claude-code/session-start/inject-morph-context.js +0 -323
  131. package/framework/hooks/claude-code/session-start/post-compact-restore.js +0 -41
  132. package/framework/hooks/claude-code/session-start/resume-context.js +0 -77
  133. package/framework/hooks/claude-code/teammate-idle/teammate-idle.js +0 -90
  134. package/framework/hooks/claude-code/user-prompt/enrich-prompt.js +0 -114
  135. package/framework/hooks/shared/compact-restore.js +0 -123
  136. package/framework/hooks/shared/dispatch-helpers.js +0 -147
  137. package/framework/hooks/shared/skill-reminder-helpers.js +0 -79
  138. package/framework/hooks/shared/stale-task-reset.js +0 -57
  139. package/framework/hooks/shared/worktree-helpers.js +0 -75
  140. package/framework/skills/level-0-meta/README.md +0 -7
  141. package/framework/skills/level-0-meta/morph-agent-teams/SKILL.md +0 -472
  142. package/framework/skills/level-0-meta/morph-brainstorming/SKILL.md +0 -174
  143. package/framework/skills/level-0-meta/morph-brainstorming/references/proposal-example.md +0 -138
  144. package/framework/skills/level-0-meta/morph-checklist/SKILL.md +0 -145
  145. package/framework/skills/level-0-meta/morph-code-review/SKILL.md +0 -250
  146. package/framework/skills/level-0-meta/morph-code-review/references/review-example.md +0 -164
  147. package/framework/skills/level-0-meta/morph-code-review/references/review-guidelines.md +0 -100
  148. package/framework/skills/level-0-meta/morph-code-review/scripts/scan-csharp.mjs +0 -151
  149. package/framework/skills/level-0-meta/morph-code-review-nextjs/SKILL.md +0 -165
  150. package/framework/skills/level-0-meta/morph-code-review-nextjs/references/review-example-nextjs.md +0 -254
  151. package/framework/skills/level-0-meta/morph-code-review-nextjs/scripts/scan-nextjs.mjs +0 -189
  152. package/framework/skills/level-0-meta/morph-frontend-review/SKILL.md +0 -344
  153. package/framework/skills/level-0-meta/morph-frontend-review/scripts/scan-accessibility.mjs +0 -376
  154. package/framework/skills/level-0-meta/morph-init/SKILL.md +0 -537
  155. package/framework/skills/level-0-meta/morph-parallel-dispatch/SKILL.md +0 -225
  156. package/framework/skills/level-0-meta/morph-post-implementation/SKILL.md +0 -382
  157. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-dev-server.mjs +0 -153
  158. package/framework/skills/level-0-meta/morph-post-implementation/scripts/detect-stack.mjs +0 -234
  159. package/framework/skills/level-0-meta/morph-simulation-checklist/SKILL.md +0 -106
  160. package/framework/skills/level-0-meta/morph-tool-usage-guide/SKILL.md +0 -243
  161. package/framework/skills/level-0-meta/morph-tool-usage-guide/references/tools-per-phase.md +0 -169
  162. package/framework/skills/level-0-meta/morph-verification-before-completion/SKILL.md +0 -148
  163. package/framework/skills/level-0-meta/morph-verification-before-completion/scripts/check-phase-outputs.mjs +0 -110
  164. package/framework/skills/level-1-workflows/README.md +0 -7
  165. package/framework/skills/level-1-workflows/morph-phase-codebase-analysis/SKILL.md +0 -269
  166. package/framework/skills/level-1-workflows/morph-phase-design/SKILL.md +0 -453
  167. package/framework/skills/level-1-workflows/morph-phase-design/references/architecture-analysis-guide.md +0 -44
  168. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-authoring-guide.md +0 -55
  169. package/framework/skills/level-1-workflows/morph-phase-design/references/spec-example.md +0 -253
  170. package/framework/skills/level-1-workflows/morph-phase-implement/.morph/logs/activity.json +0 -38
  171. package/framework/skills/level-1-workflows/morph-phase-implement/SKILL.md +0 -493
  172. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/code-quality-reviewer-prompt.md +0 -50
  173. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/implementer-prompt.md +0 -46
  174. package/framework/skills/level-1-workflows/morph-phase-implement/prompts/spec-reviewer-prompt.md +0 -47
  175. package/framework/skills/level-1-workflows/morph-phase-implement/references/recap-example.md +0 -132
  176. package/framework/skills/level-1-workflows/morph-phase-implement/references/vsa-implementation-guide.md +0 -92
  177. package/framework/skills/level-1-workflows/morph-phase-plan/SKILL.md +0 -257
  178. package/framework/skills/level-1-workflows/morph-phase-tasks/.morph/logs/activity.json +0 -14
  179. package/framework/skills/level-1-workflows/morph-phase-tasks/SKILL.md +0 -291
  180. package/framework/skills/level-1-workflows/morph-phase-tasks/references/task-planning-patterns.md +0 -143
  181. package/framework/skills/level-1-workflows/morph-phase-tasks/references/tasks-example.md +0 -404
  182. package/framework/skills/level-1-workflows/morph-phase-tasks/scripts/validate-tasks.mjs +0 -112
  183. package/framework/skills/level-1-workflows/morph-phase-uiux/SKILL.md +0 -788
  184. package/framework/skills/level-1-workflows/morph-scope-escalation/SKILL.md +0 -147
  185. package/framework/squad-templates/backend-only.json +0 -47
  186. package/framework/squad-templates/frontend-only.json +0 -56
  187. package/framework/squad-templates/full-stack.json +0 -74
  188. package/src/commands/agents/agent-metrics.js +0 -174
  189. package/src/commands/agents/audit-dispatch.js +0 -82
  190. package/src/commands/agents/detect-agents.js +0 -275
  191. package/src/commands/agents/dispatch-agents.js +0 -461
  192. package/src/commands/agents/index.js +0 -13
  193. package/src/commands/agents/manage-agents.js +0 -352
  194. package/src/commands/agents/project-metrics.js +0 -74
  195. package/src/commands/phase/phase-reset.js +0 -74
  196. package/src/commands/scope/escalate.js +0 -217
  197. package/src/commands/state/advance-phase.js +0 -600
  198. package/src/commands/state/phase-runner.js +0 -215
  199. package/src/commands/state/state.js +0 -444
  200. package/src/commands/task/expand.js +0 -100
  201. package/src/commands/tasks/index.js +0 -4
  202. package/src/commands/tasks/task.js +0 -105
  203. package/src/commands/templates/index.js +0 -5
  204. package/src/commands/templates/template-render.js +0 -93
  205. package/src/commands/trust/trust.js +0 -118
  206. package/src/lib/agents/agents-manager.js +0 -349
  207. package/src/lib/build/build-runner.js +0 -99
  208. package/src/lib/checkpoints/checkpoint-hooks.js +0 -111
  209. package/src/lib/contracts/sprint-contract-generator.js +0 -137
  210. package/src/lib/scope/impact-analyzer.js +0 -106
  211. package/src/lib/trust/trust-manager.js +0 -157
  212. package/src/utils/tier-helpers.js +0 -24
  213. /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;