Compare commits

...
7 Commits
Author SHA1 Message Date
conductor 15dbbc562a chore(release): auto-bump version to 0.98.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-05 23:56:19 +01:00
Nick 6eb5e06119 refactor(spine): carve the #2465 doc-level transforms into spine_language.py (#2461 keep-green)
spine.py crossed the 1200-line giant-file debt cap (1203) with the language-verdict addition — CI lint red. The three transforms (language placement, discovery-ID normalization, contract-discovery write-back + gatherer) move to spine_language.py intact, re-exported through spine for import-compat (structure unchanged: 256 entities, same verdicts; spine battery 87, canaries 19/19 + 21/21, henxels hold, spine.py back to 1034).
2026-10-05 23:53:33 +01:00
Nick 78e4750a77 feat(tier): tier_manager delegates get/promote/demote to the cognition_core kernels (#2461 W3.4 delegation owed)
The kernels and FFI landed in b0a682b but the Python publics still ran Python — the strangler breach this slice owed. Live probe caught the FFI-arg-shape defect: promote/demote take the FULL ENTRY (kernel recomputes current tier internally), not a tier string; passing a string silently returned threshold-default verdicts (promote cold went warm-to-hot WRONGLY via defaults, demote rock went cold ignoring rock protection). 12-case kernel==oracle sweep green: wash>=20, recency hot/warm/cold, naive-timestamp parity trap, rock protection. PY oracle bodies kept; RUNE_COGNITION_CORE=python pins fallback; canary_growth_pins 26/26 both engines; canary_unbounded 14/14.
2026-10-05 23:50:11 +01:00
conductor c3948bae98 chore(release): auto-bump version to 0.97.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-10-05 23:48:25 +01:00
Nick b8184a0f01 chore(spine): store rebuild carries subsystem language verdicts (#2465) 2026-10-05 23:47:58 +01:00
Nick 469b91f217 feat(spine): subsystem language verdicts from member majority (#2465 addition 3)
The five unassessed subsystems are containers: their language derives from member modules measured sides (classification.json, the W0 dichotomy map, rust and go counts carried in the verdict text). Unclassified members noted rather than guessed; unreadable classification degrades to unassessed. Spine canaries 19/19 and 21/21, spine battery 87 green.
2026-10-05 23:47:55 +01:00
Nick bb960b1656 test(gitea): canary the numeric-ID stale-label removal (#2467)
Replace the obsolete removeLabel(name) pattern assertion with the actual Gitea /labels/{id} request, check ID lookup under the 404 idempotency guard, and require real P0–P3 managed labels. Local canary 39/39; workflow JS regression passes.
2026-10-05 22:22:53 +01:00
9 changed files with 681 additions and 416 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.96.1**
**Current release: v0.98.0**
**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.96.1"
version = "0.98.0"
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.96.1"
__version__ = "0.98.0"
# 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
+6 -4
View File
@@ -19,7 +19,8 @@ def has_idempotent_label_removal(workflow: str) -> bool:
return bool(
re.search(
r"for \(const name of staleManaged\) \{\s*"
r"try \{\s*await github\.rest\.issues\.removeLabel\(\{.*?"
r"try \{.*?await github\.request\('DELETE /repos/\{owner\}/\{repo\}/issues/\{issue_number\}/labels/\{id\}', \{.*?"
r"id: currentLabelIds\.get\(name\),\s*"
r"\}\);\s*\} catch \(error\) \{\s*"
r"if \(error\.status !== 404\) \{\s*throw error;\s*\}\s*"
r"console\.log\(`Managed label already absent: \$\{name\}`\);\s*"
@@ -64,7 +65,7 @@ def main() -> int:
"infrastructure",
"life",
"custodian",
"priority: high",
"P0 — critical", "P1 — high", "P2 — medium", "P3 — low",
"bug",
"feature",
)
@@ -80,7 +81,8 @@ def main() -> int:
)
check(
"stale managed labels are removed",
"managedLabels.has" in auto_label and "!desiredLabels.has" in auto_label and "removeLabel" in auto_label,
"managedLabels.has" in auto_label and "!desiredLabels.has" in auto_label
and "currentLabelIds.get(name)" in auto_label,
failures,
)
check(
@@ -231,7 +233,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.96.1"' in rune_version,
'__version__ = "0.98.0"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+2 -2
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.96.1
Version: v0.98.0
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -1895,7 +1895,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.96.1 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.98.0 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
+6 -135
View File
@@ -53,6 +53,12 @@ from rune.structural_store import ( # shared generic core (SCAFFOLD-TRANCHE-001
reconcile_authorities, mark_conflict, resolve_status, authority_health,
dedupe_observations as _ss_dedupe_observations,
)
from rune.spine_language import ( # noqa: F401 — re-exported (import-compat): the
# #2465 doc-level transforms live here now (giant-file carve); tests and
# the canary_spine_meta read them THROUGH spine, exactly as before.
_classification_sides, _place_languages, _normalize_discovery_ids,
_apply_contract_discoveries, _contract_discoveries,
)
SCHEMA_VERSION = 1
@@ -337,141 +343,6 @@ def _rust_core():
return None
def _place_languages(doc: dict) -> int:
"""Stamp a language verdict per entity (no-op for entities that carry
one). Deterministic: substring classification over the entity id + the
kind; the exact same rule construction_decision.language_placement
implements (kept inline here to avoid a per-entity FFI crossing for
the spine's own 218 entities; the FFI path is used in contracts)."""
GO_HINTS = ("server", "transport", "dispatch", "entry", "bridge",
"hook_engine", "provider", "watch", "notif", "voice",
"gitea", "event_", "message_", "tool_exec", "work_manager")
RUST_HINTS = ("memory", "drift", "warm", "capab", "token", "spine",
"scaffold", "construct", "structural", "curator",
"consolidat", "elastic", "cognit", "abstract", "contract",
"ledger", "audit", "claim", "evidence", "behavio",
"verif", "pins", "witness", "anchor", "growth", "goal",
"salience", "store", "tui_", "rune_tui")
placed = 0
for e in doc.get("entities", []):
eid = e.get("id", "")
kind = e.get("kind", "")
if eid.startswith("contract:"):
lang = "n/a"
elif kind == "rust-crate":
lang = "rust"
elif kind == "go-surface":
lang = "go"
elif kind == "workflow" or kind == "config":
lang = "n/a"
elif kind == "deployment_surface":
lang = "go"
elif "tui" in eid:
lang = "python-tui (Wave 6)"
elif any(h in eid for h in GO_HINTS):
lang = "go"
elif any(h in eid for h in RUST_HINTS):
lang = "rust"
elif kind == "module":
lang = "rust"
else:
lang = "unassessed"
e.setdefault("identities", {})["language"] = lang
if lang != "unassessed":
placed += 1
return placed
def _normalize_discovery_ids(doc: dict) -> int:
"""Migrate the old unqualified contract references, preserving evidence.
A file path (or 'suite') is not a module ID. Only migrate the malformed
module records produced by the earlier discovery projector; leave all
other open-world entity classes alone.
"""
renamed = {}
observed = {
eid for _, entities in _contract_discoveries()
for eid, _, provenance in entities if provenance == "observed"
}
for e in doc.get("entities", []):
if (e.get("kind") == "module" and ":" not in e.get("id", "")
and any(ref.startswith("contract-discovery:")
for ref in e.get("evidence_refs", []))):
old = e["id"]
e["id"] = f"construction-surface:{old}"
e["kind"] = "construction-surface"
e.setdefault("identities", {})["source_ref"] = old
renamed[old] = e["id"]
if e.get("kind") == "construction-surface" and e.get("id", "").startswith("construction-surface:"):
# Earlier code promoted every *candidate* to PRESENT. Undo only
# that unsupported claim; an actually observed surface retains it.
if e["id"].split(":", 1)[1] not in observed:
e["maturity"] = "DECLARED"
e["tags"] = [tag for tag in e.get("tags", []) if tag != "code"]
for rel in doc.get("relationships", []):
for endpoint in ("from", "to"):
if rel.get(endpoint) in renamed:
rel[endpoint] = renamed[rel[endpoint]]
return len(renamed)
def _apply_contract_discoveries(doc: dict) -> int:
"""Contract-discovery write-back (issue #2465 addition 2): entities
DISCOVERED by closed contracts become modeled spine entities — the
spine learns what construction actually built. Doc-level transform,
applied to BOTH cores' output. The CANDIDATE law holds: only CLOSED
contracts write back."""
placed = 0
for cid, entities in _contract_discoveries():
for eid, kind, provenance in entities:
before = len(doc.get("entities", []))
if kind == "construction-surface":
eid = f"construction-surface:{eid}"
add_entity(doc, id=eid, kind=kind,
tags=["contract-discovery"],
identities={"source_ref": eid.split(":", 1)[1]} if kind == "construction-surface" else None,
evidence_refs=[f"contract-discovery:{cid}"],
maturity="PRESENT" if provenance == "observed" else "DECLARED")
if len(doc.get("entities", [])) > before:
placed += 1
else:
placed += 0 # high-water merge — no new entity, still evidence
return placed
def _contract_discoveries():
"""Closed contracts' DISCOVERED entities (issue #2465 addition 2):
expected_new_observed + candidate corrections, gathered from the
scaffold store. Returns [(contract_id, [(entity_id, kind), ...])]."""
try:
doc = json.loads((ROOT / "store" / "scaffold" / "current.json").read_text())
except (OSError, json.JSONDecodeError):
return []
out = []
for e in doc.get("entities", []):
cid = e.get("id", "")
if not cid.startswith("contract:") or e.get("phase") != "closed":
continue
q = e.get("ext", {}).get("contract_quality", {})
found = []
# expected-new surfaces observed at close (REAL delivered entities)
for eid in q.get("expected_new_observed", []):
kind = eid.split(":", 1)[0] if ":" in eid else "construction-surface"
if kind in ("rust-crate", "go-surface", "config", "workflow", "module"):
found.append((eid, kind, "observed"))
elif kind == "construction-surface":
found.append((eid, kind, "observed"))
# candidate corrections: the closure flagged them for modeling
for cand in q.get("candidate_corrections", []):
eid = cand.get("entity", "")
kind = eid.split(":", 1)[0] if ":" in eid else "construction-surface"
if eid and kind in ("rust-crate", "go-surface", "config", "workflow", "module", "construction-surface"):
found.append((eid, kind, "candidate"))
if found:
out.append((cid[9:], found))
return out
def _gather_build_inputs(manifest_path: Path | None = None) -> dict:
"""Every I/O read build() needs, in one place (zero-dep split law:
+200
View File
@@ -0,0 +1,200 @@
"""spine_language.py — the #2465 doc-level transforms (carved from spine.py
to honor the giant-file henxel: keep it under 1200 lines).
Three transforms, applied to BOTH cores' output at build time:
1. language placement — every entity gets a language verdict (kind
defaults; subsystems derive from member-majority over the measured
classification map);
2. discovery-ID normalization — migrate legacy unqualified contract
discoveries to construction-surface kind without losing evidence;
3. contract-discovery write-back — closed contracts teach the spine
what construction actually built (observed → PRESENT, candidate →
DECLARED; the CANDIDATE law holds).
Same dependency laws as spine.py: structural_store primitives only, no
I/O beyond the classification/scaffold store reads, deterministic.
"""
from __future__ import annotations
import json
from pathlib import Path
from rune.structural_store import add_entity, now
ROOT = Path(__file__).resolve().parents[2]
SRC_DIR = ROOT / "src" / "rune"
def _classification_sides() -> dict:
"""The measured dichotomy map (W0): module stem → RUST|GO|TUI-LAST|...
Loaded once per _place_languages call, best-effort — a missing or
unreadable classification.json degrades to {}/unassessed verdicts,
never to invented facts."""
try:
data = json.loads((ROOT / "store" / "rebase" / "classification.json").read_text())
return {name: rec.get("side", "") for name, rec in data.items() if isinstance(rec, dict)}
except (OSError, json.JSONDecodeError, AttributeError):
return {}
def _place_languages(doc: dict) -> int:
"""Stamp a language verdict per entity (no-op for entities that carry
one). Deterministic: substring classification over the entity id + the
kind; the exact same rule construction_decision.language_placement
implements (kept inline here to avoid a per-entity FFI crossing for
the spine's own 218 entities; the FFI path is used in contracts)."""
GO_HINTS = ("server", "transport", "dispatch", "entry", "bridge",
"hook_engine", "provider", "watch", "notif", "voice",
"gitea", "event_", "message_", "tool_exec", "work_manager")
RUST_HINTS = ("memory", "drift", "warm", "capab", "token", "spine",
"scaffold", "construct", "structural", "curator",
"consolidat", "elastic", "cognit", "abstract", "contract",
"ledger", "audit", "claim", "evidence", "behavio",
"verif", "pins", "witness", "anchor", "growth", "goal",
"salience", "store", "tui_", "rune_tui")
placed = 0
CLASSIFICATION_SIDES = _classification_sides()
for e in doc.get("entities", []):
eid = e.get("id", "")
kind = e.get("kind", "")
if eid.startswith("contract:"):
lang = "n/a"
elif kind == "rust-crate":
lang = "rust"
elif kind == "go-surface":
lang = "go"
elif kind == "workflow" or kind == "config":
lang = "n/a"
elif kind == "deployment_surface":
lang = "go"
elif "tui" in eid:
lang = "python-tui (Wave 6)"
elif any(h in eid for h in GO_HINTS):
lang = "go"
elif any(h in eid for h in RUST_HINTS):
lang = "rust"
elif kind == "module":
lang = "rust"
elif kind == "subsystem":
# A subsystem is a CONTAINER — its language is its members'
# majority side per the measured dichotomy. The evidence map is
# classification.json (measured, 174 modules; honest note: newer
# files may be unclassified, so the count of classified members
# rides the verdict). Members arrive as belongs_to relationships
# whose from-ids carry module:<name> — map through classification.
members = [
r.get("from", "").split(":", 1)[1]
for r in doc.get("relationships", [])
if r.get("type") == "belongs_to" and r.get("to") == eid
and r.get("from", "").startswith("module:")
]
sides = [
str(CLASSIFICATION_SIDES.get(m) or "").upper()
for m in members
]
rust = sides.count("RUST")
go = sides.count("GO")
if rust and go:
lang = f"mixed (rust {rust} / go {go} members)"
elif rust:
lang = "rust"
elif go:
lang = "go"
else:
lang = "unassessed"
else:
lang = "unassessed"
e.setdefault("identities", {})["language"] = lang
if lang != "unassessed":
placed += 1
return placed
def _normalize_discovery_ids(doc: dict) -> int:
"""Migrate the old unqualified contract references, preserving evidence.
A file path (or 'suite') is not a module ID. Only migrate the malformed
module records produced by the earlier discovery projector; leave all
other open-world entity classes alone.
"""
renamed = {}
observed = {
eid for _, entities in _contract_discoveries()
for eid, _, provenance in entities if provenance == "observed"
}
for e in doc.get("entities", []):
if (e.get("kind") == "module" and ":" not in e.get("id", "")
and any(ref.startswith("contract-discovery:")
for ref in e.get("evidence_refs", []))):
old = e["id"]
e["id"] = f"construction-surface:{old}"
e["kind"] = "construction-surface"
e.setdefault("identities", {})["source_ref"] = old
renamed[old] = e["id"]
if e.get("kind") == "construction-surface" and e.get("id", "").startswith("construction-surface:"):
# Earlier code promoted every *candidate* to PRESENT. Undo only
# that unsupported claim; an actually observed surface retains it.
if e["id"].split(":", 1)[1] not in observed:
e["maturity"] = "DECLARED"
e["tags"] = [tag for tag in e.get("tags", []) if tag != "code"]
for rel in doc.get("relationships", []):
for endpoint in ("from", "to"):
if rel.get(endpoint) in renamed:
rel[endpoint] = renamed[rel[endpoint]]
return len(renamed)
def _apply_contract_discoveries(doc: dict) -> int:
"""Contract-discovery write-back (issue #2465 addition 2): entities
DISCOVERED by closed contracts become modeled spine entities — the
spine learns what construction actually built. Doc-level transform,
applied to BOTH cores' output. The CANDIDATE law holds: only CLOSED
contracts write back."""
placed = 0
for cid, entities in _contract_discoveries():
for eid, kind, provenance in entities:
before = len(doc.get("entities", []))
if kind == "construction-surface":
eid = f"construction-surface:{eid}"
add_entity(doc, id=eid, kind=kind,
tags=["contract-discovery"],
identities={"source_ref": eid.split(":", 1)[1]} if kind == "construction-surface" else None,
evidence_refs=[f"contract-discovery:{cid}"],
maturity="PRESENT" if provenance == "observed" else "DECLARED")
if len(doc.get("entities", [])) > before:
placed += 1
else:
placed += 0 # high-water merge — no new entity, still evidence
return placed
def _contract_discoveries():
"""Closed contracts' DISCOVERED entities (issue #2465 addition 2):
expected_new_observed + candidate corrections, gathered from the
scaffold store. Returns [(contract_id, [(entity_id, kind), ...])]."""
try:
doc = json.loads((ROOT / "store" / "scaffold" / "current.json").read_text())
except (OSError, json.JSONDecodeError):
return []
out = []
for e in doc.get("entities", []):
cid = e.get("id", "")
if not cid.startswith("contract:") or e.get("phase") != "closed":
continue
q = e.get("ext", {}).get("contract_quality", {})
found = []
# expected-new surfaces observed at close (REAL delivered entities)
for eid in q.get("expected_new_observed", []):
kind = eid.split(":", 1)[0] if ":" in eid else "construction-surface"
if kind in ("rust-crate", "go-surface", "config", "workflow", "module"):
found.append((eid, kind, "observed"))
elif kind == "construction-surface":
found.append((eid, kind, "observed"))
# candidate corrections: the closure flagged them for modeling
for cand in q.get("candidate_corrections", []):
eid = cand.get("entity", "")
kind = eid.split(":", 1)[0] if ":" in eid else "construction-surface"
if eid and kind in ("rust-crate", "go-surface", "config", "workflow", "module", "construction-surface"):
found.append((eid, kind, "candidate"))
if found:
out.append((cid[9:], found))
return out
+73 -2
View File
@@ -20,6 +20,24 @@ HOT_THRESHOLD = 7 # accessed within 7 days = hot
WARM_THRESHOLD = 30 # accessed within 30 days = warm
# > 30 days = cold
def _core():
"""The Rust cognition_core when available (strangler: Python fallback is
load-bearing). RUNE_COGNITION_CORE=python pins the fallback."""
try:
from rune import cc_loader
except ImportError: # standalone script execution
import cc_loader
try:
return cc_loader if cc_loader.available() else None
except Exception: # availability probe must never crash the fallback
return None
def _py_core_now() -> str:
"""The injected clock (iso Z) — module-level for the kernel boundary."""
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
# Tier storage characteristics
TIER_CONFIG = {
"hot": {
@@ -56,7 +74,24 @@ class TierManager:
"""Determine the tier for an entry based on its access patterns.
Returns 'hot', 'warm', or 'cold'.
Delegates to the Rust kernel when built; the Python body below is
the parity oracle (W3.4: identical thresholds injected, naive
timestamps refuse per the PARITY TRAP law).
"""
core = _core()
if core is not None:
try:
from rune import cc_loader as _cl
raw = _cl.tier_get(entry, _py_core_now(),
float(HOT_THRESHOLD), float(WARM_THRESHOLD))
return str(raw)
except Exception: # noqa: BLE001 — kernel failure → oracle, never a broken call
pass
return self._py_get_tier(entry)
def _py_get_tier(self, entry: dict) -> str:
"""Python parity oracle (W3.4)."""
wash_count = entry.get("wash_count", 0) or 0
last_accessed = entry.get("last_accessed")
rock_status = entry.get("rock_status", "washable")
@@ -88,7 +123,26 @@ class TierManager:
return "warm"
def promote(self, entry: dict) -> str:
"""Promote an entry to a higher tier. Returns new tier."""
"""Promote an entry to a higher tier. Returns new tier.
Delegates to the Rust kernel when built (Python oracle below)."""
core = _core()
if core is not None:
try:
from rune import cc_loader as _cl
# The kernel's promote/demote take the FULL ENTRY (plus
# thresholds + clock) — not a tier string. They recompute
# the current tier internally, so a tier-string call was
# silently landing on defaults (the live probe caught it).
raw = _cl.tier_promote(entry, _py_core_now(),
float(HOT_THRESHOLD), float(WARM_THRESHOLD))
return str(raw)
except Exception: # noqa: BLE001
pass
return self._py_promote(entry)
def _py_promote(self, entry: dict) -> str:
"""Python parity oracle."""
current = self.get_tier(entry)
if current == "cold":
return "warm"
@@ -97,7 +151,24 @@ class TierManager:
return "hot" # already hot
def demote(self, entry: dict) -> str:
"""Demote an entry to a lower tier. Returns new tier."""
"""Demote an entry to a lower tier. Returns new tier.
Delegates to the Rust kernel when built (Python oracle below)."""
core = _core()
if core is not None:
try:
from rune import cc_loader as _cl
# Same full-entry contract as promote; the kernel handles
# rock_status internally (rock never demotes).
raw = _cl.tier_demote(entry, _py_core_now(),
float(HOT_THRESHOLD), float(WARM_THRESHOLD))
return str(raw)
except Exception: # noqa: BLE001
pass
return self._py_demote(entry)
def _py_demote(self, entry: dict) -> str:
"""Python parity oracle."""
current = self.get_tier(entry)
rock_status = entry.get("rock_status", "washable")
+391 -270
View File
File diff suppressed because it is too large. Load diff