Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffa8608f95 | ||
|
|
3612786d33 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.262.0**
|
||||
**Current release: v0.263.0**
|
||||
|
||||
**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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.262.0"
|
||||
version = "0.263.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
+161
-24
@@ -17,6 +17,23 @@ Usage (as a library, from a generator script):
|
||||
("other_fn", (deep_copied_obj,), {}),
|
||||
], oracle_module="rune.module_name")
|
||||
|
||||
Class-based modules: a case may be a dict with a dotted fn name and a
|
||||
"recv" receiver spec — the receiver is rebuilt from JSON via shape-matched
|
||||
module dataclasses, called as an unbound method, and the expected carries
|
||||
BOTH the result and the post-call receiver attributes:
|
||||
|
||||
cases = freeze("rune.build_mode", [
|
||||
{"fn": "BuildManager.start_plan", "recv": {"cls": "BuildManager", "state": {...}},
|
||||
"args": [], "kwargs": {}},
|
||||
], oracle_module="rune.build_mode")
|
||||
|
||||
Pinned clock: the harness docstring always promised one — now it exists.
|
||||
Set RUNE_FROZEN_NOW (ISO-8601) to patch the oracle module's module-global
|
||||
`datetime` with a frozen clock for BOTH freeze and verify (timestamp-bearing
|
||||
fns stay byte-identical across processes). Envelope files written via
|
||||
write_envelope({"frozen_now", "cases"}) carry their own clock, so `--verify`
|
||||
needs no env at all.
|
||||
|
||||
Usage (as a script, to re-verify a frozen file against the live oracle):
|
||||
python3 scripts/freeze_truth.py --verify src/rust/<crate>/frozen_truth.json \
|
||||
--module rune.<module>
|
||||
@@ -29,10 +46,12 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import copy
|
||||
import dataclasses
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[1]
|
||||
@@ -44,42 +63,136 @@ def _pin_oracle_cores(module_name: str) -> None:
|
||||
os.environ[f"RUNE_{stem.upper()}_CORE"] = "python"
|
||||
|
||||
|
||||
def _frozen_clock(iso: str):
|
||||
"""A datetime subclass pinned to `iso` (naive); .now(tz) honors tz."""
|
||||
base = datetime.fromisoformat(iso)
|
||||
|
||||
class FrozenDatetime(datetime):
|
||||
@classmethod
|
||||
def now(cls, tz=None):
|
||||
if tz is not None:
|
||||
return base.replace(tzinfo=tz)
|
||||
return base
|
||||
|
||||
return FrozenDatetime
|
||||
|
||||
|
||||
def _pin_clock(mod) -> None:
|
||||
"""Patch the oracle module's module-global `datetime` when RUNE_FROZEN_NOW
|
||||
is set (modules that read a global datetime — e.g. build_mode._log_phase —
|
||||
then produce identical timestamps in every process)."""
|
||||
frozen_now = os.environ.get("RUNE_FROZEN_NOW", "")
|
||||
if frozen_now and hasattr(mod, "datetime"):
|
||||
mod.datetime = _frozen_clock(frozen_now)
|
||||
|
||||
|
||||
def _resolve_fn(mod, fn_name: str):
|
||||
"""Module-level name OR dotted path to a class method ("Cls.method")."""
|
||||
obj = mod
|
||||
for part in fn_name.split("."):
|
||||
obj = getattr(obj, part)
|
||||
return obj
|
||||
|
||||
|
||||
def _shape_rebuild(mod, value):
|
||||
"""Rebuild a JSON value into module types: a dict whose key set EXACTLY
|
||||
matches a module-level dataclass's field names is reconstructed as that
|
||||
dataclass (the convention that lets a frozen `state` dict become a live
|
||||
BuildState again in a fresh process). Everything else deep-copies."""
|
||||
if isinstance(value, dict):
|
||||
for name in dir(mod):
|
||||
obj = getattr(mod, name)
|
||||
if isinstance(obj, type) and dataclasses.is_dataclass(obj):
|
||||
field_names = {f.name for f in dataclasses.fields(obj)}
|
||||
if field_names and field_names == set(value.keys()):
|
||||
return obj(**{k: _shape_rebuild(mod, v) for k, v in value.items()})
|
||||
return {k: _shape_rebuild(mod, v) for k, v in value.items()}
|
||||
if isinstance(value, list):
|
||||
return [_shape_rebuild(mod, v) for v in value]
|
||||
return copy.deepcopy(value)
|
||||
|
||||
|
||||
def _rebuild_receiver(mod, recv: dict):
|
||||
"""recv = {"cls": "BuildManager", <attr>: <json value>, ...} → a live
|
||||
instance built via __new__ (no __init__ — that would touch the real
|
||||
filesystem) with the named attrs rebuilt through _shape_rebuild."""
|
||||
cls = getattr(mod, recv["cls"])
|
||||
inst = cls.__new__(cls)
|
||||
for key, value in recv.items():
|
||||
if key == "cls":
|
||||
continue
|
||||
setattr(inst, key, _shape_rebuild(mod, value))
|
||||
return inst
|
||||
|
||||
|
||||
def _out(value):
|
||||
"""JSON-safe outward snapshot: dataclass instances → asdict."""
|
||||
if dataclasses.is_dataclass(value) and not isinstance(value, type):
|
||||
return dataclasses.asdict(value)
|
||||
return copy.deepcopy(value)
|
||||
|
||||
|
||||
def _snap(value):
|
||||
"""Deep-snapshot a value into JSON-safe form (live-reference aliasing law)."""
|
||||
return copy.deepcopy(value)
|
||||
|
||||
|
||||
def _run_case(mod, entry) -> dict:
|
||||
"""Execute one case (tuple or dict form) against the live module."""
|
||||
recv = None
|
||||
if isinstance(entry, dict):
|
||||
fn_name = entry["fn"]
|
||||
args = entry.get("args", [])
|
||||
kwargs = entry.get("kwargs", {})
|
||||
recv = entry.get("recv")
|
||||
elif len(entry) == 3:
|
||||
fn_name, args, kwargs = entry
|
||||
elif len(entry) == 2:
|
||||
fn_name, args = entry
|
||||
kwargs = {}
|
||||
else:
|
||||
raise ValueError(f"bad case entry: {entry!r}")
|
||||
|
||||
fn = _resolve_fn(mod, fn_name)
|
||||
a_snap = _snap(list(args))
|
||||
k_snap = _snap(dict(kwargs))
|
||||
|
||||
if recv is None:
|
||||
result = fn(*copy.deepcopy(a_snap), **copy.deepcopy(k_snap))
|
||||
expected = _snap(result)
|
||||
return {"fn": fn_name, "args": a_snap, "kwargs": k_snap, "expected": expected}
|
||||
|
||||
inst = _rebuild_receiver(mod, dict(recv))
|
||||
result = fn(inst, *copy.deepcopy(a_snap), **copy.deepcopy(k_snap))
|
||||
expected = {"result": _snap(result)}
|
||||
for key in recv:
|
||||
if key == "cls":
|
||||
continue
|
||||
try:
|
||||
expected[key] = _out(getattr(inst, key))
|
||||
except AttributeError:
|
||||
pass # attr was never set — the case reconstructed a bare receiver
|
||||
return {
|
||||
"fn": fn_name, "recv": dict(recv),
|
||||
"args": a_snap, "kwargs": k_snap, "expected": expected,
|
||||
}
|
||||
|
||||
|
||||
def freeze(module_name: str, cases, oracle_module: str | None = None) -> list[dict]:
|
||||
"""Run each (fn_name, args, kwargs) against the live oracle; return truth.
|
||||
"""Run each case against the live oracle; return truth.
|
||||
|
||||
Args and kwargs are deep-copied before EVERY call — the oracle may
|
||||
mutate its inputs (live-reference aliasing trap). The recorded "args"
|
||||
are the post-copy snapshots, so the battery replays byte-identical
|
||||
inputs.
|
||||
inputs. Case entries are (fn, args[, kwargs]) tuples or dicts with a
|
||||
dotted "fn" and optional "recv" receiver spec (class-based modules).
|
||||
"""
|
||||
_pin_oracle_cores(oracle_module or module_name)
|
||||
mod = importlib.import_module(oracle_module or module_name)
|
||||
_pin_clock(mod)
|
||||
truth = []
|
||||
for entry in cases:
|
||||
if len(entry) == 3:
|
||||
fn_name, args, kwargs = entry
|
||||
elif len(entry) == 2:
|
||||
fn_name, args = entry
|
||||
kwargs = {}
|
||||
else:
|
||||
raise ValueError(f"bad case entry: {entry!r}")
|
||||
fn = getattr(mod, fn_name, None)
|
||||
if fn is None:
|
||||
raise AttributeError(f"{oracle_module or module_name}.{fn_name} missing — case list must name real functions")
|
||||
a_snap = _snap(list(args))
|
||||
k_snap = _snap(dict(kwargs))
|
||||
result = fn(*copy.deepcopy(a_snap), **copy.deepcopy(k_snap))
|
||||
truth.append({
|
||||
"fn": fn_name,
|
||||
"args": a_snap,
|
||||
"kwargs": k_snap,
|
||||
"expected": _snap(result),
|
||||
})
|
||||
truth.append(_run_case(mod, entry))
|
||||
return truth
|
||||
|
||||
|
||||
@@ -88,14 +201,38 @@ def write(truth: list[dict], path: Path) -> None:
|
||||
path.write_text(json.dumps(truth, indent=2, ensure_ascii=False, sort_keys=False) + "\n")
|
||||
|
||||
|
||||
def write_envelope(frozen_now: str, truth: list[dict], path: Path) -> None:
|
||||
"""Self-contained envelope: the file carries its own pinned clock, so
|
||||
`--verify` needs no RUNE_FROZEN_NOW env. List-format files (write)
|
||||
stay legacy-valid."""
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(
|
||||
{"frozen_now": frozen_now, "cases": truth},
|
||||
indent=2, ensure_ascii=False, sort_keys=False) + "\n")
|
||||
|
||||
|
||||
def verify(path: Path, oracle_module: str) -> int:
|
||||
"""Re-freeze from the live oracle and byte-compare against the frozen file.
|
||||
|
||||
Catches self-consistent misreads: the battery and the generator can
|
||||
agree while the oracle drifted. Returns 0 on byte-exact match.
|
||||
agree while the oracle drifted. Returns 0 on byte-exact match. Accepts
|
||||
a bare case list (legacy) or a {"frozen_now", "cases"} envelope (clock
|
||||
pinned from the file itself).
|
||||
"""
|
||||
frozen = json.loads(path.read_text())
|
||||
cases = [(c["fn"], c["args"], c["kwargs"]) for c in frozen]
|
||||
raw = json.loads(path.read_text())
|
||||
if isinstance(raw, dict):
|
||||
if raw.get("frozen_now"):
|
||||
os.environ["RUNE_FROZEN_NOW"] = raw["frozen_now"]
|
||||
frozen = raw["cases"]
|
||||
else:
|
||||
frozen = raw
|
||||
cases = []
|
||||
for c in frozen:
|
||||
if isinstance(c, dict):
|
||||
cases.append({"fn": c["fn"], "args": c.get("args", []),
|
||||
"kwargs": c.get("kwargs", {}), "recv": c.get("recv")})
|
||||
else:
|
||||
cases.append((c["fn"], c["args"], c["kwargs"]))
|
||||
fresh = freeze(oracle_module, cases, oracle_module)
|
||||
if json.dumps(fresh, sort_keys=True) != json.dumps(frozen, sort_keys=True):
|
||||
print(f"MISMATCH: regenerated truth differs from {path}")
|
||||
|
||||
@@ -0,0 +1,245 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_build_mode_gap_truth.py — build-mode gap-closing truth battery
|
||||
(contract BUILDER-2026-10-13T02:02:02Z-dad27f-factory-gap-bm).
|
||||
|
||||
Closes the PARTIAL audit gaps: _load_state, _save_state, assess_complexity
|
||||
and the full start_* phase-transition family (start_research, start_feasible,
|
||||
start_consult, start_consent, start_plan, start_build_phase, start_verify,
|
||||
start_integrate, start_record) — the build-pipeline state machine, frozen
|
||||
transition-by-transition in declaration order (phase names and ordering are
|
||||
load-bearing for the pipeline).
|
||||
|
||||
Uses the SHARED harness (scripts/freeze_truth.py): dotted fn names + recv
|
||||
receiver rebuild + RUNE_FROZEN_NOW pinned clock. The envelope carries its
|
||||
own frozen_now, so `--verify` needs no env:
|
||||
|
||||
PYTHONPATH=src python3 scripts/freeze_truth.py \
|
||||
--verify src/go/build-mode/truth_buildmode_gap.json \
|
||||
--module rune.build_mode
|
||||
|
||||
NOTE: the oracle's _log_phase/_save_state write to BUILDS_DIR (module
|
||||
global from expanduser). Run generator AND verify with a sandboxed HOME to
|
||||
contain the incidental state files (truth never reads them back):
|
||||
|
||||
HOME=$(mktemp -d) PYTHONPATH=src python3 scripts/freeze_truth.py ...
|
||||
|
||||
_load_state/_save_state are IO seams: _save_state's byte contract is frozen
|
||||
as ledger_load_fixture.json (exact bytes of json.dump(asdict(state),
|
||||
indent=2)); the generator also proves the oracle roundtrip (write fixture →
|
||||
_load_state → equal state). _load_state's FileNotFoundError path is pinned
|
||||
by the Go test (error text "Build <id> not found").
|
||||
|
||||
Usage: python3 scripts/gen_build_mode_gap_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from freeze_truth import _frozen_clock, freeze, write_envelope # noqa: E402
|
||||
from rune import build_mode as bm # noqa: E402
|
||||
|
||||
FROZEN_NOW = "2026-10-13T02:20:00"
|
||||
STAMP = FROZEN_NOW + "+00:00"
|
||||
BUILD_ID = "20261013-022000"
|
||||
|
||||
os.environ["RUNE_FROZEN_NOW"] = FROZEN_NOW # pinned clock for the harness
|
||||
os.environ["RUNE_BUILD_MODE_CORE"] = "python" # poisoned-truth law
|
||||
bm.datetime = _frozen_clock(FROZEN_NOW) # pin the SEED walk too (recv states
|
||||
# must not carry live-clock timestamps — the truth must be self-consistent)
|
||||
|
||||
# BYTE-PARITY LAW for the IO fixture: Go maps marshal dict keys SORTED, so
|
||||
# every generator-supplied dict below uses sorted keys (Python insertion
|
||||
# order == sorted order) — then SaveState can reproduce json.dump bytes
|
||||
# exactly. The oracle-internal feasible payload (feasible, risk, details)
|
||||
# is the one unavoidable exception — fixture therefore comes from the
|
||||
# correction-cycle state (single-key payloads only); dict-heavy states are
|
||||
# parity-checked at the parsed level (truth replay + roundtrip test).
|
||||
|
||||
|
||||
def fresh_mgr() -> bm.BuildManager:
|
||||
m = bm.BuildManager.__new__(bm.BuildManager)
|
||||
m.state = bm.BuildState(
|
||||
build_id=BUILD_ID, description="gap battery build", created_at=STAMP)
|
||||
return m
|
||||
|
||||
|
||||
def seed(mgr) -> dict:
|
||||
return dataclasses.asdict(mgr.state)
|
||||
|
||||
|
||||
def main():
|
||||
cases = []
|
||||
mgr = fresh_mgr()
|
||||
|
||||
def call(fn, *args, **kwargs):
|
||||
"""Freeze one transition: recv = PRE-state, then apply to the live
|
||||
walk manager (the harness re-runs it on a rebuilt receiver)."""
|
||||
cases.append({
|
||||
"fn": f"BuildManager.{fn.__name__}",
|
||||
"recv": {"cls": "BuildManager", "state": seed(mgr)},
|
||||
"args": list(args), "kwargs": kwargs,
|
||||
})
|
||||
fn(mgr, *args, **kwargs)
|
||||
|
||||
# ── assess_complexity ────────────────────────────────────────────
|
||||
# bare receiver (the fn never touches state) + a seeded receiver.
|
||||
cases.append({"fn": "BuildManager.assess_complexity",
|
||||
"recv": {"cls": "BuildManager"}, "args": [], "kwargs": {}})
|
||||
cases.append({"fn": "BuildManager.assess_complexity",
|
||||
"recv": {"cls": "BuildManager", "state": seed(mgr)},
|
||||
"args": [], "kwargs": {}})
|
||||
|
||||
# ── the canonical 11-phase happy path, IN DECLARATION ORDER ──────
|
||||
call(bm.BuildManager.complete_assess,
|
||||
bm.BuildManager.assess_complexity(mgr))
|
||||
call(bm.BuildManager.start_research)
|
||||
call(bm.BuildManager.complete_research,
|
||||
{"findings": "gap battery pattern", "source": "oracle"})
|
||||
call(bm.BuildManager.start_feasible)
|
||||
call(bm.BuildManager.complete_feasible, True, "low",
|
||||
{"loc_estimate": 120, "worktree_isolated": True})
|
||||
call(bm.BuildManager.start_consult)
|
||||
call(bm.BuildManager.complete_consult, ["gap-closing pattern"])
|
||||
call(bm.BuildManager.start_consent)
|
||||
call(bm.BuildManager.complete_consent, True)
|
||||
call(bm.BuildManager.start_plan)
|
||||
call(bm.BuildManager.complete_plan,
|
||||
[{"batch": 1, "files": ["a.py"]}, {"batch": 2, "files": ["b.py"]}])
|
||||
call(bm.BuildManager.start_build_phase)
|
||||
call(bm.BuildManager.complete_build_phase,
|
||||
[{"batch": 1, "ok": True}, {"batch": 2, "ok": True}])
|
||||
call(bm.BuildManager.start_verify)
|
||||
call(bm.BuildManager.complete_verify, True, [])
|
||||
call(bm.BuildManager.start_integrate)
|
||||
call(bm.BuildManager.complete_integrate, True, [])
|
||||
call(bm.BuildManager.start_record)
|
||||
call(bm.BuildManager.complete_record)
|
||||
|
||||
# ── _save_state branches ─────────────────────────────────────────
|
||||
# state None → early return (no file, no dirs)
|
||||
cases.append({"fn": "BuildManager._save_state",
|
||||
"recv": {"cls": "BuildManager", "state": None},
|
||||
"args": [], "kwargs": {}})
|
||||
# state present → writes BUILDS_DIR/<id>/ledger.json (sandboxed HOME);
|
||||
# result None + state unchanged.
|
||||
call(bm.BuildManager._save_state)
|
||||
|
||||
# ── correction cycle (corrections increment; fixed False and True) ─
|
||||
cMgr = fresh_mgr()
|
||||
cMgr.state.corrections = 1
|
||||
def ccall(fn, *args, **kwargs):
|
||||
cases.append({
|
||||
"fn": f"BuildManager.{fn.__name__}",
|
||||
"recv": {"cls": "BuildManager", "state": seed(cMgr)},
|
||||
"args": list(args), "kwargs": kwargs,
|
||||
})
|
||||
fn(cMgr, *args, **kwargs)
|
||||
ccall(bm.BuildManager.start_correct)
|
||||
ccall(bm.BuildManager.complete_correct, False)
|
||||
ccall(bm.BuildManager.start_correct)
|
||||
ccall(bm.BuildManager.complete_correct, True)
|
||||
|
||||
# ── verify/integrate FAILURE branches (failure lives in DETAILS —
|
||||
# status stays "complete", the pinned oracle quirk) ─────────────
|
||||
fMgr = fresh_mgr()
|
||||
def fcall(fn, *args, **kwargs):
|
||||
cases.append({
|
||||
"fn": f"BuildManager.{fn.__name__}",
|
||||
"recv": {"cls": "BuildManager", "state": seed(fMgr)},
|
||||
"args": list(args), "kwargs": kwargs,
|
||||
})
|
||||
fn(fMgr, *args, **kwargs)
|
||||
fcall(bm.BuildManager.complete_verify, False, ["test_x failed"])
|
||||
fcall(bm.BuildManager.start_integrate)
|
||||
fcall(bm.BuildManager.complete_integrate, False, ["scope breach"])
|
||||
|
||||
# ── feasibility details=None branch (default {}) ─────────────────
|
||||
dMgr = fresh_mgr()
|
||||
cases.append({
|
||||
"fn": "BuildManager.complete_feasible",
|
||||
"recv": {"cls": "BuildManager", "state": seed(dMgr)},
|
||||
"args": [True, "low"], "kwargs": {},
|
||||
})
|
||||
|
||||
# ── resume scan branches (get_resume_point / can_resume) ─────────
|
||||
# state None → None/False; empty phases → None; in_progress → found;
|
||||
# failed → found; all complete → None.
|
||||
def scan_case(state_dict):
|
||||
cases.append({"fn": "BuildManager.get_resume_point",
|
||||
"recv": {"cls": "BuildManager", "state": state_dict},
|
||||
"args": [], "kwargs": {}})
|
||||
cases.append({"fn": "BuildManager.can_resume",
|
||||
"recv": {"cls": "BuildManager", "state": state_dict},
|
||||
"args": [], "kwargs": {}})
|
||||
|
||||
scan_case(None)
|
||||
scan_case(seed(fresh_mgr()))
|
||||
p = seed(fresh_mgr())
|
||||
p["phases"]["plan"] = "in_progress"
|
||||
scan_case(p)
|
||||
f = seed(fresh_mgr())
|
||||
f["phases"]["verify"] = "failed"
|
||||
scan_case(f)
|
||||
scan_case(seed(mgr)) # fully walked: every phase complete
|
||||
|
||||
# ── get_status branches ──────────────────────────────────────────
|
||||
cases.append({"fn": "BuildManager.get_status",
|
||||
"recv": {"cls": "BuildManager", "state": None},
|
||||
"args": [], "kwargs": {}})
|
||||
cases.append({"fn": "BuildManager.get_status",
|
||||
"recv": {"cls": "BuildManager", "state": seed(mgr)},
|
||||
"args": [], "kwargs": {}})
|
||||
|
||||
# ── _load_state IO roundtrip + byte fixture ──────────────────────
|
||||
# The fixture is the CORRECTION-CYCLE state: every dict in it is
|
||||
# single-key or sorted-key (see BYTE-PARITY LAW) — the one state whose
|
||||
# json.dump bytes Go can reproduce exactly. The full-walk state (with
|
||||
# the oracle-internal feasible payload) is roundtrip-proven at the
|
||||
# parsed level instead.
|
||||
full_state = dataclasses.asdict(mgr.state)
|
||||
fixture_state = dataclasses.asdict(cMgr.state)
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
bm.BUILDS_DIR = tmp # oracle reads the module global at call time
|
||||
build_dir = Path(tmp) / BUILD_ID
|
||||
build_dir.mkdir(parents=True)
|
||||
ledger_bytes = json.dumps(fixture_state, indent=2,
|
||||
ensure_ascii=False).encode("utf-8")
|
||||
(build_dir / "ledger.json").write_bytes(ledger_bytes)
|
||||
full_bytes = json.dumps(full_state, indent=2,
|
||||
ensure_ascii=False).encode("utf-8")
|
||||
(build_dir.parent / "full-walk-build").mkdir()
|
||||
(build_dir.parent / "full-walk-build" / "ledger.json").write_bytes(
|
||||
full_bytes)
|
||||
m2 = bm.BuildManager.__new__(bm.BuildManager)
|
||||
loaded = bm.BuildManager._load_state(m2, BUILD_ID)
|
||||
assert dataclasses.asdict(loaded) == fixture_state, "roundtrip broke"
|
||||
full_id = "full-walk-build"
|
||||
m3 = bm.BuildManager.__new__(bm.BuildManager)
|
||||
loaded_full = bm.BuildManager._load_state(m3, full_id)
|
||||
assert dataclasses.asdict(loaded_full) == full_state, "full broke"
|
||||
# missing build → FileNotFoundError("Build <id> not found")
|
||||
try:
|
||||
bm.BuildManager._load_state(m2, "no-such-build")
|
||||
raise SystemExit("_load_state missing-path did NOT raise")
|
||||
except FileNotFoundError as e:
|
||||
assert str(e) == f"Build no-such-build not found", str(e)
|
||||
fixture = REPO / "src/go/build-mode" / "ledger_load_fixture.json"
|
||||
fixture.write_bytes(ledger_bytes)
|
||||
print(f"wrote {fixture} ({len(ledger_bytes)} bytes, oracle roundtrip proven)")
|
||||
|
||||
truth = freeze("rune.build_mode", cases, "rune.build_mode")
|
||||
out = REPO / "src/go/build-mode" / "truth_buildmode_gap.json"
|
||||
write_envelope(FROZEN_NOW, truth, out)
|
||||
print(f"wrote {out} ({len(truth)} frozen cases)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -223,7 +223,12 @@ func StartBuildID(nowUTC string) string {
|
||||
|
||||
// ResumePoint — reversed declaration-order scan: the first phase whose
|
||||
// status is in_progress or failed; "" when all complete (Python None).
|
||||
// Nil state → "" (the Python's `if not self.state: return None` guard,
|
||||
// pinned by truth_buildmode_gap.json).
|
||||
func ResumePoint(state *BuildState) string {
|
||||
if state == nil {
|
||||
return ""
|
||||
}
|
||||
for i := len(Phases) - 1; i >= 0; i-- {
|
||||
status, has := state.Phases[Phases[i]]
|
||||
if !has {
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
package main
|
||||
|
||||
// build_mode_gap.go — gap-closing batch: the PARTIAL audit's missing fns of
|
||||
// src/rune/build_mode.py (contract BUILDER-2026-10-13T02:02:02Z-dad27f-
|
||||
// factory-gap-bm). _load_state/_save_state (the ledger IO seam, buildsDir
|
||||
// INJECTED — the Python reads a module global), assess_complexity, and the
|
||||
// full start_* phase-transition family. Phase names and ordering are
|
||||
// load-bearing for the pipeline: every transition here is frozen
|
||||
// transition-by-transition in declaration order in
|
||||
// truth_buildmode_gap.json (live oracle, clock pinned via the shared
|
||||
// freeze_truth harness).
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// LoadState — _load_state: read BUILDS_DIR/<build_id>/ledger.json into a
|
||||
// BuildState. Missing file → the Python FileNotFoundError text.
|
||||
func LoadState(buildsDir, buildID string) (*BuildState, error) {
|
||||
ledgerPath := filepath.Join(buildsDir, buildID, "ledger.json")
|
||||
if _, err := os.Stat(ledgerPath); err != nil {
|
||||
return nil, fmt.Errorf("Build %s not found", buildID)
|
||||
}
|
||||
data, err := os.ReadFile(ledgerPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var state BuildState
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &state, nil
|
||||
}
|
||||
|
||||
// SaveState — _save_state: write the state as asdict-shape JSON (indent 2,
|
||||
// no HTML escaping, no trailing newline — Python json.dump byte contract,
|
||||
// pinned by ledger_load_fixture.json). Nil state → no-op (the Python's
|
||||
// `if not self.state: return`).
|
||||
//
|
||||
// BYTE PARITY TRAP: Python dict preserves INSERTION order and json.dump
|
||||
// emits it; Go maps marshal sorted. The live manager creates the phases
|
||||
// keys via _log_phase, so the ledger's first-occurrence order IS the dict
|
||||
// order — the phases object is spliced in that order (keys missing from
|
||||
// the ledger fall back to pipeline declaration order). The nil sentinel
|
||||
// replaces exactly the one top-level "phases" key.
|
||||
func SaveState(state *BuildState, buildsDir string) error {
|
||||
if state == nil {
|
||||
return nil
|
||||
}
|
||||
buildDir := filepath.Join(buildsDir, state.BuildID)
|
||||
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := stateJSON(state)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(filepath.Join(buildDir, "ledger.json"), data, 0o644)
|
||||
}
|
||||
|
||||
// stateJSON — the Python json.dump byte contract (indent 2, no HTML
|
||||
// escaping, no trailing newline, phases in dict-insertion order).
|
||||
func stateJSON(state *BuildState) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
enc := json.NewEncoder(&buf)
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
saved := state.Phases
|
||||
state.Phases = nil
|
||||
err := enc.Encode(state)
|
||||
state.Phases = saved
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := bytes.Replace(buf.Bytes(), []byte(`"phases": null`),
|
||||
orderedPhasesJSON(state), 1)
|
||||
return bytes.TrimRight(out, "\n"), nil
|
||||
}
|
||||
|
||||
// orderedPhasesJSON — the phases object in Python-dict insertion order
|
||||
// (ledger first-occurrence, then declaration order, then sorted rest).
|
||||
func orderedPhasesJSON(state *BuildState) []byte {
|
||||
order := make([]string, 0, len(state.Phases))
|
||||
seen := make(map[string]bool, len(state.Phases))
|
||||
for _, e := range state.Ledger {
|
||||
if _, has := state.Phases[e.Phase]; has && !seen[e.Phase] {
|
||||
seen[e.Phase] = true
|
||||
order = append(order, e.Phase)
|
||||
}
|
||||
}
|
||||
for _, p := range Phases {
|
||||
if _, has := state.Phases[p]; has && !seen[p] {
|
||||
seen[p] = true
|
||||
order = append(order, p)
|
||||
}
|
||||
}
|
||||
rest := make([]string, 0)
|
||||
for p := range state.Phases {
|
||||
if !seen[p] {
|
||||
rest = append(rest, p)
|
||||
}
|
||||
}
|
||||
sort.Strings(rest)
|
||||
order = append(order, rest...)
|
||||
|
||||
var b bytes.Buffer
|
||||
// Python json.dump(indent=2) nests the phases entries one level deeper
|
||||
// (4-space entries, 2-space closing brace).
|
||||
b.WriteString(`"phases": {`)
|
||||
for i, p := range order {
|
||||
if i > 0 {
|
||||
b.WriteByte(',')
|
||||
}
|
||||
b.WriteString("\n ")
|
||||
k, _ := json.Marshal(p)
|
||||
v, _ := json.Marshal(state.Phases[p])
|
||||
b.Write(k)
|
||||
b.WriteString(": ")
|
||||
b.Write(v)
|
||||
}
|
||||
if len(order) > 0 {
|
||||
b.WriteString("\n ")
|
||||
}
|
||||
b.WriteString("}")
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// AssessComplexity — assess_complexity: the placeholder report, verbatim.
|
||||
func AssessComplexity() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"complexity": "complex", // simple/medium/complex
|
||||
"estimated_steps": 10,
|
||||
"requires_build_mode": true,
|
||||
}
|
||||
}
|
||||
|
||||
// StartResearch — start_research.
|
||||
func StartResearch(state *BuildState, nowISO string) {
|
||||
LogPhase(state, "research", StatusInProgress, nil, nil, nowISO)
|
||||
}
|
||||
|
||||
// StartFeasible — start_feasible.
|
||||
func StartFeasible(state *BuildState, nowISO string) {
|
||||
LogPhase(state, "feasible", StatusInProgress, nil, nil, nowISO)
|
||||
}
|
||||
|
||||
// StartConsult — start_consult.
|
||||
func StartConsult(state *BuildState, nowISO string) {
|
||||
LogPhase(state, "consult", StatusInProgress, nil, nil, nowISO)
|
||||
}
|
||||
|
||||
// StartConsent — start_consent.
|
||||
func StartConsent(state *BuildState, nowISO string) {
|
||||
LogPhase(state, "consent", StatusInProgress, nil, nil, nowISO)
|
||||
}
|
||||
|
||||
// StartPlan — start_plan.
|
||||
func StartPlan(state *BuildState, nowISO string) {
|
||||
LogPhase(state, "plan", StatusInProgress, nil, nil, nowISO)
|
||||
}
|
||||
|
||||
// StartBuildPhase — start_build_phase.
|
||||
func StartBuildPhase(state *BuildState, nowISO string) {
|
||||
LogPhase(state, "build", StatusInProgress, nil, nil, nowISO)
|
||||
}
|
||||
|
||||
// StartVerify — start_verify.
|
||||
func StartVerify(state *BuildState, nowISO string) {
|
||||
LogPhase(state, "verify", StatusInProgress, nil, nil, nowISO)
|
||||
}
|
||||
|
||||
// StartIntegrate — start_integrate.
|
||||
func StartIntegrate(state *BuildState, nowISO string) {
|
||||
LogPhase(state, "integrate", StatusInProgress, nil, nil, nowISO)
|
||||
}
|
||||
|
||||
// StartRecord — start_record.
|
||||
func StartRecord(state *BuildState, nowISO string) {
|
||||
LogPhase(state, "record", StatusInProgress, nil, nil, nowISO)
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"build_id": "20261013-022000",
|
||||
"description": "gap battery build",
|
||||
"created_at": "2026-10-13T02:20:00+00:00",
|
||||
"worktree_path": null,
|
||||
"current_phase": "assess",
|
||||
"phases": {
|
||||
"correct": "complete"
|
||||
},
|
||||
"ledger": [
|
||||
{
|
||||
"phase": "correct",
|
||||
"status": "in_progress",
|
||||
"timestamp": "2026-10-13T02:20:00+00:00",
|
||||
"details": {},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "correct",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-13T02:20:00+00:00",
|
||||
"details": {
|
||||
"fixed": false
|
||||
},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "correct",
|
||||
"status": "in_progress",
|
||||
"timestamp": "2026-10-13T02:20:00+00:00",
|
||||
"details": {},
|
||||
"errors": []
|
||||
},
|
||||
{
|
||||
"phase": "correct",
|
||||
"status": "complete",
|
||||
"timestamp": "2026-10-13T02:20:00+00:00",
|
||||
"details": {
|
||||
"fixed": true
|
||||
},
|
||||
"errors": []
|
||||
}
|
||||
],
|
||||
"batches": [],
|
||||
"corrections": 3,
|
||||
"max_corrections": 3,
|
||||
"current_build_phase": "foundation",
|
||||
"trajectory": [],
|
||||
"failure_patterns": [],
|
||||
"complexity_score": 0,
|
||||
"estimated_steps": 0,
|
||||
"actual_steps": 0,
|
||||
"files_changed": [],
|
||||
"contracts": []
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,257 @@
|
||||
package main
|
||||
|
||||
// truth_buildmode_gap_test.go — gap-closing parity gate: replays
|
||||
// truth_buildmode_gap.json VERBATIM (go:embed — the shared freeze_truth
|
||||
// harness output, live oracle, clock pinned) through the ported functions.
|
||||
// Every case carries a recv receiver (the PRE state), the call args, and
|
||||
// the expected post-call state + result; the Go replay must match
|
||||
// byte-for-byte after canonical JSON. Contract
|
||||
// BUILDER-2026-10-13T02:02:02Z-dad27f-factory-gap-bm.
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
//go:embed truth_buildmode_gap.json
|
||||
var gapTruth []byte
|
||||
|
||||
//go:embed ledger_load_fixture.json
|
||||
var ledgerFixture []byte
|
||||
|
||||
type gapEnvelope struct {
|
||||
FrozenNow string `json:"frozen_now"`
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Recv struct {
|
||||
Cls string `json:"cls"`
|
||||
State json.RawMessage `json:"state"`
|
||||
} `json:"recv"`
|
||||
Args []interface{} `json:"args"`
|
||||
Expected struct {
|
||||
Result json.RawMessage `json:"result"`
|
||||
State json.RawMessage `json:"state"`
|
||||
} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
|
||||
func TestBuildModeGapParity(t *testing.T) {
|
||||
var doc gapEnvelope
|
||||
if err := json.Unmarshal(gapTruth, &doc); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
stamp := doc.FrozenNow + "+00:00"
|
||||
for i, c := range doc.Cases {
|
||||
name := c.Fn
|
||||
t.Run(name, func(t *testing.T) {
|
||||
// Rebuild the receiver: recv.state null → nil state.
|
||||
var state *BuildState
|
||||
if string(c.Recv.State) != "null" && len(c.Recv.State) > 0 {
|
||||
state = &BuildState{}
|
||||
if err := json.Unmarshal(c.Recv.State, state); err != nil {
|
||||
t.Fatalf("recv state unparseable: %v", err)
|
||||
}
|
||||
}
|
||||
got, gotState := dispatchGap(t, state, c.Fn, c.Args, stamp)
|
||||
canonEq(t, "result", got, c.Expected.Result)
|
||||
// state: absent/null expected → no state attr or nil state;
|
||||
// else canonical compare.
|
||||
if len(c.Expected.State) == 0 || string(c.Expected.State) == "null" {
|
||||
if len(c.Expected.State) == 0 {
|
||||
return // receiver never had the attr — nothing to compare
|
||||
}
|
||||
if gotState != nil {
|
||||
t.Fatalf("expected nil state, got one")
|
||||
}
|
||||
} else {
|
||||
canonEq(t, "state", gotState, c.Expected.State)
|
||||
}
|
||||
})
|
||||
_ = i
|
||||
}
|
||||
}
|
||||
|
||||
// dispatchGap — replay one frozen call through the ported function; return
|
||||
// the JSON-comparable result and the post-call state pointer.
|
||||
func dispatchGap(t *testing.T, state *BuildState, fn string, args []interface{}, stamp string) (result interface{}, outState *BuildState) {
|
||||
t.Helper()
|
||||
name := strings.TrimPrefix(fn, "BuildManager.")
|
||||
arg := func(i int) interface{} {
|
||||
if i < len(args) {
|
||||
return args[i]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
switch name {
|
||||
case "assess_complexity":
|
||||
return AssessComplexity(), state
|
||||
case "complete_assess":
|
||||
CompleteAssess(state, arg(0).(map[string]interface{}), stamp)
|
||||
case "start_research":
|
||||
StartResearch(state, stamp)
|
||||
case "complete_research":
|
||||
CompleteResearch(state, arg(0).(map[string]interface{}), stamp)
|
||||
case "start_feasible":
|
||||
StartFeasible(state, stamp)
|
||||
case "complete_feasible":
|
||||
CompleteFeasible(state, arg(0) == true, arg(1).(string),
|
||||
argMap(arg(2)), stamp)
|
||||
case "start_consult":
|
||||
StartConsult(state, stamp)
|
||||
case "complete_consult":
|
||||
CompleteConsult(state, strList(arg(0)), stamp)
|
||||
case "start_consent":
|
||||
StartConsent(state, stamp)
|
||||
case "complete_consent":
|
||||
CompleteConsent(state, arg(0) == true, stamp)
|
||||
case "start_plan":
|
||||
StartPlan(state, stamp)
|
||||
case "complete_plan":
|
||||
CompletePlan(state, objList(arg(0)), stamp)
|
||||
case "start_build_phase":
|
||||
StartBuildPhase(state, stamp)
|
||||
case "complete_build_phase":
|
||||
CompleteBuildPhase(state, objList(arg(0)), stamp)
|
||||
case "start_verify":
|
||||
StartVerify(state, stamp)
|
||||
case "complete_verify":
|
||||
CompleteVerify(state, arg(0) == true, strList(arg(1)), stamp)
|
||||
case "start_correct":
|
||||
StartCorrect(state, stamp)
|
||||
case "complete_correct":
|
||||
CompleteCorrect(state, arg(0) == true, stamp)
|
||||
case "start_integrate":
|
||||
StartIntegrate(state, stamp)
|
||||
case "complete_integrate":
|
||||
CompleteIntegrate(state, arg(0) == true, strList(arg(1)), stamp)
|
||||
case "start_record":
|
||||
StartRecord(state, stamp)
|
||||
case "complete_record":
|
||||
CompleteRecord(state, stamp)
|
||||
case "_save_state":
|
||||
dir := t.TempDir()
|
||||
if err := SaveState(state, dir); err != nil {
|
||||
t.Fatalf("save: %v", err)
|
||||
}
|
||||
return nil, state
|
||||
case "get_resume_point":
|
||||
rp := ResumePoint(state)
|
||||
if rp == "" {
|
||||
return nil, state // Python None
|
||||
}
|
||||
return rp, state
|
||||
case "can_resume":
|
||||
return CanResume(state), state
|
||||
case "get_status":
|
||||
return BuildStatus(state), state
|
||||
default:
|
||||
t.Fatalf("unknown frozen fn %q", fn)
|
||||
}
|
||||
return nil, state
|
||||
}
|
||||
|
||||
func argMap(v interface{}) map[string]interface{} {
|
||||
m, _ := v.(map[string]interface{})
|
||||
return m
|
||||
}
|
||||
|
||||
// canonEq — canonical-JSON comparison (Go-built values vs the frozen
|
||||
// oracle snapshot).
|
||||
func canonEq(t *testing.T, what string, got interface{}, wantRaw json.RawMessage) {
|
||||
t.Helper()
|
||||
gb, err := json.Marshal(got)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal got %s: %v", what, err)
|
||||
}
|
||||
var gotV, wantV interface{}
|
||||
if err := json.Unmarshal(gb, &gotV); err != nil {
|
||||
t.Fatalf("reparse got %s: %v", what, err)
|
||||
}
|
||||
if err := json.Unmarshal(wantRaw, &wantV); err != nil {
|
||||
t.Fatalf("reparse want %s: %v", what, err)
|
||||
}
|
||||
gc, _ := json.Marshal(gotV)
|
||||
wc, _ := json.Marshal(wantV)
|
||||
if string(gc) != string(wc) {
|
||||
t.Fatalf("%s divergence:\n got %s\nwant %s", what, gc, wc)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLoadStateFixture — the IO seam: a build whose ledger.json holds the
|
||||
// oracle's exact save bytes must load into the identical state.
|
||||
func TestLoadStateFixture(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
buildDir := filepath.Join(dir, "20261013-022000")
|
||||
if err := os.MkdirAll(buildDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(buildDir, "ledger.json"), ledgerFixture, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state, err := LoadState(dir, "20261013-022000")
|
||||
if err != nil {
|
||||
t.Fatalf("load: %v", err)
|
||||
}
|
||||
var want BuildState
|
||||
if err := json.Unmarshal(ledgerFixture, &want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
canonEq(t, "loadstate", state, ledgerFixture)
|
||||
|
||||
if _, err := LoadState(dir, "no-such-build"); err == nil ||
|
||||
err.Error() != "Build no-such-build not found" {
|
||||
t.Fatalf("missing-build error: got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSaveStateBytes — the byte contract: Python json.dump(asdict(state),
|
||||
// indent=2) — the fixture IS those bytes; SaveState must reproduce them.
|
||||
func TestSaveStateBytes(t *testing.T) {
|
||||
var state BuildState
|
||||
if err := json.Unmarshal(ledgerFixture, &state); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
if err := SaveState(&state, dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(dir, "20261013-022000", "ledger.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(got) != string(ledgerFixture) {
|
||||
t.Fatalf("save bytes diverge from oracle fixture:\n got %d bytes\nwant %d bytes",
|
||||
len(got), len(ledgerFixture))
|
||||
}
|
||||
// nil state → no-op, nothing written (fresh dir: nothing created).
|
||||
nilDir := t.TempDir()
|
||||
if err := SaveState(nil, nilDir); err != nil {
|
||||
t.Fatalf("nil save: %v", err)
|
||||
}
|
||||
entries, _ := os.ReadDir(nilDir)
|
||||
if len(entries) != 0 {
|
||||
t.Fatalf("nil state must not create the build dir, found %d entries", len(entries))
|
||||
}
|
||||
}
|
||||
|
||||
// TestSaveStateRoundTrip — the phases splice must never corrupt content:
|
||||
// save the fixture state, load it back, require canonical equality.
|
||||
func TestSaveStateRoundTrip(t *testing.T) {
|
||||
var state BuildState
|
||||
if err := json.Unmarshal(ledgerFixture, &state); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
dir := t.TempDir()
|
||||
if err := SaveState(&state, dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
back, err := LoadState(dir, state.BuildID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
canonEq(t, "roundtrip", back, ledgerFixture)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.262.0"
|
||||
__version__ = "0.263.0"
|
||||
|
||||
# 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
|
||||
|
||||
@@ -233,7 +233,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.262.0"' in rune_version,
|
||||
'__version__ = "0.263.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -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.262.0
|
||||
Version: v0.263.0
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -2037,7 +2037,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.262.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.263.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
Reference in new issue
Block a user