Repository navigation
fix(runtime)!: delete task worktrees from Ultrafuzz once outputs are published (#1227) - #1282
Conversation
- Controller finalization still reads the task worktree for the workspace-patch Git binding, invariant source-proof Git binding and scan probes, the coverage source inventory and aggregation destinations. - Reads of finalized output (verified-output: report, bundle, dashboard, Modal, evals) pass taskWorktree "skip" and check published bytes only, so they stay verified once the worktree is deleted (#1227). - An invariant ledger read under "skip" needs its durable source-proofs/<attempt>.invariant.json; there is no workspace fallback. This keeps unpinned runs verifiable after their discovery worktree is gone. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: mrthankyou <52643283+mrthankyou@users.noreply.github.com>
… engine - The engine always runs with SMITHERS_KEEP_WORKTREES=1; the keepWorkspaces plumbing through smithers.ts and start-run.ts is removed. - The first sync that sees an ended run with keep_workspaces = false (read from the sealed launch config) deletes the worktree, Git registration and ultrafuzz/<run>/<attempt> branch of every terminal attempt that has successful finalization authority or an artifacts/<attempt>/ mirror that holds no file. A rejected output keeps its worktree. - removeRunTaskWorktrees renames a worktree into workspaces/.removing/ before Git forgets it, so an interrupted removal never leaves a task path without .git; the next sync finishes it. A failure is a TASK_WORKTREE_REMOVAL_FAILED warning. - Document the lifecycle and the strategy-worktree policy: test/foundry/ and foundry.lock get no special handling (#1227). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: mrthankyou <52643283+mrthankyou@users.noreply.github.com>
- After the first status of the ended run, workspaces/ is empty, no Git registration under the run root and no ultrafuzz/<run>/ branch remain. - The report that follows is still verified-runtime-report, with the real engine and real Git (#1227). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: mrthankyou <52643283+mrthankyou@users.noreply.github.com>
- Under taskWorktree "skip", the invariant probe check still rejects an escaping or unsafe probe path; only the worktree lstat checks are skipped. The schema does not constrain scan_probes[].source_path, so this lexical check is the only place it is enforced (#1227). - One helper, taskWorktreeMode, gives the mode for every gate. Missing snapshots mean the controller is rechecking a failed verifier, which reads. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: mrthankyou <52643283+mrthankyou@users.noreply.github.com>
… deleted - New verified-output test finalizes a workspace-patch@1 producer from a real Git worktree, deletes the worktree and reads the output through loadVerifiedNodeOutputSnapshot. It fails if verified-output passes taskWorktree "read" again, which no earlier test caught (#1227). - The remaining real-engine integration tests launch Smithers with SMITHERS_KEEP_WORKTREES=1, the only mode Ultrafuzz now uses. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: mrthankyou <52643283+mrthankyou@users.noreply.github.com>
- removeRunTaskWorktrees returns void: the workflow-sync sweep never read the list, and the tests already assert the disk and Git state. - Reword the dynamic-expansion-retry comment: Smithers creates the task worktree, the ended-run sweep deletes it, and a reopened attempt reuses or recreates it. - The CHANGELOG now says verified reads skip aggregation source and destination reconciliation, not only destinations (#1227). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: mrthankyou <52643283+mrthankyou@users.noreply.github.com>
| const leftovers = pathEntryExists(removingRoot) ? fs.readdirSync(removingRoot) : []; | ||
| if (live.size === 0 && leftovers.length === 0) return; | ||
| const listed = runGit(projectRoot, ["worktree", "list", "--porcelain"]); | ||
| if (listed.error !== undefined || listed.status !== 0) return; |
There was a problem hiding this comment.
Git listing failures go unnoticed
If git worktree list fails for an existing Git project, this returns without removing any worktrees or reporting TASK_WORKTREE_REMOVAL_FAILED. The status command can appear successful while cleanup remains incomplete, leaving operators without a warning that it needs attention. Report the failure while retaining the intended behavior for non-Git projects.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/runtime/src/stale-worktree-recovery.ts
Line: 143
Comment:
**Git listing failures go unnoticed**
If `git worktree list` fails for an existing Git project, this returns without removing any worktrees or reporting `TASK_WORKTREE_REMOVAL_FAILED`. The status command can appear successful while cleanup remains incomplete, leaving operators without a warning that it needs attention. Report the failure while retaining the intended behavior for non-Git projects.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| fs.renameSync(worktreePath, removingPath); | ||
| } | ||
| if (registration !== undefined) runGitOrThrow(projectRoot, ["worktree", "remove", "--force", worktreePath]); | ||
| runGitOrThrow(projectRoot, ["update-ref", "-d", branch]); |
There was a problem hiding this comment.
Leftover can delete reused branch
If an interrupted removal leaves an entry in .removing after its registration is gone, and that branch is later reused at another worktree, the next sync deletes the active branch here. No matching registration is required when the original task path is absent. Check that the leftover still owns the branch before deleting it.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/runtime/src/stale-worktree-recovery.ts
Line: 163
Comment:
**Leftover can delete reused branch**
If an interrupted removal leaves an entry in `.removing` after its registration is gone, and that branch is later reused at another worktree, the next sync deletes the active branch here. No matching registration is required when the original task path is absent. Check that the leftover still owns the branch before deleting it.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Problem
With
keep_workspaces = false, the workflow engine deleted a successful task's worktree only whengit statusshowed it clean. Ultrafuzz tasks write outputs,test/foundry/<node>/andfoundry.lockinto their worktree, so in practice every worktree was left behind (#1227). Reads of verified output also re-checked facts against the task worktree, so deleting it would have made reports unverifiable.Change
SMITHERS_KEEP_WORKTREES=1. ThekeepWorkspacesplumbing throughsmithers.tsandstart-run.tsis removed.keep_workspaces = false, read from the launch config. For every terminal attempt that has successful finalization authority, or whoseartifacts/<attempt>/mirror holds no file, it deletes the worktree, its Git registration and itsultrafuzz/<run>/<attempt>branch.removeRunTaskWorktreesrenames a worktree intoworkspaces/.removing/before Git forgets it. An interrupted removal therefore never leaves a task path without.git, and the next sync finishes it.TASK_WORKTREE_REMOVAL_FAILEDwarning.taskWorktree: "skip"and check published bytes only; this coversreport,report bundle, the dashboard, Modal and evals.source-proofs/<attempt>.invariant.json; there is no workspace fallback.test/foundry/andfoundry.lockget no special handling. They are deleted with the worktree. This is documented indocs/reference/configuration.md.verifyCoverageProductionInventorydrops a redundantif (evidence.status === "unavailable") returnbecause the next condition already returns for any status other than"measured". This makes room under the lint complexity cap of 83 for the new skip check. No behaviour change.Breaking changes
keep_workspaces = false, task worktrees and theirultrafuzz/<run-id>/<attempt>branches are deleted after the run ends. Undeclared edits,test/foundry/<node>/andfoundry.lockgo with them. After upgrading, the nextstatusof a run that ended earlier deletes its worktrees by the same rule.keep_workspacesis read from the configuration fixed at launch. Setting it, orULTRAFUZZ_KEEP_WORKSPACES, forresumehas no effect.Tests
stale-worktree-recovery.test.ts:test/foundry/andfoundry.lock;smithers-artifact-publication.integration.test.ts: a real engine keeps the worktree, and recreates it when the task reruns after Ultrafuzz removed it.runtime.test.ts:keep_workspacesfrom the launch config;TASK_WORKTREE_REMOVAL_FAILED.artifact-gates.test.tsandverified-output.test.ts: verified reads succeed after the worktree is deleted, including a finalizedworkspace-patch@1producer.campaign-resume.test.ts: after the firststatus,workspaces/is empty and no registration or branch remains. The report is stillverified-runtime-report.Credit: mrthankyou investigated this in #1259.
🤖 Generated with Claude Code
The latest changes appear safe to merge, although two previously reported, non-blocking cleanup issues remain.
Fix with agent prompt
Summary
The PR moves task-worktree cleanup from the workflow engine to Ultrafuzz, lets finalized-output reads verify published artifacts without the deleted worktree, and documents the resulting retention behavior. Since the previous review, it also advances the engine dependency-resolution cutoff and updates transitive packages in the lockfile.
Diagram
%%{init: {'theme': 'neutral'}}%% flowchart LR A[Task finishes] --> B[Controller finalizes outputs] B --> C[Run ends] C --> D[Sync checks sealed keep_workspaces setting] D -->|false and disposable| E[Remove task worktree and Git branch] D -->|true or rejected output retained| F[Keep worktree] E --> G[Read verified published output]Reviews (2) · Last reviewed commit: "Merge branch 'unstable' into fix/1227-re..."