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,21 +1,19 @@
1
1
  /**
2
2
  * GitHub Provider — SubIssueGateway.
3
3
  *
4
- * Owns the native GitHub Sub-Issues link surface (the GraphQL `subIssues`
5
- * field on an Issue, plus the add/remove mutations) along with the
6
- * `reconcileSubIssueLinks` walker that backfills missing parent/child
7
- * links across an Epic's child set.
4
+ * Owns the **read** side of the native GitHub Sub-Issues surface: the
5
+ * GraphQL `subIssues` field on an Issue, paginated into a child-number
6
+ * list. Story #5008 removed the write side (`addSubIssue`,
7
+ * `removeSubIssue`) and the `reconcileSubIssueLinks` walker along with the
8
+ * rest of the Epic-hierarchy provider surface — v2 delivery is Story-only,
9
+ * so nothing establishes or repairs parent/child links any more.
8
10
  *
9
- * Extracted from `../github.js` in Story #2462 / Task #2480. Public
10
- * surface on `GitHubProvider` is unchanged — `addSubIssue`,
11
- * `removeSubIssue`, and `reconcileSubIssueLinks` all delegate here.
11
+ * Extracted from `../github.js` in Story #2462 / Task #2480. The read path
12
+ * still reaches `GitHubProvider` as `_getNativeSubIssues`, and
13
+ * `IssuesGateway` calls it directly through its `getNativeSubIssues` hook.
12
14
  *
13
- * Constructed with `{ ghGraphql, hooks, classifyGithubError, cache, queries }`:
15
+ * Constructed with `{ ghGraphql, cache, classifyGithubError }`:
14
16
  * - `ghGraphql(query, variables, opts)` — bound `_ghGraphql` from the parent.
15
- * - `hooks.getTicket(id, opts)` — bound to the TicketGateway.
16
- * - `hooks.getTickets(parentId)` — bound to the TicketGateway (for the reconciler).
17
- * - `hooks.primeTicketCache(tickets)` — bound to the TicketGateway (reconciler priming).
18
- * - `hooks.invalidateTicket(id)` — bound to the TicketGateway (post-mutation invalidate).
19
17
  * - `cache.primeIfAbsent(ticket)` — bound to the shared cache (native-walk priming).
20
18
  *
21
19
  * The gateway holds **no** transport state of its own — every call goes
@@ -26,25 +24,14 @@
26
24
  */
27
25
 
28
26
  import { Logger } from '../../lib/Logger.js';
29
- import { concurrentMap } from '../../lib/util/concurrent-map.js';
30
27
  import {
31
- ADD_SUB_ISSUE_MUTATION,
32
28
  classifyGithubError as defaultClassifyGithubError,
33
- REMOVE_SUB_ISSUE_MUTATION,
34
29
  SUB_ISSUES_QUERY,
35
30
  withTransientRetry,
36
31
  } from './errors.js';
37
32
  import { subIssueNodeToTicket } from './mappers.js';
38
33
  import { defaultRetryWarn } from './request-helpers.js';
39
34
 
40
- // Concurrency + retry budgets — preserved from `../github.js` so dispatch
41
- // fan-out and sub-issue reconciliation keep their established shapes.
42
- const SUB_ISSUE_RECONCILE_CONCURRENCY = 4;
43
- const SUB_ISSUE_RETRY_MAX_ATTEMPTS = 6;
44
- const SUB_ISSUE_RETRY_BASE_DELAY_MS = 1000;
45
- const SUB_ISSUE_RETRY_MAX_DELAY_MS = 30000;
46
- const SUB_ISSUE_RETRY_JITTER_MS = 500;
47
-
48
35
  // Story #2852: cap the native sub-issue cursor walk so a runaway pagination
49
36
  // (e.g. an upstream API regression that never sets `hasNextPage = false`)
50
37
  // fails fast instead of looping forever. 50 cursor pages × 100 nodes per
@@ -55,24 +42,16 @@ export class SubIssueGateway {
55
42
  /**
56
43
  * @param {{
57
44
  * ghGraphql: (query: string, variables?: object, opts?: object) => Promise<object>,
58
- * hooks?: {
59
- * getTicket?: (id: number, opts?: object) => Promise<object>,
60
- * getTickets?: (parentId: number) => Promise<object[]>,
61
- * primeTicketCache?: (tickets: object[]) => void,
62
- * invalidateTicket?: (id: number) => void,
63
- * },
64
45
  * cache?: { primeIfAbsent: (ticket: object) => void },
65
46
  * classifyGithubError?: (err: unknown) => string,
66
47
  * }} deps
67
48
  */
68
49
  constructor({
69
50
  ghGraphql,
70
- hooks = {},
71
51
  cache,
72
52
  classifyGithubError = defaultClassifyGithubError,
73
53
  } = {}) {
74
54
  this._ghGraphql = ghGraphql;
75
- this._hooks = hooks;
76
55
  this._cache = cache;
77
56
  this._classify = classifyGithubError;
78
57
  }
@@ -132,191 +111,4 @@ export class SubIssueGateway {
132
111
  }
133
112
  return childIds;
134
113
  }
135
-
136
- /**
137
- * Establish the native sub-issue link between `parentNumber` and the
138
- * child identified by `childNodeId`. Retries on transient errors with
139
- * jittered exponential backoff before re-throwing.
140
- */
141
- async addSubIssue(
142
- parentNumber,
143
- childNodeId,
144
- opts = { replaceParent: false },
145
- ) {
146
- const getTicket = this._hooks.getTicket;
147
- if (typeof getTicket !== 'function') {
148
- throw new Error(
149
- '[SubIssueGateway] addSubIssue requires a getTicket hook',
150
- );
151
- }
152
- const parentTicket = await getTicket(parentNumber);
153
- let lastErr;
154
- for (let attempt = 0; attempt < SUB_ISSUE_RETRY_MAX_ATTEMPTS; attempt++) {
155
- try {
156
- const result = await this._ghGraphql(
157
- ADD_SUB_ISSUE_MUTATION,
158
- {
159
- parentId: parentTicket.nodeId,
160
- subIssueId: childNodeId,
161
- replaceParent: opts.replaceParent,
162
- },
163
- { headers: { 'GraphQL-Features': 'sub_issues' } },
164
- );
165
- // Sub-issue link mutates the parent's sub-issue list. Invalidate so
166
- // the next `getTicket(parentNumber)` re-fetches a coherent view.
167
- if (typeof this._hooks.invalidateTicket === 'function') {
168
- this._hooks.invalidateTicket(parentNumber);
169
- }
170
- return result;
171
- } catch (err) {
172
- lastErr = err;
173
- const category = this._classify(err);
174
- const isFinalAttempt = attempt === SUB_ISSUE_RETRY_MAX_ATTEMPTS - 1;
175
- if (category !== 'transient' || isFinalAttempt) throw err;
176
- const base = Math.min(
177
- SUB_ISSUE_RETRY_MAX_DELAY_MS,
178
- SUB_ISSUE_RETRY_BASE_DELAY_MS * 2 ** attempt,
179
- );
180
- const delay =
181
- base + Math.floor(Math.random() * SUB_ISSUE_RETRY_JITTER_MS);
182
- Logger.warn(
183
- `[GitHubProvider] sub-issue link transient error for parent #${parentNumber} (attempt ${attempt + 1}/${SUB_ISSUE_RETRY_MAX_ATTEMPTS}); retrying in ${delay}ms: ${err.message}`,
184
- );
185
- await new Promise((r) => setTimeout(r, delay));
186
- }
187
- }
188
- throw lastErr;
189
- }
190
-
191
- async removeSubIssue(parentNumber, subIssueNumber) {
192
- const getTicket = this._hooks.getTicket;
193
- if (typeof getTicket !== 'function') {
194
- throw new Error(
195
- '[SubIssueGateway] removeSubIssue requires a getTicket hook',
196
- );
197
- }
198
- const parentTicket = await getTicket(parentNumber);
199
- const childTicket = await getTicket(subIssueNumber);
200
- const result = await this._ghGraphql(
201
- REMOVE_SUB_ISSUE_MUTATION,
202
- { parentId: parentTicket.nodeId, subIssueId: childTicket.nodeId },
203
- { headers: { 'GraphQL-Features': 'sub_issues' } },
204
- );
205
- if (typeof this._hooks.invalidateTicket === 'function') {
206
- this._hooks.invalidateTicket(parentNumber);
207
- this._hooks.invalidateTicket(subIssueNumber);
208
- }
209
- return result;
210
- }
211
-
212
- /**
213
- * Walk every child of `epicId` whose body footer carries `parent: #N` and
214
- * verify the native sub-issue link is present. Re-establish missing links
215
- * via `addSubIssue` (which retries internally). Idempotent.
216
- */
217
- async reconcileSubIssueLinks(epicId) {
218
- const PARENT_RE = /(?:^|\n)parent:\s*#(\d+)/;
219
- const getTicket = this._hooks.getTicket;
220
- const getTickets = this._hooks.getTickets;
221
- if (typeof getTicket !== 'function' || typeof getTickets !== 'function') {
222
- throw new Error(
223
- '[SubIssueGateway] reconcileSubIssueLinks requires getTicket + getTickets hooks',
224
- );
225
- }
226
- const allChildren = await getTickets(epicId);
227
- const parentByChild = new Map();
228
- for (const child of allChildren) {
229
- const match = (child.body ?? '').match(PARENT_RE);
230
- if (!match) continue;
231
- parentByChild.set(child.id, Number.parseInt(match[1], 10));
232
- }
233
-
234
- const childrenByParent = new Map();
235
- for (const [childId, parentId] of parentByChild) {
236
- if (!childrenByParent.has(parentId)) childrenByParent.set(parentId, []);
237
- childrenByParent.get(parentId).push(childId);
238
- }
239
-
240
- let alreadyLinked = 0;
241
- let reconciled = 0;
242
- let failed = 0;
243
- const failures = [];
244
-
245
- const parentEntries = Array.from(childrenByParent.entries());
246
-
247
- await concurrentMap(
248
- parentEntries,
249
- async ([parentId, childIds]) => {
250
- let parent;
251
- try {
252
- parent = await getTicket(parentId);
253
- } catch (err) {
254
- for (const childId of childIds) {
255
- failures.push({ parentId, childId, reason: err.message });
256
- }
257
- failed += childIds.length;
258
- return;
259
- }
260
- if (!parent?.nodeId) {
261
- for (const childId of childIds) {
262
- failures.push({
263
- parentId,
264
- childId,
265
- reason: 'parent missing nodeId',
266
- });
267
- }
268
- failed += childIds.length;
269
- return;
270
- }
271
-
272
- const linked = new Set(
273
- await this.getNativeSubIssues(parent.nodeId, parentId),
274
- );
275
-
276
- await concurrentMap(
277
- childIds,
278
- async (childId) => {
279
- if (linked.has(childId)) {
280
- alreadyLinked++;
281
- return;
282
- }
283
- let childTicket;
284
- try {
285
- childTicket = await getTicket(childId);
286
- } catch (err) {
287
- failures.push({ parentId, childId, reason: err.message });
288
- failed++;
289
- return;
290
- }
291
- if (!childTicket?.nodeId) {
292
- failures.push({
293
- parentId,
294
- childId,
295
- reason: 'child missing nodeId',
296
- });
297
- failed++;
298
- return;
299
- }
300
- try {
301
- await this.addSubIssue(parentId, childTicket.nodeId);
302
- reconciled++;
303
- } catch (err) {
304
- failures.push({ parentId, childId, reason: err.message });
305
- failed++;
306
- }
307
- },
308
- { concurrency: SUB_ISSUE_RECONCILE_CONCURRENCY },
309
- );
310
- },
311
- { concurrency: SUB_ISSUE_RECONCILE_CONCURRENCY },
312
- );
313
-
314
- return {
315
- totalExpected: parentByChild.size,
316
- alreadyLinked,
317
- reconciled,
318
- failed,
319
- failures,
320
- };
321
- }
322
114
  }
@@ -19,8 +19,10 @@
19
19
  * module.
20
20
  *
21
21
  * Gateway map: tickets, sub-issues, comments, labels, branch-protection,
22
- * merge-methods, prs, project-board, issues (epics + sub-tickets +
23
- * branch probe + raw GraphQL).
22
+ * merge-methods, project-board, issues (epics + sub-tickets + branch probe
23
+ * + raw GraphQL). Story #5008 retired the `prs` gateway — `single-story-close`
24
+ * drives `gh pr create` directly — along with the sub-issue write surface and
25
+ * the repo-wide recent-comments feed.
24
26
  */
25
27
 
26
28
  import { createGh, gh as defaultGh } from '../lib/gh-exec.js';
@@ -125,16 +127,11 @@ const DELEGATIONS = [
125
127
  ['updateTicket', 'tickets.updateTicket'],
126
128
  ['_applyLabelMutations', 'tickets._applyLabelMutations'],
127
129
  ['_getNativeSubIssues', 'subIssues.getNativeSubIssues'],
128
- ['addSubIssue', 'subIssues.addSubIssue'],
129
- ['removeSubIssue', 'subIssues.removeSubIssue'],
130
- ['reconcileSubIssueLinks', 'subIssues.reconcileSubIssueLinks'],
131
- ['getRecentComments', 'comments.getRecentComments'],
132
130
  ['getTicketComments', 'comments.getTicketComments'],
133
131
  ['deleteComment', 'comments.deleteComment'],
134
132
  ['postComment', 'comments.postComment'],
135
133
  ['getBranchProtection', 'branchProtection.getBranchProtection'],
136
134
  ['setBranchProtection', 'branchProtection.setBranchProtection'],
137
- ['createPullRequest', 'pullRequests.createPullRequest'],
138
135
  ['ensureLabels', 'labels.ensureLabels'],
139
136
  ['_reconcileLabelsPresence', 'labels._reconcileLabelsPresence'],
140
137
  ['getMergeMethods', 'mergeMethods.getMergeMethods'],
@@ -0,0 +1,181 @@
1
+ #!/usr/bin/env node
2
+
3
+ // .agents/scripts/prune-baseline-orphans.js — Story #5012.
4
+ //
5
+ // Delete the baseline rows that are provably inert, and nothing else.
6
+ //
7
+ // This is the cheap remedy that makes `check-baseline-scope.js` fair. A gate
8
+ // that hard-fails on a stale row is only defensible while clearing one costs a
9
+ // command rather than a full re-score: the honest alternative — re-derive the
10
+ // whole baseline — spends a coverage run or a full-tree MI pass to express a
11
+ // deletion, which is why stale rows accumulate instead.
12
+ //
13
+ // Two row classes qualify, both decidable without measuring anything:
14
+ //
15
+ // - `absent` — the row's file is gone from disk.
16
+ // - `out-of-scope` — the file is still there, but the gate's own
17
+ // `targetDirs` / `ignoreGlobs` no longer match it.
18
+ //
19
+ // The pruner never adds a row, never restamps `generatedAt` (a fresh stamp
20
+ // over unmeasured rows is the exact failure an age check exists to catch), and
21
+ // recomputes `rollup` through the kind's own arithmetic so the pruned envelope
22
+ // still validates against its schema. An unreadable scope config degrades to
23
+ // orphan-only pruning rather than treating unknown scope as empty scope.
24
+ //
25
+ // Exit codes:
26
+ // 0 nothing to prune (or, without `--check`, the prune was written)
27
+ // 1 `--check` found rows that would be pruned
28
+ // 2 the pruner could not run
29
+
30
+ // Fail-fast if the framework's runtime deps are not installed — must be the
31
+ // first import so the check runs before any third-party-importing sibling
32
+ // module is evaluated (Story #3432).
33
+ import './lib/runtime-deps/ensure-installed.js';
34
+ import { PRUNABLE_KINDS, runPrune } from './lib/baselines/orphan-pruner.js';
35
+ import { runAsCli } from './lib/cli-utils.js';
36
+ import { getQuality } from './lib/config/quality.js';
37
+ import { resolveConfig } from './lib/config-resolver.js';
38
+
39
+ const EXIT_CLEAN = 0;
40
+ const EXIT_WOULD_PRUNE = 1;
41
+ const EXIT_CANNOT_RUN = 2;
42
+
43
+ const HELP = {
44
+ invocation:
45
+ 'node .agents/scripts/prune-baseline-orphans.js [--check] [--kind <kind>] [--json]',
46
+ summary:
47
+ "Remove baseline rows whose file is gone from disk or has left the gate's scope. Never adds a row, never restamps generatedAt, never re-scores.",
48
+ flags: [
49
+ ['--check', 'Report what would be pruned, write nothing, exit 1 if any.'],
50
+ ['--kind <kind>', 'Prune one kind only (repeatable). Default: all.'],
51
+ ['--json', 'Emit the report as JSON instead of text.'],
52
+ ['--cwd <dir>', 'Repository root to prune. Default: process.cwd().'],
53
+ ],
54
+ notes: [
55
+ 'Exit codes:\n 0 clean, or pruned\n 1 --check found prunable rows\n 2 the pruner could not run',
56
+ ],
57
+ };
58
+
59
+ /**
60
+ * Parse argv into an options bag. An unknown flag is an error, never a silent
61
+ * no-op — a typo'd `--dry-run` must not read as "write the files".
62
+ *
63
+ * @param {string[]} argv
64
+ * @returns {{ kinds: string[], check: boolean, json: boolean, cwd: string }}
65
+ */
66
+ export function parseArgs(argv = []) {
67
+ const out = { kinds: [], check: false, json: false, cwd: null };
68
+ let i = 0;
69
+ while (i < argv.length) {
70
+ const arg = argv[i];
71
+ const value = argv[i + 1];
72
+ i += 1;
73
+ if (arg === '--check') out.check = true;
74
+ else if (arg === '--json') out.json = true;
75
+ else if (arg === '--kind') {
76
+ out.kinds.push(value);
77
+ i += 1;
78
+ } else if (arg === '--cwd') {
79
+ out.cwd = value;
80
+ i += 1;
81
+ } else throw new Error(`unknown flag "${arg}" (try --help)`);
82
+ }
83
+ const unknown = out.kinds.filter((k) => !PRUNABLE_KINDS.includes(k));
84
+ if (unknown.length > 0) {
85
+ throw new Error(
86
+ `unknown --kind ${unknown.join(', ')}; expected one of ${PRUNABLE_KINDS.join(', ')}`,
87
+ );
88
+ }
89
+ if (out.kinds.length === 0) out.kinds = [...PRUNABLE_KINDS];
90
+ out.cwd = out.cwd ?? process.cwd();
91
+ return out;
92
+ }
93
+
94
+ /**
95
+ * Render one kind's outcome as indented text lines.
96
+ *
97
+ * @param {object} entry
98
+ * @param {boolean} check
99
+ * @returns {string[]}
100
+ */
101
+ function renderKind(entry, check) {
102
+ if (!entry.present) return [];
103
+ if (entry.skipped)
104
+ return [` - ${entry.kind}: skipped (${entry.skipReason})`];
105
+ const suffix = entry.degraded
106
+ ? ` [orphan-only: ${entry.degradedReason}]`
107
+ : '';
108
+ if (entry.removed.length === 0) {
109
+ return [` - ${entry.kind}: clean${suffix}`];
110
+ }
111
+ const verb = check ? 'would prune' : 'pruned';
112
+ return [
113
+ ` - ${entry.kind}: ${verb} ${entry.removed.length} row(s)${suffix}`,
114
+ ...entry.removed.map((row) => ` · ${row.reason}: ${row.path}`),
115
+ ];
116
+ }
117
+
118
+ /**
119
+ * Render the whole report as text.
120
+ *
121
+ * @param {object} report
122
+ * @returns {string}
123
+ */
124
+ export function formatReport(report) {
125
+ const verb = report.check ? 'would prune' : 'pruned';
126
+ const lines = [
127
+ `[prune-baseline-orphans] ${verb} ${report.removedCount} row(s) across ` +
128
+ `${report.writtenCount} file(s)`,
129
+ ...report.kinds.flatMap((entry) => renderKind(entry, report.check)),
130
+ ];
131
+ if (report.check && report.removedCount > 0) {
132
+ lines.push(
133
+ '',
134
+ 'Run without --check to write the prune:',
135
+ ' node .agents/scripts/prune-baseline-orphans.js',
136
+ );
137
+ }
138
+ return lines.join('\n');
139
+ }
140
+
141
+ /**
142
+ * CLI entry point. Returns its exit code rather than calling `process.exit()`,
143
+ * so `runAsCli`'s `propagateExitCode` path settles it through `flushStdio` and
144
+ * an unbounded report is not truncated at a pipe boundary (Story #4783).
145
+ *
146
+ * @returns {Promise<number>}
147
+ */
148
+ async function main() {
149
+ let opts;
150
+ let report;
151
+ try {
152
+ opts = parseArgs(process.argv.slice(2));
153
+ const config = resolveConfig({ cwd: opts.cwd });
154
+ report = runPrune({
155
+ cwd: opts.cwd,
156
+ kinds: opts.kinds,
157
+ check: opts.check,
158
+ quality: getQuality(config) ?? { gates: {} },
159
+ });
160
+ } catch (err) {
161
+ const message = err?.message ?? String(err);
162
+ process.stdout.write(
163
+ `${JSON.stringify({ schemaVersion: '1', error: message }, null, 2)}\n`,
164
+ );
165
+ return EXIT_CANNOT_RUN;
166
+ }
167
+ process.stdout.write(
168
+ opts.json
169
+ ? `${JSON.stringify(report, null, 2)}\n`
170
+ : `${formatReport(report)}\n`,
171
+ );
172
+ return report.check && report.removedCount > 0
173
+ ? EXIT_WOULD_PRUNE
174
+ : EXIT_CLEAN;
175
+ }
176
+
177
+ runAsCli(import.meta.url, main, {
178
+ source: 'prune-baseline-orphans',
179
+ usage: HELP,
180
+ propagateExitCode: true,
181
+ });
@@ -197,7 +197,6 @@ export async function runResolveStories(
197
197
  stories,
198
198
  nativeEdges,
199
199
  warn: (m) => Logger.warn(m),
200
- config,
201
200
  });
202
201
  const foreignDone = await resolveForeignDone({
203
202
  provider,
@@ -209,7 +208,6 @@ export async function runResolveStories(
209
208
  nativeEdges,
210
209
  foreignDone,
211
210
  warn: () => {},
212
- config,
213
211
  });
214
212
 
215
213
  stdout.write(
@@ -21,6 +21,32 @@ import process from 'node:process';
21
21
  // can spawn directly.
22
22
  const useShell = process.platform === 'win32';
23
23
 
24
+ /**
25
+ * A Node gate spelled `node .agents/scripts/<script>` — the shape six of the
26
+ * eight tasks share. Spelling it once keeps this list readable as what it
27
+ * actually is (a fan-out roster) instead of a wall of spawn tuples, and keeps
28
+ * adding a gate from costing the module a maintainability regression for the
29
+ * boilerplate rather than the behaviour. Mirrors `run-verify.js`'s `gate()`.
30
+ *
31
+ * Use bare `node` (PATH-resolved) rather than `process.execPath`:
32
+ * `process.execPath` on Windows often expands to `C:\Program
33
+ * Files\nodejs\node.exe`, and spawn(..., { shell: true }) does not quote the
34
+ * executable, so the space breaks invocation.
35
+ *
36
+ * The path is passed WHOLE rather than assembled from a basename. Composing
37
+ * it (`` `.agents/scripts/${script}` ``) reads better and is wrong:
38
+ * `check-knip-entries.js` derives which CLIs something actually invokes by
39
+ * scanning this file's source text for literal script paths, and four of the
40
+ * gates below are invoked from nowhere else. Interpolating the path made them
41
+ * invisible to that derivation, so knip called them dead and the ratchet
42
+ * offered to record live gates as expected-dead — the Story #5012 trap.
43
+ *
44
+ * @param {string} name reported in this driver's own error prefix
45
+ * @param {string} script repo-relative path, spelled as one literal
46
+ * @returns {{ name: string, cmd: string, args: string[] }}
47
+ */
48
+ const nodeGate = (name, script) => ({ name, cmd: 'node', args: [script] });
49
+
24
50
  const tasks = [
25
51
  {
26
52
  name: 'biome',
@@ -47,67 +73,41 @@ const tasks = [
47
73
  '!.worktrees/**',
48
74
  ],
49
75
  },
50
- {
51
- // Custom Node-based lint for the lifecycle bus surface (Story #2227).
52
- // Enforces:
53
- // 1. No `Promise.all` over listener arrays under
54
- // `.agents/scripts/lib/orchestration/lifecycle/**`.
55
- // 2. Wildcard-observer firewall: no state-mutating imports in
56
- // listener modules that register on `'*'`.
57
- // Both rules are mandated by Tech Spec #2189 and have no biome
58
- // equivalent.
59
- //
60
- // Use bare `node` (PATH-resolved) rather than `process.execPath`:
61
- // `process.execPath` on Windows often expands to `C:\Program
62
- // Files\nodejs\node.exe`, and spawn(..., { shell: true }) does not
63
- // quote the executable, so the space breaks invocation.
64
- name: 'lifecycle-lint',
65
- cmd: 'node',
66
- args: ['.agents/scripts/check-lifecycle-lint.js'],
67
- },
68
- {
69
- // Custom Node-based lint for the workflow prose surface (Epic #4474
70
- // PR5). Enforces: no workflow may instruct calling an exported
71
- // library function that has no CLI entrypoint the measured
72
- // shim-writing failure mode the /plan collapse killed. See
73
- // check-workflow-cli-lint.js for the paragraph-level heuristic.
74
- name: 'workflow-cli-lint',
75
- cmd: 'node',
76
- args: ['.agents/scripts/check-workflow-cli-lint.js'],
77
- },
78
- {
79
- // Custom Node-based lint for label-vocabulary citations in
80
- // `.agents/docs/SDLC.md` and `.agents/workflows/**/*.md` (Story #2892,
81
- // Tech Spec F9 under Epic #2880). Greps inline backtick code
82
- // spans for axis-shaped tokens (`type/epic`, etc.) and asserts
83
- // only the canonical `<axis>::<value>` separator from
84
- // `lib/label-constants.js` appears. Closes the drift gap that
85
- // let the original `type/epic` typo land in a since-retired
86
- // planning workflow helper.
87
- name: 'label-vocabulary',
88
- cmd: 'node',
89
- args: ['.agents/scripts/lint-label-vocabulary.js'],
90
- },
91
- {
92
- // GitHub Actions job-timeout gate (Story #4936). Enumerates every
93
- // `jobs.<id>` key across `.github/workflows/*.yml` and fails when one
94
- // sets no `timeout-minutes` (inheriting GitHub's 360-minute default) or
95
- // sets one above the ceiling. A deadlocked Windows job burned 44 minutes
96
- // of a runner and withheld the failing required check's logs for the
97
- // whole time; nothing but this check would have caught the gap.
98
- name: 'workflow-timeouts',
99
- cmd: 'node',
100
- args: ['.agents/scripts/check-workflow-timeouts.js'],
101
- },
102
- {
103
- // Architecture cycle ratchet (Story #3991). Detects directed import
104
- // cycles under `.agents/scripts/` and fails on any cycle not in the
105
- // committed allowlist (`baselines/arch-cycles.json`). Mirrors the
106
- // ratchet-down semantics of `check-dead-exports.js`.
107
- name: 'arch-cycles',
108
- cmd: 'node',
109
- args: ['.agents/scripts/check-arch-cycles.js'],
110
- },
76
+ // Lifecycle surface (Story #2227). Enforces (1) no `Promise.all` under
77
+ // `.agents/scripts/lib/orchestration/lifecycle/**` the ledger is
78
+ // append-only and a parallel write interleaves records; and (2) the
79
+ // auto-merge lockout: the `gh pr merge` literal appears only in
80
+ // `single-story-close/phases/auto-merge.js`. Neither has a biome
81
+ // equivalent. Story #5024 dropped the third rule (the wildcard-observer
82
+ // firewall) with the bus and the `listeners/` directory its predicate
83
+ // required.
84
+ nodeGate('lifecycle-lint', '.agents/scripts/check-lifecycle-lint.js'),
85
+ // Workflow prose surface (Epic #4474 PR5). No workflow may instruct calling
86
+ // an exported library function that has no CLI entrypoint — the measured
87
+ // shim-writing failure mode the /plan collapse killed. See
88
+ // check-workflow-cli-lint.js for the paragraph-level heuristic.
89
+ nodeGate('workflow-cli-lint', '.agents/scripts/check-workflow-cli-lint.js'),
90
+ // Label-vocabulary citations in `.agents/docs/SDLC.md` and
91
+ // `.agents/workflows/**/*.md` (Story #2892). Greps inline backtick code
92
+ // spans for axis-shaped tokens (`type/epic`, etc.) and asserts only the
93
+ // canonical `<axis>::<value>` separator from `lib/label-constants.js`
94
+ // appears. Closes the drift gap that let the original `type/epic` typo land.
95
+ nodeGate('label-vocabulary', '.agents/scripts/lint-label-vocabulary.js'),
96
+ // GitHub Actions job-timeout gate (Story #4936). Fails a `jobs.<id>` that
97
+ // sets no `timeout-minutes` (inheriting GitHub's 360-minute default) or sets
98
+ // one above the ceiling. A deadlocked Windows job burned 44 minutes of a
99
+ // runner and withheld the failing required check's logs for the whole time.
100
+ nodeGate('workflow-timeouts', '.agents/scripts/check-workflow-timeouts.js'),
101
+ // Architecture cycle ratchet (Story #3991). Detects directed import cycles
102
+ // under `.agents/scripts/` and fails on any cycle not in the committed
103
+ // allowlist (`baselines/arch-cycles.json`).
104
+ nodeGate('arch-cycles', '.agents/scripts/check-arch-cycles.js'),
105
+ // Static Gherkin corpus gate (Story #5013): must-compile with the real
106
+ // parser, then must-bind scoped per step root. Opt-in behind
107
+ // `qa.gherkinLint`, so it exits 0 as "not configured" in this repo. Here
108
+ // rather than in run-verify.js so it reaches CI through the same `lint`
109
+ // required check as arch-cycles; listing it in both would double-pay it.
110
+ nodeGate('gherkin-corpus', '.agents/scripts/check-gherkin-corpus.js'),
111
111
  ];
112
112
 
113
113
  function runTask({ name, cmd, args }) {