mandrel 1.58.0 → 1.60.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 (319) hide show
  1. package/.agents/README.md +100 -98
  2. package/.agents/docs/SDLC.md +140 -141
  3. package/.agents/docs/configuration.md +16 -16
  4. package/.agents/docs/workflows.md +7 -8
  5. package/.agents/instructions.md +12 -11
  6. package/.agents/personas/architect.md +1 -1
  7. package/.agents/personas/product.md +1 -1
  8. package/.agents/personas/project-manager.md +14 -14
  9. package/.agents/personas/technical-writer.md +1 -1
  10. package/.agents/rules/changelog-style.md +5 -5
  11. package/.agents/rules/git-conventions.md +3 -3
  12. package/.agents/schemas/agentrc.schema.json +3 -3
  13. package/.agents/schemas/audit-rules.json +20 -0
  14. package/.agents/schemas/dispatch-manifest.json +4 -4
  15. package/.agents/schemas/epic-spec.schema.json +15 -45
  16. package/.agents/schemas/lifecycle/README.md +1 -1
  17. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +1 -1
  18. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +1 -1
  19. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +1 -1
  20. package/.agents/schemas/validation-evidence.schema.json +1 -1
  21. package/.agents/scripts/README.md +1 -1
  22. package/.agents/scripts/acceptance-eval.js +21 -4
  23. package/.agents/scripts/acceptance-spec-reconciler.js +2 -2
  24. package/.agents/scripts/analyze-execution.js +2 -2
  25. package/.agents/scripts/assert-branch.js +1 -3
  26. package/.agents/scripts/audit-to-stories.js +1 -1
  27. package/.agents/scripts/bootstrap.js +1 -1
  28. package/.agents/scripts/check-arch-cycles.js +360 -0
  29. package/.agents/scripts/check-doc-links.js +2 -3
  30. package/.agents/scripts/coverage-capture.js +24 -3
  31. package/.agents/scripts/diagnose-friction.js +1 -1
  32. package/.agents/scripts/dispatcher.js +2 -2
  33. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  34. package/.agents/scripts/epic-audit-prepare.js +3 -3
  35. package/.agents/scripts/epic-deliver-note-intervention.js +2 -2
  36. package/.agents/scripts/epic-deliver-preflight.js +11 -9
  37. package/.agents/scripts/epic-deliver-prepare.js +13 -5
  38. package/.agents/scripts/epic-execute-record-wave.js +5 -5
  39. package/.agents/scripts/epic-plan-healthcheck.js +6 -10
  40. package/.agents/scripts/epic-plan-spec-validate.js +1 -1
  41. package/.agents/scripts/epic-reconcile.js +11 -29
  42. package/.agents/scripts/evidence-gate.js +2 -2
  43. package/.agents/scripts/generate-workflows-doc.js +1 -1
  44. package/.agents/scripts/git-rebase-and-resolve.js +1 -1
  45. package/.agents/scripts/hierarchy-gate.js +40 -24
  46. package/.agents/scripts/lib/ITicketingProvider.js +1 -1
  47. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  48. package/.agents/scripts/lib/audit-to-stories/seed-epic-from-findings.js +2 -2
  49. package/.agents/scripts/lib/baseline-snapshot.js +7 -7
  50. package/.agents/scripts/lib/baselines/kinds/coverage.js +33 -149
  51. package/.agents/scripts/lib/baselines/kinds/duplication.js +27 -116
  52. package/.agents/scripts/lib/baselines/kinds/kind-factory.js +192 -0
  53. package/.agents/scripts/lib/baselines/kinds/lighthouse.js +34 -133
  54. package/.agents/scripts/lib/baselines/kinds/maintainability.js +31 -124
  55. package/.agents/scripts/lib/baselines/kinds/mutation.js +25 -111
  56. package/.agents/scripts/lib/baselines/maintainability-baseline-io.js +59 -0
  57. package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +37 -0
  58. package/.agents/scripts/lib/baselines/writer.js +1 -1
  59. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  60. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  61. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +1 -1
  62. package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
  63. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +1 -1
  64. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  65. package/.agents/scripts/lib/close-validation/commands.js +188 -0
  66. package/.agents/scripts/lib/close-validation/gates.js +235 -0
  67. package/.agents/scripts/lib/close-validation/process.js +101 -0
  68. package/.agents/scripts/lib/close-validation/projections/maintainability.js +1 -1
  69. package/.agents/scripts/lib/close-validation/runner.js +325 -0
  70. package/.agents/scripts/lib/close-validation/telemetry.js +70 -0
  71. package/.agents/scripts/lib/codebase-snapshot.js +1 -1
  72. package/.agents/scripts/lib/config/explain.js +1 -1
  73. package/.agents/scripts/lib/config/quality.js +6 -6
  74. package/.agents/scripts/lib/config/runners.js +2 -2
  75. package/.agents/scripts/lib/config/runtime.js +1 -1
  76. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  77. package/.agents/scripts/lib/config-resolver.js +2 -5
  78. package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
  79. package/.agents/scripts/lib/config-settings-schema-quality.js +1 -1
  80. package/.agents/scripts/lib/config-settings-schema.js +3 -3
  81. package/.agents/scripts/lib/coverage-capture.js +147 -4
  82. package/.agents/scripts/lib/cpu-pool.js +14 -0
  83. package/.agents/scripts/lib/crap-utils.js +6 -11
  84. package/.agents/scripts/lib/duplicate-search.js +1 -1
  85. package/.agents/scripts/lib/dynamic-workflow/capability.js +1 -1
  86. package/.agents/scripts/lib/dynamic-workflow/documentation-report-contract.js +87 -0
  87. package/.agents/scripts/lib/epic-plan-clarity.js +1 -1
  88. package/.agents/scripts/lib/epic-plan-ideation.js +1 -1
  89. package/.agents/scripts/lib/feedback-loop/memory-freshness.js +1 -1
  90. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
  91. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  92. package/.agents/scripts/lib/findings/promote-finding.js +10 -10
  93. package/.agents/scripts/lib/git-utils.js +24 -22
  94. package/.agents/scripts/lib/label-constants.js +3 -4
  95. package/.agents/scripts/lib/label-taxonomy.js +3 -8
  96. package/.agents/scripts/lib/maintainability-engine.js +1 -1
  97. package/.agents/scripts/lib/maintainability-utils.js +4 -187
  98. package/.agents/scripts/lib/observability/perf-report-readers.js +32 -23
  99. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +81 -7
  100. package/.agents/scripts/lib/orchestration/code-review.js +95 -82
  101. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +8 -9
  102. package/.agents/scripts/lib/orchestration/dependency-analyzer.js +3 -3
  103. package/.agents/scripts/lib/orchestration/detectors-phase.js +2 -2
  104. package/.agents/scripts/lib/orchestration/dispatch-engine.js +30 -38
  105. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +14 -37
  106. package/.agents/scripts/lib/orchestration/epic-cleanup.js +1 -1
  107. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +22 -22
  108. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +1 -1
  109. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +7 -21
  110. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +3 -3
  111. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +2 -2
  112. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +206 -58
  113. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +1 -1
  114. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +27 -3
  115. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +1 -1
  116. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +28 -8
  117. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +1 -1
  118. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +3 -3
  119. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +4 -4
  120. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +3 -3
  121. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +13 -41
  122. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +7 -7
  123. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +2 -3
  124. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +2 -8
  125. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +4 -4
  126. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +103 -0
  127. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +22 -64
  128. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +38 -76
  129. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +4 -4
  130. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +10 -10
  131. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +8 -20
  132. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +7 -15
  133. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +72 -41
  134. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +2 -4
  135. package/.agents/scripts/lib/orchestration/file-assumptions.js +6 -5
  136. package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +1 -1
  137. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +2 -2
  138. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +1 -1
  139. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +144 -0
  140. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +1 -1
  141. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +3 -3
  142. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +1 -1
  143. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +1 -1
  144. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +1 -1
  145. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +1 -1
  146. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +1 -1
  147. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
  148. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +1 -1
  149. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +1 -1
  150. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +8 -8
  151. package/.agents/scripts/lib/orchestration/manifest-builder.js +5 -5
  152. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +2 -2
  153. package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +5 -5
  154. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +3 -3
  155. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +3 -3
  156. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +7 -7
  157. package/.agents/scripts/lib/orchestration/preflight-cache.js +36 -13
  158. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +1 -1
  159. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +1 -1
  160. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +2 -2
  161. package/.agents/scripts/lib/orchestration/retro-runner.js +3 -3
  162. package/.agents/scripts/lib/orchestration/review-depth.js +1 -1
  163. package/.agents/scripts/lib/orchestration/review-providers/codex.js +5 -60
  164. package/.agents/scripts/lib/orchestration/review-providers/native.js +7 -6
  165. package/.agents/scripts/lib/orchestration/review-providers/parse-findings.js +105 -0
  166. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +7 -59
  167. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +2 -4
  168. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +1 -1
  169. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +1 -1
  170. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -4
  171. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +32 -35
  172. package/.agents/scripts/lib/orchestration/skill-capsule-loader.js +1 -2
  173. package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
  174. package/.agents/scripts/lib/orchestration/spec-renderer.js +36 -73
  175. package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
  176. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +451 -503
  177. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +8 -2
  178. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +47 -2
  179. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +2 -2
  180. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +1 -1
  181. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +358 -54
  182. package/.agents/scripts/lib/orchestration/story-close/phases/close.js +1 -1
  183. package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +3 -2
  184. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +32 -5
  185. package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +5 -18
  186. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +3 -3
  187. package/.agents/scripts/lib/orchestration/story-close-recovery.js +33 -16
  188. package/.agents/scripts/lib/orchestration/story-reachability.js +47 -0
  189. package/.agents/scripts/lib/orchestration/task-body-validator.js +6 -6
  190. package/.agents/scripts/lib/orchestration/ticket-lease.js +1 -1
  191. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +4 -35
  192. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -10
  193. package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -70
  194. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +44 -73
  195. package/.agents/scripts/lib/orchestration/ticketing/reads.js +16 -7
  196. package/.agents/scripts/lib/orchestration/ticketing/state.js +53 -439
  197. package/.agents/scripts/lib/orchestration/ticketing/transition.js +471 -0
  198. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  199. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +3 -3
  200. package/.agents/scripts/lib/orchestration/wave-record-projection.js +2 -8
  201. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  202. package/.agents/scripts/lib/preflight-runner.js +1 -1
  203. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +4 -5
  204. package/.agents/scripts/lib/presentation/manifest-builder.js +28 -34
  205. package/.agents/scripts/lib/presentation/manifest-formatter.js +3 -4
  206. package/.agents/scripts/lib/presentation/manifest-helpers.js +1 -1
  207. package/.agents/scripts/lib/presentation/manifest-procedures.js +4 -4
  208. package/.agents/scripts/lib/presentation/manifest-render-waves.js +4 -23
  209. package/.agents/scripts/lib/presentation/manifest-renderer.js +1 -1
  210. package/.agents/scripts/lib/presentation/manifest-story-views.js +2 -11
  211. package/.agents/scripts/lib/project-root.js +17 -0
  212. package/.agents/scripts/lib/signals/schema.js +1 -1
  213. package/.agents/scripts/lib/spec/index.js +1 -1
  214. package/.agents/scripts/lib/spec/loader.js +2 -2
  215. package/.agents/scripts/lib/spec/state.js +7 -16
  216. package/.agents/scripts/lib/story-adjacency.js +76 -0
  217. package/.agents/scripts/lib/story-init/context-resolver.js +3 -3
  218. package/.agents/scripts/lib/story-init/state-transitioner.js +2 -2
  219. package/.agents/scripts/lib/story-init/task-graph-builder.js +7 -7
  220. package/.agents/scripts/lib/story-lifecycle.js +9 -9
  221. package/.agents/scripts/lib/story-plan.js +1 -1
  222. package/.agents/scripts/lib/templates/decomposer-prompts.js +59 -52
  223. package/.agents/scripts/lib/transpile.js +93 -0
  224. package/.agents/scripts/lib/wave-runner/tick.js +4 -153
  225. package/.agents/scripts/lib/workers/crap-worker.js +1 -1
  226. package/.agents/scripts/lib/workers/maintainability-report-worker.js +1 -1
  227. package/.agents/scripts/lib/worktree/lifecycle/creation.js +20 -2
  228. package/.agents/scripts/lib/worktree/lifecycle/force-drain.js +90 -0
  229. package/.agents/scripts/lib/worktree/lifecycle/reap.js +26 -8
  230. package/.agents/scripts/lib/worktree/node-modules-strategy.js +74 -0
  231. package/.agents/scripts/lifecycle-emit-story-dispatch.js +1 -1
  232. package/.agents/scripts/lifecycle-emit.js +1 -1
  233. package/.agents/scripts/providers/github/board-add.js +1 -1
  234. package/.agents/scripts/providers/github/errors.js +1 -1
  235. package/.agents/scripts/providers/github/mappers.js +2 -2
  236. package/.agents/scripts/providers/github/tickets.js +114 -10
  237. package/.agents/scripts/resync-status-column.js +1 -1
  238. package/.agents/scripts/retro-run.js +2 -2
  239. package/.agents/scripts/run-lint.js +10 -1
  240. package/.agents/scripts/run-tests.js +24 -4
  241. package/.agents/scripts/single-story-init.js +1 -1
  242. package/.agents/scripts/stories-wave-tick.js +13 -10
  243. package/.agents/scripts/story-close.js +1 -1
  244. package/.agents/scripts/story-init.js +162 -26
  245. package/.agents/scripts/story-phase.js +5 -5
  246. package/.agents/scripts/story-plan.js +3 -3
  247. package/.agents/scripts/sync-branch-from-base.js +2 -2
  248. package/.agents/scripts/validate-docs-freshness.js +1 -1
  249. package/.agents/scripts/wave-tick.js +1 -1
  250. package/.agents/skills/core/analyze-execution/SKILL.md +2 -2
  251. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +21 -26
  252. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -56
  253. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +4 -4
  254. package/.agents/skills/core/hydrate-context/SKILL.md +2 -2
  255. package/.agents/skills/core/idea-refinement/SKILL.md +4 -4
  256. package/.agents/skills/core/knowledge-transfer/SKILL.md +2 -2
  257. package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +1 -1
  258. package/.agents/skills/core/scope-triage/SKILL.md +9 -10
  259. package/.agents/skills/core/using-agent-skills/SKILL.md +1 -1
  260. package/.agents/skills/skills.index.json +7 -7
  261. package/.agents/skills/stack/qa/lighthouse-baseline/SKILL.md +1 -1
  262. package/.agents/templates/agent-protocol.md +2 -2
  263. package/.agents/workflows/agents-update.md +2 -2
  264. package/.agents/workflows/audit-architecture.md +2 -2
  265. package/.agents/workflows/audit-clean-code.md +2 -2
  266. package/.agents/workflows/audit-dependencies.md +1 -1
  267. package/.agents/workflows/audit-devops.md +1 -1
  268. package/.agents/workflows/audit-documentation.md +226 -0
  269. package/.agents/workflows/audit-lighthouse.md +1 -1
  270. package/.agents/workflows/audit-performance.md +2 -2
  271. package/.agents/workflows/audit-privacy.md +1 -1
  272. package/.agents/workflows/audit-quality.md +2 -2
  273. package/.agents/workflows/audit-security.md +2 -2
  274. package/.agents/workflows/audit-seo.md +1 -1
  275. package/.agents/workflows/audit-sre.md +1 -1
  276. package/.agents/workflows/audit-to-stories.md +10 -10
  277. package/.agents/workflows/audit-ux-ui.md +1 -1
  278. package/.agents/workflows/deliver.md +85 -0
  279. package/.agents/workflows/explain.md +3 -3
  280. package/.agents/workflows/git-merge-pr.md +1 -1
  281. package/.agents/workflows/git-pr-all.md +13 -10
  282. package/.agents/workflows/git-push.md +6 -3
  283. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  284. package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
  285. package/.agents/workflows/helpers/code-review.md +5 -5
  286. package/.agents/workflows/{epic-deliver.md → helpers/deliver-epic.md} +59 -66
  287. package/.agents/workflows/{story-deliver.md → helpers/deliver-stories.md} +25 -25
  288. package/.agents/workflows/helpers/diagnose.md +1 -1
  289. package/.agents/workflows/helpers/epic-audit.md +6 -6
  290. package/.agents/workflows/helpers/epic-deliver-story.md +28 -39
  291. package/.agents/workflows/helpers/epic-plan-decompose.md +23 -23
  292. package/.agents/workflows/helpers/epic-plan-spec.md +6 -6
  293. package/.agents/workflows/helpers/epic-testing.md +3 -3
  294. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  295. package/.agents/workflows/{epic-plan.md → helpers/plan-epic.md} +84 -84
  296. package/.agents/workflows/{story-plan.md → helpers/plan-story.md} +43 -43
  297. package/.agents/workflows/helpers/signals.md +1 -1
  298. package/.agents/workflows/helpers/single-story-deliver.md +12 -11
  299. package/.agents/workflows/helpers/worktree-lifecycle.md +18 -18
  300. package/.agents/workflows/onboard.md +21 -20
  301. package/.agents/workflows/plan.md +89 -0
  302. package/.agents/workflows/qa-explore.md +1 -1
  303. package/.agents/workflows/qa-run-harness.md +1 -1
  304. package/README.md +17 -20
  305. package/docs/CHANGELOG.md +1149 -0
  306. package/lib/cli/__tests__/update-changelog-surface.test.js +357 -0
  307. package/lib/cli/__tests__/update-reexec.test.js +513 -0
  308. package/lib/cli/init.js +338 -0
  309. package/lib/cli/update.js +413 -52
  310. package/package.json +3 -1
  311. package/.agents/scripts/lib/auto-refresh-baselines.js +0 -308
  312. package/.agents/scripts/lib/close-validation.js +0 -897
  313. package/.agents/scripts/lib/orchestration/cascade-grouping.js +0 -275
  314. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter.js +0 -69
  315. package/.agents/scripts/lib/orchestration/reconciler.js +0 -137
  316. package/.agents/scripts/lib/orchestration/story-close/format-autofix-scoped.js +0 -221
  317. package/.agents/scripts/lib/orchestration/story-close/format-autofix-shared.js +0 -123
  318. package/.agents/scripts/lib/task-utils.js +0 -26
  319. package/.agents/scripts/story-deliver-prepare.js +0 -267
@@ -2,13 +2,9 @@
2
2
  * Build the wave DAG from the Epic's open child Stories.
3
3
  *
4
4
  * `getSubTickets` returns the **direct** children of a parent ticket via
5
- * native sub-issues + checklist links + body reverse-lookup (despite an
6
- * older docstring claiming "every descendant"). The v5 canonical Epic
7
- * hierarchy is Epic Feature Story → Task, so Stories live as
8
- * **grandchildren** of the Epic. We therefore walk one level deeper from
9
- * each `type::feature` direct child to collect the real Story set, and
10
- * union with any direct-child Stories (some Epics still carry Stories
11
- * directly while migrating).
5
+ * native sub-issues + checklist links + body reverse-lookup. The
6
+ * canonical 2-tier hierarchy is Epic Story, so the Epic's direct
7
+ * `type::story` children are the complete Story set.
12
8
  *
13
9
  * We additionally filter out closed Stories — `getSubTickets`'s reverse-
14
10
  * reference search can surface closed-as-obsolete tickets whose body
@@ -19,14 +15,14 @@
19
15
  * Throws if no open Stories are found.
20
16
  */
21
17
 
22
- import { parseBlockedBy } from '../../../dependency-parser.js';
23
18
  import { computeWaves } from '../../../Graph.js';
24
19
  import { TYPE_LABELS } from '../../../label-constants.js';
20
+ import { buildStoryAdjacency } from '../../../story-adjacency.js';
25
21
  import { WaveScheduler } from '../wave-scheduler.js';
26
22
 
27
23
  /**
28
- * Walk Epic Feature Story (one descent past the Epic's direct
29
- * children) and return the open `type::story` tickets, deduped by id.
24
+ * Collect the Epic's direct `type::story` children and return the open
25
+ * ones, deduped by id.
30
26
  *
31
27
  * Exported so `snapshot.js#discoverStoryIds` and `epic-deliver-preflight`
32
28
  * can share the same enumeration contract — the snapshot.end payload,
@@ -34,20 +30,9 @@ import { WaveScheduler } from '../wave-scheduler.js';
34
30
  */
35
31
  export async function discoverOpenStories({ epicId, provider }) {
36
32
  const descendants = (await provider.getSubTickets(epicId)) ?? [];
37
- const features = descendants.filter((t) =>
38
- (t.labels ?? []).includes(TYPE_LABELS.FEATURE),
39
- );
40
- const grandchildren = (
41
- await Promise.all(
42
- features.map(async (f) => {
43
- const id = f.id ?? f.number;
44
- return id == null ? [] : ((await provider.getSubTickets(id)) ?? []);
45
- }),
46
- )
47
- ).flat();
48
33
  const seen = new Set();
49
34
  const stories = [];
50
- for (const t of [...descendants, ...grandchildren]) {
35
+ for (const t of descendants) {
51
36
  const labels = t.labels ?? [];
52
37
  if (!labels.includes(TYPE_LABELS.STORY)) continue;
53
38
  const rawState = t.state ?? 'open';
@@ -116,30 +101,17 @@ function normalizeWavesForEmit(waves) {
116
101
  * Convert an ordered list of story tickets into the adjacency/taskMap shape
117
102
  * that `Graph.computeWaves()` expects.
118
103
  *
119
- * Dependency source order (must match manifest-builder.js so dispatch manifest
120
- * and runtime wave scheduling never disagree):
121
- * 1. Canonical: `blocked by #NNN` / `depends on #NNN` parsed from the story
122
- * ticket body via `parseBlockedBy` (same parser the dispatcher uses).
123
- * 2. Fallback: explicit `dependencies` array on the provider-returned story
124
- * object (present in fixture / test payloads; optional in live GitHub
125
- * payloads).
126
- * Only edges to other stories in this Epic are retained — foreign IDs are
127
- * dropped so the DAG stays closed over the scheduled set.
104
+ * The adjacency comes from the shared story-level builder
105
+ * (`lib/story-adjacency.js#buildStoryAdjacency`), which owns the
106
+ * dependency-source ordering contract (body `blocked by` references via
107
+ * `parseBlockedBy`, then explicit `dependencies[]`) and drops foreign
108
+ * edges so the DAG stays closed over the scheduled set.
128
109
  */
129
110
  function buildStoryDag(stories) {
130
- const adjacency = new Map();
111
+ const adjacency = buildStoryAdjacency(stories);
131
112
  const taskMap = new Map();
132
- const storyIds = new Set(stories.map((s) => Number(s.id ?? s.number)));
133
113
  for (const s of stories) {
134
114
  const id = Number(s.id ?? s.number);
135
- const fromBody = parseBlockedBy(s.body ?? '');
136
- const fromField = Array.isArray(s.dependencies)
137
- ? s.dependencies.map(Number)
138
- : [];
139
- const merged = [...new Set([...fromBody, ...fromField])]
140
- .map(Number)
141
- .filter((dep) => dep !== id && storyIds.has(dep));
142
- adjacency.set(id, merged);
143
115
  taskMap.set(id, { ...s, id });
144
116
  }
145
117
  return { adjacency, taskMap };
@@ -2,7 +2,7 @@
2
2
  * Epic snapshot phase — fetch Epic ticket and enforce the acceptance-spec
3
3
  * start gate.
4
4
  *
5
- * Auto-close is now the default for `/epic-deliver` (the human PR-merge is
5
+ * Auto-close is now the default for `/deliver` (the human PR-merge is
6
6
  * the gate); no per-Epic label snapshot is required.
7
7
  *
8
8
  * Acceptance-spec start gate (relaxed): an Epic may be delivered when
@@ -11,9 +11,9 @@
11
11
  * `context::acceptance-spec` ticket is linked to the Epic. The ticket's
12
12
  * GitHub state (open / closed) is **not** checked — presence is
13
13
  * sufficient, matching the PRD and Tech Spec contract. The reviewer's
14
- * OK during /epic-plan Phase 7 is the approval signal, not a manual
14
+ * OK during /plan Phase 7 is the approval signal, not a manual
15
15
  * ticket-close action. This still refuses to launch Epics that skipped
16
- * the /epic-plan Phase 7 acceptance-spec authoring step (or didn't
16
+ * the /plan Phase 7 acceptance-spec authoring step (or didn't
17
17
  * waive), surfacing the gap at delivery time rather than letting Story
18
18
  * dispatch race ahead without a spec at all.
19
19
  */
@@ -66,8 +66,8 @@ export async function runSnapshotPhase(ctx, collaborators, state) {
66
66
  /**
67
67
  * Enumerate the Story IDs owned by an Epic. Delegates to
68
68
  * `discoverOpenStories` so the snapshot.end payload and the wave DAG
69
- * input set never disagree — both now walk Epic Feature → Story and
70
- * exclude closed reverse-referenced tickets.
69
+ * input set never disagree — both enumerate the Epic's direct Story
70
+ * children and exclude closed reverse-referenced tickets.
71
71
  *
72
72
  * Returns a sorted array of positive integers (sort order makes the
73
73
  * ledger record deterministic across runs and platform iteration
@@ -82,7 +82,7 @@ async function discoverStoryIds({ epicId, provider }) {
82
82
  }
83
83
 
84
84
  /**
85
- * Refuse to launch /epic-deliver when the acceptance-spec precondition has
85
+ * Refuse to launch /deliver when the acceptance-spec precondition has
86
86
  * not been satisfied. Throws a clear `Error` (per
87
87
  * orchestration-error-handling rule) so the `runAsCli` boundary maps it to
88
88
  * `process.exit(1)` with the operator-visible message intact.
@@ -104,7 +104,7 @@ function assertAcceptanceSpecGate({ epic, epicId }) {
104
104
  if (!acceptanceSpecId) {
105
105
  throw new Error(
106
106
  `[epic-deliver] Epic #${epicId} cannot launch: no context::acceptance-spec is linked and the acceptance::n-a waiver label is absent. ` +
107
- 'Run /epic-plan Phase 7 to author an acceptance-spec, or apply the acceptance::n-a label to the Epic to opt out.',
107
+ 'Run /plan Phase 7 to author an acceptance-spec, or apply the acceptance::n-a label to the Epic to opt out.',
108
108
  );
109
109
  }
110
110
  }
@@ -24,7 +24,7 @@ import { EPIC_RUN_PROGRESS_TYPE, STATE_EMOJI } from './signals.js';
24
24
  * (`…`) when the string was longer. Returns the empty string for any
25
25
  * falsy input so table cells never render `undefined`/`null`.
26
26
  */
27
- export function truncate(s, n) {
27
+ function truncate(s, n) {
28
28
  if (!s) return '';
29
29
  return s.length > n ? `${s.slice(0, n - 1)}…` : s;
30
30
  }
@@ -240,7 +240,7 @@ export async function renderProgressBody({
240
240
  /**
241
241
  * Render and upsert the rolled-up `epic-run-progress` comment on the Epic.
242
242
  *
243
- * Called by `/epic-deliver` Step 2b (`epic-execute-record-wave.js`) after
243
+ * Called by `/deliver` Step 2b (`epic-execute-record-wave.js`) after
244
244
  * each wave completes. The caller folds `state.waves[]` from the
245
245
  * `epic-run-state` checkpoint into the per-wave rows and persists the
246
246
  * unified rollup as a fenced-JSON payload on the Epic ticket via
@@ -272,7 +272,6 @@ export async function renderProgressBody({
272
272
  * wave: number,
273
273
  * concurrencyCap?: number,
274
274
  * stories?: Array<{ id: number, title?: string, state?: string,
275
- * tasksDone?: number, tasksTotal?: number,
276
275
  * blockerCommentId?: string }>,
277
276
  * }>,
278
277
  * currentWave: number,
@@ -29,7 +29,7 @@ export const PHASE_TIMINGS_TYPE = 'phase-timings';
29
29
 
30
30
  /**
31
31
  * Structured-comment kind for the per-Story run-progress snapshot that
32
- * `/story-deliver` upserts on every Task transition. Read by
32
+ * `/deliver` upserts on every Task transition. Read by
33
33
  * ProgressReporter so the Epic-level table reflects sub-agent state in
34
34
  * near-real time instead of label-derived classifications.
35
35
  */
@@ -91,7 +91,7 @@ export function phaseToState(phase) {
91
91
  }
92
92
 
93
93
  /**
94
- * Parse a `story-run-progress` structured comment posted by `/story-deliver`.
94
+ * Parse a `story-run-progress` structured comment posted by `/deliver`.
95
95
  * Returns `null` for any malformed body — the caller falls back to the
96
96
  * ticket-label state derivation in that case.
97
97
  *
@@ -100,7 +100,6 @@ export function phaseToState(phase) {
100
100
  * storyId: number,
101
101
  * branch?: string,
102
102
  * phase: 'init'|'implementing'|'closing'|'blocked'|'done',
103
- * tasks?: [{ id, title?, state, commitSha? }],
104
103
  * title?: string,
105
104
  * updatedAt?: string,
106
105
  * }
@@ -109,16 +108,11 @@ export function parseStoryRunProgressComment(comment) {
109
108
  const payload = parseFencedJsonComment(comment);
110
109
  if (!payload || typeof payload !== 'object') return null;
111
110
  const phase = typeof payload.phase === 'string' ? payload.phase : undefined;
112
- const tasks = Array.isArray(payload.tasks) ? payload.tasks : [];
113
- const tasksTotal = tasks.length;
114
- const tasksDone = tasks.filter((t) => t && t.state === 'done').length;
115
111
  return {
116
112
  storyId: Number(payload.storyId),
117
113
  title: typeof payload.title === 'string' ? payload.title : '',
118
114
  phase,
119
115
  state: phaseToState(phase),
120
- tasksDone,
121
- tasksTotal,
122
116
  };
123
117
  }
124
118
 
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * progress-reporter/transport.js — outbound I/O for the
3
- * `/epic-deliver` progress narrative.
3
+ * `/deliver` progress narrative.
4
4
  *
5
5
  * Extracted from the parent `progress-reporter.js` so the
6
6
  * GitHub-comment posting surface (in `composition.js`) and the
@@ -16,7 +16,7 @@
16
16
  *
17
17
  * The webhook events emitted from this module are:
18
18
  *
19
- * - `epic-started` — fired once at /epic-deliver kickoff
19
+ * - `epic-started` — fired once at /deliver kickoff
20
20
  * - `epic-progress` — fired at wave boundaries / blocker transitions
21
21
  * - `epic-blocked` — wave aggregated to blocked/failed outside halt path
22
22
  * - `epic-unblocked` — operator flipped back to executing
@@ -118,7 +118,7 @@ export async function emitEpicProgress({
118
118
  }
119
119
 
120
120
  /**
121
- * Fire a curated `epic-started` webhook event at /epic-deliver kickoff.
121
+ * Fire a curated `epic-started` webhook event at /deliver kickoff.
122
122
  * The Slack consumer anchors the rest of the epic narrative to this fire.
123
123
  * Failures are swallowed.
124
124
  */
@@ -157,7 +157,7 @@ export async function emitEpicStarted({
157
157
  /**
158
158
  * Fire a curated `epic-blocked` webhook event when a wave aggregates to
159
159
  * `blocked` or `failed` outside the `BlockerHandler.halt` code path (the
160
- * /epic-deliver host-LLM loop has no handler instance — it calls this
160
+ * /deliver host-LLM loop has no handler instance — it calls this
161
161
  * helper directly from `epic-execute-record-wave.js`). The payload shape
162
162
  * matches the inline emit in `BlockerHandler.halt` so downstream consumers
163
163
  * see one canonical envelope regardless of which entry point fired.
@@ -0,0 +1,103 @@
1
+ import path from 'node:path';
2
+ import { resolveComponents } from '../../../baselines/components.js';
3
+ import { componentOrder } from './_bullet-format.js';
4
+
5
+ /**
6
+ * Shared component-regression walk for the progress-signal drift detectors
7
+ * (Story #3984). `crap-drift.js` and `maintainability-drift.js` previously
8
+ * duplicated this baseline-rollup → per-component compare → bullet-list
9
+ * shape verbatim; the only divergence was the breach comparator (CRAP is
10
+ * "lower is better", maintainability is "higher is better") and the bullet
11
+ * text. Both detectors now parameterize this walk.
12
+ *
13
+ * Pure — the caller loads the baseline via `lib/baselines/reader.js#load(...)`
14
+ * and passes the resulting `{ rollup }` plus the gate config block.
15
+ *
16
+ * Only components whose rollup breaches the configured floor surface — a
17
+ * breach in a component-scoped floor does NOT report against `*` unless
18
+ * `*` itself breaches. This keeps the rollout narrowly targeted: when an
19
+ * operator wires up a per-component floor for `api`, regressing `api`
20
+ * names `api` — not `*`.
21
+ *
22
+ * @param {{
23
+ * rollup?: Record<string, Record<string, number>>,
24
+ * gateConfig?: { floors?: Record<string, Record<string, number>> } & object,
25
+ * }} params
26
+ * @param {{
27
+ * isBreach: (value: number, floor: number) => boolean,
28
+ * formatBullet: (name: string, axis: string, value: number, floor: number) => string,
29
+ * }} spec
30
+ * @returns {string[]}
31
+ */
32
+ export function walkComponentRegressions(params = {}, spec) {
33
+ const rollup = params.rollup ?? {};
34
+ const gateConfig = params.gateConfig ?? {};
35
+ const floors = gateConfig.floors ?? {};
36
+ const components = resolveComponents(gateConfig);
37
+ const names = new Set([
38
+ ...Object.keys(components),
39
+ ...Object.keys(floors),
40
+ ...Object.keys(rollup),
41
+ ]);
42
+ const bullets = [];
43
+ for (const name of [...names].sort(componentOrder)) {
44
+ const aggregate = rollup[name];
45
+ if (!aggregate || typeof aggregate !== 'object') continue;
46
+ const floor = floors[name] ?? floors['*'];
47
+ if (!floor || typeof floor !== 'object') continue;
48
+ for (const axis of Object.keys(floor).sort()) {
49
+ const target = floor[axis];
50
+ const value = aggregate[axis];
51
+ if (typeof target !== 'number' || !Number.isFinite(target)) continue;
52
+ if (typeof value !== 'number' || !Number.isFinite(value)) continue;
53
+ if (!spec.isBreach(value, target)) continue;
54
+ bullets.push(spec.formatBullet(name, axis, value, target));
55
+ }
56
+ }
57
+ return bullets;
58
+ }
59
+
60
+ /**
61
+ * Shared wave-start snapshot persistence for the drift detectors. Both
62
+ * detectors persist a `{ capturedAt, ...metadata, scores }` JSON document
63
+ * under `<cwd>/<baselineDir>/<filename>` so a resumed epic run can re-read
64
+ * the wave-start anchor rather than lose it, and both treat persistence as
65
+ * best-effort (the in-memory baseline still works when a write fails).
66
+ *
67
+ * @param {{
68
+ * fs: { readFileSync?: Function, writeFileSync?: Function, mkdirSync?: Function },
69
+ * baselinePath: string,
70
+ * metadata?: Record<string, unknown>,
71
+ * }} opts
72
+ */
73
+ export function createSnapshotStore({ fs, baselinePath, metadata = {} }) {
74
+ return {
75
+ persist(scores) {
76
+ if (!fs.writeFileSync) return;
77
+ try {
78
+ fs.mkdirSync?.(path.dirname(baselinePath), { recursive: true });
79
+ fs.writeFileSync(
80
+ baselinePath,
81
+ JSON.stringify(
82
+ { capturedAt: new Date().toISOString(), ...metadata, scores },
83
+ null,
84
+ 2,
85
+ ),
86
+ );
87
+ } catch {
88
+ // persistence is best-effort; the in-memory baseline still works
89
+ }
90
+ },
91
+
92
+ load() {
93
+ if (!fs.readFileSync) return null;
94
+ try {
95
+ const raw = fs.readFileSync(baselinePath, 'utf-8');
96
+ const parsed = JSON.parse(raw);
97
+ return parsed?.scores ?? null;
98
+ } catch {
99
+ return null;
100
+ }
101
+ },
102
+ };
103
+ }
@@ -1,9 +1,12 @@
1
1
  import nodeFs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { resolveComponents } from '../../../baselines/components.js';
4
3
  import { loadCoverage as defaultLoadCoverage } from '../../../coverage-utils.js';
5
4
  import { calculateCrapForSource } from '../../../crap-engine.js';
6
- import { componentOrder, formatNumber } from './_bullet-format.js';
5
+ import { formatNumber } from './_bullet-format.js';
6
+ import {
7
+ createSnapshotStore,
8
+ walkComponentRegressions,
9
+ } from './component-drift.js';
7
10
 
8
11
  const DEFAULT_THRESHOLD = 5.0;
9
12
  const DEFAULT_CEILING = 30;
@@ -43,11 +46,10 @@ export function coverageKeyMatches(key, suffix) {
43
46
  *
44
47
  * 🧨 crap: <component> <axis> <value> > floor <floor>
45
48
  *
46
- * Only components whose rollup exceeds the configured floor surface a
47
- * breach in a component-scoped floor does NOT report against `*` unless
48
- * `*` itself breaches. This keeps the rollout narrowly targeted: when an
49
- * operator wires up a per-component floor for `api`, regressing `api`
50
- * names `api` — not `*`.
49
+ * CRAP is a "lower is better" gate every axis breach reports when
50
+ * `value > floor`. Component-scoped breaches do NOT trigger a `*` bullet
51
+ * unless `*` itself breaches. The walk itself is the shared
52
+ * `component-drift.js` helper (Story #3984).
51
53
  *
52
54
  * @param {{
53
55
  * rollup?: Record<string, Record<string, number>>,
@@ -56,34 +58,11 @@ export function coverageKeyMatches(key, suffix) {
56
58
  * @returns {string[]}
57
59
  */
58
60
  export function detectComponentRegressions(params = {}) {
59
- const rollup = params.rollup ?? {};
60
- const gateConfig = params.gateConfig ?? {};
61
- const floors = gateConfig.floors ?? {};
62
- const components = resolveComponents(gateConfig);
63
- const names = new Set([
64
- ...Object.keys(components),
65
- ...Object.keys(floors),
66
- ...Object.keys(rollup),
67
- ]);
68
- const bullets = [];
69
- // CRAP is a "lower is better" gate — every axis under crap is ≤ floor.
70
- for (const name of [...names].sort(componentOrder)) {
71
- const aggregate = rollup[name];
72
- if (!aggregate || typeof aggregate !== 'object') continue;
73
- const floor = floors[name] ?? floors['*'];
74
- if (!floor || typeof floor !== 'object') continue;
75
- for (const axis of Object.keys(floor).sort()) {
76
- const cap = floor[axis];
77
- const value = aggregate[axis];
78
- if (typeof cap !== 'number' || !Number.isFinite(cap)) continue;
79
- if (typeof value !== 'number' || !Number.isFinite(value)) continue;
80
- if (value <= cap) continue;
81
- bullets.push(
82
- `🧨 crap: ${name} ${axis} ${formatNumber(value)} > floor ${formatNumber(cap)}`,
83
- );
84
- }
85
- }
86
- return bullets;
61
+ return walkComponentRegressions(params, {
62
+ isBreach: (value, cap) => value > cap,
63
+ formatBullet: (name, axis, value, cap) =>
64
+ `🧨 crap: ${name} ${axis} ${formatNumber(value)} > floor ${formatNumber(cap)}`,
65
+ });
87
66
  }
88
67
 
89
68
  /**
@@ -141,6 +120,11 @@ export function createCrapDriftDetector(opts = {}) {
141
120
  const baselineDir = opts.baselineDir ?? '.agents/state';
142
121
  const baselinePath = path.join(cwd, baselineDir, BASELINE_FILENAME);
143
122
  const logger = opts.logger ?? null;
123
+ const store = createSnapshotStore({
124
+ fs,
125
+ baselinePath,
126
+ metadata: { ceiling, threshold },
127
+ });
144
128
 
145
129
  let baseline = null;
146
130
 
@@ -220,39 +204,13 @@ export function createCrapDriftDetector(opts = {}) {
220
204
  }
221
205
  }
222
206
  baseline = snapshot;
223
- if (fs.writeFileSync) {
224
- try {
225
- fs.mkdirSync?.(path.dirname(baselinePath), { recursive: true });
226
- fs.writeFileSync(
227
- baselinePath,
228
- JSON.stringify(
229
- {
230
- capturedAt: new Date().toISOString(),
231
- ceiling,
232
- threshold,
233
- scores: snapshot,
234
- },
235
- null,
236
- 2,
237
- ),
238
- );
239
- } catch {
240
- // persistence is best-effort; the in-memory baseline still works
241
- }
242
- }
207
+ store.persist(snapshot);
243
208
  return snapshot;
244
209
  },
245
210
 
246
211
  loadBaseline() {
247
- if (!fs.readFileSync) return null;
248
- try {
249
- const raw = fs.readFileSync(baselinePath, 'utf-8');
250
- const parsed = JSON.parse(raw);
251
- baseline = parsed?.scores ?? null;
252
- return baseline;
253
- } catch {
254
- return null;
255
- }
212
+ baseline = store.load();
213
+ return baseline;
256
214
  },
257
215
 
258
216
  async detect() {
@@ -1,9 +1,12 @@
1
1
  import nodeFs from 'node:fs';
2
2
  import path from 'node:path';
3
3
 
4
- import { resolveComponents } from '../../../baselines/components.js';
5
4
  import { calculateForSource } from '../../../maintainability-engine.js';
6
- import { componentOrder, formatNumber } from './_bullet-format.js';
5
+ import { formatNumber } from './_bullet-format.js';
6
+ import {
7
+ createSnapshotStore,
8
+ walkComponentRegressions,
9
+ } from './component-drift.js';
7
10
 
8
11
  const DEFAULT_THRESHOLD = 2.0;
9
12
  // Distinct from the canonical ratchet baseline at `baselines/maintainability.json`
@@ -12,6 +15,35 @@ const DEFAULT_THRESHOLD = 2.0;
12
15
  // grep for the canonical baseline no longer hits the snapshot.
13
16
  const BASELINE_FILENAME = 'wave-mi-snapshot.json';
14
17
 
18
+ /**
19
+ * Detect per-component maintainability regressions from a baseline rollup
20
+ * against the gate's configured floors. Pure — the caller loads the
21
+ * baseline via `lib/baselines/reader.js#load('maintainability')` and passes
22
+ * the resulting `{ rollup }` plus the gate config block.
23
+ *
24
+ * Bullet shape (Task #1919, Epic #1786):
25
+ *
26
+ * 📉 maintainability: <component> <axis> <value> < floor <floor>
27
+ *
28
+ * Maintainability is "higher is better" — every axis breach reports when
29
+ * `value < floor`. Component-scoped breaches do NOT trigger a `*` bullet
30
+ * unless `*` itself breaches. The walk itself is the shared
31
+ * `component-drift.js` helper (Story #3984).
32
+ *
33
+ * @param {{
34
+ * rollup?: Record<string, Record<string, number>>,
35
+ * gateConfig?: { floors?: Record<string, Record<string, number>> } & object,
36
+ * }} params
37
+ * @returns {string[]}
38
+ */
39
+ export function detectComponentRegressions(params = {}) {
40
+ return walkComponentRegressions(params, {
41
+ isBreach: (value, target) => value < target,
42
+ formatBullet: (name, axis, value, target) =>
43
+ `📉 maintainability: ${name} ${axis} ${formatNumber(value)} < floor ${formatNumber(target)}`,
44
+ });
45
+ }
46
+
15
47
  /**
16
48
  * Detects per-file maintainability drop versus a wave-start baseline.
17
49
  *
@@ -40,56 +72,6 @@ const BASELINE_FILENAME = 'wave-mi-snapshot.json';
40
72
  * baselineDir?: string, // directory (under cwd) to persist snapshot
41
73
  * }} [opts]
42
74
  */
43
- /**
44
- * Detect per-component maintainability regressions from a baseline rollup
45
- * against the gate's configured floors. Pure — the caller loads the
46
- * baseline via `lib/baselines/reader.js#load('maintainability')` and passes
47
- * the resulting `{ rollup }` plus the gate config block.
48
- *
49
- * Bullet shape (Task #1919, Epic #1786):
50
- *
51
- * 📉 maintainability: <component> <axis> <value> < floor <floor>
52
- *
53
- * Maintainability is "higher is better" — every axis breach reports when
54
- * `value < floor`. Component-scoped breaches do NOT trigger a `*` bullet
55
- * unless `*` itself breaches.
56
- *
57
- * @param {{
58
- * rollup?: Record<string, Record<string, number>>,
59
- * gateConfig?: { floors?: Record<string, Record<string, number>> } & object,
60
- * }} params
61
- * @returns {string[]}
62
- */
63
- export function detectComponentRegressions(params = {}) {
64
- const rollup = params.rollup ?? {};
65
- const gateConfig = params.gateConfig ?? {};
66
- const floors = gateConfig.floors ?? {};
67
- const components = resolveComponents(gateConfig);
68
- const names = new Set([
69
- ...Object.keys(components),
70
- ...Object.keys(floors),
71
- ...Object.keys(rollup),
72
- ]);
73
- const bullets = [];
74
- for (const name of [...names].sort(componentOrder)) {
75
- const aggregate = rollup[name];
76
- if (!aggregate || typeof aggregate !== 'object') continue;
77
- const floor = floors[name] ?? floors['*'];
78
- if (!floor || typeof floor !== 'object') continue;
79
- for (const axis of Object.keys(floor).sort()) {
80
- const target = floor[axis];
81
- const value = aggregate[axis];
82
- if (typeof target !== 'number' || !Number.isFinite(target)) continue;
83
- if (typeof value !== 'number' || !Number.isFinite(value)) continue;
84
- if (value >= target) continue;
85
- bullets.push(
86
- `📉 maintainability: ${name} ${axis} ${formatNumber(value)} < floor ${formatNumber(target)}`,
87
- );
88
- }
89
- }
90
- return bullets;
91
- }
92
-
93
75
  export function createMaintainabilityDriftDetector(opts = {}) {
94
76
  const fs = opts.fs ?? nodeFs;
95
77
  const cwd = opts.cwd ?? process.cwd();
@@ -100,6 +82,7 @@ export function createMaintainabilityDriftDetector(opts = {}) {
100
82
  : DEFAULT_THRESHOLD;
101
83
  const baselineDir = opts.baselineDir ?? '.agents/state';
102
84
  const baselinePath = path.join(cwd, baselineDir, BASELINE_FILENAME);
85
+ const store = createSnapshotStore({ fs, baselinePath });
103
86
 
104
87
  let baseline = null;
105
88
 
@@ -126,34 +109,13 @@ export function createMaintainabilityDriftDetector(opts = {}) {
126
109
  if (s != null) snapshot[f] = s;
127
110
  }
128
111
  baseline = snapshot;
129
- if (fs.writeFileSync) {
130
- try {
131
- fs.mkdirSync?.(path.dirname(baselinePath), { recursive: true });
132
- fs.writeFileSync(
133
- baselinePath,
134
- JSON.stringify(
135
- { capturedAt: new Date().toISOString(), scores: snapshot },
136
- null,
137
- 2,
138
- ),
139
- );
140
- } catch {
141
- // persistence is best-effort; the in-memory baseline still works
142
- }
143
- }
112
+ store.persist(snapshot);
144
113
  return snapshot;
145
114
  },
146
115
 
147
116
  loadBaseline() {
148
- if (!fs.readFileSync) return null;
149
- try {
150
- const raw = fs.readFileSync(baselinePath, 'utf-8');
151
- const parsed = JSON.parse(raw);
152
- baseline = parsed?.scores ?? null;
153
- return baseline;
154
- } catch {
155
- return null;
156
- }
117
+ baseline = store.load();
118
+ return baseline;
157
119
  },
158
120
 
159
121
  async detect() {
@@ -5,10 +5,10 @@
5
5
  * After Story #908, in-session Agent-tool fan-out replaces the subprocess
6
6
  * spawn pipeline. The launcher's primary responsibility is `planWave(stories)`:
7
7
  * given a wave's Story tickets it returns a stable list of
8
- * `{ storyId, worktree }` entries. The `/epic-deliver` skill consumes that
8
+ * `{ storyId, worktree }` entries. The `/deliver` skill consumes that
9
9
  * list (one wave at a time) to format one assistant turn containing N
10
10
  * parallel `Agent` tool calls (subagent_type `general-purpose`), each of
11
- * which drives `/story-deliver <storyId>` for one Story.
11
+ * which drives `/deliver <storyId>` for one Story.
12
12
  *
13
13
  * `launchWave(stories)` is a convenience for callers that already hold a
14
14
  * concrete dispatch adapter (tests, future programmatic harnesses). It calls
@@ -46,7 +46,7 @@ export class StoryLauncher {
46
46
 
47
47
  /**
48
48
  * Produce the dispatch plan for a wave. Pure: no side effects, no IO. The
49
- * caller (the `/epic-deliver` skill's wave loop, or `launchWave` below)
49
+ * caller (the `/deliver` skill's wave loop, or `launchWave` below)
50
50
  * decides what to do with the plan.
51
51
  *
52
52
  * @param {Array<number|{id?:number,storyId?:number,number?:number}>} stories
@@ -78,7 +78,7 @@ export class StoryLauncher {
78
78
  async launchWave(stories, signal) {
79
79
  if (typeof this.dispatch !== 'function') {
80
80
  throw new TypeError(
81
- 'StoryLauncher.launchWave requires a dispatch adapter (in-session Agent-tool fan-out is the responsibility of the /epic-deliver skill).',
81
+ 'StoryLauncher.launchWave requires a dispatch adapter (in-session Agent-tool fan-out is the responsibility of the /deliver skill).',
82
82
  );
83
83
  }
84
84
  const plan = this.planWave(stories);