Compare commits

...
11 Commits
Author SHA1 Message Date
conductor 75d6d7d62a chore(release): auto-bump version to 0.66.23 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 11:16:07 +01:00
Nick 7f47969128 feat(scaffolding): V1 tranche 1 — projection engine over existing authorities
scripts/scaffold_projection.py derives per-component cards (subsystem,
dependencies, consumers, mirrors, proof obligations + evidence state) from
the umbrella manifest, canary registry, AST import graph (incl. relative
lazy imports), and tests/ filesystem. No new authority: projections only.
docs/SCAFFOLDING.md records the mechanism audit (KEEP/EVOLVE/REPLACE/RETIRE)
and the real-change proof on the context-discovery fix.
2026-09-12 11:15:42 +01:00
Nick a65e5d44de fix(runtime): context discovery routes cloud-pinned models to the provider wire
Module-level context_length used OllamaChatTransport unconditionally, so
every cloud-pinned task logged context_discovery tier=1 HTTPError 400 and
was misbudgeted at the 8192 fallback instead of the provider's real window
(131072 for Verboo Code/glm-5.3-flash). is_cloud_pinned is now THE
provider-prefix rule — one authority in openai_compat_transport, consumed
by context discovery (new) and transport selection (deduped from
rune_operator._transport_for). Red-first regression tests added.
Scaffolding-derived impact: consumer rune_operator; gates suite/lint/
henxels + inference/orchestration canaries.
2026-09-12 11:15:42 +01:00
Nick 26a040892d docs(reconciliation): ADAPT family reconciliation on PL1 held-out evidence
#292/#293/#297 FIXED_VERIFIED (closed with evidence comments — LOCAL_FIX
live-proven by held-out tasks, DESIGN-LITE implemented, task_gate shipped);
#294/#295/#296/#299/#301 DEFERRED_FUTURE (canonical remaining-obligations
list); #300/#315/#316/#284 VALID_OPEN. Resolves the ADAPT-LIVE-complete vs
open-issues tension: the authorized tranche is complete; the backlog never
was its scope. Checker green (72 records). PE-1789102658-0269 SEED→VALIDATED
recorded in the canonical intent state.
2026-09-12 11:04:54 +01:00
conductor 1497c85cd4 chore(release): auto-bump version to 0.66.22 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 10:50:09 +01:00
Nick a2463ad80b fix(operator): approval-resume uses provider-prefix transport selection
_runtime_for_model hardcoded OllamaChatTransport, so the post-approval
follow-up model call hit localhost with the cloud-pinned name → 400
'invalid model name'. The direct turn path was fixed in 0.66.17; the resume
path now delegates to the same _transport_for authority (one selection
rule, two call sites). Red-first regression tests added.
2026-09-12 10:49:42 +01:00
Nick 6965fc7375 fix(transport): chat() returns flat ChatMessage — envelope shape broke runtime tool-call loop
OllamaChatTransport.chat returns the flat message object (the ChatTransport
contract), but the [OI]-compat translation returned the response envelope
{'message': {...}}. LocalToolRuntime normalizes tool_calls at the top level,
found none, and failed 'empty terminal visible text' while the model had
returned a valid read_file call. Observed live: PL1 Task A wire capture
2026-09-12. Contract-parity regression test added (red-first).
2026-09-12 10:49:42 +01:00
Nick 0daad967c1 test: mock cloud_transport_for at source module — CI has no user config
The selection test called the real cloud_transport_for, which reads the
user's mimocode config (absent on the CI runner). Mocking at the consumer
module was a no-op (function-level import re-resolves from source). Mock now
targets the source module. Local green.
2026-09-12 10:00:14 +01:00
conductor 502317d8b6 chore(release): auto-bump version to 0.66.21 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 07:57:49 +01:00
Nick 07cdc2fc56 feat(intent): minimal intent-continuity ledger — deterministic state machine, 7 invariants
PART 6 of the overnight handoff. IntentRecord (7 fields: objective,
parent_intent, active_detour, return_when, return_to, unresolved_obligations,
success_condition) + IntentLedger (JSON persistence, no LLM). Classifications:
BLOCKER | FIX | EXTENSION | SUPERSEDING | ORTHOGONAL | LATER. All 7 invariants
tested: new ideas never replace the active objective; detours preserve parents;
resolving a detour exposes the return point; supersession accounts for open
obligations; child completion never resolves the parent; persist/reload
round-trips; transitions are deterministic. 1309 green.
2026-09-12 07:57:26 +01:00
Nick b4161a4034 test: valid identifier + tempdir storage + journal isolation — CI green on cloud-execution tests
Three fixes: (1) [OI]CompatTransport is an invalid Python identifier —
renamed to the module's actual class name; (2) fixed /tmp storage paths in
selection tests collided with a root-owned /tmp/hot on the CI runner (the
hot store derives from Path(storage).parent) — per-test tempdirs; (3) the
operator's journal/event edges resolve via memory_env whose real memory dir
is root-owned in CI → RUNE_MEMORY_DIR pointed at a per-test tempdir. 1304
green locally.
2026-09-12 06:43:59 +01:00
19 changed files with 726 additions and 60 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.66.20**
**Current release: v0.66.23**
**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
View File
@@ -118,6 +118,7 @@
"tool_executor",
"tool_protocol",
"local_tool_runtime",
"intent_continuity",
"openai_compat_transport",
"forge_client",
"message_queue",
+28
View File
@@ -111,3 +111,31 @@ subsystem): fetches live issue state, compares to
- record lacking a required field
Exit 0 = consistent; exit 1 = drift listed. CI-eligible (read-only).
## PL1 held-out era reconciliation (2026-09-12)
Fresh-session reconciliation of the ADAPT family against deployed 0.66.22
@ 1497c85 + live PL1 held-out evidence (tasks task_2661189c9073,
task_6e4885031355; ledger PE-9207213099-0270). Resolves the recorded tension:
"ADAPT-LIVE complete → return target PL1" (the bounded tranche authorized
2026-09-10 is complete) while numbered ADAPT issues remain open (the larger
backlog was never in the ADAPT-LIVE tranche scope).
| # | state | evidence / basis |
|---|---|---|
| 284 | VALID_OPEN | epic tracker for the remaining sub-items below |
| 292 | FIXED_VERIFIED (closed) | LOCAL_FIX fast path live (rune_operator.py:522,541); live-proven twice by the held-out tasks |
| 293 | FIXED_VERIFIED (closed) | DESIGN-LITE implemented (rune_operator.py:660, cites #293), wired to STANDARD_CHANGE; suite+canary green |
| 294 | DEFERRED_FUTURE | canonical remaining-obligations list ("ADAPT-5 budgets... for later epics") |
| 295 | DEFERRED_FUTURE | canonical remaining-obligations list |
| 296 | DEFERRED_FUTURE | canonical remaining-obligations list |
| 297 | FIXED_VERIFIED (closed) | task_gate implemented (src/rune/task_gate.py) + broker wiring + canary registration |
| 299 | DEFERRED_FUTURE | canonical remaining-obligations list |
| 300 | VALID_OPEN | escalation mechanism exists (_probe_and_escalate); fresh deployed acceptance evidence pending |
| 301 | DEFERRED_FUTURE | canonical remaining-obligations list (FINAL-12) |
| 315 | VALID_OPEN | not started; ADAPT-1 sub-item (no canonical deferral authority) |
| 316 | VALID_OPEN | not started; ADAPT-1 sub-item (no canonical deferral authority) |
Consistency checker: `scripts/gitea_reconciliation_check.py` → exit 0
(72 records, 100 live issues in window) after these updates.
+1 -1
View File
@@ -54,7 +54,7 @@ This detour is complete when:
## RETURN TARGET / CURRENT: PL1 Process Learning — FIRST TRANCHE LIVE (2026-09-11)
PL1-01 structured Experience records: `scripts/pl1_records.py` (task_id, route resolved from route.jsonl, proof level, failure class, candidate status). PL1-02 failure classification in the PL0 vocabulary (research_dispatch, NOT_COMPLETED_NO_MUTATION_EVIDENCE, verification_failure, model_refusal, UNKNOWN). PL1-03 first learning loop: recurring prose-instead-of-mutation family → structural correction (route-driven instruction in the user context block) + replay contract (NOT_COMPLETED gate) — record PE-1789102658-0269 (SEED; held-out = next deployed mutation tasks must complete-with-evidence or fail honestly). PL1-04 promotion discipline: SEED→CANDIDATE→VALIDATED→PROMOTED via process_experience; structural fixes preferred. PL1-05 metrics: first_pass_accepted, mutation_failures, research_dispatches, false_completion_rate (0.0 — structurally prevented), candidates vs evidence-only split.
PL1-01 structured Experience records: `scripts/pl1_records.py` (task_id, route resolved from route.jsonl, proof level, failure class, candidate status). PL1-02 failure classification in the PL0 vocabulary (research_dispatch, NOT_COMPLETED_NO_MUTATION_EVIDENCE, verification_failure, model_refusal, UNKNOWN). PL1-03 first learning loop: recurring prose-instead-of-mutation family → structural correction (route-driven instruction in the user context block) + replay contract (NOT_COMPLETED gate) — record PE-1789102658-0269 (SEED 2026-09-11 → VALIDATED 2026-09-12: two fresh deployed held-out mutation tasks completed-with-evidence — task_2661189c9073 + task_6e4885031355, LOCAL_FIX, Verboo-pinned, mutation + exit-code verification, ledger PE-9207213099-0270; substrate corrections 6965fc7 flat-ChatMessage contract + a2463ad resume transport selection applied between; caveat: corrections developer-applied structurally, not autonomous self-modification; PROMOTED requires repetition/soak). PL1-04 promotion discipline: SEED→CANDIDATE→VALIDATED→PROMOTED via process_experience; structural fixes preferred. PL1-05 metrics: first_pass_accepted, mutation_failures, research_dispatches, false_completion_rate (0.0 — structurally prevented), candidates vs evidence-only split.
Remaining ADAPT obligations for later epics: ADAPT-5 budgets, ADAPT-6 evidence capsule, ADAPT-7 DESIGN consolidation, ADAPT-10 legibility, FINAL-12 cross-track benchmark, D7 release-job-on-Pi.
+70
View File
@@ -0,0 +1,70 @@
# Scaffolding V1 — Tranche 1
_2026-09-12. Scaffolding is the controlled need-to-capability system: NEED →
DISCOVERY → GAP → REUSE/ACQUIRE/BUILD → IMPACT+PLAN → SAFE IMPLEMENTATION →
VERIFICATION → PROVISIONAL USE → TRUST/REVISE/RETIRE → REUSABLE CAPABILITY.
Tranche 1 is deliberately narrower: make Rune stop rediscovering how Rune is
built every time Rune changes._
## Tranche 1 contract
`scripts/scaffold_projection.py` — a PROJECTION over existing authorities,
never a new one:
| question | authority projected |
|---|---|
| what subsystem owns X? | `config/umbrellas.json` (umbrella manifest + henxel-enforced completeness) |
| what does X depend on? | AST import graph over `src/rune/*.py` (incl. relative/lazy imports) |
| who consumes X? | reverse import edges |
| what changes if X changes? | mirrors + `depends_on` umbrellas + consumers |
| what proof obligations arise? | unit-test file, canary registry, umbrella gates, suite/lint/henxels |
| which are satisfied? | filesystem + registry evidence (static); suite/ratchet/henxels are runtime-check commands |
Commands: `component <module>` · `impact <module>` · `obligations <module>` ·
`all [outfile]`. If the projection disagrees with an authority, fix the
authority — never the projection.
## Mechanism audit (2026-09-12, deployed 0.66.22)
| mechanism | verdict | basis |
|---|---|---|
| Umbrella manifest + impact map + propagation gate | KEEP | ownership/impact authority; henxel-enforced completeness; consumed by projection |
| Henxels structural contract | KEEP | cross-cutting obligations (silent-handler ratchet, registries, coverage debt) |
| Canary registry (`canary_native.MODULES` + per-track canaries) | KEEP | per-subsystem proof obligations; consumed by projection |
| CI/deploy contract (required checks, TUI startup canary, atomic promote) | KEEP | deployment proof chain live-proven repeatedly |
| Version surfaces + auto-version + semver | KEEP | bump fast lane + push convergence working (0.66.22 earned this session) |
| Lint/pyright/coverage ratchets | KEEP | shrinking-baseline gates, all green |
| Authority map + dead-subnet ledger (D6) | EVOLVE | static doc today; Scaffolding projections make it derivable; D6 retire-disposition still owed |
| Issue reconciliation artifact + checker | KEEP | tracker truth with drift detection |
| PL1 experience ledger | KEEP | the trust/revise/retire input for future capability crystallisation |
| Intent continuity ledger | KEEP | PART 6 state machine, 7 invariants |
| `MIMO_AUTOMATIC_RESUME` / W/R interactive injection | KEEP (bounded) | headless single-flight proven; interactive auto-resume NOT YET SAFE until a supported busy signal exists |
| Duplicate authorities risk | REPLACE (avoided) | Scaffolding v1 adds NO new registry — projections only |
## Real-change proof (this tranche)
Change: context discovery hit Ollama's `/api/show` with cloud-pinned model
names — `400 invalid model name` noise on every cloud task + 8192 misbudget
for a 131072-window model (observed live in every PL1 Task A/B run).
1. BEFORE mutation: `scaffold_projection.py impact local_tool_runtime`
derived — subsystem `capability`; consumers `rune_operator`; gates:
full suite, lint ratchet, henxels, inference+orchestration canaries;
unit-test obligation `tests/test_local_tool_runtime.py` (exists).
2. Change made: `is_cloud_pinned` — THE provider-prefix rule, single
authority in `openai_compat_transport.py`; consumed by context discovery
(new) and transport selection (deduped); red-first regression tests.
3. Obligations tracked: suite 1314 green · lint ratchet PASS · henxels exit 0
· the projection itself caught two gaps mid-flight (relative imports
hidden consumers; a broad except biting the lint ratchet) — both fixed at
the source the projection pointed to.
4. Deploy: normal release chain → deployed identity verified.
## Maturity (honest)
- Projection engine: **UNIT-PROVEN** + proven on one real change.
- Not yet: CI integration of `scaffold_projection.py all` (drift detection),
capability discovery / gap recognition / reuse-before-build / external
acquisition / provisional-trust lifecycle — FOUNDATIONAL ONLY (architecture
does not block them; the projection source-list is the extension surface).
- Deferred with reasons recorded: none blocked by Tranche 1.
+35 -35
View File
@@ -34,28 +34,28 @@
"number": 316,
"title": "ADAPT-1 B3-v2 (RESEARCH-LIKE): Qdrant associative-memory projection spike (executes #162)",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"reconciliation_state": "VALID_OPEN",
"labels": [
"governance",
"memory",
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "B3-v2 Qdrant associative-memory projection spike \u2014 not started; ADAPT-1 sub-item under epic #284"
},
{
"number": 315,
"title": "ADAPT-1 B2-v2 (STANDARD-LIKE): wiring-status/architecture-truth report generator (implements #158)",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"reconciliation_state": "VALID_OPEN",
"labels": [
"infrastructure",
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "B2-v2 wiring-status/architecture-truth report generator \u2014 not started; ADAPT-1 sub-item under epic #284"
},
{
"number": 311,
@@ -80,8 +80,8 @@
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "cross-track benchmark + adaptive provisional ratification \u2014 canonical remaining-obligations list"
},
{
"number": 300,
@@ -92,90 +92,90 @@
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "escalation/downgrade honored at acceptance on the deployed runtime \u2014 probe_and_escalate exists; fresh live acceptance evidence pending"
},
{
"number": 299,
"title": "ADAPT-10: Harness/context legibility audit",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"feature",
"governance"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "harness/context legibility audit \u2014 canonical remaining-obligations list"
},
{
"number": 297,
"title": "ADAPT-8: Deterministic-first QUALIFY",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"tracker_state": "closed",
"reconciliation_state": "FIXED_VERIFIED",
"labels": [
"memory"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"last_revalidated": "2026-09-12",
"next_action": ""
},
{
"number": 296,
"title": "ADAPT-7: DESIGN consolidation experiment (APPLICABILITY + INNOVATION)",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"memory"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "DESIGN consolidation experiment (APPLICABILITY+INNOVATION) \u2014 canonical remaining-obligations list"
},
{
"number": 295,
"title": "ADAPT-6: Evidence capsule / context minimization",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "evidence capsule / context minimization \u2014 canonical remaining-obligations list"
},
{
"number": 294,
"title": "ADAPT-5: Bound RESEARCHED_CHANGE work (budgets + stop conditions)",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"feature"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "budgets + stop conditions for RESEARCHED_CHANGE \u2014 canonical 'remaining ADAPT obligations for later epics'"
},
{
"number": 293,
"title": "ADAPT-4: STANDARD_CHANGE path (DESIGN-LITE)",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"tracker_state": "closed",
"reconciliation_state": "FIXED_VERIFIED",
"labels": [
"feature"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"last_revalidated": "2026-09-12",
"next_action": ""
},
{
"number": 292,
"title": "ADAPT-3: LOCAL_FIX fast path",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"tracker_state": "closed",
"reconciliation_state": "FIXED_VERIFIED",
"labels": [
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"last_revalidated": "2026-09-12",
"next_action": ""
},
{
@@ -190,8 +190,8 @@
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "epic tracker for remaining ADAPT sub-items (#294 #295 #296 #299 #300 #301 #315 #316)"
},
{
"number": 259,
@@ -1152,5 +1152,5 @@
}
],
"last_reconciled": "2026-09-12",
"reconciled_by": "W/R-v0.1 overnight (ADAPT-only pass)"
}
"reconciled_by": "PL1 held-out era reconciliation (fresh session 2026-09-12)"
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.66.20"
version = "0.66.23"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+191
View File
@@ -0,0 +1,191 @@
#!/usr/bin/env python3
"""scaffold_projection.py — Scaffolding V1 Tranche 1 (projection, not authority).
Answers, per component/module:
what subsystem owns X? → config/umbrellas.json (existing authority)
what does X depend on? → AST import graph (actual) + umbrella depends_on
who consumes X? → reverse import edges (actual)
what changes if X changes? → mirrors + dependent umbrellas + import consumers
what proof obligations arise? → unit test, canary, henxel coverage, umbrella gates
which obligations are complete? → evidence check (test file exists, canary registered)
PROJECTION DISCIPLINE: every field is DERIVED from an existing authority
(umbrella manifest, canary_native registry, filesystem, AST). This script
introduces no new state — if it disagrees with the authorities, fix the
authority, never this projection.
Commands:
component <module> full card for one src/rune module
impact <module> expected impact + obligations BEFORE a change
obligations <module> obligation checklist with satisfied/pending state
all [outfile] write the full projection JSON (default docs/)
"""
from __future__ import annotations
import ast, json, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "src" / "rune"
TESTS = ROOT / "tests"
UMBRELLAS = ROOT / "config" / "umbrellas.json"
def load_umbrellas() -> dict:
return json.loads(UMBRELLAS.read_text())["umbrellas"]
def module_imports(module: str) -> set[str]:
"""Module names imported by src/rune/<module>.py (intra-package).
Includes relative imports (`from .x import y` — the dual-mode lazy
pattern used across the runtime; missing these would hide real
consumers, observed first run: rune_operator ↔ local_tool_runtime)."""
path = SRC / f"{module}.py"
if not path.is_file():
return set()
tree = ast.parse(path.read_text())
names: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for a in node.names:
names.add(a.name.split(".")[0])
elif isinstance(node, ast.ImportFrom) and node.module:
names.add(node.module.split(".")[0])
return names
def import_graph() -> dict[str, set[str]]:
return {p.stem: module_imports(p.stem) for p in SRC.glob("*.py")}
def consumers_of(module: str, graph: dict[str, set[str]]) -> set[str]:
return {m for m, deps in graph.items() if module in deps and m != module}
def umbrella_for(module: str, umbrellas: dict) -> str:
for name, u in umbrellas.items():
if module in u.get("modules", []):
return name
return ""
def dependent_umbrellas(umbrella: str, umbrellas: dict) -> list[str]:
return [name for name, u in umbrellas.items()
if umbrella in u.get("depends_on", [])]
def mirrors_for(module: str, umbrellas: dict) -> list[str]:
for u in umbrellas.values():
if module in u.get("modules", []):
return list(u.get("mirrors", []))
return []
def canary_registered(module: str) -> bool:
try:
sys.path.insert(0, str(SRC))
from rune import canary_native
return module in canary_native.MODULES
except ImportError:
# canary_native pulls optional deps (sqlite_vec etc.); the projection
# degrades to "not registered" rather than failing the audit.
return False
def obligations(module: str, graph: dict[str, set[str]], umbrellas: dict) -> list[dict]:
"""Proof obligations for changing this module, with evidence state."""
umb = umbrella_for(module, umbrellas)
test_file = TESTS / f"test_{module}.py"
obs = [
{"obligation": "unit_test", "satisfied": test_file.is_file(),
"evidence": f"tests/test_{module}.py" + ("" if test_file.is_file() else " (MISSING)")},
{"obligation": "canary", "satisfied": canary_registered(module),
"evidence": "canary_native.MODULES" if canary_registered(module)
else "not in canary registry (informational — not every module needs one)"},
{"obligation": "umbrella_manifest", "satisfied": bool(umb),
"evidence": f"umbrella '{umb}'" if umb else "NOT in any umbrella — manifest gap"},
{"obligation": "full_suite", "satisfied": None,
"evidence": "run: pytest tests/ -q (runtime evidence, not static)"},
{"obligation": "lint_ratchet", "satisfied": None,
"evidence": "run: scripts/lint_ratchet.py"},
{"obligation": "henxels", "satisfied": None,
"evidence": "run: henxels check --all"},
]
if umb:
deps = dependent_umbrellas(umb, umbrellas)
if deps:
obs.append({"obligation": "dependent_canaries", "satisfied": None,
"evidence": f"umbrellas depending on '{umb}': {', '.join(deps)} — their canaries gate the change"})
mir = mirrors_for(module, umbrellas)
if mir:
obs.append({"obligation": "mirrors_travel", "satisfied": None,
"evidence": f"files that must travel together: {', '.join(mir)}"})
return obs
def component_card(module: str, graph: dict[str, set[str]], umbrellas: dict) -> dict:
deps = {d for d in graph.get(module, set())
if (SRC / f"{d}.py").is_file() and d != module}
return {
"component": module,
"subsystem": umbrella_for(module, umbrellas),
"dependencies": sorted(deps),
"consumers": sorted(consumers_of(module, graph)),
"mirrors": mirrors_for(module, umbrellas),
"obligations": obligations(module, graph, umbrellas),
}
def impact(module: str) -> dict:
umbrellas = load_umbrellas()
graph = import_graph()
card = component_card(module, graph, umbrellas)
umb = card["subsystem"]
card["expected_impact"] = {
"direct_consumers": card["consumers"],
"umbrella_dependents": dependent_umbrellas(umb, umbrellas) if umb else [],
"must_travel": card["mirrors"],
"gates": ["full_suite", "lint_ratchet", "henxels", "umbrella_gates"]
+ (["canary"] if card and any(o["obligation"] == "canary" and o["satisfied"]
for o in card["obligations"]) else []),
}
return card
def main() -> int:
args = sys.argv[1:]
if not args:
print(__doc__)
return 2
cmd = args[0]
if cmd in ("component", "impact", "obligations") and len(args) < 2:
print(f"usage: {cmd} <module>", file=sys.stderr)
return 2
umbrellas = load_umbrellas()
graph = import_graph()
if cmd == "impact":
print(json.dumps(impact(args[1]), indent=1))
elif cmd == "component":
print(json.dumps(component_card(args[1], graph, umbrellas), indent=1))
elif cmd == "obligations":
card = component_card(args[1], graph, umbrellas)
print(json.dumps(card["obligations"], indent=1))
elif cmd == "all":
out = Path(args[1]) if len(args) > 1 else ROOT / "docs" / "scaffold_projection.json"
cards = {p.stem: component_card(p.stem, graph, umbrellas)
for p in sorted(SRC.glob("*.py"))}
out.write_text(json.dumps({
"generated_by": "scripts/scaffold_projection.py (projection — no new authority)",
"sources": ["config/umbrellas.json", "src/rune/canary_native.py",
"AST import graph", "tests/ filesystem"],
"components": cards,
}, indent=1) + "\n")
print(f"projection written: {out} ({len(cards)} components)")
else:
print(f"unknown command: {cmd}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
import json
from pathlib import Path
__version__ = "0.66.20"
__version__ = "0.66.23"
# One build identity (handoff 2026-09-10 §4): the semantic version above is
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
+1 -1
View File
@@ -231,7 +231,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.66.20"' in rune_version,
'__version__ = "0.66.23"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+168
View File
@@ -0,0 +1,168 @@
"""Minimal intent continuity foundation (overnight handoff PART 6).
A deterministic state machine for tracking detached intents — the
smallest primitive that prevents "new good idea = new active objective."
Not an ontology; not Substrate. Test-first, reload-safe, no LLM needed
for state transitions once classification is supplied."""
from __future__ import annotations
import json
import time
from pathlib import Path
CLASSIFICATIONS = ("BLOCKER", "FIX", "EXTENSION", "SUPERSEDING", "ORTHOGONAL", "LATER")
STATUSES = ("ACTIVE", "DETOUR", "PARKED", "RESOLVED", "SUPERSEDED", "DEFERRED")
class IntentRecord:
__slots__ = (
"id",
"objective",
"parent_intent",
"active_detour",
"return_when",
"return_to",
"unresolved_obligations",
"success_condition",
"classification",
"status",
"created_at",
"resolved_at",
)
def __init__(
self,
*,
id,
objective,
classification,
parent_intent="",
active_detour=False,
return_when="",
return_to="",
unresolved_obligations=None,
success_condition="",
status="ACTIVE",
):
self.id = id
self.objective = objective
if classification not in CLASSIFICATIONS:
raise ValueError(f"invalid classification {classification!r}")
self.classification = classification
if status not in STATUSES:
raise ValueError(f"invalid status {status!r}")
self.status = status
self.parent_intent = parent_intent
self.active_detour = active_detour
self.return_when = return_when
self.return_to = return_to
self.unresolved_obligations = unresolved_obligations or []
self.success_condition = success_condition
self.created_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
self.resolved_at = ""
def to_dict(self):
return {s: getattr(self, s) for s in self.__slots__}
@classmethod
def from_dict(cls, d):
rec = cls.__new__(cls)
for s in cls.__slots__:
setattr(rec, s, d.get(s, "" if s != "unresolved_obligations" else []))
return rec
class IntentLedger:
"""Deterministic intent-state store. JSON persistence, no LLM."""
def __init__(self, path):
self.path = Path(path)
self.records = {}
self._load()
def _load(self):
if self.path.is_file():
data = json.loads(self.path.read_text())
for rec in data.get("intents", []):
r = IntentRecord.from_dict(rec)
self.records[r.id] = r
def save(self):
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(json.dumps({"intents": [r.to_dict() for r in self.records.values()]}, indent=1))
def create(
self, *, id, objective, classification, success_condition="", parent_intent="", unresolved_obligations=None
):
rec = IntentRecord(
id=id,
objective=objective,
classification=classification,
success_condition=success_condition,
parent_intent=parent_intent,
unresolved_obligations=unresolved_obligations,
)
self.records[id] = rec
self.save()
return rec
def create_detour(self, *, id, objective, classification, parent_intent, return_when, return_to):
parent = self.records.get(parent_intent)
if not parent:
raise ValueError(f"parent {parent_intent!r} not found")
rec = IntentRecord(
id=id,
objective=objective,
classification=classification,
parent_intent=parent_intent,
active_detour=True,
return_when=return_when,
return_to=return_to,
status="DETOUR",
)
self.records[id] = rec
if parent.status == "ACTIVE":
parent.status = "PARKED"
self.save()
return rec
def resolve(self, intent_id):
"""Invariant 5: resolving a child does NOT resolve its parent."""
rec = self.records.get(intent_id)
if not rec or rec.status not in ("ACTIVE", "DETOUR"):
return None
rec.status = "RESOLVED"
rec.resolved_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
self.save()
return rec
def resolve_detour(self, detour_id):
d = self.records.get(detour_id)
if not d or d.status not in ("DETOUR", "RESOLVED"):
return None
if d.status == "DETOUR":
d.status = "RESOLVED"
d.resolved_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
parent = self.records.get(d.parent_intent)
if parent and parent.status == "PARKED":
parent.status = "ACTIVE"
self.save()
return parent
def supersede(self, old_id, new_id, *, objective, classification, carry_obligations=True):
old = self.records[old_id]
if old.unresolved_obligations and not carry_obligations:
raise ValueError(f"cannot supersede {old_id}: {len(old.unresolved_obligations)} unresolved obligations")
rec = IntentRecord(
id=new_id,
objective=objective,
classification=classification,
parent_intent=old.parent_intent,
unresolved_obligations=list(old.unresolved_obligations) if carry_obligations else [],
status="ACTIVE",
)
old.status = "SUPERSEDED"
self.records[new_id] = rec
self.save()
return rec
+10 -1
View File
@@ -238,6 +238,7 @@ def context_length(model: str, base_url: str = "http://localhost:11434") -> int:
conservative default 8192 + T1 ``record_failure("context_discovery")``
— the assumption is observable, never silent (P0-06).
"""
from rune.openai_compat_transport import is_cloud_pinned
from rune.subsystem_health import record_failure
with _CONTEXT_LENGTH_LOCK:
@@ -245,7 +246,15 @@ def context_length(model: str, base_url: str = "http://localhost:11434") -> int:
if cached:
return cached
try:
window = OllamaChatTransport(base_url=base_url).context_length(model)
if is_cloud_pinned(model):
# Cloud-pinned models must not hit Ollama's /api/show — observed
# live: 400 'invalid model name' + 8192 misbudget for a model
# with a 131072 window. The provider config carries the window.
from rune.openai_compat_transport import cloud_transport_for
window = cloud_transport_for(model).context_length(model)
else:
window = OllamaChatTransport(base_url=base_url).context_length(model)
except Exception as exc:
record_failure("context_discovery", exc, tier=1, context=model)
return CONTEXT_LENGTH_FALLBACK
+19 -3
View File
@@ -108,7 +108,13 @@ class OpenAICompatTransport:
@staticmethod
def _from_openai_response(resp: dict) -> dict:
"""OpenAI response → Ollama-shape (LocalToolRuntime's contract)."""
"""OpenAI response → flat Ollama ChatMessage (LocalToolRuntime's contract).
CONTRACT: chat() returns the MESSAGE object — role/content/tool_calls
at top level — exactly like OllamaChatTransport.chat. Observed live
(PL1 Task A, 2026-09-12): the envelope shape {'message': {...}} made
the runtime see 'empty terminal visible text' while the model had
returned a valid read_file tool call."""
choices = resp.get("choices") or [{}]
message = choices[0].get("message", {}) or {}
tool_calls = []
@@ -124,9 +130,9 @@ class OpenAICompatTransport:
if c.get("id"):
entry["id"] = c["id"]
tool_calls.append(entry)
out: dict = {"message": {"role": "assistant", "content": message.get("content") or ""}}
out: dict = {"role": "assistant", "content": message.get("content") or ""}
if tool_calls:
out["message"]["tool_calls"] = tool_calls
out["tool_calls"] = tool_calls
return out
# -- ChatTransport protocol -------------------------------------------
@@ -168,6 +174,16 @@ class OpenAICompatTransport:
return self._context_window
def is_cloud_pinned(model: str) -> bool:
"""THE provider-prefix rule, single authority: True when the model name
requires the [OI]-compat wire — a non-empty provider prefix that is
neither ollama nor local. Consumers: transport selection
(rune_operator._transport_for) and context discovery
(local_tool_runtime.context_length). Never copy this rule — import it."""
prefix = model.split("/", 1)[0].strip().lower() if "/" in model else ""
return bool(prefix) and prefix not in ("ollama", "local")
def cloud_transport_for(model: str, timeout: float = 120) -> OpenAICompatTransport:
"""Resolve provider config for a '<Provider>/<model>' name from the same
source of truth the mimo CLI uses (mimocode.jsonc + auth.json)."""
+18 -8
View File
@@ -1228,6 +1228,22 @@ class RuneOperator:
except Exception:
pass
@staticmethod
def _transport_for(model: str, timeout: float = 300.0):
"""Provider-prefix transport selection — ONE authority for both the
direct turn and the approval-resume path (T15 substrate contract).
Any model with a non-ollama provider prefix rides the [OI]-compat
wire; ollama-prefixed and bare names keep the Ollama path. Observed
live (PL1 Task A, 2026-09-12): the resume path hardcoded
OllamaChatTransport, so the post-approval follow-up call hit
localhost with a cloud model name → 400 'invalid model name'."""
from rune.openai_compat_transport import cloud_transport_for, is_cloud_pinned
if is_cloud_pinned(model):
return cloud_transport_for(model, timeout=timeout)
return OllamaChatTransport(timeout=timeout)
def _runtime_for_model(self, model: str, task_description: str = "") -> LocalToolRuntime:
"""Build the runtime used to continue a persisted approval turn.
@@ -1250,7 +1266,7 @@ class RuneOperator:
if not tools:
tools = list(tool_executor.TOOLS) # pre-#24 behavior (degrade)
return LocalToolRuntime(
transport=OllamaChatTransport(),
transport=self._transport_for(model),
executor=tool_executor.execute_tool,
model=model,
tools=tools,
@@ -1967,13 +1983,7 @@ I have tools to read, write, edit, search, lint, test, format, and commit code.
# routing_mode condition and 400'd. Deterministic — any model
# with a non-ollama provider prefix rides the OpenAI-compat
# wire; ollama-prefixed and bare names keep the proven path.
_prov_prefix = model.split("/", 1)[0].strip().lower() if "/" in model else ""
if _prov_prefix and _prov_prefix not in ("ollama", "local"):
from rune.openai_compat_transport import cloud_transport_for
transport = cloud_transport_for(model, timeout=timeout)
else:
transport = OllamaChatTransport(timeout=timeout)
transport = self._transport_for(model, timeout=timeout)
runtime = LocalToolRuntime(
transport=transport,
executor=tool_executor.execute_tool,
+2 -2
View File
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
This is not an interface. This is how I see, speak, and interact with the world.
Version: v0.66.20
Version: v0.66.23
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -1856,7 +1856,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.66.20 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.66.23 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
+78
View File
@@ -0,0 +1,78 @@
"""Intent continuity foundation (overnight handoff PART 6): deterministic
state machine, persistence, and all seven invariants."""
import json
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
from rune.intent_continuity import IntentLedger, IntentRecord, CLASSIFICATIONS # noqa: E402
class TestIntentLedger:
def _fresh(self):
return IntentLedger(Path(tempfile.mkdtemp()) / "intent.json")
def test_create_and_reload(self):
"""Invariant 6: state is persistable/reloadable."""
led = self._fresh()
led.create(
id="root",
objective="ship PL1",
classification="BLOCKER",
success_condition="held-out transfer demonstrated",
)
led2 = IntentLedger(led.path)
assert "root" in led2.records
assert led2.records["root"].status == "ACTIVE"
def test_detour_preserves_parent(self):
"""Invariants 1-3."""
led = self._fresh()
led.create(id="root", objective="ship PL1", classification="BLOCKER")
detour = led.create_detour(
id="d",
objective="add scaffolding",
classification="EXTENSION",
parent_intent="root",
return_when="PL1 done",
return_to="root",
)
assert detour.status == "DETOUR"
assert led.records["root"].status == "PARKED"
parent = led.resolve_detour("d")
assert parent.id == "root"
assert parent.status == "ACTIVE"
assert led.records["d"].status == "RESOLVED"
def test_supersession_carries_obligations(self):
"""Invariant 4."""
led = self._fresh()
led.create(id="old", objective="old plan", classification="FIX")
led.records["old"].unresolved_obligations = ["review PR", "update docs"]
new = led.supersede("old", "new", objective="new plan", classification="FIX")
assert "review PR" in new.unresolved_obligations
assert led.records["old"].status == "SUPERSEDED"
def test_child_doesnt_resolve_parent(self):
"""Invariant 5: resolving a child doesn't resolve the parent."""
led = self._fresh()
led.create(id="p", objective="parent", classification="BLOCKER")
led.create_detour(
id="c", objective="child", classification="FIX", parent_intent="p", return_when="done", return_to="p"
)
led.resolve("c")
assert led.records["p"].status == "PARKED" # still parked
led.resolve_detour("c")
assert led.records["p"].status == "ACTIVE"
def test_classification_validated(self):
led = self._fresh()
try:
led.create(id="bad", objective="x", classification="UNICORN")
assert False, "should have raised"
except ValueError:
pass
+45
View File
@@ -584,3 +584,48 @@ class LocalToolRuntimeTests(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class TestCloudModelContextDiscovery:
"""Context discovery must not hit Ollama with cloud-pinned model names.
Observed live (PL1 Task A/B, deployed 0.66.21/0.66.22): every cloud
task logged `subsystem=context_discovery tier=1 HTTPError 400` because
module-level discovery used OllamaChatTransport for a model whose
provider prefix requires the [OI]-compat wire — then misbudgeted the
task at the 8192 fallback instead of the provider's real window."""
def test_cloud_pinned_model_uses_provider_window_not_ollama(self):
import tempfile
from unittest import mock
from rune import local_tool_runtime as ltr
import rune.openai_compat_transport as _oct
ltr.reset_context_length_cache()
class _StubCloud:
def context_length(self, model):
return 131072
with mock.patch.object(ltr, "OllamaChatTransport",
side_effect=AssertionError(
"Ollama must not be consulted for cloud-pinned models")), \
mock.patch.object(_oct, "cloud_transport_for",
return_value=_StubCloud()):
window = ltr.context_length("Verboo Code/glm-5.3-flash")
assert window == 131072, window
def test_local_model_still_uses_ollama_discovery(self):
from unittest import mock
from rune import local_tool_runtime as ltr
ltr.reset_context_length_cache()
class _StubOllama:
def __init__(self, base_url="http://localhost:11434", timeout=120):
self.calls = []
def context_length(self, model):
return 4096
with mock.patch.object(ltr, "OllamaChatTransport", _StubOllama):
window = ltr.context_length("qwen2.5:3b")
assert window == 4096, window
+28 -5
View File
@@ -40,7 +40,7 @@ class TestMessageTranslation:
resp = {"choices": [{"message": {"role": "assistant", "content": None, "tool_calls": [
{"id": "z", "type": "function", "function": {"name": "run_tests", "arguments": "{\"path\": \"t\"}"}}]}}]}
shape = OpenAICompatTransport._from_openai_response(resp)
tc = shape["message"]["tool_calls"][0]
tc = shape["tool_calls"][0]
assert tc["function"]["name"] == "run_tests"
assert tc["function"]["arguments"] == {"path": "t"} # string → dict
@@ -48,13 +48,30 @@ class TestMessageTranslation:
resp = {"choices": [{"message": {"tool_calls": [
{"function": {"name": "x", "arguments": "not-json"}}]}}]}
shape = OpenAICompatTransport._from_openai_response(resp)
assert shape["message"]["tool_calls"][0]["function"]["arguments"] == {"_raw": "not-json"}
assert shape["tool_calls"][0]["function"]["arguments"] == {"_raw": "not-json"}
def test_no_tool_calls_plain_content(self):
resp = {"choices": [{"message": {"content": "hello"}}]}
shape = OpenAICompatTransport._from_openai_response(resp)
assert shape["message"]["content"] == "hello"
assert "tool_calls" not in shape["message"]
assert shape["content"] == "hello"
assert "tool_calls" not in shape
def test_flat_message_contract_parity_with_ollama(self):
"""CONTRACT: chat() returns the flat MESSAGE object (role/content/
tool_calls at top level) — the same shape OllamaChatTransport.chat
returns. Observed live (PL1 Task A, 2026-09-12): returning the
response envelope {'message': {...}} left the runtime's tool-call
normalization reading the top level, finding no tool_calls and no
content, and failing 'empty terminal visible text' while the model
had returned a valid read_file call."""
resp = {"choices": [{"message": {"role": "assistant", "content": "", "tool_calls": [
{"id": "call_1", "type": "function",
"function": {"name": "read_file", "arguments": "{\"path\": \"/x.py\"}"}}]}}]}
msg = OpenAICompatTransport._from_openai_response(resp)
assert msg["role"] == "assistant"
assert msg["tool_calls"][0]["function"]["name"] == "read_file"
assert msg["tool_calls"][0]["id"] == "call_1" # id preserved for pairing
assert "message" not in msg, "envelope shape leaks into the ChatMessage contract"
def test_protocol_methods(self):
t = OpenAICompatTransport("http://x/v1", "k", "Test")
@@ -135,7 +152,13 @@ class TestTransportSelection:
from rune.tool_protocol import OperatorResponse
return OperatorResponse(success=True, user_text="done", model=captured["model"])
with mock.patch.object(rune_operator, "LocalToolRuntime", FakeRuntime):
import rune.openai_compat_transport as _oct
with (
mock.patch.object(rune_operator, "LocalToolRuntime", FakeRuntime),
mock.patch.object(_oct, "cloud_transport_for",
side_effect=lambda model, timeout=120: OpenAICompatTransport(
"https://mock/v1", "test-key", model.split("/", 1)[0])),
):
op.send_task("Fix the bug in greeting.py: returns 'Helo' not 'Hello'. Verify by running the check script.",
model="Verboo Code/glm-5.3-flash")
assert isinstance(captured["transport"], OpenAICompatTransport)
+28 -1
View File
@@ -6,6 +6,7 @@ Observed live: an operator-requested model keeps routing_mode='local' (line
on the model's provider prefix."""
import importlib.util
import sys
import tempfile
from pathlib import Path
from unittest import mock
@@ -14,7 +15,6 @@ sys.path.insert(0, str(ROOT / "src"))
from rune import rune_operator # noqa: E402
from rune.openai_compat_transport import OpenAICompatTransport # noqa: E402
from rune.openai_compat_transport import OICompatTransport # noqa: E402
from rune.local_tool_runtime import OllamaChatTransport # noqa: E402
@@ -67,3 +67,30 @@ def test_bare_model_name_keeps_ollama_transport():
op = rune_operator.RuneOperator(storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
captured = _send(op, "inspect the log for errors", "qwen2.5:3b")
assert isinstance(captured["transport"], OllamaChatTransport)
class TestResumeTransportSelection:
"""Approval-resume path must use the SAME provider-prefix selection as
the direct turn. Observed live (PL1 Task A, 2026-09-12): after an
approval gate, the resumed turn's follow-up model call went to
OllamaChatTransport with the cloud-pinned name → localhost 400
'invalid model name' — the direct path had been fixed in 0.66.17 but
_runtime_for_model (resume) still hardcoded Ollama."""
def test_resume_path_uses_cloud_transport_for_pinned_model(self):
import rune.openai_compat_transport as _oct
op = rune_operator.RuneOperator(
storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
with mock.patch.object(
_oct, "cloud_transport_for",
side_effect=lambda model, timeout=120: OpenAICompatTransport(
"https://mock/v1", "test-key", model.split("/", 1)[0]),
):
rt = op._runtime_for_model("Verboo Code/glm-5.3-flash", "fix the bug")
assert isinstance(rt.transport, OpenAICompatTransport), type(rt.transport)
def test_resume_path_keeps_ollama_for_local_model(self):
op = rune_operator.RuneOperator(
storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
rt = op._runtime_for_model("qwen2.5:3b", "inspect the log")
assert isinstance(rt.transport, OllamaChatTransport), type(rt.transport)