Files
rune/scripts/t14_builder_session.py
T
Nick 18ef9a53e3 refactor(scaffolding): §15U — resume discovery migrated to the interface
ConstructionInterface.discover_active_contract: ONE canonical home for
fresh-builder resume discovery (per-project scaffolds + main store;
most-recent contract; pure readback). T14/T15 driver helpers are now
shims. capabilities() advertises the op. Held-out: empty store invents
nothing; seeded store discovers the right contract.
2026-09-15 04:50:38 +01:00

455 lines
20 KiB
Python

#!/usr/bin/env python3
"""t14_builder_session.py — T14D/14E REAL builder session driver.
The builder is the ACTUAL mimo CLI process (mimo run, model
Xioami/mimo-v2.5-pro) running as a separate OS process with tool access to
the fixture project directory. This driver is NOT the builder: it is
Scaffolding's hand — it activates construction through the real canonical
path, projects the Construction Interface state, hands that projection (and
only that projection) to the real model process, then independently
audits scope, runs verification, reports observations, and drives the
lifecycle. Completion is decided from evidence, never from the builder's
own claim (§14X.8).
Modes:
start classify → activate → contract → project REAL builder process
resume canonical-state-only projection → FRESH builder process (14E)
status print canonical construction state (no builder invocation)
Exit codes: 0 complete · 3 INTERRUPTED (builder killed mid-build)
"""
from __future__ import annotations
import json
import os
import signal
import subprocess
import sys
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
from rune import construction_interface as ci # noqa: E402
from rune import construction_mode as cm # noqa: E402
from rune import construction_activation as ca # noqa: E402
from rune import construction_decision as cd # noqa: E402
from rune import scaffold as sc # noqa: E402
from rune import structural_store as ss # noqa: E402
USER_REQUEST = (
"Build me a small standalone practice scheduler that can create, "
"list and complete practice items."
)
EXPECTED_SCOPE = ["scheduler.js", "scheduler.test.js", "package.json"]
BUILDER_MODEL = os.environ.get("T14_BUILDER_MODEL",
"Verboo Code/glm-5.3-flash") # PL1 provider policy default
BUILDER_CMD = "mimo"
def log(msg: str) -> None:
print(f"[t14] {msg}", flush=True)
def classify_and_activate(store: Path, objective: str) -> dict:
"""The REAL canonical path: classify → activate. No shortcuts."""
parent = sc.Scaffold(str(store), "rune")
parent.init(purpose="t14 fixture parent authority")
decision = cm.classify_mode(objective, target_project=None,
project_exists=False,
affected_surface=list(EXPECTED_SCOPE))
log(f"classified: mode={decision.mode} "
f"target={decision.target_project!r}")
need = cd.Need(
need_id=f"need-{ss.now()}-t14", objective=objective,
origin="rune_native", project=decision.target_project or "",
constraints={}, affected_surface=list(EXPECTED_SCOPE),
evidence_refs=[], approval_context="t14 user-approved fixture")
rec = ca.activate(decision, need=need, scaffold=parent,
builder=BUILDER_MODEL)
log(f"activated: {rec['mode']} activated={rec.get('activated')} "
f"project={rec.get('project')}")
return {"decision": {"mode": decision.mode,
"target_project": decision.target_project,
"evidence": list(decision.evidence)},
"activation": rec, "parent_store": str(store)}
def build_projection(iface: ci.ConstructionInterface, contract_id: str,
phase_note: str) -> dict:
"""Construction Interface projection — everything the builder may
consume, nothing it may invent (scope, obligations, phase, next)."""
scope = iface.get_scope(contract_id)
obligations = iface.get_obligations(contract_id)
context = iface.construction_context(contract_id)
assert scope.get("success") and obligations.get("success"), \
f"projection failure: {scope.get('errors')} {obligations.get('errors')}"
return {
"phase": context["state"].get("phase"),
"project": context["state"].get("project"),
"need": context["state"].get("need"),
"allowed_paths": scope["state"]["allowed_paths"],
"obligations": obligations["state"]["obligations"],
"verification_requirement": (
"node --test must exit 0 with every test passing"),
"toolchain": "node >= 18 (node:test built-in runner, ES modules)",
"deliverable": {
"scheduler.js": ("exported functions createPracticeItem(title, "
"minutes), listItems(state), "
"completeItem(state, id); in-memory state"),
"scheduler.test.js": ("node:test cases proving create, list "
"and complete work"),
"package.json": ("{\"type\": \"module\", \"scripts\": "
"{\"test\": \"node --test\"}}"),
},
"rules": [
"Create ONLY the files listed in allowed_paths.",
"Do not modify or create anything else.",
"Verify with: node --test (must pass).",
"Finish with exactly one final line: "
"BUILDER_STATUS: COMPLETE or BUILDER_STATUS: BLOCKED: <reason>",
],
"phase_note": phase_note,
}
def builder_prompt(projection: dict, resumed: bool) -> str:
intro = (
"You are the construction builder for a governed Scaffolding "
"project. Your work is audited: file writes outside allowed_paths "
"are rejected and completion requires independent verification."
)
if resumed:
intro += (
" A PREVIOUS builder process was interrupted mid-build. Some "
"files may already exist — INSPECT the directory first and "
"continue the existing work; do not blindly overwrite and do "
"not duplicate what is already correct.")
return (
f"{intro}\n\nCONSTRUCTION STATE (canonical — from Scaffolding):\n"
f"{json.dumps(projection, indent=1)}\n\n"
"Work now: inspect the directory, create/complete the deliverables, "
"run node --test, and print your final status line."
)
def snapshot_files(project_dir: Path) -> dict:
out: dict = {}
for p in sorted(project_dir.rglob("*")):
if p.is_file():
out[str(p.relative_to(project_dir))] = p.stat().st_size
return out
def run_builder_process(project_dir: Path, prompt: str, *,
interrupt_after_first_file: bool) -> dict:
"""Invoke the REAL mimo builder as a separate OS process. Returns
{terminated_by, exit_code, session_output}."""
env = dict(os.environ)
proc = subprocess.Popen(
[BUILDER_CMD, "run", "-m", BUILDER_MODEL,
"--format", "json", prompt],
cwd=str(project_dir), env=env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, start_new_session=True)
terminated_by = "completed"
if interrupt_after_first_file:
log("interrupt armed: watching for first source file...")
while True:
if proc.poll() is not None:
break
files = [p for p in project_dir.iterdir()
if p.suffix in (".mjs", ".js", ".json")]
if files:
log(f"INTERRUPTING builder: first file landed "
f"({files[0].name}) — SIGKILL to the real process")
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
terminated_by = "sigkill_mid_build"
break
time.sleep(0.5)
try:
out, err = proc.communicate(timeout=900)
except subprocess.TimeoutExpired:
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
out, err = proc.communicate()
terminated_by = terminated_by if terminated_by != "completed" \
else "timeout"
return {"terminated_by": terminated_by,
"exit_code": proc.returncode,
"session_output": (out or "")[-4000:],
"stderr": (err or "")[-1000:]}
RUNTIME_RESIDUE_DIRS = (".mimocode", ".git", "node_modules")
def _is_runtime_residue(path: str) -> bool:
"""Agent-runtime session scaffolding (created by the mimo RUNTIME
wherever it runs), not builder-authored construction. Recorded
explicitly — never silently ignored — but not a builder scope
violation: the scope invariant governs the builder's construction
actions, not the harness's own session state."""
top = path.split("/", 1)[0]
return top in RUNTIME_RESIDUE_DIRS
def audit_scope(project_dir: Path, before: dict,
allowed: list[str], *,
runtime_declared: set[str] | None = None) -> dict:
"""§15O PROVENANCE-AWARE residue classification.
The T14 prefix rule (.git/.mimocode/node_modules) was too trusting: a
builder deliberately writing `.mimocode/evil` would vanish from the
audit. Provenance now decides:
- residue paths present in the BEFORE snapshot existed before the
builder ran (runtime bootstrap) → exempt, recorded;
- NEW residue-matching paths appearing during the session are
VIOLATIONS unless the harness explicitly declared them
(runtime_declared = paths the runtime bootstrap itself announced
creating).
"""
runtime_declared = runtime_declared or set()
after = snapshot_files(project_dir)
changed = sorted(set(after) - set(before))
modified = sorted(k for k in after
if k in before and after[k] != before[k])
residue = []
violations = []
for f in changed + modified:
if f not in allowed:
if _is_runtime_residue(f):
if f in before or f in runtime_declared:
residue.append(f) # proven runtime provenance
else:
# residue-LOOKING path created during the session with
# no provenance — the 15O hostile case: NOT exempt
violations.append(f)
else:
violations.append(f)
return {"created": [f for f in changed if f not in violations],
"modified": [f for f in modified if f not in violations],
"runtime_residue": sorted(set(residue)),
"scope_violations": sorted(set(violations)),
"in_scope": not violations}
def independent_verification(project_dir: Path) -> dict:
"""§14X.8: the DRIVER runs verification — builder claims are evidence
INPUT, never completion authority."""
probe = subprocess.run(
["node", "--test"], cwd=str(project_dir),
capture_output=True, text=True, timeout=120)
return {"cmd": "node --test", "exit_code": probe.returncode,
"stdout": (probe.stdout or "")[-2000:],
"stderr": (probe.stderr or "")[-500:]}
def complete_lifecycle(iface: ci.ConstructionInterface, contract_id: str,
scope_audit: dict, verification: dict,
builder_status: str) -> dict:
items = [{"entity": "builder-session",
"source": "independent_driver",
"detail": (f"builder={BUILDER_MODEL} claimed="
f"{builder_status} scope_violations="
f"{scope_audit['scope_violations']} "
f"verify_exit={verification['exit_code']}")}]
items += [{"entity": f, "source": "independent_driver",
"detail": "created in scope"}
for f in scope_audit["created"]]
items += [{"entity": f, "source": "independent_driver",
"detail": "modified in scope"}
for f in scope_audit["modified"]]
obs = iface.report_observation(contract_id, items)
assert obs.get("success"), obs.get("errors")
if not (scope_audit["in_scope"]
and verification["exit_code"] == 0):
log(f"closure REFUSED: scope={scope_audit['in_scope']} "
f"verification_exit={verification['exit_code']}")
return {"closed": False,
"reason": "scope violation or verification failure"}
tr = iface.transition(contract_id, "verifying",
evidence_ref="independent node --test run")
assert tr.get("success"), tr.get("errors")
v = iface.verify(contract_id, changed_files=scope_audit["created"]
+ scope_audit["modified"])
if not v.get("success"):
log(f"verify refused: {v.get('errors')}")
return {"closed": False, "reason": "verify refused"}
c = iface.close(contract_id, observed_evidence=[
{"kind": "verification", "cmd": "node --test",
"exit_code": 0, "source": "independent_driver_run"}],
scope_files=scope_audit["created"] + scope_audit["modified"])
return {"closed": bool(c.get("success")),
"close_envelope": {k: c.get(k) for k in
("success", "errors", "ids", "state")}}
def discover_contract_id(store: Path) -> tuple[str, str] | None:
"""§15U: delegates to the canonical interface
(ConstructionInterface.discover_active_contract). This shim stays only
so existing driver call sites keep working."""
from rune import construction_interface as _ci
env = _ci.ConstructionInterface.discover_active_contract(str(store))
if not env.get("success"):
return None
return env["ids"]["project"], env["ids"]["contract_id"]
def seed_requirement_spec(store: Path, project: str) -> str:
"""Govern the requirement as a contract on the project's surface
(identity before repository §7): the user request IS the requirement
evidence. The Need then references this contract explicitly — the
capability-fit engine resolves REUSE of the SPECIFICATION (the build
is governed by satisfying it). Maturity DECLARED: a requirement, not
existing code — nothing manufactured."""
proj = sc.Scaffold(str(store / "projects" / project), project)
doc = proj.load()
if ss.entity(doc, "contract:practice-items-spec") is None:
ss.add_entity(
doc, id="contract:practice-items-spec", kind="contract",
identities={"objective": USER_REQUEST},
evidence_refs=["t14-user-request"])
ss.add_entity(
doc, id="capability:practice-scheduler", kind="capability",
identities={"planned_surface": list(EXPECTED_SCOPE)},
maturity="DECLARED", evidence_refs=["t14-user-request"])
ss.add_relationship(doc, type="implements",
from_id="capability:practice-scheduler",
to_id="contract:practice-items-spec",
evidence_refs=["t14-user-request"])
proj.commit(doc, what="seed requirement spec: practice items "
"create/list/complete (user request)")
return "contract:practice-items-spec"
def cmd_start(store: Path, project_dir: Path, interrupt: bool) -> int:
before = snapshot_files(project_dir)
staged = classify_and_activate(store, USER_REQUEST)
project = staged["activation"]["project"]
assert staged["activation"]["mode"] == "PROJECT_BUILD", \
f"expected PROJECT_BUILD, got {staged['activation']['mode']}"
spec_contract = seed_requirement_spec(store, project)
iface = ci.ConstructionInterface(
builder=BUILDER_MODEL,
project_store=str(store / "projects" / project),
project_id=project)
sub = iface.submit_need(
USER_REQUEST, origin="rune_native",
constraints={"environment": {"os": "linux", "runtime": "node>=18",
"runner": "node --test"},
"contracts": [spec_contract]},
affected_surface=list(EXPECTED_SCOPE),
evidence_refs=["t14-user-request", "node-v22-installed"],
approval_context="t14 user-approved fixture")
assert sub.get("success"), sub.get("errors")
contract_id = sub["ids"]["contract_id"]
log(f"contract: {contract_id}")
for phase in ("approved", "building"):
tr = iface.transition(contract_id, phase,
evidence_ref=f"t14 {phase}")
assert tr.get("success"), tr.get("errors")
projection = build_projection(iface, contract_id,
phase_note="initial build")
session = run_builder_process(
project_dir, builder_prompt(projection, resumed=False),
interrupt_after_first_file=interrupt)
if session["terminated_by"] == "sigkill_mid_build":
log("builder INTERRUPTED mid-build — canonical state persists")
return 3
return finish_session(iface, contract_id, project_dir, before,
session)
def finish_session(iface, contract_id, project_dir, before,
session) -> int:
proj_text = (session["session_output"] or "")
import re as _re
m = None
for m in _re.finditer(r"BUILDER_STATUS:\s*([A-Z]+)", proj_text):
pass
builder_status = (m.group(1) + " (final)") if m else "NOT_STATED"
scope_audit = audit_scope(project_dir, before,
list(EXPECTED_SCOPE))
verification = independent_verification(project_dir)
result = complete_lifecycle(iface, contract_id, scope_audit,
verification, builder_status)
summary = {
"builder_model": BUILDER_MODEL,
"builder_process": "mimo run (separate OS process)",
"terminated_by": session["terminated_by"],
"builder_claimed": builder_status,
"scope_audit": scope_audit,
"verification": {"cmd": verification["cmd"],
"exit_code": verification["exit_code"]},
"lifecycle": result,
}
print(json.dumps(summary, indent=1))
return 0 if result.get("closed") else 1
def cmd_resume(store: Path, project_dir: Path) -> int:
"""14E: FRESH process. Canonical state ONLY — no transcript, no
memory of the previous session."""
found = discover_contract_id(store)
assert found, "no contract found in canonical state"
project, contract_id = found
log(f"fresh builder resume: project={project} "
f"contract={contract_id} (canonical only)")
iface = ci.ConstructionInterface(
builder=BUILDER_MODEL + "-resumed",
project_store=str(store / "projects" / project),
project_id=project)
context = iface.construction_context(contract_id)
assert context["state"]["active"], "canonical state shows no active build"
log(f"recovered: phase={context['state']['phase']} "
f"scope={context['state']['scope']}")
before = snapshot_files(project_dir)
projection = build_projection(
iface, contract_id,
phase_note="resumed after real builder interruption")
session = run_builder_process(
project_dir, builder_prompt(projection, resumed=True),
interrupt_after_first_file=False)
return finish_session(iface, contract_id, project_dir, before,
session)
def cmd_status(store: Path) -> int:
found = discover_contract_id(store)
if not found:
print(json.dumps({"active": False}))
return 0
project, contract_id = found
iface = ci.ConstructionInterface(
builder="inspector",
project_store=str(store / "projects" / project),
project_id=project)
print(json.dumps(iface.construction_context(contract_id)["state"],
indent=1, default=str))
return 0
def main() -> int:
import argparse
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("mode", choices=["start", "resume", "status"])
ap.add_argument("--store", required=True)
ap.add_argument("--project-dir", required=False)
ap.add_argument("--interrupt-after-first-file", action="store_true")
args = ap.parse_args()
store, project_dir = Path(args.store).resolve(), \
Path(args.project_dir).resolve() if args.project_dir else None
project_dir.mkdir(parents=True, exist_ok=True) if project_dir else None
if args.mode == "start":
return cmd_start(store, project_dir,
args.interrupt_after_first_file)
if args.mode == "resume":
return cmd_resume(store, project_dir)
return cmd_status(store)
if __name__ == "__main__":
sys.exit(main())