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
@@ -0,0 +1,243 @@
1
+ /**
2
+ * runtime-friction.js — derive friction signals from observables the
3
+ * runtime already has (Story #4578).
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * Friction telemetry was **opt-in**: a `friction` record existed only when
8
+ * an agent volunteered a `diagnose-friction.js --story <id> --cmd <...>`
9
+ * call. `plan-run-epilogue.js`'s `follow-up-rollup` then read an empty
10
+ * `signals.ndjson` and truthfully reported "No friction signals — nothing
11
+ * to follow up". A 7-Story run containing a mid-run git outage, a parked
12
+ * worker needing an operator resume, and a four-round acceptance critic
13
+ * produced a **zero-signal retro** — because the stream is least likely to
14
+ * fill exactly when a run is going badly and the agent is busy.
15
+ *
16
+ * This module closes that gap from the other side: the runtime emits
17
+ * friction from what it *already knows*, at the point it already knows it.
18
+ * No agent cooperation required.
19
+ *
20
+ * ## Not a second channel
21
+ *
22
+ * These records go through the **existing** `appendSignal` path, carry the
23
+ * **existing** `kind: 'friction'` shape, and land in the **same**
24
+ * `signals.ndjson` stream `diagnose-friction.js` writes and the roll-up
25
+ * reads. The only thing that is new is *who* decides to write.
26
+ *
27
+ * Deliberately distinct from {@link ../gates/friction.js}'s
28
+ * `emitFrictionSignal`, which early-returns unless **both** `storyId` and
29
+ * `epicId` are truthy. Every v2 Story is standalone (`epicId: null` — see
30
+ * `temp-paths.storyTempDir`'s standalone branch), and the roll-up reads the
31
+ * standalone stream via `forEachLine(null, sid, ...)`. Routing runtime
32
+ * friction through the gate helper would drop **every** record on the floor,
33
+ * silently, which is the bug this Story exists to fix.
34
+ *
35
+ * ## Robustness contract
36
+ *
37
+ * Observability MUST NOT halt the runner (`signals-writer.js`; the
38
+ * `docs/patterns.md` friction pattern). Every export here is best-effort:
39
+ * it swallows its own failures after a `Logger.warn` and resolves `false`.
40
+ * A missing signal is strictly preferable to a broken close.
41
+ *
42
+ * @module lib/observability/runtime-friction
43
+ */
44
+
45
+ import crypto from 'node:crypto';
46
+
47
+ import { Logger } from '../Logger.js';
48
+ import { appendSignal } from './signals-writer.js';
49
+
50
+ /**
51
+ * The friction categories this module emits.
52
+ *
53
+ * Values are plain strings by design: `signal-event.schema.json` types
54
+ * `category` as a free-form `{ type: 'string', minLength: 1 }`, so no schema
55
+ * change is required to add one. They are frozen here so the emitters and
56
+ * their tests name the same literal instead of spreading it.
57
+ *
58
+ * Category choice is load-bearing for the retro: `retro-proposals.js`
59
+ * aggregates **by exact category string**, so these are deliberately coarse
60
+ * — one bucket per failure mode — rather than per-incident. Two Stories that
61
+ * hit the same wall in one run aggregate to `occurrences: 2` and route as a
62
+ * real proposal instead of being discarded as two unrelated singletons.
63
+ */
64
+ export const RUNTIME_FRICTION_CATEGORIES = Object.freeze({
65
+ /** A Story was parked at `agent::blocked` — the HITL pause (§ 1.J). */
66
+ STORY_BLOCKED: 'story-blocked',
67
+ /** A close run ended on a `failed` terminal (non-zero exit). */
68
+ CLOSE_FAILED: 'close-failed',
69
+ /** A bounded merge wait expired with the PR still in flight. */
70
+ MERGE_WAIT_EXHAUSTED: 'merge-wait-exhausted',
71
+ });
72
+
73
+ /** Cap on free-form reason text copied into a signal's `details`. */
74
+ const REASON_PREVIEW_LIMIT = 500;
75
+
76
+ /**
77
+ * @param {unknown} value
78
+ * @returns {string}
79
+ */
80
+ function preview(value) {
81
+ return String(value ?? '').slice(0, REASON_PREVIEW_LIMIT);
82
+ }
83
+
84
+ /**
85
+ * @param {unknown} value
86
+ * @returns {number|null}
87
+ */
88
+ function positiveIntOrNull(value) {
89
+ const n = Number(value);
90
+ return Number.isInteger(n) && n > 0 ? n : null;
91
+ }
92
+
93
+ /**
94
+ * Append one runtime-derived `friction` record to the Story's signals
95
+ * stream. Best-effort: never throws, never rejects.
96
+ *
97
+ * `epicId` defaults to `null` (the standalone-Story stream) because that is
98
+ * where v2 Stories live and where the roll-up reads.
99
+ *
100
+ * @param {object} args
101
+ * @param {number} args.storyId
102
+ * @param {number|null} [args.epicId]
103
+ * @param {string} args.category One of {@link RUNTIME_FRICTION_CATEGORIES}.
104
+ * @param {string} args.tool Emitting surface, for `emitter.tool`.
105
+ * @param {object} [args.details] Kind-specific payload (always an object).
106
+ * @param {object} [args.config] Resolved config (for `tempRoot`).
107
+ * @returns {Promise<boolean>} true when a record was appended.
108
+ */
109
+ export async function emitRuntimeFriction({
110
+ storyId,
111
+ epicId = null,
112
+ category,
113
+ tool,
114
+ details = {},
115
+ config,
116
+ } = {}) {
117
+ const sid = positiveIntOrNull(storyId);
118
+ if (sid === null) {
119
+ // No Story context → no stream to write to. Not an error: some close
120
+ // paths (a usage error before the id is parsed) genuinely have none.
121
+ return false;
122
+ }
123
+ if (typeof category !== 'string' || category.trim() === '') {
124
+ Logger.warn('[runtime-friction] refusing to emit a category-less signal');
125
+ return false;
126
+ }
127
+
128
+ const signal = {
129
+ kind: 'friction',
130
+ eventId: crypto.randomUUID(),
131
+ ts: new Date().toISOString(),
132
+ epicId: positiveIntOrNull(epicId),
133
+ storyId: sid,
134
+ // 2-tier hierarchy (Epic #3163): no Task tier. Retained for schema
135
+ // compatibility and always null — mirrors diagnose-friction.js.
136
+ taskId: null,
137
+ category: category.trim(),
138
+ emitter: { tool: tool || 'runtime-friction' },
139
+ details: details && typeof details === 'object' ? details : {},
140
+ };
141
+
142
+ try {
143
+ return await appendSignal({
144
+ epicId: signal.epicId,
145
+ storyId: sid,
146
+ signal,
147
+ config,
148
+ });
149
+ } catch (err) {
150
+ // `appendSignal` already swallows its own I/O failures; this catch is
151
+ // defense in depth so a surprise (a poisoned config, a throwing
152
+ // validator) still cannot take down the path being observed.
153
+ Logger.warn(
154
+ `[runtime-friction] append failed for Story #${sid} (${category}): ${
155
+ err instanceof Error ? err.message : String(err)
156
+ }`,
157
+ );
158
+ return false;
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Decide whether a `story-deliver-terminal` envelope is worth a friction
164
+ * record, and describe it. **Pure** — no I/O — so the (interesting) policy
165
+ * is unit-testable without touching disk.
166
+ *
167
+ * The partition is deliberate; each observable is emitted from exactly ONE
168
+ * place, so one incident never double-counts into `occurrences: 2` and
169
+ * fabricates a filed proposal out of a single event:
170
+ *
171
+ * - `blocked` → **null here.** Every blocked terminal flips the Story to
172
+ * `agent::blocked` (`confirm-merge.js`, both the `merge.unlanded` and
173
+ * `merge.flip-failed` paths), and that transition is instrumented at the
174
+ * canonical mutator (`ticketing/transition.js`). Emitting here as well
175
+ * would count the same block twice.
176
+ * - `landed` → null. Nothing happened worth a retro.
177
+ * - `failed` → friction. A close that ended non-zero.
178
+ * - `pending` → friction **only when a `waitBudget` was exhausted**. That
179
+ * is the parked worker from the report: a bounded wait expired with the
180
+ * PR in flight and a human must resume it. A `pending` with **no**
181
+ * `waitBudget` is the `--no-wait-merge` / operator-merge path, where the
182
+ * human deliberately owns the land and nothing is broken — flagging it
183
+ * would train operators to ignore the channel.
184
+ *
185
+ * Deliberately **not exported**: it is this module's internal policy, and
186
+ * `emitTerminalFriction` is the contract callers (and tests) exercise. An
187
+ * export solely for testability would be production-dead code — the
188
+ * `--production` dead-exports ratchet exists to catch exactly that.
189
+ *
190
+ * @param {object} envelope A `story-deliver-terminal` envelope.
191
+ * @returns {{ category: string, details: object }|null}
192
+ */
193
+ function frictionForTerminal(envelope) {
194
+ if (!envelope || typeof envelope !== 'object') return null;
195
+ const { status, phase, waitBudget, failure, pr } = envelope;
196
+
197
+ if (status === 'failed') {
198
+ return {
199
+ category: RUNTIME_FRICTION_CATEGORIES.CLOSE_FAILED,
200
+ details: {
201
+ phase: phase ?? null,
202
+ reason: preview(failure?.reason),
203
+ },
204
+ };
205
+ }
206
+
207
+ if (status === 'pending' && waitBudget) {
208
+ return {
209
+ category: RUNTIME_FRICTION_CATEGORIES.MERGE_WAIT_EXHAUSTED,
210
+ details: {
211
+ phase: phase ?? null,
212
+ prNumber: pr?.number ?? null,
213
+ checksStatus: pr?.checksStatus ?? null,
214
+ waitedSeconds: waitBudget.waitedSeconds ?? null,
215
+ cumulativeSeconds: waitBudget.cumulativeSeconds ?? null,
216
+ maxBudgetSeconds: waitBudget.maxBudgetSeconds ?? null,
217
+ },
218
+ };
219
+ }
220
+
221
+ return null;
222
+ }
223
+
224
+ /**
225
+ * Emit the friction record (if any) implied by a terminal envelope.
226
+ * Best-effort; never throws.
227
+ *
228
+ * @param {object} args
229
+ * @param {object} args.envelope
230
+ * @param {object} [args.config]
231
+ * @returns {Promise<boolean>} true when a record was appended.
232
+ */
233
+ export async function emitTerminalFriction({ envelope, config } = {}) {
234
+ const verdict = frictionForTerminal(envelope);
235
+ if (!verdict) return false;
236
+ return emitRuntimeFriction({
237
+ storyId: envelope?.storyId,
238
+ category: verdict.category,
239
+ tool: 'single-story-close',
240
+ details: verdict.details,
241
+ config,
242
+ });
243
+ }
@@ -18,7 +18,7 @@
18
18
  * other draft-07 validators (see `tests/schemas/signal-schemas.test.js`).
19
19
  *
20
20
  * A per-Epic reject tally is persisted under the Epic temp tree
21
- * (`temp/epic-<eid>/signal-rejects.json`) so a cross-process reader (the
21
+ * (`temp/run-<eid>/signal-rejects.json`) so a cross-process reader (the
22
22
  * loop-health check, a follow-on Story) can surface how many records were
23
23
  * dropped. The tally is read-modify-written best-effort; a lost increment
24
24
  * under a write race is acceptable for a diagnostic counter.
@@ -32,7 +32,7 @@ import { fileURLToPath } from 'node:url';
32
32
  import Ajv from 'ajv';
33
33
  import addFormats from 'ajv-formats';
34
34
 
35
- import { epicArtifactPath } from '../config/temp-paths.js';
35
+ import { runArtifactPath } from '../config/temp-paths.js';
36
36
  import { Logger } from '../Logger.js';
37
37
 
38
38
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
@@ -141,7 +141,7 @@ export async function recordSignalReject({ epicId, config, field }) {
141
141
  if (!Number.isInteger(epicId) || epicId <= 0) return null;
142
142
  let target;
143
143
  try {
144
- target = epicArtifactPath(epicId, REJECT_TALLY_BASENAME, config);
144
+ target = runArtifactPath(epicId, REJECT_TALLY_BASENAME, config);
145
145
  } catch {
146
146
  return null;
147
147
  }
@@ -190,7 +190,7 @@ export async function readSignalRejectCount({ epicId, config }) {
190
190
  if (!Number.isInteger(epicId) || epicId <= 0) return 0;
191
191
  let target;
192
192
  try {
193
- target = epicArtifactPath(epicId, REJECT_TALLY_BASENAME, config);
193
+ target = runArtifactPath(epicId, REJECT_TALLY_BASENAME, config);
194
194
  } catch {
195
195
  return 0;
196
196
  }
@@ -2,7 +2,7 @@
2
2
  * Append-only signals/trace writer (Epic #1030 Story #1041).
3
3
  *
4
4
  * Centralizes the per-(epic, story) NDJSON streams under
5
- * `temp/epic-<eid>/stories/story-<sid>/signals.ndjson` (and a sibling
5
+ * `temp/run-<id>/stories/story-<sid>/signals.ndjson` (and a sibling
6
6
  * `traces.ndjson` for trace-shaped records). Detector modules and the
7
7
  * runtime trace hook all funnel through this writer so the on-disk
8
8
  * shape stays under one schema and one set of robustness guarantees.
@@ -18,7 +18,7 @@
18
18
  * fire from inside per-Story sub-agents that may exit abruptly, and
19
19
  * a buffered tail would silently disappear on `process.exit`.
20
20
  * - **Lazy directory creation.** The first write to a fresh Story
21
- * creates `temp/epic-<eid>/stories/story-<sid>/` via `fs.mkdir(..., { recursive: true })`.
21
+ * creates `temp/run-<id>/stories/story-<sid>/` via `fs.mkdir(..., { recursive: true })`.
22
22
  * `epicId` / `storyId` are required positive integers — the
23
23
  * `temp-paths.js` helpers assert this before we touch the disk.
24
24
  *
@@ -36,17 +36,12 @@ import fs from 'node:fs/promises';
36
36
  import path from 'node:path';
37
37
  import { createInterface } from 'node:readline';
38
38
 
39
- import {
40
- epicArtifactPath,
41
- signalsFile,
42
- storyTempDir,
43
- } from '../config/temp-paths.js';
39
+ import { signalsFile, storyTempDir } from '../config/temp-paths.js';
44
40
  import { Logger } from '../Logger.js';
45
41
  import { recordSignalReject, validateSignal } from './signal-validator.js';
46
42
  import { classifyPathSource } from './source-classifier.js';
47
43
 
48
44
  const TRACES_BASENAME = 'traces.ndjson';
49
- const EPIC_SIGNALS_BASENAME = 'signals.ndjson';
50
45
 
51
46
  /**
52
47
  * Async traces-file path (kept private — consumers thread through
@@ -167,7 +162,7 @@ async function appendOne(targetPath, record) {
167
162
  }
168
163
 
169
164
  /**
170
- * Append one signal record to `temp/epic-<eid>/stories/story-<sid>/signals.ndjson`.
165
+ * Append one signal record to `temp/run-<id>/stories/story-<sid>/signals.ndjson`.
171
166
  *
172
167
  * The `signal` is written verbatim — callers (detectors) own its shape
173
168
  * (kind, severity, message, etc.). The writer adds nothing. Errors are
@@ -200,42 +195,7 @@ export async function appendSignal(args) {
200
195
  }
201
196
 
202
197
  /**
203
- * Append one signal record to the per-Epic stream at
204
- * `temp/epic-<eid>/signals.ndjson` — used for Epic-scoped signals not tied
205
- * to an individual Story: the wave-window forensics kinds (`wave-start`,
206
- * `wave-complete`), notification-derived anchors (`state-transition`,
207
- * `notification.emitted`), and Epic-level `friction` (e.g.
208
- * `lifecycle-emit.js`). The retired write-only kinds `wave-tick` /
209
- * `epic-complete` are gone (Story #3909) and are NOT valid here.
210
- *
211
- * @param {{ epicId: number, signal: unknown, config?: object }} args
212
- * @returns {Promise<boolean>}
213
- */
214
- export async function appendEpicSignal(args) {
215
- const { epicId, signal, config } = args ?? {};
216
- let target;
217
- try {
218
- target = epicArtifactPath(epicId, EPIC_SIGNALS_BASENAME, config);
219
- } catch (err) {
220
- Logger.warn(
221
- `signals-writer: invalid epicId for appendEpicSignal: ${
222
- err instanceof Error ? err.message : String(err)
223
- }`,
224
- );
225
- return false;
226
- }
227
- const tagged = tagSignalSource(signal);
228
- const ok = await validateOrDrop(tagged, {
229
- epicId: Number.isInteger(epicId) ? epicId : null,
230
- config,
231
- label: 'epic signal',
232
- });
233
- if (!ok) return false;
234
- return appendOne(target, tagged);
235
- }
236
-
237
- /**
238
- * Append one trace record to `temp/epic-<eid>/stories/story-<sid>/traces.ndjson`.
198
+ * Append one trace record to `temp/run-<id>/stories/story-<sid>/traces.ndjson`.
239
199
  * Same robustness contract as `appendSignal` — never throws.
240
200
  *
241
201
  * @param {{ epicId: number, storyId: number, trace: unknown, config?: object }} args
@@ -270,7 +230,7 @@ export async function appendTrace(args) {
270
230
  * lines are skipped with a `Logger.warn`. A missing file resolves with
271
231
  * `missing: true` rather than throwing.
272
232
  *
273
- * Shared spine for `forEachLine` (per-Story stream) and `forEachEpicLine`
233
+ * Shared spine for `forEachLine`
274
234
  * (per-Epic stream) so the two readers cannot drift in their
275
235
  * malformed-line / missing-file / cb-throw handling.
276
236
  *
@@ -361,39 +321,3 @@ export async function forEachLine(epicId, storyId, cb, config) {
361
321
 
362
322
  return forEachLineIn(target, cb, 'forEachLine');
363
323
  }
364
-
365
- /**
366
- * Stream the per-Epic `signals.ndjson` line by line, invoking
367
- * `cb(parsed, lineNumber)` for each successfully parsed JSON line. This is
368
- * the read side of `appendEpicSignal` — the wave-lifecycle stream at
369
- * `temp/epic-<eid>/signals.ndjson` written by `appendEpicSignal` callers
370
- * (e.g. `lifecycle-emit`). A missing file resolves with `missing: true`
371
- * rather than throwing, mirroring the per-Story `forEachLine` contract so
372
- * the retro's unified counts scan can fold the Epic stream in alongside the
373
- * Story streams without a special-cased absence branch.
374
- *
375
- * @param {number} epicId
376
- * @param {(parsed: unknown, lineNumber: number) => unknown | Promise<unknown>} cb
377
- * @param {object} [config]
378
- * @returns {Promise<{ linesRead: number, linesParsed: number, missing: boolean }>}
379
- */
380
- export async function forEachEpicLine(epicId, cb, config) {
381
- if (typeof cb !== 'function') {
382
- Logger.warn('signals-writer: forEachEpicLine called without a callback');
383
- return { linesRead: 0, linesParsed: 0, missing: false };
384
- }
385
-
386
- let target;
387
- try {
388
- target = epicArtifactPath(epicId, EPIC_SIGNALS_BASENAME, config);
389
- } catch (err) {
390
- Logger.warn(
391
- `signals-writer: invalid epicId for forEachEpicLine: ${
392
- err instanceof Error ? err.message : String(err)
393
- }`,
394
- );
395
- return { linesRead: 0, linesParsed: 0, missing: false };
396
- }
397
-
398
- return forEachLineIn(target, cb, 'forEachEpicLine');
399
- }
@@ -4,8 +4,8 @@
4
4
  * `"consumer"` (the host project that consumes the framework via the
5
5
  * materialized `.agents/` directory).
6
6
  *
7
- * Used by `signals-writer.js#appendSignal` / `appendEpicSignal` so every
8
- * record in `temp/epic-<eid>/stories/story-<sid>/signals.ndjson` carries an
7
+ * Used by `signals-writer.js#appendSignal` so every
8
+ * record in `temp/run-<id>/stories/story-<sid>/signals.ndjson` carries an
9
9
  * authoritative `source` field, allowing downstream retro consumers to
10
10
  * route framework signals back to mandrel and keep consumer signals in
11
11
  * the host project (Epic #2547 / Story #2553).
@@ -5,7 +5,7 @@
5
5
  * PostToolUse hook entries. Resolves the active Epic + Story from
6
6
  * environment variables (`CC_EPIC_ID` / `CC_STORY_ID`), pairs Pre/Post
7
7
  * tool-call events, and appends one `kind:"trace"` NDJSON line per tool
8
- * call to `temp/epic-<eid>/stories/story-<sid>/traces.ndjson` via the
8
+ * call to `temp/run-<id>/stories/story-<sid>/traces.ndjson` via the
9
9
  * `signals-writer.appendTrace` helper.
10
10
  *
11
11
  * Robustness contract (Tech Spec #1032 §observability + §security):
@@ -48,7 +48,6 @@
48
48
 
49
49
  import { createHash } from 'node:crypto';
50
50
 
51
- import { emitHeartbeatFromHook } from './hook-heartbeat.js';
52
51
  import { appendTrace } from './signals-writer.js';
53
52
 
54
53
  /**
@@ -388,19 +387,10 @@ export async function main(event) {
388
387
  const phase = event.hook_event_name;
389
388
  if (phase === 'PreToolUse') {
390
389
  // Pre-pairing only matters for the trace-line duration, which only
391
- // the Story-scoped trace path records; slice-only context (single
392
- // delivery) needs no Pre.
390
+ // the Story-scoped trace path records.
393
391
  if (active) handlePre(event);
394
392
  } else if (phase === 'PostToolUse') {
395
393
  if (active) await handlePost(event, active);
396
- // Heartbeat OFF the token stream (Epic #4476 M5). A throttled
397
- // story.heartbeat / slice.heartbeat, keyed off the same active-Story /
398
- // active-slice env vars, so the §2e Idle Watchdog's forward-progress
399
- // signal is a free byproduct of ANY tool call — no dedicated
400
- // bookkeeping LLM turn. Best-effort and self-guarded; resolves its own
401
- // target (fires for slice context even when `active` is null because
402
- // there is no CC_STORY_ID under single delivery).
403
- emitHeartbeatFromHook();
404
394
  }
405
395
  // Any other phase is silently ignored — the hook is registered for
406
396
  // Pre/Post only; receiving anything else is a configuration error
@@ -15,7 +15,7 @@
15
15
  * hard-clamped to `[1, 8]`), and the executor spawns ONE maker-blind
16
16
  * fresh-context `Agent` critic per cluster. The cluster count is therefore
17
17
  * `ceil(totalACs / clusterCeiling)` — a deterministic fan-out width the
18
- * `deliver-epic-single.md` executor and its isolation test both key off.
18
+ * consuming executor and its isolation test both key off.
19
19
  *
20
20
  * Pure and total — inputs in, clusters out. No I/O, no throws on malformed
21
21
  * input (a non-array / empty AC set yields zero clusters — the executor then
@@ -41,7 +41,7 @@
41
41
 
42
42
  import { readFileSync } from 'node:fs';
43
43
 
44
- import { epicArtifactPath, signalsFile } from '../config/temp-paths.js';
44
+ import { runArtifactPath, signalsFile } from '../config/temp-paths.js';
45
45
 
46
46
  /** Epic-level signals stream basename (mirrors signals-writer). */
47
47
  const EPIC_SIGNALS_BASENAME = 'signals.ndjson';
@@ -231,7 +231,7 @@ export function deriveAcceptanceEvalRound({
231
231
  readFile = (p) => readFileSync(p, 'utf8'),
232
232
  signalsPathResolver = signalsFile,
233
233
  epicSignalsPathResolver = (eid, cfg) =>
234
- epicArtifactPath(eid, EPIC_SIGNALS_BASENAME, cfg),
234
+ runArtifactPath(eid, EPIC_SIGNALS_BASENAME, cfg),
235
235
  }) {
236
236
  // Epic #4475 (M4-B): single-delivery critics score AC clusters, not
237
237
  // Stories. When `clusterId` is supplied the round is counted per cluster
@@ -1,21 +1,29 @@
1
1
  /**
2
- * lib/orchestration/ceremony-routing.js — ceremony-profile + risk-routed
2
+ * lib/orchestration/ceremony-routing.js — ceremony-profile + derived-level
3
3
  * acceptance ceremony resolver.
4
4
  *
5
- * The sibling of `review-depth.js` (risk review depth) and
6
- * `audit-lens-routing.js#resolveAuditLenses` (risk audit lens): it folds the
7
- * operator ceremony profile and the planner-judged risk envelope into a
8
- * per-cluster ceremony decision for the single-delivery acceptance critic
9
- * **fresh-context spawn** vs the contract-identical **inline** critic. It
10
- * does NOT invent a new risk score and it does NOT own clustering.
5
+ * The sibling of `review-depth.js#resolveDepth`: it folds the operator ceremony
6
+ * profile and the **derived** change level into a per-cluster ceremony decision
7
+ * for the single-delivery acceptance critic — **fresh-context spawn** vs the
8
+ * contract-identical **inline** critic. It does NOT invent a new risk score and
9
+ * it does NOT own clustering.
10
+ *
11
+ * ## One derived source, two decisions (Story #4542)
12
+ *
13
+ * `derivedLevel` comes from `review-depth.js#deriveChangeLevel` — the same call
14
+ * that feeds review depth — so both ceremony decisions read one observable
15
+ * signal: does the change set touch a sensitive path registered in
16
+ * `audit-rules.json`? Previously this consumed the planner's own risk verdict,
17
+ * which meant a confident all-low self-assertion bought *less* independent
18
+ * checking than authoring nothing at all. A derived level cannot be talked down.
11
19
  *
12
20
  * ## Ceremony profiles (`delivery.routing.ceremonyProfile`)
13
21
  *
14
22
  * - `minimal` — always `inline` (skip fresh critic + sampling floor).
15
23
  * Use for tiny N=1 Stories the operator trusts.
16
- * - `standard` — risk-routed (default). Low → inline (+ sampling floor);
17
- * medium/high → fresh.
18
- * - `strict` — always `fresh` regardless of risk.
24
+ * - `standard` — level-routed (default). Low → inline (+ sampling floor);
25
+ * high → fresh.
26
+ * - `strict` — always `fresh` regardless of the derived level.
19
27
  *
20
28
  * ## The load-bearing invariant (M4-B acceptance floor — DO NOT VIOLATE)
21
29
  *
@@ -29,23 +37,22 @@
29
37
  *
30
38
  * ## Tier rules (per cluster, `standard` profile)
31
39
  *
32
- * - `high` risk → `fresh` (a fresh-context maker-blind spawn).
33
- * - `medium` risk → `fresh` (fail toward more ceremony, never less —
34
- * matches `review-depth`'s fail-to-middle).
35
- * - `low` risk → `inline` (the contract-identical inline critic),
40
+ * - `high` level → `fresh` (a sensitive path was touched — a
41
+ * fresh-context maker-blind spawn).
42
+ * - `low` level → `inline` (the contract-identical inline critic),
36
43
  * UNLESS the maker-checker sampling floor
37
44
  * selects this cluster → `fresh`.
38
- * - missing / unknown → `fresh` (fail-safe: a Story that skipped `/plan`
39
- * has no risk verdict; treat it as needing
40
- * the full fresh-context ceremony, exactly
41
- * as `review-depth` degrades to `standard`
42
- * and `deriveRiskEnvelope` degrades to
43
- * review-required).
45
+ * - missing / unknown → `fresh` (fail-safe: the diff could not be
46
+ * enumerated, so there is no evidence the
47
+ * change is unremarkable; treat it as
48
+ * needing the full fresh-context ceremony,
49
+ * exactly as `resolveDepth` degrades to
50
+ * `standard` on the same signal).
44
51
  *
45
52
  * ## Maker-checker sampling floor
46
53
  *
47
- * Even at `low` risk under `standard`, a fraction of clusters
48
- * (`freshCriticSampleRate`, default 0.2) is forced `fresh` so low risk never
54
+ * Even at a `low` derived level under `standard`, a fraction of clusters
55
+ * (`freshCriticSampleRate`, default 0.2) is forced `fresh` so a low level never
49
56
  * means zero independent checking. The selection is **deterministic** in the
50
57
  * cluster index (a fixed stride), so it is stable across re-runs and —
51
58
  * critically — never changes the cluster count: it only re-labels which of
@@ -56,7 +63,7 @@
56
63
  * `undefined` / malformed inputs degrade to `fresh` + `full` ceremony.
57
64
  *
58
65
  * @typedef {'fresh'|'inline'} CeremonyMode
59
- * @typedef {'low'|'medium'|'high'} RiskLevel
66
+ * @typedef {import('./review-depth.js').ChangeLevel} ChangeLevel
60
67
  * @typedef {'minimal'|'standard'|'strict'} CeremonyProfile
61
68
  */
62
69
 
@@ -112,11 +119,11 @@ export function sampledFresh(clusterIndex, rate) {
112
119
 
113
120
  /**
114
121
  * Resolve the acceptance ceremony for one cluster from the ceremony profile,
115
- * judged risk level, and the maker-checker sampling floor. See the module
116
- * header for the tier rules and the untouchable cluster-count invariant.
122
+ * the derived change level, and the maker-checker sampling floor. See the
123
+ * module header for the tier rules and the untouchable cluster-count invariant.
117
124
  *
118
125
  * @param {{
119
- * overallLevel?: (RiskLevel|string|null|undefined),
126
+ * derivedLevel?: (ChangeLevel|string|null|undefined),
120
127
  * clusterIndex?: (number|null|undefined),
121
128
  * freshCriticSampleRate?: (number|null|undefined),
122
129
  * ceremonyProfile?: (CeremonyProfile|string|null|undefined),
@@ -129,8 +136,8 @@ export function sampledFresh(clusterIndex, rate) {
129
136
  * }}
130
137
  */
131
138
  export function resolveCeremonyForRisk(input = {}) {
132
- const overallLevel =
133
- input && typeof input === 'object' ? input.overallLevel : undefined;
139
+ const derivedLevel =
140
+ input && typeof input === 'object' ? input.derivedLevel : undefined;
134
141
  const clusterIndex =
135
142
  input && typeof input === 'object' ? input.clusterIndex : undefined;
136
143
  const rate =
@@ -158,46 +165,37 @@ export function resolveCeremonyForRisk(input = {}) {
158
165
  };
159
166
  }
160
167
 
161
- if (overallLevel === 'high') {
162
- return {
163
- mode: 'fresh',
164
- reason: 'high-risk: fresh-context critic',
165
- sampled: false,
166
- profile,
167
- };
168
- }
169
- if (overallLevel === 'medium') {
168
+ if (derivedLevel === 'high') {
170
169
  return {
171
170
  mode: 'fresh',
172
- reason: 'medium-risk: fresh-context critic (fail toward more ceremony)',
171
+ reason: 'sensitive path touched: fresh-context critic',
173
172
  sampled: false,
174
173
  profile,
175
174
  };
176
175
  }
177
- if (overallLevel === 'low') {
176
+ if (derivedLevel === 'low') {
178
177
  if (sampledFresh(clusterIndex, rate)) {
179
178
  return {
180
179
  mode: 'fresh',
181
180
  reason:
182
- 'low-risk cluster forced fresh by the maker-checker sampling floor',
181
+ 'low-level cluster forced fresh by the maker-checker sampling floor',
183
182
  sampled: true,
184
183
  profile,
185
184
  };
186
185
  }
187
186
  return {
188
187
  mode: 'inline',
189
- reason: 'low-risk: contract-identical inline critic',
188
+ reason: 'no sensitive path touched: contract-identical inline critic',
190
189
  sampled: false,
191
190
  profile,
192
191
  };
193
192
  }
194
- // Missing / unknown / malformed risk → fail-safe fresh + full ceremony,
195
- // matching how review-depth.js and deriveRiskEnvelope degrade on an
196
- // unjudged Story.
193
+ // Missing / unknown / malformed level → fail-safe fresh + full ceremony,
194
+ // matching how resolveDepth degrades to `standard` on the same signal.
197
195
  return {
198
196
  mode: 'fresh',
199
197
  reason:
200
- 'risk absent/unknown: fail-safe fresh-context critic + full ceremony',
198
+ 'change level underivable: fail-safe fresh-context critic + full ceremony',
201
199
  sampled: false,
202
200
  profile,
203
201
  };