@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.
- package/CLAUDE.md +20 -9
- package/README.md +2 -2
- package/bin/morph-spec.js +60 -43
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +4 -3
- package/framework/MORPH.md +10 -9
- package/framework/agents.json +43 -26
- package/framework/docs/CLI.md +7 -5
- package/framework/evals/rubrics/code-quality.md +4 -4
- package/framework/evals/rubrics/condensed.md +2 -2
- package/framework/hooks/README.md +1 -1
- package/framework/hooks/claude-code/core/telemetry-log.js +39 -8
- package/framework/hooks/claude-code/core/worktree-create.js +22 -6
- package/framework/hooks/shared/git-paths.js +38 -5
- package/framework/mcp/registry.json +58 -66
- package/framework/rules/infrastructure-standards.md +0 -1
- package/framework/rules/morph-workflow.md +9 -4
- package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
- package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
- package/framework/skills/core/morph-apply/SKILL.md +59 -25
- package/framework/skills/core/morph-brainstorming/SKILL.md +1 -1
- package/framework/skills/core/morph-eval/SKILL.md +1 -1
- package/framework/skills/core/morph-hotfix/SKILL.md +2 -2
- package/framework/skills/core/morph-implement/SKILL.md +48 -24
- package/framework/skills/core/morph-media/SKILL.md +2 -2
- package/framework/skills/core/morph-plan/SKILL.md +10 -3
- package/framework/skills/core/morph-proposal/SKILL.md +1 -1
- package/framework/skills/core/morph-review/SKILL.md +1 -1
- package/framework/skills/core/morph-stack-scan/SKILL.md +1 -1
- package/framework/skills/core/morph-standards/SKILL.md +1 -1
- package/framework/skills/core/morph-uiux/SKILL.md +7 -6
- package/framework/standards/PROVENANCE.json +4 -4
- package/framework/standards/STANDARDS.json +9 -9
- package/framework/standards/ai-agents/context-providers.md +16 -26
- package/framework/standards/ai-agents/durable-workflows-hitl.md +15 -21
- package/framework/standards/ai-agents/modalities-image-gen.md +21 -25
- package/framework/standards/ai-agents/modalities-speech-to-text.md +4 -15
- package/framework/standards/ai-agents/modalities-text-to-speech.md +2 -11
- package/framework/standards/ai-agents/modalities-vision.md +1 -18
- package/framework/standards/ai-agents/prompt-sources.md +0 -4
- package/framework/standards/architecture/code-comments.md +52 -16
- package/framework/standards/backend/dotnet/testing.md +4 -2
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +63 -50
- package/framework/standards/frontend/design-system/ai-image-generation.md +42 -28
- package/framework/standards/frontend/design-system/ai-video-generation.md +24 -9
- package/framework/standards/frontend/nextjs/testing.md +4 -2
- package/framework/standards/integration/mcp/mcp-tools.md +64 -126
- package/framework/templates/code/dotnet/abstractions/result.cs +0 -2
- package/framework/templates/code/dotnet/ai-agents/AgentBootstrap.cs.template +26 -39
- package/framework/templates/code/dotnet/ai-agents/EmbeddingService.cs.template +28 -68
- package/framework/templates/code/dotnet/ai-agents/ModelRegistry.cs.template +99 -117
- package/framework/templates/code/dotnet/ai-agents/PromptRepository.cs.template +2 -44
- package/framework/templates/code/dotnet/ai-agents/ResilientToolMiddleware.cs.template +40 -61
- package/framework/templates/code/dotnet/ai-agents/VectorSearchTool.cs.template +54 -107
- package/framework/templates/code/dotnet/ai-agents/model-registry.json.template +53 -58
- package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +192 -281
- package/framework/templates/code/dotnet/database/migration.cs +49 -83
- package/framework/templates/code/dotnet/database/vector-entity.cs +2 -46
- package/framework/templates/code/dotnet/extensions/result-extensions.cs +0 -2
- package/framework/templates/code/dotnet/feature/validator.cs.hbs +11 -15
- package/framework/templates/code/dotnet/jobs/agent.cs +3 -51
- package/framework/templates/code/dotnet/jobs/job.cs +2 -97
- package/framework/templates/code/dotnet/test.cs +2 -87
- package/framework/templates/code/typescript/contracts.ts +140 -167
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentApi.cs +1 -48
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactory.cs +3 -79
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentFactoryOptions.cs +1 -70
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpec.cs +1 -47
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Agents/AgentSpecOptions.cs +2 -99
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/AiKitAssemblyMarker.cs +0 -6
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/ConversationStore.cs +3 -88
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/IConversationStore.cs +1 -41
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Conversation/InMemoryConversationStore.cs +0 -21
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/CountingChatClient.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalAssertion.cs +2 -63
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheMissException.cs +0 -30
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheOptions.cs +1 -52
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalCacheStamp.cs +3 -78
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalFixture.cs +4 -109
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalMode.cs +0 -22
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalQualityEvaluators.cs +1 -36
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalScenario.cs +2 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/EvalSummary.cs +0 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/OfflineChatClient.cs +2 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Evals/ShortPathResponseCache.cs +4 -111
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaGenerator.cs +0 -34
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/IMediaProvider.cs +1 -57
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaArtifact.cs +0 -19
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaDisclosure.cs +2 -74
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaGenerator.cs +2 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaIdempotencyKey.cs +1 -43
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJob.cs +1 -96
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobId.cs +0 -13
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaJobState.cs +0 -27
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaPricing.cs +0 -33
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaProvenance.cs +0 -31
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaRequest.cs +3 -75
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaResult.cs +0 -42
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaReview.cs +0 -39
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaSeams.cs +1 -55
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MediaStore.cs +3 -59
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/MorphMedia.cs +0 -46
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/GoogleImageProvider.cs +3 -28
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/OpenAIImageProvider.cs +7 -61
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Media/Providers/VeoVideoProvider.cs +2 -37
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/IUsageCostSink.cs +1 -23
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostMiddleware.cs +2 -82
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Middleware/UsageCostRecord.cs +1 -68
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphOpenTelemetryExtensions.cs +1 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Observability/MorphTelemetryOptions.cs +2 -62
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/IProviderClientFactory.cs +1 -25
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelAlias.cs +0 -124
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Providers/ModelRegistry.cs +1 -73
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaIngest.cs +1 -66
- package/framework/templates/dotnet/ai-kit/src/Morph.AiKit/Rag/DeltaPlan.cs +0 -41
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentFactoryTests.cs +2 -112
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/AgentSpecTests.cs +2 -43
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ConversationStoreTests.cs +5 -110
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/DeltaIngestTests.cs +7 -103
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalAssertionTests.cs +1 -42
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheMissTests.cs +1 -30
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalCacheStampTests.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFailuresTests.cs +0 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalFixtureTests.cs +1 -29
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalRefreshTests.cs +2 -38
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/EvalWorkspace.cs +1 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/KitEvals.cs +4 -69
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Evals/OfflineChatClientTests.cs +1 -10
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClient.cs +1 -57
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeChatClientTests.cs +0 -26
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeClock.cs +1 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeMediaProvider.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeProviderClientFactory.cs +0 -14
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/FakeUsageCostSink.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Fakes/InMemoryMediaStore.cs +0 -32
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAccessibilityTests.cs +0 -37
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaAliasIsNotAnAgentTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +0 -78
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaGeneratorTests.cs +0 -53
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaIdempotencyKeyTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaJobStateTests.cs +0 -44
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaReviewTests.cs +0 -19
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaStoreTests.cs +0 -24
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/ModelRegistryTests.cs +6 -48
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/SkeletonSmokeTests.cs +1 -13
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/TelemetryTests.cs +2 -71
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/UsageCostMiddlewareTests.cs +5 -100
- package/framework/templates/frontend/nextjs/Dockerfile.nextjs.hbs +39 -43
- package/framework/templates/frontend/nextjs/env.mjs.hbs +29 -32
- package/framework/templates/frontend/nextjs/page.tsx.hbs +21 -22
- package/framework/templates/frontend/nextjs/use-feature.ts.hbs +42 -54
- package/framework/templates/integrations/asaas-client.cs +332 -387
- package/framework/templates/integrations/asaas-webhook.cs +290 -351
- package/framework/templates/integrations/neon-auth-config.cs +9 -105
- package/framework/templates/project/validate.js +97 -120
- package/package.json +2 -2
- package/src/commands/dashboard/dashboard.js +80 -53
- package/src/commands/mcp/mcp-setup.js +37 -9
- package/src/commands/project/doctor.js +12 -6
- package/src/commands/project/init.js +6 -3
- package/src/commands/project/install-plugin-cmd.js +1 -1
- package/src/commands/project/update.js +5 -0
- package/src/commands/project/worktree.js +60 -4
- package/src/commands/state/gate-check.js +5 -0
- package/src/commands/tasks/dag.js +27 -4
- package/src/commands/validation/verify.js +22 -38
- package/src/dashboard/app/App.jsx +80 -16
- package/src/dashboard/app/components/ActivityFeed.jsx +172 -0
- package/src/dashboard/app/components/CommandPalette.jsx +155 -0
- package/src/dashboard/app/components/DetailPanel.jsx +47 -11
- package/src/dashboard/app/components/EntityLink.jsx +55 -0
- package/src/dashboard/app/components/ErrorBoundary.jsx +54 -0
- package/src/dashboard/app/components/FlowGraph.jsx +19 -17
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +53 -0
- package/src/dashboard/app/components/Markdown.jsx +47 -0
- package/src/dashboard/app/components/PhaseRail.jsx +111 -0
- package/src/dashboard/app/components/SearchFilter.jsx +40 -22
- package/src/dashboard/app/components/Sidebar.jsx +146 -37
- package/src/dashboard/app/hooks/useMorphQuery.js +54 -0
- package/src/dashboard/app/hooks/usePref.js +38 -0
- package/src/dashboard/app/lib/format.js +69 -0
- package/src/dashboard/app/lib/phases.js +56 -0
- package/src/dashboard/app/lib/project.js +50 -0
- package/src/dashboard/app/pages/Activity.jsx +24 -0
- package/src/dashboard/app/pages/AgentMap.jsx +371 -370
- package/src/dashboard/app/pages/FeatureDetail.jsx +604 -0
- package/src/dashboard/app/pages/Features.jsx +400 -0
- package/src/dashboard/app/pages/Guide.jsx +146 -0
- package/src/dashboard/app/pages/HookInspector.jsx +357 -322
- package/src/dashboard/app/pages/PhasePipeline.jsx +277 -408
- package/src/dashboard/app/pages/Portfolio.jsx +156 -0
- package/src/dashboard/app/pages/Rubrics.jsx +101 -0
- package/src/dashboard/app/pages/RulesMap.jsx +300 -273
- package/src/dashboard/app/pages/SkillsLibrary.jsx +483 -366
- package/src/dashboard/app/pages/Standards.jsx +407 -332
- package/src/dashboard/app/styles/index.css +187 -47
- package/src/dashboard/index.html +14 -12
- package/src/dashboard/package.json +32 -24
- package/src/dashboard/scanner/activity-scanner.js +351 -0
- package/src/dashboard/scanner/agents-scanner.js +59 -1
- package/src/dashboard/scanner/docs-scanner.js +59 -0
- package/src/dashboard/scanner/feature-status.js +10 -0
- package/src/dashboard/scanner/features-scanner.js +360 -0
- package/src/dashboard/scanner/index.js +14 -7
- package/src/dashboard/scanner/phases-scanner.js +14 -15
- package/src/dashboard/scanner/portfolio.js +153 -0
- package/src/dashboard/scanner/project-api.js +195 -0
- package/src/dashboard/scanner/rules-scanner.js +42 -37
- package/src/dashboard/scanner/skills-scanner.js +105 -43
- package/src/dashboard/scanner/standards-scanner.js +2 -0
- package/src/dashboard/vite-plugin-morph-data.js +134 -17
- package/src/lib/detectors/claude-config-detector.js +2 -7
- package/src/lib/detectors/index.js +5 -1
- package/src/lib/evaluator-report.js +22 -0
- package/src/lib/generators/settings-generator.js +208 -209
- package/src/lib/installers/mcp-installer.js +724 -719
- package/src/lib/installers/memory-installer.js +1 -2
- package/src/lib/projects/registry.js +154 -0
- package/src/lib/risk-detector.js +13 -0
- package/src/lib/tasks/tasks-json-parser.js +21 -0
- package/src/lib/validators/code-budget/code-budget-validator.js +32 -4
- package/src/lib/worktree/env-allocator.js +9 -3
- package/src/scripts/install-plugin.js +1 -1
- package/src/scripts/setup-infra.js +2 -4
- package/src/utils/agents-installer.js +108 -13
- package/src/utils/banner.js +1 -1
- package/src/commands/index.js +0 -13
- package/src/commands/project/checkpoint.js +0 -209
- package/src/core/state/phase-state-machine.js +0 -197
- package/src/dashboard/app/hooks/useDetailPanel.js +0 -15
- package/src/dashboard/app/pages/LiveState.jsx +0 -206
- package/src/dashboard/scanner/squads-scanner.js +0 -13
- package/src/dashboard/scanner/state-scanner.js +0 -85
- package/src/lib/generators/index.js +0 -10
- package/src/lib/hooks/hook-executor.js +0 -167
- package/src/lib/validators/checkpoint-persistence.js +0 -39
- package/src/lib/validators/shared/emit-validator-dispatch.js +0 -14
- package/src/lib/validators/shared/index.js +0 -12
- package/src/lib/validators/shared/issue-counter.js +0 -14
- 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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
+
}
|
package/src/dashboard/index.html
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<title>morph-spec
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"@
|
|
16
|
-
"@
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
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
|
+
}
|