@dev-loops/core 1.0.0 → 1.0.2-pre.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/package.json +4 -2
- package/src/claude/hook-decisions.mjs +144 -20
- package/src/config/config.mjs +11 -97
- package/src/config/extension-defaults.yaml +18 -7
- package/src/github/comment-id-guard.mjs +38 -0
- package/src/github/gh.mjs +49 -0
- package/src/loop/commit-msg-guard.mjs +1 -1
- package/src/loop/gate-carry-forward.mjs +45 -21
- package/src/loop/gate-evidence-reconcile.mjs +75 -0
- package/src/loop/gate-fanin.mjs +47 -20
- package/src/loop/handoff-envelope.mjs +2 -2
- package/src/loop/issue-refinement-artifact.mjs +608 -75
- package/src/loop/pr-gate-coordination.mjs +27 -2
- package/src/loop/queue-board-sync.mjs +7 -38
- package/src/loop/review-dispatch-plan.mjs +1 -0
- package/src/loop/spec-authority.mjs +759 -0
- package/src/loop/ui-e2e-scoping.mjs +1 -0
- package/src/loop/worktree-guard.mjs +55 -0
- package/src/projects/list-queue-items.mjs +1 -30
- package/src/projects/move-queue-item.mjs +1 -30
- package/src/projects/resolve-project.mjs +6 -6
- package/src/security/secret-scan.mjs +13 -1
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Gate carry-forward: a pure, fail-closed seam that decides whether
|
|
3
|
-
*
|
|
4
|
-
* re-running that angle's reviewer.
|
|
2
|
+
* Gate carry-forward: a pure, fail-closed seam that decides whether an angle's
|
|
3
|
+
* verdict recorded at head A (clean OR findings-present) may be CARRIED FORWARD
|
|
4
|
+
* to head B without re-running that angle's reviewer.
|
|
5
5
|
*
|
|
6
6
|
* Motivation: fresh-context-per-head re-fans ALL gate angles on every head bump,
|
|
7
7
|
* even when the delta between the two heads provably cannot affect most angles
|
|
8
8
|
* (e.g. a doc-only follow-up commit cannot change what a code-correctness angle
|
|
9
|
-
* would find). Carry-forward lets the gate reuse the prior
|
|
10
|
-
* angles — but ONLY when it is provably safe.
|
|
9
|
+
* would find). Carry-forward lets the gate reuse the prior verdict for such
|
|
10
|
+
* angles — but ONLY when it is provably safe. This holds for a findings-present
|
|
11
|
+
* prior verdict too (issue #2017): a fixer push that never touches an angle's
|
|
12
|
+
* surface must not force that angle's OPEN findings to be re-litigated from
|
|
13
|
+
* scratch — the caller carries the prior findings forward unchanged, still
|
|
14
|
+
* open, still blocking. Carry-forward NEVER converts a finding into an
|
|
15
|
+
* approval; it only ever skips re-running a reviewer whose surface the delta
|
|
16
|
+
* provably did not touch.
|
|
11
17
|
*
|
|
12
|
-
* FAIL-CLOSED is paramount. An angle carries forward ONLY when
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* FAIL-CLOSED is paramount. An angle carries forward ONLY when its prior verdict
|
|
19
|
+
* is carry-forward-eligible (clean or findings_present) AND EVERY changed file in
|
|
20
|
+
* the delta A..B is provably OUTSIDE that angle's declared review surface. The
|
|
21
|
+
* default in every uncertain case (an ineligible prior verdict — e.g. "blocked"
|
|
22
|
+
* or missing, empty/unavailable delta, an unclassifiable file, an angle with no
|
|
23
|
+
* declared surface, a mandatory / always-run angle) is MUST-RE-RUN. Carry-forward
|
|
24
|
+
* never fabricates a verdict: the caller records the carried verdict (and, for a
|
|
25
|
+
* findings-present carry, the carried findings) with provenance pointing at the
|
|
26
|
+
* PRIOR head's reviewer (that reviewer genuinely reviewed this angle's surface,
|
|
27
|
+
* which the delta did not touch), clearly marked as carried — see
|
|
20
28
|
* skills/docs/gate-review-sub-loop-contract.md and write-gate-findings-log.mjs's
|
|
21
29
|
* `carriedFromHead` provenance field.
|
|
22
30
|
*
|
|
@@ -153,10 +161,16 @@ export function angleReviewSurface(angle, { alwaysRerun } = {}) {
|
|
|
153
161
|
/**
|
|
154
162
|
* Pure, deterministic, FAIL-CLOSED carry-forward decision for a single angle.
|
|
155
163
|
*
|
|
156
|
-
* Given a prior
|
|
157
|
-
* A..B, and the angle's
|
|
158
|
-
*
|
|
159
|
-
*
|
|
164
|
+
* Given a prior carry-forward-eligible verdict recorded at head A (clean OR
|
|
165
|
+
* findings_present), the changed files of the delta A..B, and the angle's
|
|
166
|
+
* declared review surface, decide whether that verdict (and, for a
|
|
167
|
+
* findings-present angle, its open findings) may be carried forward to head B
|
|
168
|
+
* (carryForward: true) or the angle MUST re-run (carryForward: false).
|
|
169
|
+
* Defaults to must-re-run in every uncertain case. This function never
|
|
170
|
+
* inspects or mutates findings content — it only decides whether the delta
|
|
171
|
+
* proves the angle's surface untouched; the caller is responsible for
|
|
172
|
+
* carrying the actual prior findings through unchanged (never converting an
|
|
173
|
+
* open finding into an approval) when it honors `carryForward: true`.
|
|
160
174
|
*
|
|
161
175
|
* @param {object} input
|
|
162
176
|
* @param {string} input.angle
|
|
@@ -164,11 +178,18 @@ export function angleReviewSurface(angle, { alwaysRerun } = {}) {
|
|
|
164
178
|
* derived from {@link angleReviewSurface} when omitted.
|
|
165
179
|
* @param {string[]} input.changedFiles — repo-relative paths changed between head
|
|
166
180
|
* A and head B (the delta, NOT the full PR diff against base).
|
|
167
|
-
* @param {string} input.prevVerdict — the angle's verdict at head A.
|
|
168
|
-
*
|
|
181
|
+
* @param {string} input.prevVerdict — the angle's verdict at head A. "clean" and
|
|
182
|
+
* "findings_present" are carry-forward-eligible; anything else (e.g.
|
|
183
|
+
* "blocked", missing) is not.
|
|
169
184
|
* @returns {{ carryForward: boolean, reason: string }}
|
|
170
185
|
*/
|
|
171
186
|
|
|
187
|
+
// The only per-angle prior verdicts eligible to carry forward — matches the
|
|
188
|
+
// two verdict values gate-fanin's VALID_VERDICTS actually produces per angle
|
|
189
|
+
// (packages/core/src/loop/gate-fanin.mjs). Any other value (e.g. "blocked",
|
|
190
|
+
// undefined, a typo) fails closed to must-re-run.
|
|
191
|
+
const CARRY_FORWARD_ELIGIBLE_VERDICTS = new Set(["clean", "findings_present"]);
|
|
192
|
+
|
|
172
193
|
// A path whose change rewrites the dev-loop review system itself — the angle
|
|
173
194
|
// pool, mandatory floor, and reviewer personas/prompts — rather than a
|
|
174
195
|
// reviewed surface. A clean verdict produced under the OLD config cannot
|
|
@@ -187,8 +208,11 @@ export function isDevLoopConfigSourcePath(filePath) {
|
|
|
187
208
|
}
|
|
188
209
|
|
|
189
210
|
export function resolveAngleCarryForward({ angle, angleSurface, changedFiles, prevVerdict }) {
|
|
190
|
-
if (prevVerdict
|
|
191
|
-
return {
|
|
211
|
+
if (!CARRY_FORWARD_ELIGIBLE_VERDICTS.has(prevVerdict)) {
|
|
212
|
+
return {
|
|
213
|
+
carryForward: false,
|
|
214
|
+
reason: `prior verdict is ${JSON.stringify(prevVerdict ?? null)}, not carry-forward-eligible (clean or findings_present)`,
|
|
215
|
+
};
|
|
192
216
|
}
|
|
193
217
|
const surface = angleSurface ?? angleReviewSurface(angle);
|
|
194
218
|
if (surface.kind === "always") {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gate-evidence-reconcile.mjs — deterministic self-heal for a stuck
|
|
3
|
+
* `gate-evidence` required status (issue #1935).
|
|
4
|
+
*
|
|
5
|
+
* The server-side `gate-evidence` check (`.github/workflows/gate-evidence.yml`)
|
|
6
|
+
* re-fires when a gate verdict is posted (ADR 0043). That native re-fire is
|
|
7
|
+
* racy: a verdict-post run can be CANCELLED by `cancel-in-progress` when a
|
|
8
|
+
* superseding event lands, or evaluate before the just-posted verdict is
|
|
9
|
+
* API-visible, leaving the required status stuck at `failure` even though a
|
|
10
|
+
* clean current-head `pre_approval_gate` verdict now exists. Nothing re-fires
|
|
11
|
+
* afterward, so the merge stays `UNSTABLE` until a manual `gh run rerun`
|
|
12
|
+
* (observed on PR #1934; ADR 0057).
|
|
13
|
+
*
|
|
14
|
+
* This pure decision separates the two cases the reconcile must never confuse:
|
|
15
|
+
* - evidence genuinely satisfied but the status is stuck non-green → re-fire
|
|
16
|
+
* the concrete run that posted the stale status (automating the manual
|
|
17
|
+
* rerun; the rerun re-evaluates LIVE evidence, which is now satisfied).
|
|
18
|
+
* - evidence genuinely NOT satisfied → do nothing. A head that truly lacks a
|
|
19
|
+
* clean current-head verdict MUST keep failing the check (fail-closed).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** Required commit-status context posted by the gate-evidence workflow. */
|
|
23
|
+
export const GATE_EVIDENCE_STATUS_CONTEXT = "gate-evidence";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Extract the Actions run id from a gate-evidence commit-status `target_url`.
|
|
27
|
+
* The workflow points every posted status at its own run:
|
|
28
|
+
* https://github.com/<owner>/<repo>/actions/runs/<run_id>
|
|
29
|
+
* Returns the numeric run id as a string, or null when the URL is absent or
|
|
30
|
+
* not an Actions-run URL (an unexpected target_url must not be coerced).
|
|
31
|
+
*
|
|
32
|
+
* @param {string} [targetUrl]
|
|
33
|
+
* @returns {string|null}
|
|
34
|
+
*/
|
|
35
|
+
export function parseRunIdFromTargetUrl(targetUrl) {
|
|
36
|
+
if (typeof targetUrl !== "string") return null;
|
|
37
|
+
const match = targetUrl.match(/\/actions\/runs\/(\d+)(?:[/?#]|$)/);
|
|
38
|
+
return match ? match[1] : null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Decide whether a stuck `gate-evidence` status should be re-fired.
|
|
43
|
+
*
|
|
44
|
+
* @param {object} input
|
|
45
|
+
* @param {boolean} input.evidenceSatisfied detect-checkpoint-evidence reports
|
|
46
|
+
* `evidenceState === "satisfied"` for the current head (clean draft_gate +
|
|
47
|
+
* current-head pre_approval_gate verdicts present).
|
|
48
|
+
* @param {string} [input.statusState] the `gate-evidence` commit-status state
|
|
49
|
+
* on the current head: `success` | `failure` | `error` | `pending` | `none`
|
|
50
|
+
* (`none` = no gate-evidence status posted for this head yet).
|
|
51
|
+
* @param {string|null} [input.runId] the Actions run id that posted the stale
|
|
52
|
+
* status (from `parseRunIdFromTargetUrl`), or null when unknown.
|
|
53
|
+
* @returns {{ action: "refire"|"none", runId?: string, reason: string }}
|
|
54
|
+
*/
|
|
55
|
+
export function resolveGateEvidenceStatusReconcile({ evidenceSatisfied, statusState, runId } = {}) {
|
|
56
|
+
// Fail-closed: never re-fire when the verdict evidence is genuinely not
|
|
57
|
+
// satisfied. This preserves the "verdict genuinely missing" case — the head
|
|
58
|
+
// keeps failing closed exactly as before (issue #1935 AC #3).
|
|
59
|
+
if (evidenceSatisfied !== true) {
|
|
60
|
+
return { action: "none", reason: "evidence-not-satisfied-fail-closed" };
|
|
61
|
+
}
|
|
62
|
+
// Already green — nothing to reconcile.
|
|
63
|
+
if (statusState === "success") {
|
|
64
|
+
return { action: "none", reason: "already-success" };
|
|
65
|
+
}
|
|
66
|
+
// Evidence IS satisfied for the current head, but the required status is not
|
|
67
|
+
// success (the push-before-verdict race: a cancelled/stale re-fire). Re-fire
|
|
68
|
+
// the concrete run that posted the stale status. Without a run id there is
|
|
69
|
+
// nothing to re-fire deterministically; leave it to the native path rather
|
|
70
|
+
// than forging a status.
|
|
71
|
+
if (!runId) {
|
|
72
|
+
return { action: "none", reason: "no-run-to-refire" };
|
|
73
|
+
}
|
|
74
|
+
return { action: "refire", runId, reason: "evidence-satisfied-status-stale" };
|
|
75
|
+
}
|
package/src/loop/gate-fanin.mjs
CHANGED
|
@@ -63,13 +63,19 @@ export function scheduleFanoutWaves(dispatchGroups, maxConcurrent = 4) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
/**
|
|
66
|
-
* Adaptive
|
|
67
|
-
* escalating to foreground one-at-a-time fallback.
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
66
|
+
* Adaptive concurrency backoff (issue #1601; retry discipline refined by #1907):
|
|
67
|
+
* halve the active batch before escalating to foreground one-at-a-time fallback.
|
|
68
|
+
* A transient dispatch failure (429/5xx) is first retried on the SAME unit with
|
|
69
|
+
* exponential backoff — safe because a reviewer's findings artifact is an
|
|
70
|
+
* idempotent single-write at a deterministic path — and the conductor reduces
|
|
71
|
+
* concurrency ONLY after that unit's retries are exhausted (~3 failed attempts),
|
|
72
|
+
* recomputing the wave plan with `backoffMaxConcurrent(maxConcurrent)` and
|
|
73
|
+
* retrying the reduced wave; if a single-unit wave still fails, it falls back to
|
|
74
|
+
* foreground (one-at-a-time) dispatch. This "retry the unit before reducing
|
|
75
|
+
* concurrency" ordering is owned by GATE-EXEC-DISPATCH-RETRY-BACKOFF in
|
|
76
|
+
* skills/docs/gate-review-sub-loop-contract.md; the backoff is recorded in the
|
|
77
|
+
* round's provenance. Pure; never returns 0 (a backoff from 1 stays 1 →
|
|
78
|
+
* foreground fallback owns that path).
|
|
73
79
|
* @param {number} maxConcurrent
|
|
74
80
|
* @returns {number}
|
|
75
81
|
*/
|
|
@@ -281,8 +287,8 @@ export function severityRank(severity) {
|
|
|
281
287
|
/**
|
|
282
288
|
* A zero-initialized severity→count map, one key per SEVERITY_ORDER entry, in
|
|
283
289
|
* SEVERITY_ORDER's order. The shared starting point every severity tally in
|
|
284
|
-
* this codebase (consolidateFanin's own `bySeverity`,
|
|
285
|
-
*
|
|
290
|
+
* this codebase (consolidateFanin's own `bySeverity`,
|
|
291
|
+
* reconcile-draft-gate.mjs's no-findings placeholder) used
|
|
286
292
|
* to hand-roll separately via `Object.fromEntries(SEVERITY_ORDER.map((s) =>
|
|
287
293
|
* [s, 0]))` — one copy here means a severity added to SEVERITY_ORDER is
|
|
288
294
|
* zero-initialized everywhere at once.
|
|
@@ -299,10 +305,9 @@ export function zeroSeverityCounts() {
|
|
|
299
305
|
* finding whose normalized severity is not a recognized SEVERITY_ORDER member
|
|
300
306
|
* is silently excluded from the tally rather than inflating an unknown key —
|
|
301
307
|
* every routed call site here counts already-validated findings in practice
|
|
302
|
-
* (consolidateFanin validates every result's severity before this runs
|
|
303
|
-
*
|
|
304
|
-
*
|
|
305
|
-
* unvalidated severities. `findings` and its entries are NOT nullish-tolerant:
|
|
308
|
+
* (consolidateFanin validates every result's severity before this runs), so
|
|
309
|
+
* this guard is a defensive floor against future drift, not an escape hatch
|
|
310
|
+
* for accepting unvalidated severities. `findings` and its entries are NOT nullish-tolerant:
|
|
306
311
|
* a nullish `findings` argument throws (not iterable), and a nullish
|
|
307
312
|
* individual entry throws reading `.severity` — no routed caller passes
|
|
308
313
|
* either shape, so a caller that does gets a loud failure instead of a
|
|
@@ -336,6 +341,30 @@ export function normalizeSeverityCounts(counts) {
|
|
|
336
341
|
return normalized;
|
|
337
342
|
}
|
|
338
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Resolve a finding's effective file path from EITHER shape the shared floor
|
|
346
|
+
* accepts: `file` (singular string, hand-authored / future-producer ledgers) or
|
|
347
|
+
* `files[0]` (the array shape consolidate-fanin / write-gate-findings-log emit).
|
|
348
|
+
* The ONE resolver `hasLocatableShape` and every downstream consumer keys on,
|
|
349
|
+
* so a consumer reading `finding.files[0]` directly can never crash on a
|
|
350
|
+
* singular-`file` finding that already passed the floor. The value is TRIMMED
|
|
351
|
+
* and a whitespace-only `file` is treated as ABSENT (it falls back to
|
|
352
|
+
* `files[0]`, not shadows it), so the resolved path both matches the trimmed
|
|
353
|
+
* commentable-line-set keys and is a valid GitHub review-comment `path` even
|
|
354
|
+
* for an untrimmed hand-authored ledger entry (`readGateFindingsLedger` trims
|
|
355
|
+
* `files[]` but not a singular `file`). Returns `undefined` when neither shape
|
|
356
|
+
* names a non-blank string path.
|
|
357
|
+
* @param {{ file?: unknown, files?: unknown }} finding
|
|
358
|
+
* @returns {string|undefined}
|
|
359
|
+
*/
|
|
360
|
+
export function resolveFindingFile(finding) {
|
|
361
|
+
if (typeof finding?.file === "string" && finding.file.trim().length > 0) return finding.file.trim();
|
|
362
|
+
if (Array.isArray(finding?.files) && typeof finding.files[0] === "string" && finding.files[0].trim().length > 0) {
|
|
363
|
+
return finding.files[0].trim();
|
|
364
|
+
}
|
|
365
|
+
return undefined;
|
|
366
|
+
}
|
|
367
|
+
|
|
339
368
|
/**
|
|
340
369
|
* A finding is LOCATABLE-SHAPED when it names a real file (via `file` or
|
|
341
370
|
* `files[0]`) and a positive-integer `line` — the ONE shared shape check
|
|
@@ -352,9 +381,7 @@ export function normalizeSeverityCounts(counts) {
|
|
|
352
381
|
* @returns {boolean}
|
|
353
382
|
*/
|
|
354
383
|
export function hasLocatableShape(finding) {
|
|
355
|
-
const file =
|
|
356
|
-
? finding.file
|
|
357
|
-
: (Array.isArray(finding?.files) ? finding.files[0] : undefined);
|
|
384
|
+
const file = resolveFindingFile(finding);
|
|
358
385
|
return typeof file === "string" && file.trim().length > 0
|
|
359
386
|
&& Number.isInteger(finding?.line) && /** @type {number} */ (finding.line) >= 1;
|
|
360
387
|
}
|
|
@@ -690,7 +717,7 @@ export function fanoutReviewerPairingError(perAngle, resolvedGroups = null) {
|
|
|
690
717
|
|
|
691
718
|
/**
|
|
692
719
|
* Base angle name for a delta-suffixed re-review entry (`<angle>-delta-at-...`,
|
|
693
|
-
* e.g. `pr-checklist-
|
|
720
|
+
* e.g. `pr-checklist-delta-at-current-head`): a re-review scoped to only
|
|
694
721
|
* the current head's delta still counts toward its base angle for both
|
|
695
722
|
* mandatory-angle coverage and pool-membership checks.
|
|
696
723
|
*
|
|
@@ -736,12 +763,12 @@ export function checkFanoutAngleCoverage(recordedAngles, { mandatoryAngles = [],
|
|
|
736
763
|
|
|
737
764
|
/**
|
|
738
765
|
* Angles the fan-in itself mandates and may synthesize (consolidate-fanin's
|
|
739
|
-
* `--pr-checklist
|
|
766
|
+
* `--pr-checklist clean` upsert) without them appearing in any gate's
|
|
740
767
|
* configured `angles` pool. Always legal in the foreign-angle check above —
|
|
741
768
|
* requiring every consumer repo to also list them per-gate would make the two
|
|
742
769
|
* tools contradict the shared contract they implement.
|
|
743
770
|
*/
|
|
744
|
-
export const FANIN_SYNTHETIC_ANGLES = Object.freeze(["pr-checklist
|
|
771
|
+
export const FANIN_SYNTHETIC_ANGLES = Object.freeze(["pr-checklist"]);
|
|
745
772
|
|
|
746
773
|
/**
|
|
747
774
|
* Validate a round's RESOLVED angle set — the full angle list the round
|
|
@@ -807,7 +834,7 @@ export function checkResolvedAngleEvidence(resolvedAngles, { recordedAngles, car
|
|
|
807
834
|
export const DEFAULT_MAX_FANOUT_REVIEWERS = 8;
|
|
808
835
|
|
|
809
836
|
// Every sanctioned angle name is a short, hand-authored slug (e.g.
|
|
810
|
-
// "contradiction-lens", "pr-checklist
|
|
837
|
+
// "contradiction-lens", "pr-checklist"); nothing legitimate ever
|
|
811
838
|
// approaches this length. Bounding it here, at the trust boundary this
|
|
812
839
|
// function already owns, fails a pathological artifact closed as malformed —
|
|
813
840
|
// the same place every other angle-result defect is caught — instead of
|
|
@@ -123,7 +123,7 @@ register(INTERNAL_DEV_LOOP_STRATEGY.FINAL_APPROVAL, "default", {
|
|
|
123
123
|
register(INTERNAL_DEV_LOOP_STRATEGY.LOCAL_IMPLEMENTATION, "default", {
|
|
124
124
|
criteria: [
|
|
125
125
|
{ id: "phase-ac", must: "All phase acceptance criteria from the active phase doc are satisfied.", severity: "required" },
|
|
126
|
-
{ id: "verify-green", must: "`
|
|
126
|
+
{ id: "verify-green", must: "`bun run verify` passes with no failures.", severity: "required" },
|
|
127
127
|
],
|
|
128
128
|
evidence: ["commands-run", "validation-output", "changed-files"],
|
|
129
129
|
maxFinalizationTurns: 6,
|
|
@@ -138,7 +138,7 @@ register(INTERNAL_DEV_LOOP_STRATEGY.LOCAL_IMPLEMENTATION, "default", {
|
|
|
138
138
|
register(INTERNAL_DEV_LOOP_STRATEGY.LOCAL_IMPLEMENTATION, "spike", {
|
|
139
139
|
criteria: [
|
|
140
140
|
{ id: "spike-recorded", must: "The spike exploration and its recommendation are recorded (spike file + summary).", severity: "required" },
|
|
141
|
-
{ id: "verify-green", must: "`
|
|
141
|
+
{ id: "verify-green", must: "`bun run verify` passes with no failures.", severity: "required" },
|
|
142
142
|
],
|
|
143
143
|
evidence: ["commands-run", "validation-output", "changed-files"],
|
|
144
144
|
maxFinalizationTurns: 6,
|