@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.
Files changed (377) hide show
  1. package/CLAUDE.md +51 -43
  2. package/README.md +39 -38
  3. package/bin/morph-spec.js +41 -113
  4. package/bin/validate.js +1 -147
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +1 -1
  7. package/docs/QUICKSTART.md +1 -1
  8. package/framework/CLAUDE.md +41 -101
  9. package/framework/MORPH.md +95 -252
  10. package/framework/agents.json +86 -21
  11. package/framework/commands/morph-apply.md +5 -28
  12. package/framework/commands/morph-archive.md +1 -1
  13. package/framework/commands/morph-hotfix.md +8 -8
  14. package/framework/commands/morph-infra.md +1 -1
  15. package/framework/commands/morph-preflight.md +2 -3
  16. package/framework/commands/morph-status.md +4 -4
  17. package/framework/docs/CLI.md +574 -286
  18. package/framework/evals/review-checklist.md +82 -41
  19. package/framework/hooks/README.md +10 -11
  20. package/framework/hooks/claude-code/core/block-destructive-worktree-removal.js +6 -49
  21. package/framework/hooks/claude-code/core/block-raw-worktree-add.js +2 -35
  22. package/framework/hooks/claude-code/core/chain.js +4 -65
  23. package/framework/hooks/claude-code/core/comment-guard.js +3 -17
  24. package/framework/hooks/claude-code/core/denylist.json +33 -3
  25. package/framework/hooks/claude-code/core/enforce-phase-writes.js +15 -63
  26. package/framework/hooks/claude-code/core/gate-guard.js +9 -78
  27. package/framework/hooks/claude-code/core/graph-refresh-kick.js +3 -24
  28. package/framework/hooks/claude-code/core/heavy-gate.js +3 -21
  29. package/framework/hooks/claude-code/core/heavy-release.js +2 -13
  30. package/framework/hooks/claude-code/core/loop-detect.js +2 -31
  31. package/framework/hooks/claude-code/core/pre-bash-denylist.js +36 -2
  32. package/framework/hooks/claude-code/core/protect-spec-files.js +25 -297
  33. package/framework/hooks/claude-code/core/state-sync.js +15 -85
  34. package/framework/hooks/claude-code/core/telemetry-log.js +15 -96
  35. package/framework/hooks/claude-code/core/validate-completion.js +37 -147
  36. package/framework/hooks/claude-code/core/warn-bash-source-write.js +23 -0
  37. package/framework/hooks/claude-code/core/worktree-context.js +15 -114
  38. package/framework/hooks/claude-code/core/worktree-create.js +6 -70
  39. package/framework/hooks/claude-code/core/worktree-env-inject.js +5 -56
  40. package/framework/hooks/claude-code/core/worktree-remove.js +5 -31
  41. package/framework/hooks/claude-code/statusline.py +55 -41
  42. package/framework/hooks/package.json +3 -0
  43. package/framework/hooks/shared/activity-logger.js +9 -89
  44. package/framework/hooks/shared/agent-reap.js +2 -17
  45. package/framework/hooks/shared/build-check.js +11 -76
  46. package/framework/hooks/shared/claim-registry.js +9 -119
  47. package/framework/hooks/shared/command-tokens.js +168 -44
  48. package/framework/hooks/shared/comment-rules.js +5 -20
  49. package/framework/hooks/shared/comment-scanner.js +11 -43
  50. package/framework/hooks/shared/comment-stripper.js +11 -85
  51. package/framework/hooks/shared/e2e-check.js +0 -0
  52. package/framework/hooks/shared/errata.js +59 -0
  53. package/framework/hooks/shared/feature-locator.js +11 -95
  54. package/framework/hooks/shared/frontend-locations.js +4 -0
  55. package/framework/hooks/shared/gate-question.js +12 -0
  56. package/framework/hooks/shared/git-exclude.js +3 -20
  57. package/framework/hooks/shared/git-paths.js +8 -104
  58. package/framework/hooks/shared/graph-freshness.js +22 -166
  59. package/framework/hooks/shared/graph-refresh.js +21 -149
  60. package/framework/hooks/shared/heavy-command.js +4 -12
  61. package/framework/hooks/shared/hook-response.js +8 -83
  62. package/framework/hooks/shared/morph-cli.js +8 -52
  63. package/framework/hooks/shared/payload-utils.js +10 -132
  64. package/framework/hooks/shared/phase-utils.js +14 -72
  65. package/framework/hooks/shared/redact-secrets.js +4 -15
  66. package/framework/hooks/shared/reparse-scan.js +6 -66
  67. package/framework/hooks/shared/slice-progress.js +99 -0
  68. package/framework/hooks/shared/source-files.js +4 -17
  69. package/framework/hooks/shared/spawn-command.js +23 -0
  70. package/framework/hooks/shared/state-reader.js +51 -247
  71. package/framework/hooks/shared/state-writer.js +9 -54
  72. package/framework/hooks/shared/stdin-reader.js +1 -11
  73. package/framework/hooks/shared/telemetry-logger.js +7 -67
  74. package/framework/hooks/shared/transcript-usage.js +7 -29
  75. package/framework/hooks/shared/worktree-root.js +7 -29
  76. package/framework/mcp/registry.json +3 -4
  77. package/framework/phases.json +23 -39
  78. package/framework/rules/csharp-standards.md +1 -1
  79. package/framework/rules/frontend-standards.md +1 -0
  80. package/framework/rules/hangfire-standards.md +1 -1
  81. package/framework/rules/neon-standards.md +1 -1
  82. package/framework/schemas/config.schema.json +14 -9
  83. package/framework/schemas/feature.schema.json +15 -50
  84. package/framework/skills/README.md +3 -4
  85. package/framework/skills/core/create-umbraco-lp/SKILL.md +1 -1
  86. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +1 -1
  87. package/framework/skills/core/morph-apply/SKILL.md +149 -222
  88. package/framework/skills/core/morph-hotfix/SKILL.md +33 -42
  89. package/framework/skills/core/morph-machine/SKILL.md +2 -2
  90. package/framework/skills/core/morph-media/SKILL.md +1 -1
  91. package/framework/skills/core/morph-plan/SKILL.md +137 -299
  92. package/framework/skills/core/morph-review/SKILL.md +111 -136
  93. package/framework/skills/core/morph-standards/SKILL.md +27 -39
  94. package/framework/skills/core/morph-uiux/SKILL.md +94 -152
  95. package/framework/standards/STANDARDS.json +109 -1219
  96. package/framework/standards/STANDARDS.meta.json +1319 -0
  97. package/framework/standards/ai-agents/agent-archetypes.md +2 -2
  98. package/framework/standards/ai-agents/agent-session.md +1 -1
  99. package/framework/standards/ai-agents/conversational-agent-with-phases.md +4 -4
  100. package/framework/standards/ai-agents/durable-workflows-hitl.md +3 -3
  101. package/framework/standards/ai-agents/evals-with-cache.md +1 -2
  102. package/framework/standards/ai-agents/guardrails.md +1 -1
  103. package/framework/standards/ai-agents/multi-agent-patterns.md +2 -2
  104. package/framework/standards/ai-agents/observability-patterns.md +1 -1
  105. package/framework/standards/ai-agents/production.md +1 -1
  106. package/framework/standards/ai-agents/prompt-sources.md +2 -2
  107. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  108. package/framework/standards/ai-agents/rag-custom-pgvector.md +2 -2
  109. package/framework/standards/ai-agents/setup.md +3 -3
  110. package/framework/standards/ai-agents/sweet-spot.md +2 -2
  111. package/framework/standards/ai-agents/tool-adapters.md +0 -2
  112. package/framework/standards/ai-agents/vector-data-extensions.md +4 -4
  113. package/framework/standards/ai-agents/workflows.md +1 -1
  114. package/framework/standards/architecture/code-comments.md +7 -7
  115. package/framework/standards/architecture/vertical-slice/vertical-slice.md +1 -1
  116. package/framework/standards/backend/api/validation.md +1 -1
  117. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +5 -5
  118. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +1 -1
  119. package/framework/standards/backend/integrations/neon-auth/neon-auth.md +2 -2
  120. package/framework/standards/data/nosql/cache/redis.md +1 -1
  121. package/framework/standards/data/nosql/cosmos-db.md +3 -3
  122. package/framework/standards/frontend/admin-app/forms.md +1 -1
  123. package/framework/standards/frontend/design-system/art-direction.md +88 -0
  124. package/framework/standards/frontend/design-system/premium-finish.md +77 -2
  125. package/framework/standards/infrastructure/neon/neon-pgvector.md +5 -5
  126. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  127. package/framework/templates/CLAUDE.md.core.template +3 -3
  128. package/framework/templates/README.md +2 -3
  129. package/framework/templates/REGISTRY.json +15 -179
  130. package/framework/templates/docs/plan.md +72 -0
  131. package/framework/templates/docs/ui-mockups.md +4 -0
  132. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  133. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  134. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  135. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  136. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  137. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  138. package/package.json +1 -1
  139. package/src/commands/context/context-refresh.js +0 -16
  140. package/src/commands/dashboard/dashboard.js +2 -8
  141. package/src/commands/e2e/e2e.js +18 -105
  142. package/src/commands/graph/affected.js +0 -14
  143. package/src/commands/graph/build.js +11 -50
  144. package/src/commands/graph/explain.js +0 -16
  145. package/src/commands/graph/status.js +1 -12
  146. package/src/commands/mcp/mcp-setup.js +4 -62
  147. package/src/commands/observability/cost.js +17 -111
  148. package/src/commands/observability/telemetry.js +1 -14
  149. package/src/commands/project/branch.js +4 -45
  150. package/src/commands/project/claim.js +4 -48
  151. package/src/commands/project/doctor-procs.js +1 -1
  152. package/src/commands/project/doctor.js +53 -307
  153. package/src/commands/project/env.js +1 -27
  154. package/src/commands/project/fleet.js +6 -35
  155. package/src/commands/project/heavy.js +3 -3
  156. package/src/commands/project/init.js +8 -68
  157. package/src/commands/project/install-plugin-cmd.js +0 -1
  158. package/src/commands/project/setup-infra-cmd.js +2 -1
  159. package/src/commands/project/stack-scan.js +58 -0
  160. package/src/commands/project/standards.js +65 -72
  161. package/src/commands/project/status.js +28 -121
  162. package/src/commands/project/update.js +31 -223
  163. package/src/commands/project/worktree.js +109 -883
  164. package/src/commands/scaffold/scaffold.js +0 -25
  165. package/src/commands/state/advance.js +65 -120
  166. package/src/commands/state/approve.js +87 -229
  167. package/src/commands/state/archive.js +23 -139
  168. package/src/commands/state/auto-approval.js +65 -0
  169. package/src/commands/state/create.js +49 -109
  170. package/src/commands/state/delete.js +11 -63
  171. package/src/commands/state/finish.js +105 -494
  172. package/src/commands/state/gate-check.js +74 -183
  173. package/src/commands/state/gate-decision.js +8 -42
  174. package/src/commands/state/handoff.js +4 -31
  175. package/src/commands/state/list.js +21 -91
  176. package/src/commands/state/report-check.js +134 -338
  177. package/src/commands/state/retype.js +12 -28
  178. package/src/commands/validation/index.js +0 -3
  179. package/src/commands/validation/mutate.js +79 -19
  180. package/src/commands/validation/plan-check.js +81 -0
  181. package/src/commands/validation/validate-feature.js +52 -216
  182. package/src/commands/validation/verify.js +45 -160
  183. package/src/core/paths/output-schema.js +9 -155
  184. package/src/core/state/feature-identity.js +1 -25
  185. package/src/core/state/gate-order.js +2 -30
  186. package/src/core/state/lifecycle-audit.js +8 -100
  187. package/src/core/state/state-manager.js +32 -395
  188. package/src/core/templates/template-registry.js +2 -97
  189. package/src/core/templates/template-renderer.js +13 -93
  190. package/src/core/templates/template-seed.js +21 -0
  191. package/src/core/work-types.js +10 -45
  192. package/src/dashboard/app/App.jsx +1 -8
  193. package/src/dashboard/app/components/ActivityFeed.jsx +2 -8
  194. package/src/dashboard/app/components/CommandPalette.jsx +4 -11
  195. package/src/dashboard/app/components/DetailPanel.jsx +1 -5
  196. package/src/dashboard/app/components/EntityLink.jsx +0 -12
  197. package/src/dashboard/app/components/ErrorBoundary.jsx +1 -8
  198. package/src/dashboard/app/components/KnowledgeMarkdown.jsx +0 -11
  199. package/src/dashboard/app/components/Markdown.jsx +0 -9
  200. package/src/dashboard/app/components/PhaseRail.jsx +0 -11
  201. package/src/dashboard/app/components/SearchFilter.jsx +0 -4
  202. package/src/dashboard/app/components/Sidebar.jsx +0 -5
  203. package/src/dashboard/app/hooks/useMorphQuery.js +1 -11
  204. package/src/dashboard/app/hooks/usePref.js +1 -5
  205. package/src/dashboard/app/lib/format.js +0 -2
  206. package/src/dashboard/app/lib/phases.js +3 -9
  207. package/src/dashboard/app/lib/project.js +0 -12
  208. package/src/dashboard/app/pages/Activity.jsx +1 -1
  209. package/src/dashboard/app/pages/AgentMap.jsx +1 -8
  210. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -133
  211. package/src/dashboard/app/pages/Features.jsx +9 -16
  212. package/src/dashboard/app/pages/Guide.jsx +2 -3
  213. package/src/dashboard/app/pages/HookInspector.jsx +2 -23
  214. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -7
  215. package/src/dashboard/app/pages/Portfolio.jsx +0 -1
  216. package/src/dashboard/app/pages/RulesMap.jsx +1 -16
  217. package/src/dashboard/app/pages/SkillsLibrary.jsx +1 -16
  218. package/src/dashboard/app/pages/Standards.jsx +2 -17
  219. package/src/dashboard/scanner/activity-scanner.js +14 -83
  220. package/src/dashboard/scanner/agents-scanner.js +1 -23
  221. package/src/dashboard/scanner/docs-scanner.js +1 -7
  222. package/src/dashboard/scanner/feature-status.js +2 -5
  223. package/src/dashboard/scanner/features-scanner.js +92 -113
  224. package/src/dashboard/scanner/hooks-scanner.js +5 -28
  225. package/src/dashboard/scanner/index.js +0 -4
  226. package/src/dashboard/scanner/portfolio.js +5 -29
  227. package/src/dashboard/scanner/project-api.js +8 -54
  228. package/src/dashboard/scanner/rules-scanner.js +1 -2
  229. package/src/dashboard/scanner/skills-scanner.js +2 -12
  230. package/src/dashboard/scanner/standards-scanner.js +4 -7
  231. package/src/dashboard/vite-plugin-morph-data.js +5 -19
  232. package/src/lib/ai/ai-checks.js +7 -52
  233. package/src/lib/ai/ai-project-scan.js +14 -108
  234. package/src/lib/ai/code-audit.js +7 -63
  235. package/src/lib/ai/pin-drift.js +8 -51
  236. package/src/lib/ai/registry-audit.js +11 -90
  237. package/src/lib/ci/ci-verify.js +14 -13
  238. package/src/lib/ci/github-ci.js +9 -37
  239. package/src/lib/ci/workflow-scaffold.js +2 -10
  240. package/src/lib/detectors/claude-config-detector.js +13 -74
  241. package/src/lib/detectors/index.js +8 -59
  242. package/src/lib/e2e/compose-env.js +5 -66
  243. package/src/lib/e2e/stack-teardown.js +6 -40
  244. package/src/lib/generators/architecture-scanner.js +18 -156
  245. package/src/lib/generators/handoff-generator.js +37 -112
  246. package/src/lib/generators/scaffold-generator.js +5 -101
  247. package/src/lib/generators/settings-generator.js +11 -122
  248. package/src/lib/git/branch-classifier.js +11 -84
  249. package/src/lib/git/default-branch.js +14 -169
  250. package/src/lib/git/evidence-staleness.js +2 -1
  251. package/src/lib/git/gh.js +2 -17
  252. package/src/lib/git/pending-prs.js +6 -37
  253. package/src/lib/git/pr-files.js +3 -28
  254. package/src/lib/git/pr-index.js +3 -34
  255. package/src/lib/git/tree-fingerprint.js +17 -17
  256. package/src/lib/git/tree-state.js +6 -33
  257. package/src/lib/graph/god-nodes.js +2 -22
  258. package/src/lib/graph/graph-health.js +3 -24
  259. package/src/lib/graph/graph-loader.js +5 -56
  260. package/src/lib/graph/graphify-runtime.js +7 -37
  261. package/src/lib/graph/graphifyignore.js +8 -74
  262. package/src/lib/graph/sanitize.js +16 -136
  263. package/src/lib/heavy/cleanup.js +2 -6
  264. package/src/lib/installers/effects.js +8 -93
  265. package/src/lib/installers/mcp-installer.js +9 -169
  266. package/src/lib/installers/memory-installer.js +8 -33
  267. package/src/lib/installers/templates-reconcile.js +5 -60
  268. package/src/lib/mutation/commands.js +3 -2
  269. package/src/lib/mutation/dotnet-introspection.js +281 -0
  270. package/src/lib/mutation/plan.js +14 -6
  271. package/src/lib/mutation/runner.js +1 -1
  272. package/src/lib/mutation/subset.js +47 -13
  273. package/src/lib/plan/artifact-vocabulary.js +40 -0
  274. package/src/lib/plan/checks.js +208 -0
  275. package/src/lib/plan/plan-parser.js +204 -0
  276. package/src/lib/plan/plan-template.js +18 -0
  277. package/src/lib/projects/registry.js +7 -44
  278. package/src/lib/review-reports.js +204 -0
  279. package/src/lib/risk-detector.js +131 -274
  280. package/src/lib/specs/adr-index.js +38 -85
  281. package/src/lib/specs/design-seed.js +18 -0
  282. package/src/lib/specs/frontmatter.js +31 -0
  283. package/src/lib/specs/gfm-table.js +11 -0
  284. package/src/lib/specs/revocations.js +31 -102
  285. package/src/lib/specs/screen-checklist.js +56 -132
  286. package/src/lib/stack/dotnet-build-ignores.js +43 -0
  287. package/src/lib/stack/stack-profile.js +1 -28
  288. package/src/lib/stack/stack-scan.js +260 -0
  289. package/src/lib/stack-filter.js +4 -27
  290. package/src/lib/standards/registry.js +37 -0
  291. package/src/lib/standards/sections.js +47 -0
  292. package/src/lib/standards/verified-against.js +19 -191
  293. package/src/lib/tasks/eval-node.js +23 -179
  294. package/src/lib/tasks/linux-runner.js +18 -142
  295. package/src/lib/tasks/test-nodes.js +43 -313
  296. package/src/lib/tasks/test-runner.js +107 -447
  297. package/src/lib/telemetry/metrics.js +13 -103
  298. package/src/lib/telemetry/telemetry-reader.js +13 -238
  299. package/src/lib/telemetry/verify-event.js +4 -34
  300. package/src/lib/validators/code-budget/code-budget-validator.js +4 -44
  301. package/src/lib/validators/code-budget/index.js +0 -7
  302. package/src/lib/validators/contracts/contract-compliance-validator.js +4 -26
  303. package/src/lib/validators/contracts/index.js +0 -6
  304. package/src/lib/validators/nextjs/index.js +0 -5
  305. package/src/lib/validators/nextjs/next-component-validator.js +4 -81
  306. package/src/lib/validators/packages/index.js +0 -6
  307. package/src/lib/validators/packages/package-validator.js +6 -56
  308. package/src/lib/validators/reuse/index.js +0 -8
  309. package/src/lib/validators/reuse/reuse-validator.js +24 -137
  310. package/src/lib/validators/reuse/symbol-scanner.js +6 -62
  311. package/src/lib/validators/traceability-check.js +54 -156
  312. package/src/lib/validators/validation-runner.js +20 -125
  313. package/src/lib/validators/verify-runner.js +152 -575
  314. package/src/lib/work-type-detector.js +5 -67
  315. package/src/lib/worktree/docker-audit.js +17 -137
  316. package/src/lib/worktree/env-allocator.js +10 -98
  317. package/src/lib/worktree/feature-truth.js +12 -115
  318. package/src/lib/worktree/finish-backup.js +32 -242
  319. package/src/lib/worktree/fleet-scanner.js +31 -126
  320. package/src/lib/worktree/ignored-triage.js +15 -91
  321. package/src/lib/worktree/vscode-settings.js +2 -3
  322. package/src/lib/worktree/worktree-audit.js +21 -213
  323. package/src/lib/worktree/worktree-include.js +3 -5
  324. package/src/scripts/global-install.js +3 -9
  325. package/src/scripts/install-plugin.js +1 -13
  326. package/src/scripts/setup-infra.js +17 -138
  327. package/src/utils/agents-installer.js +92 -272
  328. package/src/utils/banner.js +4 -25
  329. package/src/utils/claude-md-injector.js +1 -37
  330. package/src/utils/claude-settings-manager.js +1 -87
  331. package/src/utils/file-copier.js +32 -90
  332. package/src/utils/hooks-installer.js +26 -296
  333. package/src/utils/skills-installer.js +4 -102
  334. package/src/utils/version-checker.js +4 -83
  335. package/framework/commands/morph-proposal.md +0 -57
  336. package/framework/evals/trace-schema.md +0 -67
  337. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  338. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  339. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  340. package/framework/hooks/shared/tasks-validator.js +0 -111
  341. package/framework/rules/morph-task-tracking.md +0 -54
  342. package/framework/rules/morph-workflow.md +0 -244
  343. package/framework/schemas/tasks.schema.json +0 -79
  344. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  345. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  346. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  347. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  348. package/framework/templates/docs/proposal.md +0 -192
  349. package/framework/templates/docs/spec.md +0 -57
  350. package/framework/templates/docs/ui-components.md +0 -52
  351. package/framework/templates/docs/ui-design-system.md +0 -76
  352. package/framework/templates/docs/ui-flows.md +0 -167
  353. package/framework/templates/examples/spec-examples.md +0 -91
  354. package/framework/templates/feature/decisions.md +0 -124
  355. package/framework/templates/feature/recap.md +0 -145
  356. package/framework/templates/tasks.template.json +0 -17
  357. package/src/commands/state/usecase.js +0 -143
  358. package/src/commands/tasks/dag.js +0 -181
  359. package/src/commands/utils/index.js +0 -3
  360. package/src/lib/agents/output-collector.js +0 -76
  361. package/src/lib/detectors/design-system-detector.js +0 -188
  362. package/src/lib/evaluator-report.js +0 -104
  363. package/src/lib/generators/design-system-generator.js +0 -508
  364. package/src/lib/specs/acervo-format.js +0 -70
  365. package/src/lib/specs/acervo-lifecycle.js +0 -264
  366. package/src/lib/specs/done-criteria-check.js +0 -200
  367. package/src/lib/specs/usecase-check.js +0 -301
  368. package/src/lib/specs/usecase-promoter.js +0 -301
  369. package/src/lib/specs/usecase-registry.js +0 -237
  370. package/src/lib/tasks/dispatch-planner.js +0 -319
  371. package/src/lib/tasks/dotnet-test-introspection.js +0 -632
  372. package/src/lib/tasks/mermaid-renderer.js +0 -132
  373. package/src/lib/tasks/task-classifier.js +0 -30
  374. package/src/lib/tasks/task-parser.js +0 -209
  375. package/src/lib/tasks/tasks-json-parser.js +0 -296
  376. package/src/lib/tasks/test-scope.js +0 -597
  377. package/src/utils/agent-id-mapper.js +0 -11
@@ -1,18 +1,5 @@
1
- /**
2
- * Loop measurements over the harness event stream: turns, tool calls split by
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
- const verify = /\bmorph-spec(?:\.js)?\s+verify\s+[^\s;&|)]+(\s+T\d+\b)?/.exec(executedText(cmd));
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
- * Why a command ran again within the window. Edits in between and a changed
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.scope === 'feature' && e.fingerprint && e.persisted !== false) {
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 slot = verifyByFeature[e.feature || '(sem feature)'] ||= { feature: 0, task: 0 };
292
- slot[kind === 'verify-feature' ? 'feature' : 'task'] += 1;
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] || { feature: 0, task: 0 },
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
- featureScopeMedian: median(featureRows.map((f) => f.verify.feature).filter((n) => n > 0)),
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
- * The same line reaches the reader through the live log, its rotated archives
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 { /* skip corrupt line */ }
44
+ try { out.push(JSON.parse(trimmed)); } catch {}
65
45
  }
66
- } catch { /* unreadable file contributes nothing */ }
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
- // Cost aggregation — sibling to aggregateTelemetry (kept separate rather than
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
- function newTaskState(feature, task, ts) {
288
- return {
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(); // phase -> Map(kind -> bytes)
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
- * The last complete run of the same verify (feature, task, scope) over the
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 || e.scope !== scope || (e.task ?? null) !== (task ?? null)) continue;
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);