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:
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.*
|
||||
@@ -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")
|
||||
@@ -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
@@ -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("")
|
||||
|
||||
Reference in new issue
Block a user