@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 (377) hide show
  1. package/CLAUDE.md +51 -43
  2. package/README.md +39 -38
  3. package/bin/morph-spec.js +41 -113
  4. package/bin/validate.js +1 -147
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +86 -21
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-status.md +4 -4
  17. package/framework/docs/CLI.md +574 -286
  18. package/framework/evals/review-checklist.md +82 -41
  19. package/framework/hooks/README.md +10 -11
  20. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  21. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  22. package/framework/hooks/claude-code/core/chain.js +4 -65
  23. package/framework/hooks/claude-code/core/comment-guard.js +3 -17
  24. package/framework/hooks/claude-code/core/denylist.json +33 -3
  25. package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
  26. package/framework/hooks/claude-code/core/gate-guard.js +9 -78
  27. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  28. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  29. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  30. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  31. package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
  32. package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
  33. package/framework/hooks/claude-code/core/state-sync.js +15 -85
  34. package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
  35. package/framework/hooks/claude-code/core/validate-completion.js +37 -147
  36. package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
  37. package/framework/hooks/claude-code/core/worktree-context.js +15 -114
  38. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  39. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  40. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  41. package/framework/hooks/claude-code/statusline.py +55 -41
  42. package/framework/hooks/package.json +3 -0
  43. package/framework/hooks/shared/activity-logger.js +9 -89
  44. package/framework/hooks/shared/agent-reap.js +2 -17
  45. package/framework/hooks/shared/build-check.js +11 -76
  46. package/framework/hooks/shared/claim-registry.js +9 -119
  47. package/framework/hooks/shared/command-tokens.js +168 -44
  48. package/framework/hooks/shared/comment-rules.js +5 -20
  49. package/framework/hooks/shared/comment-scanner.js +11 -43
  50. package/framework/hooks/shared/comment-stripper.js +11 -85
  51. package/framework/hooks/shared/e2e-check.js +0 -0
  52. package/framework/hooks/shared/errata.js +59 -0
  53. package/framework/hooks/shared/feature-locator.js +11 -95
  54. package/framework/hooks/shared/frontend-locations.js +4 -0
  55. package/framework/hooks/shared/gate-question.js +12 -0
  56. package/framework/hooks/shared/git-exclude.js +3 -20
  57. package/framework/hooks/shared/git-paths.js +8 -104
  58. package/framework/hooks/shared/graph-freshness.js +22 -166
  59. package/framework/hooks/shared/graph-refresh.js +21 -149
  60. package/framework/hooks/shared/heavy-command.js +4 -12
  61. package/framework/hooks/shared/hook-response.js +8 -83
  62. package/framework/hooks/shared/morph-cli.js +8 -52
  63. package/framework/hooks/shared/payload-utils.js +10 -132
  64. package/framework/hooks/shared/phase-utils.js +14 -72
  65. package/framework/hooks/shared/redact-secrets.js +4 -15
  66. package/framework/hooks/shared/reparse-scan.js +6 -66
  67. package/framework/hooks/shared/slice-progress.js +99 -0
  68. package/framework/hooks/shared/source-files.js +4 -17
  69. package/framework/hooks/shared/spawn-command.js +23 -0
  70. package/framework/hooks/shared/state-reader.js +51 -247
  71. package/framework/hooks/shared/state-writer.js +9 -54
  72. package/framework/hooks/shared/stdin-reader.js +1 -11
  73. package/framework/hooks/shared/telemetry-logger.js +7 -67
  74. package/framework/hooks/shared/transcript-usage.js +7 -29
  75. package/framework/hooks/shared/worktree-root.js +7 -29
  76. package/framework/mcp/registry.json +3 -4
  77. package/framework/phases.json +23 -39
  78. package/framework/rules/csharp-standards.md +1 -1
  79. package/framework/rules/frontend-standards.md +1 -0
  80. package/framework/rules/hangfire-standards.md +1 -1
  81. package/framework/rules/neon-standards.md +1 -1
  82. package/framework/schemas/config.schema.json +14 -9
  83. package/framework/schemas/feature.schema.json +15 -50
  84. package/framework/skills/README.md +3 -4
  85. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  86. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  87. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  88. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  89. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  90. package/framework/skills/core/morph-media/SKILL.md +1 -1
  91. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  92. package/framework/skills/core/morph-review/SKILL.md +111 -136
  93. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  94. package/framework/skills/core/morph-uiux/SKILL.md +94 -152
  95. package/framework/standards/STANDARDS.json +109 -1219
  96. package/framework/standards/STANDARDS.meta.json +1319 -0
  97. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  98. package/framework/standards/ai-agents/agent-session.md +1 -1
  99. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  100. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  101. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  102. package/framework/standards/ai-agents/guardrails.md +1 -1
  103. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  104. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  105. package/framework/standards/ai-agents/production.md +1 -1
  106. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  107. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  108. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  109. package/framework/standards/ai-agents/setup.md +3 -3
  110. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  111. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  112. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  113. package/framework/standards/ai-agents/workflows.md +1 -1
  114. package/framework/standards/architecture/code-comments.md +7 -7
  115. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  116. package/framework/standards/backend/api/validation.md +1 -1
  117. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  118. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  119. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  120. package/framework/standards/data/nosql/cache/redis.md +1 -1
  121. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  122. package/framework/standards/frontend/admin-app/forms.md +1 -1
  123. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  124. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  125. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  126. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  127. package/framework/templates/CLAUDE.md.core.template +3 -3
  128. package/framework/templates/README.md +2 -3
  129. package/framework/templates/REGISTRY.json +15 -179
  130. package/framework/templates/docs/plan.md +72 -0
  131. package/framework/templates/docs/ui-mockups.md +4 -0
  132. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  134. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  135. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  136. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  137. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  138. package/package.json +1 -1
  139. package/src/commands/context/context-refresh.js +0 -16
  140. package/src/commands/dashboard/dashboard.js +2 -8
  141. package/src/commands/e2e/e2e.js +18 -105
  142. package/src/commands/graph/affected.js +0 -14
  143. package/src/commands/graph/build.js +11 -50
  144. package/src/commands/graph/explain.js +0 -16
  145. package/src/commands/graph/status.js +1 -12
  146. package/src/commands/mcp/mcp-setup.js +4 -62
  147. package/src/commands/observability/cost.js +17 -111
  148. package/src/commands/observability/telemetry.js +1 -14
  149. package/src/commands/project/branch.js +4 -45
  150. package/src/commands/project/claim.js +4 -48
  151. package/src/commands/project/doctor-procs.js +1 -1
  152. package/src/commands/project/doctor.js +53 -307
  153. package/src/commands/project/env.js +1 -27
  154. package/src/commands/project/fleet.js +6 -35
  155. package/src/commands/project/heavy.js +3 -3
  156. package/src/commands/project/init.js +8 -68
  157. package/src/commands/project/install-plugin-cmd.js +0 -1
  158. package/src/commands/project/setup-infra-cmd.js +2 -1
  159. package/src/commands/project/stack-scan.js +58 -0
  160. package/src/commands/project/standards.js +65 -72
  161. package/src/commands/project/status.js +28 -121
  162. package/src/commands/project/update.js +31 -223
  163. package/src/commands/project/worktree.js +109 -883
  164. package/src/commands/scaffold/scaffold.js +0 -25
  165. package/src/commands/state/advance.js +65 -120
  166. package/src/commands/state/approve.js +87 -229
  167. package/src/commands/state/archive.js +23 -139
  168. package/src/commands/state/auto-approval.js +65 -0
  169. package/src/commands/state/create.js +49 -109
  170. package/src/commands/state/delete.js +11 -63
  171. package/src/commands/state/finish.js +105 -494
  172. package/src/commands/state/gate-check.js +74 -183
  173. package/src/commands/state/gate-decision.js +8 -42
  174. package/src/commands/state/handoff.js +4 -31
  175. package/src/commands/state/list.js +21 -91
  176. package/src/commands/state/report-check.js +134 -338
  177. package/src/commands/state/retype.js +12 -28
  178. package/src/commands/validation/index.js +0 -3
  179. package/src/commands/validation/mutate.js +79 -19
  180. package/src/commands/validation/plan-check.js +81 -0
  181. package/src/commands/validation/validate-feature.js +52 -216
  182. package/src/commands/validation/verify.js +45 -160
  183. package/src/core/paths/output-schema.js +9 -155
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +2 -30
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +32 -395
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +21 -0
  191. package/src/core/work-types.js +10 -45
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
  194. package/src/dashboard/app/components/CommandPalette.jsx +4 -11
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +3 -9
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/pages/Activity.jsx +1 -1
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
  211. package/src/dashboard/app/pages/Features.jsx +9 -16
  212. package/src/dashboard/app/pages/Guide.jsx +2 -3
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +14 -83
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +1 -7
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +92 -113
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +4 -7
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/lib/ai/ai-checks.js +7 -52
  233. package/src/lib/ai/ai-project-scan.js +14 -108
  234. package/src/lib/ai/code-audit.js +7 -63
  235. package/src/lib/ai/pin-drift.js +8 -51
  236. package/src/lib/ai/registry-audit.js +11 -90
  237. package/src/lib/ci/ci-verify.js +14 -13
  238. package/src/lib/ci/github-ci.js +9 -37
  239. package/src/lib/ci/workflow-scaffold.js +2 -10
  240. package/src/lib/detectors/claude-config-detector.js +13 -74
  241. package/src/lib/detectors/index.js +8 -59
  242. package/src/lib/e2e/compose-env.js +5 -66
  243. package/src/lib/e2e/stack-teardown.js +6 -40
  244. package/src/lib/generators/architecture-scanner.js +18 -156
  245. package/src/lib/generators/handoff-generator.js +37 -112
  246. package/src/lib/generators/scaffold-generator.js +5 -101
  247. package/src/lib/generators/settings-generator.js +11 -122
  248. package/src/lib/git/branch-classifier.js +11 -84
  249. package/src/lib/git/default-branch.js +14 -169
  250. package/src/lib/git/evidence-staleness.js +2 -1
  251. package/src/lib/git/gh.js +2 -17
  252. package/src/lib/git/pending-prs.js +6 -37
  253. package/src/lib/git/pr-files.js +3 -28
  254. package/src/lib/git/pr-index.js +3 -34
  255. package/src/lib/git/tree-fingerprint.js +17 -17
  256. package/src/lib/git/tree-state.js +6 -33
  257. package/src/lib/graph/god-nodes.js +2 -22
  258. package/src/lib/graph/graph-health.js +3 -24
  259. package/src/lib/graph/graph-loader.js +5 -56
  260. package/src/lib/graph/graphify-runtime.js +7 -37
  261. package/src/lib/graph/graphifyignore.js +8 -74
  262. package/src/lib/graph/sanitize.js +16 -136
  263. package/src/lib/heavy/cleanup.js +2 -6
  264. package/src/lib/installers/effects.js +8 -93
  265. package/src/lib/installers/mcp-installer.js +9 -169
  266. package/src/lib/installers/memory-installer.js +8 -33
  267. package/src/lib/installers/templates-reconcile.js +5 -60
  268. package/src/lib/mutation/commands.js +3 -2
  269. package/src/lib/mutation/dotnet-introspection.js +281 -0
  270. package/src/lib/mutation/plan.js +14 -6
  271. package/src/lib/mutation/runner.js +1 -1
  272. package/src/lib/mutation/subset.js +47 -13
  273. package/src/lib/plan/artifact-vocabulary.js +40 -0
  274. package/src/lib/plan/checks.js +208 -0
  275. package/src/lib/plan/plan-parser.js +204 -0
  276. package/src/lib/plan/plan-template.js +18 -0
  277. package/src/lib/projects/registry.js +7 -44
  278. package/src/lib/review-reports.js +204 -0
  279. package/src/lib/risk-detector.js +131 -274
  280. package/src/lib/specs/adr-index.js +38 -85
  281. package/src/lib/specs/design-seed.js +18 -0
  282. package/src/lib/specs/frontmatter.js +31 -0
  283. package/src/lib/specs/gfm-table.js +11 -0
  284. package/src/lib/specs/revocations.js +31 -102
  285. package/src/lib/specs/screen-checklist.js +56 -132
  286. package/src/lib/stack/dotnet-build-ignores.js +43 -0
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +260 -0
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +37 -0
  291. package/src/lib/standards/sections.js +47 -0
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +23 -179
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -313
  296. package/src/lib/tasks/test-runner.js +107 -447
  297. package/src/lib/telemetry/metrics.js +13 -103
  298. package/src/lib/telemetry/telemetry-reader.js +13 -238
  299. package/src/lib/telemetry/verify-event.js +4 -34
  300. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  301. package/src/lib/validators/code-budget/index.js +0 -7
  302. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
  303. package/src/lib/validators/contracts/index.js +0 -6
  304. package/src/lib/validators/nextjs/index.js +0 -5
  305. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  306. package/src/lib/validators/packages/index.js +0 -6
  307. package/src/lib/validators/packages/package-validator.js +6 -56
  308. package/src/lib/validators/reuse/index.js +0 -8
  309. package/src/lib/validators/reuse/reuse-validator.js +24 -137
  310. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  311. package/src/lib/validators/traceability-check.js +54 -156
  312. package/src/lib/validators/validation-runner.js +20 -125
  313. package/src/lib/validators/verify-runner.js +152 -575
  314. package/src/lib/work-type-detector.js +5 -67
  315. package/src/lib/worktree/docker-audit.js +17 -137
  316. package/src/lib/worktree/env-allocator.js +10 -98
  317. package/src/lib/worktree/feature-truth.js +12 -115
  318. package/src/lib/worktree/finish-backup.js +32 -242
  319. package/src/lib/worktree/fleet-scanner.js +31 -126
  320. package/src/lib/worktree/ignored-triage.js +15 -91
  321. package/src/lib/worktree/vscode-settings.js +2 -3
  322. package/src/lib/worktree/worktree-audit.js +21 -213
  323. package/src/lib/worktree/worktree-include.js +3 -5
  324. package/src/scripts/global-install.js +3 -9
  325. package/src/scripts/install-plugin.js +1 -13
  326. package/src/scripts/setup-infra.js +17 -138
  327. package/src/utils/agents-installer.js +92 -272
  328. package/src/utils/banner.js +4 -25
  329. package/src/utils/claude-md-injector.js +1 -37
  330. package/src/utils/claude-settings-manager.js +1 -87
  331. package/src/utils/file-copier.js +32 -90
  332. package/src/utils/hooks-installer.js +26 -296
  333. package/src/utils/skills-installer.js +4 -102
  334. package/src/utils/version-checker.js +4 -83
  335. package/framework/commands/morph-proposal.md +0 -57
  336. package/framework/evals/trace-schema.md +0 -67
  337. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  338. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  339. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  340. package/framework/hooks/shared/tasks-validator.js +0 -111
  341. package/framework/rules/morph-task-tracking.md +0 -54
  342. package/framework/rules/morph-workflow.md +0 -244
  343. package/framework/schemas/tasks.schema.json +0 -79
  344. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  345. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  346. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  347. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  348. package/framework/templates/docs/proposal.md +0 -192
  349. package/framework/templates/docs/spec.md +0 -57
  350. package/framework/templates/docs/ui-components.md +0 -52
  351. package/framework/templates/docs/ui-design-system.md +0 -76
  352. package/framework/templates/docs/ui-flows.md +0 -167
  353. package/framework/templates/examples/spec-examples.md +0 -91
  354. package/framework/templates/feature/decisions.md +0 -124
  355. package/framework/templates/feature/recap.md +0 -145
  356. package/framework/templates/tasks.template.json +0 -17
  357. package/src/commands/state/usecase.js +0 -143
  358. package/src/commands/tasks/dag.js +0 -181
  359. package/src/commands/utils/index.js +0 -3
  360. package/src/lib/agents/output-collector.js +0 -76
  361. package/src/lib/detectors/design-system-detector.js +0 -188
  362. package/src/lib/evaluator-report.js +0 -104
  363. package/src/lib/generators/design-system-generator.js +0 -508
  364. package/src/lib/specs/acervo-format.js +0 -70
  365. package/src/lib/specs/acervo-lifecycle.js +0 -264
  366. package/src/lib/specs/done-criteria-check.js +0 -200
  367. package/src/lib/specs/usecase-check.js +0 -301
  368. package/src/lib/specs/usecase-promoter.js +0 -301
  369. package/src/lib/specs/usecase-registry.js +0 -237
  370. package/src/lib/tasks/dispatch-planner.js +0 -319
  371. package/src/lib/tasks/dotnet-test-introspection.js +0 -632
  372. package/src/lib/tasks/mermaid-renderer.js +0 -132
  373. package/src/lib/tasks/task-classifier.js +0 -30
  374. package/src/lib/tasks/task-parser.js +0 -209
  375. package/src/lib/tasks/tasks-json-parser.js +0 -296
  376. package/src/lib/tasks/test-scope.js +0 -597
  377. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,237 +0,0 @@
1
- /**
2
- * usecase-registry.js — atomic, cross-worktree UC-{nn} number allocation.
3
- *
4
- * Two parallel worktrees planning at the same time each read the acervo
5
- * (`.morph/specs/usecases/`) plus their OWN feature's `2-plan/usecases/`, take
6
- * the max `UC-{nn}` and add one — but a worktree only ever sees its OWN tree.
7
- * Sibling features' use cases live in SIBLING worktrees (feature.json and every
8
- * UC file travel on the branch, not through any shared filesystem). Two
9
- * worktrees both computing max+1 from the same stale max both mint `UC-07` —
10
- * and at promotion time `usecase-promoter.js` overwrites by ID: the SECOND
11
- * feature to close silently destroys the FIRST one's use case. A user lost
12
- * work this way in production.
13
- *
14
- * This module closes the gap the same way `env-allocator.js` (port blocks) and
15
- * `claim-registry.js` (feature ownership) already do: a registry file under the
16
- * git COMMON dir, the one location every linked worktree of a repo shares —
17
- * `<git-common-dir>/morph-spec/uc-registry.json`. Writes are tmp+rename; a
18
- * corrupt registry reads as empty (fail-open — the cost is a re-seed from disk,
19
- * never a crash).
20
- *
21
- * Unlike the other two registries, correctness here cannot be advisory: the
22
- * whole point is that two concurrent callers must never receive the same
23
- * number. `env-allocator`/`claim-registry` key by FEATURE, so two different
24
- * features racing never contend for the same key; two worktrees reserving UC
25
- * numbers race for the SAME counter. So allocation runs inside a real mutual
26
- * exclusion lock (`uc-registry.lock`, `open(..., 'wx')` — atomic create-if-absent
27
- * on every platform Node targets, including Windows) instead of the bare
28
- * read-modify-write the other two registries use.
29
- *
30
- * No git (no common dir resolvable): falls back to a project-local registry
31
- * under `.morph/logs/` (already gitignored) — this still prevents a single
32
- * session from reserving the same number twice across repeated calls, but
33
- * carries none of the cross-worktree guarantee. Callers must surface that
34
- * degradation to the user.
35
- */
36
-
37
- import {
38
- closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync,
39
- renameSync, statSync, unlinkSync, writeFileSync,
40
- } from 'fs';
41
- import { join } from 'path';
42
-
43
- // From acervo-format.js, not usecase-promoter.js: the promoter depends on THIS
44
- // module (to look up who reserved a number), and a two-way import is circular.
45
- import { useCaseId } from './acervo-format.js';
46
-
47
- /** `UC-7` (or any width) -> `UC-07` — the archive's zero-padded convention. */
48
- function formatUcId(n) {
49
- return `UC-${String(n).padStart(2, '0')}`;
50
- }
51
-
52
- /** Directory holding both the registry file and its lock. */
53
- export function registryDir({ gitCommonDir, cwd }) {
54
- return gitCommonDir ? join(gitCommonDir, 'morph-spec') : join(cwd, '.morph', 'logs');
55
- }
56
-
57
- /** Registry file path — shared (git common dir) or local-fallback (no git). */
58
- export function ucRegistryPath({ gitCommonDir, cwd }) {
59
- return join(registryDir({ gitCommonDir, cwd }), 'uc-registry.json');
60
- }
61
-
62
- function readRegistry(path) {
63
- try {
64
- const raw = readFileSync(path, 'utf8').replace(/^/, '');
65
- const parsed = JSON.parse(raw);
66
- if (parsed && typeof parsed.reservations === 'object' && parsed.reservations !== null) {
67
- return {
68
- version: 1,
69
- counter: Number.isInteger(parsed.counter) ? parsed.counter : 0,
70
- reservations: parsed.reservations,
71
- };
72
- }
73
- } catch { /* fail-open: absent or corrupt registry restarts empty */ }
74
- return { version: 1, counter: 0, reservations: {} };
75
- }
76
-
77
- function writeRegistry(path, registry) {
78
- mkdirSync(join(path, '..'), { recursive: true });
79
- const tmp = `${path}.tmp.${process.pid}`;
80
- writeFileSync(tmp, `${JSON.stringify(registry, null, 2)}\n`, 'utf8');
81
- renameSync(tmp, path);
82
- }
83
-
84
- /** Synchronous sleep — Node (unlike browsers) allows `Atomics.wait` on the main thread. */
85
- function sleepSync(ms) {
86
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
87
- }
88
-
89
- /**
90
- * Run `fn` while holding an exclusive filesystem lock at `lockPath`.
91
- *
92
- * `openSync(path, 'wx')` is `O_CREAT | O_EXCL` — atomic create-or-fail, the
93
- * only primitive this needs and one that behaves identically on POSIX and
94
- * Windows. A lock file whose mtime is older than `staleMs` is assumed to be
95
- * left behind by a crashed process and is force-recovered — an orphaned lock
96
- * must never strand every future reservation.
97
- *
98
- * @param {string} lockPath
99
- * @param {() => T} fn
100
- * @returns {T}
101
- * @template T
102
- */
103
- function withLock(lockPath, fn, { timeoutMs = 5000, pollMs = 20, staleMs = 15000 } = {}) {
104
- mkdirSync(join(lockPath, '..'), { recursive: true });
105
- const deadline = Date.now() + timeoutMs;
106
- for (;;) {
107
- let fd;
108
- try {
109
- fd = openSync(lockPath, 'wx');
110
- } catch (err) {
111
- if (err.code !== 'EEXIST') throw err;
112
- try {
113
- const st = statSync(lockPath);
114
- if (Date.now() - st.mtimeMs > staleMs) {
115
- try { unlinkSync(lockPath); } catch { /* raced someone else's cleanup */ }
116
- continue; // retry immediately — the stale lock is (probably) gone now
117
- }
118
- } catch { continue; } // lock vanished between openSync and statSync — retry
119
- if (Date.now() >= deadline) {
120
- throw new Error(`usecase-registry: could not acquire lock at ${lockPath} within ${timeoutMs}ms`);
121
- }
122
- sleepSync(pollMs);
123
- continue;
124
- }
125
- try {
126
- closeSync(fd);
127
- return fn();
128
- } finally {
129
- try { unlinkSync(lockPath); } catch { /* best-effort */ }
130
- }
131
- }
132
- }
133
-
134
- /** Highest `UC-{nn}` number found among files matching the pattern in `dir`. 0 if none/unreadable. */
135
- function maxUcNumberInDir(dir) {
136
- let max = 0;
137
- try {
138
- if (!existsSync(dir)) return 0;
139
- for (const name of readdirSync(dir)) {
140
- // Any `UC-{nn}-` prefix counts, canonical or not: a number already on disk is never handed out again.
141
- const id = useCaseId(name);
142
- if (!id) continue;
143
- const n = parseInt(id.slice('UC-'.length), 10);
144
- if (Number.isFinite(n) && n > max) max = n;
145
- }
146
- } catch { /* unreadable dir — treat as empty, never fatal */ }
147
- return max;
148
- }
149
-
150
- /** Highest UC number across every feature's `2-plan/usecases/`, under both `.morph/features/` and `.morph/archive/`, in THIS tree. */
151
- function maxUcNumberLocalTree(cwd) {
152
- let max = 0;
153
- for (const bucket of ['features', 'archive']) {
154
- const bucketDir = join(cwd, '.morph', bucket);
155
- try {
156
- if (!existsSync(bucketDir)) continue;
157
- for (const entry of readdirSync(bucketDir, { withFileTypes: true })) {
158
- if (!entry.isDirectory()) continue;
159
- max = Math.max(max, maxUcNumberInDir(join(bucketDir, entry.name, '2-plan', 'usecases')));
160
- }
161
- } catch { /* unreadable bucket — treat as empty */ }
162
- }
163
- return max;
164
- }
165
-
166
- /**
167
- * The highest UC number this project tree can see right now: the permanent
168
- * acervo, plus every feature (active OR archived-but-not-yet-promoted, see
169
- * `usecase-promoter.js`'s refusal path) in THIS tree. Exported for tests and
170
- * for callers that want to preview the seed without reserving anything.
171
- *
172
- * @param {string} cwd
173
- * @returns {number}
174
- */
175
- export function scanLocalMax(cwd) {
176
- const acervoMax = maxUcNumberInDir(join(cwd, '.morph', 'specs', 'usecases'));
177
- return Math.max(acervoMax, maxUcNumberLocalTree(cwd));
178
- }
179
-
180
- /**
181
- * Atomically reserve the next `count` UC numbers, shared across every worktree
182
- * of this repo when a git common dir is resolvable.
183
- *
184
- * The seed is recomputed on EVERY call as `max(registry.counter, scanLocalMax(cwd))`
185
- * — monotonic by construction (a max can only grow), so this doubles as both
186
- * "seed the counter on the first-ever reservation" and "never regress even if
187
- * the registry were somehow behind disk" without needing a separate seeded flag.
188
- *
189
- * @param {string} cwd
190
- * @param {{ feature?: string|null, count?: number, gitCommonDir?: string|null }} opts
191
- * @returns {{ numbers: string[], base: number, count: number, shared: boolean, registryPath: string }}
192
- */
193
- export function reserveUseCases(cwd, { feature = null, count = 1, gitCommonDir = null } = {}) {
194
- const n = Number.isInteger(count) && count > 0 ? count : 1;
195
- const dir = registryDir({ gitCommonDir, cwd });
196
- const path = ucRegistryPath({ gitCommonDir, cwd });
197
- mkdirSync(dir, { recursive: true });
198
-
199
- return withLock(join(dir, 'uc-registry.lock'), () => {
200
- const registry = readRegistry(path);
201
- const base = Math.max(registry.counter || 0, scanLocalMax(cwd));
202
-
203
- // Test-only: widen the critical section so a concurrency test can prove
204
- // two REAL processes racing for this lock actually serialize, instead of
205
- // just happening not to overlap. Mirrors `finish.js`'s MORPH_GH_BIN test
206
- // injection. Unset in every real invocation.
207
- const testDelay = Number.parseInt(process.env.MORPH_UC_RESERVE_TEST_DELAY_MS, 10);
208
- if (Number.isFinite(testDelay) && testDelay > 0) sleepSync(testDelay);
209
-
210
- const numbers = [];
211
- const now = new Date().toISOString();
212
- for (let i = 1; i <= n; i += 1) {
213
- const id = formatUcId(base + i);
214
- numbers.push(id);
215
- registry.reservations[id] = { feature: feature || null, reservedAt: now };
216
- }
217
- registry.counter = base + n;
218
- writeRegistry(path, registry);
219
-
220
- return { numbers, base, count: n, shared: !!gitCommonDir, registryPath: path };
221
- });
222
- }
223
-
224
- /**
225
- * Which feature (if any) holds the reservation for a given `UC-{nn}` id.
226
- * Read-only — no lock, a slightly stale read is harmless for this use (the
227
- * promoter's collision backstop, never the allocation itself).
228
- *
229
- * @param {string} id - e.g. "UC-07"
230
- * @param {{ cwd: string, gitCommonDir?: string|null }} opts
231
- * @returns {string|null}
232
- */
233
- export function reservationOwner(id, { cwd, gitCommonDir = null }) {
234
- const path = ucRegistryPath({ gitCommonDir, cwd });
235
- const registry = readRegistry(path);
236
- return registry.reservations?.[id]?.feature ?? null;
237
- }
@@ -1,319 +0,0 @@
1
- /**
2
- * dispatch-planner.js — effort-weighted sequential vs parallel decision for a
3
- * feature's tasks.json DAG.
4
- *
5
- * Encodes the dispatch rules from morph-apply §2 ("Análise de DAG") so the
6
- * decision is deterministic and CLI-driven (`morph-spec dag <feature>`)
7
- * instead of re-derived by the LLM each session:
8
- *
9
- * 1. <= 3 tasks total → sequential (dispatch overhead beats the work).
10
- * 2. >= 2 independent group flows with enough weight → one agent per flow.
11
- * 3. >= 3 independent roots with effort M or heavier → parallel root batch.
12
- * 4. otherwise → sequential with auto-advance.
13
- *
14
- * Effort weighting: S=1, M=2, L=4, XL=8. S-only roots and light flows never
15
- * earn their own agent — they are returned as `inline` (the orchestrator runs
16
- * them in the sequential lane, or folds them into a single grouped agent), so
17
- * dispatch overhead never exceeds the work being dispatched.
18
- *
19
- * Pure module — operates on a parsed tasks.json document, no I/O.
20
- */
21
-
22
- export const EFFORT_WEIGHT = { S: 1, M: 2, L: 4, XL: 8 };
23
-
24
- /** A root must weigh at least this (effort M) to earn its own agent. */
25
- export const ROOT_DISPATCH_MIN_WEIGHT = 2;
26
-
27
- /** A flow must weigh at least this (an L, or 2×M…) to earn its own agent. */
28
- export const FLOW_DISPATCH_MIN_WEIGHT = 4;
29
-
30
- /** Mirror of the morph-apply cap on simultaneous agents. */
31
- export const MAX_PARALLEL_AGENTS = 4;
32
-
33
- function weightOf(task) {
34
- return EFFORT_WEIGHT[task.effort] ?? EFFORT_WEIGHT.M;
35
- }
36
-
37
- function groupOf(task) {
38
- // Same fallback as groupTasks() in tasks-json-parser.js
39
- return task.group || 'backend';
40
- }
41
-
42
- /**
43
- * Normalize a planned output path for collision comparison: forward slashes,
44
- * collapsed repeated slashes, no leading `./` or `/`, no trailing slash,
45
- * lowercased (Windows/macOS filesystems are case-insensitive). Returns '' for
46
- * empty/nullish input. `..`/`.` segments are left as-is on purpose (resolving
47
- * them would risk false-positives; two genuinely different relative spellings
48
- * stay distinct).
49
- */
50
- export function normalizeOutputPath(p) {
51
- return String(p ?? '')
52
- .trim()
53
- .replace(/\\/g, '/')
54
- .replace(/\/{2,}/g, '/')
55
- .replace(/^\.\//, '')
56
- .replace(/^\/+/, '')
57
- .replace(/\/+$/, '')
58
- .toLowerCase();
59
- }
60
-
61
- /**
62
- * Two normalized paths "overlap" if they are equal, or one is a directory
63
- * ancestor of the other — boundary-aware so `src/a` contains `src/a/b` but
64
- * NOT `src/ab`. Empty paths never overlap.
65
- */
66
- export function pathsOverlap(a, b) {
67
- if (!a || !b) return false;
68
- if (a === b) return true;
69
- const [shorter, longer] = a.length <= b.length ? [a, b] : [b, a];
70
- return longer.startsWith(`${shorter}/`);
71
- }
72
-
73
- /**
74
- * Decide whether a parallel batch's units would write to overlapping paths,
75
- * and therefore whether each agent should run in its own git worktree
76
- * (`isolation: "worktree"`, per morph-apply §3b-w) instead of the shared
77
- * working tree.
78
- *
79
- * A *unit* is the set of task ids a single agent owns (one flow, or one batch
80
- * root). Tasks WITHIN a unit run serially under that agent, so their outputs
81
- * never race — only CROSS-unit overlaps matter. The recommendation is global
82
- * per batch: any collision → isolate the whole batch. Units whose tasks
83
- * declare no `outputs` are an *unknown footprint* — they do NOT force
84
- * isolation (fail-open) but are flagged so the caller knows the dag can't
85
- * prove the batch is collision-free.
86
- *
87
- * @param {Array<{ key: string, taskIds: string[] }>} units
88
- * @param {Map<string, object>} tasksById
89
- * @returns {{ recommended: 'none'|'worktree', collisions: Array<{path: string, units: string[], tasks: string[]}>, unknownFootprint: string[], reason: string }}
90
- */
91
- export function buildIsolation(units, tasksById) {
92
- const unknownFootprint = [];
93
- const unitOutputs = units.map(unit => {
94
- const outs = [];
95
- for (const id of unit.taskIds) {
96
- const task = tasksById.get(id);
97
- const declared = Array.isArray(task?.outputs) ? task.outputs : [];
98
- let validForTask = 0;
99
- for (const raw of declared) {
100
- const path = normalizeOutputPath(raw);
101
- if (path) { outs.push({ path, task: id }); validForTask += 1; }
102
- }
103
- // No usable output paths (none declared, or all normalize to '') → the
104
- // dag can't prove this task's footprint. Flag it (fail-open).
105
- if (validForTask === 0) unknownFootprint.push(id);
106
- }
107
- return { key: unit.key, outs };
108
- });
109
-
110
- const collisions = [];
111
- for (let i = 0; i < unitOutputs.length; i++) {
112
- for (let j = i + 1; j < unitOutputs.length; j++) {
113
- for (const a of unitOutputs[i].outs) {
114
- for (const b of unitOutputs[j].outs) {
115
- if (pathsOverlap(a.path, b.path)) {
116
- collisions.push({
117
- path: a.path === b.path ? a.path : `${a.path} ∩ ${b.path}`,
118
- units: [unitOutputs[i].key, unitOutputs[j].key],
119
- tasks: [a.task, b.task],
120
- });
121
- }
122
- }
123
- }
124
- }
125
- }
126
-
127
- const recommended = collisions.length > 0 ? 'worktree' : 'none';
128
- let reason;
129
- if (collisions.length > 0) {
130
- reason =
131
- `${collisions.length} colisão(ões) de outputs entre unidades paralelas — ` +
132
- `isole cada agente num worktree (morph-apply §3b-w)`;
133
- } else if (unknownFootprint.length > 0) {
134
- reason =
135
- `sem colisão detectada, mas ${unknownFootprint.length} task(s) paralela(s) sem ` +
136
- `outputs declarados (footprint desconhecido) — o dag não garante ausência de colisão`;
137
- } else {
138
- reason = 'nenhuma colisão de outputs entre unidades paralelas — batch na working tree compartilhada';
139
- }
140
-
141
- return { recommended, collisions, unknownFootprint, reason };
142
- }
143
-
144
- /**
145
- * Ids of tasks whose dependency chain crosses group boundaries — integration
146
- * tasks and everything downstream of them. These never belong to a flow: they
147
- * run in the sequential lane after the parallel batch converges.
148
- */
149
- function crossGroupTaskIds(tasks) {
150
- const byId = new Map(tasks.map(t => [t.id, t]));
151
- const cross = new Set();
152
-
153
- for (const task of tasks) {
154
- for (const dep of task.dependencies) {
155
- const depTask = byId.get(dep);
156
- if (depTask && groupOf(depTask) !== groupOf(task)) {
157
- cross.add(task.id);
158
- break;
159
- }
160
- }
161
- }
162
-
163
- // Propagate downstream: a task depending (transitively) on a cross-group
164
- // task cannot run inside a single-group flow either.
165
- let changed = true;
166
- while (changed) {
167
- changed = false;
168
- for (const task of tasks) {
169
- if (cross.has(task.id)) continue;
170
- if (task.dependencies.some(dep => cross.has(dep))) {
171
- cross.add(task.id);
172
- changed = true;
173
- }
174
- }
175
- }
176
-
177
- return cross;
178
- }
179
-
180
- function buildStats(tasks, pending) {
181
- const groups = {};
182
- const weights = {};
183
- for (const task of pending) {
184
- const g = groupOf(task);
185
- groups[g] = (groups[g] || 0) + 1;
186
- weights[g] = (weights[g] || 0) + weightOf(task);
187
- }
188
- return { total: tasks.length, pending: pending.length, groups, weights };
189
- }
190
-
191
- /**
192
- * Decide how to execute the remaining tasks of a parsed tasks.json document.
193
- *
194
- * @param {object} data - parsed tasks.json ({ version, feature, tasks[] })
195
- * @returns {{
196
- * mode: 'sequential' | 'parallel-flows' | 'parallel-roots',
197
- * reason: string,
198
- * stats: { total: number, pending: number, groups: object, weights: object },
199
- * flows: Array<{ group: string, taskIds: string[], weight: number }>,
200
- * batch: string[],
201
- * inline: string[],
202
- * sequentialRemainder: string[],
203
- * isolation: { recommended: 'none'|'worktree', collisions: Array<{path: string, units: string[], tasks: string[]}>, unknownFootprint: string[], reason: string }
204
- * }}
205
- */
206
- export function planDispatch(data) {
207
- const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
208
- const pending = tasks.filter(t => t.status === 'pending');
209
- const stats = buildStats(tasks, pending);
210
- const empty = { flows: [], batch: [], inline: [] };
211
- const tasksById = new Map(tasks.map(t => [t.id, t]));
212
- // A batch with no parallel units can't have concurrent-write collisions.
213
- const noIsolation = buildIsolation([], tasksById);
214
-
215
- if (pending.length === 0) {
216
- return {
217
- mode: 'sequential',
218
- reason: 'nenhuma task pendente — nada a despachar',
219
- stats, ...empty, sequentialRemainder: [], isolation: noIsolation,
220
- };
221
- }
222
-
223
- // Rule 1 — tiny plans never pay for dispatch.
224
- if (tasks.length <= 3) {
225
- return {
226
- mode: 'sequential',
227
- reason: `${tasks.length} task(s) no total (<= 3) — overhead de Agent dispatch não compensa`,
228
- stats, ...empty, sequentialRemainder: pending.map(t => t.id), isolation: noIsolation,
229
- };
230
- }
231
-
232
- // Rule 2 — independent domain flows (backend ‖ frontend …).
233
- const cross = crossGroupTaskIds(tasks);
234
- const flowsMap = new Map();
235
- for (const task of pending) {
236
- if (cross.has(task.id)) continue;
237
- const g = groupOf(task);
238
- if (!flowsMap.has(g)) flowsMap.set(g, { group: g, taskIds: [], weight: 0 });
239
- const flow = flowsMap.get(g);
240
- flow.taskIds.push(task.id);
241
- flow.weight += weightOf(task);
242
- }
243
- const allFlows = [...flowsMap.values()];
244
- const worthyFlows = allFlows
245
- .filter(f => f.weight >= FLOW_DISPATCH_MIN_WEIGHT)
246
- .sort((a, b) => b.weight - a.weight);
247
- const lightFlows = allFlows.filter(f => f.weight < FLOW_DISPATCH_MIN_WEIGHT);
248
-
249
- if (worthyFlows.length >= 2) {
250
- const flows = worthyFlows.slice(0, MAX_PARALLEL_AGENTS);
251
- const overflow = worthyFlows.slice(MAX_PARALLEL_AGENTS);
252
- const inline = [
253
- ...lightFlows.flatMap(f => f.taskIds),
254
- ...overflow.flatMap(f => f.taskIds),
255
- ];
256
- const sequentialRemainder = pending
257
- .filter(t => cross.has(t.id))
258
- .map(t => t.id);
259
-
260
- const flowDesc = flows.map(f => `${f.group}:${f.taskIds.length} (peso ${f.weight})`).join(' ‖ ');
261
- return {
262
- mode: 'parallel-flows',
263
- reason:
264
- `${flows.length} fluxos de domínio independentes com peso suficiente — ${flowDesc} → ` +
265
- `1 agente por fluxo` +
266
- (inline.length ? `; ${inline.length} task(s) leve(s) inline (sem dispatch próprio)` : '') +
267
- (sequentialRemainder.length ? `; integração cross-group sequencial no fim (${sequentialRemainder.join(', ')})` : ''),
268
- stats, flows, batch: [], inline, sequentialRemainder,
269
- // Units = the dispatched flows (one agent each). Inline/overflow/cross-
270
- // group remainder run in the sequential lane, never concurrently.
271
- isolation: buildIsolation(
272
- flows.map(f => ({ key: f.group, taskIds: f.taskIds })),
273
- tasksById,
274
- ),
275
- };
276
- }
277
-
278
- // Rule 3 — root fan-out, weighted by effort (S roots never earn an agent).
279
- const roots = pending.filter(t => t.dependencies.length === 0);
280
- const weightedRoots = roots
281
- .filter(t => weightOf(t) >= ROOT_DISPATCH_MIN_WEIGHT)
282
- .sort((a, b) => weightOf(b) - weightOf(a));
283
- const sRoots = roots.filter(t => weightOf(t) < ROOT_DISPATCH_MIN_WEIGHT);
284
-
285
- if (weightedRoots.length >= 3) {
286
- const batch = weightedRoots.slice(0, MAX_PARALLEL_AGENTS).map(t => t.id);
287
- const inline = sRoots.map(t => t.id);
288
- const sequentialRemainder = pending
289
- .filter(t => !batch.includes(t.id) && !inline.includes(t.id))
290
- .map(t => t.id);
291
-
292
- return {
293
- mode: 'parallel-roots',
294
- reason:
295
- `${weightedRoots.length} raízes independentes com effort M+ → batch paralelo (${batch.join(', ')})` +
296
- (inline.length ? `; raízes S inline — não ganham dispatch próprio (${inline.join(', ')})` : '') +
297
- `; restante sequencial`,
298
- stats, flows: [], batch, inline, sequentialRemainder,
299
- // Units = each batch root (one agent each); inline S-roots and the
300
- // sequential remainder don't run concurrently with the batch.
301
- isolation: buildIsolation(
302
- batch.map(id => ({ key: id, taskIds: [id] })),
303
- tasksById,
304
- ),
305
- };
306
- }
307
-
308
- // Rule 4 — sequential.
309
- const reason = roots.length >= 3
310
- ? `${roots.length} raízes independentes, mas apenas ${weightedRoots.length} com effort M+ ` +
311
- `(raízes S não ganham dispatch próprio — overhead > trabalho) → sequencial com auto-advance`
312
- : 'sem fluxos ou raízes independentes suficientes → sequencial com auto-advance';
313
-
314
- return {
315
- mode: 'sequential',
316
- reason,
317
- stats, ...empty, sequentialRemainder: pending.map(t => t.id), isolation: noIsolation,
318
- };
319
- }