fix(e2e_live): SM1 clean-worktree check tolerates the runtime's transient scratch and records the porcelain

rc.15 run3 on b6ac8c13, lane SM1_a1: the reviewed commit landed, and the
post-task promotion enabled a one-shot evolution cycle that started
seconds later in the same clone. Its run_script wrote
<clone>/.ouroboros/tmp_scripts/script_<uuid>.py (the active-workspace
scratch root, tools/shell.py, unlinked in a finally), nothing ignores that
directory, and the stand's worktree_clean_after_commit check — a bare
`git status --porcelain == ""` right after the commit — saw
"?? .ouroboros/" and failed a lane whose commit had left the tree clean.
run2's lanes used run_script 33 and 13 times without tripping it: a race
with the neighbouring task, not a constant. The product side is issue
#701 (the runtime's scratch inside the self-modified worktree).

worktree_after_commit() records the porcelain and the transient entries
as lane facts and counts only the runtime's own `.ouroboros/` scratch as
transient; any other untracked or modified path still fails the check.
A focused test module pins both directions on a throwaway repository.

Co-authored-by: Ouroboros <311266734+ouroboros-agent@users.noreply.github.com>
This commit is contained in:
Ouroboros 2026-09-06 03:34:03 +00:00
parent b6ac8c13c9
commit 19128ad7f6
3 changed files with 61 additions and 2 deletions

View file

@ -439,6 +439,20 @@ def vision_evidence_rows(tools_rows: list) -> list:
return out
RUNTIME_SCRATCH_PREFIX = ".ouroboros/" # run_script's active-workspace scratch (tools/shell.py), unlinked in a finally
def worktree_after_commit(clone: pathlib.Path) -> tuple[bool, str, list[str]]:
"""``(clean, porcelain, transient)`` for the clean-worktree check: the runtime's OWN transient scratch under
``.ouroboros/`` does not count — a post-task evolution cycle starts seconds after the commit in the same clone
and its ``run_script`` files live there until unlinked (rc.15 run3, SM1_a1: ``?? .ouroboros/`` at check time,
issue #701) — but it is recorded, and every other untracked or modified path fails the check."""
porcelain = _git(["status", "--porcelain"], clone)
entries = [line for line in porcelain.splitlines() if line.strip()]
transient = [line for line in entries if line.split(None, 1)[-1].startswith(RUNTIME_SCRATCH_PREFIX)] # ``_git`` strips
return len(entries) == len(transient), porcelain, transient
def _git_show(clone: pathlib.Path, rev: str, path: str) -> str:
"""The exact text of ``path`` at ``rev`` ('' when absent there)."""
proc = subprocess.run(["git", "show", f"{rev}:{path}"], cwd=str(clone), check=False, capture_output=True, text=True)
@ -510,7 +524,8 @@ def run_sm1(ctx: LaneContext) -> None:
# colour on the unlock page, the site stylesheet), and the reviewers own that judgment.
ctx.check("committed_diff_includes_sheets", all(path in files for path in SM1_CSS_PATHS),
committed_files=files, committed_companions=sm1_out_of_scope(ctx.clone, rev, files))
ctx.check("worktree_clean_after_commit", _git(["status", "--porcelain"], ctx.clone) == "")
clean, porcelain, transient = worktree_after_commit(ctx.clone)
ctx.check("worktree_clean_after_commit", clean, worktree_porcelain=porcelain, worktree_transient=transient)
task_oracle = ctx.oracle.task_drive(task_id)
ledger = task_oracle.advisory_review()
runs = [r for r in (ledger.get("advisory_runs") or []) if isinstance(r, dict)]

View file

@ -1375,7 +1375,9 @@ lane seeds `owner_chat_id` ONLY, never a campaign: the scenario task's post-task
A pre-seeded active campaign (the benchmark helper's form, rc.15 paid run2) runs generic cycles from
t=0, gets the promotion refused (evolution already enabled) and its kept request file blocks the
`no_promotion` exit of the absorb wait. `--self-mod` REQUIRES a confirmed absorb per lane whose scenario
`expects_absorb` (SM1, the one that lands a commit): a pre-task snapshot
`expects_absorb` (SM1, the one that lands a commit; its clean-worktree check records the
porcelain and tolerates only the runtime's transient `.ouroboros/` scratch, which a post-task
cycle in the same clone writes seconds after the commit): a pre-task snapshot
(clone HEAD, served sha, uptime, absorbed-cycle counter) and, afterwards, the
counter advanced, the served sha moved, the uptime reset and the server ready;
anything less is a typed `self_mod_absorb_confirmed=false` and the run fails. The

View file

@ -0,0 +1,42 @@
"""SM1's clean-worktree check after the reviewed commit: the runtime's own transient scratch under ``.ouroboros/``
(``run_script`` in the active workspace, tools/shell.py) is recorded but does not fail the check — the post-task
evolution cycle starts seconds after the commit in the same clone (rc.15 run3, SM1_a1, issue #701) — while any other
untracked or modified path still does."""
from __future__ import annotations
import pathlib
import subprocess
from devtools.e2e_live import scenarios
def _repo(root: pathlib.Path) -> pathlib.Path:
subprocess.run(["git", "init", "-q", str(root)], check=True)
for key, value in (("user.name", "t"), ("user.email", "t@example.com")):
subprocess.run(["git", "-C", str(root), "config", key, value], check=True)
(root / "a.txt").write_text("a\n", encoding="utf-8")
subprocess.run(["git", "-C", str(root), "add", "a.txt"], check=True)
subprocess.run(["git", "-C", str(root), "commit", "-q", "-m", "base"], check=True)
return root
def test_the_runtimes_transient_scratch_is_recorded_but_does_not_fail_the_clean_check(tmp_path):
root = _repo(tmp_path)
assert scenarios.worktree_after_commit(root) == (True, "", [])
(root / ".ouroboros" / "tmp_scripts").mkdir(parents=True)
(root / ".ouroboros" / "tmp_scripts" / "script_deadbeef.py").write_text("print(1)\n", encoding="utf-8")
clean, porcelain, transient = scenarios.worktree_after_commit(root)
assert clean is True and porcelain == "?? .ouroboros/" and transient == ["?? .ouroboros/"]
def test_any_other_untracked_or_modified_path_still_fails_the_clean_check(tmp_path):
root = _repo(tmp_path)
(root / ".ouroboros" / "tmp_scripts").mkdir(parents=True)
(root / ".ouroboros" / "tmp_scripts" / "script_deadbeef.py").write_text("print(1)\n", encoding="utf-8")
(root / "stray.txt").write_text("x\n", encoding="utf-8")
clean, porcelain, transient = scenarios.worktree_after_commit(root)
assert clean is False and "?? stray.txt" in porcelain and transient == ["?? .ouroboros/"]
(root / "stray.txt").unlink()
(root / "a.txt").write_text("changed\n", encoding="utf-8")
clean, porcelain, transient = scenarios.worktree_after_commit(root)
assert clean is False and "M a.txt" in porcelain and transient == ["?? .ouroboros/"] # ``_git`` strips the leading column