diff --git a/.husky/pre-commit b/.husky/pre-commit index dcbd545f..e3781a31 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -80,7 +80,14 @@ node gate-engine/qavis-advisory/cli.mts --gate || qarc=$? # devkit:fallow-advisory # fallow audit — dead-code / duplication / complexity on the changed set; advisory, never blocks. -command -v fallow >/dev/null 2>&1 && fallow audit || true +# DEVKIT_SHIP_BASE_SHA (exported by devkit ship — see commit-with-gate-capture.sh) pins the audit's +# comparison ref to the exact commit the ship worktree was cut from, instead of fallow's own +# main-autodetect: a --base ship off a long-lived/stacked branch would otherwise misreport that +# branch's own pre-existing findings vs main as "new" (DK-5). Unset on a plain `git commit` — fallow +# falls back to its own default. +FALLOW_BASE_ARGS="" +[ -n "${DEVKIT_SHIP_BASE_SHA:-}" ] && FALLOW_BASE_ARGS="--base $DEVKIT_SHIP_BASE_SHA" +command -v fallow >/dev/null 2>&1 && fallow audit $FALLOW_BASE_ARGS || true # /devkit:fallow-advisory # <<< devkit-guards <<< diff --git a/cli/__tests__/husky-block.test.mts b/cli/__tests__/husky-block.test.mts index 1cf35f79..12bf597a 100644 --- a/cli/__tests__/husky-block.test.mts +++ b/cli/__tests__/husky-block.test.mts @@ -315,6 +315,47 @@ describe('buildOverlayHook — gates-only guard for the global init.sh shim', () }); }); +// DK-5: overlay's fallow gate BLOCKS on new findings (unlike the self-host advisory twin), and it +// runs inline (core.hooksPath shadows fallow's own installed hook), so it must see the same +// DEVKIT_SHIP_BASE_SHA scoping — else a --base ship off a stacked branch fails the audit on that +// branch's own pre-existing findings vs main. +describe('buildOverlayHook — fallow gate (overlay)', () => { + const hook = buildOverlayHook({ guards: [...GUARD_IDS] }, '.husky/pre-commit', '', { + fallow: true, + }); + + it('emits the fallow gate scoped by DEVKIT_SHIP_BASE_SHA', () => { + expect(hook).toContain('[ -n "${DEVKIT_SHIP_BASE_SHA:-}" ]'); + expect(hook).toContain('FALLOW_BASE_ARGS="--base $DEVKIT_SHIP_BASE_SHA"'); + expect(hook).toContain('fallow audit $FALLOW_BASE_ARGS || exit 1'); + }); + + it('omits the fallow gate entirely when fallow is not opted in', () => { + const withoutFallow = buildOverlayHook({ guards: [...GUARD_IDS] }, '.husky/pre-commit'); + expect(withoutFallow).not.toContain('fallow audit'); + }); + + it('passes the ship base through to a stubbed fallow (no real binary needed)', () => { + const fragment = hook.match( + /# devkit fallow gate \(overlay\)[\s\S]*?fallow audit \$FALLOW_BASE_ARGS \|\| exit 1; \}/, + )?.[0]; + expect(fragment).toBeDefined(); + const script = `fallow() { echo "FALLOW_ARGS:$*"; }\n${fragment}`; + + const unset = execFileSync('sh', ['-c', script], { + encoding: 'utf8', + env: { PATH: process.env.PATH }, + }); + expect(unset.trim()).toBe('FALLOW_ARGS:audit'); + + const based = execFileSync('sh', ['-c', script], { + encoding: 'utf8', + env: { PATH: process.env.PATH, DEVKIT_SHIP_BASE_SHA: 'deadbeef' }, + }); + expect(based.trim()).toBe('FALLOW_ARGS:audit --base deadbeef'); + }); +}); + describe('extras (--extra hard gates on the deterministic line)', () => { it('emits `--extra "label=cmd"` per extra', () => { const block = buildGuardBlock({ diff --git a/cli/__tests__/overlay.test.mts b/cli/__tests__/overlay.test.mts index 7e5df1b1..3fad7293 100644 --- a/cli/__tests__/overlay.test.mts +++ b/cli/__tests__/overlay.test.mts @@ -87,12 +87,28 @@ function seedLocalSettings(root, obj) { writeFileSync(join(root, '.claude', 'settings.local.json'), JSON.stringify(obj)); } +// Unlike the ship-branch/reship/reconcile suites (which spawn devkit as a SUBPROCESS and isolate git +// via an explicit `env: GENV` per call), this file calls `applyInit`/`overlay.mts` IN-PROCESS — so +// its internal `execFileSync('git', …)` calls (e.g. installHealAlias's `alias.ci` collision check) +// inherit whatever `process.env` already is, not anything this test passes per-call. A developer +// machine with its OWN global `git ci` alias already set makes that check correctly (and +// deliberately — see overlay.mts) skip installing devkit's self-heal alias, which then reads as a +// false failure here. Isolate the whole process for the file's duration, restored after, so the +// suite is deterministic regardless of the host's real ~/.gitconfig. +const ORIGINAL_GIT_CONFIG_GLOBAL = process.env.GIT_CONFIG_GLOBAL; +const ORIGINAL_GIT_CONFIG_SYSTEM = process.env.GIT_CONFIG_SYSTEM; beforeEach(() => { vi.spyOn(console, 'log').mockImplementation(() => {}); + process.env.GIT_CONFIG_GLOBAL = '/dev/null'; + process.env.GIT_CONFIG_SYSTEM = '/dev/null'; }); afterEach(() => { vi.restoreAllMocks(); cleanup(); + if (ORIGINAL_GIT_CONFIG_GLOBAL === undefined) delete process.env.GIT_CONFIG_GLOBAL; + else process.env.GIT_CONFIG_GLOBAL = ORIGINAL_GIT_CONFIG_GLOBAL; + if (ORIGINAL_GIT_CONFIG_SYSTEM === undefined) delete process.env.GIT_CONFIG_SYSTEM; + else process.env.GIT_CONFIG_SYSTEM = ORIGINAL_GIT_CONFIG_SYSTEM; }); describe('overlay (local-only) install', () => { diff --git a/cli/__tests__/reship.test.mts b/cli/__tests__/reship.test.mts index 2009b961..7a38ecc3 100644 --- a/cli/__tests__/reship.test.mts +++ b/cli/__tests__/reship.test.mts @@ -252,6 +252,59 @@ describe('reship — untracked gate configs are linked into the re-ship worktree }); }); +// DK-5: reship's worktree is cut from the fetched PR-branch tip — in-chain gates (fallow) need that +// SAME commit to scope their own audit, not their own main-autodetect. +describe('reship — exports DEVKIT_SHIP_BASE_SHA (DK-5)', () => { + it('is the fetched PR-branch tip, not a stale local ref', () => { + const bare = mkdtempSync(join(tmpdir(), 'reshipbare-')); + dirs.push(bare); + execFileSync('git', ['init', '-q', '--bare', bare], { env: { ...process.env, ...GENV } }); + const dir = mkdtempSync(join(tmpdir(), 'reshipwt-')); + dirs.push(dir); + const env = { ...process.env, ...GENV }; + const g = (a, o = {}) => + execFileSync('git', ['-C', dir, ...a], { env, encoding: 'utf8', ...o }); + mkdirSync(join(dir, '.husky/_'), { recursive: true }); + writeFileSync(join(dir, '.husky/.keep'), ''); + for (const a of [ + ['init', '-q', '-b', 'work'], + ['config', 'user.email', 'a@b.c'], + ['config', 'user.name', 'a'], + ['config', 'commit.gpgsign', 'false'], + ['add', '.husky/.keep'], + ['commit', '-q', '-m', 'base'], + ['config', 'core.hooksPath', '.husky/_'], + ['remote', 'add', 'origin', bare], + ]) + g(a, { stdio: 'ignore' }); + writeFileSync( + join(dir, '.husky/_/pre-commit'), + '#!/bin/sh\necho "HOOK_BASE=$DEVKIT_SHIP_BASE_SHA"\nexit 0\n', + ); + chmodSync(join(dir, '.husky/_/pre-commit'), 0o755); + writeFileSync(join(dir, 'a.ts'), 'v1\n'); + g(['add', 'a.ts'], { stdio: 'ignore' }); + g(['commit', '-q', '-m', 'first'], { stdio: 'ignore' }); + g(['push', '-q', 'origin', 'HEAD:feat/pr'], { stdio: 'ignore' }); + const prTip = execFileSync('git', ['-C', bare, 'rev-parse', 'feat/pr'], { + env, + encoding: 'utf8', + }).trim(); + writeFileSync(join(dir, 'a.ts'), 'v2\n'); + + const r = run(['feat/pr', 'add v2', '--pr', '--', 'a.ts'], dir, { SHIP_DRY_RUN: '1' }); + const wt = WT_RE.exec(r.stderr)?.[1]; + try { + expect(r.status, r.stderr).toBe(0); + expect(readFileSync(join(dir, '.devkit/last-ship-gates-feat-pr.log'), 'utf8')).toContain( + `HOOK_BASE=${prTip}`, + ); + } finally { + if (wt) g(['worktree', 'remove', '--force', wt], { stdio: 'ignore' }); + } + }); +}); + describe('reship — repo path with a space (linked-worktree COMMIT_EDITMSG carries the space)', () => { // A linked-worktree commit hands the commit-msg hook the ABSOLUTE $GIT_DIR/COMMIT_EDITMSG path; under // a spaced repo root that path contains the space. Devkit forwards it as one intact arg (every ship diff --git a/cli/__tests__/self-host.test.mts b/cli/__tests__/self-host.test.mts index 69b5a464..cb3b3043 100644 --- a/cli/__tests__/self-host.test.mts +++ b/cli/__tests__/self-host.test.mts @@ -4,6 +4,7 @@ * If the parity test fails, the hook drifted from the generator: regenerate it (`devkit init` in the * repo, or `devkit doctor --fix`) and re-commit. */ +import { execFileSync } from 'node:child_process'; import { existsSync, readFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -75,7 +76,9 @@ describe('buildSelfHostHook', () => { it('preserves the advisory fallow-audit gate INSIDE the block (never blocks, survives re-run)', () => { const hook = buildSelfHostHook(HOOK_SEL, '', ROOT); - expect(hook).toContain('command -v fallow >/dev/null 2>&1 && fallow audit || true'); + expect(hook).toContain( + 'command -v fallow >/dev/null 2>&1 && fallow audit $FALLOW_BASE_ARGS || true', + ); // Inside the devkit-guards block: after the start marker, before the end marker — so // replaceGuardBlock preserves it on a re-run and the parity/doctor check covers it. expect(hook.indexOf('fallow audit')).toBeGreaterThan(hook.indexOf('>>> devkit-guards')); @@ -83,6 +86,33 @@ describe('buildSelfHostHook', () => { expect(hook.trimEnd().endsWith('exit 0')).toBe(true); }); + // DK-5: a --base ship cuts the gate worktree from a possibly non-main base, so the advisory fallow + // audit must diff against THAT commit (DEVKIT_SHIP_BASE_SHA, exported by ship-branch.sh/reship.sh) + // rather than fallow's own main-autodetect — else a stacked branch's own pre-existing findings + // misreport as "new". No real fallow binary in this sandbox: stub it and assert on the args it sees. + it('scopes the fallow audit to DEVKIT_SHIP_BASE_SHA when a ship exported it', () => { + const hook = buildSelfHostHook(HOOK_SEL, '', ROOT); + expect(hook).toContain('[ -n "${DEVKIT_SHIP_BASE_SHA:-}" ]'); + expect(hook).toContain('FALLOW_BASE_ARGS="--base $DEVKIT_SHIP_BASE_SHA"'); + const fragment = extractGuardBlock(hook, '')?.match( + /# devkit:fallow-advisory[\s\S]*?# \/devkit:fallow-advisory/, + )?.[0]; + expect(fragment).toBeDefined(); + const script = `fallow() { echo "FALLOW_ARGS:$*"; }\n${fragment}`; + + const unset = execFileSync('sh', ['-c', script], { + encoding: 'utf8', + env: { PATH: process.env.PATH }, + }); + expect(unset.trim()).toBe('FALLOW_ARGS:audit'); + + const based = execFileSync('sh', ['-c', script], { + encoding: 'utf8', + env: { PATH: process.env.PATH, DEVKIT_SHIP_BASE_SHA: 'deadbeef' }, + }); + expect(based.trim()).toBe('FALLOW_ARGS:audit --base deadbeef'); + }); + it('is idempotent through replaceGuardBlock — re-applying the block keeps the fallow fragment intact', () => { const fresh = buildSelfHostHook(HOOK_SEL, '', ROOT); const block = buildSelfHostBlock(HOOK_SEL, '', ROOT); diff --git a/cli/__tests__/ship-branch.test.mts b/cli/__tests__/ship-branch.test.mts index 7e18e503..74bd0d08 100644 --- a/cli/__tests__/ship-branch.test.mts +++ b/cli/__tests__/ship-branch.test.mts @@ -155,13 +155,13 @@ function dropWorktree(git, stderr) { * bare path fails its shape check), while the absolute path stays reachable from BOTH ROOT (ls-remote) * and the ephemeral $WT (push) with no network. Drives the real non-dry push + manifest path offline. */ -function seedShipRepoLocalRemote() { +function seedShipRepoLocalRemote({ hookBody } = {}) { const ghRoot = mkdtempSync(join(tmpdir(), 'shipgh-')); dirs.push(ghRoot); const bare = join(ghRoot, 'github.com', 'acme', 'app.git'); mkdirSync(join(ghRoot, 'github.com', 'acme'), { recursive: true }); execFileSync('git', ['init', '-q', '--bare', bare], { env: { ...process.env, ...GIT_ENV } }); - return { ...seedShipRepo({ origin: bare }), bare }; + return { ...seedShipRepo({ origin: bare, ...(hookBody ? { hookBody } : {}) }), bare }; } /** @@ -294,8 +294,8 @@ describe('ship-branch.sh — PR base = the branch we branched from', () => { describe('ship-branch.sh — --base ', () => { /** A repo with an `origin` bare that has a `studio` branch, and a `finalized` branch (checked out) * whose note.txt change is ALREADY COMMITTED — exactly the DK-1 repro state. */ - function seedBaseRepo() { - const seeded = seedShipRepoLocalRemote(); + function seedBaseRepo({ hookBody } = {}) { + const seeded = seedShipRepoLocalRemote({ hookBody }); const { dir, git, bare } = seeded; writeFileSync(join(dir, 'note.txt'), 'studio\n'); git(['add', 'note.txt'], { stdio: 'ignore' }); @@ -412,6 +412,24 @@ describe('ship-branch.sh — --base ', () => { // The --base empty-commit hint ("already identical on origin/", no checkout advice) is // covered in the `empty-commit preflight` describe, beside its default-base twin. + + // DK-5: the worktree is cut from origin's fetched studio tip — in-chain gates (fallow) need that + // SAME commit exported so they scope their own audit against it instead of their own + // main-autodetect, which would misreport studio's own pre-existing findings vs main as "new". + it("exports DEVKIT_SHIP_BASE_SHA = origin's fetched studio tip, not a stale local ref", () => { + const { dir, env, git, studioTip } = seedBaseRepo({ + hookBody: 'echo "HOOK_BASE=$DEVKIT_SHIP_BASE_SHA"', + }); + const r = spawnSync( + '/bin/bash', + [scriptPath, 'feat/base-sha-flag', 't', '--base', 'studio', '--', 'note.txt'], + { cwd: dir, input: 'b\n', encoding: 'utf8', env: { ...env, SHIP_DRY_RUN: '1' } }, + ); + dropWorktree(git, r.stderr); + expect(r.status, r.stderr).toBe(0); + const log = readFileSync(join(dir, '.devkit/last-ship-gates-feat-base-sha-flag.log'), 'utf8'); + expect(log).toContain(`HOOK_BASE=${studioTip}`); + }); }); describe('ship-branch.sh — isolation + arg guards', () => { @@ -861,6 +879,32 @@ describe('ship-branch.sh — worktree integration', () => { expect(r.status, r.stderr).toBe(0); expect(git(['show', '-s', '--format=%b', 'feat/body'])).toMatch(/BODY_INLINE_XYZ/); }); + + // DK-5: the worktree is cut from $BASE (default: this checkout's HEAD; --base: origin's fetched + // tip), and in-chain gates (fallow) need that SAME commit to scope their own audit correctly — + // else a --base ship off a stacked branch misreports that branch's pre-existing findings vs main + // as "new". Assert the exported var matches the commit the worktree was ACTUALLY cut from, not a + // stale local ref. + it("exports DEVKIT_SHIP_BASE_SHA = this checkout's HEAD for a default (no --base) ship", () => { + const { dir, env, git } = seedShipRepo({ + hookBody: 'echo "HOOK_BASE=$DEVKIT_SHIP_BASE_SHA"', + }); + const headSha = git(['rev-parse', 'HEAD']).trim(); + writeFileSync(join(dir, 'note.txt'), 'hi\n'); + const r = spawnSync('/bin/bash', [scriptPath, 'feat/base-sha-default', 't', 'note.txt'], { + cwd: dir, + input: 'b\n', + encoding: 'utf8', + env: { ...env, SHIP_DRY_RUN: '1' }, + }); + dropWorktree(git, r.stderr); + expect(r.status, r.stderr).toBe(0); + const log = readFileSync( + join(dir, '.devkit/last-ship-gates-feat-base-sha-default.log'), + 'utf8', + ); + expect(log).toContain(`HOOK_BASE=${headSha}`); + }); }); // Overlay mode keeps the entire gate chain in a git-ignored .devkit/hooks/pre-commit that never diff --git a/cli/lib/husky/husky-block.mts b/cli/lib/husky/husky-block.mts index 4138a818..6be9608a 100644 --- a/cli/lib/husky/husky-block.mts +++ b/cli/lib/husky/husky-block.mts @@ -285,7 +285,11 @@ fi`; // chaining the gate inline here is the only way the audit runs. `fallow audit` exits non-zero on // NEW issues (pre-existing debt is grandfathered by the saved fallow-baselines/). const FALLOW_OVERLAY_GATE = `# devkit fallow gate (overlay) — fail-open; skipped if fallow isn't installed. -command -v fallow >/dev/null 2>&1 && { fallow audit || exit 1; }`; +# DEVKIT_SHIP_BASE_SHA (set by devkit ship) narrows the audit to the exact ship base rather than +# fallow's own main-autodetect — see self-host.mts's FALLOW_FRAGMENT for the full rationale (DK-5). +FALLOW_BASE_ARGS="" +[ -n "\${DEVKIT_SHIP_BASE_SHA:-}" ] && FALLOW_BASE_ARGS="--base $DEVKIT_SHIP_BASE_SHA" +command -v fallow >/dev/null 2>&1 && { fallow audit $FALLOW_BASE_ARGS || exit 1; }`; /** * Build the OVERLAY hook — a complete, self-contained file devkit fully owns (written to a diff --git a/cli/lib/husky/self-host.mts b/cli/lib/husky/self-host.mts index 7ebb5ba4..c9d84283 100644 --- a/cli/lib/husky/self-host.mts +++ b/cli/lib/husky/self-host.mts @@ -51,7 +51,14 @@ export const SELF_HOST_EXTRAS: Array<{ label: string; cmd: string }> = [ // covers it too. const FALLOW_FRAGMENT = `# devkit:fallow-advisory # fallow audit — dead-code / duplication / complexity on the changed set; advisory, never blocks. -command -v fallow >/dev/null 2>&1 && fallow audit || true +# DEVKIT_SHIP_BASE_SHA (exported by devkit ship — see commit-with-gate-capture.sh) pins the audit's +# comparison ref to the exact commit the ship worktree was cut from, instead of fallow's own +# main-autodetect: a --base ship off a long-lived/stacked branch would otherwise misreport that +# branch's own pre-existing findings vs main as "new" (DK-5). Unset on a plain \`git commit\` — fallow +# falls back to its own default. +FALLOW_BASE_ARGS="" +[ -n "\${DEVKIT_SHIP_BASE_SHA:-}" ] && FALLOW_BASE_ARGS="--base $DEVKIT_SHIP_BASE_SHA" +command -v fallow >/dev/null 2>&1 && fallow audit $FALLOW_BASE_ARGS || true # /devkit:fallow-advisory`; // The hook-builder's view of the self-host selection (Selection + the two hook-only fields the diff --git a/cli/lib/ship/commit-with-gate-capture.sh b/cli/lib/ship/commit-with-gate-capture.sh index be2dbf9a..bba3776c 100644 --- a/cli/lib/ship/commit-with-gate-capture.sh +++ b/cli/lib/ship/commit-with-gate-capture.sh @@ -48,6 +48,13 @@ commit_with_gate_capture() { # outside ship keep their fail-open default. # DEVKIT_REVIEW_PROGRESS where guard-review records {running,completed} reviewer names, so a # timeout can name the ones left unfinished (structured, not stderr prose). + # DEVKIT_SHIP_BASE_SHA the commit the worktree was cut from — NOT exported here, but by this + # function's CALLERS (ship-branch.sh / reship.sh, right beside their own + # DEVKIT_SHIP_MODE export), since they're the ones who resolved $BASE. + # Listed here so this stays the one place to look for every ship-mode env + # var. Consumed by devkit's own fallow-advisory/overlay fragments to scope + # `fallow audit --base` at the real ship base instead of its own + # main-autodetect (DK-5). export DEVKIT_SHIP=1 GUARD_AI_STRICT=1 DEVKIT_REVIEW_PROGRESS="$progress" # Gate telemetry (best-effort, ship-scoped). A shared append-only JSONL sink + one ship_id per diff --git a/cli/lib/ship/reship.sh b/cli/lib/ship/reship.sh index 3212b121..c7a3aada 100644 --- a/cli/lib/ship/reship.sh +++ b/cli/lib/ship/reship.sh @@ -133,6 +133,9 @@ link_untracked_gate_configs "$WT" "$ROOT" # Commit (gates run HERE). Capture + surface the gate output for the shipping agent — git buries it on # the commit's stderr. Shared with new-ship. See commit-with-gate-capture.sh. . "$(dirname "${BASH_SOURCE[0]}")/commit-with-gate-capture.sh" +# The fetched PR-branch tip the worktree was cut from — lets in-chain gates (fallow) diff against IT, +# not their own main-autodetect (DK-5). +export DEVKIT_SHIP_BASE_SHA="$BASE" export DEVKIT_SHIP_MODE=reship # tags the ship_attempt telemetry (retry onto an existing branch) commit_with_gate_capture "$WT" "$ROOT" "$BR" "$TITLE" "$BODY" diff --git a/cli/lib/ship/ship-branch.sh b/cli/lib/ship/ship-branch.sh index 25b3e34b..4c5d356d 100644 --- a/cli/lib/ship/ship-branch.sh +++ b/cli/lib/ship/ship-branch.sh @@ -265,6 +265,10 @@ link_untracked_gate_configs "$WT" "$ROOT" # Commit inside the worktree (hook gates run HERE). Capture + surface the gate output so the shipping # agent reliably sees the verdicts — git buries them on the commit's stderr. See commit-with-gate-capture.sh. . "$(dirname "${BASH_SOURCE[0]}")/commit-with-gate-capture.sh" +# The commit the worktree was cut from — lets in-chain gates (fallow) diff against IT, not their own +# main-autodetect. Unconditional (not just under --base): even the default case is more precise than +# a gate auto-detecting main, for any branch that isn't a fresh cut off main (DK-5). +export DEVKIT_SHIP_BASE_SHA="$BASE" export DEVKIT_SHIP_MODE=ship # tags the ship_attempt telemetry (new-ship vs reship retry) commit_with_gate_capture "$WT" "$ROOT" "$BR" "$TITLE" "$BODY" diff --git a/eslint/baselines/size-lines.json b/eslint/baselines/size-lines.json index 707d70dd..c4725d01 100644 --- a/eslint/baselines/size-lines.json +++ b/eslint/baselines/size-lines.json @@ -14,6 +14,6 @@ "gate-engine/review/eval/bench.mts": 878, "gate-engine/review/eval/conventions/bench.mts": 988, "gate-engine/review/eval/reviewers/bench.mts": 977, - "gate-engine/review/run-review.mts": 623 + "gate-engine/review/run-review.mts": 642 } } diff --git a/gate-engine/review/run-review.mts b/gate-engine/review/run-review.mts index 74674cb4..91fe6b4c 100644 --- a/gate-engine/review/run-review.mts +++ b/gate-engine/review/run-review.mts @@ -555,7 +555,9 @@ export async function runReviewGate( if (res.status === 'pass') // res.model = the model that actually judged (a Reviewer.model pin wins over the cascade // default) — recording firstModel here mislabeled every pinned reviewer's cached PASS. - savePasses(cwd, { [t.key]: { at: new Date().toISOString(), model: res.model ?? firstModel } }); + savePasses(cwd, { + [t.key]: { at: new Date().toISOString(), model: res.model ?? firstModel }, + }); if (progressFile) { completed.push(res.name); writeProgress(progressFile, { running, completed });