diff --git a/.changeset/kind-bears-march.md b/.changeset/kind-bears-march.md new file mode 100644 index 000000000..19a9ad52d --- /dev/null +++ b/.changeset/kind-bears-march.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4868 +--- +**Harness-worktree waves no longer degrade to sequential on a stale origin/HEAD when the fork base is confirmed from a prior worktree** — the worktree base-check now observes the harness's actual fork behavior from a clean prior harness worktree at the orchestrator HEAD before degrading; every unobservable case still degrades exactly as before. (#4588) diff --git a/src/worktree-base-ref.cts b/src/worktree-base-ref.cts index e77ab5eb7..527a81f87 100644 --- a/src/worktree-base-ref.cts +++ b/src/worktree-base-ref.cts @@ -399,7 +399,7 @@ export function cmdWorktreeSetBaseRef( export function classifyGitHead(deps?: { execGit?: ExecGitFn; cwd?: string; -}): { status: 'present'; headSha: string } | { status: 'definitive-absence' | 'ambiguous-absence' | 'indeterminate'; headSha: null } { +}): { status: 'definitive-absence'; headSha: null } | { status: 'ambiguous-absence'; headSha: null } | { status: 'indeterminate'; headSha: null } | { status: 'present'; headSha: string } { const execGit: ExecGitFn = deps?.execGit ?? execGitSeam; const cwdOpts = deps?.cwd ? { cwd: deps.cwd } : {}; const headResult = execGit(['rev-parse', 'HEAD'], cwdOpts); @@ -419,6 +419,110 @@ export function classifyGitHead(deps?: { return { status: 'present', headSha: headStdout }; } +/** + * #4588 (decision A2) — observe, from documented git metadata, whether the + * harness forks its worktrees from the orchestrator HEAD. + * + * Substrate: the harness's own prior worktrees. A linked worktree under + * `/.claude/worktrees/agent-*` that is still CLEAN (no tracked + * modifications; untracked review notes are fine) and whose HEAD equals the + * current orchestrator HEAD can only exist if the harness forked from HEAD + * after that commit was made — an origin/HEAD fork would have landed on an + * older commit once the orchestrator advanced. That combination is therefore + * POSITIVE evidence of fork-from-HEAD, and it is the only configuration that + * counts: every other observation (dirty worktree, different HEAD, no + * worktrees, git failure) is inconclusive and fails closed to the caller's + * existing flow. + * + * The verdict is cached at `/.gsd/harness-fork-probe.json` keyed by the + * orchestrator HEAD (the decision's keying): trusted only while the + * orchestrator HEAD is unchanged; any HEAD move re-probes. Cache I/O failures + * are swallowed — the probe re-runs instead (#3659 fail-closed posture). + */ +export function observeHarnessForkFromHead(deps: { + execGit?: ExecGitFn; + cwd?: string; + headSha: string; + stateRead?: (file: string) => string | null; + stateWrite?: (file: string, content: string) => void; +}): { confirmed: boolean; source: 'cache' | 'probe' | 'none'; worktreePath: string | null; worktreeHead: string | null } { + const execGit = deps.execGit ?? execGitSeam; + const cwd = deps.cwd ?? '.'; + const cacheFile = path.join(cwd, '.gsd', 'harness-fork-probe.json'); + const stateRead = deps.stateRead ?? ((file: string) => { + try { + return fs.readFileSync(file, 'utf8'); + } catch { + return null; + } + }); + const stateWrite = deps.stateWrite ?? ((file: string, content: string) => { + try { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, content, 'utf8'); + } catch { + // A cache write must never break the check that produced the verdict. + } + }); + + // 1. Cache — trusted only while the orchestrator HEAD is unchanged. + try { + const raw = stateRead(cacheFile); + if (raw) { + const cached = JSON.parse(raw) as { headSha?: string; verdict?: string; worktreePath?: string | null; worktreeHead?: string | null }; + if (cached.headSha === deps.headSha && cached.verdict === 'fork-from-head-confirmed') { + return { confirmed: true, source: 'cache', worktreePath: cached.worktreePath ?? null, worktreeHead: cached.worktreeHead ?? null }; + } + } + } catch { + // Corrupt cache — re-probe (fail open INTO the probe, which itself fails closed). + } + + // 2. Probe: list linked worktrees, keep the harness's own, require a clean + // one whose HEAD equals the orchestrator HEAD. ANY failure — timeout, + // non-zero exit, or a throwing execGit — is an inconclusive observation, + // never a crash: the caller falls through to its existing flow (#4588). + try { + const list = execGit(['worktree', 'list', '--porcelain'], { cwd }); + if (isExecGitTimeout(list) || list.exitCode !== 0) { + return { confirmed: false, source: 'none', worktreePath: null, worktreeHead: null }; + } + const candidates = String(list.stdout || '') + .split('\n\n') + .map((block) => block.split('\n').find((l) => l.startsWith('worktree '))?.slice('worktree '.length).trim()) + .filter((p): p is string => !!p && p.includes('/.claude/worktrees/agent-')); + for (const wtPath of candidates) { + const status = execGit(['-C', wtPath, 'status', '--porcelain'], { cwd }); + if (isExecGitTimeout(status) || status.exitCode !== 0) continue; + const trackedDirty = String(status.stdout || '') + .split('\n') + .some((l) => l.trim() !== '' && !l.startsWith('?? ')); + if (trackedDirty) continue; + const wtHeadResult = execGit(['-C', wtPath, 'rev-parse', 'HEAD'], { cwd }); + if (isExecGitTimeout(wtHeadResult) || wtHeadResult.exitCode !== 0) continue; + const wtHead = wtHeadResult.stdout ? wtHeadResult.stdout.trim() : ''; + if (wtHead && wtHead === deps.headSha) { + try { + stateWrite(cacheFile, `${JSON.stringify({ + headSha: deps.headSha, + worktreePath: wtPath, + worktreeHead: wtHead, + verdict: 'fork-from-head-confirmed', + probedAt: new Date().toISOString(), + })}\n`); + } catch { + // Cache write is best-effort; the verdict stands for this dispatch. + } + return { confirmed: true, source: 'probe', worktreePath: wtPath, worktreeHead: wtHead }; + } + } + } catch { + // A throwing execGit (unexpected stub shape, seam surprise) is an + // inconclusive observation, not a crash — the #3659 comparison governs. + } + return { confirmed: false, source: 'none', worktreePath: null, worktreeHead: null }; +} + /** * Evaluates whether the current worktree HEAD has diverged from the fork base * (origin/HEAD) that the Claude Code harness would use when creating a 'fresh' @@ -440,6 +544,14 @@ export function evaluateWorktreeBaseDegrade(deps?: { * 'head' is honored by construction and still suppresses. */ isolationMode?: BaseCheckIsolationMode; + /** + * #4588 (decision A2) cache I/O for the observed fork-from-HEAD verdict. + * Defaults read/write `/.gsd/harness-fork-probe.json`; inject for tests. + * The cache is keyed by the orchestrator HEAD and is trusted only while that + * HEAD is unchanged. + */ + probeStateRead?: (file: string) => string | null; + probeStateWrite?: (file: string, content: string) => void; }): { shouldDegrade: boolean; reason: string; @@ -507,6 +619,37 @@ export function evaluateWorktreeBaseDegrade(deps?: { } const headSha = head.headSha; + // b2. #4588 (decision A2): OBSERVED fork-from-HEAD confirmation. A clean + // prior harness worktree sitting exactly at the orchestrator HEAD is + // positive evidence the harness forks from HEAD — in harness mode that + // supersedes the origin/HEAD comparison for this dispatch (the stale + // origin/HEAD the comparison would degrade on is not where the harness + // forks). Fail-closed: every non-confirming observation falls through to + // the exact pre-#4588 flow below. The mode gate below (not branch a) + // excludes orchestrator-worktree mode — branch a only returns when + // worktree.baseRef:"head" is set — and probeStateRead/Write default to the + // .gsd cache file under cwd. + if ((deps?.isolationMode ?? 'harness-worktree') === 'harness-worktree') { + const observed = observeHarnessForkFromHead({ + execGit, + cwd: deps?.cwd, + headSha, + stateRead: deps?.probeStateRead, + stateWrite: deps?.probeStateWrite, + }); + if (observed.confirmed) { + return { + shouldDegrade: false, + reason: 'fork-from-head-observed', + message: null, + headSha, + forkRef: null, + forkSha: null, + headAbsenceVerified: null, + }; + } + } + // c. Resolve fork base (what the harness forks 'fresh' worktrees from = origin/HEAD). let forkRef: string | null = null; let forkSha: string | null = null; diff --git a/tests/worktree-base-ref.test.cjs b/tests/worktree-base-ref.test.cjs index 5fccfee7f..6b71f4c63 100644 --- a/tests/worktree-base-ref.test.cjs +++ b/tests/worktree-base-ref.test.cjs @@ -18,6 +18,8 @@ const fs = require('node:fs'); const path = require('node:path'); const { makeFaultyGit } = require('./helpers/faulty-deps.cjs'); +const { cleanup } = require('./helpers.cjs'); +const { GIT_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); const MODULE_PATH = path.join( __dirname, '..', 'gsd-core', 'bin', 'lib', 'worktree-base-ref.cjs' @@ -518,8 +520,8 @@ describe('evaluateWorktreeBaseDegrade', () => { 'rev-parse --verify --quiet origin/HEAD': { exitCode: 0, stdout: FORK_SHA, stderr: '', signal: null, error: null }, }), }); - assert.strictEqual(result.shouldDegrade, true); - assert.strictEqual(result.reason, 'head-diverged-from-fork'); + assert.strictEqual(result.shouldDegrade, true, `reason=${result.reason}`); + assert.strictEqual(result.reason, 'head-diverged-from-fork', `reason=${result.reason}`); assert.strictEqual(result.headSha, HEAD_SHA); assert.strictEqual(result.forkRef, 'origin/HEAD'); assert.strictEqual(result.forkSha, FORK_SHA); @@ -544,8 +546,8 @@ describe('evaluateWorktreeBaseDegrade', () => { assert.strictEqual(result.forkRef, 'origin/next'); assert.strictEqual(result.forkSha, FORK_SHA); // HEAD != FORK_SHA in this fixture → degrade - assert.strictEqual(result.shouldDegrade, true); - assert.strictEqual(result.reason, 'head-diverged-from-fork'); + assert.strictEqual(result.shouldDegrade, true, `reason=${result.reason}`); + assert.strictEqual(result.reason, 'head-diverged-from-fork', `reason=${result.reason}`); assert.ok(result.message !== null); assert.ok(result.message.includes('origin/next')); }); @@ -1020,8 +1022,8 @@ describe('evaluateWorktreeBaseDegrade — defensive trim on SHAs (FIX 3)', () => 'rev-parse --verify --quiet origin/HEAD': { exitCode: 0, stdout: FORK_SHA + '\r\n', stderr: '', signal: null, error: null }, }), }); - assert.strictEqual(result.shouldDegrade, true); - assert.strictEqual(result.reason, 'head-diverged-from-fork'); + assert.strictEqual(result.shouldDegrade, true, `reason=${result.reason}`); + assert.strictEqual(result.reason, 'head-diverged-from-fork', `reason=${result.reason}`); // After trimming, headSha and forkSha should be clean assert.strictEqual(result.headSha, HEAD_SHA); assert.strictEqual(result.forkSha, FORK_SHA); @@ -1473,3 +1475,299 @@ describe('#4734: classifyGitHead — single owner of the HEAD-resolution classes assert.strictEqual(status.headSha, null); }); }); + +// ─── #4588 A2: observed fork-from-HEAD confirmation (probe + cache, fail-closed) ── + +describe('#4588 A2: a clean prior harness worktree at the orchestrator HEAD confirms fork-from-HEAD', () => { + const HEAD_SHA = 'aabbccdd11223344aabbccdd11223344aabbccdd'; + const WT_PATH = '/repo/.claude/worktrees/agent-x'; + const ORIGIN_SHA = 'eeee1111223344abeeceeee1111223344abeeceee'; + + // Worktree-listing stub: one harness worktree at WT_PATH; origin/HEAD DIVERGED + // from HEAD so the pre-#4588 flow would degrade (head-diverged-from-fork). + function makeWorktreeExecGit({ clean = true, wtHead = HEAD_SHA, listTimeout = false } = {}) { + return function stubExecGit(args, _opts) { + const key = args.join(' '); + if (key === 'worktree list --porcelain') { + if (listTimeout) { + const err = new Error('spawnSync git ETIMEDOUT'); + err.code = 'ETIMEDOUT'; + return { exitCode: null, stdout: '', stderr: '', signal: 'SIGTERM', error: err }; + } + return { + exitCode: 0, + stdout: `worktree /repo\nbranch refs/heads/main\n\nworktree ${WT_PATH}\nbranch refs/heads/agent-x\n`, + stderr: '', signal: null, error: null, + }; + } + if (key === `-C ${WT_PATH} status --porcelain`) { + return { exitCode: 0, stdout: clean ? '' : ' M tracked.txt', stderr: '', signal: null, error: null }; + } + if (key === `-C ${WT_PATH} rev-parse HEAD`) { + return { exitCode: 0, stdout: `${wtHead}\n`, stderr: '', signal: null, error: null }; + } + if (key === 'rev-parse HEAD') { + return { exitCode: 0, stdout: `${HEAD_SHA}\n`, stderr: '', signal: null, error: null }; + } + if (key === 'rev-parse --verify --quiet origin/HEAD') { + return { exitCode: 0, stdout: `${ORIGIN_SHA}\n`, stderr: '', signal: null, error: null }; + } + if (key === 'symbolic-ref --quiet refs/remotes/origin/HEAD') { + return { exitCode: 1, stdout: '', stderr: '', signal: null, error: null }; + } + throw new Error(`Unexpected execGit call: ${JSON.stringify(args)}`); + }; + } + + test('a clean prior harness worktree at the orchestrator HEAD confirms fork-from-HEAD (no degrade)', () => { + // DIVERGED origin/HEAD: the #3659 flow degrades here unless the probe + // observes that the harness forks from HEAD. All state I/O is in-memory — + // the rows in this describe must stay hermetic (a default fs cache under + // the stub cwd is shared state across tests, and a real fs write at a + // root-level stub path pollutes root CI machines). + let cache = null; + const result = evaluateWorktreeBaseDegrade({ + execGit: makeWorktreeExecGit(), + cwd: '/repo', + probeStateRead: () => cache, + probeStateWrite: (_file, content) => { cache = content; }, + }); + assert.strictEqual(result.shouldDegrade, false, 'a clean worktree at the orchestrator HEAD is positive evidence of fork-from-HEAD'); + assert.strictEqual(result.reason, 'fork-from-head-observed'); + assert.strictEqual(result.headSha, HEAD_SHA); + }); + + test('the probe cache serves a matching verdict without re-probing', () => { + const stateRead = (_file) => JSON.stringify({ + headSha: HEAD_SHA, + worktreePath: WT_PATH, + worktreeHead: HEAD_SHA, + verdict: 'fork-from-head-confirmed', + probedAt: '2026-09-18T00:00:00.000Z', + }); + // The stub answers rev-parse HEAD (classifyGitHead needs it before the + // cache is consulted) and refuses the worktree LIST: if the probe ran, + // this throws and fails. + const execGit = (args) => { + const key = args.join(' '); + if (key === 'rev-parse HEAD') { + return { exitCode: 0, stdout: `${HEAD_SHA}\n`, stderr: '', signal: null, error: null }; + } + if (key === 'worktree list --porcelain') { + throw new Error('probe must not run when the cache matches'); + } + throw new Error(`unexpected execGit call: ${key}`); + }; + const result = evaluateWorktreeBaseDegrade({ execGit, cwd: '/repo', probeStateRead: stateRead }); + assert.strictEqual(result.shouldDegrade, false); + assert.strictEqual(result.reason, 'fork-from-head-observed'); + }); + + test('the cache is invalidated by an orchestrator HEAD move', () => { + const NEW_HEAD = '11223344aabbccdd11223344aabbccdd11223344'; + const stateRead = (_file) => JSON.stringify({ + headSha: HEAD_SHA, + worktreePath: WT_PATH, + worktreeHead: HEAD_SHA, + verdict: 'fork-from-head-confirmed', + probedAt: '2026-09-18T00:00:00.000Z', + }); + // The cached entry is keyed to the OLD orchestrator HEAD while the + // orchestrator HEAD has MOVED (rev-parse HEAD answers NEW_HEAD) and the + // worktree still sits at the old commit — the cache must be ignored, the + // probe re-run, and the flow fall through to the fork comparison. + const execGit = (args) => { + const key = args.join(' '); + if (key === 'rev-parse HEAD') { + return { exitCode: 0, stdout: `${NEW_HEAD}\n`, stderr: '', signal: null, error: null }; + } + if (key === 'worktree list --porcelain') { + return { + exitCode: 0, + stdout: `worktree /repo\nbranch refs/heads/main\n\nworktree ${WT_PATH}\nbranch refs/heads/agent-x\n`, + stderr: '', signal: null, error: null, + }; + } + if (key === `-C ${WT_PATH} status --porcelain`) { + return { exitCode: 0, stdout: '', stderr: '', signal: null, error: null }; + } + if (key === `-C ${WT_PATH} rev-parse HEAD`) { + return { exitCode: 0, stdout: `${HEAD_SHA}\n`, stderr: '', signal: null, error: null }; + } + if (key === 'rev-parse --verify --quiet origin/HEAD') { + return { exitCode: 0, stdout: `${ORIGIN_SHA}\n`, stderr: '', signal: null, error: null }; + } + if (key === 'symbolic-ref --quiet refs/remotes/origin/HEAD') { + return { exitCode: 1, stdout: '', stderr: '', signal: null, error: null }; + } + throw new Error(`unexpected execGit call: ${key}`); + }; + const result = evaluateWorktreeBaseDegrade({ + execGit, + cwd: '/repo', + probeStateRead: stateRead, + probeStateWrite: () => {}, + }); + assert.strictEqual(result.reason, 'head-diverged-from-fork', 'stale cache must not suppress the #3659 comparison'); + assert.strictEqual(result.shouldDegrade, true); + }); + + test('a dirty harness worktree is not evidence', () => { + const result = evaluateWorktreeBaseDegrade({ + execGit: makeWorktreeExecGit({ clean: false }), + cwd: '/repo', + probeStateRead: () => null, + probeStateWrite: () => {}, + }); + assert.strictEqual(result.shouldDegrade, true, `unobserved fork base → the #3659 comparison still governs (reason=${result.reason})`); + assert.strictEqual(result.reason, 'head-diverged-from-fork', `reason=${result.reason}`); + }); + + test('a harness worktree at a different commit is not evidence', () => { + const result = evaluateWorktreeBaseDegrade({ + execGit: makeWorktreeExecGit({ wtHead: ORIGIN_SHA }), + cwd: '/repo', + probeStateRead: () => null, + probeStateWrite: () => {}, + }); + assert.strictEqual(result.shouldDegrade, true, `reason=${result.reason}`); + assert.strictEqual(result.reason, 'head-diverged-from-fork', `reason=${result.reason}`); + }); + + test('no harness worktrees changes nothing', () => { + const execGit = makeWorktreeExecGit(); + const result = evaluateWorktreeBaseDegrade({ + execGit: (args, opts) => { + const key = args.join(' '); + if (key === 'worktree list --porcelain') { + return { exitCode: 0, stdout: 'worktree /repo\nbranch refs/heads/main\n', stderr: '', signal: null, error: null }; + } + return execGit(args, opts); + }, + cwd: '/repo', + probeStateRead: () => null, + probeStateWrite: () => {}, + }); + assert.strictEqual(result.shouldDegrade, true, `reason=${result.reason}`); + assert.strictEqual(result.reason, 'head-diverged-from-fork', `reason=${result.reason}`); + }); + + test('a probe timeout fails closed to the existing flow', () => { + const result = evaluateWorktreeBaseDegrade({ + execGit: makeWorktreeExecGit({ listTimeout: true }), + cwd: '/repo', + probeStateRead: () => null, + probeStateWrite: () => {}, + }); + assert.strictEqual(result.shouldDegrade, true); + assert.strictEqual(result.reason, 'head-diverged-from-fork', 'a probe timeout must fall through to the comparison, not skip it'); + }); + + test('untracked-only worktrees count as clean', () => { + const execGit = makeWorktreeExecGit(); + const result = evaluateWorktreeBaseDegrade({ + execGit: (args, opts) => { + const key = args.join(' '); + if (key === `-C ${WT_PATH} status --porcelain`) { + return { exitCode: 0, stdout: '?? pr-review-notes.md', stderr: '', signal: null, error: null }; + } + return execGit(args, opts); + }, + cwd: '/repo', + probeStateRead: () => null, + probeStateWrite: () => {}, + }); + assert.strictEqual(result.shouldDegrade, false, 'untracked review notes do not disqualify the observation'); + assert.strictEqual(result.reason, 'fork-from-head-observed'); + }); + + test('orchestrator-worktree mode never probes (the suppress predates the probe)', () => { + const refusing = () => { throw new Error('probe must not run in orchestrator-worktree mode'); }; + const result = evaluateWorktreeBaseDegrade({ + execGit: refusing, + cwd: '/repo', + effectiveBaseRef: 'head', + isolationMode: 'orchestrator-worktree', + }); + assert.strictEqual(result.reason, 'baseref-head'); + assert.strictEqual(result.shouldDegrade, false); + }); + + test('a corrupt cache is re-probed, not trusted', () => { + const stateRead = (_file) => '{ this is not json'; + let probed = false; + const execGit = (args) => { + const key = args.join(' '); + if (key === 'worktree list --porcelain') { + probed = true; + return { exitCode: 0, stdout: 'worktree /repo\nbranch refs/heads/main\n', stderr: '', signal: null, error: null }; + } + if (key === 'rev-parse HEAD') { + return { exitCode: 0, stdout: `${HEAD_SHA}\n`, stderr: '', signal: null, error: null }; + } + if (key === 'rev-parse --verify --quiet origin/HEAD') { + return { exitCode: 0, stdout: `${ORIGIN_SHA}\n`, stderr: '', signal: null, error: null }; + } + if (key === 'symbolic-ref --quiet refs/remotes/origin/HEAD') { + return { exitCode: 1, stdout: '', stderr: '', signal: null, error: null }; + } + throw new Error(`unexpected execGit call: ${key}`); + }; + const result = evaluateWorktreeBaseDegrade({ execGit, cwd: '/repo', probeStateRead: stateRead }); + assert.ok(probed, 'the probe must run past a corrupt cache'); + assert.strictEqual(result.reason, 'head-diverged-from-fork', 'corrupt cache → re-probe → the comparison governs'); + }); + + test('worktrees outside the harness directory are not candidates', () => { + const execGit = (args) => { + const key = args.join(' '); + if (key === 'worktree list --porcelain') { + return { + exitCode: 0, + stdout: `worktree /repo\nbranch refs/heads/main\n\nworktree /elsewhere/agent-y\nbranch refs/heads/agent-y\n`, + stderr: '', signal: null, error: null, + }; + } + if (key === 'rev-parse HEAD') { + return { exitCode: 0, stdout: `${HEAD_SHA}\n`, stderr: '', signal: null, error: null }; + } + if (key === 'rev-parse --verify --quiet origin/HEAD') { + return { exitCode: 0, stdout: `${ORIGIN_SHA}\n`, stderr: '', signal: null, error: null }; + } + if (key === 'symbolic-ref --quiet refs/remotes/origin/HEAD') { + return { exitCode: 1, stdout: '', stderr: '', signal: null, error: null }; + } + throw new Error(`unexpected execGit call: ${key}`); + }; + const result = evaluateWorktreeBaseDegrade({ execGit, cwd: '/repo' }); + assert.strictEqual(result.reason, 'head-diverged-from-fork', 'a non-harness worktree must not confirm the observation'); + }); + + test('end-to-end: a real repo with a clean harness worktree at HEAD passes the base-check', (t) => { + const { createTempGitProject } = require('./helpers.cjs'); + const { gitOrThrow } = require('./helpers/git-fixture.cjs'); + const dir = createTempGitProject('gsd-4588-e2e-'); + t.after(() => cleanup(dir)); + const wtPath = path.join(dir, '.claude', 'worktrees', 'agent-e2e'); + fs.mkdirSync(path.dirname(wtPath), { recursive: true }); + gitOrThrow(['worktree', 'add', '-b', 'agent-e2e', wtPath, 'HEAD'], { cwd: dir, timeoutMs: GIT_TIMEOUT_MS }); + + // The command emits its JSON via fs.writeSync(1, …) — capture through the + // approved mock mechanism and assert on the EMITTED payload too (the + // workflow-facing contract), not just the return value. + const emitted = []; + const writeSyncMock = t.mock.method(fs, 'writeSync', (fd, buf, ...rest) => { + void fd; void rest; + emitted.push(typeof buf === 'string' ? buf : Buffer.from(buf).toString('utf8')); + return (typeof buf === 'string' ? buf : Buffer.from(buf)).length; + }); + const result = cmdWorktreeBaseCheck(dir, ['--mode', 'harness-worktree']); + assert.ok(writeSyncMock.mock.calls.length > 0, 'the check must emit its JSON payload'); + const emittedJson = JSON.parse(emitted.join('')); + assert.strictEqual(emittedJson.reason, 'fork-from-head-observed', 'the emitted workflow payload must carry the observation'); + assert.strictEqual(emittedJson.shouldDegrade, false); + assert.strictEqual(result.shouldDegrade, false, + `a clean harness worktree at HEAD must confirm fork-from-HEAD; got reason=${result.reason}`); + assert.strictEqual(result.reason, 'fork-from-head-observed'); + }); +});