Compare commits

...
Author SHA1 Message Date
Nick 3497d2b7a9 feat(operator): forge projection wired into the failure path (T5.15 slice 5)
deploy / deploy (push) Skipped
CI Pipeline / go-surface (push) Successful in 4s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / lint (push) Successful in 38s
CI Pipeline / canary (push) Successful in 47s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / unit-tests (push) Failing after 4m56s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 2s
The final orphan: testing_mode.project_failure_to_forge was TEST_PROVEN with zero runtime callers. _project_failure_to_forge() invokes it from the operator failure path using the already-wired forge client (create_forge_client → NullAdapter when unconfigured = dormant by construction); a thin shim adapts create_issue to the projection's create_or_patch_release contract; ForgeUnavailable degrades to a marker, never breaking the failure path (rule 19P). Three contracts: projection fires, dormant-not-error, forge-down-never-breaks. Completion wave wire-up complete: all 5 orphans now have live callers.
2026-10-06 04:09:22 +01:00
Nick 60a21dc7da feat(scaffold): iteration impact-delta recorded at close (T5.15 slice 4)
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Successful in 33s
CI Pipeline / go-surface (push) Successful in 2s
CI Pipeline / canary (push) Successful in 43s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / type-check (push) Successful in 26s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / unit-tests (push) Successful in 4m53s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 2s
Slice 4 of the completion wave: a material closure (scope_files present) records a predicted-vs-observed impact delta into impact_deltas.jsonl (rule 19H — deltas compound into better future prediction). Governance-only closes record nothing (the delta ledger only carries material changes); delta failure degrades to a session record, never fails a closure. Two contracts, both red-first. Also resolves the stash collision: slice-3 and slice-4 test blocks both retained.
2026-10-06 03:54:01 +01:00
Nick 0873b521e3 feat(scaffold): programme_continuation wired into close() (T5.15 slice 3) + same-second contract-id collision fix
deploy / deploy (push) Skipped
CI Pipeline / go-surface (push) Successful in 4s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 2s
Auto-label / priority-gate (pull_request) Skipped
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / lint (push) Successful in 39s
Auto-label / auto-label (pull_request) Successful in 0s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / canary (push) Successful in 53s
CI Pipeline / go-surface (pull_request) Successful in 2s
CI Pipeline / lint (pull_request) Successful in 36s
CI Pipeline / best-practices (pull_request) Successful in 1s
CI Pipeline / documentation (pull_request) Successful in 2s
CI Pipeline / canary (pull_request) Successful in 41s
CI Pipeline / type-check (pull_request) Successful in 27s
CI Pipeline / gitea-offline (pull_request) Successful in 13s
Spec gate (headless) / spec-gate (pull_request) Successful in 4s
CI Pipeline / unit-tests (push) Successful in 4m59s
CI Pipeline / required-checks (push) Successful in 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 2s
CI Pipeline / unit-tests (pull_request) Successful in 4m54s
CI Pipeline / required-checks (pull_request) Successful in 0s
CI Pipeline / CI Pipeline / notify-event-bridge (pull_request) Successful in 1s
Auto-version and Release / release (pull_request) Successful in 5s
Slice 3 of the completion wave: closing a contract that is a stage of a persisted Programme marks that stage COMPLETE, persists the ledger, and rides the continuation canary on the close envelope — the next eligible stage is SURFACED, never guessed (task done ≠ programme done). No programme bound → no continuation block; ledger failure degrades without failing the closure. Three contracts. Also fixed en route: contract ids were second-granularity timestamps that collided when two needs filed in the same second (red test proves distinct ids now, uuid suffix added).
2026-10-06 03:46:24 +01:00
conductor f7e6c5cff5 chore(release): auto-bump version to 0.99.5 — earned by #2470 (TUI: chat pane LEFT + post-response dim-spam consolidation (T5.14/T5.13))
Auto-version and Release / release (push) Successful in 2s
CI Pipeline / unit-tests (push) Successful in 4m56s
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / go-surface (push) Successful in 3s
CI Pipeline / canary (push) Successful in 44s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 2s
deploy / deploy (push) Successful in 2m27s
Promotion record: http://127.0.0.1:3000/conductor/rune/pulls/2470
2026-10-06 03:27:08 +01:00
conductor fc46dbea52 Merge pull request 'TUI: chat pane LEFT + post-response dim-spam consolidation (T5.14/T5.13)' (#2470) from feat/tui-pane-swap-chat-left into master
Auto-version and Release / release (push) Failing after 3s
CI Pipeline / go-surface (push) Successful in 3s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / canary (push) Successful in 45s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / unit-tests (push) Successful in 4m56s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 2s
deploy / deploy (push) Successful in 5m49s
2026-10-06 03:27:02 +01:00
conductor d8ede8f3f8 Merge branch 'master' into feat/tui-pane-swap-chat-left
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Successful in 35s
CI Pipeline / go-surface (push) Successful in 2s
CI Pipeline / canary (push) Successful in 45s
CI Pipeline / best-practices (push) Successful in 1s
CI Pipeline / documentation (push) Successful in 1s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / lint (pull_request) Successful in 34s
CI Pipeline / go-surface (pull_request) Successful in 3s
CI Pipeline / canary (pull_request) Successful in 41s
CI Pipeline / best-practices (pull_request) Successful in 2s
CI Pipeline / documentation (pull_request) Successful in 1s
CI Pipeline / type-check (pull_request) Successful in 27s
CI Pipeline / gitea-offline (pull_request) Successful in 13s
Spec gate (headless) / spec-gate (pull_request) Successful in 5s
CI Pipeline / unit-tests (push) Successful in 4m55s
CI Pipeline / required-checks (push) Successful in 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 3s
CI Pipeline / unit-tests (pull_request) Successful in 4m54s
CI Pipeline / required-checks (pull_request) Successful in 1s
CI Pipeline / CI Pipeline / notify-event-bridge (pull_request) Successful in 2s
Auto-version and Release / release (pull_request) Successful in 4s
2026-10-06 03:19:13 +01:00
conductor 804c2f62df chore(release): auto-bump version to 0.99.4 — earned by #2472 (Scaffold: construction_commands dispatch — the serving wire (T5.15 slice 2))
Auto-version and Release / release (push) Successful in 2s
CI Pipeline / unit-tests (push) Successful in 5m3s
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / go-surface (push) Successful in 2s
CI Pipeline / canary (push) Successful in 44s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 1s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / required-checks (push) Successful in 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 1s
deploy / deploy (push) Successful in 5m9s
Promotion record: http://127.0.0.1:3000/conductor/rune/pulls/2472
2026-10-06 03:11:51 +01:00
conductor e5d10f18ae Merge pull request 'Scaffold: construction_commands dispatch — the serving wire (T5.15 slice 2)' (#2472) from feat/wire-construction-commands-dispatch into master
Auto-version and Release / release (push) Failing after 2s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / go-surface (push) Successful in 3s
CI Pipeline / best-practices (push) Successful in 1s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / canary (push) Successful in 45s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / unit-tests (push) Successful in 5m0s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 2s
deploy / deploy (push) Successful in 6m2s
2026-10-06 03:11:44 +01:00
conductor 3ed58d48a9 Merge branch 'master' into feat/tui-pane-swap-chat-left
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / go-surface (push) Successful in 2s
CI Pipeline / canary (push) Successful in 44s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 1s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / lint (pull_request) Successful in 33s
CI Pipeline / go-surface (pull_request) Successful in 2s
CI Pipeline / canary (pull_request) Successful in 40s
CI Pipeline / best-practices (pull_request) Successful in 1s
CI Pipeline / documentation (pull_request) Successful in 2s
CI Pipeline / type-check (pull_request) Successful in 27s
CI Pipeline / gitea-offline (pull_request) Successful in 12s
Spec gate (headless) / spec-gate (pull_request) Successful in 5s
CI Pipeline / unit-tests (push) Successful in 4m55s
CI Pipeline / unit-tests (pull_request) Successful in 5m0s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / required-checks (pull_request) Successful in 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 2s
CI Pipeline / CI Pipeline / notify-event-bridge (pull_request) Successful in 2s
2026-10-06 03:02:57 +01:00
conductor 6899d859da Merge branch 'master' into feat/tui-pane-swap-chat-left
deploy / deploy (push) Skipped
CI Pipeline / lint (push) Successful in 34s
CI Pipeline / go-surface (push) Successful in 3s
CI Pipeline / canary (push) Successful in 39s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 2s
CI Pipeline / type-check (push) Successful in 26s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / lint (pull_request) Successful in 34s
CI Pipeline / go-surface (pull_request) Successful in 2s
CI Pipeline / canary (pull_request) Successful in 45s
CI Pipeline / best-practices (pull_request) Successful in 1s
CI Pipeline / documentation (pull_request) Successful in 2s
CI Pipeline / type-check (pull_request) Successful in 26s
CI Pipeline / gitea-offline (pull_request) Successful in 12s
Spec gate (headless) / spec-gate (pull_request) Successful in 5s
CI Pipeline / unit-tests (push) Successful in 4m56s
CI Pipeline / unit-tests (pull_request) Successful in 4m54s
CI Pipeline / required-checks (push) Successful in 0s
CI Pipeline / required-checks (pull_request) Successful in 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 2s
CI Pipeline / CI Pipeline / notify-event-bridge (pull_request) Successful in 2s
2026-10-06 02:50:11 +01:00
Nick 0e88b6bc08 feat(tui): consolidate post-response dim spam into one collapsed line (#2468/T5.13)
deploy / deploy (push) Skipped
CI Pipeline / go-surface (push) Successful in 4s
CI Pipeline / best-practices (push) Successful in 2s
CI Pipeline / documentation (push) Successful in 1s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / lint (push) Successful in 38s
CI Pipeline / canary (push) Successful in 49s
CI Pipeline / gitea-offline (push) Successful in 13s
CI Pipeline / go-surface (pull_request) Successful in 2s
CI Pipeline / lint (pull_request) Successful in 35s
CI Pipeline / best-practices (pull_request) Successful in 2s
CI Pipeline / documentation (pull_request) Successful in 2s
CI Pipeline / canary (pull_request) Successful in 42s
CI Pipeline / type-check (pull_request) Successful in 27s
CI Pipeline / gitea-offline (pull_request) Successful in 13s
Spec gate (headless) / spec-gate (pull_request) Successful in 4s
CI Pipeline / unit-tests (push) Successful in 4m57s
CI Pipeline / unit-tests (pull_request) Successful in 4m54s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / required-checks (pull_request) Successful in 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 3s
CI Pipeline / CI Pipeline / notify-event-bridge (pull_request) Successful in 2s
Five separate dim system messages after every task/build response (budget, claims, growth, self-improve, plus the cron/queue echoes) spammed chat. New collect_post_response_signals() is a PURE headless collector with degrade-not-crash try blocks; the worker renders ONE line or silence. 6 frozen contracts: compose order [chat,task], resize semantics re-keyed, collector all-signals/silence/never-raises/cycle-multiple. 38 TUI/layout tests green; pyright error count unchanged at baseline 8.
2026-10-06 02:40:25 +01:00
Nick 004e5b3118 feat(tui): swap panes — chat LEFT, task RIGHT (#2465/T5.14)
Nick: chat-first feels natural. Compose yield order reversed (rune_tui.py compose), resize_left/resize_right actions re-keyed to the new visual positions (left=chat shrink now grows task), focus actions and all name-keyed query_one calls untouched (position-independent by design). Static source proof: yield order [chat, task]; 45 layout/TUI tests green.
2026-10-06 02:40:25 +01:00
10 changed files with 523 additions and 89 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.99.3**
**Current release: v0.99.5**
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.99.3"
version = "0.99.5"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+1 -1
View File
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
import json
from pathlib import Path
__version__ = "0.99.3"
__version__ = "0.99.5"
# One build identity (handoff 2026-09-10 §4): the semantic version above is
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
+1 -1
View File
@@ -233,7 +233,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.99.3"' in rune_version,
'__version__ = "0.99.5"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+78 -10
View File
@@ -36,6 +36,7 @@ from typing import Any
import json
import sys
import uuid
from pathlib import Path
from rune import structural_store as ss
@@ -421,7 +422,8 @@ class ConstructionInterface:
errors.append({"error": Errors.INSUFFICIENT_EVIDENCE, "detail": trace["rationale"][:200]})
contract = None
if generate and trace["resolution"] not in ("INSUFFICIENT_EVIDENCE",):
cid = contract_id or f"BUILDER-{ss.now()}-{self.builder}"
cid = (contract_id
or f"BUILDER-{ss.now()}-{uuid.uuid4().hex[:6]}-{self.builder}")
cd.generate_contract(
self.scaffold,
doc,
@@ -768,21 +770,87 @@ class ConstructionInterface:
material_closure = self._auto_material_change(
scope_files or [],
note=f"contract {contract_id} closed (material change)")
self._record_impact_delta(contract_id, scope_files or [])
continuation = self._programme_continuation(contract_id)
state = {
"phase": "closed",
"material_change_closure": material_closure,
"contract_quality": {
k: q[k]
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
if k in q
},
}
if continuation is not None:
state["programme_continuation"] = continuation
return _envelope(
"close",
success=True,
ids={"contract_id": contract_id},
state={
"phase": "closed",
"material_change_closure": material_closure,
"contract_quality": {
k: q[k]
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
if k in q
},
},
state=state,
)
def _record_impact_delta(self, contract_id: str, scope_files: list) -> None:
"""T5.15 slice 4 (the iteration wire): a material closure records a
predicted-vs-observed impact delta (rule 19H — deltas compound into
better future prediction). Governance-only closes (no scope files)
record nothing. Failure degrades: the ledger never fails a close."""
if not scope_files:
return
try:
from rune import iteration as it
it.record_impact_delta(
Path(self.scaffold.current).parent,
{
"contract_id": contract_id,
"scope_files": sorted(scope_files),
"predicted_frontier": "from-contract-scope",
"recorded_at": ss.now(),
},
)
except Exception as exc: # noqa: BLE001 — delta failure never fails a closure
self._record_session(
self.scaffold.load(), "impact_delta_degraded",
f"{contract_id}: {str(exc)[:120]}")
def _programme_continuation(self, contract_id: str) -> dict | None:
"""T5.15 slice 3 (the programme_continuation wire): after a legal
close, any persisted Programme stage bound to this contract is
marked COMPLETE and the continuation canary's verdict rides the
close envelope — 'task done' ≠ 'programme done', and the next
eligible stage is SURFACED, never guessed. No programme bound →
None (no continuation block in the envelope). Failures degrade:
a broken programme ledger never fails a real closure."""
try:
from pathlib import Path as _Path
from rune import programme_continuation as pc
base = _Path(self.scaffold.current).parent / pc.DEFAULT_PROGRAMME_AREA
if not base.is_dir():
return None
for prog_file in sorted(base.glob("*.json")):
prog = pc.Programme.load(base, prog_file.stem)
if prog is None:
continue
stage = next((s for s in prog.stages
if s.stage_id == contract_id
and s.state != pc.COMPLETE), None)
if stage is None:
continue
stage.state = pc.COMPLETE
stage.result["closed_at_contract"] = contract_id
prog.save(base)
return {
"programme_id": prog.programme_id,
"stage_marked_complete": contract_id,
"canary": prog.continuation_canary(),
}
except Exception as exc: # noqa: BLE001 — ledger failure never fails a closure
return {"error": f"programme continuation degraded: {exc}"[:200]}
return None
def _auto_material_change(self, scope_files: list[str], *,
note: str) -> dict:
"""T21.4 §21.4H (rule 19Q.2): governed construction closes → the
+47
View File
@@ -2525,6 +2525,53 @@ I have tools to read, write, edit, search, lint, test, format, and commit code.
_operator = None
def _forge_client_for_projection():
"""The client seam for the forge projection (test-overridable)."""
try:
from .forge_client import create_forge_client
except ImportError: # TUI top-level deploy (script mode)
from forge_client import create_forge_client
return create_forge_client()
def _project_failure_to_forge(*, behaviour_id: str, observation: dict,
repo: str) -> dict:
"""T5.15 slice 5 (the forge projection wire): a recorded failure
projects to the human-visible forge (rule 19P — the projection is a
VIEW; canonical truth stays in the ledger). Uses the ALREADY-WIRED
forge client (create_forge_client → NullAdapter when unconfigured, so
the projection is dormant by construction). A thin shim adapts the
client's create_issue to the testing-mode projection's
create_or_patch_release contract. Forge failure NEVER breaks the
failure path — it degrades to a recorded marker."""
import logging
log = logging.getLogger(__name__)
try:
client = _forge_client_for_projection()
class _Shim:
"""Adapts create_issue → the projection's release contract."""
@staticmethod
def create_or_patch_release(repo_: str, tag: str,
title: str, body: str) -> dict:
return client.create_issue(title, body)
try:
from .testing_mode import project_failure_to_forge
except ImportError:
from testing_mode import project_failure_to_forge
return project_failure_to_forge(
_Shim(), repo, behaviour_id=behaviour_id,
observation=observation)
except Exception as exc: # noqa: BLE001 — projection failure never rewrites truth
log.debug("forge projection degraded: %s", exc)
return {"projected": False,
"error": str(exc)[:200]}
def _wire_auto_filer() -> None:
"""Composition-root wiring for the failure auto-filer (#122).
+72 -75
View File
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
This is not an interface. This is how I see, speak, and interact with the world.
Version: v0.99.3
Version: v0.99.5
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -110,6 +110,56 @@ try:
except ImportError:
BACKEND_AVAILABLE = False
def collect_post_response_signals(response: str, messages_count: int) -> list[str]:
"""Gather the operational signals that used to each print a dim
system message after every task/build response (the dim-spam source,
T5.13): budget pressure, degraded claims, growth delta, the periodic
self-improvement cycle.
PURE and headless — no channel, no app. The caller renders ONE
collapsed line (or none when nothing fired). Queued-message execution
and on-complete cron firing stay in the worker: they move real
messages, not chatter, and keep their own echoes.
Returns short human-readable fragments; empty list = silence.
"""
signals: list[str] = []
if not BACKEND_AVAILABLE:
return signals
try:
budget_data = provider_budget.get_status()
ratios = []
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"])
if provider.get("token_limit", 0) > 0:
ratios.append(provider.get("tokens_used", 0) / provider["token_limit"])
usage_percent = max(ratios, default=0) * 100
if usage_percent > 80:
signals.append(f"⚠ Budget: {usage_percent:.0f}% used")
except Exception:
pass
try:
claims_result = claim_audit.scan_text(response[:500])
if claims_result and claims_result.get("degraded"):
signals.append(f"⚠ Claims degraded: {len(claims_result['degraded'])}")
except Exception:
pass
try:
growth = growth_delta.check()
if growth and growth.get("delta"):
signals.append(f"📈 Growth: {growth['delta']}")
except Exception:
pass
try:
if messages_count % 10 == 0:
self_improve.run()
signals.append("🔄 Self-improvement cycle triggered")
except Exception:
pass
return signals
from textual.app import App, ComposeResult
from textual.containers import Horizontal, Vertical, VerticalScroll
from textual.widgets import Button, Header, Input, ProgressBar, RichLog, Static
@@ -1533,18 +1583,21 @@ class RuneApp(App):
def compose(self) -> ComposeResult:
yield Header()
with Horizontal(id="channels"):
yield ChannelView(
"task",
"[b]TASK[/b] — focused work",
classes="channel-view",
id="task-channel",
)
# T5.14 (Nick 2026-10-05): chat LEFT, task RIGHT — feels more
# natural (conversation first, focused work second). WorkPane
# stays third.
yield ChannelView(
"chat",
"[b]CHAT[/b] — free conversation",
classes="channel-view",
id="chat-channel",
)
yield ChannelView(
"task",
"[b]TASK[/b] — focused work",
classes="channel-view",
id="task-channel",
)
# P1-07 (#30): third collapsible column — the operational index.
yield WorkPane(id="work-pane")
with Horizontal(id="footer-bar"):
@@ -1895,7 +1948,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.99.3 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.99.5 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
@@ -3737,69 +3790,13 @@ class RuneApp(App):
except Exception:
pass
# Auto-warn budget when >80% used
try:
if BACKEND_AVAILABLE:
budget_data = provider_budget.get_status()
ratios = []
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"])
if provider.get("token_limit", 0) > 0:
ratios.append(provider.get("tokens_used", 0) / provider["token_limit"])
usage_percent = max(ratios, default=0) * 100
if usage_percent > 80:
ch.add_message(
"system",
f"[dim]⚠ Budget: {usage_percent:.0f}% used[/dim]",
)
except Exception:
pass
# Auto-check claims after response
try:
if BACKEND_AVAILABLE:
claims_result = claim_audit.scan_text(response[:500])
if claims_result and claims_result.get("degraded"):
ch.add_message(
"system",
f"[dim]⚠ Claims degraded: {len(claims_result['degraded'])}[/dim]",
)
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:
if BACKEND_AVAILABLE:
growth = growth_delta.check()
if growth and growth.get("delta"):
ch.add_message(
"system",
f"[dim]📈 Growth: {growth['delta']}[/dim]",
)
except Exception:
pass
# Auto-improve every 10 messages
try:
if self.status_bar.messages_count % 10 == 0:
if BACKEND_AVAILABLE:
self_improve.run()
ch.add_message(
"system",
f"[dim]🔄 Self-improvement cycle triggered[/dim]",
)
except Exception:
pass
# T5.13 dim-spam consolidation: previously five
# separate dim system messages printed after every
# response. ONE collapsed line now — empty means
# silence, not a dash.
signals = collect_post_response_signals(response[:500], self.status_bar.messages_count)
if signals:
ch.add_message("system", "[dim]" + " · ".join(signals) + "[/dim]")
elif result.get("error") == "RESEARCH_REQUIRED":
# Nonterminal routing disposition, not failure (2026-09-10):
# the task hit the deep-research floor and was dispatched.
@@ -3980,13 +3977,13 @@ class RuneApp(App):
pass
def action_resize_left(self) -> None:
"""Shrink task channel, grow chat channel."""
self.task_width = max(20, self.task_width - 5)
"""Shrink the LEFT channel (chat, post-swap), grow the RIGHT (task)."""
self.task_width = min(80, self.task_width + 5)
self._apply_resize()
def action_resize_right(self) -> None:
"""Grow task channel, shrink chat channel."""
self.task_width = min(80, self.task_width + 5)
"""Grow the LEFT channel (chat), shrink the RIGHT (task)."""
self.task_width = max(20, self.task_width - 5)
self._apply_resize()
def _apply_resize(self):
+123
View File
@@ -6,6 +6,7 @@ import json
import os
import subprocess
import sys
from pathlib import Path
import pytest
@@ -671,3 +672,125 @@ def test_15f_transition_to_building_runs_no_guards(tmp_path):
iface.scaffold.commit(doc, what="attach guard")
env = iface.transition(cid, "approved") # NOT a completion edge
assert env["success"]
# ── T5.15 slice 3: programme_continuation wired into close() ────────────────
def _seed_programme(iface, programme_id, stage_contract_ids, tmp_path):
"""Persist a real Programme bound to the interface's scaffold store."""
from rune import programme_continuation as pc
base = Path(iface.scaffold.current).parent / pc.DEFAULT_PROGRAMME_AREA
prog = pc.Programme(programme_id=programme_id, objective="wave test")
for i, cid in enumerate(stage_contract_ids):
prog.stages.append(pc.ProgrammeStage(
stage_id=cid, objective=f"stage {i}", depends_on=[]))
prog.save(base)
return prog, base
def test_close_marks_programme_stage_complete_and_surfaces_continuation(tmp_path):
"""Closing a contract that is a stage of a persisted Programme marks
that stage COMPLETE and the envelope carries the continuation canary —
the programme's next eligible stage is surfaced, not guessed."""
from rune import programme_continuation as pc
iface = _iface(tmp_path)
env1 = _submit(iface, objective="wave stage one")
cid1 = env1["ids"]["contract_id"]
env2 = _submit(iface, objective="wave stage two")
cid2 = env2["ids"]["contract_id"]
# drive both through to verifying so close is legal
for cid in (cid1, cid2):
iface.transition(cid, "approved")
iface.transition(cid, "building")
iface.transition(cid, "verifying")
prog, base = _seed_programme(iface, "wave-x", [cid1, cid2], tmp_path)
env = iface.close(cid1, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}])
assert env["success"], env["errors"]
cont = env["state"].get("programme_continuation")
assert cont, "close envelope must carry the programme continuation"
assert cont["programme_id"] == "wave-x"
assert cont["stage_marked_complete"] == cid1
reloaded = pc.Programme.load(base, "wave-x")
stage1 = next(s for s in reloaded.stages if s.stage_id == cid1)
assert stage1.state == pc.COMPLETE
assert cont["canary"]["verdict"] == "PROGRAMME_OPEN"
assert cont["canary"]["next_action"] == ("start", cid2)
def test_close_without_programme_has_no_continuation_block(tmp_path):
iface = _iface(tmp_path)
env = _submit(iface)
cid = env["ids"]["contract_id"]
iface.transition(cid, "approved")
iface.transition(cid, "building")
iface.transition(cid, "verifying")
env = iface.close(cid, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}])
assert env["success"]
assert "programme_continuation" not in env["state"]
def test_close_programme_completion_verdict_surfaces(tmp_path):
"""Closing the LAST stage yields PROGRAMME_COMPLETE — the canary says
stop, the runner never guesses."""
iface = _iface(tmp_path)
env = _submit(iface)
cid = env["ids"]["contract_id"]
iface.transition(cid, "approved")
iface.transition(cid, "building")
iface.transition(cid, "verifying")
prog, base = _seed_programme(iface, "solo", [cid], tmp_path)
env = iface.close(cid, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}])
cont = env["state"]["programme_continuation"]
assert cont["canary"]["verdict"] == "PROGRAMME_COMPLETE"
def test_two_submits_same_second_get_distinct_contract_ids(tmp_path):
"""Second-granularity timestamps collide when two needs are filed in
the same second (real builder-pair scenario) — ids must be distinct."""
iface = _iface(tmp_path)
e1 = _submit(iface, objective="wave stage one")
e2 = _submit(iface, objective="wave stage two")
assert e1["success"] and e2["success"], (e1["errors"], e2["errors"])
assert e1["ids"]["contract_id"] != e2["ids"]["contract_id"]
# ── T5.15 slice 4: iteration impact-delta recorded at close ─────────────────
def test_close_records_impact_delta_when_scope_changed(tmp_path):
"""A closed contract with scope_files records a predicted-vs-observed
impact delta into the project store's impact_deltas.jsonl (rule 19H:
deltas compound into better future prediction)."""
from rune import iteration as it
iface = _iface(tmp_path)
env = _submit(iface)
cid = env["ids"]["contract_id"]
for ph in ("approved", "building", "verifying"):
iface.transition(cid, ph)
iface.close(cid, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}],
scope_files=["src/export_module.py"])
delta_file = tmp_path / ".rune" / "scaffold" / "impact_deltas.jsonl"
assert delta_file.exists(), "close must persist the impact delta"
import json as _json
deltas = [_json.loads(l) for l in delta_file.read_text().splitlines() if l.strip()]
assert any(d.get("contract_id") == cid for d in deltas)
d = next(d for d in deltas if d.get("contract_id") == cid)
assert d.get("scope_files") == ["src/export_module.py"]
assert "recorded_at" in d
def test_close_without_scope_records_no_delta(tmp_path):
"""A governance-only close (no scope files) records nothing — the
delta ledger only carries material changes."""
iface = _iface(tmp_path)
env = _submit(iface)
cid = env["ids"]["contract_id"]
for ph in ("approved", "building", "verifying"):
iface.transition(cid, ph)
iface.close(cid, observed_evidence=[{"entity": "capability:exporter", "detail": "done"}])
delta_file = tmp_path / ".rune" / "scaffold" / "impact_deltas.jsonl"
assert not delta_file.exists()
+84
View File
@@ -0,0 +1,84 @@
"""T5.15 slice 5: the forge projection wired into the operator failure path.
The projection (testing_mode.project_failure_to_forge) was TEST_PROVEN with
zero runtime callers. This wire invokes it from the operator's recorded-
failure sink using the ALREADY-WIRED forge client (create_forge_client —
NullAdapter when unconfigured → the projection is dormant by construction,
never an error source).
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
from rune import rune_operator as ro # noqa: E402
from rune import testing_mode as tm # noqa: E402
class RecordingAdapter:
"""Records create_issue calls — proves the projection fired."""
def __init__(self):
self.calls: list[tuple[str, str]] = []
def create_issue(self, title: str, body: str) -> dict:
self.calls.append((title, body))
return {"created": True, "number": len(self.calls)}
def test_recorded_failure_projects_to_forge(monkeypatch):
adapter = RecordingAdapter()
monkeypatch.setattr(ro, "_forge_client_for_projection", lambda: adapter)
result = ro._project_failure_to_forge(
behaviour_id="behaviour-test-1",
observation={"expected": "tests pass", "observed": "2 failed",
"observation_id": "obs-1", "reproduction": "pytest -k x"},
repo="conductor/rune",
)
# the projection returns the adapter's own dict (create_issue result) —
# the wire is proven by the adapter having been CALLED
assert len(adapter.calls) == 1
assert result.get("created") is True
title, _body = adapter.calls[0]
assert "behaviour-test-1" in title
def test_null_adapter_projection_is_dormant_not_error(monkeypatch):
"""Unconfigured forge → the projection returns a dormant marker; the
failure path is untouched."""
monkeypatch.setattr(ro, "_forge_client_for_projection", lambda: _NullAdapterProbe())
result = ro._project_failure_to_forge(
behaviour_id="b-2",
observation={"expected": "x", "observed": "y"},
repo="r",
)
assert result.get("created") is False
assert result.get("reason") == "no forge configured"
def test_forge_unavailable_never_breaks_the_failure_path(monkeypatch):
from rune import forge as _forge
def boom():
raise _forge.ForgeUnavailable("forge down")
monkeypatch.setattr(ro, "_forge_client_for_projection", lambda: boom())
result = ro._project_failure_to_forge(
behaviour_id="b-3",
observation={"expected": "x", "observed": "y"},
repo="r",
)
assert result["projected"] is False
assert "forge down" in result.get("error", "")
class _NullAdapterProbe:
"""Mimics forge_client.NullAdapter's shape for the dormant test."""
def create_issue(self, title: str, body: str) -> dict:
return {"created": False, "reason": "no forge configured"}
@@ -0,0 +1,115 @@
"""T5.13/T5.14 frozen contracts: pane order and post-response silence.
The pane swap is structural (compose order); the dim-spam consolidation
is behavioral (one collapsed line instead of five dim messages).
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import Mock
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "src"))
from rune import rune_tui # noqa: E402 — the TUI lives in the package
def test_chat_channel_is_yielded_first_in_compose():
src = (ROOT / "src" / "rune" / "rune_tui.py").read_text()
body = re.search(
r'with Horizontal\(id="channels"\):(.*?)with Horizontal\(id="footer-bar"\)',
src,
re.DOTALL,
).group(1)
order = re.findall(r'ChannelView\(\s*"(\w+)"', body)
assert order == ["chat", "task"], f"compose yields {order}"
def test_resize_actions_follow_the_new_visual_positions():
"""After the swap LEFT=chat, RIGHT=task. resize_left shrinks the LEFT
(chat) — which GROWS the task share; resize_right is the reverse."""
assert "grow the RIGHT (task)" in rune_tui.RuneApp.action_resize_left.__doc__
assert "shrink the RIGHT (task)" in rune_tui.RuneApp.action_resize_right.__doc__
# Behavioral proof against an instance WITHOUT constructing the App
# (reactive initialization needs a running textual app; the docstring
# carries the contract, the arithmetic is what matters).
app = SimpleNamespace(
task_width=50,
action_resize_left=None,
action_resize_right=None,
_apply_resize=None,
)
app._apply_resize = lambda: None
# Bind the real methods onto the stand-in and call them.
rune_tui.RuneApp.action_resize_left(app)
assert app.task_width == 55
rune_tui.RuneApp.action_resize_right(app)
assert app.task_width == 50
def _isolate_collector(monkeypatch, **modules):
monkeypatch.setattr(rune_tui, "BACKEND_AVAILABLE", True)
for name, value in modules.items():
monkeypatch.setattr(rune_tui, name, value)
def test_collector_returns_all_signals_in_one_pass(monkeypatch):
_isolate_collector(
monkeypatch,
provider_budget=SimpleNamespace(
get_status=lambda: {
"providers": {"p": {"monthly_limit": 100, "monthly_used": 90, "token_limit": 0, "tokens_used": 0}}
}
),
claim_audit=SimpleNamespace(scan_text=lambda _t: {"degraded": ["a", "b"]}),
growth_delta=SimpleNamespace(check=lambda: {"delta": "focus: +1"}),
self_improve=SimpleNamespace(run=lambda: None),
)
signals = rune_tui.collect_post_response_signals("response text", messages_count=10)
assert signals == ["⚠ Budget: 90% used", "⚠ Claims degraded: 2", "📈 Growth: focus: +1", "🔄 Self-improvement cycle triggered"]
def test_collector_silence_when_nothing_fires(monkeypatch):
_isolate_collector(
monkeypatch,
provider_budget=Mock(get_status=lambda: {"providers": {}}),
claim_audit=Mock(scan_text=lambda _t: None),
growth_delta=Mock(check=lambda: None),
self_improve=Mock(run=lambda: None),
)
collector = rune_tui.collect_post_response_signals
app_signals = collector("plain", messages_count=7) # not a cycle multiple
assert app_signals == []
def test_collector_never_raises_on_backend_noise(monkeypatch):
def boom(*_a, **_k):
raise RuntimeError("backend gone")
_isolate_collector(
monkeypatch,
provider_budget=Mock(get_status=boom),
claim_audit=Mock(scan_text=boom),
growth_delta=Mock(check=boom),
self_improve=Mock(run=boom),
)
assert rune_tui.collect_post_response_signals("x", 10) == []
def test_self_improve_only_at_cycle_multiple(monkeypatch):
ran = []
_isolate_collector(
monkeypatch,
provider_budget=Mock(get_status=lambda: {"providers": {}}),
claim_audit=Mock(scan_text=lambda _t: None),
growth_delta=Mock(check=lambda: None),
self_improve=Mock(run=lambda: ran.append(1)),
)
rune_tui.collect_post_response_signals("x", 7)
assert ran == []
rune_tui.collect_post_response_signals("x", 10)
assert ran == [1]