Skip to content

Commit a3be610

Browse files
committed
fix(loop): resume historical active targets
1 parent e85f9b3 commit a3be610

7 files changed

Lines changed: 311 additions & 234 deletions

File tree

loops/issue-dev-loop/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Run exactly one bounded issue cycle. Treat [`LOOP.md`](./LOOP.md) as the constit
77
## Start safely
88

99
1. Read `LOOP.md`, `state.md`, and `dependencies.md` completely.
10-
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.
10+
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. When the target contains a durable active run created by an older owner-merged control plane, the launcher alone selects historical-target compatibility validation: installed runtime files remain hash-verified in the trusted bundle, while the target is checked for its stable state files, owner channel, JSON history, and evidence workflow. Callers cannot request this reduced target-only mode directly, and `restore-checkpoint` still requires the durable exact branch, head, and clean worktree.
1111
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.
1212
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.
1313
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.

loops/issue-dev-loop/references/github-operations.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ Run every executor GitHub command through:
1010

1111
`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.
1212

13+
For a durable active run whose target predates newer trusted runtime files, only the installed activation router may select historical-target compatibility validation. It continues to hash-verify the complete installed control plane and both identities, then validates the target's stable state files, owner channel, JSON history, and evidence workflow. A caller-supplied compatibility flag is rejected; the subsequent restore still enforces the journal's exact branch and head on a clean worktree.
14+
1315
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`.
1416

1517
Publish reviewer output only as a non-approving comment review:

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

Lines changed: 18 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -240,10 +240,7 @@ export function hardenedGitArguments(args, { expectedRepository = null } = {}) {
240240
const rollback = lease?.match(
241241
/^--force-with-lease=(refs\/heads\/codex\/issue-[1-9][0-9]*):([0-9a-f]{40})$/,
242242
)
243-
if (
244-
rollback &&
245-
sameArguments(args, ['push', lease, 'origin', `:${rollback[1]}`])
246-
) {
243+
if (rollback && sameArguments(args, ['push', lease, 'origin', `:${rollback[1]}`])) {
247244
return ['push', lease, repositoryUrl, deleteRef]
248245
}
249246
const branch = args.at(-1)
@@ -783,9 +780,7 @@ function pullRequestMutationAllowed(kind, args, commandIndex, authorization, exp
783780
}
784781
if (kind === 'ready') {
785782
return (
786-
parsed.values.size <= 1 &&
787-
parsed.booleans.size === 1 &&
788-
parsed.booleans.get('undo') === true
783+
parsed.values.size <= 1 && parsed.booleans.size === 1 && parsed.booleans.get('undo') === true
789784
)
790785
}
791786
if (kind === 'comment') {
@@ -812,9 +807,7 @@ function reservedAutomationComment(body) {
812807

813808
function checkpointPublicationAllowed(body, authorization) {
814809
const markers = [
815-
...body.matchAll(
816-
/<!-- issue-dev-loop:checkpoint:([^:]+):sha256:([0-9a-f]{64}) -->/g,
817-
),
810+
...body.matchAll(/<!-- issue-dev-loop:checkpoint:([^:]+):sha256:([0-9a-f]{64}) -->/g),
818811
]
819812
if (markers.length !== 1 || markers[0][1] !== authorization?.issue?.runId) {
820813
return false
@@ -1082,6 +1075,9 @@ function assertRootCommandPolicy({ role, tool, args, loopRoot, trustedLoopRoot,
10821075
path.resolve(trustedLoopRoot, 'scripts', 'loopctl.mjs'),
10831076
path.resolve(trustedLoopRoot, 'triggers', 'detect-work.mjs'),
10841077
])
1078+
if (args.includes('--target-compatibility')) {
1079+
throw new Error('target compatibility validation is reserved to wrapped activation')
1080+
}
10851081
if (
10861082
script &&
10871083
allowedScripts.has(script) &&
@@ -1094,8 +1090,7 @@ function assertRootCommandPolicy({ role, tool, args, loopRoot, trustedLoopRoot,
10941090
if (
10951091
role === 'reviewer' &&
10961092
tool === 'node' &&
1097-
path.resolve(args[0] ?? '') ===
1098-
path.resolve(trustedLoopRoot, 'scripts', 'publish-review.mjs')
1093+
path.resolve(args[0] ?? '') === path.resolve(trustedLoopRoot, 'scripts', 'publish-review.mjs')
10991094
) {
11001095
parseReviewPublisherArguments(args.slice(1), {
11011096
authorization,
@@ -1301,16 +1296,11 @@ function argumentAfter(args, name) {
13011296
function withRootCommandIntent(authorization, { tool, args, trustedLoopRoot }) {
13021297
const script = args[0] ? path.resolve(args[0]) : null
13031298
const isTrustedLoopctl =
1304-
tool === 'node' &&
1305-
script === path.resolve(trustedLoopRoot, 'scripts', 'loopctl.mjs')
1299+
tool === 'node' && script === path.resolve(trustedLoopRoot, 'scripts', 'loopctl.mjs')
13061300
const withIntent = isTrustedLoopctl
13071301
? { ...authorization, rootIntent: args[1] ?? null }
13081302
: authorization
1309-
if (
1310-
!isTrustedLoopctl ||
1311-
args[1] !== 'start' ||
1312-
authorization.issue !== null
1313-
) {
1303+
if (!isTrustedLoopctl || args[1] !== 'start' || authorization.issue !== null) {
13141304
return withIntent
13151305
}
13161306
const issueNumber = Number(argumentAfter(args, '--issue'))
@@ -1478,10 +1468,7 @@ async function preflightPullRequestWrite({
14781468
}
14791469

14801470
if (['review', 'inline-review'].includes(intent.kind)) {
1481-
if (
1482-
livePullRequest.draft !== true ||
1483-
!intent.publication
1484-
) {
1471+
if (livePullRequest.draft !== true || !intent.publication) {
14851472
throw new Error('independent review publication requires the recorded Draft PR')
14861473
}
14871474
const publishedReviews = await githubPaginatedApi(
@@ -1499,9 +1486,7 @@ async function preflightPullRequestWrite({
14991486
review.body?.includes(intent.publication.marker),
15001487
)
15011488
) {
1502-
throw new Error(
1503-
`adjudication for ${intent.publication.findingId} is already published`,
1504-
)
1489+
throw new Error(`adjudication for ${intent.publication.findingId} is already published`)
15051490
}
15061491
const findingMarker = `<!-- issue-dev-loop:${runId}:${intent.publication.findingId} -->`
15071492
let findingPublished = false
@@ -1935,7 +1920,13 @@ export async function runWithGitHubRole({
19351920
? hardenedGitArguments(args, { expectedRepository: channel.repository })
19361921
: [...args]
19371922
if (activationValidation) {
1938-
executionArgs = [args[0], 'validate', '--loop-root', path.resolve(loopRoot)]
1923+
executionArgs = [
1924+
args[0],
1925+
'validate',
1926+
...(authorization.issue ? ['--target-compatibility'] : []),
1927+
'--loop-root',
1928+
path.resolve(loopRoot),
1929+
]
19391930
}
19401931
const child = spawnCommand(executable, executionArgs, {
19411932
env: childEnvironment,

0 commit comments

Comments
 (0)