mandrel 1.94.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (560) hide show
  1. package/.agents/README.md +116 -99
  2. package/.agents/agents/acceptance-critic.md +9 -7
  3. package/.agents/agents/story-worker.md +45 -51
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +498 -1287
  6. package/.agents/docs/agentrc-reference.json +185 -80
  7. package/.agents/docs/configuration.md +132 -190
  8. package/.agents/docs/execution-reference.md +51 -25
  9. package/.agents/docs/quality-gates.md +25 -33
  10. package/.agents/docs/workflows.md +8 -8
  11. package/.agents/instructions.md +113 -139
  12. package/.agents/rules/ci-remediation.md +11 -15
  13. package/.agents/rules/git-conventions-reference.md +48 -58
  14. package/.agents/rules/git-conventions.md +16 -22
  15. package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
  16. package/.agents/schemas/agentrc.schema.json +83 -254
  17. package/.agents/schemas/audit-rules.json +59 -1
  18. package/.agents/schemas/audit-rules.schema.json +33 -1
  19. package/.agents/schemas/lifecycle/README.md +1 -2
  20. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  21. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  22. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  23. package/.agents/schemas/lifecycle/retro.end.schema.json +1 -1
  24. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  25. package/.agents/schemas/signal-event.schema.json +3 -3
  26. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  27. package/.agents/schemas/validation-evidence.schema.json +1 -1
  28. package/.agents/scripts/acceptance-eval.js +22 -66
  29. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  30. package/.agents/scripts/audit-to-stories.js +7 -7
  31. package/.agents/scripts/boot-sweep.js +1 -1
  32. package/.agents/scripts/bootstrap.js +3 -3
  33. package/.agents/scripts/check-dead-exports.js +43 -104
  34. package/.agents/scripts/check-doc-links.js +2 -2
  35. package/.agents/scripts/check-lifecycle-lint.js +7 -10
  36. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  37. package/.agents/scripts/cleanup-repo-test-temp.js +6 -1
  38. package/.agents/scripts/deliver-recover.js +122 -0
  39. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  40. package/.agents/scripts/evidence-gate.js +20 -50
  41. package/.agents/scripts/generate-skills-index.js +17 -1
  42. package/.agents/scripts/generate-workflows-doc.js +4 -4
  43. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  44. package/.agents/scripts/lib/Logger.js +6 -10
  45. package/.agents/scripts/lib/audit-suite/runner.js +2 -2
  46. package/.agents/scripts/lib/audit-suite/selector.js +328 -28
  47. package/.agents/scripts/lib/audit-to-stories/{seed-epic-from-findings.js → seed-from-findings.js} +9 -9
  48. package/.agents/scripts/lib/baselines/kernel.js +206 -18
  49. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -15
  50. package/.agents/scripts/lib/baselines/reader.js +1 -6
  51. package/.agents/scripts/lib/bdd-runner-detect.js +5 -9
  52. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  53. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +32 -33
  54. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  55. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  56. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  57. package/.agents/scripts/lib/checks/core-bare-clean.js +6 -3
  58. package/.agents/scripts/lib/checks/index.js +3 -2
  59. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  60. package/.agents/scripts/lib/checks/state.js +17 -248
  61. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +26 -24
  62. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  63. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  64. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  65. package/.agents/scripts/lib/cli/standard-args.js +13 -22
  66. package/.agents/scripts/lib/cli-args.js +39 -9
  67. package/.agents/scripts/lib/close-validation/gates.js +15 -15
  68. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  69. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  70. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  71. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  72. package/.agents/scripts/lib/config/ci.js +6 -31
  73. package/.agents/scripts/lib/config/delivery-routing.js +52 -35
  74. package/.agents/scripts/lib/config/explain.js +61 -48
  75. package/.agents/scripts/lib/config/github.js +7 -5
  76. package/.agents/scripts/lib/config/limits.js +29 -80
  77. package/.agents/scripts/lib/config/paths.js +0 -2
  78. package/.agents/scripts/lib/config/quality.js +12 -15
  79. package/.agents/scripts/lib/config/runners.js +20 -66
  80. package/.agents/scripts/lib/config/temp-paths.js +30 -63
  81. package/.agents/scripts/lib/config/worktree-isolation.js +0 -5
  82. package/.agents/scripts/lib/config-resolver.js +2 -7
  83. package/.agents/scripts/lib/config-settings-schema-delivery.js +55 -161
  84. package/.agents/scripts/lib/config-settings-schema-quality.js +17 -16
  85. package/.agents/scripts/lib/config-settings-schema.js +100 -60
  86. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  87. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  88. package/.agents/scripts/lib/dependency-parser.js +3 -2
  89. package/.agents/scripts/lib/doc-tiers.js +2 -2
  90. package/.agents/scripts/lib/duplicate-search.js +242 -41
  91. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
  92. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  93. package/.agents/scripts/lib/format-generated-json.js +97 -0
  94. package/.agents/scripts/lib/framework-version.js +19 -190
  95. package/.agents/scripts/lib/gh-exec.js +8 -0
  96. package/.agents/scripts/lib/git-branch-cleanup.js +1 -10
  97. package/.agents/scripts/lib/git-branch-lifecycle.js +17 -180
  98. package/.agents/scripts/lib/git-utils.js +32 -20
  99. package/.agents/scripts/lib/github/framework-repo.js +6 -0
  100. package/.agents/scripts/lib/json-utils.js +1 -2
  101. package/.agents/scripts/lib/label-constants.js +10 -38
  102. package/.agents/scripts/lib/label-taxonomy.js +10 -55
  103. package/.agents/scripts/lib/observability/active-story-env.js +44 -165
  104. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  105. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  106. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  107. package/.agents/scripts/lib/observability/source-classifier.js +5 -5
  108. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  109. package/.agents/scripts/lib/onboard/init-tail.js +1 -3
  110. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  111. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  112. package/.agents/scripts/lib/orchestration/ceremony-routing.js +105 -44
  113. package/.agents/scripts/lib/orchestration/code-review.js +78 -436
  114. package/.agents/scripts/lib/orchestration/column-sync.js +1 -1
  115. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +4 -4
  116. package/.agents/scripts/lib/orchestration/context-envelope.js +2 -5
  117. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  118. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  119. package/.agents/scripts/lib/orchestration/docs-digest.js +8 -8
  120. package/.agents/scripts/lib/orchestration/file-assumptions.js +7 -13
  121. package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
  122. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  123. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  124. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  125. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +17 -19
  126. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  127. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  128. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +6 -3
  129. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +21 -43
  130. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  131. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  132. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +4 -4
  133. package/.agents/scripts/lib/orchestration/merge-block-class.js +77 -21
  134. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  135. package/.agents/scripts/lib/orchestration/phase-runner.js +3 -2
  136. package/.agents/scripts/lib/orchestration/plan-context.js +354 -282
  137. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +11 -22
  138. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +4 -8
  139. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  140. package/.agents/scripts/lib/orchestration/plan-navigation.js +92 -0
  141. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +71 -0
  142. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +245 -0
  143. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  144. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +432 -858
  145. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +778 -0
  146. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +47 -115
  147. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  148. package/.agents/scripts/lib/orchestration/plan-reachability.js +9 -14
  149. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +1 -1
  150. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/authoring-context.js +52 -51
  151. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +27 -0
  152. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/spec-authoring-grounding.js +1 -1
  153. package/.agents/scripts/lib/orchestration/pr-base-guard.js +18 -28
  154. package/.agents/scripts/lib/orchestration/remote-verifier.js +1 -1
  155. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  156. package/.agents/scripts/lib/orchestration/resolves-token.js +1 -1
  157. package/.agents/scripts/lib/orchestration/retro-proposals.js +140 -79
  158. package/.agents/scripts/lib/orchestration/review-depth.js +126 -47
  159. package/.agents/scripts/lib/orchestration/review-providers/codex.js +2 -2
  160. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  161. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  162. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  163. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +21 -56
  164. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  165. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  166. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  167. package/.agents/scripts/lib/orchestration/run-epilogue.js +784 -0
  168. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +25 -1
  169. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  170. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  171. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +86 -41
  172. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  173. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +77 -32
  174. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  175. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  176. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +44 -0
  177. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  178. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  179. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +275 -46
  180. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  181. package/.agents/scripts/lib/orchestration/spec-freshness.js +14 -205
  182. package/.agents/scripts/lib/orchestration/spec-section-validator.js +4 -5
  183. package/.agents/scripts/lib/orchestration/spec-spill.js +60 -0
  184. package/.agents/scripts/lib/orchestration/split-policy-validator.js +188 -0
  185. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +49 -0
  186. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  187. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +28 -42
  188. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  189. package/.agents/scripts/lib/orchestration/story-follow-ups.js +298 -0
  190. package/.agents/scripts/lib/orchestration/story-init-remote.js +51 -0
  191. package/.agents/scripts/lib/orchestration/story-plan-state.js +33 -0
  192. package/.agents/scripts/lib/orchestration/structured-comment-parser.js +1 -1
  193. package/.agents/scripts/lib/orchestration/task-body-validator.js +60 -25
  194. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  195. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +130 -75
  196. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +192 -377
  197. package/.agents/scripts/lib/orchestration/ticket-validator.js +123 -25
  198. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -72
  199. package/.agents/scripts/lib/orchestration/ticketing/reads.js +45 -55
  200. package/.agents/scripts/lib/orchestration/ticketing/transition.js +66 -6
  201. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  202. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  203. package/.agents/scripts/lib/planning-corpus.js +12 -281
  204. package/.agents/scripts/lib/preflight-runner.js +4 -4
  205. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -5
  206. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  207. package/.agents/scripts/lib/signals/index.js +4 -17
  208. package/.agents/scripts/lib/signals/read.js +35 -35
  209. package/.agents/scripts/lib/signals/schema.js +8 -11
  210. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  211. package/.agents/scripts/lib/signals/write.js +0 -1
  212. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  213. package/.agents/scripts/lib/single-story-sweep/protection-ctx.js +1 -1
  214. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  215. package/.agents/scripts/lib/story-adjacency.js +17 -19
  216. package/.agents/scripts/lib/story-body/story-body.js +130 -75
  217. package/.agents/scripts/lib/story-plan.js +2 -4
  218. package/.agents/scripts/lib/templates/decomposer-prompts.js +51 -46
  219. package/.agents/scripts/lib/templates/spec-author-prompts.js +47 -45
  220. package/.agents/scripts/lib/test-env.js +14 -1
  221. package/.agents/scripts/lib/test-tiers.js +0 -3
  222. package/.agents/scripts/lib/{epic-body-sections.js → ticket-body-sections.js} +25 -39
  223. package/.agents/scripts/lib/validation-evidence.js +32 -60
  224. package/.agents/scripts/lib/wave-runner/ready-set.js +38 -12
  225. package/.agents/scripts/lib/workspace-provisioner.js +1 -1
  226. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  227. package/.agents/scripts/lib/worktree/lifecycle/reap.js +72 -25
  228. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  229. package/.agents/scripts/lint-issue-body.js +71 -40
  230. package/.agents/scripts/mandrel-update-preflight.js +1 -1
  231. package/.agents/scripts/notify.js +4 -3
  232. package/.agents/scripts/plan-context.js +102 -80
  233. package/.agents/scripts/plan-persist.js +230 -279
  234. package/.agents/scripts/plan-run-epilogue.js +142 -0
  235. package/.agents/scripts/post-structured-comment.js +0 -38
  236. package/.agents/scripts/pr-watch-with-update.js +43 -22
  237. package/.agents/scripts/providers/github/compose.js +0 -1
  238. package/.agents/scripts/providers/github/errors.js +0 -19
  239. package/.agents/scripts/providers/github/issues.js +13 -39
  240. package/.agents/scripts/providers/github/mappers.js +5 -12
  241. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  242. package/.agents/scripts/providers/github/tickets.js +33 -156
  243. package/.agents/scripts/providers/github.js +17 -6
  244. package/.agents/scripts/resolve-stories.js +236 -0
  245. package/.agents/scripts/run-coverage.js +4 -1
  246. package/.agents/scripts/run-lint.js +2 -2
  247. package/.agents/scripts/run-verify.js +31 -2
  248. package/.agents/scripts/signals-view.js +25 -21
  249. package/.agents/scripts/single-story-close.js +178 -26
  250. package/.agents/scripts/single-story-confirm-merge.js +313 -24
  251. package/.agents/scripts/single-story-init.js +35 -30
  252. package/.agents/scripts/stories-wave-tick.js +85 -10
  253. package/.agents/scripts/story-plan.js +28 -49
  254. package/.agents/scripts/update-ticket-state.js +14 -65
  255. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  256. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  257. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  258. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  259. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  260. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  261. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  262. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  263. package/.agents/skills/core/idea-refinement/SKILL.md +9 -9
  264. package/.agents/skills/core/scope-triage/SKILL.md +31 -172
  265. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  266. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  267. package/.agents/skills/skills.index.json +3 -53
  268. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  269. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  270. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  271. package/.agents/starter-agentrc.json +0 -5
  272. package/.agents/templates/agent-protocol.md +9 -10
  273. package/.agents/workflows/audit-architecture.md +6 -7
  274. package/.agents/workflows/audit-clean-code.md +7 -7
  275. package/.agents/workflows/audit-dependencies.md +3 -3
  276. package/.agents/workflows/audit-devops.md +3 -3
  277. package/.agents/workflows/audit-documentation.md +9 -10
  278. package/.agents/workflows/audit-lighthouse.md +11 -3
  279. package/.agents/workflows/audit-navigability.md +13 -2
  280. package/.agents/workflows/audit-performance.md +5 -6
  281. package/.agents/workflows/audit-privacy.md +3 -3
  282. package/.agents/workflows/audit-quality.md +11 -12
  283. package/.agents/workflows/audit-security.md +4 -5
  284. package/.agents/workflows/audit-seo.md +13 -3
  285. package/.agents/workflows/audit-sre.md +3 -3
  286. package/.agents/workflows/audit-to-stories.md +20 -20
  287. package/.agents/workflows/audit-ux-ui.md +10 -3
  288. package/.agents/workflows/deliver.md +177 -176
  289. package/.agents/workflows/git-cleanup.md +5 -6
  290. package/.agents/workflows/git-deliver.md +1 -1
  291. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  292. package/.agents/workflows/helpers/acceptance-self-eval.md +35 -40
  293. package/.agents/workflows/helpers/code-quality-guardrails.md +7 -7
  294. package/.agents/workflows/helpers/code-review.md +75 -196
  295. package/.agents/workflows/helpers/{single-story-deliver-reference.md → deliver-story-reference.md} +83 -44
  296. package/.agents/workflows/helpers/deliver-story.md +606 -0
  297. package/.agents/workflows/helpers/diagnose.md +10 -10
  298. package/.agents/workflows/helpers/parallel-tooling.md +3 -3
  299. package/.agents/workflows/helpers/signals.md +16 -16
  300. package/.agents/workflows/helpers/worktree-lifecycle.md +66 -86
  301. package/.agents/workflows/mandrel-update.md +2 -1
  302. package/.agents/workflows/plan.md +277 -145
  303. package/.agents/workflows/qa-assist.md +27 -33
  304. package/.agents/workflows/qa-explore.md +29 -38
  305. package/.agents/workflows/qa-run.md +2 -6
  306. package/README.md +9 -8
  307. package/bin/mandrel.js +12 -1
  308. package/docs/CHANGELOG.md +70 -0
  309. package/lib/cli/registry.js +262 -19
  310. package/lib/cli/sync-agents.js +157 -0
  311. package/lib/cli/sync-commands.js +115 -6
  312. package/lib/cli/sync.js +168 -6
  313. package/lib/cli/update.js +105 -8
  314. package/lib/cli/version-helpers.js +131 -0
  315. package/lib/migrations/README.md +7 -5
  316. package/lib/migrations/index.js +12 -8
  317. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  318. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  319. package/package.json +2 -2
  320. package/.agents/agents/retro.md +0 -42
  321. package/.agents/personas/architect.md +0 -113
  322. package/.agents/personas/devops-engineer.md +0 -38
  323. package/.agents/personas/engineer.md +0 -33
  324. package/.agents/personas/project-manager.md +0 -114
  325. package/.agents/personas/qa-engineer.md +0 -95
  326. package/.agents/personas/security-engineer.md +0 -111
  327. package/.agents/personas/technical-writer.md +0 -101
  328. package/.agents/schemas/dispatch-manifest.json +0 -232
  329. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  330. package/.agents/schemas/epic-spec.schema.json +0 -153
  331. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  332. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  333. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  334. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  335. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  336. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  337. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  338. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  339. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  340. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  341. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  342. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  343. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  344. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  345. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  346. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  347. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  348. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  349. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  350. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  351. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  352. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  353. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  354. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  355. package/.agents/schemas/lifecycle/slice.end.schema.json +0 -21
  356. package/.agents/schemas/lifecycle/slice.heartbeat.schema.json +0 -20
  357. package/.agents/schemas/lifecycle/slice.start.schema.json +0 -17
  358. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  359. package/.agents/schemas/risk-verdict.schema.json +0 -66
  360. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  361. package/.agents/scripts/acceptance-spec-reconciler.js +0 -642
  362. package/.agents/scripts/analyze-execution.js +0 -444
  363. package/.agents/scripts/bookkeeping-reconcile.js +0 -117
  364. package/.agents/scripts/check-prepush-recovery.js +0 -90
  365. package/.agents/scripts/dispatcher.js +0 -295
  366. package/.agents/scripts/epic-audit-prepare.js +0 -497
  367. package/.agents/scripts/epic-audit-recheck.js +0 -274
  368. package/.agents/scripts/epic-deliver-note-intervention.js +0 -192
  369. package/.agents/scripts/epic-deliver-preflight.js +0 -462
  370. package/.agents/scripts/epic-deliver-prepare.js +0 -852
  371. package/.agents/scripts/epic-execute-record-wave.js +0 -449
  372. package/.agents/scripts/epic-plan-clarity.js +0 -211
  373. package/.agents/scripts/epic-plan-healthcheck.js +0 -581
  374. package/.agents/scripts/epic-reconcile.js +0 -625
  375. package/.agents/scripts/lib/baseline-snapshot.js +0 -979
  376. package/.agents/scripts/lib/checks/epic-merge-lock-stale.js +0 -54
  377. package/.agents/scripts/lib/checks/stale-origin-epic.js +0 -49
  378. package/.agents/scripts/lib/config/lifecycle.js +0 -40
  379. package/.agents/scripts/lib/config/preflight.js +0 -58
  380. package/.agents/scripts/lib/config/retro.js +0 -77
  381. package/.agents/scripts/lib/epic-merge-lock.js +0 -322
  382. package/.agents/scripts/lib/epic-plan-clarity.js +0 -181
  383. package/.agents/scripts/lib/epic-plan-ideation.js +0 -261
  384. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  385. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  386. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -219
  387. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  388. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  389. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  390. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -270
  391. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +0 -539
  392. package/.agents/scripts/lib/orchestration/deliver-route.js +0 -173
  393. package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -134
  394. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +0 -183
  395. package/.agents/scripts/lib/orchestration/epic-cleanup.js +0 -801
  396. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +0 -310
  397. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +0 -163
  398. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +0 -140
  399. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +0 -64
  400. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +0 -72
  401. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +0 -156
  402. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +0 -345
  403. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +0 -41
  404. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/reconcile-spawn.js +0 -86
  405. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +0 -391
  406. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +0 -94
  407. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +0 -236
  408. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/risk-verdict.js +0 -105
  409. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +0 -307
  410. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +0 -117
  411. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +0 -117
  412. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +0 -621
  413. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +0 -186
  414. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +0 -50
  415. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +0 -129
  416. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +0 -103
  417. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +0 -267
  418. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -210
  419. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +0 -238
  420. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/_bullet-format.js +0 -32
  421. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +0 -203
  422. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +0 -227
  423. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +0 -117
  424. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/stalled-worktree.js +0 -37
  425. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +0 -127
  426. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +0 -400
  427. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +0 -276
  428. package/.agents/scripts/lib/orchestration/epic-runner/wave-scheduler.js +0 -66
  429. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +0 -789
  430. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +0 -676
  431. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-discriminator.js +0 -389
  432. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-format.js +0 -230
  433. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +0 -361
  434. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  435. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +0 -306
  436. package/.agents/scripts/lib/orchestration/finalize/post-handoff-comment.js +0 -489
  437. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +0 -88
  438. package/.agents/scripts/lib/orchestration/lifecycle/emit-slice-lifecycle.js +0 -270
  439. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +0 -147
  440. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  441. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  442. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +0 -465
  443. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +0 -501
  444. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +0 -984
  445. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +0 -264
  446. package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +0 -284
  447. package/.agents/scripts/lib/orchestration/lifecycle/listeners/cleaner.js +0 -355
  448. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +0 -673
  449. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +0 -378
  450. package/.agents/scripts/lib/orchestration/lifecycle/listeners/intervention-recorder.js +0 -140
  451. package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +0 -144
  452. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  453. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +0 -174
  454. package/.agents/scripts/lib/orchestration/manifest-builder.js +0 -222
  455. package/.agents/scripts/lib/orchestration/plan-persist/amend.js +0 -359
  456. package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +0 -127
  457. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  458. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  459. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  460. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  461. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -33
  462. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  463. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  464. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  465. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -396
  466. package/.agents/scripts/lib/orchestration/post-merge-pipeline.js +0 -205
  467. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  468. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +0 -152
  469. package/.agents/scripts/lib/orchestration/retro/phases/checks.js +0 -94
  470. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +0 -571
  471. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +0 -450
  472. package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +0 -191
  473. package/.agents/scripts/lib/orchestration/retro-heuristics.js +0 -57
  474. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  475. package/.agents/scripts/lib/orchestration/retro-runner.js +0 -197
  476. package/.agents/scripts/lib/orchestration/spec-renderer.js +0 -447
  477. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +0 -747
  478. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/gate-failure.js +0 -211
  479. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +0 -158
  480. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +0 -446
  481. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +0 -297
  482. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/scope-discovery.js +0 -48
  483. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution-wiring.js +0 -67
  484. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution.js +0 -161
  485. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +0 -117
  486. package/.agents/scripts/lib/orchestration/story-close/cd-out-guard.js +0 -86
  487. package/.agents/scripts/lib/orchestration/story-close/cleanup-reconciler.js +0 -147
  488. package/.agents/scripts/lib/orchestration/story-close/close-inputs.js +0 -142
  489. package/.agents/scripts/lib/orchestration/story-close/comment-bodies.js +0 -62
  490. package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +0 -658
  491. package/.agents/scripts/lib/orchestration/story-close/merge-subject.js +0 -198
  492. package/.agents/scripts/lib/orchestration/story-close/phases/branch-restore.js +0 -105
  493. package/.agents/scripts/lib/orchestration/story-close/phases/close.js +0 -222
  494. package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +0 -292
  495. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +0 -270
  496. package/.agents/scripts/lib/orchestration/story-close/phases/preflight.js +0 -110
  497. package/.agents/scripts/lib/orchestration/story-close/phases/refresh.js +0 -86
  498. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked-emitter.js +0 -112
  499. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked.js +0 -157
  500. package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +0 -421
  501. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +0 -302
  502. package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +0 -163
  503. package/.agents/scripts/lib/orchestration/story-close-recovery.js +0 -690
  504. package/.agents/scripts/lib/orchestration/wave-marker.js +0 -28
  505. package/.agents/scripts/lib/orchestration/wave-record-io.js +0 -218
  506. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +0 -145
  507. package/.agents/scripts/lib/orchestration/wave-record-projection.js +0 -212
  508. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +0 -111
  509. package/.agents/scripts/lib/presentation/manifest-builder.js +0 -239
  510. package/.agents/scripts/lib/presentation/manifest-formatter.js +0 -242
  511. package/.agents/scripts/lib/presentation/manifest-helpers.js +0 -213
  512. package/.agents/scripts/lib/presentation/manifest-persistence.js +0 -261
  513. package/.agents/scripts/lib/presentation/manifest-procedures.js +0 -55
  514. package/.agents/scripts/lib/presentation/manifest-render-waves.js +0 -306
  515. package/.agents/scripts/lib/presentation/manifest-renderer.js +0 -188
  516. package/.agents/scripts/lib/presentation/manifest-story-views.js +0 -110
  517. package/.agents/scripts/lib/push-epic-retry.js +0 -209
  518. package/.agents/scripts/lib/spec/index.js +0 -36
  519. package/.agents/scripts/lib/spec/loader.js +0 -425
  520. package/.agents/scripts/lib/spec/state.js +0 -208
  521. package/.agents/scripts/lib/story-init/blocker-validator.js +0 -68
  522. package/.agents/scripts/lib/story-init/branch-initializer.js +0 -408
  523. package/.agents/scripts/lib/story-init/context-resolver.js +0 -92
  524. package/.agents/scripts/lib/story-init/donor-precheck.js +0 -207
  525. package/.agents/scripts/lib/story-init/state-transitioner.js +0 -80
  526. package/.agents/scripts/lib/story-init/task-graph-builder.js +0 -124
  527. package/.agents/scripts/lib/story-init/transition-summary.js +0 -34
  528. package/.agents/scripts/lib/test-reserved-epic-temp-ids.js +0 -35
  529. package/.agents/scripts/lib/wave-runner/tick.js +0 -754
  530. package/.agents/scripts/lib/wave-runner/wave-runner-error.js +0 -20
  531. package/.agents/scripts/lifecycle-emit-story-dispatch.js +0 -194
  532. package/.agents/scripts/lifecycle-emit.js +0 -510
  533. package/.agents/scripts/retro-run.js +0 -218
  534. package/.agents/scripts/slice-phase.js +0 -361
  535. package/.agents/scripts/standalone-feedback-rollup.js +0 -188
  536. package/.agents/scripts/story-close.js +0 -294
  537. package/.agents/scripts/story-init.js +0 -599
  538. package/.agents/scripts/story-phase.js +0 -369
  539. package/.agents/scripts/wave-tick.js +0 -464
  540. package/.agents/skills/core/analyze-execution/SKILL.md +0 -98
  541. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +0 -172
  542. package/.agents/skills/core/epic-plan-consolidate/examples.md +0 -51
  543. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +0 -441
  544. package/.agents/skills/core/epic-plan-decompose-author/examples.md +0 -47
  545. package/.agents/skills/core/epic-plan-premortem/SKILL.md +0 -146
  546. package/.agents/skills/core/epic-plan-premortem/examples.md +0 -53
  547. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +0 -383
  548. package/.agents/skills/core/epic-plan-spec-author/examples.md +0 -91
  549. package/.agents/workflows/helpers/deliver-epic-reference.md +0 -547
  550. package/.agents/workflows/helpers/deliver-epic-single.md +0 -331
  551. package/.agents/workflows/helpers/deliver-epic.md +0 -998
  552. package/.agents/workflows/helpers/deliver-stories.md +0 -450
  553. package/.agents/workflows/helpers/epic-audit.md +0 -189
  554. package/.agents/workflows/helpers/epic-deliver-story.md +0 -436
  555. package/.agents/workflows/helpers/epic-testing.md +0 -125
  556. package/.agents/workflows/helpers/plan-epic-reference.md +0 -160
  557. package/.agents/workflows/helpers/plan-epic.md +0 -353
  558. package/.agents/workflows/helpers/plan-story.md +0 -251
  559. package/.agents/workflows/helpers/scope-triage-gate.md +0 -108
  560. package/.agents/workflows/helpers/single-story-deliver.md +0 -557
@@ -0,0 +1,778 @@
1
+ /**
2
+ * story-ops.js — flat Story creation for v2 plan-persist (Stage 3).
3
+ *
4
+ * Under the Story collapse (`docs/roadmap.md` § Stage 3), `/plan` persists
5
+ * zero-or-more Story issues directly — no Epic parent, no reconciler tree,
6
+ * no `deliveryShape` mode matrix. Default is **one Story**; N>1 is gated by
7
+ * the Stage-1 split-policy validator (`assertAcceptancePartition`).
8
+ *
9
+ * Each Story body is the single executable document: Tech Spec stays inline
10
+ * under `## Spec`. Over-budget Specs fail closed (split / tighten) — never
11
+ * spill to `docs/`. Top-level `acceptance[]` / `verify[]` are the machine
12
+ * contract and are synced into the body so the GitHub issue stays complete
13
+ * without requiring the LLM to dual-author the same lists.
14
+ *
15
+ * @module lib/orchestration/plan-persist/story-ops
16
+ */
17
+
18
+ import { createHash } from 'node:crypto';
19
+ import { applyBlockedByDependencies } from '../../../providers/github/blocked-by-add.js';
20
+ import { Logger } from '../../Logger.js';
21
+ import { AGENT_LABELS, TYPE_LABELS } from '../../label-constants.js';
22
+ import {
23
+ parse as parseStoryBody,
24
+ serialize as serializeStoryBody,
25
+ } from '../../story-body/story-body.js';
26
+ import { assertSpecWithinBudget } from '../spec-spill.js';
27
+ import { assertAcceptancePartition } from '../split-policy-validator.js';
28
+ import {
29
+ assertSupersedePartition,
30
+ normalizeSupersedes,
31
+ } from './supersede-ops.js';
32
+
33
+ // Story #4540 removed PLAN_RUN_LABEL_PREFIX / normalizePlanRunId /
34
+ // planRunLabel from here. They minted an opaque random-hex label per N>1
35
+ // plan that nothing ever deleted, and their only external consumer was the
36
+ // (now deleted) `--run` resolver. Sibling order survives in the
37
+ // `blocked by #N` body footers this module already writes.
38
+
39
+ /**
40
+ * Marker prefix for the per-Story plan fingerprint appended to every
41
+ * created body. `createStoryIssues` greps open `type::story` issues for
42
+ * `<!-- plan-story: <fingerprint> -->` to decide whether a Story already
43
+ * exists — the idempotency half of the resumable-create contract.
44
+ */
45
+ const PLAN_FINGERPRINT_MARKER_PREFIX = 'plan-story:';
46
+
47
+ /** Length of the hex fingerprint digest. Collision-free at plan scale. */
48
+ const PLAN_FINGERPRINT_LENGTH = 16;
49
+
50
+ /**
51
+ * Compute the deterministic identity of one authored Story within a plan.
52
+ *
53
+ * Derived from `slug` + `title` + the **assembled** body, so the fingerprint
54
+ * identifies the authored *content*, not merely a name. Because the marker is
55
+ * what `createStoryIssues` adopts on, that is the whole safety property of the
56
+ * resume path: a fingerprint hit means the open Story on the tracker is
57
+ * byte-identical to what this run would author, so adopting it instead of
58
+ * creating it is a genuine no-op.
59
+ *
60
+ * **Why the body is in scope.** It used to be excluded, on the rationale that
61
+ * creation rewrites the body to substitute real issue ids into `depends_on`
62
+ * footers, so a body-derived fingerprint would differ between an aborted run
63
+ * and its resume. That conflated two different bodies. The fingerprint is
64
+ * computed in `assembleOnePlanStory` over the *assembled* body — whose
65
+ * `depends_on` are still sibling **slugs**, and which is a pure function of
66
+ * `stories.json` plus the shared Spec. The id substitution happens later, and
67
+ * only inside `renderStoryBodyForCreate`, which produces the *posted* body.
68
+ * Nothing ever re-derives a fingerprint from a posted body; the lookup reads
69
+ * the marker that body carries. The assembled body is therefore stable across
70
+ * a run and its resume, and folding it in defeats no lookup.
71
+ *
72
+ * Excluding it cost two silent failures, both closed by this:
73
+ *
74
+ * 1. A later, unrelated plan that reused a slug **and** title adopted the
75
+ * stale open Story — never rewriting its body or Spec, and landing this
76
+ * run's checkpoints, ready-flip, and supersede comments on the wrong
77
+ * issue.
78
+ * 2. A legitimate resume after the operator edited `stories.json` adopted
79
+ * the pre-edit Story and kept its stale body, discarding the edit.
80
+ *
81
+ * With the body in scope both cases simply miss the lookup, and a correct new
82
+ * Story is created; only a genuinely identical Story is ever adopted.
83
+ *
84
+ * The fields are joined on NUL separators, written as the `\u0000` escape and
85
+ * never as a raw byte — a literal NUL would make git classify this file as
86
+ * binary and silently drop its diffs. NUL cannot occur in a slug, a title, or
87
+ * a serialized body, so the join is unambiguous: `{slug:'a-b', title:'c'}` and
88
+ * `{slug:'a', title:'b-c'}` cannot collide the way a hyphen or space separator
89
+ * would let them.
90
+ *
91
+ * @param {{ slug: string, title: string, body?: string }} story
92
+ * @returns {string} Hex digest.
93
+ */
94
+ export function planStoryFingerprint({ slug, title, body = '' }) {
95
+ return createHash('sha256')
96
+ .update(`${slug}\u0000${title}\u0000${body}`)
97
+ .digest('hex')
98
+ .slice(0, PLAN_FINGERPRINT_LENGTH);
99
+ }
100
+
101
+ /**
102
+ * Render the HTML-comment marker carrying a Story's plan fingerprint. It is
103
+ * invisible in GitHub's rendered issue body and survives edits to every
104
+ * other section.
105
+ *
106
+ * @param {string} fingerprint
107
+ * @returns {string}
108
+ */
109
+ function planFingerprintMarker(fingerprint) {
110
+ return `<!-- ${PLAN_FINGERPRINT_MARKER_PREFIX} ${fingerprint} -->`;
111
+ }
112
+
113
+ /**
114
+ * Labels the authoring pass is never allowed to set. The `agent::*` axis is
115
+ * the runtime's lifecycle state (persist owns the terminal `agent::ready`
116
+ * flip itself), `type::*` is fixed to `type::story` by the v2 hierarchy, and
117
+ * `persona::*` is a retired axis.
118
+ */
119
+ const FORBIDDEN_LABEL_PREFIXES = Object.freeze([
120
+ 'agent::',
121
+ 'type::',
122
+ 'persona::',
123
+ ]);
124
+
125
+ /** GitHub's own label-name ceiling. */
126
+ const MAX_LABEL_LENGTH = 50;
127
+
128
+ /**
129
+ * Sanitize the author-supplied `labels[]` on a plan Story (Story #4541).
130
+ *
131
+ * The schema descriptor and the authoring prompt both ask for `labels[]`,
132
+ * but persist never read the field — it hard-coded its own list, so every
133
+ * authored label was silently discarded. Rather than keep asking for input
134
+ * that goes nowhere, apply it: drop the axes the runtime owns, drop
135
+ * malformed entries, dedupe, and always guarantee `type::story`.
136
+ *
137
+ * @param {unknown} rawLabels
138
+ * @param {string} slug For the dropped-label warning.
139
+ * @returns {string[]} Sanitized labels, always including `type::story`.
140
+ */
141
+ export function sanitizeAuthoredLabels(rawLabels, slug) {
142
+ const kept = new Set([TYPE_LABELS.STORY]);
143
+ const dropped = [];
144
+ for (const raw of Array.isArray(rawLabels) ? rawLabels : []) {
145
+ const label = typeof raw === 'string' ? raw.trim() : '';
146
+ if (label === '' || label.length > MAX_LABEL_LENGTH) {
147
+ dropped.push(String(raw));
148
+ continue;
149
+ }
150
+ if (label === TYPE_LABELS.STORY) continue;
151
+ if (FORBIDDEN_LABEL_PREFIXES.some((p) => label.startsWith(p))) {
152
+ dropped.push(label);
153
+ continue;
154
+ }
155
+ kept.add(label);
156
+ }
157
+ if (dropped.length > 0) {
158
+ Logger.warn(
159
+ `[plan-persist] Story "${slug}": dropped ${dropped.length} authored ` +
160
+ `label(s) the runtime owns or cannot apply: ${dropped.join(', ')}.`,
161
+ );
162
+ }
163
+ return [...kept];
164
+ }
165
+
166
+ function bodyObjectFromTicket(ticket) {
167
+ if (typeof ticket.body === 'string') {
168
+ return parseStoryBody(ticket.body).body;
169
+ }
170
+ if (ticket.body && typeof ticket.body === 'object') {
171
+ return parseStoryBody(ticket.body).body;
172
+ }
173
+
174
+ // Allow top-level structured fields (goal/changes/…) without a `body` key.
175
+ return parseStoryBody({
176
+ goal: ticket.goal ?? '',
177
+ slicing: ticket.slicing ?? '',
178
+ spec: ticket.spec ?? '',
179
+ changes: ticket.changes ?? [],
180
+ acceptance: ticket.acceptance ?? [],
181
+ verify: ticket.verify ?? [],
182
+ references: ticket.references ?? [],
183
+ non_goals: ticket.non_goals ?? [],
184
+ wide: ticket.wide ?? null,
185
+ reason_to_exist: ticket.reason_to_exist ?? null,
186
+ depends_on: ticket.depends_on ?? [],
187
+ estimated_test_files: ticket.estimated_test_files ?? null,
188
+ }).body;
189
+ }
190
+
191
+ function normalizeDependsOn(ticket, bodyObject) {
192
+ if (Array.isArray(ticket.depends_on)) {
193
+ return ticket.depends_on.filter((d) => typeof d === 'string');
194
+ }
195
+ return Array.isArray(bodyObject.depends_on) ? bodyObject.depends_on : [];
196
+ }
197
+
198
+ function arraysEqual(a, b) {
199
+ return a.length === b.length && a.every((value, index) => value === b[index]);
200
+ }
201
+
202
+ /**
203
+ * Top-level `acceptance[]` / `verify[]` are the machine contract (validator
204
+ * SSOT). Sync them into the body so the persisted GitHub issue is complete.
205
+ * When the body already lists the same items, keep them; when the body is
206
+ * empty, fill from top-level; when both disagree, fail closed.
207
+ *
208
+ * @param {object} ticket
209
+ * @param {object} bodyObject
210
+ * @param {'acceptance'|'verify'} field
211
+ */
212
+ function syncContractFieldFromTopLevel(ticket, bodyObject, field) {
213
+ if (!Array.isArray(ticket[field])) return;
214
+ const topLevel = ticket[field].map(String);
215
+ const bodyValue = Array.isArray(bodyObject[field])
216
+ ? bodyObject[field].map(String)
217
+ : [];
218
+ if (bodyValue.length > 0 && !arraysEqual(topLevel, bodyValue)) {
219
+ throw new Error(
220
+ `[plan-persist] Story "${ticket.slug ?? ticket.title ?? 'unknown'}" has mismatched top-level and body ${field} arrays`,
221
+ );
222
+ }
223
+ bodyObject[field] = topLevel;
224
+ }
225
+
226
+ /**
227
+ * Normalize a plan Story ticket into `{ slug, title, bodyObject }`.
228
+ * Accepts either a serialized markdown `body` string or a structured body.
229
+ *
230
+ * `supersedes[]` is a top-level-only field (Story #4535) — it is planning
231
+ * bookkeeping for the `--tickets` source issues, not part of the Story's
232
+ * executable body, so it is deliberately not serialized into the markdown.
233
+ *
234
+ * @param {object} ticket
235
+ * @returns {{ slug: string, title: string, bodyObject: object, depends_on: string[], labels: string[], supersedes: Array<{ id: number, note: string|null }> }}
236
+ */
237
+ export function normalizeStoryTicket(ticket) {
238
+ if (!ticket || typeof ticket !== 'object') {
239
+ throw new Error('[plan-persist] each story ticket must be an object');
240
+ }
241
+ const slug =
242
+ typeof ticket.slug === 'string' && ticket.slug.trim() !== ''
243
+ ? ticket.slug.trim()
244
+ : null;
245
+ if (!slug) {
246
+ throw new Error(
247
+ '[plan-persist] each story ticket requires a non-empty slug',
248
+ );
249
+ }
250
+ const title =
251
+ typeof ticket.title === 'string' && ticket.title.trim() !== ''
252
+ ? ticket.title.trim()
253
+ : `Story ${slug}`;
254
+ const bodyObject = bodyObjectFromTicket(ticket);
255
+ syncContractFieldFromTopLevel(ticket, bodyObject, 'acceptance');
256
+ syncContractFieldFromTopLevel(ticket, bodyObject, 'verify');
257
+ const depends_on = normalizeDependsOn(ticket, bodyObject);
258
+ const supersedes = normalizeSupersedes(ticket, slug);
259
+ const labels = sanitizeAuthoredLabels(ticket.labels, slug);
260
+
261
+ return { slug, title, bodyObject, depends_on, labels, supersedes };
262
+ }
263
+
264
+ /**
265
+ * Fold optional shared Tech Spec prose into a Story body when the Story has
266
+ * no inline Spec. Specs stay inline; over-budget Specs throw.
267
+ *
268
+ * Precedence: per-Story `body.spec` wins; otherwise `sharedSpec` is used
269
+ * (N===1 convenience only — callers must not share one Spec across N>1).
270
+ *
271
+ * @param {object} bodyObject
272
+ * @param {string} slug
273
+ * @param {object} [opts]
274
+ * @param {string|null} [opts.sharedSpec]
275
+ * @returns {{ bodyObject: object }}
276
+ */
277
+ export function foldSpecIntoStoryBody(bodyObject, slug, opts = {}) {
278
+ const { sharedSpec = null } = opts;
279
+
280
+ const next = {
281
+ ...bodyObject,
282
+ references: Array.isArray(bodyObject.references)
283
+ ? [...bodyObject.references]
284
+ : [],
285
+ };
286
+
287
+ const inline =
288
+ typeof next.spec === 'string' && next.spec.trim() !== ''
289
+ ? next.spec.trim()
290
+ : typeof sharedSpec === 'string' && sharedSpec.trim() !== ''
291
+ ? sharedSpec.trim()
292
+ : '';
293
+
294
+ if (inline === '') {
295
+ return { bodyObject: next };
296
+ }
297
+
298
+ const { content } = assertSpecWithinBudget({ storyId: slug, spec: inline });
299
+ next.spec = content;
300
+ return { bodyObject: next };
301
+ }
302
+
303
+ function assembleOnePlanStory(ticket, opts) {
304
+ const { slug, title, bodyObject, depends_on, labels, supersedes } =
305
+ normalizeStoryTicket(ticket);
306
+ const { bodyObject: folded } = foldSpecIntoStoryBody(bodyObject, slug, {
307
+ sharedSpec: opts.sharedSpec ?? null,
308
+ });
309
+ // Body first: the fingerprint is an identity over the *assembled* content,
310
+ // so it cannot be computed until that content exists.
311
+ const body = serializeStoryBody({ ...folded, depends_on });
312
+ const fingerprint = planStoryFingerprint({ slug, title, body });
313
+ return {
314
+ story: {
315
+ slug,
316
+ title,
317
+ body,
318
+ bodyObject: { ...folded, depends_on },
319
+ acceptance: Array.isArray(folded.acceptance) ? folded.acceptance : [],
320
+ depends_on,
321
+ labels,
322
+ fingerprint,
323
+ supersedes,
324
+ },
325
+ };
326
+ }
327
+
328
+ /**
329
+ * Shared techspec.md is an N===1 convenience only — folding one Spec into
330
+ * every sibling duplicates approach prose and breaks Story-as-SSOT.
331
+ *
332
+ * @param {object[]} tickets
333
+ * @param {string|null|undefined} sharedSpec
334
+ */
335
+ function assertSharedSpecAllowed(tickets, sharedSpec) {
336
+ if (tickets.length <= 1) return;
337
+ if (typeof sharedSpec !== 'string' || sharedSpec.trim() === '') return;
338
+ throw new Error(
339
+ '[plan-persist] a shared techspec.md cannot be folded into N>1 Stories — ' +
340
+ "put each Story's approach in its own ## Spec so every Story stays a " +
341
+ 'complete executable document.',
342
+ );
343
+ }
344
+
345
+ /**
346
+ * Assemble markdown bodies for every Story: normalize → fold spec →
347
+ * assertAcceptancePartition → assertSupersedePartition → serialize.
348
+ *
349
+ * Both partition checks run **before** any GitHub write so a mis-authored
350
+ * plan never leaves Stories live against an inconsistent tracker.
351
+ *
352
+ * @param {object[]} tickets
353
+ * @param {object} [opts]
354
+ * @param {string|null} [opts.sharedSpec]
355
+ * @param {string[]} [opts.planAcceptance]
356
+ * @param {number[]} [opts.sourceTicketIds] Ids passed to `/plan --tickets`.
357
+ * @returns {{ stories: Array<{ slug: string, title: string, body: string, acceptance: string[], depends_on: string[], supersedes: Array<{ id: number, note: string|null }> }> }}
358
+ */
359
+ export function assemblePlanStories(tickets, opts = {}) {
360
+ if (!Array.isArray(tickets) || tickets.length === 0) {
361
+ throw new Error(
362
+ '[plan-persist] stories payload must be a non-empty array — author at least one Story (default-single).',
363
+ );
364
+ }
365
+
366
+ assertSharedSpecAllowed(tickets, opts.sharedSpec);
367
+
368
+ const stories = tickets.map(
369
+ (ticket) => assembleOnePlanStory(ticket, opts).story,
370
+ );
371
+
372
+ assertAcceptancePartition(stories, {
373
+ planAcceptance: opts.planAcceptance,
374
+ });
375
+ assertSupersedePartition(stories, opts.sourceTicketIds ?? []);
376
+
377
+ return { stories };
378
+ }
379
+
380
+ function orderStoriesByDependencies(stories) {
381
+ const list = Array.isArray(stories) ? stories : [];
382
+ const known = new Set(list.map((story) => story.slug));
383
+ for (const story of list) {
384
+ const unknown = story.depends_on.filter((slug) => !known.has(slug));
385
+ if (unknown.length > 0) {
386
+ throw new Error(
387
+ `[plan-persist] Story "${story.slug}" depends on unknown sibling(s): ${unknown.join(', ')}`,
388
+ );
389
+ }
390
+ }
391
+ const ordered = [];
392
+ const scheduled = new Set();
393
+ const pending = [...list];
394
+ while (pending.length > 0) {
395
+ const index = pending.findIndex((story) =>
396
+ story.depends_on.every((slug) => scheduled.has(slug)),
397
+ );
398
+ if (index === -1) {
399
+ throw new Error(
400
+ `[plan-persist] dependency cycle prevents Story creation: ${pending.map((story) => story.slug).join(', ')}`,
401
+ );
402
+ }
403
+ const [story] = pending.splice(index, 1);
404
+ ordered.push(story);
405
+ scheduled.add(story.slug);
406
+ }
407
+ return ordered;
408
+ }
409
+
410
+ /**
411
+ * Index the open `type::story` backlog so a re-run can recognise Stories a
412
+ * previous, partially-failed persist already created.
413
+ *
414
+ * Two indexes come back. `byFingerprint` is the adoption key — an exact match
415
+ * on the authored content (see {@link planStoryFingerprint}). `idsByTitle` is
416
+ * only used to *warn*: it catches the near miss where a Story with this title
417
+ * is already open but its content differs, which is what an abandoned earlier
418
+ * plan or an edited `stories.json` leaves behind. Adoption deliberately does
419
+ * not key on it — a title is not an identity, and adopting on one would let a
420
+ * later unrelated plan overwrite someone else's Story.
421
+ *
422
+ * Best-effort by construction: a provider with no `listIssuesByLabel` (or a
423
+ * listing that errors) yields empty indexes and the create loop proceeds
424
+ * un-deduplicated, exactly as it did before. That degrades resume, not
425
+ * correctness of a first run — so it warns rather than throws.
426
+ *
427
+ * @param {object} provider
428
+ * @returns {Promise<{
429
+ * byFingerprint: Map<string, { id: number, title: string, url?: string }>,
430
+ * idsByTitle: Map<string, number[]>,
431
+ * }>}
432
+ */
433
+ async function indexExistingStories(provider) {
434
+ const byFingerprint = new Map();
435
+ const idsByTitle = new Map();
436
+ if (typeof provider?.listIssuesByLabel !== 'function') {
437
+ Logger.warn(
438
+ '[plan-persist] provider does not expose listIssuesByLabel — cannot ' +
439
+ 'check for Stories a previous persist already created. A re-run after ' +
440
+ 'a mid-creation failure may duplicate them.',
441
+ );
442
+ return { byFingerprint, idsByTitle };
443
+ }
444
+ let issues;
445
+ try {
446
+ issues = await provider.listIssuesByLabel({
447
+ state: 'open',
448
+ labels: TYPE_LABELS.STORY,
449
+ });
450
+ } catch (err) {
451
+ Logger.warn(
452
+ `[plan-persist] open-Story lookup failed (${err.message}) — proceeding ` +
453
+ 'without resume; a re-run may duplicate Stories.',
454
+ );
455
+ return { byFingerprint, idsByTitle };
456
+ }
457
+ for (const issue of Array.isArray(issues) ? issues : []) {
458
+ const id = Number(issue?.number ?? issue?.id);
459
+ if (!Number.isInteger(id)) continue;
460
+ const title = issue?.title ?? '';
461
+ if (title !== '') {
462
+ idsByTitle.set(title, [...(idsByTitle.get(title) ?? []), id]);
463
+ }
464
+ const body = typeof issue?.body === 'string' ? issue.body : '';
465
+ const match = body.match(
466
+ new RegExp(
467
+ `<!--\\s*${PLAN_FINGERPRINT_MARKER_PREFIX}\\s*([0-9a-f]+)\\s*-->`,
468
+ ),
469
+ );
470
+ if (!match) continue;
471
+ byFingerprint.set(match[1], {
472
+ id,
473
+ title,
474
+ url: issue.html_url ?? issue.url ?? undefined,
475
+ });
476
+ }
477
+ return { byFingerprint, idsByTitle };
478
+ }
479
+
480
+ /**
481
+ * Warn when a Story with this title is already open but did **not** match the
482
+ * fingerprint — i.e. its authored content differs from what this run is about
483
+ * to create.
484
+ *
485
+ * This is the visible half of the fingerprint tightening. Keying adoption on
486
+ * content means these cases correctly get a fresh Story rather than a silent
487
+ * stale-body adoption, but the divergent Story stays open, and a duplicate the
488
+ * operator never hears about is its own small trap. Naming it converts silent
489
+ * litter into a decision.
490
+ *
491
+ * @param {{ slug: string, title: string }} story
492
+ * @param {Map<string, number[]>} idsByTitle
493
+ */
494
+ function warnOnDivergentSameTitleStory(story, idsByTitle) {
495
+ const ids = idsByTitle.get(story.title) ?? [];
496
+ if (ids.length === 0) return;
497
+ Logger.warn(
498
+ `[plan-persist] Story "${story.slug}": ${ids.length} open Story(ies) ` +
499
+ `already carry this exact title (${ids.map((id) => `#${id}`).join(', ')}) ` +
500
+ 'but none match the authored content, so a new Story is being created ' +
501
+ 'rather than silently adopting a stale body. If that is an abandoned ' +
502
+ 'plan or a superseded draft, close it.',
503
+ );
504
+ }
505
+
506
+ /**
507
+ * Render the body actually posted for a Story: the assembled markdown with
508
+ * sibling `depends_on` slugs resolved to real issue ids, plus the invisible
509
+ * plan-fingerprint marker that makes the create loop resumable.
510
+ *
511
+ * @param {object} story
512
+ * @param {Map<string, number>} idBySlug
513
+ * @returns {string}
514
+ */
515
+ function renderStoryBodyForCreate(story, idBySlug) {
516
+ const dependencyRefs = story.depends_on.map(
517
+ (slug) => `#${idBySlug.get(slug)}`,
518
+ );
519
+ const base =
520
+ dependencyRefs.length === 0
521
+ ? story.body
522
+ : serializeStoryBody(
523
+ { ...story.bodyObject, depends_on: dependencyRefs },
524
+ { includeFooter: true },
525
+ );
526
+ return `${base}\n\n${planFingerprintMarker(story.fingerprint)}`;
527
+ }
528
+
529
+ /**
530
+ * Mirror the plan's sibling `depends_on` edges into native GitHub `blocked_by`
531
+ * dependency edges (Story #4544).
532
+ *
533
+ * Ordering is authored as slugs and, until now, survived persist only as
534
+ * `blocked by #N` prose in the body footer. That footer stays — it is what
535
+ * `/deliver`'s resolver falls back on — but a native edge is the durable,
536
+ * machine-readable form: visible in the GitHub UI, readable without parsing
537
+ * markdown, and settable by an operator later for cross-run order.
538
+ *
539
+ * **Non-fatal by design, and deliberately asymmetric with the read path.** A
540
+ * missing native edge is cosmetic here: `renderStoryBodyForCreate` has already
541
+ * written the footer, so ordering is not lost when the dependencies API says
542
+ * no. `/deliver`'s *read* of these edges is a real dispatch gate, which is why
543
+ * that side fails loud. Persist reports the failure and completes.
544
+ *
545
+ * Two shape hazards this crossing has to get right, both silent if missed:
546
+ * `applyBlockedByDependencies` indexes `slugToIssueNumber` with property
547
+ * access, so the `Map` the create loop builds must be flattened to a plain
548
+ * object — a `Map` would yield `undefined` for every lookup, skip every edge,
549
+ * and (being non-fatal) report success having written nothing. And it reads
550
+ * `dependsOn`, not the `depends_on` the assembled Story carries.
551
+ *
552
+ * @param {object} args
553
+ * @param {object} args.provider
554
+ * @param {Array<{ slug: string, depends_on: string[] }>} args.stories
555
+ * @param {Map<string, number>} args.idBySlug
556
+ * @returns {Promise<{ edgesAdded: number, edgesSkipped: number, edgesFailed: number, storiesProcessed: number }|null>}
557
+ * `null` when there was nothing to mirror or no interface to mirror through.
558
+ */
559
+ async function mirrorNativeDependencyEdges({ provider, stories, idBySlug }) {
560
+ const withEdges = stories.filter((story) => story.depends_on.length > 0);
561
+ if (withEdges.length === 0) return null;
562
+
563
+ if (
564
+ typeof provider?.getDependencyWriteContext !== 'function' ||
565
+ typeof provider?.getTicket !== 'function'
566
+ ) {
567
+ Logger.warn(
568
+ '[plan-persist] provider exposes no getDependencyWriteContext/getTicket — ' +
569
+ 'skipping native blocked_by edges. Ordering survives in the ' +
570
+ '`blocked by #N` body footers.',
571
+ );
572
+ return null;
573
+ }
574
+
575
+ try {
576
+ const { gh, owner, repo } = provider.getDependencyWriteContext();
577
+ const summary = await applyBlockedByDependencies({
578
+ stories: stories.map((story) => ({
579
+ slug: story.slug,
580
+ dependsOn: story.depends_on,
581
+ })),
582
+ slugToIssueNumber: Object.fromEntries(idBySlug),
583
+ getTicket: (issueNumber) => provider.getTicket(issueNumber),
584
+ owner,
585
+ repo,
586
+ gh,
587
+ });
588
+ if (summary.edgesFailed > 0) {
589
+ Logger.warn(
590
+ `[plan-persist] ${summary.edgesFailed} native blocked_by edge(s) could ` +
591
+ 'not be written. Ordering survives in the `blocked by #N` body ' +
592
+ 'footers; add the edges by hand if you want them in the GitHub UI.',
593
+ );
594
+ } else {
595
+ Logger.info(
596
+ `[plan-persist] native blocked_by edges: ${summary.edgesAdded} added, ` +
597
+ `${summary.edgesSkipped} already present.`,
598
+ );
599
+ }
600
+ return summary;
601
+ } catch (err) {
602
+ Logger.warn(
603
+ `[plan-persist] native blocked_by mirroring failed (${err.message}) — ` +
604
+ 'ordering survives in the `blocked by #N` body footers.',
605
+ );
606
+ return null;
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Create Story issues via `provider.createIssue`, resumably.
612
+ *
613
+ * **Stories are born without `agent::ready`** (Story #4541). They used to
614
+ * carry it in the creating POST while the `story-plan-state` checkpoint was
615
+ * upserted afterwards, so anything that picked a Story up inside that window —
616
+ * or after a comment failure aborted the loop — read the checkpoint as `null`.
617
+ * Creation now applies `type::story` plus the sanitized authored labels only;
618
+ * `markStoriesReady` performs the flip as the terminal step, once every
619
+ * checkpoint is on the ticket.
620
+ *
621
+ * **The loop is resumable, and adoption is content-keyed.** Each body carries
622
+ * a plan-fingerprint marker, and the open `type::story` backlog is indexed by
623
+ * it before the first POST. A Story whose fingerprint already exists is
624
+ * adopted rather than re-created, so a re-run after a 502 at story *k* of *N*
625
+ * completes the cohort instead of minting a second copy of `1..k-1`.
626
+ *
627
+ * The fingerprint covers the assembled body, not just slug + title, so a hit
628
+ * means the open Story *is* what this run would author and adoption changes
629
+ * nothing. A same-named Story whose content has drifted — an abandoned plan, or
630
+ * an edited `stories.json` — misses the lookup, gets a correct new Story, and
631
+ * is named in a warning. Adoption never rewrites a body, so keying it on
632
+ * anything weaker than content would silently ship a stale one.
633
+ *
634
+ * Story #4540 retired the `plan-run::<id>` label this used to apply when
635
+ * N>1. Batch identity was the wrong axis to encode: it could not express an
636
+ * edge to a Story planned in a different run, and ordering already lives in
637
+ * the `blocked by #N` footers written below — which `/deliver`'s resolver
638
+ * reads directly, alongside native GitHub edges, from live state.
639
+ *
640
+ * **Sibling order is mirrored into native GitHub `blocked_by` edges** once
641
+ * every id is known (Story #4544), so plan-created order stops depending on
642
+ * prose. That pass is non-fatal — see `mirrorNativeDependencyEdges`.
643
+ *
644
+ * @param {object} args
645
+ * @param {object} args.provider
646
+ * @param {ReturnType<typeof assemblePlanStories>['stories']} args.stories
647
+ * @param {object} [args.opts]
648
+ * @param {boolean} [args.opts.dryRun=false]
649
+ * @returns {Promise<{
650
+ * created: Array<{ slug: string, id: number, url?: string, title: string, adopted: boolean }>,
651
+ * dependencyEdges: { edgesAdded: number, edgesSkipped: number, edgesFailed: number, storiesProcessed: number }|null,
652
+ * }>}
653
+ */
654
+ export async function createStoryIssues({ provider, stories, opts = {} }) {
655
+ if (typeof provider?.createIssue !== 'function') {
656
+ throw new Error(
657
+ '[plan-persist] provider does not expose createIssue; cannot persist Stories.',
658
+ );
659
+ }
660
+
661
+ const list = Array.isArray(stories) ? stories : [];
662
+
663
+ if (opts.dryRun) {
664
+ return {
665
+ created: list.map((s, i) => ({
666
+ slug: s.slug,
667
+ id: -(i + 1),
668
+ title: s.title,
669
+ url: undefined,
670
+ adopted: false,
671
+ })),
672
+ dependencyEdges: null,
673
+ };
674
+ }
675
+
676
+ const { byFingerprint, idsByTitle } = await indexExistingStories(provider);
677
+ const created = [];
678
+ const idBySlug = new Map();
679
+
680
+ for (const story of orderStoriesByDependencies(list)) {
681
+ const already = byFingerprint.get(story.fingerprint);
682
+ if (!already) warnOnDivergentSameTitleStory(story, idsByTitle);
683
+ if (already) {
684
+ Logger.info(
685
+ `[plan-persist] resuming: Story "${story.slug}" already exists as ` +
686
+ `#${already.id} with byte-identical authored content ` +
687
+ `(plan fingerprint ${story.fingerprint}) — skipping create.`,
688
+ );
689
+ created.push({
690
+ slug: story.slug,
691
+ id: already.id,
692
+ title: story.title,
693
+ url: already.url,
694
+ adopted: true,
695
+ });
696
+ idBySlug.set(story.slug, already.id);
697
+ continue;
698
+ }
699
+
700
+ const result = await provider.createIssue({
701
+ title: story.title,
702
+ body: renderStoryBodyForCreate(story, idBySlug),
703
+ labels: [...story.labels],
704
+ });
705
+ const id = result?.id ?? result?.number;
706
+ if (!Number.isInteger(id)) {
707
+ throw new Error(
708
+ `[plan-persist] createIssue for slug "${story.slug}" did not return a numeric id`,
709
+ );
710
+ }
711
+ created.push({
712
+ slug: story.slug,
713
+ id,
714
+ title: story.title,
715
+ url: result.url,
716
+ adopted: false,
717
+ });
718
+ idBySlug.set(story.slug, id);
719
+ }
720
+
721
+ // Every id is known now — including the adopted ones a resumed run reused —
722
+ // so a re-run mirrors the whole cohort's edges, not just the Stories this
723
+ // invocation happened to POST. Re-application is idempotent.
724
+ const dependencyEdges = await mirrorNativeDependencyEdges({
725
+ provider,
726
+ stories: list,
727
+ idBySlug,
728
+ });
729
+
730
+ return { created, dependencyEdges };
731
+ }
732
+
733
+ /**
734
+ * Flip every created Story to `agent::ready` — the terminal step of persist
735
+ * (Story #4541).
736
+ *
737
+ * This is what makes `agent::ready` *mean* "fully persisted": by the time it
738
+ * lands, the Story's `story-plan-state` checkpoint is already on the ticket, so
739
+ * a `/deliver` that picks it up cannot read a null checkpoint.
740
+ *
741
+ * Fails closed: an un-flipped Story is invisible to `/deliver`, which is the
742
+ * safe direction — the operator is told exactly which ids need the label.
743
+ *
744
+ * @param {object} args
745
+ * @param {object} args.provider
746
+ * @param {Array<{ id: number, slug: string }>} args.created
747
+ * @returns {Promise<{ readied: number[] }>}
748
+ */
749
+ export async function markStoriesReady({ provider, created }) {
750
+ if (typeof provider?.updateTicket !== 'function') {
751
+ throw new Error(
752
+ '[plan-persist] provider does not expose updateTicket; cannot flip ' +
753
+ 'Stories to agent::ready.',
754
+ );
755
+ }
756
+ const readied = [];
757
+ const failed = [];
758
+ for (const story of created) {
759
+ try {
760
+ await provider.updateTicket(story.id, {
761
+ labels: { add: [AGENT_LABELS.READY] },
762
+ });
763
+ readied.push(story.id);
764
+ } catch (err) {
765
+ failed.push(`#${story.id} (${story.slug}): ${err.message}`);
766
+ }
767
+ }
768
+ if (failed.length > 0) {
769
+ throw new Error(
770
+ `[plan-persist] ${failed.length} Story(ies) were created with their ` +
771
+ 'checkpoints but could not be flipped to agent::ready:\n' +
772
+ `${failed.map((f) => ` - ${f}`).join('\n')}\n` +
773
+ 'They are invisible to /deliver until the label lands. Re-run persist ' +
774
+ '(it resumes rather than duplicating) or add the label by hand.',
775
+ );
776
+ }
777
+ return { readied };
778
+ }