mandrel 1.94.0 → 2.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 (560) hide show
  1. package/.agents/README.md +116 -99
  2. package/.agents/agents/acceptance-critic.md +9 -7
  3. package/.agents/agents/story-worker.md +45 -51
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +498 -1287
  6. package/.agents/docs/agentrc-reference.json +185 -80
  7. package/.agents/docs/configuration.md +132 -190
  8. package/.agents/docs/execution-reference.md +51 -25
  9. package/.agents/docs/quality-gates.md +25 -33
  10. package/.agents/docs/workflows.md +8 -8
  11. package/.agents/instructions.md +113 -139
  12. package/.agents/rules/ci-remediation.md +11 -15
  13. package/.agents/rules/git-conventions-reference.md +48 -58
  14. package/.agents/rules/git-conventions.md +16 -22
  15. package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
  16. package/.agents/schemas/agentrc.schema.json +83 -254
  17. package/.agents/schemas/audit-rules.json +59 -1
  18. package/.agents/schemas/audit-rules.schema.json +33 -1
  19. package/.agents/schemas/lifecycle/README.md +1 -2
  20. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  21. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  22. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  23. package/.agents/schemas/lifecycle/retro.end.schema.json +1 -1
  24. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  25. package/.agents/schemas/signal-event.schema.json +3 -3
  26. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  27. package/.agents/schemas/validation-evidence.schema.json +1 -1
  28. package/.agents/scripts/acceptance-eval.js +22 -66
  29. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  30. package/.agents/scripts/audit-to-stories.js +7 -7
  31. package/.agents/scripts/boot-sweep.js +1 -1
  32. package/.agents/scripts/bootstrap.js +3 -3
  33. package/.agents/scripts/check-dead-exports.js +43 -104
  34. package/.agents/scripts/check-doc-links.js +2 -2
  35. package/.agents/scripts/check-lifecycle-lint.js +7 -10
  36. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  37. package/.agents/scripts/cleanup-repo-test-temp.js +6 -1
  38. package/.agents/scripts/deliver-recover.js +122 -0
  39. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  40. package/.agents/scripts/evidence-gate.js +20 -50
  41. package/.agents/scripts/generate-skills-index.js +17 -1
  42. package/.agents/scripts/generate-workflows-doc.js +4 -4
  43. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  44. package/.agents/scripts/lib/Logger.js +6 -10
  45. package/.agents/scripts/lib/audit-suite/runner.js +2 -2
  46. package/.agents/scripts/lib/audit-suite/selector.js +328 -28
  47. package/.agents/scripts/lib/audit-to-stories/{seed-epic-from-findings.js → seed-from-findings.js} +9 -9
  48. package/.agents/scripts/lib/baselines/kernel.js +206 -18
  49. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -15
  50. package/.agents/scripts/lib/baselines/reader.js +1 -6
  51. package/.agents/scripts/lib/bdd-runner-detect.js +5 -9
  52. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  53. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +32 -33
  54. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  55. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  56. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  57. package/.agents/scripts/lib/checks/core-bare-clean.js +6 -3
  58. package/.agents/scripts/lib/checks/index.js +3 -2
  59. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  60. package/.agents/scripts/lib/checks/state.js +17 -248
  61. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +26 -24
  62. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  63. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  64. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  65. package/.agents/scripts/lib/cli/standard-args.js +13 -22
  66. package/.agents/scripts/lib/cli-args.js +39 -9
  67. package/.agents/scripts/lib/close-validation/gates.js +15 -15
  68. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  69. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  70. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  71. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  72. package/.agents/scripts/lib/config/ci.js +6 -31
  73. package/.agents/scripts/lib/config/delivery-routing.js +52 -35
  74. package/.agents/scripts/lib/config/explain.js +61 -48
  75. package/.agents/scripts/lib/config/github.js +7 -5
  76. package/.agents/scripts/lib/config/limits.js +29 -80
  77. package/.agents/scripts/lib/config/paths.js +0 -2
  78. package/.agents/scripts/lib/config/quality.js +12 -15
  79. package/.agents/scripts/lib/config/runners.js +20 -66
  80. package/.agents/scripts/lib/config/temp-paths.js +30 -63
  81. package/.agents/scripts/lib/config/worktree-isolation.js +0 -5
  82. package/.agents/scripts/lib/config-resolver.js +2 -7
  83. package/.agents/scripts/lib/config-settings-schema-delivery.js +55 -161
  84. package/.agents/scripts/lib/config-settings-schema-quality.js +17 -16
  85. package/.agents/scripts/lib/config-settings-schema.js +100 -60
  86. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  87. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  88. package/.agents/scripts/lib/dependency-parser.js +3 -2
  89. package/.agents/scripts/lib/doc-tiers.js +2 -2
  90. package/.agents/scripts/lib/duplicate-search.js +242 -41
  91. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
  92. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  93. package/.agents/scripts/lib/format-generated-json.js +97 -0
  94. package/.agents/scripts/lib/framework-version.js +19 -190
  95. package/.agents/scripts/lib/gh-exec.js +8 -0
  96. package/.agents/scripts/lib/git-branch-cleanup.js +1 -10
  97. package/.agents/scripts/lib/git-branch-lifecycle.js +17 -180
  98. package/.agents/scripts/lib/git-utils.js +32 -20
  99. package/.agents/scripts/lib/github/framework-repo.js +6 -0
  100. package/.agents/scripts/lib/json-utils.js +1 -2
  101. package/.agents/scripts/lib/label-constants.js +10 -38
  102. package/.agents/scripts/lib/label-taxonomy.js +10 -55
  103. package/.agents/scripts/lib/observability/active-story-env.js +44 -165
  104. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  105. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  106. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  107. package/.agents/scripts/lib/observability/source-classifier.js +5 -5
  108. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  109. package/.agents/scripts/lib/onboard/init-tail.js +1 -3
  110. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  111. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  112. package/.agents/scripts/lib/orchestration/ceremony-routing.js +105 -44
  113. package/.agents/scripts/lib/orchestration/code-review.js +78 -436
  114. package/.agents/scripts/lib/orchestration/column-sync.js +1 -1
  115. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +4 -4
  116. package/.agents/scripts/lib/orchestration/context-envelope.js +2 -5
  117. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  118. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  119. package/.agents/scripts/lib/orchestration/docs-digest.js +8 -8
  120. package/.agents/scripts/lib/orchestration/file-assumptions.js +7 -13
  121. package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
  122. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  123. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  124. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  125. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +17 -19
  126. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  127. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  128. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +6 -3
  129. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +21 -43
  130. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  131. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  132. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +4 -4
  133. package/.agents/scripts/lib/orchestration/merge-block-class.js +77 -21
  134. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  135. package/.agents/scripts/lib/orchestration/phase-runner.js +3 -2
  136. package/.agents/scripts/lib/orchestration/plan-context.js +354 -282
  137. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +11 -22
  138. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +4 -8
  139. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  140. package/.agents/scripts/lib/orchestration/plan-navigation.js +92 -0
  141. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +71 -0
  142. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +245 -0
  143. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  144. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +432 -858
  145. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +778 -0
  146. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +47 -115
  147. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  148. package/.agents/scripts/lib/orchestration/plan-reachability.js +9 -14
  149. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +1 -1
  150. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/authoring-context.js +52 -51
  151. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +27 -0
  152. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/spec-authoring-grounding.js +1 -1
  153. package/.agents/scripts/lib/orchestration/pr-base-guard.js +18 -28
  154. package/.agents/scripts/lib/orchestration/remote-verifier.js +1 -1
  155. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  156. package/.agents/scripts/lib/orchestration/resolves-token.js +1 -1
  157. package/.agents/scripts/lib/orchestration/retro-proposals.js +140 -79
  158. package/.agents/scripts/lib/orchestration/review-depth.js +126 -47
  159. package/.agents/scripts/lib/orchestration/review-providers/codex.js +2 -2
  160. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  161. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  162. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  163. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +21 -56
  164. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  165. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  166. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  167. package/.agents/scripts/lib/orchestration/run-epilogue.js +784 -0
  168. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +25 -1
  169. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  170. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  171. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +86 -41
  172. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  173. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +77 -32
  174. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  175. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  176. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +44 -0
  177. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  178. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  179. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +275 -46
  180. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  181. package/.agents/scripts/lib/orchestration/spec-freshness.js +14 -205
  182. package/.agents/scripts/lib/orchestration/spec-section-validator.js +4 -5
  183. package/.agents/scripts/lib/orchestration/spec-spill.js +60 -0
  184. package/.agents/scripts/lib/orchestration/split-policy-validator.js +188 -0
  185. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +49 -0
  186. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  187. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +28 -42
  188. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  189. package/.agents/scripts/lib/orchestration/story-follow-ups.js +298 -0
  190. package/.agents/scripts/lib/orchestration/story-init-remote.js +51 -0
  191. package/.agents/scripts/lib/orchestration/story-plan-state.js +33 -0
  192. package/.agents/scripts/lib/orchestration/structured-comment-parser.js +1 -1
  193. package/.agents/scripts/lib/orchestration/task-body-validator.js +60 -25
  194. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  195. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +130 -75
  196. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +192 -377
  197. package/.agents/scripts/lib/orchestration/ticket-validator.js +123 -25
  198. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -72
  199. package/.agents/scripts/lib/orchestration/ticketing/reads.js +45 -55
  200. package/.agents/scripts/lib/orchestration/ticketing/transition.js +66 -6
  201. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  202. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  203. package/.agents/scripts/lib/planning-corpus.js +12 -281
  204. package/.agents/scripts/lib/preflight-runner.js +4 -4
  205. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -5
  206. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  207. package/.agents/scripts/lib/signals/index.js +4 -17
  208. package/.agents/scripts/lib/signals/read.js +35 -35
  209. package/.agents/scripts/lib/signals/schema.js +8 -11
  210. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  211. package/.agents/scripts/lib/signals/write.js +0 -1
  212. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  213. package/.agents/scripts/lib/single-story-sweep/protection-ctx.js +1 -1
  214. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  215. package/.agents/scripts/lib/story-adjacency.js +17 -19
  216. package/.agents/scripts/lib/story-body/story-body.js +130 -75
  217. package/.agents/scripts/lib/story-plan.js +2 -4
  218. package/.agents/scripts/lib/templates/decomposer-prompts.js +51 -46
  219. package/.agents/scripts/lib/templates/spec-author-prompts.js +47 -45
  220. package/.agents/scripts/lib/test-env.js +14 -1
  221. package/.agents/scripts/lib/test-tiers.js +0 -3
  222. package/.agents/scripts/lib/{epic-body-sections.js → ticket-body-sections.js} +25 -39
  223. package/.agents/scripts/lib/validation-evidence.js +32 -60
  224. package/.agents/scripts/lib/wave-runner/ready-set.js +38 -12
  225. package/.agents/scripts/lib/workspace-provisioner.js +1 -1
  226. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  227. package/.agents/scripts/lib/worktree/lifecycle/reap.js +72 -25
  228. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  229. package/.agents/scripts/lint-issue-body.js +71 -40
  230. package/.agents/scripts/mandrel-update-preflight.js +1 -1
  231. package/.agents/scripts/notify.js +4 -3
  232. package/.agents/scripts/plan-context.js +102 -80
  233. package/.agents/scripts/plan-persist.js +230 -279
  234. package/.agents/scripts/plan-run-epilogue.js +142 -0
  235. package/.agents/scripts/post-structured-comment.js +0 -38
  236. package/.agents/scripts/pr-watch-with-update.js +43 -22
  237. package/.agents/scripts/providers/github/compose.js +0 -1
  238. package/.agents/scripts/providers/github/errors.js +0 -19
  239. package/.agents/scripts/providers/github/issues.js +13 -39
  240. package/.agents/scripts/providers/github/mappers.js +5 -12
  241. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  242. package/.agents/scripts/providers/github/tickets.js +33 -156
  243. package/.agents/scripts/providers/github.js +17 -6
  244. package/.agents/scripts/resolve-stories.js +236 -0
  245. package/.agents/scripts/run-coverage.js +4 -1
  246. package/.agents/scripts/run-lint.js +2 -2
  247. package/.agents/scripts/run-verify.js +31 -2
  248. package/.agents/scripts/signals-view.js +25 -21
  249. package/.agents/scripts/single-story-close.js +178 -26
  250. package/.agents/scripts/single-story-confirm-merge.js +313 -24
  251. package/.agents/scripts/single-story-init.js +35 -30
  252. package/.agents/scripts/stories-wave-tick.js +85 -10
  253. package/.agents/scripts/story-plan.js +28 -49
  254. package/.agents/scripts/update-ticket-state.js +14 -65
  255. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  256. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  257. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  258. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  259. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  260. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  261. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  262. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  263. package/.agents/skills/core/idea-refinement/SKILL.md +9 -9
  264. package/.agents/skills/core/scope-triage/SKILL.md +31 -172
  265. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  266. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  267. package/.agents/skills/skills.index.json +3 -53
  268. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  269. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  270. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  271. package/.agents/starter-agentrc.json +0 -5
  272. package/.agents/templates/agent-protocol.md +9 -10
  273. package/.agents/workflows/audit-architecture.md +6 -7
  274. package/.agents/workflows/audit-clean-code.md +7 -7
  275. package/.agents/workflows/audit-dependencies.md +3 -3
  276. package/.agents/workflows/audit-devops.md +3 -3
  277. package/.agents/workflows/audit-documentation.md +9 -10
  278. package/.agents/workflows/audit-lighthouse.md +11 -3
  279. package/.agents/workflows/audit-navigability.md +13 -2
  280. package/.agents/workflows/audit-performance.md +5 -6
  281. package/.agents/workflows/audit-privacy.md +3 -3
  282. package/.agents/workflows/audit-quality.md +11 -12
  283. package/.agents/workflows/audit-security.md +4 -5
  284. package/.agents/workflows/audit-seo.md +13 -3
  285. package/.agents/workflows/audit-sre.md +3 -3
  286. package/.agents/workflows/audit-to-stories.md +20 -20
  287. package/.agents/workflows/audit-ux-ui.md +10 -3
  288. package/.agents/workflows/deliver.md +177 -176
  289. package/.agents/workflows/git-cleanup.md +5 -6
  290. package/.agents/workflows/git-deliver.md +1 -1
  291. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  292. package/.agents/workflows/helpers/acceptance-self-eval.md +35 -40
  293. package/.agents/workflows/helpers/code-quality-guardrails.md +7 -7
  294. package/.agents/workflows/helpers/code-review.md +75 -196
  295. package/.agents/workflows/helpers/{single-story-deliver-reference.md → deliver-story-reference.md} +83 -44
  296. package/.agents/workflows/helpers/deliver-story.md +606 -0
  297. package/.agents/workflows/helpers/diagnose.md +10 -10
  298. package/.agents/workflows/helpers/parallel-tooling.md +3 -3
  299. package/.agents/workflows/helpers/signals.md +16 -16
  300. package/.agents/workflows/helpers/worktree-lifecycle.md +66 -86
  301. package/.agents/workflows/mandrel-update.md +2 -1
  302. package/.agents/workflows/plan.md +277 -145
  303. package/.agents/workflows/qa-assist.md +27 -33
  304. package/.agents/workflows/qa-explore.md +29 -38
  305. package/.agents/workflows/qa-run.md +2 -6
  306. package/README.md +9 -8
  307. package/bin/mandrel.js +12 -1
  308. package/docs/CHANGELOG.md +70 -0
  309. package/lib/cli/registry.js +262 -19
  310. package/lib/cli/sync-agents.js +157 -0
  311. package/lib/cli/sync-commands.js +115 -6
  312. package/lib/cli/sync.js +168 -6
  313. package/lib/cli/update.js +105 -8
  314. package/lib/cli/version-helpers.js +131 -0
  315. package/lib/migrations/README.md +7 -5
  316. package/lib/migrations/index.js +12 -8
  317. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  318. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  319. package/package.json +2 -2
  320. package/.agents/agents/retro.md +0 -42
  321. package/.agents/personas/architect.md +0 -113
  322. package/.agents/personas/devops-engineer.md +0 -38
  323. package/.agents/personas/engineer.md +0 -33
  324. package/.agents/personas/project-manager.md +0 -114
  325. package/.agents/personas/qa-engineer.md +0 -95
  326. package/.agents/personas/security-engineer.md +0 -111
  327. package/.agents/personas/technical-writer.md +0 -101
  328. package/.agents/schemas/dispatch-manifest.json +0 -232
  329. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  330. package/.agents/schemas/epic-spec.schema.json +0 -153
  331. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  332. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  333. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  334. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  335. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  336. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  337. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  338. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  339. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  340. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  341. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  342. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  343. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  344. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  345. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  346. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  347. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  348. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  349. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  350. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  351. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  352. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  353. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  354. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  355. package/.agents/schemas/lifecycle/slice.end.schema.json +0 -21
  356. package/.agents/schemas/lifecycle/slice.heartbeat.schema.json +0 -20
  357. package/.agents/schemas/lifecycle/slice.start.schema.json +0 -17
  358. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  359. package/.agents/schemas/risk-verdict.schema.json +0 -66
  360. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  361. package/.agents/scripts/acceptance-spec-reconciler.js +0 -642
  362. package/.agents/scripts/analyze-execution.js +0 -444
  363. package/.agents/scripts/bookkeeping-reconcile.js +0 -117
  364. package/.agents/scripts/check-prepush-recovery.js +0 -90
  365. package/.agents/scripts/dispatcher.js +0 -295
  366. package/.agents/scripts/epic-audit-prepare.js +0 -497
  367. package/.agents/scripts/epic-audit-recheck.js +0 -274
  368. package/.agents/scripts/epic-deliver-note-intervention.js +0 -192
  369. package/.agents/scripts/epic-deliver-preflight.js +0 -462
  370. package/.agents/scripts/epic-deliver-prepare.js +0 -852
  371. package/.agents/scripts/epic-execute-record-wave.js +0 -449
  372. package/.agents/scripts/epic-plan-clarity.js +0 -211
  373. package/.agents/scripts/epic-plan-healthcheck.js +0 -581
  374. package/.agents/scripts/epic-reconcile.js +0 -625
  375. package/.agents/scripts/lib/baseline-snapshot.js +0 -979
  376. package/.agents/scripts/lib/checks/epic-merge-lock-stale.js +0 -54
  377. package/.agents/scripts/lib/checks/stale-origin-epic.js +0 -49
  378. package/.agents/scripts/lib/config/lifecycle.js +0 -40
  379. package/.agents/scripts/lib/config/preflight.js +0 -58
  380. package/.agents/scripts/lib/config/retro.js +0 -77
  381. package/.agents/scripts/lib/epic-merge-lock.js +0 -322
  382. package/.agents/scripts/lib/epic-plan-clarity.js +0 -181
  383. package/.agents/scripts/lib/epic-plan-ideation.js +0 -261
  384. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  385. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  386. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -219
  387. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  388. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  389. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  390. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -270
  391. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +0 -539
  392. package/.agents/scripts/lib/orchestration/deliver-route.js +0 -173
  393. package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -134
  394. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +0 -183
  395. package/.agents/scripts/lib/orchestration/epic-cleanup.js +0 -801
  396. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +0 -310
  397. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +0 -163
  398. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +0 -140
  399. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +0 -64
  400. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +0 -72
  401. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +0 -156
  402. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +0 -345
  403. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +0 -41
  404. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/reconcile-spawn.js +0 -86
  405. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +0 -391
  406. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +0 -94
  407. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +0 -236
  408. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/risk-verdict.js +0 -105
  409. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +0 -307
  410. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +0 -117
  411. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +0 -117
  412. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +0 -621
  413. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +0 -186
  414. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +0 -50
  415. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +0 -129
  416. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +0 -103
  417. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +0 -267
  418. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -210
  419. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +0 -238
  420. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/_bullet-format.js +0 -32
  421. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +0 -203
  422. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +0 -227
  423. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +0 -117
  424. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/stalled-worktree.js +0 -37
  425. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +0 -127
  426. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +0 -400
  427. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +0 -276
  428. package/.agents/scripts/lib/orchestration/epic-runner/wave-scheduler.js +0 -66
  429. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +0 -789
  430. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +0 -676
  431. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-discriminator.js +0 -389
  432. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-format.js +0 -230
  433. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +0 -361
  434. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  435. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +0 -306
  436. package/.agents/scripts/lib/orchestration/finalize/post-handoff-comment.js +0 -489
  437. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +0 -88
  438. package/.agents/scripts/lib/orchestration/lifecycle/emit-slice-lifecycle.js +0 -270
  439. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +0 -147
  440. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  441. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  442. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +0 -465
  443. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +0 -501
  444. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +0 -984
  445. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +0 -264
  446. package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +0 -284
  447. package/.agents/scripts/lib/orchestration/lifecycle/listeners/cleaner.js +0 -355
  448. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +0 -673
  449. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +0 -378
  450. package/.agents/scripts/lib/orchestration/lifecycle/listeners/intervention-recorder.js +0 -140
  451. package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +0 -144
  452. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  453. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +0 -174
  454. package/.agents/scripts/lib/orchestration/manifest-builder.js +0 -222
  455. package/.agents/scripts/lib/orchestration/plan-persist/amend.js +0 -359
  456. package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +0 -127
  457. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  458. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  459. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  460. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  461. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -33
  462. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  463. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  464. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  465. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -396
  466. package/.agents/scripts/lib/orchestration/post-merge-pipeline.js +0 -205
  467. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  468. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +0 -152
  469. package/.agents/scripts/lib/orchestration/retro/phases/checks.js +0 -94
  470. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +0 -571
  471. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +0 -450
  472. package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +0 -191
  473. package/.agents/scripts/lib/orchestration/retro-heuristics.js +0 -57
  474. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  475. package/.agents/scripts/lib/orchestration/retro-runner.js +0 -197
  476. package/.agents/scripts/lib/orchestration/spec-renderer.js +0 -447
  477. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +0 -747
  478. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/gate-failure.js +0 -211
  479. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +0 -158
  480. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +0 -446
  481. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +0 -297
  482. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/scope-discovery.js +0 -48
  483. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution-wiring.js +0 -67
  484. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution.js +0 -161
  485. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +0 -117
  486. package/.agents/scripts/lib/orchestration/story-close/cd-out-guard.js +0 -86
  487. package/.agents/scripts/lib/orchestration/story-close/cleanup-reconciler.js +0 -147
  488. package/.agents/scripts/lib/orchestration/story-close/close-inputs.js +0 -142
  489. package/.agents/scripts/lib/orchestration/story-close/comment-bodies.js +0 -62
  490. package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +0 -658
  491. package/.agents/scripts/lib/orchestration/story-close/merge-subject.js +0 -198
  492. package/.agents/scripts/lib/orchestration/story-close/phases/branch-restore.js +0 -105
  493. package/.agents/scripts/lib/orchestration/story-close/phases/close.js +0 -222
  494. package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +0 -292
  495. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +0 -270
  496. package/.agents/scripts/lib/orchestration/story-close/phases/preflight.js +0 -110
  497. package/.agents/scripts/lib/orchestration/story-close/phases/refresh.js +0 -86
  498. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked-emitter.js +0 -112
  499. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked.js +0 -157
  500. package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +0 -421
  501. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +0 -302
  502. package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +0 -163
  503. package/.agents/scripts/lib/orchestration/story-close-recovery.js +0 -690
  504. package/.agents/scripts/lib/orchestration/wave-marker.js +0 -28
  505. package/.agents/scripts/lib/orchestration/wave-record-io.js +0 -218
  506. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +0 -145
  507. package/.agents/scripts/lib/orchestration/wave-record-projection.js +0 -212
  508. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +0 -111
  509. package/.agents/scripts/lib/presentation/manifest-builder.js +0 -239
  510. package/.agents/scripts/lib/presentation/manifest-formatter.js +0 -242
  511. package/.agents/scripts/lib/presentation/manifest-helpers.js +0 -213
  512. package/.agents/scripts/lib/presentation/manifest-persistence.js +0 -261
  513. package/.agents/scripts/lib/presentation/manifest-procedures.js +0 -55
  514. package/.agents/scripts/lib/presentation/manifest-render-waves.js +0 -306
  515. package/.agents/scripts/lib/presentation/manifest-renderer.js +0 -188
  516. package/.agents/scripts/lib/presentation/manifest-story-views.js +0 -110
  517. package/.agents/scripts/lib/push-epic-retry.js +0 -209
  518. package/.agents/scripts/lib/spec/index.js +0 -36
  519. package/.agents/scripts/lib/spec/loader.js +0 -425
  520. package/.agents/scripts/lib/spec/state.js +0 -208
  521. package/.agents/scripts/lib/story-init/blocker-validator.js +0 -68
  522. package/.agents/scripts/lib/story-init/branch-initializer.js +0 -408
  523. package/.agents/scripts/lib/story-init/context-resolver.js +0 -92
  524. package/.agents/scripts/lib/story-init/donor-precheck.js +0 -207
  525. package/.agents/scripts/lib/story-init/state-transitioner.js +0 -80
  526. package/.agents/scripts/lib/story-init/task-graph-builder.js +0 -124
  527. package/.agents/scripts/lib/story-init/transition-summary.js +0 -34
  528. package/.agents/scripts/lib/test-reserved-epic-temp-ids.js +0 -35
  529. package/.agents/scripts/lib/wave-runner/tick.js +0 -754
  530. package/.agents/scripts/lib/wave-runner/wave-runner-error.js +0 -20
  531. package/.agents/scripts/lifecycle-emit-story-dispatch.js +0 -194
  532. package/.agents/scripts/lifecycle-emit.js +0 -510
  533. package/.agents/scripts/retro-run.js +0 -218
  534. package/.agents/scripts/slice-phase.js +0 -361
  535. package/.agents/scripts/standalone-feedback-rollup.js +0 -188
  536. package/.agents/scripts/story-close.js +0 -294
  537. package/.agents/scripts/story-init.js +0 -599
  538. package/.agents/scripts/story-phase.js +0 -369
  539. package/.agents/scripts/wave-tick.js +0 -464
  540. package/.agents/skills/core/analyze-execution/SKILL.md +0 -98
  541. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +0 -172
  542. package/.agents/skills/core/epic-plan-consolidate/examples.md +0 -51
  543. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +0 -441
  544. package/.agents/skills/core/epic-plan-decompose-author/examples.md +0 -47
  545. package/.agents/skills/core/epic-plan-premortem/SKILL.md +0 -146
  546. package/.agents/skills/core/epic-plan-premortem/examples.md +0 -53
  547. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +0 -383
  548. package/.agents/skills/core/epic-plan-spec-author/examples.md +0 -91
  549. package/.agents/workflows/helpers/deliver-epic-reference.md +0 -547
  550. package/.agents/workflows/helpers/deliver-epic-single.md +0 -331
  551. package/.agents/workflows/helpers/deliver-epic.md +0 -998
  552. package/.agents/workflows/helpers/deliver-stories.md +0 -450
  553. package/.agents/workflows/helpers/epic-audit.md +0 -189
  554. package/.agents/workflows/helpers/epic-deliver-story.md +0 -436
  555. package/.agents/workflows/helpers/epic-testing.md +0 -125
  556. package/.agents/workflows/helpers/plan-epic-reference.md +0 -160
  557. package/.agents/workflows/helpers/plan-epic.md +0 -353
  558. package/.agents/workflows/helpers/plan-story.md +0 -251
  559. package/.agents/workflows/helpers/scope-triage-gate.md +0 -108
  560. package/.agents/workflows/helpers/single-story-deliver.md +0 -557
@@ -1,813 +0,0 @@
1
- /**
2
- * Performance signal aggregator (Epic #1030 / Story #1123; rewrite under
3
- * Epic #1181 / Story #1438 / Task #1460).
4
- *
5
- * Pure functions that turn the per-Story `signals.ndjson` stream into the
6
- * structured payloads posted by `analyze-execution.js`:
7
- *
8
- * - `computeStoryPerfSummary(events, opts)` → `<!-- structured:story-perf-summary -->`
9
- * - `computeEpicPerfReport(perStorySummaries, opts)` → `<!-- structured:epic-perf-report -->`
10
- *
11
- * Both take a materialised event iterable; the caller (`analyze-execution.js`)
12
- * owns NDJSON ingestion via `lib/signals/read.js`. (The former
13
- * streaming store-reading variants were retired in the Epic #4406
14
- * signal-contract cutover — they had no callers.)
15
- *
16
- * Schemas:
17
- * - `.agents/schemas/story-perf-summary.schema.json`
18
- * - `.agents/schemas/epic-perf-report.schema.json`
19
- *
20
- * Robustness contract:
21
- * - Both helpers tolerate empty / partial input. Empty streams produce a
22
- * well-formed payload with zeroed counters and empty arrays so the
23
- * analyzer can still upsert a comment without throwing.
24
- * - Malformed events (missing `kind`, non-object payload) are silently
25
- * skipped; the caller is responsible for reading them off the wire and
26
- * deciding whether to log. The aggregator never throws on bad data.
27
- * - Numeric fields are floored to non-negative integers so the schemas
28
- * (`integer`, `minimum: 0`) hold by construction.
29
- *
30
- * NDJSON ingestion discipline (Epic #1181):
31
- * - Field-name literals for event `kind` come from
32
- * `lib/signals/schema.js` so writer ↔ reader names stay in lockstep.
33
- * - All file I/O for `signals.ndjson` goes through `lib/signals/read.js`
34
- * (no direct `readFileSync` / `createReadStream` on signals.ndjson in
35
- * this module). A grep gate in `tests/lib/checks/` enforces this on
36
- * CI.
37
- */
38
-
39
- import { isObject } from '../json-utils.js';
40
- import { EVENT_KINDS } from '../signals/schema.js';
41
-
42
- const FRICTION_KIND = EVENT_KINDS.FRICTION;
43
- const HOTSPOT_KIND = EVENT_KINDS.HOTSPOT;
44
- const REWORK_KIND = EVENT_KINDS.REWORK;
45
- const RETRY_KIND = EVENT_KINDS.RETRY;
46
- const SIGNAL_COUNT_KINDS = Object.freeze([
47
- EVENT_KINDS.FRICTION,
48
- EVENT_KINDS.HOTSPOT,
49
- EVENT_KINDS.REWORK,
50
- EVENT_KINDS.CHURN,
51
- EVENT_KINDS.IDLE,
52
- EVENT_KINDS.RETRY,
53
- ]);
54
-
55
- function nonNegativeInt(v) {
56
- const n = Number(v);
57
- if (!Number.isFinite(n) || n < 0) return 0;
58
- return Math.floor(n);
59
- }
60
-
61
- function nonNegativeNumber(v) {
62
- const n = Number(v);
63
- if (!Number.isFinite(n) || n < 0) return 0;
64
- return n;
65
- }
66
-
67
- /**
68
- * Pull friction-by-category counts off a list of NDJSON events. Keys are
69
- * the **top-level** `category` strings (Epic #4406 canonical shape); a
70
- * record with no top-level category buckets under `Unknown`. Reading the
71
- * top-level key (not `details.category`) is what un-zeroes the report —
72
- * every writer emits `category` at the envelope top level.
73
- *
74
- * @param {Iterable<object>} events
75
- * @returns {Object<string, number>}
76
- */
77
- function frictionByCategory(events) {
78
- const out = {};
79
- for (const evt of events) {
80
- if (!isObject(evt) || evt.kind !== FRICTION_KIND) continue;
81
- const category =
82
- typeof evt.category === 'string' && evt.category.length > 0
83
- ? evt.category
84
- : 'Unknown';
85
- out[category] = (out[category] ?? 0) + 1;
86
- }
87
- return out;
88
- }
89
-
90
- /**
91
- * Build the `topSlowPhasesVsBaseline` array. We accept hotspot signals
92
- * carrying `{ phase, elapsedMs, baselineP95Ms, ratio }` in `details` and
93
- * surface them sorted by ratio descending. The hotspot detector is a
94
- * future Epic-#1030 Story; until it lands the input list is empty and
95
- * this returns `[]`.
96
- *
97
- * @param {Iterable<object>} events
98
- * @param {{ limit?: number }} [opts]
99
- * @returns {Array<{phase: string, elapsedMs: number, baselineP95Ms: number, ratio: number}>}
100
- */
101
- function topSlowPhasesVsBaseline(events, opts = {}) {
102
- const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : 5;
103
- const rows = [];
104
- for (const evt of events) {
105
- if (!isObject(evt) || evt.kind !== HOTSPOT_KIND) continue;
106
- const d = isObject(evt.details) ? evt.details : {};
107
- const phase =
108
- typeof evt.phase === 'string' && evt.phase.length > 0
109
- ? evt.phase
110
- : typeof d.phase === 'string' && d.phase.length > 0
111
- ? d.phase
112
- : null;
113
- if (!phase) continue;
114
- rows.push({
115
- phase,
116
- elapsedMs: nonNegativeInt(d.elapsedMs),
117
- baselineP95Ms: nonNegativeInt(d.baselineP95Ms),
118
- ratio: nonNegativeNumber(d.ratio),
119
- });
120
- }
121
- rows.sort((a, b) => b.ratio - a.ratio);
122
- return rows.slice(0, limit);
123
- }
124
-
125
- /**
126
- * Build the `reworkScore` object: `{ filesEditedBeyondThreshold, topPath?,
127
- * topPathEdits? }`. We aggregate `kind: 'rework'` signals whose details
128
- * carry a `targetHash` and an `editCount` — the exact keys
129
- * `detectors/rework.js` emits (Epic #4406 canonical shape). `topPath` is
130
- * the offending `targetHash` (a sha256; the raw path never reaches the
131
- * aggregator by the privacy contract). When the input has no rework
132
- * signals we return the zero-shape: `{ filesEditedBeyondThreshold: 0 }`.
133
- *
134
- * @param {Iterable<object>} events
135
- * @returns {{ filesEditedBeyondThreshold: number, topPath?: string|null, topPathEdits?: number|null }}
136
- */
137
- function reworkScore(events) {
138
- const editsByPath = new Map();
139
- for (const evt of events) {
140
- if (!isObject(evt) || evt.kind !== REWORK_KIND) continue;
141
- const d = isObject(evt.details) ? evt.details : {};
142
- const p =
143
- typeof d.targetHash === 'string' && d.targetHash.length > 0
144
- ? d.targetHash
145
- : null;
146
- if (!p) continue;
147
- const edits = nonNegativeInt(d.editCount);
148
- editsByPath.set(p, Math.max(editsByPath.get(p) ?? 0, edits));
149
- }
150
- if (editsByPath.size === 0) {
151
- return { filesEditedBeyondThreshold: 0 };
152
- }
153
- let topPath = null;
154
- let topPathEdits = 0;
155
- for (const [p, n] of editsByPath) {
156
- if (n > topPathEdits) {
157
- topPath = p;
158
- topPathEdits = n;
159
- }
160
- }
161
- return {
162
- filesEditedBeyondThreshold: editsByPath.size,
163
- topPath,
164
- topPathEdits,
165
- };
166
- }
167
-
168
- /**
169
- * Build the `retryDensity` object: `{ retries, uniqueCommands }`. Sums
170
- * `kind: 'retry'` signals; `uniqueCommands` is the hash-cardinality of the
171
- * distinct `details.commandHash` values observed — the exact key
172
- * `detectors/retry.js` emits (Epic #4406 canonical shape). The raw command
173
- * never reaches the aggregator by the privacy contract, so we count
174
- * distinct hashes. Zero-shape on empty input.
175
- *
176
- * @param {Iterable<object>} events
177
- * @returns {{ retries: number, uniqueCommands: number }}
178
- */
179
- function retryDensity(events) {
180
- let retries = 0;
181
- const commandHashes = new Set();
182
- for (const evt of events) {
183
- if (!isObject(evt) || evt.kind !== RETRY_KIND) continue;
184
- const d = isObject(evt.details) ? evt.details : {};
185
- retries += 1;
186
- if (typeof d.commandHash === 'string' && d.commandHash.length > 0) {
187
- commandHashes.add(d.commandHash);
188
- }
189
- }
190
- return { retries, uniqueCommands: commandHashes.size };
191
- }
192
-
193
- /**
194
- * Convert a phase-timer summary `{ phases: [{ name, elapsedMs }, ...] }`
195
- * into the flat `{ <name>: <ms> }` map the schema wants. Last entry wins
196
- * if a phase appears twice (mark/finish boundaries).
197
- *
198
- * @param {{ phases?: Array<{ name: string, elapsedMs: number }> } | null | undefined} timing
199
- * @returns {Object<string, number>}
200
- */
201
- function phaseTimingsMs(timing) {
202
- if (!isObject(timing) || !Array.isArray(timing.phases)) return {};
203
- const out = {};
204
- for (const p of timing.phases) {
205
- if (!isObject(p)) continue;
206
- if (typeof p.name !== 'string' || p.name.length === 0) continue;
207
- out[p.name] = nonNegativeInt(p.elapsedMs);
208
- }
209
- return out;
210
- }
211
-
212
- /**
213
- * Compute the StoryPerfSummary payload from a list of NDJSON events
214
- * sampled out of `temp/epic-<eid>/stories/story-<sid>/signals.ndjson` plus an
215
- * optional phase-timer summary.
216
- *
217
- * @param {Iterable<object>} events
218
- * @param {{ storyId: number, epicId: number, closedAt?: string, phaseTiming?: object|null }} opts
219
- * @returns {object} StoryPerfSummary payload (schema: story-perf-summary)
220
- */
221
- export function computeStoryPerfSummary(events, opts) {
222
- if (!isObject(opts)) {
223
- throw new TypeError('computeStoryPerfSummary: opts is required');
224
- }
225
- const storyId = Number(opts.storyId);
226
- const epicId = Number(opts.epicId);
227
- if (!Number.isInteger(storyId) || storyId < 1) {
228
- throw new RangeError(
229
- `computeStoryPerfSummary: storyId must be a positive integer (got ${opts.storyId})`,
230
- );
231
- }
232
- if (!Number.isInteger(epicId) || epicId < 1) {
233
- throw new RangeError(
234
- `computeStoryPerfSummary: epicId must be a positive integer (got ${opts.epicId})`,
235
- );
236
- }
237
- const closedAt =
238
- typeof opts.closedAt === 'string' && opts.closedAt.length > 0
239
- ? opts.closedAt
240
- : new Date().toISOString();
241
-
242
- // Materialise the iterable so each helper can scan independently.
243
- const evtArr = [];
244
- for (const e of events ?? []) {
245
- if (isObject(e) && typeof e.kind === 'string') evtArr.push(e);
246
- }
247
-
248
- return {
249
- kind: 'story-perf-summary',
250
- storyId,
251
- epicId,
252
- closedAt,
253
- frictionByCategory: frictionByCategory(evtArr),
254
- phaseTimingsMs: phaseTimingsMs(opts.phaseTiming),
255
- topSlowPhasesVsBaseline: topSlowPhasesVsBaseline(evtArr),
256
- reworkScore: reworkScore(evtArr),
257
- retryDensity: retryDensity(evtArr),
258
- };
259
- }
260
-
261
- /**
262
- * Compute the EpicPerfReport payload from a list of per-Story summaries
263
- * (each shaped like `computeStoryPerfSummary`'s return value) plus an
264
- * optional list of raw events for signal-count rollup.
265
- *
266
- * `signalCounts` rolls up across **events**, not summaries — a Story's
267
- * `frictionByCategory` only carries friction (the named slice the schema
268
- * surfaces), but the Epic-level rollup wants every kind. When `opts.events`
269
- * is absent we fall back to summing each summary's friction count and
270
- * leave the other kinds at 0.
271
- *
272
- * @param {Iterable<object>} perStorySummaries
273
- * @param {{ epicId: number, generatedAt?: string, events?: Iterable<object>, waveParallelism?: Array<object>, topHotspots?: Array<object> }} opts
274
- * @returns {object} EpicPerfReport payload (schema: epic-perf-report)
275
- */
276
- /**
277
- * Predicate / collector: walk a `perStorySummaries` iterable and emit
278
- * only the entries whose `kind === 'story-perf-summary'`. Extracted from
279
- * `computeEpicPerfReport` so the input-validation cascade is independently
280
- * testable and the parent stays straight-line. Returns `[]` for nullish
281
- * inputs, which matches the parent's prior behaviour.
282
- *
283
- * @param {Iterable<object>|null|undefined} perStorySummaries
284
- * @returns {object[]}
285
- */
286
- export function collectValidStorySamples(perStorySummaries) {
287
- const out = [];
288
- if (!perStorySummaries) return out;
289
- for (const s of perStorySummaries) {
290
- if (isObject(s) && s.kind === 'story-perf-summary') out.push(s);
291
- }
292
- return out;
293
- }
294
-
295
- /**
296
- * Build the `signalCounts` block. When `events` is supplied we roll up
297
- * across every kind in `SIGNAL_COUNT_KINDS`; otherwise we sum the
298
- * per-Story friction counts so the legacy summary-only path keeps the
299
- * same friction total.
300
- *
301
- * @param {Iterable<object>|null|undefined} events
302
- * @param {object[]} summaries
303
- * @returns {{friction: number, hotspot: number, rework: number, churn: number, idle: number, retry: number}}
304
- */
305
- function buildSignalCounts(events, summaries) {
306
- const counts = {
307
- friction: 0,
308
- hotspot: 0,
309
- rework: 0,
310
- churn: 0,
311
- idle: 0,
312
- retry: 0,
313
- };
314
- if (events) {
315
- for (const evt of events) {
316
- if (!isObject(evt) || typeof evt.kind !== 'string') continue;
317
- if (SIGNAL_COUNT_KINDS.includes(evt.kind)) {
318
- counts[evt.kind] += 1;
319
- }
320
- }
321
- return counts;
322
- }
323
- for (const s of summaries) {
324
- if (!isObject(s.frictionByCategory)) continue;
325
- for (const v of Object.values(s.frictionByCategory)) {
326
- counts.friction += nonNegativeInt(v);
327
- }
328
- }
329
- return counts;
330
- }
331
-
332
- /**
333
- * Aggregate the `topHotspots` block from per-Story samples: group each
334
- * story's `topSlowPhasesVsBaseline` rows by phase, count occurrences,
335
- * average the ratio, then sort by `occurrences desc, avgRatio desc` and
336
- * cap at 5. Extracted from `computeEpicPerfReport` so the parent stays
337
- * straight-line; callers that pass `opts.topHotspots` skip this helper
338
- * entirely.
339
- *
340
- * @param {object[]} summaries
341
- * @returns {Array<{phase: string, occurrences: number, avgRatio: number}>}
342
- */
343
- function aggregateTopHotspots(summaries) {
344
- const acc = new Map();
345
- for (const s of summaries) {
346
- const arr = Array.isArray(s.topSlowPhasesVsBaseline)
347
- ? s.topSlowPhasesVsBaseline
348
- : [];
349
- for (const row of arr) {
350
- if (!isObject(row) || typeof row.phase !== 'string') continue;
351
- const rec = acc.get(row.phase) ?? {
352
- phase: row.phase,
353
- occurrences: 0,
354
- ratioSum: 0,
355
- };
356
- rec.occurrences += 1;
357
- rec.ratioSum += nonNegativeNumber(row.ratio);
358
- acc.set(row.phase, rec);
359
- }
360
- }
361
- return [...acc.values()]
362
- .map((r) => ({
363
- phase: r.phase,
364
- occurrences: r.occurrences,
365
- avgRatio: r.occurrences > 0 ? r.ratioSum / r.occurrences : 0,
366
- }))
367
- .sort((a, b) => b.occurrences - a.occurrences || b.avgRatio - a.avgRatio)
368
- .slice(0, 5);
369
- }
370
-
371
- /**
372
- * Default verify-concurrency cap when the caller does not override it.
373
- * Mirrors the default for `delivery.deliverRunner.verifyConcurrencyCap`
374
- * in `.agentrc.json` (Epic #3019 Tech Spec §1.4).
375
- */
376
- const DEFAULT_VERIFY_CONCURRENCY_CAP = 4;
377
-
378
- /**
379
- * Default wave-execution concurrency cap used when the caller does not
380
- * supply `concurrencyCap` to {@link computeWaveParallelismRows}. The
381
- * project default (`delivery.deliverRunner.concurrencyCap`) is 2 today;
382
- * the value here is the safe fallback for offline / test contexts.
383
- */
384
- const DEFAULT_WAVE_CONCURRENCY_CAP = 2;
385
-
386
- function tsOf(evt) {
387
- return evt?.ts ?? null;
388
- }
389
-
390
- function tsToMs(ts) {
391
- if (typeof ts !== 'string') return null;
392
- const n = Date.parse(ts);
393
- return Number.isFinite(n) ? n : null;
394
- }
395
-
396
- function storyIdOf(evt) {
397
- const n = Number(evt?.storyId);
398
- return Number.isInteger(n) && n > 0 ? n : null;
399
- }
400
-
401
- /**
402
- * Materialise an event iterable into an array of `{ evt, ms }` records,
403
- * parsing each event's timestamp **exactly once** (Story #3343). Events
404
- * with a non-string `kind` are dropped (matching the prior inline guard);
405
- * the `ms` field is `null` when the timestamp is missing or unparseable so
406
- * downstream passes can skip it without re-parsing.
407
- *
408
- * @param {Iterable<object>} events
409
- * @returns {Array<{ evt: object, ms: number|null }>}
410
- */
411
- function materialiseTimedEvents(events) {
412
- const out = [];
413
- for (const evt of events ?? []) {
414
- if (isObject(evt) && typeof evt.kind === 'string') {
415
- out.push({ evt, ms: tsToMs(tsOf(evt)) });
416
- }
417
- }
418
- return out;
419
- }
420
-
421
- /**
422
- * Index Story state-transition windows from pre-timed events: first
423
- * `agent::executing` → last terminal (`agent::done` | `agent::blocked` |
424
- * `agent::failed`). Reuses the per-event `ms` parsed by
425
- * {@link materialiseTimedEvents}. Extracted from
426
- * {@link computeWaveParallelismRows} (Story #3343).
427
- *
428
- * @param {Array<{ evt: object, ms: number|null }>} timedEvents
429
- * @returns {Map<number, { startMs: number|null, endMs: number|null }>}
430
- */
431
- function indexStoryWindows(timedEvents) {
432
- const storyWindows = new Map();
433
- for (const { evt, ms } of timedEvents) {
434
- if (evt.kind !== 'state-transition') continue;
435
- const sid = storyIdOf(evt);
436
- if (sid == null) continue;
437
- if (ms == null) continue;
438
- const to =
439
- (isObject(evt.details) && evt.details.to) ?? evt.to ?? evt.toState;
440
- const rec = storyWindows.get(sid) ?? { startMs: null, endMs: null };
441
- if (to === 'agent::executing') {
442
- if (rec.startMs == null || ms < rec.startMs) rec.startMs = ms;
443
- } else if (
444
- to === 'agent::done' ||
445
- to === 'agent::blocked' ||
446
- to === 'agent::failed'
447
- ) {
448
- if (rec.endMs == null || ms > rec.endMs) rec.endMs = ms;
449
- }
450
- storyWindows.set(sid, rec);
451
- }
452
- return storyWindows;
453
- }
454
-
455
- /**
456
- * Bucket `wave-start` / `wave-complete` events by index from pre-timed
457
- * events. Reuses the per-event `ms` parsed by
458
- * {@link materialiseTimedEvents}. Extracted from
459
- * {@link computeWaveParallelismRows} (Story #3343).
460
- *
461
- * @param {Array<{ evt: object, ms: number|null }>} timedEvents
462
- * @returns {Map<number, { startMs: number|null, endMs: number|null, stories: number[] }>}
463
- */
464
- function bucketWaves(timedEvents) {
465
- const waves = new Map();
466
- for (const { evt, ms } of timedEvents) {
467
- if (ms == null) continue;
468
- if (evt.kind === 'wave-start') {
469
- const idx = Number(evt.index);
470
- if (!Number.isInteger(idx) || idx < 0) continue;
471
- const storiesField = Array.isArray(evt.stories) ? evt.stories : [];
472
- const storyIds = storiesField
473
- .map((s) => {
474
- const n = Number(isObject(s) ? (s.id ?? s.storyId) : s);
475
- return Number.isInteger(n) && n > 0 ? n : null;
476
- })
477
- .filter((n) => n != null);
478
- const rec = waves.get(idx) ?? {
479
- startMs: null,
480
- endMs: null,
481
- stories: [],
482
- };
483
- if (rec.startMs == null || ms < rec.startMs) rec.startMs = ms;
484
- rec.stories = storyIds;
485
- waves.set(idx, rec);
486
- } else if (evt.kind === 'wave-complete') {
487
- const idx = Number(evt.index);
488
- if (!Number.isInteger(idx) || idx < 0) continue;
489
- const rec = waves.get(idx) ?? {
490
- startMs: null,
491
- endMs: null,
492
- stories: [],
493
- };
494
- if (rec.endMs == null || ms > rec.endMs) rec.endMs = ms;
495
- waves.set(idx, rec);
496
- }
497
- }
498
- return waves;
499
- }
500
-
501
- /**
502
- * Largest value `< hi` that is `>= lo` in a sorted ascending array, or
503
- * `null` when the half-open window `[lo, hi)` contains no element. Pure
504
- * binary search — used by {@link fillMissingWaveEnds} to find a wave's
505
- * fallback terminator without re-scanning the whole event array per wave.
506
- *
507
- * @param {number[]} sortedMs ascending
508
- * @param {number} lo inclusive lower bound
509
- * @param {number} hi exclusive upper bound (may be Infinity)
510
- * @returns {number|null}
511
- */
512
- function maxInWindow(sortedMs, lo, hi) {
513
- // Find the first index with value >= hi (upper bound), then step back to
514
- // the last element strictly below hi.
515
- let left = 0;
516
- let right = sortedMs.length;
517
- while (left < right) {
518
- const mid = (left + right) >> 1;
519
- if (sortedMs[mid] < hi) left = mid + 1;
520
- else right = mid;
521
- }
522
- const candidate = left - 1; // last index with value < hi
523
- if (candidate < 0) return null;
524
- const val = sortedMs[candidate];
525
- return val >= lo ? val : null;
526
- }
527
-
528
- /**
529
- * Fill the `endMs` of any wave that never observed a `wave-complete`:
530
- * each such wave's terminator is the max event timestamp in the half-open
531
- * window `[startMs, nextStartMs)`, where `nextStartMs` is the start of the
532
- * next wave by ascending index (or `Infinity` for the last wave).
533
- *
534
- * Replaces the prior O(waves × events) nested scan with a single sort of
535
- * the already-parsed timestamps plus one binary search per gap-wave
536
- * (Story #3343). Output is byte-identical to the prior implementation.
537
- *
538
- * @param {Array<[number, { startMs: number|null, endMs: number|null, stories: number[] }]>} orderedWaves sorted by index asc
539
- * @param {Array<{ evt: object, ms: number|null }>} timedEvents
540
- * @returns {void} mutates the wave records in `orderedWaves` in place
541
- */
542
- function fillMissingWaveEnds(orderedWaves, timedEvents) {
543
- const needsFill = orderedWaves.some(
544
- ([, rec]) => rec.endMs == null && rec.startMs != null,
545
- );
546
- if (!needsFill) return;
547
- const sortedMs = [];
548
- for (const { ms } of timedEvents) {
549
- if (ms != null) sortedMs.push(ms);
550
- }
551
- sortedMs.sort((a, b) => a - b);
552
- for (let i = 0; i < orderedWaves.length; i += 1) {
553
- const [, rec] = orderedWaves[i];
554
- if (rec.endMs != null) continue;
555
- const startMs = rec.startMs;
556
- if (startMs == null) continue;
557
- const nextStartMs =
558
- i + 1 < orderedWaves.length ? orderedWaves[i + 1][1].startMs : Infinity;
559
- const maxMs = maxInWindow(sortedMs, startMs, nextStartMs);
560
- rec.endMs = maxMs == null ? startMs : Math.max(startMs, maxMs);
561
- }
562
- }
563
-
564
- /**
565
- * Compute per-wave parallelism rows from a chronological iterable of
566
- * lifecycle events (Task #3028, Epic #3019 / Story #3025).
567
- *
568
- * Wave windows are bracketed by `wave-start` (carrying `index` +
569
- * `stories[]`) and the matching `wave-complete` (same `index`). When no
570
- * `wave-complete` is observed for a wave, the wave's wallClockMs falls
571
- * back to the timestamp of the last in-wave event observed (so partial
572
- * runs still emit a row). When `wave-start` is missing entirely we emit
573
- * no row for that wave.
574
- *
575
- * Per-Story durations within a wave come from `state-transition` events
576
- * (`agent::executing` → `agent::done`) for the Story IDs the
577
- * `wave-start` payload enumerated. We bracket the **first**
578
- * `executing` transition and the **last** terminal transition (`done`,
579
- * `blocked`, or `failed`) per Story; if a Story is missing one boundary
580
- * its contribution to `summedStoryMs` is 0.
581
- *
582
- * Field contract (per the extended `epic-perf-report.schema.json`):
583
- * - `waveIndex`: integer ≥ 0, from `wave-start.index`
584
- * - `storyCount`: integer ≥ 0, number of Stories in the wave (from
585
- * `wave-start.stories`). Added under Story #3850.
586
- * - `wallClockMs`: integer ≥ 0, `(waveEnd - waveStart)` in ms
587
- * - `summedStoryMs`: integer ≥ 0, Σ per-Story `(end - start)`
588
- * - `utilisation`: `summedStoryMs / (wallClockMs * effectiveCap)`,
589
- * where `effectiveCap = min(storyCount, concurrencyCap)`, clamped
590
- * to `[0, 1]`. Zero when `wallClockMs === 0` or effectiveCap is 0.
591
- * Using the effective (not raw) cap means a fully-busy 1-Story wave
592
- * scores 1.0 rather than `1/cap`, eliminating false-positive
593
- * `low-utilisation` signals on serialized/narrow waves (Story #3850).
594
- * - `capBinding`: true when `summedStoryMs / wallClockMs >=
595
- * concurrencyCap`, false otherwise (and false when wallClockMs
596
- * is 0). Still uses the raw cap so the signal fires when the
597
- * configured parallelism ceiling is actually saturated.
598
- * - `verifyConcurrencyCap`: forwarded from `opts.verifyConcurrencyCap`
599
- * (or the project default 4) so the post-merge close comment can
600
- * attribute saturation back to the cap value in force at the time.
601
- *
602
- * @param {Iterable<object>} events
603
- * @param {{
604
- * concurrencyCap?: number,
605
- * verifyConcurrencyCap?: number,
606
- * }} [opts]
607
- * @returns {Array<{
608
- * waveIndex: number,
609
- * storyCount: number,
610
- * wallClockMs: number,
611
- * summedStoryMs: number,
612
- * utilisation: number,
613
- * capBinding: boolean,
614
- * verifyConcurrencyCap: number,
615
- * }>}
616
- */
617
- export function computeWaveParallelismRows(events, opts = {}) {
618
- const concurrencyCap =
619
- Number.isInteger(opts.concurrencyCap) && opts.concurrencyCap >= 1
620
- ? opts.concurrencyCap
621
- : DEFAULT_WAVE_CONCURRENCY_CAP;
622
- const verifyConcurrencyCap =
623
- Number.isInteger(opts.verifyConcurrencyCap) &&
624
- opts.verifyConcurrencyCap >= 1
625
- ? opts.verifyConcurrencyCap
626
- : DEFAULT_VERIFY_CONCURRENCY_CAP;
627
-
628
- // Materialise the iterable once, parsing each event's timestamp a
629
- // single time so every downstream pass reuses the same `ms` value
630
- // (Story #3343). Events are typically a few thousand per Epic at most.
631
- const timedEvents = materialiseTimedEvents(events);
632
-
633
- // Index Story state-transition windows: first `agent::executing` →
634
- // last terminal (`agent::done` | `agent::blocked` | `agent::failed`).
635
- const storyWindows = indexStoryWindows(timedEvents);
636
-
637
- // Bucket wave-start / wave-complete events by index, then fill any wave
638
- // that never saw `wave-complete` via a single sorted-timestamp sweep
639
- // (replacing the prior O(waves × events) nested scan).
640
- const waves = bucketWaves(timedEvents);
641
- const orderedWaves = [...waves.entries()].sort((a, b) => a[0] - b[0]);
642
- fillMissingWaveEnds(orderedWaves, timedEvents);
643
-
644
- // Build rows.
645
- const rows = [];
646
- for (const [idx, rec] of orderedWaves) {
647
- if (rec.startMs == null) continue;
648
- const wallClockMs = Math.max(
649
- 0,
650
- Math.floor((rec.endMs ?? rec.startMs) - rec.startMs),
651
- );
652
- const storyCount = rec.stories.length;
653
- let summedStoryMs = 0;
654
- for (const sid of rec.stories) {
655
- const w = storyWindows.get(sid);
656
- if (!w || w.startMs == null || w.endMs == null) continue;
657
- const dur = w.endMs - w.startMs;
658
- if (Number.isFinite(dur) && dur > 0) summedStoryMs += Math.floor(dur);
659
- }
660
- // Use the effective cap — min(storyCount, concurrencyCap) — as the
661
- // utilisation denominator so a fully-busy 1-Story wave scores 1.0
662
- // instead of 1/cap. This eliminates false-positive `low-utilisation`
663
- // signals on serialized / narrow-wave Epics (Story #3850).
664
- // capBinding still uses the raw concurrencyCap because it signals that
665
- // the configured parallelism ceiling is genuinely saturated.
666
- const effectiveCap = Math.min(storyCount, concurrencyCap);
667
- let utilisation = 0;
668
- let capBinding = false;
669
- if (wallClockMs > 0 && effectiveCap > 0) {
670
- utilisation = clamp(summedStoryMs / (wallClockMs * effectiveCap), 0, 1);
671
- }
672
- if (wallClockMs > 0 && concurrencyCap > 0) {
673
- capBinding = summedStoryMs / wallClockMs >= concurrencyCap;
674
- }
675
- rows.push({
676
- waveIndex: idx,
677
- storyCount,
678
- wallClockMs,
679
- summedStoryMs,
680
- utilisation,
681
- capBinding,
682
- verifyConcurrencyCap,
683
- });
684
- }
685
-
686
- return rows;
687
- }
688
-
689
- /**
690
- * Clamp `n` to the inclusive range [lo, hi]. Returns `lo` for NaN /
691
- * non-finite inputs so the coercer stays well-behaved on garbage.
692
- *
693
- * @param {number} n
694
- * @param {number} lo
695
- * @param {number} hi
696
- * @returns {number}
697
- */
698
- function clamp(n, lo, hi) {
699
- if (!Number.isFinite(n)) return lo;
700
- if (n < lo) return lo;
701
- if (n > hi) return hi;
702
- return n;
703
- }
704
-
705
- /**
706
- * Coerce a single waveParallelism input row into the schema-canonical
707
- * shape `{ waveIndex, storyCount, wallClockMs, summedStoryMs, utilisation,
708
- * capBinding, verifyConcurrencyCap }` (Story #3025; storyCount added
709
- * Story #3850).
710
- *
711
- * - Numeric fields are floored to non-negative integers (or clamped
712
- * numbers for utilisation) so the JSON-schema `integer` / `minimum: 0`
713
- * constraints hold by construction.
714
- * - `utilisation` is clamped to `[0, 1]` per the Tech Spec
715
- * contract (§1.1).
716
- * - `capBinding` is coerced to boolean.
717
- * - `verifyConcurrencyCap` falls back to the project default (4) when the
718
- * caller omits it or supplies a non-positive integer; the schema
719
- * requires `minimum: 1` so 0 is not a valid carrier value.
720
- * - `storyCount` falls back to 0 when absent (older payloads predating
721
- * Story #3850 do not carry the field).
722
- *
723
- * Exported for unit-testing the coercer in isolation.
724
- *
725
- * @param {object | null | undefined} row
726
- * @returns {{ waveIndex: number, storyCount: number, wallClockMs: number, summedStoryMs: number, utilisation: number, capBinding: boolean, verifyConcurrencyCap: number }}
727
- */
728
- export function coerceWaveParallelismRow(row) {
729
- const src = isObject(row) ? row : {};
730
- const cap = Number(src.verifyConcurrencyCap);
731
- const verifyConcurrencyCap =
732
- Number.isInteger(cap) && cap >= 1 ? cap : DEFAULT_VERIFY_CONCURRENCY_CAP;
733
- return {
734
- waveIndex: nonNegativeInt(src.waveIndex),
735
- storyCount: nonNegativeInt(src.storyCount),
736
- wallClockMs: nonNegativeInt(src.wallClockMs),
737
- summedStoryMs: nonNegativeInt(src.summedStoryMs),
738
- utilisation: clamp(nonNegativeNumber(src.utilisation), 0, 1),
739
- capBinding: Boolean(src.capBinding),
740
- verifyConcurrencyCap,
741
- };
742
- }
743
-
744
- export function computeEpicPerfReport(perStorySummaries, opts) {
745
- if (!isObject(opts)) {
746
- throw new TypeError('computeEpicPerfReport: opts is required');
747
- }
748
- const epicId = Number(opts.epicId);
749
- if (!Number.isInteger(epicId) || epicId < 1) {
750
- throw new RangeError(
751
- `computeEpicPerfReport: epicId must be a positive integer (got ${opts.epicId})`,
752
- );
753
- }
754
- const generatedAt =
755
- typeof opts.generatedAt === 'string' && opts.generatedAt.length > 0
756
- ? opts.generatedAt
757
- : new Date().toISOString();
758
-
759
- const summaries = collectValidStorySamples(perStorySummaries);
760
-
761
- // signalCounts: prefer the raw-event roll-up; fall back to friction-only
762
- // when the caller did not pass events.
763
- const signalCounts = buildSignalCounts(opts.events, summaries);
764
-
765
- const topHotspots = Array.isArray(opts.topHotspots)
766
- ? opts.topHotspots
767
- : aggregateTopHotspots(summaries);
768
-
769
- // mostFrictionStories: per-Story friction count, sorted desc, capped.
770
- const mostFrictionStories = summaries
771
- .map((s) => {
772
- const counts = isObject(s.frictionByCategory)
773
- ? Object.values(s.frictionByCategory).reduce(
774
- (acc, v) => acc + nonNegativeInt(v),
775
- 0,
776
- )
777
- : 0;
778
- return {
779
- storyId: nonNegativeInt(s.storyId),
780
- frictionCount: counts,
781
- };
782
- })
783
- .filter((row) => row.storyId > 0)
784
- .sort((a, b) => b.frictionCount - a.frictionCount)
785
- .slice(0, 5);
786
-
787
- let waveParallelism;
788
- if (Array.isArray(opts.waveParallelism)) {
789
- waveParallelism = opts.waveParallelism.map((row) =>
790
- coerceWaveParallelismRow(row),
791
- );
792
- } else if (opts.events) {
793
- // Derive rows from the raw lifecycle event stream when the caller
794
- // hands us the events but no pre-computed array (Story #3025 /
795
- // Task #3028).
796
- waveParallelism = computeWaveParallelismRows(opts.events, {
797
- concurrencyCap: opts.concurrencyCap,
798
- verifyConcurrencyCap: opts.verifyConcurrencyCap,
799
- });
800
- } else {
801
- waveParallelism = [];
802
- }
803
-
804
- return {
805
- kind: 'epic-perf-report',
806
- epicId,
807
- generatedAt,
808
- signalCounts,
809
- waveParallelism,
810
- topHotspots,
811
- mostFrictionStories,
812
- };
813
- }