@polymorphism-tech/morph-spec 8.38.0 → 8.41.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 (242) hide show
  1. package/CLAUDE.md +20 -9
  2. package/README.md +2 -2
  3. package/bin/morph-spec.js +60 -43
  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 +4 -3
  8. package/framework/MORPH.md +10 -9
  9. package/framework/agents.json +43 -26
  10. package/framework/docs/CLI.md +7 -5
  11. package/framework/evals/rubrics/code-quality.md +4 -4
  12. package/framework/evals/rubrics/condensed.md +2 -2
  13. package/framework/hooks/README.md +1 -1
  14. package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
  15. package/framework/hooks/claude-code/core/worktree-create.js +22 -6
  16. package/framework/hooks/shared/git-paths.js +38 -5
  17. package/framework/mcp/registry.json +58 -66
  18. package/framework/rules/infrastructure-standards.md +0 -1
  19. package/framework/rules/morph-workflow.md +9 -4
  20. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  21. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  22. package/framework/skills/core/morph-apply/SKILL.md +59 -25
  23. package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
  24. package/framework/skills/core/morph-eval/SKILL.md +1 -1
  25. package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
  26. package/framework/skills/core/morph-implement/SKILL.md +48 -24
  27. package/framework/skills/core/morph-media/SKILL.md +2 -2
  28. package/framework/skills/core/morph-plan/SKILL.md +10 -3
  29. package/framework/skills/core/morph-proposal/SKILL.md +1 -1
  30. package/framework/skills/core/morph-review/SKILL.md +1 -1
  31. package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
  32. package/framework/skills/core/morph-standards/SKILL.md +1 -1
  33. package/framework/skills/core/morph-uiux/SKILL.md +7 -6
  34. package/framework/standards/PROVENANCE.json +4 -4
  35. package/framework/standards/STANDARDS.json +9 -9
  36. package/framework/standards/ai-agents/context-providers.md +16 -26
  37. package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
  38. package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
  39. package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
  40. package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
  41. package/framework/standards/ai-agents/modalities-vision.md +1 -18
  42. package/framework/standards/ai-agents/prompt-sources.md +0 -4
  43. package/framework/standards/architecture/code-comments.md +52 -16
  44. package/framework/standards/backend/dotnet/testing.md +4 -2
  45. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
  46. package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
  47. package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
  48. package/framework/standards/frontend/nextjs/testing.md +4 -2
  49. package/framework/standards/integration/mcp/mcp-tools.md +64 -126
  50. package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
  51. package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
  52. package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
  53. package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
  54. package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
  55. package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
  56. package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
  57. package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
  58. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
  59. package/framework/templates/code/dotnet/database/migration.cs +49 -83
  60. package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
  61. package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
  62. package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
  63. package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
  64. package/framework/templates/code/dotnet/jobs/job.cs +2 -97
  65. package/framework/templates/code/dotnet/test.cs +2 -87
  66. package/framework/templates/code/typescript/contracts.ts +140 -167
  67. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
  68. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
  69. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
  70. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
  71. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
  72. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
  73. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
  74. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
  75. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
  76. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
  77. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
  78. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
  79. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
  80. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
  81. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
  82. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
  83. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
  84. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
  85. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
  86. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
  87. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
  88. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
  89. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
  90. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
  91. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
  92. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
  93. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
  94. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
  95. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
  96. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
  97. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
  98. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
  99. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
  100. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
  101. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
  102. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
  103. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
  104. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
  105. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
  106. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
  107. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
  108. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
  109. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
  110. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
  111. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
  112. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
  113. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
  114. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
  115. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
  116. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
  117. package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
  118. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
  119. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
  120. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
  121. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
  122. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
  123. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
  124. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
  127. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
  128. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
  129. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
  130. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
  131. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
  132. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
  134. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
  135. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
  136. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
  137. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
  138. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
  139. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
  140. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
  141. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
  142. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
  143. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
  144. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
  145. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
  146. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
  147. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
  148. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
  149. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
  150. package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
  151. package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
  152. package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
  153. package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
  154. package/framework/templates/integrations/asaas-client.cs +332 -387
  155. package/framework/templates/integrations/asaas-webhook.cs +290 -351
  156. package/framework/templates/integrations/neon-auth-config.cs +9 -105
  157. package/framework/templates/project/validate.js +97 -120
  158. package/package.json +2 -2
  159. package/src/commands/dashboard/dashboard.js +80 -53
  160. package/src/commands/mcp/mcp-setup.js +37 -9
  161. package/src/commands/project/doctor.js +12 -6
  162. package/src/commands/project/init.js +6 -3
  163. package/src/commands/project/install-plugin-cmd.js +1 -1
  164. package/src/commands/project/update.js +5 -0
  165. package/src/commands/project/worktree.js +60 -4
  166. package/src/commands/state/gate-check.js +5 -0
  167. package/src/commands/tasks/dag.js +27 -4
  168. package/src/commands/validation/verify.js +22 -38
  169. package/src/dashboard/app/App.jsx +80 -16
  170. package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
  171. package/src/dashboard/app/components/CommandPalette.jsx +155 -0
  172. package/src/dashboard/app/components/DetailPanel.jsx +47 -11
  173. package/src/dashboard/app/components/EntityLink.jsx +55 -0
  174. package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
  175. package/src/dashboard/app/components/FlowGraph.jsx +19 -17
  176. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
  177. package/src/dashboard/app/components/Markdown.jsx +47 -0
  178. package/src/dashboard/app/components/PhaseRail.jsx +111 -0
  179. package/src/dashboard/app/components/SearchFilter.jsx +40 -22
  180. package/src/dashboard/app/components/Sidebar.jsx +146 -37
  181. package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
  182. package/src/dashboard/app/hooks/usePref.js +38 -0
  183. package/src/dashboard/app/lib/format.js +69 -0
  184. package/src/dashboard/app/lib/phases.js +56 -0
  185. package/src/dashboard/app/lib/project.js +50 -0
  186. package/src/dashboard/app/pages/Activity.jsx +24 -0
  187. package/src/dashboard/app/pages/AgentMap.jsx +371 -370
  188. package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
  189. package/src/dashboard/app/pages/Features.jsx +400 -0
  190. package/src/dashboard/app/pages/Guide.jsx +146 -0
  191. package/src/dashboard/app/pages/HookInspector.jsx +357 -322
  192. package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
  193. package/src/dashboard/app/pages/Portfolio.jsx +156 -0
  194. package/src/dashboard/app/pages/Rubrics.jsx +101 -0
  195. package/src/dashboard/app/pages/RulesMap.jsx +300 -273
  196. package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
  197. package/src/dashboard/app/pages/Standards.jsx +407 -332
  198. package/src/dashboard/app/styles/index.css +187 -47
  199. package/src/dashboard/index.html +14 -12
  200. package/src/dashboard/package.json +32 -24
  201. package/src/dashboard/scanner/activity-scanner.js +351 -0
  202. package/src/dashboard/scanner/agents-scanner.js +59 -1
  203. package/src/dashboard/scanner/docs-scanner.js +59 -0
  204. package/src/dashboard/scanner/feature-status.js +10 -0
  205. package/src/dashboard/scanner/features-scanner.js +360 -0
  206. package/src/dashboard/scanner/index.js +14 -7
  207. package/src/dashboard/scanner/phases-scanner.js +14 -15
  208. package/src/dashboard/scanner/portfolio.js +153 -0
  209. package/src/dashboard/scanner/project-api.js +195 -0
  210. package/src/dashboard/scanner/rules-scanner.js +42 -37
  211. package/src/dashboard/scanner/skills-scanner.js +105 -43
  212. package/src/dashboard/scanner/standards-scanner.js +2 -0
  213. package/src/dashboard/vite-plugin-morph-data.js +134 -17
  214. package/src/lib/detectors/claude-config-detector.js +2 -7
  215. package/src/lib/detectors/index.js +5 -1
  216. package/src/lib/evaluator-report.js +22 -0
  217. package/src/lib/generators/settings-generator.js +208 -209
  218. package/src/lib/installers/mcp-installer.js +724 -719
  219. package/src/lib/installers/memory-installer.js +1 -2
  220. package/src/lib/projects/registry.js +154 -0
  221. package/src/lib/risk-detector.js +13 -0
  222. package/src/lib/tasks/tasks-json-parser.js +21 -0
  223. package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
  224. package/src/lib/worktree/env-allocator.js +9 -3
  225. package/src/scripts/install-plugin.js +1 -1
  226. package/src/scripts/setup-infra.js +2 -4
  227. package/src/utils/agents-installer.js +108 -13
  228. package/src/utils/banner.js +1 -1
  229. package/src/commands/index.js +0 -13
  230. package/src/commands/project/checkpoint.js +0 -209
  231. package/src/core/state/phase-state-machine.js +0 -197
  232. package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
  233. package/src/dashboard/app/pages/LiveState.jsx +0 -206
  234. package/src/dashboard/scanner/squads-scanner.js +0 -13
  235. package/src/dashboard/scanner/state-scanner.js +0 -85
  236. package/src/lib/generators/index.js +0 -10
  237. package/src/lib/hooks/hook-executor.js +0 -167
  238. package/src/lib/validators/checkpoint-persistence.js +0 -39
  239. package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
  240. package/src/lib/validators/shared/index.js +0 -12
  241. package/src/lib/validators/shared/issue-counter.js +0 -14
  242. package/src/lib/validators/shared/result-formatter.js +0 -124
@@ -1,47 +1,187 @@
1
- @import "tailwindcss";
2
- @import "@xyflow/react/dist/style.css";
3
-
4
- :root {
5
- --tier-1: #ef4444;
6
- --tier-2: #f97316;
7
- --tier-3: #3b82f6;
8
- --tier-4: #8b5cf6;
9
- --gate-color: #fbbf24;
10
- }
11
-
12
- /* React Flow dark theme overrides */
13
- .react-flow {
14
- background: #030712;
15
- }
16
-
17
- .react-flow__minimap {
18
- background: #111827;
19
- }
20
-
21
- .react-flow__minimap-mask {
22
- fill: #1f2937;
23
- }
24
-
25
- .react-flow__controls {
26
- background: #111827;
27
- border: 1px solid #374151;
28
- }
29
-
30
- .react-flow__controls-button {
31
- background: #111827;
32
- border-bottom: 1px solid #374151;
33
- color: #9ca3af;
34
- }
35
-
36
- .react-flow__controls-button:hover {
37
- background: #1f2937;
38
- color: #f9fafb;
39
- }
40
-
41
- .react-flow__edge-path {
42
- stroke: #4b5563;
43
- }
44
-
45
- .react-flow__node {
46
- border-radius: 0.5rem;
47
- }
1
+ @import "tailwindcss";
2
+ @import "@xyflow/react/dist/style.css";
3
+ @import "@fontsource-variable/geist";
4
+ @import "@fontsource-variable/geist-mono";
5
+ @import "@fontsource-variable/source-serif-4";
6
+
7
+ /*
8
+ * One token system for the whole app. The phase scale is the only palette of
9
+ * meaning: the rail, graphs, chips and the document tree all read it, so a
10
+ * colour always says the same thing. `static`: the phase colours are read as
11
+ * `var(--color-phase-${id})`, a name Tailwind cannot see in the source.
12
+ */
13
+ @theme static {
14
+ --font-sans: "Geist Variable", ui-sans-serif, system-ui, sans-serif;
15
+ --font-mono: "Geist Mono Variable", ui-monospace, monospace;
16
+ --font-serif: "Source Serif 4 Variable", ui-serif, Georgia, serif;
17
+
18
+ --color-ink: #050505;
19
+ --color-shell: #0c0e12;
20
+ --color-core: #12151b;
21
+ --color-raised: #181c24;
22
+ --color-hair: rgb(255 255 255 / 0.08);
23
+ --color-hair-strong: rgb(255 255 255 / 0.14);
24
+ --color-fg: #e8ecf1;
25
+ --color-dim: #8d97a5;
26
+ --color-faint: #5c6573;
27
+
28
+ --color-phase-proposal: #7aa2f7;
29
+ --color-phase-uiux: #bb9af7;
30
+ --color-phase-plan: #e0af68;
31
+ --color-phase-implement: #73daca;
32
+ --color-phase-review: #f7768e;
33
+
34
+ --color-ok: #9ece6a;
35
+ --color-warn: #ff9e64;
36
+ --color-bad: #f7768e;
37
+
38
+ --ease-fluid: cubic-bezier(0.32, 0.72, 0, 1);
39
+
40
+ /* The graph pages were written against Tailwind's gray scale; re-tinting the
41
+ scale to these neutrals puts them in the same room as the rest. */
42
+ --color-gray-50: #f3f5f8;
43
+ --color-gray-100: #e8ecf1;
44
+ --color-gray-200: #d6dce4;
45
+ --color-gray-300: #b8c0cc;
46
+ --color-gray-400: #8d97a5;
47
+ --color-gray-500: #6b7482;
48
+ --color-gray-600: #4a5260;
49
+ --color-gray-700: #2a303b;
50
+ --color-gray-800: #1b1f27;
51
+ --color-gray-900: #12151b;
52
+ --color-gray-950: #07080a;
53
+ }
54
+
55
+ html { color-scheme: dark; }
56
+ html, body, #root { height: 100%; }
57
+
58
+ body {
59
+ background: var(--color-ink);
60
+ color: var(--color-fg);
61
+ font-family: var(--font-sans);
62
+ font-feature-settings: "ss01", "cv11";
63
+ -webkit-font-smoothing: antialiased;
64
+ }
65
+
66
+ /* The ambient glow: fixed and inert, never on a scrolling layer. */
67
+ .ambient {
68
+ position: fixed;
69
+ inset: 0;
70
+ pointer-events: none;
71
+ z-index: 0;
72
+ background:
73
+ radial-gradient(40rem 28rem at 12% -8%, rgb(187 154 247 / 0.10), transparent 70%),
74
+ radial-gradient(36rem 26rem at 96% 108%, rgb(115 218 202 / 0.08), transparent 70%);
75
+ }
76
+
77
+ /* Double bezel: an outer tray holding an inner plate with a lit top edge. */
78
+ .bezel {
79
+ border-radius: 1.25rem;
80
+ padding: 0.3rem;
81
+ background: rgb(255 255 255 / 0.025);
82
+ box-shadow: inset 0 0 0 1px var(--color-hair);
83
+ }
84
+ .bezel > .bezel-core {
85
+ border-radius: calc(1.25rem - 0.3rem);
86
+ background: var(--color-core);
87
+ box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.06), 0 0 0 1px rgb(255 255 255 / 0.03);
88
+ }
89
+
90
+ .fluid { transition-timing-function: var(--ease-fluid); transition-duration: 420ms; }
91
+
92
+ /* A single orchestrated entrance per page. */
93
+ @keyframes rise {
94
+ from { opacity: 0; transform: translateY(10px); }
95
+ to { opacity: 1; transform: none; }
96
+ }
97
+ .rise { animation: rise 640ms var(--ease-fluid) both; }
98
+ .rise-2 { animation: rise 640ms var(--ease-fluid) 80ms both; }
99
+ .rise-3 { animation: rise 640ms var(--ease-fluid) 160ms both; }
100
+
101
+ @media (prefers-reduced-motion: reduce) {
102
+ .rise, .rise-2, .rise-3 { animation: none; }
103
+ .fluid { transition-duration: 0ms; }
104
+ }
105
+
106
+ :focus-visible { outline: 2px solid var(--color-phase-proposal); outline-offset: 2px; border-radius: 6px; }
107
+
108
+ /* Long-form reading: serif, measured, generous leading. */
109
+ .prose-morph {
110
+ font-family: var(--font-serif);
111
+ font-size: 1.02rem;
112
+ line-height: 1.65;
113
+ color: #d9dee6;
114
+ max-width: 75ch;
115
+ }
116
+ .prose-morph h1, .prose-morph h2, .prose-morph h3, .prose-morph h4 {
117
+ font-family: var(--font-sans);
118
+ color: var(--color-fg);
119
+ letter-spacing: -0.01em;
120
+ line-height: 1.25;
121
+ scroll-margin-top: 1.5rem;
122
+ }
123
+ .prose-morph h1 { font-size: 1.75rem; font-weight: 600; margin: 0 0 1.25rem; }
124
+ .prose-morph h2 { font-size: 1.3rem; font-weight: 600; margin: 2.25rem 0 0.75rem; }
125
+ .prose-morph h3 { font-size: 1.08rem; font-weight: 600; margin: 1.75rem 0 0.5rem; }
126
+ .prose-morph h4 { font-size: 0.98rem; font-weight: 600; margin: 1.25rem 0 0.4rem; }
127
+ .prose-morph p, .prose-morph ul, .prose-morph ol, .prose-morph blockquote, .prose-morph table, .prose-morph pre { margin: 0 0 1rem; }
128
+ .prose-morph ul { list-style: disc; padding-left: 1.4rem; }
129
+ .prose-morph ol { list-style: decimal; padding-left: 1.4rem; }
130
+ .prose-morph li { margin: 0.25rem 0; }
131
+ .prose-morph li::marker { color: var(--color-faint); }
132
+ .prose-morph a { color: var(--color-phase-proposal); text-decoration: underline; text-underline-offset: 3px; text-decoration-color: rgb(122 162 247 / 0.4); }
133
+ .prose-morph strong { color: var(--color-fg); font-weight: 600; }
134
+ .prose-morph blockquote { border-left: 2px solid var(--color-hair-strong); padding-left: 1rem; color: var(--color-dim); }
135
+ .prose-morph hr { border: 0; border-top: 1px solid var(--color-hair); margin: 2rem 0; }
136
+ .prose-morph code {
137
+ font-family: var(--font-mono);
138
+ font-size: 0.84em;
139
+ background: rgb(255 255 255 / 0.06);
140
+ border-radius: 5px;
141
+ padding: 0.1em 0.35em;
142
+ }
143
+ .prose-morph pre {
144
+ font-family: var(--font-mono);
145
+ font-size: 0.8rem;
146
+ line-height: 1.55;
147
+ background: var(--color-ink);
148
+ border-radius: 0.75rem;
149
+ box-shadow: inset 0 0 0 1px var(--color-hair);
150
+ padding: 0.9rem 1rem;
151
+ overflow-x: auto;
152
+ max-width: 100%;
153
+ }
154
+ .prose-morph pre code { background: none; padding: 0; font-size: inherit; }
155
+ .prose-morph table { display: block; overflow-x: auto; border-collapse: collapse; font-family: var(--font-sans); font-size: 0.86rem; }
156
+ .prose-morph th, .prose-morph td { border-bottom: 1px solid var(--color-hair); padding: 0.4rem 0.75rem; text-align: left; vertical-align: top; }
157
+ .prose-morph th { color: var(--color-dim); font-weight: 500; }
158
+
159
+ .prose-morph-sm { font-size: 0.95rem; line-height: 1.6; }
160
+ .prose-morph-sm h1 { font-size: 1.35rem; }
161
+ .prose-morph-sm h2 { font-size: 1.12rem; margin-top: 1.75rem; }
162
+ .prose-morph-sm h3 { font-size: 1rem; }
163
+
164
+ /* React Flow, re-skinned to the tokens. */
165
+ .react-flow { background: transparent; }
166
+ .react-flow__minimap { background: var(--color-shell); border-radius: 0.75rem; }
167
+ .react-flow__minimap-mask { fill: rgb(255 255 255 / 0.04); }
168
+ .react-flow__controls { background: var(--color-shell); border-radius: 0.75rem; overflow: hidden; box-shadow: inset 0 0 0 1px var(--color-hair); }
169
+ .react-flow__controls-button { background: transparent; border-bottom: 1px solid var(--color-hair); color: var(--color-dim); }
170
+ .react-flow__controls-button:hover { background: var(--color-raised); color: var(--color-fg); }
171
+ .react-flow__edge-path { stroke: #3a4250; }
172
+ .react-flow__node { border-radius: 0.6rem; }
173
+ .react-flow__attribution { background: transparent; opacity: 0.45; }
174
+ .react-flow__attribution a { color: var(--color-faint); }
175
+
176
+ /* The one ambient motion: a slow sheen on the current phase of an active feature. */
177
+ @keyframes sheen {
178
+ from { transform: translateX(-100%); }
179
+ to { transform: translateX(100%); }
180
+ }
181
+ .rail-sheen {
182
+ background: linear-gradient(90deg, transparent, rgb(255 255 255 / 0.45), transparent);
183
+ animation: sheen 2.4s var(--ease-fluid) infinite;
184
+ }
185
+ @media (prefers-reduced-motion: reduce) {
186
+ .rail-sheen { animation: none; display: none; }
187
+ }
@@ -1,12 +1,14 @@
1
- <!DOCTYPE html>
2
- <html lang="en" class="dark bg-gray-950">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>morph-spec Dashboard</title>
7
- </head>
8
- <body class="dark bg-gray-950 text-gray-100">
9
- <div id="root"></div>
10
- <script type="module" src="/app/main.jsx"></script>
11
- </body>
12
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="pt-BR" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>morph-spec</title>
7
+ <!-- Inline, so the browser's /favicon.ico request never 404s in the console. -->
8
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23030712'/%3E%3Cpath d='M8 23V9l8 8 8-8v14' fill='none' stroke='%233b82f6' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E" />
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ <script type="module" src="/app/main.jsx"></script>
13
+ </body>
14
+ </html>
@@ -1,24 +1,32 @@
1
- {
2
- "name": "morph-spec-dashboard",
3
- "private": true,
4
- "version": "0.1.0",
5
- "type": "module",
6
- "scripts": {
7
- "dev": "vite",
8
- "build": "vite build",
9
- "preview": "vite preview"
10
- },
11
- "dependencies": {
12
- "react": "^19.0.0",
13
- "react-dom": "^19.0.0",
14
- "react-router-dom": "^7.0.0",
15
- "@xyflow/react": "^12.0.0",
16
- "@dagrejs/dagre": "^1.1.0"
17
- },
18
- "devDependencies": {
19
- "@vitejs/plugin-react": "^4.4.0",
20
- "vite": "^6.0.0",
21
- "@tailwindcss/vite": "^4.0.0",
22
- "tailwindcss": "^4.0.0"
23
- }
24
- }
1
+ {
2
+ "name": "morph-spec-dashboard",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@dagrejs/dagre": "^1.1.0",
13
+ "@fontsource-variable/geist": "^5.3.0",
14
+ "@fontsource-variable/geist-mono": "^5.3.0",
15
+ "@fontsource-variable/source-serif-4": "^5.3.0",
16
+ "@phosphor-icons/react": "^2.1.10",
17
+ "@xyflow/react": "^12.0.0",
18
+ "github-slugger": "^2.0.0",
19
+ "react": "^19.0.0",
20
+ "react-dom": "^19.0.0",
21
+ "react-markdown": "^10.1.0",
22
+ "react-router-dom": "^7.0.0",
23
+ "rehype-slug": "^6.0.0",
24
+ "remark-gfm": "^4.0.1"
25
+ },
26
+ "devDependencies": {
27
+ "@tailwindcss/vite": "^4.0.0",
28
+ "@vitejs/plugin-react": "^4.4.0",
29
+ "tailwindcss": "^4.0.0",
30
+ "vite": "^6.0.0"
31
+ }
32
+ }
@@ -0,0 +1,351 @@
1
+ /**
2
+ * The harness event stream (`.morph/logs/events.jsonl`, schema v2 of
3
+ * framework/hooks/shared/telemetry-logger.js) told as a feed a person can read.
4
+ *
5
+ * Reading and summing are NOT done here: `loadTelemetryAllTrees` gathers every
6
+ * tree's log plus the finish backups, and `aggregateCost` is the one place
7
+ * tokens are added up (only `delta` can be summed; a v1 stream yields `null`,
8
+ * which the UI shows as unknown — never 0). This module only decides what each
9
+ * event means in words, and which ones are worth a line.
10
+ */
11
+
12
+ import { statSync, openSync, readSync, closeSync } from 'fs';
13
+ import { join, resolve } from 'path';
14
+ import { loadTelemetryAllTrees, aggregateCost, dedupeEvents } from '../../lib/telemetry/telemetry-reader.js';
15
+ import { isFailedToolCall } from '../../../framework/hooks/shared/telemetry-logger.js';
16
+
17
+ /** Kinds a person follows by default; the rest are one click away. */
18
+ export const DEFAULT_KINDS = ['prompt', 'question', 'skill', 'dispatch', 'handback', 'task', 'verify', 'gate', 'failure'];
19
+ export const ALL_KINDS = [...DEFAULT_KINDS, 'write', 'read', 'command', 'tool'];
20
+
21
+ const GATE_COMMAND = /\bmorph-spec\s+(approve|gate-check|gate-decision|advance|score|finish)\b/;
22
+ const MORPH_PERSONA = /^morph-(.+)$/;
23
+ const SHELL_TOOLS = new Set(['Bash', 'PowerShell']);
24
+ const SILENT_EVENTS = new Set(['ToolBatch', 'SessionUsage', 'AgentUsage']);
25
+
26
+ function shortPath(p) {
27
+ if (typeof p !== 'string') return null;
28
+ const norm = p.replace(/\\/g, '/');
29
+ const feat = norm.indexOf('/.morph/features/');
30
+ if (feat !== -1) return norm.slice(feat + '/.morph/features/'.length).split('/').slice(1).join('/');
31
+ const morph = norm.indexOf('/.morph/');
32
+ if (morph !== -1) return norm.slice(morph + 1);
33
+ return norm.split('/').slice(-3).join('/');
34
+ }
35
+
36
+ function clip(text, max = 280) {
37
+ if (typeof text !== 'string') return null;
38
+ const flat = text.trim();
39
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat;
40
+ }
41
+
42
+ function nodesSummary(nodes) {
43
+ if (!nodes || typeof nodes !== 'object') return [];
44
+ return Object.entries(nodes).map(([name, n]) => ({ name, status: typeof n === 'string' ? n : n?.status ?? null }));
45
+ }
46
+
47
+ /**
48
+ * One event → one feed line, or null when the event is bookkeeping.
49
+ *
50
+ * @returns {null | {
51
+ * id: string, ts: string, feature: string|null, phase: string|null,
52
+ * kind: string, title: string, detail: string|null, status: 'ok'|'fail'|null,
53
+ * refs: { persona?: string, skill?: string, task?: string, path?: string },
54
+ * nodes?: Array<{name: string, status: string|null}>
55
+ * }}
56
+ */
57
+ export function describeEvent(e) {
58
+ if (!e || typeof e !== 'object' || SILENT_EVENTS.has(e.event)) return null;
59
+ const base = {
60
+ id: e.eventId || `${e.ts}:${e.event}:${e.tool || ''}:${e.task || ''}`,
61
+ ts: e.ts || null,
62
+ feature: e.feature || null,
63
+ phase: e.phase || null,
64
+ agent: e.agentType || null,
65
+ status: null,
66
+ detail: null,
67
+ refs: {},
68
+ };
69
+ const line = (kind, title, extra = {}) => ({ ...base, kind, title, ...extra, refs: { ...base.refs, ...(extra.refs || {}) } });
70
+
71
+ if (e.event === 'UserPromptSubmit') {
72
+ // Hand-backs, task notifications and reminders arrive through the same door
73
+ // as a person's prompt, wrapped in a tag. A slash command is the one tagged
74
+ // prompt a person typed.
75
+ const text = typeof e.prompt === 'string' ? e.prompt.trim() : '';
76
+ const command = /^<command-name>\/?([^<]+)<\/command-name>/.exec(text);
77
+ if (command) return line('prompt', `Você rodou /${command[1].trim()}`);
78
+ if (text.startsWith('<')) return null;
79
+ return line('prompt', 'Você escreveu', { detail: clip(text) });
80
+ }
81
+ if (e.event === 'TaskStart') return line('task', `${e.task} começou`, { refs: { task: e.task } });
82
+ if (e.event === 'TaskEnd') {
83
+ const done = e.status === 'done';
84
+ return line('task', done ? `${e.task} concluída` : `${e.task} terminou (${e.status ?? 'sem status'})`, {
85
+ status: done ? 'ok' : null, refs: { task: e.task },
86
+ });
87
+ }
88
+ if (e.event === 'Verify') {
89
+ const ok = e.status === 'pass';
90
+ const scope = e.task ? e.task : 'feature inteira';
91
+ return line('verify', `verify ${scope} ${ok ? 'passou' : 'falhou'}`, {
92
+ status: ok ? 'ok' : 'fail', nodes: nodesSummary(e.nodes), refs: e.task ? { task: e.task } : {},
93
+ detail: e.persisted === false && e.reason ? `não carimbou: ${e.reason}` : null,
94
+ });
95
+ }
96
+ if (e.event !== 'PostToolUse' && e.event !== 'PostToolUseFailure') return null;
97
+
98
+ const failed = isFailedToolCall(e);
99
+ const tool = e.tool;
100
+ if (tool === 'Agent' || tool === 'Task') {
101
+ const m = MORPH_PERSONA.exec(e.persona || '');
102
+ return line('dispatch', `Despachou ${e.persona || 'sub-agent'}`, {
103
+ detail: e.desc || clip(e.prompt, 160), refs: m ? { persona: m[1] } : {},
104
+ });
105
+ }
106
+ if (tool === 'SubagentHandback') return line('handback', `${e.agentType || 'Sub-agent'} devolveu o relatório`);
107
+ if (tool === 'Skill') return line('skill', `Carregou a skill ${e.skill}`, { refs: { skill: e.skill } });
108
+ if (tool === 'AskUserQuestion') return line('question', 'Perguntou a você');
109
+ if (SHELL_TOOLS.has(tool)) {
110
+ const gate = GATE_COMMAND.exec(e.command || '');
111
+ if (gate) {
112
+ return line('gate', `morph-spec ${gate[1]}`, { detail: clip(e.command, 200), status: failed ? 'fail' : 'ok' });
113
+ }
114
+ if (failed) {
115
+ return line('failure', `Comando falhou (exit ${e.exitCode ?? '?'})`, { detail: clip(e.command, 200), status: 'fail' });
116
+ }
117
+ return line('command', tool, { detail: clip(e.command, 200) });
118
+ }
119
+ if (tool === 'Write' || tool === 'Edit') {
120
+ return line('write', `${tool === 'Write' ? 'Escreveu' : 'Editou'} ${shortPath(e.target)}`, { refs: { path: shortPath(e.target) } });
121
+ }
122
+ if (tool === 'Read') {
123
+ const what = e.kind && e.kind !== 'code' ? ` (${e.kind})` : '';
124
+ return line('read', `Leu ${shortPath(e.target)}${what}`, { refs: { path: shortPath(e.target) } });
125
+ }
126
+ if (failed) return line('failure', `${tool} falhou`, { status: 'fail' });
127
+ return line('tool', tool || e.event);
128
+ }
129
+
130
+ // The same event array is fed again on every request until the log changes:
131
+ // describing ~100k events once, not per page, is the difference that matters.
132
+ const described = new WeakMap();
133
+
134
+ function describeAll(events) {
135
+ if (!Array.isArray(events)) return [];
136
+ if (!described.has(events)) {
137
+ const lines = [];
138
+ for (const e of events) {
139
+ const d = describeEvent(e);
140
+ if (d) lines.push(d);
141
+ }
142
+ lines.sort((a, b) => (b.ts || '').localeCompare(a.ts || ''));
143
+ described.set(events, lines);
144
+ }
145
+ return described.get(events);
146
+ }
147
+
148
+ /**
149
+ * The feed, newest first, filtered and paginated by a `before` timestamp cursor.
150
+ *
151
+ * @param {Object[]} events - raw events, any order
152
+ * @param {{ feature?: string, kinds?: string[], before?: string, limit?: number }} [opts]
153
+ */
154
+ export function buildFeed(events, { feature, kinds = DEFAULT_KINDS, before, limit = 100 } = {}) {
155
+ const wanted = new Set(kinds);
156
+ const lines = describeAll(events).filter((l) => wanted.has(l.kind) && (!feature || l.feature === feature));
157
+ const start = before ? lines.findIndex((l) => (l.ts || '') < before) : 0;
158
+ const page = start === -1 ? [] : lines.slice(start, start + limit);
159
+ const last = page[page.length - 1];
160
+ return { items: page, next: page.length === limit && last ? last.ts : null, total: lines.length };
161
+ }
162
+
163
+ /**
164
+ * What the feature list shows as "the last thing that happened" in each
165
+ * feature: the newest line a person follows by default (a dispatch, a verify, a
166
+ * task — not a Read or a shell command), plus `ts`, the newest event of ANY kind,
167
+ * which is what says an agent is working right now.
168
+ *
169
+ * @param {Object[]} events
170
+ * @returns {Record<string, {ts: string, title: string|null, kind: string|null, status: string|null, titleTs: string|null}>}
171
+ */
172
+ const lastByLines = new WeakMap();
173
+
174
+ export function lastActivityByFeature(events) {
175
+ const lines = describeAll(events);
176
+ if (lastByLines.has(lines)) return lastByLines.get(lines);
177
+ const wanted = new Set(DEFAULT_KINDS);
178
+ const out = {};
179
+ for (const line of lines) {
180
+ if (!line.feature) continue;
181
+ const entry = (out[line.feature] ||= { ts: line.ts, title: null, kind: null, status: null, titleTs: null });
182
+ if (entry.title === null && wanted.has(line.kind)) {
183
+ Object.assign(entry, { title: line.title, kind: line.kind, status: line.status, titleTs: line.ts });
184
+ }
185
+ }
186
+ lastByLines.set(lines, out);
187
+ return out;
188
+ }
189
+
190
+ /** Every event of the project across its trees and finish backups. */
191
+ export function loadProjectEvents(projectDir) {
192
+ return dedupeEvents(loadTelemetryAllTrees(projectDir).flatMap((t) => t.events));
193
+ }
194
+
195
+ const LIVE_LOG = join('.morph', 'logs', 'events.jsonl');
196
+
197
+ function eventKey(e) {
198
+ return e.eventId ? `id:${e.eventId}` : `raw:${JSON.stringify(e)}`;
199
+ }
200
+
201
+ function fileSize(path) {
202
+ try { return statSync(path).size; } catch { return -1; }
203
+ }
204
+
205
+ /**
206
+ * The project's events kept in memory and followed as they are appended.
207
+ *
208
+ * A running agent appends one line to its tree's `events.jsonl` per tool call.
209
+ * Re-reading every log (≈100k events, seconds) on each append would block the
210
+ * dev server for the whole run, so the store remembers how far it read each
211
+ * live log and parses only the bytes after that. Anything it cannot follow
212
+ * that way — a log that shrank (SessionStart rotated it), a log it has never
213
+ * seen (a new worktree), an archive or backup — falls back to a full reload
214
+ * through `loadTelemetryAllTrees`, the same reader as always. Copies of one
215
+ * event are still dropped by `eventId`.
216
+ *
217
+ * Every update yields a NEW events array, so everything memoized per array
218
+ * (described lines, per-feature cost) follows the change.
219
+ *
220
+ * @param {string} projectDir
221
+ * @param {{ loadTrees?: Function }} [deps] - test seam, defaults to loadTelemetryAllTrees
222
+ */
223
+ export function createEventStore(projectDir, { loadTrees = loadTelemetryAllTrees } = {}) {
224
+ let events = null;
225
+ let seen = new Set();
226
+ let offsets = new Map(); // live log path -> bytes consumed (up to the last newline)
227
+ let pending = new Set();
228
+ let fullReload = true;
229
+ const costs = new Map();
230
+
231
+ function reload() {
232
+ const trees = loadTrees(projectDir);
233
+ const next = new Map();
234
+ // Sizes before the read: a line appended in between is read twice and
235
+ // deduplicated, never skipped.
236
+ for (const t of trees) {
237
+ if (t.tree === 'finish-backup') continue;
238
+ const path = join(t.path, LIVE_LOG);
239
+ next.set(path, Math.max(0, fileSize(path)));
240
+ }
241
+ events = dedupeEvents(trees.flatMap((t) => t.events));
242
+ seen = new Set(events.map(eventKey));
243
+ offsets = next;
244
+ fullReload = false;
245
+ pending.clear();
246
+ costs.clear();
247
+ }
248
+
249
+ /** Parse the bytes appended to `path` since the last read; false when it cannot follow. */
250
+ function tail(path) {
251
+ const from = offsets.get(path);
252
+ const size = fileSize(path);
253
+ if (from === undefined || size < from) return false;
254
+ if (size === from) return true;
255
+ const fd = openSync(path, 'r');
256
+ let text;
257
+ try {
258
+ const buf = Buffer.alloc(size - from);
259
+ readSync(fd, buf, 0, buf.length, from);
260
+ text = buf.toString('utf8');
261
+ } finally {
262
+ closeSync(fd);
263
+ }
264
+ const end = text.lastIndexOf('\n');
265
+ if (end === -1) return true; // a line still being written
266
+ offsets.set(path, from + Buffer.byteLength(text.slice(0, end + 1), 'utf8'));
267
+ const fresh = [];
268
+ for (const raw of text.slice(0, end).split('\n')) {
269
+ const trimmed = raw.trim();
270
+ if (!trimmed) continue;
271
+ let e;
272
+ try { e = JSON.parse(trimmed); } catch { continue; }
273
+ if (!e || typeof e !== 'object') continue;
274
+ const key = eventKey(e);
275
+ if (seen.has(key)) continue;
276
+ seen.add(key);
277
+ fresh.push(e);
278
+ }
279
+ if (fresh.length) {
280
+ const last = events[events.length - 1]?.ts || '';
281
+ const prevLines = described.get(events);
282
+ const merged = events.concat(fresh);
283
+ events = fresh.every((e) => (e.ts || '') >= last) ? merged : dedupeEvents(merged);
284
+ // Carry the described lines over instead of describing 100k events again.
285
+ if (prevLines) {
286
+ const byTsDesc = (a, b) => (b.ts || '').localeCompare(a.ts || '');
287
+ const added = fresh.map(describeEvent).filter(Boolean).sort(byTsDesc);
288
+ const top = prevLines[0]?.ts || '';
289
+ described.set(events, added.every((l) => (l.ts || '') >= top)
290
+ ? [...added, ...prevLines]
291
+ : [...added, ...prevLines].sort(byTsDesc));
292
+ }
293
+ costs.clear();
294
+ }
295
+ return true;
296
+ }
297
+
298
+ function current() {
299
+ if (fullReload || events === null) {
300
+ reload();
301
+ } else if (pending.size) {
302
+ const paths = [...pending];
303
+ pending.clear();
304
+ for (const p of paths) {
305
+ if (!tail(p)) { reload(); break; }
306
+ }
307
+ }
308
+ return events;
309
+ }
310
+
311
+ return {
312
+ events: current,
313
+ /**
314
+ * Something under `.morph/logs` changed. A known live log is tailed on the
315
+ * next read; anything else (rotation archive, new tree, backup) reloads.
316
+ */
317
+ changed(path) {
318
+ const known = path && [...offsets.keys()].find((p) => resolve(p) === resolve(path));
319
+ if (known) pending.add(known);
320
+ else fullReload = true;
321
+ },
322
+ reset() { fullReload = true; },
323
+ /** Loaded and not waiting for a full reload: reading it now is cheap. */
324
+ ready() { return events !== null && !fullReload; },
325
+ /** `featureCost`, memoized until the events change. */
326
+ cost(feature) {
327
+ const all = current();
328
+ if (!costs.has(feature)) costs.set(feature, featureCost(all, feature));
329
+ return costs.get(feature);
330
+ },
331
+ };
332
+ }
333
+
334
+ /**
335
+ * What the feature cost, straight from `aggregateCost`. `sessionUsage` and
336
+ * `agentUsage` stay `null` when the stream has no summable sample.
337
+ */
338
+ export function featureCost(events, feature) {
339
+ const c = aggregateCost(events, { feature });
340
+ return {
341
+ sessionUsage: c.sessionUsage,
342
+ agentUsage: c.agentUsage,
343
+ legacyUsageSamples: c.sessionUsageLegacySamples,
344
+ dispatches: c.dispatches,
345
+ bytesTotal: c.bytesTotal,
346
+ bytesByKind: c.bytesByKind,
347
+ taskDurationSeconds: c.taskDurationSeconds,
348
+ tasks: c.tasks,
349
+ events: c.total,
350
+ };
351
+ }