mandrel 2.0.0 โ†’ 2.2.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 (323) hide show
  1. package/.agents/README.md +59 -28
  2. package/.agents/agents/acceptance-critic.md +20 -9
  3. package/.agents/agents/story-worker.md +45 -48
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +60 -46
  6. package/.agents/docs/agentrc-reference.json +8 -13
  7. package/.agents/docs/configuration.md +33 -57
  8. package/.agents/docs/execution-reference.md +39 -10
  9. package/.agents/docs/quality-gates.md +17 -19
  10. package/.agents/docs/workflows.md +6 -6
  11. package/.agents/instructions.md +64 -79
  12. package/.agents/rules/ci-remediation.md +3 -3
  13. package/.agents/rules/gherkin-standards.md +10 -0
  14. package/.agents/rules/git-conventions-reference.md +42 -51
  15. package/.agents/schemas/acceptance-eval-verdict.schema.json +2 -2
  16. package/.agents/schemas/agentrc.schema.json +35 -46
  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/story.merged.schema.json +1 -1
  24. package/.agents/schemas/signal-event.schema.json +3 -3
  25. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  26. package/.agents/schemas/validation-evidence.schema.json +1 -1
  27. package/.agents/scripts/acceptance-eval.js +24 -68
  28. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  29. package/.agents/scripts/bootstrap.js +3 -3
  30. package/.agents/scripts/check-dead-exports.js +43 -104
  31. package/.agents/scripts/check-doc-links.js +2 -2
  32. package/.agents/scripts/check-lifecycle-lint.js +1 -1
  33. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  34. package/.agents/scripts/deliver-recover.js +122 -0
  35. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  36. package/.agents/scripts/evidence-gate.js +20 -50
  37. package/.agents/scripts/generate-skills-index.js +17 -1
  38. package/.agents/scripts/generate-workflows-doc.js +4 -4
  39. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  40. package/.agents/scripts/lib/audit-suite/selector.js +323 -23
  41. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
  42. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  43. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  44. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  45. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  46. package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
  47. package/.agents/scripts/lib/checks/index.js +1 -1
  48. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  49. package/.agents/scripts/lib/checks/state.js +17 -248
  50. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
  51. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  52. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  53. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  54. package/.agents/scripts/lib/cli-args.js +23 -2
  55. package/.agents/scripts/lib/close-validation/gates.js +13 -13
  56. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  57. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  58. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  59. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  60. package/.agents/scripts/lib/config/acceptance-eval.js +2 -2
  61. package/.agents/scripts/lib/config/delivery-routing.js +7 -6
  62. package/.agents/scripts/lib/config/explain.js +10 -16
  63. package/.agents/scripts/lib/config/github.js +7 -5
  64. package/.agents/scripts/lib/config/limits.js +15 -25
  65. package/.agents/scripts/lib/config/quality.js +11 -14
  66. package/.agents/scripts/lib/config/runners.js +8 -21
  67. package/.agents/scripts/lib/config/temp-paths.js +18 -56
  68. package/.agents/scripts/lib/config-settings-schema-delivery.js +34 -16
  69. package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
  70. package/.agents/scripts/lib/config-settings-schema.js +48 -22
  71. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  72. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  73. package/.agents/scripts/lib/duplicate-search.js +38 -7
  74. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  75. package/.agents/scripts/lib/format-generated-json.js +97 -0
  76. package/.agents/scripts/lib/framework-version.js +19 -189
  77. package/.agents/scripts/lib/gh-exec.js +8 -0
  78. package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
  79. package/.agents/scripts/lib/git-utils.js +0 -14
  80. package/.agents/scripts/lib/json-utils.js +1 -2
  81. package/.agents/scripts/lib/label-constants.js +0 -15
  82. package/.agents/scripts/lib/label-taxonomy.js +1 -12
  83. package/.agents/scripts/lib/observability/active-story-env.js +42 -163
  84. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  85. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  86. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  87. package/.agents/scripts/lib/observability/source-classifier.js +2 -2
  88. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  89. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  90. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  91. package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
  92. package/.agents/scripts/lib/orchestration/change-set.js +103 -0
  93. package/.agents/scripts/lib/orchestration/code-review.js +70 -191
  94. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
  95. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  96. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  97. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  98. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  99. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  100. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
  101. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  102. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  103. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
  104. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  105. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  106. package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
  107. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  108. package/.agents/scripts/lib/orchestration/plan-context.js +116 -33
  109. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +26 -36
  110. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +31 -22
  111. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  112. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
  114. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  115. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -100
  116. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
  117. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
  118. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  119. package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +230 -0
  120. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
  121. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -2
  122. package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
  123. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  124. package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
  125. package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
  126. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  127. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  128. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  129. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  130. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  131. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  132. package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
  133. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
  134. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  135. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  136. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +4 -13
  137. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  138. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
  139. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  140. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  141. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
  142. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  143. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  144. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
  145. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  146. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  147. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +104 -279
  148. package/.agents/scripts/lib/orchestration/story-close/phases/local-lens-review.js +191 -0
  149. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +120 -0
  150. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  151. package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
  152. package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
  153. package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
  154. package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
  155. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  156. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
  157. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
  158. package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
  159. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
  160. package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
  161. package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
  162. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  163. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  164. package/.agents/scripts/lib/planning-corpus.js +12 -286
  165. package/.agents/scripts/lib/preflight-runner.js +2 -2
  166. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  167. package/.agents/scripts/lib/signals/index.js +4 -17
  168. package/.agents/scripts/lib/signals/read.js +35 -35
  169. package/.agents/scripts/lib/signals/schema.js +8 -11
  170. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  171. package/.agents/scripts/lib/signals/write.js +0 -1
  172. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  173. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  174. package/.agents/scripts/lib/story-adjacency.js +8 -7
  175. package/.agents/scripts/lib/story-body/story-body.js +81 -13
  176. package/.agents/scripts/lib/templates/decomposer-prompts.js +15 -16
  177. package/.agents/scripts/lib/test-env.js +14 -1
  178. package/.agents/scripts/lib/test-tiers.js +0 -3
  179. package/.agents/scripts/lib/ticket-body-sections.js +0 -14
  180. package/.agents/scripts/lib/validation-evidence.js +31 -59
  181. package/.agents/scripts/lib/wave-runner/live-probe.js +315 -0
  182. package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
  183. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  184. package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
  185. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  186. package/.agents/scripts/plan-context.js +38 -7
  187. package/.agents/scripts/plan-critics.js +203 -0
  188. package/.agents/scripts/plan-persist.js +145 -35
  189. package/.agents/scripts/plan-run-epilogue.js +83 -38
  190. package/.agents/scripts/post-structured-comment.js +0 -38
  191. package/.agents/scripts/pr-watch-with-update.js +43 -22
  192. package/.agents/scripts/providers/github/compose.js +0 -1
  193. package/.agents/scripts/providers/github/errors.js +0 -19
  194. package/.agents/scripts/providers/github/issues.js +1 -11
  195. package/.agents/scripts/providers/github/mappers.js +5 -0
  196. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  197. package/.agents/scripts/providers/github/tickets.js +33 -153
  198. package/.agents/scripts/providers/github.js +17 -6
  199. package/.agents/scripts/quality-preview.js +13 -6
  200. package/.agents/scripts/resolve-stories.js +236 -0
  201. package/.agents/scripts/run-coverage.js +4 -1
  202. package/.agents/scripts/run-lint.js +2 -2
  203. package/.agents/scripts/run-verify.js +31 -2
  204. package/.agents/scripts/signals-view.js +9 -10
  205. package/.agents/scripts/single-story-close.js +173 -18
  206. package/.agents/scripts/single-story-confirm-merge.js +288 -15
  207. package/.agents/scripts/single-story-init.js +6 -10
  208. package/.agents/scripts/stories-wave-tick.js +380 -53
  209. package/.agents/scripts/story-plan.js +3 -3
  210. package/.agents/scripts/update-ticket-state.js +8 -50
  211. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  212. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  213. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  214. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  215. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  216. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  217. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  218. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  219. package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
  220. package/.agents/skills/core/scope-triage/SKILL.md +3 -0
  221. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  222. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  223. package/.agents/skills/skills.index.json +2 -12
  224. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  225. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  226. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  227. package/.agents/workflows/audit-architecture.md +3 -4
  228. package/.agents/workflows/audit-clean-code.md +4 -4
  229. package/.agents/workflows/audit-documentation.md +4 -5
  230. package/.agents/workflows/audit-lighthouse.md +8 -0
  231. package/.agents/workflows/audit-navigability.md +10 -0
  232. package/.agents/workflows/audit-performance.md +2 -3
  233. package/.agents/workflows/audit-quality.md +8 -9
  234. package/.agents/workflows/audit-security.md +1 -2
  235. package/.agents/workflows/audit-seo.md +10 -0
  236. package/.agents/workflows/audit-ux-ui.md +7 -0
  237. package/.agents/workflows/deliver.md +133 -45
  238. package/.agents/workflows/git-cleanup.md +2 -2
  239. package/.agents/workflows/git-deliver.md +1 -1
  240. package/.agents/workflows/helpers/acceptance-self-eval.md +34 -17
  241. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -12
  242. package/.agents/workflows/helpers/code-review.md +14 -12
  243. package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
  244. package/.agents/workflows/helpers/deliver-story.md +209 -118
  245. package/.agents/workflows/helpers/parallel-tooling.md +2 -2
  246. package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
  247. package/.agents/workflows/plan.md +239 -19
  248. package/.agents/workflows/qa-assist.md +6 -6
  249. package/.agents/workflows/qa-explore.md +3 -3
  250. package/.agents/workflows/qa-run.md +1 -5
  251. package/bin/mandrel.js +12 -1
  252. package/docs/CHANGELOG.md +62 -0
  253. package/lib/cli/registry.js +262 -19
  254. package/lib/cli/sync-agents.js +157 -0
  255. package/lib/cli/sync-commands.js +115 -6
  256. package/lib/cli/sync.js +168 -6
  257. package/lib/cli/update.js +105 -8
  258. package/lib/cli/version-helpers.js +131 -0
  259. package/lib/migrations/README.md +7 -5
  260. package/lib/migrations/index.js +17 -9
  261. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  262. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  263. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +154 -0
  264. package/package.json +2 -2
  265. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  266. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  267. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  268. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  269. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  270. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  271. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  272. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  273. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  274. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  275. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  276. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  277. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  278. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  279. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  280. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  281. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  282. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  283. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  284. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  285. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  286. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  287. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  288. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  289. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  290. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  291. package/.agents/schemas/risk-verdict.schema.json +0 -53
  292. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  293. package/.agents/scripts/analyze-execution.js +0 -444
  294. package/.agents/scripts/check-prepush-recovery.js +0 -90
  295. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  296. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  297. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
  298. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  299. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  300. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  301. package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
  302. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
  303. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  304. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  305. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  306. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  307. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  308. package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
  309. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  310. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  311. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  312. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
  313. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  314. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  315. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  316. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
  317. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  318. package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
  319. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  320. package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
  321. package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
  322. package/.agents/scripts/resolve-plan-run.js +0 -117
  323. package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
@@ -0,0 +1,51 @@
1
+ /**
2
+ * dead-exports-mode.js โ€” the two passes of the dead-export ratchet.
3
+ *
4
+ * The gate runs twice with different reachability assumptions, and each pass
5
+ * needs a matched triple: which knip invocation to make, which baseline to
6
+ * ratchet against, and how to label its output. Resolving that triple in one
7
+ * place keeps the three from drifting apart โ€” a pass that ran production knip
8
+ * against the default baseline would report every test-only export as newly
9
+ * dead and fail the build for no reason.
10
+ *
11
+ * - **default** โ€” `tests/**` are knip entry points, so an export imported only
12
+ * by a test reads as used. This is the historical gate.
13
+ * - **production** โ€” knip's `--production` drops the test entries, so an export
14
+ * no production code reaches reads as dead. Depends on the `!` pattern
15
+ * suffixes in `knip.json`; without them production mode has no entry points
16
+ * and silently reports nothing.
17
+ *
18
+ * The two ratchet against separate baselines on purpose. This repo sanctions
19
+ * exporting purely for a test (`.agents/rules/test-seams.md`), so the
20
+ * production row set is large and mostly intentional; merging it into the
21
+ * default baseline would destroy that baseline's meaning.
22
+ *
23
+ * @module lib/dead-exports-mode
24
+ */
25
+
26
+ import path from 'node:path';
27
+
28
+ /** Baseline for the default pass (test entry points included). */
29
+ const DEFAULT_BASELINE = path.join('baselines', 'dead-exports.json');
30
+
31
+ /** Baseline for the `--production` pass (test-only-importer discount). */
32
+ const PRODUCTION_BASELINE = path.join(
33
+ 'baselines',
34
+ 'dead-exports-production.json',
35
+ );
36
+
37
+ /**
38
+ * Resolve the baseline / label / mode-tag triple for a pass.
39
+ *
40
+ * @param {boolean} production
41
+ * @returns {{ mode: 'production'|'default', label: string, baseline: string }}
42
+ */
43
+ export function resolveDeadExportsMode(production) {
44
+ return production
45
+ ? {
46
+ mode: 'production',
47
+ label: 'dead-exports:production',
48
+ baseline: PRODUCTION_BASELINE,
49
+ }
50
+ : { mode: 'default', label: 'dead-exports', baseline: DEFAULT_BASELINE };
51
+ }
@@ -10,8 +10,17 @@
10
10
  * - Prefer `provider.searchIssues` to narrow open Stories server-side
11
11
  * (`label:"type::story" state:open` + top seed tokens), capped at
12
12
  * ~100 hits, then rank that set. Fall back to `listIssuesByLabel`
13
- * when search errors or is unavailable (same try/catch pattern as
14
- * `TicketGateway.getTickets`).
13
+ * when search errors, is unavailable, or returns **nothing** (same
14
+ * try/catch pattern as `TicketGateway.getTickets`).
15
+ * - The narrowing query is deliberately weak (Story #4541). GitHub ANDs
16
+ * free-text terms, so the former 8-token query demanded that a candidate
17
+ * contain all eight of a multi-sentence seed's longest words โ€” which no
18
+ * real Story does. Gate #1 duplicate triage therefore reported "no
19
+ * candidates" on every realistic seed while looking perfectly healthy.
20
+ * Search is now a cheap best-case narrowing over a handful of tokens; an
21
+ * empty result is treated as "search did not help", not "no duplicates",
22
+ * and falls through to the label listing whose Jaccard ranker has real
23
+ * recall.
15
24
  * - Scoring is intentionally simple (token Jaccard over title + body).
16
25
  * It is a triage signal, not a semantic-search replacement.
17
26
  * - Provider errors on the list fallback propagate verbatim โ€” the
@@ -79,8 +88,17 @@ const STOPWORDS = new Set([
79
88
 
80
89
  const DEFAULT_MIN_SCORE = 0.15;
81
90
  const DEFAULT_MAX_RESULTS = 5;
82
- /** How many seed tokens to pass as free-text search terms. */
83
- const DEFAULT_SEARCH_TOKEN_CAP = 8;
91
+ /**
92
+ * How many seed tokens to pass as free-text search terms.
93
+ *
94
+ * GitHub ANDs these, so the cap is a precision/recall dial pointing the
95
+ * wrong way: every extra token shrinks the candidate set. Three is a
96
+ * best-case narrowing that can plausibly hit on a focused seed; anything
97
+ * beyond that reliably matched nothing on a multi-sentence one (Story
98
+ * #4541). Recall does not rest on this number โ€” an empty result falls back
99
+ * to label-listing plus client-side ranking.
100
+ */
101
+ const DEFAULT_SEARCH_TOKEN_CAP = 3;
84
102
  /** Hard cap on search hits ranked client-side (~one Search API page). */
85
103
  const SEARCH_RESULT_CAP = 100;
86
104
 
@@ -269,8 +287,16 @@ async function fetchOpenStoriesViaList(provider) {
269
287
  }
270
288
 
271
289
  /**
272
- * Fetch open-Story candidates: prefer Search API narrowing, fall back to
273
- * `listIssuesByLabel` when search errors or is unavailable.
290
+ * Fetch open-Story candidates: try Search API narrowing first, fall back to
291
+ * `listIssuesByLabel` when search errors, is unavailable, or **returns no
292
+ * hits**.
293
+ *
294
+ * The empty-result fallback is the load-bearing one (Story #4541). The
295
+ * narrowing query ANDs seed tokens, so on any real multi-sentence seed it
296
+ * matches nothing โ€” and treating that as an authoritative "no duplicates
297
+ * exist" is what made Gate #1 triage report a clean bill of health while
298
+ * never actually looking. An empty search is evidence about the *query*, not
299
+ * about the backlog.
274
300
  *
275
301
  * @param {object} provider
276
302
  * @param {string} seed
@@ -291,7 +317,12 @@ async function fetchOpenStoryCandidates(provider, seed) {
291
317
 
292
318
  if (hasSearch) {
293
319
  try {
294
- return await fetchOpenStoriesViaSearch(provider, seed);
320
+ const hits = await fetchOpenStoriesViaSearch(provider, seed);
321
+ if (hits.length > 0 || !hasList) return hits;
322
+ Logger.info(
323
+ '[duplicate-search] narrowed search matched no open Stories; ' +
324
+ 'falling back to listIssuesByLabel + client-side ranking',
325
+ );
295
326
  } catch (err) {
296
327
  if (!hasList) throw err;
297
328
  const msg = typeof err?.message === 'string' ? err.message : String(err);
@@ -6,7 +6,7 @@
6
6
  * session's ledger items (see `.agents/schemas/qa-ledger.schema.json` and
7
7
  * `lib/qa/qa-session.js`), the still-untriaged backlog is clustered and each
8
8
  * cluster is promoted to a follow-up ticket โ€” a single Story (via `/plan`)
9
- * for a tight, one-deliverable cluster, or an Epic (via `/plan --seed`) for
9
+ * for a tight, one-deliverable cluster, or a multi-Story plan-seed (via `/plan --seed`) for
10
10
  * a broad cluster that spans multiple coverage surfaces. Each contributing
11
11
  * ledger item then has the resulting `routedTo` issue link written back onto it
12
12
  * so a resume run sees the item as filed rather than re-promoting it.
@@ -32,15 +32,24 @@ import { highestSeverity as highestSeverityOf } from './severity.js';
32
32
  /** Triaged dispositions, mirrored from the `disposition` enum in the schema. */
33
33
  const TRIAGED_DISPOSITIONS = Object.freeze(['file', 'defer', 'dismiss']);
34
34
 
35
- /** The two promotion targets a cluster routes to. */
35
+ /**
36
+ * The two promotion targets a cluster routes to.
37
+ *
38
+ * `PLAN_SEED` promotes a broad cluster by seeding a multi-Story `/plan`
39
+ * run (`/plan --seed`). Its persisted wire token is the legacy string
40
+ * `'epic'`: archived qa-ledger records (and the distributed
41
+ * `qa-ledger.schema.json` `routedTo.kind` enum) still carry `'epic'`, so
42
+ * the value is kept read-compatible while the write-side constant name is
43
+ * modernised.
44
+ */
36
45
  export const PROMOTION_TARGETS = Object.freeze({
37
46
  STORY: 'story',
38
- EPIC: 'epic',
47
+ PLAN_SEED: 'epic',
39
48
  });
40
49
 
41
50
  /**
42
51
  * A cluster of more than this many distinct coverage surfaces is broad enough
43
- * to warrant an Epic (`/plan --seed`) rather than a single Story
52
+ * to warrant a plan-seed (`/plan --seed`) rather than a single Story
44
53
  * (`/plan`). One or two surfaces is a tight, single-deliverable cluster.
45
54
  */
46
55
  const EPIC_COVERAGE_THRESHOLD = 2;
@@ -70,7 +79,7 @@ export function isPromotable(item) {
70
79
  * Stable cluster key for a ledger item: its `class`. Items sharing a class
71
80
  * describe the same kind of signal (a product bug, a tooling-DX gap, โ€ฆ) and
72
81
  * merge into one follow-up ticket. A class whose items span many distinct
73
- * coverage surfaces is broad enough to promote to an Epic (see
82
+ * coverage surfaces is broad enough to promote to a plan-seed (see
74
83
  * {@link targetForCluster}); a class confined to one or two surfaces is a
75
84
  * single-deliverable Story. Coverage is therefore a *secondary* signal that
76
85
  * sizes the cluster rather than splitting it.
@@ -159,7 +168,7 @@ export function clusterLedgerItems(items) {
159
168
  /**
160
169
  * Decide a cluster's promotion target. A cluster that spans more than
161
170
  * {@link EPIC_COVERAGE_THRESHOLD} distinct coverage surfaces is broad enough to
162
- * warrant an Epic (`/plan --seed`); otherwise it is a single-deliverable
171
+ * warrant a plan-seed (`/plan --seed`); otherwise it is a single-deliverable
163
172
  * Story (`/plan`).
164
173
  *
165
174
  * @param {{ coverages: string[] }} cluster
@@ -170,7 +179,7 @@ export function targetForCluster(cluster) {
170
179
  ? cluster.coverages.length
171
180
  : 0;
172
181
  return surfaces > EPIC_COVERAGE_THRESHOLD
173
- ? PROMOTION_TARGETS.EPIC
182
+ ? PROMOTION_TARGETS.PLAN_SEED
174
183
  : PROMOTION_TARGETS.STORY;
175
184
  }
176
185
 
@@ -224,7 +233,7 @@ function routedToLink(issue, kind) {
224
233
  }
225
234
 
226
235
  /**
227
- * Promote the clustered untriaged ledger items into Stories / Epics via the
236
+ * Promote the clustered untriaged ledger items into Stories / plan-seeds via the
228
237
  * shared findings/route logic, then write the resulting `routedTo` issue link
229
238
  * back onto each contributing ledger item.
230
239
  *
@@ -233,7 +242,7 @@ function routedToLink(issue, kind) {
233
242
  * shared `routeFinding` against existing Issues (via the injected search
234
243
  * port). This dedups against work already filed.
235
244
  * 2. On a `new` decision, open the follow-up ticket through the injected
236
- * `createStory` (`/plan`) or `createEpic` (`/plan --seed`) port,
245
+ * `createStory` (`/plan`) or `createPlanSeed` (`/plan --seed`) port,
237
246
  * chosen by {@link targetForCluster}. On any other decision, link back to
238
247
  * the matched Issue rather than creating a duplicate.
239
248
  * 3. Stamp the resolved `routedTo` link onto every contributing ledger item
@@ -251,8 +260,8 @@ function routedToLink(issue, kind) {
251
260
  * Optional semantic candidate search, forwarded to `routeFinding`.
252
261
  * @param {(cluster: object) => Promise<{ number: number, url?: string }>} ports.createStory
253
262
  * Opens a single Story (`/plan`) for a tight cluster.
254
- * @param {(cluster: object) => Promise<{ number: number, url?: string }>} ports.createEpic
255
- * Opens an Epic (`/plan --seed`) for a broad cluster.
263
+ * @param {(cluster: object) => Promise<{ number: number, url?: string }>} ports.createPlanSeed
264
+ * Opens a plan-seed (`/plan --seed`) run for a broad cluster.
256
265
  * @returns {Promise<{
257
266
  * promotions: Array<{
258
267
  * clusterKey: string,
@@ -268,7 +277,7 @@ function routedToLink(issue, kind) {
268
277
  * @throws {Error} when a required create port is missing for a routed cluster.
269
278
  */
270
279
  export async function promoteFindings(ledgerItems, ports = {}) {
271
- const { searchIssues, searchCandidates, createStory, createEpic } = ports;
280
+ const { searchIssues, searchCandidates, createStory, createPlanSeed } = ports;
272
281
  if (
273
282
  typeof searchCandidates !== 'function' &&
274
283
  typeof searchIssues !== 'function'
@@ -294,10 +303,10 @@ export async function promoteFindings(ledgerItems, ports = {}) {
294
303
 
295
304
  if (route.decision === 'new') {
296
305
  const createPort =
297
- target === PROMOTION_TARGETS.EPIC ? createEpic : createStory;
306
+ target === PROMOTION_TARGETS.PLAN_SEED ? createPlanSeed : createStory;
298
307
  if (typeof createPort !== 'function') {
299
308
  throw new Error(
300
- `promoteFindings: a ${target === PROMOTION_TARGETS.EPIC ? 'createEpic' : 'createStory'} port is required to promote cluster ${cluster.key}`,
309
+ `promoteFindings: a ${target === PROMOTION_TARGETS.PLAN_SEED ? 'createPlanSeed' : 'createStory'} port is required to promote cluster ${cluster.key}`,
301
310
  );
302
311
  }
303
312
  issue = await createPort(cluster);
@@ -0,0 +1,97 @@
1
+ /**
2
+ * format-generated-json.js โ€” run generated JSON through the project
3
+ * formatter (Biome) so a generator's output matches what the repo's
4
+ * commit-time formatting would have produced anyway.
5
+ *
6
+ * Why this exists. Generators serialize with `JSON.stringify`, which
7
+ * expands every array across multiple lines. Biome collapses short ones
8
+ * that fit inside `lineWidth` (`"allowedTools": ["Read", "Bash"]`), and
9
+ * lint-staged runs `biome format --write` over staged JSON at commit
10
+ * time โ€” so the committed artifact is Biome-shaped while a fresh
11
+ * generator run is not. The gap means regenerating on a clean tree
12
+ * always leaves format drift, and a `<generate>` โ†’ `lint` sequence fails
13
+ * on `biome ci` even when the generator's own `--check` reports the
14
+ * artifact semantically fresh (Story #4546).
15
+ *
16
+ * Running the real formatter, rather than hand-matching its array
17
+ * collapsing, keeps this correct by construction across future formatter
18
+ * and `lineWidth` changes.
19
+ *
20
+ * Stdin mode is deliberate: Biome's configured `formatWrite` command is
21
+ * whole-tree (`biome format --write .`), which is a far broader side
22
+ * effect than a generator should have. `--stdin-file-path` makes this a
23
+ * pure content transform with no filesystem writes.
24
+ */
25
+
26
+ import { spawnSync } from 'node:child_process';
27
+ import process from 'node:process';
28
+ import { Logger } from './Logger.js';
29
+
30
+ /** Wall-clock ceiling for the formatter spawn, so a hung child cannot wedge a generator run. */
31
+ const FORMATTER_TIMEOUT_MS = 30_000;
32
+
33
+ /**
34
+ * Warn once about a fallback and return null for the caller to act on.
35
+ * Owning the message here keeps every caller's write path a single
36
+ * `formatGeneratedJson(...) ?? serialized` expression.
37
+ */
38
+ function fallback(filename) {
39
+ Logger.warn(
40
+ `project formatter (biome) unavailable โ€” writing unformatted ${filename}; ` +
41
+ 'run your formatter over it if a format gate rejects it',
42
+ );
43
+ return null;
44
+ }
45
+
46
+ /**
47
+ * Format `source` as JSON using the project formatter.
48
+ *
49
+ * Best-effort by design: `.agents/` is materialized into consumer
50
+ * projects that need not have Biome installed, so an unavailable or
51
+ * failing formatter warns and returns `null` for the caller to fall back
52
+ * on its own serialization rather than failing the generator. That is
53
+ * safe wherever the artifact's freshness check compares parsed objects
54
+ * rather than bytes โ€” formatting carries no semantic content.
55
+ *
56
+ * `--no` keeps npx from reaching the network to install a missing Biome.
57
+ * `filename` is passed as a bare basename: Biome only needs it to infer
58
+ * the language and match config overrides, and a basename cannot carry
59
+ * the spaces that would break arg quoting under the Windows `shell:
60
+ * true` spawn.
61
+ *
62
+ * @param {string} source Text to format.
63
+ * @param {object} opts
64
+ * @param {string} opts.cwd Directory to resolve the formatter and its config from.
65
+ * @param {string} [opts.filename] Basename Biome attributes the stdin text to.
66
+ * @param {typeof spawnSync} [opts.spawn] Injection seam for tests.
67
+ * @returns {string|null} Formatted text with a trailing newline, or null to fall back.
68
+ */
69
+ export function formatGeneratedJson(
70
+ source,
71
+ { cwd, filename = 'generated.json', spawn = spawnSync },
72
+ ) {
73
+ let result;
74
+ try {
75
+ result = spawn(
76
+ 'npx',
77
+ ['--no', 'biome', 'format', `--stdin-file-path=${filename}`],
78
+ {
79
+ cwd,
80
+ input: source,
81
+ encoding: 'utf8',
82
+ // npm/npx ship as `.cmd` shims on Windows, which Node refuses to
83
+ // spawn without a shell since CVE-2024-27980.
84
+ shell: process.platform === 'win32',
85
+ timeout: FORMATTER_TIMEOUT_MS,
86
+ },
87
+ );
88
+ } catch {
89
+ return fallback(filename);
90
+ }
91
+ if (!result || result.error || result.status !== 0) return fallback(filename);
92
+ const stdout = result.stdout;
93
+ if (typeof stdout !== 'string' || stdout.trim() === '') {
94
+ return fallback(filename);
95
+ }
96
+ return stdout.endsWith('\n') ? stdout : `${stdout}\n`;
97
+ }
@@ -1,107 +1,35 @@
1
1
  // .agents/scripts/lib/framework-version.js
2
2
  /**
3
- * framework-version.js โ€” single source of truth for the running Mandrel
4
- * framework version and the ticket-body authoring stamp.
3
+ * framework-version.js โ€” the visible authoring-marker surface for the legacy
4
+ * ticket-body provenance stamp.
5
5
  *
6
- * Two concerns live here:
6
+ * Tickets authored under v1 carry a hybrid stamp: a hidden
7
+ * `<!-- meta: {"mandrel_version":"โ€ฆ","authored_at":"โ€ฆ"} -->` block plus a
8
+ * single visible footer line
9
+ * `> ๐Ÿท๏ธ Authored with Mandrel v<version> ยท <YYYY-MM-DD>`. The producer of
10
+ * new stamps was retired with the Epic write surface (#4574) โ€” nothing stamps
11
+ * a new ticket โ€” but bodies already stamped are live in the backlog, so the
12
+ * Story-body serializer/parser must keep round-tripping them:
7
13
  *
8
- * 1. **Version resolution.** Under npm distribution the root `package.json`
9
- * is the canonical version marker. {@link resolveFrameworkVersion} reads it
10
- * and degrades to `'unknown'` (never throws) so a missing/unreadable
11
- * manifest can never crash an authoring path. All ticket-body stamp
12
- * producers delegate here (DRY โ€” one manifest reader).
14
+ * - {@link AUTHORED_MARKER_LINE_RE} lets the parser skip the marker line during
15
+ * section parsing so it never pollutes the last structured section.
16
+ * - {@link authoredMarkerLine} lets the serializer re-emit a byte-identical
17
+ * marker line for a stamp it parsed, preserving provenance verbatim.
13
18
  *
14
- * 2. **Ticket-body stamp.** Epics and Stories are stamped **once at authoring
15
- * time** with the running version and the authoring date, via a hybrid
16
- * surface:
17
- * - a hidden machine-readable field in the trailing
18
- * `<!-- meta: {"mandrel_version":"โ€ฆ","authored_at":"โ€ฆ"} -->` block
19
- * (the source of truth, queryable by tooling), and
20
- * - a single visible footer line
21
- * `> ๐Ÿท๏ธ Authored with Mandrel v<version> ยท <YYYY-MM-DD>` so a human
22
- * reading the raw GitHub issue sees the provenance without any tooling.
23
- *
24
- * The stamp is **immutable**: {@link stampFrameworkVersion} is a no-op when
25
- * the body already carries a `mandrel_version`, so a later re-render or
26
- * Epic-body edit preserves the originally-authored version verbatim rather
27
- * than bumping it to whatever version happens to be running.
28
- *
29
- * This module imports only Node builtins so it can be pulled in from the
30
- * story-body serializer, the ticket provider, and the Epic ideation renderer
31
- * without risking an import cycle.
19
+ * This module imports nothing so it can be pulled in from the story-body
20
+ * serializer without risking an import cycle.
32
21
  */
33
22
 
34
- import fs from 'node:fs';
35
- import path from 'node:path';
36
- import { fileURLToPath } from 'node:url';
37
-
38
- /** Returned when the package manifest is absent or unreadable. */
39
- export const FALLBACK_VERSION = 'unknown';
40
-
41
- /**
42
- * Trailing machine-metadata comment block: `<!-- meta: {...} -->`. Mirrors the
43
- * regex the Story-body parser uses so both surfaces recognise the same block.
44
- */
45
- const META_BLOCK_RE = /<!--\s*meta:\s*(\{[\s\S]*?\})\s*-->/;
46
-
47
23
  /**
48
24
  * The visible authoring marker line. A blockquote so GitHub renders it as a
49
- * callout. Used both to detect an already-emitted marker (for the strip step)
50
- * and, in the Story-body parser, to skip the line during section parsing so it
51
- * never pollutes the last structured section.
25
+ * callout. Used in the Story-body parser to skip the line during section
26
+ * parsing so it never pollutes the last structured section.
52
27
  */
53
28
  export const AUTHORED_MARKER_LINE_RE = /^\s*>\s*๐Ÿท๏ธ\s+Authored with Mandrel\b/;
54
29
 
55
30
  /**
56
- * Compute the default path to the root `package.json`. This module ships inside
57
- * the `mandrel` package at `<pkgRoot>/.agents/scripts/lib/framework-version.js`,
58
- * so the manifest sits three directories up โ€” the same layout in the dev repo
59
- * and the published tarball.
60
- *
61
- * @returns {string}
62
- */
63
- function defaultPkgPath() {
64
- const moduleDir = path.dirname(fileURLToPath(import.meta.url));
65
- return path.resolve(moduleDir, '../../..', 'package.json');
66
- }
67
-
68
- /**
69
- * Resolve the running framework version from the root `package.json`. Degrades
70
- * to {@link FALLBACK_VERSION} (never throws) on any read or parse failure so a
71
- * missing/unreadable manifest can never crash an authoring or hydration path.
72
- *
73
- * @param {{ pkgPath?: string }} [opts] - `pkgPath` override (test seam).
74
- * @returns {string}
75
- */
76
- export function resolveFrameworkVersion({ pkgPath } = {}) {
77
- try {
78
- const resolved = typeof pkgPath === 'string' ? pkgPath : defaultPkgPath();
79
- const parsed = JSON.parse(fs.readFileSync(resolved, 'utf8'));
80
- return typeof parsed.version === 'string' && parsed.version.trim()
81
- ? parsed.version.trim()
82
- : FALLBACK_VERSION;
83
- } catch {
84
- return FALLBACK_VERSION;
85
- }
86
- }
87
-
88
- /**
89
- * Format an authoring date as `YYYY-MM-DD` (UTC). Matches the date shape the
90
- * rest of the authoring path uses (e.g. `qa-session`).
91
- *
92
- * @param {Date} [date=new Date()]
93
- * @returns {string}
94
- */
95
- export function formatAuthoredDate(date = new Date()) {
96
- const d =
97
- date instanceof Date && !Number.isNaN(date.getTime()) ? date : new Date();
98
- return d.toISOString().slice(0, 10);
99
- }
100
-
101
- /**
102
- * Build the visible authoring marker line for a given stamp. Centralised so
103
- * the string is byte-identical across the two producers (the Story-body
104
- * serializer and {@link stampFrameworkVersion}).
31
+ * Build the visible authoring marker line for a given stamp. The Story-body
32
+ * serializer uses this to re-emit a legacy stamp it parsed, byte-identically.
105
33
  *
106
34
  * @param {{ version: string, authoredAt: string }} stamp
107
35
  * @returns {string}
@@ -109,101 +37,3 @@ export function formatAuthoredDate(date = new Date()) {
109
37
  export function authoredMarkerLine({ version, authoredAt }) {
110
38
  return `> ๐Ÿท๏ธ Authored with Mandrel v${version} ยท ${authoredAt}`;
111
39
  }
112
-
113
- /**
114
- * Read the framework stamp from a body's trailing meta block. Returns
115
- * `{ version, authoredAt }` when a non-empty `mandrel_version` is present, or
116
- * `null` when the body carries no stamp (or the meta block is malformed).
117
- * `authoredAt` is `null` when the version is present but the date is absent.
118
- *
119
- * @param {string} markdown
120
- * @returns {{ version: string, authoredAt: string|null }|null}
121
- */
122
- export function extractFrameworkStamp(markdown) {
123
- if (typeof markdown !== 'string') return null;
124
- const match = markdown.match(META_BLOCK_RE);
125
- if (!match) return null;
126
- let parsed;
127
- try {
128
- parsed = JSON.parse(match[1]);
129
- } catch {
130
- return null;
131
- }
132
- if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
133
- return null;
134
- }
135
- const version =
136
- typeof parsed.mandrel_version === 'string' && parsed.mandrel_version.trim()
137
- ? parsed.mandrel_version.trim()
138
- : null;
139
- if (version === null) return null;
140
- const authoredAt =
141
- typeof parsed.authored_at === 'string' && parsed.authored_at.trim()
142
- ? parsed.authored_at.trim()
143
- : null;
144
- return { version, authoredAt };
145
- }
146
-
147
- /**
148
- * Stamp a ticket body (Epic or Story markdown) with the framework version and
149
- * authoring date โ€” **once**. When the body already carries a `mandrel_version`
150
- * the body is returned verbatim (immutability: never re-derive or bump an
151
- * already-authored stamp). Otherwise the version keys are merged into (or
152
- * create) the trailing `<!-- meta -->` block โ€” appended **last** so the key
153
- * order stays stable with the Story-body serializer โ€” and the visible marker
154
- * line is (re)emitted just above it.
155
- *
156
- * The `version` / `authoredAt` overrides let a caller (e.g. the Epic edit path)
157
- * preserve a previously-authored stamp; both default to the running version and
158
- * today's date when omitted.
159
- *
160
- * @param {string} markdown
161
- * @param {{ version?: string, authoredAt?: string }} [stamp]
162
- * @returns {string}
163
- */
164
- export function stampFrameworkVersion(markdown, stamp = {}) {
165
- const body = typeof markdown === 'string' ? markdown : '';
166
-
167
- // Immutability: a body that already carries a version is preserved verbatim.
168
- if (extractFrameworkStamp(body) !== null) return body;
169
-
170
- const version =
171
- typeof stamp?.version === 'string' && stamp.version.trim()
172
- ? stamp.version.trim()
173
- : resolveFrameworkVersion();
174
- const authoredAt =
175
- typeof stamp?.authoredAt === 'string' && stamp.authoredAt.trim()
176
- ? stamp.authoredAt.trim()
177
- : formatAuthoredDate();
178
-
179
- // Merge into any existing (version-less) meta block, appending the version
180
- // keys last for stable key order.
181
- const metaMatch = body.match(META_BLOCK_RE);
182
- const meta = {};
183
- if (metaMatch) {
184
- try {
185
- const parsed = JSON.parse(metaMatch[1]);
186
- if (
187
- parsed !== null &&
188
- typeof parsed === 'object' &&
189
- !Array.isArray(parsed)
190
- ) {
191
- Object.assign(meta, parsed);
192
- }
193
- } catch {
194
- // Malformed meta comment โ€” drop it and re-emit a clean block.
195
- }
196
- }
197
- meta.mandrel_version = version;
198
- meta.authored_at = authoredAt;
199
-
200
- // Strip any existing meta block / marker so both re-append canonically.
201
- const head = body
202
- .replace(META_BLOCK_RE, '')
203
- .replace(new RegExp(AUTHORED_MARKER_LINE_RE.source, 'm'), '')
204
- .replace(/\n{3,}/g, '\n\n')
205
- .trimEnd();
206
-
207
- const marker = authoredMarkerLine({ version, authoredAt });
208
- return `${head}\n\n${marker}\n\n<!-- meta: ${JSON.stringify(meta)} -->`;
209
- }
@@ -505,6 +505,14 @@ export function createGh(execImpl = exec, defaultExecOpts = {}) {
505
505
  execWithDefaults({ args: ['pr', 'edit', idStr(id), ...flags] }),
506
506
  merge: (id, flags = []) =>
507
507
  execWithDefaults({ args: ['pr', 'merge', idStr(id), ...flags] }),
508
+ /**
509
+ * Bring a `mergeStateStatus: BEHIND` PR up to date with its base
510
+ * (Story #4543). The close-and-land merge wait calls this a bounded
511
+ * number of times rather than waiting out its budget behind a base it
512
+ * could have caught up to.
513
+ */
514
+ updateBranch: (id, flags = []) =>
515
+ execWithDefaults({ args: ['pr', 'update-branch', idStr(id), ...flags] }),
508
516
  list: (flags = [], fields) =>
509
517
  execWithDefaults({
510
518
  args: ['pr', 'list', ...flags, ...jsonFlag(fields)],