@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
@@ -2,12 +2,9 @@ import chalk from 'chalk';
2
2
  import { existsSync, renameSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
3
3
  import { join, resolve, sep } from 'path';
4
4
  import { execFileSync } from 'child_process';
5
- import { promoteUseCases, SPECS_USECASES_PATHSPEC } from '../../lib/specs/usecase-promoter.js';
6
- import { applySpecRevocations } from '../../lib/specs/acervo-lifecycle.js';
7
- import { ADR_INDEX_PATHSPEC } from '../../lib/specs/adr-index.js';
5
+ import { ADR_INDEX_PATHSPEC, updateAdrIndex } from '../../lib/specs/adr-index.js';
8
6
  import { isValidFeatureName } from '../project/worktree.js';
9
7
 
10
- /** Refuse a move whose resolved target escapes `scopeDir`. */
11
8
  function assertContained(scopeDir, name, target) {
12
9
  const scopeAbs = resolve(scopeDir) + sep;
13
10
  const targetAbs = resolve(target);
@@ -16,21 +13,7 @@ function assertContained(scopeDir, name, target) {
16
13
  }
17
14
  }
18
15
 
19
- /**
20
- * `morph-spec archive <feature>` — atomic, git-aware archive of a completed
21
- * feature. Stamps the feature completed, moves `.morph/features/{name}/` →
22
- * `.morph/archive/{name}/`, COMMITS the move (scoped to the feature's pathspec),
23
- * and cleans the thin root index.
24
- *
25
- * Two callers:
26
- * - `archiveCommand` (standalone `morph-spec archive` / `/morph-archive`) —
27
- * archive-without-integrate, for legacy features and cleanup.
28
- * - `finishCommand` — runs `runArchive` INSIDE the `morph/{feature}` branch so
29
- * the committed archived state travels with the merge/PR in one integration.
30
- *
31
- * Taxonomy: archived = `status: "completed"` + `archivedAt` (per the project
32
- * decision — no separate "archived" status value).
33
- */
16
+ // Archived = `status: "completed"` + `archivedAt`; there is no separate "archived" status.
34
17
 
35
18
  function isGitRepo(cwd) {
36
19
  try {
@@ -41,7 +24,6 @@ function isGitRepo(cwd) {
41
24
  }
42
25
  }
43
26
 
44
- /** Tolerant read of the review gate — mirrors the finish path's shape handling. */
45
27
  function readReviewApproved(fjPath) {
46
28
  try {
47
29
  const fj = JSON.parse(readFileSync(fjPath, 'utf8'));
@@ -53,33 +35,14 @@ function readReviewApproved(fjPath) {
53
35
  }
54
36
 
55
37
  /**
56
- * Core archive — NO process.exit. Returns a result the caller reports.
57
- *
58
- * Reads/writes everything relative to `cwd` (never a hard-coded process.cwd()),
59
- * so `finishCommand` can archive inside a worktree's own tree.
60
- *
61
- * Idempotent: if `.morph/archive/{feature}` already exists, returns
62
- * `{ ok: true, alreadyArchived: true }` WITHOUT failing — essential so a re-run
63
- * of `finish` (which archives inside the branch) does not blow up.
64
- *
65
- * @param {string} feature
66
- * @param {{ cwd?: string, force?: boolean, git?: boolean }} [opts]
67
- * @returns {{ ok: boolean, error?: string, archived?: boolean, alreadyArchived?: boolean,
68
- * committed?: boolean, staged?: boolean, archivePath?: string, promotedUseCases?: string[],
69
- * refusedUseCases?: Array<{id: string, incoming: string, existing: string, existingFeature: string, message: string}>,
70
- * keptSuperseded?: Array<{id: string, file: string, supersededBy: string|null, scope?: string}>,
71
- * supersededUseCases?: Array<{id: string, file: string, supersededBy: string, scope?: string}>,
72
- * revocationProblems?: Array<{id: string|null, reason: string, message: string}>,
73
- * adrIndex?: {added: string[], revoked: string[], narrowed: string[]}|null }}
38
+ * Never calls process.exit, and resolves everything from `cwd` so `finish` can archive inside a
39
+ * worktree. Idempotent: an existing archive returns `{ ok: true, alreadyArchived: true }`.
74
40
  */
75
41
  export function runArchive(feature, opts = {}) {
76
42
  const cwd = opts.cwd || process.cwd();
77
43
 
78
- // Gate 0: valid kebab name — refuses BEFORE any path is built from it. Same
79
- // gate `finish` already applies upstream (defense-in-depth here); the
80
- // standalone `archiveCommand` has no other gate at all, so without this an
81
- // unvalidated `feature` reaches `renameSync` and can MOVE a directory outside
82
- // .morph/features/ (`../../.claude` resolves to `cwd/.claude`).
44
+ // Validated BEFORE any path is built: `../../.claude` would let renameSync move a directory
45
+ // outside .morph/features/.
83
46
  if (!isValidFeatureName(feature)) {
84
47
  return { ok: false, error: 'invalid-feature-name' };
85
48
  }
@@ -93,16 +56,7 @@ export function runArchive(feature, opts = {}) {
93
56
  const featPathspec = join('.morph', 'features', feature);
94
57
  const archivePathspec = join('.morph', 'archive', feature);
95
58
 
96
- // Idempotent short-circuit: already archived here. Checked FIRST so a re-run
97
- // after a partial finish reports "already done", never "not found".
98
- //
99
- // Consequence, deliberate: this short-circuits BEFORE the use-case promotion
100
- // below, so a promotion that degraded fail-open is NOT retried by re-running
101
- // `finish`. That is the price of "closing a feature never fails" — an archived
102
- // feature stays archived, and the only signal is an absent `promotedUseCases`
103
- // in the output. Recovering means copying the UCs into `.morph/specs/usecases/`
104
- // by hand; making this retry would mean re-entering the archive path for an
105
- // already-closed feature, which is a worse trade.
59
+ // Checked FIRST so a re-run after a partial finish reports "already done", never "not found".
106
60
  if (existsSync(archivePath)) {
107
61
  return { ok: true, alreadyArchived: true, archived: false, archivePath };
108
62
  }
@@ -111,8 +65,6 @@ export function runArchive(feature, opts = {}) {
111
65
  return { ok: false, error: 'feature-folder-not-found' };
112
66
  }
113
67
 
114
- // Precondition: Gate 3 (review) approved — archiving is for completed work.
115
- // Read from the feature's own folder (pre-move), tolerant of legacy shapes.
116
68
  const fjPathBefore = join(featuresPath, 'feature.json');
117
69
  if (!opts.force && !readReviewApproved(fjPathBefore)) {
118
70
  return { ok: false, error: 'review-not-approved' };
@@ -120,9 +72,7 @@ export function runArchive(feature, opts = {}) {
120
72
 
121
73
  const useGit = opts.git !== false && isGitRepo(cwd);
122
74
 
123
- // 1) Move the folder. Prefer `git mv` (keeps history + stages in one step);
124
- // fall back to a filesystem rename (+ git add) when the folder isn't
125
- // tracked yet or git mv fails.
75
+ // `git mv` keeps history and stages in one step; an untracked folder falls back to rename.
126
76
  mkdirSync(archiveDir, { recursive: true });
127
77
  let staged = false;
128
78
  if (useGit) {
@@ -140,8 +90,6 @@ export function runArchive(feature, opts = {}) {
140
90
  renameSync(featuresPath, archivePath);
141
91
  }
142
92
 
143
- // 2) Stamp the authoritative feature.json (now at the archive path) as
144
- // completed + archived, and re-stage it so the commit carries the stamp.
145
93
  const fjPathAfter = join(archivePath, 'feature.json');
146
94
  if (existsSync(fjPathAfter)) {
147
95
  try {
@@ -156,63 +104,23 @@ export function runArchive(feature, opts = {}) {
156
104
  } catch { /* non-fatal — the move is what matters */ }
157
105
  }
158
106
 
159
- // 3) Promote the feature's use cases into the project's permanent archive
160
- // (.morph/specs/usecases/). Read from the ALREADY-MOVED folder so there is
161
- // a single source path, and land inside the SAME scoped commit below — the
162
- // archive travels with the integration, exactly like the archived state
163
- // (MORPH.md §2.2). Fail-open: any failure degrades to "promoted nothing",
164
- // never to a refused close.
165
- let promotedUseCases = [];
166
- let refusedUseCases = [];
167
- let keptSuperseded = [];
168
- try {
169
- const { promoted, refused, keptSuperseded: kept } = promoteUseCases(cwd, archivePath);
170
- promotedUseCases = promoted;
171
- refusedUseCases = refused;
172
- keptSuperseded = kept;
173
- if (promoted.length && useGit && staged) {
174
- // This staging exists for the NEW files: promoted use cases are untracked,
175
- // and the scoped `commit -- <pathspec>` below only knows paths git already
176
- // tracks. The replaced-by-ID deletions are tracked, so that same commit
177
- // would carry them even without this line (verified by mutating it away) —
178
- // `-A` is belt-and-braces, but dropping the `git add` entirely would
179
- // silently promote nothing on a first delivery.
180
- try { execFileSync('git', ['add', '-A', '--', SPECS_USECASES_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
181
- }
182
- } catch { /* promotion must never block archiving */ }
183
-
184
- // 3b) Apply what the feature's spec.md declared it revokes: acervo use cases
185
- // turn `superseded`, and the ADR index gains/loses files. After the
186
- // promotion, so a substitute promoted just now is already in place (and
187
- // one it refused is known not to be); same commit, same fail-open contract.
188
- let lifecycle = { superseded: [], adrIndex: null, problems: [] };
107
+ // Fail-open: the ADR index must never block archiving.
108
+ let adrIndex = { changed: false, added: [], revoked: [], narrowed: [], problems: [] };
189
109
  try {
190
- lifecycle = applySpecRevocations(cwd, archivePath, { refused: refusedUseCases });
191
- } catch { /* revocation must never block archiving */ }
192
- const acervoChanged = promotedUseCases.length > 0 || lifecycle.superseded.length > 0;
193
- const adrIndexChanged = lifecycle.adrIndex?.changed === true;
194
- if (useGit && staged) {
195
- if (lifecycle.superseded.length) {
196
- try { execFileSync('git', ['add', '-A', '--', SPECS_USECASES_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
197
- }
198
- if (adrIndexChanged) {
199
- try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
200
- }
110
+ adrIndex = updateAdrIndex(cwd, archivePath);
111
+ } catch { /* fail-open */ }
112
+ if (useGit && staged && adrIndex.changed) {
113
+ try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
201
114
  }
202
115
 
203
- // 4) Commit the move — SCOPED to this feature's pathspec, NEVER `commit -a`
204
- // (same anti-sweep discipline as `finish`'s worktree teardown). This is what
205
- // lets the archived state travel with the branch on `finish` (archive-before-merge).
206
- // The acervo pathspecs join only when something under them changed: git
207
- // refuses a pathspec that matches nothing, and that refusal would leave the
208
- // tree dirty and break the checkout/merge that follows.
116
+ // Scoped to this feature's pathspec, never `commit -a`. The ADR index joins only when it
117
+ // changed: git refuses a pathspec that matches nothing, leaving the tree dirty for the merge.
209
118
  let committed = false;
210
119
  if (useGit && staged) {
211
120
  const pathspecs = [
212
121
  featPathspec,
213
122
  archivePathspec,
214
- ...(acervoChanged ? [SPECS_USECASES_PATHSPEC] : []),
215
- ...(adrIndexChanged ? [ADR_INDEX_PATHSPEC] : []),
123
+ ...(adrIndex.changed ? [ADR_INDEX_PATHSPEC] : []),
216
124
  ];
217
125
  try {
218
126
  execFileSync('git', ['commit', '-m', `morph-spec archive: ${feature}`, '--', ...pathspecs], { cwd, stdio: 'pipe' });
@@ -220,9 +128,7 @@ export function runArchive(feature, opts = {}) {
220
128
  } catch { /* nothing to commit / no identity — not fatal, the move + stage stand */ }
221
129
  }
222
130
 
223
- // 5) Drop the feature from the thin root index. Edit the index file directly
224
- // rather than calling saveState — saveState would re-create
225
- // .morph/features/{name}/feature.json and undo the archive.
131
+ // Not saveState: it would re-create .morph/features/{name}/feature.json and undo the archive.
226
132
  const rootPath = join(cwd, '.morph', 'state.json');
227
133
  if (existsSync(rootPath)) {
228
134
  try {
@@ -236,21 +142,14 @@ export function runArchive(feature, opts = {}) {
236
142
  } catch { /* index is derivable — non-fatal */ }
237
143
  }
238
144
 
239
- const adr = lifecycle.adrIndex;
240
145
  return {
241
- ok: true, archived: true, committed, staged, archivePath, promotedUseCases, refusedUseCases,
242
- keptSuperseded,
243
- supersededUseCases: lifecycle.superseded,
244
- revocationProblems: lifecycle.problems,
245
- adrIndex: adrIndexChanged ? { added: adr.added, revoked: adr.revoked, narrowed: adr.narrowed } : null,
146
+ ok: true, archived: true, committed, staged, archivePath,
147
+ revocationProblems: adrIndex.problems,
148
+ adrIndex: adrIndex.changed ? { added: adrIndex.added, revoked: adrIndex.revoked, narrowed: adrIndex.narrowed } : null,
246
149
  };
247
150
  }
248
151
 
249
- /**
250
- * Standalone `morph-spec archive <feature>` — thin wrapper over `runArchive`
251
- * that maps its result to the CLI's messages and exit codes. Preserves the
252
- * historical behaviour: an already-archived feature is reported as a refusal.
253
- */
152
+ // Unlike `runArchive`, an already-archived feature is reported as a refusal here.
254
153
  export async function archiveCommand(featureName, options = {}) {
255
154
  try {
256
155
  const result = runArchive(featureName, {
@@ -281,27 +180,12 @@ export async function archiveCommand(featureName, options = {}) {
281
180
 
282
181
  console.log(chalk.green(`\n📦 Archived "${featureName}" → .morph/archive/${featureName}/`));
283
182
  console.log(chalk.gray(` status: completed, archivedAt stamped in feature.json.`));
284
- if (result.promotedUseCases?.length) {
285
- console.log(chalk.gray(` ${result.promotedUseCases.length} use case(s) promoted → .morph/specs/usecases/ (status: implemented).`));
286
- }
287
- if (result.refusedUseCases?.length) {
288
- console.log(chalk.yellow(`\n⚠️ ${result.refusedUseCases.length} use case(s) NOT promoted — numbering collision:`));
289
- for (const r of result.refusedUseCases) {
290
- console.log(chalk.yellow(` ${r.message}`));
291
- }
292
- }
293
- for (const k of result.keptSuperseded ?? []) {
294
- console.log(chalk.yellow(` ${k.id} continua superseded (superseded_by: ${k.supersededBy ?? '?'}) — a cópia promovida não trazia o status, e o acervo o manteve.`));
295
- }
296
- for (const s of result.supersededUseCases ?? []) {
297
- console.log(chalk.gray(` ${s.id} → status: superseded (superseded_by: ${s.supersededBy}${s.scope ? `; ainda vale em: ${s.scope}` : ''}).`));
298
- }
299
183
  if (result.adrIndex) {
300
184
  const { added, revoked, narrowed } = result.adrIndex;
301
185
  console.log(chalk.gray(` ADR index (.morph/specs/adrs/): +${added.length} −${revoked.length} ~${narrowed.length}.`));
302
186
  }
303
187
  if (result.revocationProblems?.length) {
304
- console.log(chalk.yellow(`\n⚠️ ${result.revocationProblems.length} revogação(ões) do spec.md NÃO aplicada(s):`));
188
+ console.log(chalk.yellow(`\n⚠️ ${result.revocationProblems.length} revogação(ões) do plan.md NÃO aplicada(s):`));
305
189
  for (const p of result.revocationProblems) {
306
190
  console.log(chalk.yellow(` ${p.message}`));
307
191
  }
@@ -0,0 +1,65 @@
1
+ import { planAutoApprovalBlock } from './gate-check.js';
2
+ import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
3
+
4
+ /**
5
+ * Why an "auto" signature may not stamp `gate`, or null when it may. The one rule
6
+ * `approve` and `advance` share: they write the same `approvalGates` entry.
7
+ */
8
+ export function autoApprovalRefusal({ cwd, featureName, feature, gate, approvedBy }) {
9
+ if (approvedBy !== 'auto' || !gate) return null;
10
+
11
+ if (gate === 'review' && feature.workType === 'hotfix') {
12
+ return {
13
+ title: 'Hotfix exige aprovação humana no Gate 3 — trust:auto não se aplica.',
14
+ hints: ['Aprove com --approver <nome>, num terminal interativo, ou pela resposta humana "aprovado" (state-sync) via AskUserQuestion.'],
15
+ };
16
+ }
17
+
18
+ const decision = feature.gateDecisions?.[gate];
19
+ if (decision?.pause === true) {
20
+ return {
21
+ title: `O Gate "${gate}" registrou pause:true — auto-aprovação não se aplica.`,
22
+ hints: [
23
+ `Sinal: ${decision.reason || 'risco detectado'}`,
24
+ 'A decisão gravada exigia pausa humana. Aprove com --approver <nome>, num',
25
+ 'terminal interativo, ou pela resposta humana via AskUserQuestion (state-sync).',
26
+ 'Se o sinal de risco já não vale, regrave a decisão:',
27
+ `morph-spec gate-decision ${featureName} ${gate} --pause false --reason "<motivo>"`,
28
+ ],
29
+ };
30
+ }
31
+
32
+ if (gate === 'plan') {
33
+ const block = planAutoApprovalBlock(cwd, featureName);
34
+ if (block) {
35
+ return {
36
+ title: `O gate "plan" não pode ser auto-aprovado — ${block}.`,
37
+ hints: ['Com plan-check verde, rode de novo; ou aprove com --approver <nome>. O --force não dispensa o checks.json.'],
38
+ };
39
+ }
40
+ }
41
+
42
+ if (gate === 'uiux') {
43
+ const block = uiuxAutoApprovalBlock(cwd, featureName, feature);
44
+ if (block) {
45
+ return {
46
+ title: `O Gate "uiux" não pode ser auto-aprovado — ${block}.`,
47
+ hints: [
48
+ 'Complete o checklist (ver a skill morph-uiux) e rode de novo, aprove com',
49
+ '--approver <nome>, ou, se a pausa já foi decidida, grave a decisão:',
50
+ `morph-spec gate-decision ${featureName} uiux --pause false --reason "<motivo>"`,
51
+ 'O --force não dispensa o checklist.',
52
+ ],
53
+ };
54
+ }
55
+ }
56
+
57
+ return null;
58
+ }
59
+
60
+ export function printRefusal(chalk, refusal) {
61
+ console.error(chalk.red(`\n❌ ${refusal.title}`));
62
+ refusal.hints.forEach((line, i) => {
63
+ console.error(chalk.gray(` ${line}${i === refusal.hints.length - 1 ? '\n' : ''}`));
64
+ });
65
+ }
@@ -7,30 +7,12 @@ import { WORK_TYPES, DEFAULT_WORK_TYPE, POLICY_APPROVER_PREFIX, isValidWorkType,
7
7
  import { detectWorkType } from '../../lib/work-type-detector.js';
8
8
  import { runWorktreeSetup, materializeState, isGitRepo, branchExists, buildBranchName } from '../project/worktree.js';
9
9
  import { detectDefaultBranch } from '../../lib/git/default-branch.js';
10
+ import { writePlanSkeleton } from '../../lib/plan/plan-template.js';
11
+ import { seedDotnetBuildIgnores } from '../../lib/stack/dotnet-build-ignores.js';
10
12
 
11
- /**
12
- * Common flow (no `--worktree`): still isolate the feature on its own branch
13
- * `morph/{feature}`, branched from the DEFAULT branch (never HEAD — so it never
14
- * inherits another feature branch's commits) and checked out in place, so
15
- * `morph-spec finish` can later merge/PR it back. Symmetric with the worktree
16
- * flow, which gets its branch from `runWorktreeSetup`.
17
- *
18
- * That checkout moves the dev's PRIMARY working tree, and `stdio: 'pipe'`
19
- * swallows even git's own message — so the move is reported by the caller
20
- * (`branchMoveWarning`), never silent. `checkout: false` (`--no-checkout`)
21
- * still CREATES the branch — `finish --pr|--merge` depends on it existing in
22
- * both flows — and only skips moving the tree.
23
- *
24
- * Fail-open: outside a git repo, with no resolvable default, or on any git
25
- * error, the feature is simply left on the current branch — never a failed birth.
26
- *
27
- * @param {string} featureName
28
- * @param {{ checkout?: boolean }} [opts] - `checkout: false` keeps the tree put
29
- * @returns {{ branched: boolean, checkedOut: boolean, branch?: string, from?: string,
30
- * movedFrom?: string, stayedOn?: string, reason?: string }}
31
- * `movedFrom` is present ONLY when the working tree actually moved (it names
32
- * the branch left behind); `stayedOn` only when the move was declined.
33
- */
13
+ // Branches from the DEFAULT branch, never HEAD, so it never inherits another feature's commits.
14
+ // `checkout: false` still CREATES the branch, which `finish` depends on. Fail-open: any git error
15
+ // leaves the feature on the current branch, never a failed birth.
34
16
  function branchCommonFlow(featureName, { checkout = true } = {}) {
35
17
  const cwd = process.cwd();
36
18
  try {
@@ -42,7 +24,6 @@ function branchCommonFlow(featureName, { checkout = true } = {}) {
42
24
  execFileSync('git', ['branch', branch, defaultBranch], { cwd, stdio: 'pipe' });
43
25
  }
44
26
  const current = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
45
- // Already there: nothing moved, so nothing to warn about.
46
27
  if (current === branch) return { branched: true, checkedOut: true, branch, from: defaultBranch };
47
28
  if (!checkout) return { branched: true, checkedOut: false, branch, from: defaultBranch, stayedOn: current };
48
29
  execFileSync('git', ['checkout', branch], { cwd, stdio: 'pipe' });
@@ -52,14 +33,7 @@ function branchCommonFlow(featureName, { checkout = true } = {}) {
52
33
  }
53
34
  }
54
35
 
55
- /**
56
- * The alert for a checkout that MOVED. Built once and rendered on both
57
- * surfaces — stdout (human) and stderr (under `--json`, so it survives a
58
- * stdout redirect) — so the two can never drift apart.
59
- *
60
- * @param {{ branch: string, movedFrom: string }} branchResult
61
- * @returns {string[]} lines, unstyled and unindented
62
- */
36
+ // One builder for stdout and (under --json) stderr, so the two surfaces never drift.
63
37
  function branchMoveWarning({ branch, movedFrom }) {
64
38
  return [
65
39
  `⚠️ Working tree MOVED: ${movedFrom} → ${branch}`,
@@ -70,21 +44,12 @@ function branchMoveWarning({ branch, movedFrom }) {
70
44
  ];
71
45
  }
72
46
 
73
- /** Feature names are kebab-case: lowercase alphanumerics and hyphens. */
74
47
  const FEATURE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
75
48
 
76
49
  const VALID_STATUSES = ['draft', 'in_progress', 'completed'];
77
50
  const VALID_DETECT_SOURCES = ['deterministic', 'llm', 'user'];
78
51
 
79
- /**
80
- * Resolve the work type for a NEW feature from `--type`/`--request`, or fall
81
- * back to the default. Never touches the filesystem/state — pure control
82
- * flow so it can `process.exit` before any side effect happens.
83
- *
84
- * @param {{ type?: string, request?: string, detectSource?: string }} options
85
- * @param {string} now - ISO timestamp, stamped into workTypeDetection.decidedAt
86
- * @returns {{ workType: string, workTypeDetection?: object }}
87
- */
52
+ // Pure control flow, so it can `process.exit` before any side effect happens.
88
53
  function resolveWorkType(options, now) {
89
54
  if (options.type !== undefined) {
90
55
  if (!isValidWorkType(options.type)) {
@@ -110,10 +75,8 @@ function resolveWorkType(options, now) {
110
75
  if (options.request !== undefined) {
111
76
  const d = detectWorkType({ text: options.request });
112
77
  if (d.confidence !== 'high') {
113
- // Router contract: determinism first, ambiguity escalates to the
114
- // LLM/HCOP caller (which re-runs `create --type <chosen>`). The
115
- // feature must NOT be created on this path — always JSON, regardless
116
- // of --json, since this is the machine-consumable half of the contract.
78
+ // Ambiguity escalates to the caller, which re-runs `create --type`: nothing is created here, and
79
+ // the output is always JSON since this is the machine-consumable half of the contract.
117
80
  console.log(JSON.stringify({ needsDecision: true, candidates: d.candidates, matched: d.matched }));
118
81
  process.exit(2);
119
82
  }
@@ -131,20 +94,8 @@ function resolveWorkType(options, now) {
131
94
  return { workType: DEFAULT_WORK_TYPE };
132
95
  }
133
96
 
134
- /**
135
- * `morph-spec create <feature>` — explicit feature birth: classifies the
136
- * work type (`--type`/`--request`), creates `.morph/features/{feature}/`
137
- * scaffolded per the type's dimensioned pipeline, stamps policy-approved
138
- * gates (`approvedBy: "policy:{type}"`) for the gates that type skips, and
139
- * (with `--worktree`) births the feature already inside its own git
140
- * worktree on branch `morph/{feature}`.
141
- *
142
- * Replaces the implicit birth via the state-sync hook on the first Write —
143
- * the skills call this at proposal start so the authoritative state exists
144
- * with the full canonical shape before any artifact lands. Idempotent: an
145
- * existing feature is reported and left untouched — no re-stamping, no
146
- * re-scaffolding, no worktree setup.
147
- */
97
+ // Idempotent: an existing feature is reported and left untouched (no re-stamping, no
98
+ // re-scaffolding, no worktree setup).
148
99
  export async function createCommand(featureName, options = {}) {
149
100
  try {
150
101
  if (!FEATURE_NAME_RE.test(featureName)) {
@@ -159,15 +110,8 @@ export async function createCommand(featureName, options = {}) {
159
110
  process.exit(1);
160
111
  }
161
112
 
162
- // `--no-checkout` pede que o checkout PRINCIPAL não se mova; `--worktree`
163
- // nunca o move — a feature nasce numa árvore separada, e o caminho de
164
- // worktree sequer chama `branchCommonFlow`, que é o único leitor do flag.
165
- // Aceitar a combinação em silêncio é dizer que o CLI honrou um pedido que
166
- // ele ignorou — o mesmo anti-padrão que esta série de fixes tirou do
167
- // `finish --pr --push`. Recusa ALTA, e AQUI: junto das outras validações
168
- // de forma, ANTES de `createFeature` — a primeira mutação do `create`.
169
- // Uma recusa depois dela deixaria a feature nascida por um comando que
170
- // falhou, pior que o no-op que ela substitui.
113
+ // `--worktree` nunca move o checkout principal: aceitar `--no-checkout` junto em silêncio é fingir
114
+ // que o pedido foi honrado. Recusa AQUI, antes de `createFeature`, a primeira mutação do `create`.
171
115
  if (options.worktree && options.checkout === false) {
172
116
  console.error(chalk.red(`\n❌ no-checkout-with-worktree: --no-checkout não se combina com --worktree`));
173
117
  console.error(chalk.gray(` O --worktree já não move o checkout desta árvore: a feature nasce em worktrees/${featureName},`));
@@ -180,16 +124,10 @@ export async function createCommand(featureName, options = {}) {
180
124
  const now = new Date().toISOString();
181
125
  const { workType, workTypeDetection } = resolveWorkType(options, now);
182
126
 
183
- // Identity at birth: every feature is stamped name (slug by default) +
184
- // description, so it is never an anonymous {status,updatedAt} shell.
185
- // `--request` (which the router already consumes) doubles as the
186
- // description seed. A NEW feature with no description source is refused —
187
- // the description is the one identity field a human must supply. An
188
- // idempotent re-run on an EXISTING feature never demands it.
127
+ // A NEW feature with no description source is refused: it is the one identity field a human
128
+ // must supply. An idempotent re-run never demands it.
189
129
  const description = options.description ?? options.request ?? null;
190
- // Existence check via the filesystem, NOT getFeature/loadState — loadState
191
- // throws when no state exists yet (a feature can be the first morph action
192
- // in a fresh project), which would break birth-from-nothing.
130
+ // Filesystem, not loadState: loadState throws when no state exists yet (birth from nothing).
193
131
  const alreadyExists = existsSync(join(process.cwd(), '.morph', 'features', featureName, 'feature.json'));
194
132
  if (!alreadyExists && (description === null || String(description).trim() === '')) {
195
133
  console.error(chalk.red(`\n❌ A feature needs a description at birth: "${featureName}"`));
@@ -211,16 +149,15 @@ export async function createCommand(featureName, options = {}) {
211
149
 
212
150
  let worktreeResult = null;
213
151
  let branchResult = null;
152
+ let planSkeleton = null;
153
+ let gitignoreAdded = [];
214
154
 
215
155
  if (created) {
216
- // Scaffold the phase folders this work type starts with.
217
156
  for (const folder of wt.scaffold) {
218
157
  mkdirSync(join(featureDir, folder), { recursive: true });
219
158
  }
220
159
 
221
- // Stamp the gates this type skips by policy — NEVER "auto" (the
222
- // gate-guard hook only special-cases approvedBy === "auto"; policy:{type}
223
- // keeps the audit trail honest while staying silent to the guard).
160
+ // NEVER "auto": the gate-guard hook special-cases it; `policy:{type}` keeps the audit honest.
224
161
  for (const gate of wt.skippedGates) {
225
162
  await setApprovalGate(featureName, gate, true, {
226
163
  approvedBy: `${POLICY_APPROVER_PREFIX}${workType}`,
@@ -232,19 +169,10 @@ export async function createCommand(featureName, options = {}) {
232
169
  worktreeResult = await runWorktreeSetup(featureName, {});
233
170
  if (worktreeResult?.created && worktreeResult.path) {
234
171
  try {
235
- // feature.json in the root is NOT committed yet (create just
236
- // wrote it), so the branch `runWorktreeSetup` forked off the
237
- // DEFAULT branch cannot have it — copy the freshly-scaffolded
238
- // folder into the worktree and re-materialize its local state.json.
172
+ // The root feature.json is not committed yet, so the worktree branch cannot have it: copy it in.
239
173
  cpSync(featureDir, join(worktreeResult.path, '.morph', 'features', featureName), { recursive: true });
240
- // The `.worktree` registration is ROOT-OWNED metadata (runWorktreeSetup
241
- // stamped it onto the root's feature.json, which cpSync just copied in).
242
- // Strip it from the worktree's branch-local copy so the branch commits a
243
- // clean feature.json — otherwise it merges back into the root at
244
- // `morph-spec finish` and the post-merge cleanup leaves the root dirty.
245
- // verify.decidePersistence reads the registration from the ROOT's copy,
246
- // never this branch-local one (see verify.js), so stripping is safe and
247
- // restores the invariant that comment relies on.
174
+ // `.worktree` is ROOT-OWNED: left in the branch copy it merges back at `finish` and leaves the
175
+ // root dirty. verify reads the registration from the root copy only.
248
176
  try {
249
177
  const wtFeatureJson = join(worktreeResult.path, '.morph', 'features', featureName, 'feature.json');
250
178
  const fj = JSON.parse(readFileSync(wtFeatureJson, 'utf8'));
@@ -253,41 +181,47 @@ export async function createCommand(featureName, options = {}) {
253
181
  writeFileSync(wtFeatureJson, JSON.stringify(fj, null, 2), 'utf8');
254
182
  }
255
183
  } catch { /* fail-open — root copy still carries the registration for verify */ }
256
- // Re-materialize now that the feature.json exists in the worktree, and
257
- // report THIS result — runWorktreeSetup's earlier materialize ran before
258
- // the copy (no features yet), so its `materialized:false` was stale.
184
+ // Re-materialize after the copy: the setup's earlier materialize ran with no features yet.
259
185
  const remat = await materializeState(worktreeResult.path);
260
186
  worktreeResult.materialized = remat.materialized;
261
- // Same reason the setup path generates one: the new instance's first
262
- // read. Regenerated here because setup ran before the copy above.
263
187
  try {
264
188
  const { writeHandoff } = await import('../../lib/generators/handoff-generator.js');
265
- // Same reason as runWorktreeSetup's own call: `worktree` was just
266
- // stripped from this branch-local copy a few lines up, so the doc
267
- // must be told explicitly where it actually lives.
189
+ // `worktree` was stripped from this copy above, so pass where it actually lives.
268
190
  writeHandoff(featureName, worktreeResult.path, { worktree: { path: worktreeResult.path, branch: worktreeResult.branch } });
269
191
  } catch { /* fail-open */ }
270
192
  } catch { /* fail-open — the feature already exists in root */ }
271
193
  }
272
194
  } else {
273
- // Common flow: still isolate the feature on its own branch so `finish`
274
- // can merge/PR it back. Fail-open (see branchCommonFlow).
275
195
  // Commander's `--no-checkout` sets options.checkout === false.
276
196
  branchResult = branchCommonFlow(featureName, { checkout: options.checkout !== false });
277
197
  }
198
+
199
+ // protect-spec-files freezes an existing file once its approval is stamped,
200
+ // so a type born pre-approved must create it itself. With a worktree it goes
201
+ // only there: an extra file in the root copy makes `finish` refuse a dirty root.
202
+ if (wt.scaffold.includes('2-plan') && !wt.skippedGates.includes('plan')) {
203
+ const home = worktreeResult?.created && worktreeResult.path ? worktreeResult.path : process.cwd();
204
+ try {
205
+ planSkeleton = writePlanSkeleton(featureName, home);
206
+ } catch (err) {
207
+ planSkeleton = { written: false, error: err.message };
208
+ }
209
+ }
210
+
211
+ const home = worktreeResult?.created && worktreeResult.path ? worktreeResult.path : process.cwd();
212
+ gitignoreAdded = seedDotnetBuildIgnores(home, { featureName, configRoot: process.cwd() });
278
213
  }
279
214
 
280
- // `feature` is a snapshot from BEFORE the gate-stamping/worktree steps —
281
- // setApprovalGate/runWorktreeSetup each do their own loadState()/saveState()
282
- // round-trip on disk, so re-read the authoritative copy for reporting.
215
+ // `feature` predates the gate-stamping and worktree steps, which each round-trip state on disk.
283
216
  const finalFeature = created ? (getFeature(featureName) || feature) : feature;
284
217
 
285
218
  if (options.json) {
286
219
  const out = { feature: featureName, created, ...finalFeature };
287
220
  if (worktreeResult) out.worktree = worktreeResult;
288
221
  if (branchResult) out.branch = branchResult;
289
- // The move of the PRIMARY checkout is never silent — under --json it goes
290
- // to stderr so it survives `... --json > out.json` instead of vanishing.
222
+ if (planSkeleton) out.planSkeleton = planSkeleton;
223
+ if (gitignoreAdded.length) out.gitignoreAdded = gitignoreAdded;
224
+ // Under --json the move goes to stderr, so it survives `... --json > out.json`.
291
225
  if (branchResult?.movedFrom) {
292
226
  console.error(chalk.yellow(branchMoveWarning(branchResult).join('\n')));
293
227
  }
@@ -300,6 +234,12 @@ export async function createCommand(featureName, options = {}) {
300
234
  console.log(chalk.gray(` .morph/features/${featureName}/feature.json (canonical shape)`));
301
235
  console.log(chalk.gray(` status: ${finalFeature.status} | createdAt: ${finalFeature.createdAt}`));
302
236
  console.log(chalk.gray(` scaffolded: ${wt.scaffold.join(', ')}`));
237
+ if (gitignoreAdded.length) {
238
+ console.log(chalk.gray(` .gitignore: ${gitignoreAdded.join(' ')} adicionados (projeto .NET)`));
239
+ }
240
+ if (planSkeleton?.written) {
241
+ console.log(chalk.gray(` 2-plan/plan.md: esqueleto escrito (gramática: morph-spec plan-check --help)`));
242
+ }
303
243
  if (wt.skippedGates.length) {
304
244
  console.log(chalk.yellow(` gates skipped by policy:${finalFeature.workType}: ${wt.skippedGates.join(', ')}`));
305
245
  }