mandrel 1.94.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (560) hide show
  1. package/.agents/README.md +116 -99
  2. package/.agents/agents/acceptance-critic.md +9 -7
  3. package/.agents/agents/story-worker.md +45 -51
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +498 -1287
  6. package/.agents/docs/agentrc-reference.json +185 -80
  7. package/.agents/docs/configuration.md +132 -190
  8. package/.agents/docs/execution-reference.md +51 -25
  9. package/.agents/docs/quality-gates.md +25 -33
  10. package/.agents/docs/workflows.md +8 -8
  11. package/.agents/instructions.md +113 -139
  12. package/.agents/rules/ci-remediation.md +11 -15
  13. package/.agents/rules/git-conventions-reference.md +48 -58
  14. package/.agents/rules/git-conventions.md +16 -22
  15. package/.agents/schemas/acceptance-eval-verdict.schema.json +1 -1
  16. package/.agents/schemas/agentrc.schema.json +83 -254
  17. package/.agents/schemas/audit-rules.json +59 -1
  18. package/.agents/schemas/audit-rules.schema.json +33 -1
  19. package/.agents/schemas/lifecycle/README.md +1 -2
  20. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  21. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  22. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  23. package/.agents/schemas/lifecycle/retro.end.schema.json +1 -1
  24. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  25. package/.agents/schemas/signal-event.schema.json +3 -3
  26. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  27. package/.agents/schemas/validation-evidence.schema.json +1 -1
  28. package/.agents/scripts/acceptance-eval.js +22 -66
  29. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  30. package/.agents/scripts/audit-to-stories.js +7 -7
  31. package/.agents/scripts/boot-sweep.js +1 -1
  32. package/.agents/scripts/bootstrap.js +3 -3
  33. package/.agents/scripts/check-dead-exports.js +43 -104
  34. package/.agents/scripts/check-doc-links.js +2 -2
  35. package/.agents/scripts/check-lifecycle-lint.js +7 -10
  36. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  37. package/.agents/scripts/cleanup-repo-test-temp.js +6 -1
  38. package/.agents/scripts/deliver-recover.js +122 -0
  39. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  40. package/.agents/scripts/evidence-gate.js +20 -50
  41. package/.agents/scripts/generate-skills-index.js +17 -1
  42. package/.agents/scripts/generate-workflows-doc.js +4 -4
  43. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  44. package/.agents/scripts/lib/Logger.js +6 -10
  45. package/.agents/scripts/lib/audit-suite/runner.js +2 -2
  46. package/.agents/scripts/lib/audit-suite/selector.js +328 -28
  47. package/.agents/scripts/lib/audit-to-stories/{seed-epic-from-findings.js → seed-from-findings.js} +9 -9
  48. package/.agents/scripts/lib/baselines/kernel.js +206 -18
  49. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -15
  50. package/.agents/scripts/lib/baselines/reader.js +1 -6
  51. package/.agents/scripts/lib/bdd-runner-detect.js +5 -9
  52. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  53. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +32 -33
  54. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  55. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  56. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  57. package/.agents/scripts/lib/checks/core-bare-clean.js +6 -3
  58. package/.agents/scripts/lib/checks/index.js +3 -2
  59. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  60. package/.agents/scripts/lib/checks/state.js +17 -248
  61. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +26 -24
  62. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  63. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  64. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  65. package/.agents/scripts/lib/cli/standard-args.js +13 -22
  66. package/.agents/scripts/lib/cli-args.js +39 -9
  67. package/.agents/scripts/lib/close-validation/gates.js +15 -15
  68. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  69. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  70. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  71. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  72. package/.agents/scripts/lib/config/ci.js +6 -31
  73. package/.agents/scripts/lib/config/delivery-routing.js +52 -35
  74. package/.agents/scripts/lib/config/explain.js +61 -48
  75. package/.agents/scripts/lib/config/github.js +7 -5
  76. package/.agents/scripts/lib/config/limits.js +29 -80
  77. package/.agents/scripts/lib/config/paths.js +0 -2
  78. package/.agents/scripts/lib/config/quality.js +12 -15
  79. package/.agents/scripts/lib/config/runners.js +20 -66
  80. package/.agents/scripts/lib/config/temp-paths.js +30 -63
  81. package/.agents/scripts/lib/config/worktree-isolation.js +0 -5
  82. package/.agents/scripts/lib/config-resolver.js +2 -7
  83. package/.agents/scripts/lib/config-settings-schema-delivery.js +55 -161
  84. package/.agents/scripts/lib/config-settings-schema-quality.js +17 -16
  85. package/.agents/scripts/lib/config-settings-schema.js +100 -60
  86. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  87. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  88. package/.agents/scripts/lib/dependency-parser.js +3 -2
  89. package/.agents/scripts/lib/doc-tiers.js +2 -2
  90. package/.agents/scripts/lib/duplicate-search.js +242 -41
  91. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
  92. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  93. package/.agents/scripts/lib/format-generated-json.js +97 -0
  94. package/.agents/scripts/lib/framework-version.js +19 -190
  95. package/.agents/scripts/lib/gh-exec.js +8 -0
  96. package/.agents/scripts/lib/git-branch-cleanup.js +1 -10
  97. package/.agents/scripts/lib/git-branch-lifecycle.js +17 -180
  98. package/.agents/scripts/lib/git-utils.js +32 -20
  99. package/.agents/scripts/lib/github/framework-repo.js +6 -0
  100. package/.agents/scripts/lib/json-utils.js +1 -2
  101. package/.agents/scripts/lib/label-constants.js +10 -38
  102. package/.agents/scripts/lib/label-taxonomy.js +10 -55
  103. package/.agents/scripts/lib/observability/active-story-env.js +44 -165
  104. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  105. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  106. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  107. package/.agents/scripts/lib/observability/source-classifier.js +5 -5
  108. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  109. package/.agents/scripts/lib/onboard/init-tail.js +1 -3
  110. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  111. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  112. package/.agents/scripts/lib/orchestration/ceremony-routing.js +105 -44
  113. package/.agents/scripts/lib/orchestration/code-review.js +78 -436
  114. package/.agents/scripts/lib/orchestration/column-sync.js +1 -1
  115. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +4 -4
  116. package/.agents/scripts/lib/orchestration/context-envelope.js +2 -5
  117. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  118. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  119. package/.agents/scripts/lib/orchestration/docs-digest.js +8 -8
  120. package/.agents/scripts/lib/orchestration/file-assumptions.js +7 -13
  121. package/.agents/scripts/lib/orchestration/git-cleanup/phases/cli.js +1 -1
  122. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  123. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  124. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  125. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +17 -19
  126. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  127. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  128. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +6 -3
  129. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +21 -43
  130. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  131. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  132. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +4 -4
  133. package/.agents/scripts/lib/orchestration/merge-block-class.js +77 -21
  134. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  135. package/.agents/scripts/lib/orchestration/phase-runner.js +3 -2
  136. package/.agents/scripts/lib/orchestration/plan-context.js +354 -282
  137. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +11 -22
  138. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +4 -8
  139. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  140. package/.agents/scripts/lib/orchestration/plan-navigation.js +92 -0
  141. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +71 -0
  142. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +245 -0
  143. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  144. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +432 -858
  145. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +778 -0
  146. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +47 -115
  147. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  148. package/.agents/scripts/lib/orchestration/plan-reachability.js +9 -14
  149. package/.agents/scripts/lib/orchestration/plan-runner/worktree-sweep.js +1 -1
  150. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/authoring-context.js +52 -51
  151. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +27 -0
  152. package/.agents/scripts/lib/orchestration/{epic-plan-spec/phases → planning}/spec-authoring-grounding.js +1 -1
  153. package/.agents/scripts/lib/orchestration/pr-base-guard.js +18 -28
  154. package/.agents/scripts/lib/orchestration/remote-verifier.js +1 -1
  155. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  156. package/.agents/scripts/lib/orchestration/resolves-token.js +1 -1
  157. package/.agents/scripts/lib/orchestration/retro-proposals.js +140 -79
  158. package/.agents/scripts/lib/orchestration/review-depth.js +126 -47
  159. package/.agents/scripts/lib/orchestration/review-providers/codex.js +2 -2
  160. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  161. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  162. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  163. package/.agents/scripts/lib/orchestration/review-providers/review-provider-factory.js +21 -56
  164. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  165. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  166. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  167. package/.agents/scripts/lib/orchestration/run-epilogue.js +784 -0
  168. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +25 -1
  169. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  170. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  171. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +86 -41
  172. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  173. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +77 -32
  174. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  175. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  176. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +44 -0
  177. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  178. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  179. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +275 -46
  180. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  181. package/.agents/scripts/lib/orchestration/spec-freshness.js +14 -205
  182. package/.agents/scripts/lib/orchestration/spec-section-validator.js +4 -5
  183. package/.agents/scripts/lib/orchestration/spec-spill.js +60 -0
  184. package/.agents/scripts/lib/orchestration/split-policy-validator.js +188 -0
  185. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +49 -0
  186. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  187. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +28 -42
  188. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  189. package/.agents/scripts/lib/orchestration/story-follow-ups.js +298 -0
  190. package/.agents/scripts/lib/orchestration/story-init-remote.js +51 -0
  191. package/.agents/scripts/lib/orchestration/story-plan-state.js +33 -0
  192. package/.agents/scripts/lib/orchestration/structured-comment-parser.js +1 -1
  193. package/.agents/scripts/lib/orchestration/task-body-validator.js +60 -25
  194. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  195. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +130 -75
  196. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +192 -377
  197. package/.agents/scripts/lib/orchestration/ticket-validator.js +123 -25
  198. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -72
  199. package/.agents/scripts/lib/orchestration/ticketing/reads.js +45 -55
  200. package/.agents/scripts/lib/orchestration/ticketing/transition.js +66 -6
  201. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  202. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  203. package/.agents/scripts/lib/planning-corpus.js +12 -281
  204. package/.agents/scripts/lib/preflight-runner.js +4 -4
  205. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -5
  206. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  207. package/.agents/scripts/lib/signals/index.js +4 -17
  208. package/.agents/scripts/lib/signals/read.js +35 -35
  209. package/.agents/scripts/lib/signals/schema.js +8 -11
  210. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  211. package/.agents/scripts/lib/signals/write.js +0 -1
  212. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  213. package/.agents/scripts/lib/single-story-sweep/protection-ctx.js +1 -1
  214. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  215. package/.agents/scripts/lib/story-adjacency.js +17 -19
  216. package/.agents/scripts/lib/story-body/story-body.js +130 -75
  217. package/.agents/scripts/lib/story-plan.js +2 -4
  218. package/.agents/scripts/lib/templates/decomposer-prompts.js +51 -46
  219. package/.agents/scripts/lib/templates/spec-author-prompts.js +47 -45
  220. package/.agents/scripts/lib/test-env.js +14 -1
  221. package/.agents/scripts/lib/test-tiers.js +0 -3
  222. package/.agents/scripts/lib/{epic-body-sections.js → ticket-body-sections.js} +25 -39
  223. package/.agents/scripts/lib/validation-evidence.js +32 -60
  224. package/.agents/scripts/lib/wave-runner/ready-set.js +38 -12
  225. package/.agents/scripts/lib/workspace-provisioner.js +1 -1
  226. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  227. package/.agents/scripts/lib/worktree/lifecycle/reap.js +72 -25
  228. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  229. package/.agents/scripts/lint-issue-body.js +71 -40
  230. package/.agents/scripts/mandrel-update-preflight.js +1 -1
  231. package/.agents/scripts/notify.js +4 -3
  232. package/.agents/scripts/plan-context.js +102 -80
  233. package/.agents/scripts/plan-persist.js +230 -279
  234. package/.agents/scripts/plan-run-epilogue.js +142 -0
  235. package/.agents/scripts/post-structured-comment.js +0 -38
  236. package/.agents/scripts/pr-watch-with-update.js +43 -22
  237. package/.agents/scripts/providers/github/compose.js +0 -1
  238. package/.agents/scripts/providers/github/errors.js +0 -19
  239. package/.agents/scripts/providers/github/issues.js +13 -39
  240. package/.agents/scripts/providers/github/mappers.js +5 -12
  241. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  242. package/.agents/scripts/providers/github/tickets.js +33 -156
  243. package/.agents/scripts/providers/github.js +17 -6
  244. package/.agents/scripts/resolve-stories.js +236 -0
  245. package/.agents/scripts/run-coverage.js +4 -1
  246. package/.agents/scripts/run-lint.js +2 -2
  247. package/.agents/scripts/run-verify.js +31 -2
  248. package/.agents/scripts/signals-view.js +25 -21
  249. package/.agents/scripts/single-story-close.js +178 -26
  250. package/.agents/scripts/single-story-confirm-merge.js +313 -24
  251. package/.agents/scripts/single-story-init.js +35 -30
  252. package/.agents/scripts/stories-wave-tick.js +85 -10
  253. package/.agents/scripts/story-plan.js +28 -49
  254. package/.agents/scripts/update-ticket-state.js +14 -65
  255. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  256. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  257. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  258. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  259. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  260. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  261. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  262. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  263. package/.agents/skills/core/idea-refinement/SKILL.md +9 -9
  264. package/.agents/skills/core/scope-triage/SKILL.md +31 -172
  265. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  266. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  267. package/.agents/skills/skills.index.json +3 -53
  268. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  269. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  270. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  271. package/.agents/starter-agentrc.json +0 -5
  272. package/.agents/templates/agent-protocol.md +9 -10
  273. package/.agents/workflows/audit-architecture.md +6 -7
  274. package/.agents/workflows/audit-clean-code.md +7 -7
  275. package/.agents/workflows/audit-dependencies.md +3 -3
  276. package/.agents/workflows/audit-devops.md +3 -3
  277. package/.agents/workflows/audit-documentation.md +9 -10
  278. package/.agents/workflows/audit-lighthouse.md +11 -3
  279. package/.agents/workflows/audit-navigability.md +13 -2
  280. package/.agents/workflows/audit-performance.md +5 -6
  281. package/.agents/workflows/audit-privacy.md +3 -3
  282. package/.agents/workflows/audit-quality.md +11 -12
  283. package/.agents/workflows/audit-security.md +4 -5
  284. package/.agents/workflows/audit-seo.md +13 -3
  285. package/.agents/workflows/audit-sre.md +3 -3
  286. package/.agents/workflows/audit-to-stories.md +20 -20
  287. package/.agents/workflows/audit-ux-ui.md +10 -3
  288. package/.agents/workflows/deliver.md +177 -176
  289. package/.agents/workflows/git-cleanup.md +5 -6
  290. package/.agents/workflows/git-deliver.md +1 -1
  291. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  292. package/.agents/workflows/helpers/acceptance-self-eval.md +35 -40
  293. package/.agents/workflows/helpers/code-quality-guardrails.md +7 -7
  294. package/.agents/workflows/helpers/code-review.md +75 -196
  295. package/.agents/workflows/helpers/{single-story-deliver-reference.md → deliver-story-reference.md} +83 -44
  296. package/.agents/workflows/helpers/deliver-story.md +606 -0
  297. package/.agents/workflows/helpers/diagnose.md +10 -10
  298. package/.agents/workflows/helpers/parallel-tooling.md +3 -3
  299. package/.agents/workflows/helpers/signals.md +16 -16
  300. package/.agents/workflows/helpers/worktree-lifecycle.md +66 -86
  301. package/.agents/workflows/mandrel-update.md +2 -1
  302. package/.agents/workflows/plan.md +277 -145
  303. package/.agents/workflows/qa-assist.md +27 -33
  304. package/.agents/workflows/qa-explore.md +29 -38
  305. package/.agents/workflows/qa-run.md +2 -6
  306. package/README.md +9 -8
  307. package/bin/mandrel.js +12 -1
  308. package/docs/CHANGELOG.md +70 -0
  309. package/lib/cli/registry.js +262 -19
  310. package/lib/cli/sync-agents.js +157 -0
  311. package/lib/cli/sync-commands.js +115 -6
  312. package/lib/cli/sync.js +168 -6
  313. package/lib/cli/update.js +105 -8
  314. package/lib/cli/version-helpers.js +131 -0
  315. package/lib/migrations/README.md +7 -5
  316. package/lib/migrations/index.js +12 -8
  317. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  318. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  319. package/package.json +2 -2
  320. package/.agents/agents/retro.md +0 -42
  321. package/.agents/personas/architect.md +0 -113
  322. package/.agents/personas/devops-engineer.md +0 -38
  323. package/.agents/personas/engineer.md +0 -33
  324. package/.agents/personas/project-manager.md +0 -114
  325. package/.agents/personas/qa-engineer.md +0 -95
  326. package/.agents/personas/security-engineer.md +0 -111
  327. package/.agents/personas/technical-writer.md +0 -101
  328. package/.agents/schemas/dispatch-manifest.json +0 -232
  329. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  330. package/.agents/schemas/epic-spec.schema.json +0 -153
  331. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  332. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  333. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  334. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  335. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  336. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  337. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  338. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  339. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  340. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  341. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  342. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  343. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  344. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  345. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  346. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  347. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  348. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  349. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  350. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  351. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  352. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  353. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  354. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  355. package/.agents/schemas/lifecycle/slice.end.schema.json +0 -21
  356. package/.agents/schemas/lifecycle/slice.heartbeat.schema.json +0 -20
  357. package/.agents/schemas/lifecycle/slice.start.schema.json +0 -17
  358. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  359. package/.agents/schemas/risk-verdict.schema.json +0 -66
  360. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  361. package/.agents/scripts/acceptance-spec-reconciler.js +0 -642
  362. package/.agents/scripts/analyze-execution.js +0 -444
  363. package/.agents/scripts/bookkeeping-reconcile.js +0 -117
  364. package/.agents/scripts/check-prepush-recovery.js +0 -90
  365. package/.agents/scripts/dispatcher.js +0 -295
  366. package/.agents/scripts/epic-audit-prepare.js +0 -497
  367. package/.agents/scripts/epic-audit-recheck.js +0 -274
  368. package/.agents/scripts/epic-deliver-note-intervention.js +0 -192
  369. package/.agents/scripts/epic-deliver-preflight.js +0 -462
  370. package/.agents/scripts/epic-deliver-prepare.js +0 -852
  371. package/.agents/scripts/epic-execute-record-wave.js +0 -449
  372. package/.agents/scripts/epic-plan-clarity.js +0 -211
  373. package/.agents/scripts/epic-plan-healthcheck.js +0 -581
  374. package/.agents/scripts/epic-reconcile.js +0 -625
  375. package/.agents/scripts/lib/baseline-snapshot.js +0 -979
  376. package/.agents/scripts/lib/checks/epic-merge-lock-stale.js +0 -54
  377. package/.agents/scripts/lib/checks/stale-origin-epic.js +0 -49
  378. package/.agents/scripts/lib/config/lifecycle.js +0 -40
  379. package/.agents/scripts/lib/config/preflight.js +0 -58
  380. package/.agents/scripts/lib/config/retro.js +0 -77
  381. package/.agents/scripts/lib/epic-merge-lock.js +0 -322
  382. package/.agents/scripts/lib/epic-plan-clarity.js +0 -181
  383. package/.agents/scripts/lib/epic-plan-ideation.js +0 -261
  384. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  385. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  386. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -219
  387. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  388. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  389. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  390. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -270
  391. package/.agents/scripts/lib/orchestration/context-hydration-engine.js +0 -539
  392. package/.agents/scripts/lib/orchestration/deliver-route.js +0 -173
  393. package/.agents/scripts/lib/orchestration/dispatch-engine.js +0 -134
  394. package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +0 -183
  395. package/.agents/scripts/lib/orchestration/epic-cleanup.js +0 -801
  396. package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +0 -310
  397. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/context.js +0 -163
  398. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +0 -140
  399. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +0 -64
  400. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +0 -72
  401. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist-helpers.js +0 -156
  402. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/persist.js +0 -345
  403. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/planning-artifacts.js +0 -41
  404. package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/reconcile-spawn.js +0 -86
  405. package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +0 -391
  406. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/drain.js +0 -94
  407. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +0 -236
  408. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/risk-verdict.js +0 -105
  409. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +0 -307
  410. package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/spec-freshness.js +0 -117
  411. package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +0 -117
  412. package/.agents/scripts/lib/orchestration/epic-run-state-store.js +0 -621
  413. package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +0 -186
  414. package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +0 -50
  415. package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +0 -129
  416. package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +0 -103
  417. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +0 -267
  418. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +0 -210
  419. package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +0 -238
  420. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/_bullet-format.js +0 -32
  421. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/component-drift.js +0 -203
  422. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/crap-drift.js +0 -227
  423. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/maintainability-drift.js +0 -117
  424. package/.agents/scripts/lib/orchestration/epic-runner/progress-signals/stalled-worktree.js +0 -37
  425. package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +0 -127
  426. package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +0 -400
  427. package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +0 -276
  428. package/.agents/scripts/lib/orchestration/epic-runner/wave-scheduler.js +0 -66
  429. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +0 -789
  430. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +0 -676
  431. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-discriminator.js +0 -389
  432. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-format.js +0 -230
  433. package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +0 -361
  434. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  435. package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +0 -306
  436. package/.agents/scripts/lib/orchestration/finalize/post-handoff-comment.js +0 -489
  437. package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +0 -88
  438. package/.agents/scripts/lib/orchestration/lifecycle/emit-slice-lifecycle.js +0 -270
  439. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +0 -147
  440. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  441. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  442. package/.agents/scripts/lib/orchestration/lifecycle/listeners/acceptance-reconciler.js +0 -465
  443. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +0 -501
  444. package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +0 -984
  445. package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +0 -264
  446. package/.agents/scripts/lib/orchestration/lifecycle/listeners/checkpoint-pointer-writer.js +0 -284
  447. package/.agents/scripts/lib/orchestration/lifecycle/listeners/cleaner.js +0 -355
  448. package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +0 -673
  449. package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +0 -378
  450. package/.agents/scripts/lib/orchestration/lifecycle/listeners/intervention-recorder.js +0 -140
  451. package/.agents/scripts/lib/orchestration/lifecycle/listeners/label-transitioner.js +0 -144
  452. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  453. package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +0 -174
  454. package/.agents/scripts/lib/orchestration/manifest-builder.js +0 -222
  455. package/.agents/scripts/lib/orchestration/plan-persist/amend.js +0 -359
  456. package/.agents/scripts/lib/orchestration/plan-persist/delivery-mode.js +0 -127
  457. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  458. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  459. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  460. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  461. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -33
  462. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  463. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  464. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  465. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -396
  466. package/.agents/scripts/lib/orchestration/post-merge-pipeline.js +0 -205
  467. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  468. package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +0 -152
  469. package/.agents/scripts/lib/orchestration/retro/phases/checks.js +0 -94
  470. package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +0 -571
  471. package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +0 -450
  472. package/.agents/scripts/lib/orchestration/retro/phases/post-and-mirror.js +0 -191
  473. package/.agents/scripts/lib/orchestration/retro-heuristics.js +0 -57
  474. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  475. package/.agents/scripts/lib/orchestration/retro-runner.js +0 -197
  476. package/.agents/scripts/lib/orchestration/spec-renderer.js +0 -447
  477. package/.agents/scripts/lib/orchestration/story-close/auto-refresh-runner.js +0 -747
  478. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/gate-failure.js +0 -211
  479. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/pre-merge-attribution.js +0 -158
  480. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/refresh-commit.js +0 -446
  481. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/regression-projection.js +0 -297
  482. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution/phases/scope-discovery.js +0 -48
  483. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution-wiring.js +0 -67
  484. package/.agents/scripts/lib/orchestration/story-close/baseline-attribution.js +0 -161
  485. package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +0 -117
  486. package/.agents/scripts/lib/orchestration/story-close/cd-out-guard.js +0 -86
  487. package/.agents/scripts/lib/orchestration/story-close/cleanup-reconciler.js +0 -147
  488. package/.agents/scripts/lib/orchestration/story-close/close-inputs.js +0 -142
  489. package/.agents/scripts/lib/orchestration/story-close/comment-bodies.js +0 -62
  490. package/.agents/scripts/lib/orchestration/story-close/merge-runner.js +0 -658
  491. package/.agents/scripts/lib/orchestration/story-close/merge-subject.js +0 -198
  492. package/.agents/scripts/lib/orchestration/story-close/phases/branch-restore.js +0 -105
  493. package/.agents/scripts/lib/orchestration/story-close/phases/close.js +0 -222
  494. package/.agents/scripts/lib/orchestration/story-close/phases/gates.js +0 -292
  495. package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +0 -270
  496. package/.agents/scripts/lib/orchestration/story-close/phases/preflight.js +0 -110
  497. package/.agents/scripts/lib/orchestration/story-close/phases/refresh.js +0 -86
  498. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked-emitter.js +0 -112
  499. package/.agents/scripts/lib/orchestration/story-close/phases/timeout-blocked.js +0 -157
  500. package/.agents/scripts/lib/orchestration/story-close/post-merge-close.js +0 -421
  501. package/.agents/scripts/lib/orchestration/story-close/pre-merge-validation.js +0 -302
  502. package/.agents/scripts/lib/orchestration/story-close/shared-checkout-guard.js +0 -163
  503. package/.agents/scripts/lib/orchestration/story-close-recovery.js +0 -690
  504. package/.agents/scripts/lib/orchestration/wave-marker.js +0 -28
  505. package/.agents/scripts/lib/orchestration/wave-record-io.js +0 -218
  506. package/.agents/scripts/lib/orchestration/wave-record-notifications.js +0 -145
  507. package/.agents/scripts/lib/orchestration/wave-record-projection.js +0 -212
  508. package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +0 -111
  509. package/.agents/scripts/lib/presentation/manifest-builder.js +0 -239
  510. package/.agents/scripts/lib/presentation/manifest-formatter.js +0 -242
  511. package/.agents/scripts/lib/presentation/manifest-helpers.js +0 -213
  512. package/.agents/scripts/lib/presentation/manifest-persistence.js +0 -261
  513. package/.agents/scripts/lib/presentation/manifest-procedures.js +0 -55
  514. package/.agents/scripts/lib/presentation/manifest-render-waves.js +0 -306
  515. package/.agents/scripts/lib/presentation/manifest-renderer.js +0 -188
  516. package/.agents/scripts/lib/presentation/manifest-story-views.js +0 -110
  517. package/.agents/scripts/lib/push-epic-retry.js +0 -209
  518. package/.agents/scripts/lib/spec/index.js +0 -36
  519. package/.agents/scripts/lib/spec/loader.js +0 -425
  520. package/.agents/scripts/lib/spec/state.js +0 -208
  521. package/.agents/scripts/lib/story-init/blocker-validator.js +0 -68
  522. package/.agents/scripts/lib/story-init/branch-initializer.js +0 -408
  523. package/.agents/scripts/lib/story-init/context-resolver.js +0 -92
  524. package/.agents/scripts/lib/story-init/donor-precheck.js +0 -207
  525. package/.agents/scripts/lib/story-init/state-transitioner.js +0 -80
  526. package/.agents/scripts/lib/story-init/task-graph-builder.js +0 -124
  527. package/.agents/scripts/lib/story-init/transition-summary.js +0 -34
  528. package/.agents/scripts/lib/test-reserved-epic-temp-ids.js +0 -35
  529. package/.agents/scripts/lib/wave-runner/tick.js +0 -754
  530. package/.agents/scripts/lib/wave-runner/wave-runner-error.js +0 -20
  531. package/.agents/scripts/lifecycle-emit-story-dispatch.js +0 -194
  532. package/.agents/scripts/lifecycle-emit.js +0 -510
  533. package/.agents/scripts/retro-run.js +0 -218
  534. package/.agents/scripts/slice-phase.js +0 -361
  535. package/.agents/scripts/standalone-feedback-rollup.js +0 -188
  536. package/.agents/scripts/story-close.js +0 -294
  537. package/.agents/scripts/story-init.js +0 -599
  538. package/.agents/scripts/story-phase.js +0 -369
  539. package/.agents/scripts/wave-tick.js +0 -464
  540. package/.agents/skills/core/analyze-execution/SKILL.md +0 -98
  541. package/.agents/skills/core/epic-plan-consolidate/SKILL.md +0 -172
  542. package/.agents/skills/core/epic-plan-consolidate/examples.md +0 -51
  543. package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +0 -441
  544. package/.agents/skills/core/epic-plan-decompose-author/examples.md +0 -47
  545. package/.agents/skills/core/epic-plan-premortem/SKILL.md +0 -146
  546. package/.agents/skills/core/epic-plan-premortem/examples.md +0 -53
  547. package/.agents/skills/core/epic-plan-spec-author/SKILL.md +0 -383
  548. package/.agents/skills/core/epic-plan-spec-author/examples.md +0 -91
  549. package/.agents/workflows/helpers/deliver-epic-reference.md +0 -547
  550. package/.agents/workflows/helpers/deliver-epic-single.md +0 -331
  551. package/.agents/workflows/helpers/deliver-epic.md +0 -998
  552. package/.agents/workflows/helpers/deliver-stories.md +0 -450
  553. package/.agents/workflows/helpers/epic-audit.md +0 -189
  554. package/.agents/workflows/helpers/epic-deliver-story.md +0 -436
  555. package/.agents/workflows/helpers/epic-testing.md +0 -125
  556. package/.agents/workflows/helpers/plan-epic-reference.md +0 -160
  557. package/.agents/workflows/helpers/plan-epic.md +0 -353
  558. package/.agents/workflows/helpers/plan-story.md +0 -251
  559. package/.agents/workflows/helpers/scope-triage-gate.md +0 -108
  560. package/.agents/workflows/helpers/single-story-deliver.md +0 -557
@@ -1,789 +0,0 @@
1
- /**
2
- * lib/orchestration/epic-spec-reconciler-apply.js — apply engine for the
3
- * epic-spec reconciler (Epic #1182 / Tech Spec #1483 / Story #1494).
4
- *
5
- * Consumes a `Plan` produced by `epic-spec-reconciler-diff.js#diff()` and
6
- * materialises the operations against an `ITicketingProvider` with
7
- * bounded concurrency. The apply engine is the only place in the
8
- * structural reconciler that touches the world — every other module in
9
- * the surface (`epic-spec-reconciler-ops.js`,
10
- * `epic-spec-reconciler-diff.js`,
11
- * `epic-spec-reconciler-discriminator.js`,
12
- * `epic-spec-reconciler-format.js`) is intentionally I/O-free so the
13
- * data path can be unit-tested in isolation.
14
- *
15
- * ## Contract
16
- *
17
- * - `apply(plan, opts)` — executes the plan and returns a typed result
18
- * envelope describing what was created, updated, closed, or
19
- * relinked. The envelope's shape is stable and machine-readable.
20
- * - Concurrency is bounded at 4 via `concurrentMap` from
21
- * `lib/util/concurrent-map.js`. The cap matches the
22
- * `RECONCILE_CONCURRENCY` constant in
23
- * `lib/orchestration/reconciler.js:18` so structural and
24
- * label-hygiene reconciliation operate at the same provider load.
25
- * - `opts.dryRun === true` short-circuits with zero provider calls.
26
- * The returned envelope echoes the plan's intent (`created`,
27
- * `updated`, `closed`, `relinked` arrays populated from the plan
28
- * ops, marked `dryRun: true`).
29
- * - The discriminator gates (`mayClose`, `mayUpdate`) are re-asserted
30
- * before any mutation runs. A plan op that fails the gate aborts
31
- * `apply` synchronously (Promise rejection) **before** any provider
32
- * call is dispatched, so partial failure of a forbidden op is
33
- * impossible.
34
- * - `assertPlanLabelAllowList` is invoked unconditionally at entry to
35
- * re-prove the safety net even on hand-built plans (diff already
36
- * asserts; we double-check at apply because tests and CLI callers
37
- * may bypass the diff path).
38
- *
39
- * ## Result envelope
40
- *
41
- * @typedef {object} ApplyResultEntry
42
- * @property {string} slug
43
- * @property {string} entity
44
- * @property {string} kind 'create'|'update'|'close'|'relink'
45
- * @property {number} [issueNumber] Resulting issue number (post-create)
46
- * or the targeted issue number for
47
- * update/close/relink.
48
- * @property {string} [url] Issue URL when the provider returned one.
49
- *
50
- * @typedef {object} ApplyResult
51
- * @property {boolean} dryRun
52
- * @property {ApplyResultEntry[]} created
53
- * @property {ApplyResultEntry[]} updated
54
- * @property {ApplyResultEntry[]} closed
55
- * @property {ApplyResultEntry[]} relinked
56
- * @property {Record<string, number>} slugToIssue
57
- * Post-apply slug → issue mapping. On full success, this is the
58
- * complete map; on partial failure, it reflects creates that landed
59
- * before the failure.
60
- * @property {object} [state] The state object the apply
61
- * pipeline persisted via the
62
- * `writeState` hook. Omitted when
63
- * `opts.spec` was not supplied.
64
- * @property {string} [statePath] Absolute path the default
65
- * writer returned; omitted when a
66
- * custom `writeState` hook ran or
67
- * when no spec was supplied.
68
- * @property {Error} [failure] Present only when apply exited
69
- * via partial-failure: the
70
- * provider error that aborted
71
- * further dispatch. The state file
72
- * was still written to reflect the
73
- * successful ops so the operator
74
- * can resume cleanly.
75
- *
76
- * @typedef {object} ApplyOptions
77
- * @property {boolean} [dryRun] Skip provider calls; echo plan.
78
- * @property {number} [concurrency] Override the default cap (4).
79
- * Tests use 1 to assert order.
80
- * @property {number} [epicId] Required for create ops — the
81
- * parent Epic issue number that
82
- * feeds into createTicket's
83
- * ticketData.epicId field.
84
- * @property {Record<string, number>} [slugToIssue]
85
- * Pre-seeded slug → issue map.
86
- * Apply populates it as creates
87
- * land so child creates can
88
- * resolve parentSlug → parentId.
89
- * Callers (state writer) may
90
- * read the post-apply state from
91
- * the returned envelope's
92
- * `slugToIssue` field.
93
- * @property {object} [storySnapshots] Optional snapshot map keyed
94
- * by slug carrying live
95
- * execution state for the
96
- * close-discriminator. When
97
- * absent, close ops require
98
- * `explicitDelete: true` per
99
- * the discriminator's default.
100
- * @property {boolean} [explicitDelete] Operator opt-in: passes
101
- * through to `mayClose`.
102
- * @property {object} [spec] Parsed spec used to derive
103
- * the post-apply state-file
104
- * mapping (Task #1518). When
105
- * omitted, apply does not
106
- * touch state.json — useful
107
- * for tests that only want to
108
- * drive the provider surface.
109
- * @property {object} [priorState] Prior state loaded from
110
- * `<epicId>.state.json` (carries
111
- * pre-existing slug → issue
112
- * mappings and observed agent
113
- * states for slugs unchanged
114
- * by this apply).
115
- * @property {string} [stateNow] Override for the
116
- * `lastReconciledAt` timestamp.
117
- * Tests pass a fixed string so
118
- * the rendered state is
119
- * byte-stable.
120
- * @property {(epicId: number, state: object) => string} [writeState]
121
- * Optional state writer
122
- * injection point. Defaults to
123
- * `lib/spec/loader.js#writeState`.
124
- * Tests pass an in-memory
125
- * spy so apply does not touch
126
- * the real on-disk file.
127
- * @property {{epicsDir?: string}} [writeStateOpts]
128
- * Forwarded to the default
129
- * `writeState` adapter so tests
130
- * can redirect the on-disk
131
- * output to a tmp dir.
132
- */
133
-
134
- import { Logger } from '../Logger.js';
135
- import { writeState as defaultWriteState } from '../spec/loader.js';
136
- import { buildState } from '../spec/state.js';
137
- import { concurrentMap } from '../util/concurrent-map.js';
138
- import {
139
- assertPlanLabelAllowList,
140
- mayClose,
141
- mayUpdate,
142
- } from './epic-spec-reconciler-discriminator.js';
143
- import { isPlan, OP_KINDS } from './epic-spec-reconciler-ops.js';
144
-
145
- /**
146
- * Default concurrency cap. Hard-pinned to 4 to match
147
- * `RECONCILE_CONCURRENCY` declared in
148
- * `.agents/scripts/lib/orchestration/reconciler.js:18`. That constant is
149
- * file-local in `reconciler.js`; rather than widen its surface for one
150
- * caller, we duplicate the value with a documented cross-reference. The
151
- * sibling `concurrency-wiring.test.js` suite owns the invariant that
152
- * these two values stay in sync.
153
- */
154
- export const APPLY_CONCURRENCY = 4;
155
-
156
- /**
157
- * Error class thrown when a plan operation fails a discriminator gate.
158
- * Carrying structured metadata (`slug`, `field`, `reason`) lets the CLI
159
- * report which op was rejected without re-parsing the message.
160
- */
161
- export class ApplyGateViolation extends Error {
162
- /**
163
- * @param {string} message
164
- * @param {{slug?: string, kind?: string, field?: string, reason?: string}} [meta]
165
- */
166
- constructor(message, meta = {}) {
167
- super(message);
168
- this.name = 'ApplyGateViolation';
169
- if (meta.slug !== undefined) this.slug = meta.slug;
170
- if (meta.kind !== undefined) this.kind = meta.kind;
171
- if (meta.field !== undefined) this.field = meta.field;
172
- if (meta.reason !== undefined) this.reason = meta.reason;
173
- }
174
- }
175
-
176
- /**
177
- * Pre-flight gate check. Walks every op in the plan and asserts each one
178
- * passes its discriminator. Throws `ApplyGateViolation` on the first
179
- * failure so apply aborts before dispatching any provider call.
180
- *
181
- * Update ops are checked field-by-field: every key in `op.changes` must
182
- * pass `mayUpdate(_, field)`. The diff engine already constrains keys to
183
- * the structural allow-list, but we re-check here so a hand-built plan
184
- * cannot bypass the safety net.
185
- *
186
- * Close ops consult `mayClose(snapshot, { explicitDelete })`. The
187
- * snapshot is looked up from `opts.storySnapshots` keyed by slug;
188
- * absence is fine — `mayClose` defaults to the conservative "require
189
- * explicit delete" path.
190
- *
191
- * @param {import('./epic-spec-reconciler-ops.js').Plan} plan
192
- * @param {ApplyOptions} opts
193
- * @returns {void}
194
- */
195
- function assertGates(plan, opts) {
196
- for (const op of plan.updates) {
197
- for (const field of Object.keys(op.changes ?? {})) {
198
- const result = mayUpdate(undefined, field);
199
- if (!result.allowed) {
200
- throw new ApplyGateViolation(
201
- `update for slug=${op.slug} field=${field} blocked: ${result.reason}`,
202
- { slug: op.slug, kind: 'update', field, reason: result.reason },
203
- );
204
- }
205
- }
206
- }
207
- for (const op of plan.closes) {
208
- const snapshot = opts.storySnapshots?.[op.slug];
209
- const result = mayClose(snapshot, {
210
- explicitDelete: opts.explicitDelete === true,
211
- });
212
- if (!result.allowed) {
213
- throw new ApplyGateViolation(
214
- `close for slug=${op.slug} blocked: ${result.reason}`,
215
- { slug: op.slug, kind: 'close', reason: result.reason },
216
- );
217
- }
218
- }
219
- }
220
-
221
- /**
222
- * Build a dry-run envelope that echoes the plan's intent without making
223
- * provider calls. Useful for CLI `--dry-run` output and for the apply
224
- * pipeline's preview path.
225
- *
226
- * @param {import('./epic-spec-reconciler-ops.js').Plan} plan
227
- * @param {Record<string, number>} slugToIssue
228
- * @returns {ApplyResult}
229
- */
230
- /**
231
- * Seed `slugToIssue.epic` so child features can resolve
232
- * `parentSlug: 'epic'` through `resolveParentId`. Story #1820: the CLI
233
- * seeds `state.mapping.epic` so diff no longer emits a Create for the
234
- * epic; without this matching seed on the apply side, `slugToIssue.epic`
235
- * is never populated and the first feature create throws "parent slug
236
- * epic has no mapped issue number". Priority order:
237
- * 1. Existing `slugToIssue.epic` (caller-supplied) — wins.
238
- * 2. `opts.priorState.mapping.epic.issueNumber`.
239
- * 3. `opts.epicId`.
240
- *
241
- * @param {Record<string, number>} slugToIssue cloned mapping (mutated in place)
242
- * @param {ApplyOptions} opts
243
- * @returns {Record<string, number>} same mapping, returned for chaining
244
- */
245
- function seedEpicSlug(slugToIssue, opts) {
246
- if (typeof slugToIssue.epic === 'number') return slugToIssue;
247
- const priorEpicNumber = opts.priorState?.mapping?.epic?.issueNumber;
248
- const seed =
249
- typeof priorEpicNumber === 'number'
250
- ? priorEpicNumber
251
- : typeof opts.epicId === 'number'
252
- ? opts.epicId
253
- : null;
254
- if (seed !== null) slugToIssue.epic = seed;
255
- return slugToIssue;
256
- }
257
-
258
- function buildDryRunResult(plan, slugToIssue) {
259
- return {
260
- dryRun: true,
261
- created: plan.creates.map((op) => ({
262
- slug: op.slug,
263
- entity: op.entity,
264
- kind: OP_KINDS.CREATE,
265
- })),
266
- updated: plan.updates.map((op) => ({
267
- slug: op.slug,
268
- entity: op.entity,
269
- kind: OP_KINDS.UPDATE,
270
- issueNumber: op.issueNumber,
271
- })),
272
- closed: plan.closes.map((op) => ({
273
- slug: op.slug,
274
- entity: op.entity,
275
- kind: OP_KINDS.CLOSE,
276
- issueNumber: op.issueNumber,
277
- })),
278
- relinked: plan.relinks.map((op) => ({
279
- slug: op.slug,
280
- entity: op.entity,
281
- kind: OP_KINDS.RELINK,
282
- issueNumber: op.issueNumber,
283
- })),
284
- slugToIssue: { ...slugToIssue },
285
- };
286
- }
287
-
288
- /**
289
- * Resolve a parent slug to an issue number using the running map. Throws
290
- * a structured error if the slug is required but unknown — apply must
291
- * not silently drop a parent edge.
292
- *
293
- * @param {string|undefined} slug
294
- * @param {Record<string, number>} slugToIssue
295
- * @returns {number|undefined}
296
- */
297
- function resolveParentId(slug, slugToIssue) {
298
- if (!slug) return undefined;
299
- const id = slugToIssue[slug];
300
- if (typeof id !== 'number') {
301
- throw new ApplyGateViolation(
302
- `apply: parent slug ${slug} has no mapped issue number`,
303
- { slug, kind: 'create', reason: 'unmapped-parent' },
304
- );
305
- }
306
- return id;
307
- }
308
-
309
- /**
310
- * Resolve a dependsOn slug list to its issue numbers, failing loud on any
311
- * unresolved slug. The reconciler hands the resolved numbers to
312
- * `createTicket` as `ticketData.dependencies`; the provider's
313
- * `composeStoryBody` is the single owner that renders the canonical
314
- * `blocked by #N` footer from them (Story #3958). The reconciler must NOT
315
- * also pre-append a footer to the body — doing so doubled every dependency
316
- * line, since `composeStoryBody` appends from `dependencies` independently.
317
- *
318
- * Fail-loud is preserved here (not delegated to the body composer): an
319
- * unresolved slug means `topoSortCreates` failed to order a dependency
320
- * ahead of its dependent, which would otherwise be silently dropped by
321
- * `dependencies`' number filter. Returns `[]` when no deps.
322
- *
323
- * @param {string[]|undefined} dependsOn
324
- * @param {Record<string, number>} slugToIssue
325
- * @returns {number[]}
326
- */
327
- function resolveDependencies(dependsOn, slugToIssue) {
328
- if (!Array.isArray(dependsOn) || dependsOn.length === 0) return [];
329
- const resolved = [];
330
- const unresolved = [];
331
- for (const slug of dependsOn) {
332
- const issueNumber = slugToIssue[slug];
333
- if (typeof issueNumber === 'number') {
334
- resolved.push(issueNumber);
335
- } else {
336
- unresolved.push(slug);
337
- }
338
- }
339
- if (unresolved.length > 0) {
340
- throw new Error(
341
- `[reconciler] resolveDependencies: unresolved dependsOn slugs: ${unresolved.join(', ')}. ` +
342
- `topoSortCreates must order dependencies ahead of dependents.`,
343
- );
344
- }
345
- return resolved;
346
- }
347
-
348
- /**
349
- * Materialise a single create op. The provider returns
350
- * `{ id, url }`; we record the slug → id mapping and reflect it in the
351
- * envelope.
352
- *
353
- * @param {import('./epic-spec-reconciler-ops.js').CreateOp} op
354
- * @param {object} provider
355
- * @param {ApplyOptions} opts
356
- * @param {Record<string, number>} slugToIssue
357
- * @returns {Promise<ApplyResultEntry>}
358
- */
359
- async function applyCreate(op, provider, opts, slugToIssue) {
360
- const parentId = resolveParentId(op.parentSlug, slugToIssue);
361
- const epicId = typeof opts.epicId === 'number' ? opts.epicId : parentId;
362
- // Story #3958 — resolve (and fail-loud validate) the dependency issue
363
- // numbers, but do NOT pre-append a `blocked by #N` footer to the body.
364
- // `composeStoryBody` (the provider's createTicket renderer) is the single
365
- // owner of that footer; appending here too doubled every dependency line.
366
- const dependencies = resolveDependencies(op.dependsOn, slugToIssue);
367
- const ticketData = {
368
- epicId,
369
- title: op.title,
370
- body: op.body ?? '',
371
- labels: op.labels ?? [],
372
- dependencies,
373
- };
374
- // Epic-level create has no parent — the provider's createTicket
375
- // expects a parent for sub-issue linkage. For the epic op we route to
376
- // the same surface but the parentId fallback (epicId) is fine since
377
- // the epic *is* its own anchor; the diff engine never emits this in
378
- // practice (the epic is bootstrapped before reconciliation), but we
379
- // keep the path safe.
380
- const created = await provider.createTicket(parentId ?? epicId, ticketData);
381
- if (created && typeof created.id === 'number') {
382
- slugToIssue[op.slug] = created.id;
383
- }
384
- // Defence — Story #2063. GitHubProvider.createTicket captures a
385
- // failed addSubIssue mutation into `{ subIssueLinked: false,
386
- // subIssueError }` rather than throwing, so a transient GraphQL
387
- // failure (secondary rate-limit, "Something went wrong" envelope,
388
- // etc.) leaves the issue created on GH but with no native sub-issue
389
- // link to its parent. The previous behaviour swallowed this
390
- // entirely — a partial backlog persisted with no operator-visible
391
- // signal. Surface it here at WARN so the breadcrumb is visible; the
392
- // canonical repair runs in `runDecomposePhase` via
393
- // `reconcileSubIssueLinks(epicId)` after every apply pass.
394
- if (created && created.subIssueLinked === false) {
395
- const reason =
396
- created.subIssueError?.message ?? 'no error message captured';
397
- Logger.warn(
398
- `[reconciler.apply] sub-issue link failed for child #${created.id} (parent #${parentId ?? epicId}): ${reason}. The runDecomposePhase safety net will retry.`,
399
- );
400
- }
401
- return {
402
- slug: op.slug,
403
- entity: op.entity,
404
- kind: OP_KINDS.CREATE,
405
- issueNumber: created?.id,
406
- url: created?.url,
407
- subIssueLinked: created?.subIssueLinked,
408
- };
409
- }
410
-
411
- /**
412
- * Materialise a single update op. Translates the plan's `changes` map
413
- * into the provider's `mutations` shape: `title`/`body` map directly,
414
- * `labels` becomes `{ add, remove }` derived from the before/after
415
- * difference, and `wave` is appended to the body marker (the wave
416
- * integer lives in the body, not on a label).
417
- *
418
- * @param {import('./epic-spec-reconciler-ops.js').UpdateOp} op
419
- * @param {object} provider
420
- * @returns {Promise<ApplyResultEntry>}
421
- */
422
- async function applyUpdate(op, provider) {
423
- const mutations = {};
424
- const changes = op.changes ?? {};
425
- if (changes.title) {
426
- mutations.title = changes.title.after;
427
- }
428
- if (changes.body) {
429
- mutations.body = changes.body.after;
430
- }
431
- if (changes.labels) {
432
- const before = new Set(changes.labels.before ?? []);
433
- const after = new Set(changes.labels.after ?? []);
434
- const add = [...after].filter((l) => !before.has(l));
435
- const remove = [...before].filter((l) => !after.has(l));
436
- if (add.length || remove.length) {
437
- mutations.labels = { add, remove };
438
- }
439
- }
440
- await provider.updateTicket(op.issueNumber, mutations);
441
- return {
442
- slug: op.slug,
443
- entity: op.entity,
444
- kind: OP_KINDS.UPDATE,
445
- issueNumber: op.issueNumber,
446
- };
447
- }
448
-
449
- /**
450
- * Materialise a single close op. The provider's `updateTicket`
451
- * mutation surface accepts `{ state: 'closed' }`, matching the mock
452
- * provider in `tests/fixtures/mock-provider.js`.
453
- *
454
- * @param {import('./epic-spec-reconciler-ops.js').CloseOp} op
455
- * @param {object} provider
456
- * @returns {Promise<ApplyResultEntry>}
457
- */
458
- async function applyClose(op, provider) {
459
- await provider.updateTicket(op.issueNumber, { state: 'closed' });
460
- return {
461
- slug: op.slug,
462
- entity: op.entity,
463
- kind: OP_KINDS.CLOSE,
464
- issueNumber: op.issueNumber,
465
- };
466
- }
467
-
468
- /**
469
- * Materialise a single relink op. Parent edge changes are written by
470
- * removing the existing sub-issue link (when present) and adding the
471
- * new one. DependsOn edge changes rewrite the body's `blocked by`
472
- * footer; we surface the new edge list to the provider via an
473
- * `updateTicket` body mutation. The plan carries before/after so the
474
- * caller can render the body locally without re-fetching the ticket
475
- * (`opts.bodyRenderer` injects the renderer; absence = no-op on body).
476
- *
477
- * @param {import('./epic-spec-reconciler-ops.js').RelinkOp} op
478
- * @param {object} provider
479
- * @param {Record<string, number>} slugToIssue
480
- * @returns {Promise<ApplyResultEntry>}
481
- */
482
- async function applyRelink(op, provider, slugToIssue) {
483
- if (op.parent) {
484
- const before = op.parent.before;
485
- const after = op.parent.after;
486
- if (before) {
487
- const beforeId = slugToIssue[before];
488
- if (typeof beforeId === 'number') {
489
- await provider.removeSubIssue(beforeId, op.issueNumber);
490
- }
491
- }
492
- if (after) {
493
- const afterId = slugToIssue[after];
494
- if (typeof afterId === 'number') {
495
- await provider.addSubIssue(afterId, op.issueNumber);
496
- }
497
- }
498
- }
499
- // Story #2982 — dependsOn edge changes no longer write the body here.
500
- // The diff engine recomposes the canonical orchestrator footer
501
- // (`---\nparent:`/`Epic:`/`blocked by`) and routes a body change
502
- // through `applyUpdate`. Writing it from the relink path stripped
503
- // description + parent + Epic on every dep change. The relink op
504
- // remains the authoritative carrier of the dependsOn delta for the
505
- // state writer and for the parent sub-issue add/remove above.
506
- return {
507
- slug: op.slug,
508
- entity: op.entity,
509
- kind: OP_KINDS.RELINK,
510
- issueNumber: op.issueNumber,
511
- };
512
- }
513
-
514
- /**
515
- * Apply a plan against an `ITicketingProvider`.
516
- *
517
- * Execution order: creates → updates → closes → relinks. Creates run
518
- * first so subsequent updates/relinks can target the newly minted
519
- * issue numbers (the running `slugToIssue` map propagates IDs across
520
- * phases). Within each phase, ops are dispatched through `concurrentMap`
521
- * at cap=4.
522
- *
523
- * Errors:
524
- * - `ApplyGateViolation` — pre-flight gate failed; no provider call
525
- * was issued.
526
- * - Provider errors — `concurrentMap`'s first-rejection-wins semantics
527
- * surface the first failure; later rejections are swallowed. The
528
- * `slugToIssue` map reflects whatever creates completed before the
529
- * failure, so the caller (state writer) can persist a partial
530
- * mapping if desired.
531
- *
532
- * @param {import('./epic-spec-reconciler-ops.js').Plan} plan
533
- * @param {object} provider ITicketingProvider instance.
534
- * @param {ApplyOptions} [opts]
535
- * @returns {Promise<ApplyResult>}
536
- */
537
- export async function apply(plan, provider, opts = {}) {
538
- if (!isPlan(plan)) {
539
- throw new TypeError('apply: plan must conform to the Plan shape');
540
- }
541
- if (!provider || typeof provider !== 'object') {
542
- throw new TypeError('apply: provider is required');
543
- }
544
- // Re-prove the label allow-list safety net at the apply boundary.
545
- assertPlanLabelAllowList(plan);
546
-
547
- const concurrency =
548
- typeof opts.concurrency === 'number' && opts.concurrency > 0
549
- ? opts.concurrency
550
- : APPLY_CONCURRENCY;
551
- const slugToIssue = seedEpicSlug({ ...(opts.slugToIssue ?? {}) }, opts);
552
-
553
- if (opts.dryRun === true) {
554
- return buildDryRunResult(plan, slugToIssue);
555
- }
556
-
557
- // Pre-flight gates. Throws synchronously before any provider call.
558
- assertGates(plan, opts);
559
-
560
- const created = [];
561
- const updated = [];
562
- const closed = [];
563
- const relinked = [];
564
- let failure = null;
565
-
566
- // Phase 1: creates. Topo-batched so parent issues materialise before
567
- // their children.
568
- try {
569
- const orderedCreates = topoSortCreates(plan.creates, slugToIssue);
570
- for (const batch of orderedCreates) {
571
- const batchResults = await concurrentMap(
572
- batch,
573
- (op) => applyCreate(op, provider, opts, slugToIssue),
574
- { concurrency },
575
- );
576
- created.push(...batchResults);
577
- }
578
- // Phase 2: updates. Independent — run in parallel.
579
- updated.push(
580
- ...(await concurrentMap(plan.updates, (op) => applyUpdate(op, provider), {
581
- concurrency,
582
- })),
583
- );
584
- // Phase 3: closes. Independent — run in parallel. Note we remove the
585
- // slug from `slugToIssue` so the projected state mapping drops it
586
- // (matches the AC: closes disappear from state).
587
- closed.push(
588
- ...(await concurrentMap(
589
- plan.closes,
590
- async (op) => {
591
- const entry = await applyClose(op, provider);
592
- delete slugToIssue[op.slug];
593
- return entry;
594
- },
595
- { concurrency },
596
- )),
597
- );
598
- // Phase 4: relinks. Independent — run in parallel.
599
- relinked.push(
600
- ...(await concurrentMap(
601
- plan.relinks,
602
- (op) => applyRelink(op, provider, slugToIssue),
603
- { concurrency },
604
- )),
605
- );
606
- } catch (err) {
607
- failure = err;
608
- }
609
-
610
- const result = {
611
- dryRun: false,
612
- created,
613
- updated,
614
- closed,
615
- relinked,
616
- slugToIssue: { ...slugToIssue },
617
- };
618
-
619
- // State writer integration (Task #1518). When the caller provided a
620
- // spec, project the resulting state and persist it. On partial failure
621
- // the state reflects only completed operations — the `slugToIssue` map
622
- // above already encodes that (failed creates never landed; closed
623
- // slugs were removed; failed closes leave the slug present).
624
- if (opts.spec && typeof opts.spec === 'object') {
625
- const writeStateFn = opts.writeState ?? defaultWriteState;
626
- const epicId =
627
- typeof opts.epicId === 'number'
628
- ? opts.epicId
629
- : typeof opts.spec?.epic?.id === 'number'
630
- ? opts.spec.epic.id
631
- : undefined;
632
- const state = projectStateForWrite({
633
- spec: opts.spec,
634
- priorState: opts.priorState,
635
- slugToIssue,
636
- now: opts.stateNow,
637
- });
638
- const writeArgs = [epicId, state];
639
- if (opts.writeStateOpts) writeArgs.push(opts.writeStateOpts);
640
- const writePath = writeStateFn(...writeArgs);
641
- result.state = state;
642
- if (typeof writePath === 'string') {
643
- result.statePath = writePath;
644
- }
645
- }
646
-
647
- if (failure) {
648
- result.failure = failure;
649
- }
650
- return result;
651
- }
652
-
653
- /**
654
- * Project a state object from `(spec, priorState, slugToIssue)` for the
655
- * state writer. Layers the post-apply slug → issue map onto the
656
- * canonical `buildState` output so newly-created issues get their fresh
657
- * numbers, dropped slugs disappear, and pre-existing observed agent
658
- * state is preserved for slugs unchanged by this apply.
659
- *
660
- * Idempotency contract (AC: "successful apply followed by an immediate
661
- * second apply is a no-op"). The diff engine's empty plan, the projected
662
- * mapping, and the byte-identical state writer compose to produce the
663
- * same on-disk state across re-runs.
664
- *
665
- * @param {{spec: object, priorState?: object, slugToIssue: Record<string, number>, now?: string}} input
666
- * @returns {{epicId: number, lastReconciledAt: string, mapping: object}}
667
- */
668
- function projectStateForWrite({ spec, priorState, slugToIssue, now }) {
669
- const base = buildState(spec, priorState ?? {}, now ? { now } : {});
670
- // `buildState` projects feature/story/task slugs — `iterSpecEntries`
671
- // intentionally walks features down, so the epic slug is not yielded.
672
- // The reconciler's contract (Tech Spec §"state.json") expects the
673
- // epic to appear in the mapping too: layer it on explicitly here so
674
- // a follow-up diff sees the epic mapped and emits no create for it.
675
- if (spec?.epic && typeof spec.epic.id === 'number' && !base.mapping.epic) {
676
- const priorEpic = priorState?.mapping?.epic ? priorState.mapping.epic : {};
677
- base.mapping.epic = {
678
- issueNumber:
679
- typeof slugToIssue.epic === 'number'
680
- ? slugToIssue.epic
681
- : typeof priorEpic.issueNumber === 'number'
682
- ? priorEpic.issueNumber
683
- : spec.epic.id,
684
- contentHash:
685
- typeof priorEpic.contentHash === 'string' ? priorEpic.contentHash : '',
686
- lastObservedAgentState:
687
- typeof priorEpic.lastObservedAgentState === 'string'
688
- ? priorEpic.lastObservedAgentState
689
- : null,
690
- entity: 'epic',
691
- parentSlug: null,
692
- };
693
- }
694
- // Layer structural edge metadata (entity, parentSlug, dependsOn) onto
695
- // each mapping entry. The diff engine reads these fields to decide
696
- // whether a Relink op should fire; without them every follow-up diff
697
- // would re-emit a relink for the parent edge that already exists in
698
- // GH. The AC's idempotency invariant requires this layering — the
699
- // pure mapping projection (`spec/state.js`) intentionally does not
700
- // carry edge data so the hashing path can stay isolated.
701
- const edgeLookup = collectStructuralEdges(spec);
702
- for (const slug of Object.keys(base.mapping)) {
703
- const entry = base.mapping[slug];
704
- const issueNumber = slugToIssue[slug];
705
- if (typeof issueNumber === 'number') {
706
- entry.issueNumber = issueNumber;
707
- }
708
- const edges = edgeLookup[slug];
709
- if (edges) {
710
- entry.entity = edges.entity;
711
- entry.parentSlug = edges.parentSlug;
712
- if (edges.dependsOn) entry.dependsOn = edges.dependsOn;
713
- }
714
- }
715
- // Closed slugs (dropped from spec) are absent from `base.mapping`
716
- // because `buildState` walks the spec. That matches the AC: closed
717
- // entries do not appear in state.
718
- return base;
719
- }
720
-
721
- /**
722
- * Walk the spec once and emit `{ slug → { entity, parentSlug, dependsOn? } }`
723
- * so `projectStateForWrite` can layer the edges back onto the state
724
- * mapping. Pure / allocation-bounded; runs O(spec size).
725
- *
726
- * @param {object} spec
727
- * @returns {Record<string, {entity: string, parentSlug: string|null, dependsOn?: string[]}>}
728
- */
729
- function collectStructuralEdges(spec) {
730
- const out = {};
731
- if (!spec || typeof spec !== 'object') return out;
732
- const epicSlug = 'epic';
733
- for (const story of spec.stories ?? []) {
734
- if (!story?.slug) continue;
735
- out[story.slug] = {
736
- entity: 'story',
737
- parentSlug: epicSlug,
738
- dependsOn: [...(story.dependsOn ?? [])].sort(),
739
- };
740
- }
741
- return out;
742
- }
743
-
744
- /**
745
- * Topologically sort the create ops into dependency batches. Each batch
746
- * is a list of ops whose parents are already in `slugToIssue` or in an
747
- * earlier batch. This keeps parent creates ahead of child creates
748
- * without forcing a global single-file execution.
749
- *
750
- * @param {import('./epic-spec-reconciler-ops.js').CreateOp[]} creates
751
- * @param {Record<string, number>} slugToIssue
752
- * @returns {import('./epic-spec-reconciler-ops.js').CreateOp[][]}
753
- */
754
- function topoSortCreates(creates, slugToIssue) {
755
- if (!creates.length) return [];
756
- const remaining = [...creates];
757
- const knownSlugs = new Set(Object.keys(slugToIssue));
758
- // dependsOn slugs may reference siblings that are themselves in the
759
- // create batch — those become known once their batch lands. Any slug
760
- // that is neither pre-known nor will be created here is an external
761
- // reference we can't satisfy via topo order; treat it as "satisfiable
762
- // outside the batch" so we don't deadlock. The fail-loud renderer
763
- // catches truly unresolved slugs at apply time.
764
- const createSlugs = new Set(creates.map((op) => op.slug));
765
- const batches = [];
766
- while (remaining.length) {
767
- const ready = remaining.filter((op) => {
768
- const parentReady = !op.parentSlug || knownSlugs.has(op.parentSlug);
769
- const depsReady =
770
- !Array.isArray(op.dependsOn) ||
771
- op.dependsOn.every((d) => knownSlugs.has(d) || !createSlugs.has(d));
772
- return parentReady && depsReady;
773
- });
774
- if (ready.length === 0) {
775
- // Cycle or missing parent — break out by emitting the rest as one
776
- // batch and letting `resolveParentId` / footer renderer raise the
777
- // structured error.
778
- batches.push(remaining.splice(0));
779
- break;
780
- }
781
- batches.push(ready);
782
- for (const op of ready) knownSlugs.add(op.slug);
783
- for (const op of ready) {
784
- const idx = remaining.indexOf(op);
785
- if (idx >= 0) remaining.splice(idx, 1);
786
- }
787
- }
788
- return batches;
789
- }