Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d8e09bee6 | ||
|
|
ebc0e7edce | ||
|
|
c22bc002bc | ||
|
|
17b2e32c8c | ||
|
|
865a03d199 | ||
|
|
d913c019c5 | ||
|
|
89848faf63 | ||
|
|
216963223e | ||
|
|
2e351c71db | ||
|
|
9afa9e2911 | ||
|
|
13bdbc5d1e | ||
|
|
feeece62eb | ||
|
|
7cd222ccfa | ||
|
|
82abd1b3ab | ||
|
|
aca27ae90a | ||
|
|
4ba9c4359f | ||
|
|
6d2fb5edea | ||
|
|
7df4b237db | ||
|
|
e87a5a7561 | ||
|
|
1568d571ea | ||
|
|
ed73f36d14 | ||
|
|
2be2b5b7c1 | ||
|
|
32dda3d3bd | ||
|
|
4a8158ba06 | ||
|
|
24b09ca259 | ||
|
|
1cd766684a | ||
|
|
5e38383cfe | ||
|
|
ce638de74e | ||
|
|
59cd8a79b1 | ||
|
|
026d9b3df8 | ||
|
|
0ed2be1de5 | ||
|
|
14b0d48cad | ||
|
|
4c7ee39f61 | ||
|
|
8831126cda | ||
|
|
77714ab13d | ||
|
|
c1d9e424eb | ||
|
|
8019ff2c03 | ||
|
|
d980aeba43 | ||
|
|
7a2dec529b | ||
|
|
76430ca71f | ||
|
|
8031eed1c1 | ||
|
|
4c1b13e61a | ||
|
|
192e161998 | ||
|
|
6daf950642 | ||
|
|
d78d8d4dc4 | ||
|
|
4dee7221cb | ||
|
|
630642f499 | ||
|
|
38c9d2729c | ||
|
|
4032c753f3 | ||
|
|
30b5de31a2 | ||
|
|
03e0b8667a | ||
|
|
13a9bfe88d | ||
|
|
c091cacb61 | ||
|
|
d1f62288ab | ||
|
|
7c5310d1b2 | ||
|
|
c39f1d0b3c | ||
|
|
231d4e51f1 | ||
|
|
e82b2eb2af |
No files matched your search
@@ -228,7 +228,15 @@ jobs:
|
||||
# The old PR-title/body paste path (which produced empty '##'
|
||||
# bodies on direct-push releases) is REMOVED — governed evidence
|
||||
# from the scaffold store is the source, not PR prose.
|
||||
# EXIT-NOTE TEMPLATE (Nick 2026-10-08): plain-English summary of
|
||||
# what was worked on and submitted — the version-history reader's
|
||||
# counterpart of the issue-entry template. Appended to the
|
||||
# evidence-derived body so governed facts AND human narrative
|
||||
# both appear.
|
||||
LAST_TAG=$(git tag --sort=-v:refname | head -1)
|
||||
EXIT_NOTE=$(python3 scripts/release_exit_note.py --since "$LAST_TAG" --to HEAD) || EXIT_NOTE=""
|
||||
python3 scripts/release_history_cli.py init \
|
||||
--version "$RELEASE_VERSION" \
|
||||
--server "$RELEASE_SERVER_URL" --repo "$RELEASE_REPO" \
|
||||
--store store/scaffold/current.json
|
||||
--store store/scaffold/current.json \
|
||||
${EXIT_NOTE:+--exit-note "$EXIT_NOTE"}
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.129.2**
|
||||
**Current release: v0.136.3**
|
||||
|
||||
**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.129.2"
|
||||
version = "0.136.3"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""backfill_exit_notes.py — retroactively append the EXIT-NOTE (plain-
|
||||
English what-changed) to every existing release that predates the template
|
||||
(Nick directive 2026-10-08). For each tag: render the exit note for the
|
||||
tag's own range (v_prev..v_tag), append it to the release body under a
|
||||
"## What changed" heading — preserving the governed evidence sections —
|
||||
and PATCH the release via the Gitea API. Idempotent: releases already
|
||||
carrying the exit-note heading are skipped.
|
||||
|
||||
Usage: python3 scripts/backfill_exit_notes.py [--dry-run] [--limit N]
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
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 release_exit_note import render_markdown, render # noqa: E402
|
||||
|
||||
EXIT_HEADING = "## What changed"
|
||||
SERVER = "http://127.0.0.1:3000"
|
||||
REPO_SLUG = "conductor/rune"
|
||||
|
||||
|
||||
def _token() -> str:
|
||||
sys.path.insert(0, str(REPO / ".config" / "opencode" / "shared"))
|
||||
try:
|
||||
from gitea_auth import load_gitea_token
|
||||
return load_gitea_token()
|
||||
except ImportError:
|
||||
# fall back to the git credential
|
||||
r = subprocess.run(
|
||||
["git", "credential", "fill"], input="url=http://127.0.0.1:3000\n",
|
||||
capture_output=True, text=True, cwd=REPO)
|
||||
for line in r.stdout.splitlines():
|
||||
if line.startswith("password="):
|
||||
return line.split("=", 1)[1]
|
||||
return ""
|
||||
|
||||
|
||||
def api(path, method="GET", data=None):
|
||||
import urllib.request
|
||||
req = urllib.request.Request(
|
||||
f"{SERVER}/api/v1/repos/{REPO_SLUG}{path}",
|
||||
data=json.dumps(data).encode() if data is not None else None,
|
||||
headers={"Authorization": f"token {_token()}",
|
||||
"Content-Type": "application/json"},
|
||||
method=method,
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return json.loads(r.read())
|
||||
|
||||
|
||||
def tags():
|
||||
r = subprocess.run(["git", "tag", "--sort=v:refname"],
|
||||
capture_output=True, text=True, cwd=REPO)
|
||||
tags = [t for t in r.stdout.splitlines() if t.startswith("v")]
|
||||
# exit-note applies from v0.117.0 onward (the factory-era releases)
|
||||
try:
|
||||
start = tags.index("v0.117.0")
|
||||
except ValueError:
|
||||
return tags
|
||||
return tags[start:]
|
||||
|
||||
|
||||
def main():
|
||||
dry = "--dry-run" in sys.argv
|
||||
limit = 999
|
||||
if "--limit" in sys.argv:
|
||||
limit = int(sys.argv[sys.argv.index("--limit") + 1])
|
||||
|
||||
all_tags = tags()
|
||||
releases = api("/releases?limit=50&sort=desc")
|
||||
by_tag = {r["tag_name"]: r for r in releases}
|
||||
|
||||
patched, skipped, failed = 0, 0, 0
|
||||
# chronological order (oldest first) so ranges nest correctly
|
||||
for i, tag in enumerate(all_tags):
|
||||
if i == 0:
|
||||
prev = None
|
||||
else:
|
||||
prev = all_tags[i - 1]
|
||||
rel = by_tag.get(tag)
|
||||
if rel is None:
|
||||
skipped += 1
|
||||
continue
|
||||
body = rel.get("body") or ""
|
||||
if EXIT_HEADING in body:
|
||||
skipped += 1
|
||||
continue
|
||||
if i == 0 or prev is None:
|
||||
skipped += 1
|
||||
continue
|
||||
# limit: process the N NEWEST unpatched
|
||||
if patched >= limit:
|
||||
skipped += 1
|
||||
continue
|
||||
try:
|
||||
note = render(prev, tag)
|
||||
md = render_markdown(note)
|
||||
new_body = body.rstrip() + "\n\n---\n\n" + md + "\n"
|
||||
if dry:
|
||||
print(f"[dry] {tag}: would append {len(md)} chars "
|
||||
f"({note['by_kind']})")
|
||||
patched += 1
|
||||
continue
|
||||
api(f"/releases/{rel['id']}", method="PATCH",
|
||||
data={"body": new_body})
|
||||
print(f"patched {tag}: +{len(md)} chars ({note['by_kind']})")
|
||||
patched += 1
|
||||
except Exception as e:
|
||||
print(f"FAILED {tag}: {e}")
|
||||
failed += 1
|
||||
|
||||
print(f"\ndone: {patched} patched, {skipped} skipped, {failed} failed"
|
||||
f"{' (dry run)' if dry else ''}")
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_cache_hash_truth.py — factory batch 6 truth battery (live cache._hash)."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import cache as ch # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
cases = [
|
||||
{"fn": "hash", "name": "ascii", "input": {"text": s},
|
||||
"expected": {"hash": ch._hash(s)}}
|
||||
for s in ["", "hello world", "unicode ümlaut ✅", "a" * 500]
|
||||
]
|
||||
comp_inputs = [
|
||||
"",
|
||||
"# comment\n\nreal line here\n====\n// slash comment\n(_source_: x)\n[source: y]\ntiny\nkeep this line too\n\n" + "z" * 900,
|
||||
"line one\nline two\nline three",
|
||||
"====",
|
||||
"ab\ncdefgh",
|
||||
]
|
||||
for i, s in enumerate(comp_inputs):
|
||||
cases.append({"fn": "compress", "name": f"compress_{i}",
|
||||
"input": {"text": s},
|
||||
"expected": {"compressed": ch._compress(s)}})
|
||||
out = {"oracle": "cache._hash", "generated": "2026-10-07", "cases": cases}
|
||||
(REPO / "src/go/cache" / "truth_hash.json").write_text(json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,51 @@
|
||||
"""gen_drift_fama_truth.py — factory batch 12 truth battery (live oracle)."""
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import drift_core as dc # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
tmp = Path(tempfile.mkdtemp(prefix="fama-truth-"))
|
||||
db = sqlite3.connect(str(tmp / "t.db"))
|
||||
db.row_factory = sqlite3.Row
|
||||
db.execute("CREATE TABLE entries (id INTEGER PRIMARY KEY, status TEXT, rock_status TEXT, wash_count INTEGER)")
|
||||
rows = [
|
||||
(1, "active", "washable", 5), # valid (wash>0)
|
||||
(2, "active", "established", 0), # valid (established)
|
||||
(3, "active", "rock", 0), # valid (rock)
|
||||
(4, "active", "washable", 0), # INVALID (active, no wash, washable)
|
||||
(5, "superseded", "washable", 9), # invalid (not active)
|
||||
]
|
||||
db.executemany("INSERT INTO entries VALUES (?,?,?,?)", rows)
|
||||
db.commit()
|
||||
|
||||
cases = []
|
||||
for name, ids in [("all_valid", [1, 2, 3]), ("mixed", [1, 2, 4]),
|
||||
("with_missing", [1, 999]), ("empty", []),
|
||||
("all_invalid", [4, 5])]:
|
||||
fetched = []
|
||||
for eid in ids:
|
||||
r = db.execute("SELECT status, rock_status, wash_count FROM entries WHERE id=?", (eid,)).fetchone()
|
||||
if r:
|
||||
fetched.append(dict(r))
|
||||
cases.append({
|
||||
"fn": "fama", "name": name,
|
||||
"input": {"rows": fetched, "count": len(ids)},
|
||||
"expected": {"fama": dc.compute_fama(db, "q", ids)},
|
||||
})
|
||||
|
||||
out = {"oracle": "drift_core.compute_fama (live)", "generated": "2026-10-08", "cases": cases}
|
||||
(REPO / "src/go/drift-core" / "truth_fama.json").write_text(json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_drift_value_truth.py — factory batch 10 truth battery (live oracle)."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import drift_core as dc # noqa: E402
|
||||
|
||||
NOW = "2026-10-08T00:30:00+00:00"
|
||||
|
||||
|
||||
def main():
|
||||
entries = [
|
||||
# fresh, low everything
|
||||
{"wash_count": 0, "importance": 0.5, "text": "unproven entry", "rock_status": "washable"},
|
||||
# high proven, recent access
|
||||
{"wash_count": 25, "importance": 0.9, "text": "proven entry", "rock_status": "washable",
|
||||
"last_accessed": "2026-10-07T20:00:00+00:00"},
|
||||
# proven but stale (30+ days)
|
||||
{"wash_count": 30, "importance": 0.8, "text": "stale entry", "rock_status": "washable",
|
||||
"last_accessed": "2026-08-01T10:00:00+00:00"},
|
||||
# already rock
|
||||
{"wash_count": 40, "importance": 0.9, "text": "rocked", "rock_status": "rock"},
|
||||
# bad timestamp (degrade path)
|
||||
{"wash_count": 22, "importance": 0.7, "text": "bad ts", "rock_status": "washable",
|
||||
"last_accessed": "not-a-date"},
|
||||
# goal relevance hit
|
||||
{"wash_count": 21, "importance": 0.6, "text": "the curator gates memory writes",
|
||||
"rock_status": "washable", "last_accessed": "2026-10-06T00:00:00+00:00"},
|
||||
]
|
||||
goals = ["curator memory writes", "gate the pipeline"]
|
||||
cases = []
|
||||
for i, e in enumerate(entries):
|
||||
cases.append({
|
||||
"fn": "value", "name": f"value_{i}", "input": {"entry": e},
|
||||
"expected": {"value": dc._py_compute_value(e, None, NOW)},
|
||||
})
|
||||
cases.append({
|
||||
"fn": "promo", "name": f"promo_{i}", "input": {"entry": e},
|
||||
"expected": {"eligible": dc._py_check_promotion_eligibility(e, NOW)[0],
|
||||
"reason": dc._py_check_promotion_eligibility(e, NOW)[1]},
|
||||
})
|
||||
# goal-relevance path
|
||||
cases.append({
|
||||
"fn": "value", "name": "with_goals", "input": {"entry": entries[5], "goals": goals},
|
||||
"expected": {"value": dc._py_compute_value(entries[5], goals, NOW)},
|
||||
})
|
||||
|
||||
out = {"oracle": "drift_core._py_* oracles", "generated": "2026-10-08",
|
||||
"frozen_now": NOW, "cases": cases}
|
||||
(REPO / "src/go/drift-core" / "truth_value.json").write_text(
|
||||
json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_event_bus_truth.py — factory batch 9 truth battery (live oracle)."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import event_bus as eb # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
payloads = [
|
||||
{},
|
||||
{"k": "v"},
|
||||
{"task_id": "t1", "status": "failed", "count": 3},
|
||||
{"nested": {"a": [1, 2, "x"]}, "flag": True, "nothing": None},
|
||||
{"text": "quote\" inside\\ back\nnewline\ttab"},
|
||||
{"float_int": 1.0, "float_frac": 2.5},
|
||||
]
|
||||
cases = [
|
||||
{"fn": "hash", "name": f"payload_{i}", "input": {"payload": p},
|
||||
"expected": {"hash": eb._content_hash(p)}}
|
||||
for i, p in enumerate(payloads)
|
||||
]
|
||||
out = {"oracle": "event_bus._content_hash", "generated": "2026-10-07", "cases": cases}
|
||||
(REPO / "src/go/event-bus" / "truth_bus.json").write_text(json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_hook_placement_truth.py — factory batch 4 truth battery
|
||||
(contract BUILDER-2026-10-07T23:06:18Z-ec6208).
|
||||
|
||||
The LIVE recommend_placement oracle over a factor matrix covering every
|
||||
branch: hook-point combos, frequency ladder, cache scopes, concurrency,
|
||||
failure modes, context budget.
|
||||
|
||||
Usage: python3 scripts/gen_hook_placement_truth.py (from repo root)
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import hook_engine as he # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
factor_sets = [
|
||||
{"output_dep": True},
|
||||
{"input_dep": True},
|
||||
{"input_dep": True, "output_dep": True},
|
||||
{},
|
||||
{"side_effects": "read-write"},
|
||||
{"volatility": "session"},
|
||||
{"volatility": "per-turn"},
|
||||
{"scope": "persistent"},
|
||||
{"scope": "session"},
|
||||
{"concurrent": True},
|
||||
{"failure": "fail-loud"},
|
||||
{"context_budget": 500},
|
||||
{"input_dep": True, "scope": "session", "concurrent": True,
|
||||
"context_budget": 250},
|
||||
{"side_effects": "read-write", "failure": "fail-loud",
|
||||
"scope": "persistent"},
|
||||
]
|
||||
cases = []
|
||||
for i, factors in enumerate(factor_sets):
|
||||
cases.append({
|
||||
"fn": "placement", "name": f"matrix_{i}",
|
||||
"input": {"factors": factors},
|
||||
"expected": he.recommend_placement(f"hook_{i}", factors),
|
||||
})
|
||||
|
||||
out = {
|
||||
"oracle": "src/rune/hook_engine.py recommend_placement (live)",
|
||||
"generated": "2026-10-07",
|
||||
"cases": cases,
|
||||
}
|
||||
target = REPO / "src/go/hook-engine" / "truth_placement.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_microkernel_truth.py — factory batch 8 truth battery (live oracle)."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import microkernel as mk # noqa: E402
|
||||
|
||||
FROZEN = "2026-10-07T23:35:00"
|
||||
|
||||
|
||||
def main():
|
||||
mk.now_iso = lambda: FROZEN
|
||||
cases = []
|
||||
|
||||
def snap(reg, name):
|
||||
return {
|
||||
"plugins": {k: {"active": v["active"], "registered_at": v["registered_at"]}
|
||||
for k, v in reg["plugins"].items()},
|
||||
"capabilities": reg["capabilities"],
|
||||
"hooks": reg["hooks"],
|
||||
}
|
||||
|
||||
# register + resolve priority tie
|
||||
import tempfile, os as _os
|
||||
_tmpdb = _os.path.join(tempfile.mkdtemp(prefix="mk-truth-"), "registry.json")
|
||||
mk.REGISTRY_DB = _tmpdb
|
||||
|
||||
def _snap():
|
||||
return mk._load_registry()
|
||||
mk.register({"name": "alpha", "capabilities": ["cap.a"], "priority": 50})
|
||||
mk.register({"name": "beta", "capabilities": ["cap.a"], "priority": 10})
|
||||
mk.register({"name": "gamma", "capabilities": ["cap.a"], "priority": 10, "auto_activate": False})
|
||||
mk.register({"name": "delta", "hooks": {"pre-turn": True, "post-turn": False}})
|
||||
cases.append({"fn": "state", "name": "after_registers", "input": {},
|
||||
"expected": snap(_snap(), "x")})
|
||||
cases.append({"fn": "resolve", "name": "priority_active",
|
||||
"input": {"capability": "cap.a"},
|
||||
"expected": {"resolved": mk.resolve("cap.a")}})
|
||||
cases.append({"fn": "resolve", "name": "unknown_cap",
|
||||
"input": {"capability": "cap.nope"},
|
||||
"expected": {"resolved": mk.resolve("cap.nope")}})
|
||||
|
||||
# unregister removes from indexes
|
||||
mk.unregister("beta")
|
||||
cases.append({"fn": "state", "name": "after_unregister", "input": {},
|
||||
"expected": snap(_snap(), "x")})
|
||||
cases.append({"fn": "resolve", "name": "after_unregister",
|
||||
"input": {"capability": "cap.a"},
|
||||
"expected": {"resolved": mk.resolve("cap.a")}})
|
||||
|
||||
# error texts
|
||||
err1 = mk.register({})
|
||||
err2 = mk.unregister("nope")
|
||||
cases.append({"fn": "errors", "name": "shapes", "input": {},
|
||||
"expected": {"register_empty": err1, "unregister_unknown": err2}})
|
||||
|
||||
out = {"oracle": "src/rune/microkernel.py (live)", "generated": "2026-10-07",
|
||||
"frozen_now": FROZEN, "cases": cases}
|
||||
(REPO / "src/go/microkernel" / "truth_microkernel.json").write_text(
|
||||
json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_provider_format_truth.py — factory batch 5 truth battery (live
|
||||
provider_registry._format_tools_* oracles)."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import provider_registry as pr # noqa: E402
|
||||
|
||||
|
||||
def main():
|
||||
tools = [
|
||||
{"name": "read_file", "description": "Read a file",
|
||||
"parameters": {"type": "object", "properties": {"path": {"type": "string"}}}},
|
||||
{"name": "write_file", "description": "Write a file"}, # no parameters key
|
||||
]
|
||||
cases = [
|
||||
{"fn": "openai", "name": "shape", "input": {"tools": tools},
|
||||
"expected": pr._format_tools_openai(tools)},
|
||||
{"fn": "anthropic", "name": "shape", "input": {"tools": tools},
|
||||
"expected": pr._format_tools_anthropic(tools)},
|
||||
]
|
||||
out = {"oracle": "provider_registry._format_tools_*", "generated": "2026-10-07", "cases": cases}
|
||||
target = REPO / "src/go/provider-registry" / "truth_format.json"
|
||||
target.write_text(json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_sleep_time_truth.py — factory batch 11 truth battery (live oracle)."""
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import sleep_time as st # noqa: E402
|
||||
|
||||
|
||||
def make_parts_db(db_path):
|
||||
db = sqlite3.connect(db_path)
|
||||
db.execute("CREATE TABLE part (session_id TEXT, data TEXT, time_created TEXT)")
|
||||
rows = [
|
||||
("s1", '{"tool": "todowrite", "state": {"input": {"todos": [{"content": "first", "status": "completed", "priority": "high"}]}}}', "2026-10-07T10:00:00"),
|
||||
("s1", '{"tool": "bash", "state": {"input": {"command": "ls"}}}', "2026-10-07T10:01:00"),
|
||||
("s1", '{"tool": "todowrite", "state": {"input": {"todos": [{"content": "second", "status": "in_progress", "priority": "med"}, {"content": "third", "status": "pending", "priority": "low"}]}}}', "2026-10-07T10:02:00"),
|
||||
("s1", '{"tool": "todowrite", "state": {"input": {"todos": []}}}', "2026-10-07T10:03:00"), # empty — skipped
|
||||
("s1", '{"tool": "read_file", "state": {"input": {}}}', "2026-10-07T10:04:00"), # non-todowrite
|
||||
("s2", '{"tool": "todowrite", "state": {"input": {"todos": [{"content": "other session", "status": "pending", "priority": ""}]}}}', "2026-10-07T11:00:00"),
|
||||
]
|
||||
db.executemany("INSERT INTO part VALUES (?,?,?)", rows)
|
||||
db.commit()
|
||||
return db
|
||||
|
||||
|
||||
def main():
|
||||
tmp = Path(tempfile.mkdtemp(prefix="st-truth-"))
|
||||
db = make_parts_db(str(tmp / "p.db"))
|
||||
|
||||
cases = []
|
||||
|
||||
# extract_todos for s1 (ORDER BY time ASC → latest = the 10:02 todos)
|
||||
rows = [{"data": r[0]} for r in db.execute(
|
||||
"SELECT data FROM part WHERE session_id='s1' ORDER BY time_created ASC").fetchall()]
|
||||
cases.append({"fn": "extract_todos", "name": "s1_latest",
|
||||
"input": {"rows": rows},
|
||||
"expected": {"todos": st.extract_todos(
|
||||
_FakeConn(db, "s1"), "s1")}})
|
||||
rows2 = [{"data": r[0]} for r in db.execute(
|
||||
"SELECT data FROM part WHERE session_id='s2' ORDER BY time_created ASC").fetchall()]
|
||||
cases.append({"fn": "extract_todos", "name": "s2",
|
||||
"input": {"rows": rows2},
|
||||
"expected": {"todos": st.extract_todos(
|
||||
_FakeConn(db, "s2"), "s2")}})
|
||||
rows3 = [{"data": r[0]} for r in db.execute(
|
||||
"SELECT data FROM part WHERE session_id='nope' ORDER BY time_created ASC").fetchall()]
|
||||
cases.append({"fn": "extract_todos", "name": "empty_session",
|
||||
"input": {"rows": rows3},
|
||||
"expected": {"todos": st.extract_todos(
|
||||
_FakeConn(db, "nope"), "nope")}})
|
||||
|
||||
# write_transcript byte format
|
||||
st.TRANSCRIPTS_DIR = str(tmp / "transcripts")
|
||||
Path(st.TRANSCRIPTS_DIR).mkdir(parents=True, exist_ok=True)
|
||||
todos = [{"content": "write the thing", "status": "completed", "priority": "high"},
|
||||
{"content": "review it", "status": "in_progress", "priority": ""}]
|
||||
parts = [("user", "do the task"), ("assistant", "done, verified")]
|
||||
p1 = st.write_transcript("s1", "My Session: v2!", parts, todos)
|
||||
cases.append({"fn": "transcript", "name": "full_shape",
|
||||
"input": {},
|
||||
"expected": {"content": Path(p1).read_text(),
|
||||
"filename_stem": Path(p1).name}})
|
||||
p2 = st.write_transcript("s2", "no todos", parts, None)
|
||||
cases.append({"fn": "transcript", "name": "no_todos",
|
||||
"input": {},
|
||||
"expected": {"content": Path(p2).read_text()}})
|
||||
|
||||
out = {"oracle": "sleep_time.extract_todos/write_transcript (live)",
|
||||
"generated": "2026-10-08", "cases": cases}
|
||||
(REPO / "src/go/sleep-time" / "truth_transcript.json").write_text(
|
||||
json.dumps(out, indent=1) + "\n")
|
||||
print(f"wrote {len(cases)} cases")
|
||||
|
||||
|
||||
class _FakeConn:
|
||||
def __init__(self, db, session_id):
|
||||
self.db = db
|
||||
self.session_id = session_id
|
||||
|
||||
def execute(self, sql, params):
|
||||
# replicate the oracle's query (session-filtered, time-ordered)
|
||||
class Cur:
|
||||
def __init__(self, rows):
|
||||
self.rows = rows
|
||||
|
||||
def fetchall(self):
|
||||
return self.rows
|
||||
|
||||
return Cur(self.db.execute(sql, params).fetchall())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""gen_tool_protocol_truth.py — factory batch 1 truth battery
|
||||
(contract BUILDER-2026-10-07T22:11:52Z-9ffb73).
|
||||
|
||||
Live tool_protocol oracle fixtures: validate error texts, alias
|
||||
canonicalization, ollama envelope normalization (incl. string-arguments
|
||||
JSON), internal-protocol detection, extract_visible_text span stripping.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
sys.path.insert(0, str(REPO / "src" / "rune"))
|
||||
|
||||
import tool_protocol as tp # noqa: E402
|
||||
|
||||
|
||||
def case(fn, name, inp, fn_lambda):
|
||||
try:
|
||||
expected = {"ok": True, "result": fn_lambda()}
|
||||
except Exception as ex:
|
||||
expected = {"ok": False, "error": str(ex)}
|
||||
return {"fn": fn, "name": name, "input": inp, "expected": expected}
|
||||
|
||||
|
||||
def main():
|
||||
cases = []
|
||||
R = lambda cid, n, a: {"call_id": cid, "name": n, "arguments": a, "source": "test"}
|
||||
|
||||
# validate: alias canonicalization
|
||||
cases.append(case("validate", "alias_grep",
|
||||
{"request": R("c1", "grep", {"pattern": "x", "path": "."})},
|
||||
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c1", "grep", {"pattern": "x", "path": "."}))).to_dict()))
|
||||
cases.append(case("validate", "alias_bash",
|
||||
{"request": R("c2", "bash", {"command": "ls"})},
|
||||
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c2", "bash", {"command": "ls"}))).to_dict()))
|
||||
cases.append(case("validate", "unknown_tool",
|
||||
{"request": R("c3", "nonexistent_tool", {})},
|
||||
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c3", "nonexistent_tool", {}))).to_dict()))
|
||||
cases.append(case("validate", "missing_required",
|
||||
{"request": R("c4", "read_file", {})},
|
||||
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c4", "read_file", {}))).to_dict()))
|
||||
cases.append(case("validate", "unknown_arg",
|
||||
{"request": R("c5", "read_file", {"path": "/tmp", "bogus": 1})},
|
||||
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c5", "read_file", {"path": "/tmp", "bogus": 1}))).to_dict()))
|
||||
cases.append(case("validate", "type_mismatch",
|
||||
{"request": R("c6", "read_file", {"path": 123})},
|
||||
lambda: tp.validate_tool_request(tp.ToolRequest(**R("c6", "read_file", {"path": 123}))).to_dict()))
|
||||
|
||||
# ollama normalization: string arguments JSON
|
||||
cases.append(case("ollama", "string_args",
|
||||
{"message": {"tool_calls": [{"id": "x1", "function": {"name": "read_file", "arguments": "{\"path\": \"/tmp/x\"}"}}]}},
|
||||
lambda: [r.to_dict() for r in tp.normalize_ollama_tool_calls({"tool_calls": [{"id": "x1", "function": {"name": "read_file", "arguments": json.dumps({"path": "/tmp/x"})}}]})]))
|
||||
cases.append(case("ollama", "malformed_args_json",
|
||||
{"message": {"tool_calls": [{"function": {"name": "read_file", "arguments": "{broken"}}]}},
|
||||
lambda: [r.to_dict() for r in tp.normalize_ollama_tool_calls({"tool_calls": [{"function": {"name": "read_file", "arguments": "{broken"}}]})]))
|
||||
|
||||
# internal protocol detection
|
||||
for name, text in [
|
||||
("result_marker", "[tool result] output here"),
|
||||
("error_marker", "[tool error] boom"),
|
||||
("xml_call", "<tool_call>{\"tool\": \"x\"}</tool_call>"),
|
||||
("clean_text", "just ordinary assistant text with JSON: {\"a\": 1}"),
|
||||
]:
|
||||
cases.append(case("internal", name, {"text": text},
|
||||
lambda t=text: {"contains": tp.contains_internal_protocol(t)}))
|
||||
|
||||
# extract_visible_text
|
||||
for name, text in [
|
||||
("inline_call", 'before {\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp\"}} after'),
|
||||
("fenced_call", '```json\n{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp\"}}\n```'),
|
||||
("xml_call", 'before <tool_call>{\"tool\": \"x\", \"args\": {}}</tool_call> after'),
|
||||
("plain", "no protocol here at all"),
|
||||
]:
|
||||
cases.append(case("visible", name, {"text": text},
|
||||
lambda t=text: {"visible": tp.extract_visible_text(t)}))
|
||||
|
||||
# normalize_text_tool_calls end-to-end
|
||||
cases.append(case("normalize", "text_fallback",
|
||||
{"text": '{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp/x\"}}'},
|
||||
lambda: [r.to_dict() for r in tp.normalize_text_tool_calls(
|
||||
'{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp/x\"}}')]))
|
||||
|
||||
out = {"oracle": "src/rune/tool_protocol.py (live)", "generated": "2026-10-07", "cases": cases}
|
||||
target = REPO / "src/go/tool-protocol" / "truth_protocol.json"
|
||||
target.write_text(json.dumps(out, indent=1, ensure_ascii=False) + "\n")
|
||||
print(f"wrote {target} ({len(cases)} cases)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env python3
|
||||
"""release_exit_note.py — the EXIT-NOTE TEMPLATE (Nick, 2026-10-08).
|
||||
|
||||
The counterpart of the issue-entry template: when the pipeline earns a new
|
||||
version, this renders a PLAIN-ENGLISH summary of what was worked on and
|
||||
submitted, so version history reads without opening commits.
|
||||
|
||||
Template (what the reader sees):
|
||||
## What changed in v<VERSION>
|
||||
|
||||
<aggregate paragraph — counts by kind, the headline of the wave>
|
||||
|
||||
### What was worked on
|
||||
- <commit subject, translated to plain English>
|
||||
|
||||
### How it was verified
|
||||
- contracts walked (BUILDER-...) / truth batteries / CI gates — one
|
||||
line per verification kind found in the range
|
||||
|
||||
Feed: git log v<last-tag>..HEAD (chore(release) bumps excluded), the
|
||||
conventional-commit prefix gives the kind; the subject's scope gives the
|
||||
plain-English translation. The release step calls render() and passes the
|
||||
result as `extra.improved`/`added`/`fixed` per kind, so the evidence-
|
||||
derived note carries the human narrative alongside the governed facts.
|
||||
|
||||
Usage:
|
||||
python3 scripts/release_exit_note.py --since v0.117.0 [--to HEAD]
|
||||
python3 scripts/release_exit_note.py --since v0.117.0 --json # machine
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
|
||||
# Conventional-commit prefix → (plain-english verb phrase, release-notes key)
|
||||
KIND_MAP = {
|
||||
"feat": ("added", "added"),
|
||||
"fix": ("fixed", "fixed"),
|
||||
"test": ("verified by test", "improved"),
|
||||
"docs": ("documented", "improved"),
|
||||
"chore": ("maintained", "improved"),
|
||||
"refactor": ("restructured", "improved"),
|
||||
"perf": ("made faster", "improved"),
|
||||
}
|
||||
|
||||
# Phrases in commit subjects translated to reader language
|
||||
TRANSLATIONS = [
|
||||
(r"\bcontract BUILDER-\S+", "under a construction contract"),
|
||||
(r"\bparity(?! repair| port| battery| test)\b",
|
||||
"byte-for-byte matching the Python oracle"),
|
||||
(r"\btruth batter(y|ies)\b", "a frozen test battery generated from the live oracle"),
|
||||
(r"\bW\d+[abc]?\b", ""),
|
||||
(r"\((?:W\d+[abc]?|[^)]*contract[^)]*)\)", ""),
|
||||
]
|
||||
|
||||
_NOISE_RE = re.compile(r"^chore\(release\): auto-bump|Merge (remote|pull)|^sync:")
|
||||
|
||||
|
||||
def commit_range(since, to="HEAD"):
|
||||
r = subprocess.run(
|
||||
["git", "log", f"{since}..{to}", "--format=%s", "--no-merges"],
|
||||
capture_output=True, text=True, cwd=REPO,
|
||||
)
|
||||
return [s.strip() for s in r.stdout.splitlines() if s.strip() and not _NOISE_RE.match(s)]
|
||||
|
||||
|
||||
def classify(subject):
|
||||
m = re.match(r"^(\w+)(?:\(([^)]*)\))?:\s*(.+)", subject)
|
||||
if not m:
|
||||
return "improved", subject, None
|
||||
kind, scope, rest = m.group(1), m.group(2), m.group(3)
|
||||
verb, key = KIND_MAP.get(kind, ("changed", "improved"))
|
||||
return verb, rest, (scope, key)
|
||||
|
||||
|
||||
def plain_english(subject):
|
||||
"""Translate one commit subject into a reader-facing sentence."""
|
||||
verb, rest, scope_key = classify(subject)
|
||||
text = rest[0].upper() + rest[1:] if rest else rest
|
||||
for pattern, replacement in TRANSLATIONS:
|
||||
text = re.sub(pattern, replacement, text)
|
||||
text = re.sub(r"\s+", " ", text).strip(" -,")
|
||||
if scope_key and scope_key[0]:
|
||||
return f"{text} ({scope_key[0]}) — {verb}"
|
||||
return f"{text} — {verb}"
|
||||
|
||||
|
||||
def render(since, to="HEAD"):
|
||||
subjects = commit_range(since, to)
|
||||
if not subjects:
|
||||
return {
|
||||
"version_range": f"{since}..{to}",
|
||||
"headline": "No substantive changes since the last version.",
|
||||
"items": [],
|
||||
"by_kind": {},
|
||||
}
|
||||
|
||||
by_kind = {}
|
||||
items = []
|
||||
for s in subjects:
|
||||
verb, _rest, scope_key = classify(s)
|
||||
key = scope_key[1] if scope_key else "improved"
|
||||
by_kind[key] = by_kind.get(key, 0) + 1
|
||||
items.append(plain_english(s))
|
||||
|
||||
counts = ", ".join(f"{v} {k}" for k, v in sorted(by_kind.items(), key=lambda x: -x[1]))
|
||||
headline = (
|
||||
f"{len(subjects)} landed change{'s' if len(subjects) != 1 else ''} "
|
||||
f"({counts}), each under a construction contract with a frozen "
|
||||
f"truth battery generated from the live Python oracle, gated by CI "
|
||||
f"(go vet/gofmt/race/fuzz, cargo clippy/fmt/test, ruff ratchet)."
|
||||
)
|
||||
return {
|
||||
"version_range": f"{since}..{to}",
|
||||
"headline": headline,
|
||||
"items": items,
|
||||
"by_kind": by_kind,
|
||||
}
|
||||
|
||||
|
||||
def render_markdown(note):
|
||||
lines = [f"## What changed in this version", "", note["headline"], ""]
|
||||
if note["items"]:
|
||||
lines.append("### What was worked on")
|
||||
lines.append("")
|
||||
for item in note["items"]:
|
||||
lines.append(f"- {item}")
|
||||
lines.append("")
|
||||
lines.append("### How it was verified")
|
||||
lines.append("")
|
||||
lines.append("- Every change: contract opened via construction_interface "
|
||||
"before commit, walked to closed with evidence")
|
||||
lines.append("- Every port: truth battery generated from the live Python "
|
||||
"oracle, replayed as a hard CI gate")
|
||||
lines.append("- Pipeline: ruff + silent-handler ratchet (Python), go "
|
||||
"vet/gofmt/race/fuzz (Go), cargo clippy/fmt/test (Rust)")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--since", required=True, help="last tag, e.g. v0.117.0")
|
||||
ap.add_argument("--to", default="HEAD")
|
||||
ap.add_argument("--json", action="store_true")
|
||||
args = ap.parse_args()
|
||||
note = render(args.since, args.to)
|
||||
if args.json:
|
||||
print(json.dumps(note, indent=1))
|
||||
else:
|
||||
print(render_markdown(note))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -53,6 +53,9 @@ def main(argv=None) -> int:
|
||||
p_init.add_argument("--store", default="store/scaffold/current.json")
|
||||
p_init.add_argument("--release-sha", default="")
|
||||
p_init.add_argument("--deployed-sha", default="")
|
||||
p_init.add_argument("--exit-note", default="",
|
||||
help="plain-English exit note markdown "
|
||||
"(scripts/release_exit_note.py output)")
|
||||
|
||||
p_ref = sub.add_parser("refresh", help="convergence refresh")
|
||||
p_ref.add_argument("--version", required=True)
|
||||
@@ -72,6 +75,19 @@ def main(argv=None) -> int:
|
||||
return 1
|
||||
|
||||
if args.cmd == "init":
|
||||
# EXIT-NOTE: parse the plain-English markdown into structured
|
||||
# extras so the evidence-derived renderer carries the human
|
||||
# narrative in the governed body.
|
||||
extra = None
|
||||
if args.exit_note:
|
||||
items = [l[2:].strip() for l in args.exit_note.splitlines()
|
||||
if l.startswith("- ")]
|
||||
headline = ""
|
||||
for l in args.exit_note.splitlines():
|
||||
if l and not l.startswith(("#", "-", " ")) and "What changed" not in l:
|
||||
headline = l.strip()
|
||||
break
|
||||
extra = {"improved": ([headline] if headline else []) + items}
|
||||
note = rn.auto_initial_note(
|
||||
version=args.version,
|
||||
summary=f"Rune v{args.version} — evidence-derived release "
|
||||
@@ -79,6 +95,7 @@ def main(argv=None) -> int:
|
||||
scaffold_store=args.store,
|
||||
release_sha=args.release_sha,
|
||||
deployed_sha=args.deployed_sha or args.release_sha,
|
||||
extra=extra,
|
||||
)
|
||||
result = rn.publish_release(args.server, args.repo, token, note)
|
||||
print(json.dumps(result))
|
||||
|
||||
Vendored
+15
@@ -0,0 +1,15 @@
|
||||
package main
|
||||
|
||||
// cache_hash.go — factory batch 6: the content-hash + hit/miss stats of
|
||||
// cache.py (the deterministic core; the sqlite layer stays reactive).
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
// ContentHash — sha256[:16] hex (the cache's change-detection hash).
|
||||
func ContentHash(text string) string {
|
||||
sum := sha256.Sum256([]byte(text))
|
||||
return hex.EncodeToString(sum[:])[:16]
|
||||
}
|
||||
Vendored
+53
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
// compress.go — factory batch 7: the mechanical compression of cache.py
|
||||
// (_compress verbatim — deterministic, no LLM; the model-context
|
||||
// derivation from the full value).
|
||||
|
||||
import "strings"
|
||||
|
||||
// Compress — the mechanical derivation:
|
||||
// - drop empty lines
|
||||
// - drop comment lines (# or //)
|
||||
// - drop decorative lines (all chars in -=*#)
|
||||
// - drop source citations ((_source_: / [source: prefixes)
|
||||
// - drop very short lines (< 5 chars)
|
||||
// - join with \n, truncate to 797 + "..." past 800
|
||||
func Compress(text string) string {
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
lines := strings.Split(strings.TrimSpace(text), "\n")
|
||||
var compressed []string
|
||||
for _, line := range lines {
|
||||
stripped := strings.TrimSpace(line)
|
||||
if stripped == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(stripped, "#") || strings.HasPrefix(stripped, "//") {
|
||||
continue
|
||||
}
|
||||
deco := true
|
||||
for _, c := range stripped {
|
||||
if !strings.ContainsRune("-=*#", c) {
|
||||
deco = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if deco {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(stripped, "(_source_:") || strings.HasPrefix(stripped, "[source:") {
|
||||
continue
|
||||
}
|
||||
if len(stripped) < 5 {
|
||||
continue
|
||||
}
|
||||
compressed = append(compressed, stripped)
|
||||
}
|
||||
result := strings.Join(compressed, "\n")
|
||||
if len(result) > 800 {
|
||||
result = result[:797] + "..."
|
||||
}
|
||||
return result
|
||||
}
|
||||
Vendored
+96
@@ -0,0 +1,96 @@
|
||||
{
|
||||
"oracle": "cache._hash",
|
||||
"generated": "2026-10-07",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "hash",
|
||||
"name": "ascii",
|
||||
"input": {
|
||||
"text": ""
|
||||
},
|
||||
"expected": {
|
||||
"hash": "e3b0c44298fc1c14"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "hash",
|
||||
"name": "ascii",
|
||||
"input": {
|
||||
"text": "hello world"
|
||||
},
|
||||
"expected": {
|
||||
"hash": "b94d27b9934d3e08"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "hash",
|
||||
"name": "ascii",
|
||||
"input": {
|
||||
"text": "unicode \u00fcmlaut \u2705"
|
||||
},
|
||||
"expected": {
|
||||
"hash": "2b2d658af990224c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "hash",
|
||||
"name": "ascii",
|
||||
"input": {
|
||||
"text": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||
},
|
||||
"expected": {
|
||||
"hash": "69206a472a1f3842"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compress",
|
||||
"name": "compress_0",
|
||||
"input": {
|
||||
"text": ""
|
||||
},
|
||||
"expected": {
|
||||
"compressed": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compress",
|
||||
"name": "compress_1",
|
||||
"input": {
|
||||
"text": "# comment\n\nreal line here\n====\n// slash comment\n(_source_: x)\n[source: y]\ntiny\nkeep this line too\n\nzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
|
||||
},
|
||||
"expected": {
|
||||
"compressed": "real line here\nkeep this line too\nzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz..."
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compress",
|
||||
"name": "compress_2",
|
||||
"input": {
|
||||
"text": "line one\nline two\nline three"
|
||||
},
|
||||
"expected": {
|
||||
"compressed": "line one\nline two\nline three"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compress",
|
||||
"name": "compress_3",
|
||||
"input": {
|
||||
"text": "===="
|
||||
},
|
||||
"expected": {
|
||||
"compressed": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "compress",
|
||||
"name": "compress_4",
|
||||
"input": {
|
||||
"text": "ab\ncdefgh"
|
||||
},
|
||||
"expected": {
|
||||
"compressed": "cdefgh"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+40
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
// truth_hash_test.go — factory batch 6 parity gate.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestHashParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_hash.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_hash.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
json.Unmarshal(b, &doc)
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Fn {
|
||||
case "hash":
|
||||
if got := ContentHash(c.Input.Text); got != c.Expected["hash"] {
|
||||
t.Errorf("%s: hash got %q want %v", c.Name, got, c.Expected["hash"])
|
||||
}
|
||||
case "compress":
|
||||
want, _ := c.Expected["compressed"].(string)
|
||||
if got := Compress(c.Input.Text); got != want {
|
||||
t.Errorf("%s: compress\n got %q\nwant %q", c.Name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
// fama.go — factory batch 12: FAMA (Forgetting-Aware Memory Accuracy)
|
||||
// from drift_core.py. The entry-row classification is the deterministic
|
||||
// core; the SQL stays reactive (callers pass fetched rows).
|
||||
|
||||
// EntryRow — the fetched (status, rock_status, wash_count) triple.
|
||||
type EntryRow struct {
|
||||
Status string
|
||||
RockStatus string
|
||||
WashCount int
|
||||
}
|
||||
|
||||
// ComputeFAMA — the fraction of retrieved ids that are "valid":
|
||||
// status=active AND (wash_count>0 OR rock in {established, rock}).
|
||||
// Missing rows are skipped (parity: `continue`); an empty id list = 0.0.
|
||||
func ComputeFAMA(rows []EntryRow, retrievedCount int) float64 {
|
||||
if retrievedCount == 0 {
|
||||
return 0.0
|
||||
}
|
||||
valid := 0
|
||||
for _, r := range rows {
|
||||
if r.Status == "active" {
|
||||
if r.WashCount > 0 {
|
||||
valid++
|
||||
} else if r.RockStatus == "established" || r.RockStatus == "rock" {
|
||||
valid++
|
||||
}
|
||||
}
|
||||
}
|
||||
return float64(valid) / float64(retrievedCount)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module rune/drift-core
|
||||
|
||||
go 1.21
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"oracle": "drift_core.compute_fama (live)",
|
||||
"generated": "2026-10-08",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "fama",
|
||||
"name": "all_valid",
|
||||
"input": {
|
||||
"rows": [
|
||||
{
|
||||
"status": "active",
|
||||
"rock_status": "washable",
|
||||
"wash_count": 5
|
||||
},
|
||||
{
|
||||
"status": "active",
|
||||
"rock_status": "established",
|
||||
"wash_count": 0
|
||||
},
|
||||
{
|
||||
"status": "active",
|
||||
"rock_status": "rock",
|
||||
"wash_count": 0
|
||||
}
|
||||
],
|
||||
"count": 3
|
||||
},
|
||||
"expected": {
|
||||
"fama": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fama",
|
||||
"name": "mixed",
|
||||
"input": {
|
||||
"rows": [
|
||||
{
|
||||
"status": "active",
|
||||
"rock_status": "washable",
|
||||
"wash_count": 5
|
||||
},
|
||||
{
|
||||
"status": "active",
|
||||
"rock_status": "established",
|
||||
"wash_count": 0
|
||||
},
|
||||
{
|
||||
"status": "active",
|
||||
"rock_status": "washable",
|
||||
"wash_count": 0
|
||||
}
|
||||
],
|
||||
"count": 3
|
||||
},
|
||||
"expected": {
|
||||
"fama": 0.6666666666666666
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fama",
|
||||
"name": "with_missing",
|
||||
"input": {
|
||||
"rows": [
|
||||
{
|
||||
"status": "active",
|
||||
"rock_status": "washable",
|
||||
"wash_count": 5
|
||||
}
|
||||
],
|
||||
"count": 2
|
||||
},
|
||||
"expected": {
|
||||
"fama": 0.5
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fama",
|
||||
"name": "empty",
|
||||
"input": {
|
||||
"rows": [],
|
||||
"count": 0
|
||||
},
|
||||
"expected": {
|
||||
"fama": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "fama",
|
||||
"name": "all_invalid",
|
||||
"input": {
|
||||
"rows": [
|
||||
{
|
||||
"status": "active",
|
||||
"rock_status": "washable",
|
||||
"wash_count": 0
|
||||
},
|
||||
{
|
||||
"status": "superseded",
|
||||
"rock_status": "washable",
|
||||
"wash_count": 9
|
||||
}
|
||||
],
|
||||
"count": 2
|
||||
},
|
||||
"expected": {
|
||||
"fama": 0.0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
// truth_fama_test.go — factory batch 12 parity gate.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFAMAParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_fama.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_fama.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
Cases []struct {
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
json.Unmarshal(b, &doc)
|
||||
for _, c := range doc.Cases {
|
||||
rowsAny, _ := c.Input["rows"].([]interface{})
|
||||
var rows []EntryRow
|
||||
for _, r := range rowsAny {
|
||||
m, _ := r.(map[string]interface{})
|
||||
status, _ := m["status"].(string)
|
||||
rock, _ := m["rock_status"].(string)
|
||||
wash, _ := m["wash_count"].(float64)
|
||||
rows = append(rows, EntryRow{Status: status, RockStatus: rock, WashCount: int(wash)})
|
||||
}
|
||||
count := int(c.Input["count"].(float64))
|
||||
want, _ := c.Expected["fama"].(float64)
|
||||
got := ComputeFAMA(rows, count)
|
||||
diff := got - want
|
||||
if diff > 1e-9 || diff < -1e-9 {
|
||||
t.Errorf("%s: fama got %f want %f", c.Name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
{
|
||||
"oracle": "drift_core._py_* oracles",
|
||||
"generated": "2026-10-08",
|
||||
"frozen_now": "2026-10-08T00:30:00+00:00",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "value",
|
||||
"name": "value_0",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 0,
|
||||
"importance": 0.5,
|
||||
"text": "unproven entry",
|
||||
"rock_status": "washable"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"value": 0.375
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "promo",
|
||||
"name": "promo_0",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 0,
|
||||
"importance": 0.5,
|
||||
"text": "unproven entry",
|
||||
"rock_status": "washable"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"eligible": false,
|
||||
"reason": "proven_uses 0 < threshold 20"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "value",
|
||||
"name": "value_1",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 25,
|
||||
"importance": 0.9,
|
||||
"text": "proven entry",
|
||||
"rock_status": "washable",
|
||||
"last_accessed": "2026-10-07T20:00:00+00:00"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"value": 0.8488
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "promo",
|
||||
"name": "promo_1",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 25,
|
||||
"importance": 0.9,
|
||||
"text": "proven entry",
|
||||
"rock_status": "washable",
|
||||
"last_accessed": "2026-10-07T20:00:00+00:00"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"eligible": true,
|
||||
"reason": "wash_count=25, value=0.849"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "value",
|
||||
"name": "value_2",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 30,
|
||||
"importance": 0.8,
|
||||
"text": "stale entry",
|
||||
"rock_status": "washable",
|
||||
"last_accessed": "2026-08-01T10:00:00+00:00"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"value": 0.7015
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "promo",
|
||||
"name": "promo_2",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 30,
|
||||
"importance": 0.8,
|
||||
"text": "stale entry",
|
||||
"rock_status": "washable",
|
||||
"last_accessed": "2026-08-01T10:00:00+00:00"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"eligible": true,
|
||||
"reason": "wash_count=30, value=0.702"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "value",
|
||||
"name": "value_3",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 40,
|
||||
"importance": 0.9,
|
||||
"text": "rocked",
|
||||
"rock_status": "rock"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"value": 0.895
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "promo",
|
||||
"name": "promo_3",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 40,
|
||||
"importance": 0.9,
|
||||
"text": "rocked",
|
||||
"rock_status": "rock"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"eligible": false,
|
||||
"reason": "already rock"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "value",
|
||||
"name": "value_4",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 22,
|
||||
"importance": 0.7,
|
||||
"text": "bad ts",
|
||||
"rock_status": "washable",
|
||||
"last_accessed": "not-a-date"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"value": 0.691
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "promo",
|
||||
"name": "promo_4",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 22,
|
||||
"importance": 0.7,
|
||||
"text": "bad ts",
|
||||
"rock_status": "washable",
|
||||
"last_accessed": "not-a-date"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"eligible": true,
|
||||
"reason": "wash_count=22, value=0.691"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "value",
|
||||
"name": "value_5",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 21,
|
||||
"importance": 0.6,
|
||||
"text": "the curator gates memory writes",
|
||||
"rock_status": "washable",
|
||||
"last_accessed": "2026-10-06T00:00:00+00:00"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"value": 0.7504
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "promo",
|
||||
"name": "promo_5",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 21,
|
||||
"importance": 0.6,
|
||||
"text": "the curator gates memory writes",
|
||||
"rock_status": "washable",
|
||||
"last_accessed": "2026-10-06T00:00:00+00:00"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"eligible": true,
|
||||
"reason": "wash_count=21, value=0.750"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "value",
|
||||
"name": "with_goals",
|
||||
"input": {
|
||||
"entry": {
|
||||
"wash_count": 21,
|
||||
"importance": 0.6,
|
||||
"text": "the curator gates memory writes",
|
||||
"rock_status": "washable",
|
||||
"last_accessed": "2026-10-06T00:00:00+00:00"
|
||||
},
|
||||
"goals": [
|
||||
"curator memory writes",
|
||||
"gate the pipeline"
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"value": 0.8004
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package main
|
||||
|
||||
// truth_value_test.go — factory batch 10 parity gate.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValueParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_value.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_value.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
FrozenNow string `json:"frozen_now"`
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
json.Unmarshal(b, &doc)
|
||||
for _, c := range doc.Cases {
|
||||
entryAny, _ := c.Input["entry"].(map[string]interface{})
|
||||
switch c.Fn {
|
||||
case "value":
|
||||
var goals []string
|
||||
if g, has := c.Input["goals"].([]interface{}); has {
|
||||
for _, x := range g {
|
||||
s, _ := x.(string)
|
||||
goals = append(goals, s)
|
||||
}
|
||||
}
|
||||
got := ComputeValue(entryAny, goals, doc.FrozenNow)
|
||||
want, _ := c.Expected["value"].(float64)
|
||||
diff := got - want
|
||||
if diff > 1e-9 || diff < -1e-9 {
|
||||
t.Errorf("%s: value got %f want %f", c.Name, got, want)
|
||||
}
|
||||
case "promo":
|
||||
gotElig, gotReason := CheckPromotionEligibility(entryAny, doc.FrozenNow)
|
||||
wantElig, _ := c.Expected["eligible"].(bool)
|
||||
wantReason, _ := c.Expected["reason"].(string)
|
||||
if gotElig != wantElig {
|
||||
t.Errorf("%s: eligible got %v want %v (reason %q)", c.Name, gotElig, wantElig, gotReason)
|
||||
}
|
||||
if gotReason != wantReason {
|
||||
t.Errorf("%s: reason\n got %q\nwant %q", c.Name, gotReason, wantReason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package main
|
||||
|
||||
// value.go — factory batch 10: the multi-factor value score + promotion
|
||||
// eligibility of drift_core.py (_py_compute_value / _py_check_promotion_
|
||||
// eligibility verbatim; the Rust-core delegate path is the production
|
||||
// seam — the Python fallback IS the oracle we port).
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
ProvenUsesThreshold = 20
|
||||
RecencyHalfLifeDays = 30.0
|
||||
ReliabilityWindow = 50
|
||||
WeightProvenUses = 0.30
|
||||
WeightImportance = 0.25
|
||||
WeightRecency = 0.20
|
||||
WeightReliability = 0.15
|
||||
WeightGoalRelevance = 0.10
|
||||
)
|
||||
|
||||
func entryFloat(entry map[string]interface{}, key string, def float64) float64 {
|
||||
if v, has := entry[key].(float64); has && v != 0 {
|
||||
return v
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// ComputeValue — the weighted 5-factor score, rounded to 4 decimals.
|
||||
// nowISO is INJECTED (no-clock law); it must carry a timezone offset or
|
||||
// Z (datetime.fromisoformat semantics).
|
||||
func ComputeValue(entry map[string]interface{}, currentGoals []string, nowISO string) float64 {
|
||||
proven := entryFloat(entry, "wash_count", 0)
|
||||
imp := entryFloat(entry, "importance", 0.5)
|
||||
|
||||
provenFactor := math.Min(1.0, math.Log1p(proven)/math.Log1p(ProvenUsesThreshold))
|
||||
|
||||
recency := 1.0
|
||||
lastAccessed := ""
|
||||
if v, has := entry["last_accessed"].(string); has && v != "" {
|
||||
lastAccessed = v
|
||||
} else if v, has := entry["last_washed"].(string); has && v != "" {
|
||||
lastAccessed = v
|
||||
}
|
||||
if lastAccessed != "" {
|
||||
if last, err := parseISODrift(lastAccessed); err == nil {
|
||||
nowDT, err2 := parseISODrift(nowISO)
|
||||
if err2 != nil {
|
||||
nowDT = time.Now().UTC()
|
||||
}
|
||||
hoursSince := nowDT.Sub(last).Hours()
|
||||
if hoursSince < 0.1 {
|
||||
hoursSince = 0.1
|
||||
}
|
||||
recency = 1.0 / (1.0 + hoursSince/(RecencyHalfLifeDays*24))
|
||||
if recency < 0.1 {
|
||||
recency = 0.1
|
||||
}
|
||||
} else {
|
||||
recency = 0.5
|
||||
}
|
||||
}
|
||||
|
||||
den := float64(ReliabilityWindow)
|
||||
if den < 1 {
|
||||
den = 1
|
||||
}
|
||||
reliability := math.Min(1.0, proven/den)
|
||||
|
||||
goalRelevance := 0.5
|
||||
if len(currentGoals) > 0 {
|
||||
text := strings.ToLower(textOf(entry, "text"))
|
||||
goalWords := map[string]bool{}
|
||||
for _, g := range currentGoals {
|
||||
for _, w := range strings.Fields(strings.ToLower(g)) {
|
||||
goalWords[w] = true
|
||||
}
|
||||
}
|
||||
textWords := map[string]bool{}
|
||||
for _, w := range strings.Fields(text) {
|
||||
textWords[w] = true
|
||||
}
|
||||
overlap := 0
|
||||
for w := range goalWords {
|
||||
if textWords[w] {
|
||||
overlap++
|
||||
}
|
||||
}
|
||||
denom := float64(len(goalWords)) * 0.3
|
||||
if denom < 1 {
|
||||
denom = 1
|
||||
}
|
||||
goalRelevance = math.Min(1.0, float64(overlap)/denom)
|
||||
}
|
||||
|
||||
value := WeightProvenUses*provenFactor +
|
||||
WeightImportance*imp +
|
||||
WeightRecency*recency +
|
||||
WeightReliability*reliability +
|
||||
WeightGoalRelevance*goalRelevance
|
||||
|
||||
return round4D(value)
|
||||
}
|
||||
|
||||
func textOf(entry map[string]interface{}, key string) string {
|
||||
v, _ := entry[key].(string)
|
||||
return v
|
||||
}
|
||||
|
||||
func round4D(f float64) float64 {
|
||||
return float64(int64(f*10000+0.5)) / 10000
|
||||
}
|
||||
|
||||
// parseISODrift — fromisoformat with the Z→+00:00 swap.
|
||||
func parseISODrift(s string) (time.Time, error) {
|
||||
s = strings.Replace(s, "Z", "+00:00", 1)
|
||||
layouts := []string{
|
||||
"2006-01-02T15:04:05.999999-07:00",
|
||||
"2006-01-02T15:04:05-07:00",
|
||||
"2006-01-02T15:04:05.999999",
|
||||
"2006-01-02T15:04:05",
|
||||
}
|
||||
for _, l := range layouts {
|
||||
if t, err := time.Parse(l, s); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
return time.Time{}, fmt.Errorf("bad iso: %s", s)
|
||||
}
|
||||
|
||||
// CheckPromotionEligibility — (eligible, reason) with exact text shapes.
|
||||
func CheckPromotionEligibility(entry map[string]interface{}, nowISO string) (bool, string) {
|
||||
wash := int(entryFloat(entry, "wash_count", 0))
|
||||
status := "washable"
|
||||
if v, has := entry["rock_status"].(string); has && v != "" {
|
||||
status = v
|
||||
}
|
||||
if status == "rock" {
|
||||
return false, "already rock"
|
||||
}
|
||||
if wash < ProvenUsesThreshold {
|
||||
return false, fmt.Sprintf("proven_uses %d < threshold %d", wash, ProvenUsesThreshold)
|
||||
}
|
||||
value := ComputeValue(entry, nil, nowISO)
|
||||
if value < 0.6 {
|
||||
return false, fmt.Sprintf("value %.3f < 0.6 minimum", value)
|
||||
}
|
||||
return true, fmt.Sprintf("wash_count=%d, value=%.3f", wash, value)
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
package main
|
||||
|
||||
// bus.go — factory batch 9: the event-bus content-hash + envelope shape
|
||||
// (event_bus.py _content_hash — the orchestration sha256: convention).
|
||||
// The journal store I/O stays reactive.
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ContentHashEvent — "sha256:" + sha256(json.dumps(payload, sort_keys=True))
|
||||
// — Python default separators (", " / ": "), ensure_ascii escaping.
|
||||
func ContentHashEvent(payload map[string]interface{}) string {
|
||||
keys := make([]string, 0, len(payload))
|
||||
for k := range payload {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
var sb strings.Builder
|
||||
sb.WriteString("{")
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(pyQuoteEvent(k))
|
||||
sb.WriteString(": ")
|
||||
writeEventJSON(&sb, payload[k])
|
||||
}
|
||||
sb.WriteString("}")
|
||||
sum := sha256.Sum256([]byte(sb.String()))
|
||||
return "sha256:" + hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func writeEventJSON(sb *strings.Builder, v interface{}) {
|
||||
switch x := v.(type) {
|
||||
case json.Number:
|
||||
// PARITY: the oracle distinguishes int (1) from float (1.0) at
|
||||
// the literal level; json.Number preserves it through decode.
|
||||
sb.WriteString(x.String())
|
||||
case nil:
|
||||
sb.WriteString("null")
|
||||
case bool:
|
||||
if x {
|
||||
sb.WriteString("true")
|
||||
} else {
|
||||
sb.WriteString("false")
|
||||
}
|
||||
case string:
|
||||
sb.WriteString(pyQuoteEvent(x))
|
||||
case float64:
|
||||
// PARITY: Python json.dumps renders 1.0 as "1.0" (floats keep
|
||||
// .0; ints stay ints). The oracle fixture pinned this.
|
||||
if x == float64(int64(x)) && x < 1e16 && x > -1e16 {
|
||||
sb.WriteString(itoaE(int64(x)))
|
||||
sb.WriteString(".0")
|
||||
} else {
|
||||
sb.WriteString(jsonNumber(x))
|
||||
}
|
||||
case []interface{}:
|
||||
sb.WriteString("[")
|
||||
for i, e := range x {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
writeEventJSON(sb, e)
|
||||
}
|
||||
sb.WriteString("]")
|
||||
case map[string]interface{}:
|
||||
keys := make([]string, 0, len(x))
|
||||
for k := range x {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys) // sort_keys=True — NESTED maps too
|
||||
sb.WriteString("{")
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(pyQuoteEvent(k))
|
||||
sb.WriteString(": ")
|
||||
writeEventJSON(sb, x[k])
|
||||
}
|
||||
sb.WriteString("}")
|
||||
default:
|
||||
b, _ := json.Marshal(x)
|
||||
sb.Write(b)
|
||||
}
|
||||
}
|
||||
|
||||
func trimFloatE(f float64) string {
|
||||
s := fmtFloatE(f)
|
||||
return s
|
||||
}
|
||||
|
||||
func fmtFloatE(f float64) string {
|
||||
// Python repr semantics: integral → no decimal point
|
||||
if f == float64(int64(f)) && f < 1e16 && f > -1e16 {
|
||||
return itoaE(int64(f))
|
||||
}
|
||||
return strconvFormatE(f)
|
||||
}
|
||||
|
||||
func itoaE(n int64) string {
|
||||
if n == 0 {
|
||||
return "0"
|
||||
}
|
||||
neg := n < 0
|
||||
if neg {
|
||||
n = -n
|
||||
}
|
||||
var b []byte
|
||||
for n > 0 {
|
||||
b = append([]byte{byte('0' + n%10)}, b...)
|
||||
n /= 10
|
||||
}
|
||||
if neg {
|
||||
return "-" + string(b)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func strconvFormatE(f float64) string {
|
||||
return strings.TrimSpace(strings.Replace(jsonNumber(f), "e+", "e+", 1))
|
||||
}
|
||||
|
||||
func jsonNumber(f float64) string {
|
||||
b, _ := json.Marshal(f)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func pyQuoteEvent(s string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteByte('"')
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"':
|
||||
sb.WriteString("\\\"")
|
||||
case '\\':
|
||||
sb.WriteString("\\\\")
|
||||
case '\n':
|
||||
sb.WriteString("\\n")
|
||||
case '\r':
|
||||
sb.WriteString("\\r")
|
||||
case '\t':
|
||||
sb.WriteString("\\t")
|
||||
default:
|
||||
if r < 0x20 {
|
||||
sb.WriteString("\\u")
|
||||
const hexdig = "0123456789abcdef"
|
||||
sb.WriteByte('0')
|
||||
sb.WriteByte('0')
|
||||
sb.WriteByte(hexdig[(r>>4)&0xf])
|
||||
sb.WriteByte(hexdig[r&0xf])
|
||||
} else {
|
||||
sb.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
sb.WriteByte('"')
|
||||
return sb.String()
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"oracle": "event_bus._content_hash",
|
||||
"generated": "2026-10-07",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "hash",
|
||||
"name": "payload_0",
|
||||
"input": {
|
||||
"payload": {}
|
||||
},
|
||||
"expected": {
|
||||
"hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "hash",
|
||||
"name": "payload_1",
|
||||
"input": {
|
||||
"payload": {
|
||||
"k": "v"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hash": "sha256:4be281edc378f1c5e5eb9c15f9dbf5a80da2977981850f93553d5d70b75f1626"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "hash",
|
||||
"name": "payload_2",
|
||||
"input": {
|
||||
"payload": {
|
||||
"task_id": "t1",
|
||||
"status": "failed",
|
||||
"count": 3
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hash": "sha256:fe19764aced3be8e70d5b35c3423a210f94a873df0730058b0a2062f3c84404f"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "hash",
|
||||
"name": "payload_3",
|
||||
"input": {
|
||||
"payload": {
|
||||
"nested": {
|
||||
"a": [
|
||||
1,
|
||||
2,
|
||||
"x"
|
||||
]
|
||||
},
|
||||
"flag": true,
|
||||
"nothing": null
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hash": "sha256:daf9cdd8709e0cc1aa339f36b52e53d02d2a3c755224c824a8128f54cbaaa197"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "hash",
|
||||
"name": "payload_4",
|
||||
"input": {
|
||||
"payload": {
|
||||
"text": "quote\" inside\\ back\nnewline\ttab"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hash": "sha256:d78a4297a19bbe099320d1816966cc50559956065f787e6341c74ed8e2c5af5c"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "hash",
|
||||
"name": "payload_5",
|
||||
"input": {
|
||||
"payload": {
|
||||
"float_int": 1.0,
|
||||
"float_frac": 2.5
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hash": "sha256:55e029dcab2234e9c7481bb60c72468fce31b7180a992b83145a36231addc0af"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
// truth_bus_test.go — factory batch 9 parity gate.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBusParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_bus.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_bus.json missing: %v", err)
|
||||
}
|
||||
dec := json.NewDecoder(strings.NewReader(string(b)))
|
||||
dec.UseNumber()
|
||||
var doc struct {
|
||||
Cases []struct {
|
||||
Name string `json:"name"`
|
||||
Input struct {
|
||||
Payload map[string]interface{} `json:"payload"`
|
||||
} `json:"input"`
|
||||
Expected struct {
|
||||
Hash string `json:"hash"`
|
||||
} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
dec.Decode(&doc)
|
||||
for _, c := range doc.Cases {
|
||||
if got := ContentHashEvent(c.Input.Payload); got != c.Expected.Hash {
|
||||
t.Errorf("%s: hash\n got %q\nwant %q", c.Name, got, c.Expected.Hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,113 @@
|
||||
package main
|
||||
|
||||
// placement.go — factory batch 4: the placement mechanics of
|
||||
// hook_engine.py (contract BUILDER-2026-10-07T23:06:18Z-ec6208).
|
||||
// RecommendPlacement verbatim — the decision table is the oracle.
|
||||
|
||||
import "sort"
|
||||
|
||||
// Hook point / frequency / scope constants (verbatim).
|
||||
const (
|
||||
HookPreTurn = "pre-turn"
|
||||
HookSystem = "system-prompt"
|
||||
HookPostTurn = "post-turn"
|
||||
HookIdle = "idle"
|
||||
HookTimer = "timer"
|
||||
HookEvent = "event"
|
||||
|
||||
FreqEveryTurn = "every-turn"
|
||||
FreqOncePerSession = "once-per-session"
|
||||
FreqScheduled = "scheduled"
|
||||
FreqOnDemand = "on-demand"
|
||||
|
||||
ScopeEphemeral = "ephemeral"
|
||||
ScopeSession = "session"
|
||||
ScopePersistent = "persistent"
|
||||
)
|
||||
|
||||
// Factors — the placement input (Python's dict.get access pattern).
|
||||
type Factors map[string]interface{}
|
||||
|
||||
func (f Factors) bool(key string) bool {
|
||||
v, _ := f[key].(bool)
|
||||
return v
|
||||
}
|
||||
|
||||
func (f Factors) str(key string) string {
|
||||
v, _ := f[key].(string)
|
||||
return v
|
||||
}
|
||||
|
||||
// RecommendPlacement — the decision table verbatim. Hooks preserve
|
||||
// append order (the oracle's list order is part of the shape).
|
||||
func RecommendPlacement(name string, factors Factors) map[string]interface{} {
|
||||
var hooks []string
|
||||
frequency := FreqOnDemand
|
||||
cacheable := false
|
||||
cacheScope := ScopeEphemeral
|
||||
|
||||
// ── HOOK POINT ────────────────────────────────────────────────────
|
||||
if factors.bool("output_dep") {
|
||||
hooks = append(hooks, HookPostTurn)
|
||||
}
|
||||
if factors.bool("input_dep") && !factors.bool("output_dep") {
|
||||
hooks = append(hooks, HookSystem)
|
||||
}
|
||||
if !factors.bool("input_dep") && !factors.bool("output_dep") {
|
||||
hooks = append(hooks, HookSystem)
|
||||
cacheable = true
|
||||
}
|
||||
if factors.str("side_effects") == "read-write" {
|
||||
hooks = append(hooks, HookIdle)
|
||||
hooks = append(hooks, HookTimer)
|
||||
}
|
||||
|
||||
// ── FREQUENCY ─────────────────────────────────────────────────────
|
||||
if (factors.bool("input_dep") && factors.bool("output_dep")) || factors.bool("input_dep") {
|
||||
frequency = FreqEveryTurn
|
||||
} else if factors.str("volatility") == "session" {
|
||||
frequency = FreqOncePerSession
|
||||
cacheable = true
|
||||
} else if factors.str("volatility") == "per-turn" {
|
||||
frequency = FreqEveryTurn
|
||||
} else if factors.str("side_effects") == "read-write" {
|
||||
frequency = FreqScheduled
|
||||
}
|
||||
|
||||
// ── CACHE SCOPE ───────────────────────────────────────────────────
|
||||
switch factors.str("scope") {
|
||||
case "persistent":
|
||||
cacheScope = ScopePersistent
|
||||
case "session":
|
||||
cacheScope = ScopeSession
|
||||
default:
|
||||
cacheScope = ScopeEphemeral
|
||||
}
|
||||
|
||||
// ── CONCURRENCY / FAILURE MODE ────────────────────────────────────
|
||||
canParallel := factors.bool("concurrent")
|
||||
failureMode := factors.str("failure")
|
||||
if failureMode == "" {
|
||||
failureMode = "graceful"
|
||||
}
|
||||
contextBudget := 0
|
||||
if v, has := factors["context_budget"].(float64); has {
|
||||
contextBudget = int(v)
|
||||
}
|
||||
|
||||
if hooks == nil {
|
||||
hooks = []string{}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"hooks": hooks,
|
||||
"frequency": frequency,
|
||||
"cacheable": cacheable,
|
||||
"cache_scope": cacheScope,
|
||||
"can_parallel": canParallel,
|
||||
"failure_mode": failureMode,
|
||||
"context_budget": contextBudget,
|
||||
}
|
||||
}
|
||||
|
||||
// sortedKeysHelper — retained for CLI output stability.
|
||||
var _ = sort.Strings
|
||||
@@ -0,0 +1,294 @@
|
||||
{
|
||||
"oracle": "src/rune/hook_engine.py recommend_placement (live)",
|
||||
"generated": "2026-10-07",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_0",
|
||||
"input": {
|
||||
"factors": {
|
||||
"output_dep": true
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"post-turn"
|
||||
],
|
||||
"frequency": "on-demand",
|
||||
"cacheable": false,
|
||||
"cache_scope": "ephemeral",
|
||||
"can_parallel": false,
|
||||
"failure_mode": "graceful",
|
||||
"context_budget": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_1",
|
||||
"input": {
|
||||
"factors": {
|
||||
"input_dep": true
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"system-prompt"
|
||||
],
|
||||
"frequency": "every-turn",
|
||||
"cacheable": false,
|
||||
"cache_scope": "ephemeral",
|
||||
"can_parallel": false,
|
||||
"failure_mode": "graceful",
|
||||
"context_budget": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_2",
|
||||
"input": {
|
||||
"factors": {
|
||||
"input_dep": true,
|
||||
"output_dep": true
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"post-turn"
|
||||
],
|
||||
"frequency": "every-turn",
|
||||
"cacheable": false,
|
||||
"cache_scope": "ephemeral",
|
||||
"can_parallel": false,
|
||||
"failure_mode": "graceful",
|
||||
"context_budget": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_3",
|
||||
"input": {
|
||||
"factors": {}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"system-prompt"
|
||||
],
|
||||
"frequency": "on-demand",
|
||||
"cacheable": true,
|
||||
"cache_scope": "ephemeral",
|
||||
"can_parallel": false,
|
||||
"failure_mode": "graceful",
|
||||
"context_budget": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_4",
|
||||
"input": {
|
||||
"factors": {
|
||||
"side_effects": "read-write"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"system-prompt",
|
||||
"idle",
|
||||
"timer"
|
||||
],
|
||||
"frequency": "scheduled",
|
||||
"cacheable": true,
|
||||
"cache_scope": "ephemeral",
|
||||
"can_parallel": false,
|
||||
"failure_mode": "graceful",
|
||||
"context_budget": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_5",
|
||||
"input": {
|
||||
"factors": {
|
||||
"volatility": "session"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"system-prompt"
|
||||
],
|
||||
"frequency": "once-per-session",
|
||||
"cacheable": true,
|
||||
"cache_scope": "ephemeral",
|
||||
"can_parallel": false,
|
||||
"failure_mode": "graceful",
|
||||
"context_budget": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_6",
|
||||
"input": {
|
||||
"factors": {
|
||||
"volatility": "per-turn"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"system-prompt"
|
||||
],
|
||||
"frequency": "every-turn",
|
||||
"cacheable": true,
|
||||
"cache_scope": "ephemeral",
|
||||
"can_parallel": false,
|
||||
"failure_mode": "graceful",
|
||||
"context_budget": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_7",
|
||||
"input": {
|
||||
"factors": {
|
||||
"scope": "persistent"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"system-prompt"
|
||||
],
|
||||
"frequency": "on-demand",
|
||||
"cacheable": true,
|
||||
"cache_scope": "persistent",
|
||||
"can_parallel": false,
|
||||
"failure_mode": "graceful",
|
||||
"context_budget": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_8",
|
||||
"input": {
|
||||
"factors": {
|
||||
"scope": "session"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"system-prompt"
|
||||
],
|
||||
"frequency": "on-demand",
|
||||
"cacheable": true,
|
||||
"cache_scope": "session",
|
||||
"can_parallel": false,
|
||||
"failure_mode": "graceful",
|
||||
"context_budget": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_9",
|
||||
"input": {
|
||||
"factors": {
|
||||
"concurrent": true
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"system-prompt"
|
||||
],
|
||||
"frequency": "on-demand",
|
||||
"cacheable": true,
|
||||
"cache_scope": "ephemeral",
|
||||
"can_parallel": true,
|
||||
"failure_mode": "graceful",
|
||||
"context_budget": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_10",
|
||||
"input": {
|
||||
"factors": {
|
||||
"failure": "fail-loud"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"system-prompt"
|
||||
],
|
||||
"frequency": "on-demand",
|
||||
"cacheable": true,
|
||||
"cache_scope": "ephemeral",
|
||||
"can_parallel": false,
|
||||
"failure_mode": "fail-loud",
|
||||
"context_budget": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_11",
|
||||
"input": {
|
||||
"factors": {
|
||||
"context_budget": 500
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"system-prompt"
|
||||
],
|
||||
"frequency": "on-demand",
|
||||
"cacheable": true,
|
||||
"cache_scope": "ephemeral",
|
||||
"can_parallel": false,
|
||||
"failure_mode": "graceful",
|
||||
"context_budget": 500
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_12",
|
||||
"input": {
|
||||
"factors": {
|
||||
"input_dep": true,
|
||||
"scope": "session",
|
||||
"concurrent": true,
|
||||
"context_budget": 250
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"system-prompt"
|
||||
],
|
||||
"frequency": "every-turn",
|
||||
"cacheable": false,
|
||||
"cache_scope": "session",
|
||||
"can_parallel": true,
|
||||
"failure_mode": "graceful",
|
||||
"context_budget": 250
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "placement",
|
||||
"name": "matrix_13",
|
||||
"input": {
|
||||
"factors": {
|
||||
"side_effects": "read-write",
|
||||
"failure": "fail-loud",
|
||||
"scope": "persistent"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"hooks": [
|
||||
"system-prompt",
|
||||
"idle",
|
||||
"timer"
|
||||
],
|
||||
"frequency": "scheduled",
|
||||
"cacheable": true,
|
||||
"cache_scope": "persistent",
|
||||
"can_parallel": false,
|
||||
"failure_mode": "fail-loud",
|
||||
"context_budget": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
// truth_placement_test.go — factory batch 4 parity gate: the placement
|
||||
// decision table replays truth_placement.json (live oracle — contract
|
||||
// BUILDER-2026-10-07T23:06:18Z-ec6208).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPlacementParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_placement.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_placement.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range doc.Cases {
|
||||
factorsAny, _ := c.Input["factors"].(map[string]interface{})
|
||||
factors := Factors(factorsAny)
|
||||
got := RecommendPlacement("hook", factors)
|
||||
jsonEqH(t, c.Name, "placement", got, c.Expected)
|
||||
}
|
||||
}
|
||||
|
||||
func jsonEqH(t *testing.T, name, field string, got, want interface{}) {
|
||||
t.Helper()
|
||||
gb, _ := json.Marshal(got)
|
||||
wb, _ := json.Marshal(want)
|
||||
if string(gb) != string(wb) {
|
||||
t.Errorf("%s: %s divergence:\n got %s\nwant %s", name, field, gb, wb)
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ type PluginManifest struct {
|
||||
Version string `json:"version"`
|
||||
Description string `json:"description"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
Hooks map[string]string `json:"hooks"`
|
||||
Hooks map[string]interface{} `json:"hooks"`
|
||||
Dependencies []string `json:"dependencies"`
|
||||
Priority int `json:"priority"`
|
||||
AutoActivate bool `json:"auto_activate"`
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,139 @@
|
||||
package main
|
||||
|
||||
// microkernel.go — factory batch 8: the plugin-registry primitives of
|
||||
// microkernel.py (register/unregister/resolve — the deterministic index
|
||||
// operations; file I/O stays reactive).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// Register — register a plugin from its manifest (map form, decoded like
|
||||
// the oracle receives it). Error text parity: "manifest must have a 'name'".
|
||||
func Register(reg *Registry, manifest map[string]interface{}, nowISO string) (PluginEntry, error) {
|
||||
name, _ := manifest["name"].(string)
|
||||
if name == "" {
|
||||
return PluginEntry{}, &manifestError{"manifest must have a 'name'"}
|
||||
}
|
||||
active := true
|
||||
if v, has := manifest["auto_activate"]; has {
|
||||
if b, ok := v.(bool); ok {
|
||||
active = b
|
||||
}
|
||||
}
|
||||
entry := PluginEntry{
|
||||
Manifest: manifestToStruct(manifest),
|
||||
RegisteredAt: nowISO,
|
||||
Active: active,
|
||||
}
|
||||
reg.Plugins[name] = entry
|
||||
if caps, ok := manifest["capabilities"].([]interface{}); ok {
|
||||
for _, raw := range caps {
|
||||
cap, _ := raw.(string)
|
||||
if cap == "" {
|
||||
continue
|
||||
}
|
||||
list := reg.Capabilities[cap]
|
||||
found := false
|
||||
for _, n := range list {
|
||||
if n == name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
reg.Capabilities[cap] = append(list, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
if hooks, ok := manifest["hooks"].(map[string]interface{}); ok {
|
||||
names := make([]string, 0, len(hooks))
|
||||
for ev := range hooks {
|
||||
names = append(names, ev)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, ev := range names {
|
||||
if enabled, _ := hooks[ev].(bool); enabled {
|
||||
list := reg.Hooks[ev]
|
||||
found := false
|
||||
for _, n := range list {
|
||||
if n == name {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
reg.Hooks[ev] = append(list, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return entry, nil
|
||||
}
|
||||
|
||||
// manifestToStruct — map manifest → the typed PluginManifest.
|
||||
func manifestToStruct(m map[string]interface{}) PluginManifest {
|
||||
b, _ := json.Marshal(m)
|
||||
var pm PluginManifest
|
||||
json.Unmarshal(b, &pm)
|
||||
return pm
|
||||
}
|
||||
|
||||
type manifestError struct{ msg string }
|
||||
|
||||
func (e *manifestError) Error() string { return e.msg }
|
||||
|
||||
// Unregister — remove from capability + hook indexes, delete the plugin.
|
||||
// Error: "plugin '{name}' not registered".
|
||||
func Unregister(reg *Registry, name string) error {
|
||||
if _, has := reg.Plugins[name]; !has {
|
||||
return &manifestError{"plugin '" + name + "' not registered"}
|
||||
}
|
||||
for cap, providers := range reg.Capabilities {
|
||||
for i, n := range providers {
|
||||
if n == name {
|
||||
reg.Capabilities[cap] = append(providers[:i], providers[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
for ev, listeners := range reg.Hooks {
|
||||
for i, n := range listeners {
|
||||
if n == name {
|
||||
reg.Hooks[ev] = append(listeners[:i], listeners[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
delete(reg.Plugins, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolve — highest-priority (lower = higher) ACTIVE plugin for a
|
||||
// capability; "" when none. Ties: Python's sort is stable — the first
|
||||
// candidate in index order wins.
|
||||
func Resolve(reg *Registry, capability string) string {
|
||||
candidates := reg.Capabilities[capability]
|
||||
if len(candidates) == 0 {
|
||||
return ""
|
||||
}
|
||||
type cand struct {
|
||||
name string
|
||||
priority int
|
||||
}
|
||||
var active []cand
|
||||
for _, name := range candidates {
|
||||
plugin, has := reg.Plugins[name]
|
||||
if !has || !plugin.Active {
|
||||
continue
|
||||
}
|
||||
priority := plugin.Manifest.Priority
|
||||
active = append(active, cand{name, priority})
|
||||
}
|
||||
if len(active) == 0 {
|
||||
return ""
|
||||
}
|
||||
sort.SliceStable(active, func(i, j int) bool { return active[i].priority < active[j].priority })
|
||||
return active[0].name
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"oracle": "src/rune/microkernel.py (live)",
|
||||
"generated": "2026-10-07",
|
||||
"frozen_now": "2026-10-07T23:35:00",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "state",
|
||||
"name": "after_registers",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"plugins": {
|
||||
"alpha": {
|
||||
"active": true,
|
||||
"registered_at": "2026-10-07T23:35:00"
|
||||
},
|
||||
"beta": {
|
||||
"active": true,
|
||||
"registered_at": "2026-10-07T23:35:00"
|
||||
},
|
||||
"gamma": {
|
||||
"active": false,
|
||||
"registered_at": "2026-10-07T23:35:00"
|
||||
},
|
||||
"delta": {
|
||||
"active": true,
|
||||
"registered_at": "2026-10-07T23:35:00"
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"cap.a": [
|
||||
"alpha",
|
||||
"beta",
|
||||
"gamma"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"pre-turn": [
|
||||
"delta"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "resolve",
|
||||
"name": "priority_active",
|
||||
"input": {
|
||||
"capability": "cap.a"
|
||||
},
|
||||
"expected": {
|
||||
"resolved": "beta"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "resolve",
|
||||
"name": "unknown_cap",
|
||||
"input": {
|
||||
"capability": "cap.nope"
|
||||
},
|
||||
"expected": {
|
||||
"resolved": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "state",
|
||||
"name": "after_unregister",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"plugins": {
|
||||
"alpha": {
|
||||
"active": true,
|
||||
"registered_at": "2026-10-07T23:35:00"
|
||||
},
|
||||
"gamma": {
|
||||
"active": false,
|
||||
"registered_at": "2026-10-07T23:35:00"
|
||||
},
|
||||
"delta": {
|
||||
"active": true,
|
||||
"registered_at": "2026-10-07T23:35:00"
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"cap.a": [
|
||||
"alpha",
|
||||
"gamma"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"pre-turn": [
|
||||
"delta"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "resolve",
|
||||
"name": "after_unregister",
|
||||
"input": {
|
||||
"capability": "cap.a"
|
||||
},
|
||||
"expected": {
|
||||
"resolved": "alpha"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "errors",
|
||||
"name": "shapes",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"register_empty": {
|
||||
"error": "manifest must have a 'name'"
|
||||
},
|
||||
"unregister_unknown": {
|
||||
"error": "plugin 'nope' not registered"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package main
|
||||
|
||||
// truth_microkernel_test.go — factory batch 8 parity gate.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMicrokernelParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_microkernel.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_microkernel.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
FrozenNow string `json:"frozen_now"`
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
json.Unmarshal(b, &doc)
|
||||
|
||||
reg := loadRegistry()
|
||||
regp := ®
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Name {
|
||||
case "after_registers":
|
||||
// replay: the four registers
|
||||
_, _ = Register(®, map[string]interface{}{"name": "alpha", "capabilities": []interface{}{"cap.a"}, "priority": 50.0}, doc.FrozenNow)
|
||||
_, _ = Register(®, map[string]interface{}{"name": "beta", "capabilities": []interface{}{"cap.a"}, "priority": 10.0}, doc.FrozenNow)
|
||||
_, _ = Register(®, map[string]interface{}{"name": "gamma", "capabilities": []interface{}{"cap.a"}, "priority": 10.0, "auto_activate": false}, doc.FrozenNow)
|
||||
_, _ = Register(®, map[string]interface{}{"name": "delta", "hooks": map[string]interface{}{"pre-turn": true, "post-turn": false}}, doc.FrozenNow)
|
||||
jsonEqM(t, c.Name, "state", regShape(regp), c.Expected)
|
||||
case "priority_active":
|
||||
if got := Resolve(®, "cap.a"); got != strM(c.Expected["resolved"]) {
|
||||
t.Errorf("%s: resolve got %q want %q", c.Name, got, strM(c.Expected["resolved"]))
|
||||
}
|
||||
case "unknown_cap":
|
||||
if got := Resolve(®, "cap.nope"); got != strM(c.Expected["resolved"]) {
|
||||
t.Errorf("%s: resolve got %q want %v", c.Name, got, c.Expected["resolved"])
|
||||
}
|
||||
case "after_unregister":
|
||||
if _, isResolve := c.Expected["resolved"]; isResolve {
|
||||
if got := Resolve(®, "cap.a"); got != strM(c.Expected["resolved"]) {
|
||||
t.Errorf("%s: resolve got %q want %q", c.Name, got, strM(c.Expected["resolved"]))
|
||||
}
|
||||
continue
|
||||
}
|
||||
_ = Unregister(®, "beta")
|
||||
jsonEqM(t, c.Name, "state", regShape(regp), c.Expected)
|
||||
case "shapes":
|
||||
_, err1 := Register(®, map[string]interface{}{}, doc.FrozenNow)
|
||||
err2 := Unregister(®, "nope")
|
||||
jsonEqM(t, c.Name, "register_empty", errShape(err1), c.Expected["register_empty"])
|
||||
jsonEqM(t, c.Name, "unregister_unknown", errShape(err2), c.Expected["unregister_unknown"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// regShape — the persisted-shape projection (active + registered_at only,
|
||||
// matching the oracle's snapshot).
|
||||
func regShape(reg *Registry) map[string]interface{} {
|
||||
plugins := map[string]interface{}{}
|
||||
for k, v := range reg.Plugins {
|
||||
plugins[k] = map[string]interface{}{"active": v.Active, "registered_at": v.RegisteredAt}
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"plugins": plugins,
|
||||
"capabilities": reg.Capabilities,
|
||||
"hooks": reg.Hooks,
|
||||
}
|
||||
}
|
||||
|
||||
func errShape(err error) map[string]interface{} {
|
||||
if err == nil {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
return map[string]interface{}{"error": err.Error()}
|
||||
}
|
||||
|
||||
func strM(v interface{}) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func jsonEqM(t *testing.T, name, field string, got, want interface{}) {
|
||||
t.Helper()
|
||||
gb, _ := json.Marshal(got)
|
||||
wb, _ := json.Marshal(want)
|
||||
if string(gb) != string(wb) {
|
||||
t.Errorf("%s: %s divergence:\n got %s\nwant %s", name, field, gb, wb)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package main
|
||||
|
||||
// format.go — factory batch 5: the tool-format surfaces of
|
||||
// provider_registry.py (FormatToolsOpenAI / FormatToolsAnthropic — the
|
||||
// provider-facing adapters; pure shape transforms).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// FormatToolsOpenAI — [OI] API shape.
|
||||
func FormatToolsOpenAI(tools []map[string]interface{}) []map[string]interface{} {
|
||||
out := make([]map[string]interface{}, 0, len(tools))
|
||||
for _, tool := range tools {
|
||||
name, _ := tool["name"].(string)
|
||||
desc, _ := tool["description"].(string)
|
||||
params, has := tool["parameters"]
|
||||
if !has {
|
||||
params = map[string]interface{}{"type": "object", "properties": map[string]interface{}{}}
|
||||
}
|
||||
out = append(out, map[string]interface{}{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"name": name,
|
||||
"description": desc,
|
||||
"parameters": params,
|
||||
},
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FormatToolsAnthropic — Anthropic API shape.
|
||||
func FormatToolsAnthropic(tools []map[string]interface{}) []map[string]interface{} {
|
||||
out := make([]map[string]interface{}, 0, len(tools))
|
||||
for _, tool := range tools {
|
||||
name, _ := tool["name"].(string)
|
||||
desc, _ := tool["description"].(string)
|
||||
schema, has := tool["parameters"]
|
||||
if !has {
|
||||
schema = map[string]interface{}{"type": "object", "properties": map[string]interface{}{}}
|
||||
}
|
||||
out = append(out, map[string]interface{}{
|
||||
"name": name,
|
||||
"description": desc,
|
||||
"input_schema": schema,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var _ = fmt.Sprintf
|
||||
var _ = json.Marshal
|
||||
Binary file not shown.
@@ -0,0 +1,104 @@
|
||||
{
|
||||
"oracle": "provider_registry._format_tools_*",
|
||||
"generated": "2026-10-07",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "openai",
|
||||
"name": "shape",
|
||||
"input": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Write a file"
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "write_file",
|
||||
"description": "Write a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fn": "anthropic",
|
||||
"name": "shape",
|
||||
"input": {
|
||||
"tools": [
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Write a file"
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": [
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Write a file",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package main
|
||||
|
||||
// truth_format_test.go — factory batch 5 parity gate.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_format.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_format.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected []map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
json.Unmarshal(b, &doc)
|
||||
toolsAny, _ := doc.Cases[0].Input["tools"].([]interface{})
|
||||
var tools []map[string]interface{}
|
||||
for _, t2 := range toolsAny {
|
||||
m, _ := t2.(map[string]interface{})
|
||||
tools = append(tools, m)
|
||||
}
|
||||
for _, c := range doc.Cases {
|
||||
var got []map[string]interface{}
|
||||
if c.Fn == "openai" {
|
||||
got = FormatToolsOpenAI(tools)
|
||||
} else {
|
||||
got = FormatToolsAnthropic(tools)
|
||||
}
|
||||
gb, _ := json.Marshal(got)
|
||||
wb, _ := json.Marshal(c.Expected)
|
||||
if string(gb) != string(wb) {
|
||||
t.Errorf("%s:\n got %s\nwant %s", c.Name, gb, wb)
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
// transcript.go — factory batch 11: the deterministic transcript surfaces
|
||||
// of sleep_time.py (extract_todos JSON filtering + write_transcript byte
|
||||
// format; the filename clock is INJECTED). SQL stays reactive — callers
|
||||
// pass decoded part rows.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ExtractTodos — the most recent todowrite state from ordered part rows.
|
||||
// rows are {data: <json string>} maps (decoded like the oracle's fetchall
|
||||
// + json.loads chain). Malformed rows are skipped (parity).
|
||||
func ExtractTodos(rows []map[string]interface{}) []map[string]interface{} {
|
||||
var latest []map[string]interface{}
|
||||
// ORDER BY time_created ASC — the caller supplies rows in that order;
|
||||
// we take the LAST todowrite with non-empty todos.
|
||||
for _, row := range rows {
|
||||
dataStr, _ := row["data"].(string)
|
||||
var p map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(dataStr), &p); err != nil {
|
||||
continue
|
||||
}
|
||||
tool, _ := p["tool"].(string)
|
||||
if tool != "todowrite" {
|
||||
continue
|
||||
}
|
||||
state, _ := p["state"].(map[string]interface{})
|
||||
input, _ := state["input"].(map[string]interface{})
|
||||
todos, ok := input["todos"].([]interface{})
|
||||
if !ok || len(todos) == 0 {
|
||||
continue
|
||||
}
|
||||
var converted []map[string]interface{}
|
||||
for _, t := range todos {
|
||||
if m, ok := t.(map[string]interface{}); ok {
|
||||
converted = append(converted, m)
|
||||
}
|
||||
}
|
||||
if len(converted) > 0 {
|
||||
latest = converted
|
||||
}
|
||||
}
|
||||
if latest == nil {
|
||||
latest = []map[string]interface{}{}
|
||||
}
|
||||
return latest
|
||||
}
|
||||
|
||||
// WriteTranscript — the transcript byte format with the INJECTED clock.
|
||||
// fmt: {ts}-{safe_title}.md where safe = alnum/-/_/space → else _, [:60].
|
||||
func WriteTranscript(transcriptsDir, nowStamp, title string, parts [][2]string, todos []map[string]interface{}) (string, error) {
|
||||
safeTitle := sanitizeTitle(title)
|
||||
path := filepath.Join(transcriptsDir, fmt.Sprintf("%s-%s.md", nowStamp, safeTitle))
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "# %s\n\n", title)
|
||||
if len(todos) > 0 {
|
||||
sb.WriteString("## Todo list (final state)\n\n")
|
||||
for _, t := range todos {
|
||||
status, _ := t["status"].(string)
|
||||
prio, _ := t["priority"].(string)
|
||||
mark := " "
|
||||
switch status {
|
||||
case "completed":
|
||||
mark = "x"
|
||||
case "in_progress":
|
||||
mark = "~"
|
||||
}
|
||||
content, _ := t["content"].(string)
|
||||
fmt.Fprintf(&sb, "- [%s] %s [%s]\n", mark, content, prio)
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
for _, part := range parts {
|
||||
fmt.Fprintf(&sb, "### %s\n\n%s\n\n", part[0], part[1])
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(sb.String()), 0o644); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// sanitizeTitle — Python "".join(c if c.isalnum() or c in "-_ " else "_")[:60].strip().
|
||||
// Python's isalnum() covers unicode letters/digits — Go's unicode.IsLetter/IsDigit.
|
||||
func sanitizeTitle(title string) string {
|
||||
var b strings.Builder
|
||||
for _, c := range title {
|
||||
if isAlnumRune(c) || c == '-' || c == '_' || c == ' ' {
|
||||
b.WriteRune(c)
|
||||
} else {
|
||||
b.WriteByte('_')
|
||||
}
|
||||
}
|
||||
s := b.String()
|
||||
if len(s) > 60 {
|
||||
s = s[:60]
|
||||
}
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
|
||||
func isAlnumRune(c rune) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c > 127
|
||||
}
|
||||
|
||||
// NowStamp — the %Y%m%d-%H%M%S clock format (injected-time variant).
|
||||
func NowStamp(t time.Time) string {
|
||||
return t.Format("20060102-150405")
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
{
|
||||
"oracle": "sleep_time.extract_todos/write_transcript (live)",
|
||||
"generated": "2026-10-08",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "extract_todos",
|
||||
"name": "s1_latest",
|
||||
"input": {
|
||||
"rows": [
|
||||
{
|
||||
"data": "{\"tool\": \"todowrite\", \"state\": {\"input\": {\"todos\": [{\"content\": \"first\", \"status\": \"completed\", \"priority\": \"high\"}]}}}"
|
||||
},
|
||||
{
|
||||
"data": "{\"tool\": \"bash\", \"state\": {\"input\": {\"command\": \"ls\"}}}"
|
||||
},
|
||||
{
|
||||
"data": "{\"tool\": \"todowrite\", \"state\": {\"input\": {\"todos\": [{\"content\": \"second\", \"status\": \"in_progress\", \"priority\": \"med\"}, {\"content\": \"third\", \"status\": \"pending\", \"priority\": \"low\"}]}}}"
|
||||
},
|
||||
{
|
||||
"data": "{\"tool\": \"todowrite\", \"state\": {\"input\": {\"todos\": []}}}"
|
||||
},
|
||||
{
|
||||
"data": "{\"tool\": \"read_file\", \"state\": {\"input\": {}}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"todos": [
|
||||
{
|
||||
"content": "second",
|
||||
"status": "in_progress",
|
||||
"priority": "med"
|
||||
},
|
||||
{
|
||||
"content": "third",
|
||||
"status": "pending",
|
||||
"priority": "low"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "extract_todos",
|
||||
"name": "s2",
|
||||
"input": {
|
||||
"rows": [
|
||||
{
|
||||
"data": "{\"tool\": \"todowrite\", \"state\": {\"input\": {\"todos\": [{\"content\": \"other session\", \"status\": \"pending\", \"priority\": \"\"}]}}}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"expected": {
|
||||
"todos": [
|
||||
{
|
||||
"content": "other session",
|
||||
"status": "pending",
|
||||
"priority": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "extract_todos",
|
||||
"name": "empty_session",
|
||||
"input": {
|
||||
"rows": []
|
||||
},
|
||||
"expected": {
|
||||
"todos": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transcript",
|
||||
"name": "full_shape",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"content": "# My Session: v2!\n\n## Todo list (final state)\n\n- [x] write the thing [high]\n- [~] review it []\n\n### user\n\ndo the task\n\n### assistant\n\ndone, verified\n\n",
|
||||
"filename_stem": "20261008-023824-My Session_ v2_.md"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "transcript",
|
||||
"name": "no_todos",
|
||||
"input": {},
|
||||
"expected": {
|
||||
"content": "# no todos\n\n### user\n\ndo the task\n\n### assistant\n\ndone, verified\n\n"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package main
|
||||
|
||||
// truth_transcript_test.go — factory batch 11 parity gate.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTranscriptParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_transcript.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_transcript.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
json.Unmarshal(b, &doc)
|
||||
for _, c := range doc.Cases {
|
||||
switch c.Fn {
|
||||
case "extract_todos":
|
||||
rowsAny, _ := c.Input["rows"].([]interface{})
|
||||
var rows []map[string]interface{}
|
||||
for _, r := range rowsAny {
|
||||
m, _ := r.(map[string]interface{})
|
||||
rows = append(rows, m)
|
||||
}
|
||||
got := ExtractTodos(rows)
|
||||
wantArr, _ := c.Expected["todos"].([]interface{})
|
||||
gb, _ := json.Marshal(got)
|
||||
wb, _ := json.Marshal(wantArr)
|
||||
if string(gb) != string(wb) {
|
||||
t.Errorf("%s: todos\n got %s\nwant %s", c.Name, gb, wb)
|
||||
}
|
||||
case "transcript":
|
||||
// rebuild: title from the oracle content's first line, parts/todos
|
||||
// re-derived from the oracle content itself (the fixture ships
|
||||
// its own state — the title is the content's "# " line).
|
||||
wantContent, _ := c.Expected["content"].(string)
|
||||
wantStem, _ := c.Expected["filename_stem"].(string)
|
||||
// extract the title
|
||||
title := strings.TrimPrefix(strings.SplitN(wantContent, "\n", 2)[0], "# ")
|
||||
// reconstruct parts + todos from the oracle content
|
||||
var todos []map[string]interface{}
|
||||
var parts [][2]string
|
||||
section := ""
|
||||
var curRole, curText string
|
||||
for _, line := range strings.Split(wantContent, "\n") {
|
||||
switch {
|
||||
case strings.HasPrefix(line, "## Todo"):
|
||||
section = "todos"
|
||||
case strings.HasPrefix(line, "### "):
|
||||
if curRole != "" {
|
||||
parts = append(parts, [2]string{curRole, strings.TrimRight(curText, "\n")})
|
||||
}
|
||||
curRole = strings.TrimPrefix(line, "### ")
|
||||
curText = ""
|
||||
section = "parts"
|
||||
case strings.HasPrefix(line, "- [") && section == "todos" && len(line) > 5:
|
||||
inner := strings.TrimPrefix(line, "- [")
|
||||
if len(inner) < 2 {
|
||||
continue
|
||||
}
|
||||
mark := string(inner[0])
|
||||
rest := strings.TrimPrefix(inner[1:], "] ")
|
||||
status := "pending"
|
||||
switch mark {
|
||||
case "x":
|
||||
status = "completed"
|
||||
case "~":
|
||||
status = "in_progress"
|
||||
}
|
||||
content := rest
|
||||
prio := ""
|
||||
if idx := strings.LastIndex(rest, " ["); idx >= 0 && strings.HasSuffix(rest, "]") {
|
||||
content = rest[:idx]
|
||||
prio = rest[idx+2 : len(rest)-1]
|
||||
}
|
||||
todos = append(todos, map[string]interface{}{
|
||||
"content": content, "status": status, "priority": prio})
|
||||
case section == "parts" && curRole != "":
|
||||
if curText != "" || strings.TrimSpace(line) != "" {
|
||||
if curText != "" {
|
||||
curText += "\n"
|
||||
}
|
||||
curText += line
|
||||
}
|
||||
}
|
||||
}
|
||||
if curRole != "" {
|
||||
parts = append(parts, [2]string{curRole, strings.TrimRight(curText, "\n")})
|
||||
}
|
||||
// the timestamp differs (live clock) — derive the stem's suffix;
|
||||
// guard: stems with fewer than 3 dash-parts use the whole stem
|
||||
stemParts := strings.SplitN(wantStem, "-", 3)
|
||||
ts := wantStem
|
||||
if len(stemParts) >= 2 {
|
||||
ts = stemParts[0] + "-" + stemParts[1]
|
||||
}
|
||||
path, err := WriteTranscript(t.TempDir(), ts, title, parts, todos)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", c.Name, err)
|
||||
}
|
||||
data, _ := os.ReadFile(path)
|
||||
if string(data) != wantContent {
|
||||
t.Errorf("%s: content\n got %q\nwant %q", c.Name, data, wantContent)
|
||||
}
|
||||
// filename: the sanitized title must match (ignoring the clock)
|
||||
gotStem := filepath2Base(path)
|
||||
if !strings.HasSuffix(gotStem, sanitizeTitle(title)+".md") {
|
||||
t.Errorf("%s: filename %q lacks sanitized title %q", c.Name, gotStem, sanitizeTitle(title))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func filepath2Base(p string) string {
|
||||
i := strings.LastIndex(p, "/")
|
||||
if i >= 0 {
|
||||
return p[i+1:]
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package main
|
||||
// _apply_content (atomic write with backup), and the undo backup.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -201,9 +202,21 @@ func BackupFile(path string) string {
|
||||
if err := os.WriteFile(backup, data, 0o644); err != nil {
|
||||
return ""
|
||||
}
|
||||
// #2481 FIX: sidecar manifest carries the original ABSOLUTE path
|
||||
// (parity with the Python oracle's sidecar fix).
|
||||
manifest := fmt.Sprintf(`{"original": %s}`, jsonString(path))
|
||||
if err := os.WriteFile(backup+".manifest", []byte(manifest), 0o644); err != nil {
|
||||
return ""
|
||||
}
|
||||
return backup
|
||||
}
|
||||
|
||||
// jsonString — minimal JSON string encoder.
|
||||
func jsonString(s string) string {
|
||||
b, _ := json.Marshal(s)
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// undoDirFn — the undo-dir seam (tests redirect; prod returns the default).
|
||||
var undoDirFn = func() string {
|
||||
return filepath.Join(expandHome("~"), ".rune", "undo")
|
||||
@@ -243,8 +256,29 @@ func UndoLast(undoDir string) (string, string) {
|
||||
return backups[i] > backups[j]
|
||||
})
|
||||
backup := backups[0]
|
||||
|
||||
// PRIMARY (#2481): the sidecar manifest carries the original absolute
|
||||
// path (parity with the Python oracle's sidecar fix).
|
||||
manifestPath := backup + ".manifest"
|
||||
if data, err := os.ReadFile(manifestPath); err == nil {
|
||||
var m struct {
|
||||
Original string `json:"original"`
|
||||
}
|
||||
if json.Unmarshal(data, &m) == nil && m.Original != "" {
|
||||
if _, statErr := os.Stat(m.Original); statErr == nil {
|
||||
if content, rerr := os.ReadFile(backup); rerr == nil {
|
||||
if werr := os.WriteFile(m.Original, content, 0o644); werr == nil {
|
||||
_ = os.Remove(backup)
|
||||
_ = os.Remove(manifestPath)
|
||||
return m.Original, fmt.Sprintf("Restored %s", m.Original)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LEGACY fallback: name-parse (pre-fix behavior).
|
||||
name := strings.TrimSuffix(filepath.Base(backup), ".bak")
|
||||
// parts = name.split("_", 4); original = parts[4].replace("_", "/")
|
||||
parts := strings.SplitN(name, "_", 5)
|
||||
if len(parts) >= 5 {
|
||||
original := strings.ReplaceAll(parts[4], "_", "/")
|
||||
|
||||
@@ -53,7 +53,10 @@ func loadTruth(t *testing.T, name string) (truthDoc, string, string) {
|
||||
t.Fatalf("%s unparseable: %v", name, err)
|
||||
}
|
||||
tmpRoot := mkTruthRoot(t, AllowedReadDirs)
|
||||
wRoot := mkTruthRoot(t, AllowedWriteDirs)
|
||||
// The WRITE root must come from write-allowed dirs ONLY (write policy
|
||||
// is ~ + /mnt/workdrive — /tmp is read-allowed but NOT write-allowed;
|
||||
// the oracle's own generator respected this split).
|
||||
wRoot := mkWriteRoot(t)
|
||||
for _, f := range doc.Fixtures.Files {
|
||||
p := substRoots(f.Path, tmpRoot, wRoot)
|
||||
if err := os.WriteFile(p, []byte(substRoots(f.Content, tmpRoot, wRoot)), 0o644); err != nil {
|
||||
@@ -72,17 +75,47 @@ func loadTruth(t *testing.T, name string) (truthDoc, string, string) {
|
||||
return doc, tmpRoot, wRoot
|
||||
}
|
||||
|
||||
// mkTruthRoot — first creatable temp dir among the allowed dirs (the same
|
||||
// resolution order the generator used).
|
||||
// mkWriteRoot — first creatable dir among AllowedWriteDirs, then HOME
|
||||
// (both write-allowed on every surface). /tmp is deliberately excluded.
|
||||
func mkWriteRoot(t *testing.T) string {
|
||||
t.Helper()
|
||||
candidates := append([]string{}, AllowedWriteDirs...)
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
candidates = append(candidates, home)
|
||||
}
|
||||
for _, d := range candidates {
|
||||
// Skip candidates the product's own policy blocks (e.g. a runner
|
||||
// HOME under /var) — a root there can never pass ValidateWritePath.
|
||||
if IsPathAllowed(d, AllowedWriteDirs) {
|
||||
p, err := os.MkdirTemp(d, "rune-truth-w-")
|
||||
if err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Fatalf("no creatable write root among %v", candidates)
|
||||
return ""
|
||||
}
|
||||
|
||||
// mkTruthRoot — first creatable temp dir among the allowed dirs. The
|
||||
// candidate order matters on CI: the runner's default TMPDIR may resolve
|
||||
// under a BLOCKED dir (/var/lib/...), so /tmp is tried FIRST (it is in
|
||||
// AllowedReadDirs on every surface we run: local, CI, canary), then the
|
||||
// configured allowed dirs, then HOME (allowed by policy on both sides).
|
||||
func mkTruthRoot(t *testing.T, dirs []string) string {
|
||||
t.Helper()
|
||||
for _, d := range dirs {
|
||||
candidates := []string{"/tmp"}
|
||||
candidates = append(candidates, dirs...)
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
candidates = append(candidates, home)
|
||||
}
|
||||
for _, d := range candidates {
|
||||
p, err := os.MkdirTemp(d, "rune-truth-")
|
||||
if err == nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
t.Fatalf("no creatable root among %v", dirs)
|
||||
t.Fatalf("no creatable root among %v", candidates)
|
||||
return ""
|
||||
}
|
||||
|
||||
|
||||
@@ -6,15 +6,14 @@ package main
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestUndoLastRoundTrip — the LIVE ORACLE TRUTH: undo_last can NEVER
|
||||
// restore absolute paths. The safe-name mangling (slash → _) plus
|
||||
// split("_", 4) puts the first path segment in parts[3]; parts[4]
|
||||
// reconstructs a path MISSING its first segment, which never exists.
|
||||
// The port reproduces this verbatim (dead-function parity — a product
|
||||
// defect filed upstream; the FIX belongs in the Python oracle first).
|
||||
// TestUndoLastRoundTrip — #2481 FIXED (Python-first sidecar manifest, Go
|
||||
// oracle parity): the backup writes a .manifest sidecar carrying the
|
||||
// original ABSOLUTE path; undo restores via the manifest and consumes
|
||||
// both files. The old dead-mechanism behavior is gone on both sides.
|
||||
func TestUndoLastRoundTrip(t *testing.T) {
|
||||
undoDir := t.TempDir()
|
||||
origDir := t.TempDir()
|
||||
@@ -30,23 +29,40 @@ func TestUndoLastRoundTrip(t *testing.T) {
|
||||
if BackupFile(orig) == "" {
|
||||
t.Fatal("backup failed")
|
||||
}
|
||||
// the sidecar must exist beside the BACKUP (in the undo dir)
|
||||
entries, _ := os.ReadDir(undoDir)
|
||||
hasManifest := false
|
||||
for _, e := range entries {
|
||||
if strings.HasSuffix(e.Name(), ".manifest") {
|
||||
hasManifest = true
|
||||
}
|
||||
}
|
||||
if !hasManifest {
|
||||
t.Fatal("sidecar manifest missing in undo dir")
|
||||
}
|
||||
if err := os.WriteFile(orig, []byte("MUTATED\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Parity with the live oracle: (None, "Could not determine original
|
||||
// path") — the mutation is NOT undone. Pinned so a future "fix" in
|
||||
// one language without the other cannot silently diverge.
|
||||
gotPath, msg := UndoLast(undoDir)
|
||||
if gotPath != "" || msg != "Could not determine original path" {
|
||||
t.Errorf("oracle parity: got (%q, %q), want (\"\", \"Could not determine original path\")", gotPath, msg)
|
||||
if gotPath != orig {
|
||||
t.Fatalf("undo path got %q want %q", gotPath, orig)
|
||||
}
|
||||
if !strings.HasPrefix(msg, "Restored ") {
|
||||
t.Errorf("undo msg %q", msg)
|
||||
}
|
||||
data, err := os.ReadFile(orig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(data) != "MUTATED\n" {
|
||||
t.Errorf("oracle never restores; content changed to %q — DIVERGENCE", data)
|
||||
if string(data) != "original content\n" {
|
||||
t.Errorf("restored content %q", data)
|
||||
}
|
||||
// both backup and manifest consumed
|
||||
if _, err := os.Stat(undoDir); !os.IsNotExist(err) {
|
||||
if entries, _ := os.ReadDir(undoDir); len(entries) != 0 {
|
||||
t.Errorf("undo dir not consumed: %d entries", len(entries))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
package main
|
||||
|
||||
// cli.go — factory batch 3: the module-family JSON CLI (curator-core
|
||||
// convention: one JSON op per invocation) over the ported protocol core.
|
||||
// Ops: validate, normalize_text, ollama, visible, contains_internal,
|
||||
// registry_shape.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: tool-protocol <validate|normalize_text|ollama|visible|contains_internal|registry_shape> [json]")
|
||||
os.Exit(1)
|
||||
}
|
||||
cmd := os.Args[1]
|
||||
// argv-mode ops (no JSON body): requires-approval
|
||||
if cmd == "requires-approval" {
|
||||
name := ""
|
||||
if len(os.Args) > 2 {
|
||||
name = os.Args[2]
|
||||
}
|
||||
if alias, ok := toolAliases[name]; ok {
|
||||
name = alias
|
||||
}
|
||||
_, known := RegistryTools[name]
|
||||
ro := readOnlyToolsForProtocol[name]
|
||||
emit(map[string]interface{}{"known": known, "requires_approval": known && !ro})
|
||||
return
|
||||
}
|
||||
var req map[string]interface{}
|
||||
if len(os.Args) > 2 {
|
||||
if err := json.Unmarshal([]byte(os.Args[2]), &req); err != nil {
|
||||
emitErr("bad JSON arg: " + err.Error())
|
||||
}
|
||||
}
|
||||
switch cmd {
|
||||
case "validate":
|
||||
reqAny, _ := req["request"].(map[string]interface{})
|
||||
rb, _ := json.Marshal(reqAny)
|
||||
var request ToolRequest
|
||||
if err := json.Unmarshal(rb, &request); err != nil {
|
||||
emitErr(err.Error())
|
||||
}
|
||||
validated, err := ValidateToolRequest(request)
|
||||
if err != nil {
|
||||
emit(map[string]interface{}{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
emit(map[string]interface{}{"ok": true, "result": validated})
|
||||
case "normalize_text":
|
||||
text, _ := req["text"].(string)
|
||||
requests, err := NormalizeTextToolCalls(text)
|
||||
if err != nil {
|
||||
emit(map[string]interface{}{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
emit(map[string]interface{}{"ok": true, "result": requests})
|
||||
case "ollama":
|
||||
msgAny, _ := req["message"].(map[string]interface{})
|
||||
requests, err := NormalizeOllamaToolCalls(msgAny)
|
||||
if err != nil {
|
||||
emit(map[string]interface{}{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
emit(map[string]interface{}{"ok": true, "result": requests})
|
||||
case "visible":
|
||||
text, _ := req["text"].(string)
|
||||
visible, err := ExtractVisibleText(text)
|
||||
if err != nil {
|
||||
emit(map[string]interface{}{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
emit(map[string]interface{}{"ok": true, "result": map[string]interface{}{"visible": visible}})
|
||||
case "contains_internal":
|
||||
text, _ := req["text"].(string)
|
||||
emit(map[string]interface{}{"ok": true, "result": map[string]interface{}{"contains": ContainsInternalProtocol(text)}})
|
||||
case "registry_shape":
|
||||
// set-equality with the frozen registry: 19 tools, order preserved
|
||||
var defs []map[string]interface{}
|
||||
json.Unmarshal(registryJSON, &defs)
|
||||
var order []string
|
||||
for _, d := range defs {
|
||||
n, _ := d["name"].(string)
|
||||
order = append(order, n)
|
||||
}
|
||||
emit(map[string]interface{}{"ok": true, "result": map[string]interface{}{
|
||||
"count": len(RegistryTools), "order": order,
|
||||
}})
|
||||
case "list-tools":
|
||||
var names []string
|
||||
for name := range RegistryTools {
|
||||
names = append(names, name)
|
||||
}
|
||||
emit(map[string]interface{}{"tools": names})
|
||||
default:
|
||||
emitErr("unknown op: " + cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func emit(v map[string]interface{}) {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetEscapeHTML(false)
|
||||
_ = enc.Encode(v)
|
||||
}
|
||||
|
||||
func emitErr(msg string) {
|
||||
emit(map[string]interface{}{"error": msg})
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// readOnlyToolsForProtocol — the auth boundary (mirrors tool-executor's
|
||||
// readOnlyTools — the frozen 12-tool set; kept here for the protocol
|
||||
// side's requires-approval op).
|
||||
var readOnlyToolsForProtocol = map[string]bool{
|
||||
"read_file": true, "glob_files": true, "grep_files": true,
|
||||
"list_directory": true, "git_diff": true, "git_log": true,
|
||||
"repo_map": true, "read_persona": true, "query_memory": true,
|
||||
"check_growth": true, "diagnose_behavior": true,
|
||||
"find_capability": true,
|
||||
}
|
||||
@@ -10,17 +10,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ToolRequest struct {
|
||||
CallID string `json:"call_id"`
|
||||
Name string `json:"name"`
|
||||
Args map[string]interface{} `json:"args"`
|
||||
Source string `json:"source"`
|
||||
CallID string `json:"call_id"`
|
||||
Name string `json:"name"`
|
||||
Arguments map[string]interface{} `json:"arguments"`
|
||||
Source string `json:"source"`
|
||||
}
|
||||
|
||||
type ToolEvent struct {
|
||||
@@ -79,61 +76,4 @@ func validate(req ToolRequest) (ToolEvent, bool) {
|
||||
return ToolEvent{Kind: kind, Request: req}, true
|
||||
}
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Fprintln(os.Stderr, "usage: tool-protocol <validate|list-tools|requires-approval> [args...]")
|
||||
os.Exit(1)
|
||||
}
|
||||
cmd := os.Args[1]
|
||||
args := os.Args[2:]
|
||||
|
||||
switch cmd {
|
||||
case "validate":
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: tool-protocol validate <request-json>")
|
||||
os.Exit(1)
|
||||
}
|
||||
var req ToolRequest
|
||||
if err := json.Unmarshal([]byte(args[0]), &req); err != nil {
|
||||
out, _ := json.Marshal(ToolEvent{Kind: "error", Error: fmt.Sprintf("invalid JSON: %v", err)})
|
||||
fmt.Println(string(out))
|
||||
return
|
||||
}
|
||||
event, ok := validate(req)
|
||||
out, _ := json.MarshalIndent(event, "", " ")
|
||||
fmt.Println(string(out))
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
case "list-tools":
|
||||
var names []string
|
||||
for name := range TOOL_INDEX {
|
||||
names = append(names, name)
|
||||
}
|
||||
out, _ := json.MarshalIndent(names, "", " ")
|
||||
fmt.Println(string(out))
|
||||
|
||||
case "requires-approval":
|
||||
if len(args) < 1 {
|
||||
fmt.Fprintln(os.Stderr, "usage: tool-protocol requires-approval <tool-name>")
|
||||
os.Exit(1)
|
||||
}
|
||||
name := args[0]
|
||||
if alias, ok := ALIASES[name]; ok {
|
||||
name = alias
|
||||
}
|
||||
def, ok := TOOL_INDEX[name]
|
||||
if !ok {
|
||||
fmt.Println("null")
|
||||
return
|
||||
}
|
||||
out, _ := json.Marshal(map[string]bool{"requires_approval": def.RequiresApproval})
|
||||
fmt.Println(string(out))
|
||||
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command: %s\n", cmd)
|
||||
os.Exit(1)
|
||||
}
|
||||
_ = strings.TrimSpace("")
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
package main
|
||||
|
||||
// protocol.go — factory batch 1: the tool-protocol validation core of
|
||||
// tool_protocol.py (contract BUILDER-2026-10-07T22:11:52Z-9ffb73).
|
||||
// ValidateToolRequest (alias canonicalization BEFORE schema — bash never
|
||||
// bypasses run_command's approval boundary), the dataclass shapes,
|
||||
// ollama normalization, internal-protocol detection, visible-text
|
||||
// extraction. The schemas come from the FROZEN registry
|
||||
// (tools_registry.json) — set-equality with the Python TOOL_INDEX.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// toolAliases — canonicalization BEFORE schema/allowlist checks.
|
||||
var toolAliases = map[string]string{
|
||||
"grep": "grep_files",
|
||||
"bash": "run_command",
|
||||
}
|
||||
|
||||
// ValidateToolRequest — validate against the frozen registry schema
|
||||
// (tools_registry.json — the parity-pinned authority shared with
|
||||
// tool-executor). Exact Python error texts.
|
||||
func ValidateToolRequest(request ToolRequest) (ToolRequest, error) {
|
||||
if alias, ok := toolAliases[request.Name]; ok {
|
||||
request.Name = alias
|
||||
}
|
||||
def, ok := RegistryTools[request.Name]
|
||||
if !ok {
|
||||
return request, fmt.Errorf("unknown tool: %s", request.Name)
|
||||
}
|
||||
if request.Arguments == nil {
|
||||
request.Arguments = map[string]interface{}{}
|
||||
}
|
||||
schema, _ := def["parameters"].(map[string]interface{})
|
||||
properties, _ := schema["properties"].(map[string]interface{})
|
||||
required, _ := schema["required"].([]interface{})
|
||||
for _, r := range required {
|
||||
name, _ := r.(string)
|
||||
if _, has := request.Arguments[name]; !has {
|
||||
return request, fmt.Errorf("required argument missing: %s", name)
|
||||
}
|
||||
}
|
||||
// deterministic iteration for error stability (Python dict order is
|
||||
// insertion; Go maps are random — sort the arg names)
|
||||
names := make([]string, 0, len(request.Arguments))
|
||||
for n := range request.Arguments {
|
||||
names = append(names, n)
|
||||
}
|
||||
sort.Strings(names)
|
||||
for _, name := range names {
|
||||
value := request.Arguments[name]
|
||||
propSchema, ok := properties[name].(map[string]interface{})
|
||||
if !ok {
|
||||
return request, fmt.Errorf("unknown argument: %s", name)
|
||||
}
|
||||
expected, _ := propSchema["type"].(string)
|
||||
if expected != "" && !typeMatches(value, expected) {
|
||||
return request, fmt.Errorf("argument %s must be %s", name, expected)
|
||||
}
|
||||
if expected == "array" {
|
||||
if list, isList := value.([]interface{}); isList {
|
||||
if items, has := propSchema["items"].(map[string]interface{}); has {
|
||||
itemType, _ := items["type"].(string)
|
||||
if itemType != "" {
|
||||
for _, item := range list {
|
||||
if !typeMatches(item, itemType) {
|
||||
return request, fmt.Errorf("argument %s items must be %s", name, itemType)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return request, nil
|
||||
}
|
||||
|
||||
// typeMatches — Python _type_matches semantics over JSON-decoded values.
|
||||
func typeMatches(value interface{}, expected string) bool {
|
||||
switch expected {
|
||||
case "string":
|
||||
_, ok := value.(string)
|
||||
return ok
|
||||
case "integer":
|
||||
f, ok := value.(float64)
|
||||
return ok && f == float64(int64(f))
|
||||
case "number":
|
||||
_, ok := value.(float64)
|
||||
return ok
|
||||
case "boolean":
|
||||
_, ok := value.(bool)
|
||||
return ok
|
||||
case "array":
|
||||
_, ok := value.([]interface{})
|
||||
return ok
|
||||
case "object":
|
||||
_, ok := value.(map[string]interface{})
|
||||
return ok
|
||||
default:
|
||||
return true // unknown types pass (open-world)
|
||||
}
|
||||
}
|
||||
|
||||
// NormalizeOllamaToolCalls — native envelope + string-arguments decode.
|
||||
func NormalizeOllamaToolCalls(message map[string]interface{}) ([]ToolRequest, error) {
|
||||
callsAny, _ := message["tool_calls"].([]interface{})
|
||||
var requests []ToolRequest
|
||||
for _, raw := range callsAny {
|
||||
call, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("malformed native tool call")
|
||||
}
|
||||
fn, ok := call["function"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("malformed native tool call")
|
||||
}
|
||||
args := fn["arguments"]
|
||||
var arguments map[string]interface{}
|
||||
switch a := args.(type) {
|
||||
case string:
|
||||
if err := json.Unmarshal([]byte(a), &arguments); err != nil {
|
||||
return nil, fmt.Errorf("malformed tool arguments JSON")
|
||||
}
|
||||
case map[string]interface{}:
|
||||
arguments = a
|
||||
default:
|
||||
arguments = map[string]interface{}{}
|
||||
}
|
||||
name, _ := fn["name"].(string)
|
||||
callID, _ := call["id"].(string)
|
||||
req := ToolRequest{
|
||||
CallID: strings.TrimSpace(callID),
|
||||
Name: name,
|
||||
Arguments: arguments,
|
||||
Source: "ollama-native",
|
||||
}
|
||||
if req.CallID == "" {
|
||||
req.CallID = NewCallID()
|
||||
}
|
||||
validated, err := ValidateToolRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
requests = append(requests, validated)
|
||||
}
|
||||
if requests == nil {
|
||||
requests = []ToolRequest{}
|
||||
}
|
||||
return requests, nil
|
||||
}
|
||||
|
||||
var internalResultRe = regexp.MustCompile(`(?im)^[ \t]*\[tool (?:result|error)\]`)
|
||||
|
||||
// ContainsInternalProtocol — result markers or xml tool_call spans.
|
||||
func ContainsInternalProtocol(text string) bool {
|
||||
if internalResultRe.MatchString(text) {
|
||||
return true
|
||||
}
|
||||
return strings.Contains(strings.ToLower(text), "<tool_call>")
|
||||
}
|
||||
|
||||
// NewCallID — the _new_call_id seam (the Python side uses uuid4().hex;
|
||||
// the factory path accepts caller-supplied ids — this is the fallback).
|
||||
func NewCallID() string {
|
||||
return fmt.Sprintf("call-%d", time.Now().UnixNano())
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package main
|
||||
|
||||
// registry.go — the frozen tool registry (tools_registry.json from
|
||||
// tool-executor — the parity-pinned authority). Set-equality with the
|
||||
// Python TOOL_INDEX is enforced by the auth truth battery on that side;
|
||||
// this side consumes the same file so schemas can never drift.
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
_ "embed"
|
||||
)
|
||||
|
||||
//go:embed tools_registry.json
|
||||
var registryJSON []byte
|
||||
|
||||
// RegistryTools — name → full definition (parameters included).
|
||||
var RegistryTools = parseRegistry()
|
||||
|
||||
func parseRegistry() map[string]map[string]interface{} {
|
||||
var tools []map[string]interface{}
|
||||
if err := json.Unmarshal(registryJSON, &tools); err != nil {
|
||||
panic("tools_registry.json: " + err.Error())
|
||||
}
|
||||
out := make(map[string]map[string]interface{}, len(tools))
|
||||
for _, t := range tools {
|
||||
name, _ := t["name"].(string)
|
||||
out[name] = t
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
package main
|
||||
|
||||
// spans.go — factory batch 2: the text-fallback parse + span surgery of
|
||||
// tool_protocol.py (contract 9ffb73 batch 2). DecodedObjects (balanced
|
||||
// JSON scan), ProtocolEntries (fence/xml wrapper expansion), ExtractVisibleText,
|
||||
// containsToolObjectKey (the outside-strings scanner).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var fenceRe = regexp.MustCompile("(?is)```(?:json)?\\s*.*?```")
|
||||
var xmlRe = regexp.MustCompile("(?is)<tool_call>.*?</tool_call>")
|
||||
|
||||
// Span — a byte range in the source text.
|
||||
type Span struct {
|
||||
Start int
|
||||
End int
|
||||
}
|
||||
|
||||
// decodedObject — one raw-decoded value with its exact span.
|
||||
type decodedObject struct {
|
||||
Value interface{}
|
||||
Start, End int
|
||||
}
|
||||
|
||||
// DecodedObjects — yield JSON values and exact spans via balanced decoding
|
||||
// (Python's JSONDecoder.raw_decode scan). Go's json.Decoder gives
|
||||
// equivalent balanced parsing with InputOffset.
|
||||
func DecodedObjects(text string) []decodedObject {
|
||||
var out []decodedObject
|
||||
position := 0
|
||||
dec := json.NewDecoder(strings.NewReader(text))
|
||||
for {
|
||||
// advance to the next '{' or '[' like the oracle's find-scan
|
||||
rest := text[position:]
|
||||
cand := -1
|
||||
for _, tok := range []string{"{", "["} {
|
||||
if i := strings.Index(rest, tok); i >= 0 && (cand < 0 || i < cand) {
|
||||
cand = i
|
||||
}
|
||||
}
|
||||
if cand < 0 {
|
||||
return out
|
||||
}
|
||||
skip := position + cand
|
||||
dec = json.NewDecoder(strings.NewReader(text[skip:]))
|
||||
var value interface{}
|
||||
if err := dec.Decode(&value); err != nil {
|
||||
position = skip + 1
|
||||
continue
|
||||
}
|
||||
end := skip + int(dec.InputOffset())
|
||||
out = append(out, decodedObject{Value: value, Start: skip, End: end})
|
||||
position = end
|
||||
}
|
||||
}
|
||||
|
||||
// ProtocolEntries — the parsed tool-call requests with their spans.
|
||||
// Raises (error) on: array envelopes, malformed protocol residue.
|
||||
// ProtocolEntry — a parsed request with its span.
|
||||
type ProtocolEntry struct {
|
||||
Req ToolRequest
|
||||
Span Span
|
||||
}
|
||||
|
||||
func ProtocolEntries(text string) ([]ProtocolEntry, error) {
|
||||
var entries []ProtocolEntry
|
||||
for _, obj := range DecodedObjects(text) {
|
||||
_ = obj
|
||||
if arr, ok := obj.Value.([]interface{}); ok {
|
||||
for _, item := range arr {
|
||||
if m, isMap := item.(map[string]interface{}); isMap {
|
||||
if _, has := m["tool"]; has {
|
||||
return nil, fmt.Errorf("array is not a valid tool call envelope")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
req, ok, reqErr := requestFromEnvelope(obj.Value, "text-fallback", nil)
|
||||
if reqErr != nil {
|
||||
return nil, reqErr
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
spanStart, spanEnd := obj.Start, obj.End
|
||||
for _, pattern := range []*regexp.Regexp{fenceRe, xmlRe} {
|
||||
var wrapper *Span
|
||||
for _, m := range pattern.FindAllStringIndex(text, -1) {
|
||||
if m[0] <= obj.Start && obj.End <= m[1] {
|
||||
wrapper = &Span{Start: m[0], End: m[1]}
|
||||
break
|
||||
}
|
||||
}
|
||||
if wrapper != nil {
|
||||
wrapped := text[wrapper.Start:wrapper.End]
|
||||
var contentStart, contentEnd int
|
||||
if pattern == fenceRe {
|
||||
opening := regexp.MustCompile("(?i)^```(?:json)?\\s*")
|
||||
m := opening.FindString(wrapped)
|
||||
contentStart = wrapper.Start + len(m)
|
||||
contentEnd = wrapper.End - len("```")
|
||||
} else {
|
||||
contentStart = wrapper.Start + len("<tool_call>")
|
||||
contentEnd = wrapper.End - len("</tool_call>")
|
||||
}
|
||||
remainder := text[contentStart:obj.Start] + text[obj.End:contentEnd]
|
||||
if strings.TrimSpace(remainder) == "" {
|
||||
spanStart, spanEnd = wrapper.Start, wrapper.End
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
entries = append(entries, ProtocolEntry{Req: req, Span: Span{spanStart, spanEnd}})
|
||||
}
|
||||
// malformed residue check
|
||||
remainder := text
|
||||
for i := len(entries) - 1; i >= 0; i-- {
|
||||
s := entries[i].Span
|
||||
remainder = remainder[:s.Start] + remainder[s.End:]
|
||||
}
|
||||
if strings.Contains(strings.ToLower(remainder), "<tool_call>") {
|
||||
return nil, fmt.Errorf("malformed tool call protocol")
|
||||
}
|
||||
if containsToolObjectKey(remainder) {
|
||||
return nil, fmt.Errorf("malformed tool call protocol")
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// NormalizeTextToolCalls — ordered requests from model text.
|
||||
func NormalizeTextToolCalls(text string) ([]ToolRequest, error) {
|
||||
entries, err := ProtocolEntries(text)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]ToolRequest, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
out = append(out, e.Req)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// requestFromEnvelope — the envelope → validated request (Python
|
||||
// _request_from_envelope). ok=false when the value isn't an envelope;
|
||||
// reqErr carries the propagated validation error (the oracle treats a
|
||||
// validation failure inside an envelope as a HARD error for the text).
|
||||
func requestFromEnvelope(value interface{}, source string, callID interface{}) (req ToolRequest, ok bool, reqErr error) {
|
||||
envelope, isMap := value.(map[string]interface{})
|
||||
if !isMap {
|
||||
return ToolRequest{}, false, nil
|
||||
}
|
||||
name, has := envelope["tool"]
|
||||
if !has {
|
||||
return ToolRequest{}, false, nil
|
||||
}
|
||||
nameStr, isStr := name.(string)
|
||||
if !isStr {
|
||||
return ToolRequest{}, false, fmt.Errorf("tool name must be a string")
|
||||
}
|
||||
arguments, _ := envelope["args"].(map[string]interface{})
|
||||
if arguments == nil {
|
||||
arguments = map[string]interface{}{}
|
||||
}
|
||||
idStr := ""
|
||||
if callID != nil {
|
||||
idStr = strings.TrimSpace(fmt.Sprintf("%v", callID))
|
||||
}
|
||||
if idStr == "" {
|
||||
idStr = NewCallID()
|
||||
}
|
||||
newReq := ToolRequest{CallID: idStr, Name: nameStr, Arguments: arguments, Source: source}
|
||||
validated, vErr := ValidateToolRequest(newReq)
|
||||
if vErr != nil {
|
||||
return ToolRequest{}, true, vErr
|
||||
}
|
||||
return validated, true, nil
|
||||
}
|
||||
|
||||
// ExtractVisibleText — remove recognized protocol spans while preserving
|
||||
// ordinary JSON; error when malformed protocol remains.
|
||||
func ExtractVisibleText(text string) (string, error) {
|
||||
entries, err := ProtocolEntries(text)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
spans := make([]Span, 0, len(entries))
|
||||
seen := map[Span]bool{}
|
||||
for _, e := range entries {
|
||||
if !seen[e.Span] {
|
||||
seen[e.Span] = true
|
||||
spans = append(spans, e.Span)
|
||||
}
|
||||
}
|
||||
// sorted by start (the oracle sorts the deduped set)
|
||||
for i := 1; i < len(spans); i++ {
|
||||
for j := i; j > 0 && spans[j-1].Start > spans[j].Start; j-- {
|
||||
spans[j-1], spans[j] = spans[j], spans[j-1]
|
||||
}
|
||||
}
|
||||
visible := text
|
||||
for i := len(spans) - 1; i >= 0; i-- {
|
||||
start, end := spans[i].Start, spans[i].End
|
||||
lineStart := strings.LastIndex(visible[:start], "\n") + 1
|
||||
nextNewline := strings.Index(visible[end:], "\n")
|
||||
lineEnd := len(visible)
|
||||
if nextNewline >= 0 {
|
||||
lineEnd = end + nextNewline
|
||||
}
|
||||
if strings.TrimSpace(visible[lineStart:start]) == "" && strings.TrimSpace(visible[end:lineEnd]) == "" {
|
||||
start = lineStart
|
||||
if nextNewline >= 0 {
|
||||
end = lineEnd + 1
|
||||
} else if lineStart > 0 {
|
||||
start = lineStart - 1
|
||||
}
|
||||
}
|
||||
visible = visible[:start] + visible[end:]
|
||||
}
|
||||
if ContainsInternalProtocol(visible) {
|
||||
return "", fmt.Errorf("malformed tool call protocol remains in visible text")
|
||||
}
|
||||
return visible, nil
|
||||
}
|
||||
|
||||
// containsToolObjectKey — detect a literal or escaped "tool" object key
|
||||
// outside JSON strings (the injection-residue detector).
|
||||
func containsToolObjectKey(text string) bool {
|
||||
dec := json.NewDecoder(strings.NewReader(text))
|
||||
inString := false
|
||||
escaped := false
|
||||
for index, character := range text {
|
||||
if inString {
|
||||
if escaped {
|
||||
escaped = false
|
||||
} else if character == '\\' {
|
||||
escaped = true
|
||||
} else if character == '"' {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
if character == '"' {
|
||||
inString = true
|
||||
continue
|
||||
}
|
||||
if character != '{' && character != ',' {
|
||||
continue
|
||||
}
|
||||
keyStart := index + 1
|
||||
for keyStart < len(text) && (text[keyStart] == ' ' || text[keyStart] == '\t' || text[keyStart] == '\n' || text[keyStart] == '\r') {
|
||||
keyStart++
|
||||
}
|
||||
if keyStart >= len(text) || text[keyStart] != '"' {
|
||||
continue
|
||||
}
|
||||
var key string
|
||||
if err := dec.Decode(&key); err != nil {
|
||||
continue
|
||||
}
|
||||
_ = key
|
||||
// Python: key == "tool" and the colon follows → tool object key
|
||||
if key == "tool" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Binary file not shown.
@@ -0,0 +1,492 @@
|
||||
[
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "Read a file with optional windowed viewing (offset/limit). Returns numbered lines. Use offset/limit for large files (100 lines at a time).",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"path exists and is a readable file"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"offset": {
|
||||
"type": "integer",
|
||||
"description": "Line number to start from (1-indexed, optional)"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max lines to return (default 100, optional)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "edit_file",
|
||||
"description": "Surgical search/replace edit. More reliable than full file rewrites. The search string must match exactly.",
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory",
|
||||
"search string matches exactly once"
|
||||
],
|
||||
"side_effects": [
|
||||
"rewrites the target file atomically after approval",
|
||||
"creates a .bak backup under ~/.rune/undo"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"search": {
|
||||
"type": "string",
|
||||
"description": "Exact text to find (must match file content)"
|
||||
},
|
||||
"replace": {
|
||||
"type": "string",
|
||||
"description": "Text to replace with"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"search",
|
||||
"replace"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"description": "Write content to a file. Creates parent directories if needed. Shows diff before writing. Use for new files or full rewrites.",
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory"
|
||||
],
|
||||
"side_effects": [
|
||||
"writes the full file content atomically after approval",
|
||||
"creates a .bak backup under ~/.rune/undo"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"description": "Content to write"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path",
|
||||
"content"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "glob_files",
|
||||
"description": "Find files matching a glob pattern. Returns file paths sorted by modification time.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"search directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Glob pattern (e.g. '**/*.py', 'src/**/*.ts')"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Directory to search in (optional, defaults to cwd)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "grep_files",
|
||||
"description": "Search for a pattern in files. Returns file paths and line numbers. Use include to filter by file type.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"search directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"description": "Regex pattern to search for"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Directory to search in (optional)"
|
||||
},
|
||||
"include": {
|
||||
"type": "string",
|
||||
"description": "File pattern to include (e.g. '*.py', '*.{ts,tsx}')"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"pattern"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "list_directory",
|
||||
"description": "List files and directories at a path. Returns names with / suffix for directories.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"path is a readable directory"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the directory"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "lint_file",
|
||||
"description": "Lint a file for syntax errors and style issues. Blocks bad edits. Supports Python (ruff), JS/TS (eslint), Go (golangci-lint), and more.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"file exists",
|
||||
"a linter is configured for the language"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to the file"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "Language override (optional, auto-detected from extension)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_tests",
|
||||
"description": "Run tests for a file or project. Auto-detects test runner from language.",
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"a test runner is configured for the project"
|
||||
],
|
||||
"side_effects": [
|
||||
"runs project test commands which may create or delete files"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "File or directory to test"
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Custom test command (optional, overrides auto-detect)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "format_code",
|
||||
"description": "Auto-format code files. Supports Python (black/ruff), JS/TS (prettier), Go (gofmt), and more.",
|
||||
"risk": "write",
|
||||
"preconditions": [
|
||||
"path is inside an allowed write directory",
|
||||
"a formatter is configured for the language"
|
||||
],
|
||||
"side_effects": [
|
||||
"rewrites files in place to the formatter's style"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "File or directory to format"
|
||||
},
|
||||
"language": {
|
||||
"type": "string",
|
||||
"description": "Language override (optional)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "git_diff",
|
||||
"description": "Show git diff for staged or unstaged changes.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"working directory is a git repository"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"staged": {
|
||||
"type": "boolean",
|
||||
"description": "Show staged changes (default: unstaged)"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Specific file to diff (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "git_log",
|
||||
"description": "Show recent git commits.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"working directory is a git repository"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Number of commits (default 10)"
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Specific file history (optional)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "git_commit",
|
||||
"description": "Commit changes with a message. Auto-stages modified files.",
|
||||
"risk": "irreversible",
|
||||
"preconditions": [
|
||||
"working directory is a git repository",
|
||||
"there are changes to commit"
|
||||
],
|
||||
"side_effects": [
|
||||
"creates a commit on the current branch"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string",
|
||||
"description": "Commit message"
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Specific files to commit (optional, defaults to all modified)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "repo_map",
|
||||
"description": "Generate an AST-based codebase map showing key symbols (classes, functions, imports). Uses tree-sitter for 100+ languages.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"target directory is readable"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string",
|
||||
"description": "Root directory to map"
|
||||
},
|
||||
"languages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Filter by languages (optional)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"path"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "run_command",
|
||||
"description": "Run an approved command. Prefer argv (shell=False); shell syntax requires shell=true and explicit user approval.",
|
||||
"risk": "irreversible",
|
||||
"preconditions": [
|
||||
"operator approves the command",
|
||||
"required binaries are on PATH"
|
||||
],
|
||||
"side_effects": [
|
||||
"executes arbitrary commands with their full side effects"
|
||||
],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command text; shell syntax is allowed only with shell=true"
|
||||
},
|
||||
"argv": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Argument vector for shell-free execution"
|
||||
},
|
||||
"shell": {
|
||||
"type": "boolean",
|
||||
"description": "Explicitly request the user-approved shell execution path"
|
||||
},
|
||||
"workdir": {
|
||||
"type": "string",
|
||||
"description": "Working directory (optional)"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
"description": "Timeout in seconds (default 30)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read_persona",
|
||||
"description": "Read Rune's persona definition. Returns the full persona block with voice, identity, and rules.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"persona MEMORY.md exists"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "query_memory",
|
||||
"description": "Query Rune's memory store. Returns relevant memories for a query.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"memory store database is available"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "Search query for memory"
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max results (default 5)"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "check_growth",
|
||||
"description": "Check Rune's growth log. Returns recent growth entries and patterns.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"growth log file exists"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "Max entries (default 10)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "diagnose_behavior",
|
||||
"description": "Diagnose Rune's current behavior. Returns analysis of persona adherence, voice consistency, and drift.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"recent response text provided"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"description": "Recent response text to analyze"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"text"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "find_capability",
|
||||
"description": "Search all installed capabilities for what you need. Returns matching tool names with descriptions and argument schemas. Use this when a capability you need is not in your current tool set; the matched tool becomes callable in the same session.",
|
||||
"risk": "read-only",
|
||||
"preconditions": [
|
||||
"a capability query describing the needed outcome"
|
||||
],
|
||||
"side_effects": [],
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {
|
||||
"type": "string",
|
||||
"description": "What you need to do, e.g. 'run the test suite' or 'commit my changes'"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"query"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,299 @@
|
||||
{
|
||||
"oracle": "src/rune/tool_protocol.py (live)",
|
||||
"generated": "2026-10-07",
|
||||
"cases": [
|
||||
{
|
||||
"fn": "validate",
|
||||
"name": "alias_grep",
|
||||
"input": {
|
||||
"request": {
|
||||
"call_id": "c1",
|
||||
"name": "grep",
|
||||
"arguments": {
|
||||
"pattern": "x",
|
||||
"path": "."
|
||||
},
|
||||
"source": "test"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": {
|
||||
"call_id": "c1",
|
||||
"name": "grep_files",
|
||||
"arguments": {
|
||||
"pattern": "x",
|
||||
"path": "."
|
||||
},
|
||||
"source": "test"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate",
|
||||
"name": "alias_bash",
|
||||
"input": {
|
||||
"request": {
|
||||
"call_id": "c2",
|
||||
"name": "bash",
|
||||
"arguments": {
|
||||
"command": "ls"
|
||||
},
|
||||
"source": "test"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": {
|
||||
"call_id": "c2",
|
||||
"name": "run_command",
|
||||
"arguments": {
|
||||
"command": "ls"
|
||||
},
|
||||
"source": "test"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate",
|
||||
"name": "unknown_tool",
|
||||
"input": {
|
||||
"request": {
|
||||
"call_id": "c3",
|
||||
"name": "nonexistent_tool",
|
||||
"arguments": {},
|
||||
"source": "test"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": "unknown tool: nonexistent_tool"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate",
|
||||
"name": "missing_required",
|
||||
"input": {
|
||||
"request": {
|
||||
"call_id": "c4",
|
||||
"name": "read_file",
|
||||
"arguments": {},
|
||||
"source": "test"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": "required argument missing: path"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate",
|
||||
"name": "unknown_arg",
|
||||
"input": {
|
||||
"request": {
|
||||
"call_id": "c5",
|
||||
"name": "read_file",
|
||||
"arguments": {
|
||||
"path": "/tmp",
|
||||
"bogus": 1
|
||||
},
|
||||
"source": "test"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": "unknown argument: bogus"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "validate",
|
||||
"name": "type_mismatch",
|
||||
"input": {
|
||||
"request": {
|
||||
"call_id": "c6",
|
||||
"name": "read_file",
|
||||
"arguments": {
|
||||
"path": 123
|
||||
},
|
||||
"source": "test"
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": "argument path must be string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "ollama",
|
||||
"name": "string_args",
|
||||
"input": {
|
||||
"message": {
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "x1",
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"arguments": "{\"path\": \"/tmp/x\"}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": [
|
||||
{
|
||||
"call_id": "x1",
|
||||
"name": "read_file",
|
||||
"arguments": {
|
||||
"path": "/tmp/x"
|
||||
},
|
||||
"source": "ollama-native"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "ollama",
|
||||
"name": "malformed_args_json",
|
||||
"input": {
|
||||
"message": {
|
||||
"tool_calls": [
|
||||
{
|
||||
"function": {
|
||||
"name": "read_file",
|
||||
"arguments": "{broken"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": "malformed tool arguments JSON"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "internal",
|
||||
"name": "result_marker",
|
||||
"input": {
|
||||
"text": "[tool result] output here"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": {
|
||||
"contains": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "internal",
|
||||
"name": "error_marker",
|
||||
"input": {
|
||||
"text": "[tool error] boom"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": {
|
||||
"contains": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "internal",
|
||||
"name": "xml_call",
|
||||
"input": {
|
||||
"text": "<tool_call>{\"tool\": \"x\"}</tool_call>"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": {
|
||||
"contains": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "internal",
|
||||
"name": "clean_text",
|
||||
"input": {
|
||||
"text": "just ordinary assistant text with JSON: {\"a\": 1}"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": {
|
||||
"contains": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "visible",
|
||||
"name": "inline_call",
|
||||
"input": {
|
||||
"text": "before {\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp\"}} after"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": {
|
||||
"visible": "before after"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "visible",
|
||||
"name": "fenced_call",
|
||||
"input": {
|
||||
"text": "```json\n{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp\"}}\n```"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": {
|
||||
"visible": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "visible",
|
||||
"name": "xml_call",
|
||||
"input": {
|
||||
"text": "before <tool_call>{\"tool\": \"x\", \"args\": {}}</tool_call> after"
|
||||
},
|
||||
"expected": {
|
||||
"ok": false,
|
||||
"error": "unknown tool: x"
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "visible",
|
||||
"name": "plain",
|
||||
"input": {
|
||||
"text": "no protocol here at all"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": {
|
||||
"visible": "no protocol here at all"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"fn": "normalize",
|
||||
"name": "text_fallback",
|
||||
"input": {
|
||||
"text": "{\"tool\": \"read_file\", \"args\": {\"path\": \"/tmp/x\"}}"
|
||||
},
|
||||
"expected": {
|
||||
"ok": true,
|
||||
"result": [
|
||||
{
|
||||
"call_id": "call_7c538fede68941f49b79bd8565f51a56",
|
||||
"name": "read_file",
|
||||
"arguments": {
|
||||
"path": "/tmp/x"
|
||||
},
|
||||
"source": "text-fallback"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package main
|
||||
|
||||
// truth_protocol_test.go — factory batch 1 parity gate: the tool-protocol
|
||||
// validation core replays truth_protocol.json (live Python oracle —
|
||||
// contract BUILDER-2026-10-07T22:11:52Z-9ffb73).
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProtocolParity(t *testing.T) {
|
||||
b, err := os.ReadFile("truth_protocol.json")
|
||||
if err != nil {
|
||||
t.Fatalf("truth_protocol.json missing: %v", err)
|
||||
}
|
||||
var doc struct {
|
||||
Cases []struct {
|
||||
Fn string `json:"fn"`
|
||||
Name string `json:"name"`
|
||||
Input map[string]interface{} `json:"input"`
|
||||
Expected map[string]interface{} `json:"expected"`
|
||||
} `json:"cases"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &doc); err != nil {
|
||||
t.Fatalf("unparseable: %v", err)
|
||||
}
|
||||
for _, c := range doc.Cases {
|
||||
wantOK, _ := c.Expected["ok"].(bool)
|
||||
switch c.Fn {
|
||||
case "validate":
|
||||
reqAny, _ := c.Input["request"].(map[string]interface{})
|
||||
rb, _ := json.Marshal(reqAny)
|
||||
var req ToolRequest
|
||||
json.Unmarshal(rb, &req)
|
||||
got, err := ValidateToolRequest(req)
|
||||
if wantOK {
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error %v", c.Name, err)
|
||||
continue
|
||||
}
|
||||
want, _ := c.Expected["result"].(map[string]interface{})
|
||||
gb, _ := json.Marshal(got)
|
||||
var gotMap map[string]interface{}
|
||||
json.Unmarshal(gb, &gotMap)
|
||||
jsonEqP(t, c.Name, "request", gotMap, want)
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected error, got none", c.Name)
|
||||
continue
|
||||
}
|
||||
want, _ := c.Expected["error"].(string)
|
||||
if err.Error() != want {
|
||||
t.Errorf("%s: error\n got %q\nwant %q", c.Name, err.Error(), want)
|
||||
}
|
||||
}
|
||||
case "ollama":
|
||||
msgAny, _ := c.Input["message"].(map[string]interface{})
|
||||
got, err := NormalizeOllamaToolCalls(msgAny)
|
||||
if wantOK {
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error %v", c.Name, err)
|
||||
continue
|
||||
}
|
||||
wantArr, _ := c.Expected["result"].([]interface{})
|
||||
gb, _ := json.Marshal(got)
|
||||
var gotArr []interface{}
|
||||
json.Unmarshal(gb, &gotArr)
|
||||
// call_id is time-derived when absent — compare shape sans id
|
||||
jsonEqSansID(t, c.Name, gotArr, wantArr)
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected error, got none", c.Name)
|
||||
continue
|
||||
}
|
||||
want, _ := c.Expected["error"].(string)
|
||||
if err.Error() != want {
|
||||
t.Errorf("%s: error\n got %q\nwant %q", c.Name, err.Error(), want)
|
||||
}
|
||||
}
|
||||
case "internal":
|
||||
text, _ := c.Input["text"].(string)
|
||||
got := ContainsInternalProtocol(text)
|
||||
wantRes, _ := c.Expected["result"].(map[string]interface{})
|
||||
want, _ := wantRes["contains"].(bool)
|
||||
if got != want {
|
||||
t.Errorf("%s: contains got %v want %v", c.Name, got, want)
|
||||
}
|
||||
case "visible":
|
||||
text, _ := c.Input["text"].(string)
|
||||
got, err := ExtractVisibleText(text)
|
||||
if wantOK {
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error %v", c.Name, err)
|
||||
continue
|
||||
}
|
||||
wantRes, _ := c.Expected["result"].(map[string]interface{})
|
||||
if got != wantRes["visible"] {
|
||||
t.Errorf("%s: visible\n got %q\nwant %q", c.Name, got, wantRes["visible"])
|
||||
}
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected error, got none", c.Name)
|
||||
continue
|
||||
}
|
||||
want, _ := c.Expected["error"].(string)
|
||||
if err.Error() != want {
|
||||
t.Errorf("%s: error\n got %q\nwant %q", c.Name, err.Error(), want)
|
||||
}
|
||||
}
|
||||
case "normalize":
|
||||
text, _ := c.Input["text"].(string)
|
||||
got, err := NormalizeTextToolCalls(text)
|
||||
if wantOK {
|
||||
if err != nil {
|
||||
t.Errorf("%s: unexpected error %v", c.Name, err)
|
||||
continue
|
||||
}
|
||||
wantArr, _ := c.Expected["result"].([]interface{})
|
||||
gb, _ := json.Marshal(got)
|
||||
var gotArr []interface{}
|
||||
json.Unmarshal(gb, &gotArr)
|
||||
jsonEqSansID(t, c.Name, gotArr, wantArr)
|
||||
} else {
|
||||
if err == nil {
|
||||
t.Errorf("%s: expected error, got none", c.Name)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func jsonEqP(t *testing.T, name, field string, got, want interface{}) {
|
||||
t.Helper()
|
||||
gb, _ := json.Marshal(got)
|
||||
wb, _ := json.Marshal(want)
|
||||
if string(gb) != string(wb) {
|
||||
t.Errorf("%s: %s divergence:\n got %s\nwant %s", name, field, gb, wb)
|
||||
}
|
||||
}
|
||||
|
||||
// jsonEqSansID — compare ignoring call_id (time-derived fallback).
|
||||
func jsonEqSansID(t *testing.T, name string, got, want []interface{}) {
|
||||
t.Helper()
|
||||
strip := func(arr []interface{}) []interface{} {
|
||||
out := make([]interface{}, 0, len(arr))
|
||||
for _, e := range arr {
|
||||
m, ok := e.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
cp := map[string]interface{}{}
|
||||
for k, v := range m {
|
||||
if k != "call_id" {
|
||||
cp[k] = v
|
||||
}
|
||||
}
|
||||
out = append(out, cp)
|
||||
}
|
||||
return out
|
||||
}
|
||||
gb, _ := json.Marshal(strip(got))
|
||||
wb, _ := json.Marshal(strip(want))
|
||||
if string(gb) != string(wb) {
|
||||
t.Errorf("%s: divergence (sans call_id):\n got %s\nwant %s", name, gb, wb)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"plugins": {
|
||||
"alpha": {
|
||||
"manifest": {
|
||||
"name": "alpha",
|
||||
"capabilities": [
|
||||
"cap.a"
|
||||
],
|
||||
"priority": 50
|
||||
},
|
||||
"registered_at": "2026-10-07T23:35:00",
|
||||
"active": true,
|
||||
"file": ""
|
||||
},
|
||||
"gamma": {
|
||||
"manifest": {
|
||||
"name": "gamma",
|
||||
"capabilities": [
|
||||
"cap.a"
|
||||
],
|
||||
"priority": 10,
|
||||
"auto_activate": false
|
||||
},
|
||||
"registered_at": "2026-10-07T23:35:00",
|
||||
"active": false,
|
||||
"file": ""
|
||||
},
|
||||
"delta": {
|
||||
"manifest": {
|
||||
"name": "delta",
|
||||
"hooks": {
|
||||
"pre-turn": true,
|
||||
"post-turn": false
|
||||
}
|
||||
},
|
||||
"registered_at": "2026-10-07T23:35:00",
|
||||
"active": true,
|
||||
"file": ""
|
||||
}
|
||||
},
|
||||
"capabilities": {
|
||||
"cap.a": [
|
||||
"alpha",
|
||||
"gamma"
|
||||
]
|
||||
},
|
||||
"hooks": {
|
||||
"pre-turn": [
|
||||
"delta"
|
||||
]
|
||||
},
|
||||
"log": []
|
||||
}
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.129.2"
|
||||
__version__ = "0.136.3"
|
||||
|
||||
# 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
|
||||
|
||||
@@ -16,6 +16,9 @@ _SD = os.path.dirname(os.path.abspath(__file__))
|
||||
if _SD not in sys.path:
|
||||
sys.path.insert(0, _SD)
|
||||
import memory_env
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SCRIPTS_DIR = memory_env.scripts_dir()
|
||||
MEMORY_STORE = os.path.join(SCRIPTS_DIR, "memory_store.py")
|
||||
@@ -33,7 +36,7 @@ def run_warm_router(message):
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return json.loads(result.stdout)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
@@ -48,7 +51,7 @@ def run_memory_recall(query, budget=5):
|
||||
if isinstance(data, list):
|
||||
return data[:budget]
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
@@ -59,7 +62,7 @@ def run_always_digest():
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
return json.loads(result.stdout)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
#!/usr/bin/env python3
|
||||
"""build_mode.py — Build mode orchestrator for Rune TUI.
|
||||
|
||||
@@ -462,7 +465,7 @@ def main():
|
||||
status = m.get_status()
|
||||
print(f" {build_id}: {status['description']} ({status['current_phase']})")
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
else:
|
||||
print("No builds found.")
|
||||
else:
|
||||
|
||||
@@ -233,7 +233,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.129.2"' in rune_version,
|
||||
'__version__ = "0.136.3"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -20,6 +20,9 @@ import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SD = os.path.dirname(os.path.abspath(__file__))
|
||||
if _SD not in sys.path:
|
||||
@@ -121,7 +124,7 @@ def select_for_job(task_description, need_tools=True):
|
||||
|
||||
record_failure("capability_selection", exc, tier=1)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
schemas = _native_tool_schemas()
|
||||
names = list(schemas)
|
||||
return {
|
||||
|
||||
@@ -13,6 +13,9 @@ import sys
|
||||
import sqlite3
|
||||
import threading
|
||||
from typing import Dict
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SD = os.path.dirname(os.path.abspath(__file__))
|
||||
if _SD not in sys.path:
|
||||
@@ -62,7 +65,7 @@ class ConnectionPool:
|
||||
try:
|
||||
conn.close()
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
self.connections.clear()
|
||||
|
||||
def get_status(self) -> Dict:
|
||||
|
||||
@@ -12,6 +12,9 @@ import os
|
||||
import sys
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
@@ -525,7 +528,7 @@ def capture_signatures() -> dict:
|
||||
sig = inspect.signature(func)
|
||||
sigs[f"{contract.module}.{contract.function}"] = str(sig)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return sigs
|
||||
|
||||
|
||||
@@ -536,7 +539,7 @@ def save_signatures(sigs: dict):
|
||||
with open(SIGNATURES_FILE, "w") as f:
|
||||
json.dump(sigs, f, indent=2)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
|
||||
def load_signatures() -> dict:
|
||||
@@ -546,7 +549,7 @@ def load_signatures() -> dict:
|
||||
with open(SIGNATURES_FILE) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
#!/usr/bin/env python3
|
||||
"""Drift ledger — drift protection for long projects.
|
||||
|
||||
@@ -247,7 +250,7 @@ def check(strict=False, autofix=True):
|
||||
try:
|
||||
surface_alert(msg, a["label"], a.get("changed", ""))
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
# AUTOMATIC FIX on detection (user directive): the system fixes
|
||||
# drift itself, not just flags it.
|
||||
if autofix:
|
||||
|
||||
+4
-1
@@ -18,6 +18,9 @@ import uuid
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, List, Dict, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -63,7 +66,7 @@ class GoalManager:
|
||||
for goal_id, goal_data in data.items():
|
||||
self.goals[goal_id] = Goal(**goal_data)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
def _save(self):
|
||||
"""Save goals to storage."""
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
#!/usr/bin/env python3
|
||||
"""growth_delta.py — turn reflections into ACTIONABLE persona growth.
|
||||
|
||||
@@ -102,7 +105,7 @@ def _parse_json(text):
|
||||
try:
|
||||
return json.loads(t[s : e + 1])
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
print(
|
||||
f"[growth_delta] WARNING: failed to parse LLM response as JSON (first 120 chars): {text[:120]!r}",
|
||||
file=sys.stderr,
|
||||
|
||||
@@ -9,6 +9,8 @@ import threading
|
||||
import urllib.request
|
||||
from typing import Callable, Protocol, Sequence, TypedDict, cast
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from .tool_executor import (
|
||||
TOOLS,
|
||||
PreparedTool,
|
||||
@@ -164,7 +166,7 @@ class OllamaChatTransport:
|
||||
|
||||
register_window(model, window)
|
||||
except Exception: # pragma: no cover - broker import is guaranteed
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return window
|
||||
|
||||
def supports_tools(self, model: str) -> bool:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
#!/usr/bin/env python3
|
||||
"""memory_consolidation.py — Episodic-to-semantic memory consolidation.
|
||||
|
||||
@@ -235,7 +238,7 @@ class MemoryConsolidation:
|
||||
entry_id = row[0]
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
# Normalize triggers — may be string or list
|
||||
if isinstance(triggers, str):
|
||||
@@ -270,7 +273,7 @@ class MemoryConsolidation:
|
||||
entry_id = row[0]
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
knowledge = ConsolidatedKnowledge(
|
||||
id=f"graph-{name}",
|
||||
@@ -319,7 +322,7 @@ class MemoryConsolidation:
|
||||
f"SELECT id FROM entries WHERE {conditions} AND status = 'active' LIMIT 1", params
|
||||
).fetchone()
|
||||
except (json.JSONDecodeError, Exception):
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
# Strategy 3: FTS5 search with key terms
|
||||
if not row:
|
||||
@@ -333,7 +336,7 @@ class MemoryConsolidation:
|
||||
if fts_row:
|
||||
row = fts_row
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
# Strategy 4: substring match (least precise)
|
||||
if not row:
|
||||
@@ -345,7 +348,7 @@ class MemoryConsolidation:
|
||||
entry_id = row[0]
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
knowledge = ConsolidatedKnowledge(
|
||||
id=f"fact-{entry_id}" if entry_id else f"fact-{hash(fact) % 10000}",
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
#!/usr/bin/env python3
|
||||
"""memory_store.py — the hybrid memory store (core of the rebuild).
|
||||
|
||||
@@ -2394,7 +2397,7 @@ def main():
|
||||
db.execute("UPDATE entries SET memory_type=? WHERE id=?", (args.memory_type, res["id"]))
|
||||
db.commit()
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
|
||||
@@ -23,6 +23,9 @@ import sys
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from dataclasses import dataclass, field, asdict
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from typing import Optional
|
||||
|
||||
PROFILES_DIR = os.path.expanduser("~/.rune/model_profiles")
|
||||
@@ -194,7 +197,7 @@ def load_profile(model_name: str) -> Optional[ModelProfile]:
|
||||
with open(path) as f:
|
||||
return ModelProfile.from_dict(json.load(f))
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
@@ -206,7 +209,7 @@ def save_profile(profile: ModelProfile):
|
||||
with open(path, "w") as f:
|
||||
json.dump(profile.to_dict(), f, indent=2)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
|
||||
def detect_family(model_name: str) -> str:
|
||||
|
||||
@@ -23,6 +23,9 @@ import json
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
import threading
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Dict, List, Optional, Any
|
||||
@@ -83,7 +86,7 @@ class NightMode:
|
||||
filtered = {k: v for k, v in data.items() if k in valid_keys}
|
||||
self.state = NightModeState(**filtered)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
def _save(self):
|
||||
"""Save state to storage (atomic write)."""
|
||||
|
||||
@@ -23,6 +23,9 @@ import os
|
||||
import difflib
|
||||
import re
|
||||
import sqlite3
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -169,7 +172,7 @@ def get_defining_principles():
|
||||
if principles:
|
||||
return "\n".join(principles[:20]) # Top 20 lines
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from dataclasses import dataclass, fields
|
||||
from typing import Dict, Optional, Any
|
||||
from datetime import datetime, timezone
|
||||
@@ -114,7 +117,7 @@ class ProviderBudgetTracker:
|
||||
filtered = {k: v for k, v in provider_data.items() if k in valid_keys}
|
||||
self.providers[name] = ProviderBudget(**filtered)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
def _save(self):
|
||||
"""Save providers to storage."""
|
||||
@@ -154,7 +157,7 @@ class ProviderBudgetTracker:
|
||||
enabled=True,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
# Only save if we actually initialized providers
|
||||
if self.providers:
|
||||
|
||||
@@ -371,19 +371,24 @@ def os_readlink(p: Path) -> Path:
|
||||
|
||||
def auto_initial_note(*, version: str, summary: str, scaffold_store: str,
|
||||
release_sha: str, deployed_sha: str = "",
|
||||
provenance: list[str] | None = None) -> ReleaseNote:
|
||||
"""RELEASE INITIALIZATION: build evidence-derived notes with NO
|
||||
hand-written extra prose (§7). Facts come deterministically from the
|
||||
provenance: list[str] | None = None,
|
||||
extra: dict | None = None) -> ReleaseNote:
|
||||
"""RELEASE INITIALIZATION: build evidence-derived notes from the
|
||||
governed scaffold store — contracts closed, maintenance resolved/open,
|
||||
quality records. Routine releases require nothing from the builder."""
|
||||
quality records. `extra` (EXIT-NOTE TEMPLATE, Nick 2026-10-08) carries
|
||||
the plain-English narrative of what was worked on; merged per key
|
||||
AFTER fact extraction, deduped, sorted."""
|
||||
try:
|
||||
doc = json.loads(Path(scaffold_store).read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
doc = {"entities": [], "relationships": [], "observations": []}
|
||||
facts = extract_evidence_facts(doc)
|
||||
merged = {}
|
||||
for k in ("improved", "added", "fixed"):
|
||||
merged[k] = sorted(set(facts.get(k, [])) | set((extra or {}).get(k, [])))
|
||||
return ReleaseNote(
|
||||
version=version, summary=summary,
|
||||
improved=[], added=[], fixed=[],
|
||||
improved=merged["improved"], added=merged["added"], fixed=merged["fixed"],
|
||||
proven=facts["proven"], limitations=facts["limitations"],
|
||||
references=facts["references"],
|
||||
release_sha=release_sha, deployed_sha=deployed_sha,
|
||||
|
||||
@@ -23,6 +23,9 @@ import uuid
|
||||
import urllib.request
|
||||
from datetime import datetime, timezone
|
||||
from dataclasses import asdict, dataclass, field
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Dict, List, Optional, Any
|
||||
|
||||
@@ -1334,7 +1337,10 @@ class RuneOperator:
|
||||
task.completed_at = datetime.now(timezone.utc).isoformat()
|
||||
self._task_state.transition(task.task_id, "failed")
|
||||
self._save()
|
||||
self._emit_task_outcome(task, channel=channel)
|
||||
try:
|
||||
self._emit_task_outcome(task, channel=channel)
|
||||
except Exception: # noqa: BLE001 — contract (tests/test_task_lifecycle): journal failure never masks the operator path; overrides may raise
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
# P0-04 (#20): first endogenous meaningful-event trigger — a task
|
||||
# failure is worth hearing about (#48 grows the trigger taxonomy
|
||||
# on top of this seed). The typed event already exists in the
|
||||
@@ -1383,7 +1389,10 @@ class RuneOperator:
|
||||
self.state.current_task = None
|
||||
self._task_state.transition(task.task_id, "failed", outcome_ref=outcome_ref)
|
||||
self._save()
|
||||
self._emit_task_outcome(task, channel=channel)
|
||||
try:
|
||||
self._emit_task_outcome(task, channel=channel)
|
||||
except Exception: # noqa: BLE001 — contract (tests/test_task_lifecycle): journal failure never masks the operator path; overrides may raise
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return self._response_envelope(
|
||||
OperatorResponse(
|
||||
success=False,
|
||||
@@ -1423,7 +1432,7 @@ class RuneOperator:
|
||||
"completed_at": task.completed_at or "",
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001, S110 — contract: journal failure never masks the operator path (degrade-not-crash)
|
||||
pass
|
||||
|
||||
def _emit_task_failed(self, task: RuneTask, channel: str = "") -> None:
|
||||
@@ -1443,7 +1452,7 @@ class RuneOperator:
|
||||
"error": (task.result or "")[:300],
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001, S110 — contract: journal failure never masks the operator path (degrade-not-crash)
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
@@ -2569,7 +2578,15 @@ def _wire_auto_filer() -> None:
|
||||
from .event_bus import _store_path
|
||||
from .failure_auto_filer import AUTOFILE_OFF_ENV, FailureAutoFiler, JsonlSpool
|
||||
from .forge_client import create_forge_client
|
||||
except ImportError: # TUI top-level deploy (script mode)
|
||||
except ImportError: # script mode (no package parent)
|
||||
# Script mode: bare `python3 rune_operator.py` has no package
|
||||
# parent AND the script dir may not be on sys.path when invoked
|
||||
# from elsewhere. Add it, then use the flat imports.
|
||||
import sys as _sys
|
||||
|
||||
_here = os.path.dirname(os.path.abspath(__file__))
|
||||
if _here not in _sys.path:
|
||||
_sys.path.insert(0, _here)
|
||||
from event_bus import _store_path # type: ignore[no-redef]
|
||||
from failure_auto_filer import AUTOFILE_OFF_ENV, FailureAutoFiler, JsonlSpool # type: ignore[no-redef]
|
||||
from forge_client import create_forge_client # type: ignore[no-redef]
|
||||
|
||||
+37
-37
@@ -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.129.2
|
||||
Version: v0.136.3
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -138,25 +138,25 @@ def collect_post_response_signals(response: str, messages_count: int) -> list[st
|
||||
usage_percent = max(ratios, default=0) * 100
|
||||
if usage_percent > 80:
|
||||
signals.append(f"⚠ Budget: {usage_percent:.0f}% used")
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
try:
|
||||
claims_result = claim_audit.scan_text(response[:500])
|
||||
if claims_result and claims_result.get("degraded"):
|
||||
signals.append(f"⚠ Claims degraded: {len(claims_result['degraded'])}")
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
try:
|
||||
growth = growth_delta.check()
|
||||
if growth and growth.get("delta"):
|
||||
signals.append(f"📈 Growth: {growth['delta']}")
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
try:
|
||||
if messages_count % 10 == 0:
|
||||
self_improve.run()
|
||||
signals.append("🔄 Self-improvement cycle triggered")
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
return signals
|
||||
|
||||
@@ -201,7 +201,7 @@ def _log_teachable(error_type: str, context: str, fix: str, file: str = "", line
|
||||
os.makedirs(os.path.dirname(TEACHABLE_FILE), exist_ok=True)
|
||||
with open(TEACHABLE_FILE, "w") as f:
|
||||
json.dump(moments, f, indent=2)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ def _get_teachable_summary() -> str:
|
||||
types[t] = types.get(t, 0) + 1
|
||||
parts = [f"{t}: {c}" for t, c in sorted(types.items(), key=lambda x: -x[1])[:5]]
|
||||
return f" ({len(moments)} moments: {', '.join(parts)})"
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
return ""
|
||||
|
||||
|
||||
@@ -351,7 +351,7 @@ class ThinkingIndicator(Static):
|
||||
if os.path.exists(path):
|
||||
frames.append(Image.open(path).convert("RGBA"))
|
||||
return frames if frames else None
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
return None
|
||||
|
||||
def on_mount(self) -> None:
|
||||
@@ -399,7 +399,7 @@ def _load_history() -> dict:
|
||||
if os.path.exists(HISTORY_FILE):
|
||||
with open(HISTORY_FILE, "r") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
return {}
|
||||
|
||||
@@ -410,7 +410,7 @@ def _save_history(history: dict):
|
||||
os.makedirs(os.path.dirname(HISTORY_FILE), exist_ok=True)
|
||||
with open(HISTORY_FILE, "w") as f:
|
||||
json.dump(history, f)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
|
||||
@@ -830,7 +830,7 @@ class ModelSelector(Static):
|
||||
def _get_current_model(self) -> str:
|
||||
try:
|
||||
return self.app.model
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
return ""
|
||||
|
||||
def _rebuild_list(self):
|
||||
@@ -911,7 +911,7 @@ class ModelSelector(Static):
|
||||
try:
|
||||
if 0 <= self.selected_index < len(self._model_items):
|
||||
self._model_items[self.selected_index].scroll_visible(animate=False)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
def _toggle_category(self, category: str):
|
||||
@@ -966,7 +966,7 @@ def _load_model_state() -> str:
|
||||
with open(state_path) as f:
|
||||
data = json.load(f)
|
||||
return data.get("model", "local")
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
_log.debug("Failed to load model state", exc_info=True)
|
||||
return "local"
|
||||
|
||||
@@ -978,7 +978,7 @@ def _save_model_state(model: str):
|
||||
os.makedirs(os.path.dirname(state_path), exist_ok=True)
|
||||
with open(state_path, "w") as f:
|
||||
json.dump({"model": model, "timestamp": datetime.now(timezone.utc).isoformat()}, f)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
_log.debug("Failed to save model state", exc_info=True)
|
||||
|
||||
|
||||
@@ -1116,7 +1116,7 @@ class StatusBar(Static):
|
||||
app = self.app
|
||||
if hasattr(app, "_focused_channel"):
|
||||
return app._focused_channel
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
return "💬 chat"
|
||||
|
||||
@@ -1128,7 +1128,7 @@ class StatusBar(Static):
|
||||
|
||||
providers = provider_registry.list_providers()
|
||||
return len(providers) if providers else 0
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
return 0
|
||||
|
||||
@@ -1227,7 +1227,7 @@ class ChannelView(Vertical):
|
||||
app._focused_channel = "💬 chat"
|
||||
if hasattr(app, "status_bar") and app.status_bar:
|
||||
app.status_bar.refresh()
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
def add_message(self, role: str, content: str, metadata: dict = None):
|
||||
@@ -1693,7 +1693,7 @@ class RuneApp(App):
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
urllib.request.urlopen(req, timeout=60)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
import threading
|
||||
@@ -1948,7 +1948,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.129.2 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.136.3 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
@@ -1957,7 +1957,7 @@ class RuneApp(App):
|
||||
time_data = time_awareness.get_time_context()
|
||||
if time_data and time_data.get("period") in ("evening", "night"):
|
||||
self.chat_channel.add_message("system", f"[dim]🌙 Night mode available (/night)[/dim]")
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
# Auto-show work status if batches exist
|
||||
@@ -1974,7 +1974,7 @@ class RuneApp(App):
|
||||
"system",
|
||||
f"[dim]📋 {pending} pending work batches (/work)[/dim]",
|
||||
)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
# Auto-show resumable builds if they exist
|
||||
@@ -1987,7 +1987,7 @@ class RuneApp(App):
|
||||
"system",
|
||||
f"[dim]🔨 {len(builds)} builds available (/build)[/dim]",
|
||||
)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
def _detect_planning_threshold(self, text: str) -> bool:
|
||||
@@ -2188,7 +2188,7 @@ class RuneApp(App):
|
||||
f"[dim] Use /queue list to see pending, /queue approve to execute[/dim]",
|
||||
)
|
||||
return # Don't send to model, just queue
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
# Task mode: auto-detect if build mode is needed (T21.1 21.1E:
|
||||
@@ -2212,7 +2212,7 @@ class RuneApp(App):
|
||||
f"[yellow] Use /build start <description> to begin build pipeline[/yellow]",
|
||||
)
|
||||
# Don't auto-switch, just suggest
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
# Handle commands
|
||||
@@ -2283,7 +2283,7 @@ class RuneApp(App):
|
||||
question="Nick mentioned this idea during task work",
|
||||
outcome="Captured for later exploration",
|
||||
)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
def _open_command_selector(self):
|
||||
@@ -2413,7 +2413,7 @@ class RuneApp(App):
|
||||
"system",
|
||||
f"[dim]✅ Executed {len(approved)} queued messages[/dim]",
|
||||
)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
# Build mode: show build status or start new build
|
||||
@@ -2432,7 +2432,7 @@ class RuneApp(App):
|
||||
m = BuildManager(build_id)
|
||||
if m.can_resume():
|
||||
resumable.append(build_id)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
if resumable:
|
||||
self._ch().add_message(
|
||||
@@ -2815,7 +2815,7 @@ class RuneApp(App):
|
||||
if len(models) > 10:
|
||||
lines.append(f" ... and {len(models) - 10} more")
|
||||
self._ch().add_message("system", "\n".join(lines))
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
except Exception as e:
|
||||
self._ch().add_message("system", f"[dim]Error connecting: {e}[/dim]")
|
||||
@@ -3277,7 +3277,7 @@ class RuneApp(App):
|
||||
"system",
|
||||
f" {build_id}: {status['description']} ({status['current_phase']})",
|
||||
)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
else:
|
||||
self._ch().add_message("system", "No builds found.")
|
||||
@@ -3558,7 +3558,7 @@ class RuneApp(App):
|
||||
import model_profiles
|
||||
|
||||
model_profiles.get_or_create_profile(model_name)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
else:
|
||||
stderr = proc.stderr.read() if proc.stderr else ""
|
||||
@@ -3766,7 +3766,7 @@ class RuneApp(App):
|
||||
"system",
|
||||
f"[dim]⏰ Fired {len(fired)} on-complete triggers[/dim]",
|
||||
)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
# Auto-execute approved queue messages after task completes
|
||||
@@ -3787,7 +3787,7 @@ class RuneApp(App):
|
||||
"system",
|
||||
f"[dim]✅ Executed {len(approved)} queued messages[/dim]",
|
||||
)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
# T5.13 dim-spam consolidation: previously five
|
||||
@@ -3814,7 +3814,7 @@ class RuneApp(App):
|
||||
try:
|
||||
thinking.stop()
|
||||
thinking.remove()
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
self.status_bar.is_loading = False
|
||||
_handle_worker_error(e, f"model response for {channel}", ch)
|
||||
@@ -3851,7 +3851,7 @@ class RuneApp(App):
|
||||
}
|
||||
pane.add_object(mirror)
|
||||
pane.refresh()
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
def on_tool_call_widget_decision(self, event: ToolCallWidget.Decision) -> None:
|
||||
@@ -3924,7 +3924,7 @@ class RuneApp(App):
|
||||
self.exit()
|
||||
else:
|
||||
self._last_ctrl_c = now
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
def action_focus_task(self) -> None:
|
||||
@@ -3973,7 +3973,7 @@ class RuneApp(App):
|
||||
try:
|
||||
pane = self.query_one("#work-pane", WorkPane)
|
||||
pane.display = self._work_pane_visible_for(self.mode)
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
def action_resize_left(self) -> None:
|
||||
@@ -3993,7 +3993,7 @@ class RuneApp(App):
|
||||
chat = self.query_one("#chat-channel")
|
||||
task.styles.width = f"{self.task_width}%"
|
||||
chat.styles.width = f"{100 - self.task_width}%"
|
||||
except Exception:
|
||||
except Exception: # noqa: BLE001 — deliberate: TUI render-loop must never crash the session (degrade-not-crash)
|
||||
pass
|
||||
|
||||
def _update_status(self) -> None:
|
||||
|
||||
@@ -21,6 +21,9 @@ import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from dataclasses import dataclass, asdict
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CONFIGURATION
|
||||
@@ -296,7 +299,7 @@ class SafetyOverride:
|
||||
with open(STATS_FILE, "w") as f:
|
||||
json.dump(asdict(self.stats), f, indent=2)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
def get_params(self, text: str) -> dict:
|
||||
"""Get override parameters for a request.
|
||||
|
||||
@@ -14,6 +14,9 @@ import os
|
||||
import sys
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
KNOWLEDGE_FILE = os.path.join(SCRIPT_DIR, "knowledge.md")
|
||||
@@ -59,7 +62,7 @@ def scan_arxiv():
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return findings
|
||||
|
||||
|
||||
@@ -96,7 +99,7 @@ def scan_changelogs():
|
||||
}
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return findings
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,9 @@ import re
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
import time
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
@@ -140,7 +143,8 @@ def extract_todos(conn, session_id):
|
||||
"""
|
||||
parts = conn.execute(
|
||||
"""SELECT data FROM part
|
||||
WHERE session_id = ? AND json_extract(data, '$.tool') = 'todowrite'
|
||||
WHERE session_id = ? AND json_valid(data)
|
||||
AND json_extract(data, '$.tool') = 'todowrite'
|
||||
ORDER BY time_created ASC""",
|
||||
(session_id,),
|
||||
).fetchall()
|
||||
@@ -387,7 +391,7 @@ def refine_identity():
|
||||
timeout=15,
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return r, items[:4]
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
#!/usr/bin/env python3
|
||||
"""system_registry.py — infrastructural system file tracking for Rune.
|
||||
|
||||
@@ -84,7 +87,7 @@ def load_settings():
|
||||
settings = json.load(f)
|
||||
defaults.update(settings)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return defaults
|
||||
|
||||
|
||||
@@ -239,7 +242,7 @@ def load_registry():
|
||||
with open(REGISTRY_FILE) as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return {}
|
||||
|
||||
|
||||
|
||||
@@ -21,6 +21,9 @@ import time as _time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Any
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SD = os.path.dirname(os.path.abspath(__file__))
|
||||
if _SD not in sys.path:
|
||||
@@ -76,7 +79,7 @@ class TimeAwareness:
|
||||
if data.get("last_interaction"):
|
||||
self.last_interaction = datetime.fromisoformat(data["last_interaction"])
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
def _save(self):
|
||||
"""Save last interaction time (atomic write)."""
|
||||
|
||||
@@ -23,6 +23,9 @@ import sys
|
||||
import hashlib
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from typing import Dict, List, Any
|
||||
from datetime import datetime, timezone
|
||||
|
||||
@@ -89,7 +92,7 @@ class TokenOptimizer:
|
||||
self.cache[key] = CacheEntry(**entry_data)
|
||||
self.stats = data.get("stats", self.stats)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
def _save(self):
|
||||
"""Save cache to storage (atomic write)."""
|
||||
@@ -138,7 +141,7 @@ class TokenOptimizer:
|
||||
if result and len(result) > 50:
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
# Fallback: minimal constitution
|
||||
return (
|
||||
"You are Rune. A stateful coding assistant — you remember, you learn, "
|
||||
|
||||
@@ -514,7 +514,13 @@ def validate_write_path(path: str) -> tuple[bool, str, Optional[Path]]:
|
||||
|
||||
|
||||
def _backup_file(path: Path) -> Optional[str]:
|
||||
"""Create a backup of a file before writing. Returns backup path."""
|
||||
"""Create a backup of a file before writing. Returns backup path.
|
||||
|
||||
#2481 FIX: writes a JSON SIDECAR (.manifest) beside the .bak recording
|
||||
the original ABSOLUTE path — the old filename mangling was lossy (it
|
||||
consumed the leading slash and underscore-ambiguity made the path
|
||||
unrecoverable), making undo_last a dead safety mechanism. The manifest
|
||||
is the restore authority; legacy manifest-less backups degrade."""
|
||||
if not path.exists():
|
||||
return None
|
||||
try:
|
||||
@@ -525,6 +531,8 @@ def _backup_file(path: Path) -> Optional[str]:
|
||||
import shutil
|
||||
|
||||
shutil.copy2(str(path), backup_path)
|
||||
with open(backup_path + ".manifest", "w", encoding="utf-8") as mf:
|
||||
json.dump({"original": str(path)}, mf)
|
||||
return backup_path
|
||||
except Exception:
|
||||
logger.debug("_backup_file failed", exc_info=True)
|
||||
@@ -532,18 +540,34 @@ def _backup_file(path: Path) -> Optional[str]:
|
||||
|
||||
|
||||
def undo_last():
|
||||
"""Undo the last file write by restoring from backup."""
|
||||
"""Undo the last file write by restoring from backup.
|
||||
|
||||
#2481 FIX: the restore authority is the .manifest sidecar (which
|
||||
carries the original ABSOLUTE path), not the lossy backup filename.
|
||||
Legacy manifest-less backups fall back to the name parse."""
|
||||
try:
|
||||
backups = sorted(Path(UNDO_DIR).glob("*.bak"), key=lambda p: p.stat().st_mtime, reverse=True)
|
||||
if not backups:
|
||||
return None, "No backups to undo"
|
||||
backup = backups[0]
|
||||
# Extract original path from backup name
|
||||
name = backup.stem # e.g., "20260823_011400___mnt_workdrive_rune_src_rune_rune_tui_py.bak"
|
||||
# Restore
|
||||
import shutil
|
||||
|
||||
# Find the original path from the backup name
|
||||
# PRIMARY: the sidecar manifest carries the original absolute path.
|
||||
manifest_path = Path(str(backup) + ".manifest")
|
||||
if manifest_path.exists():
|
||||
try:
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
original = manifest.get("original", "")
|
||||
except (json.JSONDecodeError, OSError):
|
||||
original = ""
|
||||
if original and os.path.exists(original):
|
||||
shutil.copy2(str(backup), original)
|
||||
backup.unlink()
|
||||
manifest_path.unlink()
|
||||
return original, f"Restored {original}"
|
||||
|
||||
# LEGACY fallback: name-parse (pre-fix behavior).
|
||||
name = backup.stem
|
||||
parts = name.split("_", 4)
|
||||
if len(parts) >= 5:
|
||||
original = parts[4].replace("_", "/")
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys as _s
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_VENV = "/tmp/opencode/voice-venv/bin/python3"
|
||||
if _s.executable != _VENV and os.path.exists(_VENV):
|
||||
@@ -109,14 +112,14 @@ def talk():
|
||||
if key == keyboard.Key.ctrl_r:
|
||||
start_recording()
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
def on_release(key):
|
||||
try:
|
||||
if key == keyboard.Key.ctrl_r:
|
||||
stop_recording()
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
print("hold RIGHT-CTRL to talk; release to transcribe+type. Ctrl-C to quit.", file=sys.stderr)
|
||||
with keyboard.Listener(on_press=on_press, on_release=on_release) as listener:
|
||||
|
||||
@@ -23,6 +23,9 @@ import os
|
||||
import sys
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import Optional
|
||||
|
||||
@@ -225,7 +228,7 @@ def load_config() -> WorkConfig:
|
||||
data = json.load(f)
|
||||
return WorkConfig(**{k: v for k, v in data.items() if k in WorkConfig.__dataclass_fields__})
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return WorkConfig()
|
||||
|
||||
|
||||
@@ -236,7 +239,7 @@ def save_config(config: WorkConfig):
|
||||
with open(CONFIG_FILE, "w") as f:
|
||||
json.dump(asdict(config), f, indent=2)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
|
||||
def load_batches() -> list[Batch]:
|
||||
@@ -247,7 +250,7 @@ def load_batches() -> list[Batch]:
|
||||
data = json.load(f)
|
||||
return [Batch(**b) for b in data]
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return []
|
||||
|
||||
|
||||
@@ -258,7 +261,7 @@ def save_batches(batches: list[Batch]):
|
||||
with open(BATCH_FILE, "w") as f:
|
||||
json.dump([asdict(b) for b in batches], f, indent=2)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -454,7 +457,7 @@ def load_meta() -> Optional[TaskMeta]:
|
||||
data = json.load(f)
|
||||
return TaskMeta(**{k: v for k, v in data.items() if k in TaskMeta.__dataclass_fields__})
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
@@ -465,7 +468,7 @@ def save_meta(meta: TaskMeta):
|
||||
with open(META_FILE, "w") as f:
|
||||
json.dump(asdict(meta), f, indent=2)
|
||||
except Exception:
|
||||
pass
|
||||
logger.debug("suppressed exception", exc_info=True)
|
||||
|
||||
|
||||
def update_meta_from_batches():
|
||||
|
||||
@@ -16,6 +16,7 @@ const FLOOR_TOOLS: &[&str] = &[
|
||||
"run_command",
|
||||
];
|
||||
|
||||
#[allow(dead_code)] // pinned: registry surface kept for the tool-router integration
|
||||
const ALL_TOOLS: &[&str] = &[
|
||||
"read_file",
|
||||
"write_file",
|
||||
|
||||
@@ -99,6 +99,8 @@ pub extern "C" fn cs_score_card(arg: *const c_char) -> *mut c_char {
|
||||
}
|
||||
|
||||
/// Free a string returned by any cs_* function.
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
// pinned: #[no_mangle] extern "C" — the Survivor loader ABI requires this exact safe-fn signature; the raw pointer IS the FFI contract
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cs_free_string(ptr: *mut c_char) {
|
||||
if !ptr.is_null() {
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
//! like 0.30000000000000004)
|
||||
//! - indent=2 dump: newline+indent after every item separator, "{}" when
|
||||
//! empty, NO trailing newline (json.dump never writes one)
|
||||
//! Object key order is preserved (Vec of pairs) — matches Python 3.7+ dict
|
||||
//! insertion order, which the classification tie-break law depends on.
|
||||
//!
|
||||
//!
|
||||
//! Object key order is preserved (Vec of pairs) — matches Python 3.7+ dict
|
||||
//! insertion order, which the classification tie-break law depends on.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
|
||||
@@ -1008,6 +1008,7 @@ fn regex_lite_search(pattern: &str, text: &str) -> bool {
|
||||
// gap-matched against the text. A leading ANCHOR on a variant
|
||||
// requires a word boundary immediately before the match; a
|
||||
// trailing ANCHOR requires one immediately after.
|
||||
#[allow(clippy::while_let_loop)] // pinned: search-loop shape mirrors the oracle substring scan
|
||||
fn walk(
|
||||
segments: &[Vec<String>],
|
||||
low: &str,
|
||||
|
||||
@@ -155,9 +155,9 @@ pub extern "C" fn cc_persona_decide(arg: *const c_char) -> *mut c_char {
|
||||
let sems: Vec<f64> = match get(&a, "sem_scores") {
|
||||
Some(pyjson::Json::Arr(x)) => x
|
||||
.iter()
|
||||
.filter_map(|v| match v {
|
||||
pyjson::Json::Num(n) => Some(*n),
|
||||
_ => Some(0.0),
|
||||
.map(|v| match v {
|
||||
pyjson::Json::Num(n) => *n,
|
||||
_ => 0.0,
|
||||
})
|
||||
.collect(),
|
||||
_ => vec![0.0; rules.len()],
|
||||
@@ -505,6 +505,8 @@ pub extern "C" fn cc_consolidation_neuron(arg: *const c_char) -> *mut c_char {
|
||||
envelope(result)
|
||||
}
|
||||
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
// pinned: #[no_mangle] extern "C" — the Survivor loader ABI requires this exact safe-fn signature; the raw pointer IS the FFI contract
|
||||
#[no_mangle]
|
||||
pub extern "C" fn cc_free_string(ptr: *mut c_char) {
|
||||
if !ptr.is_null() {
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
//! like 0.30000000000000004)
|
||||
//! - indent=2 dump: newline+indent after every item separator, "{}" when
|
||||
//! empty, NO trailing newline (json.dump never writes one)
|
||||
//! Object key order is preserved (Vec of pairs) — matches Python 3.7+ dict
|
||||
//! insertion order, which the classification tie-break law depends on.
|
||||
//!
|
||||
//!
|
||||
//! Object key order is preserved (Vec of pairs) — matches Python 3.7+ dict
|
||||
//! insertion order, which the classification tie-break law depends on.
|
||||
|
||||
use std::fmt::Write as _;
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@ pub fn validate_command(
|
||||
match op {
|
||||
None => serde_json::json!({"accepted": false, "reason": "unknown command"}),
|
||||
Some(op) => {
|
||||
#[allow(clippy::match_like_matches_macro)]
|
||||
// pinned: explicit match arms document the classification
|
||||
let valid = match (command, current_phase) {
|
||||
("START", "draft") => true,
|
||||
("APPROVE", "approved") | ("APPROVE", "building") => true,
|
||||
|
||||
@@ -12,6 +12,7 @@ pub struct Depth {
|
||||
pub bypassed: Vec<String>,
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)] // pinned: parity signature — the port mirrors the oracle arg-for-arg
|
||||
pub fn select_depth(
|
||||
mode: &str,
|
||||
objective: &str,
|
||||
@@ -119,7 +120,16 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_local_fix() {
|
||||
let d = select_depth("LOCAL_FIX", "fix typo", None, false, false, false, "", None);
|
||||
let d = select_depth(
|
||||
"LOCAL_FIX",
|
||||
"fix typo",
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
"",
|
||||
None,
|
||||
);
|
||||
assert_eq!(d.level, "LIGHT");
|
||||
}
|
||||
|
||||
@@ -128,7 +138,7 @@ mod tests {
|
||||
let d = select_depth(
|
||||
"PROJECT_BUILD",
|
||||
"build system",
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -143,7 +153,7 @@ mod tests {
|
||||
let d = select_depth(
|
||||
"STANDARD_CHANGE",
|
||||
"add feature",
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
|
||||
@@ -124,6 +124,8 @@ mod tests {
|
||||
// ---- FFI (Survivor membrane) ----
|
||||
|
||||
/// ci_validate({"from","to"}) -> {"legal": bool, "detail": str?}
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)]
|
||||
// pinned: #[no_mangle] extern "C" — the Survivor loader ABI requires this exact safe-fn signature; the raw pointer IS the FFI contract
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ci_validate(arg: *const u8, arg_len: usize) -> *mut u8 {
|
||||
unsafe {
|
||||
@@ -144,6 +146,7 @@ pub extern "C" fn ci_validate(arg: *const u8, arg_len: usize) -> *mut u8 {
|
||||
}
|
||||
|
||||
/// ci_transition({"from_phase","to_phase","contract_id"}) -> envelope
|
||||
#[allow(clippy::not_unsafe_ptr_arg_deref)] // pinned: extern "C" ABI — the Survivor loader requires this signature
|
||||
#[no_mangle]
|
||||
pub extern "C" fn ci_transition(arg: *const u8, arg_len: usize) -> *mut u8 {
|
||||
unsafe {
|
||||
|
||||
@@ -52,13 +52,21 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_pressure_levels() {
|
||||
// fixtures derived from the code's tokenizer (CHARS_PER_TOKEN):
|
||||
// 1000 chars is far under any threshold; a payload sized to cross
|
||||
// the 0.9 critical line proves the ladder's top step.
|
||||
assert_eq!(measure_pressure(&"x".repeat(1000), 10000)["level"], "low");
|
||||
assert_eq!(measure_pressure(&"x".repeat(9000), 10000)["level"], "high");
|
||||
let big = (9.0 * CHARS_PER_TOKEN * 10000.0 / 10.0) as usize; // 90% of max_tokens in tokens
|
||||
assert_eq!(
|
||||
measure_pressure(&"x".repeat(big), 10000)["level"],
|
||||
"critical"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_degradation() {
|
||||
let responses = vec!["long response".repeat(100), "short", "tiny"];
|
||||
let responses: Vec<String> =
|
||||
vec!["long response".repeat(100), "short".into(), "tiny".into()];
|
||||
assert!(detect_degradation(&responses));
|
||||
}
|
||||
}
|
||||
@@ -198,6 +198,7 @@ fn transition(
|
||||
// ── public operations (full-doc convention) ──────────────────────────────────
|
||||
|
||||
/// define — duplicate → Err; returns the new record.
|
||||
#[allow(clippy::too_many_arguments)] // pinned: parity signature — the port mirrors the oracle arg-for-arg
|
||||
pub fn define(
|
||||
doc: &mut Json,
|
||||
behaviour_id: &str,
|
||||
|
||||
Loaded 100 of 127 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user