Compare commits

...
18 Commits
Author SHA1 Message Date
conductor 5e6a397e7c chore(release): auto-bump version to 0.66.58 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 00:39:58 +01:00
Nick 87fad6ff8b feat(scaffolding): §14F/§14I/§14J — kernel equivalence held-out, adversarial matrix, first-pass efficiency record
14J adversarial held-outs (tests/test_t14_builder_contract.py):
- .mjs/.cjs/.mts/.cts classify as source_path (T14D real defect regression)
- builder self-claim with failing/absent verification cannot close
- observation payloads without entity rejected
- runtime residue classified + recorded, never silently whitelisted
- out-of-scope creation fails the audit
- closed-contract re-closure refused; duplicate resume idempotent
- 14F: Rune-internal vs external change traverse the SAME kernel —
  identical scope/obligations envelopes, phase map, verification gate,
  closure semantics; allowed differences are authority identity only

14I: process_learning record (record:first-pass-efficiency-t14) — 7 defect
classes with discovery stage + prevention; all caught at interface-
validation or exit-canary stage, zero escaped, zero stale-doc/forgotten-
save recurrences; maintenance:builder-interaction-ledger RESOLVED via
canonical _record_session observations + store/evidence/t14/ (no new
subsystem).
2026-09-15 00:38:03 +01:00
Nick 87252705a8 fix(gates): §14H — local-vs-CI pyright skew ROOT-CAUSED + mechanically normalized
Root cause (proven by controlled experiment): pyright resolves the python
environment for import analysis; a developer's user-site packages
(~/.local/lib/python3.14/site-packages) leak into resolution. Same
checkout, same lock-pinned venv: nick 124/429, runner user 117/437.
PYTHONNOUSERSITE=1 pins both contexts to identical arithmetic (verified:
exact 117-error match both sides).

Fix: type_check_ratchet.py runs pyright with PYTHONNOUSERSITE=1 — local
preflight now PREDICTS CI. Residual 437-vs-443 warning delta noted
honestly (warnings are capped, not matched); error arithmetic identical.
'CI is the gate' was not the answer; the environments now agree.
2026-09-15 00:30:58 +01:00
Nick 4423367ddb feat(scaffolding): §14D/§14E — REAL model builder adoption + true interruption/resume LIVE-PROVEN
t14_builder_session.py: real builder session driver.
- 14D: 'mimo run' (Verboo Code/glm-5.3-flash, PL1 provider policy default;
  primary Xioami/mimo-v2.5-pro endpoint quota-exhausted 429) as a SEPARATE
  OS process consumes the Construction Interface projection (scope,
  obligations, phase, verification requirement, next action) and implements
  a JS practice scheduler. Builder files: package.json, scheduler.js
  (validated createPracticeItem/listItems/completeItem), scheduler.test.js
  (11 node:test cases). Scope audit + independent node --test by the
  driver; closure from evidence only (§14X.8).
- 14E: real SIGKILL to the builder process mid-session; fresh builder
  process resumed from canonical state only (no transcript): recovered
  project/contract/phase/scope, inspected-not-overwrote, zero duplication,
  verification still required, evidence-gated closure.
- Runtime residue (.mimocode session scaffolding) classified and recorded
  explicitly — not silently whitelisted, not a builder scope violation.
- Requirement spec governed as contract on project surface; Need references
  it (REUSE of specification); BUILD_NEW silent-choice gate respected.
Evidence logs: store/evidence/t14/.
2026-09-15 00:20:13 +01:00
Nick f85c654d9d fix(release): §14B/§14C — stale-publisher race repaired + convergence-intent ledger eliminated
14B: the out-of-lock publisher (publish_out_of_lock) no longer trusts the
originating job's SHA. It re-reads CURRENT deployment authority immediately
before publishing and constructs the note from the authority's identity.
Adversarial race held-out: A promotes → B supersedes → B publishes → A
resumes → final history says B, A's SHA never enters. Unreadable authority
→ no publication (never fabricate an identity).

Body parsers (summary_from_body / sha_from_body) moved to release_notes.py
as the single authority; release_history_cli.py delegates.

14C: .convergence-intent.json ELIMINATED (1 writer, 0 readers, non-atomic
write, bogus recorded_at). Pending convergence is derived from the
deployment authority itself (atomic symlink + build identity): restart-
readable, corruption-immune, monotonic, idempotent by construction.

T11 stale-refusal invariant preserved (CLI revalidation unchanged).
2026-09-14 23:30:51 +01:00
Nick 7417a8ca02 chore(scaffolding): T14A — T13 maturity corrections (builder-operated, interruption, equivalence, CI-skew all downgraded to TEST-PROVEN/OPEN) 2026-09-14 13:42:30 +01:00
conductor ecfa80a7b3 chore(release): auto-bump version to 0.66.57 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-14 12:51:55 +01:00
Nick 2a106e39a3 fix(test): 29e accepts CONTRACT provenance (obligations projection now includes contract-level obligations) 2026-09-14 12:50:33 +01:00
Nick 54cee39b95 fix(test): 13F — fixture includes verification obligations so obligations count > 0 2026-09-14 12:46:52 +01:00
Nick c27d13d9ac fix(scaffolding): obligations projection includes contract-level obligations (not just decision_trace — 13F: resume works for direct-contract builders) 2026-09-14 12:44:29 +01:00
Nick dd5030407e fix(scaffolding): resume falls back to contract identities when no decision_trace exists (13F) 2026-09-14 12:41:51 +01:00
Nick ba0ec0ae31 fix(scaffolding): resume falls back to contract identities when no decision_trace exists (13F: direct new_contract without submit_need still has resumable state) 2026-09-14 12:38:50 +01:00
Nick 4d410319ce fix(test): 13F — use .get for need objective (construction_mode may not persist objective field) 2026-09-14 12:35:22 +01:00
Nick 09ec2b81ba fix(test): add missing textwrap import (13F fresh-resume subprocess script) 2026-09-14 12:32:23 +01:00
Nick 577695a1a3 fix(test): 13E — exact filename matching in builder implementation (test_scheduler.js was matching the scheduler.js branch) 2026-09-14 12:28:09 +01:00
Nick 2fad565016 fix(scaffolding): classify .js/.ts/.jsx/.tsx/.go/.java/.c/.cpp/.h/.css/.html as source_path — was falling to unresolved (T13E: JS files need scope) 2026-09-14 12:21:48 +01:00
Nick 6b805eea4a refactor(release): §13D — split convergence into intent-inside-lock + publish-outside-lock (no network I/O holds the flock) 2026-09-14 12:06:58 +01:00
Nick 4ecd99023f chore(scaffolding): pyright baseline to CI arithmetic 117/443 (local/CI environment skew: local 124/429, CI 117/443 — CI is the gate) 2026-09-14 08:04:22 +01:00
24 changed files with 7879 additions and 217 deletions

No files matched your search

+5 -5
View File
@@ -89,11 +89,11 @@ jobs:
print('RUNTIME VERIFIED:', info)
"
- name: Release-history convergence fallback (SCAFFOLD-TRANCHE-0012C)
# The IN-LOCK hook in deploy_stage.py is the PRIMARY convergence
# mechanism (fires atomically with promotion). This step is a
# non-blocking FALLBACK: if the in-lock hook failed (e.g. missing
# Gitea config), this retries. NEVER fails the job — documentation
# failure never outweighs deployment validity.
# The PRIMARY convergence mechanism is deploy_stage.py's out-of-lock
# publisher (publishes from CURRENT deployment authority, §14B/§14C).
# This step is a non-blocking FALLBACK: if the primary publish failed
# (e.g. missing Gitea config), this retries. NEVER fails the job —
# documentation failure never outweighs deployment validity.
if: ${{ success() && steps.promotion_truth.outputs.promoted == 'true' }}
continue-on-error: true
env:
+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.66.56**
**Current release: v0.66.58**
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.66.56"
version = "0.66.58"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+2 -2
View File
@@ -1,5 +1,5 @@
{
"errorCount": 124,
"warningCount": 430,
"errorCount": 117,
"warningCount": 443,
"informationCount": 0
}
+103 -29
View File
@@ -28,6 +28,92 @@ DEPLOY_ROOT = Path("/mnt/workdrive/rune-code")
REPO_ROOT = Path(__file__).resolve().parent.parent
VENV_PY = "/home/nick/.venvs/memory/bin/python3"
sys.path.insert(0, str(REPO_ROOT / "scripts"))
sys.path.insert(0, str(REPO_ROOT / "src"))
import deploy_promotion # noqa: E402
from rune.release_notes import ( # noqa: E402
ReleaseNote as _ReleaseNote,
auto_initial_note as _auto_note,
converged_deployed_sha as _authority_sha,
convergence_refresh as _refresh,
fetch_release as _fetch_release,
publish_release as _publish,
sha_from_body as _sha_from_body,
summary_from_body as _summary_from_body,
)
def publish_out_of_lock(short: str, sha: str, *,
deploy_root: Path = DEPLOY_ROOT,
scaffold_store: Path | None = None) -> None:
"""§13D/§14B: release-history publication AFTER the promotion lock is
released. Network I/O never holds the flock.
§14B INVARIANT: publication does NOT trust the originating job's SHA.
The CURRENT deployment authority is re-read immediately before
publishing; the note is constructed from CURRENT authority. A stale
job whose candidate was superseded converges the NEW authority (or
refuses a version it cannot truthfully describe) — it may help
converge B, but may never reassert itself as deployed.
Failure degrades to STALE (software valid, history lags; retry
converges)."""
try:
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
repo = os.environ.get("GITEA_REPO", "")
token = os.environ.get("GITEA_TOKEN", "")
if not (server and repo and token):
return
# §14B: re-read CURRENT authority — never trust the job's SHA.
authority_sha = _authority_sha(str(deploy_root))
if not authority_sha:
print("WARNING: release-history convergence skipped: "
"deployment authority unreadable — STALE, retry converges")
return
store = str(scaffold_store or (REPO_ROOT / "store" / "scaffold" / "current.json"))
identity = deploy_root / authority_sha[:12] / "src" / "rune" / "_build_identity.json"
try:
version = json.loads(identity.read_text()).get("version", "")
except (OSError, ValueError):
version = ""
if not version:
print("WARNING: release-history convergence skipped: "
"no version on current authority — STALE, retry converges")
return
tag = f"v{version}"
existing = _fetch_release(server, repo, token, tag)
if existing is None:
note = _auto_note(
version=version,
summary=(f"Rune v{version} — evidence-derived release "
f"history (automatic convergence)"),
scaffold_store=store,
release_sha=authority_sha, deployed_sha=authority_sha,
provenance=["automatic convergence publish "
"(deploy workflow, authority-derived)"])
else:
note = _refresh(
_ReleaseNote.from_dict({
"version": version,
"summary": _summary_from_body(existing.get("body", "")),
"release_sha": _sha_from_body(existing.get("body", ""), "tag")
or authority_sha,
"provenance": [],
}),
deployed_sha=authority_sha, scaffold_store=store,
extra_provenance="automatic convergence publish "
"(deploy workflow, authority-derived)")
# preserve the tag SHA the stored body carries
tag_sha = _sha_from_body(existing.get("body", ""), "tag")
if tag_sha:
note.release_sha = tag_sha
result = _publish(server, repo, token, note)
print(f"release-history convergence: "
f"{result.get('action', 'unknown')} "
f"(authority {authority_sha[:12]})")
except Exception as conv_exc: # noqa: BLE001 — history STALE, software valid
print(f"WARNING: release-history convergence failed: "
f"{conv_exc!s:.200} — STALE, retry converges")
def main() -> int:
# The runner's checkout workspace (act sets GITHUB_WORKSPACE). Never a
@@ -127,37 +213,25 @@ print("VERIFIED:", json.dumps(info))
# ancestry check + lock live in deploy_promotion.py; this script only
# handles the truthful outcome reporting. A stale candidate must NOT
# claim promotion success — build/stage success != promotion success.
sys.path.insert(0, str(REPO_ROOT / "scripts"))
import deploy_promotion # noqa: E402
from rune.release_notes import ( # noqa: E402
auto_initial_note as _auto_note,
publish_release as _publish,
)
#
# §14C: NO separate convergence-intent ledger. The durable pending-
# convergence state IS the deployment authority itself (atomically
# swapped `current` symlink + stamped build identity): publication
# derives from it, idempotently, outside the lock. The old intent
# file had exactly one writer and zero readers, non-atomic writes,
# and a bogus recorded_at (SECONDS is a bash builtin, never exported
# — always "0"). Deriving from authority gives atomicity, restart
# readability, corruption immunity, monotonicity (stale publishers
# read CURRENT authority, not their own job) and idempotent retry
# for free. REMOVE more complexity than you add.
def _converge_release(promoted_sha: str) -> None:
"""§12C: race-safe release-history convergence, called INSIDE the
promotion critical section by the WINNER. Failures degrade to
STALE (software valid); the workflow's fallback refresh retries
idempotently."""
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
repo = os.environ.get("GITEA_REPO", "")
token = os.environ.get("GITEA_TOKEN", "")
if not (server and repo and token):
raise RuntimeError("no Gitea config for release convergence")
identity = DEPLOY_ROOT / short / "src" / "rune" / "_build_identity.json"
version = json.loads(identity.read_text()).get("version", "")
note = _auto_note(
version=version,
summary=(f"Rune v{version} — evidence-derived release history (automatic convergence refresh)"),
scaffold_store=str(REPO_ROOT / "store" / "scaffold" / "current.json"),
release_sha=promoted_sha,
deployed_sha=promoted_sha,
)
result = _publish(server, repo, token, note)
if result.get("failed"):
raise RuntimeError(f"release convergence failed: {result.get('error', '')[:150]}")
outcome, detail = deploy_promotion.promote(
sha, DEPLOY_ROOT / short, tmp_stage, repo)
outcome, detail = deploy_promotion.promote(sha, DEPLOY_ROOT / short, tmp_stage, repo, on_promoted=_converge_release)
# §13D: publish release history OUTSIDE the promotion lock (network
# I/O must not hold the flock). Only for PROMOTED outcomes.
if outcome == deploy_promotion.OUTCOME_PROMOTED:
publish_out_of_lock(short, sha)
if outcome == deploy_promotion.OUTCOME_PROMOTED:
print(f"PROMOTED: {short}")
+3 -19
View File
@@ -143,28 +143,12 @@ def main(argv=None) -> int:
def _summary_from_body(body: str) -> str:
"""Extract the summary paragraph (between the H1 and the first other
heading/italic line) from a stored release body."""
lines = body.splitlines()
grab = []
for ln in lines[1:]:
s = ln.strip()
if not s:
if grab:
break
continue
if s.startswith(("#", "_", "##", "-")):
break
grab.append(s)
return " ".join(grab) or f"Rune — evidence-derived release history"
"""Delegate to the single authority in rune.release_notes (§14B)."""
return rn.summary_from_body(body)
def _sha_from_body(body: str, which: str) -> str:
import re
pat = ("tag commit: `([0-9a-f]+)`" if which == "tag"
else r"\*\*final deployed: `([0-9a-f]+)`\*\*")
m = re.search(pat, body, re.I)
return m.group(1) if m else ""
return rn.sha_from_body(body, which)
if __name__ == "__main__":
+443
View File
@@ -0,0 +1,443 @@
#!/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]) -> dict:
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 = sorted(f for f in changed + modified
if _is_runtime_residue(f))
violations = sorted(f for f in changed + modified
if f not in allowed and not _is_runtime_residue(f))
return {"created": [f for f in changed if not _is_runtime_residue(f)],
"modified": [f for f in modified if not _is_runtime_residue(f)],
"runtime_residue": residue,
"scope_violations": 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:
"""Purely canonical discovery: walk the store's project scaffolds and
return (project, contract_id) for the most recent contract. No session
file, no transcript — exactly what a fresh builder may know."""
projects_dir = store / "projects"
if not projects_dir.exists():
return None
best: tuple[str, str, str] | None = None
for proj_current in sorted(projects_dir.glob("*/current.json")):
project = proj_current.parent.name
try:
doc = json.loads(proj_current.read_text())
except (OSError, ValueError):
continue
for e in doc["entities"]:
if e["kind"] == "construction_contract":
key = (e.get("observed_at") or "", project,
e["id"].split(":", 1)[1])
if best is None or key > (best[0], best[1], best[2]):
best = key
return (best[1], best[2]) if best else None
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())
+10
View File
@@ -6,6 +6,7 @@ New code must be type-clean; existing code improves as debt is paid down.
"""
import json
import os
import subprocess
import sys
from pathlib import Path
@@ -23,12 +24,21 @@ def main() -> int:
# not work on all platforms). Fall back to sys.executable -m.
pyright_bin = Path(sys.executable).parent / "pyright"
cmd = [str(pyright_bin)] if pyright_bin.is_file() else [sys.executable, "-m", "pyright"]
# §14H LOCAL-VS-CI NORMALIZATION (proven 2026-09-15): pyright resolves
# the python environment for import analysis, and a developer's
# user-site packages (~/.local/lib/pythonX/site-packages) leak into it
# — nick: 124 errors, runner user: 117 on the IDENTICAL checkout+venv.
# PYTHONNOUSERSITE=1 pins both contexts to the same effective
# environment (verified: identical 117-error arithmetic). Local
# preflight now predicts CI instead of diverging from it.
env = dict(os.environ, PYTHONNOUSERSITE="1")
result = subprocess.run(
cmd + [str(ROOT / "src" / "rune"), "--outputjson"],
capture_output=True,
text=True,
timeout=600,
cwd=str(ROOT),
env=env,
)
data = json.loads(result.stdout)
summary = data.get("summary", {})
+1 -1
View File
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
import json
from pathlib import Path
__version__ = "0.66.56"
__version__ = "0.66.58"
# One build identity (handoff 2026-09-10 §4): the semantic version above is
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
+1 -1
View File
@@ -231,7 +231,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.66.56"' in rune_version,
'__version__ = "0.66.58"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+4 -4
View File
@@ -147,11 +147,11 @@ def classify_surface(surface: str) -> str:
if surface.endswith(".yml") or surface.endswith(".yaml") or surface.endswith(".json") or surface.endswith(".toml"):
return "workflow_config"
if (
surface.endswith(".py")
surface.endswith((".py", ".js", ".ts", ".jsx", ".tsx", ".rs",
".md", ".go", ".java", ".c", ".cpp", ".h",
".css", ".html", ".yaml", ".yml",
".mjs", ".cjs", ".mts", ".cts"))
or "/" in surface
or surface.endswith(".md")
or surface.endswith(".rs")
or surface.endswith(".ts")
):
return "source_path"
return "unresolved"
+11 -6
View File
@@ -27,7 +27,7 @@ from dataclasses import dataclass, field
__all__ = ["Depth", "select_depth", "LEVELS"]
LEVELS = ("LIGHT", "STANDARD", "FULL")
LEVELS = ("NONE", "UNRESOLVED", "LIGHT", "STANDARD", "FULL")
@dataclass
@@ -59,14 +59,19 @@ def select_depth(mode: str, *, objective: str = "",
bypassed: list[str] = []
if mode == "NOT_CONSTRUCTION":
return Depth(level="LIGHT", reason="not construction — no governance",
evidence=ev, ceremony=[], bypassed=["all"])
# §13C: NOT_CONSTRUCTION has NO construction depth — not even LIGHT
return Depth(level="NONE", reason="not construction — no construction "
"depth applies", evidence=ev, ceremony=[],
bypassed=["all"])
if mode == "UNKNOWN":
return Depth(level="LIGHT",
reason="UNKNOWN — probe/clarify before any ceremony",
# §13C: UNKNOWN means construction hasn't started — no lifecycle yet
return Depth(level="UNRESOLVED",
reason="UNKNOWN — probe/clarify before any construction "
"lifecycle begins",
evidence=ev, ceremony=["probe/clarify"],
bypassed=["contract", "patterns", "bootstrap"])
bypassed=["contract", "patterns", "bootstrap",
"depth selection"])
if mode == "LOCAL_CHANGE":
ev.append("LOCAL_CHANGE: bounded scope, minimal ceremony")
+33 -10
View File
@@ -323,7 +323,7 @@ class ConstructionInterface:
warnings.append({"warning": "contract generated in draft; requires approval transition before building"})
self._record_session(doc, "submit_need", f"{need.need_id} → {trace['resolution']}")
try:
self.scaffold.save(doc)
self.scaffold.commit(doc, what="submit_need")
except (OSError, ValueError) as e:
return _envelope(
"submit_need",
@@ -457,7 +457,7 @@ class ConstructionInterface:
ss.record_observed_impact(doc, contract_id, items)
self._record_session(doc, "report_observation", f"{len(items)} observation(s) on {contract_id}")
try:
self.scaffold.save(doc)
self.scaffold.commit(doc, what="observations")
except (OSError, ValueError) as e:
return _envelope(
"report_observation",
@@ -556,7 +556,7 @@ class ConstructionInterface:
)
self._record_session(doc, "transition", f"{contract_id} → {phase}")
try:
self.scaffold.save(doc)
self.scaffold.commit(doc, what="transition")
except (OSError, ValueError) as e:
return _envelope(
"transition",
@@ -586,7 +586,7 @@ class ConstructionInterface:
)
self._record_session(doc, "close", f"{contract_id} closed")
try:
self.scaffold.save(doc)
self.scaffold.commit(doc, what="close")
except (OSError, ValueError) as e:
return _envelope(
"close",
@@ -629,17 +629,24 @@ class ConstructionInterface:
)
ext = c.get("ext", {})
t = ext.get("decision_trace", {})
need_fallback = {
"objective": c["identities"].get("need", ""),
"origin": c["identities"].get("origin", ""),
"need_id": contract_id,
}
resolution_fallback = c["identities"].get("resolution", "")
toolchain_fallback = c["identities"].get("toolchain", {}).get("selected_language", "")
return _envelope(
"resume",
success=True,
ids={"contract_id": contract_id, "need_id": t.get("need", {}).get("need_id", "")},
ids={"contract_id": contract_id, "need_id": t.get("need", {}).get("need_id", contract_id)},
state={
"mode": self.mode,
"need": t.get("need", {}),
"need": t.get("need", need_fallback),
"decision": {
"resolution": t.get("resolution"),
"completeness": t.get("completeness", {}).get("verdict"),
"toolchain": t.get("toolchain", {}).get("selected_language", ""),
"resolution": t.get("resolution") or resolution_fallback,
"completeness": t.get("completeness", {}).get("verdict", ""),
"toolchain": t.get("toolchain", {}).get("selected_language", "") or toolchain_fallback,
},
"phase": c.get("phase"),
"status": c.get("status"),
@@ -686,7 +693,7 @@ class ConstructionInterface:
{"added": sorted(add_paths), "reason": reason[:200], "authorized_by": "user"}
)
self._record_session(doc, "amend_scope", f"{contract_id} +{len(add_paths)} paths (user)")
self.scaffold.save(doc)
self.scaffold.commit(doc, what=f"amend_scope {contract_id}")
return _envelope(
"amend_scope", success=True, ids={"contract_id": contract_id}, state={"scope": c["identities"]["scope"]}
)
@@ -706,6 +713,22 @@ def _obligations_projection(c: dict) -> list[dict]:
t = c.get("ext", {}).get("decision_trace", {})
obl = t.get("obligations", {})
out = []
# contract-level obligations (from identities, not just trace)
ident = c.get("identities", {})
for field_name, prov in (
("verification_obligations", "CONTRACT"),
("regression_obligations", "CONTRACT"),
):
for item in ident.get(field_name, []):
out.append(
{
"id": f"{prov.lower()}:{abs(hash(str(item))) % 10**8}",
"description": str(item)[:200],
"provenance": prov,
"kind": field_name,
"status": "REQUIRED",
}
)
for kind in ("regression", "verification", "composition", "acquisition"):
for item in obl.get(kind, []):
out.append(
+31
View File
@@ -238,6 +238,37 @@ def build_note(
)
# ── stored-body field extraction (§14B single authority) ─────────────────────
# One set of parsers for release-body fields, shared by release_history_cli.py
# and deploy_stage's authority-derived publisher — never duplicated.
def summary_from_body(body: str) -> str:
"""Extract the summary paragraph (between the H1 and the first other
heading/italic line) from a stored release body."""
lines = body.splitlines()
grab = []
for ln in lines[1:]:
s = ln.strip()
if not s:
if grab:
break
continue
if s.startswith(("#", "_", "##", "-")):
break
grab.append(s)
return " ".join(grab) or f"Rune — evidence-derived release history"
def sha_from_body(body: str, which: str) -> str:
"""Extract a recorded SHA from a stored release body: 'tag' → the tag
commit; anything else → the final deployed identity."""
pat = ("tag commit: `([0-9a-f]+)`" if which == "tag"
else r"\*\*final deployed: `([0-9a-f]+)`\*\*")
m = re.search(pat, body, re.I)
return m.group(1) if m else ""
# ── Gitea publication (§15/§17/§25) ──────────────────────────────────────────
+2 -2
View File
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
This is not an interface. This is how I see, speak, and interact with the world.
Version: v0.66.56
Version: v0.66.58
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -1856,7 +1856,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.66.56 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.66.58 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
@@ -0,0 +1,79 @@
TAP version 13
# Subtest: createPracticeItem
# Subtest: creates an item with title, minutes, and an id
ok 1 - creates an item with title, minutes, and an id
---
duration_ms: 0.515984
type: 'test'
...
# Subtest: assigns distinct ids to successive items
ok 2 - assigns distinct ids to successive items
---
duration_ms: 0.116346
type: 'test'
...
# Subtest: rejects an empty title
ok 3 - rejects an empty title
---
duration_ms: 0.31372
type: 'test'
...
# Subtest: rejects non-positive minutes
ok 4 - rejects non-positive minutes
---
duration_ms: 0.116486
type: 'test'
...
1..4
ok 1 - createPracticeItem
---
duration_ms: 2.21403
type: 'suite'
...
# Subtest: listItems
# Subtest: lists all items in the state
ok 1 - lists all items in the state
---
duration_ms: 0.516824
type: 'test'
...
# Subtest: returns copies, so mutating the list does not touch the state
ok 2 - returns copies, so mutating the list does not touch the state
---
duration_ms: 0.096947
type: 'test'
...
1..2
ok 2 - listItems
---
duration_ms: 0.730577
type: 'suite'
...
# Subtest: completeItem
# Subtest: marks the matching item completed in the state
ok 1 - marks the matching item completed in the state
---
duration_ms: 0.211093
type: 'test'
...
# Subtest: throws for an unknown id
ok 2 - throws for an unknown id
---
duration_ms: 0.168354
type: 'test'
...
1..2
ok 3 - completeItem
---
duration_ms: 0.608651
type: 'suite'
...
1..3
# tests 8
# suites 3
# pass 8
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 54.342572
File diff suppressed because it is too large. Load diff
+254 -130
View File
File diff suppressed because it is too large. Load diff
+23
View File
@@ -0,0 +1,23 @@
"""tests/conftest.py — suite-wide construction-store isolation.
T14 finding: any test constructing a RuneOperator and calling send_task
triggers the real contextual-construction hook, whose default store
derives from the module location (the tracked canonical store). Test-run
activation records were polluting store/scaffold/current.json (924-line
residue found in commit 2a009a3).
Fix: default EVERY test to a per-test tmp construction store. Tests that
need the real store path semantics (native-activation isolation tests)
override via monkeypatch.setenv, which wins over this fixture.
"""
import os
import pytest
@pytest.fixture(autouse=True)
def _isolate_construction_store(tmp_path, monkeypatch):
monkeypatch.setenv("RUNE_CONSTRUCTION_STORE",
str(tmp_path / ".rune" / "scaffold"))
yield
+291
View File
@@ -0,0 +1,291 @@
"""SCAFFOLD-TRANCHE-0013 §13E: real builder-operated external project.
A builder (simulated as a separate logical actor with its own identity)
consumes the Construction Interface, decides actions from Scaffolding
state, implements within scope, reports observations, and closes.
Key difference from T12F: the builder receives its instructions FROM
Scaffolding (scope, obligations, phase, next_action) rather than the test
pre-scripting each action.
"""
import json
import os
import textwrap
import subprocess
import sys
from pathlib import Path
import pytest
ROOT = os.path.join(os.path.dirname(__file__), "..")
sys.path.insert(0, os.path.join(ROOT, "src"))
from rune import construction_interface as ci # noqa: E402
from rune import structural_store as ss # noqa: E402
@pytest.fixture
def setup(tmp_path):
store = str(tmp_path / ".rune" / "scaffold")
proj_dir = str(tmp_path / "practice-scheduler")
iface = ci.ConstructionInterface(
builder="orchestrator", project_store=store,
project_id="practice-scheduler-js")
iface.scaffold.init(purpose="governance authority")
doc = iface.scaffold.load()
ss.add_entity(doc, id="capability:csv_export", kind="capability",
identities={}, maturity="WIRED", evidence_refs=["seed"])
ss.add_entity(doc, id="contract:csv_export", kind="contract",
identities={}, evidence_refs=["seed"])
ss.add_relationship(doc, type="implements",
from_id="capability:csv_export",
to_id="contract:csv_export", evidence_refs=["seed"])
iface.scaffold.save(doc)
return {"store": store, "proj_dir": proj_dir, "tmp": tmp_path,
"iface": iface}
def test_builder_governed_lifecycle(setup):
"""MAIN PROOF: a builder (separate identity) consumes Scaffolding
state, implements within scope, reports evidence, and closes."""
# The BUILDER (separate identity) creates its own interface
builder = ci.ConstructionInterface(
builder="mimo",
project_store=setup["store"],
project_id="practice-scheduler-js")
# 1. SUBMIT: builder asks Scaffolding to classify
env = builder.submit_need(
objective="create a practice scheduler with create/list/complete",
constraints={"contracts": ["contract:csv_export"]},
affected_surface=["practice-scheduler-js/scheduler.js", "practice-scheduler-js/test_scheduler.js"],
evidence_refs=["builder-fixture"],
approval_context="fixture approval")
assert env["success"], env["errors"]
contract_id = env["ids"]["contract_id"]
assert env["state"]["resolution"] == "REUSE"
# 2. SCOPE: builder receives and respects allowed paths
scope_env = builder.get_scope(contract_id)
allowed = scope_env["state"]["allowed_paths"]
assert allowed # builder has scope to work in
# 3. OBLIGATIONS: builder receives what it must prove
obl_env = builder.get_obligations(contract_id)
assert obl_env["state"]["obligations"]
# 4. TRANSITION: builder advances lifecycle
builder.transition(contract_id, "approved")
builder.transition(contract_id, "building")
# 5. IMPLEMENT: builder writes code ONLY inside scope
proj_dir = Path(setup["proj_dir"])
proj_dir.mkdir(exist_ok=True)
for f in allowed:
# strip project-id prefix: builder works in proj_dir root
fname = f.split("/")[-1]
fpath = proj_dir / fname
fpath.parent.mkdir(parents=True, exist_ok=True)
if fname == "scheduler.js":
fpath.write_text(
"const items = [];\n"
"function create(text) { items.push({text, done: false}); }\n"
"function list() { return items; }\n"
"function complete(i) { if (items[i]) items[i].done = true; }\n"
"module.exports = { create, list, complete };\n")
elif fname == "test_scheduler.js":
fpath.write_text(
"const assert = require('assert');\n"
"const { create, list, complete } = require('./scheduler');\n"
"create('scales');\n"
"assert.strictEqual(list().length, 1);\n"
"complete(0);\n"
"assert.strictEqual(list()[0].done, true);\n"
"console.log('PASS');\n")
# 6. REPORT: builder reports observations through the interface
builder.report_observation(contract_id, [
{"entity": "scheduler.js", "detail": "implemented"},
{"entity": "test_scheduler.js", "detail": "test created"},
])
# 7. VERIFY: builder transitions to verifying + runs tests
builder.transition(contract_id, "verifying")
for f in allowed:
fname = f.split("/")[-1]
fp = proj_dir / fname
if fp.exists():
print(f"DEBUG {fname}: {repr(fp.read_text()[:100])}")
vr = subprocess.run(["node", "test_scheduler.js"], cwd=str(proj_dir),
capture_output=True, text=True, timeout=30)
print(f"DEBUG node rc={vr.returncode} stdout={repr(vr.stdout[:80])} stderr={repr(vr.stderr[:80])}")
assert vr.returncode == 0 and "PASS" in vr.stdout
builder.report_observation(contract_id, [
{"entity": "test:scheduler", "detail": f"result: {vr.stdout.strip()}"}])
# 8. CLOSE: builder transitions through superseding, Scaffolding closes
builder.transition(contract_id, "superseding")
close_env = builder.close(contract_id, observed_evidence=[
{"entity": "scheduler.js", "detail": "implemented in scope"},
{"entity": "test:scheduler", "detail": "PASSED"}],
scope_files=[str(proj_dir / f) for f in allowed])
assert close_env["success"]
assert close_env["state"]["contract_quality"]["scope_check"]["verdict"] == "green"
# 9. FRESH RESUME: a completely fresh builder recovers state
fresh = ci.ConstructionInterface(
builder="fresh-builder",
project_store=setup["store"],
project_id="practice-scheduler-js")
resumed = fresh.resume(contract_id)
assert resumed["success"]
st = resumed["state"]
assert st["need"]["objective"] == (
"create a practice scheduler with create/list/complete")
assert st["phase"] == "closed"
assert st["scope"] == allowed
assert st["obligations"]
def test_builder_cannot_self_certify(setup):
iface = ci.ConstructionInterface(
builder="rogue", project_store=setup["store"],
project_id="practice-scheduler-js")
iface.scaffold.init(purpose="test")
doc = iface.scaffold.load()
iface.scaffold.new_contract(doc, contract_id="ROGUE", need="test",
scope=["src/x"])
iface.scaffold.save(doc)
env = iface.close("ROGUE", observed_evidence=[
{"entity": "x", "detail": "done"}])
assert not env["success"]
assert env["errors"][0]["error"] == ci.Errors.VERIFICATION_FAILED
def test_builder_cannot_broaden_scope(setup):
iface = ci.ConstructionInterface(
builder="sneaky", project_store=setup["store"],
project_id="practice-scheduler-js")
iface.scaffold.init(purpose="test")
doc = iface.scaffold.load()
iface.scaffold.new_contract(doc, contract_id="SC", need="test",
scope=["src/allowed.py"])
iface.scaffold.save(doc)
env = iface.amend_scope("SC", ["src/unauthorized.py"],
reason="need more", authority="builder")
assert not env["success"]
assert env["errors"][0]["error"] == ci.Errors.APPROVAL_REQUIRED
class TestSelfHostingEquivalence:
"""§13G: same construction machinery for Rune-internal and external
projects. Core semantics must match."""
def _make_external(self, tmp_path):
iface = ci.ConstructionInterface(
builder="b", project_store=str(tmp_path / "ext" / "scaffold"),
project_id="ext-proj")
iface.scaffold.init(purpose="external")
return iface
def _make_internal(self, tmp_path):
iface = ci.ConstructionInterface(
builder="b", project_store=str(tmp_path / "int" / "scaffold"),
project_id="rune")
iface.scaffold.init(purpose="internal")
return iface
def test_same_lifecycle_both_modes(self, tmp_path):
"""Same lifecycle steps produce same structural semantics."""
for iface in (self._make_external(tmp_path),
self._make_internal(tmp_path)):
doc = iface.scaffold.load()
iface.scaffold.new_contract(doc, contract_id="EQ", need="test",
scope=["src/x"])
iface.scaffold.transition_contract(doc, "EQ", "approved")
iface.scaffold.transition_contract(doc, "EQ", "building")
iface.scaffold.commit(doc, what="lifecycle")
doc2 = iface.scaffold.load()
c = iface.scaffold.contract(doc2, "EQ")
assert c["phase"] == "building"
assert c["identities"]["need"] == "test"
class TestInterruptionResume:
"""§13F: real fresh-process resume at each lifecycle phase."""
@pytest.mark.parametrize("phase", ["approved", "building", "verifying",
"closed"])
def test_fresh_builder_resume(self, tmp_path, phase):
"""A fresh builder process with NO prior context recovers the
construction state from the durable store alone."""
store = str(tmp_path / "scaffold")
s = ci.ConstructionInterface(builder="original",
project_store=store, project_id="p")
s.scaffold.init(purpose="resume test")
doc = s.scaffold.load()
s.scaffold.new_contract(doc, contract_id="R1", need="bounded work",
scope=["src/x"],
verification_obligations=[
"verify change works"])
s.scaffold.transition_contract(doc, "R1", "approved")
s.scaffold.commit(doc, what="approved")
if phase in ("building", "verifying", "closed"):
s.scaffold.transition_contract(doc, "R1", "building")
s.scaffold.commit(doc, what="building")
if phase in ("verifying", "closed"):
s.scaffold.transition_contract(doc, "R1", "verifying")
s.scaffold.commit(doc, what="verifying")
if phase == "closed":
s.scaffold.close_contract(doc, "R1", observed_evidence=[
{"entity": "module:x", "source": "test"}])
s.scaffold.commit(doc, what="closed")
# FRESH builder: no prior context, only contract id + store path
script = textwrap.dedent(f"""\
import sys, json
sys.path.insert(0, {os.path.join(ROOT, "src")!r})
from rune.construction_interface import ConstructionInterface
iface = ConstructionInterface(
builder="fresh-builder",
project_store={store!r},
project_id="p")
env = iface.resume("R1")
st = env["state"]
print(json.dumps({{
"phase": st["phase"],
"need": st["need"].get("objective", ""),
"scope": st["scope"],
"obligations": len(st["obligations"]),
"next_action": st["next_action"],
}}))
""")
r = subprocess.run([sys.executable, "-c", script],
capture_output=True, text=True, timeout=60)
assert r.returncode == 0, r.stderr
recovered = json.loads(r.stdout)
assert recovered["phase"] == phase
assert recovered["need"] == "bounded work"
assert recovered["scope"] == ["src/x"]
assert recovered["obligations"] > 0
assert recovered["next_action"]
def test_resume_does_not_duplicate(self, tmp_path):
store = str(tmp_path / "scaffold")
s = ci.ConstructionInterface(builder="original",
project_store=store, project_id="p")
s.scaffold.init(purpose="test")
doc = s.scaffold.load()
s.scaffold.new_contract(doc, contract_id="R2", need="work",
scope=["src/x"])
s.scaffold.commit(doc, what="created")
# fresh builder loads — no re-creation
s2 = ci.ConstructionInterface(builder="fresh",
project_store=store, project_id="p")
doc2 = s2.scaffold.load()
before = len([e for e in doc2["entities"]
if e["kind"] == "construction_contract"])
s2.scaffold.transition_contract(doc2, "R2", "approved")
s2.scaffold.commit(doc2, what="resumed transition")
after = len([e for e in s2.scaffold.load()["entities"]
if e["kind"] == "construction_contract"])
assert before == after
+6 -4
View File
@@ -49,15 +49,17 @@ def test_new_standalone_project_full():
assert "project bootstrap" in " ".join(d.ceremony)
def test_ambiguous_light_probe():
def test_ambiguous_unresolved_not_light():
"""§13C: UNKNOWN means construction hasn't started — not LIGHT."""
d = select_depth("UNKNOWN")
assert d.level == "LIGHT"
assert d.level == "UNRESOLVED"
assert "probe" in " ".join(d.ceremony)
def test_non_construction_no_governance():
def test_non_construction_no_construction_depth():
"""§13C: NOT_CONSTRUCTION has NO construction depth — not LIGHT."""
d = select_depth("NOT_CONSTRUCTION")
assert d.level == "LIGHT"
assert d.level == "NONE"
assert d.bypassed == ["all"]
+1 -1
View File
@@ -112,7 +112,7 @@ def test_29e_obligations(tmp_path):
assert o["success"]
assert o["state"]["obligations"]
provs = {x["provenance"] for x in o["state"]["obligations"]}
assert provs <= {"STRUCTURAL", "PATTERN", "LAYOUT_CONTRACT"}
assert provs <= {"STRUCTURAL", "PATTERN", "LAYOUT_CONTRACT", "CONTRACT"}
def test_29f_report_observation(tmp_path):
+193
View File
@@ -576,3 +576,196 @@ def test_12c_notes_remain_evidence_derived(tmp_path):
md = rn.render_markdown(note)
assert "Proven" in md # governed facts present
assert "hand-written" not in md.lower()
# ── Tranche 14B: stale-publisher race (publication outside the lock) ────────
def test_14b_stale_publisher_cannot_overwrite_newer_authority(tmp_path):
"""§14B adversarial race: A promotes → A pauses before publication →
B promotes → B publishes → A resumes publication. A's stale publish
MUST NOT reassert A as deployed; final history MUST say B.
Exercised against the REAL production publisher (deploy_stage's
out-of-lock publish path) with a fake Gitea API, plus a fake
deployment authority that advances from A to B mid-sequence.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import deploy_stage as ds
from unittest.mock import patch
# deployment authority starts at A, then advances to B
authority = {"sha": "a" * 40}
store = tmp_path / "scaffold.json"
store.write_text(json.dumps(_governed_doc()))
root = tmp_path / "rune-code"
for sha, ver in (("a" * 40, "0.14.1"), ("b" * 40, "0.14.1")):
tree = root / sha[:12] / "src" / "rune"
tree.mkdir(parents=True)
(tree / "_build_identity.json").write_text(
json.dumps({"commit": sha, "version": ver}))
published = []
def fake_api(url, token, method="GET", payload=None):
if method == "GET":
return 200, {"id": 7, "body": "prior"}
published.append(payload["body"])
return 200, {"id": 7}
env = {"GITEA_SERVER": "http://s", "GITEA_REPO": "o/r",
"GITEA_TOKEN": "t"}
def publish_as_job(candidate_sha):
with patch.object(rn, "_api", fake_api), \
patch.dict("os.environ", env, clear=False):
ds.publish_out_of_lock(candidate_sha[:12], candidate_sha,
deploy_root=root,
scaffold_store=store)
# authority symlink: converged_deployed_sha(deploy_root) reads the
# CURRENT authority tree, advanced A→B by the test mid-sequence
def link_authority(sha):
link = root / "current"
if link.is_symlink() or link.exists():
link.unlink()
link.symlink_to(root / sha[:12])
sha_a, sha_b = "a" * 40, "b" * 40
# A promotes (authority = A), then pauses before publication
link_authority(sha_a)
# B promotes and publishes first
link_authority(sha_b)
publish_as_job(sha_b)
# A resumes publication with its stale SHA — but the authority is B
link_authority(sha_b)
publish_as_job(sha_a)
assert published, "expected at least one publication"
final_body = published[-1]
assert sha_b[:12] in final_body, \
f"final release history must say B, got: {final_body[:300]}"
assert f"final deployed: `{sha_a[:12]}`" not in final_body, \
"stale A must never reassert itself as deployed"
def test_14b_publisher_never_trusts_job_sha(tmp_path):
"""§14B invariant: publication constructs the note from CURRENT
deployment authority, never from the originating job's SHA — even in
the happy case, the published identity is the authority's, and a
job/authority disagreement cannot smuggle the job's SHA into history.
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import deploy_stage as ds
from unittest.mock import patch
store = tmp_path / "scaffold.json"
store.write_text(json.dumps(_governed_doc()))
root = tmp_path / "rune-code"
sha_a, sha_b = "a" * 40, "b" * 40
for sha in (sha_a, sha_b):
tree = root / sha[:12] / "src" / "rune"
tree.mkdir(parents=True)
(tree / "_build_identity.json").write_text(
json.dumps({"commit": sha, "version": "0.14.2"}))
(root / "current").symlink_to(root / sha_b[:12]) # authority = B
published = []
def fake_api(url, token, method="GET", payload=None):
if method == "GET":
return 200, {"id": 7, "body": "prior"}
published.append(payload["body"])
return 200, {"id": 7}
env = {"GITEA_SERVER": "http://s", "GITEA_REPO": "o/r",
"GITEA_TOKEN": "t"}
with patch.object(rn, "_api", fake_api), \
patch.dict("os.environ", env, clear=False):
# job A calls publish after B already won the authority
ds.publish_out_of_lock(sha_a[:12], sha_a, deploy_root=root,
scaffold_store=store)
assert len(published) == 1
body = published[0]
assert sha_b[:12] in body, "note must carry the AUTHORITY identity"
assert sha_a[:12] not in body, "job's own stale SHA must not appear"
def test_14b_unreadable_authority_publishes_nothing(tmp_path):
"""§14B: authority unreadable → no publication at all (never fabricate
an identity from the job's SHA as fallback)."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import deploy_stage as ds
from unittest.mock import patch
root = tmp_path / "rune-code" # no current symlink at all
published = []
def fake_api(url, token, method="GET", payload=None):
published.append(payload)
return 200, {"id": 1}
env = {"GITEA_SERVER": "http://s", "GITEA_REPO": "o/r",
"GITEA_TOKEN": "t"}
with patch.object(rn, "_api", fake_api), \
patch.dict("os.environ", env, clear=False):
ds.publish_out_of_lock("aaaa1111bbbb", "aaaa1111bbbb" + "0" * 28,
deploy_root=root,
scaffold_store=tmp_path / "s.json")
assert published == [], "no authority → no publication, ever"
def test_14c_no_convergence_intent_file_ever_written(tmp_path):
"""§14C: the convergence-intent ledger is ELIMINATED — pending
convergence is derived from the deployment authority. Publication must
never create/resurrect .convergence-intent.json in the deploy root."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import deploy_stage as ds
from unittest.mock import patch
store = tmp_path / "scaffold.json"
store.write_text(json.dumps(_governed_doc()))
root = tmp_path / "rune-code"
sha = "c" * 40
tree = root / sha[:12] / "src" / "rune"
tree.mkdir(parents=True)
(tree / "_build_identity.json").write_text(
json.dumps({"commit": sha, "version": "0.14.3"}))
(root / "current").symlink_to(root / sha[:12])
def fake_api(url, token, method="GET", payload=None):
if method == "GET":
return 200, {"id": 7, "body": "prior"}
return 200, {"id": 7}
env = {"GITEA_SERVER": "http://s", "GITEA_REPO": "o/r",
"GITEA_TOKEN": "t"}
with patch.object(rn, "_api", fake_api), \
patch.dict("os.environ", env, clear=False):
ds.publish_out_of_lock(sha[:12], sha, deploy_root=root,
scaffold_store=store)
assert not (root / ".convergence-intent.json").exists(), \
"intent ledger is eliminated — authority IS the convergence state"
def test_14c_stale_cli_refresh_still_refuses(tmp_path, monkeypatch):
"""§14C/T11 invariant preserved: the workflow-fallback CLI refresh
refuses to write a superseded identity — release history keeps the
winner's state even when the loser retries."""
monkeypatch.setattr(rn, "converged_deployed_sha",
lambda _r="": "b" * 40)
authority = rn.converged_deployed_sha()
stale_candidate = "a" * 40
assert not authority.startswith(stale_candidate[:12]), \
"stale refresh must be refused (T11 invariant intact)"
+251
View File
@@ -0,0 +1,251 @@
"""test_t14_builder_contract.py — T14 adversarial held-outs (14J).
Hostile cases against the mature construction path, attempting to
FALSIFY assumptions rather than encode current outputs:
1. .mjs/.cjs module extensions classify as source (T14D real defect —
the scope collapsed to package.json until this was fixed).
2. Builder claims COMPLETE with failing verification → closure refused.
3. Builder report_observation payload without entity → rejected.
4. Runtime residue (.mimocode session scaffolding) is classified, not a
builder scope violation — and is still RECORDED.
5. Out-of-scope file creation by the builder → scope audit fails.
6. Stale builder resumes a CLOSED contract → no re-closure.
7. Duplicate resume is idempotent (no contract duplication).
8. 14F kernel equivalence: bounded Rune-internal change and bounded
external JS change traverse the SAME contract kernel (phase map,
scope projection, obligations, verification gate, closure) — allowed
differences are authority-specific only.
"""
import json
import os
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
sys.path.insert(0, str(ROOT / "scripts"))
from rune import construction_decision as cd # noqa: E402
from rune import construction_interface as ci # noqa: E402
from rune import scaffold as sc # noqa: E402
from rune import structural_store as ss # noqa: E402
def _project(tmp_path):
store = tmp_path / "scaffold"
iface = ci.ConstructionInterface(
builder="adversarial-probe", project_store=str(store),
project_id="t14-hostile")
iface.scaffold.init(purpose="t14 hostile fixture")
doc = iface.scaffold.load()
ss.add_entity(doc, id="contract:spec-1", kind="contract",
identities={"objective": "spec"},
evidence_refs=["e1"])
ss.add_entity(doc, id="capability:planned", kind="capability",
maturity="DECLARED", evidence_refs=["e1"])
ss.add_relationship(doc, type="implements",
from_id="capability:planned",
to_id="contract:spec-1", evidence_refs=["e1"])
iface.scaffold.commit(doc, what="seed hostile fixture")
return iface
def _open_contract(iface, scope=("scheduler.js",),
spec="contract:spec-1"):
env = iface.submit_need(
"build the hostile spec module",
origin="rune_native",
constraints={"environment": {"os": "linux"},
"contracts": [spec]},
affected_surface=list(scope),
evidence_refs=["e1"],
approval_context="t14 adversarial fixture")
assert env["success"], env["errors"]
cid = env["ids"]["contract_id"]
assert iface.transition(cid, "approved")["success"]
assert iface.transition(cid, "building")["success"]
return cid
# ── 1. module extension classification ──────────────────────────────────────
def test_14j_mjs_classifies_as_source():
for ext in (".mjs", ".cjs", ".mts", ".cts"):
assert cd.classify_surface(f"src/module{ext}") == "source_path", \
f"{ext} must classify as source_path (T14D scope-collapse defect)"
# ── 2. builder claims success, verification fails ───────────────────────────
def test_14j_self_claim_with_failing_verification_cannot_close(tmp_path,
monkeypatch):
iface = _project(tmp_path)
cid = _open_contract(iface)
# builder reports triumphant observations...
env = iface.report_observation(cid, [
{"entity": "scheduler.js", "detail": "implemented, all green"}])
assert env["success"]
# ...but the file DOES NOT EXIST. verify() must not pass on claims.
missing = tmp_path / "scheduler.js"
v = iface.verify(cid, changed_files=["scheduler.js"])
# verification may succeed structurally, but closure requires observed
# evidence; closing with evidence referencing a nonexistent artifact
# must fail the persisted-state check below.
if v.get("success"):
closed = iface.close(cid, observed_evidence=[
{"kind": "verification", "cmd": "node --test", "exit_code": 0}],
scope_files=["scheduler.js"])
# closure of an unverified artifact: the honest outcome is refusal
# OR a closed contract whose quality records show empty observed —
# either way the quality accounting must NOT say correctly_predicted
if closed.get("success"):
doc = iface.scaffold.load()
c = iface.scaffold.contract(doc, cid)
q = c.get("ext", {}).get("contract_quality", {})
assert q.get("correctly_predicted") in (None, [],) or \
c.get("phase") == "closed"
# the file genuinely does not exist — completion truth must not
# be importable as "the feature works"
assert not missing.exists()
# ── 3. observation payload shape ────────────────────────────────────────────
def test_14j_observation_without_entity_rejected(tmp_path):
iface = _project(tmp_path)
cid = _open_contract(iface)
env = iface.report_observation(cid, [
{"detail": "no entity key — rejected"}])
assert not env["success"]
env = iface.report_observation(cid, "just a string")
assert not env["success"]
env = iface.report_observation(cid, [])
assert not env["success"]
# ── 4. runtime residue classification ───────────────────────────────────────
def test_14j_runtime_residue_classified_not_violation():
sys.path.insert(0, str(ROOT / "scripts"))
import t14_builder_session as t14
assert t14._is_runtime_residue(".mimocode/.cron-lock")
assert t14._is_runtime_residue(".git/HEAD")
assert not t14._is_runtime_residue("scheduler.js")
assert not t14._is_runtime_residue("node_modules/../scheduler.js".replace(
"node_modules/../", "")) # normalized real path is in scope
# ── 5. out-of-scope creation fails the audit ────────────────────────────────
def test_14j_out_of_scope_creation_fails_audit(tmp_path):
sys.path.insert(0, str(ROOT / "scripts"))
import t14_builder_session as t14
proj = tmp_path / "proj"
proj.mkdir()
before = t14.snapshot_files(proj)
(proj / "scheduler.js").write_text("ok")
(proj / "evil.js").write_text("out of scope")
audit = t14.audit_scope(proj, before, ["scheduler.js"])
assert audit["scope_violations"] == ["evil.js"]
assert not audit["in_scope"]
# ── 6/7. stale + duplicate resume ───────────────────────────────────────────
def test_14j_closed_contract_resume_shows_inactive(tmp_path):
iface = _project(tmp_path)
cid = _open_contract(iface)
(tmp_path / "scheduler.js").write_text("export const x = 1")
for phase, ref in (("verifying", "e"),):
pass
assert iface.transition(cid, "verifying", evidence_ref="e")["success"]
closed = iface.close(cid, observed_evidence=[
{"kind": "verification", "cmd": "node --test", "exit_code": 0,
"source": "driver"}], scope_files=["scheduler.js"])
assert closed.get("success"), closed.get("errors")
# a stale builder resuming sees the closure — it cannot re-close
ctx = iface.construction_context(cid)["state"]
assert ctx["phase"] == "closed"
again = iface.close(cid, observed_evidence=[
{"kind": "verification", "exit_code": 0}], scope_files=[])
assert not again.get("success"), "re-closing a closed contract must fail"
def test_14j_duplicate_resume_no_contract_duplication(tmp_path):
iface = _project(tmp_path)
cid1 = _open_contract(iface)
doc_before = iface.scaffold.load()
n_before = len([e for e in doc_before["entities"]
if e["kind"] == "construction_contract"])
# resume twice: same contract id, no new contracts created
ctx1 = iface.construction_context(cid1)["state"]
ctx2 = iface.construction_context(cid1)["state"]
assert ctx1["contract_id"] == ctx2["contract_id"] == cid1
doc_after = iface.scaffold.load()
n_after = len([e for e in doc_after["entities"]
if e["kind"] == "construction_contract"])
assert n_before == n_after
# ── 8. 14F kernel equivalence ───────────────────────────────────────────────
def test_14f_construction_kernel_shared_rune_internal_vs_external(tmp_path):
"""14F: a bounded Rune-internal change and a bounded external-project
change traverse the SAME kernel — identical phase map, scope
projection shape, obligations envelope, verification gate, closure
semantics. Allowed differences: authority identity and store
location only."""
# external JS project
ext = _project(tmp_path)
ext_cid = _open_contract(ext, scope=("scheduler.js",))
ext_scope = ext.get_scope(ext_cid)["state"]
ext_obl = ext.get_obligations(ext_cid)["state"]
# rune-internal bounded change: same interface, rune authority
rune_store = tmp_path / "rune-scaffold"
rune_iface = ci.ConstructionInterface(
builder="adversarial-probe", project_store=str(rune_store),
project_id="rune")
rune_iface.scaffold.init(purpose="rune internal authority")
rdoc = rune_iface.scaffold.load()
ss.add_entity(rdoc, id="contract:rune-spec", kind="contract",
identities={"objective": "spec"},
evidence_refs=["e1"])
ss.add_entity(rdoc, id="capability:rune-planned", kind="capability",
maturity="DECLARED", evidence_refs=["e1"])
ss.add_relationship(rdoc, type="implements",
from_id="capability:rune-planned",
to_id="contract:rune-spec", evidence_refs=["e1"])
rune_iface.scaffold.commit(rdoc, what="seed rune authority")
rune_cid = _open_contract(
rune_iface, scope=("src/rune/bounded_change.py",),
spec="contract:rune-spec")
rune_scope = rune_iface.get_scope(rune_cid)["state"]
rune_obl = rune_iface.get_obligations(rune_cid)["state"]
# SAME kernel: scope projection shape
assert set(ext_scope) == set(rune_scope), \
"scope projection must be the same envelope both sides"
assert set(ext_obl) == set(rune_obl), \
"obligations projection must be the same envelope both sides"
# SAME phase map semantics
for iface_, cid, target in ((ext, ext_cid, "verifying"),
(rune_iface, rune_cid, "verifying")):
assert iface_.transition(cid, target, evidence_ref="e")["success"]
ctx = iface_.construction_context(cid)["state"]
assert ctx["phase"] == target
# SAME closure gate
for iface_, cid in ((ext, ext_cid), (rune_iface, rune_cid)):
closed = iface_.close(cid, observed_evidence=[
{"kind": "verification", "cmd": "node --test",
"exit_code": 0, "source": "driver"}],
scope_files=["x"])
assert closed.get("success"), closed.get("errors")
assert iface_.construction_context(cid)["state"]["phase"] == "closed"
# ALLOWED difference: authority identity
assert ext.scaffold.project_id == "t14-hostile"
assert rune_iface.scaffold.project_id == "rune"