feat(reachability): Live Rune Slice P0 — outcome producer edge + spine defect fixes

Runtime Reachability audit (reports/RUNTIME_REACHABILITY_AUDIT_2026-09-10.md,
4 parallel tracers + live canaries) established: ONE real end-to-end path
exists (send_task → ContextBroker → Ollama tool loop → durable events),
proven live by canary during the audit. The spine exists; the missing
pieces were edges, not architecture.

P0 implemented (choice-free items):

1. task.outcome producer edge: send_task emits a typed journal event on
   EVERY terminal transition (completed AND failed) with task/model/
   verification/route fields — the producer PL0/PL1 needs (outcome_ledger
   was de-wired; experience ledger had zero producers).
2. Experience consumer: scripts/process_experience.py
   record_operator_outcomes() reads the authoritative task-state
   checkpoint (the journal is a one-way integrity journal — payload_hash
   only) and appends idempotent SEED experience records. 132 live records
   created; rerun = 0 (idempotent).
3. Spine defect: _routing_profiles_path and the profile-save site had bare
   relative imports that fail in standalone CLI mode — trace recording
   degraded to tier-2 on every CLI run (mislabeled 'inference_trace').
   Fixed with the established dual-import pattern; verified clean run.
4. Standalone bootstrap: package parent now on sys.path for script
   execution.
5. hook_engine.fire dead call removed (rune_tui called a nonexistent
   function; AttributeError silently swallowed — hooks never ran). Left
   as an honest note, not a false-liveness surface.

Verification: Phase 8 canary through the real spine — all 9 rungs
observable from persisted state (trace v5 fields incl. memory
abstention + broker budget, routing reason, checkpoint outcome, journal
event, idempotent experience record). 1213 tests pass; ruff clean;
ratchet at baseline.

The route-authority placement choice (A/B/C) is presented in the audit
report §G and awaits Nick's decision before wiring adaptive routing into
the operator path.
This commit is contained in:
Nick committed 2026-09-10 17:04:52 +01:00
1 parent fdc9e6566f
commit ec2cba62ee
4 files changed
+552 -358

No files matched your search

@@ -0,0 +1,276 @@
# RUNTIME REACHABILITY AUDIT — Live Rune Slice Tranche
**Date: 2026-09-10 · Master v0.66.0 · Method: code tracing with file:line evidence, 4 parallel tracers + first-hand canaries**
---
## A. The actual current execution path
**One real end-to-end path exists and executes today:**
```text
USER INPUT (TUI chat/task channel, or `rune_operator.py send` CLI)
↓ confirmed rune_tui.py:2115 on_input_submitted → _send_to_model (2223) → rune_operator.py:494 send_task
TASK IDENTITY / STATE
↓ confirmed TaskStateRepository amend-or-create (rune_operator.py:430-433, 502-521)
↓ backed by orchestration/event_store.DurableEventStore + atomic.AtomicStore
ROUTING DECISION
↓ confirmed infer_task_family (rune_operator.py:541) → model_routing.build_model_router().rank()
↓ advisory, cheapest-verified-first over static CAPABILITY_FLOOR (model_routing.py:306-312)
CONTEXT / MEMORY ACQUISITION
→ confirmed ContextBroker.assemble (rune_operator.py:1294; context_broker.py:616)
→ confirmed contributors: persona(static literal) → tools(capability_broker, rune_operator.py:725)
→ memory(memory_selector.select_for_task → memory_store.recall + warm_neuron_store
+ graph_memory + abstraction, rune_operator.py:1126-1190; memory_selector.py:269-496)
→ model_profile → history → user
→ confirmed budget admission against real model window (context_broker.py:616)
SAFETY ESCALATION
→ confirmed safety_override.get_params + tier loop decorates user block (rune_operator.py:1315-1392)
MODEL INVOCATION
→ confirmed LocalToolRuntime(transport=OllamaChatTransport) → POST localhost:11434/api/chat
(rune_operator.py:1400-1431; local_tool_runtime.py:102,178-199)
→ confirmed tool loop: tool_executor.execute_tool = prepare_tool, mutation tools gated
by approval round-trip (tool_executor.py:830-833, 675, 752)
RESPONSE → OUTCOME
→ confirmed refusal escalation tier+1 (rune_operator.py:1452-1461); usage folding (1356-1367)
→ confirmed task transition completed/failed + event_bus emit (rune_operator.py:608-643)
→ confirmed durable events: ~/.rune/events.jsonl (task_state.transition, conversation.message)
```
**First-hand canary (2026-09-10, this audit):** `rune_operator.py send "Reply with exactly: SPINE_ALIVE_20260910"` →
router elected `qwen2.5:3b` → completed ✓ → task state + conversation events persisted. **The spine is alive.**
(One degrade observed: `inference_trace` tier-2 ImportError in standalone CLI mode.)
### Fragmentation points (confirmed)
1. **Cloud routing is telemetry theater.** The router can elect `mimo-v2.5-pro` provider=openai and record
`routing_mode="cloud"` (rune_operator.py:561, 1593) — but `_execute_on_model` hardwires
`OllamaChatTransport` (rune_operator.py:1400). A cloud election sends a nonexistent model to localhost:11434
→ guaranteed failure. `CloudStubAdapter` is explicitly "declared, not live" (model_routing.py:134-155).
2. **`model_connect.generate`, `provider_registry.generate`, `microkernel` dispatch**: imported, never invoked
in the send path. The registries are display furniture; execution is a bare urllib POST in local_tool_runtime.
3. **The research pipeline's orchestrator is the agent session itself.** Nothing in-repo invokes
`StageExecutor` except its own CLI and tests — B3-v3's 12 stage executions were the session driving
`research_stage_executor.py` stage-by-stage (telemetry/attempts.jsonl timestamps confirm).
4. **No console script in packaging** — `python -m rune` works only via manual sys.path patching
(rune_tui.py:28-33; dual `from .X` / `from rune.X` fallbacks throughout).
5. **inference_trace degrades** in standalone mode (relative-import failure, tier-2) — trace persistence is
partial in the CLI path.
---
## B. Reachability table (highest continuously proven rung)
Ladder: IMPLEMENTED → PERSISTED → CONSUMED → AFFECTS LIVE BEHAVIOUR → OBSERVABLE → END-TO-END PROVEN
### LIVE (affects live behaviour, evidenced)
| System | Rung | Evidence |
|---|---|---|
| ContextBroker | CONSUMED→AFFECTS (context assembly every call) | rune_operator.py:997-1294; context_broker.py:616 |
| memory_selector (single memory contributor) | CONSUMED→AFFECTS | rune_operator.py:1126-1190; fail-closed, trace fields |
| memory_store + drift_core rerank | CONSUMED→AFFECTS (retrieval order) | memory_store.py:891-925, 1152 |
| capability_broker | CONSUMED→AFFECTS (tool choice) | rune_operator.py:725; tool_executor.py:1643 |
| LocalToolRuntime + OllamaChatTransport | END-TO-END PROVEN (live canary) | local_tool_runtime.py:102,199 |
| tool_executor (prepare/approve) | END-TO-END PROVEN | tool_executor.py:830-833 |
| TaskState/Conversation repos + event_bus | END-TO-END PROVEN (durable events) | ~/.rune/events.jsonl verified |
| model_routing (advisory rank) | CONSUMED (decision feeds execution) | rune_operator.py:543-560 |
| complexity_scorer | CONSUMED (task-channel advisory) | rune_tui.py:2182 |
| claim_audit | CONSUMED (advisory-only effect — warns, never blocks) | rune_tui.py:3829-3836 |
| pins | PARTIALLY_LIVE (display only, not injected) | rune_tui.py:1754, 2265 |
### IMPLEMENTED_NOT_CONSUMED — the dead cognitive subnet
The entire salience→budget→tier→escalate→verify→outcome ladder is a **closed dead subnet**: each module's only
consumers are the other dead modules plus canary import checks.
| System | Status | Note |
|---|---|---|
| salience_gate | IMPLEMENTED_NOT_CONSUMED | role absorbed by elastic_cognition (itself unconsumed) |
| cognitive_budgeter | IMPLEMENTED_NOT_CONSUMED | consumers are tiered_executor/elastic_cognition — both dead |
| tiered_executor | IMPLEMENTED_NOT_CONSUMED | zero importers |
| progressive_escalation | IMPLEMENTED_NOT_CONSUMED | **fully duplicated** by scripts/adaptive_router.py |
| verification_layer | IMPLEMENTED_NOT_CONSUMED | zero importers |
| outcome_ledger | PERSISTED_NOT_CONSUMED | **explicitly de-wired** (model_routing.py:17 "No OutcomeLedger dependency") |
| witness / auto_tasks / behavior | IMPLEMENTED_NOT_CONSUMED | deliberately retired per P0-04 #20 (rune_tui.py:2208-2214, docs/pipeline-removal.md) — imports remain as vestiges |
| task_gate | IMPLEMENTED_NOT_CONSUMED | zero callers anywhere |
| self_improve | IMPLEMENTED_NOT_CONSUMED | manual /improve report only |
| persona_gate | IMPLEMENTED_NOT_CONSUMED | manual /persona-gate only; permissive by design |
| work_manager | IMPLEMENTED_NOT_CONSUMED | manual /audit //work commands |
| warm_router (in-repo) | TEST_ONLY | TUI imports it; zero calls; per-message pipeline removed |
| microkernel | PARTIALLY_LIVE | /plugins status only; dispatch engine unused |
| tier_manager | PERSISTED_NOT_CONSUMED | demotion sweep never invoked |
| drift_ledger | IMPLEMENTED_NOT_CONSUMED | manual /drift diagnostic |
| persona_definition | PARTIALLY_LIVE | nightly persona-file writes; NOT used for live prompts (persona block is a static literal) |
### Memory & context (tracer-2)
| System | Rung | Evidence |
|---|---|---|
| memory_selector | LIVE (sole v1_owner of memory injection, every turn) | rune_operator.py:1144-1186; memory_selector.py:3-9, 497-511 |
| memory_store (read side) | LIVE (## Context block every turn) | memory_selector.py:269-276; rune_operator.py:1184-1186 |
| memory_store (write side) | PERSISTED_NOT_CONSUMED — **no live chat-path writer in-repo**; producers are CLI add/hook, retrieval_benchmark, external curator | memory_store.py:501, 2227 |
| memory_consolidation | PARTIALLY_LIVE (read live every turn; write on-demand/night) | memory_selector.py:439-460; sleep_time.py:65-68 |
| warm_neuron_store (read) | PARTIALLY_LIVE (trigger-matched read every turn; write via external curator only) | memory_selector.py:335-337 |
| warm_router | PARTIALLY_LIVE — route() **dead by design** since P0-04 (enforced by test); write path on-demand | rune_tui.py:3644-3660; test_p0_04_pipeline_removal.py:33-52 |
| attention_capsule | LIVE (the sent payload itself) | context_broker.py:762-767; rune_operator.py:1385+ |
| cache | LIVE (latency memoization only, no context shaping) | memory_store.py:610, 1159 |
| recall_router | IMPLEMENTED_NOT_CONSUMED — **notable orphan**: its abstention→deep-research routing (recall_router.py:9-19) mirrors the ADAPT-2 escalation concept; the selector abstains (memory_selector.py:188-196) but nothing routes that abstention to research | zero importers |
| auto_context | IMPLEMENTED_NOT_CONSUMED (deliberately retired, enforced by test) | canary_p0_04.py:74 |
| context_pressure | PERSISTED_NOT_CONSUMED (manual /pressure diagnostic) | rune_tui.py:2577 |
| query_memory | IMPLEMENTED_NOT_CONSUMED (canary-checked only) | query_memory.py:193 |
| subconscious_pass | IMPLEMENTED_NOT_CONSUMED | subconscious_pass.py:155 |
| token_optimizer | PERSISTED_NOT_CONSUMED (write-only; optimize_prompt zero callers) | token_optimizer.py:297 |
| sleep_time / night_mode | PARTIALLY_LIVE (on-demand trigger; model effect indirect) | rune_tui.py:2978, 3078-3109 |
**Memory findings:** the single live memory→context path is operator contributor → MemorySelector → six sources
under one 0.42 relevance floor → broker admission (memory sub-budget min(1200, window//8)). **Split-brain risk**:
three paths bypass in-repo modules by importing/subprocessing the *external harness copies* in
`~/.config/opencode/scripts` (tool_executor.py:1519, auto_context.py:22-24, night_mode.py:213) — separate files
over the same DB, byte-identity unverified. And the store has **no live writer on the chat path**: the store's
contents are produced by scripts/external runs while consumption is fully live.
---
## C. Dead/partial capability findings
1. **The dead cognitive subnet** (above) — an entire ladder built, tested, never consumed. The TUI's
per-message pipeline that used to consume some of it was deliberately removed (P0-04 #20) and never replaced.
2. **False-liveness surface**: TUI startup "module loaded" checklist (rune_tui.py:1826-1866) reports witness/
auto_tasks/behavior/elastic_cognition as present — presence checks, not consumers. Creates confidence of
liveness that the trace contradicts.
3. **Cloud execution gap**: router elects cloud, executor can't run it (fragmentation #1).
4. **outcome_ledger de-wired**: cognitive-performance evidence is collected nowhere today — PL1's foundation
has no live producer in the operator path.
5. **progressive_escalation.py duplicated** by scripts/adaptive_router.py — two escalation ladders, one wired.
6. **Persona block is a ~60-line hardcoded literal** (rune_operator.py:1079+) pointing at OpenCode-era memory
paths; persona_definition's compiled definition is not used for live prompts. prompt_sync and all voice
modules are display-only — they do not touch the cognitive path (tracer-4).
7. **hook_engine.fire is a latent bug**: rune_tui.py:3841 calls a nonexistent `hook_engine.fire`; the exception
is swallowed, so "post-message hooks" silently never run. All "called by the microkernel" hook handlers
(warm_router.py:1280, pins.py:254, graph_memory.py:379, recall_router.py:108...) are dead — `microkernel.fire`
has zero callers.
8. **event_bus is a one-way journal**: task.failed emissions are durable (event_bus.py:113-135) but **no actor
reacts** — the PL0 Experience ledger would be the natural consumer and is itself orphaned.
9. **PL0 Experience ledger is orphaned**: defined only in scripts/process_experience.py:41 (ExperienceLedger,
append-only JSONL) — zero producers, zero consumers, anywhere in the repo.
10. **shard_manager dual-copy drift hazard**: src/rune/shard_manager.py and shared/shard_manager.py differ;
memory_store.py:1134 does a bare `import shard_manager` — resolution is sys.path-dependent. Shards are
bypassed on the main recall path anyway (memory_selector.py:271 calls recall directly).
11. **Single-provider reality**: ~6 provider modules exist (provider_registry, provider_budget,
standardized_providers, model_connect, connection_pool, cloud adapters); the only executing transport is
OllamaChatTransport → localhost:11434/api/chat. No OpenAI/Anthropic/GLM code executes on the live path.
12. **safety_override is the real governor**: the only infra module besides model_routing/tool stack that
changes invocation behaviour (tier escalation, prompt decoration, temperature — rune_operator.py:1315-1430).
---
## D. ADAPT-2 qualification (Phase 6)
**Initial routing: LIVE (pipeline path, canary-proven).**
Real task → production ISSUE dispatch (`StageExecutor.execute`, scripts/research_stage_executor.py:316+) →
`_route_on_dispatch` → `classify_route` → route.jsonl (INITIAL_ROUTE) → `ROUTED_LOCAL_FIX` changes execution
(research chain not entered; tree records terminal route). Canary 1 proves the behavioural effect through the
production dispatch path. Caveat: the *dispatcher* is the agent session driving the executor CLI — there is no
standalone orchestrator process; the routing edge itself is proven.
**Targeted Evidence Probe: PARTIALLY_LIVE.**
Implemented, schema-gated, canary-proven, wired into dispatch (research_stage_executor.py:340-346). Real probing
is gated behind `MIMO_PROBE_ENABLED=1` or an injected runner. Traced: **nobody sets that env in production** —
no workflow, no script, no operator path. Without opt-in, material unknowns escalate directly to deep research
(correct but un-optimized). Qualification of the opt-in: **temporary development gate / missing runtime policy
decision** — the runtime decision "who decides probing is permitted" does not exist yet. Not changed, per
instruction.
**Verification-driven escalation: PARTIALLY_LIVE.**
`escalate_on_verification_failure()` exists and is canary-proven (module level). The `ROUTED_LOCAL_FIX` consumer
is external (the agent session/operator that receives the routed result). Nothing in-repo automatically performs
implementation→verification→escalation invocation. The chain "real local implementation → real verification
failure → automatic escalation" is **not yet demonstrated end-to-end**.
---
## E. Smallest Live Rune Slice — verdict
**The spine exists.** `rune_operator.send_task` is a real, live, end-to-end execution path (proven by canary
during this audit): input → task state → context/memory assembly → model routing → Ollama tool loop →
verification/outcome → durable events. The eventual TUI already calls exactly this path. **No new execution
architecture is needed.**
What is missing is three EDGES onto that spine — the handoff's target path steps that currently terminate:
| Missing edge | Today | Smallest fix |
|---|---|---|
| **Adaptive cognition** (route decision) | send_task uses model_routing (model choice) but NOT task-class routing; the route authority lives in the research-pipeline world (scripts/), disconnected from the operator path | Route decision at send_task entry, persisted to the durable trace + event journal (see §G choice) |
| **Outcome/experience producer** | outcome_ledger de-wired; PL0 Experience ledger orphaned (zero producers); task.completed/failed transitions fire into a journal **no actor reads** | Emit a typed `task.outcome` event on every terminal transition (payload: task_id, route, model, verification, outcome); a small consumer appends Process Experience records |
| **Abstention catch** | memory_selector abstains (measured, trace-observable) but nothing routes that signal anywhere | P1 — depends on the route-authority choice |
Plus two honest defects found on the spine itself (bounded, non-architectural):
- `inference_trace` tier-2 ImportError in standalone CLI mode (trace persistence partial).
- `hook_engine.fire` dead call site (rune_tui.py:3841 calls a nonexistent function, exception swallowed).
**What this is NOT**: not a parallel demo path — the spine IS the TUI's path. Not a mass-wiring of the dead
subnet — §G ranks it.
---
## F. Proposed canary (Phase 8 acceptance)
One bounded task through the REAL spine, asserting every rung:
```text
1. rune_operator.py send "<bounded task>" (CLI — same path the TUI calls)
2. ContextBroker assembles; trace shows memory_candidates/selected/abstained
3. Route decision recorded (INITIAL_ROUTE in durable trace/event journal)
4. Route affects execution posture (observable in trace + task record)
5. Model/tool execution via OllamaChatTransport (already proven)
6. Verification result recorded (existing verification back-fill)
7. Outcome persisted: task_state.transition + NEW task.outcome event
8. Process Experience record appended (NEW consumer of the outcome event)
9. All transitions inspectable: ~/.rune/events.jsonl + route/trace fields + experience JSONL
```
Pass criterion: steps 1–9 reconstructable from persisted state with no prose inference — the same
audit-trail standard as ADAPT-2 canary 6.
---
## G. Integration priorities
**P0 — required for the cognitive execution spine (this tranche):**
1. `task.outcome` typed event on every send_task terminal transition (producer edge for experience/PL1).
2. Experience-ledger consumer of the durable event store (PL0 gains a live producer; PL1 gains input).
3. Route decision recorded in the live path (see THE CHOICE below).
4. Fix inference_trace standalone degrade (bug).
5. Remove hook_engine.fire dead call site (honest fix of a silently-swallowed AttributeError).
**THE CHOICE (presented, not buried — consent boundary):** where does task-class route authority live?
- **Option A — Route-in-operator**: send_task calls the route authority (import from scripts/ or port to
src/rune), persists RouteDecision, and RESEARCHED_CHANGE tasks are flagged/dispatched to the research
pipeline. Operator becomes the single dispatch entry for ALL work.
- **Option B — Two worlds, formally**: operator path stays route-free; the research pipeline keeps its own
routing. The boundary is documented; no unification now.
- **Option C — Unified core authority**: move route authority into src/rune; both the operator and the
research executor consume it. Cleanest end-state, largest change.
Recommendation: **A** — smallest spine that makes routing observable in live behaviour, without moving code
across worlds; C remains the long-term shape after PL1 shows what the routes need to be.
**P1 — claims to materially alter cognition today:**
- recall_router abstention→research catch (the orphan whose concept is the missing edge)
- verification_layer consumed for LOCAL_FIX verification (completes the verification-escalation chain)
- claim_audit escalation beyond advisory (currently warns, never blocks)
- outcome_ledger: re-wire into the event-driven outcome flow OR formally retire it (not both)
- progressive_escalation.py: retire in favour of scripts/adaptive_router.py (duplicated ladder)
**P2 — required by PL1/Scaffolding:** experience-ledger schema maturity, capability-gap detection inputs.
**P3 — useful, not blocking:** persona block sourced from persona_definition (not a hardcoded literal);
microkernel dispatch decision (wire or formally retire); shard_manager unification.
**P4 — legacy/deferred:** voice* (no cognitive contact), provider cloud modules until cloud execution is
actually wanted, response_adapter (superseded), model_registry+tiered_executor (isolated pair), salience_gate
(absorbed), context_pressure/token_optimizer/query_memory/subconscious_pass (diagnostics).
---
*Generated from code tracing with file:line evidence; live canary executed 2026-09-10.*
+98 -12
View File
@@ -20,6 +20,7 @@ Root-cause layers:
EXTERNAL_DEPENDENCY — external service behaved unexpectedly
UNKNOWN — attribution uncertain
"""
from __future__ import annotations
import json
@@ -28,9 +29,17 @@ import time
from datetime import datetime, timezone
ROOT_CAUSE_LAYERS = {
"MODEL_BEHAVIOR", "PROMPT_CONTRACT", "SCHEMA", "NORMALIZER",
"TOOL_INTERFACE", "ARTIFACT_ASSEMBLY", "VALIDATOR", "ROUTING",
"STAGE_ARCHITECTURE", "DETERMINISTIC_RUNTIME", "EXTERNAL_DEPENDENCY",
"MODEL_BEHAVIOR",
"PROMPT_CONTRACT",
"SCHEMA",
"NORMALIZER",
"TOOL_INTERFACE",
"ARTIFACT_ASSEMBLY",
"VALIDATOR",
"ROUTING",
"STAGE_ARCHITECTURE",
"DETERMINISTIC_RUNTIME",
"EXTERNAL_DEPENDENCY",
"UNKNOWN",
}
@@ -45,13 +54,25 @@ class ExperienceLedger:
self.path = path
os.makedirs(os.path.dirname(path), exist_ok=True)
def record(self, task_run_stage: str, result_type: str, symptom: str,
evidence: list[str], root_cause: str, confidence: float,
human_intervention: bool = False, pipeline_sha: str = "UNKNOWN",
correction_applied: str = "", recurrence_class: str = "",
wall_cost_s: float | str = 0, model_cost_s: float | str = 0,
tool_cost_s: float | str = 0, quality_outcome: str = "",
reusable: bool = False, candidate_status: str = "SEED") -> dict:
def record(
self,
task_run_stage: str,
result_type: str,
symptom: str,
evidence: list[str],
root_cause: str,
confidence: float,
human_intervention: bool = False,
pipeline_sha: str = "UNKNOWN",
correction_applied: str = "",
recurrence_class: str = "",
wall_cost_s: float | str = 0,
model_cost_s: float | str = 0,
tool_cost_s: float | str = 0,
quality_outcome: str = "",
reusable: bool = False,
candidate_status: str = "SEED",
) -> dict:
assert result_type in RESULT_TYPES, f"invalid result_type: {result_type}"
assert root_cause in ROOT_CAUSE_LAYERS, f"invalid root_cause: {root_cause}"
assert candidate_status in CANDIDATE_STATUSES, f"invalid status: {candidate_status}"
@@ -85,8 +106,10 @@ class ExperienceLedger:
out = []
with open(self.path) as f:
for line in f:
try: out.append(json.loads(line))
except json.JSONDecodeError: continue
try:
out.append(json.loads(line))
except json.JSONDecodeError:
continue
return out
def by_root_cause(self, layer: str) -> list[dict]:
@@ -100,3 +123,66 @@ class ExperienceLedger:
def reusable_lessons(self) -> list[dict]:
return [e for e in self.load_all() if e["reusable_lesson"]]
def record_operator_outcomes(task_state_path: str | None = None, ledger_path: str | None = None) -> list[dict]:
"""Consume operator task outcomes into the Experience Ledger (Runtime
Reachability P0, 2026-09-10).
The operator's send_task persists every task in the TaskStateRepository
checkpoint (task_state.json colocated with operator.json) and emits a
durable task.outcome journal event per terminal transition. This
consumer reads the CHECKPOINT (the authoritative task store — the
journal is a one-way integrity journal: payload_hash only, no payload)
and appends a SEED experience record for every terminal task not yet
recorded (matched by evidence task_id — idempotent).
Attribution stays UNKNOWN: attribution is PL1's job, not the ledger's.
Statuses stay SEED/CANDIDATE — never auto-VALIDATED.
"""
path = task_state_path or os.path.expanduser("~/.rune/hot/task_state.json")
ledger = ExperienceLedger(
ledger_path
or os.environ.get("RUNE_EXPERIENCE_LEDGER", os.path.expanduser("~/.rune/state/process_experience.jsonl"))
)
with open(path) as f:
checkpoint = json.load(f)
tasks = checkpoint.get("tasks", [])
seen_tasks = set()
for rec in ledger.load_all():
for ev in rec.get("evidence_references", []):
if ev.startswith("task:"):
seen_tasks.add(ev.split(":", 1)[1])
recorded = []
for t in tasks:
status = t.get("status", "")
if status not in ("completed", "failed"):
continue
task_id = t.get("task_id", "")
if not task_id or task_id in seen_tasks:
continue
result_type = "SUCCESS" if status == "completed" else "FAILURE"
rec = ledger.record(
task_run_stage=f"operator:{status}",
result_type=result_type,
symptom=(t.get("title") or "")[:200],
evidence=[f"task:{task_id}", f"model:{t.get('assigned_model')}", f"outcome_ref:{t.get('outcome_ref')}"],
root_cause="UNKNOWN",
confidence=0.3,
pipeline_sha="operator",
reusable=False,
candidate_status="SEED",
)
recorded.append(rec)
return recorded
if __name__ == "__main__": # pragma: no cover — manual consumer invocation
import argparse
ap = argparse.ArgumentParser(description="Record operator task outcomes as experience evidence")
ap.add_argument("--task-state", default=None)
ap.add_argument("--ledger", default=None)
args = ap.parse_args()
recs = record_operator_outcomes(args.task_state, args.ledger)
print(f"recorded {len(recs)} experience records")
+56 -2
View File
@@ -34,6 +34,13 @@ if TYPE_CHECKING:
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if _SCRIPT_DIR not in sys.path:
sys.path.insert(0, _SCRIPT_DIR)
# Standalone (script) execution: the package PARENT must also be importable,
# or every `from rune.X import` fallback fails and trace recording degrades
# to tier-2 (observed live 2026-09-10: "attempted relative import with no
# known parent package"). The TUI path already provides this.
_PACKAGE_PARENT = os.path.dirname(_SCRIPT_DIR)
if _PACKAGE_PARENT not in sys.path:
sys.path.append(_PACKAGE_PARENT)
import safety_override # noqa: E402
import model_profiles # noqa: E402
import tool_executor # noqa: E402
@@ -418,7 +425,10 @@ class RuneOperator:
def _routing_profiles_path(self):
"""Measured-routing-profile store path under RUNE_HOME/state."""
from .model_routing import default_profiles_path
try:
from .model_routing import default_profiles_path
except ImportError: # standalone script execution (no package parent)
from rune.model_routing import default_profiles_path
return default_profiles_path()
@@ -619,6 +629,14 @@ class RuneOperator:
task.completed_at = datetime.now(timezone.utc).isoformat()
self.state.current_task = None
self._task_state.transition(task.task_id, "completed", outcome_ref="response:" + task.task_id)
# Runtime Reachability P0: outcome producer edge (every terminal
# transition is observable; PL1 consumes this later).
self._emit_task_outcome(
task,
channel=channel,
verification=next((o for o in result.runtime_objects if "method" in o), None),
route=(result.runtime_objects[0].get("route", "") if result.runtime_objects else ""),
)
self._conversation.append(
ConversationMessageRecord(
role="assistant",
@@ -636,6 +654,7 @@ class RuneOperator:
self.state.current_task = None
self._task_state.transition(task.task_id, "failed")
self._save()
self._emit_task_outcome(task, channel=channel)
# P0-04 (#20): first endogenous meaningful-event trigger — a task
# failure is worth hearing about (#48 grows the trigger taxonomy
# on top of this seed). The typed event already exists in the
@@ -660,6 +679,38 @@ class RuneOperator:
return self._response_envelope(result, task)
def _emit_task_outcome(
self, task: RuneTask, channel: str = "", verification: dict | None = None, route: str | None = None
) -> None:
"""Emit the canonical `task.outcome` journal event (Runtime
Reachability P0, 2026-09-10): the producer edge for experience
learning (PL0/PL1) and outcome evidence. Fired on EVERY terminal
transition — completed AND failed — because "no outcome recorded"
is itself outcome data. Payload carries the fields the experience
ledger needs: identity, model, verification link, outcome.
Guarded: a journal failure must never mask the operator's own path.
"""
try:
event_bus.emit(
"task.outcome",
source="rune_operator",
payload={
"task_id": task.task_id,
"channel": channel,
"model": task.assigned_model or "",
"status": task.status,
"description": (task.description or "")[:200],
"result_excerpt": (task.result or "")[:200],
"verification_id": (verification or {}).get("verification_id", ""),
"verification_passed": bool((verification or {}).get("passed")),
"route": route or "",
"completed_at": task.completed_at or "",
},
)
except Exception:
pass
def _emit_task_failed(self, task: RuneTask, channel: str = "") -> None:
"""Emit the canonical `task.failed` journal event (P0-04, #20 seed).
@@ -1628,7 +1679,10 @@ I have tools to read, write, edit, search, lint, test, format, and commit code.
from rune.model_routing import update_profiles_from_traces
merged = update_profiles_from_traces(self._routing_profiles_path(), [asdict(trace_record)])
from .model_routing import save_profiles as _save_profiles
try:
from .model_routing import save_profiles as _save_profiles
except ImportError: # standalone script execution
from rune.model_routing import save_profiles as _save_profiles
_save_profiles(self._routing_profiles_path(), merged)
except Exception as exc:
+122 -344
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
from __future__ import annotations
"""Rune TUI — the body.
Textual-based terminal interface for Rune. Two channels (task + chat),
@@ -116,9 +117,7 @@ from rich.text import Text
TEACHABLE_FILE = os.path.expanduser("~/.rune/teachable_moments.json")
def _log_teachable(
error_type: str, context: str, fix: str, file: str = "", line: int = 0
):
def _log_teachable(error_type: str, context: str, fix: str, file: str = "", line: int = 0):
"""Log a teachable moment — an error that should be prevented in the future."""
try:
moments = []
@@ -486,9 +485,7 @@ ALL_COMMANDS = [
]
# Autocomplete commands derived from ALL_COMMANDS (short form for dropdown)
ALL_AUTOCOMPLETE_COMMANDS = [
(cmd.split(" ")[0] if " " in cmd else cmd, desc) for cmd, desc in ALL_COMMANDS
]
ALL_AUTOCOMPLETE_COMMANDS = [(cmd.split(" ")[0] if " " in cmd else cmd, desc) for cmd, desc in ALL_COMMANDS]
def get_autocomplete_candidates(target_state):
@@ -501,9 +498,7 @@ def get_autocomplete_candidates(target_state):
# If no text or just starting, show all commands
if not text or text == "/":
return [
DropdownItem(cmd, prefix=desc) for cmd, desc in ALL_AUTOCOMPLETE_COMMANDS
]
return [DropdownItem(cmd, prefix=desc) for cmd, desc in ALL_AUTOCOMPLETE_COMMANDS]
# Filter commands based on what's typed
matches = []
@@ -562,9 +557,7 @@ class CommandSelector(Static):
if self.search_text:
query = self.search_text.lower()
self.filtered_commands = [
(cmd, desc)
for cmd, desc in ALL_COMMANDS
if query in cmd.lower() or query in desc.lower()
(cmd, desc) for cmd, desc in ALL_COMMANDS if query in cmd.lower() or query in desc.lower()
]
else:
self.filtered_commands = ALL_COMMANDS[:]
@@ -622,9 +615,7 @@ class ModelItem(Static):
}
"""
def __init__(
self, model: dict, index: int, is_selected: bool, is_current: bool, **kwargs
):
def __init__(self, model: dict, index: int, is_selected: bool, is_current: bool, **kwargs):
super().__init__(**kwargs)
self.model = model
self.index = index
@@ -640,11 +631,7 @@ class ModelItem(Static):
profile = model_profiles.load_profile(name)
if profile:
tips = (
"\n".join(f"• {t}" for t in profile.prompting_tips[:3])
if profile.prompting_tips
else "None"
)
tips = "\n".join(f"• {t}" for t in profile.prompting_tips[:3]) if profile.prompting_tips else "None"
self.tooltip = f"[bold]{name}[/bold]\nFamily: {profile.family} | Size: {profile.parameters}\nStrengths: {', '.join(profile.strengths[:3])}\nTips:\n{tips}"
else:
self.tooltip = name
@@ -691,9 +678,7 @@ class CategoryHeader(Static):
}
"""
def __init__(
self, label: str, icon: str, count: int, expanded: bool = False, **kwargs
):
def __init__(self, label: str, icon: str, count: int, expanded: bool = False, **kwargs):
super().__init__(**kwargs)
self.label = label
self.icon = icon
@@ -812,12 +797,8 @@ class ModelSelector(Static):
self._category_headers = []
# Separate local and cloud
local_models = [
m for m in self.filtered_models if m.get("provider", "local") == "local"
]
cloud_models = [
m for m in self.filtered_models if m.get("provider", "local") != "local"
]
local_models = [m for m in self.filtered_models if m.get("provider", "local") == "local"]
cloud_models = [m for m in self.filtered_models if m.get("provider", "local") != "local"]
# Build category structure
self._categories = {}
@@ -938,9 +919,7 @@ def _save_model_state(model: str):
try:
os.makedirs(os.path.dirname(state_path), exist_ok=True)
with open(state_path, "w") as f:
json.dump(
{"model": model, "timestamp": datetime.now(timezone.utc).isoformat()}, f
)
json.dump({"model": model, "timestamp": datetime.now(timezone.utc).isoformat()}, f)
except Exception:
_log.debug("Failed to save model state", exc_info=True)
@@ -977,7 +956,9 @@ def _resolve_model_name(setting: str, mode: str = "chat") -> str:
return cloud[0].model
except Exception as exc: # P0-06: observable fallback (T1)
record_failure("model_routing", exc, tier=1)
return "cloud-unavailable-fallback: no verified cloud candidate (router ranked none; local remains authoritative)"
return (
"cloud-unavailable-fallback: no verified cloud candidate (router ranked none; local remains authoritative)"
)
return "qwen2.5:3b" if setting == "local" else setting
@@ -1105,11 +1086,7 @@ class StatusBar(Static):
profile = model_profiles.load_profile(self.model)
if profile:
tips = (
"\n".join(f"• {t}" for t in profile.prompting_tips[:3])
if profile.prompting_tips
else "None"
)
tips = "\n".join(f"• {t}" for t in profile.prompting_tips[:3]) if profile.prompting_tips else "None"
self.tooltip = f"[bold]{self.model}[/bold]\nFamily: {profile.family} | Size: {profile.parameters} | Quant: {profile.quantization}\nStrengths: {', '.join(profile.strengths[:3])}\nTips:\n{tips}"
else:
self.tooltip = self.model
@@ -1205,9 +1182,7 @@ class ChannelView(Vertical):
log.mount(bubble)
log.scroll_end(animate=False)
self.messages.append(
{"role": role, "content": content, "time": timestamp, "metadata": metadata}
)
self.messages.append({"role": role, "content": content, "time": timestamp, "metadata": metadata})
# Section order for the work pane's operational index (P1-07 #30):
@@ -1329,9 +1304,7 @@ class WorkPane(Vertical):
the ACTIVE channel's objects; chat objects never bleed into the
task view)."""
return [
(object_id, obj)
for object_id, obj in self._entries.items()
if str(obj.get("channel", "task")) == channel
(object_id, obj) for object_id, obj in self._entries.items() if str(obj.get("channel", "task")) == channel
]
def update_object(self, obj: dict) -> bool:
@@ -1607,9 +1580,7 @@ class RuneApp(App):
if not os.path.exists(profiles_dir) or len(os.listdir(profiles_dir)) == 0:
scanned = model_profiles.scan_all_models()
if scanned:
self.chat_channel.add_message(
"system", f"[dim]◆ Scanned {len(scanned)} model profiles[/dim]"
)
self.chat_channel.add_message("system", f"[dim]◆ Scanned {len(scanned)} model profiles[/dim]")
profile = model_profiles.get_or_create_profile(self.model)
context = model_profiles.get_prompt_context(self.model)
@@ -1629,13 +1600,9 @@ class RuneApp(App):
authority = "✓ standard"
if context:
self.chat_channel.add_message(
"system", f"[dim]◆ Model: {self.model} ({context})[/dim]"
)
self.chat_channel.add_message("system", f"[dim]◆ Model: {self.model} ({context})[/dim]")
else:
self.chat_channel.add_message(
"system", f"[dim]◆ Model: {self.model}[/dim]"
)
self.chat_channel.add_message("system", f"[dim]◆ Model: {self.model}[/dim]")
self.chat_channel.add_message(
"system",
@@ -1643,18 +1610,14 @@ class RuneApp(App):
)
except Exception as e:
self.chat_channel.add_message(
"system", f"[dim]⚠ Model profile load failed: {e}[/dim]"
)
self.chat_channel.add_message("system", f"[dim]⚠ Model profile load failed: {e}[/dim]")
# Preload model into VRAM (fire-and-forget in background thread)
def _preload():
try:
import urllib.request
ollama_url = getattr(
model_connect, "OLLAMA_URL", "http://localhost:11434"
)
ollama_url = getattr(model_connect, "OLLAMA_URL", "http://localhost:11434")
data = json.dumps(
{
"model": self.model,
@@ -1686,22 +1649,16 @@ class RuneApp(App):
"[dim]◆ Cognitive loop active — Rune thinks between turns[/dim]",
)
except Exception as e:
self.chat_channel.add_message(
"system", f"[dim]⚠ Cognitive loop failed: {e}[/dim]"
)
self.chat_channel.add_message("system", f"[dim]⚠ Cognitive loop failed: {e}[/dim]")
# Start dream cycle — Rune processes while idle
try:
import dream_cycle
self.dream_cycle = dream_cycle.DreamCycle()
self.chat_channel.add_message(
"system", "[dim]◆ Dream cycle active — Rune processes while idle[/dim]"
)
self.chat_channel.add_message("system", "[dim]◆ Dream cycle active — Rune processes while idle[/dim]")
except Exception as e:
self.chat_channel.add_message(
"system", f"[dim]⚠ Dream cycle failed: {e}[/dim]"
)
self.chat_channel.add_message("system", f"[dim]⚠ Dream cycle failed: {e}[/dim]")
# Start autonomy loop — Rune evaluates and proposes improvements
try:
@@ -1713,9 +1670,7 @@ class RuneApp(App):
"[dim]◆ Autonomy loop active — Rune evaluates and proposes improvements[/dim]",
)
except Exception as e:
self.chat_channel.add_message(
"system", f"[dim]⚠ Autonomy loop failed: {e}[/dim]"
)
self.chat_channel.add_message("system", f"[dim]⚠ Autonomy loop failed: {e}[/dim]")
def on_static_clicked(self, event: Static.Clicked) -> None:
"""Handle refresh button click."""
@@ -1747,14 +1702,10 @@ class RuneApp(App):
if BACKEND_AVAILABLE:
data = task_persist.load()
tasks = data.get("tasks", [])
active = [
t for t in tasks if t.get("status") in ("open", "in_progress")
]
active = [t for t in tasks if t.get("status") in ("open", "in_progress")]
# Also load pins
all_pins = pins.list_pins()
open_pins = [
p for p in all_pins if p.get("status") == "open"
]
open_pins = [p for p in all_pins if p.get("status") == "open"]
return ("tasks", active, open_pins)
return ("tasks", [])
except Exception:
@@ -1774,21 +1725,13 @@ class RuneApp(App):
if isinstance(result, tuple):
key, value = result
if key == "persona" and value:
self.chat_channel.add_message(
"system", "[dim]✓ Persona compiled[/dim]"
)
self.chat_channel.add_message("system", "[dim]✓ Persona compiled[/dim]")
elif key == "persona_error":
self.chat_channel.add_message(
"system", f"[dim]⚠ Persona compile error: {value}[/dim]"
)
self.chat_channel.add_message("system", f"[dim]⚠ Persona compile error: {value}[/dim]")
elif key == "registry" and value:
self.chat_channel.add_message(
"system", "[dim]✓ System registry scanned[/dim]"
)
self.chat_channel.add_message("system", "[dim]✓ System registry scanned[/dim]")
elif key == "registry_error":
self.chat_channel.add_message(
"system", f"[dim]⚠ Registry scan error: {value}[/dim]"
)
self.chat_channel.add_message("system", f"[dim]⚠ Registry scan error: {value}[/dim]")
elif key == "tasks":
active = value
if active:
@@ -1799,17 +1742,13 @@ class RuneApp(App):
lines.append(f"[dim] [{status}] {summary}[/dim]")
self.task_channel.add_message("system", "\n".join(lines))
else:
self.task_channel.add_message(
"system", "[dim]No active tasks.[/dim]"
)
self.task_channel.add_message("system", "[dim]No active tasks.[/dim]")
# Handle pins (third element)
if len(result) > 2 and isinstance(result[2], list):
open_pins = result[2]
if open_pins:
self.status_bar.pins_count = len(open_pins)
self.task_channel.add_message(
"system", f"[dim]📌 {len(open_pins)} pinned items[/dim]"
)
self.task_channel.add_message("system", f"[dim]📌 {len(open_pins)} pinned items[/dim]")
# Focus the chat input by default
self.query_one("#input-chat", Input).focus()
@@ -1817,9 +1756,7 @@ class RuneApp(App):
def _startup_check(self):
"""Verify all modules loaded at startup."""
if not BACKEND_AVAILABLE:
self.chat_channel.add_message(
"system", "[dim]⚠ Backend not available. Some features disabled.[/dim]"
)
self.chat_channel.add_message("system", "[dim]⚠ Backend not available. Some features disabled.[/dim]")
return
# Check which modules are loaded
@@ -1879,9 +1816,7 @@ class RuneApp(App):
failed.append(mod)
# Report status with more detail
self.chat_channel.add_message(
"system", f"[dim]◆ {len(loaded)} modules loaded[/dim]"
)
self.chat_channel.add_message("system", f"[dim]◆ {len(loaded)} modules loaded[/dim]")
if failed:
self.chat_channel.add_message(
"system",
@@ -1916,9 +1851,7 @@ class RuneApp(App):
)
except Exception as exc: # P0-06: observable degrade (T1)
if record_failure("provider_registry", exc, tier=1):
self.chat_channel.add_message(
"system", "[dim]⚠ provider registry unavailable[/dim]"
)
self.chat_channel.add_message("system", "[dim]⚠ provider registry unavailable[/dim]")
# Show version info
self.chat_channel.add_message(
@@ -1931,9 +1864,7 @@ class RuneApp(App):
if BACKEND_AVAILABLE:
time_data = time_awareness.get_time_context()
if time_data and time_data.get("period") in ("evening", "night"):
self.chat_channel.add_message(
"system", f"[dim]🌙 Night mode available (/night)[/dim]"
)
self.chat_channel.add_message("system", f"[dim]🌙 Night mode available (/night)[/dim]")
except Exception:
pass
@@ -2076,9 +2007,7 @@ class RuneApp(App):
ch.add_message("system", "[dim]Tasks:[/dim]")
for i, task in enumerate(plan["task_list"], 1):
ch.add_message("system", f"[dim] {i}. {task}[/dim]")
ch.add_message(
"system", "[dim]Say 'start' to execute, or continue refining.[/dim]"
)
ch.add_message("system", "[dim]Say 'start' to execute, or continue refining.[/dim]")
break
def _execute_plan(self):
@@ -2090,9 +2019,7 @@ class RuneApp(App):
break
if not active_plan:
self.chat_channel.add_message(
"system", "[dim]No plan ready to execute.[/dim]"
)
self.chat_channel.add_message("system", "[dim]No plan ready to execute.[/dim]")
return
active_plan["status"] = "executing"
@@ -2101,16 +2028,12 @@ class RuneApp(App):
self._update_status()
# Move tasks to task channel
self.task_channel.add_message(
"system", f"[bold]🎯 Executing plan: {active_plan['id']}[/bold]"
)
self.task_channel.add_message("system", f"[bold]🎯 Executing plan: {active_plan['id']}[/bold]")
if active_plan.get("task_list"):
for i, task in enumerate(active_plan["task_list"], 1):
self.task_channel.add_message("system", f"[dim]{i}. {task}[/dim]")
self.chat_channel.add_message(
"system", f"[dim]Plan {active_plan['id']} moved to task channel.[/dim]"
)
self.chat_channel.add_message("system", f"[dim]Plan {active_plan['id']} moved to task channel.[/dim]")
def on_input_submitted(self, event: Input.Submitted) -> None:
"""Handle input submission — detect which channel and route accordingly."""
@@ -2337,9 +2260,7 @@ class RuneApp(App):
authority = "✓ standard"
if context:
ch.add_message(
"system", f"Model set to: {model_name} ({context}) | {authority}"
)
ch.add_message("system", f"Model set to: {model_name} ({context}) | {authority}")
else:
ch.add_message("system", f"Model set to: {model_name} | {authority}")
except Exception:
@@ -2365,9 +2286,7 @@ class RuneApp(App):
if handler:
handler(self, arg)
else:
self._ch().add_message(
"system", f"Unknown command: {cmd}. Type /help for options."
)
self._ch().add_message("system", f"Unknown command: {cmd}. Type /help for options.")
# ── Command handlers ──────────────────────────────────────────────
@@ -2495,13 +2414,9 @@ class RuneApp(App):
try:
principles = prompt_sync.get_defining_principles()
if principles:
self._ch().add_message(
"system", f"[bold]🎭 Defining Principles[/bold]\n{principles}"
)
self._ch().add_message("system", f"[bold]🎭 Defining Principles[/bold]\n{principles}")
else:
self._ch().add_message(
"system", "[dim]No defining principles found[/dim]"
)
self._ch().add_message("system", "[dim]No defining principles found[/dim]")
except Exception as e:
self._ch().add_message("system", f"[dim]Error loading persona: {e}[/dim]")
@@ -2514,21 +2429,15 @@ class RuneApp(App):
lines.append(f" {k}: {v}")
self._ch().add_message("system", "\n".join(lines))
else:
self._ch().add_message(
"system", "[dim]No abstraction stats available[/dim]"
)
self._ch().add_message("system", "[dim]No abstraction stats available[/dim]")
except Exception as e:
self._ch().add_message(
"system", f"[dim]Error loading abstractions: {e}[/dim]"
)
self._ch().add_message("system", f"[dim]Error loading abstractions: {e}[/dim]")
def _cmd_claims(self, arg):
try:
report = claim_audit.report()
if report:
self._ch().add_message(
"system", f"[bold]🔍 Claim Audit Report[/bold]\n{report}"
)
self._ch().add_message("system", f"[bold]🔍 Claim Audit Report[/bold]\n{report}")
else:
self._ch().add_message("system", "[dim]No claims tracked[/dim]")
except Exception as e:
@@ -2587,9 +2496,7 @@ class RuneApp(App):
else:
self._ch().add_message("system", "[dim]No messages to measure[/dim]")
except Exception as e:
self._ch().add_message(
"system", f"[dim]Error measuring pressure: {e}[/dim]"
)
self._ch().add_message("system", f"[dim]Error measuring pressure: {e}[/dim]")
def _cmd_events(self, arg):
try:
@@ -2674,17 +2581,13 @@ class RuneApp(App):
else:
self._ch().add_message("system", "[dim]No messages to check[/dim]")
except Exception as e:
self._ch().add_message(
"system", f"[dim]Error loading persona gate: {e}[/dim]"
)
self._ch().add_message("system", f"[dim]Error loading persona gate: {e}[/dim]")
def _cmd_hooks(self, arg):
try:
report = hook_engine.generate_report()
if report:
self._ch().add_message(
"system", f"[bold]🪝 Hook Engine[/bold]\n{report}"
)
self._ch().add_message("system", f"[bold]🪝 Hook Engine[/bold]\n{report}")
else:
self._ch().add_message("system", "[dim]No hook data[/dim]")
except Exception as e:
@@ -2697,9 +2600,7 @@ class RuneApp(App):
lines = [f"[bold]📈 Growth Delta[/bold] ({len(recent)} recent)"]
for item in recent[:5]:
if isinstance(item, dict):
lines.append(
f" {item.get('topic', '?')}: {item.get('delta', 0):.2f}"
)
lines.append(f" {item.get('topic', '?')}: {item.get('delta', 0):.2f}")
else:
lines.append(f" {item}")
self._ch().add_message("system", "\n".join(lines))
@@ -2712,25 +2613,17 @@ class RuneApp(App):
try:
analysis = self_improve.analyze_system()
if analysis:
lines = [
f"[bold]🔧 Self-Improvement[/bold] ({len(analysis)} suggestions)"
]
lines = [f"[bold]🔧 Self-Improvement[/bold] ({len(analysis)} suggestions)"]
for item in analysis[:5]:
if isinstance(item, dict):
lines.append(
f" {item.get('area', '?')}: {item.get('suggestion', '?')}"
)
lines.append(f" {item.get('area', '?')}: {item.get('suggestion', '?')}")
else:
lines.append(f" {item}")
self._ch().add_message("system", "\n".join(lines))
else:
self._ch().add_message(
"system", "[dim]No improvement suggestions[/dim]"
)
self._ch().add_message("system", "[dim]No improvement suggestions[/dim]")
except Exception as e:
self._ch().add_message(
"system", f"[dim]Error loading improvements: {e}[/dim]"
)
self._ch().add_message("system", f"[dim]Error loading improvements: {e}[/dim]")
def _cmd_voice(self, arg):
try:
@@ -2741,9 +2634,7 @@ class RuneApp(App):
lines.append(f" Voice: {blend['voice'][:100]}...")
if "dimensions" in blend:
for dim in blend["dimensions"][:5]:
lines.append(
f" {dim.get('name', '?')}: {dim.get('emphasis', 0):.0%}"
)
lines.append(f" {dim.get('name', '?')}: {dim.get('emphasis', 0):.0%}")
self._ch().add_message("system", "\n".join(lines))
else:
self._ch().add_message("system", "[dim]No voice data[/dim]")
@@ -2761,9 +2652,7 @@ class RuneApp(App):
else:
self._ch().add_message("system", "[dim]No sync data[/dim]")
except Exception as e:
self._ch().add_message(
"system", f"[dim]Error loading voice sync: {e}[/dim]"
)
self._ch().add_message("system", f"[dim]Error loading voice sync: {e}[/dim]")
def _cmd_drift(self, arg):
try:
@@ -2788,9 +2677,7 @@ class RuneApp(App):
lines.append(" Functions: reflect_local, run_abstraction, mine_transcript")
self._ch().add_message("system", "\n".join(lines))
except Exception as e:
self._ch().add_message(
"system", f"[dim]Error loading sleep-time: {e}[/dim]"
)
self._ch().add_message("system", f"[dim]Error loading sleep-time: {e}[/dim]")
def _cmd_providers(self, arg):
try:
@@ -2799,9 +2686,7 @@ class RuneApp(App):
lines = [f"[bold]🔌 Connected Providers[/bold] ({len(providers)})"]
for p in providers:
status = "✓" if p.get("enabled") else "✗"
lines.append(
f" {status} {p['name']} ({p['provider_type']}): {p['url']}"
)
lines.append(f" {status} {p['name']} ({p['provider_type']}): {p['url']}")
self._ch().add_message("system", "\n".join(lines))
else:
self._ch().add_message(
@@ -2823,12 +2708,8 @@ class RuneApp(App):
name, url = parts[0], parts[1]
provider_type = parts[2] if len(parts) > 2 else "openai"
api_key = parts[3] if len(parts) > 3 else None
result = provider_registry.connect_provider(
name, url, provider_type, api_key
)
self._ch().add_message(
"system", f"Connected: {result['name']} ({result['provider_type']})"
)
result = provider_registry.connect_provider(name, url, provider_type, api_key)
self._ch().add_message("system", f"Connected: {result['name']} ({result['provider_type']})")
try:
registry = provider_registry.get_registry()
provider = registry.get_provider(name)
@@ -2837,9 +2718,7 @@ class RuneApp(App):
if models:
lines = [f"[bold]Available models from {name}:[/bold]"]
for m in models[:10]:
lines.append(
f" - {m.get('display_name', m.get('name', '?'))}"
)
lines.append(f" - {m.get('display_name', m.get('name', '?'))}")
if len(models) > 10:
lines.append(f" ... and {len(models) - 10} more")
self._ch().add_message("system", "\n".join(lines))
@@ -2881,13 +2760,9 @@ class RuneApp(App):
self._update_status()
self._ch().add_message("system", f"Switched to: {model_name}")
else:
self._ch().add_message(
"system", f"[dim]No models available from {next_provider}[/dim]"
)
self._ch().add_message("system", f"[dim]No models available from {next_provider}[/dim]")
else:
self._ch().add_message(
"system", f"[dim]Provider not found: {next_provider}[/dim]"
)
self._ch().add_message("system", f"[dim]Provider not found: {next_provider}[/dim]")
except Exception as e:
self._ch().add_message("system", f"[dim]Error cycling providers: {e}[/dim]")
@@ -2915,9 +2790,7 @@ class RuneApp(App):
)
self._ch().add_message("system", "\n".join(lines))
except Exception as e:
self._ch().add_message(
"system", f"[dim]Error loading Rune status: {e}[/dim]"
)
self._ch().add_message("system", f"[dim]Error loading Rune status: {e}[/dim]")
def _cmd_goal(self, arg):
try:
@@ -2943,9 +2816,7 @@ class RuneApp(App):
self._ch().add_message("system", f"🎯 Goal set: {arg[4:]}")
elif arg == "check":
result = goal.check_goal()
self._ch().add_message(
"system", f"Goal status: {result['status']} — {result['message']}"
)
self._ch().add_message("system", f"Goal status: {result['status']} — {result['message']}")
elif arg == "achieve":
result = goal.achieve_goal()
self._ch().add_message("system", f"✓ {result['message']}")
@@ -2974,24 +2845,16 @@ class RuneApp(App):
self._ch().add_message("system", "[dim]💤 Consolidating memories...[/dim]")
# Try to use sleep_time for actual consolidation
try:
result = (
sleep_time.consolidate()
if hasattr(sleep_time, "consolidate")
else None
)
result = sleep_time.consolidate() if hasattr(sleep_time, "consolidate") else None
if result:
self._ch().add_message(
"system", f"✓ Memory consolidation complete: {result}"
)
self._ch().add_message("system", f"✓ Memory consolidation complete: {result}")
else:
self._ch().add_message(
"system",
"✓ Memory consolidation triggered (sleep_time.consolidate not available)",
)
except Exception as inner_e:
self._ch().add_message(
"system", f"[dim]Consolidation note: {inner_e}[/dim]"
)
self._ch().add_message("system", f"[dim]Consolidation note: {inner_e}[/dim]")
except Exception as e:
self._ch().add_message("system", f"[dim]Error consolidating: {e}[/dim]")
@@ -3008,9 +2871,7 @@ class RuneApp(App):
lines = [f"[bold]⏰ Scheduled Tasks[/bold] ({len(tasks)})"]
for t in tasks:
status = "✓" if t["enabled"] else "✗"
lines.append(
f" {status} {t['name']}: {t['cron_expression']} → {t['command'][:50]}"
)
lines.append(f" {status} {t['name']}: {t['cron_expression']} → {t['command'][:50]}")
self._ch().add_message("system", "\n".join(lines))
elif arg.startswith("add "):
parts = arg[4:].split(maxsplit=2)
@@ -3030,15 +2891,11 @@ class RuneApp(App):
# Register a task to fire after each task completes
parts = arg[12:].split(maxsplit=1)
if len(parts) < 2:
self._ch().add_message(
"system", "Usage: /cron on-complete <name> <cmd>"
)
self._ch().add_message("system", "Usage: /cron on-complete <name> <cmd>")
else:
name, cmd = parts
result = cron_engine.add_task(name, "on_complete", cmd)
self._ch().add_message(
"system", f"✓ Added on-complete trigger: {name} → {cmd[:50]}"
)
self._ch().add_message("system", f"✓ Added on-complete trigger: {name} → {cmd[:50]}")
elif arg.startswith("remove "):
name = arg[7:]
if cron_engine.remove_task(name):
@@ -3051,13 +2908,9 @@ class RuneApp(App):
if result.get("success"):
self._ch().add_message("system", f"✓ Ran: {name}")
if result.get("stdout"):
self._ch().add_message(
"system", f"[dim]{result['stdout'][:100]}[/dim]"
)
self._ch().add_message("system", f"[dim]{result['stdout'][:100]}[/dim]")
else:
self._ch().add_message(
"system", f"✗ Failed: {result.get('error', 'Unknown error')}"
)
self._ch().add_message("system", f"✗ Failed: {result.get('error', 'Unknown error')}")
elif arg == "status":
engine = cron_engine.get_cron_engine()
lines = ["[bold]⏰ Cron Engine[/bold]"]
@@ -3066,9 +2919,7 @@ class RuneApp(App):
lines.append(f" Storage: {engine.storage_path}")
self._ch().add_message("system", "\n".join(lines))
else:
self._ch().add_message(
"system", "Usage: /cron [list|add|remove|run|status]"
)
self._ch().add_message("system", "Usage: /cron [list|add|remove|run|status]")
except Exception as e:
self._ch().add_message("system", f"[dim]Error with cron: {e}[/dim]")
@@ -3077,9 +2928,7 @@ class RuneApp(App):
if not arg or arg == "status":
status = night_mode.get_status()
lines = ["[bold]🌙 Night Mode[/bold]"]
lines.append(
f" Status: {'active' if status['active'] else 'inactive'}"
)
lines.append(f" Status: {'active' if status['active'] else 'inactive'}")
lines.append(f" Phase: {status['phase']}")
lines.append(f" Iterations: {status['iterations']}")
lines.append(f" Improvements: {status['improvements']}")
@@ -3094,17 +2943,13 @@ class RuneApp(App):
if result.get("success"):
self._ch().add_message("system", f"✓ {result['message']}")
else:
self._ch().add_message(
"system", f"✗ {result.get('error', 'Unknown error')}"
)
self._ch().add_message("system", f"✗ {result.get('error', 'Unknown error')}")
elif arg == "stop":
result = night_mode.stop_night_mode()
if result.get("success"):
self._ch().add_message("system", f"✓ {result['message']}")
else:
self._ch().add_message(
"system", f"✗ {result.get('error', 'Unknown error')}"
)
self._ch().add_message("system", f"✗ {result.get('error', 'Unknown error')}")
elif arg == "report":
report = night_mode.get_report()
lines = ["[bold]🌙 Night Mode Report[/bold]"]
@@ -3114,9 +2959,7 @@ class RuneApp(App):
lines.append(f" Errors: {len(report['errors'])}")
self._ch().add_message("system", "\n".join(lines))
else:
self._ch().add_message(
"system", "Usage: /night [start [--hours N]|stop|status|report]"
)
self._ch().add_message("system", "Usage: /night [start [--hours N]|stop|status|report]")
except Exception as e:
self._ch().add_message("system", f"[dim]Error with night mode: {e}[/dim]")
@@ -3129,9 +2972,7 @@ class RuneApp(App):
lines.append(f" Day: {status['day_of_week']}")
lines.append(f" Time of day: {status['time_of_day']}")
lines.append(f" Weekend: {status['is_weekend']}")
lines.append(
f" Hours since last interaction: {status['hours_since_last_interaction']:.1f}"
)
lines.append(f" Hours since last interaction: {status['hours_since_last_interaction']:.1f}")
lines.append(f" Circadian phase: {status['circadian_phase']}")
self._ch().add_message("system", "\n".join(lines))
except Exception as e:
@@ -3157,15 +2998,9 @@ class RuneApp(App):
lines = ["[bold]💰 Provider Budgets[/bold]"]
for name, p in status["providers"].items():
budget_str = (
f"${p['monthly_used']:.2f}/${p['monthly_limit']:.2f}"
if p["monthly_limit"] > 0
else "unlimited"
)
token_str = (
f"{p['tokens_used']}/{p['token_limit']}"
if p["token_limit"] > 0
else "unlimited"
f"${p['monthly_used']:.2f}/${p['monthly_limit']:.2f}" if p["monthly_limit"] > 0 else "unlimited"
)
token_str = f"{p['tokens_used']}/{p['token_limit']}" if p["token_limit"] > 0 else "unlimited"
lines.append(f" {name}: {budget_str}, {token_str} tokens")
if status["best_provider"]:
lines.append(f" Best: {status['best_provider']}")
@@ -3179,9 +3014,7 @@ class RuneApp(App):
import importlib.util
import work_manager
audit_path = os.path.join(
os.path.dirname(__file__), "skills", "llm-craft", "full_audit.py"
)
audit_path = os.path.join(os.path.dirname(__file__), "skills", "llm-craft", "full_audit.py")
spec = importlib.util.spec_from_file_location("full_audit", audit_path)
full_audit = importlib.util.module_from_spec(spec)
spec.loader.exec_module(full_audit)
@@ -3202,13 +3035,9 @@ class RuneApp(App):
lines.append(f" {sev}: {count}")
lines.append("")
for batch in summary.get("batch_details", []):
lines.append(
f" {batch['id']}: {batch['description']} ({batch['items']} items, {batch['model']})"
)
lines.append(f" {batch['id']}: {batch['description']} ({batch['items']} items, {batch['model']})")
lines.append("")
lines.append(
"[bold]Type /work start to begin processing, or /work clear to discard[/bold]"
)
lines.append("[bold]Type /work start to begin processing, or /work clear to discard[/bold]")
self._ch().add_message("confirm", "\n".join(lines))
except Exception as e:
self._ch().add_message("system", f"[dim]Error running audit: {e}[/dim]")
@@ -3231,9 +3060,7 @@ class RuneApp(App):
elif arg == "list":
pending = queue.get_pending()
if pending:
self._ch().add_message(
"system", f"[bold]Pending messages: {len(pending)}[/bold]"
)
self._ch().add_message("system", f"[bold]Pending messages: {len(pending)}[/bold]")
for msg in pending:
self._ch().add_message(
"system",
@@ -3244,25 +3071,19 @@ class RuneApp(App):
elif arg == "approve":
count = queue.approve_all()
self._ch().add_message(
"system", f"[bold]✅ Approved {count} messages[/bold]"
)
self._ch().add_message("system", f"[bold]✅ Approved {count} messages[/bold]")
elif arg.startswith("approve "):
msg_id = arg[8:]
if queue.approve(msg_id):
self._ch().add_message(
"system", f"[bold]✅ Approved: {msg_id}[/bold]"
)
self._ch().add_message("system", f"[bold]✅ Approved: {msg_id}[/bold]")
else:
self._ch().add_message("system", f"❌ Message not found: {msg_id}")
elif arg.startswith("reject "):
msg_id = arg[7:]
if queue.reject(msg_id):
self._ch().add_message(
"system", f"[bold]✅ Rejected: {msg_id}[/bold]"
)
self._ch().add_message("system", f"[bold]✅ Rejected: {msg_id}[/bold]")
else:
self._ch().add_message("system", f"❌ Message not found: {msg_id}")
@@ -3281,16 +3102,12 @@ class RuneApp(App):
# Send to task channel
self.task_channel.add_message("user", msg.message)
queue.mark_executed(msg.id)
self._ch().add_message(
"system", f"[bold]✅ Executed {len(approved)} messages[/bold]"
)
self._ch().add_message("system", f"[bold]✅ Executed {len(approved)} messages[/bold]")
else:
self._ch().add_message("system", "No approved messages to execute.")
else:
self._ch().add_message(
"system", "Usage: /queue [status|list|approve|reject|clear|execute]"
)
self._ch().add_message("system", "Usage: /queue [status|list|approve|reject|clear|execute]")
except Exception as e:
self._ch().add_message("system", f"[dim]Error: {e}[/dim]")
@@ -3306,9 +3123,7 @@ class RuneApp(App):
if os.path.exists(builds_dir):
builds = os.listdir(builds_dir)
if builds:
self._ch().add_message(
"system", f"[bold]Builds: {len(builds)}[/bold]"
)
self._ch().add_message("system", f"[bold]Builds: {len(builds)}[/bold]")
for build_id in sorted(builds):
try:
m = BuildManager(build_id)
@@ -3328,16 +3143,10 @@ class RuneApp(App):
description = arg[6:]
manager = BuildManager()
build_id = manager.start_build(description)
self._ch().add_message(
"system", f"[bold]✅ Build {build_id} started[/bold]"
)
self._ch().add_message(
"system", f" Worktree: {manager.state.worktree_path}"
)
self._ch().add_message("system", f"[bold]✅ Build {build_id} started[/bold]")
self._ch().add_message("system", f" Worktree: {manager.state.worktree_path}")
self._ch().add_message("system", f" Description: {description}")
self._ch().add_message(
"system", " Use /build next to proceed to next phase"
)
self._ch().add_message("system", " Use /build next to proceed to next phase")
elif arg.startswith("resume "):
build_id = arg[7:]
@@ -3374,22 +3183,12 @@ class RuneApp(App):
try:
manager = BuildManager(build_id)
status = manager.get_status()
self._ch().add_message(
"system", f"[bold]Build {build_id} Ledger[/bold]"
)
self._ch().add_message(
"system", f" Phase: {status['current_phase']}"
)
self._ch().add_message("system", f"[bold]Build {build_id} Ledger[/bold]")
self._ch().add_message("system", f" Phase: {status['current_phase']}")
self._ch().add_message("system", f" Batches: {status['batches']}")
self._ch().add_message(
"system", f" Corrections: {status['corrections']}"
)
self._ch().add_message(
"system", f" Worktree: {status['worktree']}"
)
self._ch().add_message(
"system", f" Ledger entries: {status['ledger_entries']}"
)
self._ch().add_message("system", f" Corrections: {status['corrections']}")
self._ch().add_message("system", f" Worktree: {status['worktree']}")
self._ch().add_message("system", f" Ledger entries: {status['ledger_entries']}")
except FileNotFoundError:
self._ch().add_message("system", f"Build {build_id} not found")
@@ -3412,15 +3211,9 @@ class RuneApp(App):
meta = work_manager.get_meta_status()
lines = ["[bold]📋 Work Manager[/bold]"]
if meta.get("status") != "no_task":
lines.append(
f" Task: {meta.get('description', '?')} [{meta.get('status', '?')}]"
)
lines.append(
f" Progress: {meta.get('progress', '0%')} ({meta.get('items', '?')} items)"
)
lines.append(
f" Batches: {meta.get('batches', '?')} | Failed: {meta.get('failed', 0)}"
)
lines.append(f" Task: {meta.get('description', '?')} [{meta.get('status', '?')}]")
lines.append(f" Progress: {meta.get('progress', '0%')} ({meta.get('items', '?')} items)")
lines.append(f" Batches: {meta.get('batches', '?')} | Failed: {meta.get('failed', 0)}")
lines.append(f" ETA: {meta.get('eta', '?')}")
lines.append(
f" Routing: {status['model_routing']} (local: {status['local_model']}, cloud: {status['cloud_model']})"
@@ -3431,9 +3224,7 @@ class RuneApp(App):
if batch:
lines = [f"[bold]Next batch: {batch.id}[/bold]"]
lines.append(f" {batch.description}")
lines.append(
f" Items: {batch.size} | Files: {', '.join(batch.files)}"
)
lines.append(f" Items: {batch.size} | Files: {', '.join(batch.files)}")
lines.append(f" Model: {batch.model}")
self._ch().add_message("system", "\n".join(lines))
else:
@@ -3448,9 +3239,7 @@ class RuneApp(App):
work_manager.save_config(config)
self._ch().add_message("system", f"Model routing: {routing}")
else:
self._ch().add_message(
"system", "Usage: /work model [local|cloud|auto]"
)
self._ch().add_message("system", "Usage: /work model [local|cloud|auto]")
else:
config = work_manager.load_config()
self._ch().add_message(
@@ -3473,9 +3262,7 @@ class RuneApp(App):
work_manager.save_batches(work_manager.load_batches())
# TODO: actually process the batch with the model
else:
self._ch().add_message(
"system", "[dim]No pending batches. Run /audit first.[/dim]"
)
self._ch().add_message("system", "[dim]No pending batches. Run /audit first.[/dim]")
elif arg == "clear":
work_manager.clear_work()
self._ch().add_message("system", "✅ Work cleared")
@@ -3580,9 +3367,7 @@ class RuneApp(App):
"[dim]❌ ollama command not found — is Ollama installed?[/dim]",
)
except Exception as e:
self.call_from_thread(
ch.add_message, "system", f"[dim]❌ Pull error: {e}[/dim]"
)
self.call_from_thread(ch.add_message, "system", f"[dim]❌ Pull error: {e}[/dim]")
self.run_worker(_pull, thread=True)
@@ -3801,19 +3586,11 @@ class RuneApp(App):
if BACKEND_AVAILABLE:
budget_data = provider_budget.get_status()
ratios = []
for provider in budget_data.get(
"providers", {}
).values():
for provider in budget_data.get("providers", {}).values():
if provider.get("monthly_limit", 0) > 0:
ratios.append(
provider.get("monthly_used", 0)
/ provider["monthly_limit"]
)
ratios.append(provider.get("monthly_used", 0) / provider["monthly_limit"])
if provider.get("token_limit", 0) > 0:
ratios.append(
provider.get("tokens_used", 0)
/ provider["token_limit"]
)
ratios.append(provider.get("tokens_used", 0) / provider["token_limit"])
usage_percent = max(ratios, default=0) * 100
if usage_percent > 80:
ch.add_message(
@@ -3835,15 +3612,14 @@ class RuneApp(App):
except Exception:
pass
# Auto-fire hooks after model call
try:
if BACKEND_AVAILABLE:
hook_engine.fire(
"post_message",
{"response": response[:100], "channel": channel},
)
except Exception:
pass
# NOTE (Runtime Reachability audit 2026-09-10): the
# old hook_engine.fire("post_message", ...) call here
# was dead — hook_engine defines no fire(); the
# AttributeError was silently swallowed, so "hooks"
# never ran. Removed rather than left as a false-
# liveness surface. Real hook dispatch is unwired
# (microkernel.fire has zero callers) — see the
# reachability audit, P3.
# Auto-check growth delta
try:
@@ -4074,6 +3850,7 @@ def main():
if __name__ == "__main__":
main()
class ResearchStatusLabel(Static):
"""Shows research process state — reads from the task tree JSON if present."""
@@ -4082,6 +3859,7 @@ class ResearchStatusLabel(Static):
def refresh_state(self) -> None:
import glob
trees = sorted(glob.glob("research/issue-*/task_tree.json"), reverse=True)
if not trees:
self.update("")