#!/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"] # T16 §16F: NO silent default provider. The surrounding provider # layer sets T14_BUILDER_MODEL (falling back to the ambient MIMO_MODEL # context); construction never names a provider. import sys as _sys _sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent / "src")) from rune.provider_context import ProviderUnresolved, require_model # noqa: E402 try: BUILDER_MODEL = os.environ.get("T14_BUILDER_MODEL", "").strip() \ or require_model() # ambient provider context (MIMO_MODEL) except ProviderUnresolved as _exc: _sys.stderr.write(str(_exc) + "\n") raise SystemExit(3) # 3 = PROVIDER_UNRESOLVED (no silent fallback) 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: ", ], "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())