@polymorphism-tech/morph-spec 9.0.0 → 9.2.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 (345) hide show
  1. package/CLAUDE.md +8 -9
  2. package/README.md +38 -36
  3. package/bin/morph-spec.js +11 -63
  4. package/bin/validate.js +1 -39
  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 +2 -1
  9. package/framework/MORPH.md +3 -3
  10. package/framework/agents.json +19 -11
  11. package/framework/commands/morph-apply.md +1 -1
  12. package/framework/commands/morph-status.md +1 -1
  13. package/framework/docs/CLI.md +22 -31
  14. package/framework/evals/review-checklist.md +12 -2
  15. package/framework/hooks/README.md +8 -44
  16. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  17. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  18. package/framework/hooks/claude-code/core/chain.js +4 -65
  19. package/framework/hooks/claude-code/core/comment-guard.js +2 -16
  20. package/framework/hooks/claude-code/core/enforce-phase-writes.js +6 -50
  21. package/framework/hooks/claude-code/core/gate-guard.js +7 -67
  22. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  23. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  24. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  25. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  26. package/framework/hooks/claude-code/core/pre-bash-denylist.js +0 -4
  27. package/framework/hooks/claude-code/core/protect-spec-files.js +7 -67
  28. package/framework/hooks/claude-code/core/state-sync.js +12 -68
  29. package/framework/hooks/claude-code/core/telemetry-log.js +12 -93
  30. package/framework/hooks/claude-code/core/validate-completion.js +11 -57
  31. package/framework/hooks/claude-code/core/warn-bash-source-write.js +1 -9
  32. package/framework/hooks/claude-code/core/worktree-context.js +10 -110
  33. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  34. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  35. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  36. package/framework/hooks/claude-code/statusline.sh +11 -11
  37. package/framework/hooks/shared/activity-logger.js +9 -89
  38. package/framework/hooks/shared/agent-reap.js +2 -17
  39. package/framework/hooks/shared/build-check.js +6 -75
  40. package/framework/hooks/shared/claim-registry.js +9 -119
  41. package/framework/hooks/shared/command-tokens.js +6 -51
  42. package/framework/hooks/shared/comment-rules.js +5 -20
  43. package/framework/hooks/shared/comment-scanner.js +6 -25
  44. package/framework/hooks/shared/comment-stripper.js +11 -85
  45. package/framework/hooks/shared/e2e-check.js +0 -0
  46. package/framework/hooks/shared/errata.js +2 -15
  47. package/framework/hooks/shared/feature-locator.js +11 -95
  48. package/framework/hooks/shared/frontend-locations.js +1 -3
  49. package/framework/hooks/shared/gate-question.js +1 -7
  50. package/framework/hooks/shared/git-exclude.js +3 -20
  51. package/framework/hooks/shared/git-paths.js +8 -104
  52. package/framework/hooks/shared/graph-freshness.js +22 -166
  53. package/framework/hooks/shared/graph-refresh.js +21 -149
  54. package/framework/hooks/shared/heavy-command.js +4 -12
  55. package/framework/hooks/shared/hook-response.js +8 -83
  56. package/framework/hooks/shared/morph-cli.js +8 -52
  57. package/framework/hooks/shared/payload-utils.js +10 -132
  58. package/framework/hooks/shared/phase-utils.js +1 -42
  59. package/framework/hooks/shared/redact-secrets.js +4 -15
  60. package/framework/hooks/shared/reparse-scan.js +6 -66
  61. package/framework/hooks/shared/slice-progress.js +4 -23
  62. package/framework/hooks/shared/source-files.js +4 -17
  63. package/framework/hooks/shared/state-reader.js +13 -183
  64. package/framework/hooks/shared/state-writer.js +8 -52
  65. package/framework/hooks/shared/stdin-reader.js +1 -11
  66. package/framework/hooks/shared/telemetry-logger.js +7 -67
  67. package/framework/hooks/shared/transcript-usage.js +7 -29
  68. package/framework/hooks/shared/worktree-root.js +7 -29
  69. package/framework/rules/csharp-standards.md +1 -1
  70. package/framework/rules/frontend-standards.md +1 -0
  71. package/framework/rules/hangfire-standards.md +1 -1
  72. package/framework/rules/neon-standards.md +1 -1
  73. package/framework/rules/nextjs-standards.md +1 -1
  74. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  75. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  76. package/framework/skills/core/morph-apply/SKILL.md +4 -4
  77. package/framework/skills/core/morph-hotfix/SKILL.md +3 -3
  78. package/framework/skills/core/morph-machine/SKILL.md +1 -1
  79. package/framework/skills/core/morph-media/SKILL.md +1 -1
  80. package/framework/skills/core/morph-plan/SKILL.md +9 -7
  81. package/framework/skills/core/morph-review/SKILL.md +9 -6
  82. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  83. package/framework/skills/core/morph-uiux/SKILL.md +29 -12
  84. package/framework/standards/STANDARDS.json +36 -14
  85. package/framework/standards/STANDARDS.meta.json +16 -2
  86. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  87. package/framework/standards/ai-agents/agent-session.md +1 -1
  88. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  89. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  90. package/framework/standards/ai-agents/guardrails.md +1 -1
  91. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  92. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  93. package/framework/standards/ai-agents/production.md +1 -1
  94. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  95. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  96. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  97. package/framework/standards/ai-agents/setup.md +2 -2
  98. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  99. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  100. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  101. package/framework/standards/ai-agents/workflows.md +1 -1
  102. package/framework/standards/architecture/code-comments.md +1 -1
  103. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  104. package/framework/standards/backend/api/validation.md +1 -1
  105. package/framework/standards/backend/dotnet/testing.md +10 -4
  106. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  107. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  108. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  109. package/framework/standards/data/nosql/cache/redis.md +1 -1
  110. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  111. package/framework/standards/frontend/admin-app/forms.md +1 -1
  112. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  113. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  114. package/framework/standards/frontend/nextjs/testing.md +10 -6
  115. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  116. package/framework/templates/.idea/morph-templates.xml +92 -0
  117. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  118. package/framework/templates/CLAUDE.md.core.template +3 -3
  119. package/framework/templates/IDE-SNIPPETS.md +262 -262
  120. package/framework/templates/REGISTRY.json +9 -9
  121. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  122. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  123. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  124. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  125. package/framework/templates/docs/plan.md +3 -1
  126. package/framework/templates/docs/ui-mockups.md +4 -0
  127. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  128. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  129. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  130. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  131. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  132. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  133. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  134. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  135. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  136. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  137. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  138. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  139. package/framework/templates/state.template.json +17 -17
  140. package/framework/templates/ui/design-system.css +226 -226
  141. package/package.json +1 -1
  142. package/src/commands/context/context-refresh.js +0 -16
  143. package/src/commands/dashboard/dashboard.js +2 -8
  144. package/src/commands/e2e/e2e.js +17 -104
  145. package/src/commands/graph/affected.js +0 -14
  146. package/src/commands/graph/build.js +11 -50
  147. package/src/commands/graph/explain.js +0 -16
  148. package/src/commands/graph/status.js +1 -12
  149. package/src/commands/mcp/mcp-setup.js +4 -62
  150. package/src/commands/observability/cost.js +5 -57
  151. package/src/commands/observability/telemetry.js +1 -14
  152. package/src/commands/project/branch.js +4 -45
  153. package/src/commands/project/claim.js +4 -48
  154. package/src/commands/project/doctor.js +48 -302
  155. package/src/commands/project/env.js +1 -27
  156. package/src/commands/project/fleet.js +3 -32
  157. package/src/commands/project/init.js +4 -68
  158. package/src/commands/project/install-plugin-cmd.js +0 -1
  159. package/src/commands/project/standards.js +1 -62
  160. package/src/commands/project/status.js +0 -53
  161. package/src/commands/project/update.js +31 -239
  162. package/src/commands/project/worktree.js +108 -882
  163. package/src/commands/scaffold/scaffold.js +0 -25
  164. package/src/commands/state/advance.js +14 -85
  165. package/src/commands/state/approve.js +15 -97
  166. package/src/commands/state/archive.js +12 -64
  167. package/src/commands/state/auto-approval.js +2 -5
  168. package/src/commands/state/create.js +22 -109
  169. package/src/commands/state/delete.js +11 -63
  170. package/src/commands/state/finish.js +98 -475
  171. package/src/commands/state/gate-check.js +22 -132
  172. package/src/commands/state/gate-decision.js +3 -39
  173. package/src/commands/state/handoff.js +3 -30
  174. package/src/commands/state/list.js +14 -84
  175. package/src/commands/state/report-check.js +8 -58
  176. package/src/commands/state/retype.js +4 -26
  177. package/src/commands/validation/index.js +0 -3
  178. package/src/commands/validation/mutate.js +11 -11
  179. package/src/commands/validation/plan-check.js +4 -2
  180. package/src/commands/validation/tests-audit.js +7 -0
  181. package/src/commands/validation/validate-feature.js +10 -97
  182. package/src/commands/validation/verify.js +30 -113
  183. package/src/core/paths/output-schema.js +2 -101
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +0 -27
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +22 -304
  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 +0 -1
  191. package/src/core/work-types.js +1 -36
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +0 -5
  194. package/src/dashboard/app/components/CommandPalette.jsx +0 -7
  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 +1 -5
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/main.jsx +13 -13
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +1 -5
  211. package/src/dashboard/app/pages/Features.jsx +1 -8
  212. package/src/dashboard/app/pages/Guide.jsx +0 -1
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +0 -3
  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 +10 -69
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +0 -6
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +10 -66
  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 +2 -6
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/dashboard/vite.config.js +24 -24
  233. package/src/lib/ai/ai-checks.js +7 -52
  234. package/src/lib/ai/ai-project-scan.js +14 -108
  235. package/src/lib/ai/code-audit.js +7 -63
  236. package/src/lib/ai/pin-drift.js +8 -51
  237. package/src/lib/ai/registry-audit.js +11 -90
  238. package/src/lib/ci/ci-verify.js +4 -19
  239. package/src/lib/ci/github-ci.js +9 -37
  240. package/src/lib/ci/workflow-scaffold.js +14 -10
  241. package/src/lib/detectors/claude-config-detector.js +13 -74
  242. package/src/lib/detectors/index.js +4 -50
  243. package/src/lib/e2e/applicability.js +37 -0
  244. package/src/lib/e2e/compose-env.js +5 -66
  245. package/src/lib/e2e/stack-teardown.js +6 -40
  246. package/src/lib/generators/architecture-scanner.js +18 -156
  247. package/src/lib/generators/handoff-generator.js +9 -53
  248. package/src/lib/generators/scaffold-generator.js +5 -101
  249. package/src/lib/generators/settings-generator.js +4 -37
  250. package/src/lib/git/branch-classifier.js +11 -84
  251. package/src/lib/git/default-branch.js +14 -169
  252. package/src/lib/git/evidence-staleness.js +1 -0
  253. package/src/lib/git/gh.js +2 -17
  254. package/src/lib/git/pending-prs.js +6 -37
  255. package/src/lib/git/pr-files.js +3 -28
  256. package/src/lib/git/pr-index.js +3 -34
  257. package/src/lib/git/tree-fingerprint.js +3 -15
  258. package/src/lib/git/tree-state.js +6 -33
  259. package/src/lib/graph/god-nodes.js +2 -22
  260. package/src/lib/graph/graph-health.js +3 -24
  261. package/src/lib/graph/graph-loader.js +5 -56
  262. package/src/lib/graph/graphify-runtime.js +7 -37
  263. package/src/lib/graph/graphifyignore.js +8 -74
  264. package/src/lib/graph/sanitize.js +16 -136
  265. package/src/lib/heavy/cleanup.js +1 -1
  266. package/src/lib/installers/effects.js +8 -93
  267. package/src/lib/installers/mcp-installer.js +9 -169
  268. package/src/lib/installers/memory-installer.js +4 -27
  269. package/src/lib/installers/templates-reconcile.js +5 -60
  270. package/src/lib/mutation/adapters.js +6 -17
  271. package/src/lib/mutation/dotnet-introspection.js +20 -106
  272. package/src/lib/mutation/runner.js +23 -75
  273. package/src/lib/mutation/subset.js +1 -4
  274. package/src/lib/mutation/trx.js +0 -1
  275. package/src/lib/plan/artifact-vocabulary.js +5 -25
  276. package/src/lib/plan/checks.js +34 -33
  277. package/src/lib/plan/plan-parser.js +7 -8
  278. package/src/lib/projects/registry.js +7 -44
  279. package/src/lib/review-reports.js +11 -26
  280. package/src/lib/risk-detector.js +41 -173
  281. package/src/lib/specs/adr-index.js +6 -34
  282. package/src/lib/specs/frontmatter.js +2 -15
  283. package/src/lib/specs/gfm-table.js +1 -7
  284. package/src/lib/specs/revocations.js +1 -14
  285. package/src/lib/specs/screen-checklist.js +47 -124
  286. package/src/lib/stack/dotnet-build-ignores.js +2 -6
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +1 -8
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +1 -7
  291. package/src/lib/standards/sections.js +2 -13
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +21 -169
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -240
  296. package/src/lib/tasks/test-runner.js +43 -341
  297. package/src/lib/telemetry/metrics.js +7 -36
  298. package/src/lib/telemetry/telemetry-reader.js +13 -125
  299. package/src/lib/telemetry/verify-event.js +2 -30
  300. package/src/lib/tests-audit/audit.js +96 -0
  301. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  302. package/src/lib/validators/code-budget/index.js +0 -7
  303. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -24
  304. package/src/lib/validators/contracts/index.js +0 -6
  305. package/src/lib/validators/nextjs/index.js +0 -5
  306. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  307. package/src/lib/validators/packages/index.js +0 -6
  308. package/src/lib/validators/packages/package-validator.js +6 -56
  309. package/src/lib/validators/reuse/index.js +0 -8
  310. package/src/lib/validators/reuse/reuse-validator.js +11 -101
  311. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  312. package/src/lib/validators/traceability-check.js +5 -63
  313. package/src/lib/validators/validation-runner.js +21 -158
  314. package/src/lib/validators/verify-runner.js +85 -376
  315. package/src/lib/work-type-detector.js +5 -67
  316. package/src/lib/worktree/docker-audit.js +17 -136
  317. package/src/lib/worktree/env-allocator.js +10 -98
  318. package/src/lib/worktree/feature-truth.js +12 -115
  319. package/src/lib/worktree/finish-backup.js +32 -242
  320. package/src/lib/worktree/fleet-scanner.js +13 -95
  321. package/src/lib/worktree/ignored-triage.js +15 -91
  322. package/src/lib/worktree/vscode-settings.js +2 -3
  323. package/src/lib/worktree/worktree-audit.js +21 -213
  324. package/src/lib/worktree/worktree-include.js +3 -5
  325. package/src/scripts/global-install.js +3 -9
  326. package/src/scripts/install-plugin.js +1 -13
  327. package/src/scripts/setup-infra.js +17 -147
  328. package/src/utils/agents-installer.js +29 -276
  329. package/src/utils/banner.js +4 -25
  330. package/src/utils/claude-md-injector.js +1 -37
  331. package/src/utils/claude-settings-manager.js +1 -52
  332. package/src/utils/file-copier.js +11 -95
  333. package/src/utils/hooks-installer.js +21 -298
  334. package/src/utils/skills-installer.js +4 -101
  335. package/src/utils/version-checker.js +4 -83
  336. package/framework/commands/morph-proposal.md +0 -9
  337. package/framework/hooks/git/commit-msg/conventional-commits.sh +0 -33
  338. package/framework/hooks/git/pre-commit/agents.sh +0 -25
  339. package/framework/hooks/git/pre-commit/orchestrator.sh +0 -64
  340. package/framework/hooks/git/pre-push/run-tests.sh +0 -44
  341. package/src/commands/utils/index.js +0 -3
  342. package/src/lib/detectors/design-system-detector.js +0 -188
  343. package/src/lib/generators/design-system-generator.js +0 -508
  344. package/src/lib/specs/old-format.js +0 -68
  345. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,77 +1,24 @@
1
- /**
2
- * ignored-triage.js — decide, per gitignored leftover, whether removing a
3
- * worktree would DESTROY something or merely delete a copy.
4
- *
5
- * WHY THIS EXISTS: `git worktree remove` deletes gitignored files outright and
6
- * the clean-check that gates it (`git status --porcelain`) omits them by
7
- * design, so a user's `.env` dies silently while the command reports success.
8
- * The first fix for that refused the removal whenever ANY unrecognised
9
- * gitignored path was present — correct, but blind: in the observed session a
10
- * worktree carried `.env` and `.env.e2e` that were BYTE-IDENTICAL to the
11
- * primary root's, alongside a `keys/key-*.xml` that existed nowhere else. The
12
- * refusal treated them alike, and the only escape it offered
13
- * (`--discard-ignored`) destroyed the unique one together with the replicas.
14
- *
15
- * The triage below replaces "recognised vs not" with the question that actually
16
- * decides whether bytes are lost:
17
- *
18
- * REPLICABLE — the primary root has the same relative path with identical
19
- * content. Deleting the worktree's copy loses nothing; the
20
- * original stays where it always was.
21
- * DISPOSABLE — a well-known build artifact or cache (`bin/`, `obj/`,
22
- * `node_modules/`, `.next/`, `TestResults/`, `graphify-out/`…,
23
- * see DISPOSABLE_DIR_NAMES). Reproducible from source, which is a
24
- * STRONGER guarantee than backed up.
25
- * EXCLUSIVE — everything else: no counterpart in the root, or a counterpart
26
- * whose content differs. These are the only bytes at stake, and
27
- * the caller copies them to `finish-backup` before proceeding.
28
- *
29
- * Fail-CLOSED on every uncertainty: an unreadable file, a hash that cannot be
30
- * computed, a directory too large to compare, a counterpart that is a link
31
- * rather than a real file — all classify as EXCLUSIVE. Being wrong in that
32
- * direction costs a backup copy; being wrong the other way costs the file.
33
- */
1
+ // `git worktree remove` deletes gitignored files and `git status` never shows them.
2
+ // Every uncertainty classifies as EXCLUSIVE (backed up): being wrong that way costs a
3
+ // copy, the other way costs the file.
34
4
 
35
5
  import { createHash } from 'crypto';
36
6
  import { existsSync, lstatSync, readdirSync, readFileSync } from 'fs';
37
7
  import { join } from 'path';
38
8
 
39
- /**
40
- * Well-known, regenerable build artifacts and caches. Deliberately narrow — do
41
- * NOT add names a project might use for hand-written output (`dist/`,
42
- * `build/`, `coverage/`); anything unlisted still reaches the EXCLUSIVE bucket,
43
- * where it is backed up rather than assumed disposable. Each name here is a
44
- * directory its own tool rewrites from scratch: `.next/` (next build/dev),
45
- * `.turbo/`, `.pytest_cache/`, `TestResults/` (VSTest), `graphify-out/`
46
- * (`morph-spec graph build`). `.next/` is also the one that makes a backup
47
- * weigh hundreds of megabytes; `graphify-out/` is seeded into every worktree
48
- * and is past the replica size cap (18 MB in a real project), so without its
49
- * entry every `finish` would back it up.
50
- *
51
- * `.auth/` (Playwright storage state) belongs here for a reason worth stating:
52
- * it holds a captured session, so it LOOKS like precious local state — but it
53
- * is regenerated by the auth setup project on the next run, and it expires on
54
- * its own. Keeping it out of the backup avoids copying credentials around.
55
- */
9
+ // Only dirs their own tool rewrites from scratch. Never add `dist/`, `build/` or `coverage/`:
10
+ // projects hand-write those. `.auth/` (Playwright session) is regenerated by the auth setup,
11
+ // and keeping it out of the backup avoids copying credentials around.
56
12
  const DISPOSABLE_DIR_NAMES = new Set([
57
13
  'bin', 'obj', 'node_modules', '__pycache__', 'test-results', '.auth',
58
14
  '.next', '.turbo', '.pytest_cache', 'TestResults', 'graphify-out',
59
15
  ]);
60
16
  const DISPOSABLE_FILE_RE = /\.tsbuildinfo$/;
61
17
 
62
- /**
63
- * Comparing a directory recursively is only worth doing while it stays small:
64
- * the point is to spare `.env`-sized replicas from a needless refusal, not to
65
- * checksum a data set. Past either cap the answer becomes EXCLUSIVE (the safe
66
- * direction), and the caller backs it up.
67
- */
68
18
  const REPLICABLE_MAX_FILES = 500;
69
19
  const REPLICABLE_MAX_BYTES = 8 * 1024 * 1024;
70
20
 
71
- /**
72
- * @param {string} p - forward-slash relative path
73
- * @returns {boolean}
74
- */
21
+ /** @param {string} p - forward-slash relative path */
75
22
  export function isDisposableBuildArtifact(p) {
76
23
  if (DISPOSABLE_FILE_RE.test(p)) return true;
77
24
  return p.split('/').filter(Boolean).some((seg) => DISPOSABLE_DIR_NAMES.has(seg));
@@ -85,11 +32,7 @@ function sha256OfFile(path) {
85
32
  }
86
33
  }
87
34
 
88
- /**
89
- * Real file (not a link, not a directory)? A reparse point must never be read
90
- * THROUGH — following one is the exact failure mode the whole teardown path
91
- * exists to prevent — so a link on either side makes the pair incomparable.
92
- */
35
+ // lstat, never stat: reading THROUGH a junction is the failure the teardown path exists to prevent.
93
36
  function isRealFile(path) {
94
37
  try {
95
38
  return lstatSync(path).isFile();
@@ -106,11 +49,7 @@ function isRealDir(path) {
106
49
  }
107
50
  }
108
51
 
109
- /**
110
- * Every real file under `dir`, relative to it. Returns null when the walk hits
111
- * a link, an unreadable entry, or either cap — "cannot enumerate" and "too big
112
- * to compare" both mean "do not claim this is replicable".
113
- */
52
+ // null on a link, an unreadable entry or either cap: none of them may be claimed replicable.
114
53
  function walkFiles(dir, budget) {
115
54
  const out = [];
116
55
  const stack = [''];
@@ -125,9 +64,9 @@ function walkFiles(dir, budget) {
125
64
  for (const e of entries) {
126
65
  const childRel = rel ? `${rel}/${e.name}` : e.name;
127
66
  const childAbs = join(dir, childRel);
128
- if (e.isSymbolicLink()) return null; // never compare through a link
67
+ if (e.isSymbolicLink()) return null;
129
68
  if (e.isDirectory()) { stack.push(childRel); continue; }
130
- if (!e.isFile()) return null; // socket/fifo/unknown — incomparable
69
+ if (!e.isFile()) return null;
131
70
  let size;
132
71
  try { size = lstatSync(childAbs).size; } catch { return null; }
133
72
  budget.files += 1;
@@ -140,14 +79,8 @@ function walkFiles(dir, budget) {
140
79
  }
141
80
 
142
81
  /**
143
- * Does `root/rel` hold exactly the same content as `worktree/rel`?
144
- *
145
- * Files compare by SHA-256; directories compare by having the SAME set of
146
- * files, each with an identical hash — a root copy that is merely a SUPERSET
147
- * is not a replica of the worktree's, because the worktree's own extra files
148
- * would still be lost.
149
- *
150
- * @returns {boolean} false on ANY uncertainty
82
+ * False on any uncertainty. A directory needs the SAME file set: a root superset is
83
+ * not a replica, because the worktree's own extra files would still be lost.
151
84
  */
152
85
  export function isReplicatedInRoot(worktreePath, rootPath, rel) {
153
86
  const src = join(worktreePath, rel);
@@ -178,19 +111,10 @@ export function isReplicatedInRoot(worktreePath, rootPath, rel) {
178
111
  return true;
179
112
  }
180
113
 
181
- return false; // a link, or something that is neither — incomparable
114
+ return false;
182
115
  }
183
116
 
184
- /**
185
- * Split gitignored worktree paths into the three buckets above.
186
- *
187
- * ORDER IS DELIBERATE: disposable is checked FIRST, so a `bin/` that happens to
188
- * be byte-identical to the root's is discarded rather than hashed, and a
189
- * `node_modules` never enters the comparison budget at all.
190
- *
191
- * @param {{ worktreePath: string, rootPath: string, paths: string[] }} args
192
- * @returns {{ replicable: string[], disposable: string[], exclusive: string[] }}
193
- */
117
+ // Disposable is checked first so `node_modules` never enters the hashing budget.
194
118
  export function triageIgnoredPaths({ worktreePath, rootPath, paths }) {
195
119
  const replicable = [];
196
120
  const disposable = [];
@@ -63,9 +63,8 @@ function snippetFor(wanted, present) {
63
63
  }
64
64
 
65
65
  /**
66
- * Read-only: what `.vscode/settings.json` would need. `write` carries the full new text;
67
- * `suggest` carries the snippet to paste, because rewriting would drop comments, trailing
68
- * commas or a tracked file's cleanliness.
66
+ * Read-only. `write` carries the full new text; `suggest` carries the snippet to paste, because
67
+ * rewriting would drop comments, trailing commas or a tracked file's cleanliness.
69
68
  */
70
69
  export function planEditorSettings(root, { config = readProjectConfig(root), env = process.env } = {}) {
71
70
  const file = join(root, SETTINGS_REL);
@@ -1,28 +1,3 @@
1
- /**
2
- * worktree-audit.js — reconcile the three views of "which worktrees exist".
3
- *
4
- * Orphan worktrees accumulate silently. In the observed session,
5
- * `.worktrees/verify-tables-ui` and `.worktrees/tmp-cleanup` were registered in
6
- * `git worktree list` yet ABSENT from `morph-spec fleet` — fleet only reports
7
- * worktrees whose branch matches `refs/heads/morph/*`, so anything else is
8
- * invisible by construction and therefore never cleaned up (C2).
9
- *
10
- * Nothing compared the three sources that must agree:
11
- *
12
- * 1. git's registry — `git worktree list --porcelain`
13
- * 2. the disk — directories under `worktrees/` (and legacy `.worktrees/`)
14
- * 3. morph's features — `.morph/features/{f}/feature.json`
15
- *
16
- * Read-only and fail-open: anything unresolvable yields no issue rather than a
17
- * throw. A diagnostic that invents problems is worse than one that misses some.
18
- * The mutations all live in `pruneWorktrees`, and each is deliberately the
19
- * safest possible: `git worktree prune` drops ADMIN RECORDS for directories
20
- * that are already gone, and the empty-residue sweep removes only directories
21
- * with zero entries inside — nothing that can hold work. Deleting a directory
22
- * that still HOLDS something stays a human decision, the same PROVE-or-DECLINE
23
- * discipline `teardownWorktree` follows.
24
- */
25
-
26
1
  import { execFileSync } from 'child_process';
27
2
  import { existsSync, readdirSync, readFileSync, rmdirSync, statSync } from 'fs';
28
3
  import { join, resolve } from 'path';
@@ -73,7 +48,6 @@ function agentLeftoverIssue(item) {
73
48
  };
74
49
  }
75
50
 
76
- /** Feature names morph knows about in the primary root. */
77
51
  function knownFeatures(root) {
78
52
  const names = new Set();
79
53
  try {
@@ -86,14 +60,7 @@ function knownFeatures(root) {
86
60
  return names;
87
61
  }
88
62
 
89
- /**
90
- * Is the feature's authoritative feature.json COMMITTED on its own branch?
91
- *
92
- * Not "does the branch exist": `git worktree add -b morph/{f}` creates the
93
- * branch as a side effect, so branch-existence is true for every registered
94
- * worktree and proves nothing. The blob is the real evidence — it is where the
95
- * per-feature state model says the truth lives.
96
- */
63
+ // Branch existence proves nothing (`worktree add -b` creates it); the committed blob does.
97
64
  function featureCommittedOnBranch(root, feature) {
98
65
  try {
99
66
  execFileSync('git', ['cat-file', '-e', `morph/${feature}:.morph/features/${feature}/feature.json`], {
@@ -105,29 +72,8 @@ function featureCommittedOnBranch(root, feature) {
105
72
  }
106
73
  }
107
74
 
108
- /**
109
- * Does this worktree's branch still hold work that is NOT in the default
110
- * branch, or an OPEN PR?
111
- *
112
- * This is the predicate that separates "an orphan directory to clean up" from
113
- * "the only copy of somebody's work". Two independent pieces of evidence, and
114
- * ANY of them is enough to protect the worktree:
115
- *
116
- * 1. commits the default branch does not have (`git rev-list base..branch`)
117
- * 2. an OPEN pull request on that branch
118
- *
119
- * Fail-CLOSED, and that asymmetry is the whole design: whenever the answer
120
- * cannot be established — no default branch, no origin, git errored — the
121
- * worktree counts as UNINTEGRATED. The cost of being wrong that way is a
122
- * diagnostic that stays quiet; the cost of the opposite is a suggestion to
123
- * delete work that exists nowhere else.
124
- *
125
- * A squash-merged branch legitimately reports commits ahead, so the tree
126
- * comparison (`branch-classifier.js`) is consulted for the same reason
127
- * `branch prune` needs it — the two must not disagree about "is this in".
128
- *
129
- * @returns {{ unintegrated: boolean, evidence: string, pr: number|null }}
130
- */
75
+ // Fail-CLOSED: when integration cannot be established the worktree counts as
76
+ // unintegrated — a quiet diagnostic beats suggesting to delete the only copy.
131
77
  function unintegratedWork(root, branchRef, prIndex) {
132
78
  const branch = (branchRef || '').replace(/^refs\/heads\//, '');
133
79
  if (!branch) return { unintegrated: true, evidence: 'não foi possível resolver a branch do worktree', pr: null };
@@ -143,9 +89,7 @@ function unintegratedWork(root, branchRef, prIndex) {
143
89
  if (ahead === null) return { unintegrated: true, evidence: `não foi possível comparar com ${baseRef}`, pr: null };
144
90
  if (ahead === 0) return { unintegrated: false, evidence: `nenhum commit fora de ${baseRef}`, pr: pr?.number ?? null };
145
91
 
146
- // Commits ahead, but the work may still be IN: a squash-merge leaves exactly
147
- // that shape. Reuse the classifier so `doctor` and `branch prune` cannot
148
- // reach opposite conclusions about the same branch.
92
+ // A squash-merge also leaves commits ahead; the classifier keeps `doctor` and `branch prune` agreeing.
149
93
  const verdict = classifyBranch({ name: branch, sha: '', current: false, worktreePath: null }, { baseRef, cwd: root, pr });
150
94
  if (verdict.kind === 'merged-ancestral' || verdict.kind === 'merged-squash') {
151
95
  return { unintegrated: false, evidence: verdict.reason, pr: pr?.number ?? null };
@@ -153,19 +97,6 @@ function unintegratedWork(root, branchRef, prIndex) {
153
97
  return { unintegrated: true, evidence: `${ahead} commit(s) fora de ${baseRef}`, pr: pr?.number ?? null };
154
98
  }
155
99
 
156
- /**
157
- * Every feature's `worktree` registration in the primary root — the OTHER
158
- * direction from `knownFeatures`/`dirsOnDisk`: not "does git/disk know about
159
- * this worktree", but "does the ROOT's feature.json still point somewhere
160
- * real". `create --worktree` writes this pointer once at birth
161
- * (`{path, branch, createdAt}`) and nothing updates it afterwards except
162
- * `finish`'s own teardown — so a directory removed any other way (by hand,
163
- * `git worktree prune` run directly, a failed teardown that still cleared the
164
- * field, an unrelated script) leaves it dangling with nothing to notice.
165
- *
166
- * @param {string} root
167
- * @returns {Array<{ feature: string, path: string, branch: string|null }>}
168
- */
169
100
  function featureRegistrations(root) {
170
101
  const out = [];
171
102
  try {
@@ -183,23 +114,15 @@ function featureRegistrations(root) {
183
114
  return out;
184
115
  }
185
116
 
186
- /**
187
- * Is this directory literally empty — zero entries, dotfiles included?
188
- *
189
- * The whole point of the distinction: an empty residue holds nothing that can
190
- * be lost, so it can be removed without a human looking at it. Anything else —
191
- * including a directory we cannot read — is reported as NON-empty, because the
192
- * safe answer to "might there be work in here?" is yes.
193
- */
117
+ // Unreadable counts as occupied: emptiness is what licenses automatic removal.
194
118
  function isEmptyDir(path) {
195
119
  try {
196
120
  return readdirSync(path).length === 0;
197
121
  } catch {
198
- return false; // unreadable → treat as occupied
122
+ return false;
199
123
  }
200
124
  }
201
125
 
202
- /** Directories physically present under either worktree root. */
203
126
  function dirsOnDisk(root) {
204
127
  const found = [];
205
128
  const rootNorm = norm(root);
@@ -221,53 +144,8 @@ function dirsOnDisk(root) {
221
144
  }
222
145
 
223
146
  /**
224
- * Reconcile git's registry, the disk and morph's features.
225
- *
226
- * Issue types:
227
- * • registered-missing — git has a worktree whose directory is gone
228
- * • non-morph-branch — a registered worktree under worktrees/ on a
229
- * branch that is not `morph/*`, so `fleet`
230
- * cannot see it
231
- * • unregistered-dir — a directory under worktrees/ git never
232
- * registered
233
- * • no-feature — a `morph/{f}` worktree for a feature morph
234
- * does not know AND whose branch carries
235
- * nothing that is not already integrated
236
- * • unintegrated-work (info) — the same shape, except the branch still holds
237
- * commits the default does not, or an OPEN PR.
238
- * Reported so the reader knows why no removal
239
- * is suggested — this worktree may be the ONLY
240
- * copy of the work
241
- * • dangling-registration — a feature's ROOT feature.json still points
242
- * at a `worktree.path` that no longer exists
243
- * (the symmetric OTHER direction from
244
- * `no-feature`: the worktree is gone, but the
245
- * root's pointer to it survived)
246
- * • unregistered-registration — a feature's `worktree.path` names a
247
- * directory that exists but git's own
248
- * registry does not know as a worktree
249
- *
250
- * • agent-worktree-leftover (info) — a sub-agent's `agent-<hex>` tree or
251
- * `morph-task/agent-<hex>` branch left behind:
252
- * `reapable` when the agent stopped and nothing
253
- * is lost, otherwise the reason it is kept
254
- *
255
- * • docker-stack-orphan (warn) — a `morph-*` docker stack of this repository
256
- * still running for a feature this tree does
257
- * not know, and
258
- * • docker-port-held (warn) — a port from a feature's block published by
259
- * another compose project. Both come from
260
- * `docker-audit.js`: the three sources above
261
- * cannot see a CONTAINER, and a stack outlives
262
- * every one of them
263
- *
264
- * @param {string} [cwd]
265
- * @param {object} [opts]
266
- * @param {Function} [opts.exec] - injectable executor handed to the docker
267
- * audit, so the docker paths are testable with no daemon (the CI runners
268
- * have none). Absent, the real `docker` CLI is used — and answers `[]` when
269
- * it is not installed.
270
- * @returns {Array<{ type: string, detail: string, path?: string, feature?: string, fix: string }>}
147
+ * Reconciles git's worktree registry, the disk, morph's features and docker.
148
+ * Read-only and fail-open; `exec` is the docker executor seam.
271
149
  */
272
150
  export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
273
151
  if (!isGitRepo(cwd)) return [];
@@ -275,9 +153,7 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
275
153
  const rootNorm = norm(root);
276
154
  const issues = [];
277
155
 
278
- // ONE `gh pr list` for the whole audit — null means UNKNOWN (no gh, not
279
- // GitHub, not authenticated), never "no PRs"; `unintegratedWork` treats the
280
- // two differently on purpose.
156
+ // null = UNKNOWN (no gh / not GitHub / unauthenticated), never "no PRs".
281
157
  const prIndex = buildPrIndex(root);
282
158
 
283
159
  const containers = containerDirs(root);
@@ -288,7 +164,7 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
288
164
  for (const entry of registered) {
289
165
  const path = norm(entry.path);
290
166
  registeredPaths.add(path);
291
- if (path === rootNorm) continue; // the primary root is not a worktree orphan
167
+ if (path === rootNorm) continue;
292
168
 
293
169
  if (!existsSync(path)) {
294
170
  issues.push({
@@ -306,12 +182,7 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
306
182
  const task = entry.branch?.match(/^refs\/heads\/morph-task\/(.+)$/)?.[1] || null;
307
183
 
308
184
  if (!inContainer) {
309
- // A worktree OUTSIDE morph's container was created behind morph's back
310
- // (a native Claude Code run before the WorktreeCreate hook, a raw
311
- // `git worktree add ../x`, another skill). It has no junctions, no port
312
- // block, and nothing morph runs there — the exact shape that let a
313
- // root's `.claude/` be emptied. Reported as `warn` (never a failure,
314
- // NEVER pruned): the work inside may be the only copy.
185
+ // Never pruned: the work inside a tree created outside morph may be the only copy.
315
186
  const work = unintegratedWork(root, entry.branch, prIndex);
316
187
  issues.push({
317
188
  type: 'out-of-container',
@@ -327,10 +198,6 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
327
198
 
328
199
  if (task) {
329
200
  if (AGENT_TASK.test(task)) continue; // reported once, by the agent plan below
330
- // `morph-task/*`: a task-isolation tree provisioned by the WorktreeCreate
331
- // hook. Clean → nothing to say (the orchestrator removes it after the
332
- // merge). With commits the default does not have → report, never
333
- // suggest removal.
334
201
  const work = unintegratedWork(root, entry.branch, prIndex);
335
202
  if (work.unintegrated) {
336
203
  issues.push({
@@ -345,11 +212,6 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
345
212
  }
346
213
 
347
214
  if (inContainer) {
348
- // Same hazard as `no-feature` below, and it needs the same guard:
349
- // "invisible to fleet" says nothing about whether the WORK is safe
350
- // elsewhere. A worktree on a `feat/*` branch with an open PR is the
351
- // most normal thing in the world, and suggesting its removal is a
352
- // suggestion to destroy the only copy.
353
215
  const work = unintegratedWork(root, entry.branch, prIndex);
354
216
  issues.push(work.unintegrated
355
217
  ? {
@@ -365,10 +227,8 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
365
227
  type: 'non-morph-branch',
366
228
  path,
367
229
  detail: `${path} está em ${entry.branch || 'HEAD destacado'}, não numa branch morph/* — o \`fleet\` não o enxerga (e não há trabalho pendente: ${work.evidence})`,
368
- // NEVER raw `git worktree remove` here: it follows the infra junctions
369
- // out and empties the primary root's .claude/ and .morph/framework/.
370
- // `worktree remove` drops them first (PROVE-or-DECLINE) and takes
371
- // --target precisely for worktrees like this one, with no feature.
230
+ // Never raw `git worktree remove`: it follows the infra junctions and
231
+ // empties the root's .claude/ and .morph/framework/.
372
232
  fix: `morph-spec worktree remove --target "${path}" (se já não serve)`,
373
233
  });
374
234
  }
@@ -376,13 +236,8 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
376
236
  }
377
237
 
378
238
  if (!features.has(feature) && !featureCommittedOnBranch(root, feature)) {
379
- // "sem feature conhecida" is a bookkeeping fact, NOT a verdict on the
380
- // work: a `finish` that archived the feature leaves exactly this shape,
381
- // and so does a worktree whose PR is still open. In the observed session
382
- // this branch recommended `worktree remove --target` for a worktree
383
- // holding the ONLY copy of an open PR with 12 tasks and a 9.5 at Gate 3.
384
- // So the diagnosis now depends on whether the WORK is integrated, and the
385
- // suggestion to destroy appears only when it provably is.
239
+ // An archived feature or an open PR leaves this same shape: suggest removal
240
+ // only when the work is provably integrated.
386
241
  const work = unintegratedWork(root, entry.branch, prIndex);
387
242
  if (work.unintegrated) {
388
243
  issues.push({
@@ -420,19 +275,12 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
420
275
  detail: d.empty
421
276
  ? `${d.container}/${d.name} existe no disco, está VAZIO e o git não o registra — resíduo de um finish anterior`
422
277
  : `${d.container}/${d.name} existe no disco mas o git não o registra — resíduo de um finish anterior`,
423
- // An empty folder has nothing to lose, so it gets the automatic repair.
424
- // One with content keeps the manual chore: there may be work in there,
425
- // and the whole PROVE-or-DECLINE discipline exists for exactly that.
426
278
  fix: d.empty
427
279
  ? `morph-spec doctor --worktrees --prune`
428
280
  : `apague o diretório à mão depois de conferir que nada importante ficou nele`,
429
281
  });
430
282
  }
431
283
 
432
- // The other direction: does the ROOT's own bookkeeping still point at a
433
- // live worktree? `git worktree list`/disk only see what's PHYSICALLY
434
- // there; a registration surviving past its worktree's removal is invisible
435
- // to every check above.
436
284
  for (const reg of featureRegistrations(root)) {
437
285
  const path = norm(reg.path);
438
286
  if (!existsSync(reg.path)) {
@@ -454,13 +302,7 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
454
302
  }
455
303
  }
456
304
 
457
- // The fourth source, and the only one that is not a file: the docker stacks.
458
- // A container survives the removal of the worktree, of the branch and of the
459
- // feature — none of the three reconciliations above can see it, and it keeps
460
- // holding the feature's port block. The feature names come from HERE (the
461
- // ones morph knows plus the ones the registered `morph/*` worktrees carry)
462
- // rather than from a second scan inside docker-audit: two readings of the
463
- // same fact are two chances to disagree about which stack is orphaned.
305
+ // Feature names are passed in so docker-audit never rescans and disagrees.
464
306
  const treeFeatures = new Set(features);
465
307
  for (const entry of registered) {
466
308
  const f = entry.branch?.match(/^refs\/heads\/morph\/(.+)$/)?.[1];
@@ -468,31 +310,13 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
468
310
  }
469
311
  issues.push(...auditDockerStacks(root, { exec, features: treeFeatures }));
470
312
 
471
- // What a worktree removal leaves behind in `.git/morph-spec/finish-backup/`
472
- // past its retention — `warn`, pruned by `--prune` and by the next finish.
473
313
  issues.push(...auditFinishBackups(root));
474
314
 
475
315
  return issues;
476
316
  }
477
317
 
478
- /**
479
- * Remove the EMPTY directories under `worktrees/`/`.worktrees/` that git does
480
- * not register — the residue a finish leaves behind.
481
- *
482
- * "Empty" is the whole licence to act: a folder with zero entries cannot be
483
- * hiding uncommitted work, so asking a human about it is pure ceremony, and
484
- * the audit that only warns leaves it there forever. A directory with anything
485
- * in it keeps the manual path.
486
- *
487
- * Two guards beyond the emptiness check:
488
- * • only directories directly inside morph's own worktree containers, never
489
- * the containers themselves and never a path git registers;
490
- * • `rmdirSync`, not `rm -rf` — it REFUSES a non-empty directory, so a file
491
- * that appears between the check and the call is a failure, not a loss.
492
- *
493
- * @param {string} root
494
- * @returns {string[]} paths actually removed
495
- */
318
+ // `rmdirSync`, never `rm -rf`: a file that appears after the emptiness check
319
+ // makes it fail instead of being lost.
496
320
  function removeEmptyUnregisteredDirs(root) {
497
321
  const registeredPaths = new Set(parseWorktreeList(root).map((e) => norm(e.path)));
498
322
  const removed = [];
@@ -507,25 +331,9 @@ function removeEmptyUnregisteredDirs(root) {
507
331
  }
508
332
 
509
333
  /**
510
- * Drop git's admin records for worktrees whose directory is already gone,
511
- * remove the EMPTY unregistered residue directories, AND clear any feature's
512
- * `worktree` registration that names a directory that is now gone.
513
- *
514
- * These three are the ONLY prune offered, on purpose: none can destroy
515
- * anything, because everything they touch is either already absent from disk
516
- * or provably empty. Removing a directory that still HOLDS something is
517
- * exactly the operation that needs a human looking at it.
518
- *
519
- * Order is load-bearing: the empty directories go first, so a root pointer
520
- * naming one of them becomes dangling in time for the registration sweep below
521
- * to clear it — one command, not two runs.
522
- *
523
- * `clearDanglingWorktreeRegistration` re-checks the dangling predicate itself
524
- * (path unset, or still exists → no-op) rather than trusting a pre-computed
525
- * list, so this stays correct even called against a stale audit snapshot.
526
- *
527
- * @param {string} [cwd]
528
- * @returns {Promise<{ pruned: boolean, error?: string, removedEmptyDirs: string[], clearedRegistrations: Array<{feature: string, cleared: boolean}> }>}
334
+ * Touches only what is already gone from disk or provably empty. Order matters:
335
+ * empty dirs go first so a root pointer to one becomes dangling and is cleared
336
+ * in the same run.
529
337
  */
530
338
  export async function pruneWorktrees(cwd = process.cwd()) {
531
339
  if (!isGitRepo(cwd)) return { pruned: false, error: 'not-a-git-repo', removedEmptyDirs: [], clearedRegistrations: [] };
@@ -9,11 +9,9 @@ const NESTED_GIT = /(^|\/)\.git(\/|$)/;
9
9
  const gitOut = (cwd, args, input) => execFileSync('git', args, { cwd, stdio: ['pipe', 'pipe', 'ignore'], encoding: 'utf8', input });
10
10
 
11
11
  /**
12
- * Claude Code's `.worktreeinclude` for a worktree morph-spec creates (a WorktreeCreate hook replaces
13
- * the native creation, so the native copy never runs): files matching a pattern AND gitignored in the
14
- * tree are copied when the tree lacks them. Real files only; build artifacts and `node_modules` never.
15
- *
16
- * @returns {string[]} relative paths copied
12
+ * `.worktreeinclude` parity: a WorktreeCreate hook replaces the native creation, so the native copy
13
+ * never runs. Copies files matching a pattern AND gitignored in the tree, when the tree lacks them;
14
+ * never build artifacts or `node_modules`. Returns the relative paths copied.
17
15
  */
18
16
  export function copyWorktreeIncludes(worktreePath, root) {
19
17
  const file = join(root, INCLUDE_FILE);
@@ -1,4 +1,3 @@
1
- // src/scripts/global-install.js
2
1
  import { join, dirname } from 'path';
3
2
  import { fileURLToPath } from 'url';
4
3
  import { homedir, platform } from 'os';
@@ -10,10 +9,7 @@ import { installGlobalStatusline } from '../utils/hooks-installer.js';
10
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
11
10
  const FRAMEWORK_DIR = join(__dirname, '..', '..', 'framework');
12
11
 
13
- /**
14
- * Ensure the npm global bin directory is in the user's PATH on Windows.
15
- * Adds it to the PowerShell $PROFILE so new terminals find `morph-spec`.
16
- */
12
+ /** Windows only: adds the npm global bin to the PowerShell $PROFILE so new terminals find `morph-spec`. */
17
13
  export async function ensureNpmGlobalInPath() {
18
14
  if (platform() !== 'win32') return;
19
15
 
@@ -21,12 +17,11 @@ export async function ensureNpmGlobalInPath() {
21
17
  try {
22
18
  npmGlobalDir = execSync('npm prefix -g', { encoding: 'utf8' }).trim();
23
19
  } catch {
24
- return; // can't determine npm global dir
20
+ return;
25
21
  }
26
22
 
27
23
  if (!npmGlobalDir || !existsSync(npmGlobalDir)) return;
28
24
 
29
- // --- PowerShell $PROFILE ---
30
25
  const psProfile = join(homedir(), 'Documents', 'PowerShell', 'Microsoft.PowerShell_profile.ps1');
31
26
  const psProfileLegacy = join(homedir(), 'Documents', 'WindowsPowerShell', 'Microsoft.PowerShell_profile.ps1');
32
27
 
@@ -44,7 +39,7 @@ export async function ensureNpmGlobalInPath() {
44
39
  let content = '';
45
40
  if (existsSync(profilePath)) {
46
41
  content = await readFile(profilePath, 'utf8');
47
- if (content.includes(marker)) continue; // already patched
42
+ if (content.includes(marker)) continue;
48
43
  } else {
49
44
  await mkdir(dirname(profilePath), { recursive: true });
50
45
  }
@@ -56,7 +51,6 @@ export async function ensureNpmGlobalInPath() {
56
51
  }
57
52
  }
58
53
 
59
- // Run as postinstall script
60
54
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
61
55
  const HOOKS_SRC = join(FRAMEWORK_DIR, 'hooks', 'claude-code');
62
56
  const { readFileSync } = await import('fs');