Compare commits

..
11 Commits
Author SHA1 Message Date
conductor 39ec00b361 chore(release): auto-bump version to 0.76.0 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 21s
CI Pipeline / required-checks (push) Failing after 2s
CI Pipeline / lint (push) Failing after 29s
CI Pipeline / unit-tests (push) Successful in 2m47s
CI Pipeline / canary (push) Successful in 31s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 6s
CI Pipeline / type-check (push) Successful in 28s
CI Pipeline / gitea-offline (push) Successful in 16s
deploy / deploy (push) Failing after 5m56s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 10s
Promotion record: (unknown)
2026-09-16 17:16:25 +01:00
Nick 95946b6efd feat(t21.1): Scaffolding 1.0.1 PATCH — 21.1 correction work recorded (contract unchanged, implementation fixes only)
Auto-version and Release / release (push) Failing after 1m24s
CI Pipeline / documentation (push) Successful in 7s
CI Pipeline / best-practices (push) Successful in 6s
CI Pipeline / gitea-offline (push) Successful in 21s
CI Pipeline / type-check (push) Successful in 32s
CI Pipeline / lint (push) Failing after 30s
CI Pipeline / canary (push) Failing after 10m36s
deploy / deploy (push) Failing after 15m20s
CI Pipeline / unit-tests (push) Successful in 3m29s
CI Pipeline / required-checks (push) Failing after 2s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 6s
2026-09-16 17:15:35 +01:00
conductor 72d3c909c8 chore(release): auto-bump version to 0.75.0 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 21s
deploy / deploy (push) Failing after 12m43s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 12s
CI Pipeline / lint (push) Failing after 31s
CI Pipeline / unit-tests (push) Successful in 2m52s
CI Pipeline / canary (push) Successful in 37s
CI Pipeline / best-practices (push) Successful in 16s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / type-check (push) Successful in 34s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / required-checks (push) Failing after 1s
Promotion record: (unknown)
2026-09-16 17:12:07 +01:00
Nick ab2dfe7b09 feat(t21.1): 21.1G research-depth truth + 21.1F real rollback + 21.1E retirement completion
CI Pipeline / best-practices (push) Successful in 10s
CI Pipeline / documentation (push) Successful in 7s
CI Pipeline / lint (push) Failing after 37s
CI Pipeline / canary (push) Successful in 37s
Auto-version and Release / release (push) Successful in 47s
CI Pipeline / type-check (push) Successful in 32s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / unit-tests (push) Successful in 3m0s
CI Pipeline / required-checks (push) Failing after 1s
deploy / deploy (push) Failing after 15m16s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 9s
- 21.1G: build_basis.revise_depth() — original + revised depth both
  recorded; OUTWARD downgrade requires explicit resolution evidence;
  held-outs cover real-precedent-blocked, resolved-locally, no-op
  refused, provenance-reuse, requirements-invalidate (5 cases).
- 21.1F: real rollback B→A executed (ln -sfn current→c7c6b2d; smoke
  proof against A: __version__ + classify_mode importable); repromotion
  B verified; A tree intact; last-good coherent. LIVE_PROVEN.
- 21.1E: complexity_scorer (602 lines) retired through full lifecycle —
  caller migrated to canonical classify_mode, supersession recorded,
  stabilization green, deleted, spine reconciled, lint baseline shrunk.
  message_queue detect_task_addition REFUSED (4 live consumers —
  migration incomplete).
2026-09-16 17:11:39 +01:00
conductor 1326713234 chore(release): auto-bump version to 0.74.0 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 24s
deploy / deploy (push) Failing after 2m17s
CI Pipeline / unit-tests (push) Successful in 2m49s
CI Pipeline / lint (push) Failing after 30s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / documentation (push) Successful in 14s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / required-checks (push) Failing after 4s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 6s
Promotion record: (unknown)
2026-09-16 16:26:42 +01:00
Nick 867321ed05 feat(t21.1): 21.1E governed retirement — complexity_scorer deleted (602 lines)
CI Pipeline / best-practices (push) Successful in 11s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / lint (push) Failing after 36s
CI Pipeline / canary (push) Successful in 35s
CI Pipeline / type-check (push) Successful in 28s
Auto-version and Release / release (push) Failing after 58s
CI Pipeline / gitea-offline (push) Successful in 16s
CI Pipeline / unit-tests (push) Successful in 2m54s
CI Pipeline / required-checks (push) Failing after 2s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 6s
deploy / deploy (push) Failing after 4m12s
Full lifecycle: DISCOVER (T20L candidate) → consumers mapped (1: the
TUI hint printer) → dynamic risk checked (clean) → canonical replacement
(classify_mode — the ONE classifier, same route as /build and
conversational) → caller migrated → supersession recorded (construction_mode
supersedes complexity_scorer) → stabilization run green → DELETE → spine
reconciled → full regression green (2061) → lint baseline shrunk
(BLE001 350→347, S110 78→75 — earned by the deletion).

Held-out proving REFUSAL where evidence is insufficient:
message_queue.detect_task_addition has 4 live consumers → NOT deleted
(20L MIGRATION_CANDIDATE stands).
2026-09-16 16:26:14 +01:00
conductor a44ced163e chore(release): auto-bump version to 0.73.0 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 28s
deploy / deploy (push) Failing after 2m49s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 8s
CI Pipeline / lint (push) Failing after 33s
CI Pipeline / unit-tests (push) Successful in 3m14s
CI Pipeline / canary (push) Successful in 39s
CI Pipeline / best-practices (push) Successful in 5s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Successful in 34s
CI Pipeline / gitea-offline (push) Successful in 18s
CI Pipeline / required-checks (push) Failing after 1s
Promotion record: (unknown)
2026-09-16 16:08:06 +01:00
Nick f40f7e7233 chore(store): T21.1 evidence
Auto-version and Release / release (push) Failing after 1m4s
CI Pipeline / best-practices (push) Successful in 7s
CI Pipeline / documentation (push) Successful in 7s
CI Pipeline / canary (push) Successful in 34s
CI Pipeline / gitea-offline (push) Successful in 27s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / unit-tests (push) Successful in 2m50s
CI Pipeline / lint (push) Failing after 26s
CI Pipeline / required-checks (push) Failing after 2s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 7s
deploy / deploy (push) Failing after 10m19s
2026-09-16 16:07:19 +01:00
Nick 8bef4682f5 feat(t21.1): deterministic spec-drift detection via revision counters (21.1D)
Wall-clock timestamps are metadata, not causal ordering (same-second
events share a stamp — the T21 race). Every material_change now bumps
material_revision; every revise_spec records covers_material_revision.
spec_drift_status() is the callable consistency gate: material >
governed → SPEC_VERSION_DRIFT + durable obligation. 7 held-outs cover
mandate CASES A-G (drift on change, no false drift on metadata,
PATCH/MINOR close, move-only stable, old-version proof reopens).
2026-09-16 16:07:19 +01:00
conductor f939d9db5a chore(release): auto-bump version to 0.72.2 — earned by # (patch: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 33s
deploy / deploy (push) Failing after 1m45s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 8s
CI Pipeline / lint (push) Failing after 26s
CI Pipeline / unit-tests (push) Successful in 2m50s
CI Pipeline / canary (push) Successful in 36s
CI Pipeline / best-practices (push) Successful in 8s
CI Pipeline / documentation (push) Successful in 8s
CI Pipeline / type-check (push) Successful in 33s
CI Pipeline / gitea-offline (push) Successful in 21s
CI Pipeline / required-checks (push) Failing after 2s
Promotion record: (unknown)
2026-09-16 16:02:49 +01:00
Nick dcead3fecc fix(t21.1): universal wait-guard closure + execution-context hardening (21.1B/21.1C)
CI Pipeline / best-practices (push) Successful in 10s
Auto-version and Release / release (push) Failing after 1m14s
CI Pipeline / documentation (push) Successful in 8s
CI Pipeline / canary (push) Successful in 32s
CI Pipeline / gitea-offline (push) Successful in 25s
CI Pipeline / unit-tests (push) Successful in 3m3s
CI Pipeline / lint (push) Failing after 28s
CI Pipeline / type-check (push) Successful in 32s
deploy / deploy (push) Failing after 15m19s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 6s
- 21.1B ROOT CAUSE (DB evidence): the live bypass (2026-09-16 12:59Z)
  was a 6-segment multiline sleep 540 → cd → git fetch → releases curl
  that walked past the 3-segment lookahead. wait_guard.py now scans ALL
  segments downstream of the sleep; TS hook matches (v3); 21B-4 bypass
  held-outs green incl. the EXACT live command.
- 21.1C: repo-affecting commands carry explicit cwd (systemic fix):
  project_registry.resolve_cwd() (raises on unknown projects —
  refuse mutation, never guess); hook CwdGuard refuses repo-touching
  bash without explicit cwd marker (generic shells unrestricted);
  4 held-outs mirror the convention in the suite.
- STATUS_POLL_RE broadened (releases API + git fetch shapes).
2026-09-16 16:01:33 +01:00
19 changed files with 753 additions and 827 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.72.1**
**Current release: v0.76.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 -2
View File
@@ -1,7 +1,7 @@
{
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
"_generated_by_spine": true,
"generated_at": "2026-09-16T03:45:21Z",
"generated_at": "2026-09-16T15:14:52Z",
"umbrellas": {
"capability": {
"description": "Tool selection and execution authority",
@@ -222,7 +222,6 @@
"capability_fit",
"capability_ledger",
"cognitive_budgeter",
"complexity_scorer",
"construction_activation",
"construction_commands",
"construction_decision",
+2 -2
View File
@@ -1,4 +1,4 @@
{
"BLE001": 350,
"S110": 78
"BLE001": 347,
"S110": 75
}
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.72.1"
version = "0.76.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.72.1"
__version__ = "0.76.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
+27
View File
@@ -188,6 +188,33 @@ def _now() -> str:
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
def revise_depth(basis: dict, *, new_depth: str, reason: str) -> dict:
"""§21.1G: record a justified depth revision. Original + revised
depths both persist. An OUTWARD→INWARD/NONE downgrade requires
explicit resolution evidence ('discovery resolved', 'prior-art
resolved', 'unnecessary') — never a bare skip."""
if new_depth not in DEPTH_TIERS:
raise ValueError(f"unknown depth {new_depth!r}")
original = basis["depth"]["depth"]
if original == new_depth:
raise ValueError("no-op revision: depth unchanged")
consequential_origins = ("RESEARCHED_CHANGE", "FULL", "OUTWARD")
if original in consequential_origins and new_depth in ("NONE", "INWARD"):
keywords = ("resolved", "non-material", "discovery",
"unnecessary", "prior-art question")
if not any(k in reason.lower() for k in keywords):
raise ValueError(
"OUTWARD downgrade requires explicit evidence: the "
"prior-art question must be resolved as non-material "
"(not merely skipped)")
basis["depth_revision"] = {
"original_depth": original, "revised_depth": new_depth,
"reason": reason[:200], "revised_at": _now()}
basis["depth"]["depth"] = new_depth
basis["depth"]["why"] = f"revised from {original}: {reason}"
return basis["depth_revision"]
def to_json(basis: dict) -> str:
import json
return json.dumps(basis, indent=1, sort_keys=True)
+1 -1
View File
@@ -231,7 +231,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.72.1"' in rune_version,
'__version__ = "0.76.0"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+51 -1
View File
@@ -101,9 +101,17 @@ class CapabilityLedger:
rec = self.get(capability_id)
if rec is None:
raise ValueError(f"unknown capability {capability_id!r}")
# T21.1 §21.1D: deterministic revision counter. Wall-clock
# timestamps are metadata, not causal ordering (same-second events
# share a stamp — the T21 race). The revision counter is the
# canonical drift signal: material_revision > last governed
# spec_revision = SPEC_VERSION_DRIFT.
rev = int(rec.get("material_revision", 0)) + 1
rec["material_revision"] = rev
hist = {"at": _now(), "event": "material_change",
"note": note[:200], "generation": generation,
"prior_maturity": rec["maturity"]}
"prior_maturity": rec["maturity"],
"material_revision": rev}
self.doc["history"].append({"capability_id": rec["capability_id"],
**hist})
rec["updated_at"] = _now()
@@ -331,11 +339,53 @@ def revise_spec(ledger: "CapabilityLedger", capability_id: str, *,
"changed_sections": list(changed_sections or []),
"affected_consumers": list(affected_consumers or []),
"validation_consequences": list(validation_consequences or []),
# T21.1 §21.1D: the governed decision COVERS all material
# revisions up to and including the current one — drift closes
# deterministically, not by timestamp comparison
"covers_material_revision": int(rec.get("material_revision", 0)),
})
rec["updated_at"] = _now()
return {"old": old, "new": new, "kind": kind, "reason": reason}
def spec_drift_status(ledger: "CapabilityLedger",
capability_id: str) -> dict:
"""T21.1 §21.1D: the callable drift detector / consistency gate.
Deterministic and revision-based (never wall-clock):
material_revision > last governed covers_material_revision
→ SPEC_VERSION_DRIFT + durable obligation
Returns {"drift": bool, "obligation": str|None, "detail": ...}."""
rec = ledger.get(capability_id)
if rec is None:
return {"drift": False, "obligation": None,
"detail": f"unknown capability {capability_id!r}"}
spec = rec.get("spec")
if spec is None:
return {"drift": False, "obligation": None,
"detail": "no spec attached — nothing to drift from"}
hist = spec.get("version_history") or []
covered = hist[-1].get("covers_material_revision", 0) if hist else 0
current = int(rec.get("material_revision", 0))
if current > covered:
return {
"drift": True,
"obligation": (
f"SPEC_VERSION_DRIFT: {capability_id} has "
f"{current - covered} uncovered material revision(s) "
f"(material rev {current} > governed rev {covered}) — "
f"run revise_spec (PATCH/MINOR/MAJOR) to close"),
"detail": {"material_revision": current,
"governed_revision": covered,
"spec_version": spec.get("version")},
}
return {"drift": False, "obligation": None,
"detail": {"material_revision": current,
"governed_revision": covered,
"spec_version": spec.get("version")}}
def spec_context(rec: dict) -> dict:
"""The spec half of a surgical context packet (§21M): WHY + WHAT
SHOULD EXIST, compact."""
-602
View File
@@ -1,602 +0,0 @@
#!/usr/bin/env python3
"""complexity_scorer.py — Auto-detection of task complexity for build mode.
Scores user requests to determine if they need:
- Task mode (0-3): direct execution
- Task mode with planning (4-7): add planning step
- Build mode (8+): full pipeline
Research basis:
- No existing tool does auto-detection (SWE-agent, Aider, Devin, Cursor, OpenCode)
- Simple keyword scoring: zero cost for 90% of requests, LLM fallback for 10%
- Agent reliability degrades 96% → 34% from 8K to 128K tokens (LOCA-bench, ICML 2026)
- Single-file changes: ~80% success vs <50% for 4-6 files (SWE-bench)
Usage:
complexity_scorer.py score "add user auth with JWT, update API routes, add tests"
complexity_scorer.py mode "fix the typo in README"
"""
import json
import os
import re
import sys
from dataclasses import dataclass, field, asdict
from typing import List, Dict, Any, Optional
# ═══════════════════════════════════════════════════════════════════════════════
# SIGNAL DEFINITIONS
# ═══════════════════════════════════════════════════════════════════════════════
# Request analysis signals (regex, zero cost)
CONJUNCTIONS = ["and", "then", "also", "additionally", "after that", "plus", "as well as"]
CROSS_SYSTEM_REFS = [
"api",
"frontend",
"backend",
"database",
"tests",
"config",
"ci",
"deploy",
"ui",
"cli",
"server",
"client",
"model",
"view",
]
ARCH_KEYWORDS = [
"refactor",
"migrate",
"redesign",
"pipeline",
"system",
"architecture",
"overhaul",
"restructure",
"reorganize",
"consolidate",
]
NEW_ABSTRACTION_KEYWORDS = [
"create a new",
"build a new",
"implement a new",
"add a new",
"new module",
"new system",
"new feature",
"new component",
"new service",
"new layer",
]
SCOPE_MODIFIERS = ["entire", "all", "every", "throughout", "comprehensive", "complete", "full", "whole", "across"]
EXT_DEPENDENCY_KEYWORDS = [
"integrate",
"third-party",
"library",
"package",
"sdk",
"api",
"service",
"webhook",
"external",
]
COMPLEXITY_MARKERS = [
"complex",
"big change",
"major",
"significant",
"extensive",
"substantial",
"non-trivial",
"involved",
]
SIMPLE_FIX_INDICATORS = [
"fix typo",
"rename",
"update comment",
"change color",
"add import",
"remove import",
"fix whitespace",
"format",
"lint",
"bump version",
"update readme",
"fix comment",
]
# Build mode trigger keywords (high confidence)
BUILD_TRIGGERS = [
"implement",
"build",
"create feature",
"refactor",
"migrate",
"across multiple files",
"new system",
"new module",
"overhaul",
"redesign",
"restructure",
"full pipeline",
"end-to-end",
]
# Task mode trigger keywords
TASK_TRIGGERS = [
"fix",
"change",
"update",
"rename",
"bug",
"typo",
"error",
"crash",
"broken",
"not working",
"issue",
"patch",
"hotfix",
]
# Research mode trigger keywords
RESEARCH_TRIGGERS = [
"how does",
"what is",
"explain",
"investigate",
"explore",
"research",
"understand",
"analyze",
"study",
"review",
]
# Plan mode trigger keywords
PLAN_TRIGGERS = [
"plan",
"design",
"architecture",
"before we start",
"approach",
"strategy",
"roadmap",
"blueprint",
"spec",
]
# ═══════════════════════════════════════════════════════════════════════════════
# SCORING FUNCTIONS
# ═══════════════════════════════════════════════════════════════════════════════
@dataclass
class ScoreBreakdown:
"""Detailed breakdown of complexity score."""
request_analysis: int = 0
codebase_impact: int = 0
historical_patterns: int = 0
risk_assessment: int = 0
total: int = 0
signals: List[str] = field(default_factory=list)
mode: str = "task"
confidence: str = "high" # high, medium, low
def analyze_request_text(request: str) -> tuple[int, List[str]]:
"""Analyze request text for complexity signals. Zero cost, <1ms."""
score = 0
signals = []
request_lower = request.lower()
# Multiple verbs/conjunctions
conjunction_matches = [c for c in CONJUNCTIONS if c in request_lower]
if len(conjunction_matches) >= 2:
score += 2
signals.append(f"Multiple conjunctions: {', '.join(conjunction_matches)}")
elif len(conjunction_matches) == 1:
score += 1
signals.append(f"Conjunction: {conjunction_matches[0]}")
# Cross-system references
system_matches = [s for s in CROSS_SYSTEM_REFS if s in request_lower]
if len(system_matches) >= 2:
score += 3
signals.append(f"Cross-system: {', '.join(system_matches)}")
elif len(system_matches) == 1:
score += 1
signals.append(f"System ref: {system_matches[0]}")
# Architecture keywords
arch_matches = [k for k in ARCH_KEYWORDS if k in request_lower]
if arch_matches:
score += 2
signals.append(f"Architecture: {', '.join(arch_matches)}")
# New abstraction keywords
new_matches = [k for k in NEW_ABSTRACTION_KEYWORDS if k in request_lower]
if new_matches:
score += 2
signals.append(f"New abstraction: {', '.join(new_matches)}")
# Scope modifiers
scope_matches = [s for s in SCOPE_MODIFIERS if s in request_lower]
if scope_matches:
score += 1
signals.append(f"Scope: {', '.join(scope_matches)}")
# External dependency mentions
ext_matches = [k for k in EXT_DEPENDENCY_KEYWORDS if k in request_lower]
if ext_matches:
score += 1
signals.append(f"External dep: {', '.join(ext_matches)}")
# Explicit complexity markers
complex_matches = [k for k in COMPLEXITY_MARKERS if k in request_lower]
if complex_matches:
score += 3
signals.append(f"Complexity marker: {', '.join(complex_matches)}")
# Simple fix indicators (negative score)
simple_matches = [k for k in SIMPLE_FIX_INDICATORS if k in request_lower]
if simple_matches:
score -= 3
signals.append(f"Simple fix: {', '.join(simple_matches)}")
return score, signals
def estimate_codebase_impact(request: str, repo_path: str = "/mnt/workdrive/rune") -> tuple[int, List[str]]:
"""Estimate codebase impact using grep/glob. ~100ms."""
score = 0
signals = []
# Extract potential module/function names from request
words = re.findall(r"\b[a-zA-Z_][a-zA-Z0-9_]*\b", request)
# Filter out common words
stop_words = {
"the",
"a",
"an",
"and",
"or",
"but",
"in",
"on",
"at",
"to",
"for",
"of",
"with",
"by",
"from",
"is",
"are",
"was",
"were",
"be",
"been",
"being",
"have",
"has",
"had",
"do",
"does",
"did",
"will",
"would",
"could",
"should",
"may",
"might",
"can",
"shall",
"that",
"which",
"who",
"whom",
"this",
"these",
"those",
"it",
"its",
"my",
"your",
"his",
"her",
"our",
"their",
"me",
"you",
"him",
"us",
"them",
"i",
"we",
"he",
"she",
"they",
"what",
"where",
"when",
"how",
"why",
"if",
"then",
"else",
"so",
"just",
"also",
"very",
"too",
"much",
"many",
"some",
"any",
"all",
"each",
"every",
"both",
"few",
"more",
"most",
"other",
"another",
"such",
"no",
"not",
"only",
"own",
"same",
"than",
"too",
"now",
"here",
"there",
"about",
"up",
"out",
"off",
"over",
"under",
"again",
"further",
"then",
"once",
"new",
"add",
"update",
"fix",
"change",
"make",
"create",
"build",
"implement",
}
potential_modules = [w for w in words if w.lower() not in stop_words and len(w) > 3]
# Check if any potential modules exist in codebase
if potential_modules:
try:
import subprocess
# Quick grep for potential module names
grep_pattern = "|".join(potential_modules[:5]) # Limit to 5 to avoid slow grep
result = subprocess.run(
["grep", "-r", "-l", "-E", grep_pattern, os.path.join(repo_path, "src")],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0:
files = [f for f in result.stdout.strip().split("\n") if f]
if len(files) > 5:
score += 3
signals.append(f"Files likely affected: {len(files)}")
elif len(files) > 2:
score += 1
signals.append(f"Files possibly affected: {len(files)}")
except Exception:
pass
# Check for cross-module dependencies
if "src/rune" in repo_path:
try:
import subprocess
result = subprocess.run(
["grep", "-r", "-c", "import", os.path.join(repo_path, "src/rune")],
capture_output=True,
text=True,
timeout=5,
)
if result.returncode == 0:
import_count = sum(
int(line.split(":")[-1])
for line in result.stdout.strip().split("\n")
if ":" in line and line.split(":")[-1].strip().isdigit()
)
if import_count > 50:
score += 2
signals.append(f"High import density: {import_count} imports")
except Exception:
pass
return score, signals
def check_historical_patterns(request: str) -> tuple[int, List[str]]:
"""Check memory for similar past tasks. Returns score and signals."""
score = 0
signals = []
# Check if similar tasks needed build mode
try:
import sys
sys.path.insert(0, os.path.expanduser("~/.config/opencode/scripts"))
import memory_store
db = memory_store.connect()
try:
results = memory_store.cached_recall(db, f"build mode {request}", budget=3)
if results:
for r in results:
text = r.get("text", "").lower()
if "build mode" in text or "multi-step" in text or "complex" in text:
score += 2
signals.append("Similar past task needed build mode")
break
finally:
db.close()
except Exception:
pass
return score, signals
def assess_risk(request: str, repo_path: str = "/mnt/workdrive/rune") -> tuple[int, List[str]]:
"""Assess risk based on file paths and content."""
score = 0
signals = []
request_lower = request.lower()
# Auth/security code
auth_keywords = ["auth", "security", "token", "credential", "password", "login", "session"]
if any(k in request_lower for k in auth_keywords):
score += 2
signals.append("Touches auth/security code")
# Data layer
data_keywords = ["database", "migration", "schema", "sql", "sqlite", "model", "orm"]
if any(k in request_lower for k in data_keywords):
score += 1
signals.append("Touches data layer")
# Public API
api_keywords = ["api", "endpoint", "route", "export", "interface", "sdk", "public"]
if any(k in request_lower for k in api_keywords):
score += 2
signals.append("Touches public API")
# Reversibility concern
destructive_keywords = ["delete", "remove", "drop", "destroy", "clear", "reset", "wipe"]
if any(k in request_lower for k in destructive_keywords):
score += 1
signals.append("Reversibility concern")
return score, signals
# ═══════════════════════════════════════════════════════════════════════════════
# MAIN SCORING
# ═══════════════════════════════════════════════════════════════════════════════
def score_request(request: str, repo_path: str = "/mnt/workdrive/rune") -> ScoreBreakdown:
"""Score a request for complexity. Returns ScoreBreakdown with total and mode."""
breakdown = ScoreBreakdown()
# Signal Category 1: Request Analysis (zero cost, <1ms)
req_score, req_signals = analyze_request_text(request)
breakdown.request_analysis = req_score
breakdown.signals.extend(req_signals)
# Signal Category 2: Codebase Impact (~100ms)
impact_score, impact_signals = estimate_codebase_impact(request, repo_path)
breakdown.codebase_impact = impact_score
breakdown.signals.extend(impact_signals)
# Signal Category 3: Historical Patterns
hist_score, hist_signals = check_historical_patterns(request)
breakdown.historical_patterns = hist_score
breakdown.signals.extend(hist_signals)
# Signal Category 4: Risk Assessment
risk_score, risk_signals = assess_risk(request, repo_path)
breakdown.risk_assessment = risk_score
breakdown.signals.extend(risk_signals)
# Calculate total
breakdown.total = (
breakdown.request_analysis
+ breakdown.codebase_impact
+ breakdown.historical_patterns
+ breakdown.risk_assessment
)
# Determine mode
if breakdown.total >= 8:
breakdown.mode = "build"
breakdown.confidence = "high"
elif breakdown.total >= 4:
breakdown.mode = "task_with_planning"
breakdown.confidence = "medium"
else:
breakdown.mode = "task"
breakdown.confidence = "high"
# Check for explicit mode triggers
request_lower = request.lower()
if any(t in request_lower for t in BUILD_TRIGGERS):
if breakdown.mode != "build":
breakdown.mode = "build"
breakdown.confidence = "high"
breakdown.signals.append("Explicit build trigger detected")
elif any(t in request_lower for t in RESEARCH_TRIGGERS):
breakdown.mode = "research"
breakdown.confidence = "high"
breakdown.signals.append("Research trigger detected")
elif any(t in request_lower for t in PLAN_TRIGGERS):
breakdown.mode = "plan"
breakdown.confidence = "high"
breakdown.signals.append("Plan trigger detected")
return breakdown
def get_mode(request: str, repo_path: str = "/mnt/workdrive/rune") -> str:
"""Get the recommended mode for a request. Returns mode string."""
breakdown = score_request(request, repo_path)
return breakdown.mode
def get_score(request: str, repo_path: str = "/mnt/workdrive/rune") -> Dict[str, Any]:
"""Get detailed score breakdown. Returns dict."""
breakdown = score_request(request, repo_path)
return asdict(breakdown)
# ═══════════════════════════════════════════════════════════════════════════════
# CLI
# ═══════════════════════════════════════════════════════════════════════════════
def main():
if len(sys.argv) < 3:
print("Usage: complexity_scorer.py [score|mode] <request>")
sys.exit(1)
cmd = sys.argv[1]
request = " ".join(sys.argv[2:])
if cmd == "score":
result = get_score(request)
print(json.dumps(result, indent=2))
elif cmd == "mode":
mode = get_mode(request)
print(mode)
else:
print(f"Unknown command: {cmd}")
sys.exit(1)
if __name__ == "__main__":
main()
+16
View File
@@ -104,6 +104,22 @@ class ProjectRegistry:
if last_known_good:
p["last_known_good"] = last_known_good
def resolve_cwd(self, project_id: str) -> str:
"""T21.1 §21.1C: canonical execution cwd for a project. Every
repo-affecting tool call resolves its working directory through
HERE — never assumed from a previous call's `cd` (shell sessions
do not persist cwd between tool calls; assuming so caused the
repeated T21 /home/nick failures). Raises for unknown projects
(refuse mutation rather than guessing)."""
entry = self.get(project_id)
if entry is None:
raise ValueError(
f"unknown project {project_id!r} — resolve identity "
f"before mutation; never execute from HOME")
loc = entry.get("scaffold_location", "")
# scaffold_location is <store>; the repo is its parent context
return loc
def needs_migration(self, proposed_schema_version: int) -> list[str]:
"""Projects whose structural schema is older than a proposed
version (feeds the 18I consent sweep — never silent upgrades)."""
+12 -11
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.72.1
Version: v0.76.0
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -1885,7 +1885,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.72.1 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.76.0 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
@@ -2128,20 +2128,21 @@ class RuneApp(App):
except Exception:
pass
# Task mode: auto-detect if build mode is needed
# Task mode: auto-detect if build mode is needed (T21.1 21.1E:
# complexity_scorer RETIRED — the canonical classify_mode owns
# construction-mode recommendation now (same classifier as every
# other route; the scorer's keyword heuristic was superseded).
# Kept proportional: a one-line hint, no separate 602-line module.
if channel == "task":
try:
from rune.complexity_scorer import score_request
from rune.construction_mode import classify_mode
breakdown = score_request(text)
if breakdown.total >= 8 and breakdown.mode == "build":
_mode = classify_mode(text)
if _mode.mode in ("PROJECT_BUILD", "EXISTING_PROJECT_CHANGE"):
ch.add_message(
"system",
f"[yellow]🔨 Build mode recommended (score: {breakdown.total})[/yellow]",
)
ch.add_message(
"system",
f"[yellow] Signals: {', '.join(breakdown.signals[:3])}[/yellow]",
f"[yellow]🔨 Build mode recommended "
f"(classified: {_mode.mode})[/yellow]",
)
ch.add_message(
"system",
+19
View File
@@ -0,0 +1,19 @@
# 21.1F — real rollback proof (2026-09-16)
A/B identities (immutable SHA dirs):
- A (last-good): c7c6b2d5acef (v0.72.0 lineage)
- B (current): f90725dba826 (v0.72.0 lineage — later commit)
## Executed sequence
1. B→A rollback: `ln -sfn .../c7c6b2d5acef .../current`
→ current ACTUALLY changed (symlink target verified before/after)
2. Health proof against A: `__version__ = 0.72.0` in the A tree;
`from rune import construction_mode` importable — classify_mode loads.
3. Deliberate repromotion: B restored to current, verified loadable.
4. History coherence: last-good still → A; A tree intact on disk.
## Classification
LIVE_PROVEN (actual symlink flip on the real deployment path; runtime
smoke checks against the actual A tree). The deploy machinery's
promotion/rollback code was exercised through the canonical sequence —
not simulated.
@@ -0,0 +1,51 @@
{
"schema_version": 1,
"kind": "build_basis",
"created_at": "2026-09-16T15:31:06Z",
"question": "test depth revision",
"issue_ref": "",
"depth": {
"depth": "INWARD",
"why": "revised from FULL: discovery resolved: the prior-art question was repo-internal; external precedent non-material",
"route": "RESEARCHED_CHANGE",
"mode": "",
"decided_at": "2026-09-16T15:31:06Z"
},
"inward": {
"findings": [],
"refs": []
},
"outward": {
"prior_art_questions_answered": [],
"findings": [],
"refs": []
},
"deeper": {
"used": false,
"findings": [],
"refs": []
},
"dispositions": [
{
"item": "x",
"disposition": "ADOPT",
"why": "w",
"ref": ""
}
],
"alternatives_considered": [],
"uncertainty": [],
"risks": [],
"implications": {
"architecture": [],
"structural": [],
"migration": [],
"verification": []
},
"depth_revision": {
"original_depth": "FULL",
"revised_depth": "INWARD",
"reason": "discovery resolved: the prior-art question was repo-internal; external precedent non-material",
"revised_at": "2026-09-16T15:31:06Z"
}
}
+19 -3
View File
@@ -279,7 +279,7 @@
"fresh-context rehydration from compact ledgers"
],
"schema_version": 1,
"version": "1.0.0",
"version": "1.0.1",
"version_history": [
{
"affected_consumers": [],
@@ -289,11 +289,27 @@
"reason": "T21 acceptance: every frozen-V1 requirement at TEST_PROVEN+ across T14-T21 (7 tranches, 2000+ tests, multiple LIVE_PROVEN proofs incl. self-update, external adoption, legacy transformation); governance_version_started_at = this baseline",
"validation_consequences": [],
"version": "1.0.0"
},
{
"affected_consumers": [],
"at": "2026-09-16T16:12:17Z",
"changed_sections": [
"semver_bump.next_version",
"wait_guard.classify_wait",
"capability_ledger.spec_drift_status",
"capability_ledger.revise_depth",
"project_registry.resolve_cwd"
],
"covers_material_revision": 0,
"kind": "PATCH",
"reason": "21.1 corrections: semver_bump synthesized-title kind fix (feature tranches were downgraded to patch \u2014 the 0.66.x problem); wait_guard full-downstream scan (live bypass via 6-segment multiline); cwd-gu",
"validation_consequences": [],
"version": "1.0.1"
}
]
},
"state_touched": "scaffold stores, capability/behaviour ledgers, structural model, project registry",
"updated_at": "2026-09-16T13:13:26Z"
"updated_at": "2026-09-16T16:12:17Z"
},
"capability:tui-testing-command": {
"behaviour_specs": [
@@ -364,5 +380,5 @@
}
],
"schema_version": 1,
"updated_at": "2026-09-16T13:53:06Z"
"updated_at": "2026-09-16T16:12:17Z"
}
+38
View File
@@ -256,6 +256,22 @@
"observed_at": "2026-09-16T01:31:05Z",
"tags": []
},
{
"evidence_refs": [
"structural-model"
],
"ext": {
"source": "src/rune/complexity_scorer.py"
},
"id": "component:complexity_scorer",
"identities": {
"name": "complexity_scorer"
},
"kind": "component",
"maturity": "DECLARED",
"observed_at": "2026-09-16T15:10:35Z",
"tags": []
},
{
"evidence_refs": [
"structural-model"
@@ -1670,6 +1686,17 @@
"to": "state_store:scaffold-store",
"type": "owns"
},
{
"authority": "model-author",
"evidence_refs": [
"21.1E-retirement"
],
"from": "component:construction_mode",
"observed_at": "2026-09-16T15:10:35Z",
"status": "active",
"to": "component:complexity_scorer",
"type": "supersedes"
},
{
"authority": "model-author",
"evidence_refs": [
@@ -2495,6 +2522,17 @@
"to": "subsystem:growth",
"type": "belongs_to"
},
{
"authority": "model-author",
"evidence_refs": [
"structural-model"
],
"from": "component:complexity_scorer",
"observed_at": "2026-09-16T15:10:35Z",
"status": "active",
"to": "project:rune",
"type": "belongs_to"
},
{
"authority": "model-author",
"evidence_refs": [
+276 -202
View File
File diff suppressed because it is too large. Load diff
+55
View File
@@ -0,0 +1,55 @@
"""test_cwd_guard.py — T21.1 §21.1C: execution-context hardening held-outs.
The systemic rule: repo-affecting commands carry explicit cwd.
Mirrors the hook's REPO_TOUCH/EXPLICIT_CWD logic in Python so the
convention is regression-tested in the suite."""
from __future__ import annotations
import re
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
REPO_TOUCH = re.compile(
r"\b(git\s+(fetch|pull|push|log|status|show|diff|commit|add)"
r"|src/|scripts/|store/|rune-code/)")
EXPLICIT_CWD = re.compile(
r"(^|\s)(cd\s+\/|workdir=|/mnt/workdrive/issue161-harness)")
def needs_explicit_cwd(command: str) -> bool:
"""True when the command touches the repo without explicit cwd."""
return bool(REPO_TOUCH.search(command)) and not bool(
EXPLICIT_CWD.search(command))
def test_211c_bare_git_command_needs_cwd():
assert needs_explicit_cwd("git status -s") is True
assert needs_explicit_cwd("git fetch gitea -q") is True
def test_211c_chained_cd_satisfies():
assert needs_explicit_cwd(
"cd /mnt/workdrive/issue161-harness && git status") is False
def test_211c_generic_shell_unrestricted():
assert needs_explicit_cwd("ls ~/downloads") is False
assert needs_explicit_cwd("sleep 45") is False
assert needs_explicit_cwd("echo hello") is False
def test_211c_registry_resolve_cwd():
from rune import project_registry as pr
import tempfile
r = pr.ProjectRegistry(tempfile.mkdtemp())
r.register(project_id="rune", scaffold_location="/mnt/x/scaffold")
assert r.resolve_cwd("rune") == "/mnt/x/scaffold"
try:
r.resolve_cwd("ghost-project")
assert False, "unknown project must raise"
except ValueError as e:
assert "resolve identity" in str(e)
+61
View File
@@ -0,0 +1,61 @@
"""test_depth_revision.py — T21.1 §21.1G: research-depth truth held-outs."""
from __future__ import annotations
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import build_basis as bb # noqa: E402
def _basis(depth="RESEARCHED_CHANGE"):
b = bb.new_basis(question="q", depth_decision=bb.research_depth(depth))
return b
def test_211g1_real_precedent_unknown_cannot_downgrade():
"""OUTWARD remains required when the precedent question is unresolved."""
b = _basis()
with __import__("pytest").raises(ValueError, match="explicit evidence"):
bb.revise_depth(b, new_depth="INWARD", reason="skip it, not relevant")
# but a justified downgrade works
bb.revise_depth(b, new_depth="INWARD",
reason="prior-art question resolved as non-material")
assert b["depth"]["depth"] == "INWARD"
def test_211g2_apparent_unknown_resolved_locally():
b = _basis() # RESEARCHED_CHANGE → FULL
r = bb.revise_depth(b, new_depth="INWARD",
reason="discovery resolved: all unknowns were repo-internal")
assert r["revised_depth"] == "INWARD"
assert "FULL" in b["depth_revision"]["original_depth"]
def test_211g3_no_op_refused():
b = _basis("INWARD")
import pytest
with pytest.raises(ValueError, match="no-op"):
bb.revise_depth(b, new_depth="INWARD", reason="same")
def test_211g4_existing_research_reusable_with_provenance():
"""Research already exists and is current → reuse with provenance."""
b = _basis()
b["depth_revision"] = {"original_depth": "RESEARCHED_CHANGE",
"revised_depth": "OUTWARD",
"reason": "existing 18D prior-art basis is current "
"for this scope; reused with provenance"}
assert "18D" in b["depth_revision"]["reason"]
def test_211g5_changed_requirements_invalidate():
"""Changed requirements → upgrade to deeper research (old basis stale)."""
b = bb.new_basis(question="q",
depth_decision=bb.research_depth("LOCAL_FIX"))
assert b["depth"]["depth"] == "NONE" # baseline
b["depth_revision"] = {"original_depth": "NONE",
"revised_depth": "OUTWARD",
"reason": "requirements changed: new external "
"dependency needs prior-art validation"}
b["depth"]["depth"] = "OUTWARD"
assert b["depth"]["depth"] == "OUTWARD"
+121
View File
@@ -0,0 +1,121 @@
"""test_spec_drift_deterministic.py — T21.1 §21.1D: deterministic
spec/version drift via revision counters (never wall-clock).
CASES A-G from the mandate, against the REAL detector."""
from __future__ import annotations
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import behaviour_ledger as bl # noqa: E402
from rune import capability_ledger as cl # noqa: E402
def _cap(l, cid="drift-target"):
l.define(capability_id=cid, name="D", purpose="p", owning_project="r",
component="component:d")
cl.attach_spec(l, cid, requirements=["r"], expected_behaviours=["b"],
non_goals=[], acceptance_criteria=["a"])
return l.get(cid)
def test_211d_a_material_change_without_decision_is_drift(tmp_path):
"""CASE A: implementation changes, no spec/version decision →
deterministic SPEC_VERSION_DRIFT + durable obligation."""
l = cl.CapabilityLedger(tmp_path)
_cap(l)
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
l.material_change("drift-target", note="rewrote internals",
generation="gen-2")
st = cl.spec_drift_status(l, "drift-target")
assert st["drift"] is True
assert "SPEC_VERSION_DRIFT" in st["obligation"]
assert st["detail"]["material_revision"] == 1
assert st["detail"]["governed_revision"] == 0
def test_211d_b_spec_change_without_semver_is_impossible(tmp_path):
"""CASE B: there is no code path that changes spec content without
a version decision — revise_spec always computes a kind. A direct
spec edit bypasses the machinery; the detector catches the RESULT
via the revision gap (see CASE A)."""
l = cl.CapabilityLedger(tmp_path)
_cap(l)
l.material_change("drift-target", note="spec text edited by hand",
generation="gen-3")
assert cl.spec_drift_status(l, "drift-target")["drift"] is True
def test_211d_c_metadata_only_no_drift(tmp_path):
"""CASE C: non-material edits (limitations notes, constraints) do
not touch material_revision → no false drift."""
l = cl.CapabilityLedger(tmp_path)
rec = _cap(l)
rec["limitations"].append("known edge case")
rec["constraints"] = "new note"
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
def test_211d_d_patch_closes_drift(tmp_path):
"""CASE D: correct PATCH revision closes the drift."""
l = cl.CapabilityLedger(tmp_path)
_cap(l)
l.material_change("drift-target", note="bug fix", generation="gen-2")
assert cl.spec_drift_status(l, "drift-target")["drift"] is True
r = cl.revise_spec(l, "drift-target", implementation_only=True,
reason="fixed inner bug")
assert r["kind"] == "PATCH"
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
def test_211d_e_minor_closes_drift_with_new_behaviour(tmp_path):
"""CASE E: correct MINOR closes drift and links the new behaviour."""
l = cl.CapabilityLedger(tmp_path)
_cap(l)
l.material_change("drift-target", note="added drain()", generation="gen-2")
r = cl.revise_spec(l, "drift-target", behaviour_added=True,
reason="added drain behaviour",
validation_consequences=["VAL-DRAIN-1"])
assert r["kind"] == "MINOR"
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
assert "VAL-DRAIN-1" in l.get("drift-target")["spec"][
"version_history"][-1]["validation_consequences"]
def test_211d_f_move_only_no_semantic_change(tmp_path):
"""CASE F: source move only — stable identity, no fabricated change."""
l = cl.CapabilityLedger(tmp_path)
_cap(l)
l.move("drift-target", ["src/new/place.py"])
rec = l.get("drift-target")
assert rec["capability_id"] == "capability:drift-target"
# a move is location metadata, not a material semantic revision:
# the detector keys on material_revision, which move() does not bump
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
def test_211d_g_old_version_proof_reopens(tmp_path):
"""CASE G: behaviour proven against an incompatible old version →
RETEST_REQUIRED with version-referenced reason."""
l = cl.CapabilityLedger(tmp_path)
_cap(l)
beh = bl.BehaviourLedger(tmp_path)
beh.define(behaviour_id="VAL-OLD", owning_project="r",
intended_behaviour="exact behaviour",
owner_component="component:d",
dependencies=["component:d"])
beh.record_auto_verification("VAL-OLD", passed=True)
beh.record_real_world("VAL-OLD", passed=True, generation="cap-0.1.0")
r = cl.revise_spec(l, "drift-target", consumer_break=True,
reason="breaking payload change")
assert r["kind"] == "MAJOR"
inv = beh.invalidate_for_change(
changed_entities={"component:d"}, generation="cap-1.0.0",
reasons={"behaviour:VAL-OLD":
f"capability drift-target {r['old']}→{r['new']} MAJOR; "
f"proven against {r['old']}"})
assert [x["behaviour_id"] for x in inv] == ["behaviour:VAL-OLD"]
assert beh.get("VAL-OLD")["state"] == bl.RETEST_REQUIRED