@polymorphism-tech/morph-spec 8.34.2 → 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 (325) hide show
  1. package/CLAUDE.md +20 -3
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +18 -4
  4. package/bin/validate.js +379 -379
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +5 -4
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/MORPH.md +16 -17
  9. package/framework/agents.json +197 -44
  10. package/framework/ai-pin.json +22 -0
  11. package/framework/commands/morph-media.md +45 -0
  12. package/framework/docs/CLI.md +234 -9
  13. package/framework/evals/proof-rubrics/README.md +76 -0
  14. package/framework/evals/proof-rubrics/eval-de-modelo.md +64 -0
  15. package/framework/evals/proof-rubrics/fiacao.md +54 -0
  16. package/framework/evals/proof-rubrics/guard-cego.md +52 -0
  17. package/framework/evals/proof-rubrics/media-claims.md +109 -0
  18. package/framework/evals/proof-rubrics/mutation-proof.md +77 -0
  19. package/framework/evals/proof-rubrics/observabilidade-genai.md +119 -0
  20. package/framework/evals/proof-rubrics/verde-nao-e-coberto.md +48 -0
  21. package/framework/evals/rubrics/architecture.md +30 -0
  22. package/framework/evals/rubrics/code-quality.md +26 -0
  23. package/framework/evals/rubrics/test-coverage.md +20 -0
  24. package/framework/hooks/claude-code/core/protect-spec-files.js +41 -4
  25. package/framework/hooks/claude-code/statusline.sh +11 -11
  26. package/framework/hooks/git/commit-msg/conventional-commits.sh +33 -33
  27. package/framework/hooks/git/pre-commit/agents.sh +25 -25
  28. package/framework/hooks/git/pre-commit/orchestrator.sh +64 -64
  29. package/framework/hooks/git/pre-commit/specs.sh +50 -50
  30. package/framework/hooks/git/pre-push/run-tests.sh +44 -44
  31. package/framework/hooks/shared/e2e-check.js +135 -22
  32. package/framework/hooks/shared/stdin-reader.js +26 -26
  33. package/framework/rules/ai-agents-standards.md +14 -2
  34. package/framework/schemas/ai-pin.schema.json +97 -0
  35. package/framework/schemas/config.schema.json +107 -0
  36. package/framework/schemas/feature.schema.json +23 -4
  37. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  38. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  39. package/framework/skills/core/morph-apply/SKILL.md +26 -5
  40. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  41. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  42. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  43. package/framework/skills/core/morph-implement/SKILL.md +18 -2
  44. package/framework/skills/core/morph-media/SKILL.md +215 -0
  45. package/framework/skills/core/morph-plan/SKILL.md +33 -2
  46. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  47. package/framework/skills/core/morph-review/SKILL.md +166 -9
  48. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  49. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  50. package/framework/skills/core/morph-uiux/SKILL.md +1 -1
  51. package/framework/standards/PROVENANCE.json +81 -3
  52. package/framework/standards/STANDARDS.json +1483 -839
  53. package/framework/standards/ai-agents/agent-archetypes.md +160 -0
  54. package/framework/standards/ai-agents/agent-session.md +164 -68
  55. package/framework/standards/ai-agents/agent-spec.md +196 -0
  56. package/framework/standards/ai-agents/batching.md +61 -37
  57. package/framework/standards/ai-agents/context-providers.md +97 -24
  58. package/framework/standards/ai-agents/conversational-agent-with-phases.md +239 -0
  59. package/framework/standards/ai-agents/cost-and-budget.md +256 -0
  60. package/framework/standards/ai-agents/durable-workflows-hitl.md +98 -5
  61. package/framework/standards/ai-agents/evals-with-cache.md +289 -0
  62. package/framework/standards/ai-agents/guardrails.md +222 -0
  63. package/framework/standards/ai-agents/llm-runtime-defaults.md +226 -0
  64. package/framework/standards/ai-agents/mcp-server.md +119 -18
  65. package/framework/standards/ai-agents/mcp-tools.md +85 -32
  66. package/framework/standards/ai-agents/media-pipeline.md +352 -0
  67. package/framework/standards/ai-agents/media-video.md +267 -0
  68. package/framework/standards/ai-agents/middleware-patterns.md +39 -5
  69. package/framework/standards/ai-agents/modalities-image-gen.md +354 -105
  70. package/framework/standards/ai-agents/modalities-speech-to-text.md +8 -2
  71. package/framework/standards/ai-agents/modalities-text-to-speech.md +10 -2
  72. package/framework/standards/ai-agents/modalities-vision.md +6 -2
  73. package/framework/standards/ai-agents/multi-agent-patterns.md +61 -5
  74. package/framework/standards/ai-agents/observability-patterns.md +227 -17
  75. package/framework/standards/ai-agents/production.md +38 -9
  76. package/framework/standards/ai-agents/prompt-sources.md +389 -0
  77. package/framework/standards/ai-agents/providers/model-registry.md +174 -39
  78. package/framework/standards/ai-agents/rag-custom-pgvector.md +69 -4
  79. package/framework/standards/ai-agents/service-tier-flex.md +27 -19
  80. package/framework/standards/ai-agents/setup.md +166 -50
  81. package/framework/standards/ai-agents/structured-output.md +34 -3
  82. package/framework/standards/ai-agents/sweet-spot.md +23 -3
  83. package/framework/standards/ai-agents/testing-ai.md +236 -0
  84. package/framework/standards/ai-agents/vector-data-extensions.md +50 -20
  85. package/framework/standards/ai-agents/workflows.md +27 -7
  86. package/framework/standards/frontend/design-system/ai-image-generation.md +12 -0
  87. package/framework/standards/frontend/design-system/ai-video-generation.md +13 -0
  88. package/framework/standards/infrastructure/docker/local-compose-isolation.md +3 -1
  89. package/framework/templates/.idea/morph-templates.xml +92 -0
  90. package/framework/templates/.vscode/morph-templates.code-snippets +186 -0
  91. package/framework/templates/IDE-SNIPPETS.md +262 -262
  92. package/framework/templates/REGISTRY.json +13 -0
  93. package/framework/templates/code/dotnet/abstractions/api-endpoint.cs +7 -7
  94. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +39 -39
  95. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +68 -68
  96. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +117 -117
  97. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +1 -1
  98. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +61 -61
  99. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +107 -107
  100. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +58 -58
  101. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +281 -281
  102. package/framework/templates/code/dotnet/database/migration.cs +83 -83
  103. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +21 -21
  104. package/framework/templates/code/dotnet/feature/request.cs.hbs +4 -4
  105. package/framework/templates/code/dotnet/feature/response.cs.hbs +4 -4
  106. package/framework/templates/code/dotnet/feature/validator.cs.hbs +15 -15
  107. package/framework/templates/code/sql/ai-agents/agent-prompts-migration.sql.template +1 -1
  108. package/framework/templates/code/sql/ai-agents/pgvector-embeddings-migration.sql.template +91 -91
  109. package/framework/templates/code/typescript/contracts.ts +167 -167
  110. package/framework/templates/docs/clarifications.md +253 -253
  111. package/framework/templates/docs/schema-analysis.md +119 -119
  112. package/framework/templates/docs/ui-components.md +52 -52
  113. package/framework/templates/docs/ui-design-system.md +76 -76
  114. package/framework/templates/docs/ui-flows.md +167 -167
  115. package/framework/templates/docs/ui-mockups.md +98 -98
  116. package/framework/templates/dotnet/ai-kit/Directory.Build.props +25 -0
  117. package/framework/templates/dotnet/ai-kit/Directory.Packages.props +26 -0
  118. package/framework/templates/dotnet/ai-kit/Morph.AiKit.sln +56 -0
  119. package/framework/templates/dotnet/ai-kit/NuGet.config +12 -0
  120. package/framework/templates/dotnet/ai-kit/README.md +128 -0
  121. package/framework/templates/dotnet/ai-kit/global.json +9 -0
  122. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +65 -0
  123. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +289 -0
  124. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +91 -0
  125. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +91 -0
  126. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +181 -0
  127. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +14 -0
  128. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +174 -0
  129. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +54 -0
  130. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +75 -0
  131. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +71 -0
  132. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +246 -0
  133. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +57 -0
  134. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +78 -0
  135. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +225 -0
  136. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +463 -0
  137. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +32 -0
  138. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +49 -0
  139. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +214 -0
  140. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +168 -0
  141. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +142 -0
  142. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +349 -0
  143. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +42 -0
  144. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +77 -0
  145. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +28 -0
  146. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +107 -0
  147. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +253 -0
  148. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +103 -0
  149. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +259 -0
  150. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +21 -0
  151. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +47 -0
  152. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +102 -0
  153. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +44 -0
  154. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +192 -0
  155. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +94 -0
  156. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +99 -0
  157. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +97 -0
  158. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +133 -0
  159. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +93 -0
  160. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +112 -0
  161. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +288 -0
  162. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +158 -0
  163. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +29 -0
  164. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +208 -0
  165. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +95 -0
  166. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Morph.AiKit.csproj +38 -0
  167. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +119 -0
  168. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +82 -0
  169. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +33 -0
  170. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +267 -0
  171. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +474 -0
  172. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/model-registry.v2.json +87 -0
  173. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +169 -0
  174. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +49 -0
  175. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +495 -0
  176. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +260 -0
  177. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +405 -0
  178. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +379 -0
  179. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +197 -0
  180. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +139 -0
  181. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +157 -0
  182. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +178 -0
  183. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +153 -0
  184. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +194 -0
  185. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +86 -0
  186. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +213 -0
  187. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +95 -0
  188. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +225 -0
  189. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +215 -0
  190. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +33 -0
  191. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +89 -0
  192. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +55 -0
  193. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +60 -0
  194. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +131 -0
  195. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +133 -0
  196. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +127 -0
  197. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +216 -0
  198. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +264 -0
  199. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +148 -0
  200. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +184 -0
  201. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +89 -0
  202. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +98 -0
  203. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +451 -0
  204. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +76 -0
  205. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +33 -0
  206. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +261 -0
  207. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +374 -0
  208. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/.morph-eval-cache.json +12 -0
  209. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/contents.data +13 -0
  210. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/orcamento.acme.a-resposta-estruturada/1/CF70A7CE27BBD66A/entry.json +7 -0
  211. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/contents.data +13 -0
  212. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.a-saudacao/1/19F631228C6E00BA/entry.json +7 -0
  213. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/contents.data +13 -0
  214. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.acme.b-escalonamento/1/A24A7C9BDD8239D8/entry.json +7 -0
  215. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/contents.data +13 -0
  216. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-cache/cache/triagem.globex.a-saudacao/1/E38CD37DF003EDB7/entry.json +7 -0
  217. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/orcamento/acme/a-resposta-estruturada.json +24 -0
  218. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/a-saudacao.json +23 -0
  219. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/acme/b-escalonamento.json +24 -0
  220. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/eval-datasets/triagem/globex/a-saudacao.json +23 -0
  221. package/framework/templates/feature/decisions.md +122 -122
  222. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +43 -43
  223. package/framework/templates/frontend/nextjs/client-component.tsx.hbs +26 -26
  224. package/framework/templates/frontend/nextjs/env.mjs.hbs +32 -32
  225. package/framework/templates/frontend/nextjs/feature-form.tsx.hbs +56 -56
  226. package/framework/templates/frontend/nextjs/page.tsx.hbs +22 -22
  227. package/framework/templates/frontend/nextjs/tsconfig.json.hbs +26 -26
  228. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +54 -54
  229. package/framework/templates/infrastructure/docker/Dockerfile.template +58 -58
  230. package/framework/templates/infrastructure/docker/dockerfile-api.dockerfile +38 -38
  231. package/framework/templates/infrastructure/docker/dockerfile-web.dockerfile +48 -48
  232. package/framework/templates/infrastructure/github/README.md +56 -56
  233. package/framework/templates/infrastructure/github/actions/docker-build-push/action.yml.hbs +45 -45
  234. package/framework/templates/infrastructure/github/actions/health-check/action.yml.hbs +27 -27
  235. package/framework/templates/infrastructure/github/workflows/docker-build-push.yml.hbs +59 -59
  236. package/framework/templates/infrastructure/github/workflows/dotnet-build.yml.hbs +39 -39
  237. package/framework/templates/integrations/asaas-client.cs +387 -387
  238. package/framework/templates/integrations/asaas-webhook.cs +351 -351
  239. package/framework/templates/meta-prompts/fusion/fusion-agent.md +76 -76
  240. package/framework/templates/meta-prompts/fusion/fusion-aggregator.md +100 -100
  241. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +95 -95
  242. package/framework/templates/project/validate.js +120 -120
  243. package/framework/templates/state.template.json +17 -17
  244. package/framework/templates/ui/design-system.css +226 -226
  245. package/package.json +3 -3
  246. package/src/commands/dashboard/dashboard.js +53 -53
  247. package/src/commands/e2e/e2e.js +15 -2
  248. package/src/commands/index.js +13 -13
  249. package/src/commands/project/checkpoint.js +209 -209
  250. package/src/commands/project/doctor.js +140 -5
  251. package/src/commands/project/update.js +469 -153
  252. package/src/commands/project/worktree.js +8 -1
  253. package/src/commands/state/advance.js +24 -2
  254. package/src/commands/state/approve.js +33 -1
  255. package/src/commands/state/create.js +72 -11
  256. package/src/commands/state/finish.js +333 -21
  257. package/src/commands/state/gate-check.js +190 -6
  258. package/src/commands/state/score.js +36 -1
  259. package/src/commands/validation/verify.js +134 -25
  260. package/src/core/state/phase-state-machine.js +197 -197
  261. package/src/core/state/state-manager.js +9 -1
  262. package/src/core/templates/template-renderer.js +477 -477
  263. package/src/dashboard/app/components/DetailPanel.jsx +11 -11
  264. package/src/dashboard/app/components/FlowGraph.jsx +17 -17
  265. package/src/dashboard/app/components/SearchFilter.jsx +22 -22
  266. package/src/dashboard/app/hooks/useDetailPanel.js +15 -15
  267. package/src/dashboard/app/main.jsx +13 -13
  268. package/src/dashboard/app/pages/AgentMap.jsx +370 -370
  269. package/src/dashboard/app/pages/PhasePipeline.jsx +408 -408
  270. package/src/dashboard/app/pages/RulesMap.jsx +273 -273
  271. package/src/dashboard/app/pages/SkillsLibrary.jsx +366 -366
  272. package/src/dashboard/app/pages/Standards.jsx +332 -332
  273. package/src/dashboard/app/styles/index.css +47 -47
  274. package/src/dashboard/index.html +12 -12
  275. package/src/dashboard/package.json +24 -24
  276. package/src/dashboard/scanner/phases-scanner.js +15 -15
  277. package/src/dashboard/scanner/rubrics-scanner.js +12 -12
  278. package/src/dashboard/scanner/rules-scanner.js +37 -37
  279. package/src/dashboard/scanner/skills-scanner.js +43 -43
  280. package/src/dashboard/scanner/squads-scanner.js +13 -13
  281. package/src/dashboard/vite-plugin-morph-data.js +17 -17
  282. package/src/dashboard/vite.config.js +24 -24
  283. package/src/lib/ai/ai-checks.js +182 -0
  284. package/src/lib/ai/ai-project-scan.js +347 -0
  285. package/src/lib/ai/code-audit.js +181 -0
  286. package/src/lib/ai/comment-stripper.js +184 -0
  287. package/src/lib/ai/pin-drift.js +138 -0
  288. package/src/lib/ai/registry-audit.js +285 -0
  289. package/src/lib/detectors/design-system-detector.js +188 -188
  290. package/src/lib/detectors/index.js +217 -217
  291. package/src/lib/e2e/compose-env.js +50 -4
  292. package/src/lib/evaluator-report.js +269 -15
  293. package/src/lib/generators/design-system-generator.js +508 -508
  294. package/src/lib/generators/settings-generator.js +209 -209
  295. package/src/lib/git/default-branch.js +92 -4
  296. package/src/lib/hooks/hook-executor.js +167 -167
  297. package/src/lib/installers/effects.js +193 -0
  298. package/src/lib/installers/mcp-installer.js +719 -719
  299. package/src/lib/installers/templates-reconcile.js +3 -3
  300. package/src/lib/risk-detector.js +284 -0
  301. package/src/lib/standards/verified-against.js +276 -0
  302. package/src/lib/tasks/eval-node.js +485 -0
  303. package/src/lib/tasks/linux-runner.js +297 -0
  304. package/src/lib/tasks/task-classifier.js +30 -30
  305. package/src/lib/tasks/task-parser.js +209 -209
  306. package/src/lib/tasks/test-nodes.js +486 -0
  307. package/src/lib/tasks/test-runner.js +410 -33
  308. package/src/lib/validators/checkpoint-persistence.js +39 -39
  309. package/src/lib/validators/css/css-validator.js +340 -340
  310. package/src/lib/validators/shared/emit-validator-dispatch.js +14 -14
  311. package/src/lib/validators/shared/issue-counter.js +14 -14
  312. package/src/lib/validators/verify-runner.js +583 -28
  313. package/src/lib/worktree/docker-audit.js +287 -0
  314. package/src/lib/worktree/env-allocator.js +24 -0
  315. package/src/lib/worktree/worktree-audit.js +30 -1
  316. package/src/scripts/setup-infra.js +24 -11
  317. package/src/utils/agent-id-mapper.js +11 -11
  318. package/src/utils/agents-installer.js +18 -8
  319. package/src/utils/claude-md-injector.js +10 -5
  320. package/src/utils/claude-settings-manager.js +300 -300
  321. package/src/utils/file-copier.js +11 -4
  322. package/src/utils/hooks-installer.js +29 -26
  323. package/src/utils/skills-installer.js +36 -29
  324. package/src/utils/version-checker.js +68 -3
  325. package/framework/standards/ai-agents/prompts-in-db.md +0 -214
@@ -0,0 +1,287 @@
1
+ /**
2
+ * docker-audit.js — the part of "which worktrees exist" that the filesystem
3
+ * cannot answer: the docker stacks still running for them.
4
+ *
5
+ * `worktree-audit.js` reconciles git's registry, the disk and morph's features.
6
+ * None of the three can see a CONTAINER. A stack survives everything they look
7
+ * at: `finish` removes the worktree and the branch, the directory disappears,
8
+ * git forgets the registration — and `morph-{feature}` keeps running, holding
9
+ * the feature's whole port block (and its named volumes) against the next
10
+ * worktree that lands on the same block. The failure surfaces later, somewhere
11
+ * else, as `port is already allocated` during an `e2e up` that has nothing to
12
+ * do with the stack that is actually holding the port.
13
+ *
14
+ * Two findings, both `severity: 'warn'` — reported, never a failure of
15
+ * `doctor --worktrees` (`doctor.js` treats `info`/`warn` as non-problems):
16
+ *
17
+ * • docker-stack-orphan — a `morph-*` compose project of THIS repository
18
+ * running for a feature this tree does not know
19
+ * • docker-port-held — a port from a feature's allocated block published
20
+ * by a container belonging to a DIFFERENT compose project
21
+ *
22
+ * This module does not go into `doctor --full`, on purpose (D10): every check
23
+ * there must be satisfiable in the framework repo itself, and a docker check in
24
+ * a repo with no compose file could only be permanently yellow. A check that
25
+ * can never pass where it runs teaches nothing.
26
+ *
27
+ * Nothing here mutates anything — not the registry it reads (see
28
+ * `allocatedFeatures`), and certainly not docker. The `fix` strings name the
29
+ * destructive command; a human runs it.
30
+ *
31
+ * @module lib/worktree/docker-audit
32
+ */
33
+
34
+ import { execFileSync } from 'child_process';
35
+ import { resolve } from 'path';
36
+
37
+ // src/ → framework/hooks/shared/ is the allowed direction (hooks are installed
38
+ // standalone into target projects, where src/ does not exist).
39
+ import {
40
+ composeProjectName,
41
+ defaultExec,
42
+ isDockerAvailable,
43
+ parseComposePs,
44
+ } from '../../../framework/hooks/shared/e2e-check.js';
45
+ import { allocateEnvBlock, allocatedFeatures, portLayout, resolveFleetConfig } from './env-allocator.js';
46
+
47
+ /**
48
+ * The prefix `composeProjectName()` stamps on every stack morph brings up,
49
+ * derived from the function itself rather than retyped — the string `morph-`
50
+ * living in two places is how the two stop agreeing.
51
+ */
52
+ const PROJECT_PROBE = 'probe';
53
+ const MORPH_PROJECT_PREFIX = composeProjectName(PROJECT_PROBE).slice(0, -PROJECT_PROBE.length);
54
+
55
+ /**
56
+ * Same fail-open resolution the other callers use: the COMMON git dir is the
57
+ * one location every linked worktree shares, and it is where the port-block
58
+ * registry lives.
59
+ */
60
+ function resolveGitCommonDir(cwd) {
61
+ try {
62
+ const common = execFileSync('git', ['rev-parse', '--git-common-dir'], {
63
+ cwd, stdio: 'pipe', encoding: 'utf-8', windowsHide: true,
64
+ }).trim();
65
+ return common ? resolve(cwd, common) : null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Path comparison that matches the host's own rules. Windows paths differ in
73
+ * drive-letter case between what git reports and what docker echoes back;
74
+ * treating those as different paths would make every stack look foreign.
75
+ */
76
+ const samePathCase = process.platform === 'win32' ? (s) => s.toLowerCase() : (s) => s;
77
+ const norm = (p) => samePathCase(resolve(String(p)).replace(/\\/g, '/'));
78
+
79
+ /**
80
+ * Is this compose project OURS to talk about?
81
+ *
82
+ * The `morph-` prefix is global to the host, not to a repository: two different
83
+ * repos that use morph-spec and both have a feature named `triagem` produce the
84
+ * SAME compose project name. Without this test, `doctor` in repo A would report
85
+ * repo B's live stack as an orphan and hand the reader a `down -v` — volumes
86
+ * included — for somebody else's running database. The compose file's path is
87
+ * what ties a stack to a repository, and it keeps pointing inside the root even
88
+ * after the worktree directory it named is gone.
89
+ *
90
+ * Unattributable (no config file in the listing) counts as NOT ours: an
91
+ * unknown owner is not evidence of ownership.
92
+ */
93
+ function belongsToRoot(entry, rootNorm) {
94
+ const configFiles = String(entry?.ConfigFiles ?? '').split(',').map((s) => s.trim()).filter(Boolean);
95
+ if (configFiles.length === 0) return false;
96
+ return configFiles.some((f) => {
97
+ try {
98
+ return norm(f).startsWith(`${rootNorm}/`);
99
+ } catch {
100
+ return false;
101
+ }
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Host ports a `docker ps`/`docker compose ps` entry publishes.
107
+ * Both output shapes are accepted rather than pinning a docker version:
108
+ * `Ports` as the human string (`0.0.0.0:4002->5432/tcp`) and `Publishers` as
109
+ * the structured array compose emits.
110
+ *
111
+ * @returns {Set<number>}
112
+ */
113
+ function publishedPorts(entry) {
114
+ const ports = new Set();
115
+ for (const pub of Array.isArray(entry?.Publishers) ? entry.Publishers : []) {
116
+ const p = Number(pub?.PublishedPort);
117
+ if (Number.isInteger(p) && p > 0) ports.add(p);
118
+ }
119
+ const raw = String(entry?.Ports ?? '');
120
+ for (const m of raw.matchAll(/:(\d{1,5})->/g)) {
121
+ const p = Number(m[1]);
122
+ if (Number.isInteger(p) && p > 0) ports.add(p);
123
+ }
124
+ return ports;
125
+ }
126
+
127
+ /** The compose project a container belongs to, from its labels. `null` = unknown. */
128
+ function composeProjectOf(entry) {
129
+ const labels = String(entry?.Labels ?? '');
130
+ for (const part of labels.split(',')) {
131
+ const idx = part.indexOf('=');
132
+ if (idx < 0) continue;
133
+ if (part.slice(0, idx).trim() === 'com.docker.compose.project') return part.slice(idx + 1).trim() || null;
134
+ }
135
+ return null;
136
+ }
137
+
138
+ /** Readable container name for a `docker ps` entry (Names may be a list). */
139
+ function containerNameOf(entry) {
140
+ const names = String(entry?.Names ?? entry?.Name ?? '').split(',').map((s) => s.trim()).filter(Boolean);
141
+ return names[0] || String(entry?.ID ?? 'container desconhecido');
142
+ }
143
+
144
+ /**
145
+ * Parse a docker JSON listing that may be an array or NDJSON.
146
+ * `null` means the output could not be read — DISTINCT from `[]`, which means
147
+ * docker answered and there was nothing running. A caller that conflated the
148
+ * two would turn "could not ask" into "nothing to report", or worse, into a
149
+ * finding.
150
+ *
151
+ * @returns {Array<object>|null}
152
+ */
153
+ function parseDockerJson(raw) {
154
+ const text = String(raw ?? '').trim();
155
+ if (!text) return [];
156
+ const parsed = parseComposePs(text);
157
+ if (parsed.length > 0) return parsed;
158
+ // parseComposePs (the ONE docker-JSON parser in this codebase) collapses two
159
+ // very different answers into `[]`: "docker listed nothing" and "this is not
160
+ // JSON". Only a literal empty array is the first one; everything else that
161
+ // parsed to nothing is garbage, and garbage must not read as "all clear".
162
+ return /^\[\s*\]$/.test(text) ? [] : null;
163
+ }
164
+
165
+ /**
166
+ * Audit the docker stacks around this repository.
167
+ *
168
+ * Returns `[]` — asking docker nothing at all — in three situations, and the
169
+ * silence is the point: this module is loaded by `doctor --worktrees` in every
170
+ * repo, including ones that never containerize anything.
171
+ *
172
+ * 1. no feature ever allocated a port block (no registry): there is no
173
+ * isolation contract to compare a running stack against;
174
+ * 2. the docker CLI is not usable here;
175
+ * 3. any docker call exits non-zero or answers with something that is not
176
+ * JSON — fail-open, the standing rule of this repo's diagnostics.
177
+ *
178
+ * `[]` here reads as "nothing to report", not as "unknown": with no docker or
179
+ * no blocks, nothing CAN be colliding. That is why this is not the `null`-is-
180
+ * UNKNOWN shape `pr-index.js` uses — there the question stays open, here the
181
+ * absence is itself the answer.
182
+ *
183
+ * @param {string} root - primary root of the repository
184
+ * @param {object} [opts]
185
+ * @param {(file: string, args: string[], o?: object) => {code: number, output: string}} [opts.exec]
186
+ * injectable executor, exactly like `e2e-check.js` — the CI runners have no
187
+ * guaranteed daemon, so every path here must be testable without docker
188
+ * @param {Iterable<string>} [opts.features] - feature names this tree knows.
189
+ * Passed IN rather than re-derived: `auditWorktrees` already read
190
+ * `.morph/features/` and `git worktree list`, and two scans of the same fact
191
+ * are two chances to disagree about whether a stack is orphaned
192
+ * @param {string|null} [opts.gitCommonDir]
193
+ * @returns {Array<{type: string, severity: string, detail: string, fix: string, feature?: string}>}
194
+ */
195
+ export function auditDockerStacks(root, { exec = defaultExec, features = [], gitCommonDir } = {}) {
196
+ const commonDir = gitCommonDir === undefined ? resolveGitCommonDir(root) : gitCommonDir;
197
+ if (!commonDir) return [];
198
+
199
+ // (1) No port block was ever allocated → no fleet isolation to protect.
200
+ const allocated = allocatedFeatures(commonDir);
201
+ if (allocated.length === 0) return [];
202
+
203
+ // (2) No docker → nothing can be holding anything.
204
+ if (!isDockerAvailable(root, exec)) return [];
205
+
206
+ const known = new Set(features);
207
+ const rootNorm = norm(root);
208
+ const issues = [];
209
+
210
+ // ---- docker-stack-orphan ----------------------------------------------
211
+ const lsResult = exec('docker', ['compose', 'ls', '--format', 'json'], { cwd: root });
212
+ if (lsResult.code !== 0) return []; // (3) fail-open
213
+ const projects = parseDockerJson(lsResult.output);
214
+ if (projects === null) return []; // (3) unreadable answer is not a finding
215
+
216
+ const knownProjects = new Set([...known].map((f) => composeProjectName(f)));
217
+ for (const entry of projects) {
218
+ const name = String(entry?.Name ?? '').trim();
219
+ if (!name.startsWith(MORPH_PROJECT_PREFIX)) continue;
220
+ if (knownProjects.has(name)) continue;
221
+ if (!belongsToRoot(entry, rootNorm)) continue;
222
+
223
+ const slug = name.slice(MORPH_PROJECT_PREFIX.length);
224
+ issues.push({
225
+ type: 'docker-stack-orphan',
226
+ severity: 'warn',
227
+ feature: slug || null,
228
+ detail: `stack docker "${name}" está de pé (${String(entry?.Status ?? 'estado desconhecido')}) e não corresponde a nenhuma feature desta árvore — sobra de outra sessão, segurando o bloco de portas e os volumes de "${slug}"`,
229
+ // NEVER `morph-spec e2e down {slug}`: that command resolves the feature's
230
+ // compose file, and this stack's feature no longer exists here. The
231
+ // project name is the only handle that still works.
232
+ fix: `docker compose -p ${name} down -v`,
233
+ });
234
+ }
235
+
236
+ // ---- docker-port-held --------------------------------------------------
237
+ const psResult = exec('docker', ['ps', '--format', 'json'], { cwd: root });
238
+ if (psResult.code !== 0) return []; // (3) fail-open
239
+ const containers = parseDockerJson(psResult.output);
240
+ if (containers === null) return [];
241
+
242
+ const { basePort, blockSize } = resolveFleetConfig(root);
243
+ for (const feature of allocated) {
244
+ // A block allocated by a feature this tree does not know belongs to another
245
+ // worktree of the repo; its collisions are that worktree's to report.
246
+ if (!known.has(feature)) continue;
247
+
248
+ let layout;
249
+ try {
250
+ // `allocateEnvBlock` for a feature that is ALREADY in the registry takes
251
+ // the early-return path and writes nothing. The expected ports are
252
+ // DERIVED from portLayout — the same function that writes worktree.env
253
+ // and the compose env — never recomputed here.
254
+ const block = allocateEnvBlock(feature, { gitCommonDir: commonDir, basePort, blockSize });
255
+ layout = portLayout(block, { feature });
256
+ } catch {
257
+ continue; // block out of range / corrupt entry — nothing to compare
258
+ }
259
+
260
+ const ownProject = layout.COMPOSE_PROJECT_NAME;
261
+ const expected = new Map();
262
+ for (const key of ['PORT', 'API_PORT', 'POSTGRES_PORT', 'REDIS_PORT']) {
263
+ const port = Number(layout[key]);
264
+ if (Number.isInteger(port)) expected.set(port, key);
265
+ }
266
+
267
+ for (const container of containers) {
268
+ const project = composeProjectOf(container);
269
+ if (project === ownProject) continue; // the feature's own stack, as intended
270
+ for (const port of publishedPorts(container)) {
271
+ const key = expected.get(port);
272
+ if (!key) continue;
273
+ issues.push({
274
+ type: 'docker-port-held',
275
+ severity: 'warn',
276
+ feature,
277
+ detail: `a porta ${port} (${key} do bloco de "${feature}") está publicada pelo container ${containerNameOf(container)}${project ? ` do projeto compose "${project}"` : ' (sem projeto compose)'}, não por "${ownProject}" — a stack de "${feature}" não sobe enquanto ela estiver presa`,
278
+ fix: project
279
+ ? `docker compose -p ${project} down (ou pare o container ${containerNameOf(container)}) antes de subir a stack de "${feature}"`
280
+ : `docker stop ${containerNameOf(container)} antes de subir a stack de "${feature}"`,
281
+ });
282
+ }
283
+ }
284
+ }
285
+
286
+ return issues;
287
+ }
@@ -110,6 +110,30 @@ export function allocateEnvBlock(feature, { gitCommonDir, basePort = DEFAULT_BAS
110
110
  return { index, base, range: [base, base + blockSize - 1], created: true };
111
111
  }
112
112
 
113
+ /**
114
+ * The features that ALREADY own a block — read-only, allocating nothing.
115
+ *
116
+ * `allocateEnvBlock` is the only sanctioned way to get a block, and it WRITES
117
+ * when the feature has none. A diagnostic that has to ask "who owns a block?"
118
+ * must not answer by handing one out: `doctor` is read-only, and an audit that
119
+ * mutates the registry it audits invents the very state it then reports. So
120
+ * this reader exists to bound the question, and the block itself still comes
121
+ * from `allocateEnvBlock` (which, for a feature listed here, takes the
122
+ * early-return path and writes nothing).
123
+ *
124
+ * An absent or corrupt registry yields `[]` — "nobody allocated anything",
125
+ * which for every caller is the same as "nothing to compare against".
126
+ *
127
+ * @param {string} gitCommonDir
128
+ * @returns {string[]} feature names with an allocated block
129
+ */
130
+ export function allocatedFeatures(gitCommonDir) {
131
+ const registry = readRegistry(gitCommonDir);
132
+ return Object.entries(registry.allocations)
133
+ .filter(([, a]) => Number.isInteger(a?.index))
134
+ .map(([feature]) => feature);
135
+ }
136
+
113
137
  /**
114
138
  * Release a feature's block. Returns false when nothing was allocated.
115
139
  *
@@ -36,6 +36,7 @@ import {
36
36
  import { detectDefaultBranch } from '../git/default-branch.js';
37
37
  import { buildPrIndex } from '../git/pr-index.js';
38
38
  import { classifyBranch, commitsAhead, resolveBaseRef } from '../git/branch-classifier.js';
39
+ import { auditDockerStacks } from './docker-audit.js';
39
40
 
40
41
  const norm = (p) => resolve(p).replace(/\\/g, '/');
41
42
 
@@ -212,10 +213,24 @@ function dirsOnDisk(root) {
212
213
  * directory that exists but git's own
213
214
  * registry does not know as a worktree
214
215
  *
216
+ * • docker-stack-orphan (warn) — a `morph-*` docker stack of this repository
217
+ * still running for a feature this tree does
218
+ * not know, and
219
+ * • docker-port-held (warn) — a port from a feature's block published by
220
+ * another compose project. Both come from
221
+ * `docker-audit.js`: the three sources above
222
+ * cannot see a CONTAINER, and a stack outlives
223
+ * every one of them
224
+ *
215
225
  * @param {string} [cwd]
226
+ * @param {object} [opts]
227
+ * @param {Function} [opts.exec] - injectable executor handed to the docker
228
+ * audit, so the docker paths are testable with no daemon (the CI runners
229
+ * have none). Absent, the real `docker` CLI is used — and answers `[]` when
230
+ * it is not installed.
216
231
  * @returns {Array<{ type: string, detail: string, path?: string, feature?: string, fix: string }>}
217
232
  */
218
- export function auditWorktrees(cwd = process.cwd()) {
233
+ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
219
234
  if (!isGitRepo(cwd)) return [];
220
235
  const root = resolvePrimaryRoot(cwd);
221
236
  const rootNorm = norm(root);
@@ -394,6 +409,20 @@ export function auditWorktrees(cwd = process.cwd()) {
394
409
  }
395
410
  }
396
411
 
412
+ // The fourth source, and the only one that is not a file: the docker stacks.
413
+ // A container survives the removal of the worktree, of the branch and of the
414
+ // feature — none of the three reconciliations above can see it, and it keeps
415
+ // holding the feature's port block. The feature names come from HERE (the
416
+ // ones morph knows plus the ones the registered `morph/*` worktrees carry)
417
+ // rather than from a second scan inside docker-audit: two readings of the
418
+ // same fact are two chances to disagree about which stack is orphaned.
419
+ const treeFeatures = new Set(features);
420
+ for (const entry of registered) {
421
+ const f = entry.branch?.match(/^refs\/heads\/morph\/(.+)$/)?.[1];
422
+ if (f) treeFeatures.add(f);
423
+ }
424
+ issues.push(...auditDockerStacks(root, { exec, features: treeFeatures }));
425
+
397
426
  return issues;
398
427
  }
399
428
 
@@ -14,7 +14,7 @@
14
14
  import { join, dirname, resolve } from 'path';
15
15
  import { fileURLToPath } from 'url';
16
16
  import { execSync } from 'child_process';
17
- import { readFileSync, readdirSync, copyFileSync, unlinkSync } from 'fs';
17
+ import { readFileSync, readdirSync } from 'fs';
18
18
  import {
19
19
  copyDirectory,
20
20
  copyFile,
@@ -33,6 +33,7 @@ import { installSkills } from '../utils/skills-installer.js';
33
33
  import { installAgents, installDomainAgents } from '../utils/agents-installer.js';
34
34
  import { resolvePrimaryRoot } from '../commands/project/worktree.js';
35
35
  import { reconcileLegacyTemplatesCopy } from '../lib/installers/templates-reconcile.js';
36
+ import { realEffects } from '../lib/installers/effects.js';
36
37
 
37
38
  const FRAMEWORK_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'framework');
38
39
 
@@ -61,7 +62,7 @@ async function installRequiredPlugins(log, exec) {
61
62
  * @param {string} rulesDest - Destination rules directory (.claude/rules/)
62
63
  * @param {string[]} projectTags - Stack tags from config (e.g., ['dotnet', 'blazor'])
63
64
  */
64
- export function installRulesForStack(rulesSrc, rulesDest, projectTags) {
65
+ export function installRulesForStack(rulesSrc, rulesDest, projectTags, fx = realEffects()) {
65
66
  const sourceFiles = readdirSync(rulesSrc).filter(f => f.endsWith('.md'));
66
67
  const installed = new Set();
67
68
 
@@ -69,15 +70,19 @@ export function installRulesForStack(rulesSrc, rulesDest, projectTags) {
69
70
  const content = readFileSync(join(rulesSrc, file), 'utf-8');
70
71
  const stacks = parseStacks(content);
71
72
  if (shouldInstall(stacks, projectTags)) {
72
- copyFileSync(join(rulesSrc, file), join(rulesDest, file));
73
+ fx.copyFile(join(rulesSrc, file), join(rulesDest, file));
73
74
  installed.add(file);
74
75
  }
75
76
  }
76
77
 
77
- // Cleanup orphans
78
- for (const file of readdirSync(rulesDest).filter(f => f.endsWith('.md'))) {
79
- if (!installed.has(file)) {
80
- unlinkSync(join(rulesDest, file));
78
+ // Cleanup orphans — calado em `dryRun` pelo mesmo motivo dos laços gêmeos em
79
+ // skills-installer.js e agents-installer.js (D6): `cleanFrameworkDirs()` já
80
+ // apagou `.claude/rules/` inteiro antes de este código rodar de verdade.
81
+ if (!fx.dryRun) {
82
+ for (const file of readdirSync(rulesDest).filter(f => f.endsWith('.md'))) {
83
+ if (!installed.has(file)) {
84
+ fx.remove(join(rulesDest, file));
85
+ }
81
86
  }
82
87
  }
83
88
 
@@ -134,12 +139,20 @@ async function installStackAwareAssets(targetPath, log) {
134
139
  *
135
140
  * @param {string} claudeMdPath - Absolute path to the installed .claude/CLAUDE.md
136
141
  * @param {Array<{ name: string, title: string, domains: string[] }>} agents
142
+ * @param {object} [options]
143
+ * @param {ReturnType<import('../lib/installers/effects.js').createEffects>} [options.fx]
144
+ * @param {string} [options.readFrom] - De onde LER o conteúdo-base.
145
+ * Num `--dry-run` o arquivo de destino ainda não recebeu a cópia que o passo
146
+ * anterior previu, então ler o destino diria "sem marcadores" e o efeito de
147
+ * escrita sumiria da previsão — divergindo da execução real. Lendo a MESMA
148
+ * origem que seria copiada, previsão e execução decidem igual.
137
149
  */
138
- export async function updateClaudeMdAgentsBlock(claudeMdPath, agents = []) {
139
- if (!(await pathExists(claudeMdPath))) return;
150
+ export async function updateClaudeMdAgentsBlock(claudeMdPath, agents = [], options = {}) {
151
+ const { fx = realEffects(), readFrom = claudeMdPath } = options;
152
+ if (!(await pathExists(readFrom))) return;
140
153
  const START = '<!-- morph-agents:start -->';
141
154
  const END = '<!-- morph-agents:end -->';
142
- const content = readFileSync(claudeMdPath, 'utf-8');
155
+ const content = readFileSync(readFrom, 'utf-8');
143
156
  if (!content.includes(START) || !content.includes(END)) return;
144
157
 
145
158
  const list = agents.length > 0
@@ -151,7 +164,7 @@ export async function updateClaudeMdAgentsBlock(claudeMdPath, agents = []) {
151
164
  new RegExp(`${START}[\\s\\S]*?${END}`),
152
165
  block
153
166
  );
154
- await writeFile(claudeMdPath, updated);
167
+ fx.writeFile(claudeMdPath, updated);
155
168
  }
156
169
 
157
170
  /**
@@ -1,11 +1,11 @@
1
- /**
2
- * Map an agents.json ID to the Claude Code subagent_type.
3
- *
4
- * @param {string} id - Agent ID from agents.json (e.g., "nextjs-expert")
5
- * @param {number} [tier] - Agent tier (1-4). Defaults to non-validator.
6
- * @returns {string} subagent_type matching .claude/agents/ filename stem
7
- */
8
- export function agentIdToSubagentType(id, tier) {
9
- const prefix = tier === 4 ? 'morph-validator-' : 'morph-';
10
- return `${prefix}${id}`;
11
- }
1
+ /**
2
+ * Map an agents.json ID to the Claude Code subagent_type.
3
+ *
4
+ * @param {string} id - Agent ID from agents.json (e.g., "nextjs-expert")
5
+ * @param {number} [tier] - Agent tier (1-4). Defaults to non-validator.
6
+ * @returns {string} subagent_type matching .claude/agents/ filename stem
7
+ */
8
+ export function agentIdToSubagentType(id, tier) {
9
+ const prefix = tier === 4 ? 'morph-validator-' : 'morph-';
10
+ return `${prefix}${id}`;
11
+ }
@@ -21,9 +21,10 @@
21
21
  * Evaluator) are assumed by Claude itself per MORPH.md, not as separate agents.
22
22
  */
23
23
 
24
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, unlinkSync } from 'node:fs';
24
+ import { readFileSync, existsSync, readdirSync } from 'node:fs';
25
25
  import { join } from 'path';
26
26
  import { shouldInstall } from '../lib/stack-filter.js';
27
+ import { realEffects } from '../lib/installers/effects.js';
27
28
 
28
29
  /** Default tool scope for domain personas (override per agent via agents.json `tools`). */
29
30
  export const DEFAULT_AGENT_TOOLS = 'Read, Write, Edit, Bash, Glob, Grep';
@@ -79,10 +80,12 @@ const MANAGED_HEADER =
79
80
  * @param {string} frameworkDir - Path to morph-spec framework/ directory
80
81
  * @param {object} options
81
82
  * @param {string[]} [options.projectTags=[]] - Stack tags from config (e.g. ['dotnet', 'nextjs'])
83
+ * @param {ReturnType<import('../lib/installers/effects.js').createEffects>} [options.fx]
84
+ * Coletor de efeitos; o default executa e registra numa lista descartada.
82
85
  * @returns {Promise<{ installed: number }>}
83
86
  */
84
87
  export async function installAgents(projectDir, frameworkDir = 'framework', options = {}) {
85
- const { projectTags = [] } = options;
88
+ const { projectTags = [], fx = realEffects() } = options;
86
89
 
87
90
  const agentsJsonPath = join(frameworkDir, 'agents.json');
88
91
  const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf-8'));
@@ -93,7 +96,7 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
93
96
  .map(([id, data]) => ({ id, ...data }));
94
97
 
95
98
  const targetDir = join(projectDir, '.claude', 'agents');
96
- mkdirSync(targetDir, { recursive: true });
99
+ fx.ensureDir(targetDir);
97
100
 
98
101
  const knownMcpIds = loadKnownMcpIds(frameworkDir);
99
102
 
@@ -111,7 +114,7 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
111
114
  const filename = `morph-${agent.id}.md`;
112
115
  const targetPath = join(targetDir, filename);
113
116
 
114
- writeFileSync(targetPath, renderAgentMd(agent, knownMcpIds), 'utf-8');
117
+ fx.writeFile(targetPath, renderAgentMd(agent, knownMcpIds));
115
118
  installedFilenames.add(filename);
116
119
  installedAgents.push({
117
120
  id: agent.id,
@@ -122,10 +125,17 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
122
125
  installed++;
123
126
  }
124
127
 
125
- // Cleanup orphan morph-* files filtered out by stack
126
- for (const file of readdirSync(targetDir)) {
127
- if (allPossible.has(file) && !installedFilenames.has(file)) {
128
- unlinkSync(join(targetDir, file));
128
+ // Cleanup orphan morph-* files filtered out by stack.
129
+ //
130
+ // Calado em `dryRun` pelo mesmo motivo do laço gêmeo em skills-installer.js
131
+ // (D6): numa execução real do `update` este diretório acabou de ser apagado
132
+ // inteiro por `cleanFrameworkDirs()`, então o laço nunca remove nada. Num
133
+ // dry ingênuo ele veria o diretório antigo e preveria remoções inexistentes.
134
+ if (!fx.dryRun) {
135
+ for (const file of readdirSync(targetDir)) {
136
+ if (allPossible.has(file) && !installedFilenames.has(file)) {
137
+ fx.remove(join(targetDir, file));
138
+ }
129
139
  }
130
140
  }
131
141
 
@@ -8,7 +8,8 @@
8
8
  * The root CLAUDE.md belongs to the USER — morph-spec only adds its import.
9
9
  */
10
10
 
11
- import { pathExists, readFile, writeFile } from './file-copier.js';
11
+ import { pathExists, readFile } from './file-copier.js';
12
+ import { realEffects } from '../lib/installers/effects.js';
12
13
 
13
14
  const MORPH_IMPORT = '@.claude/CLAUDE.md';
14
15
  const MORPH_MARKER = 'MORPH-SPEC';
@@ -24,11 +25,15 @@ const MORPH_MARKER = 'MORPH-SPEC';
24
25
  * Idempotent — does nothing if the import already exists.
25
26
  *
26
27
  * @param {string} claudeMdPath - Absolute path to root CLAUDE.md
28
+ * @param {ReturnType<import('../lib/installers/effects.js').createEffects>} [fx]
29
+ * Coletor de efeitos; o default executa e registra numa lista descartada.
30
+ * A DECISÃO (criar / migrar / anexar / nada) sai de uma LEITURA, que
31
+ * acontece igual nos dois modos — por isso previsão e execução concordam.
27
32
  * @returns {Promise<'created'|'migrated'|'injected'|'already-present'>}
28
33
  */
29
- export async function injectMorphImport(claudeMdPath) {
34
+ export async function injectMorphImport(claudeMdPath, fx = realEffects()) {
30
35
  if (!await pathExists(claudeMdPath)) {
31
- await writeFile(claudeMdPath, `# Project\n\n${MORPH_IMPORT}\n`);
36
+ fx.writeFile(claudeMdPath, `# Project\n\n${MORPH_IMPORT}\n`);
32
37
  return 'created';
33
38
  }
34
39
 
@@ -39,13 +44,13 @@ export async function injectMorphImport(claudeMdPath) {
39
44
  }
40
45
 
41
46
  if (isMorphOnly(content)) {
42
- await writeFile(claudeMdPath, `# Project\n\n${MORPH_IMPORT}\n`);
47
+ fx.writeFile(claudeMdPath, `# Project\n\n${MORPH_IMPORT}\n`);
43
48
  return 'migrated';
44
49
  }
45
50
 
46
51
  // User has their own content — append the import section
47
52
  const separator = content.endsWith('\n') ? '\n' : '\n\n';
48
- await writeFile(claudeMdPath, `${content}${separator}## MORPH-SPEC\n\n${MORPH_IMPORT}\n`);
53
+ fx.writeFile(claudeMdPath, `${content}${separator}## MORPH-SPEC\n\n${MORPH_IMPORT}\n`);
49
54
  return 'injected';
50
55
  }
51
56