@polymorphism-tech/morph-spec 8.47.0 → 9.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (377) hide show
  1. package/CLAUDE.md +51 -43
  2. package/README.md +39 -38
  3. package/bin/morph-spec.js +41 -113
  4. package/bin/validate.js +1 -147
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +86 -21
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-status.md +4 -4
  17. package/framework/docs/CLI.md +574 -286
  18. package/framework/evals/review-checklist.md +82 -41
  19. package/framework/hooks/README.md +10 -11
  20. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  21. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  22. package/framework/hooks/claude-code/core/chain.js +4 -65
  23. package/framework/hooks/claude-code/core/comment-guard.js +3 -17
  24. package/framework/hooks/claude-code/core/denylist.json +33 -3
  25. package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
  26. package/framework/hooks/claude-code/core/gate-guard.js +9 -78
  27. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  28. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  29. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  30. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  31. package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
  32. package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
  33. package/framework/hooks/claude-code/core/state-sync.js +15 -85
  34. package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
  35. package/framework/hooks/claude-code/core/validate-completion.js +37 -147
  36. package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
  37. package/framework/hooks/claude-code/core/worktree-context.js +15 -114
  38. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  39. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  40. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  41. package/framework/hooks/claude-code/statusline.py +55 -41
  42. package/framework/hooks/package.json +3 -0
  43. package/framework/hooks/shared/activity-logger.js +9 -89
  44. package/framework/hooks/shared/agent-reap.js +2 -17
  45. package/framework/hooks/shared/build-check.js +11 -76
  46. package/framework/hooks/shared/claim-registry.js +9 -119
  47. package/framework/hooks/shared/command-tokens.js +168 -44
  48. package/framework/hooks/shared/comment-rules.js +5 -20
  49. package/framework/hooks/shared/comment-scanner.js +11 -43
  50. package/framework/hooks/shared/comment-stripper.js +11 -85
  51. package/framework/hooks/shared/e2e-check.js +0 -0
  52. package/framework/hooks/shared/errata.js +59 -0
  53. package/framework/hooks/shared/feature-locator.js +11 -95
  54. package/framework/hooks/shared/frontend-locations.js +4 -0
  55. package/framework/hooks/shared/gate-question.js +12 -0
  56. package/framework/hooks/shared/git-exclude.js +3 -20
  57. package/framework/hooks/shared/git-paths.js +8 -104
  58. package/framework/hooks/shared/graph-freshness.js +22 -166
  59. package/framework/hooks/shared/graph-refresh.js +21 -149
  60. package/framework/hooks/shared/heavy-command.js +4 -12
  61. package/framework/hooks/shared/hook-response.js +8 -83
  62. package/framework/hooks/shared/morph-cli.js +8 -52
  63. package/framework/hooks/shared/payload-utils.js +10 -132
  64. package/framework/hooks/shared/phase-utils.js +14 -72
  65. package/framework/hooks/shared/redact-secrets.js +4 -15
  66. package/framework/hooks/shared/reparse-scan.js +6 -66
  67. package/framework/hooks/shared/slice-progress.js +99 -0
  68. package/framework/hooks/shared/source-files.js +4 -17
  69. package/framework/hooks/shared/spawn-command.js +23 -0
  70. package/framework/hooks/shared/state-reader.js +51 -247
  71. package/framework/hooks/shared/state-writer.js +9 -54
  72. package/framework/hooks/shared/stdin-reader.js +1 -11
  73. package/framework/hooks/shared/telemetry-logger.js +7 -67
  74. package/framework/hooks/shared/transcript-usage.js +7 -29
  75. package/framework/hooks/shared/worktree-root.js +7 -29
  76. package/framework/mcp/registry.json +3 -4
  77. package/framework/phases.json +23 -39
  78. package/framework/rules/csharp-standards.md +1 -1
  79. package/framework/rules/frontend-standards.md +1 -0
  80. package/framework/rules/hangfire-standards.md +1 -1
  81. package/framework/rules/neon-standards.md +1 -1
  82. package/framework/schemas/config.schema.json +14 -9
  83. package/framework/schemas/feature.schema.json +15 -50
  84. package/framework/skills/README.md +3 -4
  85. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  86. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  87. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  88. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  89. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  90. package/framework/skills/core/morph-media/SKILL.md +1 -1
  91. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  92. package/framework/skills/core/morph-review/SKILL.md +111 -136
  93. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  94. package/framework/skills/core/morph-uiux/SKILL.md +94 -152
  95. package/framework/standards/STANDARDS.json +109 -1219
  96. package/framework/standards/STANDARDS.meta.json +1319 -0
  97. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  98. package/framework/standards/ai-agents/agent-session.md +1 -1
  99. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  100. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  101. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  102. package/framework/standards/ai-agents/guardrails.md +1 -1
  103. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  104. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  105. package/framework/standards/ai-agents/production.md +1 -1
  106. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  107. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  108. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  109. package/framework/standards/ai-agents/setup.md +3 -3
  110. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  111. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  112. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  113. package/framework/standards/ai-agents/workflows.md +1 -1
  114. package/framework/standards/architecture/code-comments.md +7 -7
  115. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  116. package/framework/standards/backend/api/validation.md +1 -1
  117. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  118. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  119. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  120. package/framework/standards/data/nosql/cache/redis.md +1 -1
  121. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  122. package/framework/standards/frontend/admin-app/forms.md +1 -1
  123. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  124. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  125. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  126. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  127. package/framework/templates/CLAUDE.md.core.template +3 -3
  128. package/framework/templates/README.md +2 -3
  129. package/framework/templates/REGISTRY.json +15 -179
  130. package/framework/templates/docs/plan.md +72 -0
  131. package/framework/templates/docs/ui-mockups.md +4 -0
  132. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  134. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  135. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  136. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  137. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  138. package/package.json +1 -1
  139. package/src/commands/context/context-refresh.js +0 -16
  140. package/src/commands/dashboard/dashboard.js +2 -8
  141. package/src/commands/e2e/e2e.js +18 -105
  142. package/src/commands/graph/affected.js +0 -14
  143. package/src/commands/graph/build.js +11 -50
  144. package/src/commands/graph/explain.js +0 -16
  145. package/src/commands/graph/status.js +1 -12
  146. package/src/commands/mcp/mcp-setup.js +4 -62
  147. package/src/commands/observability/cost.js +17 -111
  148. package/src/commands/observability/telemetry.js +1 -14
  149. package/src/commands/project/branch.js +4 -45
  150. package/src/commands/project/claim.js +4 -48
  151. package/src/commands/project/doctor-procs.js +1 -1
  152. package/src/commands/project/doctor.js +53 -307
  153. package/src/commands/project/env.js +1 -27
  154. package/src/commands/project/fleet.js +6 -35
  155. package/src/commands/project/heavy.js +3 -3
  156. package/src/commands/project/init.js +8 -68
  157. package/src/commands/project/install-plugin-cmd.js +0 -1
  158. package/src/commands/project/setup-infra-cmd.js +2 -1
  159. package/src/commands/project/stack-scan.js +58 -0
  160. package/src/commands/project/standards.js +65 -72
  161. package/src/commands/project/status.js +28 -121
  162. package/src/commands/project/update.js +31 -223
  163. package/src/commands/project/worktree.js +109 -883
  164. package/src/commands/scaffold/scaffold.js +0 -25
  165. package/src/commands/state/advance.js +65 -120
  166. package/src/commands/state/approve.js +87 -229
  167. package/src/commands/state/archive.js +23 -139
  168. package/src/commands/state/auto-approval.js +65 -0
  169. package/src/commands/state/create.js +49 -109
  170. package/src/commands/state/delete.js +11 -63
  171. package/src/commands/state/finish.js +105 -494
  172. package/src/commands/state/gate-check.js +74 -183
  173. package/src/commands/state/gate-decision.js +8 -42
  174. package/src/commands/state/handoff.js +4 -31
  175. package/src/commands/state/list.js +21 -91
  176. package/src/commands/state/report-check.js +134 -338
  177. package/src/commands/state/retype.js +12 -28
  178. package/src/commands/validation/index.js +0 -3
  179. package/src/commands/validation/mutate.js +79 -19
  180. package/src/commands/validation/plan-check.js +81 -0
  181. package/src/commands/validation/validate-feature.js +52 -216
  182. package/src/commands/validation/verify.js +45 -160
  183. package/src/core/paths/output-schema.js +9 -155
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +2 -30
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +32 -395
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +21 -0
  191. package/src/core/work-types.js +10 -45
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
  194. package/src/dashboard/app/components/CommandPalette.jsx +4 -11
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +3 -9
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/pages/Activity.jsx +1 -1
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
  211. package/src/dashboard/app/pages/Features.jsx +9 -16
  212. package/src/dashboard/app/pages/Guide.jsx +2 -3
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +14 -83
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +1 -7
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +92 -113
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +4 -7
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/lib/ai/ai-checks.js +7 -52
  233. package/src/lib/ai/ai-project-scan.js +14 -108
  234. package/src/lib/ai/code-audit.js +7 -63
  235. package/src/lib/ai/pin-drift.js +8 -51
  236. package/src/lib/ai/registry-audit.js +11 -90
  237. package/src/lib/ci/ci-verify.js +14 -13
  238. package/src/lib/ci/github-ci.js +9 -37
  239. package/src/lib/ci/workflow-scaffold.js +2 -10
  240. package/src/lib/detectors/claude-config-detector.js +13 -74
  241. package/src/lib/detectors/index.js +8 -59
  242. package/src/lib/e2e/compose-env.js +5 -66
  243. package/src/lib/e2e/stack-teardown.js +6 -40
  244. package/src/lib/generators/architecture-scanner.js +18 -156
  245. package/src/lib/generators/handoff-generator.js +37 -112
  246. package/src/lib/generators/scaffold-generator.js +5 -101
  247. package/src/lib/generators/settings-generator.js +11 -122
  248. package/src/lib/git/branch-classifier.js +11 -84
  249. package/src/lib/git/default-branch.js +14 -169
  250. package/src/lib/git/evidence-staleness.js +2 -1
  251. package/src/lib/git/gh.js +2 -17
  252. package/src/lib/git/pending-prs.js +6 -37
  253. package/src/lib/git/pr-files.js +3 -28
  254. package/src/lib/git/pr-index.js +3 -34
  255. package/src/lib/git/tree-fingerprint.js +17 -17
  256. package/src/lib/git/tree-state.js +6 -33
  257. package/src/lib/graph/god-nodes.js +2 -22
  258. package/src/lib/graph/graph-health.js +3 -24
  259. package/src/lib/graph/graph-loader.js +5 -56
  260. package/src/lib/graph/graphify-runtime.js +7 -37
  261. package/src/lib/graph/graphifyignore.js +8 -74
  262. package/src/lib/graph/sanitize.js +16 -136
  263. package/src/lib/heavy/cleanup.js +2 -6
  264. package/src/lib/installers/effects.js +8 -93
  265. package/src/lib/installers/mcp-installer.js +9 -169
  266. package/src/lib/installers/memory-installer.js +8 -33
  267. package/src/lib/installers/templates-reconcile.js +5 -60
  268. package/src/lib/mutation/commands.js +3 -2
  269. package/src/lib/mutation/dotnet-introspection.js +281 -0
  270. package/src/lib/mutation/plan.js +14 -6
  271. package/src/lib/mutation/runner.js +1 -1
  272. package/src/lib/mutation/subset.js +47 -13
  273. package/src/lib/plan/artifact-vocabulary.js +40 -0
  274. package/src/lib/plan/checks.js +208 -0
  275. package/src/lib/plan/plan-parser.js +204 -0
  276. package/src/lib/plan/plan-template.js +18 -0
  277. package/src/lib/projects/registry.js +7 -44
  278. package/src/lib/review-reports.js +204 -0
  279. package/src/lib/risk-detector.js +131 -274
  280. package/src/lib/specs/adr-index.js +38 -85
  281. package/src/lib/specs/design-seed.js +18 -0
  282. package/src/lib/specs/frontmatter.js +31 -0
  283. package/src/lib/specs/gfm-table.js +11 -0
  284. package/src/lib/specs/revocations.js +31 -102
  285. package/src/lib/specs/screen-checklist.js +56 -132
  286. package/src/lib/stack/dotnet-build-ignores.js +43 -0
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +260 -0
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +37 -0
  291. package/src/lib/standards/sections.js +47 -0
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +23 -179
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -313
  296. package/src/lib/tasks/test-runner.js +107 -447
  297. package/src/lib/telemetry/metrics.js +13 -103
  298. package/src/lib/telemetry/telemetry-reader.js +13 -238
  299. package/src/lib/telemetry/verify-event.js +4 -34
  300. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  301. package/src/lib/validators/code-budget/index.js +0 -7
  302. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
  303. package/src/lib/validators/contracts/index.js +0 -6
  304. package/src/lib/validators/nextjs/index.js +0 -5
  305. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  306. package/src/lib/validators/packages/index.js +0 -6
  307. package/src/lib/validators/packages/package-validator.js +6 -56
  308. package/src/lib/validators/reuse/index.js +0 -8
  309. package/src/lib/validators/reuse/reuse-validator.js +24 -137
  310. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  311. package/src/lib/validators/traceability-check.js +54 -156
  312. package/src/lib/validators/validation-runner.js +20 -125
  313. package/src/lib/validators/verify-runner.js +152 -575
  314. package/src/lib/work-type-detector.js +5 -67
  315. package/src/lib/worktree/docker-audit.js +17 -137
  316. package/src/lib/worktree/env-allocator.js +10 -98
  317. package/src/lib/worktree/feature-truth.js +12 -115
  318. package/src/lib/worktree/finish-backup.js +32 -242
  319. package/src/lib/worktree/fleet-scanner.js +31 -126
  320. package/src/lib/worktree/ignored-triage.js +15 -91
  321. package/src/lib/worktree/vscode-settings.js +2 -3
  322. package/src/lib/worktree/worktree-audit.js +21 -213
  323. package/src/lib/worktree/worktree-include.js +3 -5
  324. package/src/scripts/global-install.js +3 -9
  325. package/src/scripts/install-plugin.js +1 -13
  326. package/src/scripts/setup-infra.js +17 -138
  327. package/src/utils/agents-installer.js +92 -272
  328. package/src/utils/banner.js +4 -25
  329. package/src/utils/claude-md-injector.js +1 -37
  330. package/src/utils/claude-settings-manager.js +1 -87
  331. package/src/utils/file-copier.js +32 -90
  332. package/src/utils/hooks-installer.js +26 -296
  333. package/src/utils/skills-installer.js +4 -102
  334. package/src/utils/version-checker.js +4 -83
  335. package/framework/commands/morph-proposal.md +0 -57
  336. package/framework/evals/trace-schema.md +0 -67
  337. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  338. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  339. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  340. package/framework/hooks/shared/tasks-validator.js +0 -111
  341. package/framework/rules/morph-task-tracking.md +0 -54
  342. package/framework/rules/morph-workflow.md +0 -244
  343. package/framework/schemas/tasks.schema.json +0 -79
  344. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  345. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  346. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  347. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  348. package/framework/templates/docs/proposal.md +0 -192
  349. package/framework/templates/docs/spec.md +0 -57
  350. package/framework/templates/docs/ui-components.md +0 -52
  351. package/framework/templates/docs/ui-design-system.md +0 -76
  352. package/framework/templates/docs/ui-flows.md +0 -167
  353. package/framework/templates/examples/spec-examples.md +0 -91
  354. package/framework/templates/feature/decisions.md +0 -124
  355. package/framework/templates/feature/recap.md +0 -145
  356. package/framework/templates/tasks.template.json +0 -17
  357. package/src/commands/state/usecase.js +0 -143
  358. package/src/commands/tasks/dag.js +0 -181
  359. package/src/commands/utils/index.js +0 -3
  360. package/src/lib/agents/output-collector.js +0 -76
  361. package/src/lib/detectors/design-system-detector.js +0 -188
  362. package/src/lib/evaluator-report.js +0 -104
  363. package/src/lib/generators/design-system-generator.js +0 -508
  364. package/src/lib/specs/acervo-format.js +0 -70
  365. package/src/lib/specs/acervo-lifecycle.js +0 -264
  366. package/src/lib/specs/done-criteria-check.js +0 -200
  367. package/src/lib/specs/usecase-check.js +0 -301
  368. package/src/lib/specs/usecase-promoter.js +0 -301
  369. package/src/lib/specs/usecase-registry.js +0 -237
  370. package/src/lib/tasks/dispatch-planner.js +0 -319
  371. package/src/lib/tasks/dotnet-test-introspection.js +0 -632
  372. package/src/lib/tasks/mermaid-renderer.js +0 -132
  373. package/src/lib/tasks/task-classifier.js +0 -30
  374. package/src/lib/tasks/task-parser.js +0 -209
  375. package/src/lib/tasks/tasks-json-parser.js +0 -296
  376. package/src/lib/tasks/test-scope.js +0 -597
  377. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,4 +1,3 @@
1
- // src/utils/banner.js
2
1
  import chalk from "chalk";
3
2
  import { type as osType, release as osRelease, homedir } from "os";
4
3
  import { join } from "path";
@@ -7,7 +6,6 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
7
6
  const BRAND = chalk.hex("#7C3AED");
8
7
  const DIM = chalk.dim;
9
8
 
10
- /** Purple gradient, one tone per logo line (top → bottom). */
11
9
  const GRADIENT = [
12
10
  "#6D28D9",
13
11
  "#7C3AED",
@@ -26,16 +24,8 @@ const LOGO_LINES = [
26
24
  "╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝",
27
25
  ];
28
26
 
29
- const LOGO_WIDTH = LOGO_LINES[0].length; // 44
27
+ const LOGO_WIDTH = LOGO_LINES[0].length;
30
28
 
31
- /**
32
- * The morph-spec logo: gradient "MORPH" block art with a S P E C + version
33
- * rule underneath. Single source for the CLI help banner and the install
34
- * banner — one drawing, everywhere.
35
- *
36
- * @param {string} version - e.g. "7.7.2"
37
- * @returns {string}
38
- */
39
29
  export function renderLogo(version) {
40
30
  const art = LOGO_LINES.map(
41
31
  (line, i) => " " + chalk.hex(GRADIENT[i])(line),
@@ -56,19 +46,8 @@ export function renderLogo(version) {
56
46
  return `\n${art}\n${rule}\n`;
57
47
  }
58
48
 
59
- /**
60
- * Print the install banner once per version change, on the first CLI run
61
- * after an install/upgrade. npm ≥7 swallows lifecycle-script output, so the
62
- * postinstall banner is invisible on a normal `npm i -g` — this is the
63
- * fallback that guarantees the next-steps guidance is actually seen.
64
- *
65
- * Marker: ~/.claude/morph-spec-banner-version (last version shown).
66
- * Skips silently when stdout is not a TTY (piped/parsed output) or when
67
- * MORPH_QUIET=1. Fail-open: any fs error just skips the banner.
68
- *
69
- * @param {{version: string, nodeVersion: string, markerDir?: string}} opts
70
- * @returns {boolean} true if the banner was printed
71
- */
49
+ // npm >=7 swallows postinstall output, so the first CLI run after an upgrade
50
+ // shows the banner instead. Skips without a TTY or with MORPH_QUIET=1.
72
51
  export function maybePrintFirstRunBanner({ version, nodeVersion, markerDir }) {
73
52
  try {
74
53
  if (!process.stdout.isTTY) return false;
@@ -117,7 +96,7 @@ export function printInstallBanner({ version, nodeVersion, results = [] }) {
117
96
  sep("Next · new project"),
118
97
  ` ${chalk.cyan("1.")} ${DIM("cd")} ${chalk.white("your-project")} ${DIM("(git repo)")}`,
119
98
  ` ${chalk.cyan("2.")} Run ${chalk.bold.white("morph-spec init")}`,
120
- ` ${chalk.cyan("3.")} Open it in Claude Code and run ${chalk.bold.white("/morph-proposal <feature>")}`,
99
+ ` ${chalk.cyan("3.")} Open it in Claude Code and run ${chalk.bold.white("/morph-plan <feature>")}`,
121
100
  "",
122
101
  sep("Next · project already on morph-spec"),
123
102
  ` ${chalk.cyan("1.")} Run ${chalk.bold.white("morph-spec update")} ${DIM("— after a CLI upgrade or a project.stack change: full stack-aware resync")}`,
@@ -1,13 +1,3 @@
1
- /**
2
- * claude-md-injector.js
3
- *
4
- * Manages the root CLAUDE.md file in client projects.
5
- * Instead of overwriting the user's CLAUDE.md, injects an @import line
6
- * that references the morph-spec managed .claude/CLAUDE.md.
7
- *
8
- * The root CLAUDE.md belongs to the USER — morph-spec only adds its import.
9
- */
10
-
11
1
  import { pathExists, readFile } from './file-copier.js';
12
2
  import { realEffects } from '../lib/installers/effects.js';
13
3
 
@@ -15,20 +5,7 @@ const MORPH_IMPORT = '@.claude/CLAUDE.md';
15
5
  const MORPH_MARKER = 'MORPH-SPEC';
16
6
 
17
7
  /**
18
- * Injects the morph-spec @import into the root CLAUDE.md.
19
- *
20
- * Three scenarios:
21
- * 1. No CLAUDE.md exists → create minimal file with the import
22
- * 2. Existing morph-only CLAUDE.md (old overwrite) → replace with minimal + import
23
- * 3. Existing user CLAUDE.md → append import section at the end
24
- *
25
- * Idempotent — does nothing if the import already exists.
26
- *
27
- * @param {string} claudeMdPath - Absolute path to root CLAUDE.md
28
- * @param {ReturnType<import('../lib/installers/effects.js').createEffects>} [fx]
29
- * Coletor de efeitos; o default executa e registra numa lista descartada.
30
- * A DECISÃO (criar / migrar / anexar / nada) sai de uma LEITURA, que
31
- * acontece igual nos dois modos — por isso previsão e execução concordam.
8
+ * Leaves the user's root CLAUDE.md in place and only adds the morph-spec @import.
32
9
  * @returns {Promise<'created'|'migrated'|'injected'|'already-present'>}
33
10
  */
34
11
  export async function injectMorphImport(claudeMdPath, fx = realEffects()) {
@@ -48,20 +25,14 @@ export async function injectMorphImport(claudeMdPath, fx = realEffects()) {
48
25
  return 'migrated';
49
26
  }
50
27
 
51
- // User has their own content — append the import section
52
28
  const separator = content.endsWith('\n') ? '\n' : '\n\n';
53
29
  fx.writeFile(claudeMdPath, `${content}${separator}## MORPH-SPEC\n\n${MORPH_IMPORT}\n`);
54
30
  return 'injected';
55
31
  }
56
32
 
57
- /**
58
- * Checks if CLAUDE.md contains ONLY morph-spec content (old overwrite pattern).
59
- * Heuristic: contains MORPH-SPEC marker AND no substantial user content.
60
- */
61
33
  function isMorphOnly(content) {
62
34
  if (!content.includes(MORPH_MARKER)) return false;
63
35
 
64
- // Strip morph-spec boilerplate markers and check if anything meaningful remains
65
36
  const stripped = content
66
37
  .replace(/# MORPH-SPEC Runtime Instructions/g, '')
67
38
  .replace(/by Polymorphism Tech[^\n]*/g, '')
@@ -80,13 +51,6 @@ function isMorphOnly(content) {
80
51
  return stripped.length < 50;
81
52
  }
82
53
 
83
- /**
84
- * Checks if the root CLAUDE.md has the morph-spec import.
85
- * Used by doctor.js for health checks.
86
- *
87
- * @param {string} claudeMdPath - Absolute path to root CLAUDE.md
88
- * @returns {Promise<'ok'|'missing'|'missing-import'>}
89
- */
90
54
  export async function checkClaudeMdImport(claudeMdPath) {
91
55
  if (!await pathExists(claudeMdPath)) return 'missing';
92
56
  const content = await readFile(claudeMdPath);
@@ -1,70 +1,12 @@
1
- /**
2
- * Claude Settings Manager
3
- *
4
- * Extends the original hooks-installer with broader settings management:
5
- * permissions, env vars, and managed entry tracking.
6
- *
7
- * Re-exports installClaudeHooks for backward compatibility.
8
- */
9
-
10
1
  import { join } from 'path';
11
2
  import { readFile, writeFile, mkdir, copyFile } from 'fs/promises';
12
3
  import { existsSync } from 'fs';
13
4
 
14
5
  const MORPH_MANAGED_KEY = '_morph_managed';
15
6
 
16
- // ============================================================================
17
- // Re-export from hooks-installer (v2 registry-based)
18
- // ============================================================================
19
-
20
7
  export { installClaudeHooks, installGlobalStatusline, getHooksVersion } from './hooks-installer.js';
21
8
  export { installAgents } from './agents-installer.js';
22
9
 
23
- // ============================================================================
24
- // Extended settings management
25
- // ============================================================================
26
-
27
- /**
28
- * Install permission rules into .claude/settings.local.json
29
- * @param {string} targetPath - Project root directory
30
- * @param {Array<string>} permissions - Permission patterns to add
31
- * @returns {Promise<Object>} Summary of changes
32
- */
33
- export async function installPermissions(targetPath, permissions) {
34
- const settings = await loadSettings(targetPath);
35
-
36
- settings.permissions = settings.permissions || [];
37
-
38
- const added = [];
39
- for (const perm of permissions) {
40
- if (!settings.permissions.includes(perm)) {
41
- settings.permissions.push(perm);
42
- added.push(perm);
43
- }
44
- }
45
-
46
- // Track in _morph_managed
47
- settings[MORPH_MANAGED_KEY] = settings[MORPH_MANAGED_KEY] || {};
48
- settings[MORPH_MANAGED_KEY].permissions_added =
49
- settings[MORPH_MANAGED_KEY].permissions_added || [];
50
- for (const perm of added) {
51
- if (!settings[MORPH_MANAGED_KEY].permissions_added.includes(perm)) {
52
- settings[MORPH_MANAGED_KEY].permissions_added.push(perm);
53
- }
54
- }
55
- settings[MORPH_MANAGED_KEY].generated_at = new Date().toISOString();
56
-
57
- await saveSettings(targetPath, settings);
58
-
59
- return { added, total: settings.permissions.length };
60
- }
61
-
62
- /**
63
- * Install environment variables into .claude/settings.local.json
64
- * @param {string} targetPath - Project root directory
65
- * @param {Object} envVars - Key-value pairs to add to env
66
- * @returns {Promise<Object>} Summary of changes
67
- */
68
10
  export async function installEnvVars(targetPath, envVars) {
69
11
  const settings = await loadSettings(targetPath);
70
12
 
@@ -78,7 +20,6 @@ export async function installEnvVars(targetPath, envVars) {
78
20
  }
79
21
  }
80
22
 
81
- // Track in _morph_managed
82
23
  settings[MORPH_MANAGED_KEY] = settings[MORPH_MANAGED_KEY] || {};
83
24
  settings[MORPH_MANAGED_KEY].env_added =
84
25
  settings[MORPH_MANAGED_KEY].env_added || [];
@@ -94,12 +35,6 @@ export async function installEnvVars(targetPath, envVars) {
94
35
  return { added, total: Object.keys(settings.env).length };
95
36
  }
96
37
 
97
- /**
98
- * Remove only morph-spec managed entries from settings.
99
- * Creates backup before modifying.
100
- * @param {string} targetPath - Project root directory
101
- * @returns {Promise<Object>} Summary of what was removed
102
- */
103
38
  export async function resetMorphSettings(targetPath) {
104
39
  const claudeDir = join(targetPath, '.claude');
105
40
  const settingsPath = join(claudeDir, 'settings.local.json');
@@ -115,11 +50,9 @@ export async function resetMorphSettings(targetPath) {
115
50
  return { removed: false, reason: 'no _morph_managed section found' };
116
51
  }
117
52
 
118
- // Backup before modifying
119
53
  const backupPath = settingsPath + '.backup';
120
54
  await copyFile(settingsPath, backupPath);
121
55
 
122
- // Remove managed permissions
123
56
  if (managed.permissions_added && Array.isArray(settings.permissions)) {
124
57
  settings.permissions = settings.permissions.filter(
125
58
  p => !managed.permissions_added.includes(p)
@@ -129,7 +62,6 @@ export async function resetMorphSettings(targetPath) {
129
62
  }
130
63
  }
131
64
 
132
- // Remove managed env vars
133
65
  if (managed.env_added && settings.env) {
134
66
  for (const key of managed.env_added) {
135
67
  delete settings.env[key];
@@ -139,7 +71,6 @@ export async function resetMorphSettings(targetPath) {
139
71
  }
140
72
  }
141
73
 
142
- // Remove managed hooks (v2 format: identified by path pattern)
143
74
  if (settings.hooks) {
144
75
  const morphPattern = /framework\/hooks\/claude-code\//;
145
76
  const oldPattern = /agent-teams\/dispatch\.js/;
@@ -165,7 +96,6 @@ export async function resetMorphSettings(targetPath) {
165
96
  }
166
97
  }
167
98
 
168
- // Remove managed MCP servers from .mcp.json (primary location)
169
99
  if (managed.mcps_installed) {
170
100
  const mcpJson = await loadMcpJson(targetPath);
171
101
  if (mcpJson.mcpServers) {
@@ -175,7 +105,6 @@ export async function resetMorphSettings(targetPath) {
175
105
  if (Object.keys(mcpJson.mcpServers).length === 0) {
176
106
  delete mcpJson.mcpServers;
177
107
  }
178
- // Write back (or remove file if empty)
179
108
  if (Object.keys(mcpJson).length === 0) {
180
109
  const mcpJsonPath = join(targetPath, '.mcp.json');
181
110
  if (existsSync(mcpJsonPath)) {
@@ -187,7 +116,6 @@ export async function resetMorphSettings(targetPath) {
187
116
  }
188
117
  }
189
118
 
190
- // Also clean legacy entries from settings.local.json
191
119
  if (settings.mcpServers) {
192
120
  for (const mcpName of managed.mcps_installed) {
193
121
  delete settings.mcpServers[mcpName];
@@ -198,7 +126,6 @@ export async function resetMorphSettings(targetPath) {
198
126
  }
199
127
  }
200
128
 
201
- // Remove the managed marker
202
129
  delete settings[MORPH_MANAGED_KEY];
203
130
 
204
131
  await saveSettings(targetPath, settings);
@@ -214,15 +141,7 @@ export async function resetMorphSettings(targetPath) {
214
141
  };
215
142
  }
216
143
 
217
- /**
218
- * Install MCP server entries into .mcp.json at the project root.
219
- * Claude Code reads MCP servers from .mcp.json (not settings.local.json).
220
- * Tracks installed names in settings.local.json _morph_managed.mcps_installed for safe reset.
221
- *
222
- * @param {string} targetPath - Project root directory
223
- * @param {Object} servers - Map of server name → config { type?, command, args, env? }
224
- * @returns {Promise<Object>} Summary of changes { added, skipped, total }
225
- */
144
+ // Claude Code reads MCP servers from .mcp.json, not settings.local.json.
226
145
  export async function installMcpServers(targetPath, servers) {
227
146
  const mcpJson = await loadMcpJson(targetPath);
228
147
 
@@ -242,7 +161,6 @@ export async function installMcpServers(targetPath, servers) {
242
161
 
243
162
  await saveMcpJson(targetPath, mcpJson);
244
163
 
245
- // Track in settings.local.json _morph_managed
246
164
  const settings = await loadSettings(targetPath);
247
165
  settings[MORPH_MANAGED_KEY] = settings[MORPH_MANAGED_KEY] || {};
248
166
  settings[MORPH_MANAGED_KEY].mcps_installed =
@@ -259,10 +177,6 @@ export async function installMcpServers(targetPath, servers) {
259
177
  return { added, skipped, total: Object.keys(mcpJson.mcpServers).length };
260
178
  }
261
179
 
262
- // ============================================================================
263
- // Helpers
264
- // ============================================================================
265
-
266
180
  async function loadMcpJson(targetPath) {
267
181
  const mcpJsonPath = join(targetPath, '.mcp.json');
268
182
  try {
@@ -39,10 +39,7 @@ export async function pathExists(path) {
39
39
  }
40
40
 
41
41
  export async function readJson(path) {
42
- // BOM-tolerant: PowerShell 5.1's `Out-File -Encoding utf8` writes UTF-8
43
- // WITH BOM, and JSON.parse rejects it. A hand-edited config.json on Windows
44
- // would then silently fall back to empty project tags (fail-open reads),
45
- // installing only universal personas. Strip the BOM before parsing.
42
+ // PowerShell 5.1's `Out-File -Encoding utf8` writes a BOM that JSON.parse rejects.
46
43
  const raw = await fs.readFile(path, 'utf-8');
47
44
  return JSON.parse(raw.replace(/^/, ''));
48
45
  }
@@ -69,18 +66,7 @@ export async function readFile(path) {
69
66
  return fs.readFile(path, 'utf8');
70
67
  }
71
68
 
72
- /**
73
- * Create a link to a file or directory with platform-aware fallbacks.
74
- *
75
- * For files: hard link → symlink → copy
76
- * Hard links work on Windows without admin (same NTFS volume).
77
- * For dirs: delegates to createDirectoryLink (junction on Windows).
78
- *
79
- * @param {string} target - Path to the original file/folder
80
- * @param {string} link - Path where the link should be created
81
- * @param {string} type - 'file' or 'dir'
82
- * @returns {Promise<'hardlink' | 'symlink' | 'junction' | 'copy'>}
83
- */
69
+ // Hard link first: it needs no admin on Windows (same NTFS volume).
84
70
  export async function createSymlink(target, link, type = 'file') {
85
71
  await fs.ensureDir(dirname(link));
86
72
 
@@ -88,7 +74,6 @@ export async function createSymlink(target, link, type = 'file') {
88
74
  return createDirectoryLink(target, link);
89
75
  }
90
76
 
91
- // File linking: hard link first (no admin on Windows), then symlink, then copy
92
77
  try {
93
78
  await fs.link(target, link);
94
79
  return 'hardlink';
@@ -103,24 +88,15 @@ export async function createSymlink(target, link, type = 'file') {
103
88
  }
104
89
  }
105
90
 
106
- /**
107
- * Create a directory link (junction on Windows, symlink on other platforms)
108
- * with fallback to directory copy if linking fails.
109
- * @param {string} target - Path to the original directory
110
- * @param {string} link - Path where the link should be created
111
- * @returns {Promise<'junction' | 'symlink' | 'copy'>} - How the link was created
112
- */
113
91
  export async function createDirectoryLink(target, link) {
114
92
  await fs.ensureDir(dirname(link));
115
93
 
116
- // Remove existing link/directory before recreating
117
94
  if (await pathExists(link)) {
118
95
  await fs.remove(link);
119
96
  }
120
97
 
121
98
  if (process.platform === 'win32') {
122
- // Junction on Windows — does NOT require admin privileges.
123
- // Skip ensureSymlink('dir') fallback: it also needs admin on Windows.
99
+ // A junction needs no admin; ensureSymlink('dir') would.
124
100
  try {
125
101
  await fs.symlink(target, link, 'junction');
126
102
  return 'junction';
@@ -139,16 +115,8 @@ export async function createDirectoryLink(target, link) {
139
115
  }
140
116
  }
141
117
 
142
- /**
143
- * The morph-owned block of the installed .gitignore. Exported so the worktree
144
- * container canary (test/lib/worktree/dirs.test.js) can prove both names are
145
- * covered without rendering a file.
146
- */
147
118
  export const MORPH_GITIGNORE_ENTRIES = [
148
- // Derivable, per-machine state index — rebuilt from the committed
149
- // .morph/features/{feature}/feature.json files. Never commit it: doing so
150
- // sweeps every feature's accumulated diffs into one commit. The feature
151
- // state that matters travels inside each feature folder (which IS committed).
119
+ // Rebuilt from the committed feature.json files; committing it sweeps every feature into one commit.
152
120
  '.morph/state.json',
153
121
  '.morph/state.json.tmp.*',
154
122
  '.morph/state.json.lock/',
@@ -156,10 +124,6 @@ export const MORPH_GITIGNORE_ENTRIES = [
156
124
  '.morph/memory/',
157
125
  '.morph/plans/',
158
126
  '.morph/analytics/',
159
- '.morph/traces/',
160
- // Per-instance activity/session logs (activity-logger.js). Each worktree
161
- // writes its own — committing this guarantees a merge conflict between
162
- // feature branches.
163
127
  '.morph/logs/',
164
128
  '.claude/commands/',
165
129
  '.claude/rules/',
@@ -167,25 +131,13 @@ export const MORPH_GITIGNORE_ENTRIES = [
167
131
  '.claude/agents/',
168
132
  '.claude/CLAUDE.md',
169
133
  '.claude/settings.local.json',
170
- // Both container names, anchored at the root. The legacy name (pre-C1) is
171
- // kept so a project that already has `.worktrees/` on disk does not
172
- // suddenly see it as untracked dirt — which would also make `finish`'s
173
- // clean-root check refuse.
134
+ // The legacy name stays so an existing `.worktrees/` is not dirt that makes `finish` refuse.
174
135
  ...WORKTREE_DIR_NAMES.map((d) => `/${d}/`),
175
136
  '.playwright-mcp/',
176
137
  'docs/superpowers/',
177
- // Grafo de codebase — artefato derivado, reconstruído pelo hook do
178
- // graphify a cada commit. Committá-lo faria de todo commit um diff de
179
- // megabytes de JSON gerado.
180
138
  'graphify-out/',
181
139
  ];
182
140
 
183
- /**
184
- * @param {string} projectPath
185
- * @param {ReturnType<import('../lib/installers/effects.js').createEffects>} [fx]
186
- * Coletor de efeitos. O default executa e registra numa lista descartada, de
187
- * modo que todo chamador antigo (`setup-infra`, `init`) siga idêntico.
188
- */
189
141
  export async function updateGitignore(projectPath, fx = realEffects()) {
190
142
  const gitignorePath = join(projectPath, '.gitignore');
191
143
 
@@ -194,7 +146,6 @@ export async function updateGitignore(projectPath, fx = realEffects()) {
194
146
  let content = '';
195
147
  let hasMorphSection = false;
196
148
 
197
- // Read existing .gitignore if it exists
198
149
  if (await pathExists(gitignorePath)) {
199
150
  content = await readFile(gitignorePath);
200
151
  hasMorphSection = content.includes('# MORPH-SPEC');
@@ -202,12 +153,10 @@ export async function updateGitignore(projectPath, fx = realEffects()) {
202
153
  const original = content;
203
154
  const eol = content.includes('\r\n') ? '\r\n' : '\n';
204
155
 
205
- // If MORPH section already exists, check if rules are up to date
206
156
  if (hasMorphSection) {
207
157
  const lines = content.split(/\r?\n/);
208
158
  const morphStartIndex = lines.findIndex(line => line.trim() === '# MORPH-SPEC');
209
159
 
210
- // Find the end of MORPH section (next empty line or section header)
211
160
  let morphEndIndex = morphStartIndex + 1;
212
161
  while (morphEndIndex < lines.length &&
213
162
  lines[morphEndIndex].trim() !== '' &&
@@ -215,22 +164,18 @@ export async function updateGitignore(projectPath, fx = realEffects()) {
215
164
  morphEndIndex++;
216
165
  }
217
166
 
218
- // Extract current MORPH rules
219
167
  const currentMorphRules = lines.slice(morphStartIndex, morphEndIndex + 1);
220
- const expectedMorphRules = morphRules.slice(1, -1); // Remove empty lines from comparison
168
+ const expectedMorphRules = morphRules.slice(1, -1);
221
169
 
222
- // Check if all expected rules are present
223
170
  const missingRules = expectedMorphRules.filter(rule =>
224
171
  rule.startsWith('#') || !currentMorphRules.some(line => line.trim() === rule)
225
172
  );
226
173
 
227
174
  if (missingRules.length > 0) {
228
- // Update the section with all rules
229
175
  lines.splice(morphStartIndex, morphEndIndex - morphStartIndex, ...morphRules.slice(1, -1));
230
176
  content = lines.join(eol);
231
177
  }
232
178
  } else {
233
- // Add MORPH section
234
179
  if (content && !content.endsWith('\n')) {
235
180
  content += eol;
236
181
  }
@@ -240,23 +185,30 @@ export async function updateGitignore(projectPath, fx = realEffects()) {
240
185
  if (content !== original) fx.writeFile(gitignorePath, content);
241
186
  }
242
187
 
243
- /**
244
- * Install a MORPH-SPEC .gitattributes section pinning morph state files to LF.
245
- *
246
- * Rationale: `saveState`/`atomicWriteJson` write `.morph` JSON with LF. On
247
- * Windows with `core.autocrlf=true` (the Git-for-Windows default) and no
248
- * attribute pinning EOL, Git flags EVERY rewritten feature.json as modified
249
- * even when the content is byte-identical. That silently breaks the whole-root
250
- * clean-check in `morph-spec finish` as soon as a project has more
251
- * than one feature — precisely the multi-instance / parallel-worktree flow,
252
- * where one `saveState` touches sibling feature.json files and marks them dirty.
253
- * This mirrors the .gitattributes this framework repo already carries for its
254
- * own generated files. Scoped to `.morph/**` so it never imposes an EOL policy
255
- * on the user's own source. Idempotent and non-destructive: appends a marked
256
- * block, leaving any pre-existing .gitattributes rules untouched.
257
- *
258
- * @param {string} projectPath
259
- */
188
+ const DOTNET_BUILD_DIRS = ['bin', 'obj'];
189
+
190
+ const ignoredDir = (line) => line.trim()
191
+ .replace(/^\/|^\*\*\//, '')
192
+ .replace(/\/(\*\*)?$/, '')
193
+ .replace(/^\[(\w)(\w)\]/, (_, a, b) => (a.toLowerCase() === b.toLowerCase() ? a : `[${a}${b}]`))
194
+ .toLowerCase();
195
+
196
+ /** Never rewrites an existing line. @returns {string[]} the entries added */
197
+ export function ensureDotnetBuildIgnores(projectPath) {
198
+ const gitignorePath = join(projectPath, '.gitignore');
199
+ const content = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, 'utf8') : '';
200
+ const present = new Set(content.split(/\r?\n/).filter((l) => !l.trim().startsWith('#')).map(ignoredDir));
201
+ const missing = DOTNET_BUILD_DIRS.filter((dir) => !present.has(dir)).map((dir) => `${dir}/`);
202
+ if (!missing.length) return [];
203
+ const eol = content.includes('\r\n') ? '\r\n' : '\n';
204
+ const lead = content && !content.endsWith('\n') ? eol : '';
205
+ const block = [...(content ? [''] : []), '# .NET build output', ...missing, ''].join(eol);
206
+ fs.writeFileSync(gitignorePath, content + lead + block);
207
+ return missing;
208
+ }
209
+
210
+ // Without the LF pin, Windows autocrlf marks every rewritten feature.json dirty
211
+ // and breaks the clean-root check of `finish`. Scoped to `.morph/**` only.
260
212
  export async function ensureGitattributes(projectPath, fx = realEffects()) {
261
213
  const gitattributesPath = join(projectPath, '.gitattributes');
262
214
  const MARKER = '# MORPH-SPEC';
@@ -271,7 +223,7 @@ export async function ensureGitattributes(projectPath, fx = realEffects()) {
271
223
  let content = '';
272
224
  if (await pathExists(gitattributesPath)) {
273
225
  content = await readFile(gitattributesPath);
274
- if (content.includes(MARKER)) return; // already installed — idempotent
226
+ if (content.includes(MARKER)) return;
275
227
  if (content && !content.endsWith('\n')) content += '\n';
276
228
  content += '\n';
277
229
  }
@@ -279,17 +231,7 @@ export async function ensureGitattributes(projectPath, fx = realEffects()) {
279
231
  fx.writeFile(gitattributesPath, content);
280
232
  }
281
233
 
282
- /**
283
- * Garante entradas no .claudeignore, criando o arquivo se preciso.
284
- *
285
- * Existe por causa do grafo: um `graph.json` de 12 MB reescrito a cada commit
286
- * invalidaria o prompt cache do Claude Code em toda sessão. Append idempotente
287
- * por linha — o que o usuário já tem nunca é reescrito.
288
- *
289
- * @param {string} projectPath
290
- * @param {string[]} entries
291
- * @returns {Promise<{added: string[], path: string}>}
292
- */
234
+ /** Idempotent per-line append; never rewrites what the user already has. */
293
235
  export async function ensureClaudeignore(projectPath, entries) {
294
236
  const claudeignorePath = join(projectPath, '.claudeignore');
295
237
  const MARKER = '# MORPH-SPEC';