mandrel 2.31.0 → 2.33.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 (267) hide show
  1. package/.agents/README.md +13 -17
  2. package/.agents/agents/acceptance-critic.md +1 -2
  3. package/.agents/docs/SDLC.md +12 -9
  4. package/.agents/docs/agentrc-reference.json +63 -58
  5. package/.agents/docs/configuration.md +275 -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 +2 -2
  14. package/.agents/schemas/agentrc.schema.json +1955 -1410
  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 +16 -29
  19. package/.agents/scripts/apply-quality-bootstrap.js +27 -34
  20. package/.agents/scripts/audit-to-stories.js +160 -41
  21. package/.agents/scripts/bootstrap.js +28 -26
  22. package/.agents/scripts/check-baseline-drift.js +73 -13
  23. package/.agents/scripts/check-baseline-scope.js +362 -0
  24. package/.agents/scripts/check-dead-exports.js +9 -1
  25. package/.agents/scripts/check-gherkin-corpus.js +508 -0
  26. package/.agents/scripts/check-knip-entries.js +159 -0
  27. package/.agents/scripts/check-lifecycle-lint.js +108 -124
  28. package/.agents/scripts/check-schema-references.js +1 -1
  29. package/.agents/scripts/diagnose-friction.js +7 -4
  30. package/.agents/scripts/generate-config-docs.js +263 -171
  31. package/.agents/scripts/install-matrix-assert.js +0 -1
  32. package/.agents/scripts/lib/ITicketingProvider.js +0 -58
  33. package/.agents/scripts/lib/audit-baselines/staleness.js +6 -6
  34. package/.agents/scripts/lib/audit-baselines/trend.js +7 -8
  35. package/.agents/scripts/lib/audit-baselines/weights.js +4 -5
  36. package/.agents/scripts/lib/audit-suite/checklist-threading.js +1 -1
  37. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +81 -35
  38. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +185 -0
  39. package/.agents/scripts/lib/baselines/envelope.js +41 -60
  40. package/.agents/scripts/lib/baselines/git-base.js +30 -37
  41. package/.agents/scripts/lib/baselines/kinds/_crap-new-method-gate.js +103 -0
  42. package/.agents/scripts/lib/baselines/kinds/_crap-read.js +150 -0
  43. package/.agents/scripts/lib/baselines/kinds/crap.js +25 -65
  44. package/.agents/scripts/lib/baselines/orphan-pruner.js +233 -0
  45. package/.agents/scripts/lib/baselines/refresh-service.js +6 -8
  46. package/.agents/scripts/lib/baselines/scope-assert.js +223 -0
  47. package/.agents/scripts/lib/baselines/scope-inventory.js +314 -0
  48. package/.agents/scripts/lib/bdd-step-index.js +326 -0
  49. package/.agents/scripts/lib/bootstrap/install-ledger.js +5 -3
  50. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +4 -6
  51. package/.agents/scripts/lib/bootstrap/manifest.js +17 -40
  52. package/.agents/scripts/lib/bootstrap/project-bootstrap.js +12 -59
  53. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +62 -2
  54. package/.agents/scripts/lib/checks/loop-health.js +9 -37
  55. package/.agents/scripts/lib/child-exec.js +193 -0
  56. package/.agents/scripts/lib/cli/standard-args.js +1 -1
  57. package/.agents/scripts/lib/cli-args.js +64 -0
  58. package/.agents/scripts/lib/close-validation/gates.js +2 -2
  59. package/.agents/scripts/lib/close-validation/runner.js +3 -3
  60. package/.agents/scripts/lib/config/acceptance-eval.js +5 -52
  61. package/.agents/scripts/lib/config/commands.js +3 -5
  62. package/.agents/scripts/lib/config/explain.js +5 -7
  63. package/.agents/scripts/lib/config/gates/bundle-size.schema.js +32 -6
  64. package/.agents/scripts/lib/config/gates/coverage.schema.js +25 -5
  65. package/.agents/scripts/lib/config/gates/crap-incremental-coverage.schema.js +12 -2
  66. package/.agents/scripts/lib/config/gates/crap.schema.js +68 -23
  67. package/.agents/scripts/lib/config/gates/duplication.schema.js +29 -17
  68. package/.agents/scripts/lib/config/gates/index.js +5 -2
  69. package/.agents/scripts/lib/config/gates/lighthouse.schema.js +34 -6
  70. package/.agents/scripts/lib/config/gates/lint.schema.js +11 -2
  71. package/.agents/scripts/lib/config/gates/maintainability.schema.js +37 -15
  72. package/.agents/scripts/lib/config/gates/mutation.schema.js +15 -3
  73. package/.agents/scripts/lib/config/gates/shared.js +58 -9
  74. package/.agents/scripts/lib/config/github.js +0 -1
  75. package/.agents/scripts/lib/config/limits.js +3 -48
  76. package/.agents/scripts/lib/config/qa.js +105 -0
  77. package/.agents/scripts/lib/config/runners.js +38 -16
  78. package/.agents/scripts/lib/config/temp-paths.js +6 -5
  79. package/.agents/scripts/lib/config-settings-schema-delivery.js +245 -56
  80. package/.agents/scripts/lib/config-settings-schema-quality.js +209 -29
  81. package/.agents/scripts/lib/config-settings-schema.js +386 -39
  82. package/.agents/scripts/lib/crap-baseline-join.js +126 -9
  83. package/.agents/scripts/lib/crap-utils.js +84 -520
  84. package/.agents/scripts/lib/dead-exports-knip.js +79 -10
  85. package/.agents/scripts/lib/degraded-mode.js +2 -2
  86. package/.agents/scripts/lib/dependency-parser.js +20 -7
  87. package/.agents/scripts/lib/doc-tiers.js +3 -3
  88. package/.agents/scripts/lib/feedback-loop/graduator-core.js +46 -104
  89. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +10 -8
  90. package/.agents/scripts/lib/findings/provenance-field.js +135 -0
  91. package/.agents/scripts/lib/findings/route-finding.js +57 -8
  92. package/.agents/scripts/lib/fs-walk.js +52 -0
  93. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  94. package/.agents/scripts/lib/git-utils.js +16 -36
  95. package/.agents/scripts/lib/knip-config-resolver.js +181 -0
  96. package/.agents/scripts/lib/knip-entry-sync.js +508 -0
  97. package/.agents/scripts/lib/observability/metrics-ledger.js +1 -1
  98. package/.agents/scripts/lib/observability/runtime-friction.js +10 -0
  99. package/.agents/scripts/lib/observability/signal-validator.js +5 -85
  100. package/.agents/scripts/lib/observability/signals-writer.js +19 -62
  101. package/.agents/scripts/lib/observability/source-classifier.js +5 -7
  102. package/.agents/scripts/lib/observability/terse-result.js +3 -3
  103. package/.agents/scripts/lib/orchestration/behind-recovery.js +114 -0
  104. package/.agents/scripts/lib/orchestration/ceremony-routing.js +7 -8
  105. package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +34 -33
  106. package/.agents/scripts/lib/orchestration/code-review.js +2 -2
  107. package/.agents/scripts/lib/orchestration/complexity-gate.js +43 -161
  108. package/.agents/scripts/lib/orchestration/diff-magnitude.js +4 -4
  109. package/.agents/scripts/lib/orchestration/label-transitions.js +3 -2
  110. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +12 -38
  111. package/.agents/scripts/lib/orchestration/lifecycle/emit-merge-unlanded.js +5 -6
  112. package/.agents/scripts/lib/orchestration/plan-metrics.js +2 -3
  113. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +7 -26
  114. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +69 -5
  115. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +69 -13
  116. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +49 -0
  117. package/.agents/scripts/lib/orchestration/{lifecycle/listeners/watcher.js → pr-watch.js} +58 -208
  118. package/.agents/scripts/lib/orchestration/resolve-stories.js +77 -50
  119. package/.agents/scripts/lib/orchestration/review-providers/codex.js +1 -1
  120. package/.agents/scripts/lib/orchestration/review-providers/mi-exemptions.js +130 -0
  121. package/.agents/scripts/lib/orchestration/review-providers/native.js +30 -16
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/code-review.js +1 -1
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +37 -26
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +376 -0
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +161 -151
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +15 -3
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +10 -15
  128. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +5 -0
  129. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-override.js +157 -0
  130. package/.agents/scripts/lib/orchestration/single-story-close/phases/worktree-reap.js +0 -14
  131. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +59 -25
  132. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +20 -31
  133. package/.agents/scripts/lib/orchestration/spec-spill.js +17 -3
  134. package/.agents/scripts/lib/orchestration/story-close/phases/review-core.js +7 -6
  135. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +2 -1
  136. package/.agents/scripts/lib/orchestration/task-body-validator.js +4 -1
  137. package/.agents/scripts/lib/orchestration/ticket-lease.js +28 -127
  138. package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +116 -1
  139. package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -1
  140. package/.agents/scripts/lib/orchestration/ticket-validator.js +38 -0
  141. package/.agents/scripts/lib/orchestration/ticketing/reads.js +5 -5
  142. package/.agents/scripts/lib/orchestration/ticketing/transition.js +5 -4
  143. package/.agents/scripts/lib/orchestration/verify-tier-repair.js +107 -0
  144. package/.agents/scripts/lib/qa/coverage-verdict.js +5 -87
  145. package/.agents/scripts/lib/signals/detectors/common.js +1 -1
  146. package/.agents/scripts/lib/signals/index.js +8 -6
  147. package/.agents/scripts/lib/signals/schema.js +20 -25
  148. package/.agents/scripts/lib/signals/write.js +8 -8
  149. package/.agents/scripts/lib/story-body/footer-block.js +97 -0
  150. package/.agents/scripts/lib/story-body/story-body.js +18 -81
  151. package/.agents/scripts/lib/temp-retention.js +1 -1
  152. package/.agents/scripts/lib/templates/decomposer-prompts.js +16 -14
  153. package/.agents/scripts/lib/ticket-body-sections.js +4 -5
  154. package/.agents/scripts/lib/wave-runner/footprint.js +306 -0
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +198 -181
  156. package/.agents/scripts/lib/worktree/lifecycle/merge-reachability.js +13 -45
  157. package/.agents/scripts/lib/worktree/lifecycle/reap.js +4 -5
  158. package/.agents/scripts/lib/worktree-manager.js +2 -3
  159. package/.agents/scripts/lint-label-vocabulary.js +2 -24
  160. package/.agents/scripts/pr-watch-with-update.js +7 -5
  161. package/.agents/scripts/providers/github/blocked-by-add.js +25 -10
  162. package/.agents/scripts/providers/github/cache.js +2 -2
  163. package/.agents/scripts/providers/github/comments.js +6 -28
  164. package/.agents/scripts/providers/github/compose.js +0 -15
  165. package/.agents/scripts/providers/github/errors.js +10 -27
  166. package/.agents/scripts/providers/github/request-helpers.js +1 -2
  167. package/.agents/scripts/providers/github/sub-issues.js +10 -218
  168. package/.agents/scripts/providers/github.js +4 -7
  169. package/.agents/scripts/prune-baseline-orphans.js +181 -0
  170. package/.agents/scripts/resolve-stories.js +21 -7
  171. package/.agents/scripts/run-lint.js +61 -61
  172. package/.agents/scripts/run-test-profile.js +6 -6
  173. package/.agents/scripts/run-verify.js +48 -30
  174. package/.agents/scripts/single-story-close.js +20 -0
  175. package/.agents/scripts/single-story-init.js +12 -35
  176. package/.agents/scripts/stories-wave-tick.js +192 -9
  177. package/.agents/scripts/update-dead-exports-baseline.js +321 -0
  178. package/.agents/skills/core/gates-and-baselines/SKILL.md +2 -2
  179. package/.agents/skills/skills.index.json +1 -11
  180. package/.agents/workflows/audit-documentation.md +5 -6
  181. package/.agents/workflows/audit-to-stories.md +28 -2
  182. package/.agents/workflows/helpers/audit-lens-core.md +11 -12
  183. package/.agents/workflows/helpers/code-quality-guardrails.md +15 -14
  184. package/.agents/workflows/helpers/code-review.md +3 -8
  185. package/.agents/workflows/helpers/deliver-reference.md +30 -2
  186. package/.agents/workflows/helpers/deliver-story-reference.md +84 -16
  187. package/.agents/workflows/helpers/plan-reference.md +76 -0
  188. package/.agents/workflows/helpers/worktree-lifecycle.md +1 -2
  189. package/.agents/workflows/mandrel-update.md +10 -10
  190. package/.agents/workflows/qa-assist.md +15 -20
  191. package/.agents/workflows/qa-explore.md +9 -8
  192. package/README.md +1 -1
  193. package/docs/CHANGELOG.md +58 -0
  194. package/lib/migrations/index.js +2 -0
  195. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +127 -0
  196. package/package.json +14 -5
  197. package/.agents/schemas/lifecycle/checkpoint.written.schema.json +0 -13
  198. package/.agents/schemas/lifecycle/close-validate.end.schema.json +0 -18
  199. package/.agents/schemas/lifecycle/close-validate.start.schema.json +0 -13
  200. package/.agents/schemas/lifecycle/code-review.end.schema.json +0 -30
  201. package/.agents/schemas/lifecycle/code-review.start.schema.json +0 -12
  202. package/.agents/schemas/lifecycle/intervention.recorded.schema.json +0 -15
  203. package/.agents/schemas/lifecycle/loop.tick.schema.json +0 -20
  204. package/.agents/schemas/lifecycle/notification.emitted.schema.json +0 -18
  205. package/.agents/schemas/lifecycle/pr.created.schema.json +0 -14
  206. package/.agents/schemas/lifecycle/retro.end.schema.json +0 -16
  207. package/.agents/schemas/lifecycle/retro.start.schema.json +0 -12
  208. package/.agents/schemas/lifecycle/story.blocked.schema.json +0 -13
  209. package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +0 -17
  210. package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +0 -15
  211. package/.agents/schemas/lifecycle/story.merged.schema.json +0 -13
  212. package/.agents/scripts/check-gherkin-placeholders.js +0 -663
  213. package/.agents/scripts/check-lifecycle-doc-drift.js +0 -411
  214. package/.agents/scripts/lib/audit-suite/cli.js +0 -64
  215. package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +0 -202
  216. package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +0 -212
  217. package/.agents/scripts/lib/checks/baseline-drift-main-checkout.js +0 -104
  218. package/.agents/scripts/lib/checks/push-hook-parity.js +0 -106
  219. package/.agents/scripts/lib/checks/windows-coverage-noise-floor.js +0 -92
  220. package/.agents/scripts/lib/checks/worktree-bootstrap-env.js +0 -81
  221. package/.agents/scripts/lib/checks/worktree-residue-biome.js +0 -55
  222. package/.agents/scripts/lib/crap-baseline-index.js +0 -46
  223. package/.agents/scripts/lib/crap-utils-incremental.js +0 -113
  224. package/.agents/scripts/lib/dynamic-workflow/capability.js +0 -396
  225. package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -335
  226. package/.agents/scripts/lib/mutation/baseline-snapshot.js +0 -239
  227. package/.agents/scripts/lib/mutation/config-detector.js +0 -119
  228. package/.agents/scripts/lib/mutation/stryker-runner.js +0 -306
  229. package/.agents/scripts/lib/mutation/survivor-report.js +0 -160
  230. package/.agents/scripts/lib/observability/active-story-env.js +0 -170
  231. package/.agents/scripts/lib/observability/tool-trace-hook.js +0 -456
  232. package/.agents/scripts/lib/orchestration/acceptance-clusters.js +0 -111
  233. package/.agents/scripts/lib/orchestration/context-envelope.js +0 -277
  234. package/.agents/scripts/lib/orchestration/detectors-phase.js +0 -194
  235. package/.agents/scripts/lib/orchestration/lifecycle/bus.js +0 -309
  236. package/.agents/scripts/lib/orchestration/lifecycle/emit-loop-tick.js +0 -181
  237. package/.agents/scripts/lib/orchestration/lifecycle/ledger-writer.js +0 -229
  238. package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +0 -54
  239. package/.agents/scripts/lib/orchestration/lifecycle/trace-logger.js +0 -344
  240. package/.agents/scripts/lib/orchestration/lint-baseline-service.js +0 -114
  241. package/.agents/scripts/lib/orchestration/pr-base-guard.js +0 -37
  242. package/.agents/scripts/lib/orchestration/resolves-token.js +0 -127
  243. package/.agents/scripts/lib/orchestration/spec-section-validator.js +0 -130
  244. package/.agents/scripts/lib/orchestration/story-close/emit-blocked.js +0 -55
  245. package/.agents/scripts/lib/orchestration/story-close/phases/code-review.js +0 -211
  246. package/.agents/scripts/lib/planning-corpus.js +0 -37
  247. package/.agents/scripts/lib/qa/coverage-report.js +0 -181
  248. package/.agents/scripts/lib/qa/propose-missing-test.js +0 -95
  249. package/.agents/scripts/lib/qa/qa-context-hydrator.js +0 -217
  250. package/.agents/scripts/lib/signals/detectors/index.js +0 -14
  251. package/.agents/scripts/lib/signals/detectors/retry.js +0 -253
  252. package/.agents/scripts/lib/signals/detectors/rework.js +0 -167
  253. package/.agents/scripts/lib/signals/read.js +0 -268
  254. package/.agents/scripts/lib/signals/span-tree.js +0 -291
  255. package/.agents/scripts/lib/story-lifecycle.js +0 -194
  256. package/.agents/scripts/lib/story-plan.js +0 -379
  257. package/.agents/scripts/lib/util/phase-timer-state.js +0 -72
  258. package/.agents/scripts/lib/util/phase-timer.js +0 -163
  259. package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +0 -169
  260. package/.agents/scripts/lint-baseline.js +0 -507
  261. package/.agents/scripts/providers/github/prs.js +0 -103
  262. package/.agents/scripts/signals-view.js +0 -309
  263. package/.agents/scripts/story-plan.js +0 -370
  264. package/.agents/scripts/sync-branch-from-base.js +0 -149
  265. package/.agents/scripts/validate-docs-freshness.js +0 -314
  266. package/.agents/skills/core/diagnose-friction/SKILL.md +0 -78
  267. package/.agents/workflows/helpers/signals.md +0 -112
@@ -182,9 +182,16 @@ export function storyFootprintPaths(body, id, warn) {
182
182
  }
183
183
 
184
184
  /**
185
- * Build the DAG nodes. `dependsOn` is the union the adjacency builder already
186
- * computes (body-parsed `blocked by #N` + explicit fields), plus any native
187
- * edges threaded in via `nativeEdges`. `files` is a plain `string[]`.
185
+ * Build the DAG nodes. `dependsOn` is the **union of the two declared-edge
186
+ * channels**: the Story body's `---` footer (`blocked by #N`) and the native
187
+ * GitHub `blocked_by` relations threaded in via `nativeEdges`. `files` is a
188
+ * plain `string[]`.
189
+ *
190
+ * The body channel is footer-scoped and strict (`parseBlockedBy`, Story
191
+ * #5046) — a `blocked by #123` mention in prose no longer mints a dispatch
192
+ * gate. Only `{ id, dependsOn }` is handed to the adjacency builder, never the
193
+ * body: the edge set is decided here, once, so the builder's own body parse
194
+ * cannot re-derive a different one behind this function's back.
188
195
  *
189
196
  * @param {object[]} stories
190
197
  * @param {Map<number, number[]>} [nativeEdges]
@@ -193,7 +200,7 @@ export function storyFootprintPaths(body, id, warn) {
193
200
  */
194
201
  export function storiesToDag(stories, nativeEdges = new Map(), warn) {
195
202
  const withNative = stories.map((s) => ({
196
- ...s,
203
+ id: s.id,
197
204
  dependsOn: [
198
205
  ...new Set([
199
206
  ...parseBlockedBy(s.body ?? ''),
@@ -222,28 +229,39 @@ export function storiesToDag(stories, nativeEdges = new Map(), warn) {
222
229
  * matching no Story, foreign to the set, never satisfiable, and (because
223
230
  * foreign edges are real gates) a silent permanent wedge.
224
231
  *
225
- * Cross-repo blockers are rejected rather than matched: another repo's #4530
226
- * is not this repo's #4530, and treating it as one could satisfy a gate that
227
- * is still open.
232
+ * A cross-repo blocker is **dropped with a loud warning**, never matched:
233
+ * another repo's #4530 is not this repo's #4530, and treating it as one could
234
+ * satisfy a gate that is still open. It used to throw, which failed the WHOLE
235
+ * resolution — one Story's unsupported edge took every sibling down with it
236
+ * (Story #5046). The degrade is now scoped to the Story carrying the edge:
237
+ * its siblings resolve normally, and the operator is told, by number, which
238
+ * Story lost which edge.
228
239
  *
229
240
  * @param {unknown} data Parsed API response.
230
- * @param {{ owner: string, repo: string, issueNumber: number }} ctx
241
+ * @param {{ owner: string, repo: string, issueNumber: number, warn?: (msg: string) => void }} ctx
231
242
  * @returns {number[]}
232
243
  */
233
- export function nativeBlockedByNumbers(data, { owner, repo, issueNumber }) {
244
+ export function nativeBlockedByNumbers(
245
+ data,
246
+ { owner, repo, issueNumber, warn },
247
+ ) {
234
248
  if (!Array.isArray(data)) return [];
235
249
  const out = [];
236
250
  for (const item of data) {
237
251
  const repoUrl = item?.repository_url ?? item?.repository?.url ?? null;
238
- if (typeof repoUrl === 'string' && repoUrl.length > 0) {
239
- const expected = `/repos/${owner}/${repo}`;
240
- if (!repoUrl.endsWith(expected)) {
241
- throw new Error(
242
- `[resolve-stories] #${issueNumber} is blocked by an issue in another repository ` +
243
- `(${repoUrl}). Cross-repo dependency edges are not supported — its number cannot ` +
244
- `be matched against this repo's Stories without risking a false match.`,
245
- );
246
- }
252
+ if (
253
+ typeof repoUrl === 'string' &&
254
+ repoUrl.length > 0 &&
255
+ !repoUrl.endsWith(`/repos/${owner}/${repo}`)
256
+ ) {
257
+ warn?.(
258
+ `[resolve-stories] #${issueNumber} declares a native blocked_by edge on an issue in ` +
259
+ `another repository (${repoUrl}). Cross-repo edges are not supported — its number ` +
260
+ `cannot be matched against this repo's Stories without risking a false match, so the ` +
261
+ `edge is DROPPED for #${issueNumber} only. Its siblings resolve normally; re-declare ` +
262
+ `the ordering in this repo if #${issueNumber} must wait.`,
263
+ );
264
+ continue;
247
265
  }
248
266
  const number = Number(item?.number);
249
267
  if (Number.isInteger(number) && number > 0) out.push(number);
@@ -252,17 +270,33 @@ export function nativeBlockedByNumbers(data, { owner, repo, issueNumber }) {
252
270
  }
253
271
 
254
272
  /**
255
- * Read an issue's native `blocked_by` edges as issue numbers.
273
+ * Read an issue's native `blocked_by` edges as issue numbers, **paginated to
274
+ * exhaustion**.
275
+ *
276
+ * The read used to take the first page only, so a Story with more than a
277
+ * page of blockers silently lost every edge past the boundary — the exact
278
+ * failure this function's fail-loud contract exists to prevent, arriving
279
+ * through the one door that never raised (Story #5046). `paginate` is
280
+ * injected (the CLI passes `paginateRest`) so the lib layer stays free of a
281
+ * provider import and the page walk stays testable without a live round-trip.
256
282
  *
257
- * **Fails loud**, deliberately inverting the write path's non-fatal contract.
258
- * A dropped write-side edge is cosmetic (the ordering still lives in the
259
- * `blocked by #N` body footer); a dropped READ-side edge silently removes a
260
- * dispatch gate, so a 403 (dependencies API disabled, or a token without the
261
- * scope) would erase every native edge at once and co-dispatch the whole run
262
- * against unlanded blockers. A 404 means "no dependencies on this issue" and
263
- * is a legitimate empty result.
283
+ * **Fails loud on every non-OK read**, deliberately inverting the write path's
284
+ * non-fatal contract. A dropped write-side edge is cosmetic (the ordering
285
+ * still lives in the `blocked by #N` body footer); a dropped READ-side edge
286
+ * silently removes a dispatch gate, so one failure would erase every native
287
+ * edge at once and co-dispatch the run against unlanded blockers.
264
288
  *
265
- * @param {{ gh: object, owner: string, repo: string, issueNumber: number, parseJson: Function }} opts
289
+ * **A 404 is not an empty result.** It used to be treated as "this issue has
290
+ * no dependencies", which is how GitHub answers an issue that genuinely has
291
+ * none — but it is *also* how GitHub answers a token that cannot see the
292
+ * dependencies API at all. Reading the second as the first erases every
293
+ * native edge in the run under a mis-scoped token, silently, with a clean
294
+ * exit code. An issue with no dependencies returns `200 []`, so the empty
295
+ * case needs no 404 escape hatch and the ambiguity resolves loud.
296
+ *
297
+ * @param {{ gh: object, owner: string, repo: string, issueNumber: number,
298
+ * paginate: (gh: object, endpoint: string, opts?: object) => Promise<unknown[]>,
299
+ * warn?: (msg: string) => void }} opts
266
300
  * @returns {Promise<number[]>}
267
301
  */
268
302
  export async function readNativeBlockedBy({
@@ -270,27 +304,30 @@ export async function readNativeBlockedBy({
270
304
  owner,
271
305
  repo,
272
306
  issueNumber,
273
- parseJson,
307
+ paginate,
308
+ warn,
274
309
  }) {
275
- let result;
310
+ const endpoint = `/repos/${owner}/${repo}/issues/${issueNumber}/dependencies/blocked_by`;
311
+ let items;
276
312
  try {
277
- result = await gh.api({
278
- method: 'GET',
279
- endpoint: `/repos/${owner}/${repo}/issues/${issueNumber}/dependencies/blocked_by`,
313
+ items = await paginate(gh, endpoint, {
314
+ label: `[resolve-stories] blocked_by #${issueNumber}`,
280
315
  });
281
316
  } catch (err) {
282
317
  const detail = String(err?.message ?? err);
283
- if (/404|not found/i.test(detail)) return [];
284
318
  throw new Error(
285
319
  `[resolve-stories] Could not read native blocked_by edges for #${issueNumber}: ${detail}. ` +
286
320
  `Refusing to continue: a dropped dependency edge would silently remove a dispatch gate ` +
287
- `and co-dispatch this Story against an unlanded blocker.`,
321
+ `and co-dispatch this Story against an unlanded blocker. A 404 here is NOT "no ` +
322
+ `dependencies" (that answers 200 with an empty list) — check the token's scopes and ` +
323
+ `that the dependencies API is enabled for ${owner}/${repo}.`,
288
324
  );
289
325
  }
290
- return nativeBlockedByNumbers(parseJson(result), {
326
+ return nativeBlockedByNumbers(items, {
291
327
  owner,
292
328
  repo,
293
329
  issueNumber,
330
+ warn,
294
331
  });
295
332
  }
296
333
 
@@ -301,9 +338,6 @@ export async function readNativeBlockedBy({
301
338
  * @param {Map<number, number[]>} nativeEdges
302
339
  * @param {number[]} foreignDone Ids outside the set already satisfied.
303
340
  * @param {(msg: string) => void} [warn]
304
- * @param {object} [injectedRules] Test seam forwarded to the shape
305
- * derivation — skips the `audit-rules.json` disk read. Production callers
306
- * omit it (the real manifest, memoized per process, is the default).
307
341
  * @returns {{ kind: string, stories: object[], dag: object[], done: number[] }}
308
342
  */
309
343
  export function buildStoriesEnvelope({
@@ -311,8 +345,6 @@ export function buildStoriesEnvelope({
311
345
  nativeEdges = new Map(),
312
346
  foreignDone = [],
313
347
  warn,
314
- config,
315
- injectedRules,
316
348
  }) {
317
349
  const sorted = [...stories].sort((a, b) => a.id - b.id);
318
350
  const inSetDone = sorted.filter(isSatisfiedBlocker).map((s) => s.id);
@@ -324,8 +356,8 @@ export function buildStoriesEnvelope({
324
356
  // acceptance-critic sub-agent boots) or `subagent` (the conservative
325
357
  // default). Model-side fan-out only; close gates are untouched.
326
358
  //
327
- // `storyCount` is the premise that decides it, and it is this call site's
328
- // load-bearing argument: `inline` names the router's ONE session, so it is
359
+ // `storyCount` is the ONLY premise that decides it, and it is this call
360
+ // site's whole argument: `inline` names the router's ONE session, so it is
329
361
  // granted only to a run resolving exactly ONE Story, which has no
330
362
  // concurrent sibling to share that session with. Passing the resolved set
331
363
  // size here is therefore what makes the envelope self-consistent with the
@@ -336,19 +368,14 @@ export function buildStoriesEnvelope({
336
368
  // a caller reads for a given `--ids` list never changes as siblings land
337
369
  // mid-run. The `route::lite` label is a human-visible hint only, never the
338
370
  // control signal.
339
- stories: sorted.map(({ id, title, body, url, labels, state }) => ({
371
+ stories: sorted.map(({ id, title, url, labels, state }) => ({
340
372
  id,
341
373
  title,
342
374
  url,
343
375
  labels,
344
376
  state,
345
- dispatchMode: resolveStoryDispatchMode({
346
- body,
347
- labels,
348
- config,
349
- storyCount: sorted.length,
350
- injectedRules,
351
- }).mode,
377
+ dispatchMode: resolveStoryDispatchMode({ storyCount: sorted.length })
378
+ .mode,
352
379
  })),
353
380
  dag: storiesToDag(sorted, nativeEdges, warn),
354
381
  done: [...new Set([...inSetDone, ...foreignDone])].sort((a, b) => a - b),
@@ -19,7 +19,7 @@
19
19
  * `provider: native` explicitly; the probe is the only thing that
20
20
  * routes between "configured backend present" and "configured backend
21
21
  * missing". The adapter never consults a GitHub provider — the
22
- * orchestrator owns posting/upserting and the lifecycle bus.
22
+ * orchestrator owns posting and upserting.
23
23
  *
24
24
  * @typedef {import('./types.js').Finding} Finding
25
25
  * @typedef {import('./types.js').ReviewInput} ReviewInput
@@ -0,0 +1,130 @@
1
+ /**
2
+ * review-providers/mi-exemptions.js — the maintainability gate's exemption list,
3
+ * as the native review provider reads it.
4
+ *
5
+ * `delivery.quality.gates.maintainability.ignoreGlobs` is the one declared
6
+ * answer to "which files is the maintainability index meaningless for" —
7
+ * declarative schema blobs, generated code, vendored trees. Every other MI
8
+ * consumer already honours it: the baseline writer excludes those files
9
+ * (`update-maintainability-baseline.js`), so the `check-baselines.js` ratchet
10
+ * and the pre-merge MI advisory never see them either.
11
+ *
12
+ * `review-providers/native.js` did not, and the split produced a live
13
+ * contradiction on Story #5007 / PR #5022: the ratchet PASSED while the review
14
+ * lens raised a **critical blocker** on three exempted
15
+ * `config-settings-schema*.js` modules in the same close run. Because a critical
16
+ * finding halts `single-story-close.js` before auto-merge, the only way to land
17
+ * legitimate work was to merge the PR by hand. A gate that must be
18
+ * hand-bypassed to ship is not a gate.
19
+ *
20
+ * This module is that reconciliation, kept separate from the provider so the
21
+ * exemption concern has one home and the provider keeps one reason to change.
22
+ */
23
+
24
+ import { getQuality } from '../../config/quality.js';
25
+ import { resolveConfig } from '../../config-resolver.js';
26
+ import { isIgnoredByGlobs } from '../../maintainability-utils.js';
27
+
28
+ /**
29
+ * Read `delivery.quality.gates.maintainability.ignoreGlobs` so a review's
30
+ * maintainability dimension scores the same file set the ratchet does.
31
+ *
32
+ * Best-effort and total. A config that cannot be resolved yields `[]`, which
33
+ * scores every changed JS file. That direction is deliberate: degrading to
34
+ * "score everything" can only produce an advisory the operator must read,
35
+ * whereas degrading to "score nothing" would silently retire the dimension.
36
+ *
37
+ * @param {{ resolveConfigFn?: typeof resolveConfig, getQualityFn?: typeof getQuality }} [deps]
38
+ * @returns {string[]} minimatch patterns; `[]` when unset or unresolvable.
39
+ */
40
+ export function resolveMaintainabilityIgnoreGlobs({
41
+ resolveConfigFn = resolveConfig,
42
+ getQualityFn = getQuality,
43
+ } = {}) {
44
+ try {
45
+ const globs = getQualityFn(resolveConfigFn())?.maintainability?.ignoreGlobs;
46
+ return Array.isArray(globs) ? globs.slice() : [];
47
+ } catch {
48
+ return [];
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Pure: split a changed-file list into the set to score and the set the
54
+ * maintainability gate exempts.
55
+ *
56
+ * Matching funnels through `maintainability-utils.js#isIgnoredByGlobs` — the
57
+ * declared single source of truth for how the MI scorer decides a file is
58
+ * ignored — so an exempted file is excluded here by exactly the same rule that
59
+ * kept it out of the baseline. Re-implementing the match with a local
60
+ * `minimatch` call is what let the two surfaces disagree in the first place.
61
+ *
62
+ * Module-local: {@link scopeMaintainabilityFiles} is the single door, and its
63
+ * `scored` / `ignored` split is where this behaviour is observable.
64
+ *
65
+ * @param {string[]} files
66
+ * @param {string[]} ignoreGlobs
67
+ * @param {string} cwd root for repo-relative glob resolution
68
+ * @returns {{ scored: string[], ignored: string[] }}
69
+ */
70
+ function partitionByIgnoreGlobs(files, ignoreGlobs, cwd) {
71
+ if (!Array.isArray(ignoreGlobs) || ignoreGlobs.length === 0) {
72
+ return { scored: files, ignored: [] };
73
+ }
74
+ const scored = [];
75
+ const ignored = [];
76
+ for (const relPath of files) {
77
+ if (isIgnoredByGlobs(relPath, ignoreGlobs, cwd)) ignored.push(relPath);
78
+ else scored.push(relPath);
79
+ }
80
+ return { scored, ignored };
81
+ }
82
+
83
+ /**
84
+ * Render the operator-facing notice naming the files the gate exempted, or
85
+ * `null` when nothing was exempted so the caller can `if` past the log call.
86
+ *
87
+ * The notice exists because silence is ambiguous: an operator reading a review
88
+ * that says nothing about three changed schema modules cannot tell "scored and
89
+ * healthy" from "never scored".
90
+ *
91
+ * Module-local: reached through {@link scopeMaintainabilityFiles}'s `notice`.
92
+ *
93
+ * @param {string[]|undefined} ignoredFiles
94
+ * @returns {string|null}
95
+ */
96
+ function formatExemptionNotice(ignoredFiles) {
97
+ const files = Array.isArray(ignoredFiles) ? ignoredFiles : [];
98
+ if (files.length === 0) return null;
99
+ return (
100
+ `[native-review] Maintainability: ${files.length} changed file(s) exempt via ` +
101
+ `delivery.quality.gates.maintainability.ignoreGlobs — not scored: ${files.join(', ')}.`
102
+ );
103
+ }
104
+
105
+ /**
106
+ * Resolve the exemption list and split a review's changed-file set into the
107
+ * paths whose maintainability should be scored and the paths the gate exempts.
108
+ *
109
+ * This is the one door the native provider uses: it keeps the resolve → match →
110
+ * report sequence here rather than spread across the provider, so the provider
111
+ * carries no knowledge of how an exemption is decided.
112
+ *
113
+ * @param {string[]} changedFiles
114
+ * @param {{
115
+ * cwd: string,
116
+ * resolveIgnoreGlobsFn?: typeof resolveMaintainabilityIgnoreGlobs,
117
+ * }} opts
118
+ * @returns {{ scored: string[], ignored: string[], notice: string|null }}
119
+ */
120
+ export function scopeMaintainabilityFiles(
121
+ changedFiles,
122
+ { cwd, resolveIgnoreGlobsFn = resolveMaintainabilityIgnoreGlobs } = {},
123
+ ) {
124
+ const { scored, ignored } = partitionByIgnoreGlobs(
125
+ changedFiles,
126
+ resolveIgnoreGlobsFn(),
127
+ cwd,
128
+ );
129
+ return { scored, ignored, notice: formatExemptionNotice(ignored) };
130
+ }
@@ -7,14 +7,24 @@
7
7
  *
8
8
  * 1. Diffs `headRef` against `baseRef` to enumerate changed files.
9
9
  * 2. Runs scoped lint (biome + markdownlint) over the changed surface.
10
- * 3. Computes per-file maintainability reports for changed JS files.
10
+ * 3. Computes per-file maintainability reports for changed JS files, minus
11
+ * the files the maintainability gate exempts (see below).
11
12
  * 4. Maps each signal to a `Finding` with a `severity` ∈ {critical, high,
12
13
  * medium, suggestion}.
13
14
  *
14
- * The adapter does NOT post to GitHub, does NOT render a markdown body,
15
- * and does NOT consult the lifecycle bus. Those concerns belong to
16
- * `runCodeReview()` (which calls the renderer + the structured-comment
17
- * upserter) and the listener chain.
15
+ * **The maintainability dimension honours the gate's exemption list**, read via
16
+ * [`mi-exemptions.js`](mi-exemptions.js) see that module for why this
17
+ * provider disagreeing with the ratchet was a live delivery blocker. Exempted
18
+ * files are named on the log rather than silently dropped.
19
+ *
20
+ * The lint dimension is deliberately NOT filtered through the same list:
21
+ * lint carries its own exclusion surface (biome's `files.includes`,
22
+ * `.markdownlintignore`), and a quality-gate ignore glob makes no claim about
23
+ * whether a file should parse or format cleanly.
24
+ *
25
+ * The adapter does NOT post to GitHub and does NOT render a markdown body.
26
+ * Those concerns belong to `runCodeReview()`, which calls the renderer + the
27
+ * structured-comment upserter.
18
28
  *
19
29
  * Construction is intentionally zero-arg so the factory can instantiate
20
30
  * it without threading config through every call. Per-invocation config
@@ -53,6 +63,10 @@ import {
53
63
  } from '../../observability/runtime-friction.js';
54
64
  import { PROJECT_ROOT } from '../../project-root.js';
55
65
  import { transpileIfNeeded } from '../../transpile.js';
66
+ import {
67
+ resolveMaintainabilityIgnoreGlobs,
68
+ scopeMaintainabilityFiles,
69
+ } from './mi-exemptions.js';
56
70
  import {
57
71
  parseLintOutput,
58
72
  partitionFilesForLint,
@@ -389,16 +403,6 @@ export function buildLintFindings(lintSummary) {
389
403
  return findings;
390
404
  }
391
405
 
392
- function _emptyResults() {
393
- return {
394
- totalFiles: 0,
395
- jsFiles: 0,
396
- maintainability: [],
397
- criticalFindings: [],
398
- mediumFindings: [],
399
- };
400
- }
401
-
402
406
  async function runLintPhase({
403
407
  scopeLint,
404
408
  changedFiles,
@@ -467,6 +471,7 @@ function buildLintDegradations(lintSummary) {
467
471
  * analyzeChangedFilesFn?: typeof analyzeChangedFiles,
468
472
  * buildLintFindingsFn?: typeof buildLintFindings,
469
473
  * emitToolDegradationFn?: typeof emitRuntimeFriction,
474
+ * resolveIgnoreGlobsFn?: typeof resolveMaintainabilityIgnoreGlobs,
470
475
  * logger?: { info?: Function, warn?: Function, error?: Function },
471
476
  * scopeLint?: 'changed-only'|'off',
472
477
  * }} [deps]
@@ -479,6 +484,10 @@ export function createNativeProvider(deps = {}) {
479
484
  analyzeChangedFilesFn = analyzeChangedFiles,
480
485
  buildLintFindingsFn = buildLintFindings,
481
486
  emitToolDegradationFn = emitRuntimeFriction,
487
+ // The maintainability-gate exemption seam. The resolution itself — gate-key
488
+ // read and fail-open — is unit-tested in `mi-exemptions.js`; this dep is
489
+ // here so a provider test can pin the WIRING without a config on disk.
490
+ resolveIgnoreGlobsFn = resolveMaintainabilityIgnoreGlobs,
482
491
  logger,
483
492
  scopeLint = 'changed-only',
484
493
  } = deps;
@@ -551,7 +560,12 @@ export function createNativeProvider(deps = {}) {
551
560
  logger?.info?.(
552
561
  `[native-review] Analyzing ${changedFiles.length} changed file(s)...`,
553
562
  );
554
- const results = await analyzeChangedFilesFn(changedFiles, {
563
+ const mi = scopeMaintainabilityFiles(changedFiles, {
564
+ cwd: PROJECT_ROOT,
565
+ resolveIgnoreGlobsFn,
566
+ });
567
+ if (mi.notice) logger?.info?.(mi.notice);
568
+ const results = await analyzeChangedFilesFn(mi.scored, {
555
569
  headRef,
556
570
  gitSpawnFn,
557
571
  });
@@ -21,7 +21,7 @@
21
21
  * the caller raises that to a thrown error so auto-merge is not enabled.
22
22
  *
23
23
  * Delegates the `runCodeReview` invocation to `runStoryReviewCore`
24
- * (exported from `story-close/phases/code-review.js`) so the close path
24
+ * (exported from `story-close/phases/review-core.js`) so the close path
25
25
  * shares a single invocation pattern (Story #3653). Review depth needs no
26
26
  * input here: it is derived from this Story's own diff inside `runCodeReview`
27
27
  * (Story #4542).
@@ -96,6 +96,7 @@ import {
96
96
  readPrMergeState as defaultReadPrMergeState,
97
97
  } from '../../../single-story/confirm-merge.js';
98
98
  import { pollUntil } from '../../../util/poll-loop.js';
99
+ import { applyBehindUpdate } from '../../behind-recovery.js';
99
100
  import {
100
101
  emitMergeFlipFailed as defaultEmitMergeFlipFailed,
101
102
  MERGED_FLIP_FAILED_BLOCK_CLASS,
@@ -635,7 +636,15 @@ async function blockOnUnlanded({
635
636
  /**
636
637
  * Bring a BEHIND PR up to date, bounded by `updateAttempts`. Best-effort:
637
638
  * a failed update is not itself a terminal — the next poll re-reads the
638
- * real state and lets the normal classification decide.
639
+ * real state and lets the normal classification decide, which is why a
640
+ * failed attempt still counts against the wait's tick.
641
+ *
642
+ * The BEHIND / budget / did-it-land decision itself lives in the shared
643
+ * {@link applyBehindUpdate} (Story #5006) — the CI-watch loop in
644
+ * `lib/orchestration/pr-watch.js` runs the same one. This wrapper supplies
645
+ * the merge wait's probe source, its `gh` facade (bounded by
646
+ * {@link withGhTimeout}, so a wedged child cannot strand an unattended
647
+ * async-mode wait), and its operator wording.
639
648
  *
640
649
  * @returns {Promise<boolean>} whether an update was actually attempted.
641
650
  */
@@ -648,31 +657,33 @@ async function maybeUpdateBehindPr({
648
657
  ghTimeoutMs = MERGE_WAIT_GH_TIMEOUT_MS,
649
658
  progress,
650
659
  }) {
651
- if (probe.mergeStateStatus !== 'BEHIND') return false;
652
- if (updatesUsed >= updateAttempts) {
653
- progress?.(
654
- 'CONFIRM',
655
- `⚠️ PR #${prNumber} is BEHIND but the update budget (${updateAttempts}) is spent — not updating again.`,
656
- );
657
- return false;
658
- }
659
- try {
660
- await withGhTimeout(
661
- (gh ?? defaultGh).pr.updateBranch(prNumber),
662
- ghTimeoutMs,
663
- `gh pr update-branch ${prNumber}`,
664
- );
665
- progress?.(
666
- 'CONFIRM',
667
- `⏫ PR #${prNumber} was BEHIND its base — updated (attempt ${updatesUsed + 1}/${updateAttempts}).`,
668
- );
669
- } catch (err) {
670
- progress?.(
671
- 'CONFIRM',
672
- `⚠️ gh pr update-branch failed (continuing): ${err?.message ?? err}`,
673
- );
674
- }
675
- return true;
660
+ const recovery = await applyBehindUpdate({
661
+ mergeStateStatus: probe.mergeStateStatus,
662
+ updatesUsed,
663
+ maxUpdates: updateAttempts,
664
+ updateBranch: () =>
665
+ withGhTimeout(
666
+ (gh ?? defaultGh).pr.updateBranch(prNumber),
667
+ ghTimeoutMs,
668
+ `gh pr update-branch ${prNumber}`,
669
+ ),
670
+ onBudgetSpent: () =>
671
+ progress?.(
672
+ 'CONFIRM',
673
+ `⚠️ PR #${prNumber} is BEHIND but the update budget (${updateAttempts}) is spent — not updating again.`,
674
+ ),
675
+ onUpdated: () =>
676
+ progress?.(
677
+ 'CONFIRM',
678
+ `⏫ PR #${prNumber} was BEHIND its base — updated (attempt ${updatesUsed + 1}/${updateAttempts}).`,
679
+ ),
680
+ onUpdateFailed: (detail) =>
681
+ progress?.(
682
+ 'CONFIRM',
683
+ `⚠️ gh pr update-branch failed (continuing): ${detail}`,
684
+ ),
685
+ });
686
+ return recovery.attempted;
676
687
  }
677
688
 
678
689
  /**