mandrel 2.36.0 → 2.38.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 (225) hide show
  1. package/.agents/README.md +16 -16
  2. package/.agents/agents/plan-critic.md +1 -1
  3. package/.agents/docs/SDLC.md +40 -40
  4. package/.agents/docs/configuration.md +41 -16
  5. package/.agents/docs/execution-reference.md +3 -3
  6. package/.agents/docs/quality-gates.md +2 -2
  7. package/.agents/docs/workflows.md +6 -6
  8. package/.agents/instructions.md +5 -5
  9. package/.agents/rules/changelog-style.md +3 -3
  10. package/.agents/rules/ci-remediation.md +1 -1
  11. package/.agents/rules/git-conventions-reference.md +4 -4
  12. package/.agents/rules/testing-standards.md +56 -0
  13. package/.agents/schemas/agentrc.schema.json +9 -9
  14. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  15. package/.agents/schemas/validation-evidence.schema.json +1 -1
  16. package/.agents/scripts/acceptance-eval.js +1 -1
  17. package/.agents/scripts/apply-quality-bootstrap.js +1 -1
  18. package/.agents/scripts/audit-to-stories.js +2 -2
  19. package/.agents/scripts/boot-sweep.js +7 -1
  20. package/.agents/scripts/check-context-budget.js +195 -103
  21. package/.agents/scripts/check-cyclomatic.js +112 -42
  22. package/.agents/scripts/check-doc-links.js +32 -4
  23. package/.agents/scripts/check-generated-validator.js +202 -0
  24. package/.agents/scripts/check-lifecycle-lint.js +2 -68
  25. package/.agents/scripts/check-schema-references.js +2 -28
  26. package/.agents/scripts/check-test-temp-hygiene.js +1 -1
  27. package/.agents/scripts/check-workflow-cli-lint.js +1 -1
  28. package/.agents/scripts/deliver-light.js +10 -10
  29. package/.agents/scripts/deliver-recover.js +1 -1
  30. package/.agents/scripts/drain-pending-cleanup.js +19 -2
  31. package/.agents/scripts/evidence-gate.js +1 -1
  32. package/.agents/scripts/generate-workflows-doc.js +1 -1
  33. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  34. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +1 -1
  35. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +5 -5
  37. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +1 -1
  38. package/.agents/scripts/lib/baselines/components.js +32 -2
  39. package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
  40. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  41. package/.agents/scripts/lib/bdd-scenario-budget.js +1 -1
  42. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  43. package/.agents/scripts/lib/bdd-step-index.js +1 -1
  44. package/.agents/scripts/lib/bootstrap/branch-protection.js +2 -2
  45. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  46. package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
  47. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +1 -1
  48. package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
  49. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
  50. package/.agents/scripts/lib/checks/core-bare-clean.js +1 -1
  51. package/.agents/scripts/lib/checks/loop-health.js +1 -1
  52. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +1 -1
  53. package/.agents/scripts/lib/cli-args.js +1 -3
  54. package/.agents/scripts/lib/close-validation/gates.js +1 -1
  55. package/.agents/scripts/lib/close-validation/process.js +1 -1
  56. package/.agents/scripts/lib/command-header.js +1 -1
  57. package/.agents/scripts/lib/config/delivery-routing.js +1 -1
  58. package/.agents/scripts/lib/config/explain.js +1 -1
  59. package/.agents/scripts/lib/config/runners.js +2 -2
  60. package/.agents/scripts/lib/config/runtime.js +1 -1
  61. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  62. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  63. package/.agents/scripts/lib/config-settings-schema-delivery.js +3 -3
  64. package/.agents/scripts/lib/config-settings-schema-quality.js +6 -6
  65. package/.agents/scripts/lib/config-settings-schema.js +49 -9
  66. package/.agents/scripts/lib/coverage-baseline.js +2 -2
  67. package/.agents/scripts/lib/cpu-pool.js +90 -10
  68. package/.agents/scripts/lib/crap-utils.js +6 -2
  69. package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
  70. package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
  71. package/.agents/scripts/lib/dependency-version.js +86 -0
  72. package/.agents/scripts/lib/duplicate-search.js +2 -2
  73. package/.agents/scripts/lib/errors/index.js +1 -1
  74. package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
  75. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +5 -5
  76. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +2 -2
  77. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  78. package/.agents/scripts/lib/findings/promote-finding.js +12 -12
  79. package/.agents/scripts/lib/findings/route-finding.js +2 -2
  80. package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
  81. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  82. package/.agents/scripts/lib/install-cmd-parser.js +1 -1
  83. package/.agents/scripts/lib/knip-entry-sync.js +1 -57
  84. package/.agents/scripts/lib/label-constants.js +2 -2
  85. package/.agents/scripts/lib/label-taxonomy.js +1 -1
  86. package/.agents/scripts/lib/maintainability-utils.js +108 -10
  87. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  88. package/.agents/scripts/lib/onboard/init-tail.js +4 -4
  89. package/.agents/scripts/lib/onboard/scaffold-docs.js +2 -2
  90. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  91. package/.agents/scripts/lib/orchestration/code-review.js +4 -4
  92. package/.agents/scripts/lib/orchestration/complexity-gate.js +5 -5
  93. package/.agents/scripts/lib/orchestration/deliver-recover.js +3 -3
  94. package/.agents/scripts/lib/orchestration/docs-digest.js +3 -3
  95. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +2 -2
  96. package/.agents/scripts/lib/orchestration/light-backstop.js +1 -1
  97. package/.agents/scripts/lib/orchestration/light-escalation.js +6 -6
  98. package/.agents/scripts/lib/orchestration/light-suitability.js +19 -19
  99. package/.agents/scripts/lib/orchestration/plan-context.js +4 -4
  100. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +2 -2
  101. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +2 -2
  102. package/.agents/scripts/lib/orchestration/plan-metrics.js +1 -1
  103. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +1 -1
  104. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +3 -3
  105. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +6 -6
  106. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +86 -25
  107. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -3
  108. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +6 -6
  109. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +7 -7
  110. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -1
  111. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +3 -3
  112. package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
  113. package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
  114. package/.agents/scripts/lib/orchestration/remote-verifier.js +2 -2
  115. package/.agents/scripts/lib/orchestration/resolve-stories.js +4 -4
  116. package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
  117. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
  118. package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
  119. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
  120. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
  121. package/.agents/scripts/lib/orchestration/run-epilogue.js +2 -2
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +2 -2
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +1 -1
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +5 -1
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +1 -1
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +3 -3
  128. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -2
  129. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  130. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
  131. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +14 -12
  132. package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
  133. package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
  134. package/.agents/scripts/lib/orchestration/ticket-validator.js +1 -1
  135. package/.agents/scripts/lib/orchestration/ticketing/reads.js +6 -4
  136. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  137. package/.agents/scripts/lib/preflight-runner.js +3 -3
  138. package/.agents/scripts/lib/qa/qa-session.js +1 -1
  139. package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
  140. package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
  141. package/.agents/scripts/lib/signals/schema.js +1 -1
  142. package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
  143. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  144. package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
  145. package/.agents/scripts/lib/story-adjacency.js +2 -2
  146. package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
  147. package/.agents/scripts/lib/test-isolate/runner.js +3 -3
  148. package/.agents/scripts/lib/test-runner-contract.js +14 -6
  149. package/.agents/scripts/lib/test-tiers.js +135 -18
  150. package/.agents/scripts/lib/ticket-body-sections.js +1 -1
  151. package/.agents/scripts/lib/transpile.js +16 -3
  152. package/.agents/scripts/lib/util/concurrent-map.js +1 -1
  153. package/.agents/scripts/lib/util/parse-id-list.js +1 -1
  154. package/.agents/scripts/lib/wave-runner/live-probe.js +4 -4
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
  156. package/.agents/scripts/lib/workflow-closure.js +1 -1
  157. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +170 -32
  158. package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
  159. package/.agents/scripts/lint-issue-body.js +3 -3
  160. package/.agents/scripts/plan-context.js +4 -4
  161. package/.agents/scripts/plan-critics.js +2 -2
  162. package/.agents/scripts/plan-persist.js +2 -2
  163. package/.agents/scripts/plan-run-epilogue.js +2 -2
  164. package/.agents/scripts/pr-watch-with-update.js +305 -137
  165. package/.agents/scripts/providers/github/board-add.js +1 -1
  166. package/.agents/scripts/providers/github/errors.js +1 -1
  167. package/.agents/scripts/providers/github/tickets.js +114 -21
  168. package/.agents/scripts/quality-preview.js +162 -70
  169. package/.agents/scripts/resolve-stories.js +3 -3
  170. package/.agents/scripts/resync-status-column.js +1 -1
  171. package/.agents/scripts/run-lint.js +1 -1
  172. package/.agents/scripts/run-test-profile.js +8 -5
  173. package/.agents/scripts/run-tests.js +79 -14
  174. package/.agents/scripts/single-story-close.js +2 -4
  175. package/.agents/scripts/single-story-init.js +10 -6
  176. package/.agents/scripts/stories-wave-tick.js +8 -8
  177. package/.agents/skills/core/idea-refinement/SKILL.md +5 -5
  178. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  179. package/.agents/skills/skills.index.json +1 -1
  180. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -1
  181. package/.agents/workflows/audit-data-model.md +1 -1
  182. package/.agents/workflows/audit-documentation.md +2 -2
  183. package/.agents/workflows/audit-to-stories.md +11 -11
  184. package/.agents/workflows/git-deliver.md +4 -4
  185. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  186. package/.agents/workflows/helpers/audit-lens-core.md +1 -1
  187. package/.agents/workflows/helpers/code-review.md +4 -4
  188. package/.agents/workflows/helpers/deliver-light.md +26 -26
  189. package/.agents/workflows/helpers/deliver-reference.md +15 -7
  190. package/.agents/workflows/helpers/deliver-story-reference.md +9 -4
  191. package/.agents/workflows/helpers/deliver-story.md +6 -7
  192. package/.agents/workflows/helpers/diagnose.md +2 -2
  193. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  194. package/.agents/workflows/helpers/plan-reference.md +9 -9
  195. package/.agents/workflows/helpers/qa-core.md +8 -8
  196. package/.agents/workflows/helpers/worktree-lifecycle.md +6 -6
  197. package/.agents/workflows/{deliver.md → mandrel-deliver.md} +15 -19
  198. package/.agents/workflows/{plan.md → mandrel-plan.md} +9 -9
  199. package/.agents/workflows/memory-consolidate.md +4 -4
  200. package/.agents/workflows/prototype.md +3 -3
  201. package/.agents/workflows/qa-assist.md +12 -12
  202. package/.agents/workflows/qa-explore.md +6 -6
  203. package/.agents/workflows/qa-run.md +1 -1
  204. package/README.md +7 -7
  205. package/docs/CHANGELOG.md +43 -0
  206. package/lib/cli/doctor.js +1 -1
  207. package/lib/cli/init.js +1 -1
  208. package/lib/cli/migrate.js +100 -94
  209. package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
  210. package/lib/migrations/index.js +32 -33
  211. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
  212. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
  213. package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
  214. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +1 -1
  215. package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
  216. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
  217. package/package.json +7 -3
  218. package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
  219. package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
  220. package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
  221. package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
  222. package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
  223. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
  224. package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
  225. package/.agents/scripts/lib/orchestration/recut.js +0 -56
@@ -15,7 +15,7 @@
15
15
  * Epic-hierarchy write surface. It composed the exact `Epic: #N` footer that
16
16
  * `pr-base-guard.js` hard-refuses at delivery, so the framework retained the
17
17
  * ability to generate work it would then reject; it had no production caller
18
- * (`/plan` persists through the bare `createIssue` below).
18
+ * (`/mandrel-plan` persists through the bare `createIssue` below).
19
19
  *
20
20
  * Public surface: `GitHubProvider.createIssue / getTicket /
21
21
  * getTickets / updateTicket / getTicketDependencies / primeTicketCache /
@@ -251,8 +251,8 @@ export class TicketGateway {
251
251
 
252
252
  /**
253
253
  * Create a **bare** issue — no footer composition and no sub-issue link.
254
- * Since Story #4545 this is the *only* create path: `/plan` persist and the
255
- * `/plan` Phase 4 Epic open (`openEpicFromOnePager`'s `createIssue` port)
254
+ * Since Story #4545 this is the *only* create path: `/mandrel-plan` persist and the
255
+ * `/mandrel-plan` Phase 4 Epic open (`openEpicFromOnePager`'s `createIssue` port)
256
256
  * both route through it.
257
257
  *
258
258
  * After the POST, the new issue is added to the configured Project V2
@@ -264,38 +264,43 @@ export class TicketGateway {
264
264
  * The POST is wrapped in `withTransientRetry` (Story #4541) so it absorbs
265
265
  * the same 502/429/ECONNRESET blips the read surfaces already do. It used
266
266
  * to post bare, which made a single transient failure at story *k* of *N*
267
- * abort `/plan` persist with `1..k-1` already live on the tracker.
267
+ * abort `/mandrel-plan` persist with `1..k-1` already live on the tracker.
268
268
  *
269
269
  * Retry alone is not sufficient for that failure mode — a POST whose
270
- * response is lost would double-create on the retry so the caller
271
- * (`plan-persist`'s `createStoryIssues`) carries the idempotency half via
272
- * a plan fingerprint it looks up before creating. Retry narrows the
273
- * window; the fingerprint closes it.
270
+ * response is lost would double-create on the retry. Story #5112 closed
271
+ * that: `findExisting` is consulted **before every retry POST**, and a hit
272
+ * is adopted instead of re-created, so a response-lost first attempt files
273
+ * exactly one issue. `/mandrel-plan` persist supplies the plan-fingerprint lookup
274
+ * its resume path already uses (`plan-persist`'s `createStoryIssues`);
275
+ * callers with no content identity omit it and keep the pre-#5112
276
+ * retry-only behaviour.
274
277
  *
275
278
  * @field-manifest POST /repos/{owner}/{repo}/issues: number, id, node_id,
276
279
  * html_url
277
280
  *
278
- * @param {{ title: string, body: string, labels?: string[] }} payload
281
+ * @param {{
282
+ * title: string,
283
+ * body: string,
284
+ * labels?: string[],
285
+ * findExisting?: (() => Promise<object|null>)|null,
286
+ * }} payload
279
287
  * @returns {Promise<{
280
288
  * id: number,
281
289
  * number: number,
282
290
  * internalId: number,
283
291
  * nodeId: string,
284
292
  * url: string,
293
+ * adopted: boolean,
285
294
  * boardAdd: { added: boolean, reason?: string },
286
295
  * }>}
287
296
  */
288
- async createIssue({ title, body, labels = [] }) {
289
- const result = await withTransientRetry(
290
- () =>
291
- this._gh.api({
292
- method: 'POST',
293
- endpoint: `/repos/${this.owner}/${this.repo}/issues`,
294
- body: { title, body, labels },
295
- }),
296
- { label: `createIssue "${title}"`, onRetry: defaultRetryWarn },
297
- );
298
- const issue = parseApiJson(result);
297
+ async createIssue({ title, body, labels = [], findExisting = null }) {
298
+ const { issue, adopted } = await this._createIssueOrAdopt({
299
+ title,
300
+ body,
301
+ labels,
302
+ findExisting,
303
+ });
299
304
  this._listCache.clear();
300
305
 
301
306
  const boardAdd = await addIssueToBoard({
@@ -311,10 +316,86 @@ export class TicketGateway {
311
316
  internalId: issue.id,
312
317
  nodeId: issue.node_id,
313
318
  url: issue.html_url,
319
+ adopted,
314
320
  boardAdd,
315
321
  };
316
322
  }
317
323
 
324
+ /**
325
+ * POST one issue and parse the created resource out of the response.
326
+ *
327
+ * @param {{ title: string, body: string, labels: string[] }} payload
328
+ * @returns {Promise<object>}
329
+ */
330
+ async _postIssue(payload) {
331
+ return parseApiJson(
332
+ await this._gh.api({
333
+ method: 'POST',
334
+ endpoint: `/repos/${this.owner}/${this.repo}/issues`,
335
+ body: payload,
336
+ }),
337
+ );
338
+ }
339
+
340
+ /**
341
+ * Resolve the ambiguity a lost response leaves behind: did attempt 1 land?
342
+ * `ECONNRESET` after the server committed the create looks exactly like
343
+ * `ECONNRESET` before it, so the only authority is the server's own state.
344
+ *
345
+ * @param {(() => Promise<object|null>)|null} findExisting
346
+ * @returns {Promise<object|null>} the already-created issue, or `null`.
347
+ */
348
+ async _findAlreadyCreated(findExisting) {
349
+ return typeof findExisting === 'function' ? await findExisting() : null;
350
+ }
351
+
352
+ /**
353
+ * The idempotent half of {@link TicketGateway#createIssue}: POST once, and
354
+ * on every subsequent attempt look for an already-created issue *before*
355
+ * posting again, so a retry adopts rather than duplicates.
356
+ *
357
+ * @param {{ title: string, body: string, labels: string[], findExisting: (() => Promise<object|null>)|null }} args
358
+ * @returns {Promise<{ issue: object, adopted: boolean }>}
359
+ */
360
+ async _createIssueOrAdopt({ title, body, labels, findExisting }) {
361
+ let posts = 0;
362
+ return withTransientRetry(
363
+ async () => {
364
+ if (posts > 0) {
365
+ const issue = await this._findAlreadyCreated(findExisting);
366
+ if (issue) return { issue, adopted: true };
367
+ }
368
+ posts += 1;
369
+ const issue = await this._postIssue({ title, body, labels });
370
+ return { issue, adopted: false };
371
+ },
372
+ { label: `createIssue "${title}"`, onRetry: defaultRetryWarn },
373
+ );
374
+ }
375
+
376
+ /**
377
+ * Append logins to an issue's assignee set via GitHub's additive assignees
378
+ * endpoint. No read-before-write and no replace semantics, so it cannot
379
+ * evict an assignee another run added between our read and our write. A
380
+ * non-array or empty list is a no-op.
381
+ *
382
+ * @param {number} ticketId
383
+ * @param {string[]|undefined} logins
384
+ */
385
+ async _addAssignees(ticketId, logins) {
386
+ if (!(logins?.length > 0)) return;
387
+ await withTransientRetry(
388
+ () =>
389
+ this._gh.api({
390
+ method: 'POST',
391
+ endpoint: `/repos/${this.owner}/${this.repo}/issues/${ticketId}/assignees`,
392
+ body: { assignees: logins },
393
+ }),
394
+ { label: `addAssignees #${ticketId}`, onRetry: defaultRetryWarn },
395
+ );
396
+ this.invalidateTicket(ticketId);
397
+ }
398
+
318
399
  /**
319
400
  * Add/remove labels on an issue. When the only mutation is "add", uses the
320
401
  * additive labels endpoint (POST /issues/{n}/labels) for atomicity and to
@@ -324,7 +405,7 @@ export class TicketGateway {
324
405
  *
325
406
  * The additive POST goes through `withTransientRetry` (Story #4961) on the
326
407
  * same policy as every other call in this file. Story #4952 raised the
327
- * `/plan` write loops that reach this endpoint — the `agent::ready` flips
408
+ * `/mandrel-plan` write loops that reach this endpoint — the `agent::ready` flips
328
409
  * and the checkpoint fan-out — off serial, which is precisely what makes
329
410
  * GitHub's secondary rate limit likelier; `gh-exec` already classifies that
330
411
  * as transient, so the only thing missing was a backoff behind it.
@@ -370,11 +451,23 @@ export class TicketGateway {
370
451
  * {@link TicketGateway#_applyLabelMutations}. Both are the write half of the
371
452
  * fan-outs Story #4952 raised; the reads in this file were already wrapped.
372
453
  *
454
+ * `mutations.addAssignees` (Story #5112) is the **additive** assignee
455
+ * write, on the same shape as the additive label POST above: it appends to
456
+ * the assignee set instead of replacing it. The lease claim
457
+ * (`lib/orchestration/ticket-lease.js`) needs that — a replacing PATCH
458
+ * evicts a simultaneous claimer silently, so both runs read a clean
459
+ * `[self]` on verify and both believe they hold the lease. Appending makes
460
+ * the collision *observable* as a co-assignment, which is exactly what the
461
+ * lease's `lost-race` back-out keys on. `mutations.assignees` (replace) is
462
+ * unchanged and still used by the steal and release paths.
463
+ *
464
+ * @field-manifest POST /repos/{owner}/{repo}/issues/{n}/assignees: assignees
373
465
  * @field-manifest PATCH /repos/{owner}/{repo}/issues/{n}:
374
466
  * body, assignees, state, state_reason, labels
375
467
  */
376
468
  /* node:coverage ignore next */
377
469
  async updateTicket(ticketId, mutations) {
470
+ await this._addAssignees(ticketId, mutations.addAssignees);
378
471
  const patch = {};
379
472
  if (mutations.body !== undefined) patch.body = mutations.body;
380
473
  if (mutations.assignees) patch.assignees = mutations.assignees;
@@ -292,7 +292,9 @@ export function computeExitCode(merged, miExit, crapExit) {
292
292
  *
293
293
  * The last header used to hardcode `c=8`, which quietly lied to any consumer
294
294
  * that had tuned `codingGuardrails.cyclomaticFlag`. It now names the value the
295
- * count was actually taken against, read off the merge result.
295
+ * count was actually taken against, read off the merge result — through the
296
+ * same `normalizeFlag` the merge itself uses, rather than the second, hand-
297
+ * inlined copy of that coercion this function used to carry.
296
298
  *
297
299
  * Pure — accepts pre-computed merge rows and returns a multi-line string. The
298
300
  * table renders even on a clean diff so operators see the "no drift" signal.
@@ -300,33 +302,40 @@ export function computeExitCode(merged, miExit, crapExit) {
300
302
  * @param {{ rows: Array<{ file: string, miDrop: number, worstCrapDelta: number, newOverCeilingMethods: number }>, totals: { miRegressions: number, crapViolations: number }, cyclomaticFlag?: number }} merged
301
303
  * @returns {string}
302
304
  */
305
+ /**
306
+ * The table's body: one row per regressed file, or the single placeholder
307
+ * line that keeps the "no drift" signal visible on a clean diff.
308
+ *
309
+ * Split out of `renderTable` so that function keeps a flat shape — the
310
+ * empty-vs-populated branch and the row loop together carried it above the
311
+ * per-method CRAP contract the pre-push preview enforces.
312
+ *
313
+ * @param {Array<{ file: string, miDrop: number, worstCrapDelta: number, newOverCeilingMethods: number }>} rows
314
+ * @returns {string[]}
315
+ */
316
+ function tableBodyLines(rows) {
317
+ if (rows.length === 0) return ['| _(no per-file regressions)_ | — | — | — |'];
318
+ return rows.map(
319
+ (row) =>
320
+ `| ${row.file} | -${row.miDrop.toFixed(2)} | +${row.worstCrapDelta.toFixed(2)} | ${row.newOverCeilingMethods} |`,
321
+ );
322
+ }
323
+
303
324
  export function renderTable(merged) {
304
- const flag = Number.isFinite(Number(merged?.cyclomaticFlag))
305
- ? Number(merged.cyclomaticFlag)
306
- : DEFAULT_CYCLOMATIC_FLAG;
325
+ const flag = normalizeFlag(merged?.cyclomaticFlag);
307
326
  const header = [
308
327
  'file',
309
328
  'MI delta',
310
329
  'worst CRAP delta',
311
330
  `new-method count over c=${flag}`,
312
331
  ];
313
- const lines = [];
314
- lines.push(`| ${header.join(' | ')} |`);
315
- lines.push(`| ${header.map(() => '---').join(' | ')} |`);
316
- if (merged.rows.length === 0) {
317
- lines.push('| _(no per-file regressions)_ | — | — | — |');
318
- } else {
319
- for (const row of merged.rows) {
320
- lines.push(
321
- `| ${row.file} | -${row.miDrop.toFixed(2)} | +${row.worstCrapDelta.toFixed(2)} | ${row.newOverCeilingMethods} |`,
322
- );
323
- }
324
- }
325
- lines.push('');
326
- lines.push(
332
+ return [
333
+ `| ${header.join(' | ')} |`,
334
+ `| ${header.map(() => '---').join(' | ')} |`,
335
+ ...tableBodyLines(merged.rows),
336
+ '',
327
337
  `Totals: MI regressions=${merged.totals.miRegressions} · CRAP violations=${merged.totals.crapViolations}`,
328
- );
329
- return lines.join('\n');
338
+ ].join('\n');
330
339
  }
331
340
 
332
341
  /**
@@ -354,6 +363,96 @@ function resolveCyclomaticFlag({ cwd, stderr }) {
354
363
  }
355
364
  }
356
365
 
366
+ /**
367
+ * Invoke one preview runner, degrading a thrown failure into the same
368
+ * `{ exitCode: 1, envelope: null }` shape a real gate failure produces.
369
+ *
370
+ * Both runners degraded identically before, in two hand-copied `catch`
371
+ * arms; folding them into one helper removes the copy and keeps `runCli` a
372
+ * pipeline rather than a pair of inlined error handlers. The emitted message
373
+ * is byte-identical to the arm it replaces — `label` supplies the `MI` /
374
+ * `CRAP` prefix.
375
+ *
376
+ * @param {(args: object) => Promise<{exitCode: number, envelope: object|null}>} runner
377
+ * @param {{cwd: string, staged: boolean, changedSinceRef: string|null}} args
378
+ * @param {'MI'|'CRAP'} label
379
+ * @param {{ write: (s: string) => void }} stderr
380
+ * @returns {Promise<{exitCode: number, envelope: object|null}>}
381
+ */
382
+ function runGateSafely(runner, args, label, stderr) {
383
+ return runner(args).catch((err) => {
384
+ stderr.write(
385
+ `[quality:preview] ${label} runner failed: ${err?.message ?? err}\n`,
386
+ );
387
+ return { exitCode: 1, envelope: null };
388
+ });
389
+ }
390
+
391
+ /**
392
+ * Write the run's report — the `--json` envelope, or the human-readable
393
+ * table plus any gate diagnostics and the non-zero-exit summary.
394
+ *
395
+ * Split out of `runCli` (Story #5109): the rendering half carried five of
396
+ * that function's decision points, which put it over the per-method CRAP
397
+ * contract the pre-push preview enforces. Output bytes are unchanged in both
398
+ * modes. The exit code stays with the caller — this function only reports.
399
+ *
400
+ * @param {{
401
+ * json: boolean,
402
+ * staged: boolean,
403
+ * ref: string|null,
404
+ * miResult: {exitCode: number, envelope: object|null},
405
+ * crapResult: {exitCode: number, envelope: object|null},
406
+ * merged: ReturnType<typeof mergeEnvelopes>,
407
+ * stdout: { write: (s: string) => void },
408
+ * stderr: { write: (s: string) => void },
409
+ * }} args
410
+ * @returns {void}
411
+ */
412
+ function emitReport({
413
+ json,
414
+ staged,
415
+ ref,
416
+ miResult,
417
+ crapResult,
418
+ merged,
419
+ stdout,
420
+ stderr,
421
+ }) {
422
+ const miExit = miResult.exitCode;
423
+ const crapExit = crapResult.exitCode;
424
+ if (json) {
425
+ stdout.write(
426
+ `${JSON.stringify(
427
+ {
428
+ ref: staged ? null : ref,
429
+ staged,
430
+ mi: { exit: miExit, envelope: miResult.envelope },
431
+ crap: { exit: crapExit, envelope: crapResult.envelope },
432
+ merged,
433
+ },
434
+ null,
435
+ 2,
436
+ )}\n`,
437
+ );
438
+ return;
439
+ }
440
+ stdout.write('\n--- quality:preview ---\n');
441
+ stdout.write(
442
+ staged
443
+ ? 'scope=staged (git diff --cached)\n\n'
444
+ : `scope=diff ref=${ref}\n\n`,
445
+ );
446
+ stdout.write(`${renderTable(merged)}\n`);
447
+ const diagnostics = renderDiagnostics([miResult, crapResult]);
448
+ if (diagnostics) stdout.write(`\n${diagnostics}\n`);
449
+ if (miExit !== 0 || crapExit !== 0) {
450
+ stderr.write(
451
+ `\n[quality:preview] gate exits: mi=${miExit} crap=${crapExit}\n`,
452
+ );
453
+ }
454
+ }
455
+
357
456
  /**
358
457
  * Top-level CLI entry: invoke both per-kind preview runners, merge, render,
359
458
  * and exit with the right code. Exposed as `runCli` so tests can drive the
@@ -381,62 +480,55 @@ export async function runCli({
381
480
  const staged = parseStagedFlag(argv);
382
481
  const ref = staged ? null : (parseChangedSinceArg(argv) ?? 'HEAD');
383
482
 
384
- const [miResult, crapResult] = await Promise.all([
385
- runMi({ cwd, staged, changedSinceRef: ref }).catch((err) => {
386
- stderr.write(
387
- `[quality:preview] MI runner failed: ${err?.message ?? err}\n`,
388
- );
389
- return { exitCode: 1, envelope: null };
390
- }),
391
- runCrap({ cwd, staged, changedSinceRef: ref }).catch((err) => {
392
- stderr.write(
393
- `[quality:preview] CRAP runner failed: ${err?.message ?? err}\n`,
394
- );
395
- return { exitCode: 1, envelope: null };
396
- }),
397
- ]);
398
- const miExit = miResult.exitCode;
399
- const crapExit = crapResult.exitCode;
400
- const miEnvelope = miResult.envelope;
401
- const crapEnvelope = crapResult.envelope;
483
+ // Story #5109 the two gates run **one after the other**, not under a
484
+ // `Promise.all`. Each scores its batch with its own `runOnPool` budget
485
+ // sized to `os.availableParallelism()`, so overlapping them oversubscribed
486
+ // the host by 2x and stacked two escomplex heaps: a 58-file preview peaked
487
+ // at 1.0-1.2 GB RSS for 3.9 s of CPU. Serialising them bounds the preview
488
+ // to one `availableParallelism` of workers and one heap at a time. The two
489
+ // runners share no state and neither reads the other's envelope, so the
490
+ // emitted envelopes and therefore the merged table and the exit code —
491
+ // are identical either way; only the peak cost differs.
492
+ //
493
+ // Each runner gets its own options literal rather than one shared object,
494
+ // so serialising them cannot introduce a coupling the concurrent form
495
+ // did not have.
496
+ const miResult = await runGateSafely(
497
+ runMi,
498
+ { cwd, staged, changedSinceRef: ref },
499
+ 'MI',
500
+ stderr,
501
+ );
502
+ const crapResult = await runGateSafely(
503
+ runCrap,
504
+ { cwd, staged, changedSinceRef: ref },
505
+ 'CRAP',
506
+ stderr,
507
+ );
508
+
402
509
  // Story #4923 — the over-ceiling column counts against the *resolved*
403
510
  // `codingGuardrails.cyclomaticFlag`, not the literal that used to be written
404
511
  // into `mergeEnvelopes` and the column header.
405
512
  const cyclomaticFlag = resolveCyclomaticFlag({ cwd, stderr });
406
- const merged = mergeEnvelopes(miEnvelope, crapEnvelope, { cyclomaticFlag });
513
+ const merged = mergeEnvelopes(miResult.envelope, crapResult.envelope, {
514
+ cyclomaticFlag,
515
+ });
407
516
 
408
- if (json) {
409
- stdout.write(
410
- `${JSON.stringify(
411
- {
412
- ref: staged ? null : ref,
413
- staged,
414
- mi: { exit: miExit, envelope: miEnvelope },
415
- crap: { exit: crapExit, envelope: crapEnvelope },
416
- merged,
417
- },
418
- null,
419
- 2,
420
- )}\n`,
421
- );
422
- } else {
423
- stdout.write('\n--- quality:preview ---\n');
424
- stdout.write(
425
- staged
426
- ? 'scope=staged (git diff --cached)\n\n'
427
- : `scope=diff ref=${ref}\n\n`,
428
- );
429
- stdout.write(`${renderTable(merged)}\n`);
430
- const diagnostics = renderDiagnostics([miResult, crapResult]);
431
- if (diagnostics) stdout.write(`\n${diagnostics}\n`);
432
- if (miExit !== 0 || crapExit !== 0) {
433
- stderr.write(
434
- `\n[quality:preview] gate exits: mi=${miExit} crap=${crapExit}\n`,
435
- );
436
- }
437
- }
517
+ emitReport({
518
+ json,
519
+ staged,
520
+ ref,
521
+ miResult,
522
+ crapResult,
523
+ merged,
524
+ stdout,
525
+ stderr,
526
+ });
438
527
 
439
- return { exitCode: computeExitCode(merged, miExit, crapExit), merged };
528
+ return {
529
+ exitCode: computeExitCode(merged, miResult.exitCode, crapResult.exitCode),
530
+ merged,
531
+ };
440
532
  }
441
533
 
442
534
  // cli-opt-out: Windows-aware main-guard with leading-slash drive-letter normalisation; mirrors quality-watch.js so the diagnostic surface stays consistent across the gate suite.
@@ -2,9 +2,9 @@
2
2
 
3
3
  /**
4
4
  * resolve-stories.js — resolve a list of Story ids into the
5
- * `{ stories, dag, done }` envelope `/deliver` sequences from.
5
+ * `{ stories, dag, done }` envelope `/mandrel-deliver` sequences from.
6
6
  *
7
- * This is the ONE resolution step for multi-Story delivery. `/deliver` takes
7
+ * This is the ONE resolution step for multi-Story delivery. `/mandrel-deliver` takes
8
8
  * only Story ids; the graph is discovered here, from live state, rather than
9
9
  * hand-transcribed by the host or implied by a batch label.
10
10
  *
@@ -63,7 +63,7 @@ const HELP = `\
63
63
  Usage:
64
64
  resolve-stories.js --ids <n,n,...> [--pretty] [--no-native]
65
65
 
66
- Resolve Story ids into the { stories, dag, done } envelope /deliver sequences
66
+ Resolve Story ids into the { stories, dag, done } envelope /mandrel-deliver sequences
67
67
  from. Dependencies are discovered from live state: body edges union native
68
68
  blocked_by edges, with every blocker (in-set or foreign) resolved against its
69
69
  real issue state.
@@ -5,7 +5,7 @@
5
5
  * resync-status-column.js — re-assert the GitHub Projects v2 Status
6
6
  * column for a ticket after auto-merge has fired (Story #2845).
7
7
  *
8
- * The `/single-story-deliver` and `/deliver` workflow docs call
8
+ * The `/single-story-deliver` and `/mandrel-deliver` workflow docs call
9
9
  * this CLI after Step 5 confirms `state: "MERGED"` so the orchestrator
10
10
  * wins the race against the GitHub built-in `Pull request merged`
11
11
  * workflow, which would otherwise overwrite Status to whatever value
@@ -84,7 +84,7 @@ const tasks = [
84
84
  nodeGate('lifecycle-lint', '.agents/scripts/check-lifecycle-lint.js'),
85
85
  // Workflow prose surface (Epic #4474 PR5). No workflow may instruct calling
86
86
  // an exported library function that has no CLI entrypoint — the measured
87
- // shim-writing failure mode the /plan collapse killed. See
87
+ // shim-writing failure mode the /mandrel-plan collapse killed. See
88
88
  // check-workflow-cli-lint.js for the paragraph-level heuristic.
89
89
  nodeGate('workflow-cli-lint', '.agents/scripts/check-workflow-cli-lint.js'),
90
90
  // Label-vocabulary citations in `.agents/docs/SDLC.md` and
@@ -69,11 +69,14 @@ export function runTestProfile({
69
69
  const { outDir, topN, testArgv } = parseProfileArgv(argv);
70
70
  fsLike.mkdirSync(outDir, { recursive: true });
71
71
 
72
- const nodeArgs = [
73
- ...buildNodeTestArgs({ extraArgs: testArgv }),
74
- '--test-reporter',
75
- 'tap',
76
- ];
72
+ // The reporter is a *builder option*, not a suffix. Appended after the
73
+ // file targets it was two more positionals to Node, the default reporter
74
+ // ran, and every profile parsed `Timed entries parsed: 0` off a full
75
+ // suite run. `buildNodeTestArgs` places it in flag position.
76
+ const nodeArgs = buildNodeTestArgs({
77
+ extraArgs: testArgv,
78
+ reporter: 'tap',
79
+ });
77
80
 
78
81
  const started = Date.now();
79
82
  const result = spawnChild(process.execPath, nodeArgs, {
@@ -16,16 +16,18 @@
16
16
  * to opt back in for the rare case where a contract test deliberately
17
17
  * exercises a sandbox endpoint.
18
18
  *
19
- * Windows arg-length safety: the `quick` / `integration` tiers enumerate
20
- * explicit test-file targets (they exclude specific slow suites, so a single
21
- * glob will not do). With ~700+ targets the joined command line crosses the
22
- * Windows `CreateProcess` ~32 767-char `lpCommandLine` ceiling, and
23
- * `spawnSync` throws `ENAMETOOLONG` before a single test runs. To stay safe
24
- * on every platform the runner partitions the targets into chunks whose
25
- * joined length stays well under that ceiling (`MAX_TARGET_CHARS`) and spawns
26
- * one `node --test` process per chunk, aggregating the exit codes. The
27
- * `full` tier (a single recursive `tests` glob) yields exactly one chunk, so
28
- * its behaviour is unchanged.
19
+ * Windows arg-length safety: every tier enumerates explicit test-file
20
+ * targets. `quick` / `integration` always did (they exclude specific slow
21
+ * suites, so a single glob will not do), and `full` joined them in Story
22
+ * #5111 `node --test` has no negative pattern, so "everything except
23
+ * `tests/e2e/**`" is only sayable as a file set. With ~700+ targets the
24
+ * joined command line crosses the Windows `CreateProcess` ~32 767-char
25
+ * `lpCommandLine` ceiling, and `spawnSync` throws `ENAMETOOLONG` before a
26
+ * single test runs. To stay safe on every platform the runner partitions the
27
+ * targets into chunks whose joined length stays well under that ceiling
28
+ * (`MAX_TARGET_CHARS`) and spawns one `node --test` process per chunk,
29
+ * aggregating the exit codes. On POSIX the far larger
30
+ * `POSIX_MAX_TARGET_CHARS` budget collapses every tier back into one spawn.
29
31
  */
30
32
 
31
33
  import { spawnSync } from 'node:child_process';
@@ -126,18 +128,38 @@ export function chunkTestTargets(targets, maxChars = MAX_TARGET_CHARS) {
126
128
  }
127
129
 
128
130
  /**
131
+ * Build the argv for one `node --test` invocation.
132
+ *
133
+ * **Flag position is load-bearing.** Node stops treating tokens as options
134
+ * at the first positional, so every runner flag — the fixed
135
+ * `TEST_RUNNER_FLAGS`, the optional `--test-reporter`, and any caller
136
+ * `extraArgs` — has to sit *before* the test-file targets. Appending
137
+ * `--test-reporter tap` after the targets (what `run-test-profile.js` used
138
+ * to do) made Node read the two tokens as two more file patterns: the
139
+ * default reporter ran, no TAP was emitted, and the profiler parsed zero
140
+ * timed entries out of a full suite run. `reporter` is therefore an option
141
+ * of this builder rather than something a caller concatenates on the end.
142
+ *
129
143
  * @param {object} [opts]
130
- * @param {string[]} [opts.extraArgs]
144
+ * @param {string[]} [opts.extraArgs] Extra runner arguments (flag position).
131
145
  * @param {'full' | 'quick' | 'integration'} [opts.tier]
132
146
  * @param {string} [opts.repoRoot]
147
+ * @param {string} [opts.reporter] `--test-reporter` value, e.g. `'tap'`.
148
+ * @returns {string[]}
133
149
  */
134
150
  export function buildNodeTestArgs({
135
151
  extraArgs = [],
136
152
  tier = 'full',
137
153
  repoRoot = ROOT,
154
+ reporter,
138
155
  } = {}) {
139
- const targets = listTestFilesForTier(tier, repoRoot);
140
- return [...TEST_RUNNER_FLAGS, ...targets, ...extraArgs];
156
+ return [
157
+ ...(reporter
158
+ ? [...TEST_RUNNER_FLAGS, '--test-reporter', reporter]
159
+ : TEST_RUNNER_FLAGS),
160
+ ...extraArgs,
161
+ ...listTestFilesForTier(tier, repoRoot),
162
+ ];
141
163
  }
142
164
 
143
165
  export function runTestSuite({
@@ -169,7 +191,13 @@ export function runTestSuite({
169
191
  for (const chunk of chunks) {
170
192
  const testRun = spawn(
171
193
  process.execPath,
172
- [...TEST_RUNNER_FLAGS, ...chunk, ...rest],
194
+ // `rest` carries the documented `node --test` pass-throughs
195
+ // (`--test-name-pattern`, `--test-only`) — `parseTierArgv` has already
196
+ // rejected anything else. They precede the targets for the same reason
197
+ // `buildNodeTestArgs` orders them that way: Node stops parsing options
198
+ // at the first positional, so a flag after a target is silently read as
199
+ // another file pattern.
200
+ [...TEST_RUNNER_FLAGS, ...rest, ...chunk],
173
201
  { cwd, stdio: 'inherit', env },
174
202
  );
175
203
  if (testRun.error) {
@@ -199,7 +227,44 @@ export function runTestSuite({
199
227
  return status;
200
228
  }
201
229
 
230
+ /**
231
+ * `--help` contract for the runner — the pre-rendered shape
232
+ * `lib/cli-usage.js` accepts alongside a spec object, matching how
233
+ * `check-baselines.js` declares its own.
234
+ *
235
+ * Handed to `runAsCli`, which prints it and returns **before** `main` runs.
236
+ * That ordering is the whole point: with no `usage` the flag fell through to
237
+ * `runTestSuite`, which ran the tier preflight, spawned the full ~10 000-test
238
+ * suite, and swept `temp/` — 25+ seconds and a mutated working tree in
239
+ * answer to a question about flags.
240
+ *
241
+ * Exported so the unit test can assert the documented surface without
242
+ * spawning the CLI.
243
+ */
244
+ export const USAGE = `Usage: node .agents/scripts/run-tests.js [--tier <full|quick|integration|e2e>] [runner args...]
245
+
246
+ Run the test suite: tier preflight, \`node --test\` over the tier's targets,
247
+ then reserved-temp cleanup — which runs even when the suite fails.
248
+
249
+ Flags:
250
+ --tier <full|quick|integration|e2e>
251
+ Tier to run. Default: full — every test file except
252
+ \`tests/e2e/**\`, which only \`--tier e2e\` runs.
253
+ --test-name-pattern <re>, --test-only
254
+ The documented \`node --test\` pass-throughs, forwarded
255
+ verbatim in flag position, ahead of the file targets.
256
+ Any other \`--flag\` is rejected rather than forwarded:
257
+ \`node --test\` reads an unrecognized flag as another
258
+ file pattern, so forwarding one ran nothing and
259
+ still exited 0.
260
+ --help Show this message.
261
+
262
+ Exits with the first non-zero \`node --test\` chunk status, or 2 when the tier
263
+ preflight refuses to start the suite.
264
+ `;
265
+
202
266
  runAsCli(import.meta.url, async () => runTestSuite(), {
203
267
  source: 'run-tests',
204
268
  propagateExitCode: true,
269
+ usage: USAGE,
205
270
  });