@polymorphism-tech/morph-spec 8.33.0 → 8.35.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 (253) hide show
  1. package/CLAUDE.md +22 -2
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +43 -5
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +5 -4
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +2 -2
  8. package/framework/MORPH.md +20 -22
  9. package/framework/agents.json +198 -44
  10. package/framework/ai-pin.json +22 -0
  11. package/framework/commands/morph-media.md +45 -0
  12. package/framework/docs/CLI.md +238 -10
  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/README.md +11 -1
  25. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +5 -8
  26. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +79 -0
  27. package/framework/hooks/claude-code/core/chain.js +27 -7
  28. package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
  29. package/framework/hooks/claude-code/core/worktree-context.js +83 -14
  30. package/framework/hooks/claude-code/core/worktree-create.js +206 -0
  31. package/framework/hooks/claude-code/core/worktree-env-inject.js +87 -0
  32. package/framework/hooks/claude-code/core/worktree-remove.js +81 -0
  33. package/framework/hooks/shared/command-tokens.js +23 -0
  34. package/framework/hooks/shared/e2e-check.js +0 -0
  35. package/framework/hooks/shared/git-paths.js +25 -0
  36. package/framework/hooks/shared/hook-response.js +17 -0
  37. package/framework/hooks/shared/morph-cli.js +134 -0
  38. package/framework/rules/ai-agents-standards.md +14 -2
  39. package/framework/rules/infrastructure-standards.md +3 -1
  40. package/framework/schemas/ai-pin.schema.json +97 -0
  41. package/framework/schemas/config.schema.json +107 -0
  42. package/framework/schemas/feature.schema.json +23 -4
  43. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  44. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  45. package/framework/skills/core/morph-apply/SKILL.md +28 -7
  46. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  47. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  48. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  49. package/framework/skills/core/morph-implement/SKILL.md +20 -2
  50. package/framework/skills/core/morph-media/SKILL.md +215 -0
  51. package/framework/skills/core/morph-plan/SKILL.md +38 -4
  52. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  53. package/framework/skills/core/morph-review/SKILL.md +166 -9
  54. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  55. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  56. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  57. package/framework/standards/PROVENANCE.json +81 -3
  58. package/framework/standards/STANDARDS.json +1504 -830
  59. package/framework/standards/ai-agents/agent-archetypes.md +160 -0
  60. package/framework/standards/ai-agents/agent-session.md +256 -160
  61. package/framework/standards/ai-agents/agent-spec.md +196 -0
  62. package/framework/standards/ai-agents/batching.md +217 -193
  63. package/framework/standards/ai-agents/context-providers.md +261 -188
  64. package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
  65. package/framework/standards/ai-agents/cost-and-budget.md +256 -0
  66. package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
  67. package/framework/standards/ai-agents/evals-with-cache.md +289 -0
  68. package/framework/standards/ai-agents/guardrails.md +222 -0
  69. package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
  70. package/framework/standards/ai-agents/mcp-server.md +351 -250
  71. package/framework/standards/ai-agents/mcp-tools.md +250 -197
  72. package/framework/standards/ai-agents/media-pipeline.md +352 -0
  73. package/framework/standards/ai-agents/media-video.md +267 -0
  74. package/framework/standards/ai-agents/middleware-patterns.md +241 -207
  75. package/framework/standards/ai-agents/modalities-image-gen.md +422 -173
  76. package/framework/standards/ai-agents/modalities-speech-to-text.md +177 -171
  77. package/framework/standards/ai-agents/modalities-text-to-speech.md +189 -181
  78. package/framework/standards/ai-agents/modalities-vision.md +205 -201
  79. package/framework/standards/ai-agents/multi-agent-patterns.md +211 -155
  80. package/framework/standards/ai-agents/observability-patterns.md +227 -17
  81. package/framework/standards/ai-agents/production.md +260 -231
  82. package/framework/standards/ai-agents/prompt-sources.md +389 -0
  83. package/framework/standards/ai-agents/providers/model-registry.md +293 -158
  84. package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
  85. package/framework/standards/ai-agents/service-tier-flex.md +143 -135
  86. package/framework/standards/ai-agents/setup.md +166 -50
  87. package/framework/standards/ai-agents/structured-output.md +280 -249
  88. package/framework/standards/ai-agents/sweet-spot.md +129 -109
  89. package/framework/standards/ai-agents/testing-ai.md +236 -0
  90. package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
  91. package/framework/standards/ai-agents/workflows.md +27 -7
  92. package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
  93. package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
  94. package/framework/standards/infrastructure/docker/local-compose-isolation.md +90 -0
  95. package/framework/templates/REGISTRY.json +14 -1
  96. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +95 -95
  97. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +58 -58
  98. package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
  99. package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
  100. package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
  101. package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
  102. package/framework/templates/dotnet/ai-kit/README.md +128 -0
  103. package/framework/templates/dotnet/ai-kit/global.json +9 -0
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
  110. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
  111. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
  112. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
  113. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
  114. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
  115. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
  116. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
  117. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
  118. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
  119. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
  120. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
  121. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
  122. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
  123. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
  124. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
  125. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
  126. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
  127. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
  128. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
  129. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
  130. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
  131. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
  132. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
  133. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
  134. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
  135. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
  136. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
  137. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
  138. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
  139. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
  140. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
  141. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
  142. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
  143. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
  144. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
  145. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
  146. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
  147. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
  148. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
  149. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
  150. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
  151. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
  152. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
  153. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
  154. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
  155. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
  156. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
  157. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
  158. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
  159. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
  160. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
  161. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
  162. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
  163. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
  164. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
  165. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
  166. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
  167. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
  168. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
  169. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
  170. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
  171. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
  172. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
  173. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
  174. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
  175. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
  176. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
  177. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
  178. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
  179. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
  180. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
  181. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
  182. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
  183. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
  184. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
  185. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
  186. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
  187. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
  188. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
  189. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
  190. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
  191. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
  192. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
  193. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
  194. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
  195. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
  196. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
  197. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
  198. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
  199. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
  200. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
  201. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
  202. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
  203. package/framework/templates/infrastructure/docker/docker-compose.template.yml +24 -14
  204. package/package.json +3 -3
  205. package/src/commands/e2e/e2e.js +91 -40
  206. package/src/commands/project/doctor.js +143 -6
  207. package/src/commands/project/env.js +95 -0
  208. package/src/commands/project/update.js +469 -153
  209. package/src/commands/project/worktree.js +274 -53
  210. package/src/commands/state/advance.js +24 -2
  211. package/src/commands/state/approve.js +33 -1
  212. package/src/commands/state/create.js +72 -11
  213. package/src/commands/state/delete.js +8 -0
  214. package/src/commands/state/finish.js +360 -53
  215. package/src/commands/state/gate-check.js +190 -6
  216. package/src/commands/state/score.js +36 -1
  217. package/src/commands/validation/verify.js +134 -25
  218. package/src/core/state/state-manager.js +9 -1
  219. package/src/dashboard/app/pages/HookInspector.jsx +430 -418
  220. package/src/dashboard/scanner/hooks-scanner.js +5 -1
  221. package/src/lib/ai/ai-checks.js +182 -0
  222. package/src/lib/ai/ai-project-scan.js +347 -0
  223. package/src/lib/ai/code-audit.js +181 -0
  224. package/src/lib/ai/comment-stripper.js +184 -0
  225. package/src/lib/ai/pin-drift.js +138 -0
  226. package/src/lib/ai/registry-audit.js +285 -0
  227. package/src/lib/e2e/compose-env.js +55 -14
  228. package/src/lib/e2e/stack-teardown.js +115 -0
  229. package/src/lib/evaluator-report.js +269 -15
  230. package/src/lib/generators/architecture-scanner.js +2 -1
  231. package/src/lib/git/default-branch.js +92 -4
  232. package/src/lib/graph/graphifyignore.js +2 -1
  233. package/src/lib/installers/effects.js +193 -0
  234. package/src/lib/installers/templates-reconcile.js +3 -3
  235. package/src/lib/risk-detector.js +284 -0
  236. package/src/lib/standards/verified-against.js +276 -0
  237. package/src/lib/tasks/eval-node.js +485 -0
  238. package/src/lib/tasks/linux-runner.js +297 -0
  239. package/src/lib/tasks/test-nodes.js +486 -0
  240. package/src/lib/tasks/test-runner.js +431 -37
  241. package/src/lib/validators/verify-runner.js +590 -28
  242. package/src/lib/worktree/dirs.js +32 -0
  243. package/src/lib/worktree/docker-audit.js +287 -0
  244. package/src/lib/worktree/env-allocator.js +69 -11
  245. package/src/lib/worktree/worktree-audit.js +70 -3
  246. package/src/scripts/setup-infra.js +24 -11
  247. package/src/utils/agents-installer.js +18 -8
  248. package/src/utils/claude-md-injector.js +10 -5
  249. package/src/utils/file-copier.js +28 -16
  250. package/src/utils/hooks-installer.js +75 -29
  251. package/src/utils/skills-installer.js +194 -187
  252. package/src/utils/version-checker.js +68 -3
  253. package/framework/standards/ai-agents/prompts-in-db.md +0 -214
@@ -13,7 +13,11 @@
13
13
  *
14
14
  * Subcommands:
15
15
  * morph-spec worktree setup <feature> [--fresh] — create + materialize state
16
+ * morph-spec worktree provision <name> [--base <ref>] — task-isolation tree
17
+ * (branch morph-task/{name}, same infra, NO feature identity) — what the
18
+ * WorktreeCreate hook calls for `claude -w` / EnterWorktree / Agent isolation
16
19
  * morph-spec worktree link [--target <path>] [--all] — (re)link shared infra
20
+ * morph-spec worktree remove [<feature>|--target <path>] [--if-clean] — safe teardown
17
21
  * morph-spec worktree list [--json] — list active feature worktrees
18
22
  *
19
23
  * Closing the lifecycle (integrate + teardown) lives in `morph-spec finish`
@@ -32,7 +36,7 @@
32
36
 
33
37
  import { execSync, execFileSync } from 'child_process';
34
38
  import { existsSync, mkdirSync, symlinkSync, readFileSync, writeFileSync, appendFileSync, lstatSync, unlinkSync, rmdirSync, cpSync } from 'fs';
35
- import { join, resolve, dirname } from 'path';
39
+ import { join, resolve, dirname, basename } from 'path';
36
40
  import chalk from 'chalk';
37
41
  import { detectDefaultBranch } from '../../lib/git/default-branch.js';
38
42
  import { triageIgnoredPaths } from '../../lib/worktree/ignored-triage.js';
@@ -40,29 +44,11 @@ import { triageIgnoredPaths } from '../../lib/worktree/ignored-triage.js';
40
44
  // the SAME answer this file does, and hooks can never import from `src/`.
41
45
  import { findReparsePoints } from '../../../framework/hooks/shared/reparse-scan.js';
42
46
 
43
- /**
44
- * The directory new worktrees are created under, relative to the primary root.
45
- *
46
- * NO leading dot, deliberately (C1). Jest escapes the `.` when it interpolates
47
- * an absolute `rootDir` into the `testMatch` glob, so `<root>/.worktrees/...`
48
- * stops matching and an entire frontend suite reports **0 tests without an
49
- * error** — the suite "passes" having tested nothing. The dot bought nothing:
50
- * the directory is gitignored either way.
51
- */
52
- export const WORKTREES_DIR = 'worktrees';
53
-
54
- /**
55
- * The pre-C1 directory name. New worktrees never land here, but every reader
56
- * that resolves worktrees BY CONVENTION must still accept it, so a machine
57
- * that already had `.worktrees/{f}` keeps working with no manual migration.
58
- *
59
- * Readers that go through `git worktree list` (fleet, finish, worktree list)
60
- * were never affected — git records absolute paths, not this convention.
61
- */
62
- export const LEGACY_WORKTREES_DIR = '.worktrees';
63
-
64
- /** Both names, new first — the read order for convention-based lookups. */
65
- export const WORKTREE_DIR_NAMES = [WORKTREES_DIR, LEGACY_WORKTREES_DIR];
47
+ // The container name lives in lib/worktree/dirs.js (single source, C1
48
+ // rationale documented there); re-exported here so existing importers and the
49
+ // dependency-matrix row keep pointing at this file.
50
+ import { WORKTREES_DIR, LEGACY_WORKTREES_DIR, WORKTREE_DIR_NAMES } from '../../lib/worktree/dirs.js';
51
+ export { WORKTREES_DIR, LEGACY_WORKTREES_DIR, WORKTREE_DIR_NAMES };
66
52
 
67
53
  // ── Pure helpers (exported for tests) ──────────────────────────────────────
68
54
 
@@ -362,6 +348,43 @@ export function resolvePrimaryRoot(cwd = process.cwd()) {
362
348
  * @param {string} worktreePath - Absolute path to the worktree
363
349
  * @returns {Promise<{ materialized: boolean, reason?: string }>}
364
350
  */
351
+ /**
352
+ * Guarantee the worktree owns a local `.morph/state.json`, hydrated from its
353
+ * committed features when there are any, and an EMPTY index otherwise.
354
+ *
355
+ * Why the empty index matters: every core hook fail-opens on `stateExists()`,
356
+ * which is true only with a `state.json` or a feature dir in the tree. A
357
+ * task-isolation worktree of a project with no committed features (measured
358
+ * in the sandbox e2e) had neither — so the env-inject hook, the removal guard
359
+ * and every other guardrail were silently dark inside it. `state.json` is
360
+ * gitignored and derivable, so an empty one costs nothing and can never dirty
361
+ * the tree the finish gates read.
362
+ *
363
+ * @param {string} worktreePath
364
+ * @returns {Promise<{ materialized: boolean, reason?: string, seeded?: boolean }>}
365
+ */
366
+ export async function ensureLocalState(worktreePath) {
367
+ const result = await materializeState(worktreePath);
368
+ if (result.materialized) return result;
369
+ const statePath = join(worktreePath, '.morph', 'state.json');
370
+ if (existsSync(statePath)) return { ...result, materialized: true };
371
+ try {
372
+ const { atomicWriteJson } = await import('../../core/state/state-manager.js');
373
+ const now = new Date().toISOString();
374
+ mkdirSync(join(worktreePath, '.morph'), { recursive: true });
375
+ atomicWriteJson(statePath, {
376
+ version: '5.0.0',
377
+ project: { name: basename(worktreePath), type: 'unknown', createdAt: now, updatedAt: now },
378
+ features: {},
379
+ threads: {},
380
+ metadata: { totalFeatures: 0, completedFeatures: 0, totalTimeSpent: 0, lastUpdated: now },
381
+ });
382
+ return { materialized: true, seeded: true, reason: result.reason };
383
+ } catch (err) {
384
+ return { materialized: false, reason: (err && err.message) || result.reason };
385
+ }
386
+ }
387
+
365
388
  export async function materializeState(worktreePath) {
366
389
  const featuresDir = join(worktreePath, '.morph', 'features');
367
390
  if (!existsSync(featuresDir)) {
@@ -490,33 +513,13 @@ export async function runWorktreeSetup(feature, options = {}) {
490
513
  setupInfraLinks(worktreePath, cwd);
491
514
 
492
515
  // Fleet env: allocate this feature's deterministic port block and write the
493
- // worktree-local manifest (.morph/worktree.env), excluded from git via the
494
- // same common-dir mechanism as the infra links (it must be invisible to the
495
- // finish clean-checks). Under `.morph/` on purpose: that prefix is already
496
- // shieldable at finish (backed up, never a removal blocker). Fail-open — a
497
- // worktree without ports is degraded, never broken.
498
- let envBlock = null;
499
- try {
500
- const { allocateEnvBlock, resolveFleetConfig, buildEnvFileContent } =
501
- await import('../../lib/worktree/env-allocator.js');
502
- const common = execSync('git rev-parse --git-common-dir', { cwd, stdio: 'pipe' }).toString().trim();
503
- if (common) {
504
- const gitCommonDir = resolve(cwd, common);
505
- envBlock = allocateEnvBlock(feature, { gitCommonDir, ...resolveFleetConfig(cwd) });
506
- let stack = '';
507
- try {
508
- const cfg = JSON.parse(readFileSync(join(cwd, '.morph', 'config', 'config.json'), 'utf8').replace(/^/, ''));
509
- stack = cfg?.project?.stack || '';
510
- } catch { /* fail-open — stackless env still carries PORT */ }
511
- mkdirSync(join(worktreePath, '.morph'), { recursive: true });
512
- writeFileSync(join(worktreePath, '.morph', 'worktree.env'), buildEnvFileContent(feature, envBlock, stack), 'utf8');
513
- excludeWorktreePaths(cwd, [join('.morph', 'worktree.env')]);
514
- }
515
- } catch { envBlock = null; /* fail-open */ }
516
+ // worktree-local manifest (.morph/worktree.env). Fail-open — a worktree
517
+ // without ports is degraded, never broken.
518
+ const envBlock = await allocateWorktreeEnv(worktreePath, cwd, feature);
516
519
 
517
520
  // Materialize a local, real state.json in the worktree (self-heal from the
518
521
  // committed feature.json files) so hooks work from the very first session.
519
- const materialized = await materializeState(worktreePath);
522
+ const materialized = await ensureLocalState(worktreePath);
520
523
 
521
524
  // Fleet handoff: the fresh instance that opens this worktree reads
522
525
  // handoff.md before anything else (SessionStart hook points at it). The doc
@@ -578,6 +581,109 @@ export async function worktreeSetupCommand(feature, options = {}) {
578
581
  process.exit(exitCode);
579
582
  }
580
583
 
584
+ // ── provision (task isolation — a worktree that is NOT a feature) ──────────
585
+
586
+ /**
587
+ * Branch name for a task-isolation worktree: `morph-task/<name>`. A separate
588
+ * namespace from `morph/<feature>` on purpose — `worktree list`, `fleet`,
589
+ * `finish` and the SessionStart hook all read `morph/<f>` as "this IS feature
590
+ * f", and a scratch tree named after a feature must never impersonate it.
591
+ *
592
+ * @param {string} name
593
+ * @returns {string}
594
+ */
595
+ export function buildTaskBranchName(name) {
596
+ return `morph-task/${name}`;
597
+ }
598
+
599
+ /**
600
+ * Non-exiting core of `worktree provision` — the tree `claude -w`,
601
+ * `EnterWorktree` and `Agent isolation:"worktree"` ask for when the name is
602
+ * not a feature. Everything `setup` gives a feature worktree EXCEPT the
603
+ * feature identity: it lands under `<primary root>/worktrees/<name>` (never
604
+ * nested under a secondary worktree, whatever the cwd), on `morph-task/<name>`
605
+ * off the default branch (or `--base`), with the infra junctions, a git-
606
+ * invisible `.morph/worktree.env` keyed `task/<name>`, and a materialized
607
+ * local state.json. No feature registration, no handoff.
608
+ *
609
+ * Exit codes mirror `setup`: 0 created (or fail-open), 1 refused (bad name),
610
+ * 2 already exists at this exact path.
611
+ *
612
+ * @param {string} name
613
+ * @param {{ base?: string, cwd?: string }} [options]
614
+ * @returns {Promise<{ ok: boolean, exitCode: 0|1|2, created: boolean, alreadyExists?: boolean, path?: string, branch?: string, branchedFrom?: string|null, kind?: 'task-isolation', name: string, env?: object, error?: string }>}
615
+ */
616
+ export async function runWorktreeProvision(name, options = {}) {
617
+ const cwd = options.cwd || process.cwd();
618
+
619
+ if (!isValidFeatureName(name)) {
620
+ return { ok: false, exitCode: 1, created: false, error: 'invalid-name', name };
621
+ }
622
+ if (!isGitRepo(cwd)) {
623
+ return { ok: true, exitCode: 0, created: false, error: 'not-a-git-repo', name };
624
+ }
625
+
626
+ // Always the PRIMARY root: a provision issued from inside a feature worktree
627
+ // must not nest a worktree under it — that tree would be invisible to the
628
+ // fleet and to doctor, which enumerate `<root>/worktrees/`.
629
+ const root = resolvePrimaryRoot(cwd);
630
+ const branch = buildTaskBranchName(name);
631
+ const worktreePath = buildWorktreePath(name, root);
632
+
633
+ if (worktreeExists(worktreePath, root)) {
634
+ return { ok: true, exitCode: 2, created: false, alreadyExists: true, path: worktreePath, branch, kind: 'task-isolation', name };
635
+ }
636
+
637
+ let branchedFrom = null;
638
+ try {
639
+ if (!branchExists(branch, root)) {
640
+ const base = options.base || detectDefaultBranch(root);
641
+ if (base) {
642
+ execFileSync('git', ['branch', branch, base], { cwd: root, stdio: 'pipe' });
643
+ branchedFrom = base;
644
+ } else {
645
+ execFileSync('git', ['branch', branch], { cwd: root, stdio: 'pipe' });
646
+ }
647
+ }
648
+
649
+ const parentDir = join(root, WORKTREES_DIR);
650
+ if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
651
+
652
+ execFileSync('git', ['worktree', 'add', worktreePath, branch], { cwd: root, stdio: 'pipe' });
653
+
654
+ setupInfraLinks(worktreePath, root);
655
+ const envBlock = await allocateWorktreeEnv(worktreePath, root, `task/${name}`);
656
+ const materialized = await ensureLocalState(worktreePath);
657
+
658
+ return {
659
+ ok: true,
660
+ exitCode: 0,
661
+ created: true,
662
+ path: worktreePath,
663
+ branch,
664
+ branchedFrom,
665
+ kind: 'task-isolation',
666
+ name,
667
+ materialized: materialized.materialized,
668
+ ...(envBlock ? { env: { base: envBlock.base, range: envBlock.range } } : {}),
669
+ };
670
+ } catch (err) {
671
+ return { ok: true, exitCode: 0, created: false, error: (err.message || 'unknown').slice(0, 200), name };
672
+ }
673
+ }
674
+
675
+ /**
676
+ * `morph-spec worktree provision <name> [--base <ref>]` — thin wrapper around
677
+ * `runWorktreeProvision`, same JSON-and-exit contract as `setup`.
678
+ * @param {string} name
679
+ * @param {{ base?: string, cwd?: string }} [options]
680
+ */
681
+ export async function worktreeProvisionCommand(name, options = {}) {
682
+ const { ok, exitCode, ...result } = await runWorktreeProvision(name, options);
683
+ console.log(JSON.stringify(result));
684
+ process.exit(exitCode);
685
+ }
686
+
581
687
  /**
582
688
  * Command handler for `morph-spec worktree link [--target <path>] [--all]`.
583
689
  *
@@ -588,7 +694,7 @@ export async function worktreeSetupCommand(feature, options = {}) {
588
694
  *
589
695
  * @param {{ target?: string, all?: boolean, cwd?: string }} [options]
590
696
  */
591
- export function worktreeLinkCommand(options = {}) {
697
+ export async function worktreeLinkCommand(options = {}) {
592
698
  const cwdArg = options.cwd || process.cwd();
593
699
  const root = resolvePrimaryRoot(cwdArg);
594
700
  const target = resolve(options.target || options.cwd || process.cwd()).replace(/\\/g, '/');
@@ -627,10 +733,79 @@ export function worktreeLinkCommand(options = {}) {
627
733
  }
628
734
  }
629
735
 
630
- console.log(JSON.stringify({ linked, skipped, root, target }));
736
+ // `link --all` is the documented repair for a worktree missing its infra —
737
+ // that includes a `.morph/worktree.env` written by an older layout. The
738
+ // allocation is idempotent (same key → same block), so regenerating the
739
+ // manifest never moves a worktree's ports; it only adds the keys it lacks.
740
+ let env = null;
741
+ if (options.all) {
742
+ const key = envKeyForWorktree(target, root);
743
+ const block = key ? await allocateWorktreeEnv(target, root, key) : null;
744
+ if (block) env = { key, base: block.base, range: block.range };
745
+ }
746
+
747
+ console.log(JSON.stringify({ linked, skipped, root, target, ...(env ? { env } : {}) }));
631
748
  process.exit(0);
632
749
  }
633
750
 
751
+ /**
752
+ * The env-registry key of a worktree, derived from its branch: `morph/<f>` →
753
+ * `<f>` (feature), `morph-task/<n>` → `task/<n>` (task isolation). `null` for
754
+ * a branch morph does not own — no block is allocated for those.
755
+ *
756
+ * @param {string} worktreePath
757
+ * @param {string} root
758
+ * @returns {string|null}
759
+ */
760
+ export function envKeyForWorktree(worktreePath, root) {
761
+ const norm = resolve(worktreePath).replace(/\\/g, '/');
762
+ const entry = parseWorktreeList(root).find((e) => e.path === norm);
763
+ const branch = entry?.branch?.replace(/^refs\/heads\//, '') ?? null;
764
+ if (!branch) return null;
765
+ const feature = branch.match(/^morph\/(.+)$/)?.[1];
766
+ if (feature) return feature;
767
+ const task = branch.match(/^morph-task\/(.+)$/)?.[1];
768
+ return task ? `task/${task}` : null;
769
+ }
770
+
771
+ /**
772
+ * Allocate (idempotently) the port block for `envKey` and write the worktree's
773
+ * `.morph/worktree.env`, excluded from git via the same common-dir mechanism as
774
+ * the infra links (it must be invisible to the finish clean-checks). Under
775
+ * `.morph/` on purpose: that prefix is already shieldable at finish (backed up,
776
+ * never a removal blocker). Fail-open: returns `null` when no block could be
777
+ * written — a worktree without ports is degraded, never broken.
778
+ *
779
+ * Shared by `setup` (feature key), `provision` (task key) and `link --all`
780
+ * (regeneration), so the manifest has exactly one writer.
781
+ *
782
+ * @param {string} worktreePath
783
+ * @param {string} root - primary root
784
+ * @param {string} envKey - `<feature>` or `task/<name>`
785
+ * @returns {Promise<{ index: number, base: number, range: [number, number] }|null>}
786
+ */
787
+ export async function allocateWorktreeEnv(worktreePath, root, envKey) {
788
+ try {
789
+ const { allocateEnvBlock, resolveFleetConfig, buildEnvFileContent } =
790
+ await import('../../lib/worktree/env-allocator.js');
791
+ const common = execSync('git rev-parse --git-common-dir', { cwd: root, stdio: 'pipe' }).toString().trim();
792
+ if (!common) return null;
793
+ const gitCommonDir = resolve(root, common);
794
+ const block = allocateEnvBlock(envKey, { gitCommonDir, ...resolveFleetConfig(root) });
795
+ let stack = '';
796
+ try {
797
+ const cfg = JSON.parse(readFileSync(join(root, '.morph', 'config', 'config.json'), 'utf8').replace(/^/, ''));
798
+ stack = cfg?.project?.stack || '';
799
+ } catch { /* fail-open — stackless env still carries PORT */ }
800
+ mkdirSync(join(worktreePath, '.morph'), { recursive: true });
801
+ writeFileSync(join(worktreePath, '.morph', 'worktree.env'), buildEnvFileContent(envKey, block, stack), 'utf8');
802
+ excludeWorktreePaths(root, [join('.morph', 'worktree.env')]);
803
+ return block;
804
+ } catch {
805
+ return null;
806
+ }
807
+ }
808
+
634
809
  // ── finish / list shared helpers ────────────────────────────────────────────
635
810
 
636
811
  /**
@@ -831,8 +1006,15 @@ function isLiveSiblingWorktreeRegistration(relPath, root, feature) {
831
1006
  * @param {string} rootPath - the primary root, for the replica comparison
832
1007
  * @param {{ discardIgnored?: boolean }} options
833
1008
  * @returns {{ shieldable: string[], blocking: string[], discarded: string[], replicable: string[] }}
1009
+ *
1010
+ * EXPORTADA para que o `finish --dry-run` (pré-voo) responda EXATAMENTE a
1011
+ * mesma pergunta que o teardown real vai responder. Uma segunda enumeração de
1012
+ * `git status --ignored` no finish.js — com a sua própria cópia de
1013
+ * IGNORED_SHIELDABLE_PREFIXES — faria o pré-voo prometer uma coisa e o
1014
+ * comando fazer outra, que é a única maneira de um dry-run ser pior que
1015
+ * nenhum. Consumida read-only: o pré-voo não copia, não apaga, não decide.
834
1016
  */
835
- function classifyIgnoredLeftovers(worktreePath, rootPath, options = {}) {
1017
+ export function classifyIgnoredLeftovers(worktreePath, rootPath, options = {}) {
836
1018
  let out;
837
1019
  try {
838
1020
  // `-z` + execFile: NUL-delimited, no shell, no C-style quoting to undo —
@@ -1338,7 +1520,7 @@ export function cwdIsInsideWorktree(cwd, worktreePath) {
1338
1520
  * closing a feature is still `morph-spec finish`.
1339
1521
  *
1340
1522
  * @param {string|undefined} featureArg
1341
- * @param {{ target?: string, discardIgnored?: boolean, deleteBranch?: boolean, cwd?: string }} [options]
1523
+ * @param {{ target?: string, discardIgnored?: boolean, deleteBranch?: boolean, ifClean?: boolean, cwd?: string }} [options]
1342
1524
  */
1343
1525
  export async function worktreeRemoveCommand(featureArg, options = {}) {
1344
1526
  const cwdArg = options.cwd || process.cwd();
@@ -1398,9 +1580,47 @@ export async function worktreeRemoveCommand(featureArg, options = {}) {
1398
1580
  hint: `saia do worktree antes de removê-lo — o processo trava o próprio diretório de trabalho. Rode de novo a partir de "${root}"` }, 1);
1399
1581
  }
1400
1582
 
1583
+ // `--if-clean` — the WorktreeRemove hook's contract: Claude Code fires it
1584
+ // when it believes the tree is disposable, but "disposable" is the user's
1585
+ // call, not the harness's. Any uncommitted or untracked file refuses the
1586
+ // removal BEFORE the junctions are touched, so a refusal leaves the tree
1587
+ // exactly as it was (no rollback needed, nothing to relink). The infra
1588
+ // links and the env manifest are excluded from git, so they never count.
1589
+ if (options.ifClean) {
1590
+ let status;
1591
+ try {
1592
+ status = execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd: worktreePath, stdio: 'pipe' }).toString();
1593
+ } catch {
1594
+ emit({ removed: false, error: 'status-failed', path: worktreePath, root,
1595
+ hint: 'não foi possível provar que o worktree está limpo — nada foi removido' }, 1);
1596
+ }
1597
+ const dirt = status.split(/\r?\n/).filter((l) => l.trim());
1598
+ if (dirt.length) {
1599
+ emit({ removed: false, error: 'worktree-dirty', path: worktreePath, root, dirt: dirt.slice(0, 20),
1600
+ 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);
1601
+ }
1602
+ }
1603
+
1604
+ // The env-registry key this tree allocated under: `<f>` for morph/<f>,
1605
+ // `task/<n>` for morph-task/<n>. Read BEFORE the teardown (the entry is gone
1606
+ // from `git worktree list` afterwards). The old `'worktree'` placeholder
1607
+ // meant every --target removal released a slot nobody held and leaked the
1608
+ // one the tree actually used.
1609
+ const envKey = envKeyForWorktree(worktreePath, root);
1610
+
1611
+ // The tree's compose stack goes down BEFORE the tree: a running bind-mounted
1612
+ // container holds the directory (Windows) and turns a clean removal into a
1613
+ // removalFailure. Volumes survive — removing a worktree is not closing the
1614
+ // feature, and a re-`setup` should find its seeded database intact.
1615
+ let e2e = null;
1616
+ try {
1617
+ const { tearDownFeatureStack } = await import('../../lib/e2e/stack-teardown.js');
1618
+ e2e = tearDownFeatureStack(envKey || feature || basename(worktreePath), { root, worktreePath, volumes: false });
1619
+ } catch { e2e = null; /* fail-open */ }
1620
+
1401
1621
  const teardown = await teardownWorktree(worktreePath, {
1402
1622
  root,
1403
- feature: feature || 'worktree',
1623
+ feature: feature || envKey || 'worktree',
1404
1624
  branch,
1405
1625
  options: { discardIgnored: options.discardIgnored === true },
1406
1626
  // Removing a worktree is not closing a feature: the branch is the work, and
@@ -1421,6 +1641,7 @@ export async function worktreeRemoveCommand(featureArg, options = {}) {
1421
1641
  // real `.claude/`/`.morph/framework/` were unlinked rather than followed; on
1422
1642
  // a refusal it is what `relinked` did or did not put back.
1423
1643
  if (teardown.unlinked.length) out.unlinked = teardown.unlinked;
1644
+ if (e2e?.attempted) out.e2e = { status: e2e.status, cwd: e2e.cwd, volumes: false };
1424
1645
  if (teardown.worktreeRemoved) {
1425
1646
  if (teardown.discarded.length) out.discardedIgnored = teardown.discarded;
1426
1647
  // Broken out of `discardedIgnored` on purpose: "deleted" and "deleted
@@ -198,7 +198,23 @@ export async function advanceCommand(featureName, options = {}) {
198
198
  process.exit(1);
199
199
  }
200
200
 
201
- if (gate) {
201
+ // Gate idempotency (F15): `setApprovalGate` REPLACES the gate object whole
202
+ // (state-manager.js), so re-writing an already-approved gate does not
203
+ // refresh it — it destroys `approvedBy`, `timestamp`, `notes` and
204
+ // `selfAssessed` in one assignment. And `resolveApprovedBy` above answers
205
+ // "auto" in every headless context, so `approve f proposal --approver
206
+ // lucas` followed by `advance f` used to rewrite a human's signature as
207
+ // "auto" — the one stamp the charter forbids on a human-approved gate.
208
+ // The state-sync hook already carries exactly this guard; `advance`, the
209
+ // other gate write-path, simply never got it.
210
+ //
211
+ // It skips ONLY the write. The scaffold and the handoff below still run,
212
+ // so re-running `advance` after a failed scaffold keeps working — the
213
+ // reason this is a guard and not an abort.
214
+ const priorGate = gate ? feature.approvalGates?.[gate] : null;
215
+ const alreadyApproved = priorGate?.approved === true;
216
+
217
+ if (gate && !alreadyApproved) {
202
218
  await setApprovalGate(featureName, gate, true, {
203
219
  approvedBy,
204
220
  approvedAt: new Date().toISOString(),
@@ -226,7 +242,13 @@ export async function advanceCommand(featureName, options = {}) {
226
242
  if (missingArtifacts.length) {
227
243
  console.log(chalk.yellow(` ⚠️ --force: avançou SEM ${missingArtifacts.join(', ')}.`));
228
244
  }
229
- if (gate) console.log(chalk.gray(` Gate "${gate}" approved (${approvedBy}).`));
245
+ // Report what actually happened: claiming "approved (auto)" when nothing
246
+ // was written is how the destroyed signature stayed invisible.
247
+ if (gate && alreadyApproved) {
248
+ console.log(chalk.gray(` Gate "${gate}" was already approved (${priorGate.approvedBy || 'unknown'}) — approval preserved, nothing re-written.`));
249
+ } else if (gate) {
250
+ console.log(chalk.gray(` Gate "${gate}" approved (${approvedBy}).`));
251
+ }
230
252
  if (nextDir) console.log(chalk.gray(` Scaffolded ${nextDir}/ — ready for the next phase's first artifact.\n`));
231
253
  else console.log('');
232
254
  } catch (error) {
@@ -180,7 +180,13 @@ export async function approveCommand(featureName, gateName, options = {}) {
180
180
  if (currentStatus?.approved) {
181
181
  console.log(chalk.yellow(`\n⚠️ Gate "${gateName}" is already approved`));
182
182
  console.log(chalk.gray(`Approved at: ${currentStatus.timestamp}`));
183
- console.log(chalk.gray(`Approved by: ${currentStatus.approvedBy || 'Unknown'}\n`));
183
+ console.log(chalk.gray(`Approved by: ${currentStatus.approvedBy || 'Unknown'}`));
184
+ // This early-out is a no-op that still exits 0 — deliberate, and
185
+ // `--force` does NOT cover it (that flag only covers gate ORDER). A
186
+ // caller who meant to re-approve therefore reads success and is left
187
+ // with nowhere to go. The way out already exists as `unapprove`; the
188
+ // message just never named it. Naming it is the whole fix (no new flag).
189
+ console.log(chalk.gray(`To approve it again, revoke it first: morph-spec unapprove ${featureName} ${gateName}\n`));
184
190
  return;
185
191
  }
186
192
 
@@ -349,6 +355,32 @@ export async function approveCommand(featureName, gateName, options = {}) {
349
355
  }
350
356
  selfAssessed = true;
351
357
  }
358
+
359
+ // REAVALIAÇÃO VISÍVEL. O `evaluator-report.md` é append-only, então uma
360
+ // segunda rodada acrescenta um `**Composite-Score:**` ao fim e o arquivo
361
+ // passa a carregar mais de um veredito. O gate decide pelo ÚLTIMO (o
362
+ // vigente) — mas aprovar em silêncio um relatório com notas superadas
363
+ // esconde do humano exatamente o fato que ele precisa ver: a nota MUDOU,
364
+ // e numa das rodadas medidas em campo ela CAIU (8.9 → 8.3, quando uma
365
+ // correção abriu um bloqueante).
366
+ //
367
+ // Aqui é o lugar: `approve` é a precondição humana do Gate 3 e a única
368
+ // superfície onde uma pessoa lê o veredito antes de carimbá-lo. Sem este
369
+ // bloco, `scoreTokens`/`superseded` seriam mitigação inerte — campo que
370
+ // ninguém lê não mitiga nada.
371
+ if (report.status === 'ok' && report.scoreTokens > 1) {
372
+ console.log(chalk.yellow(
373
+ `
374
+ ⚠️ O relatório do avaliador tem ${report.scoreTokens} vereditos (o arquivo é append-only).`
375
+ ));
376
+ console.log(chalk.gray(` Vigente (o que o gate usa): ${report.score}`));
377
+ console.log(chalk.gray(` Superado(s), na ordem do arquivo: ${report.superseded.join(', ')}`));
378
+ if (report.superseded.some((n) => n > report.score)) {
379
+ console.log(chalk.yellow(' A nota CAIU em relação a uma rodada anterior — confira o que a correção'));
380
+ console.log(chalk.yellow(' abriu antes de aprovar; o relatório mantém a análise das duas rodadas.'));
381
+ }
382
+ console.log('');
383
+ }
352
384
  }
353
385
  }
354
386
 
@@ -15,32 +15,61 @@ import { detectDefaultBranch } from '../../lib/git/default-branch.js';
15
15
  * `morph-spec finish` can later merge/PR it back. Symmetric with the worktree
16
16
  * flow, which gets its branch from `runWorktreeSetup`.
17
17
  *
18
+ * That checkout moves the dev's PRIMARY working tree, and `stdio: 'pipe'`
19
+ * swallows even git's own message — so the move is reported by the caller
20
+ * (`branchMoveWarning`), never silent. `checkout: false` (`--no-checkout`)
21
+ * still CREATES the branch — `finish --pr|--merge` depends on it existing in
22
+ * both flows — and only skips moving the tree.
23
+ *
18
24
  * Fail-open: outside a git repo, with no resolvable default, or on any git
19
25
  * error, the feature is simply left on the current branch — never a failed birth.
20
26
  *
21
27
  * @param {string} featureName
22
- * @returns {{ branched: boolean, branch?: string, from?: string, reason?: string }}
28
+ * @param {{ checkout?: boolean }} [opts] - `checkout: false` keeps the tree put
29
+ * @returns {{ branched: boolean, checkedOut: boolean, branch?: string, from?: string,
30
+ * movedFrom?: string, stayedOn?: string, reason?: string }}
31
+ * `movedFrom` is present ONLY when the working tree actually moved (it names
32
+ * the branch left behind); `stayedOn` only when the move was declined.
23
33
  */
24
- function branchCommonFlow(featureName) {
34
+ function branchCommonFlow(featureName, { checkout = true } = {}) {
25
35
  const cwd = process.cwd();
26
36
  try {
27
- if (!isGitRepo(cwd)) return { branched: false, reason: 'not-a-git-repo' };
37
+ if (!isGitRepo(cwd)) return { branched: false, checkedOut: false, reason: 'not-a-git-repo' };
28
38
  const branch = buildBranchName(featureName);
29
39
  const defaultBranch = detectDefaultBranch(cwd);
30
- if (!defaultBranch) return { branched: false, reason: 'no-default-branch' };
40
+ if (!defaultBranch) return { branched: false, checkedOut: false, reason: 'no-default-branch' };
31
41
  if (!branchExists(branch, cwd)) {
32
42
  execFileSync('git', ['branch', branch, defaultBranch], { cwd, stdio: 'pipe' });
33
43
  }
34
44
  const current = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { cwd, stdio: 'pipe' }).toString().trim();
35
- if (current !== branch) {
36
- execFileSync('git', ['checkout', branch], { cwd, stdio: 'pipe' });
37
- }
38
- return { branched: true, branch, from: defaultBranch };
45
+ // Already there: nothing moved, so nothing to warn about.
46
+ if (current === branch) return { branched: true, checkedOut: true, branch, from: defaultBranch };
47
+ if (!checkout) return { branched: true, checkedOut: false, branch, from: defaultBranch, stayedOn: current };
48
+ execFileSync('git', ['checkout', branch], { cwd, stdio: 'pipe' });
49
+ return { branched: true, checkedOut: true, branch, from: defaultBranch, movedFrom: current };
39
50
  } catch {
40
- return { branched: false, reason: 'git-error' };
51
+ return { branched: false, checkedOut: false, reason: 'git-error' };
41
52
  }
42
53
  }
43
54
 
55
+ /**
56
+ * The alert for a checkout that MOVED. Built once and rendered on both
57
+ * surfaces — stdout (human) and stderr (under `--json`, so it survives a
58
+ * stdout redirect) — so the two can never drift apart.
59
+ *
60
+ * @param {{ branch: string, movedFrom: string }} branchResult
61
+ * @returns {string[]} lines, unstyled and unindented
62
+ */
63
+ function branchMoveWarning({ branch, movedFrom }) {
64
+ return [
65
+ `⚠️ Working tree MOVED: ${movedFrom} → ${branch}`,
66
+ ` This checkout is now ON the feature branch — uncommitted work came along,`,
67
+ ` and anything committed here from now on lands on ${branch}, not ${movedFrom}.`,
68
+ ` Use --no-checkout to create the branch without moving the tree, or --worktree`,
69
+ ` (the recommended route for parallel work) to isolate the feature in its own tree.`,
70
+ ];
71
+ }
72
+
44
73
  /** Feature names are kebab-case: lowercase alphanumerics and hyphens. */
45
74
  const FEATURE_NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
46
75
 
@@ -130,6 +159,24 @@ export async function createCommand(featureName, options = {}) {
130
159
  process.exit(1);
131
160
  }
132
161
 
162
+ // `--no-checkout` pede que o checkout PRINCIPAL não se mova; `--worktree`
163
+ // nunca o move — a feature nasce numa árvore separada, e o caminho de
164
+ // worktree sequer chama `branchCommonFlow`, que é o único leitor do flag.
165
+ // Aceitar a combinação em silêncio é dizer que o CLI honrou um pedido que
166
+ // ele ignorou — o mesmo anti-padrão que esta série de fixes tirou do
167
+ // `finish --pr --push`. Recusa ALTA, e AQUI: junto das outras validações
168
+ // de forma, ANTES de `createFeature` — a primeira mutação do `create`.
169
+ // Uma recusa depois dela deixaria a feature nascida por um comando que
170
+ // falhou, pior que o no-op que ela substitui.
171
+ if (options.worktree && options.checkout === false) {
172
+ console.error(chalk.red(`\n❌ no-checkout-with-worktree: --no-checkout não se combina com --worktree`));
173
+ console.error(chalk.gray(` O --worktree já não move o checkout desta árvore: a feature nasce em worktrees/${featureName},`));
174
+ console.error(chalk.gray(` na branch morph/${featureName}, e o caminho de worktree nem chega a ler o --no-checkout.`));
175
+ console.error(chalk.gray(` Para isolar a feature na própria árvore: morph-spec create ${featureName} --worktree`));
176
+ console.error(chalk.gray(` Para criar só a branch morph/${featureName} sem mover esta árvore: morph-spec create ${featureName} --no-checkout\n`));
177
+ process.exit(1);
178
+ }
179
+
133
180
  const now = new Date().toISOString();
134
181
  const { workType, workTypeDetection } = resolveWorkType(options, now);
135
182
 
@@ -225,7 +272,8 @@ export async function createCommand(featureName, options = {}) {
225
272
  } else {
226
273
  // Common flow: still isolate the feature on its own branch so `finish`
227
274
  // can merge/PR it back. Fail-open (see branchCommonFlow).
228
- branchResult = branchCommonFlow(featureName);
275
+ // Commander's `--no-checkout` sets options.checkout === false.
276
+ branchResult = branchCommonFlow(featureName, { checkout: options.checkout !== false });
229
277
  }
230
278
  }
231
279
 
@@ -238,6 +286,11 @@ export async function createCommand(featureName, options = {}) {
238
286
  const out = { feature: featureName, created, ...finalFeature };
239
287
  if (worktreeResult) out.worktree = worktreeResult;
240
288
  if (branchResult) out.branch = branchResult;
289
+ // The move of the PRIMARY checkout is never silent — under --json it goes
290
+ // to stderr so it survives `... --json > out.json` instead of vanishing.
291
+ if (branchResult?.movedFrom) {
292
+ console.error(chalk.yellow(branchMoveWarning(branchResult).join('\n')));
293
+ }
241
294
  console.log(JSON.stringify(out, null, 2));
242
295
  return;
243
296
  }
@@ -262,7 +315,15 @@ export async function createCommand(featureName, options = {}) {
262
315
  }
263
316
  } else if (branchResult?.branched) {
264
317
  console.log(chalk.green(` branch: ${branchResult.branch} (from ${branchResult.from})`));
265
- console.log(chalk.gray(` close it with: morph-spec finish ${featureName} --pr | --merge\n`));
318
+ console.log(chalk.gray(` close it with: morph-spec finish ${featureName} --pr | --merge`));
319
+ if (branchResult.movedFrom) {
320
+ // Alert, not one more success line: the tree the dev is standing in moved.
321
+ console.log('');
322
+ for (const line of branchMoveWarning(branchResult)) console.log(chalk.yellow(` ${line}`));
323
+ } else if (branchResult.checkedOut === false) {
324
+ console.log(chalk.gray(` working tree left on ${branchResult.stayedOn} (--no-checkout)`));
325
+ }
326
+ console.log('');
266
327
  }
267
328
  } else {
268
329
  console.log(chalk.yellow(`\n⚠️ Feature "${featureName}" already exists — left untouched`));
@@ -4,6 +4,7 @@ import { join, resolve, sep } from 'path';
4
4
  import { execSync } from 'child_process';
5
5
  import { isValidFeatureName, cwdIsInsideWorktree, teardownWorktree, resolvePrimaryRoot } from '../project/worktree.js';
6
6
  import { resolveFeatureTree } from '../../lib/worktree/feature-truth.js';
7
+ import { tearDownFeatureStack } from '../../lib/e2e/stack-teardown.js';
7
8
 
8
9
  /**
9
10
  * `morph-spec delete <feature>` — remove a feature. Hard by default (folder is
@@ -144,6 +145,13 @@ export async function deleteCommand(featureName, options = {}) {
144
145
  // worktree actually came down. A refusal here must leave EVERYTHING
145
146
  // untouched, same contract as `worktree remove`.
146
147
  const root = resolvePrimaryRoot(cwd);
148
+ // The feature's verification stack (containers, ports, volumes, stamp)
149
+ // comes down BEFORE its worktree: the compose file and the bind mount
150
+ // live there, and a deleted feature has nothing to keep a volume for.
151
+ // Fail-open by contract — a stubborn container never blocks the delete.
152
+ try {
153
+ tearDownFeatureStack(featureName, { root, worktreePath: owner.tree, volumes: true });
154
+ } catch { /* fail-open */ }
147
155
  const teardown = await teardownWorktree(owner.tree, {
148
156
  root,
149
157
  feature: featureName,