@polymorphism-tech/morph-spec 8.46.0 → 9.0.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 (304) hide show
  1. package/CLAUDE.md +53 -44
  2. package/README.md +3 -4
  3. package/bin/morph-spec.js +38 -55
  4. package/bin/validate.js +5 -134
  5. package/claude-plugin.json +1 -1
  6. package/docs/CHEATSHEET.md +2 -2
  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 +75 -18
  11. package/framework/commands/create-umbraco-lp.md +2 -0
  12. package/framework/commands/migrate-lp-to-umbraco.md +2 -0
  13. package/framework/commands/morph-apply.md +5 -28
  14. package/framework/commands/morph-archive.md +2 -2
  15. package/framework/commands/morph-hotfix.md +8 -8
  16. package/framework/commands/morph-infra.md +17 -203
  17. package/framework/commands/morph-media.md +3 -0
  18. package/framework/commands/morph-preflight.md +7 -31
  19. package/framework/commands/morph-proposal.md +2 -50
  20. package/framework/commands/morph-status.md +3 -3
  21. package/framework/commands/morph-troubleshoot.md +2 -14
  22. package/framework/docs/CLI.md +585 -286
  23. package/framework/evals/review-checklist.md +82 -41
  24. package/framework/hooks/README.md +10 -11
  25. package/framework/hooks/claude-code/core/comment-guard.js +1 -1
  26. package/framework/hooks/claude-code/core/denylist.json +33 -3
  27. package/framework/hooks/claude-code/core/enforce-phase-writes.js +16 -20
  28. package/framework/hooks/claude-code/core/gate-guard.js +2 -11
  29. package/framework/hooks/claude-code/core/pre-bash-denylist.js +38 -0
  30. package/framework/hooks/claude-code/core/protect-spec-files.js +30 -242
  31. package/framework/hooks/claude-code/core/state-sync.js +17 -19
  32. package/framework/hooks/claude-code/core/telemetry-log.js +3 -3
  33. package/framework/hooks/claude-code/core/validate-completion.js +33 -97
  34. package/framework/hooks/claude-code/core/warn-bash-source-write.js +31 -0
  35. package/framework/hooks/claude-code/core/worktree-context.js +5 -4
  36. package/framework/hooks/claude-code/statusline.py +55 -41
  37. package/framework/hooks/package.json +3 -0
  38. package/framework/hooks/shared/activity-logger.js +1 -1
  39. package/framework/hooks/shared/build-check.js +10 -6
  40. package/framework/hooks/shared/command-tokens.js +169 -0
  41. package/framework/hooks/shared/comment-rules.js +1 -1
  42. package/framework/hooks/shared/comment-scanner.js +10 -23
  43. package/framework/hooks/shared/comment-stripper.js +1 -1
  44. package/framework/hooks/shared/e2e-check.js +67 -31
  45. package/framework/hooks/shared/errata.js +72 -0
  46. package/framework/hooks/shared/feature-locator.js +1 -1
  47. package/framework/hooks/shared/frontend-locations.js +6 -0
  48. package/framework/hooks/shared/gate-question.js +18 -0
  49. package/framework/hooks/shared/phase-utils.js +15 -32
  50. package/framework/hooks/shared/slice-progress.js +118 -0
  51. package/framework/hooks/shared/spawn-command.js +23 -0
  52. package/framework/hooks/shared/state-reader.js +61 -87
  53. package/framework/hooks/shared/state-writer.js +3 -3
  54. package/framework/mcp/registry.json +3 -4
  55. package/framework/phases.json +23 -39
  56. package/framework/rules/ai-agents-standards.md +18 -16
  57. package/framework/rules/asaas-standards.md +3 -1
  58. package/framework/rules/csharp-standards.md +10 -8
  59. package/framework/rules/data-standards.md +5 -5
  60. package/framework/rules/dotnet-templates.md +1 -1
  61. package/framework/rules/frontend-standards.md +4 -2
  62. package/framework/rules/gsap-standards.md +10 -5
  63. package/framework/rules/hangfire-standards.md +3 -1
  64. package/framework/rules/infrastructure-standards.md +4 -2
  65. package/framework/rules/neon-auth-standards.md +3 -1
  66. package/framework/rules/neon-standards.md +6 -4
  67. package/framework/rules/nextjs-standards.md +13 -11
  68. package/framework/rules/nextjs-templates.md +1 -1
  69. package/framework/rules/resend-standards.md +3 -1
  70. package/framework/schemas/config.schema.json +14 -9
  71. package/framework/schemas/feature.schema.json +15 -52
  72. package/framework/skills/README.md +4 -5
  73. package/framework/skills/core/create-umbraco-lp/SKILL.md +3 -3
  74. package/framework/skills/core/migrate-lp-to-umbraco/SKILL.md +3 -3
  75. package/framework/skills/core/morph-apply/SKILL.md +150 -223
  76. package/framework/skills/core/morph-hotfix/SKILL.md +34 -43
  77. package/framework/skills/core/morph-machine/SKILL.md +4 -4
  78. package/framework/skills/core/morph-media/SKILL.md +3 -3
  79. package/framework/skills/core/morph-plan/SKILL.md +138 -300
  80. package/framework/skills/core/morph-review/SKILL.md +112 -137
  81. package/framework/skills/core/morph-standards/SKILL.md +29 -41
  82. package/framework/skills/core/morph-uiux/SKILL.md +78 -153
  83. package/framework/standards/STANDARDS.json +90 -1219
  84. package/framework/standards/STANDARDS.meta.json +1305 -0
  85. package/framework/standards/ai-agents/evals-with-cache.md +2 -3
  86. package/framework/standards/ai-agents/llm-runtime-defaults.md +1 -1
  87. package/framework/standards/ai-agents/modalities-speech-to-text.md +36 -26
  88. package/framework/standards/ai-agents/prompt-sources.md +6 -4
  89. package/framework/standards/ai-agents/providers/model-registry.md +1 -1
  90. package/framework/standards/ai-agents/rag-custom-pgvector.md +67 -39
  91. package/framework/standards/ai-agents/setup.md +2 -2
  92. package/framework/standards/ai-agents/structured-output.md +1 -1
  93. package/framework/standards/ai-agents/sweet-spot.md +1 -1
  94. package/framework/standards/architecture/code-comments.md +8 -8
  95. package/framework/standards/architecture/vertical-slice/vertical-slice.md +18 -9
  96. package/framework/standards/backend/api/validation.md +135 -47
  97. package/framework/standards/backend/database/postgresql/database.md +11 -7
  98. package/framework/standards/backend/database/vector-search-rag.md +207 -324
  99. package/framework/standards/backend/dotnet/fluent-validation-vsa.md +14 -8
  100. package/framework/standards/backend/dotnet/result-pattern.md +40 -6
  101. package/framework/standards/backend/dotnet/scalar-ui.md +13 -7
  102. package/framework/standards/backend/dotnet/scrutor.md +1 -1
  103. package/framework/standards/backend/dotnet/testing.md +1 -1
  104. package/framework/standards/backend/dotnet/vsa-handler-patterns.md +7 -7
  105. package/framework/standards/backend/integrations/asaas/asaas-api.md +309 -165
  106. package/framework/standards/backend/integrations/hangfire/hangfire-jobs.md +134 -154
  107. package/framework/standards/data/nosql/cache/redis.md +120 -44
  108. package/framework/standards/data/nosql/cosmos-db.md +114 -71
  109. package/framework/standards/frontend/design-system/animations.md +1 -1
  110. package/framework/standards/frontend/design-system/naming.md +1 -1
  111. package/framework/standards/frontend/nextjs/motion-patterns.md +45 -29
  112. package/framework/standards/frontend/nextjs/naming-conventions.md +1 -1
  113. package/framework/standards/infrastructure/docker/coolify-deploy.md +8 -6
  114. package/framework/standards/infrastructure/neon/neon-pgvector.md +120 -75
  115. package/framework/standards/infrastructure/neon/neon-setup.md +53 -31
  116. package/framework/standards/integration/mcp/mcp-tools.md +7 -7
  117. package/framework/templates/README.md +2 -3
  118. package/framework/templates/REGISTRY.json +6 -170
  119. package/framework/templates/code/dotnet/contracts/contracts-vsa.cs +2 -2
  120. package/framework/templates/code/dotnet/extensions/database-extensions.cs +1 -1
  121. package/framework/templates/code/dotnet/extensions/result-extensions.cs +27 -0
  122. package/framework/templates/code/dotnet/feature/endpoint.cs.hbs +2 -2
  123. package/framework/templates/docs/plan.md +72 -0
  124. package/framework/templates/dotnet/ai-kit/README.md +1 -9
  125. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/MediaDisclosureTests.cs +1 -1
  126. package/framework/templates/dotnet/ai-kit/tests/Morph.AiKit.Tests/Morph.AiKit.Tests.csproj +0 -10
  127. package/framework/templates/infrastructure/docker/coolify.template.json +3 -3
  128. package/framework/templates/infrastructure/github/README.md +2 -1
  129. package/framework/templates/infrastructure/github/workflows/deploy-coolify.yml.hbs +2 -1
  130. package/framework/templates/meta-prompts/squad-leaders/backend-squad.md +1 -1
  131. package/framework/templates/meta-prompts/squad-leaders/frontend-squad.md +1 -1
  132. package/framework/templates/meta-prompts/squad-leaders/squad-leader.md +1 -1
  133. package/framework/templates/meta-prompts/validators/pre-commit-validator.md +3 -4
  134. package/package.json +1 -1
  135. package/src/commands/e2e/e2e.js +2 -2
  136. package/src/commands/observability/cost.js +18 -60
  137. package/src/commands/observability/telemetry.js +3 -5
  138. package/src/commands/project/doctor-procs.js +1 -1
  139. package/src/commands/project/doctor.js +9 -9
  140. package/src/commands/project/fleet.js +4 -4
  141. package/src/commands/project/heavy.js +3 -3
  142. package/src/commands/project/init.js +6 -1
  143. package/src/commands/project/setup-infra-cmd.js +2 -1
  144. package/src/commands/project/stack-scan.js +58 -0
  145. package/src/commands/project/standards.js +79 -25
  146. package/src/commands/project/status.js +29 -69
  147. package/src/commands/project/update.js +33 -19
  148. package/src/commands/project/worktree.js +1 -1
  149. package/src/commands/state/advance.js +62 -46
  150. package/src/commands/state/approve.js +95 -138
  151. package/src/commands/state/archive.js +20 -84
  152. package/src/commands/state/auto-approval.js +68 -0
  153. package/src/commands/state/create.js +28 -1
  154. package/src/commands/state/delete.js +1 -1
  155. package/src/commands/state/finish.js +11 -23
  156. package/src/commands/state/gate-check.js +86 -95
  157. package/src/commands/state/gate-decision.js +6 -4
  158. package/src/commands/state/handoff.js +1 -1
  159. package/src/commands/state/list.js +9 -9
  160. package/src/commands/state/report-check.js +157 -311
  161. package/src/commands/state/retype.js +9 -3
  162. package/src/commands/validation/mutate.js +77 -14
  163. package/src/commands/validation/plan-check.js +81 -0
  164. package/src/commands/validation/validate-feature.js +60 -135
  165. package/src/commands/validation/verify.js +41 -82
  166. package/src/core/paths/output-schema.js +20 -67
  167. package/src/core/state/gate-order.js +5 -6
  168. package/src/core/state/lifecycle-audit.js +2 -2
  169. package/src/core/state/state-manager.js +17 -98
  170. package/src/core/templates/template-registry.js +2 -2
  171. package/src/core/templates/template-seed.js +22 -0
  172. package/src/core/work-types.js +10 -10
  173. package/src/dashboard/app/App.jsx +0 -2
  174. package/src/dashboard/app/components/ActivityFeed.jsx +2 -3
  175. package/src/dashboard/app/components/CommandPalette.jsx +7 -8
  176. package/src/dashboard/app/components/EntityLink.jsx +1 -2
  177. package/src/dashboard/app/components/Markdown.jsx +1 -1
  178. package/src/dashboard/app/components/Sidebar.jsx +1 -2
  179. package/src/dashboard/app/hooks/useMorphQuery.js +1 -1
  180. package/src/dashboard/app/lib/phases.js +2 -4
  181. package/src/dashboard/app/pages/Activity.jsx +1 -1
  182. package/src/dashboard/app/pages/FeatureDetail.jsx +139 -129
  183. package/src/dashboard/app/pages/Features.jsx +9 -9
  184. package/src/dashboard/app/pages/Guide.jsx +2 -2
  185. package/src/dashboard/app/pages/PhasePipeline.jsx +3 -4
  186. package/src/dashboard/app/pages/RulesMap.jsx +3 -3
  187. package/src/dashboard/app/pages/SkillsLibrary.jsx +3 -3
  188. package/src/dashboard/app/pages/Standards.jsx +1 -1
  189. package/src/dashboard/scanner/activity-scanner.js +6 -16
  190. package/src/dashboard/scanner/agents-scanner.js +1 -1
  191. package/src/dashboard/scanner/docs-scanner.js +5 -23
  192. package/src/dashboard/scanner/features-scanner.js +95 -60
  193. package/src/dashboard/scanner/index.js +0 -2
  194. package/src/dashboard/scanner/project-api.js +1 -1
  195. package/src/dashboard/scanner/rules-scanner.js +15 -10
  196. package/src/dashboard/scanner/skills-scanner.js +2 -3
  197. package/src/dashboard/scanner/standards-scanner.js +2 -1
  198. package/src/lib/ci/ci-verify.js +17 -1
  199. package/src/lib/ci/github-ci.js +1 -1
  200. package/src/lib/detectors/index.js +3 -8
  201. package/src/lib/generators/handoff-generator.js +34 -65
  202. package/src/lib/generators/settings-generator.js +11 -89
  203. package/src/lib/git/evidence-staleness.js +1 -1
  204. package/src/lib/git/tree-fingerprint.js +19 -7
  205. package/src/lib/git/tree-state.js +2 -2
  206. package/src/lib/heavy/cleanup.js +1 -5
  207. package/src/lib/installers/memory-installer.js +6 -8
  208. package/src/lib/mutation/commands.js +3 -2
  209. package/src/lib/{tasks/dotnet-test-introspection.js → mutation/dotnet-introspection.js} +17 -282
  210. package/src/lib/mutation/plan.js +14 -6
  211. package/src/lib/mutation/runner.js +1 -1
  212. package/src/lib/mutation/subset.js +46 -9
  213. package/src/lib/plan/artifact-vocabulary.js +60 -0
  214. package/src/lib/plan/checks.js +234 -0
  215. package/src/lib/plan/plan-parser.js +209 -0
  216. package/src/lib/plan/plan-template.js +18 -0
  217. package/src/lib/review-reports.js +222 -0
  218. package/src/lib/risk-detector.js +134 -135
  219. package/src/lib/specs/adr-index.js +50 -69
  220. package/src/lib/specs/design-seed.js +18 -0
  221. package/src/lib/specs/frontmatter.js +44 -0
  222. package/src/lib/specs/gfm-table.js +17 -0
  223. package/src/lib/specs/old-format.js +68 -0
  224. package/src/lib/specs/revocations.js +37 -95
  225. package/src/lib/specs/screen-checklist.js +18 -17
  226. package/src/lib/stack/dotnet-build-ignores.js +47 -0
  227. package/src/lib/stack/stack-profile.js +1 -1
  228. package/src/lib/stack/stack-scan.js +267 -0
  229. package/src/lib/standards/registry.js +43 -0
  230. package/src/lib/standards/sections.js +58 -0
  231. package/src/lib/tasks/eval-node.js +4 -12
  232. package/src/lib/tasks/test-nodes.js +1 -74
  233. package/src/lib/tasks/test-runner.js +75 -117
  234. package/src/lib/telemetry/metrics.js +8 -69
  235. package/src/lib/telemetry/telemetry-reader.js +5 -118
  236. package/src/lib/telemetry/verify-event.js +5 -7
  237. package/src/lib/validators/code-budget/code-budget-validator.js +1 -1
  238. package/src/lib/validators/contracts/contract-compliance-validator.js +2 -2
  239. package/src/lib/validators/reuse/reuse-validator.js +24 -48
  240. package/src/lib/validators/reuse/symbol-scanner.js +2 -4
  241. package/src/lib/validators/traceability-check.js +85 -129
  242. package/src/lib/validators/validation-runner.js +8 -88
  243. package/src/lib/validators/verify-runner.js +134 -301
  244. package/src/lib/worktree/docker-audit.js +1 -2
  245. package/src/lib/worktree/fleet-scanner.js +25 -38
  246. package/src/scripts/setup-infra.js +42 -22
  247. package/src/utils/agents-installer.js +92 -13
  248. package/src/utils/claude-settings-manager.js +0 -35
  249. package/src/utils/file-copier.js +34 -6
  250. package/src/utils/hooks-installer.js +16 -9
  251. package/src/utils/skills-installer.js +1 -2
  252. package/framework/commands/commit.md +0 -166
  253. package/framework/evals/trace-schema.md +0 -67
  254. package/framework/hooks/claude-code/core/tasks-json-guard.js +0 -83
  255. package/framework/hooks/claude-code/core/trace-autogen.js +0 -277
  256. package/framework/hooks/git/pre-commit/specs.sh +0 -50
  257. package/framework/hooks/shared/tasks-validator.js +0 -111
  258. package/framework/rubrics/backend.json +0 -41
  259. package/framework/rubrics/default.json +0 -36
  260. package/framework/rubrics/frontend.json +0 -36
  261. package/framework/rules/morph-task-tracking.md +0 -54
  262. package/framework/rules/morph-workflow.md +0 -244
  263. package/framework/schemas/tasks.schema.json +0 -79
  264. package/framework/skills/core/morph-brainstorming/SKILL.md +0 -161
  265. package/framework/skills/core/morph-implement/SKILL.md +0 -194
  266. package/framework/skills/core/morph-proposal/SKILL.md +0 -152
  267. package/framework/skills/core/morph-stack-scan/SKILL.md +0 -145
  268. package/framework/templates/docs/proposal.md +0 -192
  269. package/framework/templates/docs/spec.md +0 -57
  270. package/framework/templates/docs/ui-components.md +0 -52
  271. package/framework/templates/docs/ui-design-system.md +0 -76
  272. package/framework/templates/docs/ui-flows.md +0 -167
  273. package/framework/templates/examples/spec-examples.md +0 -91
  274. package/framework/templates/feature/decisions.md +0 -124
  275. package/framework/templates/feature/recap.md +0 -145
  276. package/framework/templates/project/validate.js +0 -97
  277. package/framework/templates/tasks.template.json +0 -17
  278. package/src/commands/state/usecase.js +0 -143
  279. package/src/commands/tasks/dag.js +0 -181
  280. package/src/dashboard/app/pages/Rubrics.jsx +0 -97
  281. package/src/dashboard/scanner/rubrics-scanner.js +0 -12
  282. package/src/lib/agents/output-collector.js +0 -76
  283. package/src/lib/evaluator-report.js +0 -91
  284. package/src/lib/specs/acervo-format.js +0 -70
  285. package/src/lib/specs/acervo-lifecycle.js +0 -264
  286. package/src/lib/specs/done-criteria-check.js +0 -200
  287. package/src/lib/specs/usecase-check.js +0 -301
  288. package/src/lib/specs/usecase-promoter.js +0 -301
  289. package/src/lib/specs/usecase-registry.js +0 -237
  290. package/src/lib/tasks/dispatch-planner.js +0 -319
  291. package/src/lib/tasks/mermaid-renderer.js +0 -132
  292. package/src/lib/tasks/task-classifier.js +0 -30
  293. package/src/lib/tasks/task-parser.js +0 -209
  294. package/src/lib/tasks/tasks-json-parser.js +0 -296
  295. package/src/lib/tasks/test-scope.js +0 -597
  296. package/src/lib/validators/architecture/architecture-validator.js +0 -270
  297. package/src/lib/validators/architecture/index.js +0 -7
  298. package/src/lib/validators/css/css-validator.js +0 -340
  299. package/src/lib/validators/css/index.js +0 -7
  300. package/src/lib/validators/design-system/design-system-validator.js +0 -237
  301. package/src/lib/validators/design-system/index.js +0 -7
  302. package/src/lib/validators/score-aggregator.js +0 -244
  303. package/src/lib/validators/ui/index.js +0 -7
  304. package/src/lib/validators/ui/ui-contrast-validator.js +0 -422
@@ -28,7 +28,7 @@
28
28
  */
29
29
 
30
30
  import { execSync, execFileSync } from 'child_process';
31
- import { existsSync, readFileSync, readdirSync, realpathSync } from 'fs';
31
+ import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from 'fs';
32
32
  import { join, resolve, relative, isAbsolute } from 'path';
33
33
  import { globSync } from 'glob';
34
34
 
@@ -170,11 +170,6 @@ function collectDotnetTestProjects(projectPath) {
170
170
  * says "not inside" for the same directory and a linked worktree leaks into
171
171
  * the test command. Only `realpathSync.native` asks the OS. Fail-open to the
172
172
  * plain resolve for a path that does not exist (yet).
173
- *
174
- * Exported because test-scope.js spells every task output with it (#87 F4): a
175
- * mis-cased output resolves on a case-insensitive disk and was forwarded to the
176
- * runner verbatim — and the `--linux` container is case-sensitive. The OS also
177
- * returns the real CASE of each component, so this one call is both answers.
178
173
  */
179
174
  export function canonicalPath(p) {
180
175
  try {
@@ -338,6 +333,75 @@ export function resolveTestBuildTargets(node) {
338
333
  return extractCsprojTargets(node.command);
339
334
  }
340
335
 
336
+ const DOTNET_BUILDABLE = /"([^"]+\.(?:slnx?|csproj))"|'([^']+\.(?:slnx?|csproj))'|(\S+\.(?:slnx?|csproj))/gi;
337
+
338
+ /**
339
+ * What `dotnet test` compiles for this node, relative to its `cwd`: declared `build`,
340
+ * else what the command names, else the directory's single solution/project; `[]` = undiscoverable.
341
+ */
342
+ export function discoverDotnetBuildTargets(node, cwd) {
343
+ if (node?.runner !== 'dotnet') return [];
344
+ if (Array.isArray(node.build) && node.build.length > 0) return node.build;
345
+ const command = typeof node.command === 'string' ? node.command : '';
346
+ const named = [...command.matchAll(DOTNET_BUILDABLE)].map((m) => m[1] ?? m[2] ?? m[3]);
347
+ if (named.length > 0) return [...new Set(named)];
348
+
349
+ const dirArg = command.split(/\s+/).find((token) => token && !token.startsWith('-') && token !== 'dotnet' && token !== 'test'
350
+ && isDirectory(join(cwd, token)));
351
+ const dir = dirArg ?? '.';
352
+ let entries;
353
+ try { entries = readdirSync(join(cwd, dir)); } catch { return []; }
354
+ const only = singleDotnetBuildable(entries);
355
+ return only ? [dir === '.' ? only : join(dir, only).replace(/\\/g, '/')] : [];
356
+ }
357
+
358
+ /** The one solution among `files`, else the one project; `null` when `dotnet build` would have to guess. */
359
+ function singleDotnetBuildable(files) {
360
+ const solutions = files.filter((f) => /\.slnx?$/i.test(f));
361
+ const projects = files.filter((f) => /\.csproj$/i.test(f));
362
+ if (solutions.length) return solutions.length === 1 ? solutions[0] : null;
363
+ return projects.length === 1 ? projects[0] : null;
364
+ }
365
+
366
+ const DOTNET_SCAN_SKIPPED = new Set([WORKTREES_DIR, LEGACY_WORKTREES_DIR, 'node_modules', 'bin', 'obj']);
367
+ const DOTNET_SCAN_DEPTH = 6;
368
+
369
+ function dotnetBuildablesUnder(root, rel = '', depth = 0, found = []) {
370
+ let entries;
371
+ try { entries = readdirSync(join(root, rel), { withFileTypes: true }); } catch { return found; }
372
+ for (const e of entries) {
373
+ const path = rel ? `${rel}/${e.name}` : e.name;
374
+ if (e.isFile() && /\.(?:slnx?|csproj)$/i.test(e.name)) found.push(path);
375
+ else if (e.isDirectory() && depth < DOTNET_SCAN_DEPTH && !e.name.startsWith('.') && !DOTNET_SCAN_SKIPPED.has(e.name)) {
376
+ dotnetBuildablesUnder(root, path, depth + 1, found);
377
+ }
378
+ }
379
+ return found;
380
+ }
381
+
382
+ /**
383
+ * What a root `dotnet build` compiles when the root itself holds no solution or
384
+ * project: the repository's single .sln/.slnx, else its single .csproj, never
385
+ * looking inside worktree containers, node_modules, bin or obj.
386
+ *
387
+ * @returns {{ target: string|null, candidates: string[] }} `target` relative to
388
+ * `root` (`.` when the root already has one); `null` for zero or several, listed.
389
+ */
390
+ export function discoverDotnetSolution(root) {
391
+ let rootEntries;
392
+ try { rootEntries = readdirSync(root); } catch { return { target: null, candidates: [] }; }
393
+ if (rootEntries.some((e) => /\.(?:slnx?|csproj)$/i.test(e))) return { target: '.', candidates: [] };
394
+ const all = dotnetBuildablesUnder(root).sort();
395
+ const solutions = all.filter((f) => /\.slnx?$/i.test(f));
396
+ const pool = solutions.length ? solutions : all;
397
+ const single = singleDotnetBuildable(pool);
398
+ return { target: single, candidates: pool };
399
+ }
400
+
401
+ function isDirectory(path) {
402
+ try { return statSync(path).isDirectory(); } catch { return false; }
403
+ }
404
+
341
405
  /**
342
406
  * `dotnet build "<target>" …`, built from the SAME step build-check.js
343
407
  * resolves for a .NET stack — the string "dotnet build" is not typed a second
@@ -488,29 +552,6 @@ function parseDotnetCount(output) {
488
552
  return seen ? sum : null;
489
553
  }
490
554
 
491
- /**
492
- * The `total` of every MTP summary block in `output`, ANSI stripped — one per
493
- * `dotnet test` invocation (measured: a solution prints ONE aggregated block).
494
- * `[]` when none is readable. Read by `judgeNarrowedRun` (verify-runner.js): in a
495
- * narrowed `&&` chain the SUM can be > 0 while one link ran nothing — its exit 8
496
- * ignored by a path the scanner cannot read — and that link's own block says
497
- * `total: 0`.
498
- *
499
- * @param {string|null|undefined} output
500
- * @returns {number[]}
501
- */
502
- export function mtpSummaryTotals(output) {
503
- if (typeof output !== 'string' || !output) return [];
504
- const lines = stripAnsi(output).split('\n');
505
- const totals = [];
506
- lines.forEach((line, i) => {
507
- if (!MTP_SUMMARY_HEADER.test(line)) return;
508
- const { total } = mtpSummaryBlock(lines, i + 1);
509
- if (total !== null) totals.push(total);
510
- });
511
- return totals;
512
- }
513
-
514
555
  /** Last value of a `# key N` (TAP) or `ℹ key N` (spec reporter) summary line. */
515
556
  function lastSummaryValue(output, key) {
516
557
  const re = new RegExp(`^\\s*[#ℹ]\\s*${key}\\s+(\\d+)\\s*$`, 'gim');
@@ -578,8 +619,8 @@ const ANSI_PATTERN = /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][^\x07\x1b]*(?:\x07|\x1b\\))/
578
619
  /**
579
620
  * Strip ANSI escape sequences (CSI/SGR and OSC) from captured process output.
580
621
  *
581
- * Non-string input is returned UNCHANGED, never coerced to `''` — the callers
582
- * that matter (`parseTestCount`, `isZeroTestRun`) already treat a non-string
622
+ * Non-string input is returned UNCHANGED, never coerced to `''` — the caller
623
+ * that matters (`parseTestCount`) already treats a non-string
583
624
  * output as "nothing to read" on their own; making this function opinionated
584
625
  * about that too would be a second place for the same rule to drift.
585
626
  *
@@ -640,91 +681,6 @@ export function parseTestCount(output, runner) {
640
681
  return extractor(stripAnsi(output));
641
682
  }
642
683
 
643
- // ============================================================================
644
- // "Zero tests ran" signatures, per runner (#87 PR B / #110 parte 1)
645
- // ============================================================================
646
-
647
- /**
648
- * Per-runner signal for "the run matched/executed NOTHING" — a category
649
- * distinct from both a genuine `{ total: 0, ... }` summary (a suite that
650
- * legitimately declares zero cases) and from a summary `parseTestCount`
651
- * simply could not read (`null`, UNKNOWN). Its reader is `judgeNarrowedRun`
652
- * (verify-runner.js, #87): a narrowed `verify {feature} {task}` run that FAILED
653
- * with this signature, and with no count proving tests ran, is named
654
- * `narrowed-zero-tests (…)` instead of an anonymous red. (A PASSED narrowed run
655
- * needs its own proof there — a count > 0, or MTP's exit 0 — never this table.)
656
- *
657
- * Every entry was MEASURED against a real runner, not invented, and the two
658
- * dotnet entries disagree with each other on purpose:
659
- *
660
- * - **MTP** (`--filter-class`, this repo's own `ai-kit`, xunit.v3 4.x):
661
- * a non-matching filter exits **8** (`ZeroTests`) — reliable BY EXIT
662
- * CODE. See `test/fixtures/test-output/mtp-zero-tests.txt`.
663
- * - **VSTest** (`--filter "FullyQualifiedName~..."`, classic xunit 2.x):
664
- * a non-matching filter exits **0** — exit code CANNOT tell a zero-match
665
- * run from a green one here. Only the OUTPUT STRING can:
666
- * `test/fixtures/test-output/vstest-zero-tests.txt` (pt-BR, measured).
667
- * The en-US wording is the brief's documented string, not measured on
668
- * this pt-BR machine — kept alongside the pt-BR one because neither is
669
- * derivable from the other and the machine that runs `verify` may be in
670
- * either locale.
671
- *
672
- * Exit code 8 is therefore listed ONLY as a dotnet exit code, never paired
673
- * with a string requirement — and exit code 9
674
- * (`MinimumExpectedTestsPolicyViolation`, this repo's own
675
- * `--minimum-expected-tests 275`) is deliberately ABSENT: it fires on a run
676
- * that executed fewer cases than the configured floor, including a run that
677
- * was entirely green (measured:
678
- * `test/fixtures/test-output/mtp-filter-class-policy-violation.txt`, 10/10
679
- * passed, exit 9). Folding 9 into this table would misreport a real pass as
680
- * "zero tests ran".
681
- */
682
- export const ZERO_RUN_SIGNATURES = {
683
- dotnet: {
684
- exitCodes: [8],
685
- strings: [
686
- // en-US, VSTest — documented by the brief, not measured here (pt-BR machine).
687
- 'No test matches the given testcase filter',
688
- // pt-BR, VSTest — MEASURED, test/fixtures/test-output/vstest-zero-tests.txt.
689
- 'Nenhum teste corresponde ao filtro de testcase',
690
- ],
691
- },
692
- vitest: {
693
- exitCodes: [],
694
- strings: ['No test files found'],
695
- },
696
- jest: {
697
- exitCodes: [],
698
- strings: ['No tests found'],
699
- },
700
- };
701
-
702
- /**
703
- * True when `output`/`exitCode` carries this runner's "zero tests ran"
704
- * signature. Every other runner (`node-test`, `playwright`, `evals`, `none`,
705
- * or anything not in `ZERO_RUN_SIGNATURES`) answers `false` — an absent entry
706
- * is an honest "this primitive does not know that runner's zero-tests shape
707
- * yet", the same doctrine `COUNT_EXTRACTORS` uses for an unknown runner.
708
- *
709
- * Called by `judgeNarrowedRun` (verify-runner.js) on a FAILED narrowed run, to
710
- * name it `narrowed-zero-tests (…)`.
711
- *
712
- * @param {string|null|undefined} output - Combined stdout+stderr of the run
713
- * @param {string} runner - One of TEST_RUNNERS
714
- * @param {number|null|undefined} exitCode - Must be a NUMBER to match: the
715
- * exit-code check is a strict `includes`, so the string `'8'` never matches 8.
716
- * @returns {boolean}
717
- */
718
- export function isZeroTestRun(output, runner, exitCode) {
719
- const signature = ZERO_RUN_SIGNATURES[runner];
720
- if (!signature) return false;
721
-
722
- if (signature.exitCodes.includes(exitCode)) return true;
723
-
724
- const text = stripAnsi(typeof output === 'string' ? output : '');
725
- return signature.strings.some((needle) => text.includes(needle));
726
- }
727
-
728
684
  /**
729
685
  * Compare an executed count against the recorded baseline — asymmetrically,
730
686
  * and on purpose (D7).
@@ -840,7 +796,9 @@ export function runTestSuite(projectPath = '.', opts = {}) {
840
796
  });
841
797
  return { passed: true, output: output ?? '', exitCode: 0, timeoutMs: timeout };
842
798
  } catch (err) {
843
- const output = (err.stdout || '') + (err.stderr || '');
799
+ // A spawn failure or a kill leaves both streams empty; its own error is then the only lead.
800
+ const output = (err.stdout || '') + (err.stderr || '')
801
+ || [err.code, err.signal, err.message].filter(Boolean).join(' ');
844
802
 
845
803
  // A timeout is NOT a red suite: the command was interrupted, so no verdict
846
804
  // exists. Saying so by name is the whole point — the field diagnosis of
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Loop measurements over the harness event stream: turns, tool calls split by
3
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
4
+ * rereads, verify count per feature, git calls and whether the reuse
5
+ * validator ran. Computed the same way on every window so a later
6
6
  * window compares with an earlier one.
7
7
  *
8
8
  * Every number degrades to null ("not measurable here") when the stream lacks
@@ -43,8 +43,7 @@ function executedText(command) {
43
43
  */
44
44
  export function classifyCommand(command) {
45
45
  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' };
46
+ if (/\bmorph-spec(?:\.js)?\s+verify\s+[^\s;&|)]+/.test(executedText(cmd))) return { kind: 'verify', klass: 'verify' };
48
47
  if (/\bdotnet\s+test\b/.test(cmd)) {
49
48
  if (/--list-tests\b/.test(cmd)) return { kind: 'dotnet-test-list', klass: 'test' };
50
49
  return { kind: /--filter\b/.test(cmd) ? 'dotnet-test-filtered' : 'dotnet-test-full', klass: 'test' };
@@ -159,8 +158,6 @@ export function computeMetrics(events, opts = {}) {
159
158
 
160
159
  const global = newCounters();
161
160
  const byFeature = new Map();
162
- const openTask = new Map();
163
- const tasks = [];
164
161
  const lastRun = new Map();
165
162
  const editCount = new Map();
166
163
  const readsSeen = new Set();
@@ -172,8 +169,6 @@ export function computeMetrics(events, opts = {}) {
172
169
  const lastFingerprint = new Map();
173
170
  const unchangedRepeats = {};
174
171
  const reuse = { runs: 0, byTree: {}, skipReasons: {} };
175
- const tasksOf = new Map();
176
- let callsInTaskWindows = 0;
177
172
  let v2 = 0;
178
173
  let legacy = 0;
179
174
  let turnsObserved = false;
@@ -184,16 +179,6 @@ export function computeMetrics(events, opts = {}) {
184
179
  if (!byFeature.has(key)) byFeature.set(key, newCounters());
185
180
  return byFeature.get(key);
186
181
  };
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
182
  for (const e of scoped) {
198
183
  if (isV2(e)) v2 += 1; else legacy += 1;
199
184
  if (e.ts) {
@@ -202,21 +187,7 @@ export function computeMetrics(events, opts = {}) {
202
187
  }
203
188
  const fkey = e.feature || NO_FEATURE;
204
189
 
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
190
  const sinks = [global, featureCounters(fkey)];
218
- const task = openTask.get(fkey);
219
- if (task) sinks.push(task.c);
220
191
 
221
192
  if (e.event === 'ToolBatch') {
222
193
  turnsObserved = true;
@@ -225,7 +196,7 @@ export function computeMetrics(events, opts = {}) {
225
196
  }
226
197
 
227
198
  if (e.event === 'Verify') {
228
- if (e.scope === 'feature' && e.fingerprint && e.persisted !== false) {
199
+ if (e.fingerprint && e.persisted !== false) {
229
200
  if (lastFingerprint.get(fkey) === e.fingerprint) {
230
201
  unchangedRepeats[e.feature || '(sem feature)'] = (unchangedRepeats[e.feature || '(sem feature)'] || 0) + 1;
231
202
  }
@@ -242,7 +213,6 @@ export function computeMetrics(events, opts = {}) {
242
213
 
243
214
  if (!TOOL_EVENTS.has(e.event) || !e.tool) continue;
244
215
  for (const c of sinks) { c.toolCalls.total += 1; c.toolCalls[whoOf(e)] += 1; }
245
- if (task) callsInTaskWindows += 1;
246
216
 
247
217
  if (e.tool === 'Write' || e.tool === 'Edit') {
248
218
  if (!isFailedToolCall(e)) editCount.set(fkey, (editCount.get(fkey) || 0) + 1);
@@ -288,8 +258,8 @@ export function computeMetrics(events, opts = {}) {
288
258
  }
289
259
 
290
260
  if (klass === 'verify') {
291
- const slot = verifyByFeature[e.feature || '(sem feature)'] ||= { feature: 0, task: 0 };
292
- slot[kind === 'verify-feature' ? 'feature' : 'task'] += 1;
261
+ const name = e.feature || '(sem feature)';
262
+ verifyByFeature[name] = (verifyByFeature[name] || 0) + 1;
293
263
  }
294
264
 
295
265
  if (!RERUN_CLASSES.has(klass) || typeof e.command !== 'string') continue;
@@ -318,34 +288,15 @@ export function computeMetrics(events, opts = {}) {
318
288
  lastRun.set(runKey, run);
319
289
  }
320
290
 
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
291
  const featureRows = [...byFeature.entries()]
335
292
  .filter(([key]) => key !== NO_FEATURE)
336
293
  .map(([feature, c]) => ({
337
294
  feature,
338
295
  ...finishCounters(c, turnsObserved),
339
- verify: verifyByFeature[feature] || { feature: 0, task: 0 },
296
+ verify: verifyByFeature[feature] || 0,
340
297
  }))
341
298
  .sort((a, b) => b.toolCalls.total - a.toolCalls.total || a.feature.localeCompare(b.feature));
342
299
 
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
300
  const byKind = {};
350
301
  for (const [kind, slot] of Object.entries(rerunByKind)) {
351
302
  byKind[kind] = { ...slot, rate: rate(slot.reruns, slot.runs), burstRate: rate(slot.inBursts, slot.runs) };
@@ -364,23 +315,11 @@ export function computeMetrics(events, opts = {}) {
364
315
  .slice(0, top),
365
316
  verify: {
366
317
  byFeature: verifyByFeature,
367
- featureScopeMedian: median(featureRows.map((f) => f.verify.feature).filter((n) => n > 0)),
318
+ median: median(featureRows.map((f) => f.verify).filter((n) => n > 0)),
368
319
  unchangedRepeats,
369
320
  },
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
321
  dotnetFullSuite: fullSuite,
382
322
  reuse,
383
323
  features: featureRows,
384
- tasks: taskRows,
385
324
  };
386
325
  }
@@ -232,10 +232,9 @@ export function aggregateTelemetry(events, opts = {}) {
232
232
  // test/commands/telemetry.test.js, which spreads it wholesale into --json
233
233
  // output — adding fields there would leak into that contract).
234
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).
235
+ // Turns the same raw event stream into a cost-facing view: how many context
236
+ // bytes were re-ingested (by kind, globally and per phase), and how many
237
+ // sub-agent dispatches happened (and how many prompt chars they cost).
239
238
  //
240
239
  // "Bytes re-ingested" is a DETERMINISTIC PROXY for cost, not a token count —
241
240
  // hooks can only see what tools read off disk, never the model's actual
@@ -243,35 +242,6 @@ export function aggregateTelemetry(events, opts = {}) {
243
242
  // computes the numbers.
244
243
  // ─────────────────────────────────────────────────────────────────────────
245
244
 
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
- }
274
-
275
245
  function mapToObj(map) {
276
246
  const out = {};
277
247
  for (const key of [...map.keys()].sort()) out[key] = map.get(key);
@@ -284,52 +254,9 @@ function sumValues(map) {
284
254
  return total;
285
255
  }
286
256
 
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
257
  /**
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.
258
+ * Summarize an event stream for cost purposes: bytes-re-ingested buckets by
259
+ * `kind` (global and per phase) and sub-agent dispatch counts.
333
260
  *
334
261
  * Byte attribution prefers `bytesRead` (the line window a Read put in context)
335
262
  * and falls back to `bytes` (the file size — all v1 events have) — keyed off
@@ -356,7 +283,6 @@ function finalizeTask(state) {
356
283
  * @returns {{
357
284
  * total: number,
358
285
  * features: string[],
359
- * taskDurationSeconds: number,
360
286
  * bytesByKind: Object<string, number>,
361
287
  * bytesTotal: number,
362
288
  * dispatches: {count:number, promptChars:number},
@@ -364,7 +290,6 @@ function finalizeTask(state) {
364
290
  * sessionUsageLegacySamples: number,
365
291
  * agentUsage: {inputTokens:number, outputTokens:number, cacheReadTokens:number, cacheCreationTokens:number, agents:number, samples:number}|null,
366
292
  * tools: Array<{name:string,count:number}>,
367
- * tasks: Array<Object>,
368
293
  * phases: Array<{phase:string, bytesByKind:Object<string,number>, bytesTotal:number}>,
369
294
  * }}
370
295
  */
@@ -388,8 +313,6 @@ export function aggregateCost(events, opts = {}) {
388
313
  const bytesByKindGlobal = new Map();
389
314
  const dispatches = { count: 0, promptChars: 0 };
390
315
  const phaseBytes = new Map(); // phase -> Map(kind -> bytes)
391
- const activeByFeature = new Map(); // featureKey -> open task state
392
- const tasks = [];
393
316
  const newUsageBucket = () => ({
394
317
  totals: { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheCreationTokens: 0 },
395
318
  ids: new Set(),
@@ -402,24 +325,6 @@ export function aggregateCost(events, opts = {}) {
402
325
  if (!e || typeof e !== 'object') continue;
403
326
  if (e.feature) features.add(e.feature);
404
327
 
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
328
  if (e.event === 'SessionUsage' || e.event === 'AgentUsage') {
424
329
  const bucket = e.event === 'SessionUsage' ? usage.session : usage.agent;
425
330
  // Only a delta can be summed: the top-level numbers are the transcript's
@@ -437,16 +342,11 @@ export function aggregateCost(events, opts = {}) {
437
342
  }
438
343
 
439
344
  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
345
 
445
346
  const read = ingestedBytes(e);
446
347
  if (read !== null) {
447
348
  const kind = e.kind || 'unknown';
448
349
  bytesByKindGlobal.set(kind, (bytesByKindGlobal.get(kind) || 0) + read);
449
- if (active) active.bytesByKind.set(kind, (active.bytesByKind.get(kind) || 0) + read);
450
350
  if (e.phase) {
451
351
  if (!phaseBytes.has(e.phase)) phaseBytes.set(e.phase, new Map());
452
352
  const m = phaseBytes.get(e.phase);
@@ -459,18 +359,9 @@ export function aggregateCost(events, opts = {}) {
459
359
  const chars = hasPromptChars ? e.promptChars : e.prompt.length;
460
360
  dispatches.count += 1;
461
361
  dispatches.promptChars += chars;
462
- if (active) {
463
- active.dispatches.count += 1;
464
- active.dispatches.promptChars += chars;
465
- }
466
362
  }
467
363
  }
468
364
 
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
365
  const phases = [...phaseBytes.entries()]
475
366
  .map(([phase, kindMap]) => ({
476
367
  phase,
@@ -479,12 +370,9 @@ export function aggregateCost(events, opts = {}) {
479
370
  }))
480
371
  .sort((a, b) => a.phase.localeCompare(b.phase));
481
372
 
482
- const taskDurationSeconds = tasks.reduce((sum, t) => sum + (t.durationSeconds || 0), 0);
483
-
484
373
  return {
485
374
  total: scoped.length,
486
375
  features: [...features],
487
- taskDurationSeconds,
488
376
  bytesByKind: mapToObj(bytesByKindGlobal),
489
377
  bytesTotal: sumValues(bytesByKindGlobal),
490
378
  dispatches,
@@ -496,7 +384,6 @@ export function aggregateCost(events, opts = {}) {
496
384
  ? { ...usage.agent.totals, agents: usage.agent.ids.size, samples: usage.agent.samples }
497
385
  : null,
498
386
  tools: rank(toolCounts),
499
- tasks,
500
387
  phases,
501
388
  };
502
389
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
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
3
+ * per-node status and duration, the commit it covered and whether the
4
4
  * reuse validator could run. The Bash event of the same command says who
5
5
  * called it and how long the whole command took; this one says what it did.
6
6
  */
@@ -32,8 +32,6 @@ export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerp
32
32
  return {
33
33
  event: 'Verify',
34
34
  feature: result.feature,
35
- task: result.task ?? null,
36
- scope: result.scope,
37
35
  status: result.status,
38
36
  sha,
39
37
  fingerprint,
@@ -46,21 +44,21 @@ export function buildVerifyEvent(result, { sha, tree, persisted, reason, fingerp
46
44
  }
47
45
 
48
46
  /**
49
- * The last complete run of the same verify (feature, task, scope) over the
47
+ * The last complete run of the same feature's verify over the
50
48
  * same tree, or null. A partial run (`--filter`, `--skip-*`, `--no-persist`)
51
49
  * is not the proof a complete one replaces, so it is passed over. Only the
52
50
  * live log is read: a repeat worth warning about is recent.
53
51
  *
54
52
  * @param {string} projectPath
55
- * @param {{ feature: string, task: string|null, scope: string, fingerprint: string|null }} run
53
+ * @param {{ feature: string, fingerprint: string|null }} run
56
54
  * @returns {{ ts: string, status: string, sha: string|null }|null}
57
55
  */
58
- export function findRepeatedVerify(projectPath, { feature, task, scope, fingerprint }) {
56
+ export function findRepeatedVerify(projectPath, { feature, fingerprint }) {
59
57
  if (!fingerprint) return null;
60
58
  const events = readTelemetryEvents(projectPath);
61
59
  for (let i = events.length - 1; i >= 0; i--) {
62
60
  const e = events[i];
63
- if (e.event !== 'Verify' || e.feature !== feature || e.scope !== scope || (e.task ?? null) !== (task ?? null)) continue;
61
+ if (e.event !== 'Verify' || e.feature !== feature) continue;
64
62
  if (e.persisted === false) continue;
65
63
  return e.fingerprint === fingerprint ? { ts: e.ts, status: e.status, sha: e.sha ?? null } : null;
66
64
  }
@@ -89,7 +89,7 @@ export function budgetIssues(measured) {
89
89
  level: 'warning',
90
90
  message:
91
91
  `${processLines.length} de ${comments} linhas de comentário adicionadas citam processo ` +
92
- `(UC, ADR, T{n}, FR, Gate, spec.md, mockup): ${shown.join(', ')}${more > 0 ? ` (+${more})` : ''}`,
92
+ `(UC, ADR, FR, C{n}, F{n}, Gate, plan.md, checks.json, mockup): ${shown.join(', ')}${more > 0 ? ` (+${more})` : ''}`,
93
93
  file: processLines[0].file,
94
94
  line: processLines[0].line,
95
95
  solution: