@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
@@ -1,17 +1,134 @@
1
- import { scanAll } from './scanner/index.js';
2
-
3
- const VIRTUAL_MODULE_ID = 'virtual:morph-data';
4
- const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_MODULE_ID;
5
-
6
- export default function morphDataPlugin(frameworkDir, projectDir) {
7
- return {
8
- name: 'vite-plugin-morph-data',
9
- resolveId(id) { if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_ID; },
10
- load(id) {
11
- if (id === RESOLVED_VIRTUAL_ID) {
12
- const data = scanAll(frameworkDir, projectDir);
13
- return `export default ${JSON.stringify(data)}`;
14
- }
15
- },
16
- };
17
- }
1
+ import { scanAll } from './scanner/index.js';
2
+ import { classifyChange, watchTargets, isMorphDataReload } from './scanner/project-api.js';
3
+ import { createPortfolio } from './scanner/portfolio.js';
4
+
5
+ const VIRTUAL_MODULE_ID = 'virtual:morph-data';
6
+ const RESOLVED_VIRTUAL_ID = '\0' + VIRTUAL_MODULE_ID;
7
+ const DEBOUNCE_MS = 300;
8
+ const RESCAN_MS = 30_000;
9
+ const WARM_GAP_MS = 400;
10
+ const MAX_BODY = 64 * 1024;
11
+
12
+ function readBody(req) {
13
+ return new Promise((resolveBody) => {
14
+ let size = 0;
15
+ const chunks = [];
16
+ req.on('data', (c) => { size += c.length; if (size <= MAX_BODY) chunks.push(c); });
17
+ req.on('end', () => {
18
+ try { resolveBody(chunks.length ? JSON.parse(Buffer.concat(chunks).toString('utf8')) : null); } catch { resolveBody(null); }
19
+ });
20
+ req.on('error', () => resolveBody(null));
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Two data paths:
26
+ * - `virtual:morph-data` — the framework's knowledge (agents, phases, skills,
27
+ * standards, rules, hooks, docs). Static for the life of the server.
28
+ * - `/api/morph/*` — the portfolio (`/projects`) and each project's features
29
+ * and activity (`/p/:id/…`), read on demand and pushed as `morph:changed`
30
+ * over the HMR socket when a project's `.morph/` changes.
31
+ */
32
+ export default function morphDataPlugin(frameworkDir, projectDir) {
33
+ return {
34
+ name: 'vite-plugin-morph-data',
35
+ resolveId(id) { if (id === VIRTUAL_MODULE_ID) return RESOLVED_VIRTUAL_ID; },
36
+ load(id) {
37
+ if (id === RESOLVED_VIRTUAL_ID) {
38
+ const data = scanAll(frameworkDir);
39
+ return `export default ${JSON.stringify(data)}`;
40
+ }
41
+ },
42
+ configureServer(server) {
43
+ const portfolio = createPortfolio({ launchDir: projectDir });
44
+ const push = (data) => server.ws.send({ type: 'custom', event: 'morph:changed', data });
45
+
46
+ // Vite reloads the whole page for any changed .html it watches, even when
47
+ // handleHotUpdate returns [] (vite 6: no module matched + .html = full
48
+ // reload). A project's .morph/ carries .html design sources, and an agent
49
+ // writing one reloaded the dashboard under the person reading it. The
50
+ // dashboard's own code never lives under .morph/.
51
+ const clientHot = server.environments?.client?.hot;
52
+ if (clientHot?.send) {
53
+ const send = clientHot.send.bind(clientHot);
54
+ clientHot.send = (payload, ...rest) => {
55
+ if (payload?.type === 'full-reload' && isMorphDataReload(payload.path)) return;
56
+ send(payload, ...rest);
57
+ };
58
+ }
59
+
60
+ const watched = new Set();
61
+ function watchNew() {
62
+ const fresh = portfolio.list().flatMap((p) => watchTargets(p.dir)).filter((p) => !watched.has(p));
63
+ for (const p of fresh) watched.add(p);
64
+ if (fresh.length) server.watcher.add(fresh);
65
+ }
66
+
67
+ // Read the projects in the background, one at a time, so neither the first
68
+ // page nor the portfolio waits for a large project. Reading one blocks the
69
+ // event loop for seconds, so each step waits WARM_GAP_MS first: the reads
70
+ // a push (or an add) just caused are served before the next block.
71
+ let warming = false;
72
+ function warmSoon() {
73
+ if (warming) return;
74
+ warming = true;
75
+ const step = () => {
76
+ const warmed = portfolio.warmNext();
77
+ if (!warmed) { warming = false; return; }
78
+ push({ scope: 'projects' });
79
+ setTimeout(step, WARM_GAP_MS);
80
+ };
81
+ setTimeout(step, WARM_GAP_MS);
82
+ }
83
+
84
+ server.middlewares.use('/api/morph', async (req, res) => {
85
+ const body = req.method === 'POST' ? await readBody(req) : null;
86
+ const { status, body: out } = portfolio.handle(req.method || 'GET', req.url || '/', body);
87
+ res.statusCode = status;
88
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
89
+ res.setHeader('Cache-Control', 'no-store');
90
+ res.end(JSON.stringify(out));
91
+ // The portfolio's list changed: watch and read any project just added.
92
+ if (req.method !== 'GET' && status === 200) { watchNew(); push({ scope: 'projects' }); warmSoon(); }
93
+ });
94
+
95
+ watchNew();
96
+ warmSoon();
97
+ const rescan = setInterval(watchNew, RESCAN_MS);
98
+ rescan.unref?.();
99
+
100
+ const timers = new Map();
101
+ const changed = new Map(); // `${project}:${scope}` -> path -> watcher event, in the debounce window
102
+ const onFsEvent = (event, path) => {
103
+ const scope = classifyChange(path);
104
+ if (!scope) return;
105
+ const project = portfolio.ownerOf(path);
106
+ if (!project) return;
107
+ const key = `${project.id}:${scope}`;
108
+ if (!changed.has(key)) changed.set(key, new Map());
109
+ const seen = changed.get(key);
110
+ // A path added then changed in one window is still an add.
111
+ if (!seen.has(path) || seen.get(path) === 'change') seen.set(path, event);
112
+ clearTimeout(timers.get(key));
113
+ timers.set(key, setTimeout(() => {
114
+ timers.delete(key);
115
+ const paths = changed.get(key) || new Map();
116
+ changed.delete(key);
117
+ for (const [p, event] of paths) portfolio.invalidate(project, scope, p, event);
118
+ if (scope === 'features') watchNew();
119
+ push({ scope, project: project.id });
120
+ // A log append is tailed on read, so the portfolio can refresh now; a
121
+ // feature change is re-read in the background first, then pushed.
122
+ if (scope === 'activity') push({ scope: 'projects' });
123
+ else warmSoon();
124
+ }, DEBOUNCE_MS));
125
+ };
126
+ for (const evt of ['add', 'change', 'unlink', 'addDir', 'unlinkDir']) server.watcher.on(evt, (path) => onFsEvent(evt, path));
127
+ server.httpServer?.once('close', () => clearInterval(rescan));
128
+ },
129
+ // A changed `.morph/` file is data, not a module: never let it reach HMR.
130
+ handleHotUpdate(ctx) {
131
+ if (classifyChange(ctx.file)) return [];
132
+ },
133
+ };
134
+ }
@@ -2,14 +2,17 @@
2
2
  * comment-stripper.js — apagar comentários SEM apagar o que está dentro de uma
3
3
  * string, preservando comprimento e quebras de linha.
4
4
  *
5
- * Existe uma vez só, e serve a dois consumidores que fariam a MESMA coisa de
5
+ * Existe uma vez só, e serve a consumidores que fariam a MESMA coisa de
6
6
  * jeitos ligeiramente diferentes se cada um escrevesse o seu:
7
7
  *
8
8
  * - `code-audit.js` — não pode acusar `AgentThread` que só aparece num
9
9
  * comentário ou numa mensagem de log;
10
10
  * - `registry-audit.js` — precisa ler **JSONC**, a forma que o exemplar
11
11
  * canônico `model-registry.v2.json` usa e que o leitor C# do kit aceita de
12
- * propósito (`JsonCommentHandling.Skip`, `AllowTrailingCommas`).
12
+ * propósito (`JsonCommentHandling.Skip`, `AllowTrailingCommas`);
13
+ * - `dotnet-test-introspection.js` — lê o `global.json` por `jsoncToJson`;
14
+ * - `validators/code-budget/comment-scanner.js` — acha as linhas de
15
+ * comentário de `.cs`/`.ts`/`.js`, e é o único que liga a opção `js`.
13
16
  *
14
17
  * **Por que não um regex.** O exemplar tem `"http://localhost:11434/v1"` e uma
15
18
  * `provenance` com URL. Um `replace(/\/\/.*$/gm, '')` decapitaria as duas —
@@ -36,13 +39,18 @@
36
39
  * `○ não verificado`. O B1 reintroduzido pela correção do B1.
37
40
  * @param {{ unterminated?: string|null }} [options.report]
38
41
  * Se um objeto for passado, recebe `unterminated` = `'block-comment'`,
39
- * `'string'`, `'verbatim-string'`, `'raw-string'` ou `null`. É o canal de
42
+ * `'string'`, `'verbatim-string'`, `'raw-string'`, `'template'` ou `null`. É o canal de
40
43
  * diagnóstico que `jsoncToJson` usa para ficar tão estrito quanto o leitor
41
44
  * de produção, sem que este lexer precise lançar (ele também varre `.cs`,
42
45
  * onde um comentário aberto é problema do arquivo, não da varredura).
46
+ * @param {boolean} [options.js=false]
47
+ * Lê também template literal e regex literal de TS/JS. Opt-in porque só
48
+ * quem varre `.ts`/`.js` precisa: sem template, `https://${h}` vira
49
+ * comentário; sem regex, uma crase dentro de `/[`']/` abre um template que
50
+ * engole o resto do arquivo.
43
51
  * @returns {string} texto do MESMO comprimento
44
52
  */
45
- export function stripComments(text, { blankStrings = false, report = null } = {}) {
53
+ export function stripComments(text, { blankStrings = false, report = null, js = false } = {}) {
46
54
  const out = text.split('');
47
55
  const blank = (from, to) => {
48
56
  for (let k = from; k < to && k < out.length; k++) {
@@ -73,6 +81,19 @@ export function stripComments(text, { blankStrings = false, report = null } = {}
73
81
  continue;
74
82
  }
75
83
 
84
+ if (js && c === '`') {
85
+ const end = templateEnd(text, i);
86
+ if (end === -1) flag('template');
87
+ const stop = end === -1 ? text.length : end;
88
+ if (blankStrings) blank(i + 1, end === -1 ? stop : stop - 1);
89
+ i = stop;
90
+ continue;
91
+ }
92
+ if (js && c === '/' && next !== '>' && regexMayStart(out, i)) {
93
+ const end = regexEnd(text, i);
94
+ if (end !== -1) { i = end; continue; }
95
+ }
96
+
76
97
  // Raw string literal do C#: """ ... """ — delimitadores de 3 aspas.
77
98
  if (c === '"' && next === '"' && text[i + 2] === '"') {
78
99
  const end = text.indexOf('"""', i + 3);
@@ -137,6 +158,82 @@ export function stripComments(text, { blankStrings = false, report = null } = {}
137
158
  return out.join('');
138
159
  }
139
160
 
161
+ /** Índice logo após a crase que fecha o template aberto em `start`, ou -1. */
162
+ function templateEnd(text, start) {
163
+ let j = start + 1;
164
+ while (j < text.length) {
165
+ if (text[j] === '\\') { j += 2; continue; }
166
+ if (text[j] === '`') return j + 1;
167
+ if (text[j] === '$' && text[j + 1] === '{') { j = holeEnd(text, j + 2); continue; }
168
+ j += 1;
169
+ }
170
+ return -1;
171
+ }
172
+
173
+ const REGEX_AFTER_WORD = new Set([
174
+ 'return', 'typeof', 'case', 'do', 'else', 'in', 'of', 'new', 'delete', 'void',
175
+ 'throw', 'instanceof', 'yield', 'await',
176
+ ]);
177
+
178
+ /**
179
+ * Uma `/` abre regex, e não divisão, quando o que vem antes não é um valor: o
180
+ * mesmo critério de contexto dos tokenizers de JS. `out` já tem os comentários
181
+ * apagados, então o caractere anterior é código. Sem `<`: em TSX, `</b>` é tag.
182
+ */
183
+ function regexMayStart(out, i) {
184
+ let k = i - 1;
185
+ while (k >= 0 && /\s/.test(out[k])) k--;
186
+ if (k < 0) return true;
187
+ if ('(,=:[!&|?{};+-*%>~^'.includes(out[k])) return true;
188
+ if (!/[A-Za-z_$]/.test(out[k])) return false;
189
+ let w = k;
190
+ while (w > 0 && /[\w$]/.test(out[w - 1])) w--;
191
+ return REGEX_AFTER_WORD.has(out.slice(w, k + 1).join(''));
192
+ }
193
+
194
+ /** Índice logo após a `/` (e as flags) que fecha a regex aberta em `start`, ou -1 se a linha acaba antes. */
195
+ function regexEnd(text, start) {
196
+ let inClass = false;
197
+ for (let j = start + 1; j < text.length; j++) {
198
+ const c = text[j];
199
+ if (c === '\n') return -1;
200
+ if (c === '\\') { j++; continue; }
201
+ if (c === '[') inClass = true;
202
+ else if (c === ']') inClass = false;
203
+ else if (c === '/' && !inClass) {
204
+ let k = j + 1;
205
+ while (k < text.length && /[a-z]/i.test(text[k])) k++;
206
+ return k;
207
+ }
208
+ }
209
+ return -1;
210
+ }
211
+
212
+ /** Índice logo após o `}` que fecha um `${`: o código do buraco aninha chaves, strings e templates. */
213
+ function holeEnd(text, start) {
214
+ let depth = 1;
215
+ let j = start;
216
+ while (j < text.length) {
217
+ const c = text[j];
218
+ if (c === '`') {
219
+ const end = templateEnd(text, j);
220
+ if (end === -1) return text.length;
221
+ j = end;
222
+ continue;
223
+ }
224
+ if (c === '"' || c === "'") {
225
+ j += 1;
226
+ while (j < text.length && text[j] !== c && text[j] !== '\n') j += text[j] === '\\' ? 2 : 1;
227
+ j += 1;
228
+ continue;
229
+ }
230
+ if (c === '{') depth += 1;
231
+ else if (c === '}' && --depth === 0) return j + 1;
232
+ j += 1;
233
+ }
234
+ return text.length;
235
+ }
236
+
140
237
  /**
141
238
  * Converte JSONC em JSON: apaga comentários e vírgulas sobrando antes de `}`/`]`.
142
239
  *
@@ -15,13 +15,12 @@ import { homedir } from 'os';
15
15
  * Detect Claude Code configuration for a project.
16
16
  *
17
17
  * @param {string} targetPath - Root path of the project
18
- * @returns {{ plugins: string[], mcpServers: Array<{name: string, config: Object}>, superpowers: boolean, customSkills: string[], customCommands: string[], hooks: string[] }}
18
+ * @returns {{ plugins: string[], mcpServers: Array<{name: string, config: Object}>, customSkills: string[], customCommands: string[], hooks: string[] }}
19
19
  */
20
20
  export function detectClaudeConfig(targetPath) {
21
21
  const result = {
22
22
  plugins: [],
23
23
  mcpServers: [],
24
- superpowers: false,
25
24
  customSkills: [],
26
25
  customCommands: [],
27
26
  hooks: [],
@@ -86,7 +85,6 @@ export function detectClaudeConfig(targetPath) {
86
85
  const pluginsData = JSON.parse(readFileSync(pluginsPath, 'utf8'));
87
86
  const plugins = pluginsData.plugins || {};
88
87
  result.plugins = Object.keys(plugins);
89
- result.superpowers = 'superpowers@claude-plugins-official' in plugins;
90
88
  } catch { /* invalid JSON */ }
91
89
  }
92
90
 
@@ -166,7 +164,7 @@ export function mcpAvailabilityByPhase(mcpServers, phaseMatrix = {}) {
166
164
  /**
167
165
  * Format a human-readable one-liner summary of detected config.
168
166
  *
169
- * @param {{ plugins: string[], mcpServers: Array<{name: string}>, superpowers: boolean }} config
167
+ * @param {{ plugins: string[], mcpServers: Array<{name: string}> }} config
170
168
  * @returns {string}
171
169
  */
172
170
  export function formatConfigSummary(config) {
@@ -177,8 +175,5 @@ export function formatConfigSummary(config) {
177
175
  if (config.plugins.length > 0) {
178
176
  parts.push(`Plugins: ${config.plugins.length}`);
179
177
  }
180
- if (config.superpowers) {
181
- parts.push('superpowers: yes');
182
- }
183
178
  return parts.join(' | ') || 'No Claude config detected';
184
179
  }
@@ -2,7 +2,7 @@
2
2
  * @fileoverview Project Detector
3
3
  *
4
4
  * Scans a project directory for stack, architecture, UI library, and integrations.
5
- * Used by mcp-setup, morph-init, and session-start hooks.
5
+ * Used by `morph-spec init`, `mcp setup` and the test-node fallback (test-nodes.js).
6
6
  *
7
7
  * @module detectors/index
8
8
  */
@@ -106,6 +106,10 @@ export function detectProject(targetPath, opts = {}) {
106
106
  if (content.includes('Hangfire')) {
107
107
  if (!structure.integrations.includes('hangfire')) structure.integrations.push('hangfire');
108
108
  }
109
+ // Same heuristic as the package.json side (`pg`/`postgres` → neon).
110
+ if (content.includes('Npgsql')) {
111
+ if (!structure.integrations.includes('neon')) structure.integrations.push('neon');
112
+ }
109
113
  } catch { /* ignore */ }
110
114
  }
111
115
 
@@ -93,6 +93,12 @@ const MUTATION_SECTION_RE = /^##\s+Muta[çc][õo]es\s*$/m;
93
93
  */
94
94
  const MEDIA_SECTION_RE = /^##\s+M[íi]dia\s+gerada\s*$/m;
95
95
 
96
+ /**
97
+ * A seção em que o avaliador confere o checklist de tela do mockup contra a
98
+ * tela entregue (`morph-review` §4). Mesma disciplina: exige `##`.
99
+ */
100
+ const SCREEN_CHECKLIST_SECTION_RE = /^##\s+Checklist\s+de\s+tela\s*$/gim;
101
+
96
102
  /**
97
103
  * As chaves reconhecidas de `**Proof-Scores:**`, uma por rubrica de prova.
98
104
  * Chave fora desta lista é IGNORADA (não vira score, não vira erro): o relatório
@@ -287,6 +293,25 @@ export function evaluatorProofState(cwd, featureName) {
287
293
  };
288
294
  }
289
295
 
296
+ /**
297
+ * A seção `## Checklist de tela` do relatório, com o corpo da ÚLTIMA ocorrência
298
+ * — append-only outra vez: numa reavaliação a conferência nova vai para o fim.
299
+ * O corpo vai cru para `checklistCoverage` (`src/lib/specs/screen-checklist.js`),
300
+ * dono único do parser de linha do checklist.
301
+ *
302
+ * @returns {{ section: boolean, body: string }} arquivo ausente ou sem a seção:
303
+ * `{ section: false, body: '' }`. Nunca lança.
304
+ */
305
+ export function evaluatorScreenChecklist(cwd, featureName) {
306
+ const text = readReport(cwd, featureName);
307
+ const matches = text === null ? [] : [...text.matchAll(SCREEN_CHECKLIST_SECTION_RE)];
308
+ if (matches.length === 0) return { section: false, body: '' };
309
+ const last = matches[matches.length - 1];
310
+ const rest = text.slice(last.index + last[0].length);
311
+ const next = rest.search(/^#{1,2}\s/m);
312
+ return { section: true, body: next === -1 ? rest : rest.slice(0, next) };
313
+ }
314
+
290
315
  /**
291
316
  * O menor Proof-Score das provas de EVIDÊNCIA — o que impõe o teto em
292
317
  * `test-coverage`.
@@ -324,3 +349,25 @@ export function mediaProofScore(proofScores) {
324
349
  const value = proofScores?.media;
325
350
  return typeof value === 'number' && Number.isFinite(value) ? value : null;
326
351
  }
352
+
353
+ /**
354
+ * Os dois vereditos do relatório lidos de um TEXTO já em mãos — o score
355
+ * composto vigente e os Proof-Scores vigentes, pela mesma regra append-only
356
+ * (o último token vale) dos leitores por `cwd` acima.
357
+ *
358
+ * Existe para quem tem o relatório fora do caminho canônico — o dashboard lê
359
+ * features arquivadas em `.morph/archive/{f}/4-review/` —, sem que ele
360
+ * precise de uma segunda cópia dos regexes.
361
+ *
362
+ * @param {string} text
363
+ * @returns {{ score: number|undefined, proofScores: Record<string, number>|undefined }}
364
+ */
365
+ export function evaluatorVerdictFromText(text) {
366
+ if (typeof text !== 'string') return { score: undefined, proofScores: undefined };
367
+ const scores = compositeScoresIn(text);
368
+ const lines = [...text.matchAll(PROOF_SCORES_RE)];
369
+ return {
370
+ score: scores.length === 0 ? undefined : scores[scores.length - 1],
371
+ proofScores: lines.length === 0 ? undefined : parseProofScores(lines[lines.length - 1][1]),
372
+ };
373
+ }
@@ -16,9 +16,9 @@
16
16
  * degrades that section, never the document.
17
17
  */
18
18
 
19
- import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'fs';
20
- import { join, resolve } from 'path';
21
- import { execSync } from 'child_process';
19
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
20
+ import { join } from 'path';
21
+ import { ensureCommonExclude } from '../../../framework/hooks/shared/git-exclude.js';
22
22
  import { derivePhaseForFeature, getPendingGates } from '../../../framework/hooks/shared/state-reader.js';
23
23
 
24
24
  const PHASE_DIRS = {
@@ -236,21 +236,7 @@ const HANDOFF_EXCLUDE_PATTERN = '/.morph/**/handoff.md';
236
236
  * untracked is noise, never data loss.
237
237
  */
238
238
  function ensureHandoffExcluded(cwd) {
239
- try {
240
- const common = execSync('git rev-parse --git-common-dir', { cwd, stdio: 'pipe' }).toString().trim();
241
- if (!common) return;
242
- const infoDir = join(resolve(cwd, common), 'info');
243
- const excludePath = join(infoDir, 'exclude');
244
- const current = existsSync(excludePath) ? readFileSync(excludePath, 'utf8') : '';
245
- const present = new Set(current.split(/\r?\n/).map((l) => l.trim()));
246
- if (present.has(HANDOFF_EXCLUDE_PATTERN)) return;
247
- mkdirSync(infoDir, { recursive: true });
248
- appendFileSync(excludePath, [
249
- current && !current.endsWith('\n') ? '\n' : '',
250
- present.has(HANDOFF_EXCLUDE_MARKER) ? '' : `${HANDOFF_EXCLUDE_MARKER}\n`,
251
- `${HANDOFF_EXCLUDE_PATTERN}\n`,
252
- ].join(''));
253
- } catch { /* fail-open */ }
239
+ ensureCommonExclude(cwd, [HANDOFF_EXCLUDE_PATTERN], HANDOFF_EXCLUDE_MARKER);
254
240
  }
255
241
 
256
242
  /**