mandrel 2.24.0 → 2.26.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/agents/acceptance-critic.md +19 -21
- package/.agents/agents/auditor.md +16 -19
- package/.agents/agents/story-worker.md +41 -54
- package/.agents/audit-checklists/architecture.md +5 -1
- package/.agents/audit-checklists/baselines.md +21 -0
- package/.agents/audit-checklists/quality.md +3 -0
- package/.agents/docs/agentrc-reference.json +2 -1
- package/.agents/docs/configuration.md +2 -1
- package/.agents/docs/quality-gates.md +80 -18
- package/.agents/docs/workflows.md +4 -2
- package/.agents/instructions.md +1 -1
- package/.agents/rules/ci-remediation.md +68 -3
- package/.agents/schemas/agentrc.schema.json +6 -1
- package/.agents/schemas/audit-rules.json +15 -0
- package/.agents/schemas/baselines/audit-baselines-envelope.schema.json +242 -0
- package/.agents/schemas/baselines/baseline-envelope.schema.json +8 -0
- package/.agents/schemas/baselines/crap.schema.json +13 -1
- package/.agents/schemas/crap-report.schema.json +37 -0
- package/.agents/schemas/model-attribution.schema.json +4 -0
- package/.agents/scripts/acceptance-eval.js +124 -15
- package/.agents/scripts/audit-baselines.js +136 -0
- package/.agents/scripts/audit-labels-bootstrap.js +16 -95
- package/.agents/scripts/audit-to-stories.js +33 -3
- package/.agents/scripts/check-arch-cycles.js +12 -93
- package/.agents/scripts/check-baseline-drift.js +16 -3
- package/.agents/scripts/check-baselines.js +19 -3
- package/.agents/scripts/check-context-budget.js +95 -14
- package/.agents/scripts/check-cyclomatic.js +214 -0
- package/.agents/scripts/check-schema-references.js +392 -0
- package/.agents/scripts/check-test-temp-hygiene.js +121 -1
- package/.agents/scripts/check-workflow-timeouts.js +291 -0
- package/.agents/scripts/cleanup-repo-test-temp.js +7 -5
- package/.agents/scripts/deliver-light.js +3 -4
- package/.agents/scripts/deliver-recover.js +13 -0
- package/.agents/scripts/diagnose-friction.js +85 -19
- package/.agents/scripts/lib/audit-baselines/engine.js +177 -0
- package/.agents/scripts/lib/audit-baselines/gate-surface.js +63 -0
- package/.agents/scripts/lib/audit-baselines/headroom.js +72 -0
- package/.agents/scripts/lib/audit-baselines/hotspots.js +69 -0
- package/.agents/scripts/lib/audit-baselines/kinds.js +313 -0
- package/.agents/scripts/lib/audit-baselines/outliers.js +100 -0
- package/.agents/scripts/lib/audit-baselines/read.js +87 -0
- package/.agents/scripts/lib/audit-baselines/staleness.js +123 -0
- package/.agents/scripts/lib/audit-baselines/surface-entry.js +106 -0
- package/.agents/scripts/lib/audit-baselines/trend.js +125 -0
- package/.agents/scripts/lib/audit-baselines/weights.js +193 -0
- package/.agents/scripts/lib/audit-suite/index.js +0 -5
- package/.agents/scripts/lib/audit-suite/selector.js +9 -62
- package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +177 -0
- package/.agents/scripts/lib/audit-to-stories/audit-lenses.js +1 -0
- package/.agents/scripts/lib/audit-to-stories/build-story-body.js +35 -3
- package/.agents/scripts/lib/audit-to-stories/group-findings.js +20 -11
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +23 -9
- package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +9 -2
- package/.agents/scripts/lib/baseline-loader.js +0 -0
- package/.agents/scripts/lib/baseline-schema-registry.js +13 -1
- package/.agents/scripts/lib/baselines/diff-scope-cli.js +22 -160
- package/.agents/scripts/lib/baselines/duplication-scanner.js +27 -0
- package/.agents/scripts/lib/baselines/git-base.js +0 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +499 -57
- package/.agents/scripts/lib/baselines/preview-gates.js +58 -15
- package/.agents/scripts/lib/baselines/reader.js +10 -0
- package/.agents/scripts/lib/baselines/refresh-service.js +69 -11
- package/.agents/scripts/lib/baselines/scope.js +39 -90
- package/.agents/scripts/lib/baselines/writer.js +16 -7
- package/.agents/scripts/lib/changed-files.js +8 -1
- package/.agents/scripts/lib/cli-args.js +115 -1
- package/.agents/scripts/lib/close-validation/runner.js +70 -25
- package/.agents/scripts/lib/config-settings-schema-delivery.js +5 -0
- package/.agents/scripts/lib/crap-engine.js +124 -27
- package/.agents/scripts/lib/crap-method-identity.js +153 -0
- package/.agents/scripts/lib/crap-utils.js +86 -13
- package/.agents/scripts/lib/cyclomatic-ceiling.js +265 -0
- package/.agents/scripts/lib/env-loader.js +46 -16
- package/.agents/scripts/lib/feedback-loop/audit-results-graduator.js +0 -2
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +0 -2
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +0 -2
- package/.agents/scripts/lib/findings/route-finding.js +123 -17
- package/.agents/scripts/lib/findings/severity.js +80 -2
- package/.agents/scripts/lib/git/cached-fetch.js +0 -0
- package/.agents/scripts/lib/git-utils.js +136 -80
- package/.agents/scripts/lib/import-graph.js +156 -0
- package/.agents/scripts/lib/label-constants.js +17 -0
- package/.agents/scripts/lib/observability/runtime-friction.js +17 -2
- package/.agents/scripts/lib/observability/source-classifier.js +175 -2
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +110 -17
- package/.agents/scripts/lib/orchestration/ceremony-routing.js +17 -12
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +36 -6
- package/.agents/scripts/lib/orchestration/check-baselines/phases/evaluate.js +5 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/floors.js +12 -1
- package/.agents/scripts/lib/orchestration/check-baselines/phases/report.js +8 -1
- package/.agents/scripts/lib/orchestration/ci-rerun-guard.js +548 -0
- package/.agents/scripts/lib/orchestration/deliver-recover.js +2 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +10 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +39 -3
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +71 -20
- package/.agents/scripts/lib/orchestration/light-backstop.js +20 -3
- package/.agents/scripts/lib/orchestration/light-escalation.js +82 -3
- package/.agents/scripts/lib/orchestration/light-suitability.js +103 -5
- package/.agents/scripts/lib/orchestration/plan-context.js +119 -66
- package/.agents/scripts/lib/orchestration/plan-persist/fan-out-gate.js +31 -5
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +216 -109
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +61 -13
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +79 -22
- package/.agents/scripts/lib/orchestration/plan-text-hygiene.js +51 -20
- package/.agents/scripts/lib/orchestration/planning/authoring-context.js +70 -74
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +231 -0
- package/.agents/scripts/lib/orchestration/resolve-stories.js +18 -17
- package/.agents/scripts/lib/orchestration/retro-proposals.js +0 -0
- package/.agents/scripts/lib/orchestration/run-epilogue.js +12 -0
- package/.agents/scripts/lib/orchestration/run-scoped-config.js +276 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +41 -10
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +125 -37
- package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +6 -6
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +42 -38
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pull-request.js +129 -21
- package/.agents/scripts/lib/orchestration/single-story-close/phases/push.js +6 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +267 -125
- package/.agents/scripts/lib/orchestration/spec-budget.js +16 -5
- package/.agents/scripts/lib/orchestration/story-follow-ups.js +182 -95
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +22 -0
- package/.agents/scripts/lib/orchestration/ticket-validator.js +5 -11
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +4 -4
- package/.agents/scripts/lib/reserved-test-ids.js +77 -0
- package/.agents/scripts/lib/story-adjacency.js +3 -3
- package/.agents/scripts/lib/story-plan.js +137 -42
- package/.agents/scripts/lib/templates/decomposer-prompts.js +4 -4
- package/.agents/scripts/lib/test-runner-contract.js +134 -0
- package/.agents/scripts/lib/test-tiers.js +11 -2
- package/.agents/scripts/lib/ticket-body-sections.js +1 -1
- package/.agents/scripts/lib/util/concurrent-map.js +17 -0
- package/.agents/scripts/lib/util/parse-id-list.js +103 -0
- package/.agents/scripts/lib/util/poll-loop.js +18 -2
- package/.agents/scripts/lib/wave-runner/live-probe.js +29 -13
- package/.agents/scripts/lib/wave-runner/ready-set.js +254 -43
- package/.agents/scripts/lib/workers/combined-mi-crap-worker.js +10 -13
- package/.agents/scripts/lib/workers/crap-worker.js +8 -13
- package/.agents/scripts/lib/workers/maintainability-report-worker.js +4 -10
- package/.agents/scripts/lib/workers/maintainability-worker.js +4 -10
- package/.agents/scripts/lib/workers/serve-worker-messages.js +35 -0
- package/.agents/scripts/lib/worktree/git-hooks.js +206 -0
- package/.agents/scripts/lib/worktree/lifecycle/creation.js +6 -0
- package/.agents/scripts/lib/worktree-manager.js +14 -0
- package/.agents/scripts/plan-run-epilogue.js +17 -5
- package/.agents/scripts/pr-watch-with-update.js +568 -213
- package/.agents/scripts/providers/github/tickets.js +33 -10
- package/.agents/scripts/provision-git-hooks.js +85 -0
- package/.agents/scripts/quality-preview.js +137 -28
- package/.agents/scripts/resolve-stories.js +4 -1
- package/.agents/scripts/run-coverage.js +86 -35
- package/.agents/scripts/run-lint.js +20 -0
- package/.agents/scripts/run-tests.js +36 -36
- package/.agents/scripts/single-story-close.js +34 -2
- package/.agents/scripts/single-story-confirm-merge.js +22 -6
- package/.agents/scripts/single-story-init.js +7 -0
- package/.agents/scripts/stories-wave-tick.js +308 -47
- package/.agents/scripts/story-plan.js +65 -9
- package/.agents/scripts/update-coverage-baseline.js +34 -4
- package/.agents/scripts/update-crap-baseline.js +42 -4
- package/.agents/scripts/update-duplication-baseline.js +209 -83
- package/.agents/scripts/update-maintainability-baseline.js +31 -4
- package/.agents/scripts/validate-docs-freshness.js +1 -0
- package/.agents/skills/core/diagnose-friction/SKILL.md +4 -1
- package/.agents/skills/core/gates-and-baselines/SKILL.md +17 -11
- package/.agents/skills/skills.index.json +2 -2
- package/.agents/templates/single-story-body.md +16 -8
- package/.agents/workflows/audit-architecture.md +69 -13
- package/.agents/workflows/audit-baselines.md +289 -0
- package/.agents/workflows/audit-navigability.md +5 -4
- package/.agents/workflows/audit-quality.md +26 -0
- package/.agents/workflows/audit-to-stories.md +30 -4
- package/.agents/workflows/deliver.md +92 -97
- package/.agents/workflows/helpers/acceptance-self-eval.md +47 -10
- package/.agents/workflows/helpers/audit-lens-core.md +25 -4
- package/.agents/workflows/helpers/code-quality-guardrails.md +9 -2
- package/.agents/workflows/helpers/deliver-digest.md +41 -21
- package/.agents/workflows/helpers/deliver-reference.md +95 -4
- package/.agents/workflows/helpers/deliver-story-reference.md +174 -52
- package/.agents/workflows/helpers/deliver-story.md +104 -119
- package/.agents/workflows/helpers/plan-reference.md +49 -5
- package/.agents/workflows/memory-consolidate.md +116 -0
- package/.agents/workflows/plan.md +62 -75
- package/README.md +13 -6
- package/docs/CHANGELOG.md +93 -0
- package/lib/cli/update.js +14 -11
- package/lib/cli/version-check.js +9 -1
- package/package.json +9 -4
- package/.agents/schemas/friction-event.schema.json +0 -56
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +0 -707
|
@@ -321,6 +321,16 @@ export class TicketGateway {
|
|
|
321
321
|
* avoid a read-before-write. When other PATCH fields are present, or when
|
|
322
322
|
* removing labels, computes the final label set and returns it to the
|
|
323
323
|
* caller for inclusion in the PATCH.
|
|
324
|
+
*
|
|
325
|
+
* The additive POST goes through `withTransientRetry` (Story #4961) on the
|
|
326
|
+
* 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
|
|
328
|
+
* and the checkpoint fan-out — off serial, which is precisely what makes
|
|
329
|
+
* GitHub's secondary rate limit likelier; `gh-exec` already classifies that
|
|
330
|
+
* as transient, so the only thing missing was a backoff behind it.
|
|
331
|
+
* Retry does not change what the caller observes on a genuine failure: an
|
|
332
|
+
* exhausted or non-transient error still throws, so `markStoriesReady`
|
|
333
|
+
* still collects it into the complete failure set.
|
|
324
334
|
*/
|
|
325
335
|
async _applyLabelMutations(
|
|
326
336
|
ticketId,
|
|
@@ -331,11 +341,15 @@ export class TicketGateway {
|
|
|
331
341
|
const { add = [], remove = [] } = labelMutations;
|
|
332
342
|
|
|
333
343
|
if (add.length > 0 && remove.length === 0 && !hasOtherPatchFields) {
|
|
334
|
-
await
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
344
|
+
await withTransientRetry(
|
|
345
|
+
() =>
|
|
346
|
+
this._gh.api({
|
|
347
|
+
method: 'POST',
|
|
348
|
+
endpoint: `/repos/${this.owner}/${this.repo}/issues/${ticketId}/labels`,
|
|
349
|
+
body: { labels: add },
|
|
350
|
+
}),
|
|
351
|
+
{ label: `addLabels #${ticketId}`, onRetry: defaultRetryWarn },
|
|
352
|
+
);
|
|
339
353
|
return { skipPatch: true };
|
|
340
354
|
}
|
|
341
355
|
|
|
@@ -351,6 +365,11 @@ export class TicketGateway {
|
|
|
351
365
|
}
|
|
352
366
|
|
|
353
367
|
/**
|
|
368
|
+
* The issue PATCH is wrapped in `withTransientRetry` (Story #4961) for the
|
|
369
|
+
* same reason as the additive label POST above — see
|
|
370
|
+
* {@link TicketGateway#_applyLabelMutations}. Both are the write half of the
|
|
371
|
+
* fan-outs Story #4952 raised; the reads in this file were already wrapped.
|
|
372
|
+
*
|
|
354
373
|
* @field-manifest PATCH /repos/{owner}/{repo}/issues/{n}:
|
|
355
374
|
* body, assignees, state, state_reason, labels
|
|
356
375
|
*/
|
|
@@ -379,11 +398,15 @@ export class TicketGateway {
|
|
|
379
398
|
}
|
|
380
399
|
|
|
381
400
|
if (Object.keys(patch).length > 0) {
|
|
382
|
-
await
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
401
|
+
await withTransientRetry(
|
|
402
|
+
() =>
|
|
403
|
+
this._gh.api({
|
|
404
|
+
method: 'PATCH',
|
|
405
|
+
endpoint: `/repos/${this.owner}/${this.repo}/issues/${ticketId}`,
|
|
406
|
+
body: patch,
|
|
407
|
+
}),
|
|
408
|
+
{ label: `updateTicket #${ticketId}`, onRetry: defaultRetryWarn },
|
|
409
|
+
);
|
|
387
410
|
this.invalidateTicket(ticketId);
|
|
388
411
|
}
|
|
389
412
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* provision-git-hooks.js
|
|
5
|
+
*
|
|
6
|
+
* Materialize the repository's git hooks into a linked worktree, so commits
|
|
7
|
+
* made there are subject to the same `commit-msg` / `pre-commit` / `pre-push`
|
|
8
|
+
* gates as commits made in the main checkout.
|
|
9
|
+
*
|
|
10
|
+
* Worktrees created by `single-story-init.js` are provisioned automatically as
|
|
11
|
+
* part of `worktree.bootstrap`. This entry point exists for the worktrees the
|
|
12
|
+
* orchestrator never creates — a harness or hand-made `git worktree add` —
|
|
13
|
+
* which are otherwise unreachable. Run it from inside such a worktree with no
|
|
14
|
+
* arguments.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { runAsCli } from './lib/cli-utils.js';
|
|
19
|
+
import {
|
|
20
|
+
materializeGitHooks,
|
|
21
|
+
resolveCommonCheckout,
|
|
22
|
+
} from './lib/worktree/git-hooks.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} [opts]
|
|
26
|
+
* @param {string[]} [opts.argv]
|
|
27
|
+
* @param {string} [opts.cwd]
|
|
28
|
+
* @returns {{ action: string, reason?: string, hooksPath: string|null,
|
|
29
|
+
* target: string|null, hooks: string[], worktree: string, repoRoot: string }}
|
|
30
|
+
*/
|
|
31
|
+
export function runProvisionGitHooks({
|
|
32
|
+
argv = process.argv.slice(2),
|
|
33
|
+
cwd = process.cwd(),
|
|
34
|
+
} = {}) {
|
|
35
|
+
const positional = [];
|
|
36
|
+
let repoRootFlag = null;
|
|
37
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
38
|
+
if (argv[i] === '--repo-root') {
|
|
39
|
+
repoRootFlag = argv[i + 1];
|
|
40
|
+
if (!repoRootFlag) {
|
|
41
|
+
throw new Error('--repo-root requires a path');
|
|
42
|
+
}
|
|
43
|
+
i += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
positional.push(argv[i]);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const worktree = path.resolve(cwd, positional[0] ?? '.');
|
|
50
|
+
const repoRoot = repoRootFlag
|
|
51
|
+
? path.resolve(cwd, repoRootFlag)
|
|
52
|
+
: resolveCommonCheckout(worktree);
|
|
53
|
+
|
|
54
|
+
const result = materializeGitHooks({ repoRoot, worktree });
|
|
55
|
+
return { ...result, worktree, repoRoot };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
runAsCli(
|
|
59
|
+
import.meta.url,
|
|
60
|
+
async () => {
|
|
61
|
+
const result = runProvisionGitHooks();
|
|
62
|
+
// Compact single-line digest: this is the machine contract, and the
|
|
63
|
+
// hook file list is bounded by the hooks directory itself.
|
|
64
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
source: 'provision-git-hooks',
|
|
68
|
+
usage: {
|
|
69
|
+
invocation:
|
|
70
|
+
'node .agents/scripts/provision-git-hooks.js [<worktree-path>] [--repo-root <path>]',
|
|
71
|
+
summary:
|
|
72
|
+
"Materialize the repository's git hooks into a linked worktree so its commits are gated.",
|
|
73
|
+
flags: [
|
|
74
|
+
[
|
|
75
|
+
'<worktree-path>',
|
|
76
|
+
'Worktree to provision (default: the current directory).',
|
|
77
|
+
],
|
|
78
|
+
[
|
|
79
|
+
'--repo-root <path>',
|
|
80
|
+
'Checkout that owns the hooks (default: derived from --git-common-dir).',
|
|
81
|
+
],
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
);
|
|
@@ -30,6 +30,8 @@ import {
|
|
|
30
30
|
runMaintainabilityPreview,
|
|
31
31
|
} from './lib/baselines/preview-gates.js';
|
|
32
32
|
import { respondToHelp } from './lib/cli-usage.js';
|
|
33
|
+
import { getQuality, resolveConfig } from './lib/config-resolver.js';
|
|
34
|
+
import { resolveCyclomaticPolicy } from './lib/cyclomatic-ceiling.js';
|
|
33
35
|
|
|
34
36
|
const USAGE = {
|
|
35
37
|
invocation:
|
|
@@ -46,6 +48,14 @@ const USAGE = {
|
|
|
46
48
|
],
|
|
47
49
|
};
|
|
48
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Framework default for `delivery.quality.codingGuardrails.cyclomaticFlag`,
|
|
53
|
+
* used only when a caller drives `mergeEnvelopes` / `renderTable` without a
|
|
54
|
+
* resolved config in hand (tests, and the pure-function surface). `runCli`
|
|
55
|
+
* always passes the resolved value.
|
|
56
|
+
*/
|
|
57
|
+
const DEFAULT_CYCLOMATIC_FLAG = 8;
|
|
58
|
+
|
|
49
59
|
/**
|
|
50
60
|
* Parse `--changed-since <ref>` from argv. Defaults to `HEAD` when the flag is
|
|
51
61
|
* present without a value. Returns `null` when the flag is absent so callers
|
|
@@ -97,6 +107,47 @@ export function parseStagedFlag(argv) {
|
|
|
97
107
|
return argv.includes('--staged');
|
|
98
108
|
}
|
|
99
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Coerce a caller-supplied flag ceiling to a usable number, falling back to
|
|
112
|
+
* the framework default for anything non-finite.
|
|
113
|
+
*
|
|
114
|
+
* @param {unknown} value
|
|
115
|
+
* @returns {number}
|
|
116
|
+
*/
|
|
117
|
+
function normalizeFlag(value) {
|
|
118
|
+
const n = Number(value);
|
|
119
|
+
return Number.isFinite(n) ? n : DEFAULT_CYCLOMATIC_FLAG;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fold one CRAP violation into its per-file aggregate row. Mutates `row`.
|
|
124
|
+
*
|
|
125
|
+
* Split out of `mergeEnvelopes` (Story #4923): threading the resolved
|
|
126
|
+
* `cyclomaticFlag` through pushed that function from c=12 — exactly at the
|
|
127
|
+
* must-fix ceiling — to c=13, and the same Story starts *enforcing* that
|
|
128
|
+
* ceiling. Recording its own breach in `baselines/cyclomatic.json` would have
|
|
129
|
+
* been the first re-spend of the slack the Story reclaims.
|
|
130
|
+
*
|
|
131
|
+
* @param {{ worstCrapDelta: number, newOverCeilingMethods: number }} row
|
|
132
|
+
* @param {{ crap?: number, ceiling?: number, baseline?: number, cyclomatic?: number, kind?: string }} v
|
|
133
|
+
* @param {number} flag resolved `codingGuardrails.cyclomaticFlag`
|
|
134
|
+
* @returns {void}
|
|
135
|
+
*/
|
|
136
|
+
function foldCrapViolation(row, v, flag) {
|
|
137
|
+
const crap = Number(v.crap ?? 0);
|
|
138
|
+
const isNew = v.kind === 'new';
|
|
139
|
+
const against = Number((isNew ? v.ceiling : v.baseline) ?? 0);
|
|
140
|
+
const delta = crap - against;
|
|
141
|
+
if (Number.isFinite(delta) && delta > row.worstCrapDelta) {
|
|
142
|
+
row.worstCrapDelta = delta;
|
|
143
|
+
}
|
|
144
|
+
if (!isNew) return;
|
|
145
|
+
const cyclomatic = Number(v.cyclomatic ?? 0);
|
|
146
|
+
if (Number.isFinite(cyclomatic) && cyclomatic > flag) {
|
|
147
|
+
row.newOverCeilingMethods += 1;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
100
151
|
/**
|
|
101
152
|
* Merge an MI envelope (from `runMaintainabilityPreview`) and a CRAP
|
|
102
153
|
* envelope (from `runCrapPreview`) into a per-file delta map. Pure —
|
|
@@ -109,9 +160,16 @@ export function parseStagedFlag(argv) {
|
|
|
109
160
|
* methods (max of `crap - baseline` for matched-baseline rows, `crap`
|
|
110
161
|
* for new-method rows). 0 when the file has no CRAP violations.
|
|
111
162
|
* - `newOverCeilingMethods`: count of new-method violations (kind:'new')
|
|
112
|
-
* scoring above the
|
|
113
|
-
*
|
|
114
|
-
*
|
|
163
|
+
* scoring above the flag ceiling. The CRAP envelope's `cyclomatic` field
|
|
164
|
+
* is the per-method `c` reading.
|
|
165
|
+
*
|
|
166
|
+
* `cyclomaticFlag` is the resolved
|
|
167
|
+
* `delivery.quality.codingGuardrails.cyclomaticFlag` (Story #4923). It used to
|
|
168
|
+
* be the literal `8` written into this function and into the column header, so
|
|
169
|
+
* a consumer that tuned the knob saw its own value validated by the schema,
|
|
170
|
+
* defaulted by the bootstrap, resolved by `lib/config/quality.js` — and then
|
|
171
|
+
* ignored here. The parameter defaults to the framework default so a caller
|
|
172
|
+
* with no config in hand still gets the historical reading.
|
|
115
173
|
*
|
|
116
174
|
* @param {{ violations?: Array<{ file: string, drop?: number }> } | null} miEnvelope
|
|
117
175
|
* @param {{ violations?: Array<{
|
|
@@ -122,6 +180,7 @@ export function parseStagedFlag(argv) {
|
|
|
122
180
|
* cyclomatic: number,
|
|
123
181
|
* kind: 'new' | 'regression' | 'drifted-regression' | string,
|
|
124
182
|
* }>} | null} crapEnvelope
|
|
183
|
+
* @param {{ cyclomaticFlag?: number }} [opts]
|
|
125
184
|
* @returns {{
|
|
126
185
|
* rows: Array<{
|
|
127
186
|
* file: string,
|
|
@@ -130,9 +189,15 @@ export function parseStagedFlag(argv) {
|
|
|
130
189
|
* newOverCeilingMethods: number,
|
|
131
190
|
* }>,
|
|
132
191
|
* totals: { miRegressions: number, crapViolations: number },
|
|
192
|
+
* cyclomaticFlag: number,
|
|
133
193
|
* }}
|
|
134
194
|
*/
|
|
135
|
-
export function mergeEnvelopes(
|
|
195
|
+
export function mergeEnvelopes(
|
|
196
|
+
miEnvelope,
|
|
197
|
+
crapEnvelope,
|
|
198
|
+
{ cyclomaticFlag = DEFAULT_CYCLOMATIC_FLAG } = {},
|
|
199
|
+
) {
|
|
200
|
+
const flag = normalizeFlag(cyclomaticFlag);
|
|
136
201
|
/** @type {Map<string, { miDrop: number, worstCrapDelta: number, newOverCeilingMethods: number }>} */
|
|
137
202
|
const byFile = new Map();
|
|
138
203
|
const ensure = (file) => {
|
|
@@ -155,25 +220,7 @@ export function mergeEnvelopes(miEnvelope, crapEnvelope) {
|
|
|
155
220
|
const crapViolations = crapEnvelope?.violations ?? [];
|
|
156
221
|
for (const v of crapViolations) {
|
|
157
222
|
if (!v?.file) continue;
|
|
158
|
-
|
|
159
|
-
const crap = Number(v.crap ?? 0);
|
|
160
|
-
if (v.kind === 'new') {
|
|
161
|
-
const ceiling = Number(v.ceiling ?? 0);
|
|
162
|
-
const delta = crap - ceiling;
|
|
163
|
-
if (Number.isFinite(delta) && delta > row.worstCrapDelta) {
|
|
164
|
-
row.worstCrapDelta = delta;
|
|
165
|
-
}
|
|
166
|
-
const cyclomatic = Number(v.cyclomatic ?? 0);
|
|
167
|
-
if (Number.isFinite(cyclomatic) && cyclomatic > 8) {
|
|
168
|
-
row.newOverCeilingMethods += 1;
|
|
169
|
-
}
|
|
170
|
-
} else {
|
|
171
|
-
const baseline = Number(v.baseline ?? 0);
|
|
172
|
-
const delta = crap - baseline;
|
|
173
|
-
if (Number.isFinite(delta) && delta > row.worstCrapDelta) {
|
|
174
|
-
row.worstCrapDelta = delta;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
223
|
+
foldCrapViolation(ensure(v.file), v, flag);
|
|
177
224
|
}
|
|
178
225
|
|
|
179
226
|
const rows = Array.from(byFile.entries())
|
|
@@ -188,9 +235,33 @@ export function mergeEnvelopes(miEnvelope, crapEnvelope) {
|
|
|
188
235
|
(crapEnvelope?.summary?.regressions ?? 0) +
|
|
189
236
|
(crapEnvelope?.summary?.newViolations ?? 0),
|
|
190
237
|
},
|
|
238
|
+
cyclomaticFlag: flag,
|
|
191
239
|
};
|
|
192
240
|
}
|
|
193
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Render the named diagnostics a gate envelope carries, or `null` when it
|
|
244
|
+
* carries none (Story #4866).
|
|
245
|
+
*
|
|
246
|
+
* A diagnostic is what a gate emits *instead of* per-method verdicts when it
|
|
247
|
+
* has established that no verdict it could produce would be meaningful — an
|
|
248
|
+
* incomparable baseline, or a comparison basis whose drifted-row ratio proves
|
|
249
|
+
* the two sides disagree on line coordinates. It must reach the operator
|
|
250
|
+
* verbatim: the gate exits 0, so silence would read as a clean run.
|
|
251
|
+
*
|
|
252
|
+
* @param {Array<{ envelope: { diagnostics?: Array<{name: string, message: string}> } | null }>} results
|
|
253
|
+
* @returns {string | null}
|
|
254
|
+
*/
|
|
255
|
+
export function renderDiagnostics(results) {
|
|
256
|
+
const lines = [];
|
|
257
|
+
for (const { envelope } of results ?? []) {
|
|
258
|
+
for (const d of envelope?.diagnostics ?? []) {
|
|
259
|
+
lines.push(`[${d.name}] ${d.message}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return lines.length === 0 ? null : lines.join('\n');
|
|
263
|
+
}
|
|
264
|
+
|
|
194
265
|
/**
|
|
195
266
|
* Compute the CLI exit code from a merge result + per-gate exit codes. Pure.
|
|
196
267
|
*
|
|
@@ -216,21 +287,28 @@ export function computeExitCode(merged, miExit, crapExit) {
|
|
|
216
287
|
}
|
|
217
288
|
|
|
218
289
|
/**
|
|
219
|
-
* Render the per-file delta table.
|
|
220
|
-
* "file", "MI delta", "worst CRAP delta", "new-method count over c
|
|
290
|
+
* Render the per-file delta table. Columns:
|
|
291
|
+
* "file", "MI delta", "worst CRAP delta", "new-method count over c=<flag>".
|
|
292
|
+
*
|
|
293
|
+
* The last header used to hardcode `c=8`, which quietly lied to any consumer
|
|
294
|
+
* that had tuned `codingGuardrails.cyclomaticFlag`. It now names the value the
|
|
295
|
+
* count was actually taken against, read off the merge result.
|
|
221
296
|
*
|
|
222
297
|
* Pure — accepts pre-computed merge rows and returns a multi-line string. The
|
|
223
298
|
* table renders even on a clean diff so operators see the "no drift" signal.
|
|
224
299
|
*
|
|
225
|
-
* @param {{ rows: Array<{ file: string, miDrop: number, worstCrapDelta: number, newOverCeilingMethods: number }>, totals: { miRegressions: number, crapViolations: number } }} merged
|
|
300
|
+
* @param {{ rows: Array<{ file: string, miDrop: number, worstCrapDelta: number, newOverCeilingMethods: number }>, totals: { miRegressions: number, crapViolations: number }, cyclomaticFlag?: number }} merged
|
|
226
301
|
* @returns {string}
|
|
227
302
|
*/
|
|
228
303
|
export function renderTable(merged) {
|
|
304
|
+
const flag = Number.isFinite(Number(merged?.cyclomaticFlag))
|
|
305
|
+
? Number(merged.cyclomaticFlag)
|
|
306
|
+
: DEFAULT_CYCLOMATIC_FLAG;
|
|
229
307
|
const header = [
|
|
230
308
|
'file',
|
|
231
309
|
'MI delta',
|
|
232
310
|
'worst CRAP delta',
|
|
233
|
-
|
|
311
|
+
`new-method count over c=${flag}`,
|
|
234
312
|
];
|
|
235
313
|
const lines = [];
|
|
236
314
|
lines.push(`| ${header.join(' | ')} |`);
|
|
@@ -251,6 +329,31 @@ export function renderTable(merged) {
|
|
|
251
329
|
return lines.join('\n');
|
|
252
330
|
}
|
|
253
331
|
|
|
332
|
+
/**
|
|
333
|
+
* Resolve `codingGuardrails.cyclomaticFlag` for the tree at `cwd`
|
|
334
|
+
* (Story #4923), falling back to the framework default when the config cannot
|
|
335
|
+
* be resolved at all.
|
|
336
|
+
*
|
|
337
|
+
* Best-effort by design: `quality:preview` is a developer-facing report, and a
|
|
338
|
+
* run in a tree with no readable `.agentrc.json` should still render its table
|
|
339
|
+
* rather than abort. Extracted from `runCli` rather than inlined so the CLI
|
|
340
|
+
* body stays under the cyclomatic must-fix ceiling this same Story starts
|
|
341
|
+
* enforcing — a gate whose own delivery breaches it is not a gate.
|
|
342
|
+
*
|
|
343
|
+
* @param {{ cwd: string, stderr: { write: (s: string) => void } }} args
|
|
344
|
+
* @returns {number}
|
|
345
|
+
*/
|
|
346
|
+
function resolveCyclomaticFlag({ cwd, stderr }) {
|
|
347
|
+
try {
|
|
348
|
+
return resolveCyclomaticPolicy(getQuality(resolveConfig({ cwd }))).flag;
|
|
349
|
+
} catch (err) {
|
|
350
|
+
stderr.write(
|
|
351
|
+
`[quality:preview] config resolution failed, using cyclomaticFlag=${DEFAULT_CYCLOMATIC_FLAG}: ${err?.message ?? err}\n`,
|
|
352
|
+
);
|
|
353
|
+
return DEFAULT_CYCLOMATIC_FLAG;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
254
357
|
/**
|
|
255
358
|
* Top-level CLI entry: invoke both per-kind preview runners, merge, render,
|
|
256
359
|
* and exit with the right code. Exposed as `runCli` so tests can drive the
|
|
@@ -296,7 +399,11 @@ export async function runCli({
|
|
|
296
399
|
const crapExit = crapResult.exitCode;
|
|
297
400
|
const miEnvelope = miResult.envelope;
|
|
298
401
|
const crapEnvelope = crapResult.envelope;
|
|
299
|
-
|
|
402
|
+
// Story #4923 — the over-ceiling column counts against the *resolved*
|
|
403
|
+
// `codingGuardrails.cyclomaticFlag`, not the literal that used to be written
|
|
404
|
+
// into `mergeEnvelopes` and the column header.
|
|
405
|
+
const cyclomaticFlag = resolveCyclomaticFlag({ cwd, stderr });
|
|
406
|
+
const merged = mergeEnvelopes(miEnvelope, crapEnvelope, { cyclomaticFlag });
|
|
300
407
|
|
|
301
408
|
if (json) {
|
|
302
409
|
stdout.write(
|
|
@@ -320,6 +427,8 @@ export async function runCli({
|
|
|
320
427
|
: `scope=diff ref=${ref}\n\n`,
|
|
321
428
|
);
|
|
322
429
|
stdout.write(`${renderTable(merged)}\n`);
|
|
430
|
+
const diagnostics = renderDiagnostics([miResult, crapResult]);
|
|
431
|
+
if (diagnostics) stdout.write(`\n${diagnostics}\n`);
|
|
323
432
|
if (miExit !== 0 || crapExit !== 0) {
|
|
324
433
|
stderr.write(
|
|
325
434
|
`\n[quality:preview] gate exits: mi=${miExit} crap=${crapExit}\n`,
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*
|
|
25
25
|
* Usage:
|
|
26
26
|
* node .agents/scripts/resolve-stories.js --ids 101,102
|
|
27
|
+
* node .agents/scripts/resolve-stories.js --ids 101-104 # inclusive range
|
|
27
28
|
* node .agents/scripts/resolve-stories.js --ids 101,102 --pretty
|
|
28
29
|
* node .agents/scripts/resolve-stories.js --ids 101 --no-native # skip the dependencies API
|
|
29
30
|
*
|
|
@@ -65,7 +66,9 @@ blocked_by edges, with every blocker (in-set or foreign) resolved against its
|
|
|
65
66
|
real issue state.
|
|
66
67
|
|
|
67
68
|
Options:
|
|
68
|
-
--ids <csv> Comma-separated Story issue numbers. Required.
|
|
69
|
+
--ids <csv> Comma-separated Story issue numbers. Required. A token may be
|
|
70
|
+
a single id (4922) or an inclusive dash range (4922-4926);
|
|
71
|
+
ranges expand in place and dedupe against the rest.
|
|
69
72
|
--pretty Pretty-print the JSON envelope.
|
|
70
73
|
--no-native Skip the native blocked_by read (body edges only).
|
|
71
74
|
--help Show this help.
|
|
@@ -10,8 +10,16 @@
|
|
|
10
10
|
* Node script keeps the env injection portable.
|
|
11
11
|
*
|
|
12
12
|
* Pipeline:
|
|
13
|
+
* 0. Run the full tier's preflight (`runTierPreflight`). Story #4936: this
|
|
14
|
+
* used to be a `pretest:coverage` npm script that CI had to name
|
|
15
|
+
* explicitly, because `.npmrc`'s `ignore-scripts=true` (CWE-1357
|
|
16
|
+
* defence, which stays) suppresses every `pre*` hook. The runner
|
|
17
|
+
* invokes it now, so it executes however the coverage run is started.
|
|
13
18
|
* 1. Run the test suite under `NODE_V8_COVERAGE` so each worker writes
|
|
14
|
-
* raw V8 dumps under `coverage/tmp/`.
|
|
19
|
+
* raw V8 dumps under `coverage/tmp/`. The suite targets are the
|
|
20
|
+
* shared `FULL_TIER_GLOBS` from `lib/test-tiers.js` — the same set
|
|
21
|
+
* `run-tests.js` walks — so the measured surface and `npm test`'s
|
|
22
|
+
* surface are the same set by construction.
|
|
15
23
|
* 2. `c8 report` post-processes the dumps into `coverage/coverage-final.json`
|
|
16
24
|
* plus the printed text table. Include/exclude scope from `.c8rc.cjs`
|
|
17
25
|
* is passed explicitly because `c8 report` does not auto-load the
|
|
@@ -28,13 +36,15 @@
|
|
|
28
36
|
* while producing an identical `coverage-final.json` artifact for the
|
|
29
37
|
* CRAP gate.
|
|
30
38
|
*
|
|
31
|
-
* Test-runner
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* TEST_CONCURRENCY_MAX]`.
|
|
36
|
-
*
|
|
37
|
-
*
|
|
39
|
+
* Test-runner flags: the suite spawn reuses `TEST_RUNNER_FLAGS` from
|
|
40
|
+
* `lib/test-runner-contract.js` — the single source of truth for the
|
|
41
|
+
* `node --test` flag set, including the host-derived
|
|
42
|
+
* `--test-concurrency` value clamped to `[TEST_CONCURRENCY_MIN,
|
|
43
|
+
* TEST_CONCURRENCY_MAX]`. Story #4936: sharing the whole flag set (not
|
|
44
|
+
* just the concurrency number) is what stops this runner and
|
|
45
|
+
* `run-tests.js` disagreeing about whether a test can execute at all —
|
|
46
|
+
* `--experimental-test-module-mocks` decides whether `t.mock.module`
|
|
47
|
+
* works, and this is the *required* CI job.
|
|
38
48
|
*/
|
|
39
49
|
|
|
40
50
|
import { spawnSync } from 'node:child_process';
|
|
@@ -46,56 +56,90 @@ import { fileURLToPath } from 'node:url';
|
|
|
46
56
|
import { cleanupRepoTestTempArtifacts } from './cleanup-repo-test-temp.js';
|
|
47
57
|
import { C8_CLI } from './lib/c8-cli-path.js';
|
|
48
58
|
import { buildWebhookSafeTestEnv } from './lib/test-env.js';
|
|
49
|
-
import {
|
|
59
|
+
import {
|
|
60
|
+
runTierPreflight,
|
|
61
|
+
TEST_RUNNER_FLAGS,
|
|
62
|
+
} from './lib/test-runner-contract.js';
|
|
63
|
+
import { FULL_TIER_GLOBS } from './lib/test-tiers.js';
|
|
50
64
|
|
|
51
65
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
52
66
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
53
|
-
const COVERAGE_DIR = path.join(ROOT, 'coverage');
|
|
54
67
|
const require = createRequire(import.meta.url);
|
|
55
68
|
const C8_CONFIG = require('../../.c8rc.cjs');
|
|
56
|
-
const V8_TMP = path.join(COVERAGE_DIR, 'tmp');
|
|
57
69
|
|
|
58
70
|
/**
|
|
59
71
|
* Build the `node --test` argv for the coverage suite spawn.
|
|
60
72
|
*
|
|
61
|
-
* Reuses
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
73
|
+
* Reuses two shared constants so the coverage path cannot drift from
|
|
74
|
+
* `run-tests.js`:
|
|
75
|
+
*
|
|
76
|
+
* - `TEST_RUNNER_FLAGS` — the single source of truth for the host-aware,
|
|
77
|
+
* clamped `--test-concurrency` value.
|
|
78
|
+
* - `FULL_TIER_GLOBS` — the single source of truth for *which files the
|
|
79
|
+
* full tier runs*. Story #4922: this script used to restate
|
|
80
|
+
* `tests/**\/*.test.js` on its own, so the 47 colocated `__tests__`
|
|
81
|
+
* suites under `lib/` and `.agents/scripts/` executed under `npm test`
|
|
82
|
+
* but never under the measuring run — the coverage and CRAP numbers
|
|
83
|
+
* were computed over code the coverage run had not executed.
|
|
84
|
+
*
|
|
85
|
+
* Both parameters are injected in tests so the argv can be asserted without
|
|
86
|
+
* touching the OS.
|
|
65
87
|
*
|
|
66
88
|
* @param {object} [opts]
|
|
67
89
|
* @param {readonly string[]} [opts.runnerFlags]
|
|
68
|
-
* @param {string} [opts.
|
|
90
|
+
* @param {readonly string[]} [opts.testGlobs]
|
|
69
91
|
* @returns {string[]}
|
|
70
92
|
*/
|
|
71
93
|
export function buildCoverageTestArgs({
|
|
72
94
|
runnerFlags = TEST_RUNNER_FLAGS,
|
|
73
|
-
|
|
95
|
+
testGlobs = FULL_TIER_GLOBS,
|
|
74
96
|
} = {}) {
|
|
75
|
-
return [...runnerFlags,
|
|
97
|
+
return [...runnerFlags, ...testGlobs];
|
|
76
98
|
}
|
|
77
99
|
|
|
78
100
|
/**
|
|
79
|
-
* Execute the coverage pipeline: run the
|
|
80
|
-
* post-process the dumps with `c8 report`,
|
|
81
|
-
* baseline. Returns the first non-zero exit code
|
|
82
|
-
* (or the baseline check's status when
|
|
101
|
+
* Execute the coverage pipeline: run the full tier's preflight, run the
|
|
102
|
+
* suite under `NODE_V8_COVERAGE`, post-process the dumps with `c8 report`,
|
|
103
|
+
* then gate on the coverage baseline. Returns the first non-zero exit code
|
|
104
|
+
* across the stages (or the baseline check's status when the prior stages
|
|
105
|
+
* pass); a refused preflight short-circuits before anything is spawned or
|
|
106
|
+
* removed.
|
|
83
107
|
*
|
|
108
|
+
* Every collaborator is injected so the pipeline's wiring — above all
|
|
109
|
+
* *that the preflight actually runs, and runs first* — is assertable
|
|
110
|
+
* without spawning the real suite.
|
|
111
|
+
*
|
|
112
|
+
* @param {object} [opts]
|
|
113
|
+
* @param {typeof spawnSync} [opts.spawn]
|
|
114
|
+
* @param {(opts: { tier: string, repoRoot: string }) => number} [opts.preflight]
|
|
115
|
+
* @param {(opts: { repoRoot: string }) => unknown} [opts.cleanup]
|
|
116
|
+
* @param {string} [opts.repoRoot]
|
|
84
117
|
* @returns {number}
|
|
85
118
|
*/
|
|
86
|
-
function runCoveragePipeline(
|
|
87
|
-
|
|
88
|
-
|
|
119
|
+
export function runCoveragePipeline({
|
|
120
|
+
spawn = spawnSync,
|
|
121
|
+
preflight = runTierPreflight,
|
|
122
|
+
cleanup = cleanupRepoTestTempArtifacts,
|
|
123
|
+
repoRoot = ROOT,
|
|
124
|
+
} = {}) {
|
|
125
|
+
const coverageDir = path.join(repoRoot, 'coverage');
|
|
126
|
+
const v8Tmp = path.join(coverageDir, 'tmp');
|
|
127
|
+
|
|
128
|
+
const preflightStatus = preflight({ tier: 'full', repoRoot });
|
|
129
|
+
if (preflightStatus !== 0) return preflightStatus;
|
|
89
130
|
|
|
90
|
-
|
|
91
|
-
|
|
131
|
+
rmSync(coverageDir, { recursive: true, force: true });
|
|
132
|
+
mkdirSync(v8Tmp, { recursive: true });
|
|
133
|
+
|
|
134
|
+
const testRun = spawn(process.execPath, buildCoverageTestArgs(), {
|
|
135
|
+
cwd: repoRoot,
|
|
92
136
|
stdio: 'inherit',
|
|
93
137
|
// GIT_*-scrubbed: under a husky pre-push from a linked worktree the
|
|
94
138
|
// inherited GIT_DIR poisons fixture `git init` runs (#4580).
|
|
95
|
-
env: { ...buildWebhookSafeTestEnv(process.env), NODE_V8_COVERAGE:
|
|
139
|
+
env: { ...buildWebhookSafeTestEnv(process.env), NODE_V8_COVERAGE: v8Tmp },
|
|
96
140
|
});
|
|
97
141
|
|
|
98
|
-
|
|
142
|
+
cleanup({ repoRoot });
|
|
99
143
|
|
|
100
144
|
const includeArgs = (C8_CONFIG.include ?? []).flatMap((p) => [
|
|
101
145
|
'--include',
|
|
@@ -106,29 +150,36 @@ function runCoveragePipeline() {
|
|
|
106
150
|
p,
|
|
107
151
|
]);
|
|
108
152
|
|
|
109
|
-
|
|
153
|
+
// `c8 report` does not auto-load `.c8rc.cjs`, so every scope knob it needs
|
|
154
|
+
// is forwarded explicitly from the config object — including `all`, which
|
|
155
|
+
// is what puts a 0 % row on a source file no test ever loaded (Story
|
|
156
|
+
// #4922). Reading it off the config keeps `.c8rc.cjs` the one declaration.
|
|
157
|
+
const allArgs = C8_CONFIG.all ? ['--all'] : [];
|
|
158
|
+
|
|
159
|
+
const reportRun = spawn(
|
|
110
160
|
process.execPath,
|
|
111
161
|
[
|
|
112
162
|
C8_CLI,
|
|
113
163
|
'report',
|
|
114
164
|
'--reporter=json',
|
|
115
165
|
'--reporter=text',
|
|
166
|
+
...allArgs,
|
|
116
167
|
'--temp-directory',
|
|
117
|
-
|
|
168
|
+
v8Tmp,
|
|
118
169
|
...includeArgs,
|
|
119
170
|
...excludeArgs,
|
|
120
171
|
],
|
|
121
|
-
{ cwd:
|
|
172
|
+
{ cwd: repoRoot, stdio: 'inherit', shell: false },
|
|
122
173
|
);
|
|
123
174
|
|
|
124
|
-
const checkRun =
|
|
175
|
+
const checkRun = spawn(
|
|
125
176
|
process.execPath,
|
|
126
177
|
[
|
|
127
|
-
path.join(
|
|
178
|
+
path.join(repoRoot, '.agents', 'scripts', 'check-baselines.js'),
|
|
128
179
|
'--gate',
|
|
129
180
|
'coverage',
|
|
130
181
|
],
|
|
131
|
-
{ cwd:
|
|
182
|
+
{ cwd: repoRoot, stdio: 'inherit' },
|
|
132
183
|
);
|
|
133
184
|
|
|
134
185
|
return testRun.status !== 0
|
|
@@ -28,11 +28,20 @@ const tasks = [
|
|
|
28
28
|
args: ['biome', 'ci', '.'],
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
|
+
// `docs/**/*.md` sat outside these globs until PR #4970's follow-up,
|
|
32
|
+
// so `npm run lint` reported "0 error(s)" while the close-time
|
|
33
|
+
// code-review lens — which lints the whole changed surface, not just
|
|
34
|
+
// what this driver globs — raised pre-existing `docs/` violations
|
|
35
|
+
// against whichever Story happened to touch the file. Keep `docs/`
|
|
36
|
+
// here so the two surfaces agree. `docs/CHANGELOG.md` is linted too;
|
|
37
|
+
// the generator-owned rules it can never satisfy are exempted by a
|
|
38
|
+
// `markdownlint-disable-file` directive in its own header.
|
|
31
39
|
name: 'markdownlint',
|
|
32
40
|
cmd: 'npx',
|
|
33
41
|
args: [
|
|
34
42
|
'markdownlint-cli2',
|
|
35
43
|
'.agents/**/*.md',
|
|
44
|
+
'docs/**/*.md',
|
|
36
45
|
'*.md',
|
|
37
46
|
'!node_modules/**',
|
|
38
47
|
'!.worktrees/**',
|
|
@@ -79,6 +88,17 @@ const tasks = [
|
|
|
79
88
|
cmd: 'node',
|
|
80
89
|
args: ['.agents/scripts/lint-label-vocabulary.js'],
|
|
81
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
|
+
},
|
|
82
102
|
{
|
|
83
103
|
// Architecture cycle ratchet (Story #3991). Detects directed import
|
|
84
104
|
// cycles under `.agents/scripts/` and fails on any cycle not in the
|