mandrel 2.53.0 → 2.55.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/story-worker.md +24 -23
- package/.agents/audit-checklists/accessibility.md +0 -3
- package/.agents/audit-checklists/mobile.md +0 -4
- package/.agents/docs/agentrc-reference.json +4 -2
- package/.agents/docs/configuration.md +2 -0
- package/.agents/schemas/agentrc.schema.json +15 -1
- package/.agents/schemas/lifecycle/merge.unlanded.schema.json +2 -1
- package/.agents/schemas/story-deliver-terminal.schema.json +1 -0
- package/.agents/scripts/audit-to-stories.js +158 -7
- package/.agents/scripts/check-audit-attribution.js +119 -62
- package/.agents/scripts/check-test-portability.js +512 -0
- package/.agents/scripts/coverage-capture.js +17 -10
- package/.agents/scripts/evidence-gate.js +31 -4
- package/.agents/scripts/generate-workflows-doc.js +65 -14
- package/.agents/scripts/git-cleanup.js +4 -0
- package/.agents/scripts/lib/ITicketingProvider.js +78 -0
- package/.agents/scripts/lib/audit-advisories.js +195 -0
- package/.agents/scripts/lib/audit-attribution.js +22 -0
- package/.agents/scripts/lib/audit-to-stories/dedupe-against-github.js +68 -5
- package/.agents/scripts/lib/audit-to-stories/issue-index.js +83 -0
- package/.agents/scripts/lib/audit-to-stories/ledger-commit.js +60 -114
- package/.agents/scripts/lib/audit-to-stories/ledger-pr.js +347 -0
- package/.agents/scripts/lib/audit-to-stories/parse-audit-md.js +169 -44
- package/.agents/scripts/lib/baselines/merge-envelopes.js +298 -32
- package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +180 -14
- package/.agents/scripts/lib/cli-args.js +26 -0
- package/.agents/scripts/lib/close-validation/gates.js +113 -7
- package/.agents/scripts/lib/close-validation/process.js +7 -3
- package/.agents/scripts/lib/close-validation/runner.js +62 -11
- package/.agents/scripts/lib/config/ci.js +28 -9
- package/.agents/scripts/lib/config-settings-schema-delivery.js +7 -0
- package/.agents/scripts/lib/config-settings-schema.js +19 -1
- package/.agents/scripts/lib/coverage-capture-fullscope.js +23 -11
- package/.agents/scripts/lib/coverage-capture-incremental.js +22 -16
- package/.agents/scripts/lib/coverage-capture-usage.js +5 -1
- package/.agents/scripts/lib/coverage-capture.js +77 -3
- package/.agents/scripts/lib/findings/route-finding.js +4 -2
- package/.agents/scripts/lib/full-suite-lock.js +232 -6
- package/.agents/scripts/lib/generated/agentrc-validator.js +1 -1
- package/.agents/scripts/lib/git/sync-from-base.js +130 -13
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/check-baselines/phases/compare.js +10 -2
- package/.agents/scripts/lib/orchestration/check-baselines/phases/refresh-ack.js +75 -15
- package/.agents/scripts/lib/orchestration/deliver-recover.js +82 -43
- package/.agents/scripts/lib/orchestration/dependency-candidates.js +8 -4
- package/.agents/scripts/lib/orchestration/epic-candidates.js +9 -4
- package/.agents/scripts/lib/orchestration/epic-container.js +66 -4
- package/.agents/scripts/lib/orchestration/epic-rollup.js +241 -84
- package/.agents/scripts/lib/orchestration/file-assumptions.js +218 -16
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/branches.js +93 -7
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/git-probes.js +22 -6
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/parse-args.js +26 -5
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/phase-drivers.js +13 -2
- package/.agents/scripts/lib/orchestration/git-cleanup/phases/render.js +35 -5
- package/.agents/scripts/lib/orchestration/merge-block-class.js +18 -3
- package/.agents/scripts/lib/orchestration/merge-poll.js +284 -40
- package/.agents/scripts/lib/orchestration/plan-persist/epic-adoption.js +49 -2
- package/.agents/scripts/lib/orchestration/plan-persist/epic-ops.js +43 -7
- package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +24 -1
- package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +5 -0
- package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -0
- package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +119 -6
- package/.agents/scripts/lib/orchestration/plan-persist/wave-serialisation.js +110 -0
- package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +130 -40
- package/.agents/scripts/lib/orchestration/resolve-stories.js +44 -1
- package/.agents/scripts/lib/orchestration/review-providers/native.js +31 -11
- package/.agents/scripts/lib/orchestration/review-providers/scoped-lint.js +27 -24
- package/.agents/scripts/lib/orchestration/run-epilogue.js +59 -38
- package/.agents/scripts/lib/orchestration/single-story-close/close-note.js +81 -0
- package/.agents/scripts/lib/orchestration/single-story-close/failed-terminal.js +40 -51
- package/.agents/scripts/lib/orchestration/single-story-close/phases/auto-merge.js +10 -2
- package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +101 -0
- package/.agents/scripts/lib/orchestration/single-story-close/phases/confirm-merge.js +351 -28
- package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +27 -6
- package/.agents/scripts/lib/orchestration/single-story-close/runner.js +117 -22
- package/.agents/scripts/lib/orchestration/story-close/baseline-upward-writeback.js +94 -12
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +6 -1
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -14
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +70 -6
- package/.agents/scripts/lib/orchestration/verify-credit.js +37 -0
- package/.agents/scripts/lib/pinned-override-notes.js +41 -53
- package/.agents/scripts/lib/pinned-override-resolve.js +212 -0
- package/.agents/scripts/lib/qa/resolve-qa-contract.js +18 -0
- package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +173 -9
- package/.agents/scripts/lib/skills/walk-skill-files.js +24 -7
- package/.agents/scripts/lib/test-temp.js +167 -30
- package/.agents/scripts/lib/validation-evidence.js +37 -0
- package/.agents/scripts/lib/wave-runner/footprint.js +167 -14
- package/.agents/scripts/lib/wave-runner/live-probe.js +7 -1
- package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
- package/.agents/scripts/merge-baseline.js +175 -21
- package/.agents/scripts/providers/github/errors.js +22 -1
- package/.agents/scripts/providers/github/issues.js +106 -1
- package/.agents/scripts/providers/github/sub-issue-add.js +18 -1
- package/.agents/scripts/providers/github.js +6 -0
- package/.agents/scripts/resolve-stories.js +44 -34
- package/.agents/scripts/single-story-close.js +5 -0
- package/.agents/scripts/stories-wave-tick.js +37 -13
- package/.agents/templates/docs/audit-sweep-runbook.md +41 -7
- package/.agents/workflows/audit-accessibility.md +16 -31
- package/.agents/workflows/audit-mobile.md +20 -37
- package/.agents/workflows/git-cleanup.md +17 -3
- package/.agents/workflows/helpers/audit-lens-core.md +45 -0
- package/.agents/workflows/helpers/deliver-digest.md +7 -6
- package/.agents/workflows/helpers/deliver-reference.md +40 -16
- package/.agents/workflows/helpers/deliver-story-reference.md +7 -4
- package/.agents/workflows/helpers/deliver-story.md +15 -12
- package/.agents/workflows/helpers/plan-reference.md +8 -1
- package/.agents/workflows/mandrel-plan.md +4 -7
- package/.agents/workflows/memory-consolidate.md +14 -9
- package/docs/CHANGELOG.md +34 -0
- package/lib/cli/registry.js +64 -21
- package/lib/cli/sync.js +27 -2
- package/package.json +7 -4
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* 2. Rolls up friction follow-ups across every Story in the run and
|
|
8
8
|
* files/posts them on the primary Story.
|
|
9
9
|
* 3. Checks sibling Spec/acceptance coherence across Story bodies.
|
|
10
|
-
* 4. Reports the
|
|
11
|
-
*
|
|
10
|
+
* 4. Reports what the per-Story land tails left the run's container Epics
|
|
11
|
+
* in — closed, or still open (Story #5139; read-only since #5280).
|
|
12
12
|
*
|
|
13
13
|
* There is no inert planner-only path: `planRunEpilogue` enumerates steps
|
|
14
14
|
* and `runPlanRunEpilogue` executes them. Single-Story runs skip the
|
|
@@ -21,7 +21,7 @@ import { selectAudits } from '../audit-suite/index.js';
|
|
|
21
21
|
import { graduateRetroProposals } from '../feedback-loop/retro-proposals-graduator.js';
|
|
22
22
|
import { gitSpawn } from '../git-utils.js';
|
|
23
23
|
import { Logger } from '../Logger.js';
|
|
24
|
-
import {
|
|
24
|
+
import { isEpicTicket } from './epic-container.js';
|
|
25
25
|
import { composeRoutedProposals } from './retro-proposals.js';
|
|
26
26
|
import {
|
|
27
27
|
assessRollupOutcome,
|
|
@@ -44,54 +44,51 @@ export const RUN_EPILOGUE_STEP_KINDS = Object.freeze([
|
|
|
44
44
|
]);
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
|
-
*
|
|
47
|
+
* Report which container Epics this run's Stories left closed, and which are
|
|
48
|
+
* still open.
|
|
48
49
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
50
|
+
* **It derives nothing and writes nothing.** It used to: it walked every Story
|
|
51
|
+
* and re-ran the full rollup, closing containers itself. That made sense while
|
|
52
|
+
* the rollup only fired on the edges someone had wired, and the epilogue was
|
|
53
|
+
* the backstop for the ones that were missed. Every child state change is now
|
|
54
|
+
* an edge — init, post-land, the supersede close — so by the time the last
|
|
55
|
+
* Story of a run has landed, its container has already been derived from a
|
|
56
|
+
* complete child set by that Story's own land tail. Re-deriving here would ask
|
|
57
|
+
* the same question a second time and answer it identically, at the cost of a
|
|
58
|
+
* full re-read per Story and a second writer on the same issue.
|
|
55
59
|
*
|
|
56
|
-
*
|
|
57
|
-
* see
|
|
60
|
+
* What survives is the report, which is why the step exists at all: one place
|
|
61
|
+
* an operator reads to see what a multi-Story run did to its containers. A
|
|
62
|
+
* pending Epic here is a real signal — it means a land tail's rollup declined
|
|
63
|
+
* to close, and the tail's own outcome says why.
|
|
58
64
|
*
|
|
59
|
-
* Non-fatal throughout:
|
|
60
|
-
* left open costs tidiness, not correctness.
|
|
65
|
+
* Non-fatal throughout: a container it cannot read is simply not reported.
|
|
61
66
|
*
|
|
62
|
-
* @param {{ stories: string[], provider: object
|
|
67
|
+
* @param {{ stories: string[], provider: object }} opts
|
|
63
68
|
* @returns {Promise<{ kind: string, closed: number[], pending: number[] }>}
|
|
64
69
|
*/
|
|
65
|
-
async function executeEpicClose({ stories, provider
|
|
70
|
+
async function executeEpicClose({ stories, provider }) {
|
|
66
71
|
const closed = new Set();
|
|
67
72
|
const pending = new Set();
|
|
68
|
-
// Siblings share a container
|
|
69
|
-
//
|
|
70
|
-
// and re-close what the first already closed.
|
|
73
|
+
// Siblings share a container: resolve each distinct Epic once, however many
|
|
74
|
+
// of the run's Stories point at it.
|
|
71
75
|
const seen = new Set();
|
|
72
76
|
|
|
73
|
-
// `rollUpEpicForStory` never throws and always returns the full envelope,
|
|
74
|
-
// so its three lists are read directly — a `?? []` guard here would be an
|
|
75
|
-
// unreachable branch asserting a contract the module already keeps.
|
|
76
77
|
for (const raw of stories) {
|
|
77
78
|
const storyId = Number(raw);
|
|
78
79
|
if (!Number.isInteger(storyId) || storyId <= 0) continue;
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
80
|
+
const epic = await readContainerFor({ storyId, provider });
|
|
81
|
+
if (!epic) continue;
|
|
82
|
+
const epicId = Number(epic.id);
|
|
83
|
+
if (!Number.isInteger(epicId) || seen.has(epicId)) continue;
|
|
84
|
+
seen.add(epicId);
|
|
85
|
+
if (String(epic.state ?? '').toLowerCase() === 'closed') {
|
|
86
|
+
closed.add(epicId);
|
|
87
|
+
} else {
|
|
88
|
+
pending.add(epicId);
|
|
89
|
+
}
|
|
88
90
|
}
|
|
89
91
|
|
|
90
|
-
// An Epic this run closed can also have been reported pending by an
|
|
91
|
-
// earlier Story's rollup, when a sibling had not landed yet. The close is
|
|
92
|
-
// the later, truer answer.
|
|
93
|
-
for (const epicId of closed) pending.delete(epicId);
|
|
94
|
-
|
|
95
92
|
return {
|
|
96
93
|
kind: 'epic-close',
|
|
97
94
|
closed: [...closed],
|
|
@@ -99,6 +96,30 @@ async function executeEpicClose({ stories, provider, config }) {
|
|
|
99
96
|
};
|
|
100
97
|
}
|
|
101
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Read one Story's container Epic, or null.
|
|
101
|
+
*
|
|
102
|
+
* One request per Story via the declared parent port. Degrades to null on any
|
|
103
|
+
* failure and on a provider without the port — this is a report, and a
|
|
104
|
+
* container it could not read is better omitted than guessed at.
|
|
105
|
+
*
|
|
106
|
+
* @param {{ storyId: number, provider: object }} opts
|
|
107
|
+
* @returns {Promise<object|null>}
|
|
108
|
+
*/
|
|
109
|
+
async function readContainerFor({ storyId, provider }) {
|
|
110
|
+
if (typeof provider?.getParentIssue !== 'function') return null;
|
|
111
|
+
try {
|
|
112
|
+
const parent = await provider.getParentIssue(storyId);
|
|
113
|
+
return parent && isEpicTicket(parent) ? parent : null;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
Logger.warn(
|
|
116
|
+
`[run-epilogue] could not read the container for Story #${storyId} ` +
|
|
117
|
+
`(${err?.message ?? err}); omitting it from the Epic report.`,
|
|
118
|
+
);
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
102
123
|
/**
|
|
103
124
|
* @param {string|number|{ id?: string|number, slug?: string }} entry
|
|
104
125
|
* @returns {string|null}
|
|
@@ -185,7 +206,7 @@ export function planRunEpilogue({ planRunId, stories } = {}) {
|
|
|
185
206
|
},
|
|
186
207
|
{
|
|
187
208
|
kind: 'epic-close',
|
|
188
|
-
description: `
|
|
209
|
+
description: `Report the container Epic state the land tails of run ${effectiveRunId} left behind`,
|
|
189
210
|
stories: ids,
|
|
190
211
|
},
|
|
191
212
|
];
|
|
@@ -892,7 +913,7 @@ export async function runPlanRunEpilogue({
|
|
|
892
913
|
);
|
|
893
914
|
} else if (step.kind === 'epic-close') {
|
|
894
915
|
results.push(
|
|
895
|
-
await executeEpicClose({ stories: plan.stories, provider
|
|
916
|
+
await executeEpicClose({ stories: plan.stories, provider }),
|
|
896
917
|
);
|
|
897
918
|
}
|
|
898
919
|
} catch (err) {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* close-note.js — the human-readable `note` on close's result record
|
|
3
|
+
* (Story #5266).
|
|
4
|
+
*
|
|
5
|
+
* ## Why this is its own module
|
|
6
|
+
*
|
|
7
|
+
* The note used to branch on `waitedForMerge` — whether close *waited* — and
|
|
8
|
+
* not on `merged`. A bounded wait that expired with the PR still open
|
|
9
|
+
* therefore wrote "Close-and-land: PR merge confirmed … the issue closed"
|
|
10
|
+
* into `story-close-result-<id>.log` **beside `merged: false`**, while the
|
|
11
|
+
* schema-validated terminal envelope correctly reported
|
|
12
|
+
* `status: pending, phase: confirm-merge`. That log is what close's summary
|
|
13
|
+
* line points the operator at, so the contradiction is what gets read first:
|
|
14
|
+
* a merge that never happened, reported as confirmed.
|
|
15
|
+
*
|
|
16
|
+
* The invariant this module exists to hold:
|
|
17
|
+
*
|
|
18
|
+
* > **No note may assert a state the same object denies.**
|
|
19
|
+
*
|
|
20
|
+
* Every branch below is therefore derived from the result's OWN
|
|
21
|
+
* `merged` / `directMerged` / `autoMergeEnabled` / `landCompleted` fields —
|
|
22
|
+
* the ones the note ships next to — so no input can produce a note that
|
|
23
|
+
* contradicts them. Story #5279 added the fourth, because `merged: true`
|
|
24
|
+
* used to imply the flip, the issue close and the post-land tail: a direct
|
|
25
|
+
* squash-merge under `--no-wait-merge`, and a merge whose `agent::done` write
|
|
26
|
+
* failed, now reach it with none of the three, and reusing the confirmed
|
|
27
|
+
* wording for them would reintroduce the defect above one field over. The
|
|
28
|
+
* unmerged branches deliberately claim nothing about the Story's label state
|
|
29
|
+
* either: close's ending may be `pending` OR `blocked` with the same
|
|
30
|
+
* `merged: false`, and the terminal envelope is the authority on which. They
|
|
31
|
+
* also avoid the merge-completion vocabulary entirely — no `agent::done`, no
|
|
32
|
+
* "the merge confirms" — so that a reader skimming for those words cannot
|
|
33
|
+
* take a next-step instruction for a report of what happened.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The one line the note is not allowed to get wrong.
|
|
38
|
+
*
|
|
39
|
+
* @param {{ merged?: boolean, directMerged?: boolean,
|
|
40
|
+
* autoMergeEnabled?: boolean, landCompleted?: boolean }} result
|
|
41
|
+
* `landCompleted` — did THIS run flip `agent::done`, close the issue and
|
|
42
|
+
* run the post-land tail? Defaults to `merged`, the pre-#5279 equivalence.
|
|
43
|
+
* @returns {string}
|
|
44
|
+
*/
|
|
45
|
+
export function deriveCloseNote({
|
|
46
|
+
merged = false,
|
|
47
|
+
directMerged = false,
|
|
48
|
+
autoMergeEnabled = false,
|
|
49
|
+
landCompleted = merged,
|
|
50
|
+
} = {}) {
|
|
51
|
+
if (merged) {
|
|
52
|
+
const how = directMerged
|
|
53
|
+
? 'PR merge confirmed by a DIRECT squash-merge (native auto-merge was ' +
|
|
54
|
+
'unavailable on this repository).'
|
|
55
|
+
: 'PR merge confirmed.';
|
|
56
|
+
return landCompleted
|
|
57
|
+
? `Close-and-land: ${how} Story flipped agent::closing → agent::done, ` +
|
|
58
|
+
'the issue closed (confirmStoryMerged), and the post-land tail ran.'
|
|
59
|
+
: `Close-and-land: ${how} This close did NOT finish the land: ` +
|
|
60
|
+
'agent::done was not flipped and the post-land tail did not run. ' +
|
|
61
|
+
'Finish it with single-story-confirm-merge.js, idempotent against an ' +
|
|
62
|
+
'already-merged PR. The terminal envelope is authoritative.';
|
|
63
|
+
}
|
|
64
|
+
if (autoMergeEnabled) {
|
|
65
|
+
return (
|
|
66
|
+
'PR open against baseBranch and NOT merged; auto-merge is armed. ' +
|
|
67
|
+
'GitHub will squash-merge it once the required checks pass — resume ' +
|
|
68
|
+
'with single-story-confirm-merge.js then, to finish the land and ' +
|
|
69
|
+
'release the lease this close is still holding. The terminal envelope ' +
|
|
70
|
+
'(status / phase / blocked) is authoritative for what happened here.'
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return (
|
|
74
|
+
'PR open against baseBranch and NOT merged; auto-merge was not armed ' +
|
|
75
|
+
'(see autoMergeReason). The operator owns the land: merge via the GitHub ' +
|
|
76
|
+
'UI, then resume with single-story-confirm-merge.js to finish the land ' +
|
|
77
|
+
'and release the lease this close is still holding. The terminal ' +
|
|
78
|
+
'envelope (status / phase / blocked) is authoritative for what happened ' +
|
|
79
|
+
'here.'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -50,8 +50,11 @@ const GATE_PHASES = Object.freeze([
|
|
|
50
50
|
]);
|
|
51
51
|
|
|
52
52
|
/**
|
|
53
|
-
* The names the
|
|
54
|
-
* `BASELINES_GATE_NAMES` in `lib/close-validation/gates.js` (Story #5172)
|
|
53
|
+
* The names the unified baselines gate can register under, mirrored from
|
|
54
|
+
* `BASELINES_GATE_NAMES` in `lib/close-validation/gates.js` (Story #5172) —
|
|
55
|
+
* all three, matching the projection the SUCCESS path applies in
|
|
56
|
+
* `runner.js#baselinesEnvelopeGates`, so the two endings of one run cannot
|
|
57
|
+
* key the same gate differently.
|
|
55
58
|
*
|
|
56
59
|
* Deliberately a local copy rather than an import: several close suites
|
|
57
60
|
* replace that module wholesale via `t.mock.module`, and a named import here
|
|
@@ -61,48 +64,37 @@ const GATE_PHASES = Object.freeze([
|
|
|
61
64
|
* against each other so the copy cannot drift.
|
|
62
65
|
*/
|
|
63
66
|
const BASELINES_ENTRY_NAMES = Object.freeze([
|
|
67
|
+
'check-baselines',
|
|
64
68
|
'check-baselines-independent',
|
|
65
69
|
'check-baselines-coverage',
|
|
66
70
|
]);
|
|
67
71
|
|
|
68
72
|
/**
|
|
69
|
-
*
|
|
73
|
+
* Project the baselines entries out of the per-gate outcomes THIS run
|
|
74
|
+
* observed (Story #5279).
|
|
70
75
|
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
* which the failure pre-empted).
|
|
80
|
-
* - died on the coverage-consuming entry → that one `failed`, and the
|
|
81
|
-
* independent one `passed`: it is in the parallel partition that must go
|
|
82
|
-
* green before any serial gate starts.
|
|
83
|
-
* - died in validation on some other gate → both `skipped`; which of them
|
|
84
|
-
* had run is not knowable from the phase alone.
|
|
76
|
+
* This used to RECONSTRUCT them: it inferred, from the phase the run died in
|
|
77
|
+
* plus the name of the failing gate, what the two split entries "must have"
|
|
78
|
+
* done — and it did so over a hardcoded pair, so every failed close reported
|
|
79
|
+
* both split names whether or not the run had ever registered them. A repo
|
|
80
|
+
* whose config resolves to the unsplit `check-baselines`, or to only one half
|
|
81
|
+
* of the pair, got envelope keys for gates that did not exist; a run that
|
|
82
|
+
* died at `init` got them too, reported as `skipped`, which reads as "the
|
|
83
|
+
* gate was turned off" rather than "there was no such gate".
|
|
85
84
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
85
|
+
* Reporting instead of reconstructing removes the whole class: an outcome
|
|
86
|
+
* appears only for a gate the run actually observed, and the runner tags
|
|
87
|
+
* exactly that set onto the error (`err.closeGates`).
|
|
88
|
+
*
|
|
89
|
+
* @param {Record<string, string>|null|undefined} observedGates
|
|
88
90
|
* @returns {Record<string, 'passed'|'failed'|'skipped'>}
|
|
89
91
|
*/
|
|
90
|
-
function
|
|
91
|
-
const
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
const validationAt = PHASE_ORDER.indexOf('close-validation');
|
|
95
|
-
if (skipValidation || failedAt < 0 || failedAt < validationAt) {
|
|
96
|
-
return both('skipped');
|
|
97
|
-
}
|
|
98
|
-
if (failedAt > validationAt) return both('passed');
|
|
99
|
-
if (failedGate === independent) {
|
|
100
|
-
return { [independent]: 'failed', [coverage]: 'skipped' };
|
|
101
|
-
}
|
|
102
|
-
if (failedGate === coverage) {
|
|
103
|
-
return { [independent]: 'passed', [coverage]: 'failed' };
|
|
92
|
+
function baselinesGatesObserved(observedGates) {
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const [name, outcome] of Object.entries(observedGates ?? {})) {
|
|
95
|
+
if (BASELINES_ENTRY_NAMES.includes(name)) out[name] = outcome;
|
|
104
96
|
}
|
|
105
|
-
return
|
|
97
|
+
return out;
|
|
106
98
|
}
|
|
107
99
|
|
|
108
100
|
/**
|
|
@@ -119,14 +111,17 @@ function baselinesGatesForFailedPhase(phase, { skipValidation, failedGate }) {
|
|
|
119
111
|
* turned off via `--skip-validation` / `--skip-sync` is `skipped` too (it did
|
|
120
112
|
* not pass — it never ran).
|
|
121
113
|
*
|
|
122
|
-
* Story #5172 — the reported set also carries the
|
|
123
|
-
*
|
|
124
|
-
*
|
|
114
|
+
* Story #5172 — the reported set also carries the baselines entries under
|
|
115
|
+
* their own names, so a failed close says WHICH half of the baselines gate
|
|
116
|
+
* breached instead of a single generic verdict. Story #5279 — those names
|
|
117
|
+
* are REPORTED from `observedGates`, never reconstructed, so only a gate the
|
|
118
|
+
* run registered can appear.
|
|
125
119
|
*
|
|
126
120
|
* @param {string} phase The phase the run died in.
|
|
127
|
-
* @param {{ skipValidation?: boolean, skipSync?: boolean,
|
|
128
|
-
*
|
|
129
|
-
*
|
|
121
|
+
* @param {{ skipValidation?: boolean, skipSync?: boolean,
|
|
122
|
+
* observedGates?: Record<string, string>|null }} args
|
|
123
|
+
* Parsed CLI args, plus the per-gate outcomes the runner tagged onto the
|
|
124
|
+
* error.
|
|
130
125
|
* @returns {Record<string, 'passed'|'failed'|'skipped'>}
|
|
131
126
|
*/
|
|
132
127
|
export function gatesForFailedPhase(phase, args = {}) {
|
|
@@ -139,13 +134,7 @@ export function gatesForFailedPhase(phase, args = {}) {
|
|
|
139
134
|
else if (failedAt < 0 || at > failedAt) gates[gate] = 'skipped';
|
|
140
135
|
else gates[gate] = skipped[gate] ? 'skipped' : 'passed';
|
|
141
136
|
}
|
|
142
|
-
return {
|
|
143
|
-
...gates,
|
|
144
|
-
...baselinesGatesForFailedPhase(phase, {
|
|
145
|
-
skipValidation: args.skipValidation,
|
|
146
|
-
failedGate: args.failedGate ?? null,
|
|
147
|
-
}),
|
|
148
|
-
};
|
|
137
|
+
return { ...gates, ...baselinesGatesObserved(args.observedGates) };
|
|
149
138
|
}
|
|
150
139
|
|
|
151
140
|
/**
|
|
@@ -163,9 +152,9 @@ export function gatesForFailedPhase(phase, args = {}) {
|
|
|
163
152
|
* holding the script had been reaped mid-run. On failure this returns null
|
|
164
153
|
* and the caller rethrows the original.
|
|
165
154
|
*
|
|
166
|
-
* `err.
|
|
167
|
-
*
|
|
168
|
-
*
|
|
155
|
+
* `err.closeGates` — tagged by the runner — carries the per-gate outcomes the
|
|
156
|
+
* run observed, which is what lets the reported gates name the baselines
|
|
157
|
+
* entries that actually ran (Story #5172 / #5279) instead of a hardcoded pair.
|
|
169
158
|
*
|
|
170
159
|
* @param {unknown} err
|
|
171
160
|
* @param {{ storyId?: string|number, skipValidation?: boolean, skipSync?: boolean }} args
|
|
@@ -186,7 +175,7 @@ export function failedTerminalFor(err, args = {}) {
|
|
|
186
175
|
phase,
|
|
187
176
|
gates: gatesForFailedPhase(phase, {
|
|
188
177
|
...args,
|
|
189
|
-
|
|
178
|
+
observedGates: err?.closeGates ?? null,
|
|
190
179
|
}),
|
|
191
180
|
failure: { reason: String(err?.message ?? err) },
|
|
192
181
|
nextCommand: NEXT_COMMANDS.recover(storyId),
|
|
@@ -66,7 +66,7 @@ import { resolveAutoMergeArmCwd } from '../../auto-merge-cwd.js';
|
|
|
66
66
|
import {
|
|
67
67
|
advisoryCheckFailedBlocksArm,
|
|
68
68
|
deriveRedHeadRuns,
|
|
69
|
-
|
|
69
|
+
resolveAdvisoryGateVerdict,
|
|
70
70
|
selectBlockingRedRuns,
|
|
71
71
|
} from '../../merge-poll.js';
|
|
72
72
|
|
|
@@ -430,10 +430,17 @@ async function evaluateAdvisoryGate({
|
|
|
430
430
|
probe.redHeadRuns,
|
|
431
431
|
advisoryAllowlist,
|
|
432
432
|
);
|
|
433
|
+
// Story #5266 — the class travels with the reason. This pre-arm gate
|
|
434
|
+
// classifies on the text the ROLLUP carried (a legacy StatusContext's
|
|
435
|
+
// `description`); the merge wait, which owns the common case, additionally
|
|
436
|
+
// reads the check-run output. Either way a run whose failure cannot be read
|
|
437
|
+
// as "never finished" keeps the `advisory-gate-red` verdict.
|
|
438
|
+
const verdict = resolveAdvisoryGateVerdict({ blockingRuns });
|
|
433
439
|
return {
|
|
434
440
|
blocked: true,
|
|
435
441
|
blockingRuns,
|
|
436
|
-
|
|
442
|
+
blockClass: verdict.blockClass,
|
|
443
|
+
reason: verdict.reason,
|
|
437
444
|
};
|
|
438
445
|
}
|
|
439
446
|
|
|
@@ -522,6 +529,7 @@ export async function runAutoMergePhase({
|
|
|
522
529
|
autoMergeReason: 'advisory-gate-red',
|
|
523
530
|
advisoryGate: {
|
|
524
531
|
blockingRuns: advisory.blockingRuns,
|
|
532
|
+
blockClass: advisory.blockClass,
|
|
525
533
|
reason: advisory.reason,
|
|
526
534
|
},
|
|
527
535
|
};
|
|
@@ -13,8 +13,18 @@
|
|
|
13
13
|
* On a merge conflict the Story is transitioned to `agent::blocked` via
|
|
14
14
|
* `handleSyncFailure` and the caller throws — the operator resolves in
|
|
15
15
|
* the worktree and re-runs.
|
|
16
|
+
*
|
|
17
|
+
* Story #5267: a sync that lands tracked content also spends the worker's
|
|
18
|
+
* pre-push credit, because that credit is keyed on the tree. The phase says
|
|
19
|
+
* so out loud — see `buildStampInvalidatedWarning` — instead of leaving
|
|
20
|
+
* close's second full suite looking like a bug. Story #5278 splits the claim
|
|
21
|
+
* in two: gate evidence is spent by any tracked path, the capture stamp only
|
|
22
|
+
* by one under `crap.targetDirs`.
|
|
16
23
|
*/
|
|
17
24
|
|
|
25
|
+
import { getQuality } from '../../../config/quality.js';
|
|
26
|
+
import { resolveConfig } from '../../../config-resolver.js';
|
|
27
|
+
import { filterFilesUnderTargets } from '../../../coverage-capture.js';
|
|
18
28
|
import { syncBranchFromBase } from '../../../git/sync-from-base.js';
|
|
19
29
|
import { Logger } from '../../../Logger.js';
|
|
20
30
|
import { AGENT_LABELS } from '../../../label-constants.js';
|
|
@@ -40,6 +50,7 @@ import {
|
|
|
40
50
|
* storyId: number,
|
|
41
51
|
* provider: object,
|
|
42
52
|
* injectedSync?: typeof syncBranchFromBase,
|
|
53
|
+
* resolveConfigImpl?: typeof resolveConfig,
|
|
43
54
|
* progress: (tag: string, msg: string) => void,
|
|
44
55
|
* }} args
|
|
45
56
|
*/
|
|
@@ -52,6 +63,7 @@ export async function runBaseSyncPhase({
|
|
|
52
63
|
storyId,
|
|
53
64
|
provider,
|
|
54
65
|
injectedSync,
|
|
66
|
+
resolveConfigImpl = resolveConfig,
|
|
55
67
|
progress,
|
|
56
68
|
}) {
|
|
57
69
|
const syncCwd = worktreePath ?? cwd;
|
|
@@ -87,6 +99,95 @@ export async function runBaseSyncPhase({
|
|
|
87
99
|
);
|
|
88
100
|
}
|
|
89
101
|
progress('SYNC', `✅ Synced from origin/${baseBranch} (${syncResult.kind}).`);
|
|
102
|
+
for (const line of buildStampInvalidatedWarning({
|
|
103
|
+
baseBranch,
|
|
104
|
+
result: syncResult,
|
|
105
|
+
targetDirs: resolveCrapTargetDirs(resolveConfigImpl, syncCwd),
|
|
106
|
+
})) {
|
|
107
|
+
progress('SYNC', line);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The CRAP scoring scope, or `[]` when it cannot be resolved. A `[]` makes
|
|
113
|
+
* the warning below fall back to naming the capture stamp unconditionally —
|
|
114
|
+
* the pre-#5278 wording — because an unresolvable scope is no evidence that
|
|
115
|
+
* the stamp survived.
|
|
116
|
+
*
|
|
117
|
+
* @param {typeof resolveConfig} resolveConfigImpl
|
|
118
|
+
* @param {string} cwd
|
|
119
|
+
* @returns {string[]}
|
|
120
|
+
*/
|
|
121
|
+
function resolveCrapTargetDirs(resolveConfigImpl, cwd) {
|
|
122
|
+
try {
|
|
123
|
+
return getQuality(resolveConfigImpl({ cwd }))?.crap?.targetDirs ?? [];
|
|
124
|
+
} catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* How many changed paths the warning names before it stops listing.
|
|
131
|
+
* Enough to recognise the change set; short enough that the warning still
|
|
132
|
+
* reads as a warning rather than as a diff.
|
|
133
|
+
*/
|
|
134
|
+
const WARNED_PATH_LIMIT = 12;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The loud "your credit is spent" warning, or `[]` when the sync changed
|
|
138
|
+
* nothing (Story #5267, narrowed by #5278).
|
|
139
|
+
*
|
|
140
|
+
* The worker banks two kinds of credit before the push: gate evidence keyed
|
|
141
|
+
* on the tree (lint, typecheck) and one full-suite capture stamp keyed on the
|
|
142
|
+
* content of `crap.targetDirs`. This sync runs after that, and the two are
|
|
143
|
+
* spent on different conditions — which is why #5267's single blanket
|
|
144
|
+
* sentence was wrong half the time:
|
|
145
|
+
*
|
|
146
|
+
* - **Gate evidence** is spent by any tracked path at all, because the tree
|
|
147
|
+
* hash it is keyed on moves with the first byte.
|
|
148
|
+
* - **The capture stamp** is spent only when a merged path lands under
|
|
149
|
+
* `crap.targetDirs`. A sync that brings in docs, workflows or CI config
|
|
150
|
+
* leaves it perfectly valid, and announcing it as spent taught operators
|
|
151
|
+
* to expect a second full suite that close was never going to run.
|
|
152
|
+
*
|
|
153
|
+
* Quiet by construction on the outcome that cannot spend either: a
|
|
154
|
+
* `noop-already-current` sync never touched the tree. A content-changing
|
|
155
|
+
* fast-forward DOES warn — it moves the tree exactly as a merge commit does,
|
|
156
|
+
* and staying quiet there would be a lie of omission.
|
|
157
|
+
*
|
|
158
|
+
* Pure. Module-private: the phase is the seam tests drive it through
|
|
159
|
+
* (`injectedSync` + a `progress` spy), so it needs no export of its own.
|
|
160
|
+
*
|
|
161
|
+
* @param {{ baseBranch: string, result: { kind?: string, changedPaths?: string[] }, targetDirs?: string[] }} args
|
|
162
|
+
* @returns {string[]} Progress lines, in order. Empty when nothing changed.
|
|
163
|
+
*/
|
|
164
|
+
function buildStampInvalidatedWarning({ baseBranch, result, targetDirs }) {
|
|
165
|
+
const changed = Array.isArray(result?.changedPaths)
|
|
166
|
+
? result.changedPaths
|
|
167
|
+
: [];
|
|
168
|
+
if (changed.length === 0) return [];
|
|
169
|
+
const dirs = Array.isArray(targetDirs) ? targetDirs : [];
|
|
170
|
+
// An unresolvable scope (`[]`) cannot prove the stamp survived, so it fails
|
|
171
|
+
// closed to the unconditional wording.
|
|
172
|
+
const scored =
|
|
173
|
+
dirs.length === 0 ? changed : filterFilesUnderTargets(changed, dirs);
|
|
174
|
+
const shown = changed.slice(0, WARNED_PATH_LIMIT);
|
|
175
|
+
const overflow = changed.length - shown.length;
|
|
176
|
+
return [
|
|
177
|
+
`⚠️ BASE MOVED: the ${result?.kind ?? 'sync'} from origin/${baseBranch} ` +
|
|
178
|
+
`brought ${changed.length} tracked path(s) into this branch, so the tree ` +
|
|
179
|
+
`hash the pre-push lint/typecheck evidence was keyed on has changed. ` +
|
|
180
|
+
`That evidence cannot be credited; those gates re-run below.`,
|
|
181
|
+
scored.length > 0
|
|
182
|
+
? `⚠️ The full-suite capture stamp is spent too: ${scored.length} of ` +
|
|
183
|
+
`those path(s) fall under the CRAP target dirs [${dirs.join(', ')}], ` +
|
|
184
|
+
`so the suite re-runs against the merged tree. This is expected, not a fault.`
|
|
185
|
+
: `⚠️ The full-suite capture stamp SURVIVES: no merged path falls under ` +
|
|
186
|
+
`the CRAP target dirs [${dirs.join(', ')}], so the coverage artifact still ` +
|
|
187
|
+
`describes this tree and the suite is not re-run.`,
|
|
188
|
+
...shown.map((f) => `⚠️ ${f}`),
|
|
189
|
+
...(overflow > 0 ? [`⚠️ …and ${overflow} more`] : []),
|
|
190
|
+
];
|
|
90
191
|
}
|
|
91
192
|
|
|
92
193
|
/**
|