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
@@ -15,8 +15,9 @@
15
15
  * @module lib/orchestration/plan-persist/story-ops
16
16
  */
17
17
 
18
- import { randomBytes } from 'node:crypto';
19
-
18
+ import { createHash } from 'node:crypto';
19
+ import { applyBlockedByDependencies } from '../../../providers/github/blocked-by-add.js';
20
+ import { Logger } from '../../Logger.js';
20
21
  import { AGENT_LABELS, TYPE_LABELS } from '../../label-constants.js';
21
22
  import {
22
23
  parse as parseStoryBody,
@@ -24,42 +25,142 @@ import {
24
25
  } from '../../story-body/story-body.js';
25
26
  import { assertSpecWithinBudget } from '../spec-spill.js';
26
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:';
27
46
 
28
- /** Label prefix grouping sibling Stories from one plan run (N>1). */
29
- export const PLAN_RUN_LABEL_PREFIX = 'plan-run::';
47
+ /** Length of the hex fingerprint digest. Collision-free at plan scale. */
48
+ const PLAN_FINGERPRINT_LENGTH = 16;
30
49
 
31
50
  /**
32
- * Normalize a caller-supplied plan-run token. Persistence and resolution
33
- * share this helper so human-readable ids map to one canonical label.
51
+ * Compute the deterministic identity of one authored Story within a plan.
34
52
  *
35
- * @param {string} id
36
- * @returns {string}
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.
37
93
  */
38
- export function normalizePlanRunId(id) {
39
- const token = String(id ?? '')
40
- .trim()
41
- .toLowerCase()
42
- .replace(/^plan-run::/, '')
43
- .replace(/[^a-z0-9._-]+/g, '-');
44
- if (!token) {
45
- throw new Error('plan-run id requires a non-empty planRunId');
46
- }
47
- return token;
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);
48
99
  }
49
100
 
50
101
  /**
51
- * Build a `plan-run::<id>` label. When `id` is omitted, generates a short
52
- * random hex token (8 chars) suitable for a rare multi-Story plan.
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.
53
105
  *
54
- * @param {string} [id]
106
+ * @param {string} fingerprint
55
107
  * @returns {string}
56
108
  */
57
- export function planRunLabel(id) {
58
- const token =
59
- typeof id === 'string' && id.trim() !== ''
60
- ? normalizePlanRunId(id)
61
- : randomBytes(4).toString('hex');
62
- return `${PLAN_RUN_LABEL_PREFIX}${token}`;
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];
63
164
  }
64
165
 
65
166
  function bodyObjectFromTicket(ticket) {
@@ -126,8 +227,12 @@ function syncContractFieldFromTopLevel(ticket, bodyObject, field) {
126
227
  * Normalize a plan Story ticket into `{ slug, title, bodyObject }`.
127
228
  * Accepts either a serialized markdown `body` string or a structured body.
128
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
+ *
129
234
  * @param {object} ticket
130
- * @returns {{ slug: string, title: string, bodyObject: object, depends_on: string[] }}
235
+ * @returns {{ slug: string, title: string, bodyObject: object, depends_on: string[], labels: string[], supersedes: Array<{ id: number, note: string|null }> }}
131
236
  */
132
237
  export function normalizeStoryTicket(ticket) {
133
238
  if (!ticket || typeof ticket !== 'object') {
@@ -150,8 +255,10 @@ export function normalizeStoryTicket(ticket) {
150
255
  syncContractFieldFromTopLevel(ticket, bodyObject, 'acceptance');
151
256
  syncContractFieldFromTopLevel(ticket, bodyObject, 'verify');
152
257
  const depends_on = normalizeDependsOn(ticket, bodyObject);
258
+ const supersedes = normalizeSupersedes(ticket, slug);
259
+ const labels = sanitizeAuthoredLabels(ticket.labels, slug);
153
260
 
154
- return { slug, title, bodyObject, depends_on };
261
+ return { slug, title, bodyObject, depends_on, labels, supersedes };
155
262
  }
156
263
 
157
264
  /**
@@ -194,11 +301,15 @@ export function foldSpecIntoStoryBody(bodyObject, slug, opts = {}) {
194
301
  }
195
302
 
196
303
  function assembleOnePlanStory(ticket, opts) {
197
- const { slug, title, bodyObject, depends_on } = normalizeStoryTicket(ticket);
304
+ const { slug, title, bodyObject, depends_on, labels, supersedes } =
305
+ normalizeStoryTicket(ticket);
198
306
  const { bodyObject: folded } = foldSpecIntoStoryBody(bodyObject, slug, {
199
307
  sharedSpec: opts.sharedSpec ?? null,
200
308
  });
309
+ // Body first: the fingerprint is an identity over the *assembled* content,
310
+ // so it cannot be computed until that content exists.
201
311
  const body = serializeStoryBody({ ...folded, depends_on });
312
+ const fingerprint = planStoryFingerprint({ slug, title, body });
202
313
  return {
203
314
  story: {
204
315
  slug,
@@ -207,6 +318,9 @@ function assembleOnePlanStory(ticket, opts) {
207
318
  bodyObject: { ...folded, depends_on },
208
319
  acceptance: Array.isArray(folded.acceptance) ? folded.acceptance : [],
209
320
  depends_on,
321
+ labels,
322
+ fingerprint,
323
+ supersedes,
210
324
  },
211
325
  };
212
326
  }
@@ -230,13 +344,17 @@ function assertSharedSpecAllowed(tickets, sharedSpec) {
230
344
 
231
345
  /**
232
346
  * Assemble markdown bodies for every Story: normalize → fold spec →
233
- * assertAcceptancePartition → serialize.
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.
234
351
  *
235
352
  * @param {object[]} tickets
236
353
  * @param {object} [opts]
237
354
  * @param {string|null} [opts.sharedSpec]
238
355
  * @param {string[]} [opts.planAcceptance]
239
- * @returns {{ stories: Array<{ slug: string, title: string, body: string, acceptance: string[], depends_on: string[] }> }}
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 }> }> }}
240
358
  */
241
359
  export function assemblePlanStories(tickets, opts = {}) {
242
360
  if (!Array.isArray(tickets) || tickets.length === 0) {
@@ -254,6 +372,7 @@ export function assemblePlanStories(tickets, opts = {}) {
254
372
  assertAcceptancePartition(stories, {
255
373
  planAcceptance: opts.planAcceptance,
256
374
  });
375
+ assertSupersedePartition(stories, opts.sourceTicketIds ?? []);
257
376
 
258
377
  return { stories };
259
378
  }
@@ -289,16 +408,248 @@ function orderStoriesByDependencies(stories) {
289
408
  }
290
409
 
291
410
  /**
292
- * Create Story issues via `provider.createIssue`. Applies `type::story`,
293
- * `agent::ready`, and when N>1 — a shared plan-run label.
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`.
294
643
  *
295
644
  * @param {object} args
296
645
  * @param {object} args.provider
297
646
  * @param {ReturnType<typeof assemblePlanStories>['stories']} args.stories
298
647
  * @param {object} [args.opts]
299
- * @param {string} [args.opts.planRunId]
300
648
  * @param {boolean} [args.opts.dryRun=false]
301
- * @returns {Promise<{ created: Array<{ slug: string, id: number, url?: string, title: string }>, planRunLabel: string|null }>}
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
+ * }>}
302
653
  */
303
654
  export async function createStoryIssues({ provider, stories, opts = {} }) {
304
655
  if (typeof provider?.createIssue !== 'function') {
@@ -308,9 +659,6 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
308
659
  }
309
660
 
310
661
  const list = Array.isArray(stories) ? stories : [];
311
- const planLabel = list.length > 1 ? planRunLabel(opts.planRunId) : null;
312
- const labels = [TYPE_LABELS.STORY, AGENT_LABELS.READY];
313
- if (planLabel) labels.push(planLabel);
314
662
 
315
663
  if (opts.dryRun) {
316
664
  return {
@@ -319,28 +667,40 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
319
667
  id: -(i + 1),
320
668
  title: s.title,
321
669
  url: undefined,
670
+ adopted: false,
322
671
  })),
323
- planRunLabel: planLabel,
672
+ dependencyEdges: null,
324
673
  };
325
674
  }
326
675
 
676
+ const { byFingerprint, idsByTitle } = await indexExistingStories(provider);
327
677
  const created = [];
328
- const createdBySlug = new Map();
678
+ const idBySlug = new Map();
679
+
329
680
  for (const story of orderStoriesByDependencies(list)) {
330
- const dependencyRefs = story.depends_on.map(
331
- (slug) => `#${createdBySlug.get(slug)}`,
332
- );
333
- const body =
334
- dependencyRefs.length === 0
335
- ? story.body
336
- : serializeStoryBody(
337
- { ...story.bodyObject, depends_on: dependencyRefs },
338
- { includeFooter: true },
339
- );
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
+
340
700
  const result = await provider.createIssue({
341
701
  title: story.title,
342
- body,
343
- labels: [...labels],
702
+ body: renderStoryBodyForCreate(story, idBySlug),
703
+ labels: [...story.labels],
344
704
  });
345
705
  const id = result?.id ?? result?.number;
346
706
  if (!Number.isInteger(id)) {
@@ -353,9 +713,66 @@ export async function createStoryIssues({ provider, stories, opts = {} }) {
353
713
  id,
354
714
  title: story.title,
355
715
  url: result.url,
716
+ adopted: false,
356
717
  });
357
- createdBySlug.set(story.slug, id);
718
+ idBySlug.set(story.slug, id);
358
719
  }
359
720
 
360
- return { created, planRunLabel: planLabel };
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 };
361
778
  }