@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
@@ -2,10 +2,12 @@ import chalk from 'chalk';
2
2
  import { existsSync, readFileSync } from 'fs';
3
3
  import { getFeature } from '../../core/state/state-manager.js';
4
4
  import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
5
- import { shouldPauseGate, getRiskSignals } from '../../lib/risk-detector.js';
6
- import { evaluatorCompositeScore } from '../../lib/evaluator-report.js';
5
+ import { shouldPauseGate, getRiskSignals, classifyE2eSkip } from '../../lib/risk-detector.js';
6
+ import { evaluatorCompositeScore, evaluatorProofState, minProofScore, mediaProofScore } from '../../lib/evaluator-report.js';
7
7
  import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-decision.js';
8
- import { detectDefaultBranch, getHeadSha, getDiffStat } from '../../lib/git/default-branch.js';
8
+ import { minimatch } from 'minimatch';
9
+ import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } from '../../lib/git/default-branch.js';
10
+ import { readMorphConfig } from '../../lib/tasks/test-runner.js';
9
11
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
10
12
 
11
13
  /**
@@ -27,6 +29,59 @@ import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/featu
27
29
  */
28
30
  const OVERRIDABLE_SIGNAL_KEYS = ['standardsViolations', 'designSystemDivergence', 'evalScore'];
29
31
 
32
+ /**
33
+ * Os paths de standard que fazem uma feature ser "de midia".
34
+ *
35
+ * Escritos aqui, uma vez, e lidos do campo `standards[]` das tasks — que o Gate
36
+ * 2 congelou. Nao ha julgamento envolvido: ou a task pinou o standard, ou nao
37
+ * pinou. Um sinal derivado de opiniao seria sobrescrivivel na pratica ainda que
38
+ * nao estivesse em OVERRIDABLE_SIGNAL_KEYS.
39
+ *
40
+ * A forma dos valores e a de `tasks.json` (`standards/...`), nao a do disco
41
+ * (`framework/standards/...`): e o vocabulario que o plano usa.
42
+ */
43
+ export const MEDIA_STANDARD_PATHS = [
44
+ 'standards/ai-agents/modalities-image-gen.md',
45
+ 'standards/ai-agents/media-video.md',
46
+ 'standards/ai-agents/media-pipeline.md',
47
+ ];
48
+
49
+ /**
50
+ * Uma feature "gera midia" quando ALGUMA task pinou um dos tres standards de
51
+ * midia no seu array `standards[]`.
52
+ *
53
+ * Isto e uma FUNCAO EXPORTADA, e nao uma expressao dentro de `collectSignals`,
54
+ * porque um predicado que decide se um guardrail arma tem de ser testavel
55
+ * sozinho — o irmao `productionCode` tem quatro casos, e este nao tinha nenhum.
56
+ * Um predicado que sempre devolve `false` desarma a clausula em silencio, e
57
+ * silencio e o defeito que esta epic catalogou sete vezes.
58
+ *
59
+ * **O prefixo `framework/` e normalizado, e essa e a razao de existir da
60
+ * funcao.** O vocabulario do plano e `standards/...` (o exemplo do
61
+ * `tasks.schema.json`, e a unica forma medida hoje nos `standards[]` desta
62
+ * arvore), mas o disco e `framework/standards/...` — e a mesma prosa das skills
63
+ * cita as duas formas. Um plano que escrevesse a forma do disco desarmaria o
64
+ * guardrail sem erro nenhum: `standards --find`, `validate` e o proprio Gate 3
65
+ * continuariam verdes. Aceitar as duas grafias custa uma linha; recusar uma
66
+ * delas em silencio custa o gate.
67
+ *
68
+ * Fail-open por AUSENCIA (nao por opiniao): lista vazia, `standards` ausente ou
69
+ * de tipo errado ⇒ `false` ⇒ nao pausa. Sem plano legivel nao ha como AFIRMAR
70
+ * que a feature gera midia, e uma regra nova nao trava feature legada.
71
+ *
72
+ * @param {Array<object>} tasks - `tasks.json` → `tasks[]` (qualquer coisa nao-array vira []).
73
+ * @returns {boolean} true se ao menos uma task pinou um standard de midia.
74
+ */
75
+ export function featureGeneratesMedia(tasks) {
76
+ if (!Array.isArray(tasks)) return false;
77
+
78
+ return tasks.some((t) =>
79
+ Array.isArray(t?.standards) &&
80
+ t.standards.some((s) =>
81
+ typeof s === 'string' &&
82
+ MEDIA_STANDARD_PATHS.includes(s.replace(/^framework\//, ''))));
83
+ }
84
+
30
85
  /** Read tasks.json's task list for a feature; [] if missing/corrupt. Mirrors verify.js's readTasks(). */
31
86
  function readTasks(featureName) {
32
87
  try {
@@ -63,6 +118,41 @@ function compositeEvalScore(feature) {
63
118
  return Math.round(avg * 10) / 10;
64
119
  }
65
120
 
121
+ /**
122
+ * "Esta feature mexeu em prompt ou em modelo?" — em duas metades, porque uma só
123
+ * não distingue as duas situações que pedem ações diferentes.
124
+ *
125
+ * `declared: false` é "o projeto não declara caminho algum de prompt/modelo" —
126
+ * a regra é inerte por construção e a razão registrada diz isso. `declared:
127
+ * true` com `touched: []` é "declara, e nada foi tocado" — a regra rodou e não
128
+ * viu nada. Colapsar os dois num array vazio faria um projeto que nunca
129
+ * configurou `watch[]` parecer um projeto que foi verificado.
130
+ *
131
+ * O predicado NÃO é invenção deste framework: o repositório de referência já
132
+ * roda exatamente ele em produção, disparando publicação por `paths:` sobre os
133
+ * mesmos globs e decidindo o escopo com `git diff --name-only`.
134
+ *
135
+ * COLETA, NUNCA DECIDE. Nada aqui julga se o que foi tocado deveria pausar um
136
+ * gate — política escrita no coletor cria duas fontes para o mesmo fato.
137
+ *
138
+ * @param {string} cwd
139
+ * @param {string|null} defaultBranch
140
+ * @returns {{declared: boolean, watch: string[], touched: string[]}}
141
+ */
142
+ function collectPromptModel(cwd, defaultBranch) {
143
+ const watch = readMorphConfig(cwd)?.project?.evals?.watch;
144
+ const globs = Array.isArray(watch) ? watch.filter((g) => typeof g === 'string' && g.trim()) : [];
145
+
146
+ if (globs.length === 0) return { declared: false, watch: [], touched: [] };
147
+
148
+ const changed = changedPathsAgainst(defaultBranch, cwd);
149
+ return {
150
+ declared: true,
151
+ watch: globs,
152
+ touched: changed.filter((path) => globs.some((glob) => minimatch(path, glob))),
153
+ };
154
+ }
155
+
66
156
  /**
67
157
  * Collect every signal `shouldPauseGate()` (risk-detector.js) is documented
68
158
  * to consume, straight off disk — the deterministic call site risk-detector
@@ -72,9 +162,15 @@ function compositeEvalScore(feature) {
72
162
  * política adicionada em risk-detector.js no fecho do R4, qualquer gate).
73
163
  * `verificationStale` também alimenta uma regra própria, mas só no ship
74
164
  * (`gate === 'review'`) — sha do stamp divergindo do HEAD atual. `gate` em si
75
- * é o que permite risk-detector.js diferenciar as duas. `verification`
76
- * completo, diffstat e sha seguem como contexto de auditoria persistido, sem
77
- * regra própria.
165
+ * é o que permite risk-detector.js diferenciar as duas. `e2eStatus`/`e2eReason`
166
+ * alimentam a regra da dispensa de e2e (também só no ship): um nó e2e que
167
+ * PULOU não reprova o rollup, logo `verificationStatus` sai 'pass' e a razão
168
+ * literal é o único fato que distingue dispensa legítima de ausência de prova.
169
+ * `verificationScope` é o quarto sinal da mesma regra: um `verify {f} {task}`
170
+ * carimba `scope: 'task:{id}'` e o e2e dele pula com a razão do GRUPO daquela
171
+ * task, que risk-detector.js recusa como dispensa da feature.
172
+ * `verification` completo, `e2eSkipClass`, diffstat e sha seguem como contexto
173
+ * de auditoria persistido, sem regra própria.
78
174
  *
79
175
  * @param {string} featureName
80
176
  * @param {object} feature
@@ -113,6 +209,61 @@ function collectSignals(featureName, feature, cwd, gateName) {
113
209
  signals.evalScore = trueComposite;
114
210
  signals.evalScoreSource = 'evaluator-report';
115
211
  }
212
+
213
+ // PROVA DE MUTAÇÃO, derivada do MESMO módulo leitor do relatório. Um score
214
+ // alto diz o que o avaliador ACHOU; a seção `## Mutações` diz se ele
215
+ // PROVOU. Sem ela, um Gate 3 auto-passa sobre uma suíte que ninguém mostrou
216
+ // que morde — o defeito que as rubricas de `evals/proof-rubrics/` existem
217
+ // para nomear.
218
+ //
219
+ // `productionCode` viaja DENTRO do sinal para que risk-detector.js continue
220
+ // puro sobre fatos já reunidos: a divisão é "gate-check COLETA do disco,
221
+ // risk-detector DECIDE". Uma feature 100% `docs` não tem o que mutar, e
222
+ // exigir prova dela seria pedir uma cerimônia vazia — que é como uma regra
223
+ // vira ruído e depois é desligada.
224
+ //
225
+ // Task SEM `group` conta como código de produção: `group` é opcional no
226
+ // schema e o default documentado é `backend`. O conservador aqui é assumir
227
+ // que HÁ código — errar para o lado de pedir prova custa uma pausa; errar
228
+ // para o outro lado devolve o gate cego.
229
+ //
230
+ // `tasks` vazio (plano ausente ou ilegível) ⇒ `productionCode: false` ⇒ NÃO
231
+ // pausa. Fail-open deliberado: sem plano, não há como AFIRMAR que existe
232
+ // código de produção, e uma regra nova não pode travar feature legada.
233
+ //
234
+ // Nada disto entra em OVERRIDABLE_SIGNAL_KEYS: são fatos de disco, como
235
+ // `gate`/`workType`/`e2e*`. Um `--signals '{"mutationProof":{"section":true}}'`
236
+ // compraria o Gate 3 exatamente como um `e2eStatus: "pass"` forjado.
237
+ const proof = evaluatorProofState(cwd, featureName);
238
+ signals.mutationProof = {
239
+ section: proof.mutationSection,
240
+ min: minProofScore(proof.proofScores),
241
+ productionCode: tasks.some((t) => (t?.group ?? 'backend') !== 'docs'),
242
+ };
243
+
244
+ // PROVA DE MÍDIA, mesmo módulo leitor e mesma disciplina anti-spoof. As
245
+ // quatro afirmações de uma feature de mídia (custo, revisão humana,
246
+ // marcação, hot path não bloqueado) são propriedades ESTRUTURAIS, e por isso
247
+ // `score` limita `architecture` e não `test-coverage` — daí `media` ficar
248
+ // FORA do `min` acima.
249
+ //
250
+ // `featureGeneratesMedia` é determinístico e vem do DISCO, nunca de
251
+ // julgamento: existe ao menos uma task cujo array `standards` cita um dos
252
+ // três paths de standard de mídia. É o mesmo formato do `productionCode`
253
+ // acima e usa um campo que o Gate 2 congelou. Mora numa função exportada
254
+ // (acima) porque um predicado que decide se um guardrail arma precisa de
255
+ // canário próprio — e porque o prefixo `framework/` precisa ser normalizado
256
+ // ali, num lugar só.
257
+ //
258
+ // A isenção é automática e é o comportamento certo: a maioria das features
259
+ // não gera mídia, e exigir a seção delas seria cerimônia vazia — que é como
260
+ // uma regra vira ruído e depois é desligada. `tasks` vazio ⇒ false ⇒ não
261
+ // pausa, o mesmo fail-open por ausência do sinal irmão.
262
+ signals.mediaClaims = {
263
+ section: proof.mediaSection,
264
+ score: mediaProofScore(proof.proofScores),
265
+ featureGeneratesMedia: featureGeneratesMedia(tasks),
266
+ };
116
267
  }
117
268
 
118
269
  // verificationStatus alimenta a regra "verify vermelho pausa"; o resto do
@@ -121,6 +272,39 @@ function collectSignals(featureName, feature, cwd, gateName) {
121
272
  signals.verification = verification;
122
273
  if (verification?.status) signals.verificationStatus = verification.status;
123
274
  signals.verificationStale = Boolean(verification?.sha && sha && verification.sha !== sha);
275
+
276
+ // O nó e2e, separado do rollup de propósito: um `skip` NÃO reprova
277
+ // `verification.status`, então sem estas duas chaves o gate lê 'pass' tanto
278
+ // quando o e2e rodou e passou quanto quando ele nunca rodou. `null` aqui é
279
+ // DESCONHECIDO (stamp ausente, ou anterior ao nó e2e), nunca 'skip'.
280
+ // `e2eSkipClass` é DERIVADO pela fonte única (classifyE2eSkip, em
281
+ // risk-detector.js) e persistido só como auditoria — quem decide continua
282
+ // sendo shouldPauseGate, aqui não há política.
283
+ //
284
+ // Nenhuma das três entra em OVERRIDABLE_SIGNAL_KEYS: descrevem O QUE
285
+ // ACONTECEU no disco, como `gate`/`workType`, não uma opinião que o chamador
286
+ // possa afirmar. Um `--signals '{"e2eStatus":"pass"}'` compraria o Gate 3.
287
+ signals.e2eStatus = verification?.nodes?.e2e ?? null;
288
+ signals.e2eReason = verification?.e2eReason ?? null;
289
+ signals.e2eSkipClass = signals.e2eStatus === 'skip' ? classifyE2eSkip(signals.e2eReason) : null;
290
+ // De QUEM é o carimbo. `verify {f} {task}` grava 'task:{id}'; só 'feature'
291
+ // responde pela app inteira, e é por isso que este sinal existe: a razão
292
+ // 'task group "docs" does not touch runtime' é dispensa legítima do que
293
+ // AQUELA task verificou, e sem o escopo ela dispensaria o Gate 3 da feature.
294
+ // Fato de disco como os outros três — jamais sobrescrevível por --signals.
295
+ signals.verificationScope = verification?.scope ?? null;
296
+
297
+ // O nó de eval, separado do rollup pela mesma razão que o e2e: um eval que
298
+ // PULOU (escopo de task) não avermelha `verification.status`, então o gate
299
+ // leria 'pass' tanto para "o eval rodou e passou" quanto para "o eval nunca
300
+ // rodou". `null` aqui é "este projeto não declara nó de eval" — DISTINTO de
301
+ // 'skip', que é "declara, e esta corrida não o executou".
302
+ signals.evalsStatus = verification?.nodes?.evals ?? null;
303
+ // O que a feature TOCOU, contra os globs que o projeto declarou como
304
+ // prompt/modelo. Fato de disco: `declared` vem do config commitado, `touched`
305
+ // do git. Nenhuma política mora aqui — quem decide é shouldPauseGate.
306
+ signals.promptModel = collectPromptModel(cwd, defaultBranch);
307
+
124
308
  signals.diffstat = getDiffStat(defaultBranch, cwd);
125
309
  signals.sha = sha;
126
310
 
@@ -7,8 +7,28 @@ import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/featu
7
7
  /** Fixed --dims order: a,c,q,t (mirrors the morph-eval dimension weights). */
8
8
  const DIM_KEYS = ['architecture', 'contracts', 'codeQuality', 'testCoverage'];
9
9
 
10
+ /**
11
+ * taskScores keys are pinned to `^T\d+$` by feature.schema.json, and tasks.schema.json
12
+ * pins the same shape for the task ids themselves. Anything else — a dotted `T3.2`
13
+ * above all — is split on the dot by `updateFeature` and lands as a NESTED key that
14
+ * neither the schema nor the gate-check proxy can see.
15
+ */
16
+ const TASK_ID_PATTERN = /^T\d+$/;
17
+
18
+ const USAGE_LINE = 'Usage: morph-spec score <feature> <task> <score> [--dims a,c,q,t] e.g. morph-spec score my-feature T1 9.2 --dims "9,10,9,9"';
19
+
10
20
  function parseScoreValue(raw, label) {
11
- const value = Number(raw);
21
+ // `Number('')` and `Number(' ')` are 0 — finite and inside 0-10, so the range check
22
+ // below waves them through and a blank value gets recorded as a real zero. Refuse the
23
+ // empty shape BEFORE Number() so no score can ever be fabricated.
24
+ const text = raw === undefined || raw === null ? '' : String(raw).trim();
25
+ if (text === '') {
26
+ const got = raw === undefined ? 'no value was passed' : `got "${raw}"`;
27
+ console.error(chalk.red(`\n❌ Missing ${label}: ${got} — a blank value is refused, never recorded as 0`));
28
+ console.error(chalk.gray(` ${USAGE_LINE}\n`));
29
+ process.exit(1);
30
+ }
31
+ const value = Number(text);
12
32
  if (!Number.isFinite(value) || value < 0 || value > 10) {
13
33
  console.error(chalk.red(`\n❌ Invalid ${label}: "${raw}" — expected a number between 0 and 10\n`));
14
34
  process.exit(1);
@@ -16,6 +36,18 @@ function parseScoreValue(raw, label) {
16
36
  return value;
17
37
  }
18
38
 
39
+ function assertTaskIdShape(taskId) {
40
+ const id = taskId === undefined || taskId === null ? '' : String(taskId);
41
+ if (TASK_ID_PATTERN.test(id)) return;
42
+ console.error(chalk.red(`\n❌ Invalid task id: "${id}" — expected the pattern T<n> (T1, T7, T12)`));
43
+ console.error(chalk.gray(
44
+ ` taskScores keys are pinned by feature.schema.json (propertyNames.pattern "^T\\d+$").\n` +
45
+ ` A dotted id is split on the "." and written as a nested key, invisible to the gate-check proxy.\n` +
46
+ ` ${USAGE_LINE}\n`
47
+ ));
48
+ process.exit(1);
49
+ }
50
+
19
51
  /**
20
52
  * `morph-spec score <feature> <task> <score> [--dims a,c,q,t]` — record a
21
53
  * task eval score under `taskScores` in the authoritative feature.json.
@@ -26,6 +58,9 @@ function parseScoreValue(raw, label) {
26
58
  */
27
59
  export async function scoreCommand(featureName, taskId, scoreArg, options = {}) {
28
60
  try {
61
+ // Argument validation runs before any state read or write: a refusal must never
62
+ // leave a fabricated or nested taskScores entry behind.
63
+ assertTaskIdShape(taskId);
29
64
  const score = parseScoreValue(scoreArg, 'score');
30
65
 
31
66
  const feature = getFeature(featureName);
@@ -3,9 +3,10 @@ import { existsSync, readFileSync } from 'fs';
3
3
  import { resolve, join } from 'path';
4
4
  import { getFeature, updateFeature } from '../../core/state/state-manager.js';
5
5
  import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
6
- import { runVerify } from '../../lib/validators/verify-runner.js';
6
+ import { runVerify, VerifyUsageError } from '../../lib/validators/verify-runner.js';
7
7
  import { resolvePrimaryRoot } from '../project/worktree.js';
8
8
  import { getHeadSha } from '../../lib/git/default-branch.js';
9
+ import { EVAL_RUNNER } from '../../lib/tasks/eval-node.js';
9
10
 
10
11
  /** ✓ pass · ✗ fail · – skip */
11
12
  function statusIcon(status) {
@@ -29,6 +30,11 @@ function formatVerifyResult(result) {
29
30
  lines.push('');
30
31
  lines.push(result.status === 'pass' ? chalk.green.bold('✓ PASS') : chalk.red.bold('✗ FAIL'));
31
32
  lines.push('');
33
+ const env = result.nodes.tests?.environment;
34
+ if (env) {
35
+ lines.push(chalk.cyan(`environment: ${env.kind} (${env.image})`));
36
+ lines.push('');
37
+ }
32
38
  lines.push('Nodes:');
33
39
  for (const [name, node] of Object.entries(result.nodes)) {
34
40
  const icon = statusIcon(node.status);
@@ -112,6 +118,54 @@ function decidePersistence({ cwd, featureName, noPersist }) {
112
118
  return { persist: false, reason: 'unregistered-worktree' };
113
119
  }
114
120
 
121
+ /**
122
+ * The per-node status map, in the shape `feature.schema.json` fixes for BOTH
123
+ * writers of it — the per-scope history entry (`verifications.*`) and the
124
+ * singular gate stamp (`verification.nodes`). One function because they were
125
+ * two literal copies twenty-five lines apart: a fifth node meant remembering
126
+ * both, and a stamp missing a node the history has is precisely the kind of
127
+ * half-truth the gate reads as fact.
128
+ *
129
+ * @param {{ nodes: Record<string, { status: string }> }} result - runVerify() output
130
+ * @returns {{ build: string, tests: string, validators: string, e2e: string }}
131
+ */
132
+ function nodeStatuses(result) {
133
+ return {
134
+ build: result.nodes.build.status,
135
+ tests: result.nodes.tests.status,
136
+ validators: result.nodes.validators.status,
137
+ e2e: result.nodes.e2e.status,
138
+ evals: evalsStatusOf(result),
139
+ };
140
+ }
141
+
142
+ /**
143
+ * The status of the eval suites, pulled OUT of the tests rollup.
144
+ *
145
+ * The rollup cannot answer the question Gate 3 asks. A red eval and a red unit
146
+ * test both arrive as `tests: 'fail'`, and — worse — an eval node that was
147
+ * SKIPPED (task scope) leaves the rollup green, so a gate reading `tests` alone
148
+ * cannot tell "the eval ran and passed" from "the eval never ran".
149
+ *
150
+ * Four values, and the distinction between the last two is the whole point:
151
+ * - `'pass'` / `'fail'`: every declared eval node agreed, or at least one did
152
+ * not. Same pessimistic rollup rule the tests node already uses;
153
+ * - `'skip'`: eval nodes ARE declared, but this run did not execute them;
154
+ * - `null`: this project declares NO eval node. Not a skip — there is nothing
155
+ * to skip. `risk-detector.js` pauses on the two for different reasons,
156
+ * because they call for different actions.
157
+ *
158
+ * @param {{nodes: {tests: {runs?: Array}}}} result - runVerify() output
159
+ * @returns {'pass'|'fail'|'skip'|null}
160
+ */
161
+ function evalsStatusOf(result) {
162
+ const runs = (result.nodes.tests?.runs || []).filter((run) => run.runner === EVAL_RUNNER);
163
+ if (runs.length === 0) return null;
164
+ if (runs.some((run) => run.status === 'fail')) return 'fail';
165
+ if (runs.some((run) => run.status === 'pass')) return 'pass';
166
+ return 'skip';
167
+ }
168
+
115
169
  /** Read tasks.json's task list for a feature; [] if missing/corrupt. */
116
170
  function readTasks(featureName) {
117
171
  try {
@@ -129,10 +183,20 @@ function readTasks(featureName) {
129
183
  * for the node cadence; this command wraps it with CLI ergonomics
130
184
  * (human/JSON output, exit codes) and context-aware persistence.
131
185
  *
132
- * Exit codes: 0 = pass (including all-skip) · 1 = fail (>=1 red node) ·
133
- * 2 = usage error (unknown feature or task — the task's `group` decides the
134
- * rubric, so an unknown task id is a hard usage error here, unlike the
135
- * advisory warning `morph-spec score` gives for the same situation).
186
+ * Exit codes: 0 = pass (including all-skip) · 1 = fail (>=1 red node, or a
187
+ * `--record-baseline` the run refused to write) · 2 = usage error.
188
+ *
189
+ * Exit 2 is "the command cannot mean what you typed", and there are four
190
+ * paths to it — never a verdict, because a verdict about nothing is worse
191
+ * than a refusal:
192
+ * - unknown feature, or unknown task id (the task's `group` decides the
193
+ * rubric, so it is a hard usage error here, unlike the advisory warning
194
+ * `morph-spec score` gives for the same situation);
195
+ * - `--filter` that matches no declared node (a filtered-to-empty run is
196
+ * green and proves nothing);
197
+ * - `--record-baseline` with nowhere to write (no `config.json`);
198
+ * - `--linux` without Docker.
199
+ * The last three arrive as `VerifyUsageError` from the runner.
136
200
  */
137
201
  export async function verifyCommand(featureName, taskId, options = {}) {
138
202
  let feature;
@@ -154,21 +218,40 @@ export async function verifyCommand(featureName, taskId, options = {}) {
154
218
  }
155
219
  }
156
220
 
157
- const result = await runVerify(process.cwd(), featureName, taskId || null, {
158
- rubric: options.rubric,
159
- timeout: options.timeout,
160
- skipTests: options.skipTests,
161
- skipBuild: options.skipBuild,
162
- skipE2e: options.skipE2e,
163
- freshE2e: options.freshE2e,
164
- });
221
+ let result;
222
+ try {
223
+ result = await runVerify(process.cwd(), featureName, taskId || null, {
224
+ rubric: options.rubric,
225
+ timeout: options.timeout,
226
+ skipTests: options.skipTests,
227
+ skipBuild: options.skipBuild,
228
+ skipE2e: options.skipE2e,
229
+ freshE2e: options.freshE2e,
230
+ filter: options.filter,
231
+ recordBaseline: options.recordBaseline,
232
+ refreshCache: options.refreshCache,
233
+ linux: options.linux,
234
+ linuxImage: options.linuxImage,
235
+ });
236
+ } catch (err) {
237
+ // A wrong invocation is exit 2, like an unknown feature or task — never a
238
+ // verdict. `--filter` that selects nothing must not print green (or red):
239
+ // there is no verification to report, only a command to fix.
240
+ if (err instanceof VerifyUsageError) {
241
+ console.error(chalk.red(`\n❌ ${err.message}\n`));
242
+ process.exit(2);
243
+ }
244
+ throw err;
245
+ }
165
246
 
166
247
  const scopeKey = taskId ? `taskVerifications.${taskId}` : 'verifications.feature';
167
- const { persist, reason } = decidePersistence({
168
- cwd: process.cwd(),
169
- featureName,
170
- noPersist: options.noPersist,
171
- });
248
+ // `--record-baseline` is config maintenance, not gate evidence: it measures
249
+ // the suite in order to write `expectedCount`. Persisting a `verification`
250
+ // stamp from it would let a maintenance run stand in for the proof a gate
251
+ // asks for — the same class of confusion as a hand-typed baseline.
252
+ const { persist, reason } = options.recordBaseline
253
+ ? { persist: false, reason: 'record-baseline' }
254
+ : decidePersistence({ cwd: process.cwd(), featureName, noPersist: options.noPersist });
172
255
 
173
256
  let compact = null;
174
257
  if (persist) {
@@ -176,12 +259,7 @@ export async function verifyCommand(featureName, taskId, options = {}) {
176
259
  const now = new Date().toISOString();
177
260
  compact = {
178
261
  status: result.status,
179
- nodes: {
180
- build: result.nodes.build.status,
181
- tests: result.nodes.tests.status,
182
- validators: result.nodes.validators.status,
183
- e2e: result.nodes.e2e.status,
184
- },
262
+ nodes: nodeStatuses(result),
185
263
  score: result.nodes.validators.score ?? null,
186
264
  attempts: (prevEntry?.attempts ?? 0) + 1,
187
265
  updatedAt: now,
@@ -195,8 +273,19 @@ export async function verifyCommand(featureName, taskId, options = {}) {
195
273
  // `**Reviewed-SHA:**` token lets approve.js detect a stale review. null
196
274
  // in a non-git checkout (or before the first commit) — fail-open, never
197
275
  // blocks persistence.
276
+ //
277
+ // `nodes` + `e2eReason` são acréscimo de campo, não mudança de forma: o
278
+ // rollup `status` continua onde estava. Existem porque um nó e2e que PULA
279
+ // não reprova o rollup — sem o status do nó e a razão LITERAL do skip, o
280
+ // gate-check lê 'pass' tanto quando o e2e rodou e passou quanto quando ele
281
+ // nunca rodou, e não tem como separar dispensa legítima ("o projeto não
282
+ // containeriza") de ausência de prova ("docker indisponível"). `e2eReason`
283
+ // é null quando o nó não pulou — e um null NUM SKIP é lido como dispensa
284
+ // sem motivo, que risk-detector.js recusa.
198
285
  const stamp = {
199
286
  status: result.status,
287
+ nodes: nodeStatuses(result),
288
+ e2eReason: result.nodes.e2e.status === 'skip' ? (result.nodes.e2e.reason ?? null) : null,
200
289
  sha: getHeadSha(process.cwd()),
201
290
  ts: now,
202
291
  scope: taskId ? `task:${taskId}` : 'feature',
@@ -204,11 +293,26 @@ export async function verifyCommand(featureName, taskId, options = {}) {
204
293
  await updateFeature(featureName, 'verification', stamp);
205
294
  }
206
295
 
296
+ // The refusal always goes to stderr, so it survives --json (whose stdout has
297
+ // to stay parseable) and is never something the reader has to go looking for.
298
+ if (result.baseline && !result.baseline.recorded) {
299
+ console.error(chalk.red(`
300
+ ❌ baseline não gravada: ${result.baseline.reason}
301
+ `));
302
+ }
303
+
207
304
  if (options.json) {
208
305
  const output = { ...result, persisted: persist, ...(persist ? {} : { reason }) };
209
306
  console.log(JSON.stringify(output, null, 2));
210
307
  } else {
211
308
  console.log(formatVerifyResult(result));
309
+ if (result.baseline?.recorded) {
310
+ for (const u of result.baseline.updates) {
311
+ console.log(chalk.green(` baseline ${u.id}: ${u.from ?? '—'} → ${u.to}`));
312
+ }
313
+ console.log(chalk.gray(`Gravada em ${result.baseline.path}
314
+ `));
315
+ }
212
316
  if (persist) {
213
317
  console.log(chalk.gray(`Persisted: ${scopeKey} (attempts: ${compact.attempts})\n`));
214
318
  } else {
@@ -216,5 +320,10 @@ export async function verifyCommand(featureName, taskId, options = {}) {
216
320
  }
217
321
  }
218
322
 
219
- process.exit(result.status === 'pass' ? 0 : 1);
323
+ // A recording the user asked for and did not get is a failure of the
324
+ // invocation even when the run itself was green (a filtered run is green and
325
+ // unrecordable): exiting 0 there would report success for something that did
326
+ // not happen.
327
+ const baselineRefused = Boolean(result.baseline && !result.baseline.recorded);
328
+ process.exit(result.status === 'pass' && !baselineRefused ? 0 : 1);
220
329
  }
@@ -12,7 +12,15 @@ import { getAllOutputPaths, getOutputPath } from '../paths/output-schema.js';
12
12
  const STATE_FILE_NAME = '.morph/state.json';
13
13
  const FEATURES_DIR_NAME = '.morph/features';
14
14
  const FEATURE_FILE_NAME = 'feature.json';
15
- const CONFIG_FILE_NAME = '.morph/config/config.json';
15
+ /**
16
+ * The one spelling of the committed project config's location, relative to a
17
+ * project root. Exported because three modules used to write it out by hand
18
+ * (`readProjectFromConfig` here, `readMorphConfig` and `morphConfigPath` in
19
+ * src/lib/tasks/) — and `morphConfigPath`'s own JSDoc promised the reader that
20
+ * the writer and the reader "can never disagree about which file is meant",
21
+ * a guarantee three copies of a string cannot give. Now they can't.
22
+ */
23
+ export const CONFIG_FILE_NAME = '.morph/config/config.json';
16
24
 
17
25
  // The on-disk state version. NOTE: this tracks the *feature-object* schema, which
18
26
  // is unchanged by the per-feature storage split — features are now persisted to