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
@@ -41,7 +41,13 @@ export async function flipLabelAndNotify({
41
41
  config,
42
42
  progress,
43
43
  }) {
44
- const labelFlipped = await flipLabel(provider, storyId, story, progress);
44
+ const labelFlipped = await flipLabel(
45
+ provider,
46
+ storyId,
47
+ story,
48
+ progress,
49
+ config,
50
+ );
45
51
  if (!labelFlipped) return;
46
52
  await fireStoryClosingNotify({
47
53
  notifyFn: notifyFn ?? defaultNotify,
@@ -55,7 +61,7 @@ export async function flipLabelAndNotify({
55
61
  });
56
62
  }
57
63
 
58
- async function flipLabel(provider, storyId, story, progress) {
64
+ async function flipLabel(provider, storyId, story, progress, config) {
59
65
  try {
60
66
  // Story #3385 — flip to `agent::closing`, NOT `agent::done`. The
61
67
  // canonical mutator (`transitionTicketState`) only closes the GitHub
@@ -81,8 +87,13 @@ async function flipLabel(provider, storyId, story, progress) {
81
87
  // notification that `transitionTicketState` would dispatch is
82
88
  // redundant with the typed `story-closing` event that
83
89
  // `fireStoryClosingNotify` emits immediately afterwards.
90
+ //
91
+ // `config` is threaded so the on-disk board-metadata cache (issue #4555)
92
+ // lands under the project's configured `tempRoot` rather than the
93
+ // framework default, matching every other transition call site.
84
94
  await transitionTicketState(provider, storyId, STATE_LABELS.CLOSING, {
85
95
  ticketSnapshot: story,
96
+ config,
86
97
  });
87
98
  progress?.('LABELS', `🏷️ Story #${storyId} → agent::closing`);
88
99
  return true;
@@ -30,6 +30,8 @@ const FRONTMATTER_DELIMITER = '---';
30
30
  const POLICY_HEADING_RE = /^## Policy Capsule\s*$/;
31
31
  const ANY_H2_RE = /^## /;
32
32
  const BULLET_RE = /^- /;
33
+ /** An indented (wrapped) continuation of the bullet above it. */
34
+ const CONTINUATION_RE = /^\s+\S/;
33
35
 
34
36
  /**
35
37
  * Repo root resolver. Walks up from the SKILL.md path until we find a
@@ -85,8 +87,14 @@ function extractFrontmatterBlock(lines, skillPath) {
85
87
  /**
86
88
  * Scan the body for the '## Policy Capsule' heading and count contiguous
87
89
  * top-level '- ' bullets that follow it, stopping at the next '## ' heading
88
- * or end-of-file. Blank lines inside the bullet run do not reset the count;
89
- * a non-bullet, non-blank line does.
90
+ * or end-of-file. Blank lines inside the bullet run do not reset the count.
91
+ *
92
+ * An **indented** continuation line belongs to the bullet above it — capsule
93
+ * bullets routinely wrap at the document's prose width — so it neither counts
94
+ * as a new bullet nor terminates the run. Only a **flush-left**, non-blank,
95
+ * non-bullet line after the run terminates it (Story #4546: treating any
96
+ * non-bullet line as a terminator counted a wrapped 5-bullet capsule as 1 and
97
+ * tripped the floor in validate-skills.js).
90
98
  */
91
99
  function findPolicyCapsule(lines, bodyStart) {
92
100
  let headingIndex = -1;
@@ -114,8 +122,13 @@ function findPolicyCapsule(lines, bodyStart) {
114
122
  // Blank lines inside or before the bullet run are tolerated.
115
123
  continue;
116
124
  }
125
+ if (sawBulletRun && CONTINUATION_RE.test(line)) {
126
+ // Indented continuation of the bullet above — same bullet, not a
127
+ // terminator. Wrapped capsule bullets are the norm, not an error.
128
+ continue;
129
+ }
117
130
  if (sawBulletRun) {
118
- // Non-blank, non-bullet line after the bullet run terminates it.
131
+ // Flush-left, non-blank, non-bullet line after the run terminates it.
119
132
  break;
120
133
  }
121
134
  // Leading prose between heading and the first bullet is allowed.
@@ -42,16 +42,17 @@ import { parseBlockedBy } from './dependency-parser.js';
42
42
  * Story records (live ticket payloads, fixture tickets, or operator
43
43
  * DAG nodes).
44
44
  * @param {object} [opts]
45
- * @param {boolean} [opts.dropForeign=true] When true (the default,
46
- * matching pre-v2 Epic-scoped wrappers), edges pointing at ids outside
45
+ * @param {boolean} [opts.dropForeign=false] When `false` (the v2 default,
46
+ * matching the `/deliver` path `stories-wave-tick.js` and the
47
+ * `selectReadySet` core), the operator-DAG contract is preserved: a
48
+ * dependency on an id absent from the input is treated as not-yet-done
49
+ * and withholds the dependent until it completes. When `true` (the
50
+ * pre-v2 Epic-scoped-wrapper semantics), edges pointing at ids outside
47
51
  * the supplied story set are dropped so the DAG stays closed over the
48
- * scheduled set. The v2 `/deliver` path (`stories-wave-tick.js` and the
49
- * `selectReadySet` core) passes `false` to preserve the operator-DAG
50
- * contract, where a dependency on an id absent from the input is treated
51
- * as not-yet-done and withholds the dependent until it completes.
52
+ * scheduled set.
52
53
  * @returns {Map<number, number[]>}
53
54
  */
54
- export function buildStoryAdjacency(stories, { dropForeign = true } = {}) {
55
+ export function buildStoryAdjacency(stories, { dropForeign = false } = {}) {
55
56
  const records = Array.isArray(stories) ? stories : [];
56
57
  const storyIds = new Set(records.map((s) => Number(s?.id ?? s?.number)));
57
58
  const adjacency = new Map();
@@ -105,10 +105,9 @@ import { FILE_ASSUMPTION_VALUES } from '../orchestration/file-assumption-enum.js
105
105
 
106
106
  /**
107
107
  * @typedef {object} SerializeOptions
108
- * @property {boolean} [includeFooter=false] - Include `---\nparent/epic/blocked-by` footer.
108
+ * @property {boolean} [includeFooter=false] - Include `---\nparent/blocked-by` footer.
109
109
  * @property {object} [footer] - Footer fields when `includeFooter` is true.
110
110
  * @property {number} [footer.parent] - Parent feature issue number.
111
- * @property {number} [footer.epic] - Epic issue number.
112
111
  */
113
112
 
114
113
  // ---------------------------------------------------------------------------
@@ -166,13 +165,35 @@ function stripListMarker(line) {
166
165
  return line.replace(/^-\s+(?:\[\s*[xX ]?\s*\]\s+)?/, '').trim();
167
166
  }
168
167
 
168
+ // Humanized PathEntry bullet (Story #4600): `path` — assumption. This is the
169
+ // shape serialize() now emits for `## Changes` / `## References`; the legacy
170
+ // inline-JSON object bullet remains accepted at parse time indefinitely (live
171
+ // issue bodies are never rewritten).
172
+ const HUMANIZED_PATH_ENTRY_RE = /^`([^`]+)`\s+—\s+(\S+)$/;
173
+
174
+ // AC-<n> presentation prefix on acceptance checkboxes (Story #4600). The
175
+ // numbering is a stable 1-based human handle only — parse() strips it so the
176
+ // top-level acceptance[] machine contract round-trips byte-identical.
177
+ const AC_PREFIX_RE = /^AC-\d+:\s+/;
178
+
179
+ // Visible wide-rationale line (Story #4600): `> **Wide:** <reason>` rendered
180
+ // under `## Goal`. Presentation only — the `<!-- meta -->` block stays the
181
+ // canonical machine carrier, so the parser skips this line wherever it
182
+ // appears (same treatment as the authored-provenance marker).
183
+ const WIDE_MARKER_LINE_RE = /^>\s*\*\*Wide:\*\*/;
184
+
169
185
  /**
170
186
  * Parse a single `changes` / `references` bullet into a `PathEntry`.
171
187
  *
172
- * Object form: `{ path: "...", assumption: "creates" }` (stored as
173
- * `- { "path": "...", "assumption": "..." }` or just recognized from the
174
- * structured body directly when deserializing from a structured object
175
- * that was never serialized to markdown, the entry arrives as-is).
188
+ * Accepted markdown shapes (both parsed indefinitely live issue bodies
189
+ * are never rewritten):
190
+ * - Humanized bullet (canonical serialize() output since Story #4600):
191
+ * `` `src/x.js` refactors-existing ``
192
+ * - Legacy inline-JSON object bullet:
193
+ * `- { "path": "...", "assumption": "..." }`
194
+ *
195
+ * A structured object entry (from a parsed JSON body that was never
196
+ * serialized to markdown) arrives as-is and is validated directly.
176
197
  *
177
198
  * @param {string|object} raw
178
199
  * @param {string[]} warnings
@@ -198,6 +219,21 @@ function parsePathEntry(raw, warnings) {
198
219
  const str = typeof raw === 'string' ? raw.trim() : String(raw).trim();
199
220
  if (str.length === 0) return null;
200
221
 
222
+ // Humanized bullet shape (the canonical serialize() output since
223
+ // Story #4600): `path` — assumption.
224
+ const humanized = str.match(HUMANIZED_PATH_ENTRY_RE);
225
+ if (humanized) {
226
+ const path = humanized[1].trim();
227
+ if (path.length > 0 && FILE_ASSUMPTION_VALUES.includes(humanized[2])) {
228
+ return { path, assumption: humanized[2] };
229
+ }
230
+ // Recognized the humanized shape but the fields are invalid: fail closed.
231
+ throw new StoryBodyParseError(
232
+ `changes/references entry is a humanized bullet but not a valid PathEntry: ${str.slice(0, 120)}`,
233
+ { field: 'changes', raw: str },
234
+ );
235
+ }
236
+
201
237
  // Try to detect inline JSON object shape: `{ "path": "...", "assumption": "..." }`
202
238
  if (str.startsWith('{')) {
203
239
  try {
@@ -434,6 +470,14 @@ function splitSections(markdown) {
434
470
  continue;
435
471
  }
436
472
 
473
+ // The visible `> **Wide:** <reason>` rationale line is presentation only
474
+ // (Story #4600): the meta block remains the canonical carrier for
475
+ // `wide.reason`, so this line must not bleed into the goal (or any other)
476
+ // section. Skip it wherever it appears.
477
+ if (WIDE_MARKER_LINE_RE.test(line)) {
478
+ continue;
479
+ }
480
+
437
481
  if (inPreamble) {
438
482
  preambleLines.push(line);
439
483
  } else if (currentSection !== null) {
@@ -638,7 +682,11 @@ export function parse(input) {
638
682
  sections.get('changes') ?? [],
639
683
  warnings,
640
684
  );
641
- const acceptance = parseTextListSection(sections.get('acceptance') ?? []);
685
+ // The AC-<n> checkbox prefix is presentation-only (Story #4600): strip it
686
+ // so acceptance[] round-trips byte-identical to the authored array.
687
+ const acceptance = parseTextListSection(sections.get('acceptance') ?? []).map(
688
+ (a) => a.replace(AC_PREFIX_RE, ''),
689
+ );
642
690
  const verify = parseTextListSection(sections.get('verify') ?? []);
643
691
  const references = parsePathEntrySection(
644
692
  sections.get('references') ?? [],
@@ -821,8 +869,10 @@ function parseStructuredObject(obj) {
821
869
  */
822
870
  function serializePathEntry(entry) {
823
871
  if (typeof entry === 'string') return entry;
824
- // Canonical object form: render as JSON inline for round-trip fidelity.
825
- return JSON.stringify({ path: entry.path, assumption: entry.assumption });
872
+ // Canonical object form (Story #4600): render as a human-readable bullet
873
+ // path in backticks, em-dash, assumption. parsePathEntry recognizes this
874
+ // shape (and the legacy inline-JSON shape) for round-trip fidelity.
875
+ return `\`${entry.path}\` — ${entry.assumption}`;
826
876
  }
827
877
 
828
878
  /**
@@ -846,6 +896,18 @@ const SERIALIZE_SECTIONS = [
846
896
  ? `## Goal\n${goal.trim()}`
847
897
  : null,
848
898
  },
899
+ {
900
+ // Visible wide-rationale line (Story #4600), rendered directly under
901
+ // `## Goal`. Presentation only: the `<!-- meta -->` block remains the
902
+ // canonical machine carrier and the parser skips this line, so `wide`
903
+ // round-trips through the meta block alone. Absent/invalid wide emits
904
+ // nothing, keeping every non-wide body byte-identical to before.
905
+ field: 'wide',
906
+ render: (wide) => {
907
+ const normalized = normalizeWide(wide);
908
+ return normalized === null ? null : `> **Wide:** ${normalized.reason}`;
909
+ },
910
+ },
849
911
  {
850
912
  // Optional v2 intra-Story delivery slice plan. Single-token `## Slicing`
851
913
  // heading (recognized by the `[\w-]+` field-heading regex). Verbatim text
@@ -874,10 +936,14 @@ const SERIALIZE_SECTIONS = [
874
936
  : null,
875
937
  },
876
938
  {
939
+ // Each checkbox carries a stable 1-based `AC-<n>:` handle (Story #4600)
940
+ // so humans and reviewers can reference criteria by number. The prefix
941
+ // is presentation-only — parse() strips it, and validators compare the
942
+ // top-level acceptance[] array, so numbering never affects gating.
877
943
  field: 'acceptance',
878
944
  render: (acceptance) =>
879
945
  Array.isArray(acceptance) && acceptance.length > 0
880
- ? `## Acceptance\n${acceptance.map((a) => `- [ ] ${a}`).join('\n')}`
946
+ ? `## Acceptance\n${acceptance.map((a, i) => `- [ ] AC-${i + 1}: ${a}`).join('\n')}`
881
947
  : null,
882
948
  },
883
949
  {
@@ -967,8 +1033,8 @@ function serializeAuthoredMarker(body) {
967
1033
  }
968
1034
 
969
1035
  /**
970
- * Build the optional `---` footer block (`parent` / `Epic` / `blocked by`
971
- * lines). Returns the empty string when `opts.includeFooter` is falsy.
1036
+ * Build the optional `---` footer block (`parent` / `blocked by` lines).
1037
+ * Returns the empty string when `opts.includeFooter` is falsy.
972
1038
  *
973
1039
  * @param {StoryBody} body
974
1040
  * @param {SerializeOptions} opts
@@ -978,7 +1044,9 @@ function serializeFooter(body, opts) {
978
1044
  if (!opts.includeFooter) return '';
979
1045
  const footerLines = ['---'];
980
1046
  if (opts.footer?.parent) footerLines.push(`parent: #${opts.footer.parent}`);
981
- if (opts.footer?.epic) footerLines.push(`Epic: #${opts.footer.epic}`);
1047
+ // Story #4545 — no `Epic: #N` branch. `pr-base-guard.js` hard-refuses a
1048
+ // Story body carrying that footer, so composing one here would let the
1049
+ // framework generate work it would then reject at delivery.
982
1050
  if (Array.isArray(body.depends_on)) {
983
1051
  for (const dep of body.depends_on) {
984
1052
  footerLines.push(`blocked by ${dep}`);
@@ -26,9 +26,8 @@ import {
26
26
  */
27
27
  export function renderDecomposerSystemPrompt({
28
28
  maxTickets = LIMITS_DEFAULTS.maxTickets,
29
- epicId = null,
30
29
  } = {}) {
31
- return render2TierPrompt({ maxTickets, epicId });
30
+ return render2TierPrompt({ maxTickets });
32
31
  }
33
32
 
34
33
  /**
@@ -37,7 +36,7 @@ export function renderDecomposerSystemPrompt({
37
36
  * on the Story body so the executing agent has everything it needs in one
38
37
  * ticket. Thematic grouping lives as prose in the Epic body / Tech Spec.
39
38
  */
40
- function render2TierPrompt({ maxTickets, epicId = null }) {
39
+ function render2TierPrompt({ maxTickets }) {
41
40
  // v2 Stage 3: default-single — emit one Story unless the split policy clears.
42
41
  // Capacity thresholds are sourced from the single DEFAULT_MODEL_CAPACITY
43
42
  // constant (ticket-validator-sizing.js) so the prompt and the validator
@@ -64,13 +63,6 @@ function render2TierPrompt({ maxTickets, epicId = null }) {
64
63
  advisoryCaveat,
65
64
  newFileContract,
66
65
  } = AUTHORING_ALTITUDE_GUIDANCE;
67
- // The namespaced AC-tag token the wave-0 BDD scaffold section below must
68
- // require on every scaffolded scenario (Story #4301). When the Epic ID is
69
- // known at render time, interpolate the concrete tag so the author has no
70
- // placeholder to get wrong; otherwise fall back to the documented pattern.
71
- const acTagExample = Number.isInteger(epicId)
72
- ? `@epic-${epicId}-ac-1`
73
- : '@epic-<id>-ac-N';
74
66
  return `You are an expert Senior Project Manager and Orchestrator.
75
67
  Your job is to turn a plan seed / Tech Spec into a Story ticket array for an AI Agent to execute.
76
68
 
@@ -82,7 +74,9 @@ Your job is to turn a plan seed / Tech Spec into a Story ticket array for an AI
82
74
  - Thematic grouping is prose in the Story's folded \`## Spec\` / \`## Slicing\`, never sibling tickets for coupled work.
83
75
 
84
76
  ### LABEL CONVENTIONS:
85
- - Every ticket must have the \`type::story\` label. No other type label is allowed the retired Feature and Task tiers have no labels under this hierarchy.
77
+ - \`type::story\` is applied automatically by persist — you do not need to emit it, and no other type label is allowed (the retired Feature and Task tiers have no labels under this hierarchy).
78
+ - \`labels[]\` is **optional**. Emit it only to request an *additional* label; persist sanitizes the list before applying it.
79
+ - Do **not** emit \`agent::*\` labels — lifecycle state is runtime-owned, and persist applies \`agent::ready\` itself once every checkpoint is on the ticket.
86
80
  - Do **not** emit \`persona::*\` labels — the behavioral persona concept (and its label axis) was removed in v2.
87
81
 
88
82
  ### OUTPUT FORMAT:
@@ -97,7 +91,7 @@ You MUST respond ONLY with a valid JSON array of objects. No prose, no markdown
97
91
  "body": <string — see STORY BODY SCHEMA below>,
98
92
  "acceptance": ["<testable, observable criterion>", ...],
99
93
  "verify": ["<exact command or test path> (<tier>)", ...],
100
- "labels": ["type::story"],
94
+ "labels": ["<extra-label>"] (optional — type::story is applied automatically; omit this field unless you need an additional label),
101
95
  "depends_on": ["slug-of-blocking-dependency"] (optional array of Story slugs that block execution)
102
96
  }
103
97
  ]
@@ -107,7 +101,9 @@ You MUST respond ONLY with a valid JSON array of objects. No prose, no markdown
107
101
  ### STORY BODY SCHEMA (REQUIRED FOR EVERY STORY):
108
102
  \`body\` MUST be a **string** — the serialized markdown produced by \`serialize()\` from \`lib/story-body/story-body.js\`. Do NOT emit \`body\` as a JSON object: an object body throws \`StoryBodyParseError\` in the reconciler (Story #3302) and is discarded by the GitHub provider, producing an empty issue body. Stories are consumed by non-interactive sub-agents that must self-verify from the Story ticket alone — so the ticket must carry everything an agent needs to execute and self-verify.
109
103
 
110
- The \`acceptance[]\` and \`verify[]\` arrays live at the **top level** of the Story ticket object — that is the machine contract the validator reads. Persist syncs those arrays into the body's \`## Acceptance\` / \`## Verify\` sections so the GitHub issue stays a complete executable document. Prefer authoring the lists **once at top-level** and omitting those sections from the authored \`body\` string (or mirroring the exact same items). Do **not** invent a second criteria list inside \`## Spec\`, and do not author a separate Acceptance Spec / PRD artifact.
104
+ The \`acceptance[]\` and \`verify[]\` arrays live at the **top level** of the Story ticket object — that is the machine contract the validator reads. Author each list **once, at top level**, and **omit** the \`## Acceptance\` / \`## Verify\` sections from the authored \`body\` string: persist syncs the top-level arrays into those sections so the GitHub issue stays a complete executable document. The validator resolves both fields from the top level, so an omitted section is the expected shape, not a violation.
105
+
106
+ If you do write those sections into the \`body\` string anyway, they must mirror the top-level arrays **item for item** — persist fails closed on a disagreement rather than guessing which list is authoritative. Do **not** invent a second criteria list inside \`## Spec\`, and do not author a separate Acceptance Spec / PRD artifact.
111
107
 
112
108
  The serialized \`body\` string renders these markdown sections (in order):
113
109
 
@@ -150,6 +146,10 @@ The serialized \`body\` string renders these markdown sections (in order):
150
146
  - **verify** (top-level array on the ticket object): Each entry MUST name a testing tier in parentheses, drawn from \`unit\` / \`contract\` / \`e2e\` / \`validate\`. Example: \`npm run test -- src/x.test.ts (unit)\`, \`npm run validate (validate)\`. Stories with zero verify entries SHOULD fail validation; if a story is genuinely unverifiable in isolation (e.g., a copy edit auditor will eyeball), the literal entry \`manual:<reason>\` is allowed so the absence is intentional, not lazy. Manual entries without a reason are rejected.
151
147
  - **reason to exist** (REQUIRED, encoded as the \`reason_to_exist\` field of the \`<!-- meta: {...} -->\` comment appended to the serialized body string — NOT a top-level ticket field): One sentence stating the single coherent reason this Story exists, distinct from its broader \`## Goal\` prose. Every Story MUST carry a non-empty \`reason_to_exist\`; it is the machine-checkable form of the cohesion rule (**one Story = one coherent change with one reason to exist**) and the \`epic-plan-consolidate\` critic flags any Story whose body carries no non-empty reason to exist. Encode it as \`<!-- meta: {"reason_to_exist": "..."} -->\`.
152
148
  - **estimated_test_files** (optional, encoded in the \`<!-- meta: {...} -->\` comment appended to the serialized body string — NOT a top-level ticket field): Integer estimate of how many test files this Story creates or modifies. Omit when the number is not estimable. Informational only — it does not gate the decompose.
149
+ - **Observed-behavior claims open with \`Current state (verified <date>)\`.** Any Spec claim about how the codebase behaves today MUST open with that preamble (e.g. \`Current state (verified 2026-07-17): …\`) so a reader can tell a verified observation from an assumption, and can tell when the observation went stale.
150
+ - **Intent-then-proxy acceptance shape.** When an acceptance item verifies through a proxy check (a grep, a file-exists probe, an exit-code test), state the intent clause before the proxy check — what outcome the check stands in for — so the proxy never becomes the goal (e.g. "the workflow names hygiene findings as re-author input: \`grep -n "textHygiene" …\` exits 0").
151
+ - **Slicing checkpoints are one line each.** Each \`## Slicing\` checkpoint is a single line naming the checkpoint; implementation detail lives in \`## Spec\`, never duplicated into Slicing. A Slicing section outweighing its Spec is a defect the text-hygiene lint flags.
152
+ - **Bodies record decisions, never questions to the operator.** Never persist an open question ("Flag if…", "TBD", "confirm with the operator") into a Story body — the executing sub-agent is non-interactive and cannot answer it. Resolve the unknown before authoring, or restate it as a declarative Key Assumption the agent can act on.
153
153
  - **non_goals** (OPTIONAL, in body string as the \`## Non-Goals\` section): A short list of capabilities or changes this Story explicitly does NOT deliver — an advisory negative-scope bound that fences the executing agent away from adjacent work. It is **advisory and NON-GATING**: the validator does not require, count, or reject on it, and an absent or empty section renders nothing. Use the EXACT single-word hyphenated heading spelling \`## Non-Goals\` (a space-separated heading like \`## Out of Scope\` is NOT recognized by the parser and will be dropped). Reach for it when a Story's negative boundary is non-obvious from its \`acceptance[]\` alone; omit it otherwise.
154
154
 
155
155
  #### AUTHORING ALTITUDE — BINDING ACCEPTANCE vs ADVISORY CHANGES:
@@ -225,9 +225,8 @@ When the Acceptance Spec contains **one or more \`Disposition: new\` rows**, you
225
225
  - **goal**: contains the literal token \`bdd-scaffold\` (e.g. "bdd-scaffold: create the @skip-tagged feature files the implementation Stories verify against").
226
226
  - **depends_on**: EMPTY (\`[]\`) — it runs first, in wave 0.
227
227
  - **changes**: one entry per distinct \`.feature\` file named in a \`new\` row, each \`{ "path": "<feature file path>", "assumption": "creates" }\`.
228
- - **acceptance**: MUST assert (a) every new \`.feature\` file exists, (b) every new scenario within them carries an \`@skip\` tag, AND (c) every new scenario also carries its **namespaced per-Epic AC tag** \`${acTagExample}\` (one tag per AC ID the scenario satisfies — see below). Keep these observable (a grep/validate command exits 0, a file exists at a path).
229
- - **Namespaced AC tag is REQUIRED at scaffold time, not only at de-skip time.** Phase 7 finalize's \`acceptance-spec-reconciler.js\` matches AC IDs only against \`@epic-<id>-ac-*\` / \`@pending\` tags in \`tests/features/**\` — a bare \`@ac-N\` tag is deliberately ignored to prevent cross-Epic collision. A scaffolded scenario that carries \`@skip\` but omits \`@epic-<id>-ac-N\` reads as \`missing[]\` at finalize and aborts the close even after the implementation Story de-skips it, because the tag was never added. Tag each scenario with both \`@skip\` AND \`${acTagExample}\` (substituting the AC's own number) in this SAME wave-0 pass — do not defer the AC tag to the later de-skip edit.
230
- - **verify**: a grep/validate command (tier \`validate\`), NOT an e2e runner — verifying that a file exists with the required tags needs no browser/playwright run. Example: \`grep -rL '@skip' tests/features/<area>/*.feature (validate)\` paired with an existence check, AND a check that every new AC ID's namespaced tag (\`${acTagExample}\`) appears in the scaffolded files, e.g. \`grep -q '${acTagExample}' tests/features/<area>/<file>.feature (validate)\` for each new AC row.
228
+ - **acceptance**: MUST assert (a) every new \`.feature\` file exists AND (b) every new scenario within them carries an \`@skip\` tag. Keep these observable (a grep/validate command exits 0, a file exists at a path).
229
+ - **verify**: a grep/validate command (tier \`validate\`), NOT an e2e runner verifying that a file exists with the required tags needs no browser/playwright run. Example: \`grep -rL '@skip' tests/features/<area>/*.feature (validate)\` paired with an existence check.
231
230
  - Each implementation Story whose \`verify[]\` references one of these scaffolded \`.feature\` paths MUST \`depends_on\` the scaffold Story (so the scaffold lands in an earlier wave). Omitting the link trips the soft \`missing-bdd-scaffold\` validator finding.
232
231
 
233
232
  When the Acceptance Spec contains **zero \`new\`-disposition rows** (every row is \`updated\` or \`unchanged\`), do NOT emit a scaffold Story — there is nothing to create.
@@ -17,12 +17,25 @@
17
17
  * the NODE_ENV band-aid was removed in Story #3342. Tests that need to
18
18
  * exercise the webhook POST inject `opts.fetchImpl` instead, so the
19
19
  * request never reaches the real network even if a URL resolves.)
20
+ * - Every `GIT_*` variable is dropped. When the suite runs inside a git
21
+ * hook (husky pre-push via the coverage-capture path), the parent git
22
+ * invocation exports `GIT_DIR` — from a linked worktree, the absolute
23
+ * `<main>/.git/worktrees/<name>` path. A test fixture's `git init`
24
+ * under that env re-initializes the shared gitdir and writes
25
+ * `core.bare=true` into the MAIN checkout's `.git/config`, breaking
26
+ * every worktree at once (#4580). Production git call sites are
27
+ * covered by `cleanGitEnv` in `git-utils.js`; this is the same scrub
28
+ * for test child processes, which may spawn git directly. Tests that
29
+ * need a `GIT_*` variable set it explicitly on their own spawn.
20
30
  *
21
31
  * @param {NodeJS.ProcessEnv} baseEnv
22
32
  * @returns {NodeJS.ProcessEnv}
23
33
  */
24
34
  export function buildWebhookSafeTestEnv(baseEnv = process.env) {
25
- const env = { ...baseEnv, NODE_ENV: baseEnv.NODE_ENV ?? 'test' };
35
+ const env = Object.fromEntries(
36
+ Object.entries(baseEnv).filter(([k]) => !k.startsWith('GIT_')),
37
+ );
38
+ env.NODE_ENV = baseEnv.NODE_ENV ?? 'test';
26
39
  if (env.MANDREL_ALLOW_TEST_WEBHOOKS !== '1') {
27
40
  delete env.NOTIFICATION_WEBHOOK_URL;
28
41
  }
@@ -12,13 +12,10 @@ import picomatch from 'picomatch';
12
12
  */
13
13
  export const INTEGRATION_INCLUDE = [
14
14
  'tests/**/*.integration.test.js',
15
- 'tests/epic-execute/epic-execute-record-wave.test.js',
16
15
  'tests/hook-chain-reflog-invariant.test.js',
17
- 'tests/push-epic-retry.test.js',
18
16
  'tests/contract/check-baselines-regression.test.js',
19
17
  'tests/contract/check-baselines-kernel-mismatch.test.js',
20
18
  'tests/integration-prime-after-sweep.test.js',
21
- 'tests/concurrency-wiring.test.js',
22
19
  'tests/scripts/git-cleanup.test.js',
23
20
  'tests/lib/checks/runner-integration.test.js',
24
21
  'tests/single-story-close-sync.test.js',
@@ -107,20 +107,6 @@ export function hasTicketSection(body, kind) {
107
107
  return locate(body, kind) !== null;
108
108
  }
109
109
 
110
- /**
111
- * Extract a managed region's content (between the markers, trimmed).
112
- * Returns `null` when the region is absent.
113
- *
114
- * @param {string} body
115
- * @param {'techSpec'|'acceptanceTable'} kind
116
- * @returns {string|null}
117
- */
118
- export function extractTicketSection(body, kind) {
119
- const loc = locate(body, kind);
120
- if (!loc) return null;
121
- return body.slice(loc.contentStart, loc.contentEnd).trim();
122
- }
123
-
124
110
  /**
125
111
  * Insert or replace a managed region, preserving every byte outside it.
126
112
  *
@@ -3,15 +3,12 @@
3
3
  *
4
4
  * Tech Spec #819 §"Evidence record (Story 7)". Each successful gate run
5
5
  * writes a record keyed by `{ gateName, commitSha, commandConfigHash }` to
6
- * a per-Epic-tree path under the resolved `tempRoot`:
6
+ * a storyId-anchored path under the resolved `tempRoot`:
7
7
  *
8
- * - Epic-scoped (scopeId === epicId):
9
- * `<tempRoot>/epic-<epicId>/validation-evidence.json`
10
- * - Story-scoped (scopeId === storyId):
11
- * `<tempRoot>/epic-<epicId>/story-<storyId>/validation-evidence.json`
8
+ * `<tempRoot>/standalone/stories/story-<storyId>/validation-evidence.json`
12
9
  *
13
- * Both paths sit inside the per-Epic durable workspace (Epic #1030, Stories
14
- * #1039 + #1054) and are gitignored via `temp/`.
10
+ * The path is gitignored via `temp/`. v2.0.0 removed the Epic tier along
11
+ * with the Epic-keyed `<tempRoot>/epic-<epicId>/…` keyspace.
15
12
  *
16
13
  * A subsequent caller can `shouldSkip(...)` to learn whether the same gate
17
14
  * has already passed against the current HEAD with an identical
@@ -34,7 +31,7 @@ import path from 'node:path';
34
31
  import { fileURLToPath } from 'node:url';
35
32
  import Ajv from 'ajv/dist/2020.js';
36
33
  import addFormats from 'ajv-formats';
37
- import { epicTempDir, storyTempDir } from './config/temp-paths.js';
34
+ import { storyTempDir } from './config/temp-paths.js';
38
35
 
39
36
  export const SCHEMA_VERSION = 1;
40
37
  const DEFAULT_TEMP_DIR = 'temp';
@@ -97,37 +94,28 @@ function requirePositiveInt(value, label) {
97
94
 
98
95
  /**
99
96
  * Compute the absolute path of the evidence file for `scopeId` under the
100
- * per-Epic temp tree.
97
+ * storyId-anchored temp tree.
101
98
  *
102
- * The resolution rule mirrors `lib/config/temp-paths.js`:
103
- * - `scopeId === epicId` → `<tempRoot>/epic-<epicId>/validation-evidence.json`
104
- * - `scopeId !== epicId` → treated as a Story id → `<tempRoot>/epic-<epicId>/story-<scopeId>/validation-evidence.json`
105
- *
106
- * **Standalone keyspace (Story #4250).** When `opts.standalone === true`,
99
+ * **Standalone keyspace (Story #4250).** `opts.standalone` must be `true`:
107
100
  * the Story has no parent Epic, so the evidence file is anchored on the
108
101
  * Story id alone at
109
102
  * `<tempRoot>/standalone/stories/story-<scopeId>/validation-evidence.json`
110
- * (the `storyTempDir(null, sid)` layout from Story #2874). In this mode
111
- * `epicId` is ignored — callers MUST NOT feed a `0`/`null` epicId into the
112
- * Epic-keyed branch (the historical bug this keyspace replaces). Outside
113
- * standalone mode `epicId` remains required.
103
+ * (the `storyTempDir(null, sid)` layout from Story #2874).
114
104
  *
115
- * The legacy flat `temp/validation-evidence-<scopeId>.json` layout is no
116
- * longer supported — Epic-scoped callers must thread the Epic id through
117
- * (Epic #1030 follow-up to Story #1054). The synthetic config bag passed to
118
- * `epicTempDir` / `storyTempDir` keeps the resolver from doing a disk-bound
119
- * `.agentrc.json` lookup; bare callers can pass `tempDir` via `opts` to
120
- * override the default `'temp'`.
105
+ * The legacy flat `temp/validation-evidence-<scopeId>.json` layout and the
106
+ * Epic-keyed `<tempRoot>/epic-<epicId>/…` layout are both gone. The
107
+ * synthetic config bag passed to `storyTempDir` keeps the resolver from
108
+ * doing a disk-bound `.agentrc.json` lookup; bare callers can pass
109
+ * `tempDir` via `opts` to override the default `'temp'`.
121
110
  *
122
111
  * @param {number|string} scopeId
123
- * @param {{ cwd?: string, tempDir?: string, epicId?: number|string|null, standalone?: boolean }} opts
112
+ * @param {{ cwd?: string, tempDir?: string, standalone?: boolean }} opts
124
113
  * @returns {string}
125
114
  */
126
115
  export function evidencePath(scopeId, opts = {}) {
127
- const standalone = opts.standalone === true;
128
- if (!standalone && opts.epicId == null) {
116
+ if (opts.standalone !== true) {
129
117
  throw new Error(
130
- '[validation-evidence] evidencePath requires opts.epicId (Epic-scoped path resolution) unless opts.standalone is set.',
118
+ '[validation-evidence] evidencePath requires opts.standalone (the storyId-anchored keyspace is the only keyspace).',
131
119
  );
132
120
  }
133
121
  const { cwd, tempDir } = resolveOpts(opts);
@@ -143,18 +131,9 @@ export function evidencePath(scopeId, opts = {}) {
143
131
  ? tempDir
144
132
  : path.join(cwd, tempDir);
145
133
  const configBag = { project: { paths: { tempRoot: absTempRoot } } };
146
- let dir;
147
- if (standalone) {
148
- // Story #4250 — storyId-anchored standalone keyspace. `null` is the
149
- // standalone-story sentinel `storyTempDir` accepts (Story #2874).
150
- dir = storyTempDir(null, scope, configBag);
151
- } else {
152
- const epicId = requirePositiveInt(opts.epicId, 'epicId');
153
- dir =
154
- scope === epicId
155
- ? epicTempDir(epicId, configBag)
156
- : storyTempDir(epicId, scope, configBag);
157
- }
134
+ // Story #4250 — storyId-anchored standalone keyspace. `null` is the
135
+ // standalone-story sentinel `storyTempDir` accepts (Story #2874).
136
+ const dir = storyTempDir(null, scope, configBag);
158
137
  return path.join(dir, EVIDENCE_FILENAME);
159
138
  }
160
139
 
@@ -189,19 +168,17 @@ function emptyDoc(scopeId) {
189
168
  * cross-scopeId cases — callers don't have to branch on those failure
190
169
  * modes; they manifest as `shouldSkip()` returning `skip: false`.
191
170
  *
192
- * `opts.epicId` is required so the per-Epic-tree path can be resolved,
193
- * unless `opts.standalone === true` (Story #4250) routes to the
194
- * storyId-anchored standalone keyspace.
171
+ * `opts.standalone` is required (Story #4250) it routes to the
172
+ * storyId-anchored keyspace.
195
173
  *
196
174
  * @param {number|string} scopeId
197
- * @param {{ cwd?: string, tempDir?: string, epicId?: number|string|null, standalone?: boolean, fs?: object }} opts
175
+ * @param {{ cwd?: string, tempDir?: string, standalone?: boolean, fs?: object }} opts
198
176
  * @returns {{ storyId: number, schemaVersion: number, records: object[] }}
199
177
  */
200
178
  export function loadEvidence(scopeId, opts = {}) {
201
179
  const resolved = resolveOpts(opts);
202
180
  const file = evidencePath(scopeId, {
203
181
  ...resolved,
204
- epicId: opts.epicId,
205
182
  standalone: opts.standalone,
206
183
  });
207
184
  if (!resolved.fs.existsSync(file)) return emptyDoc(scopeId);
@@ -223,9 +200,8 @@ export function loadEvidence(scopeId, opts = {}) {
223
200
  * Validates the resulting document against the schema before writing — a
224
201
  * malformed write throws so the bug surfaces immediately.
225
202
  *
226
- * `opts.epicId` is required so the per-Epic-tree path can be resolved,
227
- * unless `opts.standalone === true` (Story #4250) routes to the
228
- * storyId-anchored standalone keyspace.
203
+ * `opts.standalone` is required (Story #4250) it routes to the
204
+ * storyId-anchored keyspace.
229
205
  *
230
206
  * @param {{
231
207
  * storyId: number|string,
@@ -235,7 +211,7 @@ export function loadEvidence(scopeId, opts = {}) {
235
211
  * exitCode?: number,
236
212
  * durationMs?: number|null,
237
213
  * }} input
238
- * @param {{ cwd?: string, tempDir?: string, epicId?: number|string|null, standalone?: boolean, fs?: object, now?: Function }} opts
214
+ * @param {{ cwd?: string, tempDir?: string, standalone?: boolean, fs?: object, now?: Function }} opts
239
215
  * @returns {object} The persisted record.
240
216
  */
241
217
  export function recordPass(
@@ -258,7 +234,6 @@ export function recordPass(
258
234
  const resolved = resolveOpts(opts);
259
235
  const evidenceOpts = {
260
236
  ...resolved,
261
- epicId: opts.epicId,
262
237
  standalone: opts.standalone,
263
238
  };
264
239
  const doc = loadEvidence(storyId, evidenceOpts);
@@ -297,13 +272,12 @@ export function recordPass(
297
272
  * with a machine-readable `reason` so callers can log why the skip didn't
298
273
  * fire.
299
274
  *
300
- * `opts.epicId` is required so the per-Epic-tree path can be resolved,
301
- * unless `opts.standalone === true` (Story #4250) routes to the
302
- * storyId-anchored standalone keyspace. `opts` is forwarded verbatim to
275
+ * `opts.standalone` is required (Story #4250) it routes to the
276
+ * storyId-anchored keyspace. `opts` is forwarded verbatim to
303
277
  * `loadEvidence`, so `standalone` flows through unchanged.
304
278
  *
305
279
  * @param {{ storyId: number|string, gateName: string, currentSha: string, configHash: string }} input
306
- * @param {{ cwd?: string, tempDir?: string, epicId?: number|string|null, standalone?: boolean, fs?: object }} opts
280
+ * @param {{ cwd?: string, tempDir?: string, standalone?: boolean, fs?: object }} opts
307
281
  * @returns {{ skip: boolean, reason: string, record?: object }}
308
282
  */
309
283
  export function shouldSkip(
@@ -340,19 +314,17 @@ export function shouldSkip(
340
314
  * start of each Story so a re-run always starts clean. Idempotent —
341
315
  * absent file is not an error.
342
316
  *
343
- * `opts.epicId` is required so the per-Epic-tree path can be resolved,
344
- * unless `opts.standalone === true` (Story #4250) routes to the
345
- * storyId-anchored standalone keyspace.
317
+ * `opts.standalone` is required (Story #4250) it routes to the
318
+ * storyId-anchored keyspace.
346
319
  *
347
320
  * @param {number|string} scopeId
348
- * @param {{ cwd?: string, tempDir?: string, epicId?: number|string|null, standalone?: boolean, fs?: object }} opts
321
+ * @param {{ cwd?: string, tempDir?: string, standalone?: boolean, fs?: object }} opts
349
322
  * @returns {{ cleared: boolean, path: string }}
350
323
  */
351
324
  export function forceClear(scopeId, opts = {}) {
352
325
  const resolved = resolveOpts(opts);
353
326
  const file = evidencePath(scopeId, {
354
327
  ...resolved,
355
- epicId: opts.epicId,
356
328
  standalone: opts.standalone,
357
329
  });
358
330
  if (!resolved.fs.existsSync(file)) return { cleared: false, path: file };