Skip to content

fix(runtime): init ignores generated state and says to commit project files (#1226) - #1261

Closed
mrthankyou wants to merge 2 commits into
unstablefrom
fix/1226-init-gitignore-generated-state
Closed

mrthankyou wants to merge 2 commits into
unstablefrom
fix/1226-init-gitignore-generated-state

Conversation

@mrthankyou

@mrthankyou mrthankyou commented Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #1226.

Problem

Campaigns default to private, and a private launch requires a clean Git target. ultrafuzz init wrote ultrafuzz.toml, .ultrafuzz/ and .smithers/ into the target without touching .gitignore or saying to commit them. A first-time user's first run failed with DATA_GOVERNANCE_PRIVATE_TARGET_UNBOUND before any agent started.

Confirmed on unstable (5c1a9775): right after init in a clean repository, targetIdentity reports dirty: true.

Fix

This takes the issue's first option. The second option, counting the project files as Ultrafuzz-owned, is only safe if they stay part of the recorded identity, and ignoring them would remove them from it.

  • .gitignore block: init adds a block between # BEGIN Ultrafuzz generated state and # END Ultrafuzz generated state to the target's root .gitignore, creating the file if needed. It covers only generated state:

    • .ultrafuzz/{runs,workspaces,cache,evals/runs,modal/results}/ and .ultrafuzz/*-audit.jsonl;
    • .smithers/{node_modules,workflows,continuations,logs,executions}/;
    • smithers.db*.

    Every init refreshes the block in place and keeps every other line byte for byte. If the block is current, the file is untouched.

  • Commit note: init reports INIT_COMMIT_PROJECT_FILES (info), telling the user to commit what it created before a private campaign.

  • Project files stay tracked: the config, topology, prompts, references and agent registry are run inputs, so they stay part of the recorded target identity.

  • Root .gitignore, not one inside .ultrafuzz/: smithers.db lives in the project root. Also, a committed .ultrafuzz/.gitignore would stop fix(runtime): ignore Ultrafuzz's own worktree roots so successful runs reap task worktrees (#1227) #1259 (keep_workspaces = false never reaps task worktrees, and clean leaves worktrees, branches and the engine run record behind #1227) from writing its worktree ignore file, so worktrees would never be cleaned up.

  • Fallback: if .gitignore can't be written safely, for example because it's a symlink, init still succeeds and reports INIT_GITIGNORE_NOT_UPDATED (warning).

  • No engine name in output: neither message names the workflow engine, which the CLI's product-surface test enforces.

  • Docs: first-campaign tutorial (with the commit step), CLI reference, CHANGELOG.

Tests

  • New data-governance.test.ts tests:

    • Full flow:
      • init keeps an existing line and adds the block, and the advice is info;
      • the identity is dirty until the project files are committed, then clean;
      • files under every ignored path leave both git status and the identity unchanged;
      • a second init preserves .gitignore.
    • Outdated block: refreshed in place, with the lines before and after it kept.
    • Symlinked .gitignore: warning, and nothing is written through the link.
  • CLI: the existing init tests pass, including init and validate emit schema-versioned launch JSON, which caught the engine name in an earlier wording.

  • Smoke test through the built CLI, on Damn Vulnerable DeFi v4.1.0 trimmed to Unstoppable:

    • init, then commit, then validate --audit-profile smoke passes.
    • A private launch gets past target identity. It then stops only on the policy and acknowledgement for model:anthropic.
    • With both supplied, the private smoke campaign launches, and git status stays clean with the run's files on disk.

    The campaign was still running when this PR was opened.

  • Local failures:

    • 3 governance tests fail the same way on plain unstable on this machine, because they pick up the local ~/.codex model route.
    • report bundle creates a portable ZIP without workspaces fails here because the case-insensitive macOS filesystem merges two files whose names differ only in case. It doesn't touch init, but I didn't rerun it on unstable.

    The full CLI test file did not finish locally within 40 minutes, so CI is its first full run.

🤖 Generated with Claude Code

RetriggerConfidence Score: 3/5

The PR is not yet safe to merge because the previously reported .gitignore corruption and failed-write risks remain.

Fix All in Claude CodeFindings

  1. P1 Non-UTF-8 ignore rules are corrupted ▶
  2. P1 Failed writes can erase ignore rules ▶
  3. P2 Marker text can replace user content ▶
  4. P2 Custom run directories remain unignored ▶
Fix with agent prompt
### Issue 1
packages/runtime/src/init.ts:undefined-276
If an existing `.gitignore` contains non-UTF-8 bytes, `init` decodes the whole file as UTF-8 and then rewrites it as UTF-8. Invalid byte sequences are replaced, permanently changing user-owned rules outside the managed block. Preserve those bytes or decline to rewrite a file that cannot be decoded without loss.

### Issue 2
packages/runtime/src/init.ts:199-201
If a write fails after an existing `.gitignore` has been truncated, such as when the filesystem fills, this catch turns the error into a warning and reports a successful `init`. The user's ignore file can be left partly written or empty. Keep the original intact until the replacement is complete, and do not report a partial write as success.

### Issue 3
packages/runtime/src/init.ts:281-284
If an existing `.gitignore` mentions both marker strings within a comment or pattern, the substring search treats that text as a managed block. Rerunning `init` then replaces the text between the markers, changing user-owned content and creating avoidable cleanup work. Only standalone marker lines should delimit the block.

### Issue 4
packages/runtime/src/init.ts:236-237
If `run.output_dir` is set to a project-local path such as `analysis-output`, this block still ignores only `.ultrafuzz/runs/`. Run files then appear as untracked in `git status`, despite the guidance that a run leaves the target clean. The governance check separately excludes the selected run root, so it can report a clean identity while Git status is dirty. Account for the configured directory or qualify the guidance.

---

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

Summary

The PR adds a managed root .gitignore block for generated run state and advises users to commit project files before a private campaign.

  • Adds tests for target cleanliness, block refresh, and symlink handling.
  • Updates the first-campaign tutorial, CLI reference, and changelog.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[init] --> B[Create or refresh managed .gitignore block]
  B --> C[Advise user to commit project files]
  C --> D[Clean Git target for private run]
Loading

Reviews (2) · Last reviewed commit: "test(runtime): expect init's commit advi..."

… files (#1226)

Campaigns default to private, and a private launch requires a clean Git
target, but init wrote its files into the target without touching
.gitignore or saying to commit them. A first-time user's first run failed
with DATA_GOVERNANCE_PRIVATE_TARGET_UNBOUND.

init now adds a managed block to the root .gitignore for the state runs
generate (.ultrafuzz/runs, workspaces, cache, evals/runs, modal/results,
audit logs, the engine's generated directories and database), refreshes it
in place on every init, and reports INIT_COMMIT_PROJECT_FILES. The project
files themselves stay unignored: they are run inputs and part of the
recorded target identity. A .gitignore init cannot write safely yields
INIT_GITIGNORE_NOT_UPDATED instead of failing init.

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 19:36
const existing =
lstatIfPresent(filePath) === undefined
? undefined
: readStableInitReviewFile(projectRoot, filePath, MAX_GITIGNORE_BYTES, "project .gitignore").toString("utf8");

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 Non-UTF-8 ignore rules are corrupted

If an existing .gitignore contains non-UTF-8 bytes, init decodes the whole file as UTF-8 and then rewrites it as UTF-8. Invalid byte sequences are replaced, permanently changing user-owned rules outside the managed block. Preserve those bytes or decline to rewrite a file that cannot be decoded without loss.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/runtime/src/init.ts
Line: 276

Comment:
**Non-UTF-8 ignore rules are corrupted**

If an existing `.gitignore` contains non-UTF-8 bytes, `init` decodes the whole file as UTF-8 and then rewrites it as UTF-8. Invalid byte sequences are replaced, permanently changing user-owned rules outside the managed block. Preserve those bytes or decline to rewrite a file that cannot be decoded without loss.

---

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

Fix in Claude Code

Comment on lines +199 to +201
try {
ignoreGeneratedState(projectRoot, created, preserved, overwritten);
} catch (error) {

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 Failed writes can erase ignore rules

If a write fails after an existing .gitignore has been truncated, such as when the filesystem fills, this catch turns the error into a warning and reports a successful init. The user's ignore file can be left partly written or empty. Keep the original intact until the replacement is complete, and do not report a partial write as success.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/runtime/src/init.ts
Line: 199-201

Comment:
**Failed writes can erase ignore rules**

If a write fails after an existing `.gitignore` has been truncated, such as when the filesystem fills, this catch turns the error into a warning and reports a successful `init`. The user's ignore file can be left partly written or empty. Keep the original intact until the replacement is complete, and do not report a partial write as success.

---

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

Fix in Claude Code

Comment on lines +281 to +284
const begin = existing.indexOf(GENERATED_STATE_IGNORE_BEGIN);
const end = begin < 0 ? -1 : existing.indexOf(GENERATED_STATE_IGNORE_END, begin);
if (begin >= 0 && end >= 0) {
contents = `${existing.slice(0, begin)}${block}${existing.slice(end + GENERATED_STATE_IGNORE_END.length)}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Marker text can replace user content

If an existing .gitignore mentions both marker strings within a comment or pattern, the substring search treats that text as a managed block. Rerunning init then replaces the text between the markers, changing user-owned content and creating avoidable cleanup work. Only standalone marker lines should delimit the block.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/runtime/src/init.ts
Line: 281-284

Comment:
**Marker text can replace user content**

If an existing `.gitignore` mentions both marker strings within a comment or pattern, the substring search treats that text as a managed block. Rerunning `init` then replaces the text between the markers, changing user-owned content and creating avoidable cleanup work. Only standalone marker lines should delimit the block.

---

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

Fix in Claude Code

Comment on lines +236 to +237
const GENERATED_STATE_IGNORE_ENTRIES = [
"/.ultrafuzz/runs/",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Custom run directories remain unignored

If run.output_dir is set to a project-local path such as analysis-output, this block still ignores only .ultrafuzz/runs/. Run files then appear as untracked in git status, despite the guidance that a run leaves the target clean. The governance check separately excludes the selected run root, so it can report a clean identity while Git status is dirty. Account for the configured directory or qualify the guidance.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/runtime/src/init.ts
Line: 236-237

Comment:
**Custom run directories remain unignored**

If `run.output_dir` is set to a project-local path such as `analysis-output`, this block still ignores only `.ultrafuzz/runs/`. Run files then appear as untracked in `git status`, despite the guidance that a run leaves the target clean. The governance check separately excludes the selected run root, so it can report a clean identity while Git status is dirty. Account for the configured directory or qualify the guidance.

---

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

Fix in Claude Code

#1226)

init now always reports INIT_COMMIT_PROJECT_FILES (info). The test still
requires that a refreshing init reports no warning or error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@aviggiano

Copy link
Copy Markdown
Collaborator

Thanks for this, and for the careful write-up in #1226. We're closing it because we've decided to remove data governance from Ultrafuzz entirely (#1269).

The clean-target requirement that init trips over (DATA_GOVERNANCE_PRIVATE_TARGET_UNBOUND) exists only for the private-campaign governance mode. Where campaign data goes is really a trust assumption of the inference providers the operator chooses (OpenAI, Anthropic, OpenRouter, …). Ultrafuzz can't enforce it: agents run unsandboxed, and the policy/acknowledgement machinery documents intent without stopping any egress. So instead of making init edit the user's .gitignore, we'll drop the feature, state the trust assumption in docs/, and ship that as a breaking change in the next release. Once governance is gone, #1226 goes away with it.

One finding from review, for the record: the benchmark workers run init --force on pinned clones that track a .gitignore (public-worker.ts:876, worker.ts:398). With this change those clones become dirty, and eval history publication fails with EVAL_HISTORY_LINEAGE_INCOMPLETE. That is one more reason not to have init touch tracked files.

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.

init leaves the target repo dirty, so the first private campaign fails DATA_GOVERNANCE_PRIVATE_TARGET_UNBOUND

2 participants