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
@@ -1,273 +0,0 @@
1
- /**
2
- * bookkeeping-outbox.js — Epic #4476 (M5): buffered GitHub bookkeeping.
3
- *
4
- * Delivery bookkeeping — structured-comment upserts (progress, friction,
5
- * wave-stall, …) and `agent::*` label flips — is human-visible surface that,
6
- * in an **unattended** (`--yes` / headless) run, no operator is watching in
7
- * real time. Emitting each one as a live GitHub round-trip mid-run costs an
8
- * LLM turn per transition for a surface nobody reads until the run finishes.
9
- *
10
- * This module lets those non-urgent mutations be **buffered to a local NDJSON
11
- * outbox** during the run and **reconciled to GitHub once at finalize**.
12
- * GitHub stays the source of truth *at rest* (post-reconcile); only the
13
- * per-transition chatter moves off the token stream. The outbox file survives
14
- * a crash, so crash recovery drains whatever was buffered before the finalize
15
- * reconcile ran.
16
- *
17
- * ## What is NEVER buffered
18
- * The `agent::blocked` HITL gate (`.agents/instructions.md` §1.J) is the single
19
- * authoritative runtime pause point. A genuine blocker MUST surface on GitHub
20
- * **immediately**, not batched to finalize — an operator can only resume a run
21
- * they can see is blocked. The {@link transitionStateOrBuffer} facade forces
22
- * `agent::blocked` (and any caller-marked `urgent`) transition through the
23
- * live path regardless of headless mode.
24
- *
25
- * ## Attended runs are unchanged
26
- * Buffering is gated on the explicit `headless` signal (Story #4427's
27
- * `--headless`/`--yes` plumbing). An attended run passes `headless: false` and
28
- * every comment/label posts live exactly as before — byte-for-byte behaviour.
29
- */
30
-
31
- import {
32
- appendFileSync,
33
- existsSync,
34
- mkdirSync,
35
- readFileSync,
36
- writeFileSync,
37
- } from 'node:fs';
38
- import path from 'node:path';
39
-
40
- import { runTempDir } from '../config/temp-paths.js';
41
- import { STATE_LABELS } from './ticketing/reads.js';
42
- import { upsertStructuredComment } from './ticketing/state.js';
43
- import { transitionTicketState } from './ticketing/transition.js';
44
-
45
- /** Canonical basename for the per-run bookkeeping outbox. */
46
- const OUTBOX_BASENAME = 'bookkeeping-outbox.ndjson';
47
-
48
- /**
49
- * Resolve the canonical outbox path for a run:
50
- * `temp/run-<id>/bookkeeping-outbox.ndjson`.
51
- *
52
- * Uses `runTempDir` directly; callers may still pass an `epicId`-named
53
- * variable while the broader file/flag rename remains out of this pass.
54
- *
55
- * @param {number} epicId
56
- * @param {object} [config] Resolved config (tempRoot).
57
- * @returns {string}
58
- */
59
- export function outboxPathFor(epicId, config) {
60
- return path.join(runTempDir(epicId, config), OUTBOX_BASENAME);
61
- }
62
-
63
- /**
64
- * Append one operation record to the outbox, creating the parent dir on
65
- * demand. Best-effort at the storage layer is the caller's concern; here we
66
- * let a genuine fs failure propagate so a mis-configured outbox path surfaces
67
- * loudly in tests.
68
- *
69
- * @param {string} outboxPath
70
- * @param {object} op
71
- */
72
- function appendOp(outboxPath, op) {
73
- mkdirSync(path.dirname(outboxPath), { recursive: true });
74
- appendFileSync(outboxPath, `${JSON.stringify(op)}\n`, 'utf8');
75
- }
76
-
77
- /**
78
- * Buffer a structured-comment upsert. The op captures everything
79
- * `upsertStructuredComment` needs at drain time so the reconcile is a pure
80
- * replay with no re-derivation.
81
- *
82
- * @param {{ outboxPath: string, ticketId: number, marker: string,
83
- * body: string, attrs?: Record<string, string|number>|null,
84
- * ts?: string }} args
85
- */
86
- export function enqueueComment({
87
- outboxPath,
88
- ticketId,
89
- marker,
90
- body,
91
- attrs = null,
92
- ts = new Date().toISOString(),
93
- }) {
94
- appendOp(outboxPath, {
95
- kind: 'comment',
96
- ts,
97
- ticketId,
98
- marker,
99
- body,
100
- ...(attrs ? { attrs } : {}),
101
- });
102
- }
103
-
104
- /**
105
- * Buffer an `agent::*` label transition.
106
- *
107
- * @param {{ outboxPath: string, ticketId: number, state: string,
108
- * ts?: string }} args
109
- */
110
- export function enqueueLabel({
111
- outboxPath,
112
- ticketId,
113
- state,
114
- ts = new Date().toISOString(),
115
- }) {
116
- appendOp(outboxPath, { kind: 'label', ts, ticketId, state });
117
- }
118
-
119
- /**
120
- * Parse the outbox into an ordered array of operation records. Malformed
121
- * lines are skipped (never throw). A missing / empty outbox yields `[]`.
122
- *
123
- * @param {string} outboxPath
124
- * @returns {Array<object>}
125
- */
126
- export function readOutbox(outboxPath) {
127
- if (!outboxPath || !existsSync(outboxPath)) return [];
128
- let raw;
129
- try {
130
- raw = readFileSync(outboxPath, 'utf8');
131
- } catch {
132
- return [];
133
- }
134
- if (!raw) return [];
135
- const ops = [];
136
- for (const line of raw.split(/\r?\n/)) {
137
- if (!line) continue;
138
- try {
139
- const op = JSON.parse(line);
140
- if (op && typeof op === 'object') ops.push(op);
141
- } catch {
142
- // skip a torn / malformed line — a partial write from a crash is not
143
- // fatal to the rest of the batch.
144
- }
145
- }
146
- return ops;
147
- }
148
-
149
- /**
150
- * Drain the outbox to GitHub in FIFO order and clear it on success. Both sink
151
- * operations are idempotent (comment upserts are marker-scoped;
152
- * `transitionTicketState` applies one state via the canonical
153
- * remove-all-then-add path), so a re-run after a partial drain converges. A
154
- * per-op failure is recorded and the drain continues — a single bad ticket
155
- * must not strand the rest of the batch. The outbox file is only truncated
156
- * when EVERY op succeeded, so a crash mid-drain leaves the un-applied
157
- * remainder for the next reconcile.
158
- *
159
- * @param {{ outboxPath: string,
160
- * provider: import('../ITicketingProvider.js').ITicketingProvider,
161
- * logger?: { warn?: (m: string) => void } }} args
162
- * @returns {Promise<{ drained: number, comments: number, labels: number,
163
- * errors: Array<{ op: object, error: string }>, cleared: boolean }>}
164
- */
165
- export async function reconcileOutbox({ outboxPath, provider, logger }) {
166
- const ops = readOutbox(outboxPath);
167
- const result = {
168
- drained: 0,
169
- comments: 0,
170
- labels: 0,
171
- errors: [],
172
- cleared: false,
173
- };
174
- if (ops.length === 0) {
175
- result.cleared = true;
176
- return result;
177
- }
178
-
179
- for (const op of ops) {
180
- try {
181
- if (op.kind === 'comment') {
182
- await upsertStructuredComment(
183
- provider,
184
- op.ticketId,
185
- op.marker,
186
- op.body,
187
- op.attrs ?? null,
188
- );
189
- result.comments += 1;
190
- result.drained += 1;
191
- } else if (op.kind === 'label') {
192
- await transitionTicketState(provider, op.ticketId, op.state);
193
- result.labels += 1;
194
- result.drained += 1;
195
- } else {
196
- result.errors.push({ op, error: `unknown op kind "${op.kind}"` });
197
- }
198
- } catch (err) {
199
- const message = err instanceof Error ? err.message : String(err);
200
- result.errors.push({ op, error: message });
201
- logger?.warn?.(
202
- `[bookkeeping-outbox] reconcile op failed (${op.kind} #${op.ticketId}): ${message}`,
203
- );
204
- }
205
- }
206
-
207
- // Only truncate when the whole batch landed — otherwise the un-applied
208
- // ops must survive for the next reconcile (crash-recovery contract).
209
- if (result.errors.length === 0) {
210
- try {
211
- writeFileSync(outboxPath, '', 'utf8');
212
- result.cleared = true;
213
- } catch {
214
- // A clear failure is non-fatal: the ops all landed, and the idempotent
215
- // sinks make a redundant re-drain a no-op.
216
- }
217
- }
218
- return result;
219
- }
220
-
221
- /**
222
- * Facade: post a structured comment live, OR buffer it to the outbox when the
223
- * run is headless. Comments are never urgent (the operator reads them at
224
- * finalize), so headless always buffers when an `outboxPath` is available.
225
- *
226
- * @param {{ provider: object, ticketId: number, marker: string, body: string,
227
- * attrs?: Record<string, string|number>|null, headless?: boolean,
228
- * outboxPath?: string|null }} args
229
- * @returns {Promise<{ buffered: boolean }>}
230
- */
231
- export async function postCommentOrBuffer({
232
- provider,
233
- ticketId,
234
- marker,
235
- body,
236
- attrs = null,
237
- headless = false,
238
- outboxPath = null,
239
- }) {
240
- if (headless && outboxPath) {
241
- enqueueComment({ outboxPath, ticketId, marker, body, attrs });
242
- return { buffered: true };
243
- }
244
- await upsertStructuredComment(provider, ticketId, marker, body, attrs);
245
- return { buffered: false };
246
- }
247
-
248
- /**
249
- * Facade: flip an `agent::*` state live, OR buffer it when headless — EXCEPT
250
- * `agent::blocked` (and any caller-marked `urgent` flip), which ALWAYS goes
251
- * live so the HITL gate surfaces immediately (§1.J).
252
- *
253
- * @param {{ provider: object, ticketId: number, state: string,
254
- * headless?: boolean, outboxPath?: string|null,
255
- * urgent?: boolean }} args
256
- * @returns {Promise<{ buffered: boolean }>}
257
- */
258
- export async function transitionStateOrBuffer({
259
- provider,
260
- ticketId,
261
- state,
262
- headless = false,
263
- outboxPath = null,
264
- urgent = false,
265
- }) {
266
- const mustSurfaceNow = urgent || state === STATE_LABELS.BLOCKED;
267
- if (headless && outboxPath && !mustSurfaceNow) {
268
- enqueueLabel({ outboxPath, ticketId, state });
269
- return { buffered: true };
270
- }
271
- await transitionTicketState(provider, ticketId, state);
272
- return { buffered: false };
273
- }
@@ -1,139 +0,0 @@
1
- /**
2
- * ErrorJournal — append-only JSONL log of swallowed errors from the
3
- * orchestration engine's "silent catch" sites.
4
- *
5
- * Design notes (see tech spec #382):
6
- * - JSONL format: a crashed orchestrator still leaves a valid file up to the
7
- * last full line.
8
- * - Lazy open: the fd opens on first `record()` call, so runs without any
9
- * swallowed errors produce no file.
10
- * - Additive: does not replace `logger.warn`. Callers emit both so live
11
- * tails stay useful.
12
- * - Secret masking: before writing, any string value whose parent key name
13
- * or content looks secret-like gets a `::add-mask::<value>` directive
14
- * emitted to stdout so any host with mask support redacts accidental
15
- * echoes.
16
- * - Finalize is idempotent: repeat calls are safe.
17
- */
18
-
19
- import fs from 'node:fs/promises';
20
- import path from 'node:path';
21
-
22
- const SECRET_KEY_RE = /token|secret|password|bearer|apikey|api[-_]?key|auth/i;
23
- const SECRET_VALUE_RE =
24
- /\b(gh[pous]_[A-Za-z0-9]{30,}|sk-[A-Za-z0-9]{20,}|xox[bpars]-[A-Za-z0-9-]+|AKIA[0-9A-Z]{16})\b/;
25
- // Hoisted so the maintainability analyzer (escomplex) doesn't choke on an
26
- // inline RegExp literal inside .split() — it mis-parses the sequence.
27
- const NEWLINE_RE = /\r?\n/;
28
-
29
- export class ErrorJournal {
30
- /**
31
- * @param {{ epicId: number, logDir?: string }} opts
32
- */
33
- constructor({ epicId, logDir = 'temp' } = {}) {
34
- if (!Number.isInteger(epicId)) {
35
- throw new TypeError('ErrorJournal requires a numeric epicId');
36
- }
37
- this.epicId = epicId;
38
- this.logDir = logDir;
39
- this._path = path.join(logDir, `epic-${epicId}-errors.log`);
40
- this._fh = null;
41
- this._opening = null;
42
- this._finalized = false;
43
- }
44
-
45
- /** @returns {string} Absolute or relative path to the journal file. */
46
- get path() {
47
- return this._path;
48
- }
49
-
50
- /**
51
- * Append one entry to the journal. Emits `::add-mask::` directives for any
52
- * secret-looking values before writing.
53
- *
54
- * @param {{
55
- * module: string,
56
- * op: string,
57
- * error: unknown,
58
- * recovery?: string,
59
- * }} entry
60
- */
61
- async record({ module, op, error, recovery } = {}) {
62
- if (this._finalized) return;
63
- const payload = {
64
- ts: new Date().toISOString(),
65
- epicId: this.epicId,
66
- module: module ?? null,
67
- op: op ?? null,
68
- error: serializeError(error),
69
- recovery: recovery ?? null,
70
- };
71
- emitMasksFor(payload);
72
- const fh = await this._ensureOpen();
73
- await fh.write(`${JSON.stringify(payload)}\n`);
74
- }
75
-
76
- /**
77
- * Close the underlying file handle. Idempotent: repeat calls are no-ops.
78
- * Calls to `record` after finalize silently drop.
79
- */
80
- async finalize() {
81
- if (this._finalized) return;
82
- this._finalized = true;
83
- const fh = this._fh;
84
- this._fh = null;
85
- this._opening = null;
86
- if (fh) {
87
- try {
88
- await fh.close();
89
- } catch {
90
- // idempotent — closing a closed handle is fine
91
- }
92
- }
93
- }
94
-
95
- async _ensureOpen() {
96
- if (this._fh) return this._fh;
97
- if (!this._opening) {
98
- this._opening = (async () => {
99
- await fs.mkdir(this.logDir, { recursive: true });
100
- this._fh = await fs.open(this._path, 'a');
101
- return this._fh;
102
- })();
103
- }
104
- return this._opening;
105
- }
106
- }
107
-
108
- function serializeError(err) {
109
- if (err == null) return null;
110
- if (err instanceof Error) {
111
- return { name: err.name, message: err.message, stack: err.stack };
112
- }
113
- if (typeof err === 'string') return { message: err };
114
- if (typeof err === 'object') return err;
115
- return { message: String(err) };
116
- }
117
-
118
- function emitMasksFor(node, parentKey = null) {
119
- if (node == null) return;
120
- if (typeof node === 'string') {
121
- if (
122
- (parentKey && SECRET_KEY_RE.test(parentKey)) ||
123
- SECRET_VALUE_RE.test(node)
124
- ) {
125
- for (const line of node.split(NEWLINE_RE)) {
126
- const trimmed = line.trim();
127
- if (trimmed) console.log(`::add-mask::${trimmed}`);
128
- }
129
- }
130
- return;
131
- }
132
- if (Array.isArray(node)) {
133
- for (const item of node) emitMasksFor(item, parentKey);
134
- return;
135
- }
136
- if (typeof node === 'object') {
137
- for (const [k, v] of Object.entries(node)) emitMasksFor(v, k);
138
- }
139
- }
@@ -1,155 +0,0 @@
1
- /**
2
- * emit-story-heartbeat.js — Story #3057.
3
- *
4
- * Programmatic helper that appends a single `story.heartbeat` NDJSON
5
- * record to the Story lifecycle ledger (`temp/.../lifecycle.ndjson` via
6
- * `lib/config/temp-paths.js`) during a Story's implementation loop. Story-implementation phases can run for many
7
- * minutes between dispatch and merge; `story.heartbeat` is the
8
- * inspectable in-progress signal the host-loop reconciler reads to
9
- * confirm forward progress.
10
- *
11
- * Distinct from:
12
- * - `story.dispatch.start` — one per Story per dispatch attempt
13
- * (lifecycle-emit-story-dispatch.js).
14
- * - `story.merged` — one per Story per close, post-merge.
15
- *
16
- * The emit is best-effort: a failure to append (missing schema,
17
- * unreachable ledger path, validation error) MUST NOT block the phase
18
- * transition itself. Callers should catch and log via the script's
19
- * Logger; the heartbeat is observability, not state.
20
- *
21
- * Schema contract (story.heartbeat.schema.json):
22
- * { event, storyId, epicId, phase, timestamp, operator? }
23
- *
24
- * The schema declares `additionalProperties: false`, so this emitter's
25
- * signature is deliberately narrow: only the schema-allowed fields are
26
- * accepted. The earlier per-child Task id and progress counters
27
- * were dropped under Epic #3078's
28
- * 2-tier hard cutover — they would fail strict validation and have no
29
- * meaning now that the Story is the leaf execution unit with no child
30
- * tickets. The optional `operator` field (Story #3480) records the handle
31
- * holding the assignee-as-lease claim; it is included only when supplied so
32
- * pre-lease callers continue to emit the unchanged shape.
33
- */
34
-
35
- import { appendFileSync, mkdirSync, readFileSync } from 'node:fs';
36
- import path from 'node:path';
37
- import { fileURLToPath } from 'node:url';
38
-
39
- import Ajv2020 from 'ajv/dist/2020.js';
40
- import addFormats from 'ajv-formats';
41
-
42
- import { epicLedgerPath } from '../../config/temp-paths.js';
43
-
44
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
45
- const SCHEMA_PATH = path.resolve(
46
- __dirname,
47
- '..',
48
- '..',
49
- '..',
50
- '..',
51
- 'schemas',
52
- 'lifecycle',
53
- 'story.heartbeat.schema.json',
54
- );
55
-
56
- const VALID_PHASES = new Set([
57
- 'init',
58
- 'implementing',
59
- 'closing',
60
- 'blocked',
61
- 'done',
62
- ]);
63
-
64
- let _validator;
65
-
66
- function getValidator() {
67
- if (_validator) return _validator;
68
- const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
69
- const ajv = new Ajv2020({ allErrors: true, strict: false });
70
- addFormats(ajv);
71
- _validator = ajv.compile(schema);
72
- return _validator;
73
- }
74
-
75
- /**
76
- * Append exactly one `story.heartbeat` NDJSON record to the Epic ledger.
77
- *
78
- * @param {object} opts
79
- * @param {number} opts.storyId Story whose implementation loop is firing.
80
- * @param {number} opts.epicId Parent Epic — required for the ledger path.
81
- * @param {string} [opts.phase='implementing']
82
- * One of init|implementing|closing|blocked|done.
83
- * @param {string} [opts.timestamp] ISO-8601 wall clock. Defaults to now().
84
- * @param {string} [opts.operator] Optional lease-owner handle (Story #3480).
85
- * When a non-empty string is supplied it is
86
- * recorded on the payload so the
87
- * assignee-as-lease primitive can decide a
88
- * claim's liveness from the latest heartbeat
89
- * for a given owner. Omitted when absent so
90
- * pre-lease callers emit the unchanged shape.
91
- * @param {object} [opts.config] Optional resolved config for tempRoot.
92
- * @param {string} [opts.ledgerPath] Override for tests.
93
- * @returns {{ ledgerPath: string, record: object }}
94
- */
95
- export function emitStoryHeartbeat(opts) {
96
- const {
97
- storyId,
98
- epicId,
99
- phase = 'implementing',
100
- timestamp = new Date().toISOString(),
101
- operator,
102
- config,
103
- ledgerPath: ledgerPathOverride,
104
- } = opts ?? {};
105
-
106
- if (!Number.isInteger(storyId) || storyId < 1) {
107
- throw new Error('emitStoryHeartbeat: storyId must be a positive integer');
108
- }
109
- if (!Number.isInteger(epicId) || epicId < 1) {
110
- throw new Error('emitStoryHeartbeat: epicId must be a positive integer');
111
- }
112
- if (!VALID_PHASES.has(phase)) {
113
- throw new Error(
114
- `emitStoryHeartbeat: phase "${phase}" must be one of: ${[...VALID_PHASES].join(', ')}`,
115
- );
116
- }
117
- if (
118
- operator !== undefined &&
119
- (typeof operator !== 'string' || operator.length === 0)
120
- ) {
121
- throw new Error(
122
- 'emitStoryHeartbeat: operator, when supplied, must be a non-empty string',
123
- );
124
- }
125
-
126
- const payload = {
127
- event: 'story.heartbeat',
128
- storyId,
129
- epicId,
130
- phase,
131
- timestamp,
132
- ...(operator !== undefined ? { operator } : {}),
133
- };
134
-
135
- const validator = getValidator();
136
- if (!validator(payload)) {
137
- const detail = (validator.errors ?? [])
138
- .map((e) => `${e.instancePath || '/'} ${e.message}`)
139
- .join('; ');
140
- throw new Error(
141
- `emitStoryHeartbeat: payload failed schema validation: ${detail}`,
142
- );
143
- }
144
-
145
- const ledgerPath = ledgerPathOverride ?? epicLedgerPath(epicId, config);
146
- mkdirSync(path.dirname(ledgerPath), { recursive: true });
147
- const record = {
148
- kind: 'emitted',
149
- ts: timestamp,
150
- event: 'story.heartbeat',
151
- payload,
152
- };
153
- appendFileSync(ledgerPath, `${JSON.stringify(record)}\n`, 'utf8');
154
- return { ledgerPath, record };
155
- }
@@ -1,140 +0,0 @@
1
- /**
2
- * ledger-diff.js — structural-diff and ordering-assertion helpers over a
3
- * lifecycle ledger (`temp/epic-<id>/lifecycle.ndjson`) for the
4
- * repeatability invariants.
5
- *
6
- * Relocated from the retired `.agents/scripts/lifecycle-diff.js` CLI
7
- * (#4482): the assertion helpers are the live surface — they pin the
8
- * cross-listener ordering contracts in the lifecycle invariant tests —
9
- * while the CLI wrapper had no workflow, script, or CI consumer.
10
- *
11
- * Invariants are derived from Tech Spec #2189 § Repeatability Acceptance
12
- * Criteria.
13
- */
14
-
15
- /**
16
- * Parse an NDJSON lifecycle ledger into an array of records. Blank
17
- * lines tolerated; malformed JSON throws with line number. Duplicated
18
- * from `trace-logger.js` to keep the diff surface decoupled from the
19
- * listener surface.
20
- */
21
- export function parseLedgerText(text) {
22
- const out = [];
23
- const lines = String(text || '').split('\n');
24
- for (let i = 0; i < lines.length; i += 1) {
25
- const line = lines[i].trim();
26
- if (line.length === 0) continue;
27
- try {
28
- out.push(JSON.parse(line));
29
- } catch (_err) {
30
- throw new Error(
31
- `ledger-diff: malformed JSON in ledger on line ${i + 1}: ${line.slice(0, 80)}`,
32
- );
33
- }
34
- }
35
- return out;
36
- }
37
-
38
- /**
39
- * Project a single record into a comparison key. `ts` and `seqId` are
40
- * intentionally elided; the rest of the record is included so that the
41
- * structural shape (event order, payload contents, listener attribution
42
- * on failed records) is the diff surface.
43
- *
44
- * Exported for unit tests that pin the diff contract.
45
- */
46
- export function projectRecord(rec) {
47
- if (!rec || typeof rec !== 'object') return rec;
48
- const { ts: _ts, seqId: _seqId, ...rest } = rec;
49
- return rest;
50
- }
51
-
52
- /**
53
- * Structural diff of two ledger arrays. Returns an array of mismatch
54
- * descriptors (empty when identical modulo `ts`/`seqId`).
55
- */
56
- export function diff(ledgerA, ledgerB) {
57
- const a = ledgerA.map(projectRecord);
58
- const b = ledgerB.map(projectRecord);
59
- const out = [];
60
- const max = Math.max(a.length, b.length);
61
- for (let i = 0; i < max; i += 1) {
62
- const left = a[i];
63
- const right = b[i];
64
- const leftJson = JSON.stringify(left);
65
- const rightJson = JSON.stringify(right);
66
- if (leftJson !== rightJson) {
67
- out.push({ index: i, left: left ?? null, right: right ?? null });
68
- }
69
- }
70
- return out;
71
- }
72
-
73
- /**
74
- * Assert: epic.merge.armed must be preceded by epic.merge.ready
75
- * (same run). Returns `{ ok: true }` or `{ ok: false, reason }`.
76
- */
77
- export function assertMergeGateOrdering(records) {
78
- let sawReady = false;
79
- let sawReadySeq = null;
80
- for (const rec of records) {
81
- if (rec.kind !== 'emitted') continue;
82
- if (rec.event === 'epic.merge.ready') {
83
- sawReady = true;
84
- sawReadySeq = rec.seqId;
85
- } else if (rec.event === 'epic.merge.armed') {
86
- if (!sawReady) {
87
- return {
88
- ok: false,
89
- reason: `epic.merge.armed at seqId=${rec.seqId} without preceding epic.merge.ready`,
90
- };
91
- }
92
- if (sawReadySeq != null && rec.seqId <= sawReadySeq) {
93
- return {
94
- ok: false,
95
- reason: `epic.merge.armed seqId=${rec.seqId} must be > epic.merge.ready seqId=${sawReadySeq}`,
96
- };
97
- }
98
- }
99
- }
100
- return { ok: true };
101
- }
102
-
103
- /**
104
- * Assert: pr.created must be preceded by either acceptance.reconcile.ok
105
- * or acceptance.reconcile.waived in the same run. Story #2893 split the
106
- * waiver path out of `.skipped` so the Finalizer now subscribes to both
107
- * `.ok` and `.waived`; either is a valid predecessor for pr.created.
108
- * A reconcile.skipped (empty-spec only after #2893) or reconcile.failed
109
- * before pr.created remains a violation.
110
- */
111
- export function assertReconcileOrdering(records) {
112
- let sawReconcileGate = false;
113
- let sawReconcileGateSeq = null;
114
- let sawReconcileGateEvent = null;
115
- for (const rec of records) {
116
- if (rec.kind !== 'emitted') continue;
117
- if (
118
- rec.event === 'acceptance.reconcile.ok' ||
119
- rec.event === 'acceptance.reconcile.waived'
120
- ) {
121
- sawReconcileGate = true;
122
- sawReconcileGateSeq = rec.seqId;
123
- sawReconcileGateEvent = rec.event;
124
- } else if (rec.event === 'pr.created') {
125
- if (!sawReconcileGate) {
126
- return {
127
- ok: false,
128
- reason: `pr.created at seqId=${rec.seqId} without preceding acceptance.reconcile.ok or acceptance.reconcile.waived`,
129
- };
130
- }
131
- if (sawReconcileGateSeq != null && rec.seqId <= sawReconcileGateSeq) {
132
- return {
133
- ok: false,
134
- reason: `pr.created seqId=${rec.seqId} must be > ${sawReconcileGateEvent} seqId=${sawReconcileGateSeq}`,
135
- };
136
- }
137
- }
138
- }
139
- return { ok: true };
140
- }