Repository navigation
Description
Follow-up to #68 (F2b: per-item execution and the runner's commit step). Independent review of #68 found five gaps. They open only once the real lane D workspace from #66 part 2 replaces the test fake: until then, no production code runs ItemExecutor. They were deferred from #68 on purpose. Related: #66, #22, #51.
1. Partial-output export before task storage is removed
docs/implementation/runner-lifecycle.md (task storage row): "settled → the partial-output export if the attempt did not complete → the terminal write, which stores diagnostic_ref → removeTaskFilesystems". plan-format.md, "After each run": for a safety violation, "preserve the rejected output separately for diagnosis".
- Today:
TaskWorkspacehas no export operation. A writable attempt that does not complete, including a safety violation, has its storage removed with no export and nodiagnostic_ref.
2. Audit the changes of an attempt whose agent exited non-zero
- Today:
finish()runs only on a clean exit. An agent that makes unsafe changes (for example under.git) and then exits non-zero never reaches needs human: its work is discarded, and the task can be retried. - Contract: "Any safety violation stops tests and commits and moves the task to needs human."
- Needs: the same inspection as item 1, run on failed attempts too.
3. Durable safety findings
-
Today:
SafetyFindingsis in memory. A finding is lost when:- the terminal write fails (the attempt stays active, so the move to needs human is refused);
- the process crashes between the terminal write and that move.
After a restart, startup recovery finalizes the attempt as "Interrupted", and the task can be retried.
-
Proposed: a durable mark on the attempt, written like the first reason, before the terminal write. The terminal write, startup recovery and admission (retry) would all respect it.
4. Act on a finding outside ItemExecutor
- Today: an execute attempt started through
runner.startorrunner.retry(for example the/api/runnerretry) records a finding that nothing takes. The task staysrunning, and the finding stays in memory. - Fix: item 3 covers this. The move to needs human must not depend on which caller started the attempt.
5. Take the runner's commit out of task storage before release
- Today: the runner commit exists only inside task storage, and
releaseremoves it:- the next item's
materialize(head)has no way to get that object; - the review screen's HEAD observation (
ReviewService.load) comparesconfig.repositoryHEAD with the snapshot head. It would then record the old HEAD as a new snapshot, which rolls back the base and makes the next attempt stale.
- the next item's
- Plan (D: inspect task changes and make runner commits (needed by F2) #66 decision 1):
exportTaskCommit(storage, {base, head})bundles the commit into a runner-owned host repository, and F verifies the SHA.TaskWorkspaceneeds that step, run after the terminal write and beforerelease. - Also needed: the review screen's HEAD observation must account for runner commits.
🤖 Generated with Claude Code
Activity
mchwang commentedon Sep 30, 2026
Also from the round-3 review of #68: once the review screen observes a HEAD that differs from the runner commit (item 5 above), the current snapshot moves away from a scope checkpoint's snapshot, and
approveContinuation(which requirescheckpoint.snapshotIdto equal the current snapshot) then refuses to approve continuing. Fixing the HEAD observation should cover this; add a test that a paused task can still be approved after a review load.mchwang commentedon Sep 30, 2026
Round 4 of the #68 review found that F2a's audit types (
core/run-audit.ts, merged in #67) do not match what lane D's inspection actually returns (agents/container/changes.ts, merged in #85). Nothing hits this until the real workspace replaces the test fake, but connecting it needs the following:through-linkdeclared links are never refused before launch.TaskWorkspace.snapshotDeclaredLinksreturns an opaque value, and F never reads it.through-link: it reports the status in theDeclaredLinkSnapshotand leaves the refusal to F.through-linkis refused before launch."links[].statusbefore launch.linkTargetChangesareLinkTargetChangeobjects, not strings, so the audit's validation now fails the whole report.retargetedentry for a link the item itself edits must be judged by the plan's link rules, not counted as a target change. That exemption is not implemented.linkTargetTraversesLink: traversal is carried by the snapshot and bystatuschanges. SoauditRunrejects every symlink edit as "not checked".auditRunon D'sTaskChangeManifesttype, not a hand-written copy.ignored: true, and reports an ignored new directory as a singleaddof typedirectory.ignoredfield, so:__pycache__/,node_modules/ortarget/becomes an "Unexpected directory entry" safety violation;coverage.xml) is counted out of scope and staged in the runner commit.mchwang commentedon Sep 30, 2026
Two more from the round-8 review of #68:
executionDeps.preparerefuses every kind butexecute, and the coordinator usesfinishfor every kind whenever it is defined. Connecting the real runner needs deps that dispatch by attempt kind (execute and fix versus review and check), or one coordinator per kind.mchwang commentedon Sep 30, 2026
From the round-9 review of #68: merge admission does not know about owed findings. A safety finding owed at a human gate, or a scope pause refused because a merge was already in progress, is acted on only by the task's next
ItemExecutorrun. If a person moves the task from the gate straight to review and merges, the finding never reaches needs human. Once findings and owed pauses are durable (item 3),beginMergeAttemptshould refuse while the task has one.mchwang commentedon Sep 30, 2026
From the round-11 review of #68:
finishtreats any inspection error as a safety finding unless it is the stop's own abort (signal.reasonor anAbortError). This fails closed on purpose. So when D'sinspectTaskChangesis aborted (a user stop or shutdown), it must reject withsignal.reasonor anAbortError, never with a generic Error from the killeddocker exec. Otherwise a stop during inspection sends the task to needs human, and in shutdown it skips the requeue.mchwang commentedon Sep 30, 2026
From the round-17 review of #68: shutdown does not await an
ItemExecutorrun. After the last item's attempt settles, the executor writes its pause or escalation through the shutdown capability, butrunner.close()awaits only the coordinator's own jobs, and step 6 awaits only the question and suggestion coordinators. When the executor is wired to the server, shutdown must await in-flightrunTaskcalls before it closes the Store (step 7). Otherwise, whether that write lands depends on timing. A lost pause is re-derived on the next run; a lost safety finding is the in-memory gap of item 3.mchwang commentedon Sep 30, 2026
From the round-18 review of #68: retry admission ignores an owed safety finding. When a finding is owed at a human gate and a person moves the task to queued,
/api/runnershows the failed attempt as retryable.runner.retrythen admits the violating item again in a fresh workspace, before anyItemExecutorrun escalates the finding. Once findings are durable (item 3), retry admission should refuse while one is owed, like merge admission.mchwang commentedon Oct 1, 2026
From the round-21 review of #68: if shutdown starts between items,
runTaskreturns stopped with the next item not started. The task staysrunningand its latest attempt iscompleted. Startup recovery requeues only tasks whose latest attempt was interrupted or stopped by shutdown, so this task is never resumed. When the executor is wired in, recovery (or the I3 requeue) must also resume a running task whose plan has items left and no active attempt.mchwang commentedon Oct 1, 2026
Item 5 changes with #90 (#66 part 2). There is no
exportTaskCommit. Instead:commitTaskChanges(storage, { base, linkSnapshot, digest, message, trailers, author, committer, imageId })returns{ head, unchanged, bundle }.bundleinto its runner-owned repository and checks thatTASK_COMMIT_REF(refs/heads/codeboost) ishead.finish(), before the terminal write. After that,releaseloses nothing.Adapter notes for
TaskWorkspace.commit:authorandcommitter(name, email, anddateas"<seconds> <offset>", never-0000). D takes nothing from the environment.pathsis not needed. D commits exactly the approved manifest.TaskCommitRefusedmeans needs human.Found in the PR's review; these block real runs:
core/run-audit.tsrequireslinkTargetTraversesLink === falsefor any symlink change. D never reports that field, so every symlink change is a violation today.directoryentries. So a file replaced by a package directory, and new ignored directories (__pycache__after tests), go to needs human.🤖 Generated with Claude Code
mchwang commentedon Oct 1, 2026
Decisions before implementation (2026-10-01)
config.repository. Bundles fromcommitTaskChangesare fetched into it under a per-task ref.materializeclones from it, the push reads from it, and the review screen's HEAD observation reads that task ref. codeboost never writes the user's repository..env,*.pyc)ignored: truesends the task to needs human, and nothing is committed.linkTargetTraversesLink=== falsecheck. (A D change, in part 1 of this issue.)TaskWorkspaceover D, the runner-owned repository and bundle fetch, the audit on D's manifest shape (with decisions 2 and 3), and HEAD observation (item 5). Part 2: partial export and diagnostics, auditing failed runs, durable safety findings, findings outsideItemExecutor(items 1–4). Wiring the runner intoweb/cli.tsand calling startup recovery at server start becomes its own issue.Facts behind these, from research on main plus #90:
RunnerDeps:web/cli.tspasses none, so the coordinator isnull.recoverStartuphas no production caller. ItsexportTaskDiffdependency does not match D's signature: D takesbase,imageIdandmetadataBaseline, and returns{ diff, truncated }.metadataBaseline, which a recovery handle needs.ReviewService.loadcomparesconfig.repositoryHEAD with the snapshot head. After a runner commit it would record the old head as a new snapshot.🤖 Generated with Claude Code
F2b: connect the real lane D workspace (#87, part 1) (#92)
F2b: durable safety findings, failed-run audit and partial output (#87,…
mchwang commentedon Oct 1, 2026
Done in two parts:
TaskWorkspaceover lane D;linkTargetTraversesLink;through-linkdeclared links refused before launch.Three follow-ups from this thread are now on #91, where the runner gets wired into the server:
ItemExecutorruns;🤖 Generated with Claude Code