@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,12 +7,20 @@
7
7
  * @module graph-build
8
8
  */
9
9
  import chalk from 'chalk';
10
- import { spawnSync } from 'node:child_process';
11
- import { readFileSync, existsSync } from 'node:fs';
10
+ import { readFileSync, existsSync, unlinkSync, writeFileSync } from 'node:fs';
12
11
  import { join } from 'node:path';
13
- import { GRAPHIFY_BIN, GRAPHIFY_VERSION, detectGraphifyVersion } from '../../lib/graph/graphify-runtime.js';
12
+ import { GRAPHIFY_PACKAGE, GRAPHIFY_VERSION } from '../../lib/graph/graphify-runtime.js';
14
13
  import { getProjectTags } from '../../lib/stack-filter.js';
15
- import { writeGraphifyignore } from '../../lib/graph/graphifyignore.js';
14
+ import {
15
+ buildGraphifyignore, writeGraphifyignore, describeOutdatedIgnore,
16
+ } from '../../lib/graph/graphifyignore.js';
17
+ import {
18
+ acquireRefreshLock, isPrimaryTree, readRefreshLock, releaseRefreshLock, resolveGraphify, runGraphify,
19
+ } from '../../../framework/hooks/shared/graph-refresh.js';
20
+ import { isSecondaryWorktree } from '../../../framework/hooks/shared/git-paths.js';
21
+ import {
22
+ TEMP_IGNORE_MARKER, ignoreRulesHash, ignoreTextOf, isTemporaryIgnore, readHead, recordRefreshRun,
23
+ } from '../../../framework/hooks/shared/graph-freshness.js';
16
24
  import { resolveGraphPath } from '../../lib/graph/graph-loader.js';
17
25
 
18
26
  function readProjectTags(projectPath) {
@@ -25,6 +33,44 @@ function readProjectTags(projectPath) {
25
33
  }
26
34
  }
27
35
 
36
+ /**
37
+ * O `.graphifyignore` com que o graphify vai rodar.
38
+ *
39
+ * Na raiz: escrito ou migrado (writeGraphifyignore). Numa worktree o arquivo é
40
+ * do checkout e nunca é reescrito — seria uma mudança na branch da feature.
41
+ * Ausente ali (a raiz ainda não o versionou), as regras — as da raiz
42
+ * (`ignoreTextOf`), ou as geradas — entram só durante a rodada e saem depois
43
+ * (`cleanup`): um `?? .graphifyignore` deixado na worktree faz o `finish` e
44
+ * o `worktree remove --if-clean` recusarem com worktree-dirty (medido), e pôr
45
+ * o arquivo no exclude do common dir esconderia também o da raiz. O arquivo leva
46
+ * `TEMP_IGNORE_MARKER` na 1ª linha: se este processo morrer antes do
47
+ * `cleanup`, a sobra é reconhecida pelo conteúdo — a próxima rodada a reusa e o
48
+ * `finish` a apaga (`dropLeftoverIgnore`). O graphify não herda o da raiz: o
49
+ * ignore dele para na raiz de VCS mais próxima, que numa worktree é ela mesma
50
+ * (detect.py `_load_graphifyignore`).
51
+ *
52
+ * @param {string} projectPath
53
+ * @param {string[]} tags
54
+ * @returns {{written: boolean, migrated?: boolean, outdated?: object, temporary?: boolean, path: string, cleanup: () => void}}
55
+ */
56
+ function prepareIgnore(projectPath, tags) {
57
+ const none = () => {};
58
+ if (!isSecondaryWorktree(projectPath)) return { ...writeGraphifyignore(projectPath, tags), cleanup: none };
59
+ const path = join(projectPath, '.graphifyignore');
60
+ if (existsSync(path) && !isTemporaryIgnore(readFileSync(path, 'utf-8'))) {
61
+ const preview = writeGraphifyignore(projectPath, tags, { fx: { writeFile: none } });
62
+ return { written: false, path, ...(preview.outdated ? { outdated: preview.outdated } : {}), cleanup: none };
63
+ }
64
+ const content = ignoreTextOf(projectPath) ?? buildGraphifyignore(tags);
65
+ writeFileSync(path, `${TEMP_IGNORE_MARKER}\n${content.replace(/^/, '')}`, 'utf-8');
66
+ return {
67
+ written: false,
68
+ temporary: true,
69
+ path,
70
+ cleanup: () => { try { unlinkSync(path); } catch { /* já saiu */ } },
71
+ };
72
+ }
73
+
28
74
  /**
29
75
  * @param {{path?: string, json?: boolean, refresh?: boolean}} [options]
30
76
  */
@@ -32,8 +78,10 @@ export async function graphBuildCommand(options = {}) {
32
78
  const projectPath = options.path || process.cwd();
33
79
  const refresh = options.refresh === true;
34
80
 
35
- if (!detectGraphifyVersion()) {
36
- const hint = `graphify não está no PATH. Instale com: uv tool install ${'graphifyy'}==${GRAPHIFY_VERSION}`;
81
+ // O mesmo resolvedor do refresh em background: no win32 um shim `.cmd` só
82
+ // é achado pelo shell, e os dois caminhos precisam ver o mesmo binário.
83
+ if (!resolveGraphify(process.env)) {
84
+ const hint = `graphify não está no PATH. Instale com: uv tool install ${GRAPHIFY_PACKAGE}==${GRAPHIFY_VERSION}`;
37
85
  const payload = { ok: false, reason: 'graphify-unavailable', hint };
38
86
  if (options.json) {
39
87
  console.log(JSON.stringify(payload, null, 2));
@@ -47,7 +95,33 @@ export async function graphBuildCommand(options = {}) {
47
95
 
48
96
  // O ignore precisa existir ANTES da extração, senão o grafo nasce com o
49
97
  // ruído que a ADR-004 existe para tirar.
50
- writeGraphifyignore(projectPath, readProjectTags(projectPath));
98
+ const ignore = prepareIgnore(projectPath, readProjectTags(projectPath));
99
+ if (ignore.outdated && !options.json) {
100
+ console.error(chalk.yellow(`\n⚠ ${describeOutdatedIgnore(ignore.outdated)}`));
101
+ }
102
+
103
+ // Na árvore principal o refresh em background (framework/hooks/shared/
104
+ // graph-refresh.js) escreve o mesmo graph.json: dois graphify ao mesmo tempo
105
+ // disputam o arquivo e todos os núcleos. Numa worktree ninguém mais escreve
106
+ // o graph.json dela — o background só toca a raiz —, e esperar o lock da
107
+ // raiz ali prometeria "o grafo chega ao HEAD" sobre um refresh que nunca a
108
+ // alcança. Sem git não há lock, e o comando segue sozinho.
109
+ const primary = isPrimaryTree(projectPath);
110
+ const lock = primary ? acquireRefreshLock(projectPath) : null;
111
+ const running = primary && !lock ? readRefreshLock(projectPath) : null;
112
+ if (running?.live) {
113
+ ignore.cleanup();
114
+ const since = new Date(running.since).toLocaleTimeString();
115
+ if (options.json) {
116
+ console.log(JSON.stringify({
117
+ ok: true, running: true, since: running.since, pid: running.pid, mode: refresh ? 'refresh' : 'build',
118
+ }, null, 2));
119
+ } else {
120
+ console.log(chalk.yellow(`\n⟳ Refresh em background já em andamento desde ${since} (pid ${running.pid}).`));
121
+ console.log(chalk.gray(' O grafo chega ao HEAD quando ele terminar — confira com: morph-spec graph status\n'));
122
+ }
123
+ return;
124
+ }
51
125
 
52
126
  const args = refresh
53
127
  ? ['update', projectPath]
@@ -56,19 +130,33 @@ export async function graphBuildCommand(options = {}) {
56
130
  // Capturamos o output em vez de herdar o terminal porque precisamos
57
131
  // inspecioná-lo: o graphify sai 1 tanto quando quebra quanto quando não há
58
132
  // código para indexar, e essas duas coisas não podem virar a mesma mensagem.
59
- const run = spawnSync(GRAPHIFY_BIN, args, {
60
- cwd: projectPath,
61
- encoding: 'utf-8',
62
- stdio: ['ignore', 'pipe', 'pipe'],
63
- });
133
+ // HEAD e regras lidos ANTES: são o que o graphify leu.
134
+ const head = readHead(projectPath);
135
+ const ignoreHash = ignoreRulesHash(projectPath);
136
+ let run;
137
+ let empty = false;
138
+ try {
139
+ run = runGraphify(projectPath, args, { timeout: null });
140
+ // Projeto sem código é o estado normal logo depois de um `init`, não uma
141
+ // falha: o graphify diz "found 0 code" / "graph is empty", sai 1 e não
142
+ // escreve graph.json. Tratar isso como erro faria todo init greenfield
143
+ // terminar em vermelho.
144
+ empty = run.status !== 0 && (/found 0 code/i.test(run.output) || /graph is empty/i.test(run.output));
145
+ // O registro é o que torna "untouched" em dia: sem ele, um refresh que não
146
+ // mudou a topologia deixa o carimbo no commit antigo e o grafo parece
147
+ // velho para sempre (framework/hooks/shared/graph-freshness.js).
148
+ if (!empty) {
149
+ recordRefreshRun(projectPath, {
150
+ head, exit: run.status ?? null, ignoreHash, unavailable: Boolean(run.unavailable),
151
+ });
152
+ }
153
+ } finally {
154
+ releaseRefreshLock(lock);
155
+ ignore.cleanup();
156
+ }
64
157
 
65
- const output = `${run.stdout ?? ''}${run.stderr ?? ''}`;
158
+ const { output } = run;
66
159
  const ok = run.status === 0;
67
- // Projeto sem código é o estado normal logo depois de um `init`, não uma
68
- // falha: o graphify diz "found 0 code" / "graph is empty", sai 1 e não
69
- // escreve graph.json. Tratar isso como erro faria todo init greenfield
70
- // terminar em vermelho.
71
- const empty = !ok && (/found 0 code/i.test(output) || /graph is empty/i.test(output));
72
160
 
73
161
  if (options.json) {
74
162
  console.log(JSON.stringify({
@@ -77,6 +165,9 @@ export async function graphBuildCommand(options = {}) {
77
165
  mode: refresh ? 'refresh' : 'build',
78
166
  path: resolveGraphPath(projectPath),
79
167
  exitCode: run.status,
168
+ ...(ignore.migrated ? { ignoreMigrated: true } : {}),
169
+ ...(ignore.outdated ? { ignoreOutdated: ignore.outdated } : {}),
170
+ ...(ignore.temporary ? { ignoreTemporary: true } : {}),
80
171
  }, null, 2));
81
172
  } else {
82
173
  if (!empty && output.trim()) process.stdout.write(`${output.trimEnd()}\n`);
@@ -11,8 +11,8 @@
11
11
  * @module graph-explain
12
12
  */
13
13
  import chalk from 'chalk';
14
- import { loadGraph, getFreshness } from '../../lib/graph/graph-loader.js';
15
- import { buildIndex, resolveLabel, neighborhood } from '../../lib/graph/sanitize.js';
14
+ import { loadGraph, getFreshness, describeStale } from '../../lib/graph/graph-loader.js';
15
+ import { buildIndex, resolveLabel, neighborhood, isTestPath } from '../../lib/graph/sanitize.js';
16
16
 
17
17
  const FALLBACK_HINT = 'Sem resposta do grafo, caia para grep/Glob.';
18
18
 
@@ -62,6 +62,7 @@ export async function graphExplainCommand(symbol, options = {}) {
62
62
  label: node.label,
63
63
  source_file: node.source_file,
64
64
  source_location: node.source_location ?? '',
65
+ test: isTestPath(node.source_file),
65
66
  },
66
67
  incoming,
67
68
  outgoing,
@@ -74,16 +75,18 @@ export async function graphExplainCommand(symbol, options = {}) {
74
75
  }
75
76
 
76
77
  const nodeLoc = node.source_location ? `:${node.source_location}` : '';
77
- console.log(chalk.green(`\n${node.label}`) + chalk.gray(` ${node.source_file}${nodeLoc}`));
78
+ const testTag = payload.node.test ? chalk.gray(' [teste]') : '';
79
+ console.log(chalk.green(`\n${node.label}`) + chalk.gray(` ${node.source_file}${nodeLoc}`) + testTag);
78
80
  if (freshness.state === 'stale') {
79
- console.log(chalk.yellow(' ⚠ grafo stale — o rebuild do hook é assíncrono'));
81
+ console.log(chalk.yellow(` ⚠ grafo stale — ${describeStale(projectPath, freshness)}`));
80
82
  }
81
83
 
82
84
  console.log(chalk.bold('\n quem depende dele:'));
83
85
  for (const e of incoming) {
84
86
  const loc = e.source_location ? `:${e.source_location}` : '';
85
87
  const via = e.via === 'direct' ? '' : chalk.gray(` [${e.via}]`);
86
- console.log(` ${chalk.cyan(e.relation.padEnd(14))} ${e.source_file}${loc}${via}`);
88
+ const test = e.test ? chalk.gray(' [teste]') : '';
89
+ console.log(` ${chalk.cyan(e.relation.padEnd(14))} ${e.source_file}${loc}${via}${test}`);
87
90
  }
88
91
  if (incoming.length === 0) console.log(chalk.gray(' (nenhum)'));
89
92
 
@@ -8,8 +8,10 @@
8
8
  * @module graph-status
9
9
  */
10
10
  import chalk from 'chalk';
11
- import { loadGraph, getFreshness } from '../../lib/graph/graph-loader.js';
12
- import { buildIndex, classifyLabel } from '../../lib/graph/sanitize.js';
11
+ import {
12
+ loadGraph, getFreshness, describeStale, refreshState, staleCause,
13
+ } from '../../lib/graph/graph-loader.js';
14
+ import { buildIndex, classifyLabel, isTestPath } from '../../lib/graph/sanitize.js';
13
15
  import { GRAPHIFY_VERSION, detectGraphifyVersion } from '../../lib/graph/graphify-runtime.js';
14
16
 
15
17
  function summarizeSanitize(index) {
@@ -52,13 +54,16 @@ export async function graphStatusCommand(options = {}) {
52
54
 
53
55
  const { graph } = loaded;
54
56
  const freshness = getFreshness(projectPath, graph);
55
- const sanitize = summarizeSanitize(buildIndex(graph));
57
+ const index = buildIndex(graph);
58
+ const sanitize = { ...summarizeSanitize(index), recoveredDispatch: index.recovered.length };
56
59
  const payload = {
57
60
  available: true,
58
61
  path: loaded.path,
59
62
  nodes: graph.nodes.length,
63
+ testNodes: graph.nodes.filter((n) => isTestPath(n.source_file)).length,
60
64
  links: graph.links.length,
61
65
  freshness,
66
+ refresh: refreshState(projectPath),
62
67
  version,
63
68
  sanitize,
64
69
  };
@@ -72,10 +77,8 @@ export async function graphStatusCommand(options = {}) {
72
77
  console.log(chalk.gray(` ${loaded.path}`));
73
78
 
74
79
  if (freshness.state === 'stale') {
75
- // O hook post-commit rebuilda em BACKGROUND: logo após um commit o grafo
76
- // ainda é o do commit anterior.
77
- console.log(chalk.yellow(` ⚠ stale — construído em ${freshness.builtAtCommit.slice(0, 8)}, HEAD é ${freshness.headCommit.slice(0, 8)}`));
78
- console.log(chalk.gray(' Atualize com: morph-spec graph refresh'));
80
+ console.log(chalk.yellow(` ⚠ stale — ${staleCause(freshness)}`));
81
+ console.log(chalk.gray(` ${describeStale(projectPath, freshness)}`));
79
82
  } else if (freshness.state === 'fresh') {
80
83
  console.log(chalk.gray(` fresco (${freshness.headCommit.slice(0, 8)})`));
81
84
  } else {
@@ -88,6 +91,9 @@ export async function graphStatusCommand(options = {}) {
88
91
  console.log(chalk.yellow(` ⚠ graphify ${installed} instalado; ${GRAPHIFY_VERSION} é a versão pinada e medida`));
89
92
  }
90
93
 
91
- console.log(chalk.gray(` saneamento: ${sanitize.coalescible} coalescível · ${sanitize.external} externo · ${sanitize.ambiguous} ambíguo`));
94
+ console.log(chalk.gray(` saneamento: ${sanitize.coalescible} coalescível · ${sanitize.external} externo · ${sanitize.ambiguous} ambíguo · ${sanitize.recoveredDispatch} dispatch recuperado`));
95
+ if (payload.testNodes) {
96
+ console.log(chalk.gray(` ${payload.testNodes} nós de teste (dublês, fixtures, helpers) — achados pelo explain, fora dos god nodes`));
97
+ }
92
98
  console.log('');
93
99
  }
@@ -64,21 +64,49 @@ export async function detectStack(targetPath) {
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * Detect the project's integrations (`neon`, `stripe`...), same sources and
69
+ * order as detectStack. The detectors never fold them into the stack string, so
70
+ * without this an MCP scoped to an integration (neon, asaas) is never offered.
71
+ *
72
+ * @param {string} targetPath
73
+ * @returns {Promise<string[]>}
74
+ */
75
+ export async function detectIntegrations(targetPath) {
76
+ try {
77
+ const configPath = join(targetPath, '.morph', 'config', 'config.json');
78
+ if (existsSync(configPath)) {
79
+ const integrations = JSON.parse(readFileSync(configPath, 'utf-8')).project?.integrations;
80
+ if (Array.isArray(integrations)) return integrations;
81
+ }
82
+ } catch {
83
+ // Fall through to filesystem detection
84
+ }
85
+
86
+ try {
87
+ const results = await detectProject(targetPath, { conversation: false, generateStandards: false });
88
+ return results.structure?.integrations ?? [];
89
+ } catch {
90
+ return [];
91
+ }
92
+ }
93
+
67
94
  export async function mcpSetupCommand(name, options) {
68
95
  const targetPath = process.cwd();
69
96
 
70
97
  try {
71
98
  // Detect current environment
72
99
  const stack = await detectStack(targetPath);
100
+ const integrations = await detectIntegrations(targetPath);
73
101
  const claudeConfig = await detectClaudeConfig(targetPath);
74
102
  const existingMcps = claudeConfig.mcpServers || [];
75
103
 
76
104
  if (options.list) {
77
- return showStatusList(targetPath, stack, existingMcps);
105
+ return showStatusList(targetPath, stack, existingMcps, integrations);
78
106
  }
79
107
 
80
108
  if (options.auto) {
81
- return autoInstall(targetPath, stack, existingMcps);
109
+ return autoInstall(targetPath, stack, existingMcps, integrations);
82
110
  }
83
111
 
84
112
  if (name) {
@@ -86,7 +114,7 @@ export async function mcpSetupCommand(name, options) {
86
114
  }
87
115
 
88
116
  // Interactive wizard
89
- return interactiveWizard(targetPath, stack, existingMcps);
117
+ return interactiveWizard(targetPath, stack, existingMcps, integrations);
90
118
  } catch (err) {
91
119
  console.error(chalk.red(`Error: ${err.message}`));
92
120
  process.exit(1);
@@ -96,8 +124,8 @@ export async function mcpSetupCommand(name, options) {
96
124
  /**
97
125
  * Show MCP status list (--list)
98
126
  */
99
- async function showStatusList(targetPath, stack, existingMcps) {
100
- const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps);
127
+ async function showStatusList(targetPath, stack, existingMcps, integrations) {
128
+ const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { integrations });
101
129
  const statusRows = formatMcpStatusTable(orchestration, {});
102
130
 
103
131
  console.log(chalk.cyan('\n MCP Server Status\n'));
@@ -130,8 +158,8 @@ async function showStatusList(targetPath, stack, existingMcps) {
130
158
  /**
131
159
  * Auto-install credential-free MCPs (--auto)
132
160
  */
133
- async function autoInstall(targetPath, stack, existingMcps) {
134
- const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { autoOnly: true });
161
+ async function autoInstall(targetPath, stack, existingMcps, integrations) {
162
+ const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { autoOnly: true, integrations });
135
163
  const autoNames = Object.keys(orchestration.autoInstallable);
136
164
 
137
165
  if (autoNames.length === 0) {
@@ -391,8 +419,8 @@ async function setupSpecificMcp(targetPath, name, stack, existingMcps) {
391
419
  /**
392
420
  * Interactive wizard showing all MCPs
393
421
  */
394
- async function interactiveWizard(targetPath, stack, existingMcps) {
395
- const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps);
422
+ async function interactiveWizard(targetPath, stack, existingMcps, integrations) {
423
+ const orchestration = orchestrateMcpSetup(targetPath, stack, existingMcps, { integrations });
396
424
 
397
425
  console.log(chalk.cyan('\n MCP Setup Wizard'));
398
426
  console.log(chalk.gray(` Stack: ${stack}`));
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `morph-spec cost [feature] [--by task|phase] [--all-trees] [--json]`
2
+ * `morph-spec cost [feature] [--by task|phase] [--all-trees] [--metrics [--top n]] [--json]`
3
3
  *
4
4
  * A cost-facing view over the same harness event stream `telemetry` reads
5
5
  * (.morph/logs/events.jsonl): how much wall-clock time was spent inside
@@ -16,11 +16,14 @@
16
16
  * `sessionUsage` (when present) is the exception: real tokens, observed
17
17
  * directly, not a proxy — see aggregateCost's docstring for the contract.
18
18
  *
19
- * `--all-trees` (Furo 3) widens the source from "just the current tree" to
20
- * every git worktree that has its own events.jsonl — a fleet running N
21
- * features in N worktrees otherwise only ever sees whichever tree `cost` was
22
- * run from. Reuses the same worktree enumeration `doctor --worktrees`/`fleet`
23
- * use (see loadTelemetryAllTrees in telemetry-reader.js).
19
+ * `--all-trees` widens the source from "just the current tree" to every git
20
+ * worktree that has its own events.jsonl plus the logs `finish` saved before
21
+ * removing a worktree, deduplicated by eventId.
22
+ *
23
+ * `--metrics` measures the loop instead: turns, tool calls by orchestrator and
24
+ * sub-agent, time per command class, identical reruns and why, rereads, how
25
+ * many feature-scope verifies each feature ran, git calls per task and whether
26
+ * the reuse validator ran in root and worktree (see lib/telemetry/metrics.js).
24
27
  *
25
28
  * Read-only, always exits 0 on a normal (even empty) run — fail-open, an
26
29
  * empty stream is an answer, not an error. Only `--by` with an unknown value
@@ -28,7 +31,8 @@
28
31
  */
29
32
 
30
33
  import chalk from 'chalk';
31
- import { loadTelemetry, loadTelemetryAllTrees, aggregateCost } from '../../lib/telemetry/telemetry-reader.js';
34
+ import { loadTelemetry, loadTelemetryAllTrees, aggregateCost, dedupeEvents } from '../../lib/telemetry/telemetry-reader.js';
35
+ import { computeMetrics } from '../../lib/telemetry/metrics.js';
32
36
 
33
37
  const BYTES_CAVEAT =
34
38
  'Bytes re-ingeridos são um PROXY determinístico de custo (o que as ferramentas leram do disco) — NÃO são tokens reais. Hooks não enxergam o uso real de tokens do modelo.';
@@ -87,17 +91,30 @@ function printPhases(phases) {
87
91
  }
88
92
  }
89
93
 
94
+ function tokenLine(u) {
95
+ return ` input=${u.inputTokens} output=${u.outputTokens} `
96
+ + `cacheRead=${u.cacheReadTokens} cacheCreation=${u.cacheCreationTokens}`;
97
+ }
98
+
90
99
  /**
91
- * Real (hook-observed) token counts, when the stream has `SessionUsage`
92
- * events — the non-proxy sibling of the bytes-re-ingested numbers above.
93
- * No-op when absent (aggregateCost returns `null`, never a zeroed object).
100
+ * Real (hook-observed) token counts — the non-proxy sibling of the bytes
101
+ * above: the main sessions and, separately, the sub-agents.
94
102
  */
95
- function printSessionUsage(sessionUsage) {
96
- if (!sessionUsage) return;
97
- console.log('\n' + chalk.white.bold('Tokens reais (sessões observadas)')
98
- + chalk.gray(` · ${sessionUsage.sessions} SessionUsage`));
99
- console.log(` input=${sessionUsage.inputTokens} output=${sessionUsage.outputTokens} `
100
- + `cacheRead=${sessionUsage.cacheReadTokens} cacheCreation=${sessionUsage.cacheCreationTokens}`);
103
+ function printSessionUsage(summary) {
104
+ const { sessionUsage, agentUsage, sessionUsageLegacySamples } = summary;
105
+ if (sessionUsage) {
106
+ console.log('\n' + chalk.white.bold('Tokens reais (sessões observadas)')
107
+ + chalk.gray(` · ${sessionUsage.sessions} sessão(ões), ${sessionUsage.samples} amostra(s)`));
108
+ console.log(tokenLine(sessionUsage));
109
+ }
110
+ if (agentUsage) {
111
+ console.log('\n' + chalk.white.bold('Tokens reais (sub-agents)')
112
+ + chalk.gray(` · ${agentUsage.agents} agente(s), ${agentUsage.samples} amostra(s)`));
113
+ console.log(tokenLine(agentUsage));
114
+ }
115
+ if (sessionUsageLegacySamples > 0) {
116
+ console.log(chalk.gray(` ${sessionUsageLegacySamples} amostra(s) SessionUsage antigas (sem delta) fora da soma: eram o total acumulado, repetido a cada Stop.`));
117
+ }
101
118
  }
102
119
 
103
120
  function printSummary(summary) {
@@ -112,7 +129,115 @@ function printSummary(summary) {
112
129
  for (const t of summary.tools) console.log(` ${t.name.padEnd(16)} ${t.count}`);
113
130
  }
114
131
 
115
- printSessionUsage(summary.sessionUsage);
132
+ printSessionUsage(summary);
133
+ }
134
+
135
+ function pct(r) {
136
+ return r === null || r === undefined ? '-' : `${Math.round(r * 1000) / 10}%`;
137
+ }
138
+
139
+ function split(counts) {
140
+ return `${counts.total} (orq ${counts.orchestrator} · sub ${counts.subagent}${counts.unknown ? ` · sem agente ${counts.unknown}` : ''})`;
141
+ }
142
+
143
+ function printMetricsBody(m, { by }) {
144
+ const legacyShare = m.window.events ? m.coverage.legacy / m.window.events : 0;
145
+ console.log(chalk.gray(`janela: ${m.window.from ?? '-'} → ${m.window.to ?? '-'} · ${m.window.events} eventos`
146
+ + ` · ${pct(1 - legacyShare)} no esquema v2`));
147
+ if (m.coverage.legacy) {
148
+ console.log(chalk.gray(' eventos v1 não têm agente, exit code, duração nem janela de Read: entram como "sem agente" e rerun "unknown-*".'));
149
+ }
150
+
151
+ console.log('\n' + chalk.white.bold('Turnos e chamadas'));
152
+ console.log(` turnos: ${m.turns ? split(m.turns) : 'desconhecido (nenhum PostToolBatch na janela)'}`
153
+ + (m.turnsPerTaskMedian !== null ? chalk.gray(` · mediana por task ${m.turnsPerTaskMedian}`) : ''));
154
+ console.log(` chamadas de ferramenta: ${split(m.toolCalls)}`);
155
+ const perTask = m.perTaskByFeature;
156
+ if (perTask.toolCalls !== null) {
157
+ console.log(` por task (total da feature ÷ tasks dela, mediana): ${perTask.toolCalls} chamadas`
158
+ + (perTask.turns !== null ? ` · ${perTask.turns} turnos` : '') + ` · ${perTask.git} git status+diff`
159
+ + chalk.gray(` · as janelas TaskStart→TaskEnd têm ${pct(m.taskWindowCoverage)} das chamadas`));
160
+ }
161
+
162
+ console.log('\n' + chalk.white.bold('Tempo por classe de comando'));
163
+ for (const [klass, t] of Object.entries(m.timeByClass).sort((a, b) => b[1].measuredMs - a[1].measuredMs)) {
164
+ const unmeasured = t.unmeasured ? chalk.gray(` (${t.unmeasured} sem duração)`) : '';
165
+ console.log(` ${klass.padEnd(12)} ${fmtDuration(t.measuredMs / 1000).padStart(8)} ${t.runs} execução(ões)${unmeasured}`);
166
+ }
167
+
168
+ console.log('\n' + chalk.white.bold('Rerun do mesmo comando em ≤10 min (mesma feature; o filtro de saída não conta)'));
169
+ console.log(` total: ${pct(m.reruns.rate)} repetidas (${m.reruns.reruns}/${m.reruns.runs}) · ${pct(m.reruns.burstRate)} em rajada — testes, builds e verifies`);
170
+ for (const [kind, r] of Object.entries(m.rerunsByKind).sort((a, b) => b[1].runs - a[1].runs)) {
171
+ console.log(` ${kind.padEnd(22)} ${pct(r.rate).padStart(6)} repetidas ${pct(r.burstRate).padStart(6)} em rajada (${r.runs} execuções)`);
172
+ }
173
+ const classes = Object.entries(m.rerunClasses);
174
+ if (classes.length) console.log(chalk.gray(` por quê: ${classes.map(([k, n]) => `${k} ${n}`).join(' · ')}`));
175
+
176
+ console.log('\n' + chalk.white.bold('Releitura de arquivo (mesma janela, mesma feature)'));
177
+ console.log(` ${pct(m.rereads.rate)} dos bytes lidos (${fmtBytes(m.rereads.redundantBytes)} de ${fmtBytes(m.rereads.bytes)}, ${m.rereads.reads} leituras)`);
178
+ if (m.rereads.windowed < m.rereads.reads) {
179
+ console.log(chalk.gray(` ${m.rereads.reads - m.rereads.windowed} leitura(s) sem janela gravada (v1) contam o arquivo inteiro: a taxa é um teto`));
180
+ }
181
+ for (const f of m.topRereads) {
182
+ console.log(` ${fmtBytes(f.redundantBytes).padStart(9)} ${f.reads}× ${f.file}${chalk.gray(` [${f.feature ?? '-'}]`)}`);
183
+ }
184
+
185
+ console.log('\n' + chalk.white.bold('verify, git e reuso'));
186
+ const perFeature = Object.entries(m.verify.byFeature).sort((a, b) => b[1].feature - a[1].feature);
187
+ console.log(` verify de feature por feature: mediana ${m.verify.featureScopeMedian ?? '-'}`
188
+ + (perFeature.length ? chalk.gray(` (${perFeature.slice(0, 5).map(([f, v]) => `${f} ${v.feature}`).join(', ')})`) : ''));
189
+ const repeats = Object.entries(m.verify.unchangedRepeats);
190
+ if (repeats.length) {
191
+ console.log(` verify de feature sobre a mesma árvore do anterior: ${repeats.reduce((n, [, v]) => n + v, 0)}`
192
+ + chalk.gray(` (${repeats.map(([f, n]) => `${f} ${n}`).join(', ')})`));
193
+ }
194
+ if (m.dotnetFullSuite.runs) {
195
+ const phases = Object.entries(m.dotnetFullSuite.byPhase).map(([p, v]) => `${p} ${v.withBuild}/${v.runs}`).join(' · ');
196
+ console.log(` suíte .NET inteira que recompila (sem --no-build): ${m.dotnetFullSuite.withBuild}/${m.dotnetFullSuite.runs}`
197
+ + chalk.gray(` (${phases})`));
198
+ }
199
+ console.log(` git status / diff por task: mediana ${m.gitPerTaskMedian.status ?? '-'} / ${m.gitPerTaskMedian.diff ?? '-'}`
200
+ + chalk.gray(` · total ${m.git.status} / ${m.git.diff}`));
201
+ const trees = Object.entries(m.reuse.byTree);
202
+ console.log(` reuse-validator: ${trees.length
203
+ ? trees.map(([tree, s]) => `${tree} rodou ${s.ran}, pulou ${s.skipped}`).join(' · ')
204
+ : 'nenhum evento Verify na janela'}`);
205
+ for (const [reason, n] of Object.entries(m.reuse.skipReasons)) console.log(chalk.gray(` pulou ${n}×: ${reason}`));
206
+
207
+ if (by === 'task') {
208
+ for (const t of m.tasks) {
209
+ const status = t.open ? chalk.yellow('[em aberto]') : (t.status || '-');
210
+ console.log('\n' + chalk.white.bold(`Task ${t.task}`) + chalk.gray(` · ${t.feature ?? '(sem feature)'}`) + ` · ${status}`);
211
+ console.log(` turnos ${t.turns ? split(t.turns) : '-'} · chamadas ${split(t.toolCalls)}`);
212
+ const time = Object.entries(t.timeByClass).map(([k, v]) => `${k} ${fmtDuration(v.measuredMs / 1000)}`).join(' · ');
213
+ if (time) console.log(` tempo: ${time}`);
214
+ console.log(` rerun ${pct(t.reruns.rate)} (${t.reruns.reruns}/${t.reruns.runs}) · releitura ${pct(t.rereads.rate)} · git status/diff ${t.git.status}/${t.git.diff}`);
215
+ }
216
+ }
217
+ }
218
+
219
+ /**
220
+ * `--metrics`: the loop measurements over the current tree (and, with
221
+ * `--all-trees`, every tree plus the finish backups, deduplicated).
222
+ */
223
+ function printMetrics(feature, options, cwd) {
224
+ const events = options.allTrees
225
+ ? dedupeEvents(loadTelemetryAllTrees(cwd).flatMap((t) => t.events))
226
+ : loadTelemetry(cwd);
227
+ const metrics = computeMetrics(events, { feature, top: options.top });
228
+
229
+ if (options.json) {
230
+ console.log(JSON.stringify(metrics));
231
+ return;
232
+ }
233
+ if (metrics.window.events === 0) {
234
+ const scope = feature ? ` para a feature "${feature}"` : '';
235
+ console.log(chalk.gray(`Telemetria vazia${scope} — nada a medir.`));
236
+ return;
237
+ }
238
+ const scope = feature ? chalk.cyan(` · feature ${feature}`) : '';
239
+ console.log(chalk.white.bold(`Medições do loop${options.allTrees ? ' — todas as árvores + finish-backup' : ''}`) + scope);
240
+ printMetricsBody(metrics, options);
116
241
  }
117
242
 
118
243
  /**
@@ -132,7 +257,7 @@ function printAllTrees(feature, options, cwd) {
132
257
  path,
133
258
  ...aggregateCost(events, { feature }),
134
259
  }));
135
- const combined = aggregateCost(treesData.flatMap((t) => t.events), { feature });
260
+ const combined = aggregateCost(dedupeEvents(treesData.flatMap((t) => t.events)), { feature });
136
261
 
137
262
  if (options.json) {
138
263
  console.log(JSON.stringify({ allTrees: true, trees: perTree, combined }));
@@ -183,6 +308,11 @@ export function costCommand(feature, options = {}) {
183
308
 
184
309
  const cwd = options.cwd || process.cwd();
185
310
 
311
+ if (options.metrics) {
312
+ printMetrics(feature, options, cwd);
313
+ return;
314
+ }
315
+
186
316
  if (options.allTrees) {
187
317
  // Same fail-open + Windows-flush discipline as the single-tree path
188
318
  // below — no forced process.exit(), --json included.