@polymorphism-tech/morph-spec 8.47.0 → 9.1.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 +51 -43
- package/README.md +39 -38
- package/bin/morph-spec.js +41 -113
- package/bin/validate.js +1 -147
- package/claude-plugin.json +1 -1
- package/docs/CHEATSHEET.md +1 -1
- package/docs/QUICKSTART.md +1 -1
- package/framework/CLAUDE.md +41 -101
- package/framework/MORPH.md +95 -252
- package/framework/agents.json +86 -21
- package/framework/commands/morph-apply.md +5 -28
- package/framework/commands/morph-archive.md +1 -1
- package/framework/commands/morph-hotfix.md +8 -8
- package/framework/commands/morph-infra.md +1 -1
- package/framework/commands/morph-preflight.md +2 -3
- package/framework/commands/morph-status.md +4 -4
- package/framework/docs/CLI.md +574 -286
- package/framework/evals/review-checklist.md +82 -41
- package/framework/hooks/README.md +10 -11
- package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
- package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
- package/framework/hooks/claude-code/core/chain.js +4 -65
- package/framework/hooks/claude-code/core/comment-guard.js +3 -17
- package/framework/hooks/claude-code/core/denylist.json +33 -3
- package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
- package/framework/hooks/claude-code/core/gate-guard.js +9 -78
- package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
- package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
- package/framework/hooks/claude-code/core/heavy-release.js +2 -13
- package/framework/hooks/claude-code/core/loop-detect.js +2 -31
- package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
- package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
- package/framework/hooks/claude-code/core/state-sync.js +15 -85
- package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
- package/framework/hooks/claude-code/core/validate-completion.js +37 -147
- package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
- package/framework/hooks/claude-code/core/worktree-context.js +15 -114
- package/framework/hooks/claude-code/core/worktree-create.js +6 -70
- package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
- package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
- package/framework/hooks/claude-code/statusline.py +55 -41
- package/framework/hooks/package.json +3 -0
- package/framework/hooks/shared/activity-logger.js +9 -89
- package/framework/hooks/shared/agent-reap.js +2 -17
- package/framework/hooks/shared/build-check.js +11 -76
- package/framework/hooks/shared/claim-registry.js +9 -119
- package/framework/hooks/shared/command-tokens.js +168 -44
- package/framework/hooks/shared/comment-rules.js +5 -20
- package/framework/hooks/shared/comment-scanner.js +11 -43
- package/framework/hooks/shared/comment-stripper.js +11 -85
- package/framework/hooks/shared/e2e-check.js +0 -0
- package/framework/hooks/shared/errata.js +59 -0
- package/framework/hooks/shared/feature-locator.js +11 -95
- package/framework/hooks/shared/frontend-locations.js +4 -0
- package/framework/hooks/shared/gate-question.js +12 -0
- package/framework/hooks/shared/git-exclude.js +3 -20
- package/framework/hooks/shared/git-paths.js +8 -104
- package/framework/hooks/shared/graph-freshness.js +22 -166
- package/framework/hooks/shared/graph-refresh.js +21 -149
- package/framework/hooks/shared/heavy-command.js +4 -12
- package/framework/hooks/shared/hook-response.js +8 -83
- package/framework/hooks/shared/morph-cli.js +8 -52
- package/framework/hooks/shared/payload-utils.js +10 -132
- package/framework/hooks/shared/phase-utils.js +14 -72
- package/framework/hooks/shared/redact-secrets.js +4 -15
- package/framework/hooks/shared/reparse-scan.js +6 -66
- package/framework/hooks/shared/slice-progress.js +99 -0
- package/framework/hooks/shared/source-files.js +4 -17
- package/framework/hooks/shared/spawn-command.js +23 -0
- package/framework/hooks/shared/state-reader.js +51 -247
- package/framework/hooks/shared/state-writer.js +9 -54
- package/framework/hooks/shared/stdin-reader.js +1 -11
- package/framework/hooks/shared/telemetry-logger.js +7 -67
- package/framework/hooks/shared/transcript-usage.js +7 -29
- package/framework/hooks/shared/worktree-root.js +7 -29
- package/framework/mcp/registry.json +3 -4
- package/framework/phases.json +23 -39
- package/framework/rules/csharp-standards.md +1 -1
- package/framework/rules/frontend-standards.md +1 -0
- package/framework/rules/hangfire-standards.md +1 -1
- package/framework/rules/neon-standards.md +1 -1
- package/framework/schemas/config.schema.json +14 -9
- package/framework/schemas/feature.schema.json +15 -50
- package/framework/skills/README.md +3 -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 +149 -222
- package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
- package/framework/skills/core/morph-machine/SKILL.md +2 -2
- package/framework/skills/core/morph-media/SKILL.md +1 -1
- package/framework/skills/core/morph-plan/SKILL.md +137 -299
- package/framework/skills/core/morph-review/SKILL.md +111 -136
- package/framework/skills/core/morph-standards/SKILL.md +27 -39
- package/framework/skills/core/morph-uiux/SKILL.md +94 -152
- package/framework/standards/STANDARDS.json +109 -1219
- package/framework/standards/STANDARDS.meta.json +1319 -0
- package/framework/standards/ai-agents/agent-archetypes.md +2 -2
- package/framework/standards/ai-agents/agent-session.md +1 -1
- package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
- package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
- package/framework/standards/ai-agents/evals-with-cache.md +1 -2
- package/framework/standards/ai-agents/guardrails.md +1 -1
- package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
- package/framework/standards/ai-agents/observability-patterns.md +1 -1
- package/framework/standards/ai-agents/production.md +1 -1
- package/framework/standards/ai-agents/prompt-sources.md +2 -2
- package/framework/standards/ai-agents/providers/model-registry.md +1 -1
- package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
- package/framework/standards/ai-agents/setup.md +3 -3
- package/framework/standards/ai-agents/sweet-spot.md +2 -2
- package/framework/standards/ai-agents/tool-adapters.md +0 -2
- package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
- package/framework/standards/ai-agents/workflows.md +1 -1
- package/framework/standards/architecture/code-comments.md +7 -7
- package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
- package/framework/standards/backend/api/validation.md +1 -1
- package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
- package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
- package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
- package/framework/standards/data/nosql/cache/redis.md +1 -1
- package/framework/standards/data/nosql/cosmos-db.md +3 -3
- package/framework/standards/frontend/admin-app/forms.md +1 -1
- package/framework/standards/frontend/design-system/art-direction.md +88 -0
- package/framework/standards/frontend/design-system/premium-finish.md +77 -2
- package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
- package/framework/standards/integration/mcp/mcp-tools.md +7 -7
- package/framework/templates/CLAUDE.md.core.template +3 -3
- package/framework/templates/README.md +2 -3
- package/framework/templates/REGISTRY.json +15 -179
- package/framework/templates/docs/plan.md +72 -0
- package/framework/templates/docs/ui-mockups.md +4 -0
- package/framework/templates/dotnet/ai-kit/README.md +1 -9
- package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
- package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
- package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
- package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
- package/package.json +1 -1
- package/src/commands/context/context-refresh.js +0 -16
- package/src/commands/dashboard/dashboard.js +2 -8
- package/src/commands/e2e/e2e.js +18 -105
- package/src/commands/graph/affected.js +0 -14
- package/src/commands/graph/build.js +11 -50
- package/src/commands/graph/explain.js +0 -16
- package/src/commands/graph/status.js +1 -12
- package/src/commands/mcp/mcp-setup.js +4 -62
- package/src/commands/observability/cost.js +17 -111
- package/src/commands/observability/telemetry.js +1 -14
- package/src/commands/project/branch.js +4 -45
- package/src/commands/project/claim.js +4 -48
- package/src/commands/project/doctor-procs.js +1 -1
- package/src/commands/project/doctor.js +53 -307
- package/src/commands/project/env.js +1 -27
- package/src/commands/project/fleet.js +6 -35
- package/src/commands/project/heavy.js +3 -3
- package/src/commands/project/init.js +8 -68
- package/src/commands/project/install-plugin-cmd.js +0 -1
- package/src/commands/project/setup-infra-cmd.js +2 -1
- package/src/commands/project/stack-scan.js +58 -0
- package/src/commands/project/standards.js +65 -72
- package/src/commands/project/status.js +28 -121
- package/src/commands/project/update.js +31 -223
- package/src/commands/project/worktree.js +109 -883
- package/src/commands/scaffold/scaffold.js +0 -25
- package/src/commands/state/advance.js +65 -120
- package/src/commands/state/approve.js +87 -229
- package/src/commands/state/archive.js +23 -139
- package/src/commands/state/auto-approval.js +65 -0
- package/src/commands/state/create.js +49 -109
- package/src/commands/state/delete.js +11 -63
- package/src/commands/state/finish.js +105 -494
- package/src/commands/state/gate-check.js +74 -183
- package/src/commands/state/gate-decision.js +8 -42
- package/src/commands/state/handoff.js +4 -31
- package/src/commands/state/list.js +21 -91
- package/src/commands/state/report-check.js +134 -338
- package/src/commands/state/retype.js +12 -28
- package/src/commands/validation/index.js +0 -3
- package/src/commands/validation/mutate.js +79 -19
- package/src/commands/validation/plan-check.js +81 -0
- package/src/commands/validation/validate-feature.js +52 -216
- package/src/commands/validation/verify.js +45 -160
- package/src/core/paths/output-schema.js +9 -155
- package/src/core/state/feature-identity.js +1 -25
- package/src/core/state/gate-order.js +2 -30
- package/src/core/state/lifecycle-audit.js +8 -100
- package/src/core/state/state-manager.js +32 -395
- package/src/core/templates/template-registry.js +2 -97
- package/src/core/templates/template-renderer.js +13 -93
- package/src/core/templates/template-seed.js +21 -0
- package/src/core/work-types.js +10 -45
- package/src/dashboard/app/App.jsx +1 -8
- package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
- package/src/dashboard/app/components/CommandPalette.jsx +4 -11
- package/src/dashboard/app/components/DetailPanel.jsx +1 -5
- package/src/dashboard/app/components/EntityLink.jsx +0 -12
- package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
- package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
- package/src/dashboard/app/components/Markdown.jsx +0 -9
- package/src/dashboard/app/components/PhaseRail.jsx +0 -11
- package/src/dashboard/app/components/SearchFilter.jsx +0 -4
- package/src/dashboard/app/components/Sidebar.jsx +0 -5
- package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
- package/src/dashboard/app/hooks/usePref.js +1 -5
- package/src/dashboard/app/lib/format.js +0 -2
- package/src/dashboard/app/lib/phases.js +3 -9
- package/src/dashboard/app/lib/project.js +0 -12
- package/src/dashboard/app/pages/Activity.jsx +1 -1
- package/src/dashboard/app/pages/AgentMap.jsx +1 -8
- package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
- package/src/dashboard/app/pages/Features.jsx +9 -16
- package/src/dashboard/app/pages/Guide.jsx +2 -3
- package/src/dashboard/app/pages/HookInspector.jsx +2 -23
- package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
- package/src/dashboard/app/pages/Portfolio.jsx +0 -1
- package/src/dashboard/app/pages/RulesMap.jsx +1 -16
- package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
- package/src/dashboard/app/pages/Standards.jsx +2 -17
- package/src/dashboard/scanner/activity-scanner.js +14 -83
- package/src/dashboard/scanner/agents-scanner.js +1 -23
- package/src/dashboard/scanner/docs-scanner.js +1 -7
- package/src/dashboard/scanner/feature-status.js +2 -5
- package/src/dashboard/scanner/features-scanner.js +92 -113
- package/src/dashboard/scanner/hooks-scanner.js +5 -28
- package/src/dashboard/scanner/index.js +0 -4
- package/src/dashboard/scanner/portfolio.js +5 -29
- package/src/dashboard/scanner/project-api.js +8 -54
- package/src/dashboard/scanner/rules-scanner.js +1 -2
- package/src/dashboard/scanner/skills-scanner.js +2 -12
- package/src/dashboard/scanner/standards-scanner.js +4 -7
- package/src/dashboard/vite-plugin-morph-data.js +5 -19
- package/src/lib/ai/ai-checks.js +7 -52
- package/src/lib/ai/ai-project-scan.js +14 -108
- package/src/lib/ai/code-audit.js +7 -63
- package/src/lib/ai/pin-drift.js +8 -51
- package/src/lib/ai/registry-audit.js +11 -90
- package/src/lib/ci/ci-verify.js +14 -13
- package/src/lib/ci/github-ci.js +9 -37
- package/src/lib/ci/workflow-scaffold.js +2 -10
- package/src/lib/detectors/claude-config-detector.js +13 -74
- package/src/lib/detectors/index.js +8 -59
- package/src/lib/e2e/compose-env.js +5 -66
- package/src/lib/e2e/stack-teardown.js +6 -40
- package/src/lib/generators/architecture-scanner.js +18 -156
- package/src/lib/generators/handoff-generator.js +37 -112
- package/src/lib/generators/scaffold-generator.js +5 -101
- package/src/lib/generators/settings-generator.js +11 -122
- package/src/lib/git/branch-classifier.js +11 -84
- package/src/lib/git/default-branch.js +14 -169
- package/src/lib/git/evidence-staleness.js +2 -1
- package/src/lib/git/gh.js +2 -17
- package/src/lib/git/pending-prs.js +6 -37
- package/src/lib/git/pr-files.js +3 -28
- package/src/lib/git/pr-index.js +3 -34
- package/src/lib/git/tree-fingerprint.js +17 -17
- package/src/lib/git/tree-state.js +6 -33
- package/src/lib/graph/god-nodes.js +2 -22
- package/src/lib/graph/graph-health.js +3 -24
- package/src/lib/graph/graph-loader.js +5 -56
- package/src/lib/graph/graphify-runtime.js +7 -37
- package/src/lib/graph/graphifyignore.js +8 -74
- package/src/lib/graph/sanitize.js +16 -136
- package/src/lib/heavy/cleanup.js +2 -6
- package/src/lib/installers/effects.js +8 -93
- package/src/lib/installers/mcp-installer.js +9 -169
- package/src/lib/installers/memory-installer.js +8 -33
- package/src/lib/installers/templates-reconcile.js +5 -60
- package/src/lib/mutation/commands.js +3 -2
- package/src/lib/mutation/dotnet-introspection.js +281 -0
- package/src/lib/mutation/plan.js +14 -6
- package/src/lib/mutation/runner.js +1 -1
- package/src/lib/mutation/subset.js +47 -13
- package/src/lib/plan/artifact-vocabulary.js +40 -0
- package/src/lib/plan/checks.js +208 -0
- package/src/lib/plan/plan-parser.js +204 -0
- package/src/lib/plan/plan-template.js +18 -0
- package/src/lib/projects/registry.js +7 -44
- package/src/lib/review-reports.js +204 -0
- package/src/lib/risk-detector.js +131 -274
- package/src/lib/specs/adr-index.js +38 -85
- package/src/lib/specs/design-seed.js +18 -0
- package/src/lib/specs/frontmatter.js +31 -0
- package/src/lib/specs/gfm-table.js +11 -0
- package/src/lib/specs/revocations.js +31 -102
- package/src/lib/specs/screen-checklist.js +56 -132
- package/src/lib/stack/dotnet-build-ignores.js +43 -0
- package/src/lib/stack/stack-profile.js +1 -28
- package/src/lib/stack/stack-scan.js +260 -0
- package/src/lib/stack-filter.js +4 -27
- package/src/lib/standards/registry.js +37 -0
- package/src/lib/standards/sections.js +47 -0
- package/src/lib/standards/verified-against.js +19 -191
- package/src/lib/tasks/eval-node.js +23 -179
- package/src/lib/tasks/linux-runner.js +18 -142
- package/src/lib/tasks/test-nodes.js +43 -313
- package/src/lib/tasks/test-runner.js +107 -447
- package/src/lib/telemetry/metrics.js +13 -103
- package/src/lib/telemetry/telemetry-reader.js +13 -238
- package/src/lib/telemetry/verify-event.js +4 -34
- package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
- package/src/lib/validators/code-budget/index.js +0 -7
- package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
- package/src/lib/validators/contracts/index.js +0 -6
- package/src/lib/validators/nextjs/index.js +0 -5
- package/src/lib/validators/nextjs/next-component-validator.js +4 -81
- package/src/lib/validators/packages/index.js +0 -6
- package/src/lib/validators/packages/package-validator.js +6 -56
- package/src/lib/validators/reuse/index.js +0 -8
- package/src/lib/validators/reuse/reuse-validator.js +24 -137
- package/src/lib/validators/reuse/symbol-scanner.js +6 -62
- package/src/lib/validators/traceability-check.js +54 -156
- package/src/lib/validators/validation-runner.js +20 -125
- package/src/lib/validators/verify-runner.js +152 -575
- package/src/lib/work-type-detector.js +5 -67
- package/src/lib/worktree/docker-audit.js +17 -137
- package/src/lib/worktree/env-allocator.js +10 -98
- package/src/lib/worktree/feature-truth.js +12 -115
- package/src/lib/worktree/finish-backup.js +32 -242
- package/src/lib/worktree/fleet-scanner.js +31 -126
- package/src/lib/worktree/ignored-triage.js +15 -91
- package/src/lib/worktree/vscode-settings.js +2 -3
- package/src/lib/worktree/worktree-audit.js +21 -213
- package/src/lib/worktree/worktree-include.js +3 -5
- package/src/scripts/global-install.js +3 -9
- package/src/scripts/install-plugin.js +1 -13
- package/src/scripts/setup-infra.js +17 -138
- package/src/utils/agents-installer.js +92 -272
- package/src/utils/banner.js +4 -25
- package/src/utils/claude-md-injector.js +1 -37
- package/src/utils/claude-settings-manager.js +1 -87
- package/src/utils/file-copier.js +32 -90
- package/src/utils/hooks-installer.js +26 -296
- package/src/utils/skills-installer.js +4 -102
- package/src/utils/version-checker.js +4 -83
- package/framework/commands/morph-proposal.md +0 -57
- package/framework/evals/trace-schema.md +0 -67
- package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
- package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
- package/framework/hooks/git/pre-commit/specs.sh +0 -50
- package/framework/hooks/shared/tasks-validator.js +0 -111
- package/framework/rules/morph-task-tracking.md +0 -54
- package/framework/rules/morph-workflow.md +0 -244
- package/framework/schemas/tasks.schema.json +0 -79
- package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
- package/framework/skills/core/morph-implement/SKILL.md +0 -194
- package/framework/skills/core/morph-proposal/SKILL.md +0 -152
- package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
- package/framework/templates/docs/proposal.md +0 -192
- package/framework/templates/docs/spec.md +0 -57
- package/framework/templates/docs/ui-components.md +0 -52
- package/framework/templates/docs/ui-design-system.md +0 -76
- package/framework/templates/docs/ui-flows.md +0 -167
- package/framework/templates/examples/spec-examples.md +0 -91
- package/framework/templates/feature/decisions.md +0 -124
- package/framework/templates/feature/recap.md +0 -145
- package/framework/templates/tasks.template.json +0 -17
- package/src/commands/state/usecase.js +0 -143
- package/src/commands/tasks/dag.js +0 -181
- package/src/commands/utils/index.js +0 -3
- package/src/lib/agents/output-collector.js +0 -76
- package/src/lib/detectors/design-system-detector.js +0 -188
- package/src/lib/evaluator-report.js +0 -104
- package/src/lib/generators/design-system-generator.js +0 -508
- package/src/lib/specs/acervo-format.js +0 -70
- package/src/lib/specs/acervo-lifecycle.js +0 -264
- package/src/lib/specs/done-criteria-check.js +0 -200
- package/src/lib/specs/usecase-check.js +0 -301
- package/src/lib/specs/usecase-promoter.js +0 -301
- package/src/lib/specs/usecase-registry.js +0 -237
- package/src/lib/tasks/dispatch-planner.js +0 -319
- package/src/lib/tasks/dotnet-test-introspection.js +0 -632
- package/src/lib/tasks/mermaid-renderer.js +0 -132
- package/src/lib/tasks/task-classifier.js +0 -30
- package/src/lib/tasks/task-parser.js +0 -209
- package/src/lib/tasks/tasks-json-parser.js +0 -296
- package/src/lib/tasks/test-scope.js +0 -597
- package/src/utils/agent-id-mapper.js +0 -11
|
@@ -1,18 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
* orchestrator and sub-agent, time per command class, identical reruns, file
|
|
4
|
-
* rereads, feature-scope verify count, git calls per task and whether the
|
|
5
|
-
* reuse validator ran. Computed the same way on every window so a later
|
|
6
|
-
* window compares with an earlier one.
|
|
7
|
-
*
|
|
8
|
-
* Every number degrades to null ("not measurable here") when the stream lacks
|
|
9
|
-
* the field it needs — a v1 event never counts as a zero.
|
|
10
|
-
*
|
|
11
|
-
* A rerun is the same producer command (what runs before the output filter:
|
|
12
|
-
* `dotnet test | tail -25` then `| tail -20` is one test run twice) in the same
|
|
13
|
-
* feature within 10 minutes of its previous run. `rate` counts the repeats;
|
|
14
|
-
* `burstRate` counts every run that belongs to a burst, the first included.
|
|
15
|
-
*/
|
|
1
|
+
// Every number degrades to null when the stream lacks its field: a v1 event never counts as a zero.
|
|
2
|
+
// A rerun is the same producer command (before the output filter) in the same feature within 10 minutes.
|
|
16
3
|
|
|
17
4
|
import { ingestedBytes } from './telemetry-reader.js';
|
|
18
5
|
import { producerOf } from '../../../framework/hooks/shared/command-tokens.js';
|
|
@@ -25,26 +12,16 @@ const READ_ONLY = new Set(['grep', 'rg', 'sed', 'ls', 'cat', 'find', 'head', 'ta
|
|
|
25
12
|
const TOOL_EVENTS = new Set(['PostToolUse', 'PostToolUseFailure']);
|
|
26
13
|
const NO_FEATURE = '\u0000';
|
|
27
14
|
|
|
28
|
-
|
|
29
|
-
* The command without quoted text and heredoc bodies: `morph-spec verify`
|
|
30
|
-
* inside a commit message is not a verify run.
|
|
31
|
-
*/
|
|
15
|
+
// `morph-spec verify` inside a commit message is not a verify run.
|
|
32
16
|
function executedText(command) {
|
|
33
17
|
const beforeHeredoc = command.split('<<', 1)[0];
|
|
34
18
|
return beforeHeredoc.replace(/"(?:[^"\\]|\\.)*"|'[^']*'/g, (q) => q[0] + ' '.repeat(q.length - 2) + q[0]);
|
|
35
19
|
}
|
|
36
20
|
|
|
37
|
-
/**
|
|
38
|
-
* Kind (fine) and class (coarse) of a shell command. Searched anywhere in the
|
|
39
|
-
* command, most specific first, so `cd x && dotnet test` is a test.
|
|
40
|
-
*
|
|
41
|
-
* @param {string} command
|
|
42
|
-
* @returns {{ kind: string, klass: string }}
|
|
43
|
-
*/
|
|
21
|
+
/** Searched anywhere in the command, most specific first, so `cd x && dotnet test` is a test. */
|
|
44
22
|
export function classifyCommand(command) {
|
|
45
23
|
const cmd = String(command || '');
|
|
46
|
-
|
|
47
|
-
if (verify) return { kind: verify[1] ? 'verify-task' : 'verify-feature', klass: 'verify' };
|
|
24
|
+
if (/\bmorph-spec(?:\.js)?\s+verify\s+[^\s;&|)]+/.test(executedText(cmd))) return { kind: 'verify', klass: 'verify' };
|
|
48
25
|
if (/\bdotnet\s+test\b/.test(cmd)) {
|
|
49
26
|
if (/--list-tests\b/.test(cmd)) return { kind: 'dotnet-test-list', klass: 'test' };
|
|
50
27
|
return { kind: /--filter\b/.test(cmd) ? 'dotnet-test-filtered' : 'dotnet-test-full', klass: 'test' };
|
|
@@ -121,11 +98,8 @@ function finishCounters(c, turnsObserved) {
|
|
|
121
98
|
};
|
|
122
99
|
}
|
|
123
100
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
* output filter need no exit code; the pass/fail split does, so a masked or v1
|
|
127
|
-
* exit code falls back to "after an edit or not".
|
|
128
|
-
*/
|
|
101
|
+
// Edits in between and a changed output filter need no exit code; the pass/fail split does, so a masked
|
|
102
|
+
// or v1 exit code falls back to "after an edit or not".
|
|
129
103
|
function rerunClass(prev, current, editsBetween) {
|
|
130
104
|
if (isV2(prev) && prev.outcome === 'timeout') return 'timeout';
|
|
131
105
|
if (editsBetween === 0 && prev.command !== current.command) return 'refilter';
|
|
@@ -148,19 +122,13 @@ function reuseStateOf(e) {
|
|
|
148
122
|
return { state: 'absent', reason: null };
|
|
149
123
|
}
|
|
150
124
|
|
|
151
|
-
/**
|
|
152
|
-
* @param {Object[]} events - deduplicated, time-ordered (loadTelemetry)
|
|
153
|
-
* @param {{ feature?: string, top?: number }} [opts]
|
|
154
|
-
* @returns {Object} see framework/docs/CLI.md, "cost --metrics"
|
|
155
|
-
*/
|
|
125
|
+
/** The returned shape is documented in framework/docs/CLI.md, "cost --metrics". */
|
|
156
126
|
export function computeMetrics(events, opts = {}) {
|
|
157
127
|
const top = Number.isInteger(opts.top) && opts.top > 0 ? opts.top : 10;
|
|
158
128
|
const scoped = (events || []).filter((e) => e && typeof e === 'object' && (!opts.feature || e.feature === opts.feature));
|
|
159
129
|
|
|
160
130
|
const global = newCounters();
|
|
161
131
|
const byFeature = new Map();
|
|
162
|
-
const openTask = new Map();
|
|
163
|
-
const tasks = [];
|
|
164
132
|
const lastRun = new Map();
|
|
165
133
|
const editCount = new Map();
|
|
166
134
|
const readsSeen = new Set();
|
|
@@ -172,8 +140,6 @@ export function computeMetrics(events, opts = {}) {
|
|
|
172
140
|
const lastFingerprint = new Map();
|
|
173
141
|
const unchangedRepeats = {};
|
|
174
142
|
const reuse = { runs: 0, byTree: {}, skipReasons: {} };
|
|
175
|
-
const tasksOf = new Map();
|
|
176
|
-
let callsInTaskWindows = 0;
|
|
177
143
|
let v2 = 0;
|
|
178
144
|
let legacy = 0;
|
|
179
145
|
let turnsObserved = false;
|
|
@@ -184,16 +150,6 @@ export function computeMetrics(events, opts = {}) {
|
|
|
184
150
|
if (!byFeature.has(key)) byFeature.set(key, newCounters());
|
|
185
151
|
return byFeature.get(key);
|
|
186
152
|
};
|
|
187
|
-
const closeTask = (key, endTs, status) => {
|
|
188
|
-
const task = openTask.get(key);
|
|
189
|
-
if (!task) return;
|
|
190
|
-
openTask.delete(key);
|
|
191
|
-
task.endTs = endTs;
|
|
192
|
-
task.status = status;
|
|
193
|
-
task.open = endTs === null;
|
|
194
|
-
tasks.push(task);
|
|
195
|
-
};
|
|
196
|
-
|
|
197
153
|
for (const e of scoped) {
|
|
198
154
|
if (isV2(e)) v2 += 1; else legacy += 1;
|
|
199
155
|
if (e.ts) {
|
|
@@ -202,21 +158,7 @@ export function computeMetrics(events, opts = {}) {
|
|
|
202
158
|
}
|
|
203
159
|
const fkey = e.feature || NO_FEATURE;
|
|
204
160
|
|
|
205
|
-
if (e.event === 'TaskStart' && e.task) {
|
|
206
|
-
if (!tasksOf.has(fkey)) tasksOf.set(fkey, new Set());
|
|
207
|
-
tasksOf.get(fkey).add(e.task);
|
|
208
|
-
closeTask(fkey, null, null);
|
|
209
|
-
openTask.set(fkey, { feature: e.feature ?? null, task: e.task, startTs: e.ts, endTs: null, status: null, open: true, c: newCounters() });
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (e.event === 'TaskEnd' && e.task) {
|
|
213
|
-
if (openTask.get(fkey)?.task === e.task) closeTask(fkey, e.ts, e.status ?? null);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
161
|
const sinks = [global, featureCounters(fkey)];
|
|
218
|
-
const task = openTask.get(fkey);
|
|
219
|
-
if (task) sinks.push(task.c);
|
|
220
162
|
|
|
221
163
|
if (e.event === 'ToolBatch') {
|
|
222
164
|
turnsObserved = true;
|
|
@@ -225,7 +167,7 @@ export function computeMetrics(events, opts = {}) {
|
|
|
225
167
|
}
|
|
226
168
|
|
|
227
169
|
if (e.event === 'Verify') {
|
|
228
|
-
if (e.
|
|
170
|
+
if (e.fingerprint && e.persisted !== false) {
|
|
229
171
|
if (lastFingerprint.get(fkey) === e.fingerprint) {
|
|
230
172
|
unchangedRepeats[e.feature || '(sem feature)'] = (unchangedRepeats[e.feature || '(sem feature)'] || 0) + 1;
|
|
231
173
|
}
|
|
@@ -242,7 +184,6 @@ export function computeMetrics(events, opts = {}) {
|
|
|
242
184
|
|
|
243
185
|
if (!TOOL_EVENTS.has(e.event) || !e.tool) continue;
|
|
244
186
|
for (const c of sinks) { c.toolCalls.total += 1; c.toolCalls[whoOf(e)] += 1; }
|
|
245
|
-
if (task) callsInTaskWindows += 1;
|
|
246
187
|
|
|
247
188
|
if (e.tool === 'Write' || e.tool === 'Edit') {
|
|
248
189
|
if (!isFailedToolCall(e)) editCount.set(fkey, (editCount.get(fkey) || 0) + 1);
|
|
@@ -288,8 +229,8 @@ export function computeMetrics(events, opts = {}) {
|
|
|
288
229
|
}
|
|
289
230
|
|
|
290
231
|
if (klass === 'verify') {
|
|
291
|
-
const
|
|
292
|
-
|
|
232
|
+
const name = e.feature || '(sem feature)';
|
|
233
|
+
verifyByFeature[name] = (verifyByFeature[name] || 0) + 1;
|
|
293
234
|
}
|
|
294
235
|
|
|
295
236
|
if (!RERUN_CLASSES.has(klass) || typeof e.command !== 'string') continue;
|
|
@@ -318,34 +259,15 @@ export function computeMetrics(events, opts = {}) {
|
|
|
318
259
|
lastRun.set(runKey, run);
|
|
319
260
|
}
|
|
320
261
|
|
|
321
|
-
for (const key of [...openTask.keys()]) closeTask(key, null, null);
|
|
322
|
-
tasks.sort((a, b) => String(a.startTs).localeCompare(String(b.startTs)));
|
|
323
|
-
|
|
324
|
-
const taskRows = tasks.map((t) => ({
|
|
325
|
-
feature: t.feature,
|
|
326
|
-
task: t.task,
|
|
327
|
-
startTs: t.startTs,
|
|
328
|
-
endTs: t.endTs,
|
|
329
|
-
open: t.open,
|
|
330
|
-
status: t.status,
|
|
331
|
-
...finishCounters(t.c, turnsObserved),
|
|
332
|
-
}));
|
|
333
|
-
|
|
334
262
|
const featureRows = [...byFeature.entries()]
|
|
335
263
|
.filter(([key]) => key !== NO_FEATURE)
|
|
336
264
|
.map(([feature, c]) => ({
|
|
337
265
|
feature,
|
|
338
266
|
...finishCounters(c, turnsObserved),
|
|
339
|
-
verify: verifyByFeature[feature] ||
|
|
267
|
+
verify: verifyByFeature[feature] || 0,
|
|
340
268
|
}))
|
|
341
269
|
.sort((a, b) => b.toolCalls.total - a.toolCalls.total || a.feature.localeCompare(b.feature));
|
|
342
270
|
|
|
343
|
-
// A task window only holds what ran between its TaskStart and TaskEnd, and
|
|
344
|
-
// a parallel batch or a late `in_progress` leaves most calls outside any
|
|
345
|
-
// window. Dividing each feature's totals by its task count needs no window.
|
|
346
|
-
const perTaskOfFeature = (pick) => median(featureRows
|
|
347
|
-
.filter((f) => tasksOf.has(f.feature))
|
|
348
|
-
.map((f) => Math.round((pick(f) / tasksOf.get(f.feature).size) * 10) / 10));
|
|
349
271
|
const byKind = {};
|
|
350
272
|
for (const [kind, slot] of Object.entries(rerunByKind)) {
|
|
351
273
|
byKind[kind] = { ...slot, rate: rate(slot.reruns, slot.runs), burstRate: rate(slot.inBursts, slot.runs) };
|
|
@@ -364,23 +286,11 @@ export function computeMetrics(events, opts = {}) {
|
|
|
364
286
|
.slice(0, top),
|
|
365
287
|
verify: {
|
|
366
288
|
byFeature: verifyByFeature,
|
|
367
|
-
|
|
289
|
+
median: median(featureRows.map((f) => f.verify).filter((n) => n > 0)),
|
|
368
290
|
unchangedRepeats,
|
|
369
291
|
},
|
|
370
|
-
gitPerTaskMedian: {
|
|
371
|
-
status: median(taskRows.map((t) => t.git.status)),
|
|
372
|
-
diff: median(taskRows.map((t) => t.git.diff)),
|
|
373
|
-
},
|
|
374
|
-
turnsPerTaskMedian: turnsObserved ? median(taskRows.map((t) => t.turns.total)) : null,
|
|
375
|
-
taskWindowCoverage: rate(callsInTaskWindows, global.toolCalls.total),
|
|
376
|
-
perTaskByFeature: {
|
|
377
|
-
git: perTaskOfFeature((f) => f.git.status + f.git.diff),
|
|
378
|
-
toolCalls: perTaskOfFeature((f) => f.toolCalls.total),
|
|
379
|
-
turns: turnsObserved ? perTaskOfFeature((f) => f.turns.total) : null,
|
|
380
|
-
},
|
|
381
292
|
dotnetFullSuite: fullSuite,
|
|
382
293
|
reuse,
|
|
383
294
|
features: featureRows,
|
|
384
|
-
tasks: taskRows,
|
|
385
295
|
};
|
|
386
296
|
}
|
|
@@ -1,14 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Telemetry aggregation — reads the append-only harness event stream
|
|
3
|
-
* (.morph/logs/events.jsonl, written by the telemetry-log hook) and summarizes
|
|
4
|
-
* the whole execution path: which skills ran, which personas were dispatched,
|
|
5
|
-
* which standards (knowledge base) were read, the tool mix, and the prompts.
|
|
6
|
-
*
|
|
7
|
-
* Pure aggregation (`aggregateTelemetry`) is separated from disk I/O
|
|
8
|
-
* (`loadTelemetry`) so the summary logic is unit-testable without fixtures —
|
|
9
|
-
* the same split fleet-scanner uses. CLI-side; runs in the target project cwd.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
1
|
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
13
2
|
import { join, basename } from 'path';
|
|
14
3
|
import { parseWorktreeList, resolvePrimaryRoot } from '../../commands/project/worktree.js';
|
|
@@ -21,11 +10,8 @@ const EVENTS_LOG = /^events(-.+)?\.jsonl$/;
|
|
|
21
10
|
// Caches a finish backup may hold; none of them contains a log.
|
|
22
11
|
const BACKUP_SKIP_DIRS = new Set(['node_modules', '.next', 'bin', 'obj', 'TestResults', 'graphify-out', '.git']);
|
|
23
12
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* and every finish backup that copied it. v2 events carry an `eventId`; a v1
|
|
27
|
-
* event is identical only to a byte-identical copy of itself.
|
|
28
|
-
*/
|
|
13
|
+
// The same line arrives through the live log, its rotated archives and every finish backup. v2 events
|
|
14
|
+
// carry an `eventId`; a v1 event is identical only to a byte-identical copy of itself.
|
|
29
15
|
function dedupeKey(event) {
|
|
30
16
|
return event.eventId ? `id:${event.eventId}` : `raw:${JSON.stringify(event)}`;
|
|
31
17
|
}
|
|
@@ -35,13 +21,7 @@ function eventTime(event) {
|
|
|
35
21
|
return Number.isNaN(t) ? 0 : t;
|
|
36
22
|
}
|
|
37
23
|
|
|
38
|
-
/**
|
|
39
|
-
* Collapse copies of the same event and restore time order across sources.
|
|
40
|
-
* The sort is stable, so events sharing a timestamp keep their write order.
|
|
41
|
-
*
|
|
42
|
-
* @param {Object[]} events
|
|
43
|
-
* @returns {Object[]}
|
|
44
|
-
*/
|
|
24
|
+
/** Stable sort, so events sharing a timestamp keep their write order. */
|
|
45
25
|
export function dedupeEvents(events) {
|
|
46
26
|
const seen = new Set();
|
|
47
27
|
const out = [];
|
|
@@ -61,9 +41,9 @@ function readJsonlFile(path, out) {
|
|
|
61
41
|
for (const line of readFileSync(path, 'utf-8').split('\n')) {
|
|
62
42
|
const trimmed = line.trim();
|
|
63
43
|
if (!trimmed) continue;
|
|
64
|
-
try { out.push(JSON.parse(trimmed)); } catch {
|
|
44
|
+
try { out.push(JSON.parse(trimmed)); } catch {}
|
|
65
45
|
}
|
|
66
|
-
} catch {
|
|
46
|
+
} catch {}
|
|
67
47
|
}
|
|
68
48
|
|
|
69
49
|
function archiveFiles(sessionsDir) {
|
|
@@ -77,15 +57,7 @@ function archiveFiles(sessionsDir) {
|
|
|
77
57
|
}
|
|
78
58
|
}
|
|
79
59
|
|
|
80
|
-
/**
|
|
81
|
-
* Read the event stream of a project root: the live log plus, by default, the
|
|
82
|
-
* archives SessionStart rotated into `.morph/logs/sessions/` — reading only the
|
|
83
|
-
* live file loses everything older than the last 5 MB.
|
|
84
|
-
*
|
|
85
|
-
* @param {string} [projectPath]
|
|
86
|
-
* @param {{ archives?: boolean }} [options]
|
|
87
|
-
* @returns {Object[]} deduplicated, in time order; [] when there is no log
|
|
88
|
-
*/
|
|
60
|
+
/** Live log plus, by default, the rotated archives: the live file alone loses everything past 5 MB. */
|
|
89
61
|
export function loadTelemetry(projectPath, { archives = true } = {}) {
|
|
90
62
|
const root = projectPath || process.cwd();
|
|
91
63
|
const events = [];
|
|
@@ -108,14 +80,7 @@ function collectBackupLogs(dir, depth, out) {
|
|
|
108
80
|
}
|
|
109
81
|
}
|
|
110
82
|
|
|
111
|
-
/**
|
|
112
|
-
* Event logs that `finish` saved under `<git-common-dir>/morph-spec/finish-backup/`
|
|
113
|
-
* before tearing a worktree down — once the worktree is gone, a feature built
|
|
114
|
-
* there leaves its telemetry nowhere else.
|
|
115
|
-
*
|
|
116
|
-
* @param {string} [cwd]
|
|
117
|
-
* @returns {{ path: string, events: Object[] }|null}
|
|
118
|
-
*/
|
|
83
|
+
/** Logs `finish` saved before tearing a worktree down: once it is gone, they are the feature's only telemetry. */
|
|
119
84
|
export function loadFinishBackupTelemetry(cwd = process.cwd()) {
|
|
120
85
|
const common = resolveGitCommonDir(cwd);
|
|
121
86
|
if (!common) return null;
|
|
@@ -128,19 +93,7 @@ export function loadFinishBackupTelemetry(cwd = process.cwd()) {
|
|
|
128
93
|
return { path: base, events: dedupeEvents(events) };
|
|
129
94
|
}
|
|
130
95
|
|
|
131
|
-
/**
|
|
132
|
-
* Every git worktree that has its OWN `.morph/logs/events.jsonl` — the source
|
|
133
|
-
* of `cost --all-trees` — plus a `finish-backup` entry for worktrees already
|
|
134
|
-
* torn down. Reuses the worktree enumeration of `doctor --worktrees`/`fleet`.
|
|
135
|
-
*
|
|
136
|
-
* The PRIMARY worktree is labeled `'root'`; every other entry by its directory
|
|
137
|
-
* basename (typically the feature slug). A tree that never ran the harness
|
|
138
|
-
* contributes nothing. Outside a git repo this degrades to a single root
|
|
139
|
-
* entry — never throws.
|
|
140
|
-
*
|
|
141
|
-
* @param {string} [cwd]
|
|
142
|
-
* @returns {Array<{ tree: string, path: string, events: Object[] }>}
|
|
143
|
-
*/
|
|
96
|
+
/** Labels the primary worktree `'root'`, the others by directory basename. Outside git, a single root entry. */
|
|
144
97
|
export function loadTelemetryAllTrees(cwd = process.cwd()) {
|
|
145
98
|
const root = resolvePrimaryRoot(cwd);
|
|
146
99
|
const entries = parseWorktreeList(root);
|
|
@@ -167,21 +120,6 @@ function rank(map) {
|
|
|
167
120
|
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
168
121
|
}
|
|
169
122
|
|
|
170
|
-
/**
|
|
171
|
-
* Summarize an event stream.
|
|
172
|
-
*
|
|
173
|
-
* @param {Object[]} events - raw events (from loadTelemetry)
|
|
174
|
-
* @param {{ feature?: string }} [opts] - optional feature filter
|
|
175
|
-
* @returns {{
|
|
176
|
-
* total: number,
|
|
177
|
-
* skills: Array<{name:string,count:number}>,
|
|
178
|
-
* personas: Array<{name:string,count:number}>,
|
|
179
|
-
* standards: Array<{path:string,count:number}>,
|
|
180
|
-
* tools: Array<{name:string,count:number}>,
|
|
181
|
-
* prompts: Array<{ts:string,source:'user'|'agent',feature:?string,phase:?string,text:string}>,
|
|
182
|
-
* features: string[]
|
|
183
|
-
* }}
|
|
184
|
-
*/
|
|
185
123
|
export function aggregateTelemetry(events, opts = {}) {
|
|
186
124
|
const scoped = opts.feature
|
|
187
125
|
? (events || []).filter(e => e.feature === opts.feature)
|
|
@@ -226,51 +164,8 @@ export function aggregateTelemetry(events, opts = {}) {
|
|
|
226
164
|
};
|
|
227
165
|
}
|
|
228
166
|
|
|
229
|
-
//
|
|
230
|
-
//
|
|
231
|
-
// folded in: aggregateTelemetry's return shape is pinned by
|
|
232
|
-
// test/commands/telemetry.test.js, which spreads it wholesale into --json
|
|
233
|
-
// output — adding fields there would leak into that contract).
|
|
234
|
-
//
|
|
235
|
-
// Turns the same raw event stream into a cost-facing view: how much
|
|
236
|
-
// wall-clock time was spent inside measured tasks, how many context bytes
|
|
237
|
-
// were re-ingested (by kind), and how many sub-agent dispatches happened
|
|
238
|
-
// (and how many prompt chars they cost).
|
|
239
|
-
//
|
|
240
|
-
// "Bytes re-ingested" is a DETERMINISTIC PROXY for cost, not a token count —
|
|
241
|
-
// hooks can only see what tools read off disk, never the model's actual
|
|
242
|
-
// token usage. The `cost` command surfaces that caveat; this module only
|
|
243
|
-
// computes the numbers.
|
|
244
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
245
|
-
|
|
246
|
-
// Sentinel used to key the open-task map when an event carries no feature
|
|
247
|
-
// (e.g. telemetry-log's getActiveFeature() found none). Without this, a
|
|
248
|
-
// TaskStart with feature: null would silently fail to open (map keyed on
|
|
249
|
-
// undefined/null never matches itself across two different `null`-feature
|
|
250
|
-
// pairs is fine, but `if (e.feature)` truthiness checks would just drop the
|
|
251
|
-
// event) — keying explicitly avoids that class of "counter never increments"
|
|
252
|
-
// bug.
|
|
253
|
-
const NO_FEATURE = '\u0000no-feature';
|
|
254
|
-
|
|
255
|
-
function featureKey(e) {
|
|
256
|
-
return e && e.feature ? e.feature : NO_FEATURE;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function tsToMs(ts) {
|
|
260
|
-
const t = Date.parse(ts);
|
|
261
|
-
return Number.isNaN(t) ? null : t;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
/**
|
|
265
|
-
* Seconds between two ISO timestamps, rounded to millisecond precision to
|
|
266
|
-
* absorb float noise from the division. null if either ts is unparsable.
|
|
267
|
-
*/
|
|
268
|
-
function durationSeconds(startTs, endTs) {
|
|
269
|
-
const a = tsToMs(startTs);
|
|
270
|
-
const b = tsToMs(endTs);
|
|
271
|
-
if (a === null || b === null) return null;
|
|
272
|
-
return Math.round(((b - a) / 1000) * 1000) / 1000;
|
|
273
|
-
}
|
|
167
|
+
// Kept apart from aggregateTelemetry: its return shape is spread into `telemetry --json`, so new fields
|
|
168
|
+
// there would leak into that contract. Bytes re-ingested is a proxy for cost, never a token count.
|
|
274
169
|
|
|
275
170
|
function mapToObj(map) {
|
|
276
171
|
const out = {};
|
|
@@ -284,90 +179,8 @@ function sumValues(map) {
|
|
|
284
179
|
return total;
|
|
285
180
|
}
|
|
286
181
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
feature: feature ?? null,
|
|
290
|
-
task,
|
|
291
|
-
startTs: ts,
|
|
292
|
-
endTs: null,
|
|
293
|
-
status: null,
|
|
294
|
-
open: true,
|
|
295
|
-
tools: new Map(),
|
|
296
|
-
bytesByKind: new Map(),
|
|
297
|
-
dispatches: { count: 0, promptChars: 0 },
|
|
298
|
-
};
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function finalizeTask(state) {
|
|
302
|
-
return {
|
|
303
|
-
feature: state.feature,
|
|
304
|
-
task: state.task,
|
|
305
|
-
startTs: state.startTs,
|
|
306
|
-
endTs: state.endTs,
|
|
307
|
-
durationSeconds: state.endTs ? durationSeconds(state.startTs, state.endTs) : null,
|
|
308
|
-
status: state.status,
|
|
309
|
-
open: state.open,
|
|
310
|
-
tools: rank(state.tools),
|
|
311
|
-
bytesByKind: mapToObj(state.bytesByKind),
|
|
312
|
-
bytesTotal: sumValues(state.bytesByKind),
|
|
313
|
-
dispatches: { ...state.dispatches },
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Summarize an event stream for cost purposes: task segmentation (duration +
|
|
319
|
-
* per-task tool/byte/dispatch aggregates), bytes-re-ingested buckets by
|
|
320
|
-
* `kind` (global, per task, per phase), and sub-agent dispatch counts.
|
|
321
|
-
*
|
|
322
|
-
* Segmentation model: a task is "open" for a feature from its TaskStart
|
|
323
|
-
* event until the matching TaskEnd (same feature + task name). Only one task
|
|
324
|
-
* is assumed open per feature at a time — matches how the implement phase
|
|
325
|
-
* actually runs (auto-advance processes tasks sequentially, never two tasks
|
|
326
|
-
* of the same feature concurrently). Events for a feature with no open task
|
|
327
|
-
* simply don't attribute to any task (they still count toward the
|
|
328
|
-
* global/phase totals). A TaskEnd with no matching open TaskStart is ignored
|
|
329
|
-
* — a truncated stream or a legacy event mix never throws. A task left open
|
|
330
|
-
* at the end of the stream (TaskStart with no TaskEnd yet) is still reported,
|
|
331
|
-
* with `open: true` and `durationSeconds: null`, rather than silently
|
|
332
|
-
* dropped.
|
|
333
|
-
*
|
|
334
|
-
* Byte attribution prefers `bytesRead` (the line window a Read put in context)
|
|
335
|
-
* and falls back to `bytes` (the file size — all v1 events have) — keyed off
|
|
336
|
-
* the number alone, not the tool.
|
|
337
|
-
*
|
|
338
|
-
* Dispatch prompt size (Furo 3 — contract with the hooks-side telemetry-log
|
|
339
|
-
* rewrite): a dispatch event's `prompt` field is now TRUNCATED to 2k chars on
|
|
340
|
-
* write, with the original length carried separately as `promptChars`. When
|
|
341
|
-
* `promptChars` (a number) is present it is preferred; a legacy stream that
|
|
342
|
-
* only ever had the untruncated `prompt` string falls back to
|
|
343
|
-
* `prompt.length`, unchanged from before — every existing byte count stays
|
|
344
|
-
* exact against old logs.
|
|
345
|
-
*
|
|
346
|
-
* SessionUsage (main session) and AgentUsage (each sub-agent) are real token
|
|
347
|
-
* counts, not the bytes proxy, and never touch toolCounts/bytesByKind/
|
|
348
|
-
* dispatches. Only their `delta` is summed — the top-level numbers are a
|
|
349
|
-
* running total repeated on every sample — so a v1 sample (no `delta`) is
|
|
350
|
-
* counted in `sessionUsageLegacySamples` and never added in. Like any event
|
|
351
|
-
* they obey the `--feature` filter. `sessionUsage`/`agentUsage` are `null`
|
|
352
|
-
* when nothing summable was observed, never a zeroed object.
|
|
353
|
-
*
|
|
354
|
-
* @param {Object[]} events - raw events (from loadTelemetry)
|
|
355
|
-
* @param {{ feature?: string }} [opts] - optional feature filter
|
|
356
|
-
* @returns {{
|
|
357
|
-
* total: number,
|
|
358
|
-
* features: string[],
|
|
359
|
-
* taskDurationSeconds: number,
|
|
360
|
-
* bytesByKind: Object<string, number>,
|
|
361
|
-
* bytesTotal: number,
|
|
362
|
-
* dispatches: {count:number, promptChars:number},
|
|
363
|
-
* sessionUsage: {inputTokens:number, outputTokens:number, cacheReadTokens:number, cacheCreationTokens:number, sessions:number, samples:number}|null,
|
|
364
|
-
* sessionUsageLegacySamples: number,
|
|
365
|
-
* agentUsage: {inputTokens:number, outputTokens:number, cacheReadTokens:number, cacheCreationTokens:number, agents:number, samples:number}|null,
|
|
366
|
-
* tools: Array<{name:string,count:number}>,
|
|
367
|
-
* tasks: Array<Object>,
|
|
368
|
-
* phases: Array<{phase:string, bytesByKind:Object<string,number>, bytesTotal:number}>,
|
|
369
|
-
* }}
|
|
370
|
-
*/
|
|
182
|
+
// Prefers `promptChars` over `prompt.length`: the prompt is truncated on write. Session and agent usage sum
|
|
183
|
+
// only their `delta`, never the running total; they are `null` when nothing summable was seen.
|
|
371
184
|
const SESSION_USAGE_KEYS = ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheCreationTokens'];
|
|
372
185
|
|
|
373
186
|
/** Bytes a Read put in context: its window when known, else the file size. */
|
|
@@ -387,9 +200,7 @@ export function aggregateCost(events, opts = {}) {
|
|
|
387
200
|
const toolCounts = new Map();
|
|
388
201
|
const bytesByKindGlobal = new Map();
|
|
389
202
|
const dispatches = { count: 0, promptChars: 0 };
|
|
390
|
-
const phaseBytes = new Map();
|
|
391
|
-
const activeByFeature = new Map(); // featureKey -> open task state
|
|
392
|
-
const tasks = [];
|
|
203
|
+
const phaseBytes = new Map();
|
|
393
204
|
const newUsageBucket = () => ({
|
|
394
205
|
totals: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 },
|
|
395
206
|
ids: new Set(),
|
|
@@ -402,24 +213,6 @@ export function aggregateCost(events, opts = {}) {
|
|
|
402
213
|
if (!e || typeof e !== 'object') continue;
|
|
403
214
|
if (e.feature) features.add(e.feature);
|
|
404
215
|
|
|
405
|
-
if (e.event === 'TaskStart' && e.task) {
|
|
406
|
-
activeByFeature.set(featureKey(e), newTaskState(e.feature, e.task, e.ts));
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
if (e.event === 'TaskEnd' && e.task) {
|
|
411
|
-
const key = featureKey(e);
|
|
412
|
-
const state = activeByFeature.get(key);
|
|
413
|
-
if (state && state.task === e.task) {
|
|
414
|
-
state.endTs = e.ts;
|
|
415
|
-
state.status = e.status ?? null;
|
|
416
|
-
state.open = false;
|
|
417
|
-
activeByFeature.delete(key);
|
|
418
|
-
tasks.push(finalizeTask(state));
|
|
419
|
-
}
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
216
|
if (e.event === 'SessionUsage' || e.event === 'AgentUsage') {
|
|
424
217
|
const bucket = e.event === 'SessionUsage' ? usage.session : usage.agent;
|
|
425
218
|
// Only a delta can be summed: the top-level numbers are the transcript's
|
|
@@ -437,16 +230,11 @@ export function aggregateCost(events, opts = {}) {
|
|
|
437
230
|
}
|
|
438
231
|
|
|
439
232
|
if (e.tool) toolCounts.set(e.tool, (toolCounts.get(e.tool) || 0) + 1);
|
|
440
|
-
const active = activeByFeature.get(featureKey(e));
|
|
441
|
-
if (active && e.tool) {
|
|
442
|
-
active.tools.set(e.tool, (active.tools.get(e.tool) || 0) + 1);
|
|
443
|
-
}
|
|
444
233
|
|
|
445
234
|
const read = ingestedBytes(e);
|
|
446
235
|
if (read !== null) {
|
|
447
236
|
const kind = e.kind || 'unknown';
|
|
448
237
|
bytesByKindGlobal.set(kind, (bytesByKindGlobal.get(kind) || 0) + read);
|
|
449
|
-
if (active) active.bytesByKind.set(kind, (active.bytesByKind.get(kind) || 0) + read);
|
|
450
238
|
if (e.phase) {
|
|
451
239
|
if (!phaseBytes.has(e.phase)) phaseBytes.set(e.phase, new Map());
|
|
452
240
|
const m = phaseBytes.get(e.phase);
|
|
@@ -459,18 +247,9 @@ export function aggregateCost(events, opts = {}) {
|
|
|
459
247
|
const chars = hasPromptChars ? e.promptChars : e.prompt.length;
|
|
460
248
|
dispatches.count += 1;
|
|
461
249
|
dispatches.promptChars += chars;
|
|
462
|
-
if (active) {
|
|
463
|
-
active.dispatches.count += 1;
|
|
464
|
-
active.dispatches.promptChars += chars;
|
|
465
|
-
}
|
|
466
250
|
}
|
|
467
251
|
}
|
|
468
252
|
|
|
469
|
-
// Tasks still open when the stream ends (no TaskEnd observed yet) are
|
|
470
|
-
// reported, not dropped — see the "open" contract in the docstring above.
|
|
471
|
-
for (const state of activeByFeature.values()) tasks.push(finalizeTask(state));
|
|
472
|
-
tasks.sort((a, b) => (a.startTs || '').localeCompare(b.startTs || ''));
|
|
473
|
-
|
|
474
253
|
const phases = [...phaseBytes.entries()]
|
|
475
254
|
.map(([phase, kindMap]) => ({
|
|
476
255
|
phase,
|
|
@@ -479,12 +258,9 @@ export function aggregateCost(events, opts = {}) {
|
|
|
479
258
|
}))
|
|
480
259
|
.sort((a, b) => a.phase.localeCompare(b.phase));
|
|
481
260
|
|
|
482
|
-
const taskDurationSeconds = tasks.reduce((sum, t) => sum + (t.durationSeconds || 0), 0);
|
|
483
|
-
|
|
484
261
|
return {
|
|
485
262
|
total: scoped.length,
|
|
486
263
|
features: [...features],
|
|
487
|
-
taskDurationSeconds,
|
|
488
264
|
bytesByKind: mapToObj(bytesByKindGlobal),
|
|
489
265
|
bytesTotal: sumValues(bytesByKindGlobal),
|
|
490
266
|
dispatches,
|
|
@@ -496,7 +272,6 @@ export function aggregateCost(events, opts = {}) {
|
|
|
496
272
|
? { ...usage.agent.totals, agents: usage.agent.ids.size, samples: usage.agent.samples }
|
|
497
273
|
: null,
|
|
498
274
|
tools: rank(toolCounts),
|
|
499
|
-
tasks,
|
|
500
275
|
phases,
|
|
501
276
|
};
|
|
502
277
|
}
|
|
@@ -1,22 +1,9 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `Verify` telemetry event: one line per `morph-spec verify` run with the
|
|
3
|
-
* scope, per-node status and duration, the commit it covered and whether the
|
|
4
|
-
* reuse validator could run. The Bash event of the same command says who
|
|
5
|
-
* called it and how long the whole command took; this one says what it did.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { existsSync } from 'fs';
|
|
9
2
|
import { join } from 'path';
|
|
10
3
|
import { appendTelemetryEvent, readTelemetryEvents } from '../../../framework/hooks/shared/telemetry-logger.js';
|
|
11
4
|
|
|
12
5
|
const NODE_NAMES = ['build', 'tests', 'validators', 'e2e'];
|
|
13
6
|
|
|
14
|
-
/**
|
|
15
|
-
* @param {Object} result - runVerify() output
|
|
16
|
-
* @param {{ sha: string|null, tree: 'root'|'worktree', persisted: boolean, reason: string|null,
|
|
17
|
-
* fingerprint?: string|null, source?: 'local'|'ci' }} context
|
|
18
|
-
* @returns {Object}
|
|
19
|
-
*/
|
|
20
7
|
export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerprint = null, source = 'local' }) {
|
|
21
8
|
const nodes = {};
|
|
22
9
|
for (const name of NODE_NAMES) {
|
|
@@ -32,8 +19,6 @@ export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerp
|
|
|
32
19
|
return {
|
|
33
20
|
event: 'Verify',
|
|
34
21
|
feature: result.feature,
|
|
35
|
-
task: result.task ?? null,
|
|
36
|
-
scope: result.scope,
|
|
37
22
|
status: result.status,
|
|
38
23
|
sha,
|
|
39
24
|
fingerprint,
|
|
@@ -45,35 +30,20 @@ export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerp
|
|
|
45
30
|
};
|
|
46
31
|
}
|
|
47
32
|
|
|
48
|
-
/**
|
|
49
|
-
|
|
50
|
-
* same tree, or null. A partial run (`--filter`, `--skip-*`, `--no-persist`)
|
|
51
|
-
* is not the proof a complete one replaces, so it is passed over. Only the
|
|
52
|
-
* live log is read: a repeat worth warning about is recent.
|
|
53
|
-
*
|
|
54
|
-
* @param {string} projectPath
|
|
55
|
-
* @param {{ feature: string, task: string|null, scope: string, fingerprint: string|null }} run
|
|
56
|
-
* @returns {{ ts: string, status: string, sha: string|null }|null}
|
|
57
|
-
*/
|
|
58
|
-
export function findRepeatedVerify(projectPath, { feature, task, scope, fingerprint }) {
|
|
33
|
+
/** A partial run (`--filter`, `--skip-*`, `--no-persist`) is not the proof a complete one replaces. */
|
|
34
|
+
export function findRepeatedVerify(projectPath, { feature, fingerprint }) {
|
|
59
35
|
if (!fingerprint) return null;
|
|
60
36
|
const events = readTelemetryEvents(projectPath);
|
|
61
37
|
for (let i = events.length - 1; i >= 0; i--) {
|
|
62
38
|
const e = events[i];
|
|
63
|
-
if (e.event !== 'Verify' || e.feature !== feature
|
|
39
|
+
if (e.event !== 'Verify' || e.feature !== feature) continue;
|
|
64
40
|
if (e.persisted === false) continue;
|
|
65
41
|
return e.fingerprint === fingerprint ? { ts: e.ts, status: e.status, sha: e.sha ?? null } : null;
|
|
66
42
|
}
|
|
67
43
|
return null;
|
|
68
44
|
}
|
|
69
45
|
|
|
70
|
-
|
|
71
|
-
* Appends only where a morph project already exists: this command must never
|
|
72
|
-
* be the one that creates `.morph/`.
|
|
73
|
-
*
|
|
74
|
-
* @param {string} projectPath
|
|
75
|
-
* @param {Object} event
|
|
76
|
-
*/
|
|
46
|
+
// Appends only where a morph project exists: this command must never be the one that creates `.morph/`.
|
|
77
47
|
export function recordVerifyEvent(projectPath, event) {
|
|
78
48
|
if (!existsSync(join(projectPath, '.morph'))) return;
|
|
79
49
|
appendTelemetryEvent(event, projectPath);
|