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.
- package/.agents/README.md +16 -16
- package/.agents/agents/plan-critic.md +1 -1
- package/.agents/docs/SDLC.md +40 -40
- package/.agents/docs/configuration.md +41 -16
- package/.agents/docs/execution-reference.md +3 -3
- package/.agents/docs/quality-gates.md +2 -2
- package/.agents/docs/workflows.md +6 -6
- package/.agents/instructions.md +5 -5
- package/.agents/rules/changelog-style.md +3 -3
- package/.agents/rules/ci-remediation.md +1 -1
- package/.agents/rules/git-conventions-reference.md +4 -4
- package/.agents/rules/testing-standards.md +56 -0
- package/.agents/schemas/agentrc.schema.json +9 -9
- package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/acceptance-eval.js +1 -1
- package/.agents/scripts/apply-quality-bootstrap.js +1 -1
- package/.agents/scripts/audit-to-stories.js +2 -2
- package/.agents/scripts/boot-sweep.js +7 -1
- package/.agents/scripts/check-context-budget.js +195 -103
- package/.agents/scripts/check-cyclomatic.js +112 -42
- package/.agents/scripts/check-doc-links.js +32 -4
- package/.agents/scripts/check-generated-validator.js +202 -0
- package/.agents/scripts/check-lifecycle-lint.js +2 -68
- package/.agents/scripts/check-schema-references.js +2 -28
- package/.agents/scripts/check-test-temp-hygiene.js +1 -1
- package/.agents/scripts/check-workflow-cli-lint.js +1 -1
- package/.agents/scripts/deliver-light.js +10 -10
- package/.agents/scripts/deliver-recover.js +1 -1
- package/.agents/scripts/drain-pending-cleanup.js +19 -2
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/generate-workflows-doc.js +1 -1
- package/.agents/scripts/lib/audit-suite/selector.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +5 -5
- package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +1 -1
- package/.agents/scripts/lib/baselines/components.js +32 -2
- package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-budget.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
- package/.agents/scripts/lib/bdd-step-index.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +2 -2
- package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
- package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
- package/.agents/scripts/lib/bootstrap/issue-forms-template.js +1 -1
- package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
- package/.agents/scripts/lib/checks/core-bare-clean.js +1 -1
- package/.agents/scripts/lib/checks/loop-health.js +1 -1
- package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +1 -1
- package/.agents/scripts/lib/cli-args.js +1 -3
- package/.agents/scripts/lib/close-validation/gates.js +1 -1
- package/.agents/scripts/lib/close-validation/process.js +1 -1
- package/.agents/scripts/lib/command-header.js +1 -1
- package/.agents/scripts/lib/config/delivery-routing.js +1 -1
- package/.agents/scripts/lib/config/explain.js +1 -1
- package/.agents/scripts/lib/config/runners.js +2 -2
- package/.agents/scripts/lib/config/runtime.js +1 -1
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config/temp-paths.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +3 -3
- package/.agents/scripts/lib/config-settings-schema-quality.js +6 -6
- package/.agents/scripts/lib/config-settings-schema.js +49 -9
- package/.agents/scripts/lib/coverage-baseline.js +2 -2
- package/.agents/scripts/lib/cpu-pool.js +90 -10
- package/.agents/scripts/lib/crap-utils.js +6 -2
- package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
- package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
- package/.agents/scripts/lib/dependency-version.js +86 -0
- package/.agents/scripts/lib/duplicate-search.js +2 -2
- package/.agents/scripts/lib/errors/index.js +1 -1
- package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +5 -5
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +2 -2
- package/.agents/scripts/lib/findings/classify-finding.js +1 -1
- package/.agents/scripts/lib/findings/promote-finding.js +12 -12
- package/.agents/scripts/lib/findings/route-finding.js +2 -2
- package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
- package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
- package/.agents/scripts/lib/install-cmd-parser.js +1 -1
- package/.agents/scripts/lib/knip-entry-sync.js +1 -57
- package/.agents/scripts/lib/label-constants.js +2 -2
- package/.agents/scripts/lib/label-taxonomy.js +1 -1
- package/.agents/scripts/lib/maintainability-utils.js +108 -10
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/onboard/init-tail.js +4 -4
- package/.agents/scripts/lib/onboard/scaffold-docs.js +2 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +4 -4
- package/.agents/scripts/lib/orchestration/complexity-gate.js +5 -5
- package/.agents/scripts/lib/orchestration/deliver-recover.js +3 -3
- package/.agents/scripts/lib/orchestration/docs-digest.js +3 -3
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +2 -2
- package/.agents/scripts/lib/orchestration/light-backstop.js +1 -1
- package/.agents/scripts/lib/orchestration/light-escalation.js +6 -6
- package/.agents/scripts/lib/orchestration/light-suitability.js +19 -19
- package/.agents/scripts/lib/orchestration/plan-context.js +4 -4
- package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +2 -2
- package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +2 -2
- package/.agents/scripts/lib/orchestration/plan-metrics.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +1 -1
- package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +3 -3
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +6 -6
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +86 -25
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -3
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +6 -6
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +7 -7
- package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -1
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +3 -3
- package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
- package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
- package/.agents/scripts/lib/orchestration/remote-verifier.js +2 -2
- package/.agents/scripts/lib/orchestration/resolve-stories.js +4 -4
- package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
- package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
- package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
- package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
- package/.agents/scripts/lib/orchestration/run-epilogue.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +5 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +3 -3
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -2
- package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
- package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +14 -12
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
- package/.agents/scripts/lib/orchestration/ticket-validator.js +1 -1
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +6 -4
- package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
- package/.agents/scripts/lib/preflight-runner.js +3 -3
- package/.agents/scripts/lib/qa/qa-session.js +1 -1
- package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
- package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
- package/.agents/scripts/lib/signals/schema.js +1 -1
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
- package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
- package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
- package/.agents/scripts/lib/story-adjacency.js +2 -2
- package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
- package/.agents/scripts/lib/test-isolate/runner.js +3 -3
- package/.agents/scripts/lib/test-runner-contract.js +14 -6
- package/.agents/scripts/lib/test-tiers.js +135 -18
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/transpile.js +16 -3
- package/.agents/scripts/lib/util/concurrent-map.js +1 -1
- package/.agents/scripts/lib/util/parse-id-list.js +1 -1
- package/.agents/scripts/lib/wave-runner/live-probe.js +4 -4
- package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
- package/.agents/scripts/lib/workflow-closure.js +1 -1
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +170 -32
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
- package/.agents/scripts/lint-issue-body.js +3 -3
- package/.agents/scripts/plan-context.js +4 -4
- package/.agents/scripts/plan-critics.js +2 -2
- package/.agents/scripts/plan-persist.js +2 -2
- package/.agents/scripts/plan-run-epilogue.js +2 -2
- package/.agents/scripts/pr-watch-with-update.js +305 -137
- package/.agents/scripts/providers/github/board-add.js +1 -1
- package/.agents/scripts/providers/github/errors.js +1 -1
- package/.agents/scripts/providers/github/tickets.js +114 -21
- package/.agents/scripts/quality-preview.js +162 -70
- package/.agents/scripts/resolve-stories.js +3 -3
- package/.agents/scripts/resync-status-column.js +1 -1
- package/.agents/scripts/run-lint.js +1 -1
- package/.agents/scripts/run-test-profile.js +8 -5
- package/.agents/scripts/run-tests.js +79 -14
- package/.agents/scripts/single-story-close.js +2 -4
- package/.agents/scripts/single-story-init.js +10 -6
- package/.agents/scripts/stories-wave-tick.js +8 -8
- package/.agents/skills/core/idea-refinement/SKILL.md +5 -5
- package/.agents/skills/core/scope-triage/SKILL.md +3 -3
- package/.agents/skills/skills.index.json +1 -1
- package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -1
- package/.agents/workflows/audit-data-model.md +1 -1
- package/.agents/workflows/audit-documentation.md +2 -2
- package/.agents/workflows/audit-to-stories.md +11 -11
- package/.agents/workflows/git-deliver.md +4 -4
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/audit-lens-core.md +1 -1
- package/.agents/workflows/helpers/code-review.md +4 -4
- package/.agents/workflows/helpers/deliver-light.md +26 -26
- package/.agents/workflows/helpers/deliver-reference.md +15 -7
- package/.agents/workflows/helpers/deliver-story-reference.md +9 -4
- package/.agents/workflows/helpers/deliver-story.md +6 -7
- package/.agents/workflows/helpers/diagnose.md +2 -2
- package/.agents/workflows/helpers/parallel-tooling.md +1 -1
- package/.agents/workflows/helpers/plan-reference.md +9 -9
- package/.agents/workflows/helpers/qa-core.md +8 -8
- package/.agents/workflows/helpers/worktree-lifecycle.md +6 -6
- package/.agents/workflows/{deliver.md → mandrel-deliver.md} +15 -19
- package/.agents/workflows/{plan.md → mandrel-plan.md} +9 -9
- package/.agents/workflows/memory-consolidate.md +4 -4
- package/.agents/workflows/prototype.md +3 -3
- package/.agents/workflows/qa-assist.md +12 -12
- package/.agents/workflows/qa-explore.md +6 -6
- package/.agents/workflows/qa-run.md +1 -1
- package/README.md +7 -7
- package/docs/CHANGELOG.md +43 -0
- package/lib/cli/doctor.js +1 -1
- package/lib/cli/init.js +1 -1
- package/lib/cli/migrate.js +100 -94
- package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
- package/lib/migrations/index.js +32 -33
- package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
- package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
- package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
- package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +1 -1
- package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
- package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
- package/package.json +7 -3
- package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
- package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
- package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
- package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
- package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
- package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
- package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
- 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
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
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 {{
|
|
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
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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 =
|
|
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
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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(
|
|
513
|
+
const merged = mergeEnvelopes(miResult.envelope, crapResult.envelope, {
|
|
514
|
+
cyclomaticFlag,
|
|
515
|
+
});
|
|
407
516
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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 {
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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:
|
|
20
|
-
*
|
|
21
|
-
* glob will not do)
|
|
22
|
-
*
|
|
23
|
-
* `
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
});
|