mandrel 2.35.0 → 2.37.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/docs/agentrc-reference.json +3 -1
- package/.agents/docs/configuration.md +27 -0
- package/.agents/rules/testing-standards.md +56 -0
- package/.agents/schemas/agentrc.schema.json +11 -0
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- 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 +23 -2
- 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/drain-pending-cleanup.js +18 -1
- package/.agents/scripts/git-cleanup.js +2 -0
- package/.agents/scripts/lib/baselines/components.js +32 -2
- package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
- package/.agents/scripts/lib/bootstrap/install-ledger.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/loop-health.js +1 -1
- package/.agents/scripts/lib/cli-args.js +0 -2
- package/.agents/scripts/lib/close-validation/gates.js +1 -1
- package/.agents/scripts/lib/close-validation/process.js +1 -1
- package/.agents/scripts/lib/config/ci.js +18 -0
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config-settings-schema-delivery.js +13 -0
- package/.agents/scripts/lib/config-settings-schema.js +46 -6
- 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 +1 -1
- 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 +1 -1
- package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +1 -1
- package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
- package/.agents/scripts/lib/install-cmd-parser.js +1 -1
- package/.agents/scripts/lib/knip-entry-sync.js +1 -57
- package/.agents/scripts/lib/maintainability-utils.js +108 -10
- package/.agents/scripts/lib/observability/source-classifier.js +1 -1
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +22 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -14
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/merged-tip.js +132 -0
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +56 -11
- package/.agents/scripts/lib/orchestration/merge-block-class.js +10 -1
- package/.agents/scripts/lib/orchestration/merge-poll.js +164 -0
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +70 -9
- 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/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/single-story-close/phases/auto-merge.js +145 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +96 -5
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +9 -1
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
- 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/ticketing/reads.js +3 -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/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/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/transpile.js +16 -3
- package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +169 -31
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
- package/.agents/scripts/notify.js +4 -10
- package/.agents/scripts/pr-watch-with-update.js +305 -137
- package/.agents/scripts/providers/github/tickets.js +109 -16
- package/.agents/scripts/quality-preview.js +162 -70
- package/.agents/scripts/run-test-profile.js +8 -5
- package/.agents/scripts/run-tests.js +79 -14
- package/.agents/scripts/single-story-close.js +0 -2
- package/.agents/scripts/single-story-init.js +5 -1
- package/.agents/workflows/audit-documentation.md +5 -6
- package/docs/CHANGELOG.md +50 -0
- 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.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 +9 -5
- package/.agents/scripts/generate-lifecycle-docs.js +0 -237
- 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
|
@@ -90,6 +90,7 @@
|
|
|
90
90
|
* confirm.
|
|
91
91
|
*/
|
|
92
92
|
|
|
93
|
+
import { getCiDelivery } from '../../../config/ci.js';
|
|
93
94
|
import { createGh } from '../../../gh-exec.js';
|
|
94
95
|
import {
|
|
95
96
|
confirmStoryMerged as defaultConfirmStoryMerged,
|
|
@@ -106,8 +107,10 @@ import { classifyMergeBlock as defaultClassifyMergeBlock } from '../../merge-blo
|
|
|
106
107
|
import {
|
|
107
108
|
DEFAULT_INTERVAL_SECONDS,
|
|
108
109
|
DEFAULT_MAX_BUDGET_SECONDS,
|
|
110
|
+
decideAdvisoryGateBlock,
|
|
109
111
|
decideMergeWaitFailFast,
|
|
110
112
|
deriveChecksStatus,
|
|
113
|
+
deriveRedHeadRuns,
|
|
111
114
|
deriveRequiredRunEvidence,
|
|
112
115
|
MERGE_WAIT_GH_TIMEOUT_MS,
|
|
113
116
|
} from '../../merge-poll.js';
|
|
@@ -117,6 +120,7 @@ import {
|
|
|
117
120
|
STATE_LABELS,
|
|
118
121
|
transitionTicketState,
|
|
119
122
|
} from '../../ticketing.js';
|
|
123
|
+
import { disarmAutoMerge } from './auto-merge.js';
|
|
120
124
|
import { runPostLandTail as defaultRunPostLandTail } from './post-land.js';
|
|
121
125
|
|
|
122
126
|
/**
|
|
@@ -256,6 +260,10 @@ export async function readPrWaitProbe({
|
|
|
256
260
|
// the aggregate `checksStatus` folds together. `null` when the rollup is
|
|
257
261
|
// absent/empty — the loop's consecutive-probe fallback owns that path.
|
|
258
262
|
requiredRunEvidence: deriveRequiredRunEvidence(view?.statusCheckRollup),
|
|
263
|
+
// The named red head runs (Story #5096), so an `advisory-gate-red`
|
|
264
|
+
// verdict can name the offending job and match the allowlist. Same
|
|
265
|
+
// red-ness test as `requiredRunEvidence`, so the two cannot disagree.
|
|
266
|
+
redHeadRuns: deriveRedHeadRuns(view?.statusCheckRollup),
|
|
259
267
|
};
|
|
260
268
|
} catch (err) {
|
|
261
269
|
return {
|
|
@@ -263,6 +271,7 @@ export async function readPrWaitProbe({
|
|
|
263
271
|
mergedAt: null,
|
|
264
272
|
createdAt: null,
|
|
265
273
|
checksStatus: 'pending',
|
|
274
|
+
redHeadRuns: [],
|
|
266
275
|
error: `PR probe failed: ${err?.message ?? err}`,
|
|
267
276
|
};
|
|
268
277
|
}
|
|
@@ -541,6 +550,46 @@ async function blockOnFlipFailed({
|
|
|
541
550
|
* is best-effort logged rather than thrown — the caller owns surfacing the
|
|
542
551
|
* non-zero exit once this returns.
|
|
543
552
|
*/
|
|
553
|
+
/**
|
|
554
|
+
* Story #5096 — resolve the advisory-gate terminal for one poll.
|
|
555
|
+
*
|
|
556
|
+
* Takes the poll's current `unlanded` and returns it unchanged when a terminal
|
|
557
|
+
* is already decided, so the caller is a single assignment with NO added
|
|
558
|
+
* branch. `runMergePoll` is already above `check-cyclomatic`'s ceiling; the
|
|
559
|
+
* three decision points this would otherwise cost inline are a real gate
|
|
560
|
+
* regression, and they belong with the policy either way.
|
|
561
|
+
*
|
|
562
|
+
* Disarms BEFORE returning the terminal: an armed PR can merge out from under
|
|
563
|
+
* the block the caller is about to record.
|
|
564
|
+
*/
|
|
565
|
+
async function resolveAdvisoryUnlanded({
|
|
566
|
+
unlanded,
|
|
567
|
+
probe,
|
|
568
|
+
blockOnAdvisoryFailure,
|
|
569
|
+
advisoryAllowlist,
|
|
570
|
+
prNumber,
|
|
571
|
+
gh,
|
|
572
|
+
progress,
|
|
573
|
+
disarmAutoMergeFn,
|
|
574
|
+
elapsedSeconds,
|
|
575
|
+
}) {
|
|
576
|
+
if (unlanded) return unlanded;
|
|
577
|
+
const advisory = decideAdvisoryGateBlock({
|
|
578
|
+
probe,
|
|
579
|
+
blockOnAdvisoryFailure,
|
|
580
|
+
advisoryAllowlist,
|
|
581
|
+
});
|
|
582
|
+
if (!advisory) return null;
|
|
583
|
+
progress?.('CONFIRM', `🛑 PR #${prNumber}: ${advisory.reason}`);
|
|
584
|
+
await disarmAutoMergeFn({ prNumber, gh, progress });
|
|
585
|
+
return {
|
|
586
|
+
prProbe: probe,
|
|
587
|
+
budget: { exhausted: false, elapsedSeconds },
|
|
588
|
+
blockClassOverride: 'advisory-gate-red',
|
|
589
|
+
reasonOverride: advisory.reason,
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
544
593
|
async function blockOnUnlanded({
|
|
545
594
|
storyId,
|
|
546
595
|
prNumber,
|
|
@@ -552,12 +601,24 @@ async function blockOnUnlanded({
|
|
|
552
601
|
progress,
|
|
553
602
|
classifyMergeBlockFn,
|
|
554
603
|
emitMergeUnlandedFn,
|
|
604
|
+
blockClassOverride,
|
|
605
|
+
reasonOverride,
|
|
555
606
|
}) {
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
607
|
+
// Story #5096 — `advisory-gate-red` is emitted DIRECTLY, never derived.
|
|
608
|
+
// `classifyMergeBlock` cannot produce it: by construction GitHub is NOT
|
|
609
|
+
// gating this merge (`mergeStateStatus: UNSTABLE`), which is the entire
|
|
610
|
+
// condition the class names, so every classifier heuristic reads the PR as
|
|
611
|
+
// healthy.
|
|
612
|
+
const { blockClass, reason } = blockClassOverride
|
|
613
|
+
? {
|
|
614
|
+
blockClass: blockClassOverride,
|
|
615
|
+
reason: reasonOverride ?? blockClassOverride,
|
|
616
|
+
}
|
|
617
|
+
: classifyMergeBlockFn({
|
|
618
|
+
armResult,
|
|
619
|
+
prProbe,
|
|
620
|
+
budget,
|
|
621
|
+
});
|
|
561
622
|
const elapsedSeconds = budget?.elapsedSeconds ?? 0;
|
|
562
623
|
// Which evidence path produced a `checks-failed` verdict (Story #4695):
|
|
563
624
|
// `per-run` (head-anchored required-run evidence) or `consecutive-probe`
|
|
@@ -820,6 +881,7 @@ export async function runConfirmMergePhase({
|
|
|
820
881
|
prUrl,
|
|
821
882
|
autoMergeEnabled,
|
|
822
883
|
autoMergeReason,
|
|
884
|
+
advisoryGate,
|
|
823
885
|
provider,
|
|
824
886
|
config,
|
|
825
887
|
maxWaitSeconds: maxWaitSecondsOverride,
|
|
@@ -834,6 +896,7 @@ export async function runConfirmMergePhase({
|
|
|
834
896
|
emitMergeUnlandedFn = defaultEmitMergeUnlanded,
|
|
835
897
|
emitMergeFlipFailedFn = defaultEmitMergeFlipFailed,
|
|
836
898
|
runPostLandTailFn = defaultRunPostLandTail,
|
|
899
|
+
disarmAutoMergeFn = disarmAutoMerge,
|
|
837
900
|
sleepFn = defaultSleep,
|
|
838
901
|
nowMsFn = Date.now,
|
|
839
902
|
ghTimeoutMs = MERGE_WAIT_GH_TIMEOUT_MS,
|
|
@@ -857,6 +920,15 @@ export async function runConfirmMergePhase({
|
|
|
857
920
|
progress,
|
|
858
921
|
classifyMergeBlockFn,
|
|
859
922
|
emitMergeUnlandedFn,
|
|
923
|
+
// Story #5096 — the arm phase already refused over a red advisory gate;
|
|
924
|
+
// carry its verdict through instead of letting the classifier read this
|
|
925
|
+
// as a generic `arm-failure`.
|
|
926
|
+
...(autoMergeReason === 'advisory-gate-red'
|
|
927
|
+
? {
|
|
928
|
+
blockClassOverride: 'advisory-gate-red',
|
|
929
|
+
reasonOverride: advisoryGate?.reason,
|
|
930
|
+
}
|
|
931
|
+
: {}),
|
|
860
932
|
});
|
|
861
933
|
}
|
|
862
934
|
|
|
@@ -871,6 +943,8 @@ export async function runConfirmMergePhase({
|
|
|
871
943
|
maxWaitSecondsOverride,
|
|
872
944
|
mergeWatchModeOverride,
|
|
873
945
|
);
|
|
946
|
+
// Story #5096 — the advisory-gate knobs, read once for the whole wait.
|
|
947
|
+
const { blockOnAdvisoryFailure, advisoryAllowlist } = getCiDelivery(config);
|
|
874
948
|
const intervalMs = intervalSeconds * 1000;
|
|
875
949
|
const startedAtMs = nowMsFn();
|
|
876
950
|
let anchorMs = startedAtMs;
|
|
@@ -1020,6 +1094,23 @@ export async function runConfirmMergePhase({
|
|
|
1020
1094
|
},
|
|
1021
1095
|
};
|
|
1022
1096
|
}
|
|
1097
|
+
|
|
1098
|
+
// Story #5096 — the ADVISORY counterpart, and the half that catches the
|
|
1099
|
+
// common shape. Close arms immediately after opening the PR, while the
|
|
1100
|
+
// gate is still QUEUED, so the pre-arm refusal in `auto-merge.js` sees
|
|
1101
|
+
// nothing; the gate reddens here, mid-wait, and native auto-merge would
|
|
1102
|
+
// land the PR the moment the REQUIRED contexts go green.
|
|
1103
|
+
unlanded = await resolveAdvisoryUnlanded({
|
|
1104
|
+
unlanded,
|
|
1105
|
+
probe,
|
|
1106
|
+
blockOnAdvisoryFailure,
|
|
1107
|
+
advisoryAllowlist,
|
|
1108
|
+
prNumber,
|
|
1109
|
+
gh: injectedGh,
|
|
1110
|
+
progress,
|
|
1111
|
+
disarmAutoMergeFn,
|
|
1112
|
+
elapsedSeconds: Math.round(waitedMs / 1000),
|
|
1113
|
+
});
|
|
1023
1114
|
}
|
|
1024
1115
|
|
|
1025
1116
|
if (!unlanded) {
|
|
@@ -87,6 +87,50 @@ export function resolveWaitForMerge({
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Flags this CLI advertised in its `--help` descriptor and never implemented
|
|
92
|
+
* (Story #5100). `--dry-run` was transcribed into the usage block by the
|
|
93
|
+
* #4750 self-describing sweep and read by nothing in this pipeline —
|
|
94
|
+
* `git log -S dryRun` over it is empty in every revision — so an operator who
|
|
95
|
+
* trusted it got a real base-sync merge, an `agent::blocked` Story, and a
|
|
96
|
+
* terminal envelope that then captured `deliver-recover`'s routing.
|
|
97
|
+
* `--no-evidence` names a working flag on the gate wrappers
|
|
98
|
+
* (`evidence-gate.js`), but the slot this CLI parsed had no reader at all.
|
|
99
|
+
*
|
|
100
|
+
* Deleting them from the descriptor is NOT enough. `parseSprintArgs` runs
|
|
101
|
+
* `parseArgs` with `strict: false`, so an unknown flag is silently ignored and
|
|
102
|
+
* the close proceeds for real — the identical injury, now with no help text to
|
|
103
|
+
* explain it. Fail closed instead, exactly as `parseMergeWatchMode` does for an
|
|
104
|
+
* unrecognized mode: throw during option parsing, before the first phase, so
|
|
105
|
+
* the CLI boundary reports `failed` at phase `init` and nothing is mutated.
|
|
106
|
+
*/
|
|
107
|
+
const RETIRED_FLAGS = Object.freeze({
|
|
108
|
+
'--dry-run':
|
|
109
|
+
'this pipeline has never had a dry-run mode; it was advertised in error.',
|
|
110
|
+
'--no-evidence':
|
|
111
|
+
'per-close evidence control was never wired here; the working flag of that name belongs to the gate wrappers.',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Reject a retired flag before any phase runs.
|
|
116
|
+
*
|
|
117
|
+
* Matches `--flag` and `--flag=value`; a bare `--` terminator or a positional
|
|
118
|
+
* that merely contains the text is not a flag and does not trip it.
|
|
119
|
+
*
|
|
120
|
+
* @param {string[]} argv argv tail (`process.argv.slice(2)`)
|
|
121
|
+
* @throws {Error} naming the flag, why it is gone, and that nothing was mutated
|
|
122
|
+
*/
|
|
123
|
+
function assertNoRetiredFlags(argv) {
|
|
124
|
+
for (const [flag, why] of Object.entries(RETIRED_FLAGS)) {
|
|
125
|
+
const present = argv.some((a) => a === flag || a.startsWith(`${flag}=`));
|
|
126
|
+
if (!present) continue;
|
|
127
|
+
throw new Error(
|
|
128
|
+
`${flag} was retired: ${why} Nothing was mutated — no branch, label, ` +
|
|
129
|
+
`comment, or PR was touched. Re-run without it to close for real.`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
90
134
|
/**
|
|
91
135
|
* Parse and resolve all CLI / injection options for `runSingleStoryClose`.
|
|
92
136
|
*
|
|
@@ -116,7 +160,14 @@ export function parseCloseOptions({
|
|
|
116
160
|
// which is precisely what `resolveFlag` already does below, preferring the
|
|
117
161
|
// param over the parsed slot. One expression per flag now serves both
|
|
118
162
|
// callers, so a new flag is added in one place instead of two that can drift.
|
|
119
|
-
|
|
163
|
+
// The guard runs on the argv door only: an injecting caller supplies
|
|
164
|
+
// `storyIdParam` and never reads argv, so the host process's flags (a test
|
|
165
|
+
// runner's, say) are none of its business.
|
|
166
|
+
let parsed = {};
|
|
167
|
+
if (storyIdParam === undefined) {
|
|
168
|
+
assertNoRetiredFlags(process.argv.slice(2));
|
|
169
|
+
parsed = parseSprintArgs();
|
|
170
|
+
}
|
|
120
171
|
// Preserve undefined so resolveWaitForMerge can apply the closeAndLand
|
|
121
172
|
// config default when neither flag was supplied.
|
|
122
173
|
const waitForMergeExplicit = resolveFlag(
|
|
@@ -485,6 +485,7 @@ async function resolveAutoMergeOutcome({ alreadyMerged, ...phaseArgs }) {
|
|
|
485
485
|
autoMergeReason: null,
|
|
486
486
|
localCleanupDeferred: false,
|
|
487
487
|
directMerged: false,
|
|
488
|
+
advisoryGate: null,
|
|
488
489
|
};
|
|
489
490
|
}
|
|
490
491
|
return await runAutoMergePhase(phaseArgs);
|
|
@@ -541,6 +542,7 @@ async function finishWithMergeWait(prCtx, deps) {
|
|
|
541
542
|
prUrl: prCtx.prUrl,
|
|
542
543
|
autoMergeEnabled: prCtx.autoMergeEnabled,
|
|
543
544
|
autoMergeReason: prCtx.autoMergeReason,
|
|
545
|
+
advisoryGate: prCtx.advisoryGate,
|
|
544
546
|
provider: deps.provider,
|
|
545
547
|
config: prCtx.config,
|
|
546
548
|
maxWaitSeconds: deps.maxWaitSeconds,
|
|
@@ -796,18 +798,23 @@ async function runClosePipeline({
|
|
|
796
798
|
WorktreeManager,
|
|
797
799
|
});
|
|
798
800
|
setPhase('auto-merge');
|
|
801
|
+
const ciDelivery = getCiDelivery(config);
|
|
799
802
|
const {
|
|
800
803
|
autoMergeEnabled,
|
|
801
804
|
autoMergeReason,
|
|
802
805
|
localCleanupDeferred,
|
|
803
806
|
directMerged,
|
|
807
|
+
advisoryGate,
|
|
804
808
|
} = await resolveAutoMergeOutcome({
|
|
805
809
|
alreadyMerged,
|
|
806
810
|
cwd: options.cwd,
|
|
807
811
|
prNumber,
|
|
808
812
|
prUrl,
|
|
809
813
|
noAutoMerge: options.noAutoMerge,
|
|
810
|
-
autoMergePolicy:
|
|
814
|
+
autoMergePolicy: ciDelivery.autoMerge,
|
|
815
|
+
// Story #5096 — the pre-arm advisory-gate refusal.
|
|
816
|
+
blockOnAdvisoryFailure: ciDelivery.blockOnAdvisoryFailure,
|
|
817
|
+
advisoryAllowlist: ciDelivery.advisoryAllowlist,
|
|
811
818
|
gh: injectedGh,
|
|
812
819
|
progress,
|
|
813
820
|
});
|
|
@@ -866,6 +873,7 @@ async function runClosePipeline({
|
|
|
866
873
|
prUrl,
|
|
867
874
|
autoMergeEnabled,
|
|
868
875
|
autoMergeReason,
|
|
876
|
+
advisoryGate,
|
|
869
877
|
worktreeReaped,
|
|
870
878
|
localCleanupDeferred,
|
|
871
879
|
directMerged,
|
|
@@ -63,7 +63,7 @@ export const FORMAT_AUTOFIX_TIMEOUT_EXIT_CODE = 124;
|
|
|
63
63
|
* @param {(args: string[], opts: object) => string} git
|
|
64
64
|
* @returns {string[]}
|
|
65
65
|
*/
|
|
66
|
-
|
|
66
|
+
function listDirtyPaths(cwd, git) {
|
|
67
67
|
const out = git(['status', '--porcelain'], {
|
|
68
68
|
cwd,
|
|
69
69
|
encoding: 'utf8',
|
|
@@ -91,10 +91,7 @@ export function listDirtyPaths(cwd, git) {
|
|
|
91
91
|
* }} [opts]
|
|
92
92
|
* @returns {{ writeCmdString: string, writeCmd: string, writeArgs: string[] }}
|
|
93
93
|
*/
|
|
94
|
-
|
|
95
|
-
commands,
|
|
96
|
-
dropTrailingDot = false,
|
|
97
|
-
} = {}) {
|
|
94
|
+
function resolveFormatterCmd({ commands, dropTrailingDot = false } = {}) {
|
|
98
95
|
// `resolveFormatWriteCommand` reads `config.project.commands`; wrap the
|
|
99
96
|
// caller-supplied `commands` map into that canonical shape.
|
|
100
97
|
const writeCmdString = resolveFormatWriteCommand({ project: { commands } });
|
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
} from './retro-proposals.js';
|
|
25
25
|
import { upsertStructuredComment } from './ticketing.js';
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
const FOLLOW_UPS_COMMENT_TYPE = 'follow-ups';
|
|
28
28
|
|
|
29
29
|
/** Milliseconds in one day — the unit `frictionWindowDays` is expressed in. */
|
|
30
30
|
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
@@ -29,8 +29,9 @@
|
|
|
29
29
|
* a claim back from whoever took over).
|
|
30
30
|
*
|
|
31
31
|
* Provider contract (a subset of `ITicketingProvider`):
|
|
32
|
-
* - `getTicket(id)`
|
|
33
|
-
* - `updateTicket(id, { assignees })`
|
|
32
|
+
* - `getTicket(id)` → `{ assignees: string[], ... }`
|
|
33
|
+
* - `updateTicket(id, { assignees })` replaces the assignee list.
|
|
34
|
+
* - `updateTicket(id, { addAssignees })` appends to it (Story #5112).
|
|
34
35
|
*/
|
|
35
36
|
|
|
36
37
|
/**
|
|
@@ -128,11 +129,13 @@ function normaliseOpts(op, opts) {
|
|
|
128
129
|
* operator out, `acquired: false`,
|
|
129
130
|
* `owner: <foreign>`, `reason: 'lost-race'`.
|
|
130
131
|
*
|
|
131
|
-
* Every claiming write is verified: GitHub's assignee
|
|
132
|
+
* Every claiming write is verified: GitHub's assignee write is not a
|
|
132
133
|
* compare-and-set, so two runs that both read the ticket unassigned will both
|
|
133
134
|
* write themselves. {@link claimAndVerify} re-reads after the write and refuses
|
|
134
135
|
* (fail-closed) when a foreign login is present, so the loser of a simultaneous
|
|
135
|
-
* claim never proceeds as though it holds the lease.
|
|
136
|
+
* claim never proceeds as though it holds the lease. Story #5112 made the
|
|
137
|
+
* first claim **additive** so that verify can actually see the collision —
|
|
138
|
+
* see {@link claimAndVerify}.
|
|
136
139
|
*
|
|
137
140
|
* @param {object} opts
|
|
138
141
|
* @param {object} opts.provider Ticketing provider.
|
|
@@ -199,7 +202,7 @@ export async function acquireLease(opts) {
|
|
|
199
202
|
*
|
|
200
203
|
* The assignee write is not atomic — GitHub offers no compare-and-set on the
|
|
201
204
|
* assignees surface — so two runs that both observed the ticket unassigned (or
|
|
202
|
-
* a stale foreign claim) will both
|
|
205
|
+
* a stale foreign claim) will both write themselves in. Without a check the
|
|
203
206
|
* loser of that race returns `acquired: true` and marches into the worktree
|
|
204
207
|
* the winner is already building. The verify closes that window: it re-reads
|
|
205
208
|
* with `fresh: true` (bypassing any provider cache so it sees the other run's
|
|
@@ -208,9 +211,16 @@ export async function acquireLease(opts) {
|
|
|
208
211
|
* returns `acquired: false` / `reason: 'lost-race'` so the fail-closed caller
|
|
209
212
|
* refuses. A clean read (assignees exactly `[operator]`) confirms the claim.
|
|
210
213
|
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
+
* **The write must be additive for the verify to work (Story #5112).** With
|
|
215
|
+
* the replacing PATCH this used unconditionally, a simultaneous claim
|
|
216
|
+
* *evicted* the other operator rather than joining it, so the co-assignment
|
|
217
|
+
* the `lost-race` branch keys on was a state the PATCH could never produce:
|
|
218
|
+
* each run read a clean `[self]` on verify and both proceeded. Claiming an
|
|
219
|
+
* unowned ticket therefore goes through the additive assignees endpoint
|
|
220
|
+
* (`addAssignees`), which makes the collision observable and lets exactly one
|
|
221
|
+
* claimer survive. The replacing form stays for the two cases that genuinely
|
|
222
|
+
* mean "replace": the `--steal` transfer of a foreign claim, and the loser's
|
|
223
|
+
* own back-out below.
|
|
214
224
|
*
|
|
215
225
|
* @param {object} args
|
|
216
226
|
* @param {object} args.provider Ticketing provider.
|
|
@@ -220,6 +230,21 @@ export async function acquireLease(opts) {
|
|
|
220
230
|
* @param {string} args.reason Success reason when the claim holds.
|
|
221
231
|
* @returns {Promise<{ acquired: boolean, owner: string, previousOwner: string|null, reason: string }>}
|
|
222
232
|
*/
|
|
233
|
+
/**
|
|
234
|
+
* The assignee mutation a claim writes. Additive when the ticket has no
|
|
235
|
+
* previous owner — that is what makes a simultaneous claim show up as a
|
|
236
|
+
* co-assignment {@link claimAndVerify} can detect. Replacing only for a
|
|
237
|
+
* steal, where evicting the previous owner *is* the intent.
|
|
238
|
+
*
|
|
239
|
+
* @param {string} operator
|
|
240
|
+
* @param {string|null} previousOwner
|
|
241
|
+
* @returns {{ addAssignees: string[] }|{ assignees: string[] }}
|
|
242
|
+
*/
|
|
243
|
+
function claimMutation(operator, previousOwner) {
|
|
244
|
+
if (previousOwner === null) return { addAssignees: [operator] };
|
|
245
|
+
return { assignees: [operator] };
|
|
246
|
+
}
|
|
247
|
+
|
|
223
248
|
async function claimAndVerify({
|
|
224
249
|
provider,
|
|
225
250
|
ticketId,
|
|
@@ -227,7 +252,7 @@ async function claimAndVerify({
|
|
|
227
252
|
previousOwner,
|
|
228
253
|
reason,
|
|
229
254
|
}) {
|
|
230
|
-
await provider.updateTicket(ticketId,
|
|
255
|
+
await provider.updateTicket(ticketId, claimMutation(operator, previousOwner));
|
|
231
256
|
|
|
232
257
|
const after = await provider.getTicket(ticketId, { fresh: true });
|
|
233
258
|
const assignees = Array.isArray(after?.assignees) ? after.assignees : [];
|
|
@@ -81,7 +81,9 @@ export const STRUCTURED_COMMENT_TYPES = Object.freeze([
|
|
|
81
81
|
'epic-run-state',
|
|
82
82
|
'epic-run-progress',
|
|
83
83
|
'epic-plan-state',
|
|
84
|
-
|
|
84
|
+
// `parked-follow-ons` retired in Story #5114 with the module that was its
|
|
85
|
+
// only writer. A kind the reader still recognises but nothing emits is the
|
|
86
|
+
// same dead wiring in a new place.
|
|
85
87
|
// Story #566 — per-phase wall-clock summary posted by single-story-close.js.
|
|
86
88
|
'phase-timings',
|
|
87
89
|
// Story #831 — story-init upserts a `story-init` comment that
|
|
@@ -124,7 +124,7 @@ function pick(logger, level) {
|
|
|
124
124
|
* @param {Array<object>} fixed
|
|
125
125
|
* @param {object} [logger]
|
|
126
126
|
*/
|
|
127
|
-
|
|
127
|
+
function logFixes(fixed, logger = DEFAULT_LOGGER) {
|
|
128
128
|
const info = pick(logger, 'info');
|
|
129
129
|
info(`[preflight] auto-fixed ${fixed.length} finding(s):`);
|
|
130
130
|
for (const f of fixed) {
|
|
@@ -141,7 +141,7 @@ export function logFixes(fixed, logger = DEFAULT_LOGGER) {
|
|
|
141
141
|
* @param {Array<object>} blockers
|
|
142
142
|
* @param {object} [logger]
|
|
143
143
|
*/
|
|
144
|
-
|
|
144
|
+
function logBlockers(scope, blockers, logger = DEFAULT_LOGGER) {
|
|
145
145
|
const error = pick(logger, 'error');
|
|
146
146
|
error(
|
|
147
147
|
`[preflight] ${scope}: ${blockers.length} blocker finding(s) — refusing to proceed.`,
|
|
@@ -180,7 +180,7 @@ export function logBlockers(scope, blockers, logger = DEFAULT_LOGGER) {
|
|
|
180
180
|
* @param {Array<object>} findings
|
|
181
181
|
* @param {object} [logger]
|
|
182
182
|
*/
|
|
183
|
-
|
|
183
|
+
function logNonBlockers(findings, logger = DEFAULT_LOGGER) {
|
|
184
184
|
const warn = pick(logger, 'warn');
|
|
185
185
|
for (const f of findings) {
|
|
186
186
|
warn(`[preflight] ${f.severity}: ${f.id} — ${f.summary ?? ''}`);
|
|
@@ -36,7 +36,7 @@ import path from 'node:path';
|
|
|
36
36
|
import { tempRootFrom } from '../config/temp-paths.js';
|
|
37
37
|
|
|
38
38
|
/** Directory segment (under `tempRoot`) that holds every QA session ledger. */
|
|
39
|
-
|
|
39
|
+
const QA_LEDGER_DIRNAME = 'qa';
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
42
|
* Triaged dispositions, mirrored from the `disposition` enum in
|
|
@@ -26,13 +26,7 @@ const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
|
26
26
|
* `.agents/scripts/lib/runtime-deps/`; the manifest sits at the `.agents/`
|
|
27
27
|
* root, three directories up.
|
|
28
28
|
*/
|
|
29
|
-
|
|
30
|
-
HERE,
|
|
31
|
-
'..',
|
|
32
|
-
'..',
|
|
33
|
-
'..',
|
|
34
|
-
'runtime-deps.json',
|
|
35
|
-
);
|
|
29
|
+
const MANIFEST_PATH = path.resolve(HERE, '..', '..', '..', 'runtime-deps.json');
|
|
36
30
|
|
|
37
31
|
/**
|
|
38
32
|
* @typedef {object} RuntimeDepsManifest
|
|
@@ -26,9 +26,10 @@
|
|
|
26
26
|
import fs from 'node:fs';
|
|
27
27
|
import { builtinModules } from 'node:module';
|
|
28
28
|
import path from 'node:path';
|
|
29
|
+
import { stripJsComments } from '../source-text/strip-js-comments.js';
|
|
29
30
|
|
|
30
31
|
/** Node builtins, with and without the `node:` prefix. */
|
|
31
|
-
|
|
32
|
+
const BUILTIN_MODULES = new Set([
|
|
32
33
|
...builtinModules,
|
|
33
34
|
...builtinModules.map((m) => `node:${m}`),
|
|
34
35
|
]);
|
|
@@ -71,62 +72,6 @@ const SIDE_EFFECT = /^\s*import\s*['"]([^'"]+)['"]/gm;
|
|
|
71
72
|
// `require(...)` and dynamic `import(...)` may appear mid-expression.
|
|
72
73
|
const CALL_FORM = /\b(?:require|import)\s*\(\s*['"]([^'"]+)['"]/g;
|
|
73
74
|
|
|
74
|
-
/**
|
|
75
|
-
* Remove `//` line comments and block comments while preserving string and
|
|
76
|
-
* template literals (which carry the import specifiers we extract). A
|
|
77
|
-
* char-by-char state machine is used rather than a regex so that comment
|
|
78
|
-
* markers inside string literals (e.g. a `https://` URL) are not mistaken
|
|
79
|
-
* for comments, and example import syntax inside *comments* (e.g. a
|
|
80
|
-
* `// require('x')` doc line) does not register as a phantom dependency.
|
|
81
|
-
*
|
|
82
|
-
* @param {string} source
|
|
83
|
-
* @returns {string}
|
|
84
|
-
*/
|
|
85
|
-
export function stripComments(source) {
|
|
86
|
-
let out = '';
|
|
87
|
-
let i = 0;
|
|
88
|
-
const n = source.length;
|
|
89
|
-
while (i < n) {
|
|
90
|
-
const ch = source[i];
|
|
91
|
-
const next = source[i + 1];
|
|
92
|
-
// Enter a string / template literal — copy verbatim until it closes.
|
|
93
|
-
if (ch === '"' || ch === "'" || ch === '`') {
|
|
94
|
-
const quote = ch;
|
|
95
|
-
out += ch;
|
|
96
|
-
i += 1;
|
|
97
|
-
while (i < n) {
|
|
98
|
-
const c = source[i];
|
|
99
|
-
out += c;
|
|
100
|
-
if (c === '\\') {
|
|
101
|
-
// Copy the escaped char too, then continue.
|
|
102
|
-
if (i + 1 < n) out += source[i + 1];
|
|
103
|
-
i += 2;
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
i += 1;
|
|
107
|
-
if (c === quote) break;
|
|
108
|
-
}
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
// Line comment — drop to end of line (keep the newline).
|
|
112
|
-
if (ch === '/' && next === '/') {
|
|
113
|
-
i += 2;
|
|
114
|
-
while (i < n && source[i] !== '\n') i += 1;
|
|
115
|
-
continue;
|
|
116
|
-
}
|
|
117
|
-
// Block comment — drop until the closing `*/`.
|
|
118
|
-
if (ch === '/' && next === '*') {
|
|
119
|
-
i += 2;
|
|
120
|
-
while (i < n && !(source[i] === '*' && source[i + 1] === '/')) i += 1;
|
|
121
|
-
i += 2;
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
out += ch;
|
|
125
|
-
i += 1;
|
|
126
|
-
}
|
|
127
|
-
return out;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
75
|
/**
|
|
131
76
|
* Extract the set of third-party top-level package names imported by a
|
|
132
77
|
* single source string.
|
|
@@ -136,7 +81,7 @@ export function stripComments(source) {
|
|
|
136
81
|
*/
|
|
137
82
|
export function extractThirdPartyImports(source) {
|
|
138
83
|
const found = new Set();
|
|
139
|
-
const cleaned =
|
|
84
|
+
const cleaned = stripJsComments(source);
|
|
140
85
|
for (const re of [STATIC_FROM, SIDE_EFFECT, CALL_FORM]) {
|
|
141
86
|
re.lastIndex = 0;
|
|
142
87
|
let match = re.exec(cleaned);
|