Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fdfe195e7 | ||
|
|
71d2d63a36 | ||
|
|
0873b521e3 | ||
|
|
f7e6c5cff5 | ||
|
|
fc46dbea52 | ||
|
|
d8ede8f3f8 | ||
|
|
804c2f62df | ||
|
|
e5d10f18ae | ||
|
|
3ed58d48a9 | ||
|
|
6c997b6af4 | ||
|
|
9b5befb137 | ||
|
|
6899d859da | ||
|
|
8ef70448c3 | ||
|
|
55f268f978 | ||
|
|
320026a4a3 | ||
|
|
0e88b6bc08 | ||
|
|
004e5b3118 | ||
|
|
db15ae3650 | ||
|
|
d8ce21b76d | ||
|
|
d4d43dbf67 | ||
|
|
8a6c950140 | ||
|
|
d67fc46581 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.99.1**
|
||||
**Current release: v0.99.6**
|
||||
|
||||
**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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.99.1"
|
||||
version = "0.99.6"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.99.1"
|
||||
__version__ = "0.99.6"
|
||||
|
||||
# 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
|
||||
|
||||
@@ -233,7 +233,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.99.1"' in rune_version,
|
||||
'__version__ = "0.99.6"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -171,3 +171,98 @@ def _accept(command_id: str, construction_id: str, correlation_id: str,
|
||||
"recorded_at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime()),
|
||||
"idempotent_replay": False}
|
||||
|
||||
|
||||
def dispatch(command_type: str, *, construction_id: str,
|
||||
correlation_id: str = "", command_id: str = "",
|
||||
payload: dict | None = None,
|
||||
store_path: str = "store/scaffold") -> dict:
|
||||
"""The SERVING layer (T5.15 slice 2): validate the command against the
|
||||
canonical store, execute the accepted intent through the Construction
|
||||
Interface, and return the interface's envelope with the command
|
||||
provenance attached.
|
||||
|
||||
Single entry point for `rune scaffold <command>` (the Go proxy) and
|
||||
any future transport. Idempotency: a repeated command_id replays the
|
||||
recorded outcome without re-executing.
|
||||
|
||||
Refusal still returns success=False envelope? NO — a refused command
|
||||
is a VALID protocol outcome: the envelope carries accepted=False and
|
||||
success=True (the dispatch itself worked; the lifecycle refused the
|
||||
intent). Execution failures carry success=False.
|
||||
"""
|
||||
iface_path = None
|
||||
if store_path and store_path != "store/scaffold":
|
||||
iface_path = store_path
|
||||
try:
|
||||
from rune import construction_interface as ci
|
||||
|
||||
# store_path carries the serving layer's "path|project_id" contract
|
||||
# (a bare path = last segment is the project id, matching how the
|
||||
# Go proxy and the interface's own default name it).
|
||||
if iface_path:
|
||||
path_part, _, proj_part = iface_path.partition("|")
|
||||
proj = proj_part or path_part.rstrip("/").split("/")[-1]
|
||||
iface = ci.ConstructionInterface(builder="substrate",
|
||||
project_store=path_part,
|
||||
project_id=proj)
|
||||
else:
|
||||
iface = ci.ConstructionInterface(builder="substrate")
|
||||
except Exception as exc: # noqa: BLE001 — serving failure is an envelope, not a crash
|
||||
return {"accepted": False, "executed": False,
|
||||
"success": False, "error": f"serving unavailable: {exc}"[:200],
|
||||
"command_type": command_type,
|
||||
"construction_id": construction_id}
|
||||
|
||||
intent = command(command_type, construction_id=construction_id,
|
||||
correlation_id=correlation_id, command_id=command_id,
|
||||
payload=payload, scaffold=iface.scaffold)
|
||||
if intent.get("idempotent_replay"):
|
||||
# Repeated delivery: return the recorded outcome WITHOUT executing
|
||||
# again (the ledger's whole point). The first execution's envelope
|
||||
# is not re-derivable here, so the replay carries the intent and
|
||||
# the flag — the caller knows the effect already happened.
|
||||
return {"accepted": True, "executed": False,
|
||||
"success": True, "idempotent_replay": True,
|
||||
"op": intent.get("op"),
|
||||
"command_type": command_type,
|
||||
"construction_id": construction_id,
|
||||
"correlation_id": intent.get("correlation_id", ""),
|
||||
"command_id": intent.get("command_id", "")}
|
||||
if not intent.get("accepted"):
|
||||
return {"accepted": False, "executed": False, "success": True,
|
||||
"reason": intent.get("reason"),
|
||||
"command_type": command_type,
|
||||
"construction_id": construction_id,
|
||||
"correlation_id": intent.get("correlation_id", ""),
|
||||
"command_id": intent.get("command_id", ""),
|
||||
"idempotent_replay": intent.get("idempotent_replay", False)}
|
||||
|
||||
op = intent["op"]
|
||||
args = intent["args"]
|
||||
ops = {
|
||||
"resume": lambda: iface.resume(args["contract_id"]),
|
||||
"transition": lambda: iface.transition(
|
||||
args["contract_id"], args["phase"],
|
||||
evidence_ref=f"command:{intent['command_id']}"
|
||||
+ (f" approved_by:{args['approved_by']}" if args.get("approved_by") else "")),
|
||||
"amend_scope": lambda: iface.amend_scope(
|
||||
args["contract_id"], args["add_paths"],
|
||||
reason=args.get("reason", "substrate context"),
|
||||
authority=args.get("authority", "substrate")),
|
||||
}
|
||||
fn = ops.get(op)
|
||||
if fn is None:
|
||||
return {"accepted": True, "executed": False, "success": False,
|
||||
"error": f"no executor for op {op!r}",
|
||||
"command_type": command_type,
|
||||
"construction_id": construction_id}
|
||||
env = fn()
|
||||
return {"accepted": True, "executed": True,
|
||||
"success": bool(env.get("success")),
|
||||
"envelope": env,
|
||||
"command_type": command_type,
|
||||
"construction_id": construction_id,
|
||||
"correlation_id": intent.get("correlation_id", ""),
|
||||
"command_id": intent.get("command_id", ""),
|
||||
"idempotent_replay": False}
|
||||
@@ -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
|
||||
@@ -88,6 +89,39 @@ def _envelope(
|
||||
return env
|
||||
|
||||
|
||||
def _to_stage_check(spec: dict):
|
||||
"""One guard spec → a stage_guards.StageCheck (T5.15 wire)."""
|
||||
from rune import stage_guards as sg
|
||||
|
||||
return sg.StageCheck(
|
||||
name=str(spec.get("name", "guard")),
|
||||
kind=str(spec.get("kind", "assert")),
|
||||
argv=spec.get("argv"),
|
||||
output_pattern=str(spec.get("output_pattern", "")),
|
||||
path=str(spec.get("path", "")),
|
||||
check=spec.get("check"),
|
||||
description=str(spec.get("description", "")),
|
||||
)
|
||||
|
||||
|
||||
def _guard_specs_from_obligations(c: dict) -> list[dict]:
|
||||
"""The contract's guard declarations: verification_obligations rows
|
||||
shaped `guard:<json-spec>` (the decision path records them; anything
|
||||
else stays a free-text obligation for the projection). JSONDecodeError
|
||||
→ skipped with no invented facts."""
|
||||
specs: list[dict] = []
|
||||
for item in (c.get("identities") or {}).get("verification_obligations", []) or []:
|
||||
if not isinstance(item, str) or not item.startswith("guard:"):
|
||||
continue
|
||||
try:
|
||||
spec = json.loads(item[6:])
|
||||
if isinstance(spec, dict):
|
||||
specs.append(spec)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return specs
|
||||
|
||||
|
||||
class ConstructionInterface:
|
||||
"""The builder-facing construction lifecycle for ONE project target.
|
||||
|
||||
@@ -291,6 +325,13 @@ class ConstructionInterface:
|
||||
"transition": {
|
||||
"phase": {"type": "str", "required": True,
|
||||
"allowed": list(sc.PHASES)},
|
||||
"evidence_ref": {"type": "str", "required": False},
|
||||
"guard_checks": {
|
||||
"type": "list[dict] | None", "required": False,
|
||||
"notes": ("stage-guard specs run on the "
|
||||
"completion edges (verifying/closed); "
|
||||
"a failure BLOCKS with "
|
||||
"VERIFICATION_FAILED — T5.15 wire")},
|
||||
},
|
||||
"close": {
|
||||
"observed_evidence": {
|
||||
@@ -381,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,
|
||||
@@ -607,8 +649,57 @@ class ConstructionInterface:
|
||||
warnings=warnings,
|
||||
)
|
||||
|
||||
def transition(self, contract_id: str, phase: str, *, evidence_ref: str = "") -> dict:
|
||||
def transition(self, contract_id: str, phase: str, *, evidence_ref: str = "",
|
||||
guard_checks: list[dict] | None = None) -> dict:
|
||||
"""Move a contract phase. Completion-wave slice (T5.15, the
|
||||
stage_guards wire): entering `verifying` and `closed` runs the
|
||||
§15F/§15G guards — readiness/exit StageChecks and test-truth
|
||||
verification — when the contract declares them
|
||||
(identities.verification_obligations carries `guard:<spec>` rows,
|
||||
or guard_checks passes inline specs).
|
||||
|
||||
guard_checks spec: {"name", "kind": "command"|"path_exists"|"assert",
|
||||
"argv"/"path"/"output_pattern", "check": callable (in-process only),
|
||||
"description"}. A guard failure is a VERIFICATION_FAILED envelope:
|
||||
the phase does NOT move.
|
||||
|
||||
The legality map stays the scaffold's authority; guards ADD evidence
|
||||
obligations on the completion-facing edges (building→verifying,
|
||||
verifying→closed) — a tiny bounded change can pass guards=[] freely.
|
||||
"""
|
||||
doc = self.scaffold.load()
|
||||
c = self.scaffold.contract(doc, contract_id)
|
||||
if c is None:
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
errors=[{"error": Errors.UNKNOWN_CONTRACT, "detail": f"no contract {contract_id}"}],
|
||||
)
|
||||
if phase in ("verifying", "closed"):
|
||||
from rune import stage_guards as sg
|
||||
|
||||
specs = guard_checks
|
||||
if specs is None:
|
||||
specs = _guard_specs_from_obligations(c)
|
||||
if specs:
|
||||
stage = sg.StageContract(
|
||||
stage_id=f"{contract_id}:{phase}",
|
||||
contract_id=contract_id,
|
||||
ready_when=[_to_stage_check(s) for s in specs],
|
||||
done_when=[_to_stage_check(s) for s in specs],
|
||||
)
|
||||
canary = sg.readiness_canary(stage)
|
||||
if canary["verdict"] == "BLOCKED":
|
||||
return _envelope(
|
||||
"transition",
|
||||
success=False,
|
||||
ids={"contract_id": contract_id},
|
||||
state={"phase": c.get("phase"), "guard": canary},
|
||||
errors=[{"error": Errors.VERIFICATION_FAILED,
|
||||
"detail": "stage guards blocked the transition",
|
||||
"failures": canary["failures"]}],
|
||||
)
|
||||
try:
|
||||
self.scaffold.transition_contract(
|
||||
doc, contract_id, phase, evidence_ref=evidence_ref or f"builder:{self.builder}"
|
||||
@@ -679,21 +770,62 @@ class ConstructionInterface:
|
||||
material_closure = self._auto_material_change(
|
||||
scope_files or [],
|
||||
note=f"contract {contract_id} closed (material change)")
|
||||
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 _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
|
||||
@@ -1019,7 +1151,9 @@ def main(argv=None) -> int:
|
||||
if cmd == "verify":
|
||||
return _out(iface.verify(args[0], changed_files=(json.loads(args[1]) if len(args) > 1 else None)))
|
||||
if cmd == "transition":
|
||||
return _out(iface.transition(args[0], args[1], evidence_ref=args[2] if len(args) > 2 else ""))
|
||||
guard_checks = json.loads(args[3]) if len(args) > 3 else None
|
||||
return _out(iface.transition(args[0], args[1], evidence_ref=args[2] if len(args) > 2 else "",
|
||||
guard_checks=guard_checks))
|
||||
if cmd == "close":
|
||||
return _out(
|
||||
iface.close(
|
||||
|
||||
+72
-75
@@ -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.1
|
||||
Version: v0.99.6
|
||||
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.1 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.99.6 | 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):
|
||||
|
||||
@@ -19,7 +19,9 @@ from __future__ import annotations
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from rune.structural_store import add_entity, now
|
||||
from rune.structural_store import (
|
||||
add_entity, add_relationship, entity, new_doc, relationships, _sorted, now,
|
||||
)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
SRC_DIR = ROOT / "src" / "rune"
|
||||
@@ -56,6 +58,14 @@ def _place_languages(doc: dict) -> int:
|
||||
for e in doc.get("entities", []):
|
||||
eid = e.get("id", "")
|
||||
kind = e.get("kind", "")
|
||||
# AUTHORITY ORDER (Nick's dichotomy, 2026-10-06 correction): the
|
||||
# MEASURED classification map (import-evidence per module) decides
|
||||
# FIRST. The name-hint lists are only a fallback for entities with
|
||||
# no measurement — never allowed to override the evidence. A module
|
||||
# with neither measurement nor hint is "unclassified", not a silent
|
||||
# rust default.
|
||||
measured = str(CLASSIFICATION_SIDES.get(
|
||||
eid.split(":", 1)[1] if ":" in eid else eid, "") or "").upper()
|
||||
if eid.startswith("contract:"):
|
||||
lang = "n/a"
|
||||
elif kind == "rust-crate":
|
||||
@@ -66,14 +76,22 @@ def _place_languages(doc: dict) -> int:
|
||||
lang = "n/a"
|
||||
elif kind == "deployment_surface":
|
||||
lang = "go"
|
||||
elif measured == "RUST":
|
||||
lang = "rust"
|
||||
elif measured == "GO":
|
||||
lang = "go"
|
||||
elif measured == "TUI-LAST":
|
||||
lang = "python-tui (Wave 6)"
|
||||
elif measured == "TEST-INFRA" or measured == "RETIRES":
|
||||
lang = "test-infra" if measured == "TEST-INFRA" else "retires"
|
||||
elif "tui" in eid:
|
||||
lang = "python-tui (Wave 6)"
|
||||
elif any(h in eid for h in GO_HINTS):
|
||||
lang = "go"
|
||||
lang = "go (name hint)"
|
||||
elif any(h in eid for h in RUST_HINTS):
|
||||
lang = "rust"
|
||||
lang = "rust (name hint)"
|
||||
elif kind == "module":
|
||||
lang = "rust"
|
||||
lang = "unclassified"
|
||||
elif kind == "subsystem":
|
||||
# A subsystem is a CONTAINER — its language is its members'
|
||||
# majority side per the measured dichotomy. The evidence map is
|
||||
|
||||
+384
-348
File diff suppressed because it is too large.
Load diff
+6946
-6922
File diff suppressed because it is too large.
Load diff
+377
-353
File diff suppressed because it is too large.
Load diff
@@ -6,6 +6,7 @@ import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -585,3 +586,172 @@ def test_15u_discover_active_contract_migrated_to_interface(tmp_path):
|
||||
# capabilities() advertises the new op
|
||||
assert "discover_active_contract" in \
|
||||
iface.capabilities()["state"]["operations"]
|
||||
|
||||
|
||||
# ── T5.15 completion wave: stage_guards wired into transition ───────────────
|
||||
|
||||
|
||||
def _guard_contract(iface, tmp_path):
|
||||
"""A contract whose obligations declare a PATH guard and a failing
|
||||
command guard — proving both BLOCK shapes."""
|
||||
doc = iface.scaffold.load()
|
||||
ss.add_entity(
|
||||
doc, id="contract:guarded-1", kind="contract",
|
||||
identities={
|
||||
"objective": "guarded stage",
|
||||
"verification_obligations": [
|
||||
"guard:" + json.dumps({
|
||||
"name": "artifact present", "kind": "path_exists",
|
||||
"path": str(tmp_path / "artifact.txt"),
|
||||
}),
|
||||
],
|
||||
},
|
||||
evidence_refs=["e"],
|
||||
)
|
||||
iface.scaffold.commit(doc, what="seed guarded contract")
|
||||
doc = iface.scaffold.load()
|
||||
# move through the legal path so guards face verifying from building
|
||||
sc.Scaffold.transition_contract(iface.scaffold, doc, "guarded-1", "approved",
|
||||
evidence_ref="t")
|
||||
sc.Scaffold.transition_contract(iface.scaffold, doc, "guarded-1", "building",
|
||||
evidence_ref="t")
|
||||
iface.scaffold.commit(doc, what="advance to building")
|
||||
return "guarded-1"
|
||||
|
||||
|
||||
def test_15f_transition_to_verifying_blocked_when_guard_fails(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _guard_contract(iface, tmp_path)
|
||||
env = iface.transition(cid, "verifying")
|
||||
assert not env["success"]
|
||||
assert env["errors"][0]["error"] == ci.Errors.VERIFICATION_FAILED
|
||||
assert env["state"]["guard"]["verdict"] == "BLOCKED"
|
||||
# phase UNCHANGED — the blocked stage does not move
|
||||
doc = iface.scaffold.load()
|
||||
c = iface.scaffold.contract(doc, cid)
|
||||
assert c["phase"] == "building"
|
||||
|
||||
|
||||
def test_15f_transition_passes_when_guard_satisfied(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _guard_contract(iface, tmp_path)
|
||||
(tmp_path / "artifact.txt").write_text("real artifact")
|
||||
env = iface.transition(cid, "verifying")
|
||||
assert env["success"], env["errors"]
|
||||
doc = iface.scaffold.load()
|
||||
assert iface.scaffold.contract(doc, cid)["phase"] == "verifying"
|
||||
|
||||
|
||||
def test_15f_inline_guard_checks_override_obligations(tmp_path):
|
||||
iface = _iface(tmp_path)
|
||||
cid = _guard_contract(iface, tmp_path) # has the path guard, NOT satisfied
|
||||
env = iface.transition(
|
||||
cid, "verifying",
|
||||
guard_checks=[{"name": "inline truth", "kind": "command",
|
||||
"argv": [sys.executable, "-c", "print('ok')"]}],
|
||||
)
|
||||
assert env["success"], env["errors"]
|
||||
assert iface.scaffold.contract(iface.scaffold.load(), cid)["phase"] == "verifying"
|
||||
|
||||
|
||||
def test_15f_transition_to_building_runs_no_guards(tmp_path):
|
||||
"""Completion-facing edges ONLY (verifying/closed): building→verifying
|
||||
is guarded, but draft→approved passes freely even with obligations."""
|
||||
iface = _iface(tmp_path)
|
||||
doc = iface.scaffold.load()
|
||||
ss.add_entity(doc, id="contract:guarded-2", kind="contract",
|
||||
identities={"objective": "2"}, evidence_refs=["e"])
|
||||
iface.scaffold.commit(doc, what="seed guarded-2")
|
||||
cid = "guarded-2"
|
||||
doc = iface.scaffold.load()
|
||||
c = iface.scaffold.contract(doc, cid)
|
||||
c["identities"]["verification_obligations"] = [
|
||||
"guard:" + json.dumps({"name": "never", "kind": "path_exists",
|
||||
"path": "/nonexistent/for-sure"})
|
||||
]
|
||||
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"]
|
||||
@@ -0,0 +1,143 @@
|
||||
"""spine_language — the #2465 doc-level transforms (T5.15 keep-green:
|
||||
the new-module henxel demands these ship WITH tests; they were carved
|
||||
from spine.py whose battery covered them, but the carved module is the
|
||||
unit under test now)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune import spine_language as sl # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def doc():
|
||||
return sl.new_doc()
|
||||
|
||||
|
||||
def test_classification_sides_reads_the_measured_map():
|
||||
sides = sl._classification_sides()
|
||||
assert sides, "classification.json must be readable in-repo"
|
||||
assert sides.get("spine") == "RUST"
|
||||
assert sides.get("spine_language") == "RUST"
|
||||
|
||||
|
||||
def test_classification_sides_degrades_to_empty_when_unreadable(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(sl, "ROOT", tmp_path)
|
||||
assert sl._classification_sides() == {}
|
||||
|
||||
|
||||
def test_language_verdicts_kind_defaults(doc):
|
||||
sl.add_entity(doc, id="rust-crate:cognition_core", kind="rust-crate")
|
||||
sl.add_entity(doc, id="go-surface:rune-entry", kind="go-surface")
|
||||
sl.add_entity(doc, id="workflow:ci.yml", kind="workflow")
|
||||
sl.add_entity(doc, id="contract:BUILDER-x", kind="contract")
|
||||
sl._place_languages(doc)
|
||||
by = {e["id"]: e["identities"]["language"] for e in doc["entities"]}
|
||||
assert by["rust-crate:cognition_core"] == "rust"
|
||||
assert by["go-surface:rune-entry"] == "go"
|
||||
assert by["workflow:ci.yml"] == "n/a"
|
||||
assert by["contract:BUILDER-x"] == "n/a"
|
||||
|
||||
|
||||
def test_language_verdicts_measured_authoritative_then_hints(doc):
|
||||
"""Nick's dichotomy correction: the measured map decides; hints only
|
||||
label UNMEASURED entities and say so; nothing silently defaults."""
|
||||
sl.add_entity(doc, id="module:rune_tui", kind="module") # TUI-LAST measured
|
||||
sl.add_entity(doc, id="module:spine", kind="module") # RUST measured
|
||||
sl.add_entity(doc, id="module:totally_unknown_name", kind="module")
|
||||
sl._place_languages(doc)
|
||||
by = {e["id"]: e["identities"]["language"] for e in doc["entities"]}
|
||||
assert by["module:rune_tui"] == "python-tui (Wave 6)" # from the MAP
|
||||
assert by["module:spine"] == "rust" # from the MAP
|
||||
assert by["module:totally_unknown_name"] == "unclassified"
|
||||
|
||||
|
||||
def test_subsystem_language_from_member_majority(doc):
|
||||
"""identity: 7 RUST / 3 GO members per classification.json → mixed with
|
||||
counts carried in the verdict text (never a silent guess)."""
|
||||
for m in ("behavior", "identity_anchors", "persona_definition",
|
||||
"persona_gate", "prompt_sync", "voice_hybrid", "voice_injector",
|
||||
"voice_sync", "voicebox", "safety_override"):
|
||||
sl.add_entity(doc, id=f"module:{m}", kind="module")
|
||||
sl.add_relationship(doc, type="belongs_to",
|
||||
from_id=f"module:{m}", to_id="subsystem:identity",
|
||||
evidence_refs=["t"])
|
||||
sl.add_entity(doc, id="subsystem:identity", kind="subsystem")
|
||||
sl._place_languages(doc)
|
||||
e = sl.entity(doc, "subsystem:identity")
|
||||
lang = e["identities"]["language"]
|
||||
assert lang.startswith("mixed (rust ")
|
||||
assert "go" in lang
|
||||
|
||||
|
||||
def test_unqualified_discovery_becomes_construction_surface(doc):
|
||||
"""A bare file path from a closed contract is NOT a module — provenance
|
||||
preserved, kind honest, candidate stays DECLARED."""
|
||||
monkey_specs = [("example", [("src/rune/audit_ledger.py",
|
||||
"construction-surface", "candidate")])]
|
||||
original = sl._contract_discoveries
|
||||
sl._contract_discoveries = lambda: monkey_specs
|
||||
try:
|
||||
placed = sl._apply_contract_discoveries(doc)
|
||||
finally:
|
||||
sl._contract_discoveries = original
|
||||
assert placed == 1
|
||||
e = doc["entities"][0]
|
||||
assert e["id"] == "construction-surface:src/rune/audit_ledger.py"
|
||||
assert e["kind"] == "construction-surface"
|
||||
assert e["maturity"] == "DECLARED"
|
||||
assert e["evidence_refs"] == ["contract-discovery:example"]
|
||||
|
||||
|
||||
def test_legacy_unqualified_module_normalized_with_edges(doc):
|
||||
sl.add_entity(doc, id="src/rune/claim_audit.py", kind="module",
|
||||
maturity="PRESENT",
|
||||
evidence_refs=["contract-discovery:old"])
|
||||
sl.add_relationship(doc, type="depends_on",
|
||||
from_id="src/rune/claim_audit.py",
|
||||
to_id="module:other", evidence_refs=["t"])
|
||||
sl._normalize_discovery_ids(doc)
|
||||
assert doc["entities"][0]["id"] == \
|
||||
"construction-surface:src/rune/claim_audit.py"
|
||||
assert doc["entities"][0]["kind"] == "construction-surface"
|
||||
assert doc["relationships"][0]["from"] == \
|
||||
"construction-surface:src/rune/claim_audit.py"
|
||||
# Not in the observed set (no closed contract recorded it) → the
|
||||
# unsupported PRESENT claim is demoted to DECLARED; evidence survives.
|
||||
assert doc["entities"][0]["maturity"] == "DECLARED"
|
||||
assert doc["entities"][0]["evidence_refs"] == ["contract-discovery:old"]
|
||||
|
||||
|
||||
def test_discoveries_gather_reads_real_scaffold_store(tmp_path, monkeypatch):
|
||||
"""The gatherer only trusts CLOSED contracts' ext.contract_quality."""
|
||||
store = tmp_path / "scaffold"
|
||||
store.mkdir()
|
||||
doc = sl.new_doc()
|
||||
sl.add_entity(doc, id="contract:C1", kind="contract",
|
||||
ext={"contract_quality": {
|
||||
"expected_new_observed": ["rust-crate:kernel_x"],
|
||||
"candidate_corrections": [
|
||||
{"entity": "suite"}]}})
|
||||
sl.add_entity(doc, id="contract:C2", kind="contract",
|
||||
ext={"contract_quality": {
|
||||
"expected_new_observed": ["rust-crate:never"]}})
|
||||
for e in doc["entities"]:
|
||||
if e["id"] == "contract:C1":
|
||||
e["phase"] = "closed"
|
||||
if e["id"] == "contract:C2":
|
||||
e["phase"] = "building"
|
||||
monkeypatch.setattr(sl, "ROOT", tmp_path)
|
||||
(tmp_path / "store" / "scaffold").mkdir(parents=True, exist_ok=True)
|
||||
(tmp_path / "store" / "scaffold" / "current.json").write_text(
|
||||
json.dumps(doc))
|
||||
got = sl._contract_discoveries()
|
||||
assert got == [("C1", [("rust-crate:kernel_x", "rust-crate", "observed"),
|
||||
("suite", "construction-surface", "candidate")])]
|
||||
@@ -174,3 +174,76 @@ def test_16k_projection_schema_validated(tmp_path):
|
||||
assert proj.validate(bad) != []
|
||||
bad2 = dict(p, schema_version=999)
|
||||
assert proj.validate(bad2) != []
|
||||
|
||||
|
||||
# ── T5.15 slice 2: the dispatch wire (command → interface execution) ────────
|
||||
|
||||
|
||||
def test_dispatch_executes_accepted_transition_end_to_end(tmp_path):
|
||||
"""A START command on a draft contract executes through the interface:
|
||||
the phase actually moves — not just an accepted intent."""
|
||||
store = str(tmp_path / ".rune" / "scaffold")
|
||||
iface = cmds if False else None # noqa: F841 — readability anchor
|
||||
from rune import construction_interface as ci
|
||||
ciface = ci.ConstructionInterface(builder="seed", project_store=store, project_id="proj")
|
||||
ciface.scaffold.init(purpose="t")
|
||||
doc = ciface.scaffold.load()
|
||||
sc.add_entity(doc, id="contract:disp-1", kind="contract",
|
||||
identities={"objective": "dispatch"}, evidence_refs=["e"])
|
||||
doc["entities"][-1]["phase"] = "draft"
|
||||
ciface.scaffold.commit(doc, what="seed")
|
||||
out = cmds.dispatch("START", construction_id="disp-1",
|
||||
store_path=store + "|proj")
|
||||
# external-project store path: dispatch builds its own interface bound
|
||||
# to the same store
|
||||
assert out["executed"] is True, out
|
||||
assert out["success"] is True, out
|
||||
doc2 = ciface.scaffold.load()
|
||||
c = sc.entity(doc2, "contract:disp-1")
|
||||
assert c["phase"] == "approved"
|
||||
|
||||
|
||||
def test_dispatch_refusal_is_a_valid_protocol_outcome(tmp_path):
|
||||
"""START on a non-draft contract: the lifecycle refuses — the dispatch
|
||||
itself succeeds (success=True) with accepted=False."""
|
||||
store = str(tmp_path / ".rune" / "scaffold")
|
||||
from rune import construction_interface as ci
|
||||
ciface = ci.ConstructionInterface(builder="seed", project_store=store, project_id="proj")
|
||||
ciface.scaffold.init(purpose="t")
|
||||
doc = ciface.scaffold.load()
|
||||
sc.add_entity(doc, id="contract:disp-2", kind="contract",
|
||||
identities={"objective": "x"}, evidence_refs=["e"])
|
||||
doc["entities"][-1]["phase"] = "draft"
|
||||
ciface.scaffold.commit(doc, what="seed")
|
||||
out = cmds.dispatch("START", construction_id="disp-2",
|
||||
payload={"phase": "building"},
|
||||
store_path=store + "|proj")
|
||||
# contract starts in draft so START is ACCEPTED here; drive to building
|
||||
# first to prove the refusal shape:
|
||||
assert out["executed"] and out["success"]
|
||||
out2 = cmds.dispatch("START", construction_id="disp-2",
|
||||
store_path=store + "|proj")
|
||||
assert out2["accepted"] is False and out2["executed"] is False
|
||||
assert out2["success"] is True
|
||||
assert "draft" in out2["reason"]
|
||||
|
||||
|
||||
def test_dispatch_idempotent_replay_does_not_double_execute(tmp_path):
|
||||
"""Same command_id twice: the second is a replay — the phase does not
|
||||
move twice and idempotent_replay=True comes back."""
|
||||
store = str(tmp_path / ".rune" / "scaffold")
|
||||
from rune import construction_interface as ci
|
||||
ciface = ci.ConstructionInterface(builder="seed", project_store=store, project_id="proj")
|
||||
ciface.scaffold.init(purpose="t")
|
||||
doc = ciface.scaffold.load()
|
||||
sc.add_entity(doc, id="contract:disp-3", kind="contract",
|
||||
identities={"objective": "idem"}, evidence_refs=["e"])
|
||||
doc["entities"][-1]["phase"] = "draft"
|
||||
ciface.scaffold.commit(doc, what="seed")
|
||||
first = cmds.dispatch("START", construction_id="disp-3",
|
||||
command_id="fixed-cmd-1", store_path=store + "|proj")
|
||||
assert first["executed"] and first["success"]
|
||||
second = cmds.dispatch("START", construction_id="disp-3",
|
||||
command_id="fixed-cmd-1", store_path=store + "|proj")
|
||||
assert second["idempotent_replay"] is True
|
||||
assert second.get("executed") in (False, None) # replay ≠ execution
|
||||
@@ -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]
|
||||
Reference in new issue
Block a user