mandrel 2.0.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 (311) hide show
  1. package/.agents/README.md +59 -28
  2. package/.agents/agents/acceptance-critic.md +9 -7
  3. package/.agents/agents/story-worker.md +41 -46
  4. package/.agents/audit-checklists/performance.md +1 -1
  5. package/.agents/docs/SDLC.md +51 -44
  6. package/.agents/docs/agentrc-reference.json +8 -13
  7. package/.agents/docs/configuration.md +32 -56
  8. package/.agents/docs/execution-reference.md +39 -10
  9. package/.agents/docs/quality-gates.md +14 -16
  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/git-conventions-reference.md +42 -51
  14. package/.agents/schemas/agentrc.schema.json +34 -45
  15. package/.agents/schemas/audit-rules.json +59 -1
  16. package/.agents/schemas/audit-rules.schema.json +33 -1
  17. package/.agents/schemas/lifecycle/README.md +1 -2
  18. package/.agents/schemas/lifecycle/ledger-record.schema.json +1 -1
  19. package/.agents/schemas/lifecycle/merge.flip-failed.schema.json +33 -0
  20. package/.agents/schemas/lifecycle/merge.unlanded.schema.json +1 -0
  21. package/.agents/schemas/lifecycle/story.merged.schema.json +1 -1
  22. package/.agents/schemas/signal-event.schema.json +3 -3
  23. package/.agents/schemas/story-deliver-terminal.schema.json +152 -0
  24. package/.agents/schemas/validation-evidence.schema.json +1 -1
  25. package/.agents/scripts/acceptance-eval.js +22 -66
  26. package/.agents/scripts/agents-bootstrap-github.js +1 -1
  27. package/.agents/scripts/bootstrap.js +3 -3
  28. package/.agents/scripts/check-dead-exports.js +43 -104
  29. package/.agents/scripts/check-doc-links.js +2 -2
  30. package/.agents/scripts/check-lifecycle-lint.js +1 -1
  31. package/.agents/scripts/check-workflow-cli-lint.js +91 -0
  32. package/.agents/scripts/deliver-recover.js +122 -0
  33. package/.agents/scripts/drain-pending-cleanup.js +1 -1
  34. package/.agents/scripts/evidence-gate.js +20 -50
  35. package/.agents/scripts/generate-skills-index.js +17 -1
  36. package/.agents/scripts/generate-workflows-doc.js +4 -4
  37. package/.agents/scripts/lib/ITicketingProvider.js +1 -19
  38. package/.agents/scripts/lib/audit-suite/selector.js +323 -23
  39. package/.agents/scripts/lib/baselines/kinds/maintainability.js +0 -11
  40. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +28 -33
  41. package/.agents/scripts/lib/bootstrap/manifest.js +8 -11
  42. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +30 -53
  43. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +0 -2
  44. package/.agents/scripts/lib/checks/core-bare-clean.js +4 -1
  45. package/.agents/scripts/lib/checks/index.js +1 -1
  46. package/.agents/scripts/lib/checks/loop-health.js +12 -11
  47. package/.agents/scripts/lib/checks/state.js +17 -248
  48. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +3 -3
  49. package/.agents/scripts/lib/checks/subagent-agent-tool-required.js +3 -4
  50. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +2 -2
  51. package/.agents/scripts/lib/checks/worktree-residue-biome.js +3 -3
  52. package/.agents/scripts/lib/cli-args.js +23 -2
  53. package/.agents/scripts/lib/close-validation/gates.js +13 -13
  54. package/.agents/scripts/lib/close-validation/projections/inputs.js +7 -7
  55. package/.agents/scripts/lib/close-validation/projections/maintainability.js +12 -12
  56. package/.agents/scripts/lib/close-validation/runner.js +13 -21
  57. package/.agents/scripts/lib/close-validation/telemetry.js +17 -8
  58. package/.agents/scripts/lib/config/delivery-routing.js +7 -6
  59. package/.agents/scripts/lib/config/explain.js +10 -16
  60. package/.agents/scripts/lib/config/github.js +7 -5
  61. package/.agents/scripts/lib/config/limits.js +15 -25
  62. package/.agents/scripts/lib/config/quality.js +11 -14
  63. package/.agents/scripts/lib/config/runners.js +8 -21
  64. package/.agents/scripts/lib/config/temp-paths.js +18 -56
  65. package/.agents/scripts/lib/config-settings-schema-delivery.js +31 -13
  66. package/.agents/scripts/lib/config-settings-schema-quality.js +9 -2
  67. package/.agents/scripts/lib/config-settings-schema.js +48 -22
  68. package/.agents/scripts/lib/dead-exports-knip.js +105 -0
  69. package/.agents/scripts/lib/dead-exports-mode.js +51 -0
  70. package/.agents/scripts/lib/duplicate-search.js +38 -7
  71. package/.agents/scripts/lib/findings/promote-finding.js +23 -14
  72. package/.agents/scripts/lib/format-generated-json.js +97 -0
  73. package/.agents/scripts/lib/framework-version.js +19 -189
  74. package/.agents/scripts/lib/gh-exec.js +8 -0
  75. package/.agents/scripts/lib/git-branch-lifecycle.js +0 -158
  76. package/.agents/scripts/lib/git-utils.js +0 -14
  77. package/.agents/scripts/lib/json-utils.js +1 -2
  78. package/.agents/scripts/lib/label-constants.js +0 -15
  79. package/.agents/scripts/lib/label-taxonomy.js +1 -12
  80. package/.agents/scripts/lib/observability/active-story-env.js +42 -163
  81. package/.agents/scripts/lib/observability/runtime-friction.js +243 -0
  82. package/.agents/scripts/lib/observability/signal-validator.js +4 -4
  83. package/.agents/scripts/lib/observability/signals-writer.js +6 -82
  84. package/.agents/scripts/lib/observability/source-classifier.js +2 -2
  85. package/.agents/scripts/lib/observability/tool-trace-hook.js +2 -12
  86. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +1 -1
  87. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +2 -2
  88. package/.agents/scripts/lib/orchestration/ceremony-routing.js +43 -45
  89. package/.agents/scripts/lib/orchestration/code-review.js +58 -168
  90. package/.agents/scripts/lib/orchestration/consolidation-precondition.js +3 -3
  91. package/.agents/scripts/lib/orchestration/deliver-recover.js +328 -0
  92. package/.agents/scripts/lib/orchestration/detectors-phase.js +12 -6
  93. package/.agents/scripts/lib/orchestration/git-cleanup/phases/fast-forward.js +34 -0
  94. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -2
  95. package/.agents/scripts/lib/orchestration/lifecycle/emit-ledger-event.js +142 -0
  96. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +9 -11
  97. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-flip-failed.js +86 -0
  98. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +37 -103
  99. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +7 -3
  100. package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +50 -85
  101. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +3 -14
  102. package/.agents/scripts/lib/orchestration/merge-block-class.js +76 -20
  103. package/.agents/scripts/lib/orchestration/merge-poll.js +104 -0
  104. package/.agents/scripts/lib/orchestration/plan-context.js +114 -24
  105. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +11 -22
  106. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +3 -7
  107. package/.agents/scripts/lib/orchestration/plan-metrics.js +38 -6
  108. package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +16 -6
  109. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +173 -25
  110. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +116 -0
  111. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +280 -75
  112. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +472 -55
  113. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +21 -16
  114. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +509 -0
  115. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +41 -40
  116. package/.agents/scripts/lib/orchestration/planning/spec-authoring-grounding.js +1 -1
  117. package/.agents/scripts/lib/orchestration/resolve-stories.js +344 -0
  118. package/.agents/scripts/lib/orchestration/retro-proposals.js +7 -7
  119. package/.agents/scripts/lib/orchestration/review-depth.js +105 -40
  120. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +3 -13
  121. package/.agents/scripts/lib/orchestration/review-providers/native.js +1 -154
  122. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +3 -2
  123. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +1 -1
  124. package/.agents/scripts/lib/orchestration/review-providers/types.js +5 -4
  125. package/.agents/scripts/lib/orchestration/review-providers/ultrareview.js +1 -1
  126. package/.agents/scripts/lib/orchestration/run-epilogue.js +374 -16
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +24 -0
  128. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +11 -9
  129. package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +4 -4
  130. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +3 -12
  131. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +608 -152
  132. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +72 -30
  133. package/.agents/scripts/lib/orchestration/single-story-close/phases/post-land.js +305 -0
  134. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +1 -1
  135. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +12 -8
  136. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +37 -4
  137. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +2 -2
  138. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +264 -43
  139. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  140. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +10 -10
  141. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +15 -32
  142. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +360 -0
  143. package/.agents/scripts/lib/orchestration/story-follow-ups.js +75 -14
  144. package/.agents/scripts/lib/orchestration/story-init-remote.js +12 -8
  145. package/.agents/scripts/lib/orchestration/story-plan-state.js +14 -29
  146. package/.agents/scripts/lib/orchestration/task-body-validator.js +52 -7
  147. package/.agents/scripts/lib/orchestration/ticket-lease.js +27 -74
  148. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +119 -14
  149. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +3 -4
  150. package/.agents/scripts/lib/orchestration/ticket-validator.js +121 -18
  151. package/.agents/scripts/lib/orchestration/ticketing/bulk.js +14 -47
  152. package/.agents/scripts/lib/orchestration/ticketing/reads.js +19 -32
  153. package/.agents/scripts/lib/orchestration/ticketing/transition.js +61 -1
  154. package/.agents/scripts/lib/orchestration/ticketing.js +0 -1
  155. package/.agents/scripts/lib/plan-phase-cleanup.js +12 -14
  156. package/.agents/scripts/lib/planning-corpus.js +12 -286
  157. package/.agents/scripts/lib/preflight-runner.js +2 -2
  158. package/.agents/scripts/lib/qa/qa-context-hydrator.js +5 -5
  159. package/.agents/scripts/lib/signals/index.js +4 -17
  160. package/.agents/scripts/lib/signals/read.js +35 -35
  161. package/.agents/scripts/lib/signals/schema.js +8 -11
  162. package/.agents/scripts/lib/signals/span-tree.js +7 -7
  163. package/.agents/scripts/lib/signals/write.js +0 -1
  164. package/.agents/scripts/lib/single-story/story-merged-notify.js +13 -2
  165. package/.agents/scripts/lib/skills/parse-skill.js +16 -3
  166. package/.agents/scripts/lib/story-adjacency.js +8 -7
  167. package/.agents/scripts/lib/story-body/story-body.js +6 -5
  168. package/.agents/scripts/lib/templates/decomposer-prompts.js +7 -3
  169. package/.agents/scripts/lib/test-env.js +14 -1
  170. package/.agents/scripts/lib/test-tiers.js +0 -3
  171. package/.agents/scripts/lib/ticket-body-sections.js +0 -14
  172. package/.agents/scripts/lib/validation-evidence.js +31 -59
  173. package/.agents/scripts/lib/wave-runner/ready-set.js +32 -6
  174. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +1 -1
  175. package/.agents/scripts/lib/worktree/lifecycle/reap.js +68 -19
  176. package/.agents/scripts/lib/worktree/lifecycle-manager.js +1 -2
  177. package/.agents/scripts/plan-context.js +38 -6
  178. package/.agents/scripts/plan-persist.js +145 -35
  179. package/.agents/scripts/plan-run-epilogue.js +83 -38
  180. package/.agents/scripts/post-structured-comment.js +0 -38
  181. package/.agents/scripts/pr-watch-with-update.js +43 -22
  182. package/.agents/scripts/providers/github/compose.js +0 -1
  183. package/.agents/scripts/providers/github/errors.js +0 -19
  184. package/.agents/scripts/providers/github/issues.js +1 -11
  185. package/.agents/scripts/providers/github/mappers.js +5 -0
  186. package/.agents/scripts/providers/github/sub-issues.js +0 -47
  187. package/.agents/scripts/providers/github/tickets.js +33 -153
  188. package/.agents/scripts/providers/github.js +17 -6
  189. package/.agents/scripts/resolve-stories.js +236 -0
  190. package/.agents/scripts/run-coverage.js +4 -1
  191. package/.agents/scripts/run-lint.js +2 -2
  192. package/.agents/scripts/run-verify.js +31 -2
  193. package/.agents/scripts/signals-view.js +9 -10
  194. package/.agents/scripts/single-story-close.js +173 -18
  195. package/.agents/scripts/single-story-confirm-merge.js +288 -15
  196. package/.agents/scripts/single-story-init.js +6 -10
  197. package/.agents/scripts/stories-wave-tick.js +79 -4
  198. package/.agents/scripts/story-plan.js +3 -3
  199. package/.agents/scripts/update-ticket-state.js +8 -50
  200. package/.agents/skills/core/code-review-and-quality/SKILL.md +28 -450
  201. package/.agents/skills/core/code-review-and-quality/reference.md +458 -0
  202. package/.agents/skills/core/debugging-and-error-recovery/SKILL.md +22 -315
  203. package/.agents/skills/core/debugging-and-error-recovery/reference.md +323 -0
  204. package/.agents/skills/core/diagnose-friction/SKILL.md +14 -18
  205. package/.agents/skills/core/documentation-and-adrs/SKILL.md +25 -397
  206. package/.agents/skills/core/documentation-and-adrs/reference.md +403 -0
  207. package/.agents/skills/core/gates-and-baselines/SKILL.md +12 -12
  208. package/.agents/skills/core/idea-refinement/SKILL.md +3 -3
  209. package/.agents/skills/core/scope-triage/SKILL.md +3 -0
  210. package/.agents/skills/core/security-and-hardening/SKILL.md +22 -367
  211. package/.agents/skills/core/security-and-hardening/reference.md +375 -0
  212. package/.agents/skills/skills.index.json +2 -12
  213. package/.agents/skills/stack/qa/playwright-bdd/SKILL.md +2 -4
  214. package/.agents/skills/stack/qa/qa-explore-driving/SKILL.md +1 -1
  215. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -3
  216. package/.agents/workflows/audit-architecture.md +3 -4
  217. package/.agents/workflows/audit-clean-code.md +4 -4
  218. package/.agents/workflows/audit-documentation.md +4 -5
  219. package/.agents/workflows/audit-lighthouse.md +8 -0
  220. package/.agents/workflows/audit-navigability.md +10 -0
  221. package/.agents/workflows/audit-performance.md +2 -3
  222. package/.agents/workflows/audit-quality.md +8 -9
  223. package/.agents/workflows/audit-security.md +1 -2
  224. package/.agents/workflows/audit-seo.md +10 -0
  225. package/.agents/workflows/audit-ux-ui.md +7 -0
  226. package/.agents/workflows/deliver.md +98 -45
  227. package/.agents/workflows/git-cleanup.md +2 -2
  228. package/.agents/workflows/git-deliver.md +1 -1
  229. package/.agents/workflows/helpers/acceptance-self-eval.md +21 -13
  230. package/.agents/workflows/helpers/code-quality-guardrails.md +7 -7
  231. package/.agents/workflows/helpers/code-review.md +12 -10
  232. package/.agents/workflows/helpers/deliver-story-reference.md +73 -32
  233. package/.agents/workflows/helpers/deliver-story.md +193 -118
  234. package/.agents/workflows/helpers/parallel-tooling.md +2 -2
  235. package/.agents/workflows/helpers/worktree-lifecycle.md +28 -32
  236. package/.agents/workflows/plan.md +184 -19
  237. package/.agents/workflows/qa-assist.md +6 -6
  238. package/.agents/workflows/qa-explore.md +3 -3
  239. package/.agents/workflows/qa-run.md +1 -5
  240. package/bin/mandrel.js +12 -1
  241. package/docs/CHANGELOG.md +40 -0
  242. package/lib/cli/registry.js +262 -19
  243. package/lib/cli/sync-agents.js +157 -0
  244. package/lib/cli/sync-commands.js +115 -6
  245. package/lib/cli/sync.js +168 -6
  246. package/lib/cli/update.js +105 -8
  247. package/lib/cli/version-helpers.js +131 -0
  248. package/lib/migrations/README.md +7 -5
  249. package/lib/migrations/index.js +12 -9
  250. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +100 -0
  251. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +101 -0
  252. package/package.json +1 -1
  253. package/.agents/schemas/epic-perf-report.schema.json +0 -89
  254. package/.agents/schemas/lifecycle/acceptance.reconcile.failed.schema.json +0 -13
  255. package/.agents/schemas/lifecycle/acceptance.reconcile.ok.schema.json +0 -13
  256. package/.agents/schemas/lifecycle/acceptance.reconcile.skipped.schema.json +0 -13
  257. package/.agents/schemas/lifecycle/acceptance.reconcile.start.schema.json +0 -12
  258. package/.agents/schemas/lifecycle/acceptance.reconcile.waived.schema.json +0 -13
  259. package/.agents/schemas/lifecycle/epic.automerge.end.schema.json +0 -15
  260. package/.agents/schemas/lifecycle/epic.automerge.start.schema.json +0 -13
  261. package/.agents/schemas/lifecycle/epic.blocked.schema.json +0 -13
  262. package/.agents/schemas/lifecycle/epic.cleanup.end.schema.json +0 -12
  263. package/.agents/schemas/lifecycle/epic.cleanup.start.schema.json +0 -12
  264. package/.agents/schemas/lifecycle/epic.close.end.schema.json +0 -12
  265. package/.agents/schemas/lifecycle/epic.complete.schema.json +0 -13
  266. package/.agents/schemas/lifecycle/epic.finalize.end.schema.json +0 -13
  267. package/.agents/schemas/lifecycle/epic.finalize.start.schema.json +0 -12
  268. package/.agents/schemas/lifecycle/epic.merge.armed.schema.json +0 -13
  269. package/.agents/schemas/lifecycle/epic.merge.blocked.schema.json +0 -14
  270. package/.agents/schemas/lifecycle/epic.merge.confirmed.schema.json +0 -17
  271. package/.agents/schemas/lifecycle/epic.merge.ready.schema.json +0 -15
  272. package/.agents/schemas/lifecycle/epic.plan.end.schema.json +0 -18
  273. package/.agents/schemas/lifecycle/epic.plan.start.schema.json +0 -12
  274. package/.agents/schemas/lifecycle/epic.snapshot.end.schema.json +0 -16
  275. package/.agents/schemas/lifecycle/epic.snapshot.start.schema.json +0 -12
  276. package/.agents/schemas/lifecycle/epic.watch.end.schema.json +0 -29
  277. package/.agents/schemas/lifecycle/epic.watch.start.schema.json +0 -16
  278. package/.agents/schemas/lifecycle/story.heartbeat.schema.json +0 -20
  279. package/.agents/schemas/risk-verdict.schema.json +0 -53
  280. package/.agents/schemas/story-perf-summary.schema.json +0 -73
  281. package/.agents/scripts/analyze-execution.js +0 -444
  282. package/.agents/scripts/check-prepush-recovery.js +0 -90
  283. package/.agents/scripts/lib/git-merge-orchestrator.js +0 -261
  284. package/.agents/scripts/lib/observability/baseline-refresh-rate.js +0 -221
  285. package/.agents/scripts/lib/observability/hook-heartbeat.js +0 -187
  286. package/.agents/scripts/lib/observability/perf-aggregator.js +0 -813
  287. package/.agents/scripts/lib/observability/perf-report-readers.js +0 -328
  288. package/.agents/scripts/lib/observability/perf-report-render.js +0 -182
  289. package/.agents/scripts/lib/orchestration/audit-lens-routing.js +0 -128
  290. package/.agents/scripts/lib/orchestration/bookkeeping-outbox.js +0 -273
  291. package/.agents/scripts/lib/orchestration/error-journal.js +0 -139
  292. package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +0 -155
  293. package/.agents/scripts/lib/orchestration/lifecycle/ledger-diff.js +0 -140
  294. package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +0 -665
  295. package/.agents/scripts/lib/orchestration/plan-review-routing.js +0 -63
  296. package/.agents/scripts/lib/orchestration/planning/risk-verdict.js +0 -104
  297. package/.agents/scripts/lib/orchestration/planning-context-budget.js +0 -213
  298. package/.agents/scripts/lib/orchestration/planning-risk.js +0 -194
  299. package/.agents/scripts/lib/orchestration/post-merge/phases/branch-cleanup.js +0 -56
  300. package/.agents/scripts/lib/orchestration/post-merge/phases/dashboard-refresh.js +0 -21
  301. package/.agents/scripts/lib/orchestration/post-merge/phases/notification.js +0 -78
  302. package/.agents/scripts/lib/orchestration/post-merge/phases/temp-cleanup.js +0 -68
  303. package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +0 -118
  304. package/.agents/scripts/lib/orchestration/post-merge/phases/worktree-reap.js +0 -397
  305. package/.agents/scripts/lib/orchestration/preflight-cache.js +0 -187
  306. package/.agents/scripts/lib/orchestration/resolve-plan-run.js +0 -155
  307. package/.agents/scripts/lib/orchestration/retro-perf-heuristics.js +0 -275
  308. package/.agents/scripts/lib/orchestration/story-progress/story-run-progress-writer.js +0 -400
  309. package/.agents/scripts/lib/single-story/confirm-merge-follow-ups.js +0 -36
  310. package/.agents/scripts/resolve-plan-run.js +0 -117
  311. package/.agents/skills/core/analyze-execution/SKILL.md +0 -101
@@ -0,0 +1,360 @@
1
+ /**
2
+ * story-deliver-terminal.js — the one terminal envelope every Story
3
+ * close-and-land invocation emits, and the shared next-command vocabulary
4
+ * (Story #4543).
5
+ *
6
+ * Before this module the delivery tail had **two** divergent return
7
+ * contracts, both prose and neither validated:
8
+ * `.agents/workflows/helpers/deliver-story.md` defined one shape and
9
+ * `.agents/agents/story-worker.md` a different one, so a caller could not
10
+ * distinguish a landed Story from a parked one without re-probing GitHub.
11
+ * `story-deliver-terminal.schema.json` is now the SSOT both docs reference
12
+ * rather than restate, and this module is its only writer.
13
+ *
14
+ * Two exports carry the contract:
15
+ *
16
+ * - {@link buildTerminalEnvelope} assembles and **validates** the envelope.
17
+ * It throws on a schema violation rather than emitting a malformed
18
+ * terminal: a silently-wrong terminal is exactly the failure this Story
19
+ * exists to eliminate, so failing loudly at the writer is the point.
20
+ * - {@link NEXT_COMMANDS} is the next-command vocabulary shared with
21
+ * `deliver-recover.js`, so a `pending` envelope and a recovery probe
22
+ * name the same command for the same state instead of inventing two
23
+ * dialects for one condition.
24
+ *
25
+ * `TERMINAL_EXIT_CODES` maps status → process exit code. `pending` gets its
26
+ * **own** code (3) precisely so a caller can tell "slow CI, resume me" from
27
+ * "hard block, come look" without parsing stdout — the distinction the
28
+ * pre-#4543 pipeline collapsed by treating budget exhaustion as a block.
29
+ */
30
+
31
+ import { readFileSync } from 'node:fs';
32
+ import path from 'node:path';
33
+ import { fileURLToPath } from 'node:url';
34
+
35
+ import Ajv2020 from 'ajv/dist/2020.js';
36
+ import addFormats from 'ajv-formats';
37
+
38
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
39
+ const SCHEMA_PATH = path.resolve(
40
+ __dirname,
41
+ '..',
42
+ '..',
43
+ '..',
44
+ 'schemas',
45
+ 'story-deliver-terminal.schema.json',
46
+ );
47
+
48
+ export const TERMINAL_ENVELOPE_KIND = 'story-deliver-terminal';
49
+
50
+ /**
51
+ * status → process exit code.
52
+ *
53
+ * `pending` is **3**, not 0 and not 1. Zero would tell a headless caller the
54
+ * Story landed when it did not; one would conflate a resumable slow-CI wait
55
+ * with a hard block and send the operator to diagnose branch protection that
56
+ * is working fine. A distinct code is what lets `/deliver` resume without
57
+ * classifying.
58
+ */
59
+ export const TERMINAL_EXIT_CODES = Object.freeze({
60
+ landed: 0,
61
+ pending: 3,
62
+ blocked: 1,
63
+ failed: 1,
64
+ });
65
+
66
+ export const TERMINAL_STATUSES = Object.freeze([
67
+ 'landed',
68
+ 'pending',
69
+ 'blocked',
70
+ 'failed',
71
+ ]);
72
+
73
+ /**
74
+ * The shared next-command vocabulary. Every producer of a "what now?"
75
+ * answer — the `pending` terminal envelope and `deliver-recover.js` —
76
+ * builds its command from here, so the two surfaces never drift into
77
+ * naming different commands for the same observed state.
78
+ */
79
+ export const NEXT_COMMANDS = Object.freeze({
80
+ /**
81
+ * Resume a bounded merge wait that expired with the PR still in flight.
82
+ *
83
+ * `--wait` is load-bearing: without it the confirm CLI probes once and
84
+ * answers `pending` again, so the cumulative-budget give-up never fires and
85
+ * a wedged PR is never escalated to anyone.
86
+ */
87
+ resumeLand: (storyId) =>
88
+ `node .agents/scripts/single-story-confirm-merge.js --story ${storyId} --wait`,
89
+ /** Confirm a merged-but-mislabelled Story (the idempotent flip + tail). */
90
+ confirmMerge: (storyId) =>
91
+ `node .agents/scripts/single-story-confirm-merge.js --story ${storyId}`,
92
+ /** Enter the red-CI fix loop against the failing PR. */
93
+ watchCi: (storyId, prNumber) =>
94
+ `node .agents/scripts/pr-watch-with-update.js --pr ${prNumber} --story ${storyId}`,
95
+ /** Re-run close for a Story whose PR was never opened. */
96
+ close: (storyId) =>
97
+ `node .agents/scripts/single-story-close.js --story ${storyId}`,
98
+ /** Resume implementation in the Story worktree. */
99
+ implement: (storyId) =>
100
+ `node .agents/scripts/single-story-init.js --story ${storyId}`,
101
+ /** Re-assert a drifted Projects v2 Status column. */
102
+ resync: (storyId) =>
103
+ `node .agents/scripts/resync-status-column.js --story ${storyId}`,
104
+ /** Probe a stranded Story and print its single next command. */
105
+ recover: (storyId) =>
106
+ `node .agents/scripts/deliver-recover.js --story ${storyId}`,
107
+ });
108
+
109
+ /** @type {Function|null} */
110
+ let _validator = null;
111
+
112
+ /**
113
+ * Compile (once) and return the terminal-envelope validator.
114
+ *
115
+ * @returns {Function}
116
+ */
117
+ function getValidator() {
118
+ if (_validator) return _validator;
119
+ const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
120
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
121
+ addFormats(ajv);
122
+ _validator = ajv.compile(schema);
123
+ return _validator;
124
+ }
125
+
126
+ /**
127
+ * Validate a candidate envelope against the shipped schema.
128
+ *
129
+ * @param {object} envelope
130
+ * @returns {{ valid: boolean, errors: string[] }}
131
+ */
132
+ export function validateTerminalEnvelope(envelope) {
133
+ const validate = getValidator();
134
+ const valid = validate(envelope);
135
+ if (valid) return { valid: true, errors: [] };
136
+ const errors = (validate.errors ?? []).map(
137
+ (e) => `${e.instancePath || '/'} ${e.message}`,
138
+ );
139
+ return { valid: false, errors };
140
+ }
141
+
142
+ /**
143
+ * Drop `undefined`-valued keys so the schema's `additionalProperties: false`
144
+ * and its nullable unions both stay satisfiable from one optional-argument
145
+ * builder signature.
146
+ *
147
+ * @param {object} obj
148
+ * @returns {object}
149
+ */
150
+ function compact(obj) {
151
+ const out = {};
152
+ for (const [k, v] of Object.entries(obj)) {
153
+ if (v !== undefined) out[k] = v;
154
+ }
155
+ return out;
156
+ }
157
+
158
+ /**
159
+ * Assemble the terminal envelope and validate it before returning.
160
+ *
161
+ * Throws a `TypeError` naming the schema violations when the assembled
162
+ * object does not validate. That is deliberate: the whole point of the
163
+ * envelope is that a caller can trust its status without re-probing
164
+ * GitHub, so emitting an unvalidated one would reintroduce the ambiguity
165
+ * this replaces.
166
+ *
167
+ * @param {object} args
168
+ * @param {number} args.storyId
169
+ * @param {'landed'|'pending'|'blocked'|'failed'} args.status
170
+ * @param {string} args.phase
171
+ * @param {string} [args.storyBranch]
172
+ * @param {string} [args.baseBranch]
173
+ * @param {object|null} [args.pr]
174
+ * @param {object} [args.gates]
175
+ * @param {object|null} [args.tail]
176
+ * @param {object|null} [args.blocked]
177
+ * @param {object|null} [args.failure]
178
+ * @param {string|null} [args.nextCommand]
179
+ * @param {number} args.elapsedSeconds
180
+ * @param {object|null} [args.waitBudget]
181
+ * @param {string} [args.timestamp]
182
+ * @returns {object} The validated envelope.
183
+ */
184
+ export function buildTerminalEnvelope({
185
+ storyId,
186
+ status,
187
+ phase,
188
+ storyBranch,
189
+ baseBranch,
190
+ pr,
191
+ gates,
192
+ tail,
193
+ blocked,
194
+ failure,
195
+ nextCommand,
196
+ elapsedSeconds = 0,
197
+ waitBudget,
198
+ timestamp = new Date().toISOString(),
199
+ }) {
200
+ const envelope = compact({
201
+ kind: TERMINAL_ENVELOPE_KIND,
202
+ storyId: Number(storyId),
203
+ status,
204
+ phase,
205
+ storyBranch: storyBranch ?? null,
206
+ baseBranch: baseBranch ?? null,
207
+ pr: pr ?? null,
208
+ gates,
209
+ tail: tail ?? null,
210
+ blocked: blocked ?? null,
211
+ failure: failure ?? null,
212
+ nextCommand: nextCommand ?? null,
213
+ elapsedSeconds: Math.max(0, Number(elapsedSeconds) || 0),
214
+ waitBudget: waitBudget ?? null,
215
+ timestamp,
216
+ });
217
+
218
+ const { valid, errors } = validateTerminalEnvelope(envelope);
219
+ if (!valid) {
220
+ throw new TypeError(
221
+ `buildTerminalEnvelope: assembled envelope violates story-deliver-terminal.schema.json:\n` +
222
+ errors.map((e) => ` - ${e}`).join('\n'),
223
+ );
224
+ }
225
+ return envelope;
226
+ }
227
+
228
+ /**
229
+ * Resolve the process exit code for a terminal envelope.
230
+ *
231
+ * @param {object} envelope
232
+ * @returns {number}
233
+ */
234
+ export function exitCodeForTerminal(envelope) {
235
+ return TERMINAL_EXIT_CODES[envelope?.status] ?? 1;
236
+ }
237
+
238
+ /** The markers a caller scans stdout for to recover the envelope. */
239
+ export const TERMINAL_BEGIN_MARKER = '--- STORY DELIVER TERMINAL ---';
240
+ export const TERMINAL_END_MARKER = '--- END TERMINAL ---';
241
+
242
+ /**
243
+ * Write a terminal envelope to stdout, between its markers.
244
+ *
245
+ * **Deliberately not `Logger.info`.** The envelope is this CLI's
246
+ * machine-readable contract — every invocation emits exactly ONE, and a
247
+ * headless caller parses it out of stdout to decide what happened.
248
+ * `Logger.info` is level-gated, so under the documented
249
+ * `AGENT_LOG_LEVEL=silent` (§ 1.H) the envelope silently vanished and the
250
+ * caller got a bare exit code: precisely the "no envelope at all" outcome
251
+ * Story #4543 exists to remove. A contract payload must not be suppressible
252
+ * by a verbosity knob.
253
+ *
254
+ * Single home for the marker format so the four emit sites (the runner's
255
+ * terminal, the close CLI's failed-terminal catch, and both confirm-CLI
256
+ * paths) cannot drift apart.
257
+ *
258
+ * @param {object} envelope
259
+ * @param {{ write?: (s: string) => void }} [opts] `write` is a test seam.
260
+ * @returns {void}
261
+ */
262
+ export function emitTerminalEnvelope(
263
+ envelope,
264
+ { write = (s) => process.stdout.write(s) } = {},
265
+ ) {
266
+ write(
267
+ `\n${TERMINAL_BEGIN_MARKER}\n${JSON.stringify(envelope, null, 2)}\n${TERMINAL_END_MARKER}\n`,
268
+ );
269
+ }
270
+
271
+ /**
272
+ * Map a `runConfirmMergePhase` outcome onto the schema-validated terminal
273
+ * envelope (Story #4543). One writer, one shape — the two prose contracts
274
+ * this replaces disagreed with each other precisely because each surface
275
+ * assembled its own.
276
+ *
277
+ * @returns {object} A validated `story-deliver-terminal` envelope.
278
+ */
279
+ export function terminalFromWaitOutcome({
280
+ waitOutcome,
281
+ storyId,
282
+ storyBranch,
283
+ baseBranch,
284
+ prNumber,
285
+ prUrl,
286
+ autoMergeEnabled,
287
+ gates,
288
+ elapsedSeconds,
289
+ }) {
290
+ const prBase = {
291
+ number: prNumber,
292
+ url: prUrl ?? null,
293
+ autoMergeEnabled: Boolean(autoMergeEnabled),
294
+ };
295
+ const common = {
296
+ storyId,
297
+ storyBranch,
298
+ baseBranch,
299
+ gates,
300
+ elapsedSeconds,
301
+ };
302
+
303
+ if (waitOutcome.terminal === 'landed') {
304
+ return buildTerminalEnvelope({
305
+ ...common,
306
+ status: 'landed',
307
+ phase: 'post-land',
308
+ pr: {
309
+ ...prBase,
310
+ state: 'MERGED',
311
+ // The observed rollup, not an assumed 'success' — a merge can land by
312
+ // admin override or with non-required checks red.
313
+ checksStatus: waitOutcome.prProbe?.checksStatus ?? null,
314
+ },
315
+ tail: waitOutcome.tail,
316
+ nextCommand: null,
317
+ });
318
+ }
319
+
320
+ if (waitOutcome.terminal === 'pending') {
321
+ return buildTerminalEnvelope({
322
+ ...common,
323
+ status: 'pending',
324
+ phase: 'confirm-merge',
325
+ pr: {
326
+ ...prBase,
327
+ state: waitOutcome.prProbe?.state ?? 'OPEN',
328
+ checksStatus: waitOutcome.prProbe?.checksStatus ?? null,
329
+ },
330
+ waitBudget: waitOutcome.waitBudget,
331
+ nextCommand: NEXT_COMMANDS.resumeLand(storyId),
332
+ });
333
+ }
334
+
335
+ // blocked — the classifier already named the class and the friction
336
+ // comment already carries the class-specific remediation, so the next
337
+ // command mirrors it rather than inventing a second opinion.
338
+ const nextCommand =
339
+ waitOutcome.blockClass === 'checks-failed'
340
+ ? NEXT_COMMANDS.watchCi(storyId, prNumber)
341
+ : waitOutcome.blockClass === 'merged-flip-failed'
342
+ ? NEXT_COMMANDS.confirmMerge(storyId)
343
+ : NEXT_COMMANDS.recover(storyId);
344
+ return buildTerminalEnvelope({
345
+ ...common,
346
+ status: 'blocked',
347
+ phase: 'confirm-merge',
348
+ pr: {
349
+ ...prBase,
350
+ state: waitOutcome.prProbe?.state ?? null,
351
+ checksStatus: waitOutcome.prProbe?.checksStatus ?? null,
352
+ },
353
+ blocked: {
354
+ blockClass: waitOutcome.blockClass,
355
+ reason: waitOutcome.reason,
356
+ frictionCommentId: waitOutcome.frictionCommentId ?? null,
357
+ },
358
+ nextCommand,
359
+ });
360
+ }
@@ -72,15 +72,65 @@ export async function gatherStoryFrictionSignals(storyId, config) {
72
72
  return signals;
73
73
  }
74
74
 
75
+ /**
76
+ * Render the empty-roll-up line.
77
+ *
78
+ * Story #4578 — an empty roll-up over a multi-Story run must NOT read as
79
+ * success. The pre-#4578 text ("No friction signals — nothing to follow up")
80
+ * was *truthful* about the stream and *false* about the run: a 7-Story
81
+ * delivery containing a mid-run git outage, a parked worker, and a
82
+ * four-round acceptance critic rendered byte-identically to a genuinely
83
+ * clean run. An operator cannot tell "nothing went wrong" from "the
84
+ * telemetry never fired", and the second is likeliest exactly when the run
85
+ * went worst.
86
+ *
87
+ * So the line is a function of `storyCount`:
88
+ * - `storyCount <= 1` → the honest, quiet reading is retained. A single
89
+ * Story that emitted nothing plausibly *was* clean, and crying wolf on
90
+ * every clean Story is how a warning channel gets tuned out.
91
+ * - `storyCount > 1` → zero signals across N Stories is a **claim**, and
92
+ * the surrounding text says so and names the two readings, rather than
93
+ * asserting the flattering one.
94
+ *
95
+ * This mirrors the sibling precedent in `run-epilogue.js`'s
96
+ * `renderDiffLines`, which refuses to let an unresolvable base diff render
97
+ * as "0 changed files".
98
+ *
99
+ * @param {number} storyCount
100
+ * @returns {string[]}
101
+ */
102
+ function renderEmptyRollupLines(storyCount) {
103
+ if (storyCount <= 1) {
104
+ return ['_No friction signals — nothing to follow up._'];
105
+ }
106
+ return [
107
+ `> ⚠️ **0 friction signals across ${storyCount} Stories — this is a claim, not a clean bill of health.**`,
108
+ '> Either the run was genuinely friction-free, or telemetry never fired.',
109
+ '> An empty stream is indistinguishable from a clean run, and it is least',
110
+ '> likely to fill exactly when a run is going badly and the agent is busy.',
111
+ '> The runtime emits friction from its own observables (`agent::blocked`',
112
+ '> transitions, failed closes, exhausted merge waits) — so zero here also',
113
+ '> means none of those fired. If the run had friction you can name, that',
114
+ '> gap is itself the follow-up worth filing.',
115
+ ];
116
+ }
117
+
75
118
  /**
76
119
  * @param {{
77
120
  * storyId: number,
78
121
  * proposals: object,
79
122
  * graduated: object,
80
- * }} args
123
+ * storyCount?: number,
124
+ * }} args - `storyCount` (default 1) is how many Stories the roll-up spans;
125
+ * it decides whether an empty result reads as quiet or as a flagged claim.
81
126
  * @returns {string}
82
127
  */
83
- export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
128
+ export function buildFollowUpsCommentBody({
129
+ storyId,
130
+ proposals,
131
+ graduated,
132
+ storyCount = 1,
133
+ }) {
84
134
  const filed = Array.isArray(graduated?.filed) ? graduated.filed : [];
85
135
  const framework = proposals?.framework ?? [];
86
136
  const consumer = proposals?.consumer ?? [];
@@ -124,7 +174,7 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
124
174
  consumer.length === 0 &&
125
175
  discarded.length === 0
126
176
  ) {
127
- lines.push('_No friction signals — nothing to follow up._');
177
+ lines.push(...renderEmptyRollupLines(storyCount));
128
178
  lines.push('');
129
179
  }
130
180
  lines.push('```json');
@@ -132,6 +182,7 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
132
182
  JSON.stringify(
133
183
  {
134
184
  storyId,
185
+ storyCount,
135
186
  framework: framework.map((i) => i.category),
136
187
  consumer: consumer.map((i) => i.category),
137
188
  discarded: discarded.map((i) => i.category),
@@ -139,6 +190,15 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
139
190
  category: i.category,
140
191
  url: i.url ?? null,
141
192
  })),
193
+ // Story #4578 — an empty roll-up over N>1 Stories is a claim worth
194
+ // flagging, not a success. Machine-readable twin of the warning
195
+ // prose so a caller need not regex the body.
196
+ emptyRollupSuspect:
197
+ storyCount > 1 &&
198
+ filed.length === 0 &&
199
+ framework.length === 0 &&
200
+ consumer.length === 0 &&
201
+ discarded.length === 0,
142
202
  },
143
203
  null,
144
204
  2,
@@ -149,8 +209,18 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
149
209
  }
150
210
 
151
211
  /**
152
- * Capture and persist Story follow-ups. Never throws — close must not fail
153
- * because follow-up filing flaked.
212
+ * Capture and persist Story follow-ups. Never throws — the land must not
213
+ * fail because follow-up filing flaked.
214
+ *
215
+ * Story #4543 retired the `captureFollowUpsAfterConfirm` action-gate wrapper
216
+ * (and its `withConfirmFollowUps` sibling) that used to front this function.
217
+ * Re-deriving "did the merge land?" from a confirmation envelope's `action`
218
+ * field was the coupling that made close-and-land — the DEFAULT path — skip
219
+ * capture entirely: the gate only opened on the standalone CLI's `done`, and
220
+ * a belated manual confirm could not backfill because the Story was already
221
+ * `agent::done` (confirm returns `noop`, the gate never opens). The shared
222
+ * land tail (`single-story-close/phases/post-land.js`) now calls this
223
+ * directly, after the merge is already confirmed.
154
224
  *
155
225
  * @param {object} args
156
226
  * @param {number} args.storyId
@@ -160,15 +230,6 @@ export function buildFollowUpsCommentBody({ storyId, proposals, graduated }) {
160
230
  * @param {(tag: string, msg: string) => void} [args.progress]
161
231
  * @returns {Promise<object>}
162
232
  */
163
- /**
164
- * Capture follow-ups only when merge confirm landed (`action === 'done'`).
165
- * One-liner seam for the confirm-merge CLI.
166
- */
167
- export async function captureFollowUpsAfterConfirm(confirmation, ctx) {
168
- if (confirmation?.action !== 'done') return null;
169
- return captureStoryFollowUps(ctx);
170
- }
171
-
172
233
  export async function captureStoryFollowUps({
173
234
  storyId,
174
235
  provider,
@@ -1,6 +1,9 @@
1
1
  import { Logger } from '../Logger.js';
2
- import { AGENT_LABELS } from '../label-constants.js';
3
- import { upsertStructuredComment } from './ticketing.js';
2
+ import {
3
+ STATE_LABELS,
4
+ transitionTicketState,
5
+ upsertStructuredComment,
6
+ } from './ticketing.js';
4
7
 
5
8
  /**
6
9
  * Fail closed when the repository remote cannot be verified. The Story is
@@ -30,13 +33,14 @@ export async function handleRemoteVerificationFailure({
30
33
  `[single-story-init] failed to post remote-verification friction: ${err?.message ?? err}`,
31
34
  );
32
35
  }
36
+ // Story #4539 — the canonical mutator. This is the path where the
37
+ // skipped Projects v2 column sync (Story #2548) visibly drifts: the
38
+ // Story is still agent::ready (To Do) when the remote probe fails, so
39
+ // a direct label write leaves the board reading To Do for a blocked
40
+ // Story. single-story-init.js's own comment explains exactly why this
41
+ // must not bypass the mutator.
33
42
  try {
34
- await provider.updateTicket(storyId, {
35
- labels: {
36
- add: [AGENT_LABELS.BLOCKED],
37
- remove: [AGENT_LABELS.READY, AGENT_LABELS.EXECUTING],
38
- },
39
- });
43
+ await transitionTicketState(provider, storyId, STATE_LABELS.BLOCKED, {});
40
44
  } catch (err) {
41
45
  Logger.warn(
42
46
  `[single-story-init] failed to block Story after remote verification: ${err?.message ?? err}`,
@@ -1,9 +1,22 @@
1
+ /**
2
+ * story-plan-state.js — read the v2 Story planning checkpoint.
3
+ *
4
+ * `plan-persist.js` upserts one `story-plan-state` structured comment per
5
+ * created Story carrying the persist receipt (when the plan completed, how many
6
+ * Stories it created, and their ids). Story #4542 removed the risk fields it
7
+ * used to carry: the planner-authored verdict, the envelope derived from it,
8
+ * and the review routing computed from that envelope. Nothing read any of them
9
+ * back — review depth is now derived from the diff at close time
10
+ * (`review-depth.js#deriveChangeLevel`), so no checkpoint read sits on the
11
+ * delivery path at all.
12
+ */
13
+
1
14
  import { parseFencedJsonComment } from './structured-comment-parser.js';
2
15
  import { findStructuredComment } from './ticketing.js';
3
16
 
4
17
  /**
5
18
  * Read the v2 Story planning checkpoint. Missing/malformed comments degrade
6
- * to null so unplanned Stories still receive the neutral review posture.
19
+ * to null an unplanned Story simply has no persist receipt.
7
20
  */
8
21
  export async function readStoryPlanState({
9
22
  provider,
@@ -18,31 +31,3 @@ export async function readStoryPlanState({
18
31
  const state = parseFencedJsonComment(comment);
19
32
  return state && typeof state === 'object' ? state : null;
20
33
  }
21
-
22
- export async function readStoryPlanningRisk(args) {
23
- const state = await readStoryPlanState(args);
24
- return state?.planningRisk ?? null;
25
- }
26
-
27
- export async function readStoryPlanningRiskSafe(args) {
28
- try {
29
- return await readStoryPlanningRisk(args);
30
- } catch {
31
- return null;
32
- }
33
- }
34
-
35
- /**
36
- * Prefer an explicit `planningRisk` override; otherwise load the Story
37
- * checkpoint. Callers that omit the field (or pass `undefined`) get the
38
- * persisted plan risk; callers that pass `null` keep the neutral posture.
39
- */
40
- export async function resolveStoryPlanningRisk({
41
- provider,
42
- storyId,
43
- planningRisk,
44
- findCommentFn,
45
- }) {
46
- if (planningRisk !== undefined) return planningRisk;
47
- return readStoryPlanningRiskSafe({ provider, storyId, findCommentFn });
48
- }
@@ -24,6 +24,15 @@
24
24
  * name at least one path-shaped token so vague verbs ("clean up",
25
25
  * "refactor") can't slip through.
26
26
  *
27
+ * `acceptance` / `verify` are the **top-level machine contract** (Story
28
+ * #4541). The decomposer prompt tells authors to write those lists once at
29
+ * the ticket's top level and omit the matching body sections; persist syncs
30
+ * them into the body at assemble time. Validation runs *before* that sync,
31
+ * so this validator resolves each contract field from the parsed body and
32
+ * falls back to the ticket's top-level array when the body section is
33
+ * absent. Without that fallback the validator rejected the very shape its
34
+ * own prompt prescribes.
35
+ *
27
36
  * `body.changes` items must be object-form `{ path: string, assumption: enum }`
28
37
  * entries (Story #2636 shape). Plain string bullets are rejected at parse
29
38
  * time and by this validator.
@@ -51,8 +60,8 @@ import { FILE_ASSUMPTION_VALUES } from './file-assumption-enum.js';
51
60
 
52
61
  /**
53
62
  * Canonical testing-tier labels that a `verify[]` entry must name (in
54
- * parentheses) to pass plan-time validation. Mirrors the skill contract in
55
- * `core/epic-plan-decompose-author/SKILL.md § verify rules`.
63
+ * parentheses) to pass plan-time validation. Mirrors the verify-rules contract
64
+ * in `.agents/scripts/lib/templates/decomposer-prompts.js`.
56
65
  *
57
66
  * Entries that do not end with `(<tier>)` and are not `manual:<reason>` are
58
67
  * rejected by `collectVerifyErrors`.
@@ -164,6 +173,39 @@ function resolveStructuredBody(ticket) {
164
173
  }
165
174
  }
166
175
 
176
+ /**
177
+ * The two contract fields that live at the ticket's top level and are
178
+ * synced into the body by `plan-persist` at assemble time.
179
+ */
180
+ const CONTRACT_FIELDS = Object.freeze(['acceptance', 'verify']);
181
+
182
+ /**
183
+ * Resolve the body's contract fields against the ticket's top-level arrays
184
+ * (Story #4541). The decomposer prompt prescribes authoring `acceptance[]`
185
+ * / `verify[]` **once** at top level and omitting the matching body
186
+ * sections; `assemblePlanStories#syncContractFieldFromTopLevel` performs
187
+ * the sync, but it runs *after* validation. So an absent body section is
188
+ * not a violation when the ticket carries the list at top level — it is the
189
+ * preferred shape. A body section that is present and disagrees with the
190
+ * top level is left alone here: the sync itself fails closed on that
191
+ * mismatch, and duplicating the check would report it twice.
192
+ *
193
+ * @param {object} ticket
194
+ * @param {object} bodyObject Parsed / structured body.
195
+ * @returns {object} A copy of `bodyObject` with the contract fields resolved.
196
+ */
197
+ function resolveContractFieldsFromTopLevel(ticket, bodyObject) {
198
+ const resolved = { ...bodyObject };
199
+ for (const field of CONTRACT_FIELDS) {
200
+ const bodyValue = Array.isArray(resolved[field]) ? resolved[field] : [];
201
+ if (bodyValue.length > 0) continue;
202
+ const topLevel = Array.isArray(ticket?.[field]) ? ticket[field] : [];
203
+ if (topLevel.length === 0) continue;
204
+ resolved[field] = topLevel.map(String);
205
+ }
206
+ return resolved;
207
+ }
208
+
167
209
  /**
168
210
  * Validate one Story body and return every violation it exhibits. Empty
169
211
  * array means clean. Splits the per-ticket cascade out of
@@ -182,13 +224,14 @@ function resolveStructuredBody(ticket) {
182
224
  */
183
225
  export function validateTaskBodyShape(ticket) {
184
226
  const prefix = `Story "${ticket.title}" (${ticket.slug})`;
185
- const { body, error } = resolveStructuredBody(ticket);
227
+ const { body: parsed, error } = resolveStructuredBody(ticket);
186
228
  if (error !== null) {
187
229
  return [error];
188
230
  }
189
- if (body === null || typeof body !== 'object') {
190
- return [`${prefix}: body must be an object, got ${typeof body}.`];
231
+ if (parsed === null || typeof parsed !== 'object') {
232
+ return [`${prefix}: body must be an object, got ${typeof parsed}.`];
191
233
  }
234
+ const body = resolveContractFieldsFromTopLevel(ticket, parsed);
192
235
  const errors = [];
193
236
  if (typeof body.goal !== 'string' || body.goal.trim() === '') {
194
237
  errors.push(`${prefix}: body.goal must be a non-empty string.`);
@@ -300,7 +343,9 @@ function collectReferencesErrors(prefix, rawReferences) {
300
343
  function collectAcceptanceErrors(prefix, rawAcceptance) {
301
344
  const acceptance = Array.isArray(rawAcceptance) ? rawAcceptance : [];
302
345
  if (acceptance.length === 0) {
303
- return [`${prefix}: body.acceptance must list at least one criterion.`];
346
+ return [
347
+ `${prefix}: acceptance must list at least one criterion — author it at the ticket's top level (preferred) or in the body's ## Acceptance section.`,
348
+ ];
304
349
  }
305
350
  return [];
306
351
  }
@@ -323,7 +368,7 @@ function collectVerifyErrors(prefix, rawVerify) {
323
368
  const verify = Array.isArray(rawVerify) ? rawVerify : [];
324
369
  if (verify.length === 0) {
325
370
  return [
326
- `${prefix}: body.verify must list at least one entry. Use "manual:<reason>" only when truly unverifiable in isolation.`,
371
+ `${prefix}: verify must list at least one entry — author it at the ticket's top level (preferred) or in the body's ## Verify section. Use "manual:<reason>" only when truly unverifiable in isolation.`,
327
372
  ];
328
373
  }
329
374
  const errors = [];