mandrel 2.47.0 → 2.49.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 +49 -49
- package/.agents/docs/configuration.md +1 -0
- package/.agents/docs/quality-gates.md +48 -0
- package/.agents/scripts/lib/baselines/kernel.js +19 -0
- package/.agents/scripts/lib/baselines/kinds/bundle-size.js +12 -0
- package/.agents/scripts/lib/baselines/kinds/coverage.js +1 -0
- package/.agents/scripts/lib/baselines/kinds/crap.js +21 -5
- package/.agents/scripts/lib/baselines/kinds/duplication.js +1 -0
- package/.agents/scripts/lib/baselines/kinds/kind-factory.js +26 -1
- package/.agents/scripts/lib/baselines/kinds/lighthouse.js +1 -0
- package/.agents/scripts/lib/baselines/kinds/lint.js +12 -0
- package/.agents/scripts/lib/baselines/kinds/maintainability.js +1 -0
- package/.agents/scripts/lib/baselines/kinds/mutation.js +1 -0
- package/.agents/scripts/lib/baselines/merge-envelopes.js +272 -0
- package/.agents/scripts/lib/bootstrap/baseline-merge-driver.js +175 -0
- package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +8 -2
- package/.agents/scripts/lib/observability/source-classifier.js +1 -0
- package/.agents/scripts/lib/orchestration/epic-container.js +48 -21
- package/.agents/scripts/lib/orchestration/epic-expansion.js +28 -6
- package/.agents/scripts/lib/orchestration/epic-rollup.js +66 -7
- package/.agents/scripts/lib/orchestration/single-story-close/phases/close-validation.js +32 -61
- package/.agents/scripts/lib/orchestration/single-story-close/phases/pre-gate-steps.js +171 -0
- package/.agents/scripts/lib/orchestration/story-close/baseline-upward-writeback.js +483 -0
- package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +6 -1
- package/.agents/scripts/merge-baseline.js +238 -0
- package/.agents/scripts/providers/github/errors.js +66 -10
- package/.agents/scripts/providers/github/sub-issues.js +8 -1
- package/.agents/workflows/helpers/deliver-digest.md +30 -26
- package/.agents/workflows/helpers/parallel-tooling.md +17 -0
- package/docs/CHANGELOG.md +25 -0
- package/lib/cli/registry.js +63 -0
- package/package.json +1 -1
|
@@ -28,9 +28,18 @@
|
|
|
28
28
|
* 3. **Never throws.** Every step degrades with a reason. A stale
|
|
29
29
|
* container costs tidiness; a delivery failed on a board mutation
|
|
30
30
|
* costs a landed Story its terminal envelope.
|
|
31
|
+
* 4. **Closure requires an authoritative child list.** Invariant 3 makes
|
|
32
|
+
* every read degrade rather than fail, which is right for the writes
|
|
33
|
+
* that recompute next tick and wrong for the one that does not. When
|
|
34
|
+
* the native sub-issue read fails, the body checklist still answers
|
|
35
|
+
* "who are the children" — but no longer "are these *all* of them",
|
|
36
|
+
* and closing on that difference shut an Epic over 23 open children
|
|
37
|
+
* (Story #5210). Degraded reads keep the Status and assignee writes
|
|
38
|
+
* and lose only the close.
|
|
31
39
|
*
|
|
32
40
|
* @module lib/orchestration/epic-rollup
|
|
33
41
|
* @see Story #5205
|
|
42
|
+
* @see Story #5210 — fail closed on a degraded child read.
|
|
34
43
|
*/
|
|
35
44
|
|
|
36
45
|
import { Logger } from '../Logger.js';
|
|
@@ -120,6 +129,11 @@ function isClosed(issue) {
|
|
|
120
129
|
* the list it is handed, so a silently dropped child could close a container
|
|
121
130
|
* with work still open under it.
|
|
122
131
|
*
|
|
132
|
+
* Note the scope: this validates the **readability of the ids it was given**,
|
|
133
|
+
* never the **completeness of the id list**. Completeness is
|
|
134
|
+
* `nativeReadFailed`'s job in {@link rollUpOneEpic} — checking only this one
|
|
135
|
+
* is what let three readable ids stand in for 58 (Story #5210).
|
|
136
|
+
*
|
|
123
137
|
* @param {{ epicId: number, childIds: number[], provider: object }} opts
|
|
124
138
|
* @returns {Promise<object[]|null>}
|
|
125
139
|
*/
|
|
@@ -214,10 +228,24 @@ async function applyClosure({ epicId, provider }) {
|
|
|
214
228
|
/**
|
|
215
229
|
* Roll one Epic up from the children it lists.
|
|
216
230
|
*
|
|
217
|
-
*
|
|
231
|
+
* `nativeReadFailed` splits the writes by reversibility. Column and assignee
|
|
232
|
+
* are recomputed from scratch on every later tick, so applying them to a
|
|
233
|
+
* possibly-truncated list costs at most a stale board cell that self-corrects.
|
|
234
|
+
* Closure does not: it is the one write no subsequent tick undoes (invariant 2
|
|
235
|
+
* — a reopened child pulls Status back but MUST NOT reopen the issue), so it
|
|
236
|
+
* requires a child list we know to be complete.
|
|
237
|
+
*
|
|
238
|
+
* @param {{ epic: object, childIds: number[], nativeReadFailed?: boolean, provider: object, columnSync: object, owner: string|null }} opts
|
|
218
239
|
* @returns {Promise<object>} Per-Epic outcome record.
|
|
219
240
|
*/
|
|
220
|
-
async function rollUpOneEpic({
|
|
241
|
+
async function rollUpOneEpic({
|
|
242
|
+
epic,
|
|
243
|
+
childIds,
|
|
244
|
+
nativeReadFailed = false,
|
|
245
|
+
provider,
|
|
246
|
+
columnSync,
|
|
247
|
+
owner,
|
|
248
|
+
}) {
|
|
221
249
|
const epicId = Number(epic?.number ?? epic?.id);
|
|
222
250
|
const outcome = {
|
|
223
251
|
epicId,
|
|
@@ -261,6 +289,24 @@ async function rollUpOneEpic({ epic, childIds, provider, columnSync, owner }) {
|
|
|
261
289
|
|
|
262
290
|
if (isClosed(epic)) return outcome;
|
|
263
291
|
|
|
292
|
+
if (nativeReadFailed) {
|
|
293
|
+
// Every child we could see has landed — but the authoritative read threw,
|
|
294
|
+
// so "every child" is exactly the claim we cannot make. `readChildren`
|
|
295
|
+
// above validates that the ids we were handed are *readable*; nothing
|
|
296
|
+
// there validates that the list is *complete*, which is how an Epic with
|
|
297
|
+
// 23 open children closed off the three its body happened to spell in the
|
|
298
|
+
// bare `- [ ] #N` form (Story #5210). Overwrites any column/owner detail
|
|
299
|
+
// deliberately: this is the reason the Epic is still pending.
|
|
300
|
+
outcome.pending = true;
|
|
301
|
+
outcome.detail = 'child-read-degraded';
|
|
302
|
+
Logger.warn(
|
|
303
|
+
`[epic-rollup] Epic #${epicId}: every child read looks done, but the ` +
|
|
304
|
+
'native sub-issue read degraded — refusing to close on a possibly ' +
|
|
305
|
+
'incomplete child list. Re-run once the API read succeeds.',
|
|
306
|
+
);
|
|
307
|
+
return outcome;
|
|
308
|
+
}
|
|
309
|
+
|
|
264
310
|
const closure = await applyClosure({ epicId, provider });
|
|
265
311
|
outcome.closed = closure.closed;
|
|
266
312
|
outcome.pending = !closure.closed;
|
|
@@ -277,7 +323,7 @@ async function rollUpOneEpic({ epic, childIds, provider, columnSync, owner }) {
|
|
|
277
323
|
* containers are few, and only one listing this Story is ever read further.
|
|
278
324
|
*
|
|
279
325
|
* @param {{ storyId: number, provider: object, skipEpicIds: Set<number> }} opts
|
|
280
|
-
* @returns {Promise<Array<{ epic: object, childIds: number[] }>>}
|
|
326
|
+
* @returns {Promise<Array<{ epic: object, childIds: number[], nativeReadFailed: boolean }>>}
|
|
281
327
|
*/
|
|
282
328
|
async function findEpicsForStory({ storyId, provider, skipEpicIds }) {
|
|
283
329
|
let epics;
|
|
@@ -304,13 +350,25 @@ async function findEpicsForStory({ storyId, provider, skipEpicIds }) {
|
|
|
304
350
|
// delivery expansion uses. Reading the body alone here is what made an
|
|
305
351
|
// Epic whose children were linked in the GitHub UI expandable but
|
|
306
352
|
// permanently unclosable.
|
|
307
|
-
const childIds = await readEpicChildIdsFrom({
|
|
353
|
+
const { ids: childIds, nativeReadFailed } = await readEpicChildIdsFrom({
|
|
308
354
|
epic,
|
|
309
355
|
readNativeChildIds: nativeChildReader(provider),
|
|
310
356
|
onWarn: (message) => Logger.warn(message),
|
|
311
357
|
});
|
|
312
|
-
if (!childIds.includes(storyId))
|
|
313
|
-
|
|
358
|
+
if (!childIds.includes(storyId)) {
|
|
359
|
+
// A degraded read can truncate this Story out of its own container's
|
|
360
|
+
// child list, which drops the Epic from the run entirely rather than
|
|
361
|
+
// rolling it up wrongly. Non-destructive, but silent — say so, since it
|
|
362
|
+
// is the same root cause as the refusal in `rollUpOneEpic`.
|
|
363
|
+
if (nativeReadFailed) {
|
|
364
|
+
Logger.warn(
|
|
365
|
+
`[epic-rollup] Epic #${epicId}: skipped for Story #${storyId} on a ` +
|
|
366
|
+
'degraded child read — the Story may in fact be linked to it.',
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
matches.push({ epic, childIds, nativeReadFailed });
|
|
314
372
|
}
|
|
315
373
|
return matches;
|
|
316
374
|
}
|
|
@@ -374,11 +432,12 @@ export async function rollUpEpicForStory({
|
|
|
374
432
|
owner === undefined ? resolveEpicOwner(config) : owner;
|
|
375
433
|
|
|
376
434
|
const epics = [];
|
|
377
|
-
for (const { epic, childIds } of matches) {
|
|
435
|
+
for (const { epic, childIds, nativeReadFailed } of matches) {
|
|
378
436
|
epics.push(
|
|
379
437
|
await rollUpOneEpic({
|
|
380
438
|
epic,
|
|
381
439
|
childIds,
|
|
440
|
+
nativeReadFailed,
|
|
382
441
|
provider,
|
|
383
442
|
columnSync: sync,
|
|
384
443
|
owner: resolvedOwner,
|
|
@@ -17,12 +17,12 @@
|
|
|
17
17
|
* `temp/standalone/stories/story-<id>/validation-evidence.json`, so a
|
|
18
18
|
* second close at unchanged HEAD short-circuits the already-passed gates.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
20
|
+
* Pre-gate self-heal (Stories #4250, #5224). Two best-effort steps run on the
|
|
21
|
+
* Story branch before the check-only gates score it — the scoped format
|
|
22
|
+
* autofix and the upward maintainability write-back — so whatever they commit
|
|
23
|
+
* is part of what the gates see and part of the branch's own PR. Both live in
|
|
24
|
+
* [`pre-gate-steps.js`](pre-gate-steps.js); neither can fail a close, because
|
|
25
|
+
* an authoritative gate for each runs immediately below.
|
|
26
26
|
*
|
|
27
27
|
* Bounded gate output (Story #4736). Every gate line goes to the run's
|
|
28
28
|
* `gate-log.js` sink — an artifact under the gitignored temp tree — instead
|
|
@@ -40,25 +40,24 @@
|
|
|
40
40
|
* `*:update` + `baseline-refresh:` remedy — advisory only, so the close
|
|
41
41
|
* verdict is unchanged.
|
|
42
42
|
*
|
|
43
|
-
* `runCloseValidation`, `buildDefaultGates
|
|
44
|
-
* are accepted as injected dependencies so
|
|
45
|
-
* bindings win in tests that mock the upstream
|
|
43
|
+
* `runCloseValidation`, `buildDefaultGates` and the pre-gate steps (whole, or
|
|
44
|
+
* their two individual collaborators) are accepted as injected dependencies so
|
|
45
|
+
* the parent CLI's cache-busted bindings win in tests that mock the upstream
|
|
46
|
+
* module URLs.
|
|
46
47
|
*/
|
|
47
48
|
|
|
48
49
|
import { buildDefaultGates as defaultBuildDefaultGates } from '../../../close-validation/gates.js';
|
|
49
50
|
import { runCloseValidation as defaultRunCloseValidation } from '../../../close-validation/runner.js';
|
|
50
|
-
import { Logger } from '../../../Logger.js';
|
|
51
|
-
import { runScopedFormatAutofix as defaultRunScopedFormatAutofix } from '../../story-close/format-autofix.js';
|
|
52
51
|
import { createGateLogSink as defaultCreateGateLogSink } from '../gate-log.js';
|
|
52
|
+
import { runPreGateSteps as defaultRunPreGateSteps } from './pre-gate-steps.js';
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Run the close-validation gate chain. Throws on first gate failure.
|
|
56
56
|
*
|
|
57
|
-
* Order
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* with a log line rather than failing.
|
|
57
|
+
* Order: pre-gate self-heal steps → close-validation gates. The steps scope
|
|
58
|
+
* to the `baseBranch...storyBranch` diff, commit inside the Story worktree,
|
|
59
|
+
* and are best-effort — a missing `storyBranch` (resume/legacy callers) skips
|
|
60
|
+
* them with a log line rather than failing.
|
|
62
61
|
*
|
|
63
62
|
* Gates are built from the canonical resolved config (`buildDefaultGates`
|
|
64
63
|
* reads `project.commands` and `delivery.quality.gates.crap.enabled`); the
|
|
@@ -76,7 +75,9 @@ import { createGateLogSink as defaultCreateGateLogSink } from '../gate-log.js';
|
|
|
76
75
|
* progress: (tag: string, msg: string) => void,
|
|
77
76
|
* runCloseValidation?: typeof defaultRunCloseValidation,
|
|
78
77
|
* buildDefaultGates?: typeof defaultBuildDefaultGates,
|
|
79
|
-
*
|
|
78
|
+
* runPreGateSteps?: typeof defaultRunPreGateSteps,
|
|
79
|
+
* runScopedFormatAutofix?: Function,
|
|
80
|
+
* runBaselineUpwardWriteback?: Function,
|
|
80
81
|
* createGateLogSink?: typeof defaultCreateGateLogSink,
|
|
81
82
|
* }} args
|
|
82
83
|
* @returns {Promise<{ gates: Record<string, 'passed'|'skipped'> }>} Per-gate
|
|
@@ -94,52 +95,22 @@ export async function runCloseValidationPhase({
|
|
|
94
95
|
progress,
|
|
95
96
|
runCloseValidation = defaultRunCloseValidation,
|
|
96
97
|
buildDefaultGates = defaultBuildDefaultGates,
|
|
97
|
-
|
|
98
|
+
runPreGateSteps = defaultRunPreGateSteps,
|
|
99
|
+
runScopedFormatAutofix,
|
|
100
|
+
runBaselineUpwardWriteback,
|
|
98
101
|
createGateLogSink = defaultCreateGateLogSink,
|
|
99
102
|
}) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// missing ref) must never abort close — the format check gate downstream
|
|
112
|
-
// is the source of truth for "is the tree formatted". We log and proceed.
|
|
113
|
-
try {
|
|
114
|
-
const autofix = runScopedFormatAutofix({
|
|
115
|
-
cwd,
|
|
116
|
-
worktreePath,
|
|
117
|
-
storyId,
|
|
118
|
-
baseBranch,
|
|
119
|
-
storyBranch,
|
|
120
|
-
config,
|
|
121
|
-
logger: Logger,
|
|
122
|
-
});
|
|
123
|
-
if (autofix?.committed) {
|
|
124
|
-
progress(
|
|
125
|
-
'FORMAT',
|
|
126
|
-
`✅ Auto-applied format fix committed as ${autofix.sha} on ${storyBranch}.`,
|
|
127
|
-
);
|
|
128
|
-
} else {
|
|
129
|
-
progress(
|
|
130
|
-
'FORMAT',
|
|
131
|
-
`⏭ No format-autofix commit (${autofix?.reason ?? 'clean'}).`,
|
|
132
|
-
);
|
|
133
|
-
}
|
|
134
|
-
} catch (err) {
|
|
135
|
-
progress(
|
|
136
|
-
'FORMAT',
|
|
137
|
-
`⚠️ scoped format-autofix failed (close continues; format gate is authoritative): ${err?.message ?? err}`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
} else {
|
|
141
|
-
progress('FORMAT', '⏭ Skipped scoped format-autofix (no story branch).');
|
|
142
|
-
}
|
|
103
|
+
await runPreGateSteps({
|
|
104
|
+
cwd,
|
|
105
|
+
worktreePath,
|
|
106
|
+
storyId,
|
|
107
|
+
baseBranch,
|
|
108
|
+
storyBranch,
|
|
109
|
+
config,
|
|
110
|
+
progress,
|
|
111
|
+
runScopedFormatAutofix,
|
|
112
|
+
runBaselineUpwardWriteback,
|
|
113
|
+
});
|
|
143
114
|
|
|
144
115
|
progress(
|
|
145
116
|
'VALIDATE',
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* phases/pre-gate-steps.js — the self-heal steps the standalone close runs on
|
|
3
|
+
* the Story branch *before* the check-only gate chain scores it.
|
|
4
|
+
*
|
|
5
|
+
* Two steps live here, and they share one contract that is the reason they
|
|
6
|
+
* share a module: each may author a commit on `story-<id>`, each must run
|
|
7
|
+
* ahead of the gates so its commit is part of what the gates score and part of
|
|
8
|
+
* the branch's own PR, and **neither may ever fail the close**. Downstream
|
|
9
|
+
* there is always an authoritative gate — `biome ci` for formatting,
|
|
10
|
+
* `check-baselines` for the ratchet — so a failure here is a missed
|
|
11
|
+
* opportunity to self-heal, never a verdict.
|
|
12
|
+
*
|
|
13
|
+
* 1. **Scoped format-autofix** (Story #4250) — run the formatter over the
|
|
14
|
+
* `baseBranch...storyBranch` diff and fold any rewrite into a
|
|
15
|
+
* `fix(story-close):` commit, so benign JSON/YAML drift that lint-staged
|
|
16
|
+
* does not glob never reaches the check-only format gate.
|
|
17
|
+
* 2. **Upward baseline write-back** (Story #5224) — persist the
|
|
18
|
+
* maintainability rows this branch improved on files it touched, as a
|
|
19
|
+
* `baseline-refresh:` commit, so the committed baseline stops falling
|
|
20
|
+
* behind the tree in the upward direction.
|
|
21
|
+
*
|
|
22
|
+
* Extracted from `close-validation.js` when the second step landed. The phase
|
|
23
|
+
* module's job is the gate chain, the evidence keyspace and the gate-log sink;
|
|
24
|
+
* carrying two multi-branch best-effort wrappers inline alongside that was the
|
|
25
|
+
* mass that made it the file it was. Both collaborators stay injectable — the
|
|
26
|
+
* parent CLI's cache-busted bindings must win in tests that mock the upstream
|
|
27
|
+
* module URLs — and are threaded through from the phase unchanged.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { Logger } from '../../../Logger.js';
|
|
31
|
+
import { runBaselineUpwardWriteback as defaultRunBaselineUpwardWriteback } from '../../story-close/baseline-upward-writeback.js';
|
|
32
|
+
import { runScopedFormatAutofix as defaultRunScopedFormatAutofix } from '../../story-close/format-autofix.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Run the scoped formatter self-heal.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} ctx the shared step context (see {@link runPreGateSteps})
|
|
38
|
+
* @returns {void}
|
|
39
|
+
*/
|
|
40
|
+
function formatAutofixStep({
|
|
41
|
+
cwd,
|
|
42
|
+
worktreePath,
|
|
43
|
+
storyId,
|
|
44
|
+
baseBranch,
|
|
45
|
+
storyBranch,
|
|
46
|
+
config,
|
|
47
|
+
progress,
|
|
48
|
+
runScopedFormatAutofix,
|
|
49
|
+
}) {
|
|
50
|
+
progress(
|
|
51
|
+
'FORMAT',
|
|
52
|
+
`Running scoped format-autofix on ${baseBranch}...${storyBranch}${worktreePath ? ` in ${worktreePath}` : ''}...`,
|
|
53
|
+
);
|
|
54
|
+
const autofix = runScopedFormatAutofix({
|
|
55
|
+
cwd,
|
|
56
|
+
worktreePath,
|
|
57
|
+
storyId,
|
|
58
|
+
baseBranch,
|
|
59
|
+
storyBranch,
|
|
60
|
+
config,
|
|
61
|
+
logger: Logger,
|
|
62
|
+
});
|
|
63
|
+
progress(
|
|
64
|
+
'FORMAT',
|
|
65
|
+
autofix?.committed
|
|
66
|
+
? `✅ Auto-applied format fix committed as ${autofix.sha} on ${storyBranch}.`
|
|
67
|
+
: `⏭ No format-autofix commit (${autofix?.reason ?? 'clean'}).`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run the upward maintainability write-back.
|
|
73
|
+
*
|
|
74
|
+
* @param {object} ctx the shared step context (see {@link runPreGateSteps})
|
|
75
|
+
* @returns {Promise<void>}
|
|
76
|
+
*/
|
|
77
|
+
async function baselineWritebackStep({
|
|
78
|
+
cwd,
|
|
79
|
+
worktreePath,
|
|
80
|
+
storyId,
|
|
81
|
+
baseBranch,
|
|
82
|
+
storyBranch,
|
|
83
|
+
config,
|
|
84
|
+
progress,
|
|
85
|
+
runBaselineUpwardWriteback,
|
|
86
|
+
}) {
|
|
87
|
+
const writeback = await runBaselineUpwardWriteback({
|
|
88
|
+
cwd,
|
|
89
|
+
worktreePath,
|
|
90
|
+
storyId,
|
|
91
|
+
baseBranch,
|
|
92
|
+
storyBranch,
|
|
93
|
+
config,
|
|
94
|
+
logger: Logger,
|
|
95
|
+
});
|
|
96
|
+
progress(
|
|
97
|
+
'BASELINE',
|
|
98
|
+
writeback?.committed
|
|
99
|
+
? `✅ Wrote back ${writeback.improvedPaths?.length ?? 0} improved maintainability row(s) as ${writeback.sha} on ${storyBranch}.`
|
|
100
|
+
: `⏭ No baseline write-back (${writeback?.reason ?? 'nothing to write'}).`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run one step, absorbing any throw into a progress line.
|
|
106
|
+
*
|
|
107
|
+
* The absorption is the point, not laziness about error handling: every step
|
|
108
|
+
* here is the *refresh* half of a loop whose *enforcement* half runs
|
|
109
|
+
* immediately afterwards. A step that could abort the close would convert a
|
|
110
|
+
* self-heal opportunity into an outage, and would do it on the path with the
|
|
111
|
+
* least operator attention.
|
|
112
|
+
*
|
|
113
|
+
* @param {{ tag: string, label: string, progress: Function, run: () => Promise<void>|void }} opts
|
|
114
|
+
* @returns {Promise<void>}
|
|
115
|
+
*/
|
|
116
|
+
async function bestEffort({ tag, label, progress, run }) {
|
|
117
|
+
try {
|
|
118
|
+
await run();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
progress(
|
|
121
|
+
tag,
|
|
122
|
+
`⚠️ ${label} failed (close continues; the gate chain is authoritative): ${err?.message ?? err}`,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Run every pre-gate self-heal step for a standalone Story close.
|
|
129
|
+
*
|
|
130
|
+
* Both steps commit to `story-<id>`, so both are skipped — with a log line, not
|
|
131
|
+
* a throw — when the caller has no `storyBranch`. That is the resume/legacy
|
|
132
|
+
* path, which has no branch to commit onto and must not trip an exception for
|
|
133
|
+
* saying so.
|
|
134
|
+
*
|
|
135
|
+
* @param {{
|
|
136
|
+
* cwd: string,
|
|
137
|
+
* worktreePath: string|null,
|
|
138
|
+
* storyId: number,
|
|
139
|
+
* baseBranch: string,
|
|
140
|
+
* storyBranch?: string,
|
|
141
|
+
* config: object,
|
|
142
|
+
* progress: (tag: string, msg: string) => void,
|
|
143
|
+
* runScopedFormatAutofix?: typeof defaultRunScopedFormatAutofix,
|
|
144
|
+
* runBaselineUpwardWriteback?: typeof defaultRunBaselineUpwardWriteback,
|
|
145
|
+
* }} args
|
|
146
|
+
* @returns {Promise<void>}
|
|
147
|
+
*/
|
|
148
|
+
export async function runPreGateSteps({
|
|
149
|
+
runScopedFormatAutofix = defaultRunScopedFormatAutofix,
|
|
150
|
+
runBaselineUpwardWriteback = defaultRunBaselineUpwardWriteback,
|
|
151
|
+
...ctx
|
|
152
|
+
}) {
|
|
153
|
+
const { storyBranch, progress } = ctx;
|
|
154
|
+
if (!storyBranch) {
|
|
155
|
+
progress('FORMAT', '⏭ Skipped scoped format-autofix (no story branch).');
|
|
156
|
+
progress('BASELINE', '⏭ Skipped baseline write-back (no story branch).');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
await bestEffort({
|
|
160
|
+
tag: 'FORMAT',
|
|
161
|
+
label: 'scoped format-autofix',
|
|
162
|
+
progress,
|
|
163
|
+
run: () => formatAutofixStep({ ...ctx, runScopedFormatAutofix }),
|
|
164
|
+
});
|
|
165
|
+
await bestEffort({
|
|
166
|
+
tag: 'BASELINE',
|
|
167
|
+
label: 'baseline write-back',
|
|
168
|
+
progress,
|
|
169
|
+
run: () => baselineWritebackStep({ ...ctx, runBaselineUpwardWriteback }),
|
|
170
|
+
});
|
|
171
|
+
}
|