Skip to content

Commit e7065ea

Browse files
committed
fix(loop): enforce exact clean resume worktree
1 parent f1c4a89 commit e7065ea

4 files changed

Lines changed: 204 additions & 7 deletions

File tree

loops/issue-dev-loop/scripts/lib/active-journal.mjs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,16 +211,23 @@ export async function reconcileActiveJournal({
211211

212212
async function defaultWorkspaceValidator({ loopRoot, record }) {
213213
const repositoryRoot = path.resolve(loopRoot, '..', '..')
214-
const [branch, head, status, gitDirectory, commonDirectory] = await Promise.all([
214+
const [branch, head, status, gitDirectory, commonDirectory, indexState] = await Promise.all([
215215
execFileAsync('git', ['branch', '--show-current'], { cwd: repositoryRoot }),
216216
execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repositoryRoot }),
217-
execFileAsync('git', ['status', '--porcelain'], { cwd: repositoryRoot }),
217+
execFileAsync('git', ['status', '--porcelain=v1', '--untracked-files=all'], {
218+
cwd: repositoryRoot,
219+
maxBuffer: 1024 * 1024,
220+
}),
218221
execFileAsync('git', ['rev-parse', '--path-format=absolute', '--git-dir'], {
219222
cwd: repositoryRoot,
220223
}),
221224
execFileAsync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
222225
cwd: repositoryRoot,
223226
}),
227+
execFileAsync('git', ['ls-files', '-v', '-z'], {
228+
cwd: repositoryRoot,
229+
maxBuffer: 8 * 1024 * 1024,
230+
}),
224231
])
225232
if (gitDirectory.stdout.trim() === commonDirectory.stdout.trim()) {
226233
throw new Error('restore requires an isolated linked Git worktree')
@@ -232,9 +239,40 @@ async function defaultWorkspaceValidator({ loopRoot, record }) {
232239
if (head.stdout.trim() !== expectedHead) {
233240
throw new Error(`restore requires exact durable head ${expectedHead}`)
234241
}
242+
const concealedIndexEntries = indexState.stdout
243+
.split('\0')
244+
.filter(Boolean)
245+
.filter((entry) => !entry.startsWith('H '))
246+
if (concealedIndexEntries.length > 0) {
247+
throw new Error('restore rejects index concealment and nonstandard tracked state')
248+
}
235249
if (status.stdout.trim()) {
236250
throw new Error('restore requires a clean isolated worktree')
237251
}
252+
try {
253+
await execFileAsync(
254+
'git',
255+
[
256+
'-c',
257+
'core.fileMode=true',
258+
'diff',
259+
'--quiet',
260+
'--no-ext-diff',
261+
'--no-textconv',
262+
'HEAD',
263+
'--',
264+
],
265+
{
266+
cwd: repositoryRoot,
267+
maxBuffer: 1024 * 1024,
268+
},
269+
)
270+
} catch (error) {
271+
if (error?.code === 1) {
272+
throw new Error('restore requires tracked filesystem contents to match HEAD')
273+
}
274+
throw error
275+
}
238276
}
239277

240278
export async function restoreActiveCheckpoint({

loops/issue-dev-loop/scripts/lib/github-identity.mjs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1373,7 +1373,7 @@ async function assertCleanExactDurableWorktree({
13731373
cwd: repositoryRoot,
13741374
env: environment,
13751375
}),
1376-
execFileAsync(realGit, ['status', '--porcelain'], {
1376+
execFileAsync(realGit, ['status', '--porcelain=v1', '--untracked-files=all'], {
13771377
cwd: repositoryRoot,
13781378
env: environment,
13791379
maxBuffer: 1024 * 1024,
@@ -1818,7 +1818,7 @@ async function preflightIssueBranchPush({
18181818
cwd: repositoryRoot,
18191819
env: environment,
18201820
}),
1821-
execFileAsync(realGit, ['status', '--porcelain'], {
1821+
execFileAsync(realGit, ['status', '--porcelain=v1', '--untracked-files=all'], {
18221822
cwd: repositoryRoot,
18231823
env: environment,
18241824
maxBuffer: 1024 * 1024,

loops/issue-dev-loop/tests/github-identity-routing.test.mjs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,7 +506,10 @@ if (commandArguments[0] === 'validate' && ${JSON.stringify(historicalActivation)
506506
)
507507
await writeFile(
508508
path.join(trustedLoopRoot, 'scripts', 'lib', 'validation.mjs'),
509-
`export async function validateLoop() {
509+
`import { consumeHistoricalValidationCapability } from './github-identity.mjs'
510+
511+
export async function validateLoop({ historicalCapability } = {}) {
512+
consumeHistoricalValidationCapability(historicalCapability)
510513
return { valid: true, historicalTargetCompatibility: true }
511514
}
512515
export function validateFinalizationHistory() {}
@@ -623,7 +626,7 @@ if [ "$1 $2" = "rev-parse HEAD" ]; then
623626
echo "${'b'.repeat(40)}"
624627
exit 0
625628
fi
626-
if [ "$1 $2" = "status --porcelain" ]; then
629+
if [ "$1 $2 $3" = "status --porcelain=v1 --untracked-files=all" ]; then
627630
if [ -f ${JSON.stringify(path.join(parent, 'dirty-git'))} ]; then
628631
echo " M src/unsafe.ts"
629632
fi
@@ -2388,7 +2391,7 @@ if [ "$1 $2" = "rev-parse HEAD" ]; then
23882391
echo "${'b'.repeat(40)}"
23892392
exit 0
23902393
fi
2391-
if [ "$1 $2" = "status --porcelain" ] || [ "$1" = "merge-base" ] || [ "$1 $2" = "diff --name-status" ]; then
2394+
if [ "$1 $2 $3" = "status --porcelain=v1 --untracked-files=all" ] || [ "$1" = "merge-base" ] || [ "$1 $2" = "diff --name-status" ]; then
23922395
exit 0
23932396
fi
23942397
if [ "$1 $2 $3" = "remote get-url origin" ]; then

loops/issue-dev-loop/tests/runtime.test.mjs

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,162 @@ test('durable candidate validation supports pre-implementation and later repair
12091209
)
12101210
})
12111211

1212+
test('default checkpoint restore ignores hidden-untracked config and rejects concealed index state', async () => {
1213+
const parent = await mkdtemp(path.join(os.tmpdir(), 'echo-ui-restore-cleanliness-test-'))
1214+
const repository = path.join(parent, 'repository')
1215+
const worktree = path.join(parent, 'worktree')
1216+
const loopRoot = path.join(worktree, 'loops', 'issue-dev-loop')
1217+
const git = async (cwd, ...args) => execFileAsync('git', args, { cwd })
1218+
try {
1219+
await mkdir(path.join(repository, 'loops', 'issue-dev-loop'), { recursive: true })
1220+
await writeFile(path.join(repository, 'tracked.txt'), 'tracked\n', 'utf8')
1221+
await writeFile(
1222+
path.join(repository, 'loops', 'issue-dev-loop', '.gitkeep'),
1223+
'',
1224+
'utf8',
1225+
)
1226+
await git(repository, 'init', '--initial-branch=dev')
1227+
await git(repository, 'config', 'user.name', 'Loop Test')
1228+
await git(repository, 'config', 'user.email', 'loop-test@example.invalid')
1229+
await git(repository, 'add', '.')
1230+
await git(repository, 'commit', '-m', 'base')
1231+
const baseSha = (await git(repository, 'rev-parse', 'HEAD')).stdout.trim()
1232+
await git(repository, 'worktree', 'add', '-b', 'codex/issue-444', worktree, baseSha)
1233+
1234+
const startedAt = '2026-07-24T00:00:00.000Z'
1235+
const record = {
1236+
schemaVersion: 1,
1237+
kind: 'active-checkpoint',
1238+
run: {
1239+
schemaVersion: 1,
1240+
runId: 'restore-untracked-run',
1241+
issueNumber: 444,
1242+
issueTitle: 'Restore exact durable worktree',
1243+
issueUrl: 'https://github.com/codeacme17/echo-ui/issues/444',
1244+
baseBranch: 'dev',
1245+
baseSha,
1246+
branch: 'codex/issue-444',
1247+
status: 'running',
1248+
startedAt,
1249+
finishedAt: null,
1250+
prUrl: null,
1251+
headSha: null,
1252+
mergeSha: null,
1253+
issueSnapshot: {
1254+
title: 'Restore exact durable worktree',
1255+
body: 'Fixture',
1256+
labels: ['codex-ready'],
1257+
url: 'https://github.com/codeacme17/echo-ui/issues/444',
1258+
capturedAt: startedAt,
1259+
},
1260+
briefDigest: null,
1261+
uiEvidenceRequired: false,
1262+
implementationCommit: null,
1263+
},
1264+
briefSource: '',
1265+
events: [
1266+
{
1267+
schemaVersion: 1,
1268+
runId: 'restore-untracked-run',
1269+
type: 'loop_started',
1270+
timestamp: startedAt,
1271+
status: 'running',
1272+
payload: { issueNumber: 444, branch: 'codex/issue-444' },
1273+
},
1274+
],
1275+
artifacts: [],
1276+
updatedAt: startedAt,
1277+
}
1278+
const checkpoint = { record, commentUrl: null, createdAt: startedAt }
1279+
1280+
const restoredPreImplementation = await restoreActiveCheckpoint({ loopRoot, checkpoint })
1281+
assert.equal(restoredPreImplementation.implementationCommit, null)
1282+
for (const directory of ['logs', 'handoffs', 'screen-shots', 'evidence']) {
1283+
await rm(path.join(loopRoot, directory), { recursive: true, force: true })
1284+
}
1285+
1286+
await git(worktree, 'config', 'status.showUntrackedFiles', 'no')
1287+
await writeFile(path.join(worktree, 'hidden-untracked.txt'), 'not clean\n', 'utf8')
1288+
const configuredStatus = await git(worktree, 'status', '--porcelain')
1289+
assert.equal(configuredStatus.stdout, '')
1290+
await assert.rejects(
1291+
restoreActiveCheckpoint({ loopRoot, checkpoint }),
1292+
/clean isolated worktree/,
1293+
)
1294+
1295+
await rm(path.join(worktree, 'hidden-untracked.txt'))
1296+
await git(worktree, 'update-index', '--skip-worktree', 'tracked.txt')
1297+
await assert.rejects(
1298+
restoreActiveCheckpoint({ loopRoot, checkpoint }),
1299+
/index concealment/,
1300+
)
1301+
1302+
await git(worktree, 'update-index', '--no-skip-worktree', 'tracked.txt')
1303+
await writeFile(path.join(worktree, 'tracked.txt'), 'repaired\n', 'utf8')
1304+
await git(worktree, 'add', 'tracked.txt')
1305+
await git(worktree, 'commit', '-m', 'repair implementation')
1306+
const repairCommit = (await git(worktree, 'rev-parse', 'HEAD')).stdout.trim()
1307+
const repairBrief = 'Repair the issue and retain the durable resume boundary.\n'
1308+
const repairBriefDigest = createHash('sha256').update(repairBrief).digest('hex')
1309+
const repairResult = {
1310+
schemaVersion: 1,
1311+
runId: record.run.runId,
1312+
agent: '$implement',
1313+
invocationId: 'repair-invocation',
1314+
startedAt: '2026-07-24T00:01:00.000Z',
1315+
finishedAt: '2026-07-24T00:02:00.000Z',
1316+
briefDigest: repairBriefDigest,
1317+
commitSha: repairCommit,
1318+
checks: [{ command: 'pnpm verify', status: 'passed' }],
1319+
}
1320+
const repairResultSource = `${JSON.stringify(repairResult)}\n`
1321+
const repairResultDigest = createHash('sha256')
1322+
.update(repairResultSource)
1323+
.digest('hex')
1324+
const repairResultPath = `logs/runs/${record.run.runId}/repair-result.json`
1325+
const repairFinishedAt = repairResult.finishedAt
1326+
const repairRecord = structuredClone(record)
1327+
repairRecord.run.implementationCommit = repairCommit
1328+
repairRecord.run.briefDigest = repairBriefDigest
1329+
repairRecord.briefSource = repairBrief
1330+
repairRecord.events.push({
1331+
schemaVersion: 1,
1332+
runId: record.run.runId,
1333+
type: 'implementation_completed',
1334+
timestamp: repairFinishedAt,
1335+
status: 'passed',
1336+
payload: {
1337+
agent: '$implement',
1338+
invocationId: repairResult.invocationId,
1339+
startedAt: repairResult.startedAt,
1340+
finishedAt: repairFinishedAt,
1341+
briefDigest: repairBriefDigest,
1342+
commitSha: repairCommit,
1343+
resultPath: repairResultPath,
1344+
resultDigest: repairResultDigest,
1345+
},
1346+
})
1347+
repairRecord.artifacts.push({
1348+
path: repairResultPath,
1349+
sha256: repairResultDigest,
1350+
source: repairResultSource,
1351+
})
1352+
repairRecord.updatedAt = repairFinishedAt
1353+
1354+
const restoredRepair = await restoreActiveCheckpoint({
1355+
loopRoot,
1356+
checkpoint: {
1357+
record: repairRecord,
1358+
commentUrl: null,
1359+
createdAt: repairFinishedAt,
1360+
},
1361+
})
1362+
assert.equal(restoredRepair.implementationCommit, repairCommit)
1363+
} finally {
1364+
await rm(parent, { recursive: true, force: true })
1365+
}
1366+
})
1367+
12121368
test('review publication digest excludes assigned review URLs but binds review content', () => {
12131369
const review = {
12141370
schemaVersion: 1,

0 commit comments

Comments
 (0)