Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75d6d7d62a | ||
|
|
7f47969128 | ||
|
|
a65e5d44de | ||
|
|
26a040892d | ||
|
|
1497c85cd4 | ||
|
|
a2463ad80b | ||
|
|
6965fc7375 | ||
|
|
0daad967c1 | ||
|
|
502317d8b6 | ||
|
|
07cdc2fc56 | ||
|
|
b4161a4034 |
No files matched your search
@@ -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.**
|
||||
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"tool_executor",
|
||||
"tool_protocol",
|
||||
"local_tool_runtime",
|
||||
"intent_continuity",
|
||||
"openai_compat_transport",
|
||||
"forge_client",
|
||||
"message_queue",
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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"
|
||||
|
||||
|
||||
Executable
+191
@@ -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())
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)."""
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user