@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
@@ -24,113 +24,48 @@ import { runArchive } from './archive.js';
24
24
  import { dropLeftoverIgnore, withoutLeftoverIgnore } from '../../../framework/hooks/shared/graph-freshness.js';
25
25
  import { newBackupStamp, enforceBackupRetention, inspectFinishBackups } from '../../lib/worktree/finish-backup.js';
26
26
 
27
- /** workType → conventional-commit type. `hotfix` IS a fix; anything else is a chore. */
28
27
  const PR_TYPE_BY_WORK_TYPE = { feature: 'feat', bug: 'fix', hotfix: 'fix', chore: 'chore' };
29
28
 
30
29
  /** Git's soft limit for a subject line. */
31
30
  const PR_TITLE_MAX = 72;
32
31
 
33
- /**
34
- * The PR title for `finish --pr`.
35
- *
36
- * Was always `morph: {slug}` — so every PR in a repo whose hand-made ones follow
37
- * conventional commits (`fix(ui): coluna CRM nos dossiês…`) arrived shapeless
38
- * and said nothing about what changed (D4).
39
- *
40
- * Now: `{type}({slug}): {subject}` — the type from the feature's `workType`, the
41
- * slug kept as the scope (it is the traceability the old title at least had),
42
- * and the subject from the feature's own identity: `name` first (a human
43
- * deliberately titled it), then `description`, first line only. With neither,
44
- * degrade to `{type}: {slug}` rather than emit an empty subject.
45
- *
46
- * @param {string} feature
47
- * @param {object|null} fj - the feature.json
48
- * @returns {string}
49
- */
32
+ // `{type}({slug}): {subject}`, the subject from `name` (unless it is the default slug), then
33
+ // `description`, first line only; with neither, `{type}: {slug}`.
50
34
  export function buildPrTitle(feature, fj) {
51
35
  const type = PR_TYPE_BY_WORK_TYPE[fj?.workType] || 'chore';
52
36
 
53
37
  const named = typeof fj?.name === 'string' ? fj.name.trim() : '';
54
- // The default `name` IS the slug (create stamps it), which as a subject would
55
- // only repeat the scope — treat it as absent.
38
+ // The default `name` IS the slug, which as a subject would only repeat the scope.
56
39
  const described = typeof fj?.description === 'string' ? fj.description.trim() : '';
57
40
  const raw = (named && named !== feature ? named : described).split('\n')[0].trim();
58
41
  if (!raw) return `${type}: ${feature}`;
59
42
 
60
43
  const prefix = `${type}(${feature}): `;
61
44
  const room = PR_TITLE_MAX - prefix.length;
62
- // A scope so long there is no room left for a subject: drop the scope rather
63
- // than ship a title that is all prefix.
45
+ // No room left for a subject: drop the scope rather than ship a title that is all prefix.
64
46
  if (room < 12) return `${type}: ${raw}`.slice(0, PR_TITLE_MAX);
65
47
  return prefix + (raw.length <= room ? raw : `${raw.slice(0, room - 1)}…`);
66
48
  }
67
49
 
68
- const MEMORY_NOTE = 'o fechamento desta feature já está registrado no repositório (commit de archive, `decisions.md`, use cases promovidos, `git log`) — memória de sessão só para o que NENHUM deles guarda: decisão do usuário, pendência a observar em produção, custo medido.';
50
+ const MEMORY_NOTE = 'o fechamento desta feature já está registrado no repositório (commit de archive, `plan.md` com as decisões, índice de ADRs, `git log`) — memória de sessão só para o que NENHUM deles guarda: decisão do usuário, pendência a observar em produção, custo medido.';
69
51
 
70
- /**
71
- * `morph-spec finish <feature> --pr|--merge` — the single, symmetric close of a
72
- * feature's lifecycle, for BOTH the worktree flow and the common flow.
73
- *
74
- * Order (see the plan's backbone): validate → ARCHIVE inside the `morph/{f}`
75
- * branch (committed, so the archived state travels in one integration) →
76
- * integrate:
77
- * --merge : merge `--no-ff` into the default branch, LOCAL (no push).
78
- * --pr : push `morph/{f}` + `gh pr create` (does NOT merge locally).
79
- * → teardown the worktree (PROVE-or-DECLINE) when there is one.
80
- *
81
- * Every refusal is fail-CLOSED, exit 1, and — since the `--pr` remote
82
- * preconditions run BEFORE the archive — a refused `--pr` leaves no side effect.
83
- * `finished` is the integration (the actual value); `worktreeRemoved` is the
84
- * best-effort teardown — two independent signals, as before.
85
- *
86
- * RESUMABLE. `merge-conflict`, `checkout-default-failed`, `push-failed` and
87
- * `pr-create-failed` all fail AFTER the archive, and legitimately so: the
88
- * archive commit is what travels with the branch. Re-running then found no
89
- * `features/{f}` and refused from scratch, so every mid-way failure demanded a
90
- * hand repair. Now a second run that finds the feature ARCHIVED on its own
91
- * branch, not yet merged into the default, picks up at the integration and says
92
- * `resumed: true` in its output — see Validation 0c.
93
- *
94
- * @param {string} feature
95
- * `--dry-run` desvia para {@link finishPreflight} ANTES de qualquer validação
96
- * que saia com exit 1 e ANTES de qualquer mutação: o pré-voo existe para
97
- * EXPLICAR, e um pré-voo que recusa pelos mesmos motivos do comando real não
98
- * explica nada.
99
- *
100
- * @param {{ pr?: boolean, merge?: boolean, keep?: boolean, keepVolumes?: boolean,
101
- * push?: boolean, force?: boolean, discardIgnored?: boolean, dryRun?: boolean,
102
- * cwd?: string }} [options]
103
- */
52
+ // Every refusal is fail-CLOSED and happens BEFORE the archive, the first mutation. Resumable: a
53
+ // later run that finds the feature archived on its branch but not integrated picks up there.
54
+ // `--dry-run` diverts to finishPreflight before any exit-1 validation: a preflight explains.
104
55
  export async function finishCommand(feature, options = {}) {
105
56
  const cwd = options.cwd || process.cwd();
106
57
  const root = resolvePrimaryRoot(cwd);
107
58
  const rootNorm = resolve(root).replace(/\\/g, '/');
108
59
 
109
- // A closed feature must not keep warning other sessions that someone is
110
- // working on it (B1). Released on EVERY success path — hooking it into emit
111
- // is the single point that covers merge/PR/--keep alike. Fail-open: a claim
112
- // that outlives its feature only expires on its own TTL.
113
- //
114
- // The e2e stack is NOT reclaimed here: by the time a success is emitted the
115
- // worktree that owns the compose file may already be gone. Each integration
116
- // path calls `reclaimStack` immediately BEFORE its worktree teardown (or
117
- // before its final emit in the common flow) — see stack-teardown.js.
118
- //
119
- // Set by Validation 0c when this run picks up an already-archived feature.
120
- // Stamped onto EVERY payload emitted from that point on, success or refusal:
121
- // a resume merges a branch the user did not archive in this run, and that has
122
- // to be visible. Someone who mistypes a name and lands on an archived feature
123
- // would otherwise get a silent merge they never asked for.
60
+ // The claim is released on every success through emit (fail-open: a stale claim expires on its
61
+ // TTL). The e2e stack is reclaimed by each path BEFORE its worktree teardown, never here.
62
+ // `resumed` goes on every payload, so a merge of a feature archived by an earlier run is visible.
124
63
  let resumed = false;
125
64
 
126
- // ONE stamp for every backup this run writes: the shed's and the teardown's
127
- // folders then count as a single backup under the retention.
65
+ // ONE stamp for every backup this run writes, so they count as a single backup in the retention.
128
66
  const backupStamp = newBackupStamp();
129
67
 
130
- // The backup retention rides the same single point, AFTER this run's own
131
- // backups exist (the shed and the teardown both precede every success emit).
132
- // This run's stamp is protected: the prune never takes the backup it just
133
- // made room for, even when backups stamped by a clock running ahead outrank it.
68
+ // Retention runs after this run's own backups exist, and never prunes this run's stamp.
134
69
  const emit = (obj) => {
135
70
  if (resumed) obj = { ...obj, resumed: true };
136
71
  if (obj?.finished === true) {
@@ -146,15 +81,12 @@ export async function finishCommand(feature, options = {}) {
146
81
  console.log(JSON.stringify(obj));
147
82
  };
148
83
 
149
- // Gate 0: valid kebab name — anti command-injection (the name flows into every
150
- // git/gh call below) and the same gate the worktree path always applied.
84
+ // Anti command-injection: the name flows into every git/gh call below.
151
85
  if (!isValidFeatureName(feature)) {
152
86
  emit({ finished: false, error: 'invalid-feature-name', feature });
153
87
  process.exit(1);
154
88
  }
155
89
 
156
- // Mode: exactly one of --pr / --merge (they are alternative integrations — the
157
- // finish merges OR opens a PR, never both, never neither).
158
90
  const wantPr = !!options.pr;
159
91
  const wantMerge = !!options.merge;
160
92
  if (wantPr === wantMerge) {
@@ -163,15 +95,8 @@ export async function finishCommand(feature, options = {}) {
163
95
  }
164
96
  const mode = wantPr ? 'pr' : 'merge';
165
97
 
166
- // `--push` publica a DEFAULT depois de um merge local; o fluxo `--pr` não tem
167
- // merge local nenhum (a default só recebe a feature quando o PR mergear), e
168
- // já publica `morph/{f}` por conta própria. A combinação é um pedido que o
169
- // comando não consegue honrar — e aceitar em silêncio o que não se pode
170
- // honrar é exatamente o defeito que esta série de fixes existe para tirar do
171
- // CLI (o `score` que gravava um zero fabricado, o `approve` que saía 0 sem
172
- // aprovar nada). Recusa ALTA, e AQUI: junto do gate de modo, antes do
173
- // archive — que é a primeira mutação do finish. Uma recusa depois dele
174
- // deixaria a feature meio fechada, pior que o no-op que ela substitui.
98
+ // `--push` publica a default depois de um merge local, e o `--pr` não faz merge local: recusa
99
+ // AQUI, antes do archive (a primeira mutação), em vez de aceitar em silêncio o que não honra.
175
100
  if (wantPr && options.push) {
176
101
  emit({
177
102
  finished: false,
@@ -184,23 +109,16 @@ export async function finishCommand(feature, options = {}) {
184
109
 
185
110
  const expectedBranch = buildBranchName(feature);
186
111
 
187
- // ── Resolve the flow ──────────────────────────────────────────────────────
188
- // findWorktreeForFeature matches ANY worktree on morph/{feature} — including
189
- // the PRIMARY ROOT itself when the common flow checked the branch out there.
190
- // A match whose path IS the root is the common flow (no separate worktree to
191
- // tear down); a match elsewhere is the worktree flow.
112
+ // findWorktreeForFeature also matches the PRIMARY ROOT when the common flow checked the branch
113
+ // out there: a match at the root is the common flow.
192
114
  const found = findWorktreeForFeature(feature, root);
193
115
  const isWorktreeFlow = !!found && found.path.replace(/\\/g, '/') !== rootNorm;
194
116
 
195
117
  let worktreePath = null;
196
- let featureCwd = root; // where the feature's own tree (and Gate 3) lives
197
-
198
- // ── PRÉ-VOO ───────────────────────────────────────────────────────────────
199
- // AQUI, e não mais abaixo: as validações seguintes saem com exit 1 (branch
200
- // divergente, cwd dentro do worktree, feature ausente), e o pré-voo existe
201
- // justamente para ser rodado ANTES de o estado estar bom. Ele reporta o que
202
- // encontrar — inclusive o cwd dentro do worktree, como AVISO — e sai 0 sem
203
- // ter tocado em nada.
118
+ let featureCwd = root; // where the feature's own tree (and its review) lives
119
+
120
+ // Pré-voo AQUI, antes das validações que saem com exit 1: ele existe para rodar antes de o estado
121
+ // estar bom, reporta o que encontrar e sai 0 sem tocar em nada.
204
122
  if (options.dryRun) {
205
123
  emit(finishPreflight({
206
124
  feature,
@@ -209,17 +127,14 @@ export async function finishCommand(feature, options = {}) {
209
127
  cwd,
210
128
  mode,
211
129
  branch: expectedBranch,
212
- // `options` INTEIRO, e não um subconjunto escolhido a dedo: `--discard-ignored`
213
- // INVERTE a classificação (o exclusivo deixa de ser copiado e passa a ser
214
- // destruído), então um pré-voo que não recebe as flags do comando prevê
215
- // outro comando. Ver B1 na errata E4.
130
+ // `options` INTEIRO: `--discard-ignored` inverte a classificação, e um pré-voo sem as flags do
131
+ // comando prevê outro comando.
216
132
  options,
217
133
  }));
218
134
  return;
219
135
  }
220
136
 
221
137
  if (isWorktreeFlow) {
222
- // Validation 0: the worktree must actually be ON the branch we will merge.
223
138
  if (found.branch !== found.expectedBranch) {
224
139
  emit({
225
140
  finished: false,
@@ -235,14 +150,8 @@ export async function finishCommand(feature, options = {}) {
235
150
  worktreePath = found.path;
236
151
  featureCwd = worktreePath;
237
152
 
238
- // Validation 0b: the session's cwd must not be INSIDE the worktree we are
239
- // about to tear down (A4). Windows holds a lock on the process's working
240
- // directory, so `git worktree remove` fails — and by then the archive is
241
- // committed, the branch merged, and the infra junctions already dropped.
242
- // The result is a worse state than never having tried: an orphan directory,
243
- // a de-registered worktree, and a `relinkHint` telling the user to repair
244
- // by hand. Refuse HERE, before the first mutation — the cost is one
245
- // `cd`, and it is the exact failure observed in the ProspectPRO session.
153
+ // Windows locks the process cwd, so `git worktree remove` would fail AFTER the archive and the
154
+ // merge, leaving an orphan directory. Refuse here, before the first mutation.
246
155
  if (cwdIsInsideWorktree(cwd, worktreePath)) {
247
156
  emit({
248
157
  finished: false,
@@ -255,15 +164,9 @@ export async function finishCommand(feature, options = {}) {
255
164
  process.exit(1);
256
165
  }
257
166
  } else {
258
- // COMMON flow: the feature branch must exist and be the ROOT's current
259
- // branch (so the archive commits on it and we can checkout the default to
260
- // merge). A feature with NO morph/{f} branch was born before this flow —
261
- // route it explicitly to archive-without-integrate, never a generic refusal.
167
+ // A feature with no morph/{f} branch predates this flow: route it to archive-without-integrate.
262
168
  if (!branchExists(expectedBranch, root)) {
263
- // No branch AND no feature on disk is not a legacy feature — it is a
264
- // feature that is not here at all (wrong name, wrong tree, or already
265
- // archived). Routing that to `archive legacy` would tell the user to
266
- // archive something that does not exist.
169
+ // No branch AND no folder is not a legacy feature: it is not here at all.
267
170
  if (!existsSync(join(root, '.morph', 'features', feature))) {
268
171
  emit({ finished: false, error: 'feature-not-found', feature, ...featureElsewhereHint(root, root, feature) });
269
172
  process.exit(1);
@@ -291,19 +194,8 @@ export async function finishCommand(feature, options = {}) {
291
194
  featureCwd = root;
292
195
  }
293
196
 
294
- // Validation 0c: the feature must still be in `features/` — or, if it is not,
295
- // this run has to decide which of three states it is in before doing anything:
296
- // gone entirely, already ARCHIVED and awaiting integration (resume), or
297
- // already integrated (nothing to do).
298
- //
299
- // Originally all three arrived as `review-not-approved`, because a missing
300
- // feature.json parses to null and the optional chaining below fell straight
301
- // through — being told "Gate 3 not approved" about an approved gate is what
302
- // sent the user looking in the wrong place after an interrupted finish.
303
- //
304
- // Keyed on the DIRECTORY, not on the parse: readFeatureJson also returns null
305
- // for malformed JSON, and a legacy feature with a folder but no (or broken)
306
- // feature.json is a real thing that `--force` is expected to push through.
197
+ // Not in `features/`: gone, archived awaiting integration (resume), or already integrated. Keyed
198
+ // on the DIRECTORY, not the parse: a legacy folder with a broken feature.json is --force's case.
307
199
  if (!existsSync(join(featureCwd, '.morph', 'features', feature))) {
308
200
  const archivePath = join(featureCwd, '.morph', 'archive', feature);
309
201
  if (!existsSync(archivePath)) {
@@ -311,19 +203,8 @@ export async function finishCommand(feature, options = {}) {
311
203
  process.exit(1);
312
204
  }
313
205
 
314
- // ── RESUME ──────────────────────────────────────────────────────────────
315
- // Archived HERE, on the branch this finish would merge — every path that
316
- // reaches this line has already proved we are standing on `morph/{f}`
317
- // (worktree flow: found.branch === expectedBranch; common flow: branchExists
318
- // + currentBranch === expectedBranch). So the archive step is done and only
319
- // the integration is missing. Which is exactly what a failed merge/push
320
- // leaves behind, and what used to require repairing by hand.
321
- //
322
- // The one thing still to establish is whether the integration ALSO
323
- // happened. A branch already contained in the default is a finish that
324
- // COMPLETED (`--keep`, or a `--pr` whose PR was merged) — re-merging it is
325
- // the hazard this check exists to stop, and it is why resume is keyed on
326
- // the branch's relationship to the default rather than on the archive alone.
206
+ // Every path here already stands on `morph/{f}`, so only the integration is missing. A branch
207
+ // already contained in the default COMPLETED: re-merging it is the hazard this check stops.
327
208
  const target = detectDefaultBranch(root);
328
209
  if (!target) {
329
210
  emit({ finished: false, error: 'no-default-branch', hint: 'não foi possível resolver a branch default (origin/HEAD, main ou master)', feature });
@@ -331,9 +212,7 @@ export async function finishCommand(feature, options = {}) {
331
212
  }
332
213
  const merged = branchMergedInto(expectedBranch, target, root);
333
214
  if (merged === null) {
334
- // PROVE-or-DECLINE, same discipline as the worktree teardown: we could not
335
- // establish the branch's state, so we do not act on a guess. Refusing is
336
- // recoverable; a duplicate merge is not.
215
+ // PROVE-or-DECLINE: refusing is recoverable, a duplicate merge is not.
337
216
  emit({
338
217
  finished: false,
339
218
  error: 'resume-undecidable',
@@ -358,17 +237,8 @@ export async function finishCommand(feature, options = {}) {
358
237
  process.exit(1);
359
238
  }
360
239
 
361
- // "Already integrated" is MODE-SPECIFIC, and the ancestor test above only
362
- // answers it for `--merge`. A `--pr` that completed leaves its branch
363
- // deliberately UNMERGED (that is what the open PR is for), so the ancestor
364
- // test says "not merged" and would wave the resume through — straight into
365
- // a second `gh pr create` on a head that already has one. The common flow
366
- // makes that trivially reachable: integratePr never leaves `morph/{f}`, so
367
- // simply re-running the command lands here.
368
- //
369
- // `gh pr create` refuses a duplicate, so the damage was never two PRs — it
370
- // was `pr-create-failed` standing in for "this already finished", which is
371
- // the same collapsed diagnosis the `feature-not-found` split removed.
240
+ // "Already integrated" is per mode: a completed `--pr` leaves its branch unmerged on purpose, so
241
+ // the ancestor test alone would wave a second `gh pr create` through.
372
242
  if (mode === 'pr') {
373
243
  const openPr = openPrFor(expectedBranch, root);
374
244
  if (openPr === null) {
@@ -400,17 +270,8 @@ export async function finishCommand(feature, options = {}) {
400
270
  resumed = true;
401
271
  }
402
272
 
403
- // Validation 1: Gate 3 (review) approved on the feature's own feature.json —
404
- // which on a resume lives in `archive/{f}`, where runArchive moved it.
405
- //
406
- // NOT re-checked on a resume, deliberately. The archive IS the gate's receipt:
407
- // runArchive refuses `review-not-approved` itself, so a feature that reached
408
- // `archive/` either passed Gate 3 or was explicitly forced past it. Re-reading
409
- // it here would only add a way to fail — an archive whose feature.json is
410
- // absent or unparseable (legacy shapes, a forced close) parses to null and
411
- // falls straight through to `review-not-approved`, which is the exact
412
- // collapsed diagnosis the `feature-not-found` fix removed. Resume integrates
413
- // what was already archived; it does not re-litigate why.
273
+ // Not re-checked on a resume: the archive IS the gate's receipt (runArchive refuses without
274
+ // the review gate), and an archive with no readable feature.json would only fail here wrongly.
414
275
  const fj = readFeatureJson(featureCwd, feature, resumed ? 'archive' : 'features');
415
276
  if (!resumed && !options.force && !(fj?.approvalGates?.review?.approved === true || fj?.approvals?.review?.approved === true)) {
416
277
  emit({ finished: false, error: 'review-not-approved', feature });
@@ -421,8 +282,7 @@ export async function finishCommand(feature, options = {}) {
421
282
  ? String(fj.description).trim()
422
283
  : `Feature ${feature} — integrada via morph-spec finish --pr.`;
423
284
 
424
- // Validation 2: the tree that will carry the archive commit must be clean
425
- // (work committed), so the scoped archive commit contains only the move.
285
+ // Clean, so the scoped archive commit contains only the move.
426
286
  const cleanCwd = isWorktreeFlow ? worktreePath : root;
427
287
  const dirty = gitStatusPorcelain(cleanCwd);
428
288
  if (dirty === null) {
@@ -434,8 +294,7 @@ export async function finishCommand(feature, options = {}) {
434
294
  process.exit(1);
435
295
  }
436
296
 
437
- // Default branch — needed as the merge target and the PR base. Never assume
438
- // "master"; refuse clearly when it cannot be resolved.
297
+ // Never assume "master".
439
298
  const defaultBranch = detectDefaultBranch(root);
440
299
  if (!defaultBranch) {
441
300
  emit({ finished: false, error: 'no-default-branch', hint: 'não foi possível resolver a branch default (origin/HEAD, main ou master)', feature });
@@ -446,20 +305,8 @@ export async function finishCommand(feature, options = {}) {
446
305
  process.exit(1);
447
306
  }
448
307
 
449
- // ── MERGE precondition, BEFORE the archive ────────────────────────────────
450
- // `--merge` lands its merge commit in the PRIMARY ROOT, so the root has to be
451
- // clean. That check used to live inside integrateMerge — i.e. AFTER
452
- // runArchive had already moved the feature to .morph/archive/, promoted its
453
- // use cases and COMMITTED. A root dirtied by unrelated work therefore got a
454
- // refusal on a half-finished feature, and the retry then failed for a
455
- // different reason entirely (the feature was no longer in features/).
456
- //
457
- // `exemptOwnScope` because shedRootFeatureScope has not run yet: this
458
- // feature's own root-side scaffold is still on disk and is not a reason to
459
- // refuse — the shed backs it up and discards it moments later.
460
- //
461
- // `--pr` is exempt by design: integratePr pushes from the root but never
462
- // checks out or merges there, so root dirt cannot corrupt it.
308
+ // BEFORE the archive: a dirty root refused after it left a half-finished feature. The own scope
309
+ // is exempt (the shed discards it next), and `--pr` never checks out or merges in the root.
463
310
  if (mode === 'merge' && isWorktreeFlow) {
464
311
  const rootPre = primaryRootUnexplainedDirt(root, feature, { exemptOwnScope: true });
465
312
  if (!rootPre.ok) {
@@ -477,8 +324,7 @@ export async function finishCommand(feature, options = {}) {
477
324
  }
478
325
  }
479
326
 
480
- // ── PR preconditions run BEFORE the archive, so a refused --pr has NO side
481
- // effect (nothing archived, nothing committed). ─────────────────────────
327
+ // BEFORE the archive, so a refused --pr has no side effect.
482
328
  if (mode === 'pr') {
483
329
  if (!hasOriginRemote(root)) {
484
330
  emit({ finished: false, error: 'no-origin-remote', hint: 'não há remoto `origin` para abrir PR — configure um ou use --merge', feature });
@@ -490,9 +336,7 @@ export async function finishCommand(feature, options = {}) {
490
336
  }
491
337
  }
492
338
 
493
- // ── ARCHIVE (inside morph/{f}) ────────────────────────────────────────────
494
- // Every refusal is behind us: the leftover of a killed `graph refresh` goes now,
495
- // before the archive — never before a precondition that could still refuse.
339
+ // Every refusal is behind us: the leftover of a killed `graph refresh` goes now, never before.
496
340
  dropLeftoverIgnore(cleanCwd);
497
341
  const arch = runArchive(feature, { cwd: featureCwd, force: options.force, git: true });
498
342
  if (!arch.ok) {
@@ -506,66 +350,26 @@ export async function finishCommand(feature, options = {}) {
506
350
  process.exit(1);
507
351
  }
508
352
 
509
- // ── INTEGRATE ─────────────────────────────────────────────────────────────
510
353
  if (mode === 'merge') {
511
354
  return integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp });
512
355
  }
513
356
  return integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp });
514
357
  }
515
358
 
516
- /**
517
- * O aviso que acompanha CADA reparse point listado pelo pré-voo.
518
- *
519
- * Exportado porque é a frase que carrega o fato: uma junction/symlink dentro do
520
- * worktree aponta para conteúdo que vive na RAIZ, e `rm -rf` / `git worktree
521
- * remove --force` atravessam o link. Já aconteceu: um `worktree remove --force`
522
- * atravessou a junction e apagou o `node_modules` da raiz.
523
- */
359
+ // A junction/symlink in the worktree points at content in the ROOT, and `rm -rf` or
360
+ // `git worktree remove --force` traverse it.
524
361
  export const REPARSE_WARNING =
525
362
  'estes são junctions/symlinks: apagá-los fora do morph-spec apaga o conteúdo REAL na raiz';
526
363
 
527
- /**
528
- * `morph-spec finish <feature> --pr|--merge --dry-run` — o PRÉ-VOO.
529
- *
530
- * NÃO MUTA NADA. Nenhum commit, nenhum archive, nenhum worktree removido,
531
- * nenhum `gh` que crie coisa alguma, nenhum `git fetch`. Ele responde, ANTES,
532
- * as três perguntas que hoje só se respondem depois do estrago:
533
- *
534
- * 1. O que este `finish` vai COPIAR para o backup e o que vai DESCARTAR?
535
- * (`classifyIgnoredLeftovers` — a MESMA função que o teardown consulta;
536
- * um segundo enumerador aqui faria o pré-voo prometer o que o comando não
537
- * cumpre, e um dry-run que mente é pior que nenhum.)
538
- * 2. Que reparse points existem, e o que significa apagá-los por fora?
539
- * (`findReparsePoints`, exaustivo e fail-closed.)
540
- * 3. A base andou? — a pergunta INVERSA da de #89. Lá se mede "quantos
541
- * commits EU tenho que o upstream não tem"; aqui, "quantos commits
542
- * `origin/{default}` tem que a MINHA base não tem", que é o que decide se
543
- * este merge vai ser trivial ou uma surpresa. Mesma função
544
- * (`commitsAhead`), argumentos invertidos.
545
- *
546
- * `cwd` dentro do worktree é AVISO, não recusa: o caminho real continua
547
- * recusando antes da primeira mutação (Validation 0b), e recusar aqui negaria
548
- * ao usuário justamente a explicação que ele veio buscar — do lugar mais
549
- * provável de estar quando a pede.
550
- *
551
- * SEM `git fetch` implícito: um fetch dentro de um comando que promete não
552
- * mexer em nada é efeito colateral de rede. O pré-voo AVISA que o dado pode
553
- * estar velho e sugere o fetch.
554
- *
555
- * @param {{ feature: string, root: string, worktreePath: string|null,
556
- * cwd: string, mode: 'pr'|'merge', branch: string,
557
- * options?: { discardIgnored?: boolean, keep?: boolean } }} args
558
- * @returns {object} o payload emitido — nunca lança
559
- */
364
+ // NÃO MUTA NADA: nenhum commit, archive, remoção, `gh` que crie algo nem `git fetch` (efeito de
365
+ // rede). Usa as MESMAS funções do comando real, senão o pré-voo promete o que o comando não
366
+ // cumpre. `cwd` dentro do worktree é AVISO, não recusa. Nunca lança.
560
367
  export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch, options = {} }) {
561
368
  const warnings = [];
562
369
 
563
- // ── A base andou? (vale COM e SEM worktree — UC-16 A1) ────────────────────
564
370
  const defaultBranch = detectDefaultBranch(root);
565
371
  const baseRef = defaultBranch ? resolveBaseRef(defaultBranch, root) : null;
566
- // `commitsAhead(base, branch)` conta `base..branch`. Passando a branch da
567
- // feature como BASE e o baseRef como BRANCH, contamos o que a default tem e a
568
- // feature não — exatamente a pergunta inversa.
372
+ // `commitsAhead` conta `base..branch`: invertidos, contam o que a default tem e a feature não.
569
373
  const commits = baseRef && branchExists(branch, root)
570
374
  ? commitsAhead(branch, baseRef, root)
571
375
  : null;
@@ -577,9 +381,7 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
577
381
  mode,
578
382
  branch,
579
383
  baseAdvanced: { ref: baseRef, commits },
580
- // `null` é DESCONHECIDO e nunca é renderizado como 0 (UC-16 P6): "não deu
581
- // para perguntar" e "a default não andou" são fatos opostos, e confundi-los
582
- // é como um `--dry-run` passa confiança que não tem.
384
+ // `null` é DESCONHECIDO, nunca 0: "não deu para perguntar" e "não andou" são fatos opostos.
583
385
  baseAdvancedNote: commits === null
584
386
  ? (baseRef === null
585
387
  ? 'sem contraparte remota nem branch default resolvível — a contagem é DESCONHECIDA, não zero'
@@ -601,10 +403,8 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
601
403
 
602
404
  if (defaultBranch) out.defaultBranch = defaultBranch;
603
405
 
604
- // A retenção dos backups também é mutação do finish real — a AUTOMÁTICA, que
605
- // não toca backup sem manifesto (esses só o `doctor --worktrees --prune`
606
- // poda). A contagem é a de AGORA: o backup que este finish gravar ainda pode
607
- // empurrar mais um para fora.
406
+ // A retenção automática também é mutação do finish real; a contagem é a de AGORA, antes do
407
+ // backup que este finish gravar.
608
408
  try {
609
409
  const retention = inspectFinishBackups(root);
610
410
  if (retention.policy.skipped) {
@@ -625,7 +425,6 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
625
425
  }
626
426
  } catch { /* o pré-voo nunca lança */ }
627
427
 
628
- // ── Seções de worktree — omitidas quando não há worktree (UC-16 A1) ───────
629
428
  if (worktreePath) {
630
429
  out.worktreePath = worktreePath.replace(/\\/g, '/');
631
430
 
@@ -647,24 +446,17 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
647
446
  );
648
447
  }
649
448
 
650
- // AS MESMAS FLAGS do comando real. `--discard-ignored` INVERTE a
651
- // classificação — o exclusivo deixa de ser copiado e entra no lote
652
- // destruído sem backup — então prever com `options` vazio é prever OUTRO
653
- // comando. Medido antes do conserto: o pré-voo prometia salvar caminhos que
654
- // o `--discard-ignored` destruía. Normalizado com `=== true` para que
655
- // `undefined` e `false` sejam a mesma coisa, como no `worktree remove`.
449
+ // As MESMAS flags do comando real: `--discard-ignored` inverte a classificação, então prever com
450
+ // `options` vazio é prever outro comando.
656
451
  const discardIgnored = options.discardIgnored === true;
657
452
  const ignored = classifyIgnoredLeftovers(worktreePath, root, { discardIgnored });
658
453
  out.discardIgnored = discardIgnored;
659
454
  out.ignored = {
660
- // COPIADO para .git/morph-spec/finish-backup antes da remoção: existe só
661
- // aqui, ou difere do que está na raiz. Com `--discard-ignored` esta lista
662
- // sai VAZIA por construção — a flag significa "pule o backup".
455
+ // Copiado para o backup; vazio por construção sob `--discard-ignored`.
663
456
  shieldable: ignored.shieldable,
664
457
  // Idêntico byte a byte ao da raiz (SHA-256): apagar não perde nada.
665
458
  replicable: ignored.replicable,
666
- // Artefato regenerável + os replicáveis (+ o exclusivo, sob a flag):
667
- // descartado sem backup.
459
+ // Descartado sem backup.
668
460
  discardable: ignored.discarded,
669
461
  };
670
462
  if (ignored.blocking.length) {
@@ -673,12 +465,8 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
673
465
  }
674
466
 
675
467
  if (discardIgnored) {
676
- // SEGUNDA leitura, deliberada e só aqui: sob a flag, `shieldable` sai
677
- // vazia e a informação mais valiosa do pré-voo — QUAIS destes existem só
678
- // aqui — desapareceria justamente no modo destrutivo. Perguntar à MESMA
679
- // função, sem a flag, é a única forma de nomeá-los sem recriar a regra
680
- // (uma segunda cópia da triagem é o defeito que o export de
681
- // `classifyIgnoredLeftovers` existe para evitar).
468
+ // Sob a flag `shieldable` sai vazia: perguntar à MESMA função sem a flag nomeia o que existe só
469
+ // aqui, sem recriar a regra.
682
470
  const semFlag = classifyIgnoredLeftovers(worktreePath, root);
683
471
  out.ignored.discardableExclusive = semFlag.shieldable;
684
472
  if (semFlag.shieldable.length) {
@@ -705,23 +493,17 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
705
493
  return out;
706
494
  }
707
495
 
708
- // ── merge integration (local, no push) ───────────────────────────────────────
709
-
710
496
  async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp }) {
711
497
  const mergeMsg = `morph-spec finish: merge ${expectedBranch}`;
712
- const ucNote = promotedNote(arch);
498
+ const adrNote = adrIndexNote(arch);
713
499
 
714
500
  if (isWorktreeFlow) {
715
- // The root stays on the default; shed this feature's own root-side scaffold,
716
- // then guard the whole root before landing the merge commit.
501
+ // Shed this feature's root-side scaffold, then guard the whole root before the merge commit.
717
502
  const { shedBackup } = shedRootFeatureScope(root, feature, { stamp: backupStamp });
718
503
  const backupNote = shedBackup ? { shedBackup } : {};
719
504
 
720
- // Final gate before the merge commit lands. The pre-archive check above
721
- // already refused an unrelated-dirt root WITHOUT side effects, so reaching
722
- // a refusal HERE means the shed itself dirtied the root — a bug signal, not
723
- // user error. Kept because this is the last moment before we mutate the
724
- // default branch, and it is nearly free.
505
+ // The pre-archive check already refused unrelated dirt, so a refusal here means the shed
506
+ // dirtied the root: a bug signal. Last moment before mutating the default branch.
725
507
  const rootClean = primaryRootUnexplainedDirt(root, feature);
726
508
  if (!rootClean.ok) {
727
509
  emit({ finished: false, error: 'primary-root-status-failed', feature, ...backupNote });
@@ -748,30 +530,24 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
748
530
  dropFromRootIndex(root, feature);
749
531
  emit({
750
532
  ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, shedBackup, kept: true }),
751
- ...ucNote,
533
+ ...adrNote,
752
534
  });
753
535
  process.exit(0);
754
536
  }
755
537
 
756
- // The stack comes down BEFORE the worktree: the compose file, the bind
757
- // mount and the port block all belong to that tree, and once it is gone
758
- // there is nothing left to address the containers by.
538
+ // The stack comes down BEFORE the worktree, which holds its compose file, bind mount and ports.
759
539
  const e2e = reclaimStack(feature, { root, worktreePath, options });
760
- // The session that ran this finish may be ANCHORED in the worktree (its
761
- // hooks resolve `.morph/framework` through it): tearing it down now would
762
- // kill every hook of that session. The integration is done; only the
763
- // teardown waits (see sessionAnchoredInWorktree).
540
+ // A session ANCHORED in the worktree resolves its hooks through it: tearing it down now would
541
+ // kill every hook of that session, so only the teardown waits.
764
542
  const deferred = sessionAnchoredInWorktree(worktreePath);
765
543
  const teardown = deferred
766
544
  ? null
767
545
  : await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: true, stamp: backupStamp });
768
546
  dropFromRootIndex(root, feature);
769
- emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, teardown, shedBackup, deferred }), ...ucNote, e2e });
547
+ emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, teardown, shedBackup, deferred }), ...adrNote, e2e });
770
548
  process.exit(0);
771
549
  }
772
550
 
773
- // COMMON flow: we are on morph/{f} in the root, archive committed. Move to the
774
- // default and merge the feature branch in.
775
551
  try {
776
552
  execFileSync('git', ['checkout', defaultBranch], { cwd: root, stdio: 'pipe' });
777
553
  } catch (err) {
@@ -786,8 +562,6 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
786
562
  finished: false,
787
563
  error: 'merge-conflict',
788
564
  output: gitErr(err),
789
- // The abort left the working dir on the default WITHOUT the feature — say
790
- // where the intact work is so the user can recover it.
791
565
  hint: `conflito ao mergear ${expectedBranch} em ${defaultBranch}. Seu trabalho está intacto em ${expectedBranch} — rode \`git checkout ${expectedBranch}\` para voltar a ele`,
792
566
  feature,
793
567
  });
@@ -796,29 +570,16 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
796
570
  tryGit(['branch', '-d', expectedBranch], root);
797
571
  const publish = publicationState({ root, defaultBranch, push: options.push });
798
572
  const e2e = reclaimStack(feature, { root, worktreePath: null, options });
799
- // The root index (gitignored) was already updated by the archive (same cwd).
800
- emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish }), ...ucNote, e2e });
573
+ emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish }), ...adrNote, e2e });
801
574
  process.exit(0);
802
575
  }
803
576
 
804
- /**
805
- * O número do PR a partir da url que o `gh pr create` imprime.
806
- *
807
- * Existe porque o número é o que um humano usa para falar do PR ("mergeia o
808
- * #51"), e a url é a única coisa que o `gh` devolve. Um formato inesperado
809
- * devolve `null` em vez de um palpite: o payload prefere admitir que não sabe
810
- * o número a afirmar um errado.
811
- *
812
- * @param {string|null} url
813
- * @returns {number|null}
814
- */
577
+ // `null` for an unexpected format: better to admit not knowing the number than state a wrong one.
815
578
  export function prNumberFromUrl(url) {
816
579
  const m = typeof url === 'string' ? url.match(/\/pull\/(\d+)(?:[/?#]|$)/) : null;
817
580
  return m ? Number(m[1]) : null;
818
581
  }
819
582
 
820
- // ── PR integration (push + gh, no local merge) ───────────────────────────────
821
-
822
583
  async function integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp }) {
823
584
  try {
824
585
  execFileSync('git', ['push', '-u', 'origin', expectedBranch], { cwd: root, stdio: 'pipe' });
@@ -836,37 +597,27 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
836
597
  process.exit(1);
837
598
  }
838
599
 
839
- // Worktree flow: remove the worktree but KEEP the branch (the open PR needs it).
840
- // Common flow: nothing to tear down; stay on morph/{f} (the default has no
841
- // feature yet — honest until the PR merges).
600
+ // The worktree flow keeps the branch (the open PR needs it); the common flow stays on morph/{f},
601
+ // since the default has no feature until the PR merges.
842
602
  let teardown = null;
843
603
  let shedBackup = null;
844
- // Reclaimed from the owning tree, before that tree goes away; skipped on
845
- // --keep, where the worktree (and its stack) stays alive on purpose.
604
+ // Skipped on --keep, where the worktree and its stack stay alive on purpose.
846
605
  const e2e = isWorktreeFlow
847
606
  ? (options.keep ? null : reclaimStack(feature, { root, worktreePath, options }))
848
607
  : reclaimStack(feature, { root, worktreePath: null, options });
849
608
  let teardownDeferred = false;
850
609
  if (isWorktreeFlow && !options.keep) {
851
- // Shed the transient root-side scaffold BEFORE teardown — exactly as --merge
852
- // does. The real `create --worktree` leaves the root's .morph/features/{f}
853
- // UNTRACKED; without this, teardown's clearRootWorktreeRegistration would
854
- // `git add` + commit that scaffold onto the default branch. Then when the PR
855
- // later merges (carrying features/{f} → archive/{f}), the default would hold
856
- // the feature in BOTH features/ and archive/ at once — active and archived.
857
- // The branch/PR owns the authoritative copy; the root scaffold is transient.
610
+ // Shed BEFORE teardown: otherwise clearRootWorktreeRegistration commits the untracked root
611
+ // scaffold to the default, and the merged PR leaves the feature in both features/ and archive/.
858
612
  shedBackup = shedRootFeatureScope(root, feature, { stamp: backupStamp }).shedBackup;
859
- // Same deferral as --merge: never tear down the worktree the running
860
- // session is anchored in (see sessionAnchoredInWorktree).
613
+ // Same deferral as --merge (sessionAnchoredInWorktree).
861
614
  if (sessionAnchoredInWorktree(worktreePath)) {
862
615
  teardownDeferred = true;
863
616
  } else {
864
617
  teardown = await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: false, stamp: backupStamp });
865
618
  }
866
- // The shed removed the root's features/{f} scaffold from disk, but runArchive
867
- // dropped the feature only from the WORKTREE's index (its cwd). Drop it from
868
- // the ROOT index too — exactly as --merge does — or `doctor`'s lifecycle audit
869
- // flags a `stale-index` phantom (index lists {f}, no folder on disk).
619
+ // runArchive dropped the feature only from the WORKTREE index: drop it from the root's too, or
620
+ // `doctor` flags a `stale-index` phantom.
870
621
  dropFromRootIndex(root, feature);
871
622
  }
872
623
 
@@ -875,10 +626,7 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
875
626
 
876
627
  const out = {
877
628
  finished: true,
878
- // O comando terminou; a INTEGRAÇÃO não aconteceu. Eram a mesma coisa no
879
- // payload antigo, e é dessa colisão que sai o "shipped" para uma feature
880
- // cujo PR seguia aberto: quem lê `finished: true` sem mais nada conclui
881
- // que acabou. A default só recebe archive/{f} quando o PR mergear.
629
+ // O comando terminou, a INTEGRAÇÃO não: a default só recebe archive/{f} quando o PR mergear.
882
630
  integrated: false,
883
631
  awaiting: 'merge',
884
632
  mode: 'pr',
@@ -887,15 +635,14 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
887
635
  base: defaultBranch,
888
636
  pr: { number: prNumber, url: prUrl },
889
637
  nextStep: `mergeie ${prLabel} — a feature só estará encerrada quando ${defaultBranch} receber o archive pelo merge`,
890
- ...promotedNote(arch),
638
+ ...adrIndexNote(arch),
891
639
  ...(shedBackup ? { shedBackup } : {}),
892
640
  ...(e2e ? { e2e } : {}),
893
641
  ...(teardown
894
642
  ? { worktreeRemoved: teardown.worktreeRemoved, ...teardownExtras(teardown, worktreePath, expectedBranch, false) }
895
643
  : {
896
644
  worktreeRemoved: false,
897
- // isWorktreeFlow && options.keep is the only way `teardown` stays null
898
- // with a worktree still alive — the common flow has none to report.
645
+ // Only --keep leaves `teardown` null with a worktree still alive.
899
646
  ...(isWorktreeFlow && options.keep
900
647
  ? {
901
648
  worktree: { path: worktreePath, branch: expectedBranch },
@@ -914,59 +661,20 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
914
661
  process.exit(0);
915
662
  }
916
663
 
917
- // ── output helpers ────────────────────────────────────────────────────────
918
-
919
- /**
920
- * Report what the archive did to the acervo: use cases promoted into
921
- * `.morph/specs/usecases/`, any REFUSED for a numbering collision
922
- * (`ucCollisions` — see `usecase-promoter.js`'s reservation-backed guard), the
923
- * promoted copies that stayed superseded because the acervo said so
924
- * (`keptSuperseded`), the ones the feature's spec.md revoked
925
- * (`supersededUseCases`), the ADR index files it moved (`adrIndex`) and the
926
- * revocations it could not apply
927
- * (`revocationProblems`). Every key is absent when there is nothing to report,
928
- * so a feature that touches no acervo keeps the exact output shape it had
929
- * before any of them existed.
930
- */
931
- function promotedNote(arch) {
664
+ function adrIndexNote(arch) {
932
665
  return {
933
- ...(arch?.promotedUseCases?.length ? { promotedUseCases: arch.promotedUseCases } : {}),
934
- ...(arch?.refusedUseCases?.length ? { ucCollisions: arch.refusedUseCases } : {}),
935
- ...(arch?.keptSuperseded?.length ? { keptSuperseded: arch.keptSuperseded } : {}),
936
- ...(arch?.supersededUseCases?.length ? { supersededUseCases: arch.supersededUseCases } : {}),
937
666
  ...(arch?.adrIndex ? { adrIndex: arch.adrIndex } : {}),
938
667
  ...(arch?.revocationProblems?.length ? { revocationProblems: arch.revocationProblems } : {}),
939
668
  };
940
669
  }
941
670
 
942
- /**
943
- * O único remoto com que o `finish` conversa — o mesmo que `hasOriginRemote` e
944
- * o `resolveBaseRef` do branch-classifier assumem. Um nome só, aqui, para que o
945
- * comando que a saída IMPRIME e o push que o `--push` EXECUTA jamais possam
946
- * nomear remotos diferentes.
947
- */
671
+ // Um nome só, para que o comando que a saída IMPRIME e o push que o `--push` EXECUTA jamais
672
+ // nomeiem remotos diferentes.
948
673
  const PUSH_REMOTE = 'origin';
949
674
 
950
- /**
951
- * O que sobrou por publicar depois que o merge local aterrissou — e, com
952
- * `--push`, o resultado da publicação.
953
- *
954
- * A contagem REUSA `resolveBaseRef` + `commitsAhead` (branch-classifier), em vez
955
- * de uma terceira cópia de `rev-list --count`. Detalhe que decide o P4:
956
- * `resolveBaseRef` degrada para a branch LOCAL quando não existe ref de
957
- * rastreamento remoto — comparar a branch consigo mesma devolveria um `0` de
958
- * aparência honesta para um repo que nunca deu push. Por isso a comparação só
959
- * vale quando o ref resolvido é diferente do nome da branch; caso contrário
960
- * `ahead` fica `null` ("incomparável"), nunca zero.
961
- *
962
- * Fail-open por desenho: o merge JÁ aconteceu quando esta função roda, então
963
- * nenhum resultado daqui — nem um push que falhou — pode transformar um sucesso
964
- * em exit 1. A falha vira `pushError` no payload.
965
- *
966
- * @param {{ root: string, defaultBranch: string, push?: boolean }} ctx
967
- * @returns {{ remote: string, branch: string, ahead: number|null, pushed: boolean,
968
- * published?: number|null, pushError?: string }}
969
- */
675
+ // `resolveBaseRef` degrada para a branch LOCAL sem ref remoto: comparada consigo mesma daria um
676
+ // `0` falso, então `ahead` fica `null`. Fail-open: o merge JÁ aconteceu, e um push que falha
677
+ // vira `pushError`, nunca exit 1.
970
678
  function publicationState({ root, defaultBranch, push }) {
971
679
  const baseRef = resolveBaseRef(defaultBranch, root);
972
680
  const tracked = !!baseRef && baseRef !== defaultBranch;
@@ -990,19 +698,8 @@ function publicationState({ root, defaultBranch, push }) {
990
698
  return { ...state, published: ahead, ahead: remaining, pushed: true };
991
699
  }
992
700
 
993
- /**
994
- * A frase do próximo passo — PRODUTOR ÚNICO.
995
- *
996
- * O comando se chama `finish`, e no modo `--merge` ele NÃO publica nada por
997
- * conta própria: o merge é local, o remoto continua sem a feature. Quem lê
998
- * "finished: true" conclui o contrário — foi pegadinha repetida em sessões
999
- * diferentes. Três saídas de `--merge` montavam esse aviso por conta própria, e
1000
- * uma delas (o `--keep`) simplesmente esquecia dele; um produtor único é o que
1001
- * impede a divergência de voltar.
1002
- *
1003
- * @param {ReturnType<typeof publicationState>} publish
1004
- * @returns {string}
1005
- */
701
+ // PRODUTOR ÚNICO: no `--merge` o merge é local e o remoto continua sem a feature; cada saída
702
+ // que montava o aviso por conta própria podia esquecê-lo.
1006
703
  function mergeNextStep(publish) {
1007
704
  const { remote, branch, ahead, pushed, published, pushError } = publish;
1008
705
  const pushCmd = `\`git push ${remote} ${branch}\``;
@@ -1017,25 +714,15 @@ function mergeNextStep(publish) {
1017
714
  if (pushError) {
1018
715
  return `integração LOCAL — o push pedido por --push FALHOU e nada foi publicado. Rode ${pushCmd} manualmente.`;
1019
716
  }
1020
- // Sem contraparte remota não se inventa contagem (P4): fica a frase estática.
717
+ // Sem contraparte remota não se inventa contagem: fica a frase estática.
1021
718
  if (ahead === null) {
1022
719
  return 'integração LOCAL — nada foi publicado. Rode `git push` para levar ao remoto.';
1023
720
  }
1024
721
  return `integração LOCAL — nada foi publicado: ${branch} está ${ahead} commit(s) à frente de ${remote}/${branch}. Rode ${pushCmd} para levar ao remoto (ou re-execute o finish com --push).`;
1025
722
  }
1026
723
 
1027
- /**
1028
- * CONSTRUTOR ÚNICO da saída de sucesso do `--merge`. As três emissões (worktree
1029
- * normal, worktree `--keep` e fluxo comum) passam por aqui — era a terceira
1030
- * cópia, montada à mão no `--keep`, que saía sem `nextStep` nenhum.
1031
- *
1032
- * `teardown === null` cobre os dois casos sem desmonte: o `--keep` (worktree
1033
- * vivo de propósito) e o fluxo comum (não há worktree algum).
1034
- *
1035
- * @param {{ feature: string, branch: string, defaultBranch: string,
1036
- * publish: ReturnType<typeof publicationState>, worktreePath?: string|null,
1037
- * teardown?: object|null, shedBackup?: string|null, kept?: boolean }} args
1038
- */
724
+ // CONSTRUTOR ÚNICO das três saídas de sucesso do `--merge`. `teardown === null` cobre o `--keep`
725
+ // e o fluxo comum.
1039
726
  function mergeOutput({ feature, branch, defaultBranch, publish, worktreePath = null, teardown = null, shedBackup = null, kept = false, deferred = false }) {
1040
727
  return {
1041
728
  finished: true,
@@ -1052,9 +739,7 @@ function mergeOutput({ feature, branch, defaultBranch, publish, worktreePath = n
1052
739
  ...(shedBackup ? { shedBackup } : {}),
1053
740
  ...(kept
1054
741
  ? {
1055
- // Worktree preserved on purpose: the root's `worktree` registration stays
1056
- // VALID (it still names a live directory), so nothing to clean up here —
1057
- // only report where it lives and how to close it later.
742
+ // The root registration stays VALID (it names a live directory): nothing to clean up.
1058
743
  worktree: { path: worktreePath, branch },
1059
744
  hint: `worktree preservado em ${worktreePath} — feche depois com \`morph-spec worktree remove ${feature}\`.`,
1060
745
  }
@@ -1063,12 +748,7 @@ function mergeOutput({ feature, branch, defaultBranch, publish, worktreePath = n
1063
748
  };
1064
749
  }
1065
750
 
1066
- /**
1067
- * Hint for a teardown deferred because the session is anchored in the worktree
1068
- * (both modes). Names the safe command, never `git worktree remove`. `--merge`
1069
- * would have deleted the (now integrated) branch, so its hint carries
1070
- * `--delete-branch`; `--pr` keeps the branch — the PR needs it.
1071
- */
751
+ // Names the safe command, never `git worktree remove`. `--pr` keeps the branch: the PR needs it.
1072
752
  function deferredTeardownHint(feature, worktreePath, { deleteBranch = false } = {}) {
1073
753
  return `worktree mantido em ${worktreePath}: a sessão do Claude Code que rodou o finish está ancorada nele, `
1074
754
  + 'e desmontá-lo agora quebraria todos os hooks dela (eles resolvem `.morph/framework` por ele) — '
@@ -1076,19 +756,14 @@ function deferredTeardownHint(feature, worktreePath, { deleteBranch = false } =
1076
756
  + 'de uma sessão aberta na raiz do repo.';
1077
757
  }
1078
758
 
1079
- /** Shared teardown reporting for both modes (kept identical to the old finish). */
1080
759
  function teardownExtras(teardown, worktreePath, branch, deleteBranch) {
1081
760
  const out = {};
1082
761
  if (teardown.ignoredBackup) out.worktreeIgnoredBackup = teardown.ignoredBackup;
1083
762
  if (teardown.worktreeRemoved && teardown.discarded.length) out.discardedIgnored = teardown.discarded;
1084
- // WHAT was copied out, and what needed no copy because the primary root holds
1085
- // the identical bytes. Reported on both paths: after a removal these two lists
1086
- // are the whole answer to "did I just lose something?", and the user should
1087
- // not have to open the backup directory to find out.
763
+ // After a removal these two lists answer "did I just lose something?".
1088
764
  if (teardown.shieldable?.length) out.worktreeIgnoredBackedUp = teardown.shieldable;
1089
765
  if (teardown.replicable?.length) out.replicatedInRoot = teardown.replicable;
1090
- // The infra reparse points dropped BEFORE git touched the directory — the
1091
- // proof the primary root's real `.claude/` was unlinked, never followed.
766
+ // Proof the primary root's real `.claude/` was unlinked, never followed.
1092
767
  if (teardown.unlinked?.length) out.unlinked = teardown.unlinked;
1093
768
  if (!teardown.worktreeRemoved) {
1094
769
  out.path = worktreePath;
@@ -1096,16 +771,8 @@ function teardownExtras(teardown, worktreePath, branch, deleteBranch) {
1096
771
  if (teardown.blockers.length) out.reparsePoints = teardown.blockers;
1097
772
  if (teardown.ignoredBlocking.length) out.ignoredFiles = teardown.ignoredBlocking;
1098
773
  out.cleanupHint = teardown.declineReason;
1099
- // The unlink runs before the removal by design (it is what keeps the removal
1100
- // from following the junctions out into the primary root), so every
1101
- // non-removal passes through a moment with the infra loose. What happens
1102
- // next depends on WHY, and the two must not be reported alike:
1103
- // REFUSED — we proved removal unsafe, the user keeps working here, so the
1104
- // teardown re-links (A4).
1105
- // FAILED — removal was the intent and a lock got in the way. The infra
1106
- // stays down ON PURPOSE, so the retry (or anything pasted from an old
1107
- // transcript) has nothing to traverse. Say so explicitly, or the user
1108
- // reads a bare "failed" and reaches for the destructive command.
774
+ // REFUSED re-links (the user keeps working here); FAILED leaves the infra down ON PURPOSE so a
775
+ // retry has nothing to traverse. Say which, or a bare "failed" invites the destructive command.
1109
776
  out.relinked = teardown.relinked === true;
1110
777
  if (!teardown.refused) {
1111
778
  if (teardown.infraLeftUnlinked?.length) out.infraLeftUnlinked = teardown.infraLeftUnlinked;
@@ -1119,8 +786,6 @@ function teardownExtras(teardown, worktreePath, branch, deleteBranch) {
1119
786
  return out;
1120
787
  }
1121
788
 
1122
- // ── small git helpers ────────────────────────────────────────────────────────
1123
-
1124
789
  function currentBranch(cwd) {
1125
790
  try {
1126
791
  return execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
@@ -1155,27 +820,8 @@ function gitErr(err) {
1155
820
  return (((err.stdout && err.stdout.toString()) || '') + ((err.stderr && err.stderr.toString()) || '')).slice(0, 2000) || (err.message || '').slice(0, 500);
1156
821
  }
1157
822
 
1158
- /**
1159
- * Where a feature that is NOT in `features/` actually went, as extra fields on
1160
- * the `feature-not-found` payload.
1161
- *
1162
- * Exists because "not found" is the least useful thing we can say about a
1163
- * feature the user just worked on. The two real cases:
1164
- *
1165
- * - ARCHIVED: an earlier finish archived it and then failed to integrate.
1166
- * What is missing is the merge, not the whole finish — say so, because the
1167
- * user's instinct is to re-run `finish` and get refused again.
1168
- * - ON ITS BRANCH: the feature was created on `morph/{f}` and the command is
1169
- * running from the default branch, where `.morph/features/{f}` does not
1170
- * exist yet. Naming the branch turns a dead end into one checkout.
1171
- *
1172
- * Fail-open in every direction: a hint that cannot be computed is simply absent.
1173
- *
1174
- * @param {string} featureCwd - the tree we looked in
1175
- * @param {string} root - the primary root
1176
- * @param {string} feature
1177
- * @returns {{ archived?: string, hint?: string, onBranch?: string }}
1178
- */
823
+ // Where a feature not in `features/` went: ARCHIVED (only the merge is missing) or on its
824
+ // `morph/{f}` branch. Fail-open: a hint that cannot be computed is absent.
1179
825
  function featureElsewhereHint(featureCwd, root, feature) {
1180
826
  try {
1181
827
  const archived = join(featureCwd, '.morph', 'archive', feature);
@@ -1201,12 +847,6 @@ function featureElsewhereHint(featureCwd, root, feature) {
1201
847
  return { hint: `'${feature}' não existe em .morph/features/ nesta árvore. Confira o nome com \`morph-spec list --all\`.` };
1202
848
  }
1203
849
 
1204
- /**
1205
- * @param {string} cwd
1206
- * @param {string} feature
1207
- * @param {'features'|'archive'} [bucket] - `archive` on a resume, where the
1208
- * feature.json has already been moved (and stamped `completed`).
1209
- */
1210
850
  function readFeatureJson(cwd, feature, bucket = 'features') {
1211
851
  try {
1212
852
  return JSON.parse(readFileSync(join(cwd, '.morph', bucket, feature, 'feature.json'), 'utf8'));
@@ -1216,19 +856,8 @@ function readFeatureJson(cwd, feature, bucket = 'features') {
1216
856
  }
1217
857
 
1218
858
  /**
1219
- * Is `branch` already contained in `target` — i.e. did an earlier finish get its
1220
- * merge in?
1221
- *
1222
- * Three-valued ON PURPOSE. `git merge-base --is-ancestor` answers by exit code,
1223
- * and the two failure modes mean opposite things: **1** is a real "no, not
1224
- * merged", while **128** is "one of these refs does not resolve" and proves
1225
- * nothing at all. Collapsing them to a boolean would read an unresolvable ref as
1226
- * "not merged" and let a resume re-merge a completed feature.
1227
- *
1228
- * @param {string} branch
1229
- * @param {string} target
1230
- * @param {string} cwd
1231
- * @returns {boolean|null} null when the comparison could not be made
859
+ * Three-valued ON PURPOSE: `merge-base --is-ancestor` exit 1 is "not merged", 128 is "a ref does
860
+ * not resolve". `null` when the comparison could not be made.
1232
861
  */
1233
862
  export function branchMergedInto(branch, target, cwd) {
1234
863
  try {
@@ -1239,20 +868,8 @@ export function branchMergedInto(branch, target, cwd) {
1239
868
  }
1240
869
  }
1241
870
 
1242
- /**
1243
- * Does `branch` already have an OPEN pull request? The `--pr` half of
1244
- * "already integrated" — a completed `--pr` leaves the branch unmerged on
1245
- * purpose, so `branchMergedInto` cannot see it.
1246
- *
1247
- * Three-valued for the same reason: an unanswerable query (no `gh`, no auth, no
1248
- * network) is not evidence of "no PR", and treating it as such would resume
1249
- * into a duplicate `gh pr create`.
1250
- *
1251
- * @param {string} branch
1252
- * @param {string} cwd
1253
- * @returns {string|boolean|null} the PR url, `true` (open PR, url unavailable),
1254
- * `false` (none open), or `null` when the question could not be asked
1255
- */
871
+ // Three-valued: an unanswerable query (no `gh`, auth or network) is not evidence of "no PR".
872
+ // Returns the PR url, `true` (open, url unknown), `false` (none) or `null`.
1256
873
  function openPrFor(branch, cwd) {
1257
874
  const { bin, prefix } = resolveGh();
1258
875
  try {
@@ -1267,13 +884,7 @@ function openPrFor(branch, cwd) {
1267
884
  }
1268
885
  }
1269
886
 
1270
- /**
1271
- * Reclaim the feature's verification stack (containers, ports, named volumes,
1272
- * freshness stamp) from the tree that owns it. `--keep-volumes` keeps the
1273
- * named volumes (e.g. a seeded database) for the next feature. Best-effort by
1274
- * contract — a stack that refuses to come down must never fail a finish that
1275
- * already merged, so this never throws.
1276
- */
887
+ // Never throws: a stack that refuses to come down must not fail a finish that already merged.
1277
888
  function reclaimStack(feature, { root, worktreePath, options }) {
1278
889
  try {
1279
890
  return tearDownFeatureStack(feature, { root, worktreePath, volumes: !options.keepVolumes });
@@ -1282,7 +893,7 @@ function reclaimStack(feature, { root, worktreePath, options }) {
1282
893
  }
1283
894
  }
1284
895
 
1285
- /** Drop a feature from the thin, gitignored root index (never via saveState). */
896
+ // Never via saveState, which would re-materialize the feature folder.
1286
897
  function dropFromRootIndex(root, feature) {
1287
898
  const rootPath = join(root, '.morph', 'state.json');
1288
899
  if (!existsSync(rootPath)) return;