@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
@@ -0,0 +1,285 @@
1
+ /**
2
+ * registry-audit.js — o registry de modelos do projeto: tarifa por alias e
3
+ * protocolo (`api`) por alias.
4
+ *
5
+ * As duas perguntas saem do standard `ai-agents/providers/model-registry.md`,
6
+ * não da memória de quem implementa:
7
+ *
8
+ * - **tarifa por alias** — um alias sem tarifa é um custo que ninguém
9
+ * consegue prever antes de a chamada ser paga. O caso medido no acervo é o
10
+ * GHLBrain, cujo mapa `Pricing.Models` é literalmente `{}`.
11
+ * - **`api` por alias** — o standard mede, sobre o pin, que
12
+ * `AgentApi.Responses = 0`, ou seja `default(AgentApi)` é Responses, e que
13
+ * `ModelRegistry.ReadApi` devolve `AgentApi.Responses` quando o campo está
14
+ * ausente. A ausência do campo, portanto, NÃO é defeito: é uma decisão
15
+ * tomada por omissão. O achado NOMEIA o default assumido e pede
16
+ * confirmação — nunca afirma defeito.
17
+ *
18
+ * Severidade: `warn` nas duas. Só `pin-drift` produz `error`.
19
+ */
20
+
21
+ import { readFileSync } from 'node:fs';
22
+ import { existsSync } from 'node:fs';
23
+ import { isAbsolute, join, basename } from 'node:path';
24
+ import { collectProjectFiles } from './ai-project-scan.js';
25
+ import { jsoncToJson } from './comment-stripper.js';
26
+
27
+ /** Limite de tamanho para um JSON candidato (bytes de texto) — evita ler dumps. */
28
+ const MAX_JSON_CHARS = 512 * 1024;
29
+
30
+ /**
31
+ * Lê um candidato a registry aceitando **JSONC**.
32
+ *
33
+ * Não é indulgência: é paridade com o leitor de produção. O exemplar canônico
34
+ * `framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json`
35
+ * — que diz na primeira linha "copie para `config/model-registry.json` no
36
+ * projeto" — tem 20 linhas de `//`, e `ModelRegistry.FromJson` do kit declara
37
+ * `CommentHandling = JsonCommentHandling.Skip` + `AllowTrailingCommas = true`.
38
+ * Com `JSON.parse` cru, as duas checagens de registry saíam `skip` sobre a
39
+ * ÚNICA forma que existem para auditar — e `skip` não muda o código de saída,
40
+ * então o silêncio era total. A auditoria tem de enxergar a mesma forma que o
41
+ * leitor de produção enxerga.
42
+ */
43
+ function readJsonSafe(file) {
44
+ try {
45
+ const raw = readFileSync(file, 'utf8');
46
+ if (raw.length > MAX_JSON_CHARS) return null;
47
+ const text = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
48
+ try {
49
+ return JSON.parse(text);
50
+ } catch {
51
+ return JSON.parse(jsoncToJson(text));
52
+ }
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function isPlainObject(value) {
59
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
60
+ }
61
+
62
+ /** A forma que `ai-agents/providers/model-registry.md` define. */
63
+ function asAliasSource(file, data) {
64
+ if (!isPlainObject(data)) return null;
65
+ if (typeof data.defaultAlias !== 'string') return null;
66
+ if (!isPlainObject(data.aliases)) return null;
67
+ return { file, aliases: data.aliases, defaultAlias: data.defaultAlias, data };
68
+ }
69
+
70
+ /** A forma medida no GHLBrain: a seção `Pricing` de um `appsettings*.json`. */
71
+ function asPricingSource(file, data) {
72
+ if (!isPlainObject(data)) return null;
73
+ const pricing = data.Pricing ?? data.pricing;
74
+ if (!isPlainObject(pricing)) return null;
75
+ const models = pricing.Models ?? pricing.models;
76
+ if (!isPlainObject(models)) return null;
77
+ return { file, models };
78
+ }
79
+
80
+ /**
81
+ * Localiza o registry de modelos do projeto.
82
+ *
83
+ * Ordem, primeira ocorrência ganha (spec §3.5):
84
+ * 1. path declarado em `.morph/config/config.json → project.ai.modelRegistry`;
85
+ * 2. um `*.json` cujo objeto raiz tenha `defaultAlias` **e** um mapa `aliases`;
86
+ * 3. a seção `Pricing` de um `appsettings*.json`.
87
+ *
88
+ * @param {string} root - raiz do projeto
89
+ * @param {object|null} config - conteúdo de `.morph/config/config.json`, se houver
90
+ * @returns {null | { aliasSource: object|null, pricingSource: object|null }}
91
+ * `null` quando NENHUMA das três formas foi encontrada.
92
+ */
93
+ export function locateAiRegistry(root, config = null) {
94
+ let aliasSource = null;
95
+ let pricingSource = null;
96
+
97
+ const declared = config?.project?.ai?.modelRegistry;
98
+ if (typeof declared === 'string' && declared.trim()) {
99
+ const file = isAbsolute(declared) ? declared : join(root, declared);
100
+ if (existsSync(file)) {
101
+ const data = readJsonSafe(file);
102
+ aliasSource = asAliasSource(file, data);
103
+ pricingSource = extractPricingFromRegistry(file, data) ?? pricingSource;
104
+ }
105
+ }
106
+
107
+ if (!aliasSource) {
108
+ const candidates = collectProjectFiles(root, (name) => name.toLowerCase().endsWith('.json'));
109
+ for (const file of candidates) {
110
+ const data = readJsonSafe(file);
111
+ const found = asAliasSource(file, data);
112
+ if (found) {
113
+ aliasSource = found;
114
+ pricingSource = pricingSource ?? extractPricingFromRegistry(file, data);
115
+ break;
116
+ }
117
+ }
118
+ }
119
+
120
+ if (!pricingSource) {
121
+ const appsettings = collectProjectFiles(root, (name) => {
122
+ const lower = name.toLowerCase();
123
+ return lower.startsWith('appsettings') && lower.endsWith('.json');
124
+ });
125
+ for (const file of appsettings) {
126
+ const found = asPricingSource(file, readJsonSafe(file));
127
+ if (found) { pricingSource = found; break; }
128
+ }
129
+ }
130
+
131
+ if (!aliasSource && !pricingSource) return null;
132
+ return { aliasSource, pricingSource };
133
+ }
134
+
135
+ /** Mapa de tarifas embutido no próprio registry de modelos, quando existir. */
136
+ function extractPricingFromRegistry(file, data) {
137
+ if (!isPlainObject(data)) return null;
138
+ const pricing = data.pricing ?? data.Pricing;
139
+ if (isPlainObject(pricing)) {
140
+ const models = pricing.Models ?? pricing.models;
141
+ return { file, models: isPlainObject(models) ? models : pricing };
142
+ }
143
+ return null;
144
+ }
145
+
146
+ /** O alias declara tarifa **no próprio elemento** (a forma v2 do exemplar canônico)? */
147
+ function aliasHasPricing(def) {
148
+ return isPlainObject(def) && isPlainObject(def.pricing);
149
+ }
150
+
151
+ /** O alias declara a AUSÊNCIA de tarifa por escrito? */
152
+ function aliasHasWaiver(def) {
153
+ return isPlainObject(def) && typeof def.pricingWaiver === 'string' && def.pricingWaiver.trim() !== '';
154
+ }
155
+
156
+ /**
157
+ * O documento usa a forma **por alias** (`pricing` / `pricingWaiver` dentro de
158
+ * cada alias) em vez da forma de mapa no topo?
159
+ *
160
+ * Basta UM alias declarar qualquer uma das duas: um documento meio-preenchido
161
+ * ainda é um documento na forma v2, e os aliases que faltam é que são o achado.
162
+ */
163
+ export function usesPerAliasPricing(aliasSource) {
164
+ if (!aliasSource) return false;
165
+ return Object.values(aliasSource.aliases).some((d) => aliasHasPricing(d) || aliasHasWaiver(d));
166
+ }
167
+
168
+ /**
169
+ * Todo alias do registry tem uma DECISÃO de tarifa?
170
+ *
171
+ * Duas formas, e a checagem cobra a que o documento usa:
172
+ *
173
+ * 1. **Por alias** (`model-registry.v2.json`, o exemplar que o framework manda
174
+ * copiar) — a REGRA DURA escrita no próprio exemplar e imposta por
175
+ * `ModelRegistry.ReadPricing`: *"todo alias declara `pricing` OU
176
+ * `pricingWaiver` — nunca os dois, nunca nenhum"*. Um `pricingWaiver` é uma
177
+ * tarifa **decidida**: a ausência está escrita, não esquecida, e acusá-la
178
+ * seria gritar em código correto.
179
+ * 2. **Mapa no topo** (`pricing` na raiz, ou a seção `Pricing.Models` de um
180
+ * `appsettings*.json` — a forma medida no GHLBrain, cujo mapa é
181
+ * literalmente `{}`).
182
+ *
183
+ * Os achados de MAPA (`pricing-map-missing` / `pricing-map-empty`) só existem
184
+ * na forma 2: num documento v2 não há mapa a faltar, e emitir o achado ali era
185
+ * o primeiro dos seis falsos positivos sobre o exemplar canônico.
186
+ *
187
+ * @param {ReturnType<typeof locateAiRegistry>} registry
188
+ * @returns {null | Array<{ id: string, severity: 'warn', alias?: string, message: string }>}
189
+ */
190
+ export function auditModelPricing(registry) {
191
+ if (!registry) return null;
192
+ const { aliasSource, pricingSource } = registry;
193
+ const findings = [];
194
+
195
+ const models = pricingSource?.models ?? null;
196
+ const tariffKeys = models ? new Set(Object.keys(models)) : new Set();
197
+ const perAlias = usesPerAliasPricing(aliasSource);
198
+
199
+ if (!perAlias) {
200
+ if (models === null) {
201
+ findings.push({
202
+ id: 'pricing-map-missing',
203
+ severity: 'warn',
204
+ message: 'Nenhuma tarifa encontrada: nem `pricing`/`pricingWaiver` por alias (a forma do '
205
+ + 'exemplar `model-registry.v2.json`), nem `pricing` na raiz, nem `Pricing.Models` num '
206
+ + 'appsettings. Sem tarifa não há previsão de custo antes de a chamada ser paga.',
207
+ });
208
+ } else if (tariffKeys.size === 0) {
209
+ findings.push({
210
+ id: 'pricing-map-empty',
211
+ severity: 'warn',
212
+ message: `O mapa de tarifas em \`${basename(pricingSource.file)}\` está vazio — declarado e `
213
+ + 'sem uma única entrada. É a forma mais silenciosa de não ter tarifa: o campo existe, o '
214
+ + 'dado não.',
215
+ });
216
+ }
217
+ }
218
+
219
+ if (aliasSource) {
220
+ for (const [alias, def] of Object.entries(aliasSource.aliases)) {
221
+ const model = isPlainObject(def) ? def.model : null;
222
+ const rotulo = `\`${alias}\`${model ? ` (modelo \`${model}\`)` : ''}`;
223
+ const hasPricing = aliasHasPricing(def);
224
+ const hasWaiver = aliasHasWaiver(def);
225
+
226
+ if (hasPricing && hasWaiver) {
227
+ findings.push({
228
+ id: 'alias-pricing-and-waiver',
229
+ severity: 'warn',
230
+ alias,
231
+ message: `O alias ${rotulo} declara \`pricing\` E \`pricingWaiver\`. A regra do exemplar `
232
+ + 'é uma OU a outra, nunca as duas: com as duas presentes não dá para saber se a '
233
+ + 'tarifa vale ou se a isenção vale.',
234
+ });
235
+ continue;
236
+ }
237
+ if (hasPricing || hasWaiver) continue; // decisão tomada — inclusive a de não haver tarifa
238
+
239
+ const cobertoPeloMapa = tariffKeys.has(alias) || (model !== null && tariffKeys.has(model));
240
+ if (cobertoPeloMapa) continue;
241
+
242
+ findings.push({
243
+ id: 'alias-without-pricing',
244
+ severity: 'warn',
245
+ alias,
246
+ message: `O alias ${rotulo} não declara \`pricing\` nem \`pricingWaiver\`, e não aparece em `
247
+ + 'nenhum mapa de tarifas. Não é a tarifa que falta: falta a DECISÃO — um `pricingWaiver` '
248
+ + 'com o motivo por extenso também fecha a questão.',
249
+ });
250
+ }
251
+ }
252
+
253
+ return findings;
254
+ }
255
+
256
+ /**
257
+ * Todo alias declara `api`?
258
+ *
259
+ * O default do standard é **`responses`**: medido sobre o pin, `AgentApi.Responses = 0`
260
+ * e `ModelRegistry.ReadApi` devolve Responses quando o campo está ausente. A mensagem
261
+ * nomeia esse default e PEDE CONFIRMAÇÃO — a ausência do campo é uma decisão por
262
+ * omissão, não um defeito. É a pergunta que o `ModelRegistry.cs` do ProspectPRO
263
+ * documenta ter custado um bug.
264
+ *
265
+ * @param {ReturnType<typeof locateAiRegistry>} registry
266
+ * @returns {null | Array<{ id: string, severity: 'warn', alias: string, message: string }>}
267
+ * `null` quando não há aliases para perguntar.
268
+ */
269
+ export function auditApiPerAlias(registry) {
270
+ if (!registry || !registry.aliasSource) return null;
271
+ const findings = [];
272
+ for (const [alias, def] of Object.entries(registry.aliasSource.aliases)) {
273
+ const api = isPlainObject(def) ? def.api : undefined;
274
+ if (typeof api === 'string' && api.trim()) continue;
275
+ findings.push({
276
+ id: 'alias-without-api',
277
+ severity: 'warn',
278
+ alias,
279
+ message: `O alias \`${alias}\` não declara o campo \`api\`. O registry trata a omissão como `
280
+ + '`responses` (Responses API) — confirme que este alias não precisa falar `chat` '
281
+ + '(Chat Completions). Não é acusação: é a decisão que está sendo tomada por omissão.',
282
+ });
283
+ }
284
+ return findings;
285
+ }
@@ -20,23 +20,37 @@
20
20
 
21
21
  import { join } from 'path';
22
22
 
23
+ import chalk from 'chalk';
24
+
23
25
  import { loadEnvFile } from '../../../framework/hooks/shared/e2e-check.js';
24
- import { allocateEnvBlock, resolveFleetConfig } from '../worktree/env-allocator.js';
26
+ import { allocateEnvBlock, portLayout, resolveFleetConfig } from '../worktree/env-allocator.js';
25
27
 
26
28
  /**
27
29
  * Build the environment a `docker compose` invocation should carry.
28
30
  *
29
- * Port variables mirror `buildEnvFileContent()` in env-allocator.js so a
30
- * compose file and a `.morph/worktree.env` describe the same world: `PORT` for
31
- * the web tier, `ASPNETCORE_URLS` on base+1 for a .NET backend.
31
+ * Port variables come from `portLayout()` in env-allocator.js — the SAME
32
+ * function that renders `.morph/worktree.env` — so a compose file and the
33
+ * worktree manifest describe the same world by construction (`PORT`,
34
+ * `API_PORT`/`ASPNETCORE_URLS`, `POSTGRES_PORT`, `COMPOSE_PROJECT_NAME`).
32
35
  *
33
36
  * `PLAYWRIGHT_BASE_URL` is derived here rather than left to the spec config —
34
37
  * the suite has to be told where the app actually landed, otherwise a feature
35
38
  * on port 4020 gets tested against a hardcoded localhost:3000 and fails with
36
39
  * ECONNREFUSED that looks exactly like a broken app.
37
40
  *
41
+ * The credentials env file loads AFTER the layout but cannot outrank it: a
42
+ * `.env.e2e` copied from a sibling worktree carries THAT worktree's block, and
43
+ * letting it win points this stack at the neighbour's postgres while every
44
+ * other surface still reports the allocated port. The protected keys are the
45
+ * ones actually assigned above — never a second literal list, which is exactly
46
+ * how an offset lands in one writer and not the other. A key that gets ignored
47
+ * is announced on stderr (never stdout: `morph-spec env` writes shell-eval
48
+ * output there), because swapping a silent override for a silent discard just
49
+ * moves the debugging session.
50
+ *
38
51
  * Fail-open throughout: with no git dir (or a corrupt registry) the caller gets
39
- * default ports, which only matters when two stacks run at once.
52
+ * default ports, which only matters when two stacks run at once. With no layout
53
+ * applied nothing is filtered — there the env file is the only port source.
40
54
  *
41
55
  * @param {object} params
42
56
  * @param {string} params.cwd - project root
@@ -47,20 +61,20 @@ import { allocateEnvBlock, resolveFleetConfig } from '../worktree/env-allocator.
47
61
  */
48
62
  export function buildComposeEnv({ cwd, config, featureName, gitCommonDir }) {
49
63
  const env = {};
64
+ /** @type {Set<string>|null} - null while no layout has been applied */
65
+ let layoutKeys = null;
50
66
 
51
67
  if (gitCommonDir) {
52
68
  try {
53
69
  const { basePort, blockSize } = resolveFleetConfig(cwd);
54
70
  const block = allocateEnvBlock(featureName, { gitCommonDir, basePort, blockSize });
55
71
 
56
- env.MORPH_PORT_BASE = String(block.base);
57
- env.MORPH_PORT_RANGE = `${block.range[0]}-${block.range[1]}`;
58
- env.PORT = String(block.base);
59
- env.PLAYWRIGHT_BASE_URL = `http://localhost:${block.base}`;
60
-
61
- if (/\bdotnet\b/i.test(String(config?.project?.stack ?? ''))) {
62
- env.ASPNETCORE_URLS = `http://localhost:${block.base + 1}`;
63
- }
72
+ const layout = {
73
+ ...portLayout(block, { stack: config?.project?.stack ?? '', feature: featureName }),
74
+ PLAYWRIGHT_BASE_URL: `http://localhost:${block.base}`,
75
+ };
76
+ Object.assign(env, layout);
77
+ layoutKeys = new Set(Object.keys(layout));
64
78
  } catch {
65
79
  // No block → default ports. Surfaces later as a port conflict, never here.
66
80
  }
@@ -68,7 +82,34 @@ export function buildComposeEnv({ cwd, config, featureName, gitCommonDir }) {
68
82
 
69
83
  // Credentials live in a gitignored env file, never in the committed config.
70
84
  const envFile = config?.e2e?.auth?.envFile;
71
- if (envFile) Object.assign(env, loadEnvFile(join(cwd, envFile)));
85
+ if (envFile) {
86
+ const ignored = [];
87
+ for (const [key, value] of Object.entries(loadEnvFile(join(cwd, envFile)))) {
88
+ if (layoutKeys?.has(key)) ignored.push(key);
89
+ else env[key] = value;
90
+ }
91
+ if (ignored.length) reportIgnoredLayoutKeys(envFile, ignored, env);
92
+ }
72
93
 
73
94
  return env;
74
95
  }
96
+
97
+ /**
98
+ * Tell the caller — `e2e up`, the `e2e` node of `verify`, `stack-teardown` and
99
+ * `morph-spec env` all reach this through `buildComposeEnv` — which keys of the
100
+ * credentials file were dropped and what won instead.
101
+ *
102
+ * stderr, always: `morph-spec env` prints `export K=v` on stdout for a
103
+ * shell to eval, and `verify` prints JSON there.
104
+ *
105
+ * @param {string} envFile
106
+ * @param {string[]} ignored
107
+ * @param {Record<string,string>} env - the layout that won
108
+ */
109
+ function reportIgnoredLayoutKeys(envFile, ignored, env) {
110
+ const shown = ignored.map((key) => `${key}=${env[key]}`).join(', ');
111
+ console.warn(chalk.yellow(
112
+ ` ⚠ ${envFile}: ${ignored.join(', ')} ignorada(s) — o bloco de portas desta worktree prevalece (${shown}). `
113
+ + 'O envFile carrega credenciais; portas vêm do bloco alocado.',
114
+ ));
115
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Reclaim a feature's verification stack — containers, ports, named volumes
3
+ * and the freshness stamp — from the tree that owns the feature.
4
+ *
5
+ * `morph-spec verify` leaves the stack UP on purpose (cold-starting a
6
+ * container per task is what makes the loop unusable), so something has to be
7
+ * the one place that brings it down when the feature closes. That place is
8
+ * this module, shared by `finish` and `delete --with-worktree`.
9
+ *
10
+ * Three things the old root-side teardown got wrong, each pinned by a test:
11
+ * 1. It ran from the ROOT. A compose that exists only in the worktree was
12
+ * never found, and a bind mount resolved to the wrong checkout.
13
+ * 2. It passed a bare `COMPOSE_PROJECT_NAME`. Without the port block
14
+ * (`PORT`, `POSTGRES_PORT`, …) compose resolves a different config than
15
+ * the one that came up, and `down` misses the containers.
16
+ * 3. It never removed volumes or the stamp. Postgres data and a stamp saying
17
+ * "source unchanged since build" outlived the feature.
18
+ *
19
+ * Entirely fail-open: no compose, no docker, or a stubborn container are all
20
+ * non-events for the caller — a stack that refuses to come down must never
21
+ * fail a finish that already merged.
22
+ *
23
+ * @module lib/e2e/stack-teardown
24
+ */
25
+
26
+ import { execFileSync } from 'child_process';
27
+ import { existsSync, readFileSync } from 'fs';
28
+ import { join, resolve } from 'path';
29
+
30
+ import {
31
+ composeProjectName,
32
+ defaultExec,
33
+ deleteStamp,
34
+ resolveComposeFile,
35
+ tearDownStack,
36
+ } from '../../../framework/hooks/shared/e2e-check.js';
37
+ import { buildComposeEnv } from './compose-env.js';
38
+
39
+ /**
40
+ * @param {string} feature
41
+ * @param {object} params
42
+ * @param {string} params.root - primary root (absolute)
43
+ * @param {string|null} [params.worktreePath] - the feature's live worktree, if any
44
+ * @param {boolean} [params.volumes=true] - also remove named volumes (`down -v`)
45
+ * @param {Function} [params._exec] - injectable executor (testing)
46
+ * @returns {{ attempted: boolean, cwd: string, status: 'pass'|'fail'|'skip', stampDeleted: boolean, output?: string }}
47
+ */
48
+ export function tearDownFeatureStack(feature, { root, worktreePath = null, volumes = true, _exec = defaultExec } = {}) {
49
+ const cwd = worktreePath && existsSync(worktreePath) ? worktreePath : root;
50
+ const projectName = composeProjectName(feature);
51
+ const gitCommonDir = resolveGitCommonDir(root);
52
+
53
+ try {
54
+ const config = readConfig(cwd) ?? readConfig(root);
55
+
56
+ // The owning tree's compose first; the root's copy is the fallback for a
57
+ // worktree already half torn down (versioned file, so equivalent).
58
+ let composeCwd = cwd;
59
+ let composeFile = resolveComposeFile(cwd, config);
60
+ if (!composeFile && cwd !== root) {
61
+ composeCwd = root;
62
+ composeFile = resolveComposeFile(root, config);
63
+ }
64
+
65
+ if (!composeFile) {
66
+ // Nothing to bring down — but a stamp left behind would tell the next
67
+ // `up` that a stack it cannot see is still fresh.
68
+ return { attempted: false, cwd, status: 'skip', stampDeleted: deleteStamp(gitCommonDir, projectName) };
69
+ }
70
+
71
+ const env = {
72
+ COMPOSE_PROJECT_NAME: projectName,
73
+ ...buildComposeEnv({ cwd: composeCwd, config, featureName: feature, gitCommonDir }),
74
+ };
75
+ const result = tearDownStack(composeCwd, { composeFile, projectName, env, volumes }, _exec);
76
+
77
+ return {
78
+ attempted: true,
79
+ cwd: composeCwd,
80
+ status: result.status,
81
+ stampDeleted: deleteStamp(gitCommonDir, projectName),
82
+ ...(result.status === 'fail' && result.output ? { output: result.output } : {}),
83
+ };
84
+ } catch {
85
+ // Unconditional even when the teardown itself blew up: the stamp must not
86
+ // outlive the stack it described.
87
+ return { attempted: false, cwd, status: 'fail', stampDeleted: deleteStamp(gitCommonDir, projectName) };
88
+ }
89
+ }
90
+
91
+ /** BOM-tolerant config read; null when absent or malformed. */
92
+ function readConfig(dir) {
93
+ try {
94
+ const path = join(dir, '.morph', 'config', 'config.json');
95
+ if (!existsSync(path)) return null;
96
+ return JSON.parse(readFileSync(path, 'utf8').replace(/^/, ''));
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ /** Same resolution (and same fail-open) as verify-runner's. */
103
+ function resolveGitCommonDir(cwd) {
104
+ try {
105
+ const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
106
+ cwd,
107
+ stdio: 'pipe',
108
+ encoding: 'utf-8',
109
+ windowsHide: true,
110
+ }).trim();
111
+ return common ? resolve(cwd, common) : null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }