Fix loop: `initial_code.py` has no write-once contract, so the Stage-0 baseline gets overwritten in place and "initial == final" is unmeasurable
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 191
- Forks
- 12
- Avg merge
- 3d 18h
- Merged PRs (30d)
- 1
Description
Summary
The LIBERO fix loop asks the subagent to write a Stage-0 baseline program, initial_code.py, and sweep it over the development seeds. Nothing after that protects the file: no hash, no copy, no read-only bit, and no instruction to leave it alone. In my libero_goal_swap campaign (10 tasks) the subagent overwrote initial_code.py in place on 4 of 10 tasks after the initial sweep had already run, so on those tasks initial_code.py is byte-identical to fix_code.py and the pre-debug program only survives inside the per-trial code.py copies.
Two consequences:
- "Initial equals final" cannot be interpreted. Six of ten tasks ended byte-identical. Checked against the per-trial evidence, those six decompose as 4 overwritten, 1 genuinely unchanged, 1 unverifiable. From the two files alone you cannot tell "the loop changed nothing" from "the loop destroyed its baseline".
- Anything that replays
initial_code.pyas the un-debugged baseline re-runs the debugged program. I had a control arm doing exactly that. Onput_the_bowl_on_the_plateit read 15/15 vs 15/15 against the Stage-2 result, which invites "debugging bought nothing", when the dev seeds had actually run a different, 11,636-byte program.
I am filing this as reporting hygiene, not data loss: nothing corrupted is committed (outputs/ is gitignored, .gitignore:34). The latent defect is in the prompt on main.
Where it comes from
aspire/sim/.claude/libero/fix-loop/subagent-prompt.md:84— "Write$TASK_DIR/initial_code.pyusing only allowed APIs." The subject of the experiment writes its own baseline into its own writable working directory.:88-98— the initial sweep replays it on seeds 51–65 and logs to$TASK_DIR/initial_seed_<seed>.log.- Stage 1, Step 3 — fixes are tested from a reused scratch path (
/tmp/fix_attempt.py); the prompt says to "improve ONE task-level program". An agent that editsinitial_code.pyin place is following the letter of the prompt. :180-184— Step 4 writes$TASK_DIR/fix_code.py. If the agent iterated in place, this is a copy ofinitial_code.py, and the baseline is gone.
There is no buggy line to point at. The absent contract is the finding.
Evidence
The per-trial evidence exists because scripts/libero/replay_trial.py:550 writes the executed program into every trial directory as code.py (with # Code block N headers; one header line plus the stripped trailing newline makes it 14 bytes longer than the source file). Each initial_seed_<seed>.log prints the trial directory on an Output: line (replay_trial.py:594), so a sweep's trial dirs can be bound to the sweep without guessing.
| task | initial-sweep code.py (all seeds, one program) |
sweep window | initial_code.py now on disk |
|---|---|---|---|
push_the_plate_to_the_front_of_the_stove |
13/13 mismatch, 19,714 B | 21:24–21:35 | 21,402 B, written 21:40:02 (== fix_code.py) |
put_the_bowl_on_the_plate |
15/15 mismatch, 11,636 B | 23:38–23:52 | 12,635 B, written 00:25:37 next day (== fix_code.py) |
put_the_cream_cheese_in_the_bowl |
15/15 mismatch, 10,290 B | 21:42–21:54 | 13,167 B, written 22:34:02 (== fix_code.py) |
put_the_wine_bottle_on_the_rack |
15/15 mismatch, 15,343 B | 23:33–23:59 | 20,583 B, written 00:27:12 next day (== fix_code.py) |
put_the_wine_bottle_on_top_of_the_cabinet |
15/15 match | == fix_code.py, genuinely unchanged |
|
put_the_bowl_on_the_stove |
unverifiable (see below) | == fix_code.py |
In all four overwrite cases the file's mtime is later than the entire sweep it is supposed to describe, and its contents differ from what every trial in that sweep ran. The other four tasks (initial ≠ fix) all match their sweep byte for byte, which is what validates the comparison.
Two traps worth recording for anyone who re-checks this:
- Comparing
initial_code.pytofix_code.pyby mtime is wrong in both directions.put_the_wine_bottle_on_the_rackhas the two files 77 minutes apart yet is overwritten;put_the_wine_bottle_on_top_of_the_cabinethas them 4 minutes apart yet is clean. Only the contents of the sweep's owncode.pydecide it. - Trial directories are keyed on
(seed, reward)and every sweep writes into the same--output-dir. A later replay that reproduces a reward silently replaces the earliercode.py. Onpush_the_plateseeds 51 and 61 were re-run after the overwrite and now hold the new program, so a naive check calls that task "mixed".put_the_bowl_on_the_stoveis unverifiable for the same reason: itsinitial_code.pyandfix_code.pyare byte-identical with the same mtime, so a surviving match proves nothing.
Repro
cd aspire/sim
T=outputs/libero_fix_loop/libero_goal_swap/put_the_bowl_on_the_plate
for f in $T/initial_seed_*.log; do
od=$(grep -m1 -E '^\s*Output: ' "$f" | sed 's/^\s*Output: //')
sha256sum "$od/code.py"
done | sort | uniq -c # 15 × one sha, 11,650 B
sha256sum $T/initial_code.py # a different sha, 12,635 B
Suggested fix
The repo already has the pattern: scripts/libero/run_fix_loop_validation.py:50 writes "code_sha256": sha256_file(fix_code) into a manifest. Applying it to Stage 0:
- After the smoke test and before the sweep, record
sha256(initial_code.py)in a task manifest andchmod 444the file; state insubagent-prompt.mdthat it is frozen after Stage 0. - Have Stage 1 write each tested program once, as
$TASK_DIR/attempts/attempt_<NNN>.py, instead of a reused/tmp/fix_attempt.py, and makefix_code.pya copy of the winning attempt rather than an in-place edit. Theattempts/directory already exists forBLOCKED.md. - Optionally have
replay_trial.pyrecord the sha256 of--args.replay-codeinsummary.txt, so a trial can be bound to its program without normalisingcode.py.
A related site with the same shape: scripts/libero/evosearch_eval.py:182 writes code.py raw, without the # Code block N headers replay_trial.py emits, so the two harnesses' code.py files are not directly comparable.
Happy to send a PR for (1) and (2) if that is the direction you want.
cc @LRY89757
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read aspire/sim/.claude/libero/fix-loop/subagent-prompt.md around lines 84–98 and 180–184, then inspect scripts/libero/run_fix_loop_validation.py:50 and scripts/libero/replay_trial.py:550,594. Reproduce the overwrite with the provided command. Done means the Stage-0 baseline is hash-recorded and frozen, each Stage-1 attempt is retained separately, and the resulting files and manifests make initial-versus-final comparisons unambiguous.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, shell
- Domain
- testing-qa, tooling
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 64/100