@polymorphism-tech/morph-spec 8.37.1 → 8.40.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 (317) hide show
  1. package/CLAUDE.md +37 -13
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +75 -49
  4. package/claude-plugin.json +1 -1
  5. package/docs/CHEATSHEET.md +1 -1
  6. package/docs/QUICKSTART.md +1 -1
  7. package/framework/CLAUDE.md +6 -5
  8. package/framework/MORPH.md +25 -22
  9. package/framework/agents.json +96 -36
  10. package/framework/docs/CLI.md +54 -17
  11. package/framework/evals/rubrics/code-quality.md +5 -0
  12. package/framework/evals/rubrics/condensed.md +6 -2
  13. package/framework/evals/rubrics/contracts.md +20 -0
  14. package/framework/evals/rubrics/test-coverage.md +17 -6
  15. package/framework/hooks/README.md +9 -3
  16. package/framework/hooks/claude-code/core/graph-refresh-kick.js +52 -0
  17. package/framework/hooks/claude-code/core/telemetry-log.js +172 -87
  18. package/framework/hooks/claude-code/core/validate-completion.js +16 -90
  19. package/framework/hooks/claude-code/core/worktree-context.js +10 -0
  20. package/framework/hooks/shared/command-tokens.js +96 -0
  21. package/framework/hooks/shared/git-exclude.js +44 -0
  22. package/framework/hooks/shared/git-paths.js +52 -0
  23. package/framework/hooks/shared/graph-freshness.js +380 -0
  24. package/framework/hooks/shared/graph-refresh.js +416 -0
  25. package/framework/hooks/shared/morph-cli.js +17 -5
  26. package/framework/hooks/shared/redact-secrets.js +55 -0
  27. package/framework/hooks/shared/telemetry-logger.js +25 -2
  28. package/framework/hooks/shared/transcript-usage.js +80 -0
  29. package/framework/mcp/registry.json +33 -2
  30. package/framework/rubrics/backend.json +2 -2
  31. package/framework/rubrics/default.json +2 -2
  32. package/framework/rubrics/frontend.json +2 -2
  33. package/framework/rules/csharp-standards.md +1 -0
  34. package/framework/rules/infrastructure-standards.md +0 -1
  35. package/framework/rules/morph-workflow.md +10 -4
  36. package/framework/rules/nextjs-standards.md +1 -0
  37. package/framework/schemas/config.schema.json +41 -1
  38. package/framework/schemas/feature.schema.json +5 -1
  39. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  40. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  41. package/framework/skills/core/morph-apply/SKILL.md +40 -11
  42. package/framework/skills/core/morph-brainstorming/SKILL.md +16 -7
  43. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  44. package/framework/skills/core/morph-hotfix/SKILL.md +1 -1
  45. package/framework/skills/core/morph-implement/SKILL.md +44 -10
  46. package/framework/skills/core/morph-media/SKILL.md +1 -1
  47. package/framework/skills/core/morph-plan/SKILL.md +73 -15
  48. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  49. package/framework/skills/core/morph-review/SKILL.md +51 -13
  50. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  51. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  52. package/framework/skills/core/morph-uiux/SKILL.md +39 -13
  53. package/framework/standards/STANDARDS.json +189 -14
  54. package/framework/standards/ai-agents/context-providers.md +16 -26
  55. package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
  56. package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
  57. package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
  58. package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
  59. package/framework/standards/ai-agents/modalities-vision.md +1 -18
  60. package/framework/standards/ai-agents/prompt-sources.md +0 -4
  61. package/framework/standards/ai-agents/testing-ai.md +2 -1
  62. package/framework/standards/architecture/code-comments.md +100 -0
  63. package/framework/standards/backend/dotnet/testing.md +22 -0
  64. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
  65. package/framework/standards/frontend/admin-app/data-table.md +562 -0
  66. package/framework/standards/frontend/admin-app/feedback.md +236 -0
  67. package/framework/standards/frontend/admin-app/forms.md +232 -0
  68. package/framework/standards/frontend/admin-app/page-structure.md +203 -0
  69. package/framework/standards/frontend/design-system/ai-image-generation.md +5 -5
  70. package/framework/standards/frontend/nextjs/components.md +37 -58
  71. package/framework/standards/frontend/nextjs/data-fetching.md +1 -0
  72. package/framework/standards/frontend/nextjs/forms.md +31 -30
  73. package/framework/standards/frontend/nextjs/state-management.md +2 -1
  74. package/framework/standards/frontend/nextjs/testing.md +18 -0
  75. package/framework/standards/integration/mcp/mcp-tools.md +48 -117
  76. package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
  77. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
  78. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
  79. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
  80. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
  81. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
  82. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
  83. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
  84. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
  85. package/framework/templates/code/dotnet/database/migration.cs +49 -83
  86. package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
  87. package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
  88. package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
  89. package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
  90. package/framework/templates/code/dotnet/jobs/job.cs +2 -97
  91. package/framework/templates/code/dotnet/test.cs +2 -87
  92. package/framework/templates/code/typescript/contracts.ts +140 -167
  93. package/framework/templates/context/design-system.md +3 -3
  94. package/framework/templates/docs/proposal.md +11 -0
  95. package/framework/templates/docs/spec.md +15 -0
  96. package/framework/templates/docs/ui-mockups.md +115 -98
  97. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
  98. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
  99. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
  100. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
  101. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
  102. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
  103. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
  110. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
  111. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
  112. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
  113. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
  114. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
  115. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
  116. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
  117. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
  118. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
  119. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
  120. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
  121. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
  122. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
  123. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
  124. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
  125. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
  126. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
  127. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
  128. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
  129. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
  130. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
  131. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
  132. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
  133. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
  134. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
  135. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
  136. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
  137. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
  138. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
  139. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
  140. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
  141. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
  142. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
  143. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
  144. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
  145. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
  146. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
  147. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
  148. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
  149. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
  150. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
  151. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
  152. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
  153. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
  154. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
  155. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
  156. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
  157. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
  158. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
  159. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
  160. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
  161. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
  162. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
  163. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
  164. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
  165. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
  166. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
  167. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
  168. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
  169. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
  170. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
  171. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
  172. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
  173. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
  174. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
  175. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
  176. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
  177. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
  178. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
  179. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
  180. package/framework/templates/feature/decisions.md +124 -122
  181. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
  182. package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
  183. package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
  184. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
  185. package/framework/templates/integrations/asaas-client.cs +332 -387
  186. package/framework/templates/integrations/asaas-webhook.cs +290 -351
  187. package/framework/templates/integrations/neon-auth-config.cs +9 -105
  188. package/framework/templates/project/validate.js +97 -120
  189. package/package.json +2 -2
  190. package/src/commands/dashboard/dashboard.js +80 -53
  191. package/src/commands/graph/affected.js +4 -3
  192. package/src/commands/graph/build.js +109 -18
  193. package/src/commands/graph/explain.js +8 -5
  194. package/src/commands/graph/status.js +14 -8
  195. package/src/commands/mcp/mcp-setup.js +37 -9
  196. package/src/commands/observability/cost.js +148 -18
  197. package/src/commands/project/doctor.js +85 -15
  198. package/src/commands/project/init.js +6 -3
  199. package/src/commands/project/install-plugin-cmd.js +1 -1
  200. package/src/commands/project/update.js +30 -0
  201. package/src/commands/project/worktree.js +73 -90
  202. package/src/commands/state/advance.js +18 -0
  203. package/src/commands/state/approve.js +15 -0
  204. package/src/commands/state/archive.js +62 -9
  205. package/src/commands/state/finish.js +62 -14
  206. package/src/commands/state/gate-check.js +8 -1
  207. package/src/commands/state/gate-decision.js +8 -4
  208. package/src/commands/state/report-check.js +90 -9
  209. package/src/commands/state/usecase.js +66 -0
  210. package/src/commands/tasks/dag.js +17 -2
  211. package/src/commands/validation/validate-feature.js +6 -1
  212. package/src/commands/validation/verify.js +20 -1
  213. package/src/dashboard/app/App.jsx +80 -16
  214. package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
  215. package/src/dashboard/app/components/CommandPalette.jsx +155 -0
  216. package/src/dashboard/app/components/DetailPanel.jsx +47 -11
  217. package/src/dashboard/app/components/EntityLink.jsx +55 -0
  218. package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
  219. package/src/dashboard/app/components/FlowGraph.jsx +19 -17
  220. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
  221. package/src/dashboard/app/components/Markdown.jsx +47 -0
  222. package/src/dashboard/app/components/PhaseRail.jsx +111 -0
  223. package/src/dashboard/app/components/SearchFilter.jsx +40 -22
  224. package/src/dashboard/app/components/Sidebar.jsx +146 -37
  225. package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
  226. package/src/dashboard/app/hooks/usePref.js +38 -0
  227. package/src/dashboard/app/lib/format.js +69 -0
  228. package/src/dashboard/app/lib/phases.js +56 -0
  229. package/src/dashboard/app/lib/project.js +50 -0
  230. package/src/dashboard/app/pages/Activity.jsx +24 -0
  231. package/src/dashboard/app/pages/AgentMap.jsx +371 -370
  232. package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
  233. package/src/dashboard/app/pages/Features.jsx +400 -0
  234. package/src/dashboard/app/pages/Guide.jsx +146 -0
  235. package/src/dashboard/app/pages/HookInspector.jsx +357 -322
  236. package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
  237. package/src/dashboard/app/pages/Portfolio.jsx +156 -0
  238. package/src/dashboard/app/pages/Rubrics.jsx +101 -0
  239. package/src/dashboard/app/pages/RulesMap.jsx +300 -273
  240. package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
  241. package/src/dashboard/app/pages/Standards.jsx +407 -332
  242. package/src/dashboard/app/styles/index.css +187 -47
  243. package/src/dashboard/index.html +14 -12
  244. package/src/dashboard/package.json +32 -24
  245. package/src/dashboard/scanner/activity-scanner.js +351 -0
  246. package/src/dashboard/scanner/agents-scanner.js +63 -1
  247. package/src/dashboard/scanner/docs-scanner.js +59 -0
  248. package/src/dashboard/scanner/feature-status.js +10 -0
  249. package/src/dashboard/scanner/features-scanner.js +360 -0
  250. package/src/dashboard/scanner/hooks-scanner.js +8 -1
  251. package/src/dashboard/scanner/index.js +14 -7
  252. package/src/dashboard/scanner/phases-scanner.js +14 -15
  253. package/src/dashboard/scanner/portfolio.js +153 -0
  254. package/src/dashboard/scanner/project-api.js +195 -0
  255. package/src/dashboard/scanner/rules-scanner.js +42 -37
  256. package/src/dashboard/scanner/skills-scanner.js +105 -43
  257. package/src/dashboard/scanner/standards-scanner.js +4 -1
  258. package/src/dashboard/vite-plugin-morph-data.js +134 -17
  259. package/src/lib/ai/comment-stripper.js +101 -4
  260. package/src/lib/detectors/claude-config-detector.js +2 -7
  261. package/src/lib/detectors/index.js +5 -1
  262. package/src/lib/evaluator-report.js +47 -0
  263. package/src/lib/generators/handoff-generator.js +4 -18
  264. package/src/lib/generators/settings-generator.js +208 -209
  265. package/src/lib/git/default-branch.js +52 -0
  266. package/src/lib/git/tree-fingerprint.js +58 -0
  267. package/src/lib/git/tree-state.js +167 -0
  268. package/src/lib/graph/god-nodes.js +12 -3
  269. package/src/lib/graph/graph-health.js +48 -2
  270. package/src/lib/graph/graph-loader.js +99 -25
  271. package/src/lib/graph/graphifyignore.js +145 -28
  272. package/src/lib/graph/sanitize.js +117 -11
  273. package/src/lib/installers/mcp-installer.js +724 -719
  274. package/src/lib/installers/memory-installer.js +1 -2
  275. package/src/lib/projects/registry.js +154 -0
  276. package/src/lib/risk-detector.js +90 -0
  277. package/src/lib/specs/acervo-format.js +70 -0
  278. package/src/lib/specs/acervo-lifecycle.js +264 -0
  279. package/src/lib/specs/adr-index.js +169 -0
  280. package/src/lib/specs/revocations.js +156 -0
  281. package/src/lib/specs/screen-checklist.js +357 -0
  282. package/src/lib/specs/usecase-check.js +20 -16
  283. package/src/lib/specs/usecase-promoter.js +103 -38
  284. package/src/lib/specs/usecase-registry.js +7 -7
  285. package/src/lib/tasks/tasks-json-parser.js +2 -3
  286. package/src/lib/telemetry/metrics.js +386 -0
  287. package/src/lib/telemetry/telemetry-reader.js +161 -51
  288. package/src/lib/telemetry/verify-event.js +79 -0
  289. package/src/lib/validators/code-budget/code-budget-validator.js +200 -0
  290. package/src/lib/validators/code-budget/comment-scanner.js +75 -0
  291. package/src/lib/validators/code-budget/index.js +9 -0
  292. package/src/lib/validators/reuse/reuse-validator.js +4 -2
  293. package/src/lib/validators/reuse/symbol-scanner.js +27 -22
  294. package/src/lib/validators/validation-runner.js +21 -5
  295. package/src/lib/validators/verify-runner.js +11 -0
  296. package/src/lib/worktree/finish-backup.js +739 -0
  297. package/src/lib/worktree/ignored-triage.js +15 -8
  298. package/src/lib/worktree/worktree-audit.js +5 -0
  299. package/src/scripts/install-plugin.js +1 -1
  300. package/src/scripts/setup-infra.js +31 -22
  301. package/src/utils/agents-installer.js +338 -24
  302. package/src/utils/banner.js +1 -1
  303. package/src/utils/hooks-installer.js +75 -15
  304. package/src/commands/index.js +0 -13
  305. package/src/commands/project/checkpoint.js +0 -209
  306. package/src/core/state/phase-state-machine.js +0 -197
  307. package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
  308. package/src/dashboard/app/pages/LiveState.jsx +0 -206
  309. package/src/dashboard/scanner/squads-scanner.js +0 -13
  310. package/src/dashboard/scanner/state-scanner.js +0 -85
  311. package/src/lib/generators/index.js +0 -10
  312. package/src/lib/hooks/hook-executor.js +0 -167
  313. package/src/lib/validators/checkpoint-persistence.js +0 -39
  314. package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
  315. package/src/lib/validators/shared/index.js +0 -12
  316. package/src/lib/validators/shared/issue-counter.js +0 -14
  317. package/src/lib/validators/shared/result-formatter.js +0 -124
@@ -18,10 +18,10 @@
18
18
  * REPLICABLE — the primary root has the same relative path with identical
19
19
  * content. Deleting the worktree's copy loses nothing; the
20
20
  * original stays where it always was.
21
- * DISPOSABLE — a well-known build artifact (`bin/`, `obj/`, `node_modules/`,
22
- * `__pycache__/`, `test-results/`, `.auth/`, `*.tsbuildinfo`).
23
- * Reproducible from source, which is a STRONGER guarantee than
24
- * backed up.
21
+ * DISPOSABLE — a well-known build artifact or cache (`bin/`, `obj/`,
22
+ * `node_modules/`, `.next/`, `TestResults/`, `graphify-out/`…,
23
+ * see DISPOSABLE_DIR_NAMES). Reproducible from source, which is a
24
+ * STRONGER guarantee than backed up.
25
25
  * EXCLUSIVE — everything else: no counterpart in the root, or a counterpart
26
26
  * whose content differs. These are the only bytes at stake, and
27
27
  * the caller copies them to `finish-backup` before proceeding.
@@ -37,10 +37,16 @@ import { existsSync, lstatSync, readdirSync, readFileSync } from 'fs';
37
37
  import { join } from 'path';
38
38
 
39
39
  /**
40
- * Well-known, regenerable build artifacts. Deliberately narrow — do NOT add
41
- * speculative patterns (`dist/`, `.next/`) that a project might legitimately
42
- * hand-edit; anything unlisted still reaches the EXCLUSIVE bucket, where it is
43
- * backed up rather than assumed disposable.
40
+ * Well-known, regenerable build artifacts and caches. Deliberately narrow — do
41
+ * NOT add names a project might use for hand-written output (`dist/`,
42
+ * `build/`, `coverage/`); anything unlisted still reaches the EXCLUSIVE bucket,
43
+ * where it is backed up rather than assumed disposable. Each name here is a
44
+ * directory its own tool rewrites from scratch: `.next/` (next build/dev),
45
+ * `.turbo/`, `.pytest_cache/`, `TestResults/` (VSTest), `graphify-out/`
46
+ * (`morph-spec graph build`). `.next/` is also the one that makes a backup
47
+ * weigh hundreds of megabytes; `graphify-out/` is seeded into every worktree
48
+ * and is past the replica size cap (18 MB in a real project), so without its
49
+ * entry every `finish` would back it up.
44
50
  *
45
51
  * `.auth/` (Playwright storage state) belongs here for a reason worth stating:
46
52
  * it holds a captured session, so it LOOKS like precious local state — but it
@@ -49,6 +55,7 @@ import { join } from 'path';
49
55
  */
50
56
  const DISPOSABLE_DIR_NAMES = new Set([
51
57
  'bin', 'obj', 'node_modules', '__pycache__', 'test-results', '.auth',
58
+ '.next', '.turbo', '.pytest_cache', 'TestResults', 'graphify-out',
52
59
  ]);
53
60
  const DISPOSABLE_FILE_RE = /\.tsbuildinfo$/;
54
61
 
@@ -37,6 +37,7 @@ 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
39
  import { auditDockerStacks } from './docker-audit.js';
40
+ import { auditFinishBackups } from './finish-backup.js';
40
41
 
41
42
  const norm = (p) => resolve(p).replace(/\\/g, '/');
42
43
 
@@ -423,6 +424,10 @@ export function auditWorktrees(cwd = process.cwd(), { exec } = {}) {
423
424
  }
424
425
  issues.push(...auditDockerStacks(root, { exec, features: treeFeatures }));
425
426
 
427
+ // What a worktree removal leaves behind in `.git/morph-spec/finish-backup/`
428
+ // past its retention — `warn`, pruned by `--prune` and by the next finish.
429
+ issues.push(...auditFinishBackups(root));
430
+
426
431
  return issues;
427
432
  }
428
433
 
@@ -100,7 +100,7 @@ async function downloadPluginFiles(pluginName, sha, destDir) {
100
100
 
101
101
  /**
102
102
  * Install a Claude Code plugin from GitHub.
103
- * @param {string} pluginName - e.g. "superpowers"
103
+ * @param {string} pluginName - e.g. "context7"
104
104
  * @returns {{ installed: boolean, version: string, alreadyInstalled: boolean }}
105
105
  */
106
106
  export async function installPlugin(pluginName) {
@@ -2,9 +2,8 @@
2
2
  * setup-infra.js
3
3
  *
4
4
  * Headless infrastructure setup for MORPH-SPEC.
5
- * Extracted from init.js (steps 1-10) so it can be called by the
6
- * /morph-init Claude Code skill via `morph-spec setup-infra` without
7
- * interactive prompts or stack detection.
5
+ * Extracted from init.js (steps 1-10) so it can run as
6
+ * `morph-spec setup-infra` without interactive prompts or stack detection.
8
7
  *
9
8
  * Usage:
10
9
  * import { setupInfra } from './setup-infra.js';
@@ -38,7 +37,6 @@ import { realEffects } from '../lib/installers/effects.js';
38
37
  const FRAMEWORK_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'framework');
39
38
 
40
39
  const REQUIRED_PLUGINS = [
41
- 'superpowers@claude-plugins-official',
42
40
  'context7@claude-plugins-official',
43
41
  ];
44
42
 
@@ -174,16 +172,23 @@ export async function updateClaudeMdAgentsBlock(claudeMdPath, agents = [], optio
174
172
  * @param {string} targetPath - Absolute path to the target project directory
175
173
  * @param {object} options
176
174
  * @param {Function} options._exec - exec function (for testing)
175
+ * @param {Function} [options._detectGraphifyVersion] - graphify probe (for testing: the
176
+ * step-18b branches differ with graphify installed or not, and CI has no Python)
177
+ * @param {Function} [options._log] - log sink (for testing: replaces stdout, which the test
178
+ * runner itself reports through — stubbing it swallows test results)
177
179
  * @param {boolean} options.reinstall - If true, only re-install stack-aware assets (rules, skills, agents)
178
180
  * @param {boolean} options.force - Bypass the secondary-worktree guard (see below). Callers
179
181
  * (e.g. `initCommand`) that already performed their own equivalent check pass this so the
180
182
  * check isn't redundantly re-applied here.
181
183
  */
182
- export async function setupInfra(targetPath, { _exec = execSync, reinstall = false, force = false } = {}) {
184
+ export async function setupInfra(targetPath, {
185
+ _exec = execSync, _detectGraphifyVersion = null, _log = null, reinstall = false, force = false,
186
+ } = {}) {
183
187
  const quiet = process.env.MORPH_QUIET === '1';
184
188
 
185
189
  function log(msg) {
186
- if (!quiet) process.stdout.write(msg + '\n');
190
+ if (_log) _log(msg);
191
+ else if (!quiet) process.stdout.write(msg + '\n');
187
192
  }
188
193
 
189
194
  // Guard: refuse a FULL install from inside a secondary (linked) worktree.
@@ -441,24 +446,26 @@ export async function setupInfra(targetPath, { _exec = execSync, reinstall = fal
441
446
  // (setup-infra-cmd.js), então nada aqui pode escapar do try.
442
447
  log('Step 18b: Codebase graph (graphify)...');
443
448
  try {
444
- const { writeGraphifyignore } = await import('../lib/graph/graphifyignore.js');
449
+ const { writeGraphifyignore, describeOutdatedIgnore } = await import('../lib/graph/graphifyignore.js');
445
450
  const {
446
451
  detectInstaller, installArgs, detectGraphifyVersion,
447
- GRAPHIFY_VERSION, GRAPHIFY_BIN, GRAPHIFY_PACKAGE,
452
+ GRAPHIFY_VERSION, GRAPHIFY_PACKAGE,
448
453
  } = await import('../lib/graph/graphify-runtime.js');
449
454
  const { ensureClaudeignore } = await import('../utils/file-copier.js');
450
455
  const { scanDatabaseSchema } = await import('../lib/generators/architecture-scanner.js');
451
456
 
452
457
  const ignore = writeGraphifyignore(targetPath, projectTags ?? []);
453
- log(ignore.written
454
- ? ' ✓ .graphifyignore (stack-aware)'
455
- : ' ✓ .graphifyignore já existia — preservado');
458
+ if (ignore.migrated) log(' ✓ .graphifyignore atualizado (dublês, fixtures e helpers de teste no grafo)');
459
+ else if (ignore.written) log(' ✓ .graphifyignore (stack-aware)');
460
+ else log(' ✓ .graphifyignore já existia — preservado');
461
+ if (ignore.outdated) log(` ⚠ ${describeOutdatedIgnore(ignore.outdated)}`);
456
462
 
457
463
  // Um graph.json de 12 MB reescrito a cada commit invalida o prompt cache.
458
464
  await ensureClaudeignore(targetPath, ['graphify-out/']);
459
465
  log(' ✓ graphify-out/ no .claudeignore');
460
466
 
461
- const installed = detectGraphifyVersion();
467
+ const probe = _detectGraphifyVersion ?? detectGraphifyVersion;
468
+ const installed = probe();
462
469
  if (installed === GRAPHIFY_VERSION) {
463
470
  log(` ✓ graphify ${installed} já instalado`);
464
471
  } else {
@@ -479,16 +486,18 @@ export async function setupInfra(targetPath, { _exec = execSync, reinstall = fal
479
486
  }
480
487
  }
481
488
 
482
- if (detectGraphifyVersion()) {
483
- try {
484
- // Só os git hooks (post-commit/post-checkout/merge driver). NUNCA
485
- // `graphify claude install`: ele escreveria uma seção no CLAUDE.md e um
486
- // PreToolUse hook próprio, colidindo com os hooks do morph-spec.
487
- _exec(`${GRAPHIFY_BIN} hook install`, { cwd: targetPath, stdio: 'ignore' });
488
- log(' ✓ git hooks do graphify (rebuild assíncrono no post-commit)');
489
- } catch {
490
- log(' ⚠ graphify hook install falhou — rode manualmente se quiser rebuild automático');
491
- }
489
+ // Nenhum hook do graphify: o refresh mora nos hooks do Claude (step 16 —
490
+ // graph-refresh-kick + SessionStart), que o `update` reinstala. O
491
+ // `graphify hook install` ficou de fora por três defeitos medidos no 0.9.50:
492
+ // sai sem fazer nada em worktree vinculada, não tem post-merge (merge e pull
493
+ // nunca rebuildam) e, num commit depois de um fast-forward, carimba o HEAD
494
+ // num grafo sem os arquivos do merge. E nunca `graphify claude install`:
495
+ // escreveria no CLAUDE.md e um PreToolUse próprio.
496
+ //
497
+ // A linha só sai com o graphify presente, e diz o que dispara de fato: um
498
+ // commit feito no terminal não dispara nada até o próximo desses.
499
+ if (probe()) {
500
+ log(' ✓ refresh automático do grafo da raiz: depois dos comandos git/gh/morph-spec do agente e no início de cada sessão');
492
501
  }
493
502
  } catch (err) {
494
503
  log(` ⚠ Step do grafo pulado: ${err.message}`);
@@ -30,19 +30,229 @@ import { realEffects } from '../lib/installers/effects.js';
30
30
  export const DEFAULT_AGENT_TOOLS = 'Read, Write, Edit, Bash, Glob, Grep';
31
31
 
32
32
  /**
33
- * Load the ids of MCP servers known to the framework registry
34
- * (framework/mcp/registry.json). Used to validate agents.json `mcp` entries:
35
- * only registry-known servers become tool grants (fail-open on any error).
33
+ * A persona that can change files: its effective tool scope grants Write or Edit.
34
+ * Derived from the tools, so a persona made read-only stops being one.
35
+ *
36
+ * @param {object} agent - agents.json entry
37
+ * @returns {boolean}
38
+ */
39
+ export function isImplementer(agent) {
40
+ const tools = String(agent?.tools ?? DEFAULT_AGENT_TOOLS).split(',').map((t) => t.trim());
41
+ return tools.includes('Write') || tools.includes('Edit');
42
+ }
43
+
44
+ /**
45
+ * Turn ceiling for a persona that omits agents.json `maxTurns` (ADR-004).
46
+ *
47
+ * 20 is the value the generator hardcoded before the field existed — the v4
48
+ * Tier-2 default (commit 93aad05f) that survived the v5 flattening with no
49
+ * recorded rationale. Keeping it as the default is what lets an old
50
+ * agents.json render byte-for-byte the same .md as before.
51
+ */
52
+ export const DEFAULT_MAX_TURNS = 20;
53
+
54
+ /** Share of the ceiling (percent) at which a sub-agent checkpoints (partial report). */
55
+ const CHECKPOINT_PERCENT = 70;
56
+
57
+ /**
58
+ * The ONE rule for a valid `maxTurns`. Four readers — installAgents,
59
+ * checkAgentsDrift, doctor's validateAgentsJson and validate-framework.js
60
+ * Check 6 — so the same agents.json can never pass in one and fail in another.
61
+ *
62
+ * Absent (`undefined`) is valid: it means "use the default". Anything present
63
+ * must be an integer >= 1. `null` is NOT absent — whoever wants the default
64
+ * removes the field.
65
+ *
66
+ * @param {unknown} value - the raw agents.json value
67
+ * @returns {string|null} what was expected, or null when valid
68
+ */
69
+ export function maxTurnsProblem(value) {
70
+ if (value === undefined) return null;
71
+ return Number.isInteger(value) && value >= 1 ? null : 'esperado inteiro >= 1';
72
+ }
73
+
74
+ /**
75
+ * The full message for an invalid `maxTurns` — persona, value received, what
76
+ * is expected and how to get the default back. Every reader prints this one.
77
+ *
78
+ * @param {string} id - persona id (agents.json key)
79
+ * @param {unknown} value - the raw agents.json value
80
+ * @returns {string|null} null when valid
81
+ */
82
+ export function maxTurnsError(id, value) {
83
+ const problem = maxTurnsProblem(value);
84
+ if (!problem) return null;
85
+ const shown = typeof value === 'string' || (value !== null && typeof value === 'object')
86
+ ? JSON.stringify(value)
87
+ : String(value);
88
+ return `agents.json: persona "${id}" tem maxTurns inválido (${shown}) — ${problem}. ` +
89
+ `Remova o campo para usar o default ${DEFAULT_MAX_TURNS}.`;
90
+ }
91
+
92
+ /**
93
+ * Resolve the ceiling a persona is generated with: its `maxTurns`, or the
94
+ * default when absent. Throws `maxTurnsError()` on an invalid value.
95
+ *
96
+ * @param {object} agent - agents.json entry with `id` merged in
97
+ * @returns {number}
98
+ */
99
+ export function resolveMaxTurns(agent) {
100
+ const error = maxTurnsError(agent.id, agent.maxTurns);
101
+ if (error) throw new Error(error);
102
+ return agent.maxTurns ?? DEFAULT_MAX_TURNS;
103
+ }
104
+
105
+ /** A surface id is the key the mockup checklist names (`Superfície: admin-app`). */
106
+ const SURFACE_ID_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
107
+
108
+ const SURFACE_KEYS = new Set(['when', 'standards']);
109
+
110
+ /**
111
+ * The ONE rule for a valid `surfaces` — the optional map that splits a
112
+ * persona's standards by the kind of screen it works on
113
+ * (`{ "admin-app": { when, standards: [ids] } }`). Same four readers as
114
+ * `maxTurnsProblem`, for the same reason.
115
+ *
116
+ * Absent (`undefined`) is valid: the persona reads only `standards`. A standard
117
+ * listed both in `standards` (read on every surface) and in a surface is
118
+ * refused: that is exactly how a "separated" persona keeps loading marketing
119
+ * standards on a CRUD screen without anyone noticing.
120
+ *
121
+ * @param {unknown} surfaces - the raw agents.json value
122
+ * @param {unknown} [standards] - the persona's universal `standards`
123
+ * @returns {string|null} what is wrong, or null when valid
124
+ */
125
+ export function surfacesProblem(surfaces, standards = []) {
126
+ if (surfaces === undefined) return null;
127
+ if (surfaces === null || typeof surfaces !== 'object' || Array.isArray(surfaces)) {
128
+ return 'esperado objeto { "<superfície>": { when, standards } }';
129
+ }
130
+ const entries = Object.entries(surfaces);
131
+ if (entries.length === 0) return 'esperado ao menos uma superfície';
132
+
133
+ const universal = new Set(Array.isArray(standards) ? standards : []);
134
+ for (const [name, def] of entries) {
135
+ if (!SURFACE_ID_RE.test(name)) return `superfície "${name}" — o id é kebab-case (ex.: "admin-app")`;
136
+ if (def === null || typeof def !== 'object' || Array.isArray(def)) {
137
+ return `superfície "${name}" — esperado { when, standards }`;
138
+ }
139
+ const unknown = Object.keys(def).filter((k) => !SURFACE_KEYS.has(k));
140
+ if (unknown.length > 0) return `superfície "${name}" — campo desconhecido: ${unknown.join(', ')}`;
141
+ if (typeof def.when !== 'string' || def.when.trim() === '') {
142
+ return `superfície "${name}" — "when" precisa dizer que telas são dessa superfície`;
143
+ }
144
+ const ids = def.standards;
145
+ if (!Array.isArray(ids) || ids.length === 0 || ids.some((s) => typeof s !== 'string' || s.trim() === '')) {
146
+ return `superfície "${name}" — "standards" precisa ser uma lista não vazia de ids`;
147
+ }
148
+ const repeated = ids.filter((s, i) => ids.indexOf(s) !== i);
149
+ if (repeated.length > 0) return `superfície "${name}" repete ${[...new Set(repeated)].join(', ')}`;
150
+ const overlap = ids.filter((s) => universal.has(s));
151
+ if (overlap.length > 0) {
152
+ return `superfície "${name}" repete standard da lista universal (${overlap.join(', ')}) — ` +
153
+ 'um standard é universal OU de uma superfície';
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
159
+ /**
160
+ * The full message for an invalid `surfaces`. Every reader prints this one.
161
+ *
162
+ * @param {string} id - persona id (agents.json key)
163
+ * @param {unknown} surfaces
164
+ * @param {unknown} [standards]
165
+ * @returns {string|null} null when valid
166
+ */
167
+ export function surfacesError(id, surfaces, standards) {
168
+ const problem = surfacesProblem(surfaces, standards);
169
+ if (!problem) return null;
170
+ return `agents.json: persona "${id}" tem surfaces inválido — ${problem}. ` +
171
+ 'Remova o campo para a persona ler só `standards`.';
172
+ }
173
+
174
+ /**
175
+ * Every standard id a persona can cite: the universal list plus each
176
+ * surface's. Reference checks read this, so a surface id that does not exist
177
+ * fails like any other.
178
+ *
179
+ * @param {object} persona - agents.json entry
180
+ * @returns {string[]} unique ids, universal first
181
+ */
182
+ export function personaStandardIds(persona) {
183
+ const ids = [...(Array.isArray(persona?.standards) ? persona.standards : [])];
184
+ const surfaces = persona?.surfaces;
185
+ if (surfaces && typeof surfaces === 'object' && !Array.isArray(surfaces)) {
186
+ for (const def of Object.values(surfaces)) {
187
+ if (Array.isArray(def?.standards)) ids.push(...def.standards);
188
+ }
189
+ }
190
+ return [...new Set(ids)];
191
+ }
192
+
193
+ /** The first validity error of a persona entry, or null — what drift reports as `invalid`. */
194
+ function personaError(agent) {
195
+ return maxTurnsError(agent.id, agent.maxTurns) ?? surfacesError(agent.id, agent.surfaces, agent.standards);
196
+ }
197
+
198
+ /**
199
+ * Tools every persona gets on top of its scope. A `tools:` line is a strict
200
+ * allowlist: without `Skill` the sub-agent cannot invoke a single skill, and
201
+ * without `ToolSearch` it cannot load an MCP tool the session defers.
202
+ */
203
+ export const PLATFORM_TOOLS = ['Skill', 'ToolSearch'];
204
+
205
+ /**
206
+ * The server names an MCP registry entry is granted under. Claude Code names a
207
+ * server's tools after WHERE it is configured — `mcp__context7__*` from
208
+ * `.mcp.json`, `mcp__plugin_context7_context7__*` from the plugin — and a grant
209
+ * only matches its own spelling (measured 2026-09-24: `mcp__context7` gave a
210
+ * plugin-installed context7 nothing). So every spelling becomes a grant; one
211
+ * the project does not configure is inert.
212
+ *
213
+ * @param {string} id - registry key
214
+ * @param {object} entry - registry entry
215
+ * @returns {string[]}
216
+ */
217
+ export function mcpServerNames(id, entry) {
218
+ // A malformed list falls back to the id instead of breaking the render:
219
+ // installAgents and checkAgentsDrift both go through here (fail-open).
220
+ return serverNamesProblem(id, entry?.serverNames) === null ? entry?.serverNames ?? [id] : [id];
221
+ }
222
+
223
+ /**
224
+ * The ONE rule for a registry entry's `serverNames` (validate-framework Check 8
225
+ * reads it). Absent is valid (`[id]`). Present: non-empty, only the characters
226
+ * Claude Code keeps in a tool name, and it must include the id itself — that is
227
+ * the name the MCP installer writes into `.mcp.json`.
228
+ *
229
+ * @param {string} id - registry key
230
+ * @param {unknown} value - the raw `serverNames`
231
+ * @returns {string|null} what was expected, or null when valid
232
+ */
233
+ export function serverNamesProblem(id, value) {
234
+ if (value === undefined) return null;
235
+ if (!Array.isArray(value) || value.length === 0) return 'esperado array não vazio';
236
+ const bad = value.find(n => typeof n !== 'string' || !/^[A-Za-z0-9_-]+$/.test(n));
237
+ if (bad !== undefined) return `nome inválido ${JSON.stringify(bad)} (só [A-Za-z0-9_-])`;
238
+ return value.includes(id) ? null : `precisa incluir o próprio id "${id}"`;
239
+ }
240
+
241
+ /**
242
+ * Load the MCP servers known to the framework registry
243
+ * (framework/mcp/registry.json) with the server names each is granted under.
244
+ * Only registry-known ids in agents.json `mcp` become tool grants (fail-open
245
+ * on any error).
36
246
  *
37
247
  * @param {string} frameworkDir
38
- * @returns {Set<string>|null} null = no registry found (accept nothing)
248
+ * @returns {Map<string, string[]>|null} id → server names; null = no registry found (accept nothing)
39
249
  */
40
250
  export function loadKnownMcpIds(frameworkDir) {
41
251
  try {
42
252
  const registryPath = join(frameworkDir, 'mcp', 'registry.json');
43
253
  if (!existsSync(registryPath)) return null;
44
254
  const registry = JSON.parse(readFileSync(registryPath, 'utf-8'));
45
- return new Set(Object.keys(registry.mcps ?? {}));
255
+ return new Map(Object.entries(registry.mcps ?? {}).map(([id, entry]) => [id, mcpServerNames(id, entry)]));
46
256
  } catch {
47
257
  return null;
48
258
  }
@@ -95,6 +305,13 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
95
305
  .filter(([key]) => !key.startsWith('_comment'))
96
306
  .map(([id, data]) => ({ id, ...data }));
97
307
 
308
+ // Validate EVERY persona before the first write: refusing halfway through
309
+ // would leave some personas regenerated and others stale.
310
+ for (const agent of agents) {
311
+ const error = personaError(agent);
312
+ if (error) throw new Error(error);
313
+ }
314
+
98
315
  const targetDir = join(projectDir, '.claude', 'agents');
99
316
  fx.ensureDir(targetDir);
100
317
 
@@ -148,29 +365,35 @@ export async function installAgents(projectDir, frameworkDir = 'framework', opti
148
365
  * stale file differs byte-for-byte from this output).
149
366
  *
150
367
  * @param {object} agent - agents.json entry with `id` merged in
151
- * @param {Set<string>|null} [knownMcpIds] - registry-known MCP ids; agents.json
152
- * `mcp` entries outside this set are ignored (null = ignore all)
368
+ * @param {Map<string, string[]>|null} [knownMcpIds] - registry-known MCP ids and
369
+ * their server names; agents.json `mcp` entries outside it are ignored (null = ignore all)
153
370
  * @returns {string} complete file content
154
371
  */
155
372
  export function renderAgentMd(agent, knownMcpIds = null) {
156
373
  const description = buildDescription(agent);
157
- const frontmatter = buildFrontmatter(agent, description, knownMcpIds);
158
- const body = buildBody(agent);
374
+ // Resolved ONCE: the frontmatter ceiling and the budget the body announces
375
+ // must be the same number — the sub-agent never sees its own frontmatter.
376
+ const maxTurns = resolveMaxTurns(agent);
377
+ const surfaces = surfacesError(agent.id, agent.surfaces, agent.standards);
378
+ if (surfaces) throw new Error(surfaces);
379
+ const frontmatter = buildFrontmatter(agent, description, knownMcpIds, maxTurns);
380
+ const body = buildBody(agent, maxTurns);
159
381
  return `---\n${frontmatter}---\n\n${MANAGED_HEADER}\n\n${body}\n`;
160
382
  }
161
383
 
162
384
  function buildToolsLine(agent, knownMcpIds) {
163
- const base = agent.tools ?? DEFAULT_AGENT_TOOLS;
385
+ const base = String(agent.tools ?? DEFAULT_AGENT_TOOLS).split(',').map(t => t.trim()).filter(Boolean);
386
+ const platform = PLATFORM_TOOLS.filter(t => !base.includes(t));
164
387
  // Domain-relevant MCP servers (agents.json `mcp: [ids]`) become server-level
165
- // grants (`mcp__{id}`). Only registry-known ids are emitted; in projects
166
- // without that server configured the grant is inert.
388
+ // grants, one per server name (`mcp__{name}`). Only registry-known ids are
389
+ // emitted; in projects without that server configured the grant is inert.
167
390
  const mcpGrants = (agent.mcp ?? [])
168
391
  .filter(id => knownMcpIds?.has(id))
169
- .map(id => `mcp__${id}`);
170
- return [base, ...mcpGrants].join(', ');
392
+ .flatMap(id => knownMcpIds.get(id).map(name => `mcp__${name}`));
393
+ return [...base, ...platform, ...mcpGrants].join(', ');
171
394
  }
172
395
 
173
- function buildFrontmatter(agent, description, knownMcpIds) {
396
+ function buildFrontmatter(agent, description, knownMcpIds, maxTurns) {
174
397
  const lines = [
175
398
  // Canonical 1:1 naming: frontmatter name === filename === subagent_type.
176
399
  `name: morph-${agent.id}`,
@@ -181,7 +404,10 @@ function buildFrontmatter(agent, description, knownMcpIds) {
181
404
  // personas that omit the field.
182
405
  `model: ${agent.model ?? 'inherit'}`,
183
406
  `tools: ${buildToolsLine(agent, knownMcpIds)}`,
184
- `maxTurns: 20`,
407
+ // Per persona in agents.json (ADR-004); absent → DEFAULT_MAX_TURNS. The
408
+ // line keeps its position so an agents.json without the field renders
409
+ // exactly what the hardcoded `maxTurns: 20` rendered.
410
+ `maxTurns: ${maxTurns}`,
185
411
  ];
186
412
  if (agent.color) lines.push(`color: ${agent.color}`);
187
413
  lines.push(`memory: project`, '');
@@ -195,7 +421,7 @@ function buildDescription(agent) {
195
421
  return desc.length > 200 ? desc.slice(0, 197) + '...' : desc;
196
422
  }
197
423
 
198
- function buildBody(agent) {
424
+ function buildBody(agent, maxTurns) {
199
425
  const lines = [
200
426
  `# ${agent.title ?? agent.id}`,
201
427
  '',
@@ -213,15 +439,51 @@ function buildBody(agent) {
213
439
  lines.push('');
214
440
  }
215
441
 
442
+ if (agent.surfaces) {
443
+ const read = agent.standards?.length ? 'Read the list above plus only' : 'Read only';
444
+ lines.push(
445
+ '## Standards by surface',
446
+ '',
447
+ 'A screen is ONE surface — the mockup checklist names it (`Superfície`). ' +
448
+ `${read} the list of that screen's surface; never another surface's standards for that screen.`,
449
+ '',
450
+ );
451
+ for (const [name, def] of Object.entries(agent.surfaces)) {
452
+ lines.push(`### ${name}`, def.when, ...def.standards.map((s) => `- ${s}`), '');
453
+ }
454
+ }
455
+
216
456
  lines.push(
217
457
  '## How you operate',
218
458
  '',
219
459
  'You are a domain persona dispatched per task. Read the spec/mandate/tasks/standards passed to you, do the work, and return a concise summary plus an auto-score 0-10. Refer to MORPH.md section 6 for the dispatch contract.',
460
+ '',
461
+ // Generic on purpose: this body reaches every dispatch (implement, apply,
462
+ // uiux, hotfix, review). The scout has no canonical file and no Write; the
463
+ // evaluator's file is evaluator-report.md. `PARCIAL` on line 1 is what
464
+ // report-check reads (verdict report-incomplete, action resume); "no
465
+ // score" keeps an evaluator partial from carrying a Composite-Score the
466
+ // gate would read as final.
467
+ '## Turn budget',
468
+ '',
469
+ // Pasted into EVERY dispatch of every persona — kept to the minimum that
470
+ // still carries the contract (test/framework/maf-expert-persona.test.js
471
+ // pins the rendered size).
472
+ `You have ${maxTurns} turns (\`maxTurns\`); at the ceiling you are cut off mid-work, without warning. ` +
473
+ 'One response is one turn, however many parallel tool calls it makes. ' +
474
+ `By turn ${checkpointTurn(maxTurns)}, if your dispatch names a report file, save a partial there — ` +
475
+ '`PARCIAL` on line 1, done, remaining, files, no score (final report only) — then continue or stop, your call; the final report replaces it.',
220
476
  );
221
477
 
222
478
  return lines.join('\n');
223
479
  }
224
480
 
481
+ /** The turn at which a sub-agent with `maxTurns` saves its partial report. */
482
+ function checkpointTurn(maxTurns) {
483
+ // Integer arithmetic: 0.7 × N in floating point is not always exact.
484
+ return Math.max(1, Math.floor((maxTurns * CHECKPOINT_PERCENT) / 100));
485
+ }
486
+
225
487
  /**
226
488
  * Compatibility shim for older callers that expected installDomainAgents.
227
489
  * In v5 there is no separate "domain" install step — installAgents handles
@@ -279,19 +541,30 @@ export function checkMissingAgents(projectDir, frameworkDir, projectTags = []) {
279
541
  *
280
542
  * - missing: eligible persona with no .md installed → run setup-infra --reinstall
281
543
  * - stale: .md exists but differs from renderAgentMd() output (hand-edited
282
- * or generated by an older CLI) → regenerate
544
+ * or generated by an older CLI) → regenerate. `staleFields` names
545
+ * what differs per file: the frontmatter keys, and `body`.
546
+ * - invalid: persona whose agents.json entry cannot be rendered (an invalid
547
+ * `maxTurns` or `surfaces`) — reported with the reason instead of throwing, so
548
+ * the doctor prints the cause and not "failed to compute"
283
549
  * - orphans: morph-*.md files with no matching agents.json entry
284
550
  *
285
551
  * @param {string} projectDir
286
552
  * @param {string} frameworkDir - directory containing agents.json
287
553
  * @param {string[]} projectTags
288
- * @returns {{ missing: string[], stale: string[], orphans: string[], expected: number }}
554
+ * @param {object} [options]
555
+ * @param {string} [options.mcpRegistryDir=frameworkDir] - where `mcp/registry.json`
556
+ * lives. It must be the directory the INSTALLER read: an installed project's
557
+ * `.morph/framework/` carries agents.json but no `mcp/`, so rendering against
558
+ * it drops every `mcp__*` grant and flags a pristine install as stale.
559
+ * @returns {{ missing: string[], stale: string[], staleFields: Object<string, string[]>,
560
+ * invalid: Array<{ file: string, reason: string }>, orphans: string[], expected: number }}
289
561
  */
290
- export function checkAgentsDrift(projectDir, frameworkDir, projectTags = []) {
562
+ export function checkAgentsDrift(projectDir, frameworkDir, projectTags = [], options = {}) {
563
+ const { mcpRegistryDir = frameworkDir } = options;
291
564
  const targetDir = join(projectDir, '.claude', 'agents');
292
565
  const agentsJsonPath = join(frameworkDir, 'agents.json');
293
566
  if (!existsSync(agentsJsonPath)) {
294
- return { missing: [], stale: [], orphans: [], expected: 0 };
567
+ return { missing: [], stale: [], staleFields: {}, invalid: [], orphans: [], expected: 0 };
295
568
  }
296
569
 
297
570
  const agentsJson = JSON.parse(readFileSync(agentsJsonPath, 'utf-8'));
@@ -299,24 +572,33 @@ export function checkAgentsDrift(projectDir, frameworkDir, projectTags = []) {
299
572
  .filter(([key]) => !key.startsWith('_comment'))
300
573
  .map(([id, data]) => ({ id, ...data }));
301
574
 
302
- const knownMcpIds = loadKnownMcpIds(frameworkDir);
575
+ const knownMcpIds = loadKnownMcpIds(mcpRegistryDir);
303
576
  const knownFilenames = new Set(agents.map(a => `morph-${a.id}.md`));
304
577
  const missing = [];
305
578
  const stale = [];
579
+ const staleFields = {};
580
+ const invalid = [];
306
581
  let expected = 0;
307
582
 
308
583
  for (const agent of agents) {
309
584
  if (!agentEligible(agent, projectTags)) continue;
310
585
  expected++;
311
586
  const filename = `morph-${agent.id}.md`;
587
+ const error = personaError(agent);
588
+ if (error) {
589
+ invalid.push({ file: filename, reason: error });
590
+ continue;
591
+ }
312
592
  const filePath = join(targetDir, filename);
313
593
  if (!existsSync(filePath)) {
314
594
  missing.push(filename);
315
595
  continue;
316
596
  }
317
597
  const onDisk = readFileSync(filePath, 'utf-8').replace(/\r\n/g, '\n');
318
- if (onDisk !== renderAgentMd(agent, knownMcpIds)) {
598
+ const rendered = renderAgentMd(agent, knownMcpIds);
599
+ if (onDisk !== rendered) {
319
600
  stale.push(filename);
601
+ staleFields[filename] = diffAgentMd(onDisk, rendered);
320
602
  }
321
603
  }
322
604
 
@@ -329,7 +611,39 @@ export function checkAgentsDrift(projectDir, frameworkDir, projectTags = []) {
329
611
  }
330
612
  }
331
613
 
332
- return { missing, stale, orphans, expected };
614
+ return { missing, stale, staleFields, invalid, orphans, expected };
615
+ }
616
+
617
+ /**
618
+ * Split a generated .md into its frontmatter map and the rest.
619
+ * @returns {{ fields: Map<string, string>, body: string }}
620
+ */
621
+ function splitAgentMd(content) {
622
+ const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
623
+ if (!match) return { fields: new Map(), body: content };
624
+ const fields = new Map();
625
+ for (const line of match[1].split('\n')) {
626
+ const kv = line.match(/^([A-Za-z][\w-]*):\s?(.*)$/);
627
+ if (kv) fields.set(kv[1], kv[2]);
628
+ }
629
+ return { fields, body: match[2] };
630
+ }
631
+
632
+ /**
633
+ * Name what differs between a .md on disk and its render: the frontmatter
634
+ * keys whose value (or presence) differs, in render order, then `body`.
635
+ * A file with no parseable frontmatter differs in everything → `frontmatter`.
636
+ */
637
+ function diffAgentMd(onDisk, rendered) {
638
+ const disk = splitAgentMd(onDisk);
639
+ const want = splitAgentMd(rendered);
640
+ if (disk.fields.size === 0) return disk.body === want.body ? ['frontmatter'] : ['frontmatter', 'body'];
641
+ const keys = [...new Set([...want.fields.keys(), ...disk.fields.keys()])];
642
+ const fields = keys.filter(k => disk.fields.get(k) !== want.fields.get(k));
643
+ if (disk.body !== want.body) fields.push('body');
644
+ // Bytes differ but no key/value does (e.g. a stray blank line between the
645
+ // fences): still drift, and it lives in the frontmatter.
646
+ return fields.length > 0 ? fields : ['frontmatter'];
333
647
  }
334
648
 
335
649
  /**