feat(plane): the improvement loop as a plane Run (harnessx-removal task 4)

`ousast plane workspaces --validation-set ... --loop` appends the loop to a Run:
per case `alerts` (plane/tasks/alerts.py: the quick-mode scan on the vulnerable
and the fixed pin, a fixed-pin Workspace per case, alerts attributed to their
enclosing function by the repo-facts patterns, `in_fix_range` from the fix's
old-side and new-side ranges, failing on an unread tree) feeding `remember`,
then `memory-snapshot` -> `loop-measure` -> `loop-propose` -> `loop-improve`
(plane/tasks/loop.py). Every new task binds no Model and has budget
{usd: 0, calls: 0}. The snapshot is the store's rows after the train-on-test
guard, written on the host by memory.seed (a sandboxed run of it fails loudly);
propose runs M1/M2 against this repository at the commit under test; improve
runs one unchanged evolve.run_round and delivers gate.json, journal.json, the
sidecar, rule_policy.json only when accepted, and proposal_outcome rows that
the host ingests. Adoption stays a maintainer commit.

`remember` is now a Run entry per case (the image contains it), with the facts
row keyed by the image the facts were computed on (repo-facts records it in
its summary; unknown -> the row is omitted, never guessed). Alert rows carry
their own pin; improve/memory.py counts a repeated alert once.

Runner image rebuilt (+0.39 MB: only the new modules; the quick scan needs no
extra and the rulesets were already in the package), pushed, plane/ re-pinned
and regenerated (validation-46: 105 entries). Fixture test runs the whole loop
graph through the fake ax, whose new `exec` mode runs the real task modules.
Live: one loop Run over three validation-46 cases on the local ax, recorded in
benchmarks/measurements/2026-09-30-harnessx-removal-loop/ (all done, $0 new
spend, 0 proposals, gate verdict no_proposals).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-30 11:59:50 +02:00
co-authored by Claude Opus 5.5
parent d104166697
commit b68fe27c74
29 changed files with 2651 additions and 140 deletions
+22
View File
@@ -91,6 +91,26 @@ def gone():
sys.exit(1)
def run_task(rec, task, env, spec):
"""``python -m openultrasast.plane.tasks.<command>`` with the rendered env, ``AX_TASK_YAML``/``AX_WORKSPACES_YAML``,
each fetched input at its ``OUSAST_INPUT_<NAME>``, and ``spec["env"]`` (local checkouts for bound Workspaces)."""
import os, subprocess
doc = next(d for d in rec["docs"] if d["kind"] == "Task")
run_env = {{**os.environ, **env, "AX_TASK_YAML": yaml.safe_dump(doc)}}
run_env["AX_WORKSPACES_YAML"] = yaml.safe_dump_all([d for d in rec["docs"] if d["kind"] == "Workspace"])
for name, ref in json.loads(env.get("OUSAST_INPUTS", "{{}}")).items():
run_env["OUSAST_INPUT_" + name] = str(HERE / "fetched" / task / ref)
out = HERE / "out" / task
run_env.update(spec.get("env", {{}}), OUSAST_OUTPUT_DIR=str(out))
command = doc["spec"]["command"]
argv = [sys.executable, "-m", "openultrasast.plane.tasks." + command[0].replace("-", "_"), *command[1:]]
proc = subprocess.run(argv, env=run_env, capture_output=True, text=True)
(HERE / "ax.log").open("a").write(f"{{time.monotonic():.4f}} exec {{task}} {{proc.returncode}}\\n")
(HERE / "stderr").mkdir(exist_ok=True)
(HERE / "stderr" / task).write_text(proc.stderr)
return {{p.relative_to(out).as_posix(): p.read_text() for p in sorted(out.rglob("*")) if p.is_file()}}
if argv[:2] == ["apply", "-f"]:
docs = list(yaml.safe_load_all(Path(argv[2]).read_text()))
for doc in docs:
@@ -139,6 +159,8 @@ elif argv[:2] == ["get", "task"]:
except urllib.error.HTTPError as exc: # the runner delivers a failed summary naming the input
(HERE / "ax.log").open("a").write(f"{{time.monotonic():.4f}} unfetched {{task}} {{name}} {{exc.code}}\\n")
files = {{"summary.json": {{"status": "failed", "reason": f"input {{name}} ({{ref}}) not fetched: HTTP {{exc.code}}"}}}}
if "exec" in script and "summary.json" not in files: # the real task module, as the runner runs it
files = run_task(rec, task, env, script["exec"])
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w") as tar:
for path, content in files.items():