Skip to content
Merged
2 changes: 1 addition & 1 deletion loops/issue-dev-loop/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <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.
Expand Down
2 changes: 2 additions & 0 deletions loops/issue-dev-loop/references/github-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
45 changes: 42 additions & 3 deletions loops/issue-dev-loop/scripts/lib/active-journal.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
checkpointJournalConfiguration,
checkpointPublicationBody,
checkpointRecordDigest,
checkpointWorktreeHead,
parseCheckpointRecord,
validateCheckpointRecord,
verifyPublishedCheckpoint,
Expand Down Expand Up @@ -210,30 +211,68 @@ 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')
}
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({
Expand Down
14 changes: 14 additions & 0 deletions loops/issue-dev-loop/scripts/lib/checkpoint-proof.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
19 changes: 17 additions & 2 deletions loops/issue-dev-loop/scripts/lib/finalization-journal.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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'))
Expand Down
Loading