@polymorphism-tech/morph-spec 8.34.2 → 8.35.1

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 (328) hide show
  1. package/CLAUDE.md +22 -5
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +18 -4
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +5 -4
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/MORPH.md +16 -17
  9. package/framework/agents.json +197 -44
  10. package/framework/ai-pin.json +22 -0
  11. package/framework/commands/morph-media.md +45 -0
  12. package/framework/docs/CLI.md +236 -11
  13. package/framework/evals/proof-rubrics/README.md +76 -0
  14. package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
  15. package/framework/evals/proof-rubrics/fiacao.md +54 -0
  16. package/framework/evals/proof-rubrics/guard-cego.md +52 -0
  17. package/framework/evals/proof-rubrics/media-claims.md +109 -0
  18. package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
  19. package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
  20. package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
  21. package/framework/evals/rubrics/architecture.md +30 -0
  22. package/framework/evals/rubrics/code-quality.md +26 -0
  23. package/framework/evals/rubrics/test-coverage.md +20 -0
  24. package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
  25. package/framework/hooks/claude-code/core/worktree-env-inject.js +43 -0
  26. package/framework/hooks/claude-code/statusline.sh +11 -11
  27. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  28. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  29. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  30. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  31. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  32. package/framework/hooks/shared/e2e-check.js +135 -22
  33. package/framework/hooks/shared/payload-utils.js +15 -2
  34. package/framework/hooks/shared/stdin-reader.js +26 -26
  35. package/framework/rules/ai-agents-standards.md +14 -2
  36. package/framework/schemas/ai-pin.schema.json +97 -0
  37. package/framework/schemas/config.schema.json +107 -0
  38. package/framework/schemas/feature.schema.json +23 -4
  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 +26 -5
  42. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  43. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  44. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  45. package/framework/skills/core/morph-implement/SKILL.md +18 -2
  46. package/framework/skills/core/morph-media/SKILL.md +215 -0
  47. package/framework/skills/core/morph-plan/SKILL.md +33 -2
  48. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  49. package/framework/skills/core/morph-review/SKILL.md +166 -9
  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 +1 -1
  53. package/framework/standards/PROVENANCE.json +81 -3
  54. package/framework/standards/STANDARDS.json +1483 -839
  55. package/framework/standards/ai-agents/agent-archetypes.md +160 -0
  56. package/framework/standards/ai-agents/agent-session.md +164 -68
  57. package/framework/standards/ai-agents/agent-spec.md +196 -0
  58. package/framework/standards/ai-agents/batching.md +61 -37
  59. package/framework/standards/ai-agents/context-providers.md +97 -24
  60. package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
  61. package/framework/standards/ai-agents/cost-and-budget.md +256 -0
  62. package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
  63. package/framework/standards/ai-agents/evals-with-cache.md +289 -0
  64. package/framework/standards/ai-agents/guardrails.md +222 -0
  65. package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
  66. package/framework/standards/ai-agents/mcp-server.md +119 -18
  67. package/framework/standards/ai-agents/mcp-tools.md +85 -32
  68. package/framework/standards/ai-agents/media-pipeline.md +352 -0
  69. package/framework/standards/ai-agents/media-video.md +267 -0
  70. package/framework/standards/ai-agents/middleware-patterns.md +39 -5
  71. package/framework/standards/ai-agents/modalities-image-gen.md +354 -105
  72. package/framework/standards/ai-agents/modalities-speech-to-text.md +8 -2
  73. package/framework/standards/ai-agents/modalities-text-to-speech.md +10 -2
  74. package/framework/standards/ai-agents/modalities-vision.md +6 -2
  75. package/framework/standards/ai-agents/multi-agent-patterns.md +61 -5
  76. package/framework/standards/ai-agents/observability-patterns.md +227 -17
  77. package/framework/standards/ai-agents/production.md +38 -9
  78. package/framework/standards/ai-agents/prompt-sources.md +389 -0
  79. package/framework/standards/ai-agents/providers/model-registry.md +174 -39
  80. package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
  81. package/framework/standards/ai-agents/service-tier-flex.md +27 -19
  82. package/framework/standards/ai-agents/setup.md +166 -50
  83. package/framework/standards/ai-agents/structured-output.md +34 -3
  84. package/framework/standards/ai-agents/sweet-spot.md +23 -3
  85. package/framework/standards/ai-agents/testing-ai.md +236 -0
  86. package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
  87. package/framework/standards/ai-agents/workflows.md +27 -7
  88. package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
  89. package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
  90. package/framework/standards/infrastructure/docker/local-compose-isolation.md +3 -1
  91. package/framework/templates/.idea/morph-templates.xml +92 -0
  92. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  93. package/framework/templates/IDE-SNIPPETS.md +262 -262
  94. package/framework/templates/REGISTRY.json +13 -0
  95. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  96. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  97. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  98. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  99. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +1 -1
  100. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  101. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  102. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  103. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  104. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  105. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  106. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  107. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  108. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  109. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +1 -1
  110. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  111. package/framework/templates/code/typescript/contracts.ts +167 -167
  112. package/framework/templates/docs/clarifications.md +253 -253
  113. package/framework/templates/docs/schema-analysis.md +119 -119
  114. package/framework/templates/docs/ui-components.md +52 -52
  115. package/framework/templates/docs/ui-design-system.md +76 -76
  116. package/framework/templates/docs/ui-flows.md +167 -167
  117. package/framework/templates/docs/ui-mockups.md +98 -98
  118. package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
  119. package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
  120. package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
  121. package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
  122. package/framework/templates/dotnet/ai-kit/README.md +128 -0
  123. package/framework/templates/dotnet/ai-kit/global.json +9 -0
  124. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
  125. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
  126. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
  127. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
  128. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
  129. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
  130. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
  131. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
  132. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
  133. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
  134. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
  135. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
  136. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
  137. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
  138. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
  139. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
  140. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
  141. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
  142. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
  143. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
  144. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
  145. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
  146. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
  147. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
  148. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
  149. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
  150. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
  151. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
  152. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
  153. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
  154. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
  155. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
  156. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
  157. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
  158. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
  159. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
  160. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
  161. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
  162. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
  163. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
  164. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
  165. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
  166. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
  167. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
  168. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
  169. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
  170. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
  171. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
  172. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
  173. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
  174. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
  175. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
  176. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
  177. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
  178. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
  179. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
  180. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
  181. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
  182. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
  183. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
  184. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
  185. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
  186. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
  187. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
  188. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
  189. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
  190. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
  191. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
  192. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
  193. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
  194. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
  195. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
  196. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
  197. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
  198. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
  199. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
  200. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
  201. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
  202. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
  203. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
  204. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
  205. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
  206. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
  207. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
  208. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
  209. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
  210. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
  211. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
  212. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
  213. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
  214. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
  215. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
  216. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
  217. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
  218. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
  219. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
  220. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
  221. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
  222. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
  223. package/framework/templates/feature/decisions.md +122 -122
  224. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  225. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  226. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  227. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  228. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  229. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  230. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  231. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  232. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  233. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  234. package/framework/templates/infrastructure/github/README.md +56 -56
  235. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  236. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  237. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  238. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  239. package/framework/templates/integrations/asaas-client.cs +387 -387
  240. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  241. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  242. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  243. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  244. package/framework/templates/project/validate.js +120 -120
  245. package/framework/templates/state.template.json +17 -17
  246. package/framework/templates/ui/design-system.css +226 -226
  247. package/package.json +3 -3
  248. package/src/commands/dashboard/dashboard.js +53 -53
  249. package/src/commands/e2e/e2e.js +15 -2
  250. package/src/commands/index.js +13 -13
  251. package/src/commands/project/checkpoint.js +209 -209
  252. package/src/commands/project/doctor.js +148 -6
  253. package/src/commands/project/update.js +469 -153
  254. package/src/commands/project/worktree.js +76 -4
  255. package/src/commands/state/advance.js +24 -2
  256. package/src/commands/state/approve.js +33 -1
  257. package/src/commands/state/create.js +72 -11
  258. package/src/commands/state/finish.js +379 -24
  259. package/src/commands/state/gate-check.js +190 -6
  260. package/src/commands/state/score.js +36 -1
  261. package/src/commands/validation/verify.js +134 -25
  262. package/src/core/state/phase-state-machine.js +197 -197
  263. package/src/core/state/state-manager.js +9 -1
  264. package/src/core/templates/template-renderer.js +477 -477
  265. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  266. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  267. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  268. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  269. package/src/dashboard/app/main.jsx +13 -13
  270. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  271. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  272. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  273. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  274. package/src/dashboard/app/pages/Standards.jsx +332 -332
  275. package/src/dashboard/app/styles/index.css +47 -47
  276. package/src/dashboard/index.html +12 -12
  277. package/src/dashboard/package.json +24 -24
  278. package/src/dashboard/scanner/phases-scanner.js +15 -15
  279. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  280. package/src/dashboard/scanner/rules-scanner.js +37 -37
  281. package/src/dashboard/scanner/skills-scanner.js +43 -43
  282. package/src/dashboard/scanner/squads-scanner.js +13 -13
  283. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  284. package/src/dashboard/vite.config.js +24 -24
  285. package/src/lib/ai/ai-checks.js +182 -0
  286. package/src/lib/ai/ai-project-scan.js +347 -0
  287. package/src/lib/ai/code-audit.js +181 -0
  288. package/src/lib/ai/comment-stripper.js +184 -0
  289. package/src/lib/ai/pin-drift.js +138 -0
  290. package/src/lib/ai/registry-audit.js +285 -0
  291. package/src/lib/detectors/design-system-detector.js +188 -188
  292. package/src/lib/detectors/index.js +217 -217
  293. package/src/lib/e2e/compose-env.js +50 -4
  294. package/src/lib/evaluator-report.js +269 -15
  295. package/src/lib/generators/design-system-generator.js +508 -508
  296. package/src/lib/generators/settings-generator.js +209 -209
  297. package/src/lib/git/default-branch.js +92 -4
  298. package/src/lib/git/pr-files.js +56 -0
  299. package/src/lib/hooks/hook-executor.js +167 -167
  300. package/src/lib/installers/effects.js +193 -0
  301. package/src/lib/installers/mcp-installer.js +719 -719
  302. package/src/lib/installers/templates-reconcile.js +3 -3
  303. package/src/lib/risk-detector.js +284 -0
  304. package/src/lib/standards/verified-against.js +276 -0
  305. package/src/lib/tasks/eval-node.js +485 -0
  306. package/src/lib/tasks/linux-runner.js +297 -0
  307. package/src/lib/tasks/task-classifier.js +30 -30
  308. package/src/lib/tasks/task-parser.js +209 -209
  309. package/src/lib/tasks/test-nodes.js +486 -0
  310. package/src/lib/tasks/test-runner.js +410 -33
  311. package/src/lib/validators/checkpoint-persistence.js +39 -39
  312. package/src/lib/validators/css/css-validator.js +340 -340
  313. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  314. package/src/lib/validators/shared/issue-counter.js +14 -14
  315. package/src/lib/validators/verify-runner.js +583 -28
  316. package/src/lib/worktree/docker-audit.js +287 -0
  317. package/src/lib/worktree/env-allocator.js +24 -0
  318. package/src/lib/worktree/worktree-audit.js +30 -1
  319. package/src/scripts/setup-infra.js +24 -11
  320. package/src/utils/agent-id-mapper.js +11 -11
  321. package/src/utils/agents-installer.js +18 -8
  322. package/src/utils/claude-md-injector.js +10 -5
  323. package/src/utils/claude-settings-manager.js +300 -300
  324. package/src/utils/file-copier.js +11 -4
  325. package/src/utils/hooks-installer.js +29 -26
  326. package/src/utils/skills-installer.js +36 -29
  327. package/src/utils/version-checker.js +68 -3
  328. package/framework/standards/ai-agents/prompts-in-db.md +0 -214
@@ -11,11 +11,15 @@ import {
11
11
  buildBranchName,
12
12
  branchExists,
13
13
  cwdIsInsideWorktree,
14
+ sessionAnchoredInWorktree,
15
+ classifyIgnoredLeftovers,
14
16
  } from '../project/worktree.js';
15
17
  import { detectDefaultBranch, hasOriginRemote } from '../../lib/git/default-branch.js';
18
+ import { commitsAhead, resolveBaseRef } from '../../lib/git/branch-classifier.js';
16
19
  import { resolveGh } from '../../lib/git/gh.js';
17
20
  import { releaseClaim } from '../../../framework/hooks/shared/claim-registry.js';
18
21
  import { tearDownFeatureStack } from '../../lib/e2e/stack-teardown.js';
22
+ import { findReparsePoints } from '../../../framework/hooks/shared/reparse-scan.js';
19
23
  import { runArchive } from './archive.js';
20
24
 
21
25
  /** workType → conventional-commit type. `hotfix` IS a fix; anything else is a chore. */
@@ -84,8 +88,14 @@ export function buildPrTitle(feature, fj) {
84
88
  * `resumed: true` in its output — see Validation 0c.
85
89
  *
86
90
  * @param {string} feature
87
- * @param {{ pr?: boolean, merge?: boolean, keep?: boolean, force?: boolean,
88
- * discardIgnored?: boolean, cwd?: string }} [options]
91
+ * `--dry-run` desvia para {@link finishPreflight} ANTES de qualquer validação
92
+ * que saia com exit 1 e ANTES de qualquer mutação: o pré-voo existe para
93
+ * EXPLICAR, e um pré-voo que recusa pelos mesmos motivos do comando real não
94
+ * explica nada.
95
+ *
96
+ * @param {{ pr?: boolean, merge?: boolean, keep?: boolean, keepVolumes?: boolean,
97
+ * push?: boolean, force?: boolean, discardIgnored?: boolean, dryRun?: boolean,
98
+ * cwd?: string }} [options]
89
99
  */
90
100
  export async function finishCommand(feature, options = {}) {
91
101
  const cwd = options.cwd || process.cwd();
@@ -136,6 +146,26 @@ export async function finishCommand(feature, options = {}) {
136
146
  process.exit(1);
137
147
  }
138
148
  const mode = wantPr ? 'pr' : 'merge';
149
+
150
+ // `--push` publica a DEFAULT depois de um merge local; o fluxo `--pr` não tem
151
+ // merge local nenhum (a default só recebe a feature quando o PR mergear), e
152
+ // já publica `morph/{f}` por conta própria. A combinação é um pedido que o
153
+ // comando não consegue honrar — e aceitar em silêncio o que não se pode
154
+ // honrar é exatamente o defeito que esta série de fixes existe para tirar do
155
+ // CLI (o `score` que gravava um zero fabricado, o `approve` que saía 0 sem
156
+ // aprovar nada). Recusa ALTA, e AQUI: junto do gate de modo, antes do
157
+ // archive — que é a primeira mutação do finish. Uma recusa depois dele
158
+ // deixaria a feature meio fechada, pior que o no-op que ela substitui.
159
+ if (wantPr && options.push) {
160
+ emit({
161
+ finished: false,
162
+ error: 'push-with-pr',
163
+ hint: `--push não se combina com --pr: o --pr já publica ${buildBranchName(feature)} e abre o PR, enquanto o --push publica a branch default depois de um merge LOCAL — e no fluxo --pr não há merge local. Para publicar a branch e abrir o PR: \`morph-spec finish ${feature} --pr\`. Para integrar na default localmente e publicá-la: \`morph-spec finish ${feature} --merge --push\`.`,
164
+ feature,
165
+ });
166
+ process.exit(1);
167
+ }
168
+
139
169
  const expectedBranch = buildBranchName(feature);
140
170
 
141
171
  // ── Resolve the flow ──────────────────────────────────────────────────────
@@ -149,6 +179,29 @@ export async function finishCommand(feature, options = {}) {
149
179
  let worktreePath = null;
150
180
  let featureCwd = root; // where the feature's own tree (and Gate 3) lives
151
181
 
182
+ // ── PRÉ-VOO ───────────────────────────────────────────────────────────────
183
+ // AQUI, e não mais abaixo: as validações seguintes saem com exit 1 (branch
184
+ // divergente, cwd dentro do worktree, feature ausente), e o pré-voo existe
185
+ // justamente para ser rodado ANTES de o estado estar bom. Ele reporta o que
186
+ // encontrar — inclusive o cwd dentro do worktree, como AVISO — e sai 0 sem
187
+ // ter tocado em nada.
188
+ if (options.dryRun) {
189
+ emit(finishPreflight({
190
+ feature,
191
+ root,
192
+ worktreePath: isWorktreeFlow ? found.path : null,
193
+ cwd,
194
+ mode,
195
+ branch: expectedBranch,
196
+ // `options` INTEIRO, e não um subconjunto escolhido a dedo: `--discard-ignored`
197
+ // INVERTE a classificação (o exclusivo deixa de ser copiado e passa a ser
198
+ // destruído), então um pré-voo que não recebe as flags do comando prevê
199
+ // outro comando. Ver B1 na errata E4.
200
+ options,
201
+ }));
202
+ return;
203
+ }
204
+
152
205
  if (isWorktreeFlow) {
153
206
  // Validation 0: the worktree must actually be ON the branch we will merge.
154
207
  if (found.branch !== found.expectedBranch) {
@@ -441,6 +494,174 @@ export async function finishCommand(feature, options = {}) {
441
494
  return integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch });
442
495
  }
443
496
 
497
+ /**
498
+ * O aviso que acompanha CADA reparse point listado pelo pré-voo.
499
+ *
500
+ * Exportado porque é a frase que carrega o fato: uma junction/symlink dentro do
501
+ * worktree aponta para conteúdo que vive na RAIZ, e `rm -rf` / `git worktree
502
+ * remove --force` atravessam o link. Já aconteceu: um `worktree remove --force`
503
+ * atravessou a junction e apagou o `node_modules` da raiz.
504
+ */
505
+ export const REPARSE_WARNING =
506
+ 'estes são junctions/symlinks: apagá-los fora do morph-spec apaga o conteúdo REAL na raiz';
507
+
508
+ /**
509
+ * `morph-spec finish <feature> --pr|--merge --dry-run` — o PRÉ-VOO.
510
+ *
511
+ * NÃO MUTA NADA. Nenhum commit, nenhum archive, nenhum worktree removido,
512
+ * nenhum `gh` que crie coisa alguma, nenhum `git fetch`. Ele responde, ANTES,
513
+ * as três perguntas que hoje só se respondem depois do estrago:
514
+ *
515
+ * 1. O que este `finish` vai COPIAR para o backup e o que vai DESCARTAR?
516
+ * (`classifyIgnoredLeftovers` — a MESMA função que o teardown consulta;
517
+ * um segundo enumerador aqui faria o pré-voo prometer o que o comando não
518
+ * cumpre, e um dry-run que mente é pior que nenhum.)
519
+ * 2. Que reparse points existem, e o que significa apagá-los por fora?
520
+ * (`findReparsePoints`, exaustivo e fail-closed.)
521
+ * 3. A base andou? — a pergunta INVERSA da de #89. Lá se mede "quantos
522
+ * commits EU tenho que o upstream não tem"; aqui, "quantos commits
523
+ * `origin/{default}` tem que a MINHA base não tem", que é o que decide se
524
+ * este merge vai ser trivial ou uma surpresa. Mesma função
525
+ * (`commitsAhead`), argumentos invertidos.
526
+ *
527
+ * `cwd` dentro do worktree é AVISO, não recusa: o caminho real continua
528
+ * recusando antes da primeira mutação (Validation 0b), e recusar aqui negaria
529
+ * ao usuário justamente a explicação que ele veio buscar — do lugar mais
530
+ * provável de estar quando a pede.
531
+ *
532
+ * SEM `git fetch` implícito: um fetch dentro de um comando que promete não
533
+ * mexer em nada é efeito colateral de rede. O pré-voo AVISA que o dado pode
534
+ * estar velho e sugere o fetch.
535
+ *
536
+ * @param {{ feature: string, root: string, worktreePath: string|null,
537
+ * cwd: string, mode: 'pr'|'merge', branch: string,
538
+ * options?: { discardIgnored?: boolean, keep?: boolean } }} args
539
+ * @returns {object} o payload emitido — nunca lança
540
+ */
541
+ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch, options = {} }) {
542
+ const warnings = [];
543
+
544
+ // ── A base andou? (vale COM e SEM worktree — UC-16 A1) ────────────────────
545
+ const defaultBranch = detectDefaultBranch(root);
546
+ const baseRef = defaultBranch ? resolveBaseRef(defaultBranch, root) : null;
547
+ // `commitsAhead(base, branch)` conta `base..branch`. Passando a branch da
548
+ // feature como BASE e o baseRef como BRANCH, contamos o que a default tem e a
549
+ // feature não — exatamente a pergunta inversa.
550
+ const commits = baseRef && branchExists(branch, root)
551
+ ? commitsAhead(branch, baseRef, root)
552
+ : null;
553
+
554
+ const out = {
555
+ preflight: true,
556
+ finished: false,
557
+ feature,
558
+ mode,
559
+ branch,
560
+ baseAdvanced: { ref: baseRef, commits },
561
+ // `null` é DESCONHECIDO e nunca é renderizado como 0 (UC-16 P6): "não deu
562
+ // para perguntar" e "a default não andou" são fatos opostos, e confundi-los
563
+ // é como um `--dry-run` passa confiança que não tem.
564
+ baseAdvancedNote: commits === null
565
+ ? (baseRef === null
566
+ ? 'sem contraparte remota nem branch default resolvível — a contagem é DESCONHECIDA, não zero'
567
+ : 'não foi possível comparar as refs — a contagem é DESCONHECIDA, não zero')
568
+ : `o dado é do último \`git fetch\`; rode \`git fetch ${PUSH_REMOTE}\` para atualizá-lo (o pré-voo não faz fetch: ele promete não mexer em nada)`,
569
+ warnings,
570
+ wouldDo: mode === 'merge'
571
+ ? [
572
+ `arquivar .morph/features/${feature} em .morph/archive/${feature} (commit na branch ${branch})`,
573
+ `fazer merge --no-ff de ${branch} em ${defaultBranch || '<default não resolvida>'} LOCALMENTE (sem push)`,
574
+ ...(worktreePath ? [`derrubar o stack e remover o worktree ${worktreePath.replace(/\\/g, '/')}${options.discardIgnored === true ? ' — SEM copiar os gitignored exclusivos para o backup (--discard-ignored)' : ''}`] : []),
575
+ ]
576
+ : [
577
+ `arquivar .morph/features/${feature} em .morph/archive/${feature} (commit na branch ${branch})`,
578
+ `publicar ${branch} em ${PUSH_REMOTE} e abrir um PR contra ${defaultBranch || '<default não resolvida>'} (NÃO integra: o merge do PR é que encerra)`,
579
+ ...(worktreePath ? [`derrubar o stack e remover o worktree ${worktreePath.replace(/\\/g, '/')}${options.discardIgnored === true ? ' — SEM copiar os gitignored exclusivos para o backup (--discard-ignored)' : ''}`] : []),
580
+ ],
581
+ };
582
+
583
+ if (defaultBranch) out.defaultBranch = defaultBranch;
584
+
585
+ // ── Seções de worktree — omitidas quando não há worktree (UC-16 A1) ───────
586
+ if (worktreePath) {
587
+ out.worktreePath = worktreePath.replace(/\\/g, '/');
588
+
589
+ const inside = cwdIsInsideWorktree(cwd, worktreePath);
590
+ out.cwdInsideWorktree = inside;
591
+ if (!inside && sessionAnchoredInWorktree(worktreePath)) {
592
+ out.teardownDeferred = 'session-anchored';
593
+ warnings.push(
594
+ `a sessão do Claude Code está ancorada em ${out.worktreePath}: o finish vai integrar, mas ADIAR o desmonte `
595
+ + '(desmontar agora quebraria os hooks desta sessão). Depois, feche a sessão e rode '
596
+ + `\`morph-spec worktree remove ${feature}\` de uma sessão aberta na raiz`,
597
+ );
598
+ }
599
+ if (inside) {
600
+ warnings.push(
601
+ `você está DENTRO de ${out.worktreePath}: o finish de verdade recusa aqui, antes de qualquer mutação `
602
+ + `(o Windows tranca o diretório de trabalho do processo e a remoção falharia no meio). `
603
+ + `Rode o finish a partir de "${root.replace(/\\/g, '/')}"`,
604
+ );
605
+ }
606
+
607
+ // AS MESMAS FLAGS do comando real. `--discard-ignored` INVERTE a
608
+ // classificação — o exclusivo deixa de ser copiado e entra no lote
609
+ // destruído sem backup — então prever com `options` vazio é prever OUTRO
610
+ // comando. Medido antes do conserto: o pré-voo prometia salvar caminhos que
611
+ // o `--discard-ignored` destruía. Normalizado com `=== true` para que
612
+ // `undefined` e `false` sejam a mesma coisa, como no `worktree remove`.
613
+ const discardIgnored = options.discardIgnored === true;
614
+ const ignored = classifyIgnoredLeftovers(worktreePath, root, { discardIgnored });
615
+ out.discardIgnored = discardIgnored;
616
+ out.ignored = {
617
+ // COPIADO para .git/morph-spec/finish-backup antes da remoção: existe só
618
+ // aqui, ou difere do que está na raiz. Com `--discard-ignored` esta lista
619
+ // sai VAZIA por construção — a flag significa "pule o backup".
620
+ shieldable: ignored.shieldable,
621
+ // Idêntico byte a byte ao da raiz (SHA-256): apagar não perde nada.
622
+ replicable: ignored.replicable,
623
+ // Artefato regenerável + os replicáveis (+ o exclusivo, sob a flag):
624
+ // descartado sem backup.
625
+ discardable: ignored.discarded,
626
+ };
627
+ if (ignored.blocking.length) {
628
+ out.ignored.blocking = ignored.blocking;
629
+ warnings.push('não foi possível enumerar os gitignored do worktree — o finish real vai recusar a remoção');
630
+ }
631
+
632
+ if (discardIgnored) {
633
+ // SEGUNDA leitura, deliberada e só aqui: sob a flag, `shieldable` sai
634
+ // vazia e a informação mais valiosa do pré-voo — QUAIS destes existem só
635
+ // aqui — desapareceria justamente no modo destrutivo. Perguntar à MESMA
636
+ // função, sem a flag, é a única forma de nomeá-los sem recriar a regra
637
+ // (uma segunda cópia da triagem é o defeito que o export de
638
+ // `classifyIgnoredLeftovers` existe para evitar).
639
+ const semFlag = classifyIgnoredLeftovers(worktreePath, root);
640
+ out.ignored.discardableExclusive = semFlag.shieldable;
641
+ if (semFlag.shieldable.length) {
642
+ warnings.push(
643
+ `--discard-ignored: ${semFlag.shieldable.length} caminho(s) gitignored existem SÓ neste worktree e serão `
644
+ + `DESTRUÍDOS sem cópia (${semFlag.shieldable.slice(0, 5).join(', ')}). Sem a flag eles iriam para `
645
+ + '.git/morph-spec/finish-backup e a remoção aconteceria do mesmo jeito.',
646
+ );
647
+ }
648
+ }
649
+
650
+ const scan = findReparsePoints(worktreePath);
651
+ out.reparsePoints = scan.points;
652
+ if (scan.unprovable.length) out.reparseUnprovable = scan.unprovable;
653
+ out.reparseWarning = REPARSE_WARNING;
654
+ if (scan.points.length) {
655
+ warnings.push(
656
+ `${scan.points.length} reparse point(s) no worktree — ${REPARSE_WARNING}. `
657
+ + 'O `morph-spec worktree remove` derruba os links ANTES de apagar; um `git worktree remove --force` cru, não.',
658
+ );
659
+ }
660
+ }
661
+
662
+ return out;
663
+ }
664
+
444
665
  // ── merge integration (local, no push) ───────────────────────────────────────
445
666
 
446
667
  async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch }) {
@@ -476,16 +697,15 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
476
697
  process.exit(1);
477
698
  }
478
699
 
700
+ // Uma medição só para as DUAS saídas deste merge (--keep e normal): é o
701
+ // mesmo merge, então a contagem e a publicação têm de ser as mesmas.
702
+ const publish = publicationState({ root, defaultBranch, push: options.push });
703
+
479
704
  if (options.keep) {
480
705
  dropFromRootIndex(root, feature);
481
- // Worktree preserved on purpose: the root's `worktree` registration stays
482
- // VALID (it still names a live directory), so nothing to clean up here —
483
- // only report where it lives and how to close it later.
484
706
  emit({
485
- finished: true, integrated: true, mode: 'merge', feature, merged: expectedBranch, kept: true, worktreeRemoved: false,
486
- worktree: { path: worktreePath, branch: expectedBranch },
487
- hint: `worktree preservado em ${worktreePath} — feche depois com \`morph-spec worktree remove ${feature}\`.`,
488
- ...backupNote, ...ucNote,
707
+ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, shedBackup, kept: true }),
708
+ ...ucNote,
489
709
  });
490
710
  process.exit(0);
491
711
  }
@@ -494,9 +714,16 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
494
714
  // mount and the port block all belong to that tree, and once it is gone
495
715
  // there is nothing left to address the containers by.
496
716
  const e2e = reclaimStack(feature, { root, worktreePath, options });
497
- const teardown = await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: true });
717
+ // The session that ran this finish may be ANCHORED in the worktree (its
718
+ // hooks resolve `.morph/framework` through it): tearing it down now would
719
+ // kill every hook of that session. The integration is done; only the
720
+ // teardown waits (see sessionAnchoredInWorktree).
721
+ const deferred = sessionAnchoredInWorktree(worktreePath);
722
+ const teardown = deferred
723
+ ? null
724
+ : await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: true });
498
725
  dropFromRootIndex(root, feature);
499
- emit({ ...mergeOutput({ feature, branch: expectedBranch, worktreePath, teardown, shedBackup }), ...ucNote, e2e });
726
+ emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, teardown, shedBackup, deferred }), ...ucNote, e2e });
500
727
  process.exit(0);
501
728
  }
502
729
 
@@ -524,9 +751,10 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
524
751
  process.exit(1);
525
752
  }
526
753
  tryGit(['branch', '-d', expectedBranch], root);
754
+ const publish = publicationState({ root, defaultBranch, push: options.push });
527
755
  const e2e = reclaimStack(feature, { root, worktreePath: null, options });
528
756
  // The root index (gitignored) was already updated by the archive (same cwd).
529
- emit({ finished: true, integrated: true, mode: 'merge', feature, merged: expectedBranch, into: defaultBranch, worktreeRemoved: false, nextStep: MERGE_NEXT_STEP, ...ucNote, e2e });
757
+ emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish }), ...ucNote, e2e });
530
758
  process.exit(0);
531
759
  }
532
760
 
@@ -575,6 +803,7 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
575
803
  const e2e = isWorktreeFlow
576
804
  ? (options.keep ? null : reclaimStack(feature, { root, worktreePath, options }))
577
805
  : reclaimStack(feature, { root, worktreePath: null, options });
806
+ let teardownDeferred = false;
578
807
  if (isWorktreeFlow && !options.keep) {
579
808
  // Shed the transient root-side scaffold BEFORE teardown — exactly as --merge
580
809
  // does. The real `create --worktree` leaves the root's .morph/features/{f}
@@ -584,7 +813,13 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
584
813
  // the feature in BOTH features/ and archive/ at once — active and archived.
585
814
  // The branch/PR owns the authoritative copy; the root scaffold is transient.
586
815
  shedBackup = shedRootFeatureScope(root, feature).shedBackup;
587
- teardown = await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: false });
816
+ // Same deferral as --merge: never tear down the worktree the running
817
+ // session is anchored in (see sessionAnchoredInWorktree).
818
+ if (sessionAnchoredInWorktree(worktreePath)) {
819
+ teardownDeferred = true;
820
+ } else {
821
+ teardown = await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: false });
822
+ }
588
823
  // The shed removed the root's features/{f} scaffold from disk, but runArchive
589
824
  // dropped the feature only from the WORKTREE's index (its cwd). Drop it from
590
825
  // the ROOT index too — exactly as --merge does — or `doctor`'s lifecycle audit
@@ -623,7 +858,13 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
623
858
  worktree: { path: worktreePath, branch: expectedBranch },
624
859
  hint: `worktree preservado em ${worktreePath} — feche depois com \`morph-spec worktree remove ${feature}\`.`,
625
860
  }
626
- : {}),
861
+ : teardownDeferred
862
+ ? {
863
+ teardownDeferred: 'session-anchored',
864
+ worktree: { path: worktreePath, branch: expectedBranch },
865
+ hint: deferredTeardownHint(feature, worktreePath),
866
+ }
867
+ : {}),
627
868
  }),
628
869
  };
629
870
  emit(out);
@@ -647,28 +888,142 @@ function promotedNote(arch) {
647
888
  }
648
889
 
649
890
  /**
650
- * O comando se chama `finish`, e no modo `--merge` ele NÃO publica nada: o
651
- * merge é local, o remoto continua sem a feature. Quem lê "finished: true"
652
- * conclui o contrário — foi pegadinha repetida em sessões diferentes. O modo
653
- * `--pr` empurra e abre o PR; o `--merge` deixa o push para o humano, e é isso
654
- * que esta linha diz em voz alta.
891
+ * O único remoto com que o `finish` conversa — o mesmo que `hasOriginRemote` e
892
+ * o `resolveBaseRef` do branch-classifier assumem. Um nome só, aqui, para que o
893
+ * comando que a saída IMPRIME e o push que o `--push` EXECUTA jamais possam
894
+ * nomear remotos diferentes.
895
+ */
896
+ const PUSH_REMOTE = 'origin';
897
+
898
+ /**
899
+ * O que sobrou por publicar depois que o merge local aterrissou — e, com
900
+ * `--push`, o resultado da publicação.
901
+ *
902
+ * A contagem REUSA `resolveBaseRef` + `commitsAhead` (branch-classifier), em vez
903
+ * de uma terceira cópia de `rev-list --count`. Detalhe que decide o P4:
904
+ * `resolveBaseRef` degrada para a branch LOCAL quando não existe ref de
905
+ * rastreamento remoto — comparar a branch consigo mesma devolveria um `0` de
906
+ * aparência honesta para um repo que nunca deu push. Por isso a comparação só
907
+ * vale quando o ref resolvido é diferente do nome da branch; caso contrário
908
+ * `ahead` fica `null` ("incomparável"), nunca zero.
909
+ *
910
+ * Fail-open por desenho: o merge JÁ aconteceu quando esta função roda, então
911
+ * nenhum resultado daqui — nem um push que falhou — pode transformar um sucesso
912
+ * em exit 1. A falha vira `pushError` no payload.
913
+ *
914
+ * @param {{ root: string, defaultBranch: string, push?: boolean }} ctx
915
+ * @returns {{ remote: string, branch: string, ahead: number|null, pushed: boolean,
916
+ * published?: number|null, pushError?: string }}
917
+ */
918
+ function publicationState({ root, defaultBranch, push }) {
919
+ const baseRef = resolveBaseRef(defaultBranch, root);
920
+ const tracked = !!baseRef && baseRef !== defaultBranch;
921
+ const ahead = tracked ? commitsAhead(baseRef, defaultBranch, root) : null;
922
+ const state = { remote: PUSH_REMOTE, branch: defaultBranch, ahead, pushed: false };
923
+
924
+ if (!push) return state;
925
+
926
+ if (!hasOriginRemote(root)) {
927
+ return { ...state, pushError: 'no-origin-remote' };
928
+ }
929
+ try {
930
+ execFileSync('git', ['push', PUSH_REMOTE, defaultBranch], { cwd: root, stdio: 'pipe' });
931
+ } catch (err) {
932
+ return { ...state, pushError: gitErr(err) };
933
+ }
934
+ // Re-medir DEPOIS do push: o que ficou para trás é a única prova de que a
935
+ // publicação foi completa, e um `ahead` congelado do antes seria mentira.
936
+ const after = resolveBaseRef(defaultBranch, root);
937
+ const remaining = after && after !== defaultBranch ? commitsAhead(after, defaultBranch, root) : null;
938
+ return { ...state, published: ahead, ahead: remaining, pushed: true };
939
+ }
940
+
941
+ /**
942
+ * A frase do próximo passo — PRODUTOR ÚNICO.
943
+ *
944
+ * O comando se chama `finish`, e no modo `--merge` ele NÃO publica nada por
945
+ * conta própria: o merge é local, o remoto continua sem a feature. Quem lê
946
+ * "finished: true" conclui o contrário — foi pegadinha repetida em sessões
947
+ * diferentes. Três saídas de `--merge` montavam esse aviso por conta própria, e
948
+ * uma delas (o `--keep`) simplesmente esquecia dele; um produtor único é o que
949
+ * impede a divergência de voltar.
950
+ *
951
+ * @param {ReturnType<typeof publicationState>} publish
952
+ * @returns {string}
655
953
  */
656
- const MERGE_NEXT_STEP = 'integração LOCAL — nada foi publicado. Rode `git push` para levar ao remoto.';
954
+ function mergeNextStep(publish) {
955
+ const { remote, branch, ahead, pushed, published, pushError } = publish;
956
+ const pushCmd = `\`git push ${remote} ${branch}\``;
957
+
958
+ if (pushed) {
959
+ const n = typeof published === 'number' ? `${published} commit(s)` : 'a integração';
960
+ return `PUBLICADO — ${n} de ${branch} enviado(s) para ${remote}/${branch} pelo --push.`;
961
+ }
962
+ if (pushError === 'no-origin-remote') {
963
+ return `integração LOCAL — nada foi publicado: --push foi pedido mas não há remoto \`${remote}\` configurado.`;
964
+ }
965
+ if (pushError) {
966
+ return `integração LOCAL — o push pedido por --push FALHOU e nada foi publicado. Rode ${pushCmd} manualmente.`;
967
+ }
968
+ // Sem contraparte remota não se inventa contagem (P4): fica a frase estática.
969
+ if (ahead === null) {
970
+ return 'integração LOCAL — nada foi publicado. Rode `git push` para levar ao remoto.';
971
+ }
972
+ return `integração LOCAL — nada foi publicado: ${branch} está ${ahead} commit(s) à frente de ${remote}/${branch}. Rode ${pushCmd} para levar ao remoto (ou re-execute o finish com --push).`;
973
+ }
657
974
 
658
- function mergeOutput({ feature, branch, worktreePath, teardown, shedBackup }) {
975
+ /**
976
+ * CONSTRUTOR ÚNICO da saída de sucesso do `--merge`. As três emissões (worktree
977
+ * normal, worktree `--keep` e fluxo comum) passam por aqui — era a terceira
978
+ * cópia, montada à mão no `--keep`, que saía sem `nextStep` nenhum.
979
+ *
980
+ * `teardown === null` cobre os dois casos sem desmonte: o `--keep` (worktree
981
+ * vivo de propósito) e o fluxo comum (não há worktree algum).
982
+ *
983
+ * @param {{ feature: string, branch: string, defaultBranch: string,
984
+ * publish: ReturnType<typeof publicationState>, worktreePath?: string|null,
985
+ * teardown?: object|null, shedBackup?: string|null, kept?: boolean }} args
986
+ */
987
+ function mergeOutput({ feature, branch, defaultBranch, publish, worktreePath = null, teardown = null, shedBackup = null, kept = false, deferred = false }) {
659
988
  return {
660
989
  finished: true,
661
990
  integrated: true,
662
991
  mode: 'merge',
663
992
  feature,
664
993
  merged: branch,
665
- worktreeRemoved: teardown.worktreeRemoved,
666
- nextStep: MERGE_NEXT_STEP,
994
+ into: defaultBranch,
995
+ ...(kept ? { kept: true } : {}),
996
+ ...(deferred ? { teardownDeferred: 'session-anchored', worktree: { path: worktreePath, branch }, hint: deferredTeardownHint(feature, worktreePath, { deleteBranch: true }) } : {}),
997
+ worktreeRemoved: teardown ? teardown.worktreeRemoved : false,
998
+ nextStep: mergeNextStep(publish),
999
+ publish,
667
1000
  ...(shedBackup ? { shedBackup } : {}),
668
- ...teardownExtras(teardown, worktreePath, branch, true),
1001
+ ...(kept
1002
+ ? {
1003
+ // Worktree preserved on purpose: the root's `worktree` registration stays
1004
+ // VALID (it still names a live directory), so nothing to clean up here —
1005
+ // only report where it lives and how to close it later.
1006
+ worktree: { path: worktreePath, branch },
1007
+ hint: `worktree preservado em ${worktreePath} — feche depois com \`morph-spec worktree remove ${feature}\`.`,
1008
+ }
1009
+ : {}),
1010
+ ...(teardown ? teardownExtras(teardown, worktreePath, branch, true) : {}),
669
1011
  };
670
1012
  }
671
1013
 
1014
+ /**
1015
+ * Hint for a teardown deferred because the session is anchored in the worktree
1016
+ * (both modes). Names the safe command, never `git worktree remove`. `--merge`
1017
+ * would have deleted the (now integrated) branch, so its hint carries
1018
+ * `--delete-branch`; `--pr` keeps the branch — the PR needs it.
1019
+ */
1020
+ function deferredTeardownHint(feature, worktreePath, { deleteBranch = false } = {}) {
1021
+ return `worktree mantido em ${worktreePath}: a sessão do Claude Code que rodou o finish está ancorada nele, `
1022
+ + 'e desmontá-lo agora quebraria todos os hooks dela (eles resolvem `.morph/framework` por ele) — '
1023
+ + `o diretório, aliás, seguiria travado pela sessão. Feche esta sessão e rode \`morph-spec worktree remove ${feature}${deleteBranch ? ' --delete-branch' : ''}\` `
1024
+ + 'de uma sessão aberta na raiz do repo.';
1025
+ }
1026
+
672
1027
  /** Shared teardown reporting for both modes (kept identical to the old finish). */
673
1028
  function teardownExtras(teardown, worktreePath, branch, deleteBranch) {
674
1029
  const out = {};