- provider_context.py: require_model() resolves the model from the ambient provider context; missing context fails LOUDLY (PROVIDER_UNRESOLVED, exit 3) — no silent default provider anywhere. - t14/t15 builder drivers, evidence_probe, research executor/relay: hardcoded 'Verboo Code/glm-5.3-flash' defaults REMOVED (6+ sites); provider/model selection now belongs exclusively to the provider layer via the ambient context. No caller anywhere set MIMO_MODEL — every run silently used the code-chosen provider until now. - failure_recovery: PROVIDER_UNAVAILABLE class (context-forced; real status/API code preserved verbatim in evidence; policy block_branch_preserve_error, 0 retries, no fallback — 16G-D). - tests/test_provider_ownership.py: structural guard (provider names in Scaffolding code lines fail the suite; module list live-discovered) + 16G held-outs (verbatim codes, zero fallback attempts, resumable after block, provider preference never mutates scaffold truth). - 16U root-cause completion: pyright skew 117⇄124 driven by ambient VIRTUAL_ENV/PATH leakage into pyright's interpreter resolution — the ratchet now neutralizes ambient-venv signals (PYTHONNOUSERSITE was necessary but not sufficient; reproduced and closed). - spine reconciliation: T15 modules (event_wait, stage_guards, failure_recovery, programme_continuation) were missing belongs_to relationships in the tracked store while the generated manifest had them — registered with SCAFFOLD-TRANCHE-015 evidence; provider_context registered under capability; manifest regenerated via export-umbrellas. Suite 1869 green; ratchets PASS; Henxels rc=0.
465 lines
20 KiB
Python
465 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"]
|
|
# 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: <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())
|