diff --git a/loops/issue-dev-loop/SKILL.md b/loops/issue-dev-loop/SKILL.md index dcecc252..18857613 100644 --- a/loops/issue-dev-loop/SKILL.md +++ b/loops/issue-dev-loop/SKILL.md @@ -7,7 +7,7 @@ Run exactly one bounded issue cycle. Treat [`LOOP.md`](./LOOP.md) as the constit ## Start safely 1. Read `LOOP.md`, `state.md`, and `dependencies.md` completely. -2. Require absolute `ECHO_UI_LOOP_CONTROL_PLANE` and `ECHO_UI_LOOP_TARGET_ROOT` values from the scheduler. Run activation through `"$ECHO_UI_LOOP_CONTROL_PLANE/scripts/with-github-identity" --loop-root "$ECHO_UI_LOOP_TARGET_ROOT" automation -- node "$ECHO_UI_LOOP_CONTROL_PLANE/scripts/loopctl.mjs" validate --activation --loop-root "$ECHO_UI_LOOP_TARGET_ROOT"`. The installed launcher verifies its hash manifest and probes both configured profiles before starting validation. +2. Require absolute `ECHO_UI_LOOP_CONTROL_PLANE` and `ECHO_UI_LOOP_TARGET_ROOT` values from the scheduler. Run activation through `"$ECHO_UI_LOOP_CONTROL_PLANE/scripts/with-github-identity" --loop-root "$ECHO_UI_LOOP_TARGET_ROOT" automation -- node "$ECHO_UI_LOOP_CONTROL_PLANE/scripts/loopctl.mjs" validate --activation --loop-root "$ECHO_UI_LOOP_TARGET_ROOT"`. The installed launcher verifies its hash manifest, probes both configured profiles, and attempts full validation first. It may fall back for an older target only after excluding durably finalized runs, matching the worktree to an automation-authored remote active checkpoint, proving the clean event-derived branch and head without index concealment, and proving the issue diff did not modify the protected control or verification plane. A one-use in-memory router capability then checks stable target state, owner channel, JSON history, and a conservatively parsed low-privilege evidence workflow before rechecking the exact clean worktree. Activation, detection, and restore share the same event-derived head resolver. There is no standalone reduced validator, and callers and the public validation API cannot request this mode. 3. Read [`references/github-operations.md`](./references/github-operations.md). Run every operational `loopctl`, executor GitHub command, remote Git command, trigger, and reviewer publication through the installed control plane with the explicit target root. Never use the credential-refusing repository launcher, invoke the `.mjs` router directly, install control code from an issue branch, or alter global `gh` or Git credential configuration. 4. Run `loopctl.mjs reconcile` through the automation wrapper to rebuild verified terminal history, pending/completed evolve state, and active runs from the append-only GitHub state journal. It tombstones local terminal-cache rows with no durable counterpart before recomputing metrics. For returned `workType: resume`, fetch the recorded branch through the wrapper, create a clean isolated worktree at the returned exact head, and run `restore-checkpoint --run-id ` inside that worktree. The restore command rejects the wrong branch, a dirty checkout, or any head other than the durable head. Resume it before selecting a new issue. 5. Run `loopctl.mjs evolve-status`. If `evolveDue` is true, start `echo_ui_loop_evolver` with fresh context; do not silently replace it with product work. diff --git a/loops/issue-dev-loop/references/github-operations.md b/loops/issue-dev-loop/references/github-operations.md index 15e58eb1..98e9aa49 100644 --- a/loops/issue-dev-loop/references/github-operations.md +++ b/loops/issue-dev-loop/references/github-operations.md @@ -10,6 +10,8 @@ Run every executor GitHub command through: `ECHO_UI_LOOP_CONTROL_PLANE` must name the versioned installation created from a clean owner-merged `dev`; `ECHO_UI_LOOP_TARGET_ROOT` names the active worktree's `loops/issue-dev-loop`. Operational `loopctl` and trigger commands must use the scripts inside the installed root and pass `--loop-root "$ECHO_UI_LOOP_TARGET_ROOT"`. The repository launcher intentionally refuses credentials. +For a durable active run whose target predates newer trusted runtime files, the installed activation router attempts full validation first. Only if that fails may it select historical-target compatibility, and only after excluding durably finalized runs, matching an automation-authored remote active checkpoint, proving its event-derived clean exact branch and head with no index concealment flags, and proving the issue diff does not modify the protected control or verification plane. A one-use in-memory router capability then checks stable target state, owner channel, JSON history, and conservatively rejects unrecognized YAML, triggers, or job-level/write permissions before rechecking the worktree. Activation, detection, and restore use the same checkpoint-head resolver. No standalone reduced validator exists, and caller-supplied flags and the public validation API cannot select the reduced mode. + Run every reviewer publication command through the installed wrapper with role `reviewer`. Before reading either profile, it verifies every installed file, pins absolute Node/Git/`gh` executables, compares the target's security-critical owner-channel values to its trusted copy, removes token environment overrides, runs `gh api user`, and refuses an unexpected or owner identity. For Git, it clears global credential helpers and injects `gh auth git-credential` for the entire trusted child tree. Descendant `git` and `gh` processes pass through a role gate; arbitrary `sh`, `env`, Node scripts, caller PATH shims, and issue-worktree router changes are not authenticated. Never use owner credentials for executor or reviewer actions, never run raw remote `gh`/`git push` commands, and never call `gh auth setup-git`. Publish reviewer output only as a non-approving comment review: diff --git a/loops/issue-dev-loop/scripts/lib/active-journal.mjs b/loops/issue-dev-loop/scripts/lib/active-journal.mjs index 92dcca9f..ea565a22 100644 --- a/loops/issue-dev-loop/scripts/lib/active-journal.mjs +++ b/loops/issue-dev-loop/scripts/lib/active-journal.mjs @@ -19,6 +19,7 @@ import { checkpointJournalConfiguration, checkpointPublicationBody, checkpointRecordDigest, + checkpointWorktreeHead, parseCheckpointRecord, validateCheckpointRecord, verifyPublishedCheckpoint, @@ -210,16 +211,23 @@ export async function reconcileActiveJournal({ async function defaultWorkspaceValidator({ loopRoot, record }) { const repositoryRoot = path.resolve(loopRoot, '..', '..') - const [branch, head, status, gitDirectory, commonDirectory] = await Promise.all([ + const [branch, head, status, gitDirectory, commonDirectory, indexState] = await Promise.all([ execFileAsync('git', ['branch', '--show-current'], { cwd: repositoryRoot }), execFileAsync('git', ['rev-parse', 'HEAD'], { cwd: repositoryRoot }), - execFileAsync('git', ['status', '--porcelain'], { cwd: repositoryRoot }), + execFileAsync('git', ['status', '--porcelain=v1', '--untracked-files=all'], { + cwd: repositoryRoot, + maxBuffer: 1024 * 1024, + }), execFileAsync('git', ['rev-parse', '--path-format=absolute', '--git-dir'], { cwd: repositoryRoot, }), execFileAsync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], { cwd: repositoryRoot, }), + execFileAsync('git', ['ls-files', '-v', '-z'], { + cwd: repositoryRoot, + maxBuffer: 8 * 1024 * 1024, + }), ]) if (gitDirectory.stdout.trim() === commonDirectory.stdout.trim()) { throw new Error('restore requires an isolated linked Git worktree') @@ -227,13 +235,44 @@ async function defaultWorkspaceValidator({ loopRoot, record }) { if (branch.stdout.trim() !== record.run.branch) { throw new Error(`restore requires isolated worktree branch ${record.run.branch}`) } - const expectedHead = record.run.headSha ?? record.run.implementationCommit ?? record.run.baseSha + const expectedHead = checkpointWorktreeHead(record) if (head.stdout.trim() !== expectedHead) { throw new Error(`restore requires exact durable head ${expectedHead}`) } + const concealedIndexEntries = indexState.stdout + .split('\0') + .filter(Boolean) + .filter((entry) => !entry.startsWith('H ')) + if (concealedIndexEntries.length > 0) { + throw new Error('restore rejects index concealment and nonstandard tracked state') + } if (status.stdout.trim()) { throw new Error('restore requires a clean isolated worktree') } + try { + await execFileAsync( + 'git', + [ + '-c', + 'core.fileMode=true', + 'diff', + '--quiet', + '--no-ext-diff', + '--no-textconv', + 'HEAD', + '--', + ], + { + cwd: repositoryRoot, + maxBuffer: 1024 * 1024, + }, + ) + } catch (error) { + if (error?.code === 1) { + throw new Error('restore requires tracked filesystem contents to match HEAD') + } + throw error + } } export async function restoreActiveCheckpoint({ diff --git a/loops/issue-dev-loop/scripts/lib/checkpoint-proof.mjs b/loops/issue-dev-loop/scripts/lib/checkpoint-proof.mjs index 49346a6c..c1f4d973 100644 --- a/loops/issue-dev-loop/scripts/lib/checkpoint-proof.mjs +++ b/loops/issue-dev-loop/scripts/lib/checkpoint-proof.mjs @@ -13,6 +13,20 @@ import { const ACTIVE_STATUSES = new Set(['running', 'waiting_for_owner', 'awaiting_owner_review']) +export function checkpointWorktreeHead(record) { + let expectedHead = record?.run?.baseSha + for (const event of record?.events ?? []) { + if (event.type === 'implementation_completed' && event.status === 'passed') { + expectedHead = event.payload?.commitSha + } + if (event.type === 'pr_published') expectedHead = event.payload?.headSha + } + if (!/^[0-9a-f]{40}$/i.test(expectedHead ?? '')) { + throw new Error('durable active checkpoint has no valid working head') + } + return expectedHead +} + export async function checkpointJournalConfiguration(loopRoot) { const channel = await readJson( path.resolve(loopRoot, '..', '_shared', 'owner-channel', 'channel.json'), diff --git a/loops/issue-dev-loop/scripts/lib/finalization-journal.mjs b/loops/issue-dev-loop/scripts/lib/finalization-journal.mjs index ede358ff..1de1a22a 100644 --- a/loops/issue-dev-loop/scripts/lib/finalization-journal.mjs +++ b/loops/issue-dev-loop/scripts/lib/finalization-journal.mjs @@ -320,9 +320,8 @@ export async function recordFinalizationPublication({ return { record, digest, commentUrl } } -export async function reconcileFinalizationJournal({ +export async function loadDurableFinalizationRecords({ loopRoot = DEFAULT_LOOP_ROOT, - now = new Date(), githubPaginatedApi = defaultGitHubPaginatedApi, githubApi = defaultGitHubApi, latestActiveCheckpoints = null, @@ -380,6 +379,22 @@ export async function reconcileFinalizationJournal({ effectiveRecords.sort( (left, right) => Date.parse(left.finishedAt) - Date.parse(right.finishedAt), ) + return effectiveRecords +} + +export async function reconcileFinalizationJournal({ + loopRoot = DEFAULT_LOOP_ROOT, + now = new Date(), + githubPaginatedApi = defaultGitHubPaginatedApi, + githubApi = defaultGitHubApi, + latestActiveCheckpoints = null, +} = {}) { + const effectiveRecords = await loadDurableFinalizationRecords({ + loopRoot, + githubPaginatedApi, + githubApi, + latestActiveCheckpoints, + }) const indexPath = path.join(loopRoot, 'logs', 'index.jsonl') const existing = (await readFile(indexPath, 'utf8')) diff --git a/loops/issue-dev-loop/scripts/lib/github-identity.mjs b/loops/issue-dev-loop/scripts/lib/github-identity.mjs index d10001ab..e3178ceb 100644 --- a/loops/issue-dev-loop/scripts/lib/github-identity.mjs +++ b/loops/issue-dev-loop/scripts/lib/github-identity.mjs @@ -14,13 +14,16 @@ import { readJson, sameGitHubLogin, } from './common.mjs' +import { reconcileActiveJournal } from './active-journal.mjs' import { checkpointRecordDigest, + checkpointWorktreeHead, parseCheckpointRecord, validateCheckpointRecord, verifyLatestDurableCheckpoint, } from './checkpoint-proof.mjs' import { verifyPublishedEvolveRequest } from './evolve.mjs' +import { loadDurableFinalizationRecords } from './finalization-journal.mjs' import { parseReviewPublisherArguments, reviewPublisherSyntheticGitHubArguments, @@ -41,6 +44,7 @@ const roleFields = { environmentVariable: 'reviewerGitHubConfigEnvironmentVariable', }, } +const historicalValidationCapabilities = new WeakSet() const inheritedEnvironmentNames = new Set([ 'CI', @@ -240,10 +244,7 @@ export function hardenedGitArguments(args, { expectedRepository = null } = {}) { const rollback = lease?.match( /^--force-with-lease=(refs\/heads\/codex\/issue-[1-9][0-9]*):([0-9a-f]{40})$/, ) - if ( - rollback && - sameArguments(args, ['push', lease, 'origin', `:${rollback[1]}`]) - ) { + if (rollback && sameArguments(args, ['push', lease, 'origin', `:${rollback[1]}`])) { return ['push', lease, repositoryUrl, deleteRef] } const branch = args.at(-1) @@ -783,9 +784,7 @@ function pullRequestMutationAllowed(kind, args, commandIndex, authorization, exp } if (kind === 'ready') { return ( - parsed.values.size <= 1 && - parsed.booleans.size === 1 && - parsed.booleans.get('undo') === true + parsed.values.size <= 1 && parsed.booleans.size === 1 && parsed.booleans.get('undo') === true ) } if (kind === 'comment') { @@ -812,9 +811,7 @@ function reservedAutomationComment(body) { function checkpointPublicationAllowed(body, authorization) { const markers = [ - ...body.matchAll( - //g, - ), + ...body.matchAll(//g), ] if (markers.length !== 1 || markers[0][1] !== authorization?.issue?.runId) { return false @@ -1082,6 +1079,9 @@ function assertRootCommandPolicy({ role, tool, args, loopRoot, trustedLoopRoot, path.resolve(trustedLoopRoot, 'scripts', 'loopctl.mjs'), path.resolve(trustedLoopRoot, 'triggers', 'detect-work.mjs'), ]) + if (args.includes('--target-compatibility')) { + throw new Error('target compatibility validation is reserved to wrapped activation') + } if ( script && allowedScripts.has(script) && @@ -1094,8 +1094,7 @@ function assertRootCommandPolicy({ role, tool, args, loopRoot, trustedLoopRoot, if ( role === 'reviewer' && tool === 'node' && - path.resolve(args[0] ?? '') === - path.resolve(trustedLoopRoot, 'scripts', 'publish-review.mjs') + path.resolve(args[0] ?? '') === path.resolve(trustedLoopRoot, 'scripts', 'publish-review.mjs') ) { parseReviewPublisherArguments(args.slice(1), { authorization, @@ -1301,16 +1300,11 @@ function argumentAfter(args, name) { function withRootCommandIntent(authorization, { tool, args, trustedLoopRoot }) { const script = args[0] ? path.resolve(args[0]) : null const isTrustedLoopctl = - tool === 'node' && - script === path.resolve(trustedLoopRoot, 'scripts', 'loopctl.mjs') + tool === 'node' && script === path.resolve(trustedLoopRoot, 'scripts', 'loopctl.mjs') const withIntent = isTrustedLoopctl ? { ...authorization, rootIntent: args[1] ?? null } : authorization - if ( - !isTrustedLoopctl || - args[1] !== 'start' || - authorization.issue !== null - ) { + if (!isTrustedLoopctl || args[1] !== 'start' || authorization.issue !== null) { return withIntent } const issueNumber = Number(argumentAfter(args, '--issue')) @@ -1353,6 +1347,204 @@ function activationValidationRequested({ role, tool, args, loopRoot, trustedLoop ) } +export function consumeHistoricalValidationCapability(capability) { + if ( + (typeof capability !== 'object' && typeof capability !== 'function') || + capability === null || + !historicalValidationCapabilities.delete(capability) + ) { + throw new Error('historical target validation requires an authorized router capability') + } +} + +async function assertCleanExactDurableWorktree({ + realGit, + repositoryRoot, + environment, + run, + expectedHead, +}) { + const [branch, head, status, gitDirectory, commonDirectory, indexState] = await Promise.all([ + execFileAsync(realGit, ['branch', '--show-current'], { + cwd: repositoryRoot, + env: environment, + }), + execFileAsync(realGit, ['rev-parse', 'HEAD'], { + cwd: repositoryRoot, + env: environment, + }), + execFileAsync(realGit, ['status', '--porcelain=v1', '--untracked-files=all'], { + cwd: repositoryRoot, + env: environment, + maxBuffer: 1024 * 1024, + }), + execFileAsync(realGit, ['rev-parse', '--path-format=absolute', '--git-dir'], { + cwd: repositoryRoot, + env: environment, + }), + execFileAsync(realGit, ['rev-parse', '--path-format=absolute', '--git-common-dir'], { + cwd: repositoryRoot, + env: environment, + }), + execFileAsync(realGit, ['ls-files', '-v', '-z'], { + cwd: repositoryRoot, + env: environment, + maxBuffer: 8 * 1024 * 1024, + }), + ]) + if (gitDirectory.stdout.trim() === commonDirectory.stdout.trim()) { + throw new Error('historical target validation requires an isolated linked Git worktree') + } + const concealedIndexEntries = indexState.stdout + .split('\0') + .filter(Boolean) + .filter((entry) => !entry.startsWith('H ')) + if (concealedIndexEntries.length > 0) { + throw new Error( + 'historical target validation rejects index concealment and nonstandard tracked state', + ) + } + if ( + branch.stdout.trim() !== run.branch || + head.stdout.trim() !== expectedHead || + status.stdout.trim() + ) { + throw new Error( + 'historical target validation requires the clean exact durable branch and head', + ) + } + try { + await execFileAsync( + realGit, + [ + '-c', + 'core.fileMode=true', + 'diff', + '--quiet', + '--no-ext-diff', + '--no-textconv', + 'HEAD', + '--', + ], + { + cwd: repositoryRoot, + env: environment, + maxBuffer: 1024 * 1024, + }, + ) + } catch (error) { + if (error?.code === 1) { + throw new Error( + 'historical target validation requires tracked filesystem contents to match HEAD', + ) + } + throw error + } +} + +async function authorizeHistoricalTargetValidation({ + authorization, + loopRoot, + trustedLoopRoot, + realGit, + realGh, + realNode, + environment, +}) { + const localIssue = authorization.issue + const repositoryRoot = repositoryRootForLoop(loopRoot) + const [checkedOutBranch, checkedOutHead] = await Promise.all([ + execFileAsync(realGit, ['branch', '--show-current'], { + cwd: repositoryRoot, + env: environment, + }), + execFileAsync(realGit, ['rev-parse', 'HEAD'], { + cwd: repositoryRoot, + env: environment, + }), + ]) + const githubApi = async (endpoint) => { + const { stdout } = await execFileAsync(realGh, ['api', endpoint], { + env: environment, + maxBuffer: 1024 * 1024, + }) + return JSON.parse(stdout) + } + const paginatedApi = (endpoint) => + paginateGitHubApi(githubApi, endpoint.replace(/[?&]per_page=100$/, '')) + const { activeCheckpoints: allActiveCheckpoints } = await reconcileActiveJournal({ + loopRoot, + githubPaginatedApi: paginatedApi, + }) + const finalizations = await loadDurableFinalizationRecords({ + loopRoot, + githubPaginatedApi: paginatedApi, + githubApi, + latestActiveCheckpoints: allActiveCheckpoints, + }) + const terminalRunIds = new Set(finalizations.map((record) => record.runId)) + const activeCheckpoints = allActiveCheckpoints.filter( + (checkpoint) => !terminalRunIds.has(checkpoint.record.run.runId), + ) + const durableMatches = activeCheckpoints.filter( + (checkpoint) => + checkpoint.record.run.branch === checkedOutBranch.stdout.trim() && + checkpointWorktreeHead(checkpoint.record) === checkedOutHead.stdout.trim(), + ) + const durable = durableMatches.length === 1 ? durableMatches[0] : null + const run = durable?.record.run + const expectedHead = durable ? checkpointWorktreeHead(durable.record) : null + if ( + !run || + run.finishedAt !== null || + (localIssue && + (run.runId !== localIssue.runId || + run.issueNumber !== localIssue.issueNumber || + run.branch !== localIssue.branch)) + ) { + throw new Error( + 'historical target validation requires the exact remote durable active checkpoint', + ) + } + + await assertCleanExactDurableWorktree({ + realGit, + repositoryRoot, + environment, + run, + expectedHead, + }) + + await execFileAsync( + realNode, + [ + path.resolve(trustedLoopRoot, 'scripts', 'validate-candidate-control-plane.mjs'), + '--loop-root', + path.resolve(loopRoot), + '--run-id', + run.runId, + '--base-sha', + run.baseSha, + '--head-sha', + expectedHead, + '--durable-issue-number', + String(run.issueNumber), + '--durable-implementation-commit', + run.implementationCommit ?? 'none', + '--durable-pr-head', + run.headSha ?? 'none', + ], + { + cwd: repositoryRoot, + env: environment, + maxBuffer: 4 * 1024 * 1024, + }, + ) + const capability = Object.freeze({}) + historicalValidationCapabilities.add(capability) + return { capability, durable, expectedHead, repositoryRoot, run } +} + function pullRequestWriteIntent(role, args, authorization) { const group = githubGroup(args) if (role === 'reviewer' && group.name === 'api') { @@ -1478,10 +1670,7 @@ async function preflightPullRequestWrite({ } if (['review', 'inline-review'].includes(intent.kind)) { - if ( - livePullRequest.draft !== true || - !intent.publication - ) { + if (livePullRequest.draft !== true || !intent.publication) { throw new Error('independent review publication requires the recorded Draft PR') } const publishedReviews = await githubPaginatedApi( @@ -1499,9 +1688,7 @@ async function preflightPullRequestWrite({ review.body?.includes(intent.publication.marker), ) ) { - throw new Error( - `adjudication for ${intent.publication.findingId} is already published`, - ) + throw new Error(`adjudication for ${intent.publication.findingId} is already published`) } const findingMarker = `` let findingPublished = false @@ -1642,7 +1829,7 @@ async function preflightIssueBranchPush({ cwd: repositoryRoot, env: environment, }), - execFileAsync(realGit, ['status', '--porcelain'], { + execFileAsync(realGit, ['status', '--porcelain=v1', '--untracked-files=all'], { cwd: repositoryRoot, env: environment, maxBuffer: 1024 * 1024, @@ -1935,7 +2122,49 @@ export async function runWithGitHubRole({ ? hardenedGitArguments(args, { expectedRepository: channel.repository }) : [...args] if (activationValidation) { - executionArgs = [args[0], 'validate', '--loop-root', path.resolve(loopRoot)] + const fullValidationArguments = [ + args[0], + 'validate', + '--loop-root', + path.resolve(loopRoot), + ] + try { + const { stdout } = await execFileAsync(executable, fullValidationArguments, { + env: childEnvironment, + maxBuffer: 4 * 1024 * 1024, + }) + process.stdout.write(stdout) + return 0 + } catch (fullValidationError) { + const historicalAuthorization = await authorizeHistoricalTargetValidation({ + authorization, + loopRoot, + trustedLoopRoot: trustedControlPlane.loopRoot, + realGit, + realGh, + realNode, + environment: childEnvironment, + }) + try { + const { validateLoop } = await import('./validation.mjs') + const result = await validateLoop({ + loopRoot, + historicalCapability: historicalAuthorization.capability, + }) + await assertCleanExactDurableWorktree({ + realGit, + repositoryRoot: historicalAuthorization.repositoryRoot, + environment: childEnvironment, + run: historicalAuthorization.run, + expectedHead: historicalAuthorization.expectedHead, + }) + process.stdout.write(`${JSON.stringify(result, null, 2)}\n`) + return 0 + } catch (historicalValidationError) { + historicalValidationError.cause = fullValidationError + throw historicalValidationError + } + } } const child = spawnCommand(executable, executionArgs, { env: childEnvironment, diff --git a/loops/issue-dev-loop/scripts/lib/github.mjs b/loops/issue-dev-loop/scripts/lib/github.mjs index bc2de3a0..f2afc6d8 100644 --- a/loops/issue-dev-loop/scripts/lib/github.mjs +++ b/loops/issue-dev-loop/scripts/lib/github.mjs @@ -26,7 +26,7 @@ import { reconcileActiveJournal } from './active-journal.mjs' import { reconcileEvolveJournal } from './evolve.mjs' import { defaultReleaseIssueClaim } from './issue-claim.mjs' import { appendValidatedEvent, finalizeRun, readEvents, readRun } from './run-store.mjs' -import { verifyLatestDurableCheckpoint } from './checkpoint-proof.mjs' +import { checkpointWorktreeHead, verifyLatestDurableCheckpoint } from './checkpoint-proof.mjs' import { validateFinalizationHistory } from './validation.mjs' const PRIORITY = new Map([ @@ -168,10 +168,7 @@ export async function detectWork({ workType: 'resume', runId: resumable.record.run.runId, branch: resumable.record.run.branch, - expectedHeadSha: - resumable.record.run.headSha ?? - resumable.record.run.implementationCommit ?? - resumable.record.run.baseSha, + expectedHeadSha: checkpointWorktreeHead(resumable.record), issue: { number: resumable.record.run.issueNumber, title: resumable.record.run.issueTitle, diff --git a/loops/issue-dev-loop/scripts/lib/validation.mjs b/loops/issue-dev-loop/scripts/lib/validation.mjs index a4ad0d13..e5df25e3 100644 --- a/loops/issue-dev-loop/scripts/lib/validation.mjs +++ b/loops/issue-dev-loop/scripts/lib/validation.mjs @@ -2,7 +2,13 @@ import { readFile, readdir } from 'node:fs/promises' import path from 'node:path' import { DEFAULT_LOOP_ROOT, pathExists, readJson, sameGitHubLogin } from './common.mjs' -import { assertGitHubRoleIdentity } from './github-identity.mjs' +import { + assertGitHubRoleIdentity, + consumeHistoricalValidationCapability, +} from './github-identity.mjs' + +const unsupportedYamlCharacters = + /[\u0000-\u0009\u000b\u000c\u000e-\u001f\u007f-\u009f\u2028\u2029]/u async function collectFiles(root, output = []) { const entries = await readdir(root, { withFileTypes: true }) @@ -25,9 +31,7 @@ export function validateFinalizationHistory(historyLines) { const prior = stateByRunId.get(entry.runId) if (entry.event === 'run_finalization_unverified') { if (prior?.state !== 'finalized') { - throw new Error( - `logs/index.jsonl run is not currently finalized: ${entry.runId}`, - ) + throw new Error(`logs/index.jsonl run is not currently finalized: ${entry.runId}`) } stateByRunId.set(entry.runId, { ...prior, state: 'unverified' }) continue @@ -46,17 +50,188 @@ export function validateFinalizationHistory(historyLines) { } } -export async function validateLoop({ +function activeYamlLines(source) { + return source + .split(/\r\n|[\r\n]/) + .map((line) => line.replace(/\s+$/, '')) + .filter((line) => line.trim() && !line.trimStart().startsWith('#')) +} + +function yamlMapping(line) { + const match = line.match( + /^(\s*)(?:(["'])([^"']+)\2|([A-Za-z_][A-Za-z0-9_-]*))\s*:(.*)$/, + ) + if (!match) return null + return { + indent: match[1].length, + key: match[3] ?? match[4], + lineIndent: match[1].length, + quoted: Boolean(match[2]), + value: match[5].replace(/\s+#.*$/, '').trim(), + } +} + +function yamlSequenceMapping(line) { + const match = line.match( + /^(\s*)-\s+(?:(["'])([^"']+)\2|([A-Za-z_][A-Za-z0-9_-]*))\s*:(.*)$/, + ) + if (!match) return null + return { + indent: match[1].length + 2, + key: match[3] ?? match[4], + lineIndent: match[1].length, + quoted: Boolean(match[2]), + value: match[5].replace(/\s+#.*$/, '').trim(), + } +} + +function conservativeYamlBlock( + lines, + { rejectFlowValues = true, rejectPermissionKeys = true } = {}, +) { + let scalarParentIndent = null + for (const line of lines) { + const lineIndent = line.match(/^\s*/)?.[0].length ?? 0 + if (scalarParentIndent !== null && lineIndent > scalarParentIndent) continue + scalarParentIndent = null + + const mapping = yamlMapping(line) ?? yamlSequenceMapping(line) + if ( + !mapping || + mapping.quoted || + (rejectPermissionKeys && mapping.key === 'permissions') || + (rejectFlowValues && /^[{[]/.test(mapping.value)) || + /^[!&*]/.test(mapping.value) + ) { + return false + } + if (/^[>|][+-]?(?:[1-9])?$/.test(mapping.value)) { + scalarParentIndent = mapping.lineIndent + } + } + return true +} + +export function historicalWorkflowIsLowPrivilege(source) { + if (unsupportedYamlCharacters.test(source)) return false + const lines = activeYamlLines(source) + if ( + lines.some( + (line) => + line.includes('\t') || + /^\s*<<\s*:/.test(line) || + /^\s*[?:]\s/.test(line) || + /^(?:---|\.\.\.)\s*(?:#.*)?$/.test(line), + ) + ) { + return false + } + if ( + !conservativeYamlBlock(lines, { + rejectFlowValues: false, + rejectPermissionKeys: false, + }) + ) { + return false + } + const mappings = lines.map((line, index) => ({ + index, + mapping: yamlMapping(line), + })) + if ( + mappings.some(({ mapping }) => mapping?.quoted) || + mappings.some( + ({ mapping }) => + mapping && + (mapping.key === 'pull_request_target' || + (mapping.key === 'permissions' && mapping.indent > 0)), + ) + ) { + return false + } + const topLevelMappings = mappings.filter(({ mapping }) => mapping?.indent === 0) + const topLevelKeys = topLevelMappings.map(({ mapping }) => mapping.key) + if (new Set(topLevelKeys).size !== topLevelKeys.length) return false + + const blockLines = ({ index }) => { + const endOffset = lines.slice(index + 1).findIndex((line) => /^\S/.test(line)) + return endOffset === -1 + ? lines.slice(index + 1) + : lines.slice(index + 1, index + 1 + endOffset) + } + + const onEntries = topLevelMappings.filter(({ mapping }) => mapping.key === 'on') + if (onEntries.length !== 1 || onEntries[0].mapping.value) return false + const triggerBlock = blockLines(onEntries[0]) + const triggerBoundaryLines = triggerBlock.filter( + (line) => (line.match(/^\s*/)?.[0].length ?? 0) <= 2, + ) + const triggerMappings = triggerBoundaryLines + .map(yamlMapping) + .filter((mapping) => mapping?.indent === 2) + if ( + triggerBoundaryLines.length !== 1 || + triggerMappings.length !== 1 || + triggerMappings[0].key !== 'pull_request' || + triggerMappings[0].value + ) { + return false + } + + const permissionEntries = topLevelMappings.filter( + ({ mapping }) => mapping.key === 'permissions', + ) + if (permissionEntries.length !== 1 || permissionEntries[0].mapping.value) return false + const permissionLines = blockLines(permissionEntries[0]) + const permissions = new Map() + for (const line of permissionLines) { + const mapping = yamlMapping(line) + if ( + !mapping || + mapping.indent !== 2 || + !['read', 'none'].includes(mapping.value) || + permissions.has(mapping.key) + ) { + return false + } + permissions.set(mapping.key, mapping.value) + } + if (permissions.get('contents') !== 'read') return false + + const jobsEntries = topLevelMappings.filter(({ mapping }) => mapping.key === 'jobs') + if (jobsEntries.length !== 1 || jobsEntries[0].mapping.value) return false + const jobsBlock = blockLines(jobsEntries[0]) + if (!conservativeYamlBlock(jobsBlock)) return false + const jobDeclarations = jobsBlock.filter( + (line) => (line.match(/^\s*/)?.[0].length ?? 0) <= 2, + ) + return ( + jobDeclarations.length > 0 && + jobDeclarations.every((line) => { + const mapping = yamlMapping(line) + return mapping?.indent === 2 && !mapping.value + }) + ) +} + +async function validateLoopMode({ loopRoot = DEFAULT_LOOP_ROOT, activation = false, + targetCompatibility = false, environment = process.env, identityCommand, } = {}) { - const required = [ + const targetRequired = [ 'SKILL.md', 'LOOP.md', 'state.md', 'dependencies.md', + 'evolve/metrics.json', + 'logs/index.jsonl', + 'logs/triggers.jsonl', + 'screen-shots/.gitignore', + ] + const controlPlaneRequired = [ 'agents/openai.yaml', 'agents/echo-ui-pr-reviewer.toml', 'agents/echo-ui-review-adjudicator.toml', @@ -105,10 +280,10 @@ export async function validateLoop({ 'scripts/loopctl.mjs', 'scripts/runtime.mjs', 'triggers/detect-work.mjs', - 'logs/index.jsonl', - 'logs/triggers.jsonl', - 'screen-shots/.gitignore', ] + const required = targetCompatibility + ? targetRequired + : [...targetRequired, ...controlPlaneRequired] const missing = [] for (const relative of required) { if (!(await pathExists(path.join(loopRoot, relative)))) missing.push(relative) @@ -194,6 +369,11 @@ export async function validateLoop({ throw new Error('missing .github/workflows/issue-dev-loop-evidence.yml') } const evidenceWorkflowSource = await readFile(evidenceWorkflow, 'utf8') + if (!historicalWorkflowIsLowPrivilege(evidenceWorkflowSource)) { + throw new Error( + 'historical target evidence workflow must remain a low-privilege pull_request workflow', + ) + } const verificationStep = evidenceWorkflowSource.match( / - name: Run authoritative verification\n([\s\S]*?)(?=\n - name:)/, )?.[1] @@ -201,107 +381,103 @@ export async function validateLoop({ / - name: Enforce verification result\n([\s\S]*?)(?=\n - name:|$)/, )?.[1] if ( - !verificationStep?.includes('pnpm verify') || - verificationStep.includes('if:') || - !enforcementStep || - enforcementStep.includes("steps.run.outputs.has_run == 'true'") || - !evidenceWorkflowSource.includes('pull_request:') || - evidenceWorkflowSource.includes('pull_request_target:') || - !evidenceWorkflowSource.includes('permissions:\n contents: read') || - !evidenceWorkflowSource.includes( - "github.event.pull_request.head.ref != 'codex/issue-dev-loop'", - ) || - !evidenceWorkflowSource.includes('Check out owner-merged control plane') || - !evidenceWorkflowSource.includes('Check out frozen owner-merged baseline') || - !evidenceWorkflowSource.includes('ref: ${{ github.event.pull_request.base.sha }}') || - !evidenceWorkflowSource.includes('ref: ${{ steps.run.outputs.base_sha }}') || - !evidenceWorkflowSource.includes('path: trusted') || - (evidenceWorkflowSource.match(/persist-credentials: false/g)?.length ?? 0) < 3 || - !evidenceWorkflowSource.includes( - 'trusted/loops/issue-dev-loop/scripts/validate-candidate-control-plane.mjs', - ) || - !evidenceWorkflowSource.includes('verifier.Dockerfile') || - !evidenceWorkflowSource.includes('pnpm install --frozen-lockfile --ignore-scripts') || - (evidenceWorkflowSource.match(/docker run --rm --network none/g)?.length ?? 0) < 2 || - !evidenceWorkflowSource.includes('src=${GITHUB_WORKSPACE}/trusted,dst=/source,readonly') || - !evidenceWorkflowSource.includes('pnpm test') || - !evidenceWorkflowSource.includes( - 'git config --global --add safe.directory /work; pnpm verify', - ) || - !evidenceWorkflowSource.includes( - 'git config --global --add safe.directory /work; pnpm test', - ) || - !evidenceWorkflowSource.includes( - '--trusted-workflow-sha "${{ steps.run.outputs.base_sha }}"', - ) || - !evidenceWorkflowSource.includes( - '--workflow-base-sha "${{ github.event.pull_request.base.sha }}"', - ) || - !evidenceWorkflowSource.includes( - '--workflow-run-sha "${{ github.event.pull_request.head.sha }}"', - ) || - !evidenceWorkflowSource.includes('PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}') || - !evidenceWorkflowSource.includes('--branch "$PR_HEAD_REF"') || - !evidenceWorkflowSource.includes('--baseline-status') + (!targetCompatibility && !verificationStep?.includes('pnpm verify')) || + (!targetCompatibility && + (verificationStep.includes('if:') || + !enforcementStep || + enforcementStep.includes("steps.run.outputs.has_run == 'true'") || + !evidenceWorkflowSource.includes( + "github.event.pull_request.head.ref != 'codex/issue-dev-loop'", + ) || + !evidenceWorkflowSource.includes('Check out owner-merged control plane') || + !evidenceWorkflowSource.includes('Check out frozen owner-merged baseline') || + !evidenceWorkflowSource.includes('ref: ${{ github.event.pull_request.base.sha }}') || + !evidenceWorkflowSource.includes('ref: ${{ steps.run.outputs.base_sha }}') || + !evidenceWorkflowSource.includes('path: trusted') || + (evidenceWorkflowSource.match(/persist-credentials: false/g)?.length ?? 0) < 3 || + !evidenceWorkflowSource.includes( + 'trusted/loops/issue-dev-loop/scripts/validate-candidate-control-plane.mjs', + ) || + !evidenceWorkflowSource.includes('verifier.Dockerfile') || + !evidenceWorkflowSource.includes('pnpm install --frozen-lockfile --ignore-scripts') || + (evidenceWorkflowSource.match(/docker run --rm --network none/g)?.length ?? 0) < 2 || + !evidenceWorkflowSource.includes('src=${GITHUB_WORKSPACE}/trusted,dst=/source,readonly') || + !evidenceWorkflowSource.includes('pnpm test') || + !evidenceWorkflowSource.includes( + 'git config --global --add safe.directory /work; pnpm verify', + ) || + !evidenceWorkflowSource.includes( + 'git config --global --add safe.directory /work; pnpm test', + ) || + !evidenceWorkflowSource.includes( + '--trusted-workflow-sha "${{ steps.run.outputs.base_sha }}"', + ) || + !evidenceWorkflowSource.includes( + '--workflow-base-sha "${{ github.event.pull_request.base.sha }}"', + ) || + !evidenceWorkflowSource.includes( + '--workflow-run-sha "${{ github.event.pull_request.head.sha }}"', + ) || + !evidenceWorkflowSource.includes( + 'PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}', + ) || + !evidenceWorkflowSource.includes('--branch "$PR_HEAD_REF"') || + !evidenceWorkflowSource.includes('--baseline-status'))) ) { throw new Error( 'evidence workflow must use a low-privilege isolated PR run plus owner-merged controls and baseline tests', ) } - const codexConfig = await readFile( - path.resolve(loopRoot, '..', '..', '.codex', 'config.toml'), - 'utf8', - ) - const roleRegistrations = { - echo_ui_pr_reviewer: 'echo-ui-pr-reviewer.toml', - echo_ui_review_adjudicator: 'echo-ui-review-adjudicator.toml', - echo_ui_loop_evolver: 'echo-ui-loop-evolver.toml', - } - for (const [role, roleFile] of Object.entries(roleRegistrations)) { - const registration = `config_file = "../loops/issue-dev-loop/agents/${roleFile}"` - if (!codexConfig.includes(`[agents.${role}]`) || !codexConfig.includes(registration)) { - throw new Error(`Codex role is not registered through config_file: ${role}`) - } - } - for (const roleFile of [ - 'echo-ui-pr-reviewer.toml', - 'echo-ui-review-adjudicator.toml', - ]) { - const roleSource = await readFile( - path.resolve(loopRoot, 'agents', roleFile), + if (!targetCompatibility) { + const codexConfig = await readFile( + path.resolve(loopRoot, '..', '..', '.codex', 'config.toml'), 'utf8', ) - if ( - !roleSource.includes('$ECHO_UI_LOOP_CONTROL_PLANE/scripts/with-github-identity') || - !roleSource.includes('--loop-root') || - !roleSource.includes('$ECHO_UI_LOOP_TARGET_ROOT') || - !roleSource.includes('repository launcher') - ) { - throw new Error(`${roleFile} must publish only through the installed identity launcher`) + const roleRegistrations = { + echo_ui_pr_reviewer: 'echo-ui-pr-reviewer.toml', + echo_ui_review_adjudicator: 'echo-ui-review-adjudicator.toml', + echo_ui_loop_evolver: 'echo-ui-loop-evolver.toml', + } + for (const [role, roleFile] of Object.entries(roleRegistrations)) { + const registration = `config_file = "../loops/issue-dev-loop/agents/${roleFile}"` + if (!codexConfig.includes(`[agents.${role}]`) || !codexConfig.includes(registration)) { + throw new Error(`Codex role is not registered through config_file: ${role}`) + } + } + for (const roleFile of ['echo-ui-pr-reviewer.toml', 'echo-ui-review-adjudicator.toml']) { + const roleSource = await readFile(path.resolve(loopRoot, 'agents', roleFile), 'utf8') + if ( + !roleSource.includes('$ECHO_UI_LOOP_CONTROL_PLANE/scripts/with-github-identity') || + !roleSource.includes('--loop-root') || + !roleSource.includes('$ECHO_UI_LOOP_TARGET_ROOT') || + !roleSource.includes('repository launcher') + ) { + throw new Error(`${roleFile} must publish only through the installed identity launcher`) + } } - } - const contract = await readFile(path.join(loopRoot, 'LOOP.md'), 'utf8') - const skill = await readFile(path.join(loopRoot, 'SKILL.md'), 'utf8') - for (const phrase of [ - 'draft PR targeting `dev`', - 'approve, auto-merge, or merge any PR', - 'Only the remote owner-merge gate', - 'exact reviewed head SHA', - 'No eligible work is a successful no-op', - ]) { - if (!contract.includes(phrase)) throw new Error(`LOOP.md is missing invariant: ${phrase}`) - } - for (const phrase of [ - '$implement', - 'echo_ui_pr_reviewer', - 'echo_ui_loop_evolver', - 'record-pr', - 'record-evidence', - 'pnpm verify', - ]) { - if (!skill.includes(phrase)) { - throw new Error(`SKILL.md is missing runtime dependency: ${phrase}`) + const contract = await readFile(path.join(loopRoot, 'LOOP.md'), 'utf8') + const skill = await readFile(path.join(loopRoot, 'SKILL.md'), 'utf8') + for (const phrase of [ + 'draft PR targeting `dev`', + 'approve, auto-merge, or merge any PR', + 'Only the remote owner-merge gate', + 'exact reviewed head SHA', + 'No eligible work is a successful no-op', + ]) { + if (!contract.includes(phrase)) throw new Error(`LOOP.md is missing invariant: ${phrase}`) + } + for (const phrase of [ + '$implement', + 'echo_ui_pr_reviewer', + 'echo_ui_loop_evolver', + 'record-pr', + 'record-evidence', + 'pnpm verify', + ]) { + if (!skill.includes(phrase)) { + throw new Error(`SKILL.md is missing runtime dependency: ${phrase}`) + } } } @@ -317,3 +493,16 @@ export async function validateLoop({ } return { valid: true, checkedFiles: required.length + jsonFiles.length } } + +export function validateLoop(options = {}) { + const { historicalCapability, ...validatedOptions } = options + if (historicalCapability) { + consumeHistoricalValidationCapability(historicalCapability) + return validateLoopMode({ + ...validatedOptions, + activation: false, + targetCompatibility: true, + }) + } + return validateLoopMode({ ...validatedOptions, targetCompatibility: false }) +} diff --git a/loops/issue-dev-loop/scripts/loopctl.mjs b/loops/issue-dev-loop/scripts/loopctl.mjs index ff582069..9bd74e2f 100644 --- a/loops/issue-dev-loop/scripts/loopctl.mjs +++ b/loops/issue-dev-loop/scripts/loopctl.mjs @@ -236,7 +236,15 @@ async function main() { ) break case 'validate': - output(await validateLoop({ loopRoot, activation: Boolean(args.activation) })) + if (args['target-compatibility']) { + throw new Error('target compatibility validation is reserved to wrapped activation') + } + output( + await validateLoop({ + loopRoot, + activation: Boolean(args.activation), + }), + ) break case 'evolve-status': output(await getEvolveStatus({ loopRoot })) diff --git a/loops/issue-dev-loop/scripts/validate-candidate-control-plane.mjs b/loops/issue-dev-loop/scripts/validate-candidate-control-plane.mjs index ed1e8586..4ffca2a5 100644 --- a/loops/issue-dev-loop/scripts/validate-candidate-control-plane.mjs +++ b/loops/issue-dev-loop/scripts/validate-candidate-control-plane.mjs @@ -11,6 +11,11 @@ const repositoryRoot = path.resolve(loopRoot, '..', '..') const runId = assertNonEmpty(args['run-id'], '--run-id') const baseSha = assertNonEmpty(args['base-sha'], '--base-sha') const headSha = assertNonEmpty(args['head-sha'], '--head-sha') +const durableIssueNumber = args['durable-issue-number'] + ? Number(args['durable-issue-number']) + : null +const durableImplementationCommit = args['durable-implementation-commit'] +const durablePrHead = args['durable-pr-head'] for (const [name, sha] of [ ['baseSha', baseSha], @@ -27,25 +32,52 @@ if (checkedOutHead.stdout.trim() !== headSha || mergeBase.stdout.trim() !== base throw new Error('candidate control-plane validation requires the exact descendant PR head') } -const runPath = path.join(loopRoot, 'logs', 'runs', runId, 'run.json') -const runStats = await lstat(runPath) -if (!runStats.isFile() || runStats.isSymbolicLink()) { - throw new Error('candidate run metadata must be a regular file') +const durableMode = durableIssueNumber !== null +let run +if (durableMode) { + if ( + !Number.isInteger(durableIssueNumber) || + durableIssueNumber < 1 || + ![durableImplementationCommit, durablePrHead].every( + (value) => value === 'none' || /^[0-9a-f]{40}$/i.test(value ?? ''), + ) + ) { + throw new Error('durable candidate metadata is invalid') + } + run = { + runId, + issueNumber: durableIssueNumber, + baseSha, + branch: `codex/issue-${durableIssueNumber}`, + finishedAt: null, + implementationCommit: + durableImplementationCommit === 'none' ? null : durableImplementationCommit, + headSha: durablePrHead === 'none' ? null : durablePrHead, + } +} else { + const runPath = path.join(loopRoot, 'logs', 'runs', runId, 'run.json') + const runStats = await lstat(runPath) + if (!runStats.isFile() || runStats.isSymbolicLink()) { + throw new Error('candidate run metadata must be a regular file') + } + run = await readJson(runPath) } -const run = await readJson(runPath) if ( run.runId !== runId || run.baseSha !== baseSha || run.branch !== `codex/issue-${run.issueNumber}` || run.finishedAt !== null || - !/^[0-9a-f]{40}$/i.test(run.implementationCommit ?? '') || + (run.implementationCommit !== null && + !/^[0-9a-f]{40}$/i.test(run.implementationCommit ?? '')) || (run.headSha !== null && !/^[0-9a-f]{40}$/i.test(run.headSha ?? '')) ) { throw new Error('candidate run metadata does not match the protected diff') } -await execFileAsync('git', ['merge-base', '--is-ancestor', run.implementationCommit, headSha], { - cwd: repositoryRoot, -}) +if (run.implementationCommit) { + await execFileAsync('git', ['merge-base', '--is-ancestor', run.implementationCommit, headSha], { + cwd: repositoryRoot, + }) +} if (run.headSha) { await execFileAsync('git', ['merge-base', '--is-ancestor', run.headSha, headSha], { cwd: repositoryRoot, diff --git a/loops/issue-dev-loop/tests/github-identity-routing.test.mjs b/loops/issue-dev-loop/tests/github-identity-routing.test.mjs index b4e646ed..bc2158d2 100644 --- a/loops/issue-dev-loop/tests/github-identity-routing.test.mjs +++ b/loops/issue-dev-loop/tests/github-identity-routing.test.mjs @@ -1,7 +1,17 @@ import assert from 'node:assert/strict' import { execFile } from 'node:child_process' import { createHash } from 'node:crypto' -import { chmod, cp, mkdir, mkdtemp, readFile, readdir, realpath, writeFile } from 'node:fs/promises' +import { + chmod, + cp, + mkdir, + mkdtemp, + readFile, + readdir, + realpath, + rm, + writeFile, +} from 'node:fs/promises' import os from 'node:os' import path from 'node:path' import test from 'node:test' @@ -18,6 +28,10 @@ import { prepareEvolveRequestPublication, recordEvolveRequestPublication, } from '../scripts/lib/evolve.mjs' +import { + canonicalFinalizationRecord, + finalizationRecordDigest, +} from '../scripts/lib/finalization-proof.mjs' import { resolveExecutable } from '../scripts/lib/github-identity.mjs' const execFileAsync = promisify(execFile) @@ -54,6 +68,11 @@ async function createFixture({ realGit = false, liveDraft = true, ownerFeedback = false, + historicalActivation = false, + remoteCheckpoint = true, + removeLocalRun = false, + terminalFinalization = false, + isolatedWorktree = true, } = {}) { const parent = await mkdtemp(path.join(os.tmpdir(), 'echo-ui-identity-routing-')) const loopRoot = path.join(parent, 'issue-dev-loop') @@ -97,6 +116,7 @@ async function createFixture({ ]) await writeFile(path.join(automationProfile, 'identity'), 'executor-user\n', 'utf8') await writeFile(path.join(reviewerProfile, 'identity'), 'reviewer-user\n', 'utf8') + await writeFile(path.join(parent, 'checkpoint-journal.json'), '[]\n', 'utf8') await Promise.all([ chmod(automationProfile, 0o700), chmod(reviewerProfile, 0o700), @@ -127,8 +147,7 @@ async function createFixture({ const implementationResultDigest = createHash('sha256') .update(implementationSource) .digest('hex') - const implementationResultPath = - 'logs/runs/fixture-run/implementation-result.json' + const implementationResultPath = 'logs/runs/fixture-run/implementation-result.json' const run = { schemaVersion: 1, runId: 'fixture-run', @@ -257,11 +276,7 @@ async function createFixture({ }) await Promise.all([mkdir(runRoot, { recursive: true }), mkdir(briefRoot, { recursive: true })]) await writeFile(path.join(runRoot, 'run.json'), `${JSON.stringify(run)}\n`, 'utf8') - await writeFile( - path.join(runRoot, 'implementation-result.json'), - implementationSource, - 'utf8', - ) + await writeFile(path.join(runRoot, 'implementation-result.json'), implementationSource, 'utf8') await writeFile( path.join(runRoot, 'events.jsonl'), `${events.map((event) => JSON.stringify(event)).join('\n')}\n`, @@ -281,10 +296,75 @@ async function createFixture({ })}\n`, 'utf8', ) + if (remoteCheckpoint) { + await writeFile( + path.join(parent, 'checkpoint-journal.json'), + `${JSON.stringify([ + { + id: 1, + user: { login: 'executor-user' }, + body: publication.body, + html_url: 'https://github.com/example/repo/issues/999#issuecomment-1', + created_at: '2026-07-23T00:05:00.000Z', + }, + ])}\n`, + 'utf8', + ) + } + if (terminalFinalization) { + const finalization = { + schemaVersion: 1, + runId: run.runId, + issueNumber: run.issueNumber, + status: 'cancelled', + startedAt: run.startedAt, + finishedAt: '2026-07-23T00:06:00.000Z', + prUrl: run.prUrl, + headSha, + mergeSha: null, + failureFingerprint: null, + notificationUrl: null, + readyNotificationUrl: null, + readyNotifiedAt: null, + completionNotifiedAt: null, + notificationWebhookStatus: null, + predecessorCheckpointUrl: null, + predecessorCheckpointDigest: null, + pauseStartedAt: null, + notificationNotifiedAt: null, + } + const finalizationBody = [ + ``, + '```json', + canonicalFinalizationRecord(finalization), + '```', + ].join('\n') + const finalizationComment = { + id: 3, + user: { login: 'executor-user' }, + body: finalizationBody, + html_url: 'https://github.com/example/repo/issues/999#issuecomment-3', + created_at: finalization.finishedAt, + } + const journal = JSON.parse( + await readFile(path.join(parent, 'checkpoint-journal.json'), 'utf8'), + ) + await writeFile( + path.join(parent, 'checkpoint-journal.json'), + `${JSON.stringify([...journal, finalizationComment])}\n`, + 'utf8', + ) + await writeFile( + path.join(parent, 'finalization-comment.json'), + `${JSON.stringify(finalizationComment)}\n`, + 'utf8', + ) + } await writeFile( path.join(parent, 'live-pr.json'), `${JSON.stringify({ - state: 'open', + state: terminalFinalization ? 'closed' : 'open', + merged: false, draft: liveDraft, user: { login: 'executor-user' }, base: { ref: 'dev', repo: { full_name: 'example/repo' } }, @@ -316,7 +396,10 @@ const commandArguments = process.argv.slice(2) const loopRootIndex = commandArguments.indexOf('--loop-root') if (loopRootIndex !== -1) commandArguments.splice(loopRootIndex, 2) -if (commandArguments[0] === 'spawn') { +if (commandArguments[0] === 'validate' && ${JSON.stringify(historicalActivation)}) { + process.stderr.write('missing required loop files: scripts/lib/review-publication.mjs\\n') + process.exitCode = 1 +} else if (commandArguments[0] === 'spawn') { if (!['git', 'gh'].includes(commandArguments[1])) { throw new Error('descendant processes cannot run untrusted executables') } @@ -381,6 +464,7 @@ if (commandArguments[0] === 'spawn') { } } else { process.stdout.write(JSON.stringify({ + ...(commandArguments.length > 0 ? { arguments: commandArguments } : {}), config: process.env.GH_CONFIG_DIR, hasGhToken: Boolean(process.env.GH_TOKEN || process.env.GITHUB_TOKEN), gitConfig: [ @@ -421,6 +505,23 @@ if (commandArguments[0] === 'spawn') { `, 'utf8', ) + await writeFile( + path.join(trustedLoopRoot, 'scripts', 'lib', 'validation.mjs'), + `import { consumeHistoricalValidationCapability } from './github-identity.mjs' + +export async function validateLoop({ historicalCapability } = {}) { + consumeHistoricalValidationCapability(historicalCapability) + return { valid: true, historicalTargetCompatibility: true } +} +export function validateFinalizationHistory() {} +`, + 'utf8', + ) + await writeFile( + path.join(trustedLoopRoot, 'scripts', 'validate-candidate-control-plane.mjs'), + `process.stdout.write(JSON.stringify({ valid: true, protectedControlPlane: true }))\n`, + 'utf8', + ) const fakeGh = path.join(binRoot, 'gh') await writeFile( @@ -442,6 +543,14 @@ if [ "$1 $2 $3 $4" != "api user --jq .login" ]; then first_line "$parent_dir/checkpoint-comment.json" exit 0 fi + if [ "$1" = "api" ] && [ "$2" = "repos/example/repo/issues/999/comments?per_page=100&page=1" ]; then + first_line "$parent_dir/checkpoint-journal.json" + exit 0 + fi + if [ "$1" = "api" ] && [ "$2" = "repos/example/repo/issues/999/comments?per_page=100&page=2" ]; then + printf '[]\\n' + exit 0 + fi if [ "$1" = "api" ] && [ "$2" = "repos/example/repo/pulls/106" ]; then first_line "$parent_dir/live-pr.json" exit 0 @@ -458,6 +567,10 @@ if [ "$1 $2 $3 $4" != "api user --jq .login" ]; then first_line "$parent_dir/evolve-comment.json" exit 0 fi + if [ "$1" = "api" ] && [ "$2" = "repos/example/repo/issues/comments/3" ]; then + first_line "$parent_dir/finalization-comment.json" + exit 0 + fi if [ "$1 $2" = "pr review" ]; then echo "comment review published" exit 0 @@ -514,12 +627,32 @@ if [ "$1 $2" = "rev-parse HEAD" ]; then echo "${'b'.repeat(40)}" exit 0 fi -if [ "$1 $2" = "status --porcelain" ]; then +if [ "$1 $2 $3" = "rev-parse --path-format=absolute --git-dir" ]; then + echo ${JSON.stringify( + isolatedWorktree + ? path.join(parent, '.git', 'worktrees', 'issue-123') + : path.join(parent, '.git'), + )} + exit 0 +fi +if [ "$1 $2 $3" = "rev-parse --path-format=absolute --git-common-dir" ]; then + echo ${JSON.stringify(path.join(parent, '.git'))} + exit 0 +fi +if [ "$1 $2 $3" = "status --porcelain=v1 --untracked-files=all" ]; then if [ -f ${JSON.stringify(path.join(parent, 'dirty-git'))} ]; then echo " M src/unsafe.ts" fi exit 0 fi +if [ "$1 $2 $3" = "ls-files -v -z" ]; then + if [ -f ${JSON.stringify(path.join(parent, 'hidden-index-state'))} ]; then + printf "S tracked-file\\0" + else + printf "H tracked-file\\0" + fi + exit 0 +fi if [ "$1" = "merge-base" ]; then exit 0 fi @@ -564,9 +697,15 @@ ${JSON.stringify(process.execPath)} -e 'process.stdout.write(JSON.stringify({arg gh: await realpath(fakeGh), }, executableDigests: { - node: createHash('sha256').update(await readFile(process.execPath)).digest('hex'), - git: createHash('sha256').update(await readFile(gitExecutable)).digest('hex'), - gh: createHash('sha256').update(await readFile(fakeGh)).digest('hex'), + node: createHash('sha256') + .update(await readFile(process.execPath)) + .digest('hex'), + git: createHash('sha256') + .update(await readFile(gitExecutable)) + .digest('hex'), + gh: createHash('sha256') + .update(await readFile(fakeGh)) + .digest('hex'), }, files: await fixtureManifestFiles(trustedBundleRoot), } @@ -579,6 +718,12 @@ ${JSON.stringify(process.execPath)} -e 'process.stdout.write(JSON.stringify({arg realpath(automationProfile), realpath(reviewerProfile), ]) + if (removeLocalRun) { + await rm(path.join(loopRoot, 'logs', 'runs', 'fixture-run'), { + recursive: true, + force: true, + }) + } return { loopRoot, @@ -743,7 +888,7 @@ test('authenticated routing refuses a replaced pinned executable', async () => { ) }) -test('wrapped activation validates both profiles without exposing their paths to loopctl', async () => { +test('wrapped activation first uses full validation without exposing profile paths', async () => { const fixture = await createFixture() const { stdout } = await execFileAsync( routerLauncherPath, @@ -761,11 +906,254 @@ test('wrapped activation validates both profiles without exposing their paths to ], { env: fixture.env }, ) - assert.equal(JSON.parse(stdout).exposesOtherProfiles, false) + const result = JSON.parse(stdout) + assert.equal(result.exposesOtherProfiles, false) + assert.deepEqual(result.arguments, ['validate']) assert.match(await readFile(path.join(fixture.automationProfile, 'probes'), 'utf8'), /probe/) assert.match(await readFile(path.join(fixture.reviewerProfile, 'probes'), 'utf8'), /probe/) }) +test('wrapped activation allows an exact durable target ahead of its committed local head cache', async () => { + const fixture = await createFixture({ historicalActivation: true }) + const runPath = path.join(fixture.loopRoot, 'logs', 'runs', 'fixture-run', 'run.json') + const run = JSON.parse(await readFile(runPath, 'utf8')) + await writeFile(runPath, `${JSON.stringify({ ...run, headSha: null })}\n`, 'utf8') + const { stdout } = await execFileAsync( + routerLauncherPath, + [ + '--loop-root', + fixture.loopRoot, + 'automation', + '--', + process.execPath, + fixture.loopctlPath, + 'validate', + '--activation', + '--loop-root', + fixture.loopRoot, + ], + { env: fixture.env }, + ) + assert.deepEqual(JSON.parse(stdout), { + valid: true, + historicalTargetCompatibility: true, + }) +}) + +test('wrapped activation rejects an exact durable target in the primary checkout', async () => { + const fixture = await createFixture({ + historicalActivation: true, + isolatedWorktree: false, + }) + await assert.rejects( + execFileAsync( + routerLauncherPath, + [ + '--loop-root', + fixture.loopRoot, + 'automation', + '--', + process.execPath, + fixture.loopctlPath, + 'validate', + '--activation', + '--loop-root', + fixture.loopRoot, + ], + { env: fixture.env }, + ), + /historical target validation requires an isolated linked Git worktree/, + ) +}) + +test('wrapped activation rejects historical validation without a remote durable checkpoint', async () => { + const fixture = await createFixture({ + historicalActivation: true, + remoteCheckpoint: false, + }) + await assert.rejects( + execFileAsync( + routerLauncherPath, + [ + '--loop-root', + fixture.loopRoot, + 'automation', + '--', + process.execPath, + fixture.loopctlPath, + 'validate', + '--activation', + '--loop-root', + fixture.loopRoot, + ], + { env: fixture.env }, + ), + /historical target validation requires the exact remote durable active checkpoint/, + ) +}) + +test('wrapped activation can select an exact durable checkpoint without local run cache', async () => { + const fixture = await createFixture({ + historicalActivation: true, + removeLocalRun: true, + }) + const { stdout } = await execFileAsync( + routerLauncherPath, + [ + '--loop-root', + fixture.loopRoot, + 'automation', + '--', + process.execPath, + fixture.loopctlPath, + 'validate', + '--activation', + '--loop-root', + fixture.loopRoot, + ], + { env: fixture.env }, + ) + assert.deepEqual(JSON.parse(stdout), { + valid: true, + historicalTargetCompatibility: true, + }) +}) + +test('wrapped activation excludes checkpoints superseded by durable finalization', async () => { + const fixture = await createFixture({ + historicalActivation: true, + removeLocalRun: true, + terminalFinalization: true, + }) + await assert.rejects( + execFileAsync( + routerLauncherPath, + [ + '--loop-root', + fixture.loopRoot, + 'automation', + '--', + process.execPath, + fixture.loopctlPath, + 'validate', + '--activation', + '--loop-root', + fixture.loopRoot, + ], + { env: fixture.env }, + ), + /historical target validation requires the exact remote durable active checkpoint/, + ) +}) + +test('wrapped activation rejects local active state that differs from its durable checkpoint', async () => { + const fixture = await createFixture({ historicalActivation: true }) + const runPath = path.join(fixture.loopRoot, 'logs', 'runs', 'fixture-run', 'run.json') + const run = JSON.parse(await readFile(runPath, 'utf8')) + await writeFile( + runPath, + `${JSON.stringify({ + ...run, + issueNumber: 124, + branch: 'codex/issue-124', + })}\n`, + 'utf8', + ) + await assert.rejects( + execFileAsync( + routerLauncherPath, + [ + '--loop-root', + fixture.loopRoot, + 'automation', + '--', + process.execPath, + fixture.loopctlPath, + 'validate', + '--activation', + '--loop-root', + fixture.loopRoot, + ], + { env: fixture.env }, + ), + /historical target validation requires the exact remote durable active checkpoint/, + ) +}) + +test('wrapped activation rejects tracked files hidden by Git index flags', async () => { + const fixture = await createFixture({ historicalActivation: true }) + await writeFile( + path.join(path.dirname(fixture.loopRoot), 'hidden-index-state'), + 'skip-worktree\n', + 'utf8', + ) + await assert.rejects( + execFileAsync( + routerLauncherPath, + [ + '--loop-root', + fixture.loopRoot, + 'automation', + '--', + process.execPath, + fixture.loopctlPath, + 'validate', + '--activation', + '--loop-root', + fixture.loopRoot, + ], + { env: fixture.env }, + ), + /historical target validation rejects index concealment/, + ) +}) + +test('wrapped activation keeps full validation when there is no active run', async () => { + const fixture = await createFixture({ activeRun: false, recordedPr: false }) + const { stdout } = await execFileAsync( + routerLauncherPath, + [ + '--loop-root', + fixture.loopRoot, + 'automation', + '--', + process.execPath, + fixture.loopctlPath, + 'validate', + '--activation', + '--loop-root', + fixture.loopRoot, + ], + { env: fixture.env }, + ) + const result = JSON.parse(stdout) + assert.equal(result.exposesOtherProfiles, false) + assert.deepEqual(result.arguments, ['validate']) +}) + +test('target compatibility validation cannot be requested outside wrapped activation', async () => { + const fixture = await createFixture() + await assert.rejects( + execFileAsync( + routerLauncherPath, + [ + '--loop-root', + fixture.loopRoot, + 'automation', + '--', + process.execPath, + fixture.loopctlPath, + 'validate', + '--target-compatibility', + '--loop-root', + fixture.loopRoot, + ], + { env: fixture.env }, + ), + /target compatibility validation is reserved to wrapped activation/, + ) +}) + test('reviewer role refuses a profile authenticated as the wrong account', async () => { const fixture = await createFixture() await writeFile(path.join(fixture.reviewerProfile, 'identity'), 'owner-user\n', 'utf8') @@ -911,15 +1299,7 @@ test('checkpoint publication is reserved to a semantically attested current run' ] const allowed = await execFileAsync( process.execPath, - [ - routerPath, - '--loop-root', - fixture.loopRoot, - 'automation', - '--', - 'gh', - ...publishArguments, - ], + [routerPath, '--loop-root', fixture.loopRoot, 'automation', '--', 'gh', ...publishArguments], { env: fixture.env }, ) assert.match(allowed.stdout, /issues\/999\/comments/) @@ -1485,15 +1865,7 @@ test('PR and issue mutations reject unsafe shapes and targets', async () => { ['automation', ['pr', 'comment', '106', '--body', 'Missing repository']], [ 'automation', - [ - 'pr', - 'comment', - '106', - '--repo', - 'example/repo', - '--body', - '@owner **pr_completed**', - ], + ['pr', 'comment', '106', '--repo', 'example/repo', '--body', '@owner **pr_completed**'], ], [ 'automation', @@ -2058,7 +2430,7 @@ if [ "$1 $2" = "rev-parse HEAD" ]; then echo "${'b'.repeat(40)}" exit 0 fi -if [ "$1 $2" = "status --porcelain" ] || [ "$1" = "merge-base" ] || [ "$1 $2" = "diff --name-status" ]; then +if [ "$1 $2 $3" = "status --porcelain=v1 --untracked-files=all" ] || [ "$1" = "merge-base" ] || [ "$1 $2" = "diff --name-status" ]; then exit 0 fi if [ "$1 $2 $3" = "remote get-url origin" ]; then diff --git a/loops/issue-dev-loop/tests/runtime.test.mjs b/loops/issue-dev-loop/tests/runtime.test.mjs index 48411324..5841f917 100644 --- a/loops/issue-dev-loop/tests/runtime.test.mjs +++ b/loops/issue-dev-loop/tests/runtime.test.mjs @@ -3,6 +3,7 @@ import { execFile } from 'node:child_process' import { createHash } from 'node:crypto' import { chmod, + cp, mkdtemp, mkdir, readFile, @@ -56,10 +57,18 @@ import { validateLoop, } from '../scripts/runtime.mjs' import { observeOwnerApprovedMerge } from '../scripts/lib/owner-gate.mjs' -import { assertCredentialProfileIsolation } from '../scripts/lib/github-identity.mjs' +import { + assertCredentialProfileIsolation, +} from '../scripts/lib/github-identity.mjs' import { verifyTerminalExternalProof } from '../scripts/lib/finalization-proof.mjs' -import { validateFinalizationHistory } from '../scripts/lib/validation.mjs' -import { checkpointPublicationBody } from '../scripts/lib/checkpoint-proof.mjs' +import { + historicalWorkflowIsLowPrivilege, + validateFinalizationHistory, +} from '../scripts/lib/validation.mjs' +import { + checkpointPublicationBody, + checkpointWorktreeHead, +} from '../scripts/lib/checkpoint-proof.mjs' const bypassCheckpointVerifier = async () => {} const createNotification = (options) => @@ -731,10 +740,7 @@ async function writeFixtureFinalization({ ) : null const checkpointEvent = failureStatus - ? (await readFile( - path.join(loopRoot, 'logs', 'runs', runId, 'events.jsonl'), - 'utf8', - )) + ? (await readFile(path.join(loopRoot, 'logs', 'runs', runId, 'events.jsonl'), 'utf8')) .split('\n') .filter(Boolean) .map((line) => JSON.parse(line)) @@ -742,8 +748,7 @@ async function writeFixtureFinalization({ : null const pauseStartedAt = failureStatus ? checkpointRecord.events.findLast( - (event) => - event.type === 'run_status_changed' && event.status === 'waiting_for_owner', + (event) => event.type === 'run_status_changed' && event.status === 'waiting_for_owner', )?.timestamp : null const record = { @@ -1006,11 +1011,7 @@ test('candidate control-plane validation permits run evidence but rejects verifi await git('add', '.') await git('commit', '-m', 'run evidence') const permittedHead = (await git('rev-parse', 'HEAD')).stdout.trim() - const validator = path.join( - repositoryLoopRoot, - 'scripts', - 'validate-candidate-control-plane.mjs', - ) + const validator = path.join(repositoryLoopRoot, 'scripts', 'validate-candidate-control-plane.mjs') const permitted = await execFileAsync(process.execPath, [ validator, '--loop-root', @@ -1100,11 +1101,7 @@ test('candidate control-plane validation permits run evidence but rejects verifi '# Candidate-controlled adapter\n', /\.agents\/skills\/issue-dev-loop\/SKILL\.md/, ], - [ - 'vercel.json', - '{"git":{"deploymentEnabled":{"codex/issue-*":true}}}\n', - /vercel\.json/, - ], + ['vercel.json', '{"git":{"deploymentEnabled":{"codex/issue-*":true}}}\n', /vercel\.json/], ]) { const target = path.join(repository, relativePath) await mkdir(path.dirname(target), { recursive: true }) @@ -1129,6 +1126,245 @@ test('candidate control-plane validation permits run evidence but rejects verifi } }) +test('durable candidate validation supports pre-implementation and later repair heads', async () => { + const parent = await mkdtemp(path.join(os.tmpdir(), 'echo-ui-durable-candidate-test-')) + const repository = path.join(parent, 'repository') + const loopRoot = path.join(repository, 'loops', 'issue-dev-loop') + const validator = path.join(repositoryLoopRoot, 'scripts', 'validate-candidate-control-plane.mjs') + const runId = 'run-issue-321' + const git = async (...args) => execFileAsync('git', args, { cwd: repository }) + await mkdir(path.join(repository, 'src'), { recursive: true }) + await mkdir(loopRoot, { recursive: true }) + await git('init', '--initial-branch=dev') + await git('config', 'user.name', 'Loop Test') + await git('config', 'user.email', 'loop-test@example.invalid') + await writeFile(path.join(repository, 'src', 'feature.js'), 'export const value = 0\n', 'utf8') + await git('add', '.') + await git('commit', '-m', 'base') + await git('switch', '-c', 'codex/issue-321') + const baseSha = (await git('rev-parse', 'HEAD')).stdout.trim() + + const preImplementation = await execFileAsync(process.execPath, [ + validator, + '--loop-root', + loopRoot, + '--run-id', + runId, + '--base-sha', + baseSha, + '--head-sha', + baseSha, + '--durable-issue-number', + '321', + '--durable-implementation-commit', + 'none', + '--durable-pr-head', + 'none', + ]) + assert.equal(JSON.parse(preImplementation.stdout).valid, true) + + await writeFile(path.join(repository, 'src', 'feature.js'), 'export const value = 1\n', 'utf8') + await git('add', 'src/feature.js') + await git('commit', '-m', 'first PR head') + const oldPrHead = (await git('rev-parse', 'HEAD')).stdout.trim() + await writeFile(path.join(repository, 'src', 'feature.js'), 'export const value = 2\n', 'utf8') + await git('add', 'src/feature.js') + await git('commit', '-m', 'owner feedback implementation') + const repairCommit = (await git('rev-parse', 'HEAD')).stdout.trim() + + const repair = await execFileAsync(process.execPath, [ + validator, + '--loop-root', + loopRoot, + '--run-id', + runId, + '--base-sha', + baseSha, + '--head-sha', + repairCommit, + '--durable-issue-number', + '321', + '--durable-implementation-commit', + repairCommit, + '--durable-pr-head', + oldPrHead, + ]) + assert.equal(JSON.parse(repair.stdout).valid, true) + assert.equal( + checkpointWorktreeHead({ + run: { baseSha }, + events: [ + { + type: 'pr_published', + payload: { headSha: oldPrHead }, + }, + { + type: 'implementation_completed', + status: 'passed', + payload: { commitSha: repairCommit }, + }, + ], + }), + repairCommit, + ) +}) + +test('default checkpoint restore ignores hidden-untracked config and rejects concealed index state', async () => { + const parent = await mkdtemp(path.join(os.tmpdir(), 'echo-ui-restore-cleanliness-test-')) + const repository = path.join(parent, 'repository') + const worktree = path.join(parent, 'worktree') + const loopRoot = path.join(worktree, 'loops', 'issue-dev-loop') + const git = async (cwd, ...args) => execFileAsync('git', args, { cwd }) + try { + await mkdir(path.join(repository, 'loops', 'issue-dev-loop'), { recursive: true }) + await writeFile(path.join(repository, 'tracked.txt'), 'tracked\n', 'utf8') + await writeFile( + path.join(repository, 'loops', 'issue-dev-loop', '.gitkeep'), + '', + 'utf8', + ) + await git(repository, 'init', '--initial-branch=dev') + await git(repository, 'config', 'user.name', 'Loop Test') + await git(repository, 'config', 'user.email', 'loop-test@example.invalid') + await git(repository, 'add', '.') + await git(repository, 'commit', '-m', 'base') + const baseSha = (await git(repository, 'rev-parse', 'HEAD')).stdout.trim() + await git(repository, 'worktree', 'add', '-b', 'codex/issue-444', worktree, baseSha) + + const startedAt = '2026-07-24T00:00:00.000Z' + const record = { + schemaVersion: 1, + kind: 'active-checkpoint', + run: { + schemaVersion: 1, + runId: 'restore-untracked-run', + issueNumber: 444, + issueTitle: 'Restore exact durable worktree', + issueUrl: 'https://github.com/codeacme17/echo-ui/issues/444', + baseBranch: 'dev', + baseSha, + branch: 'codex/issue-444', + status: 'running', + startedAt, + finishedAt: null, + prUrl: null, + headSha: null, + mergeSha: null, + issueSnapshot: { + title: 'Restore exact durable worktree', + body: 'Fixture', + labels: ['codex-ready'], + url: 'https://github.com/codeacme17/echo-ui/issues/444', + capturedAt: startedAt, + }, + briefDigest: null, + uiEvidenceRequired: false, + implementationCommit: null, + }, + briefSource: '', + events: [ + { + schemaVersion: 1, + runId: 'restore-untracked-run', + type: 'loop_started', + timestamp: startedAt, + status: 'running', + payload: { issueNumber: 444, branch: 'codex/issue-444' }, + }, + ], + artifacts: [], + updatedAt: startedAt, + } + const checkpoint = { record, commentUrl: null, createdAt: startedAt } + + const restoredPreImplementation = await restoreActiveCheckpoint({ loopRoot, checkpoint }) + assert.equal(restoredPreImplementation.implementationCommit, null) + for (const directory of ['logs', 'handoffs', 'screen-shots', 'evidence']) { + await rm(path.join(loopRoot, directory), { recursive: true, force: true }) + } + + await git(worktree, 'config', 'status.showUntrackedFiles', 'no') + await writeFile(path.join(worktree, 'hidden-untracked.txt'), 'not clean\n', 'utf8') + const configuredStatus = await git(worktree, 'status', '--porcelain') + assert.equal(configuredStatus.stdout, '') + await assert.rejects( + restoreActiveCheckpoint({ loopRoot, checkpoint }), + /clean isolated worktree/, + ) + + await rm(path.join(worktree, 'hidden-untracked.txt')) + await git(worktree, 'update-index', '--skip-worktree', 'tracked.txt') + await assert.rejects( + restoreActiveCheckpoint({ loopRoot, checkpoint }), + /index concealment/, + ) + + await git(worktree, 'update-index', '--no-skip-worktree', 'tracked.txt') + await writeFile(path.join(worktree, 'tracked.txt'), 'repaired\n', 'utf8') + await git(worktree, 'add', 'tracked.txt') + await git(worktree, 'commit', '-m', 'repair implementation') + const repairCommit = (await git(worktree, 'rev-parse', 'HEAD')).stdout.trim() + const repairBrief = 'Repair the issue and retain the durable resume boundary.\n' + const repairBriefDigest = createHash('sha256').update(repairBrief).digest('hex') + const repairResult = { + schemaVersion: 1, + runId: record.run.runId, + agent: '$implement', + invocationId: 'repair-invocation', + startedAt: '2026-07-24T00:01:00.000Z', + finishedAt: '2026-07-24T00:02:00.000Z', + briefDigest: repairBriefDigest, + commitSha: repairCommit, + checks: [{ command: 'pnpm verify', status: 'passed' }], + } + const repairResultSource = `${JSON.stringify(repairResult)}\n` + const repairResultDigest = createHash('sha256') + .update(repairResultSource) + .digest('hex') + const repairResultPath = `logs/runs/${record.run.runId}/repair-result.json` + const repairFinishedAt = repairResult.finishedAt + const repairRecord = structuredClone(record) + repairRecord.run.implementationCommit = repairCommit + repairRecord.run.briefDigest = repairBriefDigest + repairRecord.briefSource = repairBrief + repairRecord.events.push({ + schemaVersion: 1, + runId: record.run.runId, + type: 'implementation_completed', + timestamp: repairFinishedAt, + status: 'passed', + payload: { + agent: '$implement', + invocationId: repairResult.invocationId, + startedAt: repairResult.startedAt, + finishedAt: repairFinishedAt, + briefDigest: repairBriefDigest, + commitSha: repairCommit, + resultPath: repairResultPath, + resultDigest: repairResultDigest, + }, + }) + repairRecord.artifacts.push({ + path: repairResultPath, + sha256: repairResultDigest, + source: repairResultSource, + }) + repairRecord.updatedAt = repairFinishedAt + + const restoredRepair = await restoreActiveCheckpoint({ + loopRoot, + checkpoint: { + record: repairRecord, + commentUrl: null, + createdAt: repairFinishedAt, + }, + }) + assert.equal(restoredRepair.implementationCommit, repairCommit) + } finally { + await rm(parent, { recursive: true, force: true }) + } +}) + test('review publication digest excludes assigned review URLs but binds review content', () => { const review = { schemaVersion: 1, @@ -1543,17 +1779,18 @@ test('UI draft PR requires embedded before and after screenshots pinned to its e test('evidence workflow marks volume checkouts safe before Git-backed verification', async () => { const workflow = await readFile( - path.resolve(repositoryLoopRoot, '..', '..', '.github', 'workflows', 'issue-dev-loop-evidence.yml'), + path.resolve( + repositoryLoopRoot, + '..', + '..', + '.github', + 'workflows', + 'issue-dev-loop-evidence.yml', + ), 'utf8', ) - assert.match( - workflow, - /git config --global --add safe\.directory \/work; pnpm verify/, - ) - assert.match( - workflow, - /git config --global --add safe\.directory \/work; pnpm test/, - ) + assert.match(workflow, /git config --global --add safe\.directory \/work; pnpm verify/) + assert.match(workflow, /git config --global --add safe\.directory \/work; pnpm test/) }) test('automation identity cannot overlap the repository owner', async () => { @@ -2770,8 +3007,7 @@ test('forged local blocked finalization cannot release an issue claim', async () readyNotifiedAt: null, completionNotifiedAt: null, notificationWebhookStatus: null, - predecessorCheckpointUrl: - 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-8801', + predecessorCheckpointUrl: 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-8801', predecessorCheckpointDigest: 'a'.repeat(64), pauseStartedAt: '2030-01-01T00:00:00.000Z', notificationNotifiedAt: '2030-01-01T00:01:00.000Z', @@ -3052,9 +3288,7 @@ test('review gate verifies published findings and classified replies', async () : [ 'PASS', ...(includePriorFinding - ? [ - 'Resolved RVW-1-1-1 and RVW-1-1-2 with published executor responses.', - ] + ? ['Resolved RVW-1-1-1 and RVW-1-1-2 with published executor responses.'] : []), ``, ``, @@ -3621,10 +3855,9 @@ test('canonical GitHub notification persists before a bounded webhook mirror', a ) assert.equal(persisted.delivery.github, 'delivered') assert.match(persisted.delivery.webhook, /^failed: timed out/) - const events = (await readFile( - path.join(loopRoot, 'logs', 'runs', run.runId, 'events.jsonl'), - 'utf8', - )) + const events = ( + await readFile(path.join(loopRoot, 'logs', 'runs', run.runId, 'events.jsonl'), 'utf8') + ) .trim() .split('\n') .map((line) => JSON.parse(line)) @@ -3990,10 +4223,7 @@ test('three matching failures make a fresh evolve session due', async () => { }) assert.deepEqual(tombstoned.durableRunIds, []) await finalizeRun(finalizationOptions) - const restoredHistory = (await readFile( - path.join(loopRoot, 'logs', 'index.jsonl'), - 'utf8', - )) + const restoredHistory = (await readFile(path.join(loopRoot, 'logs', 'index.jsonl'), 'utf8')) .split('\n') .filter(Boolean) .map((line) => JSON.parse(line)) @@ -4027,8 +4257,7 @@ test('three matching failures make a fresh evolve session due', async () => { test('fresh worktrees rebuild finalization history and evolve metrics from GitHub journal', async () => { const { loopRoot } = await createFixture() const durableRunId = '20260722T120000Z-issue-205-journal' - const notificationUrl = - 'https://github.com/codeacme17/echo-ui/issues/205#issuecomment-8802' + const notificationUrl = 'https://github.com/codeacme17/echo-ui/issues/205#issuecomment-8802' const predecessorCheckpointUrl = 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-8801' const pauseStartedAt = '2026-07-22T12:30:00.000Z' @@ -4223,8 +4452,7 @@ test('fresh worktrees rebuild finalization history and evolve metrics from GitHu payload: { notificationType: 'clarification_required', delivery: { github: 'delivered' }, - deliveryUrl: - 'https://github.com/codeacme17/echo-ui/issues/205#issuecomment-8811', + deliveryUrl: 'https://github.com/codeacme17/echo-ui/issues/205#issuecomment-8811', targetUrl: 'https://github.com/codeacme17/echo-ui/issues/205', }, }, @@ -4239,8 +4467,7 @@ test('fresh worktrees rebuild finalization history and evolve metrics from GitHu ) latestCheckpointRecord.updatedAt = '2026-07-22T13:20:00.000Z' const latestCheckpointDigest = checkpointDigest(latestCheckpointRecord) - const latestCheckpointUrl = - 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-8810' + const latestCheckpointUrl = 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-8810' const latestCheckpointBody = [ ``, '```json', @@ -4273,10 +4500,7 @@ test('fresh worktrees rebuild finalization history and evolve metrics from GitHu latestActiveCheckpoints: allActive.activeCheckpoints, }) assert.deepEqual(superseded.durableRunIds, []) - const supersededHistory = await readFile( - path.join(loopRoot, 'logs', 'index.jsonl'), - 'utf8', - ) + const supersededHistory = await readFile(path.join(loopRoot, 'logs', 'index.jsonl'), 'utf8') assert.match(supersededHistory, /run_finalization_unverified/) const supersededMetrics = await getEvolveStatus({ loopRoot }) assert.equal(supersededMetrics.finalizedRuns, 0) @@ -4302,10 +4526,7 @@ test('fresh worktrees rebuild finalization history and evolve metrics from GitHu }) assert.equal(restored.reconciled, 1) assert.deepEqual(restored.durableRunIds, [record.runId]) - const restoredHistory = (await readFile( - path.join(loopRoot, 'logs', 'index.jsonl'), - 'utf8', - )) + const restoredHistory = (await readFile(path.join(loopRoot, 'logs', 'index.jsonl'), 'utf8')) .split('\n') .filter(Boolean) .map((line) => JSON.parse(line)) @@ -4341,10 +4562,7 @@ test('reconciliation excludes local finalization rows without a durable journal })) await writeFile( indexPath, - `${[ - { schemaVersion: 1, event: 'loop_initialized' }, - ...forgedRows, - ] + `${[{ schemaVersion: 1, event: 'loop_initialized' }, ...forgedRows] .map((entry) => JSON.stringify(entry)) .join('\n')}\n`, 'utf8', @@ -4588,6 +4806,89 @@ test('fresh worktrees restore active checkpoints and trigger resumable work', as assert.equal(detected.hasWork, true) assert.equal(detected.workType, 'resume') assert.equal(detected.runId, run.runId) + + const repairRecord = structuredClone(prepared.record) + const oldPrHead = 'a'.repeat(40) + const repairCommit = 'b'.repeat(40) + const repairBrief = 'repair implementation brief\n' + const repairBriefDigest = createHash('sha256').update(repairBrief).digest('hex') + const repairResult = { + schemaVersion: 1, + runId: run.runId, + agent: '$implement', + invocationId: 'repair-invocation', + startedAt: '2026-07-22T12:03:00.000Z', + finishedAt: '2026-07-22T12:04:00.000Z', + briefDigest: repairBriefDigest, + commitSha: repairCommit, + checks: [{ command: 'pnpm verify', status: 'passed' }], + } + const repairResultSource = `${JSON.stringify(repairResult)}\n` + const repairResultDigest = createHash('sha256') + .update(repairResultSource) + .digest('hex') + const repairResultPath = `logs/runs/${run.runId}/repair-result.json` + repairRecord.run.prUrl = 'https://github.com/codeacme17/echo-ui/pull/206' + repairRecord.run.headSha = oldPrHead + repairRecord.run.implementationCommit = repairCommit + repairRecord.run.briefDigest = repairBriefDigest + repairRecord.briefSource = repairBrief + repairRecord.events.push( + { + schemaVersion: 1, + runId: run.runId, + type: 'pr_published', + timestamp: '2026-07-22T12:02:30.000Z', + status: 'draft', + payload: { + prUrl: repairRecord.run.prUrl, + headSha: oldPrHead, + baseBranch: 'dev', + branch: repairRecord.run.branch, + }, + }, + { + schemaVersion: 1, + runId: run.runId, + type: 'implementation_completed', + timestamp: repairResult.finishedAt, + status: 'passed', + payload: { + agent: '$implement', + invocationId: repairResult.invocationId, + startedAt: repairResult.startedAt, + finishedAt: repairResult.finishedAt, + briefDigest: repairBriefDigest, + commitSha: repairCommit, + resultPath: repairResultPath, + resultDigest: repairResultDigest, + }, + }, + ) + repairRecord.artifacts.push({ + path: repairResultPath, + sha256: repairResultDigest, + source: repairResultSource, + }) + repairRecord.updatedAt = repairResult.finishedAt + const repairCheckpoint = { + record: repairRecord, + commentUrl: 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-9912', + createdAt: '2026-07-22T12:04:30.000Z', + } + const repairDetection = await detectWork({ + loopRoot, + now: new Date('2026-07-22T12:05:00.000Z'), + reconcileJournal: async () => ({ activeCheckpoints: [repairCheckpoint] }), + }) + assert.equal(repairDetection.expectedHeadSha, repairCommit) + await restoreActiveCheckpoint({ + loopRoot, + checkpoint: repairCheckpoint, + workspaceValidator: async ({ record }) => { + assert.equal(checkpointWorktreeHead(record), repairDetection.expectedHeadSha) + }, + }) }) test('checkpoint publication rejects an unattested implementation boundary', async () => { @@ -4736,8 +5037,7 @@ test('evolve completion rejects an unrelated historical owner-merged PR', async await recordEvolveRequestPublication({ loopRoot, requestId, - commentUrl: - 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-7000', + commentUrl: 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-7000', githubApi: async () => ({ user: { login: 'echo-ui-loop[bot]' }, body: preparedRequest.body, @@ -4758,12 +5058,12 @@ test('evolve completion rejects an unrelated historical owner-merged PR', async } if (endpoint.includes('/reviews')) { return [ - { - user: { login: 'codeacme17' }, - state: 'APPROVED', - commit_id: 'a'.repeat(40), - }, - ] + { + user: { login: 'codeacme17' }, + state: 'APPROVED', + commit_id: 'a'.repeat(40), + }, + ] } if (endpoint.includes('/timeline')) { return [ @@ -4792,10 +5092,8 @@ test('evolve completion rejects an unrelated historical owner-merged PR', async test('fresh worktrees rebuild pending and completed evolve state from the durable journal', async () => { const { loopRoot } = await createFixture() const requestId = 'EVL-000010-TEN-FINALIZED-RUNS' - const requestUrl = - 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-7100' - const completionUrl = - 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-7101' + const requestUrl = 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-7100' + const completionUrl = 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-7101' const prUrl = 'https://github.com/codeacme17/echo-ui/pull/710' const headSha = '7'.repeat(40) const mergeSha = '8'.repeat(40) @@ -4949,8 +5247,7 @@ test('fresh worktrees rebuild pending and completed evolve state from the durabl await writeFile(metricsPath, `${JSON.stringify(initialMetrics)}\n`, 'utf8') await rm(requestPath) - const duplicateRequestUrl = - 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-7200' + const duplicateRequestUrl = 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-7200' const duplicateCompletionUrl = 'https://github.com/codeacme17/echo-ui/issues/999#issuecomment-7201' const reconciled = await reconcileEvolveJournal({ @@ -5020,14 +5317,170 @@ test('repository loop package satisfies its structural invariants', async () => assert.equal(result.valid, true) }) +test('historical workflow parsing is fail-closed without exposing reduced validation', async () => { + const parent = await mkdtemp(path.join(os.tmpdir(), 'echo-ui-historical-target-')) + const repositoryRoot = path.join(parent, 'repository') + const historicalLoopRoot = path.join(repositoryRoot, 'loops', 'issue-dev-loop') + try { + await Promise.all([ + cp(repositoryLoopRoot, historicalLoopRoot, { recursive: true }), + cp( + path.resolve(repositoryLoopRoot, '..', '_shared'), + path.join(repositoryRoot, 'loops', '_shared'), + { recursive: true }, + ), + cp( + path.resolve(repositoryLoopRoot, '..', '..', '.codex'), + path.join(repositoryRoot, '.codex'), + { recursive: true }, + ), + mkdir(path.join(repositoryRoot, '.github', 'workflows'), { recursive: true }), + ]) + await cp( + path.resolve( + repositoryLoopRoot, + '..', + '..', + '.github', + 'workflows', + 'issue-dev-loop-evidence.yml', + ), + path.join(repositoryRoot, '.github', 'workflows', 'issue-dev-loop-evidence.yml'), + ) + const workflowPath = path.join( + repositoryRoot, + '.github', + 'workflows', + 'issue-dev-loop-evidence.yml', + ) + await writeFile( + workflowPath, + (await readFile(workflowPath, 'utf8')).replace( + '--baseline-status', + '--historical-baseline-status', + ), + 'utf8', + ) + await assert.rejects( + execFileAsync( + process.execPath, + [ + path.join(historicalLoopRoot, 'scripts', 'loopctl.mjs'), + 'validate', + '--target-compatibility', + '--loop-root', + historicalLoopRoot, + ], + { cwd: repositoryRoot }, + ), + /target compatibility validation is reserved to wrapped activation/, + ) + await Promise.all([ + rm(path.join(historicalLoopRoot, 'scripts', 'lib', 'review-publication.mjs')), + rm(path.join(historicalLoopRoot, 'scripts', 'publish-review.mjs')), + ]) + + await assert.rejects( + validateLoop({ loopRoot: historicalLoopRoot }), + /missing required loop files: .*review-publication\.mjs.*publish-review\.mjs/, + ) + await assert.rejects( + validateLoop({ + loopRoot: historicalLoopRoot, + targetCompatibility: true, + }), + /missing required loop files: .*review-publication\.mjs.*publish-review\.mjs/, + ) + const historicalWorkflow = await readFile(workflowPath, 'utf8') + assert.equal(historicalWorkflowIsLowPrivilege(historicalWorkflow), true) + const unsafeWorkflows = [ + `${historicalWorkflow} + unsafe: + permissions: write-all + runs-on: ubuntu-latest + steps: [] +`, + `${historicalWorkflow} + unsafe: + "permissions": write-all + runs-on: ubuntu-latest + steps: [] +`, + historicalWorkflow.replace(' pull_request:\n', ' pull_request:\n workflow_dispatch:\n'), + historicalWorkflow.replace(' pull_request:\n', ' "pull_request_target":\n'), + historicalWorkflow.replace( + 'permissions:\n contents: read\n', + 'permissions:\n contents: read\npermissions:\n contents: read\n', + ), + historicalWorkflow.replace('permissions:\n', 'permissions: &shared_permissions\n'), + historicalWorkflow.replace( + 'jobs:\n', + 'jobs:\n unsafe: {permissions: write-all, runs-on: ubuntu-latest, steps: []}\n', + ), + historicalWorkflow.replace( + 'jobs:\n', + `jobs: + unsafe: + {permissions: write-all, runs-on: ubuntu-latest, steps: []} +`, + ), + historicalWorkflow.replace( + ' runs-on: ubuntu-latest\n', + ' &permission_key permissions: write-all\n runs-on: ubuntu-latest\n', + ), + historicalWorkflow.replace( + ' runs-on: ubuntu-latest\n', + ' !!str permissions: write-all\n runs-on: ubuntu-latest\n', + ), + historicalWorkflow.replace( + ' runs-on: ubuntu-latest\n', + ' - permissions: write-all\n runs-on: ubuntu-latest\n', + ), + historicalWorkflow.replace( + ' runs-on: ubuntu-latest\n', + ' runs-on: ubuntu-latest\r permissions: write-all\n', + ), + historicalWorkflow.replace( + ' runs-on: ubuntu-latest\n', + ' runs-on: ubuntu-latest\u0085 permissions: write-all\n', + ), + historicalWorkflow.replace( + ' runs-on: ubuntu-latest\n', + ' runs-on: ubuntu-latest\u2028 permissions: write-all\n', + ), + historicalWorkflow.replace( + ' runs-on: ubuntu-latest\n', + ' runs-on: ubuntu-latest\u2029 permissions: write-all\n', + ), + historicalWorkflow.replace('ubuntu-latest', 'ubuntu-latest\u0000'), + `${historicalWorkflow} +!!str permissions: write-all +`, + `${historicalWorkflow} +@not-yaml +`, + ] + for (const unsafeWorkflow of unsafeWorkflows) { + assert.equal(historicalWorkflowIsLowPrivilege(unsafeWorkflow), false) + } + await assert.rejects( + async () => + validateLoop({ + loopRoot: historicalLoopRoot, + historicalCapability: {}, + }), + /historical target validation requires an authorized router capability/, + ) + } finally { + await rm(parent, { recursive: true, force: true }) + } +}) + test('repository activation verifies both configured GitHub profiles', async () => { const profileRoot = await mkdtemp(path.join(os.tmpdir(), 'echo-ui-activation-profiles-')) const automationProfile = path.join(profileRoot, 'automation') const reviewerProfile = path.join(profileRoot, 'reviewer') - await Promise.all([ - mkdir(automationProfile), - mkdir(reviewerProfile), - ]) + await Promise.all([mkdir(automationProfile), mkdir(reviewerProfile)]) await Promise.all([chmod(automationProfile, 0o700), chmod(reviewerProfile, 0o700)]) const [canonicalAutomationProfile, canonicalReviewerProfile] = await Promise.all([ realpath(automationProfile), @@ -5036,9 +5489,7 @@ test('repository activation verifies both configured GitHub profiles', async () const environment = { ECHO_UI_LOOP_AUTOMATION_GH_CONFIG_DIR: canonicalAutomationProfile, ECHO_UI_LOOP_REVIEWER_GH_CONFIG_DIR: canonicalReviewerProfile, - ECHO_UI_LOOP_UNTRUSTED_ROOTS: JSON.stringify([ - path.resolve(repositoryLoopRoot, '..', '..'), - ]), + ECHO_UI_LOOP_UNTRUSTED_ROOTS: JSON.stringify([path.resolve(repositoryLoopRoot, '..', '..')]), } const observedProfiles = [] const identityCommand = async (_command, _args, options) => {