@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,91 +1,74 @@
1
- import { readFileSync, existsSync } from 'fs';
2
- import { join } from 'path';
3
-
4
- /** @typedef {'nextjs'|'dotnet'|'nodejs-cli'|'unknown'} StackName */
5
-
6
- /**
7
- * @typedef {Object} StackProfile
8
- * @property {StackName} stack
9
- * @property {boolean} isDotnet
10
- * @property {boolean} isNextjs
11
- * @property {string[]} defaultValidators
12
- * @property {string[]} checkpointValidators
13
- * @property {'contracts'|'contractsTs'} contractsOutputType
14
- * @property {'contracts.cs'|'contracts.ts'} contractsFilename
15
- * @property {'csharp'|'typescript'} contractsLanguage
16
- */
17
-
18
- const DOTNET_STACKS = new Set(['dotnet', 'aspnet']);
19
- const NEXTJS_STACKS = new Set(['nextjs', 'next', 'next.js']);
20
- const NO_VALIDATOR_STACKS = new Set(['nodejs-cli', 'node-cli', 'cli']);
21
-
22
- const PROFILES = {
23
- dotnet: {
24
- isDotnet: true, isNextjs: false,
25
- defaultValidators: ['architecture', 'packages'],
26
- checkpointValidators: ['architecture', 'packages', 'design-system', 'security'],
27
- buildCommand: 'dotnet build --no-restore',
28
- contractsOutputType: 'contracts',
29
- contractsFilename: 'contracts.cs',
30
- contractsLanguage: 'csharp',
31
- },
32
- nextjs: {
33
- isDotnet: false, isNextjs: true,
34
- defaultValidators: ['nextjs-component'],
35
- checkpointValidators: ['nextjs-component', 'css'],
36
- buildCommand: 'npx tsc --noEmit',
37
- contractsOutputType: 'contractsTs',
38
- contractsFilename: 'contracts.ts',
39
- contractsLanguage: 'typescript',
40
- },
41
- empty: {
42
- isDotnet: false, isNextjs: false,
43
- defaultValidators: [],
44
- checkpointValidators: [],
45
- buildCommand: null,
46
- contractsOutputType: 'contracts',
47
- contractsFilename: 'contracts.cs',
48
- contractsLanguage: 'csharp',
49
- },
50
- };
51
-
52
- /**
53
- * Read project stack from .morph/config/config.json.
54
- * @param {string} cwd - Project root
55
- * @returns {string|null}
56
- */
57
- function readStack(cwd) {
58
- const configPath = join(cwd, '.morph', 'config', 'config.json');
59
- if (!existsSync(configPath)) return null;
60
- try {
61
- return JSON.parse(readFileSync(configPath, 'utf8')).project?.stack ?? null;
62
- } catch {
63
- return null;
64
- }
65
- }
66
-
67
- /**
68
- * Get the StackProfile for the current project.
69
- * @param {string} [cwd] - Project root. Defaults to process.cwd().
70
- * @param {string} [stackOverride] - Skip config read and use this stack directly.
71
- * @returns {StackProfile}
72
- */
73
- export function getStackProfile(cwd, stackOverride) {
74
- const root = cwd ?? process.cwd();
75
- const raw = stackOverride ?? readStack(root);
76
- const stack = raw?.toLowerCase() ?? 'unknown';
77
-
78
- let profile;
79
- if (NEXTJS_STACKS.has(stack)) {
80
- profile = PROFILES.nextjs;
81
- } else if (NO_VALIDATOR_STACKS.has(stack)) {
82
- profile = PROFILES.empty;
83
- } else if (DOTNET_STACKS.has(stack) || stack === 'unknown') {
84
- profile = PROFILES.dotnet;
85
- } else {
86
- // Unknown non-.NET stack → empty validators, dotnet contracts (backward compat)
87
- profile = PROFILES.empty;
88
- }
89
-
90
- return { stack, ...profile };
91
- }
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ /** @typedef {'nextjs'|'dotnet'|'nodejs-cli'|'unknown'} StackName */
5
+
6
+ /**
7
+ * @typedef {Object} StackProfile
8
+ * @property {StackName} stack
9
+ * @property {boolean} isDotnet
10
+ * @property {boolean} isNextjs
11
+ * @property {string[]} defaultValidators
12
+ * @property {string[]} checkpointValidators
13
+ * @property {string|null} buildCommand
14
+ */
15
+
16
+ const DOTNET_STACKS = new Set(['dotnet', 'aspnet']);
17
+ const NEXTJS_STACKS = new Set(['nextjs', 'next', 'next.js']);
18
+ const NO_VALIDATOR_STACKS = new Set(['nodejs-cli', 'node-cli', 'cli']);
19
+
20
+ const PROFILES = {
21
+ dotnet: {
22
+ isDotnet: true, isNextjs: false,
23
+ defaultValidators: ['architecture', 'packages'],
24
+ checkpointValidators: ['architecture', 'packages', 'design-system', 'security'],
25
+ buildCommand: 'dotnet build --no-restore',
26
+ },
27
+ nextjs: {
28
+ isDotnet: false, isNextjs: true,
29
+ defaultValidators: ['nextjs-component'],
30
+ checkpointValidators: ['nextjs-component', 'css'],
31
+ buildCommand: 'npx tsc --noEmit',
32
+ },
33
+ empty: {
34
+ isDotnet: false, isNextjs: false,
35
+ defaultValidators: [],
36
+ checkpointValidators: [],
37
+ buildCommand: null,
38
+ },
39
+ };
40
+
41
+ /**
42
+ * Read project stack from .morph/config/config.json.
43
+ */
44
+ function readStack(cwd) {
45
+ const configPath = join(cwd, '.morph', 'config', 'config.json');
46
+ if (!existsSync(configPath)) return null;
47
+ try {
48
+ return JSON.parse(readFileSync(configPath, 'utf8')).project?.stack ?? null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Get the StackProfile for the current project.
56
+ */
57
+ export function getStackProfile(cwd, stackOverride) {
58
+ const root = cwd ?? process.cwd();
59
+ const raw = stackOverride ?? readStack(root);
60
+ const stack = raw?.toLowerCase() ?? 'unknown';
61
+
62
+ let profile;
63
+ if (NEXTJS_STACKS.has(stack)) {
64
+ profile = PROFILES.nextjs;
65
+ } else if (NO_VALIDATOR_STACKS.has(stack)) {
66
+ profile = PROFILES.empty;
67
+ } else if (DOTNET_STACKS.has(stack) || stack === 'unknown') {
68
+ profile = PROFILES.dotnet;
69
+ } else {
70
+ profile = PROFILES.empty;
71
+ }
72
+
73
+ return { stack, ...profile };
74
+ }
@@ -114,7 +114,7 @@ export function parseAndGroupTasks(tasksPath) {
114
114
  * @returns {Promise<Array<{id, title, status, dependencies, files, checkpoint}>>}
115
115
  */
116
116
  export async function parseTasksMd(featureName, cwd = process.cwd()) {
117
- const tasksPath = join(cwd, `.morph/features/${featureName}/4-tasks/tasks.md`);
117
+ const tasksPath = join(cwd, `.morph/features/${featureName}/2-plan/tasks.md`);
118
118
  let content = '';
119
119
  try {
120
120
  content = await readFile(tasksPath, 'utf-8');
@@ -0,0 +1,353 @@
1
+ /**
2
+ * @fileoverview Wave Resolver — Dependency-aware task grouping into execution waves.
3
+ *
4
+ * Pure algorithm module (no I/O, no state access). Takes parsed task lists and
5
+ * returns wave assignments. Tasks within a wave have no interdependencies and
6
+ * don't touch the same files, so they can safely run in parallel.
7
+ *
8
+ * @module wave-resolver
9
+ */
10
+
11
+ /**
12
+ * Validate that all dependency references point to existing task IDs.
13
+ *
14
+ * @param {Array<{id: string, dependencies: string[]}>} taskList
15
+ * @returns {{ valid: boolean, errors: string[] }}
16
+ */
17
+ export function validateDependencies(taskList) {
18
+ const ids = new Set(taskList.map(t => t.id));
19
+ const errors = [];
20
+
21
+ for (const task of taskList) {
22
+ for (const dep of task.dependencies || []) {
23
+ if (dep === task.id) {
24
+ errors.push(`${task.id} depends on itself`);
25
+ } else if (!ids.has(dep)) {
26
+ errors.push(`${task.id} depends on ${dep} which does not exist`);
27
+ }
28
+ }
29
+ }
30
+
31
+ return { valid: errors.length === 0, errors };
32
+ }
33
+
34
+ /**
35
+ * Detect circular dependencies using Kahn's algorithm.
36
+ * Returns the cycle path if found, or null if acyclic.
37
+ *
38
+ * @param {Array<{id: string, dependencies: string[]}>} taskList
39
+ * @returns {{ hasCycle: boolean, cycle: string[]|null }}
40
+ */
41
+ export function detectCircularDeps(taskList) {
42
+ const ids = new Set(taskList.map(t => t.id));
43
+ const inDegree = new Map();
44
+ const adjacency = new Map(); // dependency → dependents
45
+
46
+ for (const task of taskList) {
47
+ inDegree.set(task.id, 0);
48
+ if (!adjacency.has(task.id)) adjacency.set(task.id, []);
49
+ }
50
+
51
+ for (const task of taskList) {
52
+ for (const dep of task.dependencies || []) {
53
+ if (!ids.has(dep)) continue; // skip invalid refs (caught by validateDependencies)
54
+ inDegree.set(task.id, (inDegree.get(task.id) || 0) + 1);
55
+ if (!adjacency.has(dep)) adjacency.set(dep, []);
56
+ adjacency.get(dep).push(task.id);
57
+ }
58
+ }
59
+
60
+ // BFS: process zero-in-degree nodes
61
+ const queue = [];
62
+ for (const [id, deg] of inDegree) {
63
+ if (deg === 0) queue.push(id);
64
+ }
65
+
66
+ const processed = new Set();
67
+ while (queue.length > 0) {
68
+ const node = queue.shift();
69
+ processed.add(node);
70
+ for (const dependent of adjacency.get(node) || []) {
71
+ inDegree.set(dependent, inDegree.get(dependent) - 1);
72
+ if (inDegree.get(dependent) === 0) queue.push(dependent);
73
+ }
74
+ }
75
+
76
+ if (processed.size === taskList.length) {
77
+ return { hasCycle: false, cycle: null };
78
+ }
79
+
80
+ // Extract cycle from unprocessed nodes via DFS
81
+ const unprocessed = taskList.filter(t => !processed.has(t.id));
82
+ const depMap = new Map(taskList.map(t => [t.id, t.dependencies || []]));
83
+ const visiting = new Set();
84
+ const path = [];
85
+
86
+ function dfs(nodeId) {
87
+ if (visiting.has(nodeId)) {
88
+ const cycleStart = path.indexOf(nodeId);
89
+ return path.slice(cycleStart).concat(nodeId);
90
+ }
91
+ if (processed.has(nodeId)) return null;
92
+
93
+ visiting.add(nodeId);
94
+ path.push(nodeId);
95
+
96
+ for (const dep of depMap.get(nodeId) || []) {
97
+ if (!ids.has(dep)) continue;
98
+ const cycle = dfs(dep);
99
+ if (cycle) return cycle;
100
+ }
101
+
102
+ visiting.delete(nodeId);
103
+ path.pop();
104
+ return null;
105
+ }
106
+
107
+ for (const task of unprocessed) {
108
+ const cycle = dfs(task.id);
109
+ if (cycle) return { hasCycle: true, cycle };
110
+ }
111
+
112
+ // Fallback: cycle exists but couldn't extract path
113
+ return { hasCycle: true, cycle: unprocessed.map(t => t.id) };
114
+ }
115
+
116
+ /**
117
+ * Assign a wave level to each task via topological sort.
118
+ * Tasks with no dependencies go to wave 0. Tasks whose dependencies
119
+ * are all in wave N go to wave N+1.
120
+ *
121
+ * @param {Array<{id: string, dependencies: string[], status?: string}>} taskList
122
+ * @param {{ includeCompleted?: boolean }} options
123
+ * @returns {Array<{ waveIndex: number, tasks: string[] }>}
124
+ */
125
+ export function resolveWaves(taskList, options = {}) {
126
+ // Filter tasks
127
+ const activeTasks = options.includeCompleted
128
+ ? taskList
129
+ : taskList.filter(t => t.status !== 'completed' && t.status !== 'expanded');
130
+
131
+ if (activeTasks.length === 0) return [];
132
+
133
+ const ids = new Set(activeTasks.map(t => t.id));
134
+ const completedIds = new Set(
135
+ taskList.filter(t => t.status === 'completed' || t.status === 'expanded').map(t => t.id)
136
+ );
137
+
138
+ // Assign levels via BFS
139
+ const level = new Map();
140
+ const inDegree = new Map();
141
+ const adjacency = new Map();
142
+
143
+ for (const task of activeTasks) {
144
+ inDegree.set(task.id, 0);
145
+ adjacency.set(task.id, []);
146
+ level.set(task.id, 0);
147
+ }
148
+
149
+ for (const task of activeTasks) {
150
+ for (const dep of task.dependencies || []) {
151
+ if (!ids.has(dep)) continue; // dep is completed or doesn't exist → satisfied
152
+ inDegree.set(task.id, inDegree.get(task.id) + 1);
153
+ adjacency.get(dep).push(task.id);
154
+ }
155
+ }
156
+
157
+ // BFS with level tracking
158
+ const queue = [];
159
+ for (const [id, deg] of inDegree) {
160
+ if (deg === 0) {
161
+ queue.push(id);
162
+ level.set(id, 0);
163
+ }
164
+ }
165
+
166
+ while (queue.length > 0) {
167
+ const node = queue.shift();
168
+ const nodeLevel = level.get(node);
169
+
170
+ for (const dependent of adjacency.get(node)) {
171
+ const newDeg = inDegree.get(dependent) - 1;
172
+ inDegree.set(dependent, newDeg);
173
+
174
+ // Level = max of all dependency levels + 1
175
+ const candidateLevel = nodeLevel + 1;
176
+ if (candidateLevel > level.get(dependent)) {
177
+ level.set(dependent, candidateLevel);
178
+ }
179
+
180
+ if (newDeg === 0) queue.push(dependent);
181
+ }
182
+ }
183
+
184
+ // Group by level
185
+ const waveMap = new Map();
186
+ for (const [id, lvl] of level) {
187
+ if (!waveMap.has(lvl)) waveMap.set(lvl, []);
188
+ waveMap.get(lvl).push(id);
189
+ }
190
+
191
+ // Sort waves and task IDs within waves
192
+ const waves = [...waveMap.entries()]
193
+ .sort((a, b) => a[0] - b[0])
194
+ .map(([idx, tasks]) => ({
195
+ waveIndex: idx,
196
+ tasks: tasks.sort()
197
+ }));
198
+
199
+ return waves;
200
+ }
201
+
202
+ /**
203
+ * Parse file paths from a task's deliverables/steps section.
204
+ * Looks for backtick-wrapped paths in deliverable lines.
205
+ *
206
+ * @param {string} taskBlock - The markdown block for a single task
207
+ * @returns {string[]} Array of file paths (deduplicated, normalized to forward slashes)
208
+ */
209
+ export function parseTaskFiles(taskBlock) {
210
+ const files = new Set();
211
+
212
+ // Match: - [ ] Create/Update/Modify/Delete: `path/to/file`
213
+ const deliverableRe = /- \[[ x]\]\s*(?:Create|Update|Modify|Delete|Add|Edit|Remove):\s*`([^`]+)`/gi;
214
+ let match;
215
+ while ((match = deliverableRe.exec(taskBlock)) !== null) {
216
+ files.add(match[1].replace(/\\/g, '/'));
217
+ }
218
+
219
+ // Also match: **Files:** `path1`, `path2`
220
+ const filesFieldRe = /\*\*Files?:\*\*\s*(.+)/i;
221
+ const filesMatch = filesFieldRe.exec(taskBlock);
222
+ if (filesMatch) {
223
+ const backtickRe = /`([^`]+)`/g;
224
+ let fMatch;
225
+ while ((fMatch = backtickRe.exec(filesMatch[1])) !== null) {
226
+ files.add(fMatch[1].replace(/\\/g, '/'));
227
+ }
228
+ }
229
+
230
+ return [...files];
231
+ }
232
+
233
+ /**
234
+ * Detect file conflicts within each wave and redistribute conflicting
235
+ * tasks to later waves.
236
+ *
237
+ * @param {Array<{ waveIndex: number, tasks: string[] }>} waves
238
+ * @param {Map<string, string[]>} taskFileMap - taskId → file paths
239
+ * @returns {Array<{ waveIndex: number, tasks: string[] }>}
240
+ */
241
+ export function resolveFileConflicts(waves, taskFileMap) {
242
+ if (!taskFileMap || taskFileMap.size === 0) return waves;
243
+
244
+ // Deep copy to avoid mutation
245
+ let result = waves.map(w => ({ waveIndex: w.waveIndex, tasks: [...w.tasks] }));
246
+ const maxIterations = waves.reduce((sum, w) => sum + w.tasks.length, 0) + 1;
247
+
248
+ for (let iter = 0; iter < maxIterations; iter++) {
249
+ let changed = false;
250
+
251
+ for (let wi = 0; wi < result.length; wi++) {
252
+ const wave = result[wi];
253
+ const fileOwner = new Map(); // file → first taskId
254
+ const toDefer = [];
255
+
256
+ for (const taskId of wave.tasks) {
257
+ const files = taskFileMap.get(taskId) || [];
258
+ let hasConflict = false;
259
+
260
+ for (const file of files) {
261
+ if (fileOwner.has(file)) {
262
+ hasConflict = true;
263
+ break;
264
+ }
265
+ }
266
+
267
+ if (hasConflict) {
268
+ toDefer.push(taskId);
269
+ } else {
270
+ for (const file of files) {
271
+ fileOwner.set(file, taskId);
272
+ }
273
+ }
274
+ }
275
+
276
+ if (toDefer.length > 0) {
277
+ changed = true;
278
+ wave.tasks = wave.tasks.filter(t => !toDefer.includes(t));
279
+
280
+ // Push to next wave or create one
281
+ const nextWi = wi + 1;
282
+ if (nextWi < result.length) {
283
+ result[nextWi].tasks.push(...toDefer);
284
+ result[nextWi].tasks.sort();
285
+ } else {
286
+ result.push({ waveIndex: result.length, tasks: toDefer.sort() });
287
+ }
288
+ }
289
+ }
290
+
291
+ if (!changed) break;
292
+ }
293
+
294
+ // Remove empty waves and re-index
295
+ result = result.filter(w => w.tasks.length > 0);
296
+ result.forEach((w, i) => { w.waveIndex = i; });
297
+
298
+ return result;
299
+ }
300
+
301
+ /**
302
+ * Full resolution pipeline: validate → detect cycles → resolve waves → resolve conflicts.
303
+ *
304
+ * @param {Array<{id: string, title?: string, status?: string, dependencies: string[]}>} taskList
305
+ * @param {Map<string, string[]>} taskFileMap - Optional file map for conflict detection
306
+ * @returns {{ waves: Array<{ waveIndex: number, tasks: string[] }>, errors: string[], warnings: string[] }}
307
+ */
308
+ export function resolveWavePlan(taskList, taskFileMap = new Map()) {
309
+ const errors = [];
310
+ const warnings = [];
311
+
312
+ if (!taskList || taskList.length === 0) {
313
+ return { waves: [], errors: [], warnings: ['No tasks to resolve'] };
314
+ }
315
+
316
+ // Step 1: Validate dependencies
317
+ const validation = validateDependencies(taskList);
318
+ if (!validation.valid) {
319
+ errors.push(...validation.errors);
320
+ return { waves: [], errors, warnings };
321
+ }
322
+
323
+ // Step 2: Detect circular dependencies
324
+ const cycleCheck = detectCircularDeps(taskList);
325
+ if (cycleCheck.hasCycle) {
326
+ errors.push(`Circular dependency detected: ${cycleCheck.cycle.join(' → ')}`);
327
+ return { waves: [], errors, warnings };
328
+ }
329
+
330
+ // Step 3: Resolve waves from dependencies
331
+ let waves = resolveWaves(taskList);
332
+
333
+ // Step 4: Resolve file conflicts
334
+ if (taskFileMap.size > 0) {
335
+ const before = waves.map(w => w.tasks.length);
336
+ waves = resolveFileConflicts(waves, taskFileMap);
337
+ const after = waves.map(w => w.tasks.length);
338
+
339
+ // Report redistributions
340
+ if (JSON.stringify(before) !== JSON.stringify(after)) {
341
+ warnings.push('Some tasks were moved to later waves due to file conflicts');
342
+ }
343
+ }
344
+
345
+ // Report skipped tasks
346
+ const activeTasks = taskList.filter(t => t.status !== 'completed' && t.status !== 'expanded');
347
+ const skipped = taskList.length - activeTasks.length;
348
+ if (skipped > 0) {
349
+ warnings.push(`${skipped} completed/expanded task(s) excluded from wave plan`);
350
+ }
351
+
352
+ return { waves, errors, warnings };
353
+ }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Persists checkpoint validation results to disk for traceability.
5
5
  * Each checkpoint writes a JSON file to:
6
- * .morph/features/{feature}/5-implement/validations/checkpoint-{N}.json
6
+ * .morph/features/{feature}/3-implement/validations/checkpoint-{N}.json
7
7
  */
8
8
 
9
9
  import { mkdirSync, writeFileSync } from 'fs';
@@ -18,7 +18,7 @@ import { join } from 'path';
18
18
  * @returns {string} Path to the saved JSON file
19
19
  */
20
20
  export function persistCheckpointValidation(projectPath, featureName, result) {
21
- const dir = join(projectPath, `.morph/features/${featureName}/5-implement/validations`);
21
+ const dir = join(projectPath, `.morph/features/${featureName}/3-implement/validations`);
22
22
  mkdirSync(dir, { recursive: true });
23
23
 
24
24
  const filename = `checkpoint-${result.checkpointNum}.json`;