Skip to content

fix(runtime): ignore Ultrafuzz's own worktree roots so successful runs reap task worktrees (#1227) - #1259

Open
mrthankyou wants to merge 2 commits into
unstablefrom
fix/1227-ignore-runtime-worktree-roots
Open

mrthankyou wants to merge 2 commits into
unstablefrom
fix/1227-ignore-runtime-worktree-roots

Conversation

@mrthankyou

@mrthankyou mrthankyou commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Refs #1227. This covers the worktree-reaping part. The rest is split out into #1257 (clean leaves worktree registrations, branches and the engine run record) and #1258 (run should reject a run ID the engine already knows before building the snapshot).

Problem

With keep_workspaces = false, a successful run keeps every task worktree. The engine (@smthrs/engine 0.35.0, reapFinishedRunWorktrees and gitWorktreeHasUnsavedWork) reaps a finished run's worktree only when git status --porcelain is empty. Task preparation writes .ultrafuzz/ (schemas, validator, prompt authority) and the artifacts/ mirror into every worktree. Ultrafuzz leaves those out of its own git commands by pathspec (WORKSPACE_RUNTIME_ROOTS), but nothing tells git to ignore them. So every worktree showed ?? .ultrafuzz/ and ?? artifacts/, and the engine logged keeping worktree with unsaved work once per task.

Still present on unstable (5c1a9775), confirmed by code reading. No commit since the issue was filed touches this.

Fix

ignoreWorkspaceRuntimeRoots (packages/runtime/src/workspace-runtime-roots.ts) runs in prepareArtifactMirror just before the schema bundle is written.

  • .ultrafuzz/ holds only Ultrafuzz's files, so it gets a .gitignore of *, which also ignores itself.
  • artifacts/ gets a .gitignore naming only the task's declared outputs (/<attempt>/<output path>) and itself. Any other file an agent leaves there stays untracked and keeps the worktree. Verification publishes only declared outputs, so ignoring everything there would delete such a file when the worktree is removed (Greptile). An output path that isn't a literal pattern is not listed.
  • Why not info/exclude: a linked worktree shares it with the project's own checkout.
  • Best effort: an existing .gitignore in either directory, which the target may track, is left alone. That worktree is still kept, because editing a tracked file would itself count as unsaved work. A root that isn't a real directory is skipped, and any failure only leaves the worktree kept, as today.
  • Docs: docs/reference/configuration.md and a CHANGELOG entry.

Tests

  • workspace-runtime-roots.test.ts uses a real repository with a linked worktree:
    • .ultrafuzz/ files and declared outputs leave git status --porcelain empty, the same check the engine applies;
    • the project's own checkout still shows a new .ultrafuzz/ file;
    • an undeclared artifacts/<attempt>/debug.log, or a test/foundry/ file, keeps the worktree;
    • a non-literal output path is not listed;
    • a target's tracked artifacts/.gitignore is not overwritten, and that worktree stays unclean;
    • nothing is written through a symlinked .ultrafuzz;
    • a second call changes nothing.
  • generated-workflow-verifier.test.ts: adds the helper to the no-op stubs of the test that extracts prepareArtifactMirror.
  • Results: the generated-workflow render, footprint, controller-source and verifier tests passed (124 of 125) before the follow-up commit. The failure is "pinned source proof counts hidden unreachable commits", which also fails on plain unstable on this machine because it assumes git's default branch is master. After the follow-up commit I reran the new tests and the stubbed preparation test, and both pass.
  • Not run: a real campaign, to watch the engine reap the worktrees.

🤖 Generated with Claude Code

RetriggerConfidence Score: 2/5

The PR is not yet safe to merge because successful tasks can still retain worktrees containing only accepted outputs or runtime-published companions.

Fix All in Claude CodeFindings

  1. P1 Valid output paths remain visible ▶
  2. P1 Runtime companions prevent cleanup ▶
  3. P1 Selective ignores prevent cleanup ▶
Fix with agent prompt
### Issue 1
packages/runtime/src/workspace-runtime-roots.ts:31
Declared output paths may contain `@` or `+`, but this filter leaves those paths out of `artifacts/.gitignore`. When a successful task writes such an output to its worktree mirror, Git still reports it as untracked, so the worktree is kept despite `keep_workspaces = false`.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

### Issue 2
packages/runtime/src/workspace-runtime-roots.ts:35
The runtime writes verified companion files into the worktree mirror that are not listed in `task.outputs`. For example, a successful invariant-suite task writes `invariant-suite-manifest.json` and suite files there. Because this ignore file covers only declared outputs, Git reports those runtime-written files as untracked and keeps the worktree instead of reaping it.

### Issue 3
packages/runtime/src/workspace-runtime-roots.ts:undefined-28
If a target already tracks an `artifacts/.gitignore` or `.ultrafuzz/.gitignore` that does not cover every file Ultrafuzz creates there, the `wx` write leaves that file unchanged and adds no new ignore rule. Those generated files remain untracked, so Git status is not clean and a successful run still cannot remove the worktree when `keep_workspaces = false`. The new test includes a selective `artifacts/.gitignore`, but does not check whether its worktree becomes clean.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

The PR adds worktree-local ignore files so Smithers can reap successful task worktrees without ignoring files in the project checkout.

  • It narrows the artifacts/ rules to declared outputs, preserving worktrees with other agent files.
  • The narrowed rules omit some valid output names and runtime-published companions, so cleanup still fails for those tasks.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Task preparation] --> B[Write worktree-local ignore rules]
  B --> C[Task writes outputs and companions]
  C --> D[Git status checks worktree]
  D -->|Clean| E[Reap worktree]
  D -->|Untracked files remain| F[Keep worktree]
Loading

Reviews (2) · Last reviewed commit: "fix(runtime): ignore only declared outpu..."

…s reap task worktrees (#1227)

Smithers reaps a finished run's worktree only when `git status --porcelain`
is empty. Task preparation writes `.ultrafuzz/` and `artifacts/` into every
worktree, so with keep_workspaces = false a successful run still kept every
task worktree as unsaved work. Preparation now gives each of those roots a
`.gitignore` of `*`. An existing `.gitignore` there is left alone, a root
that is not a real directory is skipped, and a failure only leaves the
worktree kept, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mrthankyou
mrthankyou requested a review from a team as a code owner October 1, 2026 17:22
Comment thread packages/runtime/src/workspace-runtime-roots.ts Outdated
const directory = path.join(workspaceRoot, root);
fs.mkdirSync(directory, { recursive: true });
if (!fs.lstatSync(directory).isDirectory()) continue;
fs.writeFileSync(path.join(directory, ".gitignore"), "*\n", { flag: "wx" });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Selective ignores prevent cleanup

If a target already tracks an artifacts/.gitignore or .ultrafuzz/.gitignore that does not cover every file Ultrafuzz creates there, the wx write leaves that file unchanged and adds no new ignore rule. Those generated files remain untracked, so Git status is not clean and a successful run still cannot remove the worktree when keep_workspaces = false. The new test includes a selective artifacts/.gitignore, but does not check whether its worktree becomes clean.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/runtime/src/workspace-runtime-roots.ts
Line: 28

Comment:
**Selective ignores prevent cleanup**

If a target already tracks an `artifacts/.gitignore` or `.ultrafuzz/.gitignore` that does not cover every file Ultrafuzz creates there, the `wx` write leaves that file unchanged and adds no new ignore rule. Those generated files remain untracked, so Git status is not clean and a successful run still cannot remove the worktree when `keep_workspaces = false`. The new test includes a selective `artifacts/.gitignore`, but does not check whether its worktree becomes clean.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

…ts/ (#1227)

The `*` rule also hid files an agent leaves beside its declared outputs,
which verification does not publish, so reaping a successful run's
worktree would have deleted them. artifacts/.gitignore now names only the
task's declared outputs, so any other file keeps the worktree as before.
Tests also show that a target's own .gitignore there leaves the worktree
kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
): void {
const declared = LITERAL_IGNORE_PATH.test(task.attemptId)
? task.outputPaths
.filter((outputPath) => LITERAL_IGNORE_PATH.test(outputPath))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Valid output paths remain visible

Declared output paths may contain @ or +, but this filter leaves those paths out of artifacts/.gitignore. When a successful task writes such an output to its worktree mirror, Git still reports it as untracked, so the worktree is kept despite keep_workspaces = false.

Knowledge Base Used: Runtime orchestration

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/runtime/src/workspace-runtime-roots.ts
Line: 31

Comment:
**Valid output paths remain visible**

Declared output paths may contain `@` or `+`, but this filter leaves those paths out of `artifacts/.gitignore`. When a successful task writes such an output to its worktree mirror, Git still reports it as untracked, so the worktree is kept despite `keep_workspaces = false`.

**Knowledge Base Used:** [Runtime orchestration](https://app.greptile.com/monad-foudnation/-/custom-context/knowledge-base/monad-developers/ultrafuzz/-/docs/runtime-orchestration.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

.map((outputPath) => `/${task.attemptId}/${outputPath}\n`)
: [];
writeRootIgnoreFile(workspaceRoot, ".ultrafuzz", "*\n");
writeRootIgnoreFile(workspaceRoot, "artifacts", ["/.gitignore\n", ...declared].join(""));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Runtime companions prevent cleanup

The runtime writes verified companion files into the worktree mirror that are not listed in task.outputs. For example, a successful invariant-suite task writes invariant-suite-manifest.json and suite files there. Because this ignore file covers only declared outputs, Git reports those runtime-written files as untracked and keeps the worktree instead of reaping it.

Knowledge Base Used:

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/runtime/src/workspace-runtime-roots.ts
Line: 35

Comment:
**Runtime companions prevent cleanup**

The runtime writes verified companion files into the worktree mirror that are not listed in `task.outputs`. For example, a successful invariant-suite task writes `invariant-suite-manifest.json` and suite files there. Because this ignore file covers only declared outputs, Git reports those runtime-written files as untracked and keeps the worktree instead of reaping it.

**Knowledge Base Used:**
- [Execution runtime](https://app.greptile.com/monad-foudnation/-/custom-context/knowledge-base/monad-developers/ultrafuzz/-/docs/execution-runtime.md)
- [Runtime orchestration](https://app.greptile.com/monad-foudnation/-/custom-context/knowledge-base/monad-developers/ultrafuzz/-/docs/runtime-orchestration.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

@aviggiano

Copy link
Copy Markdown
Collaborator

Thanks for digging into #1227. We're not merging this yet. Two concerns, both from reading the code. I haven't reproduced either end to end.

1. Workspace-patch producers get reaped, and the post-run gate then can't read them. Declared outputs are ignored from task preparation onward. A setup-foundry or base-test-setup task with an empty patch therefore ends with a clean worktree, and it is removed. But the post-run workspace-patch-git-binding gate still reads that live worktree: workspacePatchGitContext → deriveWorkspacePatchGitFacts(workspacePath, …) in artifact-gates.ts. With the worktree gone the gate fails closed. The report falls back to unverified, and report bundle / --require-verified fail. Once the worktree is deleted this can't be recovered. keep_workspaces = false is the default, so default runs take this path.

2. A rejected output loses its only copy. The ignore entries are written before verification runs. So a failure_policy: continue task whose output is rejected (for example a schema-invalid findings.json) has a clean-looking worktree and gets reaped. That deletes the only copy of what the agent produced, which goes against the PR's own rule of keeping anything verification doesn't publish.

Smaller:

More broadly, we've seen many failed runs tied to workspaces and declared outputs. We think this area needs a bigger simplification, not another layer on top of the current lifecycle. We'd like to step back and look at the workspace / declared-output lifecycle as a whole before changing when worktrees are reaped. Leaving this open for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants