@polymorphism-tech/morph-spec 4.22.0 → 6.0.0

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