@polymorphism-tech/morph-spec 8.37.1 → 8.40.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 (317) hide show
  1. package/CLAUDE.md +37 -13
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +75 -49
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +6 -5
  8. package/framework/MORPH.md +25 -22
  9. package/framework/agents.json +96 -36
  10. package/framework/docs/CLI.md +54 -17
  11. package/framework/evals/rubrics/code-quality.md +5 -0
  12. package/framework/evals/rubrics/condensed.md +6 -2
  13. package/framework/evals/rubrics/contracts.md +20 -0
  14. package/framework/evals/rubrics/test-coverage.md +17 -6
  15. package/framework/hooks/README.md +9 -3
  16. package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
  17. package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
  18. package/framework/hooks/claude-code/core/validate-completion.js +16 -90
  19. package/framework/hooks/claude-code/core/worktree-context.js +10 -0
  20. package/framework/hooks/shared/command-tokens.js +96 -0
  21. package/framework/hooks/shared/git-exclude.js +44 -0
  22. package/framework/hooks/shared/git-paths.js +52 -0
  23. package/framework/hooks/shared/graph-freshness.js +380 -0
  24. package/framework/hooks/shared/graph-refresh.js +416 -0
  25. package/framework/hooks/shared/morph-cli.js +17 -5
  26. package/framework/hooks/shared/redact-secrets.js +55 -0
  27. package/framework/hooks/shared/telemetry-logger.js +25 -2
  28. package/framework/hooks/shared/transcript-usage.js +80 -0
  29. package/framework/mcp/registry.json +33 -2
  30. package/framework/rubrics/backend.json +2 -2
  31. package/framework/rubrics/default.json +2 -2
  32. package/framework/rubrics/frontend.json +2 -2
  33. package/framework/rules/csharp-standards.md +1 -0
  34. package/framework/rules/infrastructure-standards.md +0 -1
  35. package/framework/rules/morph-workflow.md +10 -4
  36. package/framework/rules/nextjs-standards.md +1 -0
  37. package/framework/schemas/config.schema.json +41 -1
  38. package/framework/schemas/feature.schema.json +5 -1
  39. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  40. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  41. package/framework/skills/core/morph-apply/SKILL.md +40 -11
  42. package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
  43. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  44. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  45. package/framework/skills/core/morph-implement/SKILL.md +44 -10
  46. package/framework/skills/core/morph-media/SKILL.md +1 -1
  47. package/framework/skills/core/morph-plan/SKILL.md +73 -15
  48. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  49. package/framework/skills/core/morph-review/SKILL.md +51 -13
  50. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  51. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  52. package/framework/skills/core/morph-uiux/SKILL.md +39 -13
  53. package/framework/standards/STANDARDS.json +189 -14
  54. package/framework/standards/ai-agents/context-providers.md +16 -26
  55. package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
  56. package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
  57. package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
  58. package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
  59. package/framework/standards/ai-agents/modalities-vision.md +1 -18
  60. package/framework/standards/ai-agents/prompt-sources.md +0 -4
  61. package/framework/standards/ai-agents/testing-ai.md +2 -1
  62. package/framework/standards/architecture/code-comments.md +100 -0
  63. package/framework/standards/backend/dotnet/testing.md +22 -0
  64. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
  65. package/framework/standards/frontend/admin-app/data-table.md +562 -0
  66. package/framework/standards/frontend/admin-app/feedback.md +236 -0
  67. package/framework/standards/frontend/admin-app/forms.md +232 -0
  68. package/framework/standards/frontend/admin-app/page-structure.md +203 -0
  69. package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
  70. package/framework/standards/frontend/nextjs/components.md +37 -58
  71. package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
  72. package/framework/standards/frontend/nextjs/forms.md +31 -30
  73. package/framework/standards/frontend/nextjs/state-management.md +2 -1
  74. package/framework/standards/frontend/nextjs/testing.md +18 -0
  75. package/framework/standards/integration/mcp/mcp-tools.md +48 -117
  76. package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
  77. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
  78. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
  79. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
  80. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
  81. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
  82. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
  83. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
  84. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
  85. package/framework/templates/code/dotnet/database/migration.cs +49 -83
  86. package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
  87. package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
  88. package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
  89. package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
  90. package/framework/templates/code/dotnet/jobs/job.cs +2 -97
  91. package/framework/templates/code/dotnet/test.cs +2 -87
  92. package/framework/templates/code/typescript/contracts.ts +140 -167
  93. package/framework/templates/context/design-system.md +3 -3
  94. package/framework/templates/docs/proposal.md +11 -0
  95. package/framework/templates/docs/spec.md +15 -0
  96. package/framework/templates/docs/ui-mockups.md +115 -98
  97. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
  98. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
  99. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
  100. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
  101. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
  102. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
  103. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
  110. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
  111. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
  112. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
  113. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
  114. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
  115. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
  116. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
  117. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
  118. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
  119. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
  120. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
  121. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
  122. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
  123. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
  124. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
  125. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
  126. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
  127. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
  128. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
  129. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
  130. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
  131. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
  132. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
  133. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
  134. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
  135. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
  136. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
  137. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
  138. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
  139. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
  140. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
  141. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
  142. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
  143. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
  144. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
  145. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
  146. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
  147. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
  148. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
  149. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
  150. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
  151. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
  152. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
  153. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
  154. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
  155. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
  156. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
  157. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
  158. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
  159. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
  160. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
  161. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
  162. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
  163. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
  164. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
  165. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
  166. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
  167. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
  168. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
  169. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
  170. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
  171. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
  172. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
  173. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
  174. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
  175. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
  176. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
  177. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
  178. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
  179. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
  180. package/framework/templates/feature/decisions.md +124 -122
  181. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
  182. package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
  183. package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
  184. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
  185. package/framework/templates/integrations/asaas-client.cs +332 -387
  186. package/framework/templates/integrations/asaas-webhook.cs +290 -351
  187. package/framework/templates/integrations/neon-auth-config.cs +9 -105
  188. package/framework/templates/project/validate.js +97 -120
  189. package/package.json +2 -2
  190. package/src/commands/dashboard/dashboard.js +80 -53
  191. package/src/commands/graph/affected.js +4 -3
  192. package/src/commands/graph/build.js +109 -18
  193. package/src/commands/graph/explain.js +8 -5
  194. package/src/commands/graph/status.js +14 -8
  195. package/src/commands/mcp/mcp-setup.js +37 -9
  196. package/src/commands/observability/cost.js +148 -18
  197. package/src/commands/project/doctor.js +85 -15
  198. package/src/commands/project/init.js +6 -3
  199. package/src/commands/project/install-plugin-cmd.js +1 -1
  200. package/src/commands/project/update.js +30 -0
  201. package/src/commands/project/worktree.js +73 -90
  202. package/src/commands/state/advance.js +18 -0
  203. package/src/commands/state/approve.js +15 -0
  204. package/src/commands/state/archive.js +62 -9
  205. package/src/commands/state/finish.js +62 -14
  206. package/src/commands/state/gate-check.js +8 -1
  207. package/src/commands/state/gate-decision.js +8 -4
  208. package/src/commands/state/report-check.js +90 -9
  209. package/src/commands/state/usecase.js +66 -0
  210. package/src/commands/tasks/dag.js +17 -2
  211. package/src/commands/validation/validate-feature.js +6 -1
  212. package/src/commands/validation/verify.js +20 -1
  213. package/src/dashboard/app/App.jsx +80 -16
  214. package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
  215. package/src/dashboard/app/components/CommandPalette.jsx +155 -0
  216. package/src/dashboard/app/components/DetailPanel.jsx +47 -11
  217. package/src/dashboard/app/components/EntityLink.jsx +55 -0
  218. package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
  219. package/src/dashboard/app/components/FlowGraph.jsx +19 -17
  220. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
  221. package/src/dashboard/app/components/Markdown.jsx +47 -0
  222. package/src/dashboard/app/components/PhaseRail.jsx +111 -0
  223. package/src/dashboard/app/components/SearchFilter.jsx +40 -22
  224. package/src/dashboard/app/components/Sidebar.jsx +146 -37
  225. package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
  226. package/src/dashboard/app/hooks/usePref.js +38 -0
  227. package/src/dashboard/app/lib/format.js +69 -0
  228. package/src/dashboard/app/lib/phases.js +56 -0
  229. package/src/dashboard/app/lib/project.js +50 -0
  230. package/src/dashboard/app/pages/Activity.jsx +24 -0
  231. package/src/dashboard/app/pages/AgentMap.jsx +371 -370
  232. package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
  233. package/src/dashboard/app/pages/Features.jsx +400 -0
  234. package/src/dashboard/app/pages/Guide.jsx +146 -0
  235. package/src/dashboard/app/pages/HookInspector.jsx +357 -322
  236. package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
  237. package/src/dashboard/app/pages/Portfolio.jsx +156 -0
  238. package/src/dashboard/app/pages/Rubrics.jsx +101 -0
  239. package/src/dashboard/app/pages/RulesMap.jsx +300 -273
  240. package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
  241. package/src/dashboard/app/pages/Standards.jsx +407 -332
  242. package/src/dashboard/app/styles/index.css +187 -47
  243. package/src/dashboard/index.html +14 -12
  244. package/src/dashboard/package.json +32 -24
  245. package/src/dashboard/scanner/activity-scanner.js +351 -0
  246. package/src/dashboard/scanner/agents-scanner.js +63 -1
  247. package/src/dashboard/scanner/docs-scanner.js +59 -0
  248. package/src/dashboard/scanner/feature-status.js +10 -0
  249. package/src/dashboard/scanner/features-scanner.js +360 -0
  250. package/src/dashboard/scanner/hooks-scanner.js +8 -1
  251. package/src/dashboard/scanner/index.js +14 -7
  252. package/src/dashboard/scanner/phases-scanner.js +14 -15
  253. package/src/dashboard/scanner/portfolio.js +153 -0
  254. package/src/dashboard/scanner/project-api.js +195 -0
  255. package/src/dashboard/scanner/rules-scanner.js +42 -37
  256. package/src/dashboard/scanner/skills-scanner.js +105 -43
  257. package/src/dashboard/scanner/standards-scanner.js +4 -1
  258. package/src/dashboard/vite-plugin-morph-data.js +134 -17
  259. package/src/lib/ai/comment-stripper.js +101 -4
  260. package/src/lib/detectors/claude-config-detector.js +2 -7
  261. package/src/lib/detectors/index.js +5 -1
  262. package/src/lib/evaluator-report.js +47 -0
  263. package/src/lib/generators/handoff-generator.js +4 -18
  264. package/src/lib/generators/settings-generator.js +208 -209
  265. package/src/lib/git/default-branch.js +52 -0
  266. package/src/lib/git/tree-fingerprint.js +58 -0
  267. package/src/lib/git/tree-state.js +167 -0
  268. package/src/lib/graph/god-nodes.js +12 -3
  269. package/src/lib/graph/graph-health.js +48 -2
  270. package/src/lib/graph/graph-loader.js +99 -25
  271. package/src/lib/graph/graphifyignore.js +145 -28
  272. package/src/lib/graph/sanitize.js +117 -11
  273. package/src/lib/installers/mcp-installer.js +724 -719
  274. package/src/lib/installers/memory-installer.js +1 -2
  275. package/src/lib/projects/registry.js +154 -0
  276. package/src/lib/risk-detector.js +90 -0
  277. package/src/lib/specs/acervo-format.js +70 -0
  278. package/src/lib/specs/acervo-lifecycle.js +264 -0
  279. package/src/lib/specs/adr-index.js +169 -0
  280. package/src/lib/specs/revocations.js +156 -0
  281. package/src/lib/specs/screen-checklist.js +357 -0
  282. package/src/lib/specs/usecase-check.js +20 -16
  283. package/src/lib/specs/usecase-promoter.js +103 -38
  284. package/src/lib/specs/usecase-registry.js +7 -7
  285. package/src/lib/tasks/tasks-json-parser.js +2 -3
  286. package/src/lib/telemetry/metrics.js +386 -0
  287. package/src/lib/telemetry/telemetry-reader.js +161 -51
  288. package/src/lib/telemetry/verify-event.js +79 -0
  289. package/src/lib/validators/code-budget/code-budget-validator.js +200 -0
  290. package/src/lib/validators/code-budget/comment-scanner.js +75 -0
  291. package/src/lib/validators/code-budget/index.js +9 -0
  292. package/src/lib/validators/reuse/reuse-validator.js +4 -2
  293. package/src/lib/validators/reuse/symbol-scanner.js +27 -22
  294. package/src/lib/validators/validation-runner.js +21 -5
  295. package/src/lib/validators/verify-runner.js +11 -0
  296. package/src/lib/worktree/finish-backup.js +739 -0
  297. package/src/lib/worktree/ignored-triage.js +15 -8
  298. package/src/lib/worktree/worktree-audit.js +5 -0
  299. package/src/scripts/install-plugin.js +1 -1
  300. package/src/scripts/setup-infra.js +31 -22
  301. package/src/utils/agents-installer.js +338 -24
  302. package/src/utils/banner.js +1 -1
  303. package/src/utils/hooks-installer.js +75 -15
  304. package/src/commands/index.js +0 -13
  305. package/src/commands/project/checkpoint.js +0 -209
  306. package/src/core/state/phase-state-machine.js +0 -197
  307. package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
  308. package/src/dashboard/app/pages/LiveState.jsx +0 -206
  309. package/src/dashboard/scanner/squads-scanner.js +0 -13
  310. package/src/dashboard/scanner/state-scanner.js +0 -85
  311. package/src/lib/generators/index.js +0 -10
  312. package/src/lib/hooks/hook-executor.js +0 -167
  313. package/src/lib/validators/checkpoint-persistence.js +0 -39
  314. package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
  315. package/src/lib/validators/shared/index.js +0 -12
  316. package/src/lib/validators/shared/issue-counter.js +0 -14
  317. package/src/lib/validators/shared/result-formatter.js +0 -124
@@ -16,7 +16,7 @@ import {
16
16
  getInstalledCLIVersion
17
17
  } from '../../utils/version-checker.js';
18
18
  import { resetMorphSettings } from '../../utils/claude-settings-manager.js';
19
- import { checkMissingAgents, checkAgentsDrift } from '../../utils/agents-installer.js';
19
+ import { checkMissingAgents, checkAgentsDrift, maxTurnsError, surfacesError } from '../../utils/agents-installer.js';
20
20
  import { getProjectTags } from '../../lib/stack-filter.js';
21
21
  import { auditGraph } from '../../lib/graph/graph-health.js';
22
22
  import { auditFeatureLifecycle } from '../../core/state/lifecycle-audit.js';
@@ -44,10 +44,10 @@ function normCwd(root, cwd) {
44
44
  /**
45
45
  * Check which required Claude Code plugins are installed.
46
46
  * @param {Object} pluginsData - The `plugins` object from installed_plugins.json
47
- * @returns {{ superpowers: boolean, context7: boolean }}
47
+ * @returns {{ context7: boolean }}
48
48
  */
49
49
  export function checkInstalledPlugins(pluginsData) {
50
- const REQUIRED = ['superpowers', 'context7'];
50
+ const REQUIRED = ['context7'];
51
51
  return Object.fromEntries(
52
52
  REQUIRED.map(name => [name, `${name}@claude-plugins-official` in (pluginsData || {})])
53
53
  );
@@ -191,6 +191,23 @@ export function validateAgentsJson(agentsData) {
191
191
  });
192
192
  }
193
193
 
194
+ // 4: maxTurns (ADR-004) — the installer's own rule, so this cannot pass a
195
+ // value the installer refuses. ERROR, not warn: that persona is not generated.
196
+ const maxTurnsErrors = agentEntries
197
+ .map(([id, data]) => maxTurnsError(id, data?.maxTurns))
198
+ .filter(Boolean);
199
+ checks.push(maxTurnsErrors.length > 0
200
+ ? { name: 'agents.json maxTurns', status: 'error', msg: maxTurnsErrors.join(' | ') }
201
+ : { name: 'agents.json maxTurns', status: 'ok' });
202
+
203
+ // 5: surfaces — same installer rule, same severity: the persona is not generated.
204
+ const surfacesErrors = agentEntries
205
+ .map(([id, data]) => surfacesError(id, data?.surfaces, data?.standards))
206
+ .filter(Boolean);
207
+ checks.push(surfacesErrors.length > 0
208
+ ? { name: 'agents.json surfaces', status: 'error', msg: surfacesErrors.join(' | ') }
209
+ : { name: 'agents.json surfaces', status: 'ok' });
210
+
194
211
  if (missingV5Fields.length > 0 && isV5) {
195
212
  const sample = missingV5Fields.slice(0, 3).join(', ');
196
213
  checks.push({
@@ -348,12 +365,23 @@ async function doctorFullCommand(frameworkRoot) {
348
365
  projectTags = getProjectTags(JSON.parse(rawCfg));
349
366
  } catch { /* no tags */ }
350
367
  }
351
- const drift = checkAgentsDrift(frameworkRoot, join(frameworkRoot, '.morph/framework'), projectTags);
368
+ // The MCP registry comes from the package, like the installer's: the
369
+ // installed .morph/framework/ has no mcp/, and rendering without it
370
+ // flagged every persona with an `mcp` grant as stale on a clean install.
371
+ const drift = checkAgentsDrift(frameworkRoot, join(frameworkRoot, '.morph/framework'), projectTags,
372
+ { mcpRegistryDir: join(import.meta.dirname, '..', '..', '..', 'framework') });
373
+ if (drift.invalid.length > 0) {
374
+ check('Personas agents.json valid', false, false,
375
+ `${drift.invalid.map(i => i.reason).join(' | ')} — corrija o agents.json e rode 'morph-spec update'`);
376
+ }
352
377
  check(`Personas installed (${drift.expected - drift.missing.length}/${drift.expected})`,
353
378
  drift.missing.length === 0, false,
354
379
  drift.missing.length > 0 ? `missing: ${drift.missing.join(', ')} — run 'morph-spec setup-infra --reinstall'` : '');
380
+ // Name WHAT differs per file (frontmatter key or body): a hand-edited
381
+ // maxTurns and a CLI upgrade that changed the body read differently.
382
+ const staleList = drift.stale.map(f => `${f} (${(drift.staleFields[f] ?? []).join(', ')})`);
355
383
  check('Personas up-to-date (no drift)', drift.stale.length === 0, true,
356
- drift.stale.length > 0 ? `stale/hand-edited: ${drift.stale.join(', ')} — regenerate via 'morph-spec setup-infra --reinstall'` : '');
384
+ drift.stale.length > 0 ? `stale/hand-edited: ${staleList.join(', ')} — regenerate via 'morph-spec setup-infra --reinstall'` : '');
357
385
  check('No orphan morph-* agents', drift.orphans.length === 0, true,
358
386
  drift.orphans.length > 0 ? `orphans: ${drift.orphans.join(', ')}` : '');
359
387
  } catch {
@@ -653,23 +681,54 @@ async function doctorMcpCommand(targetPath) {
653
681
  * Deleting a directory that still HOLDS something is reported with the exact
654
682
  * command, never performed — same PROVE-or-DECLINE discipline as the finish
655
683
  * teardown.
684
+ *
685
+ * `--prune` also applies the retention of the finish backups — the same policy
686
+ * every `finish` applies on its own, and the ONLY path that also applies it to
687
+ * the backups written before the retention existed (no manifest), counting
688
+ * them against the budget. Under `--json` its result rides in the JSON.
656
689
  */
657
690
  export async function doctorWorktreesCommand(targetPath, options = {}) {
658
691
  const { auditWorktrees, pruneWorktrees } = await import('../../lib/worktree/worktree-audit.js');
659
692
 
693
+ let backupRetention = null;
694
+ let prune = null;
660
695
  if (options.prune) {
661
696
  const beforeIssues = auditWorktrees(targetPath);
662
697
  const beforeMissing = beforeIssues.filter((i) => i.type === 'registered-missing').length;
663
698
  const beforeDangling = beforeIssues.filter((i) => i.type === 'dangling-registration').length;
664
699
  const beforeEmptyDirs = beforeIssues.filter((i) => i.type === 'unregistered-dir' && i.empty).length;
665
700
  const result = await pruneWorktrees(targetPath);
666
- if (result.pruned) logger.success(`git worktree prune: ${beforeMissing} registro(s) órfão(s) removido(s).`);
667
- else logger.warn(`prune falhou: ${result.error}`);
668
- if (result.removedEmptyDirs.length) {
669
- logger.success(`${result.removedEmptyDirs.length}/${beforeEmptyDirs} diretório(s) vazio(s) removido(s): ${result.removedEmptyDirs.join(', ')}`);
701
+ // Under --json the only thing on stdout is the JSON: a status line in front
702
+ // of it breaks every caller that parses the output.
703
+ prune = {
704
+ pruned: Boolean(result.pruned),
705
+ ...(result.pruned ? { orphanRegistrations: beforeMissing } : { error: result.error }),
706
+ removedEmptyDirs: result.removedEmptyDirs,
707
+ clearedRegistrations: result.clearedRegistrations.map((r) => r.feature),
708
+ };
709
+ if (!options.json) {
710
+ if (result.pruned) logger.success(`git worktree prune: ${beforeMissing} registro(s) órfão(s) removido(s).`);
711
+ else logger.warn(`prune falhou: ${result.error}`);
712
+ if (result.removedEmptyDirs.length) {
713
+ logger.success(`${result.removedEmptyDirs.length}/${beforeEmptyDirs} diretório(s) vazio(s) removido(s): ${result.removedEmptyDirs.join(', ')}`);
714
+ }
715
+ if (result.clearedRegistrations.length) {
716
+ logger.success(`registro de worktree limpo em ${result.clearedRegistrations.length}/${beforeDangling} feature(s): ${result.clearedRegistrations.map((r) => r.feature).join(', ')}`);
717
+ }
670
718
  }
671
- if (result.clearedRegistrations.length) {
672
- logger.success(`registro de worktree limpo em ${result.clearedRegistrations.length}/${beforeDangling} feature(s): ${result.clearedRegistrations.map((r) => r.feature).join(', ')}`);
719
+
720
+ const { resolvePrimaryRoot } = await import('./worktree.js');
721
+ const { applyBackupRetention, summarizeRetention } = await import('../../lib/worktree/finish-backup.js');
722
+ // The explicit prune is the one that reaches the backups written before the
723
+ // retention existed (no manifest): the automatic one never touches them.
724
+ backupRetention = summarizeRetention(applyBackupRetention(resolvePrimaryRoot(targetPath), { includeLegacy: true }));
725
+ if (backupRetention?.skipped && !options.json) {
726
+ logger.warn(`retenção dos backups do finish PULADA: ${backupRetention.skipped}`);
727
+ } else if (backupRetention && !options.json) {
728
+ const stripped = backupRetention.secretsStripped ? `, ${backupRetention.secretsStripped} arquivo(s) .env* apagado(s)` : '';
729
+ const adopted = backupRetention.adopted ? `, ${backupRetention.adopted} backup(s) antigo(s) mantido(s) ganharam manifesto` : '';
730
+ logger.success(`retenção dos backups do finish: ${backupRetention.removed ?? 0} pasta(s) removida(s)${stripped}${adopted}`);
731
+ for (const d of backupRetention.declined || []) logger.warn(`backup mantido: ${d.backup} (${d.reason})`);
673
732
  }
674
733
  }
675
734
 
@@ -684,7 +743,7 @@ export async function doctorWorktreesCommand(targetPath, options = {}) {
684
743
  const isProblem = (i) => !['info', 'warn'].includes(i.severity);
685
744
 
686
745
  if (options.json) {
687
- console.log(JSON.stringify({ issues }, null, 2));
746
+ console.log(JSON.stringify({ issues, ...(prune ? { prune } : {}), ...(backupRetention ? { backupRetention } : {}) }, null, 2));
688
747
  return !issues.some(isProblem);
689
748
  }
690
749
 
@@ -1271,15 +1330,13 @@ export async function doctorCommand(options = {}) {
1271
1330
  } catch { /* non-fatal */ }
1272
1331
 
1273
1332
  // ── Claude Code Plugins ──────────────────────────────────────────────────
1274
- console.log(chalk.cyan('\n Claude Code Plugins (required for /morph-init)'));
1333
+ console.log(chalk.cyan('\n Claude Code Plugins (required by morph-spec init)'));
1275
1334
  const pluginsFile = join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
1276
1335
  let pluginData = { plugins: {} };
1277
1336
  try {
1278
1337
  pluginData = JSON.parse(await fs.readFile(pluginsFile, 'utf8'));
1279
1338
  } catch { /* file may not exist */ }
1280
1339
  const pluginStatus = checkInstalledPlugins(pluginData.plugins || {});
1281
- check('superpowers plugin', pluginStatus.superpowers, false,
1282
- 'Run: morph-spec install-plugin superpowers');
1283
1340
  check('context7 plugin', pluginStatus.context7, false,
1284
1341
  'Run: morph-spec install-plugin context7');
1285
1342
  } else {
@@ -1390,6 +1447,19 @@ export async function doctorCommand(options = {}) {
1390
1447
  checks.push({ name: 'codebase graph', status: 'ok', msg: graphAudit.msg });
1391
1448
  }
1392
1449
 
1450
+ // O que o morph-spec guarda dentro do `.git` (backups do finish, registros de
1451
+ // porta e de claim) cresce sem aparecer em `git status`. Informação — `ok` ou
1452
+ // `warn`, nunca falha —, e nenhuma linha quando não há nada guardado.
1453
+ try {
1454
+ const { resolvePrimaryRoot } = await import('./worktree.js');
1455
+ const { morphStoreCheck } = await import('../../lib/worktree/finish-backup.js');
1456
+ const storeCheck = morphStoreCheck(resolvePrimaryRoot(targetPath));
1457
+ if (storeCheck) {
1458
+ checks.push(storeCheck);
1459
+ if (storeCheck.status === 'warn') hasWarnings = true;
1460
+ }
1461
+ } catch { /* fail-open */ }
1462
+
1393
1463
  // Check key templates exist (in framework package, not project copy)
1394
1464
  const frameworkTemplatesRoot = join(import.meta.dirname, '..', '..', '..', 'framework', 'templates');
1395
1465
  const keyTemplates = [
@@ -34,6 +34,7 @@ import { installAgents, installDomainAgents } from '../../utils/agents-installer
34
34
  import { getProjectTags } from '../../lib/stack-filter.js';
35
35
  import { updateCommand } from './update.js';
36
36
  import { resolvePrimaryRoot } from './worktree.js';
37
+ import { registerProject } from '../../lib/projects/registry.js';
37
38
 
38
39
  /**
39
40
  * Guard against running a full `init` install from inside a secondary
@@ -135,8 +136,7 @@ export async function initCommand(options) {
135
136
  try {
136
137
  claudeConfigDetected = await detectClaudeConfig(targetPath);
137
138
  const hasIntegrations = claudeConfigDetected.plugins.length > 0
138
- || claudeConfigDetected.mcpServers.length > 0
139
- || claudeConfigDetected.superpowers.installed;
139
+ || claudeConfigDetected.mcpServers.length > 0;
140
140
 
141
141
  if (hasIntegrations) {
142
142
  spinner.stop();
@@ -287,7 +287,8 @@ export async function initCommand(options) {
287
287
  try {
288
288
  spinner.stop();
289
289
  const existingMcps = claudeConfigDetected?.mcpServers || [];
290
- const orchestration = orchestrateMcpSetup(targetPath, detectedStack, existingMcps);
290
+ const orchestration = orchestrateMcpSetup(targetPath, detectedStack, existingMcps,
291
+ { integrations: config.project?.integrations ?? [] });
291
292
 
292
293
  const autoNames = Object.keys(orchestration.autoInstallable);
293
294
  const manualNames = Object.keys(orchestration.needsManualSetup);
@@ -467,6 +468,8 @@ export async function initCommand(options) {
467
468
  }
468
469
 
469
470
  spinner.succeed('MORPH-SPEC installed successfully!');
471
+ // The dashboard's portfolio learns about the project here (fail-open).
472
+ registerProject(targetPath, 'init');
470
473
 
471
474
  // Show next steps
472
475
  logger.blank();
@@ -5,7 +5,7 @@ import { logger } from '../../utils/logger.js';
5
5
  export async function installPluginCommand(pluginName, _options) {
6
6
  if (!pluginName) {
7
7
  logger.error('Plugin name required. Usage: morph-spec install-plugin <name>');
8
- logger.dim(' Available: superpowers, context7');
8
+ logger.dim(' Available: context7');
9
9
  process.exit(1);
10
10
  }
11
11
  try {
@@ -33,6 +33,10 @@ import { getProjectTags } from '../../lib/stack-filter.js';
33
33
  import { updateClaudeMdAgentsBlock, installRulesForStack } from '../../scripts/setup-infra.js';
34
34
  import { reconcileLegacyTemplatesCopy } from '../../lib/installers/templates-reconcile.js';
35
35
  import { createEffects } from '../../lib/installers/effects.js';
36
+ import { registerProject } from '../../lib/projects/registry.js';
37
+ import {
38
+ writeGraphifyignore, describeOutdatedIgnore, graphifyignoreTracked,
39
+ } from '../../lib/graph/graphifyignore.js';
36
40
 
37
41
  /**
38
42
  * Backup user's config.json before cleaning
@@ -725,6 +729,24 @@ export async function updateCommand(options = {}) {
725
729
  await updateGitignore(targetPath, fx);
726
730
  await ensureGitattributes(targetPath, fx);
727
731
 
732
+ // Migration: a .graphifyignore the previous version generated and nobody
733
+ // edited is swapped for the current one when its RULES change (test doubles
734
+ // and fixtures back in the graph — a header-only difference is left alone);
735
+ // an edited one is left alone, with the change to make. Only an existing
736
+ // file — the graph is opt-in, and `graph build` writes it. A tracked file
737
+ // reaches worktrees and teammates only once committed: each checkout reads
738
+ // its own.
739
+ let graphIgnore = null;
740
+ if (existsSync(join(targetPath, '.graphifyignore'))) {
741
+ updateSpinner.text = 'Checking .graphifyignore...';
742
+ graphIgnore = writeGraphifyignore(targetPath, projectTags, { fx });
743
+ if (graphIgnore.outdated) notices.push(describeOutdatedIgnore(graphIgnore.outdated));
744
+ if (graphIgnore.migrated && graphifyignoreTracked(targetPath)) {
745
+ notices.push('.graphifyignore é versionado: commite a migração (git add .graphifyignore) — '
746
+ + 'as worktrees e o resto do time leem o arquivo do próprio checkout e só recebem as regras novas depois do commit');
747
+ }
748
+ }
749
+
728
750
  // Migration: untrack a pre-v5 committed state.json so it stops being swept
729
751
  // into feature commits. Gitignore alone can't do this for an already-tracked file.
730
752
  const untracked = untrackCommittedStateJson(targetPath, fx);
@@ -737,6 +759,10 @@ export async function updateCommand(options = {}) {
737
759
  logger.blank();
738
760
 
739
761
  if (!dryRun) {
762
+ // The dashboard's portfolio learns about the project here (fail-open;
763
+ // outside the project, so not an effect of the update itself).
764
+ registerProject(targetPath, 'update');
765
+
740
766
  // Show version change
741
767
  if (projectCheck.current && projectCheck.current !== cliCheck.current) {
742
768
  logger.success(`Updated: ${projectCheck.current} → ${cliCheck.current}`);
@@ -769,6 +795,10 @@ export async function updateCommand(options = {}) {
769
795
  logger.dim(` ✓ .claude/agents/ (${agentsResult?.agents?.length ?? '?'} personas — stack: ${tagsLabel})`);
770
796
  logger.dim(` ✓ .claude/rules/ (${rulesResult?.installed ?? '?'} stack-filtered rules)`);
771
797
  if (commandsCopied) logger.dim(' ✓ .claude/commands/');
798
+ if (graphIgnore?.migrated) {
799
+ logger.dim(' ✓ .graphifyignore (dublês, fixtures e helpers de teste voltam ao grafo da raiz no próximo refresh');
800
+ logger.dim(' em background — início de sessão ou comando git do agente; numa worktree: morph-spec graph refresh)');
801
+ }
772
802
  if (untracked) {
773
803
  logger.dim(' ✓ .morph/state.json untracked (git rm --cached) — commit to finalize');
774
804
  logger.warn('Migration: .morph/state.json was removed from git tracking. The');
@@ -35,14 +35,24 @@
35
35
  */
36
36
 
37
37
  import { execSync, execFileSync } from 'child_process';
38
- import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, appendFileSync, lstatSync, unlinkSync, rmdirSync, cpSync } from 'fs';
38
+ import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, lstatSync, unlinkSync, rmdirSync, cpSync } from 'fs';
39
39
  import { join, resolve, dirname, basename } from 'path';
40
40
  import chalk from 'chalk';
41
41
  import { detectDefaultBranch } from '../../lib/git/default-branch.js';
42
42
  import { triageIgnoredPaths } from '../../lib/worktree/ignored-triage.js';
43
+ import {
44
+ finishBackupDir,
45
+ writeFeatureScopeBackup,
46
+ writeWorktreeIgnoredBackup,
47
+ enforceBackupRetention,
48
+ newBackupStamp,
49
+ } from '../../lib/worktree/finish-backup.js';
43
50
  // The reparse scan lives in hooks/shared because the PreToolUse guardrail needs
44
51
  // the SAME answer this file does, and hooks can never import from `src/`.
45
52
  import { findReparsePoints } from '../../../framework/hooks/shared/reparse-scan.js';
53
+ import { ensureCommonExclude } from '../../../framework/hooks/shared/git-exclude.js';
54
+ import { seedGraphFromRoot } from '../../../framework/hooks/shared/graph-refresh.js';
55
+ import { dropLeftoverIgnore, withoutLeftoverIgnore } from '../../../framework/hooks/shared/graph-freshness.js';
46
56
 
47
57
  // The container name lives in lib/worktree/dirs.js (single source, C1
48
58
  // rationale documented there); re-exported here so existing importers and the
@@ -288,28 +298,9 @@ const EXCLUDE_MARKER = '# morph-spec: infra links inside worktrees (managed —
288
298
  * @param {string[]} rels - worktree-relative paths to exclude
289
299
  */
290
300
  function excludeWorktreePaths(mainCwd, rels) {
291
- if (!rels.length) return;
292
- try {
293
- // `--git-common-dir`, never `join(mainCwd, '.git')`: the primary root may
294
- // itself be a worktree (whose `.git` is a FILE), and the common dir is the
295
- // only exclude every linked worktree actually reads.
296
- const common = execSync('git rev-parse --git-common-dir', { cwd: mainCwd, stdio: 'pipe' }).toString().trim();
297
- if (!common) return;
298
- const infoDir = join(resolve(mainCwd, common), 'info');
299
- const excludePath = join(infoDir, 'exclude');
300
- const current = existsSync(excludePath) ? readFileSync(excludePath, 'utf8') : '';
301
- const present = new Set(current.split(/\r?\n/).map((l) => l.trim()));
302
- // Anchored (`/x`): these live at the top of a worktree, and an unanchored
303
- // `node_modules` would also swallow nested ones the user may track.
304
- const missing = rels.map((rel) => `/${rel.replace(/\\/g, '/')}`).filter((p) => !present.has(p));
305
- if (!missing.length) return;
306
- mkdirSync(infoDir, { recursive: true });
307
- appendFileSync(excludePath, [
308
- current && !current.endsWith('\n') ? '\n' : '',
309
- present.has(EXCLUDE_MARKER) ? '' : `${EXCLUDE_MARKER}\n`,
310
- `${missing.join('\n')}\n`,
311
- ].join(''));
312
- } catch { /* fail-open */ }
301
+ // Anchored (`/x`): these live at the top of a worktree, and an unanchored
302
+ // `node_modules` would also swallow nested ones the user may track.
303
+ ensureCommonExclude(mainCwd, rels.map((rel) => `/${rel.replace(/\\/g, '/')}`), EXCLUDE_MARKER);
313
304
  }
314
305
 
315
306
  /**
@@ -412,6 +403,28 @@ export async function materializeState(worktreePath) {
412
403
  }
413
404
  }
414
405
 
406
+ /**
407
+ * Codebase graph for a new worktree. The tree is born from a root commit, so
408
+ * the root's graph is either this tree's HEAD graph already or one
409
+ * `morph-spec graph refresh` away — and the `reuse` validator reads the graph
410
+ * of the tree `verify` runs in, so a worktree without one skips reuse checking
411
+ * exactly where the new code is written. Nothing is launched in the
412
+ * background: the automatic refresh only ever touches the root (a detached
413
+ * process holding a worktree directory made `finish` fail on win32).
414
+ * Fail-open: no root graph, no copy.
415
+ *
416
+ * @param {string} worktreePath
417
+ * @param {string} root
418
+ * @returns {{seeded: boolean, reason?: string}}
419
+ */
420
+ function seedWorktreeGraph(worktreePath, root) {
421
+ try {
422
+ return seedGraphFromRoot(worktreePath, root);
423
+ } catch {
424
+ return { seeded: false, reason: 'seed-failed' };
425
+ }
426
+ }
427
+
415
428
  // ── Command handler ─────────────────────────────────────────────────────────
416
429
 
417
430
  /**
@@ -521,6 +534,8 @@ export async function runWorktreeSetup(feature, options = {}) {
521
534
  // committed feature.json files) so hooks work from the very first session.
522
535
  const materialized = await ensureLocalState(worktreePath);
523
536
 
537
+ const graph = seedWorktreeGraph(worktreePath, cwd);
538
+
524
539
  // Fleet handoff: the fresh instance that opens this worktree reads
525
540
  // handoff.md before anything else (SessionStart hook points at it). The doc
526
541
  // is derived and git-invisible (see handoff-generator). Fail-open — and a
@@ -560,6 +575,7 @@ export async function runWorktreeSetup(feature, options = {}) {
560
575
  feature,
561
576
  materialized: materialized.materialized,
562
577
  ...(envBlock ? { env: { base: envBlock.base, range: envBlock.range } } : {}),
578
+ ...(graph.seeded ? { graph: 'seeded' } : {}),
563
579
  nextStep: `Abra o Claude Code em ${worktreePath} para trabalhar nesta feature`,
564
580
  };
565
581
  } catch (err) {
@@ -654,6 +670,7 @@ export async function runWorktreeProvision(name, options = {}) {
654
670
  setupInfraLinks(worktreePath, root);
655
671
  const envBlock = await allocateWorktreeEnv(worktreePath, root, `task/${name}`);
656
672
  const materialized = await ensureLocalState(worktreePath);
673
+ const graph = seedWorktreeGraph(worktreePath, root);
657
674
 
658
675
  return {
659
676
  ok: true,
@@ -666,6 +683,7 @@ export async function runWorktreeProvision(name, options = {}) {
666
683
  name,
667
684
  materialized: materialized.materialized,
668
685
  ...(envBlock ? { env: { base: envBlock.base, range: envBlock.range } } : {}),
686
+ ...(graph.seeded ? { graph: 'seeded' } : {}),
669
687
  };
670
688
  } catch (err) {
671
689
  return { ok: true, exitCode: 0, created: false, error: (err.message || 'unknown').slice(0, 200), name };
@@ -930,7 +948,7 @@ export function unlinkWorktreeInfraLinks(worktreePath, mainCwd) {
930
948
 
931
949
  /**
932
950
  * Gitignored paths a morph worktree owns and that are safe to remove — but ONLY
933
- * once backed up (see backupWorktreeIgnored). `.morph/state.json` is provably
951
+ * once backed up (see writeWorktreeIgnoredBackup). `.morph/state.json` is provably
934
952
  * derivable (loadState self-heals it from the committed feature.json files) and
935
953
  * the rest is morph's own diagnostic output, which `setupInfraLinks` documents
936
954
  * as worktree-local. They are small, so backing them up costs nothing and buys
@@ -1054,60 +1072,6 @@ export function classifyIgnoredLeftovers(worktreePath, rootPath, options = {}) {
1054
1072
  return { shieldable, blocking: [], discarded: [...disposable, ...replicable], replicable };
1055
1073
  }
1056
1074
 
1057
- /**
1058
- * Back up the worktree's morph-owned gitignored leftovers before the removal
1059
- * deletes them. Same destination and rationale as backupFeatureScope.
1060
- *
1061
- * @param {string} worktreePath
1062
- * @param {string} root
1063
- * @param {string} feature
1064
- * @param {string[]} rels - worktree-relative paths
1065
- * @param {string} stamp
1066
- * @returns {string|null} backup path, or null if ANY copy failed (caller must
1067
- * then decline the removal — a partial backup authorizes nothing)
1068
- */
1069
- function backupWorktreeIgnored(worktreePath, root, feature, rels, stamp) {
1070
- const dest = join(root, '.git', 'morph-spec', 'finish-backup', `${feature}-${stamp}-worktree-ignored`);
1071
- try {
1072
- for (const rel of rels) {
1073
- const src = join(worktreePath, rel);
1074
- if (!existsSync(src)) continue; // a directory entry already covered by its children
1075
- const target = join(dest, rel);
1076
- mkdirSync(dirname(target), { recursive: true });
1077
- cpSync(src, target, { recursive: true });
1078
- }
1079
- return existsSync(dest) ? dest : null;
1080
- } catch {
1081
- return null;
1082
- }
1083
- }
1084
-
1085
- /**
1086
- * Copy the feature's root-side scope to a backup OUTSIDE the working tree, so
1087
- * the shed below can never be the reason something is lost.
1088
- *
1089
- * Lives under `.git/` on purpose: it is not a working-tree path, so it can never
1090
- * dirty `git status` (which the very next validation reads) and is never merged,
1091
- * committed, or cleaned by `git clean`.
1092
- *
1093
- * @param {string} root
1094
- * @param {string} feature
1095
- * @param {string} stamp - caller-supplied timestamp (keeps this pure/testable)
1096
- * @returns {string|null} absolute backup path, or null if the copy failed
1097
- */
1098
- function backupFeatureScope(root, feature, stamp) {
1099
- const src = join(root, '.morph', 'features', feature);
1100
- if (!existsSync(src)) return null;
1101
- const dest = join(root, '.git', 'morph-spec', 'finish-backup', `${feature}-${stamp}`);
1102
- try {
1103
- mkdirSync(dirname(dest), { recursive: true });
1104
- cpSync(src, dest, { recursive: true });
1105
- return dest;
1106
- } catch {
1107
- return null; // caller MUST treat this as "do not shed"
1108
- }
1109
- }
1110
-
1111
1075
  // ── finish primitives (used by `morph-spec finish`) ───────────────────────
1112
1076
  //
1113
1077
  // The old `worktree finish` command dissolved here: `morph-spec finish` (see
@@ -1119,17 +1083,19 @@ function backupFeatureScope(root, feature, stamp) {
1119
1083
  /**
1120
1084
  * Shed the feature's OWN transient root-side bookkeeping so the merge can own
1121
1085
  * this feature's state while the whole-root clean-check keeps guarding UNRELATED
1122
- * work. Backs up FIRST (see backupFeatureScope), then discards — the shed is
1123
- * safe WITHOUT having to be right about what is in scope: if the classification
1124
- * the merge implies turns out wrong, the bytes are still on disk. Worktree-flow
1125
- * only; the common flow committed the archive on the branch, so there is no
1126
- * untracked root-side scaffold to shed.
1086
+ * work. Backs up FIRST (see writeFeatureScopeBackup), then discards — the shed
1087
+ * is safe WITHOUT having to be right about what is in scope: if the
1088
+ * classification the merge implies turns out wrong, the bytes are still on
1089
+ * disk. Worktree-flow only; the common flow committed the archive on the
1090
+ * branch, so there is no untracked root-side scaffold to shed.
1127
1091
  *
1128
1092
  * @param {string} root
1129
1093
  * @param {string} feature
1094
+ * @param {{ stamp?: string }} [opts] - the finish's one stamp, shared with the
1095
+ * teardown's backup so the two folders count as one backup
1130
1096
  * @returns {{ shedBackup: string|null }} backup path if anything was shed
1131
1097
  */
1132
- export function shedRootFeatureScope(root, feature) {
1098
+ export function shedRootFeatureScope(root, feature, { stamp } = {}) {
1133
1099
  const featPathspec = `.morph/features/${feature}`;
1134
1100
  let dirtyInScope = '';
1135
1101
  try {
@@ -1138,7 +1104,7 @@ export function shedRootFeatureScope(root, feature) {
1138
1104
 
1139
1105
  if (dirtyInScope.trim() === '') return { shedBackup: null };
1140
1106
 
1141
- const shedBackup = backupFeatureScope(root, feature, new Date().toISOString().replace(/[:.]/g, '-'));
1107
+ const shedBackup = writeFeatureScopeBackup({ root, feature, stamp });
1142
1108
  // No backup, no shed. Falling through leaves the root dirty, so the caller's
1143
1109
  // clean-check refuses — the exact fail-closed behaviour from before any shed.
1144
1110
  if (shedBackup) {
@@ -1363,14 +1329,17 @@ export function pickRemovalFailure(stderrs) {
1363
1329
  * `deleteBranch` decouples `git branch -d` from removal: true for `--merge` (the
1364
1330
  * branch is integrated), false for `--pr` (the open PR still needs the branch).
1365
1331
  *
1332
+ * `stamp` names the backup; `finish` passes the one it gave the shed, so both
1333
+ * folders of that finish count as one backup under the retention.
1334
+ *
1366
1335
  * @param {string} worktreePath
1367
- * @param {{ root: string, feature: string, branch: string, options?: object, deleteBranch?: boolean }} ctx
1336
+ * @param {{ root: string, feature: string, branch: string, options?: object, deleteBranch?: boolean, stamp?: string }} ctx
1368
1337
  * @returns {Promise<{ worktreeRemoved: boolean, ignoredBackup: string|null, discarded: string[],
1369
1338
  * blockers: string[], ignoredBlocking: string[], shieldable: string[], declineReason: string|null,
1370
1339
  * relinked: boolean, refused: boolean, infraLeftUnlinked: string[],
1371
1340
  * removalFailure: {kind: string, detail: string}|null }>}
1372
1341
  */
1373
- export async function teardownWorktree(worktreePath, { root, feature, branch, options = {}, deleteBranch = true, addressByTarget = false }) {
1342
+ export async function teardownWorktree(worktreePath, { root, feature, branch, options = {}, deleteBranch = true, addressByTarget = false, stamp }) {
1374
1343
  // Drop the infra junctions FIRST, without following them — this is what makes
1375
1344
  // a removal survivable (see unlinkWorktreeInfraLinks). Runs unconditionally.
1376
1345
  //
@@ -1392,7 +1361,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
1392
1361
  let ignoredBackup = null;
1393
1362
  const clearedToRemove = blockers.length === 0 && ignored.blocking.length === 0;
1394
1363
  if (clearedToRemove && ignored.shieldable.length) {
1395
- ignoredBackup = backupWorktreeIgnored(worktreePath, root, feature, ignored.shieldable, new Date().toISOString().replace(/[:.]/g, '-'));
1364
+ ignoredBackup = writeWorktreeIgnoredBackup({ worktreePath, root, feature, rels: ignored.shieldable, stamp });
1396
1365
  }
1397
1366
  const canRemove = clearedToRemove && (!ignored.shieldable.length || ignoredBackup !== null);
1398
1367
 
@@ -1462,7 +1431,7 @@ export async function teardownWorktree(worktreePath, { root, feature, branch, op
1462
1431
  const removeCmd = (addressByTarget || !isValidFeatureName(feature))
1463
1432
  ? `morph-spec worktree remove --target "${worktreePath}"`
1464
1433
  : `morph-spec worktree remove ${feature}`;
1465
- const backupDir = join(root, '.git', 'morph-spec', 'finish-backup');
1434
+ const backupDir = finishBackupDir(root);
1466
1435
  const sample = (list) => `${list.slice(0, 5).join(', ')}${list.length > 5 ? `, +${list.length - 5}` : ''}`;
1467
1436
  const declineReason = worktreeRemoved
1468
1437
  ? null
@@ -1659,13 +1628,18 @@ export async function worktreeRemoveCommand(featureArg, options = {}) {
1659
1628
  emit({ removed: false, error: 'status-failed', path: worktreePath, root,
1660
1629
  hint: 'não foi possível provar que o worktree está limpo — nada foi removido' }, 1);
1661
1630
  }
1662
- const dirt = status.split(/\r?\n/).filter((l) => l.trim());
1631
+ const dirt = withoutLeftoverIgnore(worktreePath, status.split(/\r?\n/).filter((l) => l.trim()));
1663
1632
  if (dirt.length) {
1664
1633
  emit({ removed: false, error: 'worktree-dirty', path: worktreePath, root, dirt: dirt.slice(0, 20),
1665
1634
  hint: `o worktree tem trabalho não commitado — commit ou descarte antes, ou rode \`morph-spec worktree remove --target "${worktreePath}"\` sem --if-clean para remover com backup` }, 1);
1666
1635
  }
1667
1636
  }
1668
1637
 
1638
+ // A leftover temporary .graphifyignore (a `graph refresh` killed mid-run) is a
1639
+ // copy of the root's rules: not a file to back up. Deleted only now, after
1640
+ // every refusal, so a refused removal leaves the tree as it was.
1641
+ dropLeftoverIgnore(worktreePath);
1642
+
1669
1643
  // The env-registry key this tree allocated under: `<f>` for morph/<f>,
1670
1644
  // `task/<n>` for morph-task/<n>. Read BEFORE the teardown (the entry is gone
1671
1645
  // from `git worktree list` afterwards). The old `'worktree'` placeholder
@@ -1683,9 +1657,13 @@ export async function worktreeRemoveCommand(featureArg, options = {}) {
1683
1657
  e2e = tearDownFeatureStack(envKey || feature || basename(worktreePath), { root, worktreePath, volumes: false });
1684
1658
  } catch { e2e = null; /* fail-open */ }
1685
1659
 
1660
+ // The stamp this removal's backup is written under — protected from the
1661
+ // retention that runs after it.
1662
+ const stamp = newBackupStamp();
1686
1663
  const teardown = await teardownWorktree(worktreePath, {
1687
1664
  root,
1688
1665
  feature: feature || envKey || 'worktree',
1666
+ stamp,
1689
1667
  branch,
1690
1668
  options: { discardIgnored: options.discardIgnored === true },
1691
1669
  // Removing a worktree is not closing a feature: the branch is the work, and
@@ -1724,6 +1702,11 @@ export async function worktreeRemoveCommand(featureArg, options = {}) {
1724
1702
  if (teardown.infraLeftUnlinked.length) out.infraLeftUnlinked = teardown.infraLeftUnlinked;
1725
1703
  out.hint = teardown.declineReason;
1726
1704
  }
1705
+ // After the new backup, never before — and never the new backup itself.
1706
+ if (teardown.ignoredBackup) {
1707
+ const retention = enforceBackupRetention(root, { protectStamps: [stamp] });
1708
+ if (retention) out.backupRetention = retention;
1709
+ }
1727
1710
  emit(out, teardown.worktreeRemoved ? 0 : 1);
1728
1711
  }
1729
1712
 
@@ -6,6 +6,7 @@ import { getFeature, setApprovalGate, derivePhase } from '../../core/state/state
6
6
  import { unmetPrereqs } from '../../core/state/gate-order.js';
7
7
  import { OUTPUT_SCHEMA } from '../../core/paths/output-schema.js';
8
8
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
9
+ import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
9
10
 
10
11
  const FRAMEWORK_DIR = fileURLToPath(new URL('../../../framework/', import.meta.url));
11
12
 
@@ -169,6 +170,19 @@ export async function advanceCommand(featureName, options = {}) {
169
170
  const gate = PHASE_GATE[phase];
170
171
  const approvedBy = resolveApprovedBy(options);
171
172
 
173
+ // The screen checklist is part of what the uiux phase owes. The predicate
174
+ // is approve.js's too: an "auto" signature never closes a gate the
175
+ // gate-check would pause, and a recorded decision or a human signature
176
+ // (--approver) is the answer to that pause; --force is not, as in approve.
177
+ const checklistBlock = phase === 'uiux' ? uiuxAutoApprovalBlock(process.cwd(), featureName, feature) : null;
178
+ if (checklistBlock && approvedBy === 'auto') {
179
+ console.error(chalk.red(`\n❌ A fase "uiux" não pode ser encerrada com assinatura automática — ${checklistBlock}.`));
180
+ console.error(chalk.gray('\n Complete o checklist (ver a skill morph-uiux) e rode de novo. Quem decidiu aprovar assim'));
181
+ console.error(chalk.gray(` mesmo grava a decisão (morph-spec gate-decision ${featureName} uiux --pause false --reason "...")`));
182
+ console.error(chalk.gray(' ou assina com --approver <nome>. O --force não dispensa o checklist.\n'));
183
+ process.exit(1);
184
+ }
185
+
172
186
  // Gate monotonicity (mirrors approve.js): advance is a gate write-path too,
173
187
  // so it must not stamp gate N while a predecessor is pending. In the
174
188
  // sanctioned flow advance is always in-order (it approves G1 before the plan
@@ -242,6 +256,10 @@ export async function advanceCommand(featureName, options = {}) {
242
256
  if (missingArtifacts.length) {
243
257
  console.log(chalk.yellow(` ⚠️ --force: avançou SEM ${missingArtifacts.join(', ')}.`));
244
258
  }
259
+ if (checklistBlock) {
260
+ const by = options.force ? '--force' : `aprovado por ${approvedBy}`;
261
+ console.log(chalk.yellow(` ⚠️ ${by}: avançou com o ${checklistBlock}.`));
262
+ }
245
263
  // Report what actually happened: claiming "approved (auto)" when nothing
246
264
  // was written is how the destroyed signature stayed invisible.
247
265
  if (gate && alreadyApproved) {