@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
@@ -7,6 +7,7 @@ import { getFeature, setApprovalGate, getApprovalGate, getApprovalHistory, deriv
7
7
  import { unmetPrereqs } from '../../core/state/gate-order.js';
8
8
  import { evaluatorReportState, COMPOSITE_SCORE_TOKEN } from '../../lib/evaluator-report.js';
9
9
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
10
+ import { uiuxAutoApprovalBlock } from '../../lib/specs/screen-checklist.js';
10
11
 
11
12
  /**
12
13
  * The literal token `review-report.md` carries to declare which tree it
@@ -308,6 +309,20 @@ export async function approveCommand(featureName, gateName, options = {}) {
308
309
  process.exit(1);
309
310
  }
310
311
 
312
+ // The same backstop when no gate-check ran: the pause it WOULD record for
313
+ // the screen checklist, by the predicate advance.js reads too. `--force`
314
+ // does not bypass it, for the reason above.
315
+ if (gateName === 'uiux' && approvedBy === 'auto') {
316
+ const block = uiuxAutoApprovalBlock(process.cwd(), featureName, feature);
317
+ if (block) {
318
+ console.error(chalk.red(`\n❌ O Gate "uiux" não pode ser auto-aprovado — ${block}.`));
319
+ console.error(chalk.gray(' Complete o checklist (ver a skill morph-uiux) e rode de novo, aprove com'));
320
+ console.error(chalk.gray(' --approver <nome>, ou, se a pausa já foi decidida, grave a decisão:'));
321
+ console.error(chalk.gray(` morph-spec gate-decision ${featureName} uiux --pause false --reason "<motivo>"\n`));
322
+ process.exit(1);
323
+ }
324
+ }
325
+
311
326
  let selfAssessed = false;
312
327
  if (gateName === 'review') {
313
328
  if (feature.workType === 'hotfix') {
@@ -3,6 +3,8 @@ import { existsSync, renameSync, mkdirSync, readFileSync, writeFileSync } from '
3
3
  import { join, resolve, sep } from 'path';
4
4
  import { execFileSync } from 'child_process';
5
5
  import { promoteUseCases, SPECS_USECASES_PATHSPEC } from '../../lib/specs/usecase-promoter.js';
6
+ import { applySpecRevocations } from '../../lib/specs/acervo-lifecycle.js';
7
+ import { ADR_INDEX_PATHSPEC } from '../../lib/specs/adr-index.js';
6
8
  import { isValidFeatureName } from '../project/worktree.js';
7
9
 
8
10
  /** Refuse a move whose resolved target escapes `scopeDir`. */
@@ -64,7 +66,11 @@ function readReviewApproved(fjPath) {
64
66
  * @param {{ cwd?: string, force?: boolean, git?: boolean }} [opts]
65
67
  * @returns {{ ok: boolean, error?: string, archived?: boolean, alreadyArchived?: boolean,
66
68
  * committed?: boolean, staged?: boolean, archivePath?: string, promotedUseCases?: string[],
67
- * refusedUseCases?: Array<{id: string, incoming: string, existing: string, existingFeature: string, message: string}> }}
69
+ * refusedUseCases?: Array<{id: string, incoming: string, existing: string, existingFeature: string, message: string}>,
70
+ * keptSuperseded?: Array<{id: string, file: string, supersededBy: string|null, scope?: string}>,
71
+ * supersededUseCases?: Array<{id: string, file: string, supersededBy: string, scope?: string}>,
72
+ * revocationProblems?: Array<{id: string|null, reason: string, message: string}>,
73
+ * adrIndex?: {added: string[], revoked: string[], narrowed: string[]}|null }}
68
74
  */
69
75
  export function runArchive(feature, opts = {}) {
70
76
  const cwd = opts.cwd || process.cwd();
@@ -158,10 +164,12 @@ export function runArchive(feature, opts = {}) {
158
164
  // never to a refused close.
159
165
  let promotedUseCases = [];
160
166
  let refusedUseCases = [];
167
+ let keptSuperseded = [];
161
168
  try {
162
- const { promoted, refused } = promoteUseCases(cwd, archivePath);
169
+ const { promoted, refused, keptSuperseded: kept } = promoteUseCases(cwd, archivePath);
163
170
  promotedUseCases = promoted;
164
171
  refusedUseCases = refused;
172
+ keptSuperseded = kept;
165
173
  if (promoted.length && useGit && staged) {
166
174
  // This staging exists for the NEW files: promoted use cases are untracked,
167
175
  // and the scoped `commit -- <pathspec>` below only knows paths git already
@@ -173,17 +181,39 @@ export function runArchive(feature, opts = {}) {
173
181
  }
174
182
  } catch { /* promotion must never block archiving */ }
175
183
 
184
+ // 3b) Apply what the feature's spec.md declared it revokes: acervo use cases
185
+ // turn `superseded`, and the ADR index gains/loses files. After the
186
+ // promotion, so a substitute promoted just now is already in place (and
187
+ // one it refused is known not to be); same commit, same fail-open contract.
188
+ let lifecycle = { superseded: [], adrIndex: null, problems: [] };
189
+ try {
190
+ lifecycle = applySpecRevocations(cwd, archivePath, { refused: refusedUseCases });
191
+ } catch { /* revocation must never block archiving */ }
192
+ const acervoChanged = promotedUseCases.length > 0 || lifecycle.superseded.length > 0;
193
+ const adrIndexChanged = lifecycle.adrIndex?.changed === true;
194
+ if (useGit && staged) {
195
+ if (lifecycle.superseded.length) {
196
+ try { execFileSync('git', ['add', '-A', '--', SPECS_USECASES_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
197
+ }
198
+ if (adrIndexChanged) {
199
+ try { execFileSync('git', ['add', '-A', '--', ADR_INDEX_PATHSPEC], { cwd, stdio: 'pipe' }); } catch { /* non-fatal */ }
200
+ }
201
+ }
202
+
176
203
  // 4) Commit the move — SCOPED to this feature's pathspec, NEVER `commit -a`
177
204
  // (same anti-sweep discipline as `finish`'s worktree teardown). This is what
178
205
  // lets the archived state travel with the branch on `finish` (archive-before-merge).
179
- // The acervo pathspec joins only when something was promoted: git refuses a
180
- // pathspec that matches nothing, and that refusal would leave the tree dirty
181
- // and break the checkout/merge that follows.
206
+ // The acervo pathspecs join only when something under them changed: git
207
+ // refuses a pathspec that matches nothing, and that refusal would leave the
208
+ // tree dirty and break the checkout/merge that follows.
182
209
  let committed = false;
183
210
  if (useGit && staged) {
184
- const pathspecs = promotedUseCases.length
185
- ? [featPathspec, archivePathspec, SPECS_USECASES_PATHSPEC]
186
- : [featPathspec, archivePathspec];
211
+ const pathspecs = [
212
+ featPathspec,
213
+ archivePathspec,
214
+ ...(acervoChanged ? [SPECS_USECASES_PATHSPEC] : []),
215
+ ...(adrIndexChanged ? [ADR_INDEX_PATHSPEC] : []),
216
+ ];
187
217
  try {
188
218
  execFileSync('git', ['commit', '-m', `morph-spec archive: ${feature}`, '--', ...pathspecs], { cwd, stdio: 'pipe' });
189
219
  committed = true;
@@ -206,7 +236,14 @@ export function runArchive(feature, opts = {}) {
206
236
  } catch { /* index is derivable — non-fatal */ }
207
237
  }
208
238
 
209
- return { ok: true, archived: true, committed, staged, archivePath, promotedUseCases, refusedUseCases };
239
+ const adr = lifecycle.adrIndex;
240
+ return {
241
+ ok: true, archived: true, committed, staged, archivePath, promotedUseCases, refusedUseCases,
242
+ keptSuperseded,
243
+ supersededUseCases: lifecycle.superseded,
244
+ revocationProblems: lifecycle.problems,
245
+ adrIndex: adrIndexChanged ? { added: adr.added, revoked: adr.revoked, narrowed: adr.narrowed } : null,
246
+ };
210
247
  }
211
248
 
212
249
  /**
@@ -253,6 +290,22 @@ export async function archiveCommand(featureName, options = {}) {
253
290
  console.log(chalk.yellow(` ${r.message}`));
254
291
  }
255
292
  }
293
+ for (const k of result.keptSuperseded ?? []) {
294
+ console.log(chalk.yellow(` ${k.id} continua superseded (superseded_by: ${k.supersededBy ?? '?'}) — a cópia promovida não trazia o status, e o acervo o manteve.`));
295
+ }
296
+ for (const s of result.supersededUseCases ?? []) {
297
+ console.log(chalk.gray(` ${s.id} → status: superseded (superseded_by: ${s.supersededBy}${s.scope ? `; ainda vale em: ${s.scope}` : ''}).`));
298
+ }
299
+ if (result.adrIndex) {
300
+ const { added, revoked, narrowed } = result.adrIndex;
301
+ console.log(chalk.gray(` ADR index (.morph/specs/adrs/): +${added.length} −${revoked.length} ~${narrowed.length}.`));
302
+ }
303
+ if (result.revocationProblems?.length) {
304
+ console.log(chalk.yellow(`\n⚠️ ${result.revocationProblems.length} revogação(ões) do spec.md NÃO aplicada(s):`));
305
+ for (const p of result.revocationProblems) {
306
+ console.log(chalk.yellow(` ${p.message}`));
307
+ }
308
+ }
256
309
  if (result.committed) console.log(chalk.gray(` Move committed on the current branch.\n`));
257
310
  else if (result.staged) console.log(chalk.gray(` Move staged in git; commit it when ready.\n`));
258
311
  else console.log('');
@@ -21,6 +21,8 @@ import { releaseClaim } from '../../../framework/hooks/shared/claim-registry.js'
21
21
  import { tearDownFeatureStack } from '../../lib/e2e/stack-teardown.js';
22
22
  import { findReparsePoints } from '../../../framework/hooks/shared/reparse-scan.js';
23
23
  import { runArchive } from './archive.js';
24
+ import { dropLeftoverIgnore, withoutLeftoverIgnore } from '../../../framework/hooks/shared/graph-freshness.js';
25
+ import { newBackupStamp, enforceBackupRetention, inspectFinishBackups } from '../../lib/worktree/finish-backup.js';
24
26
 
25
27
  /** workType → conventional-commit type. `hotfix` IS a fix; anything else is a chore. */
26
28
  const PR_TYPE_BY_WORK_TYPE = { feature: 'feat', bug: 'fix', hotfix: 'fix', chore: 'chore' };
@@ -119,6 +121,14 @@ export async function finishCommand(feature, options = {}) {
119
121
  // would otherwise get a silent merge they never asked for.
120
122
  let resumed = false;
121
123
 
124
+ // ONE stamp for every backup this run writes: the shed's and the teardown's
125
+ // folders then count as a single backup under the retention.
126
+ const backupStamp = newBackupStamp();
127
+
128
+ // The backup retention rides the same single point, AFTER this run's own
129
+ // backups exist (the shed and the teardown both precede every success emit).
130
+ // This run's stamp is protected: the prune never takes the backup it just
131
+ // made room for, even when backups stamped by a clock running ahead outrank it.
122
132
  const emit = (obj) => {
123
133
  if (resumed) obj = { ...obj, resumed: true };
124
134
  if (obj?.finished === true) {
@@ -126,6 +136,8 @@ export async function finishCommand(feature, options = {}) {
126
136
  const common = execFileSync('git', ['rev-parse', '--git-common-dir'], { cwd: root, stdio: 'pipe' }).toString().trim();
127
137
  if (common) releaseClaim(feature, { gitCommonDir: resolve(root, common) });
128
138
  } catch { /* fail-open */ }
139
+ const retention = enforceBackupRetention(root, { protectStamps: [backupStamp] });
140
+ if (retention) obj = { ...obj, backupRetention: retention };
129
141
  }
130
142
  console.log(JSON.stringify(obj));
131
143
  };
@@ -413,7 +425,7 @@ export async function finishCommand(feature, options = {}) {
413
425
  emit({ finished: false, error: 'status-failed', feature });
414
426
  process.exit(1);
415
427
  }
416
- if (dirty.trim() !== '') {
428
+ if (withoutLeftoverIgnore(cleanCwd, dirty.split(/\r?\n/).filter((l) => l.trim())).length) {
417
429
  emit({ finished: false, error: isWorktreeFlow ? 'worktree-dirty' : 'working-dir-dirty', hint: 'commit seu trabalho na branch antes do finish', feature });
418
430
  process.exit(1);
419
431
  }
@@ -475,6 +487,9 @@ export async function finishCommand(feature, options = {}) {
475
487
  }
476
488
 
477
489
  // ── ARCHIVE (inside morph/{f}) ────────────────────────────────────────────
490
+ // Every refusal is behind us: the leftover of a killed `graph refresh` goes now,
491
+ // before the archive — never before a precondition that could still refuse.
492
+ dropLeftoverIgnore(cleanCwd);
478
493
  const arch = runArchive(feature, { cwd: featureCwd, force: options.force, git: true });
479
494
  if (!arch.ok) {
480
495
  emit({ finished: false, error: `archive-${arch.error}`, feature });
@@ -489,9 +504,9 @@ export async function finishCommand(feature, options = {}) {
489
504
 
490
505
  // ── INTEGRATE ─────────────────────────────────────────────────────────────
491
506
  if (mode === 'merge') {
492
- return integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch });
507
+ return integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp });
493
508
  }
494
- return integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch });
509
+ return integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp });
495
510
  }
496
511
 
497
512
  /**
@@ -582,6 +597,30 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
582
597
 
583
598
  if (defaultBranch) out.defaultBranch = defaultBranch;
584
599
 
600
+ // A retenção dos backups também é mutação do finish real — a AUTOMÁTICA, que
601
+ // não toca backup sem manifesto (esses só o `doctor --worktrees --prune`
602
+ // poda). A contagem é a de AGORA: o backup que este finish gravar ainda pode
603
+ // empurrar mais um para fora.
604
+ try {
605
+ const retention = inspectFinishBackups(root);
606
+ if (retention.policy.skipped) {
607
+ out.backupRetention = { skipped: retention.policy.skipped };
608
+ warnings.push(`a retenção dos backups do finish será PULADA: ${retention.policy.skipped}`);
609
+ } else if (retention.outsideRetention || retention.overdueSecrets.length || retention.legacy) {
610
+ out.backupRetention = {
611
+ outsideRetention: retention.outsideRetention,
612
+ overdueSecrets: retention.overdueSecrets.length,
613
+ ...(retention.legacy ? { legacyPending: retention.legacy } : {}),
614
+ };
615
+ if (retention.outsideRetention || retention.overdueSecrets.length) {
616
+ out.wouldDo.push(
617
+ `aplicar a retenção de .git/morph-spec/finish-backup: apagar ${retention.outsideRetention} backup(s) fora da política`
618
+ + ` e ${retention.overdueSecrets.length} arquivo(s) .env* além de ${retention.policy.secretMaxAgeDays} dias`,
619
+ );
620
+ }
621
+ }
622
+ } catch { /* o pré-voo nunca lança */ }
623
+
585
624
  // ── Seções de worktree — omitidas quando não há worktree (UC-16 A1) ───────
586
625
  if (worktreePath) {
587
626
  out.worktreePath = worktreePath.replace(/\\/g, '/');
@@ -664,14 +703,14 @@ export function finishPreflight({ feature, root, worktreePath, cwd, mode, branch
664
703
 
665
704
  // ── merge integration (local, no push) ───────────────────────────────────────
666
705
 
667
- async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch }) {
706
+ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, arch, backupStamp }) {
668
707
  const mergeMsg = `morph-spec finish: merge ${expectedBranch}`;
669
708
  const ucNote = promotedNote(arch);
670
709
 
671
710
  if (isWorktreeFlow) {
672
711
  // The root stays on the default; shed this feature's own root-side scaffold,
673
712
  // then guard the whole root before landing the merge commit.
674
- const { shedBackup } = shedRootFeatureScope(root, feature);
713
+ const { shedBackup } = shedRootFeatureScope(root, feature, { stamp: backupStamp });
675
714
  const backupNote = shedBackup ? { shedBackup } : {};
676
715
 
677
716
  // Final gate before the merge commit lands. The pre-archive check above
@@ -721,7 +760,7 @@ async function integrateMerge({ emit, root, feature, expectedBranch, defaultBran
721
760
  const deferred = sessionAnchoredInWorktree(worktreePath);
722
761
  const teardown = deferred
723
762
  ? null
724
- : await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: true });
763
+ : await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: true, stamp: backupStamp });
725
764
  dropFromRootIndex(root, feature);
726
765
  emit({ ...mergeOutput({ feature, branch: expectedBranch, defaultBranch, publish, worktreePath, teardown, shedBackup, deferred }), ...ucNote, e2e });
727
766
  process.exit(0);
@@ -776,7 +815,7 @@ export function prNumberFromUrl(url) {
776
815
 
777
816
  // ── PR integration (push + gh, no local merge) ───────────────────────────────
778
817
 
779
- async function integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch }) {
818
+ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch, isWorktreeFlow, worktreePath, options, prTitle, prBody, arch, backupStamp }) {
780
819
  try {
781
820
  execFileSync('git', ['push', '-u', 'origin', expectedBranch], { cwd: root, stdio: 'pipe' });
782
821
  } catch (err) {
@@ -812,13 +851,13 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
812
851
  // later merges (carrying features/{f} → archive/{f}), the default would hold
813
852
  // the feature in BOTH features/ and archive/ at once — active and archived.
814
853
  // The branch/PR owns the authoritative copy; the root scaffold is transient.
815
- shedBackup = shedRootFeatureScope(root, feature).shedBackup;
854
+ shedBackup = shedRootFeatureScope(root, feature, { stamp: backupStamp }).shedBackup;
816
855
  // Same deferral as --merge: never tear down the worktree the running
817
856
  // session is anchored in (see sessionAnchoredInWorktree).
818
857
  if (sessionAnchoredInWorktree(worktreePath)) {
819
858
  teardownDeferred = true;
820
859
  } else {
821
- teardown = await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: false });
860
+ teardown = await teardownWorktree(worktreePath, { root, feature, branch: expectedBranch, options, deleteBranch: false, stamp: backupStamp });
822
861
  }
823
862
  // The shed removed the root's features/{f} scaffold from disk, but runArchive
824
863
  // dropped the feature only from the WORKTREE's index (its cwd). Drop it from
@@ -874,16 +913,25 @@ async function integratePr({ emit, root, feature, expectedBranch, defaultBranch,
874
913
  // ── output helpers ────────────────────────────────────────────────────────
875
914
 
876
915
  /**
877
- * Report the use cases the archive promoted into `.morph/specs/usecases/`, and
878
- * any that were REFUSED for a numbering collision (`ucCollisions` — see
879
- * `usecase-promoter.js`'s reservation-backed guard). Both absent when there is
880
- * nothing to report, so a feature with no use cases keeps the exact output
881
- * shape it had before either existed.
916
+ * Report what the archive did to the acervo: use cases promoted into
917
+ * `.morph/specs/usecases/`, any REFUSED for a numbering collision
918
+ * (`ucCollisions` — see `usecase-promoter.js`'s reservation-backed guard), the
919
+ * promoted copies that stayed superseded because the acervo said so
920
+ * (`keptSuperseded`), the ones the feature's spec.md revoked
921
+ * (`supersededUseCases`), the ADR index files it moved (`adrIndex`) and the
922
+ * revocations it could not apply
923
+ * (`revocationProblems`). Every key is absent when there is nothing to report,
924
+ * so a feature that touches no acervo keeps the exact output shape it had
925
+ * before any of them existed.
882
926
  */
883
927
  function promotedNote(arch) {
884
928
  return {
885
929
  ...(arch?.promotedUseCases?.length ? { promotedUseCases: arch.promotedUseCases } : {}),
886
930
  ...(arch?.refusedUseCases?.length ? { ucCollisions: arch.refusedUseCases } : {}),
931
+ ...(arch?.keptSuperseded?.length ? { keptSuperseded: arch.keptSuperseded } : {}),
932
+ ...(arch?.supersededUseCases?.length ? { supersededUseCases: arch.supersededUseCases } : {}),
933
+ ...(arch?.adrIndex ? { adrIndex: arch.adrIndex } : {}),
934
+ ...(arch?.revocationProblems?.length ? { revocationProblems: arch.revocationProblems } : {}),
887
935
  };
888
936
  }
889
937
 
@@ -4,6 +4,7 @@ import { getFeature } from '../../core/state/state-manager.js';
4
4
  import { getAbsoluteOutputPath } from '../../core/paths/output-schema.js';
5
5
  import { shouldPauseGate, getRiskSignals, classifyE2eSkip } from '../../lib/risk-detector.js';
6
6
  import { evaluatorCompositeScore, evaluatorProofState, minProofScore, mediaProofScore } from '../../lib/evaluator-report.js';
7
+ import { collectScreenChecklist } from '../../lib/specs/screen-checklist.js';
7
8
  import { persistGateDecision, VALID_GATES, readTrustFromConfig } from './gate-decision.js';
8
9
  import { minimatch } from 'minimatch';
9
10
  import { detectDefaultBranch, getHeadSha, getDiffStat, changedPathsAgainst } from '../../lib/git/default-branch.js';
@@ -195,6 +196,12 @@ function collectSignals(featureName, feature, cwd, gateName) {
195
196
  designSystemDivergence: false,
196
197
  };
197
198
 
199
+ // Só onde o checklist decide algo: no fecho do design e no ship. Fato de
200
+ // disco, fora de OVERRIDABLE_SIGNAL_KEYS como `mutationProof`.
201
+ if (gateName === 'uiux' || gateName === 'review') {
202
+ signals.screenChecklist = collectScreenChecklist(cwd, featureName, gateName);
203
+ }
204
+
198
205
  const evalScore = compositeEvalScore(feature);
199
206
  if (evalScore !== undefined) {
200
207
  signals.evalScore = evalScore;
@@ -392,7 +399,7 @@ export async function gateCheckCommand(featureName, gateName, options = {}) {
392
399
  const trust = options.trust || readTrustFromConfig();
393
400
  const { pause, reason } = shouldPauseGate({ trust }, signals);
394
401
 
395
- const entry = await persistGateDecision(featureName, gateName, { pause, reason, signals, trust });
402
+ const entry = await persistGateDecision(featureName, gateName, { pause, reason, signals, trust, cwd });
396
403
 
397
404
  if (options.json) {
398
405
  console.log(JSON.stringify({ feature: featureName, gate: gateName, ...entry }, null, 2));
@@ -3,6 +3,7 @@ import { existsSync, readFileSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { getFeature, updateFeature } from '../../core/state/state-manager.js';
5
5
  import { assertOwnerTree, ownerTreeRefusalLines } from '../../lib/worktree/feature-truth.js';
6
+ import { mockupsHash } from '../../lib/specs/screen-checklist.js';
6
7
 
7
8
  export const VALID_GATES = ['proposal', 'uiux', 'plan', 'review'];
8
9
 
@@ -42,17 +43,20 @@ export function readTrustFromConfig() {
42
43
  *
43
44
  * @param {string} featureName
44
45
  * @param {string} gateName
45
- * @param {{ pause: boolean, reason?: string, signals?: object, trust?: string }} decision
46
- * @returns {Promise<{ pause: boolean, reason: string, signals: object, trust: string, decidedAt: string }>}
47
- * the exact entry written (decidedAt is server-side, generated here).
46
+ * @param {{ pause: boolean, reason?: string, signals?: object, trust?: string, cwd?: string }} decision
47
+ * @returns {Promise<{ pause: boolean, reason: string, signals: object, trust: string, decidedAt: string, mockupsHash?: string|null }>}
48
+ * the exact entry written (decidedAt is server-side, generated here). A uiux
49
+ * decision also carries the hash of the mockup it saw: it answers that
50
+ * mockup's pause, not the next one's.
48
51
  */
49
- export async function persistGateDecision(featureName, gateName, { pause, reason, signals = {}, trust } = {}) {
52
+ export async function persistGateDecision(featureName, gateName, { pause, reason, signals = {}, trust, cwd = process.cwd() } = {}) {
50
53
  const entry = {
51
54
  pause,
52
55
  reason: reason || (pause ? 'risco detectado' : 'nenhum sinal de risco'),
53
56
  signals,
54
57
  trust: trust || readTrustFromConfig(),
55
58
  decidedAt: new Date().toISOString(),
59
+ ...(gateName === 'uiux' ? { mockupsHash: mockupsHash(cwd, featureName) } : {}),
56
60
  };
57
61
 
58
62
  await updateFeature(featureName, `gateDecisions.${gateName}`, entry);
@@ -23,12 +23,18 @@
23
23
  *
24
24
  * Exit 0 only for `ok` — anything needing an action exits non-zero so a caller
25
25
  * can branch on it. `--json` always prints the machine-readable verdict.
26
+ *
27
+ * The output also carries `tree` (see tree-state.js): the working tree the
28
+ * orchestrator would otherwise ask git for, call by call, right after the same
29
+ * dispatch. It is information only and never changes the verdict.
26
30
  */
27
31
 
28
32
  import chalk from 'chalk';
29
33
  import { existsSync, readFileSync, statSync } from 'fs';
30
34
  import { join } from 'path';
31
- import { readTelemetryEvents } from '../../../framework/hooks/shared/telemetry-logger.js';
35
+ import { readTelemetryEvents, isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logger.js';
36
+ import { treeState, snapshotTree } from '../../lib/git/tree-state.js';
37
+ import { atomicWriteJson } from '../../core/state/state-manager.js';
32
38
 
33
39
  /**
34
40
  * The four mandatory items of the report contract, as detectable markers.
@@ -51,6 +57,16 @@ const REPORT_MARKERS = [
51
57
  /** (1) "what was done" — prose beyond the structural markers. */
52
58
  const MIN_PROSE_CHARS = 80;
53
59
 
60
+ /**
61
+ * The partial-report marker of the dispatch contract (clause 5, ADR-004): a
62
+ * sub-agent nearing its `maxTurns` ceiling writes `PARCIAL` on the FIRST line.
63
+ * Read on purpose, never inferred from a missing score: the score marker above
64
+ * is loose by design, and a partial saying "auto-score: pendente (0-10 no
65
+ * final)" matches it — with the other items present it would audit as `ok`.
66
+ * Tolerates heading/bold/quote typography (`# PARCIAL`, `**PARCIAL**`).
67
+ */
68
+ const PARTIAL_MARKER = /^[\s#>*_]*PARCIAL\b/;
69
+
54
70
  function reportPath(cwd, feature, taskId) {
55
71
  return join(cwd, '.morph', 'features', feature, '3-implement', 'reports', `${taskId}.md`);
56
72
  }
@@ -99,7 +115,11 @@ export function inspectReport(path) {
99
115
  .trim();
100
116
  if (prose.length < MIN_PROSE_CHARS) missing.unshift('what');
101
117
 
102
- return { exists: true, complete: missing.length === 0, missing };
118
+ const firstLine = body.split('\n').find((l) => l.trim() !== '') ?? '';
119
+ const partial = PARTIAL_MARKER.test(firstLine);
120
+ if (partial) missing.push('partial');
121
+
122
+ return { exists: true, complete: missing.length === 0, missing, partial };
103
123
  }
104
124
 
105
125
  /** How many of a task's declared outputs actually exist on disk. */
@@ -206,6 +226,7 @@ function observedEvidence(cwd, feature, task) {
206
226
  const writes = [];
207
227
  for (const e of events) {
208
228
  if (e?.tool !== 'Write' && e?.tool !== 'Edit') continue;
229
+ if (isFailedToolCall(e)) continue;
209
230
  const ts = Date.parse(e?.ts ?? '');
210
231
  if (!Number.isFinite(ts) || ts < startMs || ts > endMs) continue;
211
232
  const target = toRepoRelative(e.target, cwd);
@@ -249,6 +270,10 @@ function decide(report, evidence, stale) {
249
270
  ? { verdict: 'report-stale', action: 'refresh-from-evidence' }
250
271
  : { verdict: 'ok', action: 'proceed' };
251
272
  }
273
+ // A PARCIAL report is an agent that stopped short (turn ceiling): the work
274
+ // resumes from its last `wip` commit — completing it DEGRADADO would stamp
275
+ // unfinished work as done.
276
+ if (report.exists && report.partial) return { verdict: 'report-incomplete', action: 'resume' };
252
277
  if (report.exists) return { verdict: 'report-incomplete', action: 'complete-degraded' };
253
278
  if (evidence.outputsExpected === 0) {
254
279
  // No declared outputs means no evidence either way. Re-dispatching costs a
@@ -273,13 +298,36 @@ const ACTION_HINT = {
273
298
  proceed: 'relatório íntegro — siga o fluxo normal.',
274
299
  'refresh-from-evidence': 'o relatório está COMPLETO mas VELHO: a task seguiu escrevendo arquivos depois que ele foi gravado, então ele descreve um estado já ultrapassado. NÃO re-despache e não confie no texto — confira os arquivos em `observed` (evidência real, não auto-declarada) e atualize o relatório a partir deles.',
275
300
  'complete-degraded': 'complete os itens faltantes a partir da mensagem de retorno do sub-agent, carimbe o relatório como DEGRADADO e registre a falha de protocolo no recap.',
301
+ resume: 'relatório PARCIAL: o sub-agent parou antes do fim (limite de turnos) e a task NÃO está pronta. Não carimbe DEGRADADO nem pontue — retome o MESMO agente por `SendMessage` (cole no recado `tree.dirty` e `tree.commits` desta saída e o que falta) ou re-despache só o que falta, a partir do último commit `wip`.',
276
302
  'accept-degraded': 'o TRABALHO entrou — não re-despache. Grave você mesmo o relatório canônico a partir da mensagem de retorno, carimbado como DEGRADADO, e registre a falha de protocolo no recap e nas `notes` da task.',
277
303
  redispatch: 're-despache a task UMA vez com o mesmo prompt. Segunda ausência sem trabalho entregue = falha real: escale ao humano.',
278
304
  };
279
305
 
306
+ function snapshotPath(cwd, feature) {
307
+ return join(cwd, '.morph', 'features', feature, '4-review', 'tree-before-eval.json');
308
+ }
309
+
310
+ /**
311
+ * The tree block, compared with the snapshot taken before the evaluator was
312
+ * dispatched when there is one. `beforeEval` takes a new snapshot after the
313
+ * comparison, so the output still shows what changed since the previous one.
314
+ */
315
+ function treeFor(cwd, feature, beforeEval) {
316
+ const path = snapshotPath(cwd, feature);
317
+ let snapshot = null;
318
+ try { if (existsSync(path)) snapshot = JSON.parse(readFileSync(path, 'utf8')); } catch { snapshot = null; }
319
+ const tree = treeState(cwd, { snapshot });
320
+ if (!beforeEval) return { tree };
321
+ const next = snapshotTree(cwd);
322
+ if (!next) return { tree, beforeEval: null };
323
+ atomicWriteJson(path, next);
324
+ return { tree, beforeEval: { at: next.at, files: Object.keys(next.files).length } };
325
+ }
326
+
280
327
  /**
281
328
  * @param {string} feature
282
- * @param {string|undefined} taskId
329
+ * @param {string|string[]|undefined} taskId - several ids audit a parallel
330
+ * batch whose tasks are not `done` yet, which the whole-feature mode skips
283
331
  * @param {{ json?: boolean, cwd?: string }} [options]
284
332
  */
285
333
  export function reportCheckCommand(feature, taskId, options = {}) {
@@ -303,18 +351,26 @@ export function reportCheckCommand(feature, taskId, options = {}) {
303
351
  emit({ error: `2-plan/tasks.json de "${feature}" está corrompido`, feature }, 1);
304
352
  }
305
353
 
354
+ if (Array.isArray(taskId)) {
355
+ const unknown = taskId.filter((id) => !tasks.some((t) => t.id === id));
356
+ if (unknown.length) emit({ error: `task(s) ${unknown.join(', ')} não existe(m) em tasks.json`, feature }, 1);
357
+ const results = taskId.map((id) => checkTask(cwd, feature, tasks.find((t) => t.id === id)));
358
+ const clean = results.every((r) => r.verdict === 'ok');
359
+ emit({ feature, results, clean, ...treeFor(cwd, feature, options.beforeEval) }, clean ? 0 : 1);
360
+ }
361
+
306
362
  if (taskId) {
307
363
  const task = tasks.find((t) => t.id === taskId);
308
364
  if (!task) emit({ error: `task "${taskId}" não existe em tasks.json`, feature }, 1);
309
365
  const result = checkTask(cwd, feature, task);
310
- emit({ feature, ...result, hint: ACTION_HINT[result.action] }, result.verdict === 'ok' ? 0 : 1);
366
+ emit({ feature, ...result, hint: ACTION_HINT[result.action], ...treeFor(cwd, feature, options.beforeEval) }, result.verdict === 'ok' ? 0 : 1);
311
367
  }
312
368
 
313
369
  // Whole-feature audit: every DONE task owes a report; a pending one does not.
314
370
  const results = tasks.filter((t) => t.status === 'done').map((t) => checkTask(cwd, feature, t));
315
371
  const evaluator = inspectReport(evaluatorPath(cwd, feature));
316
372
  const clean = results.every((r) => r.verdict === 'ok');
317
- emit({ feature, results, evaluator, clean }, clean ? 0 : 1);
373
+ emit({ feature, results, evaluator, clean, ...treeFor(cwd, feature, options.beforeEval) }, clean ? 0 : 1);
318
374
  }
319
375
 
320
376
  function printHuman(obj) {
@@ -339,6 +395,7 @@ function printHuman(obj) {
339
395
  if (obj.task) {
340
396
  console.log(chalk.bold(`\n📄 report-check ${obj.feature} ${obj.task}`));
341
397
  line(obj);
398
+ printTree(obj.tree);
342
399
  console.log('');
343
400
  return;
344
401
  }
@@ -346,9 +403,33 @@ function printHuman(obj) {
346
403
  console.log(chalk.bold(`\n📄 report-check ${obj.feature}`));
347
404
  if (!obj.results.length) console.log(chalk.gray(' nenhuma task done — nada a auditar'));
348
405
  for (const r of obj.results) line(r);
349
- console.log(
350
- ` ${obj.evaluator.exists ? chalk.green('✓') : chalk.yellow('⚠')} ${'Gate 3'.padEnd(6)} `
351
- + `evaluator-report.md ${obj.evaluator.exists ? (obj.evaluator.complete ? 'íntegro' : 'incompleto') : 'ausente'}`,
352
- );
406
+ if (obj.evaluator) {
407
+ console.log(
408
+ ` ${obj.evaluator.exists ? chalk.green('✓') : chalk.yellow('⚠')} ${'Gate 3'.padEnd(6)} `
409
+ + `evaluator-report.md ${obj.evaluator.exists ? (obj.evaluator.complete ? 'íntegro' : obj.evaluator.partial ? 'PARCIAL' : 'incompleto') : 'ausente'}`,
410
+ );
411
+ }
412
+ printTree(obj.tree);
353
413
  console.log('');
354
414
  }
415
+
416
+ function printTree(tree) {
417
+ if (!tree) {
418
+ console.log(chalk.gray(' árvore: fora de um repositório git — não conferida'));
419
+ return;
420
+ }
421
+ const stat = tree.diffstat ? ` (+${tree.diffstat.insertions} −${tree.diffstat.deletions})` : '';
422
+ console.log(` árvore ${tree.branch ?? '-'}@${tree.head ?? '-'}: ${tree.dirtyCount} arquivo(s) não commitado(s) fora de .morph/${stat}`);
423
+ for (const d of tree.dirty.slice(0, 10)) console.log(chalk.gray(` ${d.status.padEnd(2)} ${d.path}`));
424
+ if (tree.dirtyCount > 10) console.log(chalk.gray(` … e mais ${tree.dirtyCount - 10}`));
425
+ if (tree.markers.length) {
426
+ console.log(chalk.yellow(` ⚠ marcador de mutação esquecido: ${tree.markers.map((m) => `${m.file}:${m.line}`).join(', ')}`));
427
+ }
428
+ if (tree.commits?.length) console.log(chalk.gray(` commits sobre a default: ${tree.commits.join(' · ')}`));
429
+ const since = tree.sinceBeforeEval;
430
+ if (since) {
431
+ console.log(since.changedCount
432
+ ? chalk.yellow(` ⚠ desde a foto de ${since.at}: ${since.changedCount} arquivo(s) mudaram — ${since.changed.join(', ')}`)
433
+ : chalk.green(` ✓ nada mudou fora de .morph/ desde a foto de ${since.at}`));
434
+ }
435
+ }
@@ -11,6 +11,8 @@
11
11
  * how `morph-plan` gets that protection; omitting it degrades to "reserved,
12
12
  * no owner recorded" — still collision-free, just unable to name the
13
13
  * conflicting feature if one ever surfaces.
14
+ *
15
+ * `morph-spec usecase check <feature>` — see `usecaseCheckCommand` below.
14
16
  */
15
17
 
16
18
  import chalk from 'chalk';
@@ -18,6 +20,11 @@ import { existsSync, readdirSync } from 'fs';
18
20
  import { join } from 'path';
19
21
  import { resolveGitCommonDir } from '../../../framework/hooks/shared/git-paths.js';
20
22
  import { reserveUseCases } from '../../lib/specs/usecase-registry.js';
23
+ import { checkUseCases } from '../../lib/specs/usecase-check.js';
24
+ import { checkLifecycle } from '../../lib/specs/acervo-lifecycle.js';
25
+ import { parseTasksJsonLenient } from '../../lib/tasks/tasks-json-parser.js';
26
+ import { isValidFeatureName } from '../project/worktree.js';
27
+ import { printLifecycleWarnings } from '../tasks/dag.js';
21
28
 
22
29
  /**
23
30
  * Best-effort feature-name inference when `--feature` is omitted: only when
@@ -75,3 +82,62 @@ export function usecaseReserveCommand(options = {}) {
75
82
  else console.log('');
76
83
  process.exit(0);
77
84
  }
85
+
86
+ /**
87
+ * `morph-spec usecase check <feature> [--json]` — the use-case half of `dag`,
88
+ * runnable before `tasks.json` exists: the feature's use cases and the
89
+ * revocations its `spec.md` declares, against the acervo's lifecycle. With a
90
+ * `tasks.json`, also the task → use case plumbing `dag` checks.
91
+ *
92
+ * Exit 1 only for what `dag` also refuses (a task pointing at a use case that
93
+ * does not exist, a tasks.json that does not validate); lifecycle findings are
94
+ * warnings.
95
+ *
96
+ * @param {string} feature
97
+ * @param {{ json?: boolean, cwd?: string }} [options]
98
+ */
99
+ export async function usecaseCheckCommand(feature, options = {}) {
100
+ const cwd = options.cwd || process.cwd();
101
+ const fail = (error) => {
102
+ if (options.json) console.log(JSON.stringify({ feature, error }));
103
+ else console.error(chalk.red(`\n❌ ${error}\n`));
104
+ process.exit(1);
105
+ };
106
+
107
+ if (!isValidFeatureName(feature)) fail(`nome de feature inválido: "${feature}"`);
108
+ const featureDir = join(cwd, '.morph', 'features', feature);
109
+ if (!existsSync(featureDir)) fail(`feature não encontrada: .morph/features/${feature}`);
110
+
111
+ const tasksPath = join(featureDir, '2-plan', 'tasks.json');
112
+ let tasksData = null;
113
+ if (existsSync(tasksPath)) {
114
+ try {
115
+ ({ data: tasksData } = await parseTasksJsonLenient(tasksPath));
116
+ } catch (error) {
117
+ fail(`tasks.json inválido: ${error.message}`);
118
+ }
119
+ }
120
+
121
+ const useCases = tasksData ? checkUseCases(tasksData, join(featureDir, '2-plan', 'usecases')) : null;
122
+ const lifecycle = checkLifecycle({ cwd, featureDir, tasksData });
123
+ const broken = useCases?.errors.length > 0;
124
+
125
+ if (options.json) {
126
+ console.log(JSON.stringify({ feature, useCases, lifecycle }, null, 2));
127
+ process.exit(broken ? 1 : 0);
128
+ }
129
+
130
+ if (broken) {
131
+ console.error(chalk.red(`\n❌ Use cases — ${useCases.errors.length} erro(s)\n`));
132
+ for (const e of useCases.errors) console.error(chalk.red(` ${e}`));
133
+ }
134
+ if (useCases?.warnings.length) {
135
+ console.error(chalk.yellow(`\n⚠ Use cases — ${useCases.warnings.length} aviso(s)`));
136
+ for (const w of useCases.warnings) console.error(chalk.yellow(` ${w}`));
137
+ }
138
+ printLifecycleWarnings(lifecycle);
139
+ if (!broken && !useCases?.warnings.length && !lifecycle.warnings.length) {
140
+ console.log(chalk.green(`\n✅ ${feature}: use cases e acervo sem achados\n`));
141
+ }
142
+ process.exit(broken ? 1 : 0);
143
+ }