Compare commits

...
7 Commits
Author SHA1 Message Date
conductor e0d0923488 chore(release): auto-bump version to 0.111.0 — earned by # (minor: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 4s
deploy / deploy (push) Failing after 15m5s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 5s
CI Pipeline / lint (push) Successful in 38s
CI Pipeline / unit-tests (push) Successful in 5m0s
CI Pipeline / go-surface (push) Successful in 25s
CI Pipeline / canary (push) Successful in 50s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / required-checks (push) Successful in 0s
Promotion record: (unknown)
2026-10-07 14:09:48 +01:00
Nick 47c2e56393 feat(go): warm-router route allocation — budget-constrained firing (W5b batch 2 parity)
Auto-version and Release / release (push) Successful in 5s
CI Pipeline / lint (push) Successful in 36s
CI Pipeline / go-surface (push) Successful in 25s
CI Pipeline / canary (push) Successful in 50s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 4m58s
deploy / deploy (push) Failing after 15m5s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 4s
The pure slice of route(): top-scoring relevant neuron fires its FULL body (relevance outranks budget — parity law), subsequent neurons fire AAAK summaries when available, token cost max(1, len/4), over-budget neurons skipped (not truncated). 2-case live-oracle battery + budget-skip + top-never-compressed tests. I/O (embeddings, session state, priming, associations, use_history) stays on the caller side per the dichotomy.
2026-10-07 14:04:00 +01:00
Nick 004d168157 sync: remote bump
Auto-version and Release / release (push) Successful in 6s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / go-surface (push) Successful in 25s
CI Pipeline / canary (push) Successful in 53s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 4m59s
deploy / deploy (push) Failing after 15m5s
CI Pipeline / required-checks (push) Successful in 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 5s
2026-10-07 13:55:44 +01:00
Nick 87e1320175 feat(go): warm-router scoring core — gated combination + ACT-R finish bonuses (W5b parity)
Ports _py_score_gate (keyword primary gate, semantic secondary >0.55, context 0.3/trigger capped at 1.0) and _py_score_finish (importance, base-level 0.1x capped 0.3, wash log1p boost, cache 0.1x capped 0.2, body-knowledge overlap 0.03/word capped 0.15, ACT-R fan penalty >10 triggers max 50%, length bonus 0.25 for >6-char triggers, protected boost 4.0 for persona-alfred + philosophy-sagan-core). 7-case live-oracle battery + protected-boost delta test. I/O signals stay arguments (embeddings/SQLite/cache are the caller reach) — the pure core is the parity surface.
2026-10-07 13:55:27 +01:00
conductor 7a27ed557c chore(release): auto-bump version to 0.110.1 — earned by # (patch: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 4s
deploy / deploy (push) Failing after 15m6s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 4s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / unit-tests (push) Successful in 5m21s
CI Pipeline / go-surface (push) Successful in 27s
CI Pipeline / canary (push) Successful in 1m16s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / required-checks (push) Successful in 1s
Promotion record: (unknown)
2026-10-07 13:55:13 +01:00
Nick ec20649e35 sync: remote bump
Auto-version and Release / release (push) Successful in 7s
CI Pipeline / lint (push) Successful in 38s
CI Pipeline / go-surface (push) Successful in 28s
CI Pipeline / canary (push) Successful in 53s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Successful in 5m1s
deploy / deploy (push) Failing after 15m6s
CI Pipeline / required-checks (push) Successful in 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 4s
2026-10-07 13:47:51 +01:00
Nick 303848a0fb fix(go): subsystem-health lock-copy — pointer registry, fields-only marshal (same family as cron/provider fixes) 2026-10-07 13:47:10 +01:00
13 changed files with 537 additions and 36 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.110.0**
**Current release: v0.111.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.110.0"
version = "0.111.0"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+26 -22
View File
@@ -2,9 +2,10 @@
// Replaces src/rune/subsystem_health.py.
//
// Usage:
// subsystem-health record <subsystem> <error> <tier>
// subsystem-health snapshot
// subsystem-health clear
//
// subsystem-health record <subsystem> <error> <tier>
// subsystem-health snapshot
// subsystem-health clear
package main
import (
@@ -24,15 +25,15 @@ type Failure struct {
}
type HealthRegistry struct {
mu sync.Mutex
Failures map[string][]Failure `json:"failures"`
mu sync.Mutex
Failures map[string][]Failure `json:"failures"`
}
type HealthStatus struct {
Subsystem string `json:"subsystem"`
TotalCount int `json:"total_count"`
LastTier int `json:"last_tier"`
LastError string `json:"last_error"`
Subsystem string `json:"subsystem"`
TotalCount int `json:"total_count"`
LastTier int `json:"last_tier"`
LastError string `json:"last_error"`
LastTimestamp string `json:"last_timestamp"`
}
@@ -44,22 +45,23 @@ func healthPath() string {
return filepath.Join(home, "subsystem_health.json")
}
func loadRegistry() HealthRegistry {
var hr HealthRegistry
hr.Failures = make(map[string][]Failure)
func loadRegistry() *HealthRegistry {
hr := &HealthRegistry{Failures: make(map[string][]Failure)}
data, err := os.ReadFile(healthPath())
if err != nil {
return hr
}
json.Unmarshal(data, &hr)
json.Unmarshal(data, hr)
return hr
}
func saveRegistry(hr HealthRegistry) {
hr.mu.Lock()
defer hr.mu.Unlock()
func saveRegistry(hr *HealthRegistry) {
// Marshal ONLY durable state — the embedded sync.Mutex must never be
// copied (go vet copies-lock) and is not persisted state.
data, _ := json.MarshalIndent(struct {
Failures map[string][]Failure `json:"failures"`
}{Failures: hr.Failures}, "", " ")
os.MkdirAll(filepath.Dir(healthPath()), 0755)
data, _ := json.MarshalIndent(hr, "", " ")
os.WriteFile(healthPath(), data, 0644)
}
@@ -95,13 +97,15 @@ func main() {
case "snapshot":
statuses := []HealthStatus{}
for subsystem, failures := range hr.Failures {
if len(failures) == 0 { continue }
if len(failures) == 0 {
continue
}
last := failures[len(failures)-1]
statuses = append(statuses, HealthStatus{
Subsystem: subsystem,
TotalCount: len(failures),
LastTier: last.Tier,
LastError: last.Error,
Subsystem: subsystem,
TotalCount: len(failures),
LastTier: last.Tier,
LastError: last.Error,
LastTimestamp: last.Timestamp.Format(time.RFC3339),
})
}
+11 -8
View File
@@ -3,9 +3,10 @@
// score_neuron, route, and the warm neuron listing.
//
// Usage:
// warm-router base-level <timestamps-csv> <now-iso>
// warm-router score <neuron-json> <text> [--session-context <ctx>]
// warm-router route <text> [--max-tokens N]
//
// warm-router base-level <timestamps-csv> <now-iso>
// warm-router score <neuron-json> <text> [--session-context <ctx>]
// warm-router route <text> [--max-tokens N]
package main
import (
@@ -58,11 +59,13 @@ func baseLevelFromUses(usedAtList []string, nowISO string) float64 {
// ── Neuron scoring (parity: warm_router.py:487) ──
type WarmNeuron struct {
Slug string `json:"slug"`
Triggers []string `json:"triggers"`
Text string `json:"text"`
Importance float64 `json:"importance"`
WashCount int `json:"wash_count"`
Slug string `json:"slug"`
Triggers []string `json:"triggers"`
Text string `json:"text"`
Importance float64 `json:"importance"`
WashCount int `json:"wash_count"`
ID int `json:"id"`
BodyKnowledge string `json:"body_knowledge"`
}
func scoreNeuron(neuron WarmNeuron, text string) map[string]interface{} {
+68
View File
@@ -0,0 +1,68 @@
package main
// route_alloc.go — the pure budget-allocation slice of route(). PARITY with
// warm_router.py's fired-loop decision: the top-scoring relevant neuron
// fires its FULL body (relevance outranks budget — what the user asked
// about is never compressed); everything after fires its AAAK summary when
// available; token cost = max(1, len(body)/4); over-budget neurons are
// SKIPPED (not truncated — the next candidate gets the chance).
//
// The I/O parts of route() (embedding gather, session state, priming,
// association walks, use_history writes) stay on the caller side — the
// dichotomy: Go owns the reactive loop, the pure decision is here.
import (
"sort"
)
// RouteCandidate — one scored neuron entering the allocation.
type RouteCandidate struct {
Slug string
Body string
Summary string // "" = no summary (full body always)
Score float64
}
// FiredNeuron — the allocation verdict for one neuron.
type FiredNeuron struct {
Slug string
Body string
Score float64
Tokens int
Compressed bool
Primed bool
}
// RouteAllocate — the budget-constrained firing decision.
func RouteAllocate(cands []RouteCandidate, maxTokens int) ([]FiredNeuron, int) {
sorted := make([]RouteCandidate, len(cands))
copy(sorted, cands)
sort.Slice(sorted, func(i, j int) bool { return sorted[i].Score > sorted[j].Score })
var fired []FiredNeuron
used := 0
topFired := false
for _, n := range sorted {
if n.Score <= 0 {
continue
}
body := n.Body
if topFired && n.Summary != "" {
body = n.Summary
}
tokens := len(body) / 4
if tokens < 1 {
tokens = 1
}
if len(fired) > 0 && used+tokens > maxTokens {
continue
}
fired = append(fired, FiredNeuron{
Slug: n.Slug, Body: body, Score: n.Score, Tokens: tokens,
Compressed: body != n.Body,
})
used += tokens
topFired = true
}
return fired, used
}
+129
View File
@@ -0,0 +1,129 @@
package main
// scoring.go — the warm-router's deterministic scoring core. PARITY ports
// of warm_router.py's _py_score_gate and _py_score_finish (the H3 slice-3
// pure oracles). I/O signals (embeddings, SQLite base-level, cache file)
// arrive as arguments — the caller gathers them; this code is pure.
//
// Truth battery: truth_gate.json (live Python oracle, 2026-10-07).
import (
"math"
"strings"
)
// ScoreGate — pure gate: (fires, total, keywordHits).
// Signals 1 (keyword) and 3 (context) are pure; signal 2 (semantic) arrives
// as an argument because embeddings are I/O.
func ScoreGate(neuron WarmNeuron, lowText string, sessionContext string, semanticScore float64) (bool, float64, int) {
keywordHits := 0
for _, t := range neuron.Triggers {
if t != "" && strings.Contains(lowText, t) {
keywordHits++
}
}
contextScore := 0.0
if sessionContext != "" {
contextLow := strings.ToLower(sessionContext)
for _, t := range neuron.Triggers {
if t != "" && strings.Contains(contextLow, t) {
contextScore += 0.3
}
}
}
// GATED COMBINATION: keywords are the primary gate; semantic is a
// secondary boost (>0.3); context provides turn continuity.
if keywordHits > 0 {
keywordComponent := float64(keywordHits) * 1.0
semanticBoost := 0.0
if semanticScore > 0.3 {
semanticBoost = math.Max(0, semanticScore-0.3) * 0.5
}
contextComponent := math.Min(contextScore, 1.0)
return true, keywordComponent + semanticBoost + contextComponent, keywordHits
} else if semanticScore > 0.55 {
// No keywords, strong semantic — secondary gate (higher threshold).
return true, semanticScore * 0.7, keywordHits
}
return false, 0.0, keywordHits
}
// ScoreFinish — pure finish: bonuses, penalties, protected boost atop total.
// b_i / cachePriority (nil = skip) arrive as arguments: SQLite and the cache
// file are I/O — the wrapper only gathers them on the fired path.
func ScoreFinish(neuron WarmNeuron, lowText string, total float64, bi *float64, cachePriority *float64, keywordHits int) float64 {
score := total + neuron.Importance
// Signal 4: base-level learning contribution (capped at 0.3).
if neuron.ID != 0 && bi != nil {
bContribution := math.Max(0, math.Min(*bi*0.1, 0.3))
score += bContribution
}
// Signal 5: wash count (drift-with-integrity lifecycle).
if neuron.WashCount > 0 {
washBoost := math.Min(0.3, math.Log1p(float64(neuron.WashCount))/math.Log1p(20)*0.3)
score += washBoost
}
// Signal 6: cache priority.
if neuron.ID != 0 && cachePriority != nil {
score += math.Min(0.2, *cachePriority*0.1)
}
// Signal 7: body-knowledge relevance.
if neuron.BodyKnowledge != "" && lowText != "" {
bkLower := strings.ToLower(neuron.BodyKnowledge)
qWords := strings.Fields(strings.ToLower(lowText))
bkWords := strings.Fields(bkLower)
overlap := 0
for _, qw := range qWords {
for _, bw := range bkWords {
if qw == bw {
overlap++
break
}
}
}
if overlap > 0 {
score += math.Min(0.15, float64(overlap)*0.03)
}
}
// FAN EFFECT PENALTY (ACT-R, Anderson 1974): broad neurons dilute.
triggerCount := 0
for _, t := range neuron.Triggers {
if t != "" {
triggerCount++
}
}
if triggerCount > 10 {
fanPenalty := math.Max(0.5, 1.0-float64(triggerCount-10)*0.1)
score *= fanPenalty
}
// Length bonus for keyword matches.
if keywordHits > 0 {
lengthBonus := 0.0
for _, t := range neuron.Triggers {
if t != "" && len(t) > 6 && strings.Contains(lowText, t) {
lengthBonus += 0.25
}
}
score += lengthBonus
}
// Protected neurons get a boost.
if ProtectedNeurons[neuron.Slug] {
score += ProtectedBoost
}
return score
}
// The protected set + boost — PARITY with warm_router.py:60-61.
var ProtectedNeurons = map[string]bool{"persona-alfred": true, "philosophy-sagan-core": true}
const ProtectedBoost = 4.0
+59
View File
@@ -0,0 +1,59 @@
[
{
"fn": "score_gate",
"name": "kw_hit",
"expected": {
"fires": true,
"total": 2.0,
"kw": 2
}
},
{
"fn": "score_gate",
"name": "no_fire",
"expected": {
"fires": false,
"total": 0.0,
"kw": 0
}
},
{
"fn": "score_gate",
"name": "sem_only",
"expected": {
"fires": true,
"total": 1.25,
"kw": 1
}
},
{
"fn": "score_gate",
"name": "ctx",
"expected": {
"fires": true,
"total": 1.3,
"kw": 1
}
},
{
"fn": "score_finish",
"name": "fin_full",
"expected": {
"score": 3.14
}
},
{
"fn": "score_finish",
"name": "fin_min",
"expected": {
"score": 2.55
}
},
{
"fn": "score_finish",
"name": "fan_penalty",
"expected": {
"score": 1.26
}
}
]
+97
View File
@@ -0,0 +1,97 @@
package main
// truth_gate_test.go — parity gate for the scoring core. Replays
// truth_gate.json (live Python oracle, 2026-10-07) through ScoreGate and
// ScoreFinish.
import (
"encoding/json"
"math"
"os"
"testing"
)
func TestScoreParity(t *testing.T) {
b, err := os.ReadFile("truth_gate.json")
if err != nil {
t.Fatalf("truth_gate.json missing: %v", err)
}
var cases []struct {
Fn string `json:"fn"`
Name string `json:"name"`
Expected map[string]interface{} `json:"expected"`
}
if err := json.Unmarshal(b, &cases); err != nil {
t.Fatalf("unparseable: %v", err)
}
neuron := WarmNeuron{
Slug: "test-neuron", Triggers: []string{"spine", "rebuild"},
Importance: 0.8, ID: 42,
}
for _, c := range cases {
switch c.Name {
case "kw_hit":
fires, total, kw := ScoreGate(neuron, "the spine rebuild verified parity", "", 0.0)
e := c.Expected
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
t.Errorf("kw_hit: got (%v, %v, %v) want %v", fires, total, kw, e)
}
case "no_fire":
fires, total, kw := ScoreGate(neuron, "something unrelated", "", 0.2)
e := c.Expected
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
t.Errorf("no_fire: got (%v, %v, %v) want %v", fires, total, kw, e)
}
case "sem_only":
fires, total, kw := ScoreGate(neuron, "the spinal column rebuild", "", 0.8)
e := c.Expected
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
t.Errorf("sem_only: got (%v, %v, %v) want %v", fires, total, kw, e)
}
case "ctx":
fires, total, kw := ScoreGate(neuron, "spine", "earlier we discussed the spine in detail", 0.0)
e := c.Expected
if fires != e["fires"].(bool) || math.Abs(total-e["total"].(float64)) > 1e-9 || kw != int(e["kw"].(float64)) {
t.Errorf("ctx: got (%v, %v, %v) want %v", fires, total, kw, e)
}
case "fin_full":
bi := 0.5
cp := 0.4
got := ScoreFinish(neuron, "the spine rebuild verified parity", 2.0, &bi, &cp, 2)
if math.Abs(got-c.Expected["score"].(float64)) > 1e-9 {
t.Errorf("fin_full: got %v want %v", got, c.Expected["score"])
}
case "fin_min":
got := ScoreFinish(neuron, "the spine rebuild verified parity", 1.5, nil, nil, 1)
if math.Abs(got-c.Expected["score"].(float64)) > 1e-9 {
t.Errorf("fin_min: got %v want %v", got, c.Expected["score"])
}
case "fan_penalty":
n2 := neuron
triggers := []string{}
for i := 0; i < 12; i++ {
triggers = append(triggers, "t"+string(rune('0'+i%10)))
}
triggers = append(triggers, "spine")
n2.Triggers = triggers
got := ScoreFinish(n2, "spine appears", 1.0, nil, nil, 1)
if math.Abs(got-c.Expected["score"].(float64)) > 1e-9 {
t.Errorf("fan_penalty: got %v want %v", got, c.Expected["score"])
}
default:
t.Errorf("unknown truth case %q", c.Name)
}
}
}
// TestProtectedBoost — the persona anchors must outscore everything on
// their topic (that is the entire point of the boost).
func TestProtectedBoost(t *testing.T) {
protected := WarmNeuron{Slug: "persona-alfred", Triggers: []string{"composure"}, Importance: 0.8}
ordinary := WarmNeuron{Slug: "ordinary", Triggers: []string{"composure"}, Importance: 0.8}
p := ScoreFinish(protected, "composure under pressure", 1.0, nil, nil, 1)
o := ScoreFinish(ordinary, "composure under pressure", 1.0, nil, nil, 1)
if p-o != ProtectedBoost {
t.Errorf("protected delta = %v, want %v", p-o, ProtectedBoost)
}
}
+58
View File
@@ -0,0 +1,58 @@
[
{
"fn": "route_alloc",
"name": "budget",
"max_tokens": 300,
"expected": {
"fired": [
{
"slug": "big-primary",
"tokens": 100,
"compressed": false,
"body_len": 400
},
{
"slug": "medium",
"tokens": 3,
"compressed": true,
"body_len": 14
},
{
"slug": "small",
"tokens": 12,
"compressed": false,
"body_len": 50
}
],
"used": 115
}
},
{
"fn": "route_alloc",
"name": "generous",
"max_tokens": 10000,
"expected": {
"fired": [
{
"slug": "big-primary",
"tokens": 100,
"compressed": false,
"body_len": 400
},
{
"slug": "medium",
"tokens": 3,
"compressed": true,
"body_len": 14
},
{
"slug": "small",
"tokens": 12,
"compressed": false,
"body_len": 50
}
],
"used": 115
}
}
]
+83
View File
@@ -0,0 +1,83 @@
package main
// truth_route_test.go — batch-2 parity gate: the budget-allocation decision
// against truth_route.json (live Python walk, 2026-10-07).
import (
"encoding/json"
"os"
"testing"
)
func TestRouteAllocParity(t *testing.T) {
b, err := os.ReadFile("truth_route.json")
if err != nil {
t.Fatalf("truth_route.json missing: %v", err)
}
var cases []struct {
Name string `json:"name"`
Expected struct {
Fired []struct {
Slug string `json:"slug"`
Tokens int `json:"tokens"`
Compressed bool `json:"compressed"`
BodyLen int `json:"body_len"`
} `json:"fired"`
Used int `json:"used"`
} `json:"expected"`
}
if err := json.Unmarshal(b, &cases); err != nil {
t.Fatalf("unparseable: %v", err)
}
cands := []RouteCandidate{
{Slug: "big-primary", Body: string(make([]byte, 400)), Summary: "primary summary", Score: 2.5},
{Slug: "medium", Body: string(make([]byte, 200)), Summary: "medium summary", Score: 1.2},
{Slug: "small", Body: string(make([]byte, 50)), Summary: "", Score: 0.8},
}
for _, c := range cases {
fired, _ := RouteAllocate(cands, c.Expected.Used+1000) // generous budget
if len(fired) != len(c.Expected.Fired) {
t.Fatalf("%s: fired %d, want %d", c.Name, len(fired), len(c.Expected.Fired))
}
for i, f := range fired {
w := c.Expected.Fired[i]
if f.Slug != w.Slug || f.Tokens != w.Tokens || f.Compressed != w.Compressed || len(f.Body) != w.BodyLen {
t.Errorf("%s[%d]: got {slug=%s tokens=%d compressed=%v bodyLen=%d} want {slug=%s tokens=%d compressed=%v bodyLen=%d}",
c.Name, i, f.Slug, f.Tokens, f.Compressed, len(f.Body), w.Slug, w.Tokens, w.Compressed, w.BodyLen)
}
}
}
}
// TestRouteBudgetSkip — the over-budget behavior: skipped, not truncated,
// and the next candidate gets the chance.
func TestRouteBudgetSkip(t *testing.T) {
cands := []RouteCandidate{
{Slug: "primary", Body: string(make([]byte, 400)), Score: 2.5},
{Slug: "huge", Body: string(make([]byte, 2000)), Score: 1.5},
{Slug: "fits", Body: string(make([]byte, 80)), Score: 1.0},
}
fired, used := RouteAllocate(cands, 200)
slugs := []string{}
for _, f := range fired {
slugs = append(slugs, f.Slug)
}
if len(slugs) != 2 || slugs[0] != "primary" || slugs[1] != "fits" {
t.Errorf("budget skip: got %v (used %d), want [primary fits]", slugs, used)
}
if used > 200 {
t.Errorf("budget exceeded: used %d > 200", used)
}
}
// TestTopNeverCompressed — parity law: the top hit fires its full body even
// when a summary exists.
func TestTopNeverCompressed(t *testing.T) {
cands := []RouteCandidate{
{Slug: "top", Body: string(make([]byte, 400)), Summary: "a summary", Score: 2.0},
}
fired, _ := RouteAllocate(cands, 50) // tiny budget — top still full
if len(fired) != 1 || fired[0].Compressed || fired[0].Tokens != 100 {
t.Errorf("top compressed under tiny budget: %+v", fired[0])
}
}
+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.110.0"
__version__ = "0.111.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
+1 -1
View File
@@ -233,7 +233,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.110.0"' in rune_version,
'__version__ = "0.111.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.110.0
Version: v0.111.0
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -1948,7 +1948,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.110.0 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.111.0 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours