mandrel 2.31.0 → 2.32.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 (250) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +4 -4
  4. package/.agents/docs/agentrc-reference.json +61 -57
  5. package/.agents/docs/configuration.md +274 -227
  6. package/.agents/docs/execution-reference.md +13 -14
  7. package/.agents/docs/quality-gates.md +195 -23
  8. package/.agents/instructions.md +2 -5
  9. package/.agents/rules/git-conventions-reference.md +27 -27
  10. package/.agents/rules/git-conventions.md +4 -2
  11. package/.agents/rules/known-tooling-behavior.md +66 -30
  12. package/.agents/rules/testing-standards.md +35 -71
  13. package/.agents/runtime-deps.json +0 -1
  14. package/.agents/schemas/agentrc.schema.json +1939 -1400
  15. package/.agents/schemas/lifecycle/README.md +21 -14
  16. package/.agents/schemas/lifecycle/ledger-record.schema.json +76 -22
  17. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  18. package/.agents/scripts/README.md +7 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/bootstrap.js +28 -26
  21. package/.agents/scripts/check-baseline-drift.js +73 -13
  22. package/.agents/scripts/check-baseline-scope.js +362 -0
  23. package/.agents/scripts/check-dead-exports.js +9 -1
  24. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  25. package/.agents/scripts/check-knip-entries.js +136 -0
  26. package/.agents/scripts/check-lifecycle-lint.js +36 -112
  27. package/.agents/scripts/check-schema-references.js +1 -1
  28. package/.agents/scripts/diagnose-friction.js +7 -4
  29. package/.agents/scripts/generate-config-docs.js +263 -171
  30. package/.agents/scripts/install-matrix-assert.js +0 -1
  31. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  32. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  33. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  34. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  35. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +0 -1
  37. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  38. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  39. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  40. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  41. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  42. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  43. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  44. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  45. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  46. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  47. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  48. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  49. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  50. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  51. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  52. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  53. package/.agents/scripts/lib/child-exec.js +193 -0
  54. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  55. package/.agents/scripts/lib/cli-args.js +64 -0
  56. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  57. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  58. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  59. package/.agents/scripts/lib/config/commands.js +3 -5
  60. package/.agents/scripts/lib/config/explain.js +5 -7
  61. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  62. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  63. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  64. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  65. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  66. package/.agents/scripts/lib/config/gates/index.js +5 -2
  67. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  68. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  69. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  70. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  71. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  72. package/.agents/scripts/lib/config/github.js +0 -1
  73. package/.agents/scripts/lib/config/limits.js +3 -48
  74. package/.agents/scripts/lib/config/qa.js +105 -0
  75. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  76. package/.agents/scripts/lib/config-settings-schema-delivery.js +237 -56
  77. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  78. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  79. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  80. package/.agents/scripts/lib/crap-utils.js +84 -520
  81. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  82. package/.agents/scripts/lib/degraded-mode.js +2 -2
  83. package/.agents/scripts/lib/doc-tiers.js +3 -3
  84. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  85. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  86. package/.agents/scripts/lib/fs-walk.js +52 -0
  87. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  88. package/.agents/scripts/lib/git-utils.js +16 -36
  89. package/.agents/scripts/lib/knip-entry-sync.js +469 -0
  90. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  91. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  92. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  93. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  94. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  95. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  96. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  97. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  98. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  99. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  100. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  101. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  102. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  103. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  104. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  105. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  106. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +6 -0
  107. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +0 -1
  108. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  109. package/.agents/scripts/lib/orchestration/resolve-stories.js +5 -15
  110. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  111. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  112. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  113. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  114. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  115. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  116. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  117. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  118. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  119. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  120. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  121. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  122. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  123. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  124. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  125. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  126. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  127. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  128. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  129. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  130. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  131. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  132. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  133. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  134. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  135. package/.agents/scripts/lib/signals/index.js +8 -6
  136. package/.agents/scripts/lib/signals/schema.js +20 -25
  137. package/.agents/scripts/lib/signals/write.js +8 -8
  138. package/.agents/scripts/lib/story-body/story-body.js +12 -59
  139. package/.agents/scripts/lib/temp-retention.js +1 -1
  140. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  141. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  142. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  143. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  144. package/.agents/scripts/lib/worktree-manager.js +2 -3
  145. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  146. package/.agents/scripts/pr-watch-with-update.js +7 -5
  147. package/.agents/scripts/providers/github/cache.js +2 -2
  148. package/.agents/scripts/providers/github/comments.js +6 -28
  149. package/.agents/scripts/providers/github/compose.js +0 -15
  150. package/.agents/scripts/providers/github/errors.js +10 -27
  151. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  152. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  153. package/.agents/scripts/providers/github.js +4 -7
  154. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  155. package/.agents/scripts/resolve-stories.js +0 -2
  156. package/.agents/scripts/run-lint.js +61 -61
  157. package/.agents/scripts/run-test-profile.js +6 -6
  158. package/.agents/scripts/run-verify.js +48 -30
  159. package/.agents/scripts/single-story-close.js +20 -0
  160. package/.agents/scripts/single-story-init.js +12 -35
  161. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  162. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  163. package/.agents/skills/skills.index.json +1 -11
  164. package/.agents/workflows/audit-documentation.md +5 -6
  165. package/.agents/workflows/audit-to-stories.md +2 -2
  166. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  167. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  168. package/.agents/workflows/helpers/code-review.md +3 -8
  169. package/.agents/workflows/helpers/deliver-reference.md +2 -1
  170. package/.agents/workflows/helpers/deliver-story-reference.md +27 -16
  171. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  172. package/.agents/workflows/mandrel-update.md +10 -10
  173. package/.agents/workflows/qa-assist.md +15 -20
  174. package/.agents/workflows/qa-explore.md +9 -8
  175. package/README.md +1 -1
  176. package/docs/CHANGELOG.md +42 -0
  177. package/lib/migrations/index.js +2 -0
  178. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  179. package/package.json +12 -3
  180. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  181. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  182. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  183. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  184. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  185. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  186. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  187. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  188. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  189. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  190. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  191. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  192. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  193. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  194. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  195. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  196. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  197. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  198. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  199. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  200. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  201. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  202. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  203. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  204. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  205. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  206. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  207. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  208. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  209. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  210. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  211. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  212. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  213. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  214. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  215. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  216. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  217. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  218. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  219. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  220. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  221. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  222. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  223. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  224. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  225. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  226. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  227. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  228. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  229. package/.agents/scripts/lib/planning-corpus.js +0 -37
  230. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  231. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  232. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  233. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  234. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  235. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  236. package/.agents/scripts/lib/signals/read.js +0 -268
  237. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  238. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  239. package/.agents/scripts/lib/story-plan.js +0 -379
  240. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  241. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  242. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  243. package/.agents/scripts/lint-baseline.js +0 -507
  244. package/.agents/scripts/providers/github/prs.js +0 -103
  245. package/.agents/scripts/signals-view.js +0 -309
  246. package/.agents/scripts/story-plan.js +0 -370
  247. package/.agents/scripts/sync-branch-from-base.js +0 -149
  248. package/.agents/scripts/validate-docs-freshness.js +0 -314
  249. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  250. package/.agents/workflows/helpers/signals.md +0 -112
@@ -1,20 +1,21 @@
1
1
  /**
2
- * Append-only signals/trace writer (Epic #1030 Story #1041).
2
+ * Append-only signals writer (Epic #1030 Story #1041).
3
3
  *
4
4
  * Centralizes the per-(epic, story) NDJSON streams under
5
- * `temp/run-<id>/stories/story-<sid>/signals.ndjson` (and a sibling
6
- * `traces.ndjson` for trace-shaped records). Detector modules and the
7
- * runtime trace hook all funnel through this writer so the on-disk
8
- * shape stays under one schema and one set of robustness guarantees.
5
+ * `temp/run-<id>/stories/story-<sid>/signals.ndjson`. Every live emitter
6
+ * funnels through this writer so the on-disk shape stays under one schema
7
+ * and one set of robustness guarantees. The sibling `traces.ndjson` stream
8
+ * and its `appendTrace` entry point went in Story #5003 with the tool-trace
9
+ * hook that was its only producer.
9
10
  *
10
11
  * Robustness contract (Tech Spec #1032 §observability):
11
12
  * - **Best-effort.** Every entry point swallows fs / JSON failures
12
13
  * after logging via `Logger.warn`. Observability MUST NOT take down
13
14
  * the runner — a failed write is a missing signal, not a halted
14
15
  * wave.
15
- * - **No buffering.** Each `appendSignal` / `appendTrace` opens the
16
- * target file, writes one newline-terminated JSON line, and closes.
17
- * The Tech Spec explicitly forbids in-process buffering: detectors
16
+ * - **No buffering.** Each `appendSignal` opens the target file, writes
17
+ * one newline-terminated JSON line, and closes.
18
+ * The Tech Spec explicitly forbids in-process buffering: emitters
18
19
  * fire from inside per-Story sub-agents that may exit abruptly, and
19
20
  * a buffered tail would silently disappear on `process.exit`.
20
21
  * - **Lazy directory creation.** The first write to a fresh Story
@@ -42,23 +43,11 @@ import {
42
43
  STANDALONE_DIRNAME,
43
44
  STORIES_DIRNAME,
44
45
  signalsFile,
45
- storyTempDir,
46
46
  } from '../config/temp-paths.js';
47
47
  import { Logger } from '../Logger.js';
48
- import { recordSignalReject, validateSignal } from './signal-validator.js';
48
+ import { validateSignal } from './signal-validator.js';
49
49
  import { classifySignalSource } from './source-classifier.js';
50
50
 
51
- const TRACES_BASENAME = 'traces.ndjson';
52
-
53
- /**
54
- * Async traces-file path (kept private — consumers thread through
55
- * `appendTrace`). Mirrors `signalsFile` but with the `traces.ndjson`
56
- * sibling so the analyzer can scan signals and traces independently.
57
- */
58
- function tracesFile(eid, sid, config) {
59
- return path.join(storyTempDir(eid, sid, config), TRACES_BASENAME);
60
- }
61
-
62
51
  /**
63
52
  * Best-effort decoration of a signal record with a `source` field
64
53
  * (`"framework"` or `"consumer"`) produced by `classifyPathSource`.
@@ -111,21 +100,23 @@ function tagSignalSource(signal) {
111
100
  /**
112
101
  * Validate a record against the canonical `signal-event.schema.json`
113
102
  * before it is appended. On failure the record is **dropped** (never
114
- * appended), a `Logger.warn` names the violating field, and the per-Epic
115
- * reject tally is incremented under the Epic temp tree. Never throws —
103
+ * appended) and a `Logger.warn` names the violating field. Never throws
116
104
  * the writer's best-effort contract is preserved.
117
105
  *
106
+ * Story #5003 removed the persisted per-Epic reject tally this used to
107
+ * increment: v2 Stories are standalone (`epicId` is always null), so the
108
+ * tally was never written and its only reader could never see one.
109
+ *
118
110
  * @param {unknown} record
119
- * @param {{ epicId?: number|null, config?: object, label: string }} ctx
120
- * @returns {Promise<boolean>} true when the record is valid (safe to append).
111
+ * @param {string} label
112
+ * @returns {boolean} true when the record is valid (safe to append).
121
113
  */
122
- async function validateOrDrop(record, { epicId, config, label }) {
114
+ function validateOrDrop(record, label) {
123
115
  const { valid, violatingField, message } = validateSignal(record);
124
116
  if (valid) return true;
125
117
  Logger.warn(
126
118
  `signals-writer: dropping schema-invalid ${label} record — violating field '${violatingField}' (${message}).`,
127
119
  );
128
- await recordSignalReject({ epicId, config, field: violatingField });
129
120
  return false;
130
121
  }
131
122
 
@@ -189,44 +180,10 @@ export async function appendSignal(args) {
189
180
  return false;
190
181
  }
191
182
  const tagged = tagSignalSource(signal);
192
- const ok = await validateOrDrop(tagged, {
193
- epicId: Number.isInteger(epicId) ? epicId : null,
194
- config,
195
- label: 'signal',
196
- });
197
- if (!ok) return false;
183
+ if (!validateOrDrop(tagged, 'signal')) return false;
198
184
  return appendOne(target, tagged);
199
185
  }
200
186
 
201
- /**
202
- * Append one trace record to `temp/run-<id>/stories/story-<sid>/traces.ndjson`.
203
- * Same robustness contract as `appendSignal` — never throws.
204
- *
205
- * @param {{ epicId: number, storyId: number, trace: unknown, config?: object }} args
206
- * @returns {Promise<boolean>}
207
- */
208
- export async function appendTrace(args) {
209
- const { epicId, storyId, trace, config } = args ?? {};
210
- let target;
211
- try {
212
- target = tracesFile(epicId, storyId, config);
213
- } catch (err) {
214
- Logger.warn(
215
- `signals-writer: invalid epicId/storyId for appendTrace: ${
216
- err instanceof Error ? err.message : String(err)
217
- }`,
218
- );
219
- return false;
220
- }
221
- const ok = await validateOrDrop(trace, {
222
- epicId: Number.isInteger(epicId) ? epicId : null,
223
- config,
224
- label: 'trace',
225
- });
226
- if (!ok) return false;
227
- return appendOne(target, trace);
228
- }
229
-
230
187
  /**
231
188
  * Stream any NDJSON `target` file line by line, invoking
232
189
  * `cb(parsed, lineNumber)` for each successfully parsed JSON line.
@@ -88,13 +88,14 @@ const FRAMEWORK_SCRIPT_BASENAMES = Object.freeze([
88
88
  'check-action-pinning.js',
89
89
  'check-arch-cycles.js',
90
90
  'check-baseline-drift.js',
91
+ 'check-baseline-scope.js',
91
92
  'check-baselines.js',
92
93
  'check-context-budget.js',
93
94
  'check-cyclomatic.js',
94
95
  'check-dead-exports.js',
95
96
  'check-doc-links.js',
96
- 'check-gherkin-placeholders.js',
97
- 'check-lifecycle-doc-drift.js',
97
+ 'check-gherkin-corpus.js',
98
+ 'check-knip-entries.js',
98
99
  'check-lifecycle-lint.js',
99
100
  'check-schema-references.js',
100
101
  'check-test-temp-hygiene.js',
@@ -117,7 +118,6 @@ const FRAMEWORK_SCRIPT_BASENAMES = Object.freeze([
117
118
  'generate-workflows-doc.js',
118
119
  'git-cleanup.js',
119
120
  'install-matrix-assert.js',
120
- 'lint-baseline.js',
121
121
  'lint-issue-body.js',
122
122
  'lint-label-vocabulary.js',
123
123
  'mandrel-update-preflight.js',
@@ -130,6 +130,7 @@ const FRAMEWORK_SCRIPT_BASENAMES = Object.freeze([
130
130
  'post-structured-comment.js',
131
131
  'pr-watch-with-update.js',
132
132
  'provision-git-hooks.js',
133
+ 'prune-baseline-orphans.js',
133
134
  'quality-preview.js',
134
135
  'quality-watch.js',
135
136
  'resolve-doc-tiers.js',
@@ -140,24 +141,21 @@ const FRAMEWORK_SCRIPT_BASENAMES = Object.freeze([
140
141
  'run-test-profile.js',
141
142
  'run-tests.js',
142
143
  'run-verify.js',
143
- 'signals-view.js',
144
144
  'single-story-close.js',
145
145
  'single-story-confirm-merge.js',
146
146
  'single-story-init.js',
147
147
  'stories-wave-tick.js',
148
- 'story-plan.js',
149
148
  'sync-agentrc.js',
150
- 'sync-branch-from-base.js',
151
149
  'sync-claude-agents.js',
152
150
  'sync-claude-commands.js',
153
151
  'test-isolate.js',
154
152
  'test-wrapper.js',
155
153
  'update-coverage-baseline.js',
156
154
  'update-crap-baseline.js',
155
+ 'update-dead-exports-baseline.js',
157
156
  'update-duplication-baseline.js',
158
157
  'update-maintainability-baseline.js',
159
158
  'update-ticket-state.js',
160
- 'validate-docs-freshness.js',
161
159
  'validate-skills.js',
162
160
  ]);
163
161
 
@@ -8,9 +8,9 @@ import { Logger } from '../Logger.js';
8
8
  * Terse hot-path result emission (Story #4685).
9
9
  *
10
10
  * The orchestration CLIs an agent invokes on every delivery turn
11
- * (`single-story-init`, `single-story-close`, `single-story-confirm-merge`,
12
- * `sync-branch-from-base`, the close `emit-blocked` path) historically dumped
13
- * their whole result object to stdout as pretty-printed JSON:
11
+ * (`single-story-init`, `single-story-close`, `single-story-confirm-merge`)
12
+ * historically dumped their whole result object to stdout as pretty-printed
13
+ * JSON:
14
14
  *
15
15
  * --- STORY CLOSE RESULT ---
16
16
  * { ... every field, 2-space indented ... }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * behind-recovery.js — the one bounded `BEHIND` → `gh pr update-branch`
3
+ * recovery decision (Story #5006).
4
+ *
5
+ * Two loops watch an open PR and both must fast-forward it when GitHub
6
+ * reports `mergeStateStatus: BEHIND`:
7
+ *
8
+ * - the CI-watch loop (`pr-watch.js#watchPrToTerminal`),
9
+ * which probes with a synchronous `gh pr view` spawn and re-enters its
10
+ * poll loop after each update; and
11
+ * - the merge wait (`single-story-close/phases/confirm-merge.js`), which
12
+ * probes with an async `gh` facade and simply re-reads on the next tick.
13
+ *
14
+ * They had two copies of the same three-way decision — *is it BEHIND, is
15
+ * there update budget left, did the update land* — differing only in
16
+ * incidental ordering, which is exactly the shape that drifts. This module
17
+ * owns the decision; the callers keep their own probe source, their own
18
+ * timing model, and their own operator-facing wording (supplied as the
19
+ * `on*` callbacks).
20
+ *
21
+ * Deliberately NOT owned here: probing `mergeStateStatus`, counting the
22
+ * budget across iterations, and deciding what a caller does next with the
23
+ * verdict. The helper is a single bounded step, not a loop.
24
+ */
25
+
26
+ /**
27
+ * The `gh pr view --json mergeStateStatus` value that licenses a recovery.
28
+ * Module-private: both call sites reach it through {@link applyBehindUpdate},
29
+ * so exporting it would only add a symbol the reachability ratchet scores
30
+ * dead.
31
+ */
32
+ const BEHIND_MERGE_STATE = 'BEHIND';
33
+
34
+ /**
35
+ * Normalise an `updateBranch()` settlement into `{ ok, detail }`.
36
+ *
37
+ * Both callers' invokers signal failure differently — the merge wait's `gh`
38
+ * facade **throws**, the watcher's `spawnSync` port **resolves** a non-zero
39
+ * status — so both shapes are accepted: a throw is a failure, and so is a
40
+ * resolved object carrying `ok === false`. Anything else resolved (including
41
+ * `undefined`) is a success, which is what a facade that only throws on
42
+ * failure returns.
43
+ *
44
+ * @param {() => Promise<unknown>} updateBranch
45
+ * @returns {Promise<{ ok: boolean, detail: string }>}
46
+ */
47
+ async function settleUpdate(updateBranch) {
48
+ try {
49
+ const result = await updateBranch();
50
+ if (result && typeof result === 'object' && result.ok === false) {
51
+ return { ok: false, detail: String(result.detail ?? 'update-failed') };
52
+ }
53
+ return { ok: true, detail: '' };
54
+ } catch (err) {
55
+ return { ok: false, detail: String(err?.message ?? err) };
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Apply at most one bounded `gh pr update-branch` to a PR that is BEHIND its
61
+ * base.
62
+ *
63
+ * The order of the two guards is load-bearing: **not-BEHIND is checked
64
+ * first**, so the budget-spent callback fires only for a PR that actually
65
+ * wanted an update. Reversing them would announce an exhausted budget on
66
+ * every poll of a perfectly up-to-date PR.
67
+ *
68
+ * @param {object} args
69
+ * @param {string|undefined|null} args.mergeStateStatus As probed by the
70
+ * caller. Anything other than `BEHIND` (including a degraded/unknown probe)
71
+ * is a no-op — an unreadable merge state must never license a write.
72
+ * @param {number} [args.updatesUsed=0] Updates already applied this wait.
73
+ * @param {number} [args.maxUpdates=0] Hard cap on updates per wait.
74
+ * @param {() => Promise<unknown>} args.updateBranch Invoker that issues the
75
+ * fast-forward. See {@link settleUpdate} for the accepted settlements.
76
+ * @param {(args: { maxUpdates: number }) => void} [args.onBudgetSpent]
77
+ * @param {(args: { updatesUsed: number, maxUpdates: number }) => void} [args.onUpdated]
78
+ * @param {(detail: string) => void} [args.onUpdateFailed]
79
+ * @returns {Promise<{
80
+ * attempted: boolean,
81
+ * updated: boolean,
82
+ * outcome: 'not-behind'|'budget-spent'|'updated'|'update-failed',
83
+ * }>}
84
+ * `attempted` is true whenever the invoker ran — a failed update still
85
+ * counts as an attempt (the merge wait treats it as a consumed tick and
86
+ * re-reads the real state next poll). `updated` is true only when the
87
+ * fast-forward actually landed, which is the signal a caller needs before
88
+ * invalidating a terminal check outcome and re-polling.
89
+ */
90
+ export async function applyBehindUpdate({
91
+ mergeStateStatus,
92
+ updatesUsed = 0,
93
+ maxUpdates = 0,
94
+ updateBranch,
95
+ onBudgetSpent,
96
+ onUpdated,
97
+ onUpdateFailed,
98
+ }) {
99
+ if (mergeStateStatus !== BEHIND_MERGE_STATE) {
100
+ return { attempted: false, updated: false, outcome: 'not-behind' };
101
+ }
102
+ if (updatesUsed >= maxUpdates) {
103
+ onBudgetSpent?.({ maxUpdates });
104
+ return { attempted: false, updated: false, outcome: 'budget-spent' };
105
+ }
106
+
107
+ const settled = await settleUpdate(updateBranch);
108
+ if (!settled.ok) {
109
+ onUpdateFailed?.(settled.detail);
110
+ return { attempted: true, updated: false, outcome: 'update-failed' };
111
+ }
112
+ onUpdated?.({ updatesUsed, maxUpdates });
113
+ return { attempted: true, updated: true, outcome: 'updated' };
114
+ }
@@ -28,12 +28,11 @@
28
28
  * ## The load-bearing invariant (M4-B acceptance floor — DO NOT VIOLATE)
29
29
  *
30
30
  * Risk-routing chooses fresh-vs-inline **PER CLUSTER**. It NEVER changes the
31
- * cluster COUNT. The cluster count is `ceil(totalACs / clusterCeiling)` with
32
- * the non-disableable `[1, 8]` clamp, owned entirely by
33
- * `acceptance-clusters.js` and untouched here. A low-risk Story still gets one
34
- * verdict per cluster just possibly authored inline instead of by a fresh
35
- * sub-agent. This module takes the cluster index as an INPUT and returns a
36
- * decision for that one cluster; it has no way to add or remove clusters.
31
+ * cluster COUNT the caller owns clustering and hands this module a cluster
32
+ * index. A low-risk Story still gets one verdict per cluster — just possibly
33
+ * authored inline instead of by a fresh sub-agent. This module takes the
34
+ * cluster index as an INPUT and returns a decision for that one cluster; it
35
+ * has no way to add or remove clusters.
37
36
  *
38
37
  * ## One verdict-owner per cluster (Story #4723)
39
38
  *
@@ -127,8 +126,8 @@ function normalizeCeremonyProfile(value) {
127
126
  * 2·stride, …) is forced fresh, yielding ≈`r` of clusters fresh. `r <= 0`
128
127
  * disables the floor (no cluster forced); `r >= 1` forces every cluster.
129
128
  *
130
- * @param {number} clusterIndex Zero-based cluster position (from the fixed
131
- * `ceil(totalACs / clusterCeiling)` fan-out — an INPUT, never mutated here).
129
+ * @param {number} clusterIndex Zero-based cluster position (from the
130
+ * caller-owned fan-out — an INPUT, never mutated here).
132
131
  * @param {number} rate Sampling rate, already clamped into [0, 1] by
133
132
  * `getDeliveryRouting`.
134
133
  * @returns {boolean} `true` when the floor forces this cluster fresh.
@@ -32,7 +32,6 @@
32
32
  * an unrecognized check state.
33
33
  */
34
34
 
35
- import { spawnSync } from 'node:child_process';
36
35
  import {
37
36
  existsSync,
38
37
  mkdirSync,
@@ -41,6 +40,7 @@ import {
41
40
  writeFileSync,
42
41
  } from 'node:fs';
43
42
  import path from 'node:path';
43
+ import { spawnChild } from '../child-exec.js';
44
44
  import { resolveConfig } from '../config-resolver.js';
45
45
  import { Logger } from '../Logger.js';
46
46
  import { createProvider } from '../provider-factory.js';
@@ -119,14 +119,13 @@ function ciDigestPaths({ storyId = null, tempRoot, cwd }) {
119
119
  * returns an empty tail when the run id is unknown or `gh` errors.
120
120
  * Injected into `writeCiDigest` so tests never shell out.
121
121
  */
122
- function ghRunLogTail({ runId, cwd, spawnFn = spawnSync, maxLines = 40 }) {
122
+ function ghRunLogTail({ runId, cwd, spawnFn, maxLines = 40 }) {
123
123
  if (!runId) return '';
124
- const result = spawnFn('gh', ['run', 'view', String(runId), '--log-failed'], {
125
- cwd,
126
- encoding: 'utf-8',
127
- shell: false,
128
- maxBuffer: 10 * 1024 * 1024,
129
- });
124
+ const result = spawnChild(
125
+ 'gh',
126
+ ['run', 'view', String(runId), '--log-failed'],
127
+ { run: spawnFn, cwd },
128
+ );
130
129
  const out = (result.stdout ?? '').trim();
131
130
  if (out.length === 0) return '';
132
131
  const lines = out.split('\n');
@@ -142,17 +141,12 @@ function ghRunLogTail({ runId, cwd, spawnFn = spawnSync, maxLines = 40 }) {
142
141
  *
143
142
  * @returns {{ runId: string|null, url: string|null }}
144
143
  */
145
- function resolveFailingCheckRun({
146
- prRef,
147
- checkName,
148
- cwd,
149
- spawnFn = spawnSync,
150
- }) {
151
- const result = spawnFn('gh', ['pr', 'checks', prRef, '--json', 'name,link'], {
152
- cwd,
153
- encoding: 'utf-8',
154
- shell: false,
155
- });
144
+ function resolveFailingCheckRun({ prRef, checkName, cwd, spawnFn }) {
145
+ const result = spawnChild(
146
+ 'gh',
147
+ ['pr', 'checks', prRef, '--json', 'name,link'],
148
+ { run: spawnFn, cwd },
149
+ );
156
150
  try {
157
151
  const parsed = JSON.parse((result.stdout ?? '').trim() || '[]');
158
152
  const entry = Array.isArray(parsed)
@@ -172,15 +166,18 @@ function resolveFailingCheckRun({
172
166
  * `null` when `gh` fails or the payload is unparseable; callers treat an
173
167
  * unresolvable head as unverifiable, never as "changed".
174
168
  *
175
- * @param {{ prRef: string, cwd: string, spawnFn?: typeof spawnSync }} opts
169
+ * @param {{ prRef: string, cwd: string, spawnFn?: Function }} opts
176
170
  * @returns {string|null}
177
171
  */
178
- export function resolvePrHeadSha({ prRef, cwd, spawnFn = spawnSync }) {
179
- const result = spawnFn('gh', ['pr', 'view', prRef, '--json', 'headRefOid'], {
180
- cwd,
181
- encoding: 'utf-8',
182
- shell: false,
183
- });
172
+ export function resolvePrHeadSha({ prRef, cwd, spawnFn }) {
173
+ const result = spawnChild(
174
+ 'gh',
175
+ ['pr', 'view', prRef, '--json', 'headRefOid'],
176
+ {
177
+ run: spawnFn,
178
+ cwd,
179
+ },
180
+ );
184
181
  if ((result?.status ?? 1) !== 0) return null;
185
182
  try {
186
183
  const parsed = JSON.parse((result.stdout ?? '').trim() || '{}');
@@ -204,18 +201,17 @@ const NOT_ARMED = /not enabled|isn't enabled|is not set|no auto-?merge/i;
204
201
  * Disarm GitHub native auto-merge for the PR — the race-free response to the
205
202
  * first red. Never throws.
206
203
  *
207
- * @param {{ prRef: string, cwd: string, spawnFn?: typeof spawnSync }} opts
204
+ * @param {{ prRef: string, cwd: string, spawnFn?: Function }} opts
208
205
  * @returns {{ disarmed: boolean, alreadyUnarmed: boolean, detail: string }}
209
206
  * `disarmed` is true when the PR is (now) un-armed; `alreadyUnarmed`
210
207
  * distinguishes "there was nothing armed" from an executed disarm.
211
208
  */
212
- export function disarmAutoMerge({ prRef, cwd, spawnFn = spawnSync }) {
209
+ export function disarmAutoMerge({ prRef, cwd, spawnFn }) {
213
210
  let result;
214
211
  try {
215
- result = spawnFn('gh', ['pr', 'merge', prRef, '--disable-auto'], {
212
+ result = spawnChild('gh', ['pr', 'merge', prRef, '--disable-auto'], {
213
+ run: spawnFn,
216
214
  cwd,
217
- encoding: 'utf-8',
218
- shell: false,
219
215
  });
220
216
  } catch (err) {
221
217
  return {
@@ -362,6 +358,9 @@ function renderDigestMarkdown(digest, failures) {
362
358
  * @param {string} opts.prRef
363
359
  * @param {Function} [opts.checkRunFn]
364
360
  * @param {Function} [opts.logTailFn]
361
+ * @param {Function} [opts.spawnFn] Child runner handed to the two default
362
+ * `gh` probes, so their real bodies can be exercised without shelling out.
363
+ * Ignored when `checkRunFn` / `logTailFn` are replaced outright.
365
364
  * @returns {{ jsonPath: string, mdPath: string } | null}
366
365
  */
367
366
  export function writeCiDigest({
@@ -374,12 +373,14 @@ export function writeCiDigest({
374
373
  prRef,
375
374
  checkRunFn = resolveFailingCheckRun,
376
375
  logTailFn = ghRunLogTail,
376
+ spawnFn,
377
377
  }) {
378
378
  const paths = ciDigestPaths({ storyId, tempRoot, cwd });
379
379
  if (!paths) return null;
380
380
  const primary = failures[0] ?? { name: 'unknown', outcome: 'failure' };
381
- const checkRun = checkRunFn({ prRef, checkName: primary.name, cwd }) ?? {};
382
- const logTail = logTailFn({ runId: checkRun.runId, cwd });
381
+ const checkRun =
382
+ checkRunFn({ prRef, checkName: primary.name, cwd, spawnFn }) ?? {};
383
+ const logTail = logTailFn({ runId: checkRun.runId, cwd, spawnFn });
383
384
  const previous = readCiDigest({ storyId, tempRoot, cwd });
384
385
  const digest = {
385
386
  storyId: paths.scope.id,
@@ -15,8 +15,8 @@
15
15
  *
16
16
  * v2.0.0 removed the Epic tier. The Epic-scope envelope and the
17
17
  * Epic-scoped `code-review.start` / `.end` lifecycle emits (whose schema
18
- * requires `epicId`) went with it; Story scope is the only scope and the
19
- * module no longer touches the lifecycle bus.
18
+ * requires `epicId`) went with it; Story scope is the only scope. Story #5024
19
+ * deleted those schemas along with the bus that was their only publish path.
20
20
  *
21
21
  * Public API:
22
22
  * - `runCodeReview({ ticketId, headRef, provider, logger, ... })` →