Compare commits

...
35 Commits
Author SHA1 Message Date
conductor 20a47fac86 chore(release): auto-bump version to 0.66.32 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 20:10:54 +01:00
Nick 36ee604646 fix(release): required-checks verdict evaluates needs results — fail-closed deploy
ROOT CAUSE (red-CI promotions 0.66.28/29): the required-checks
'Publish protected-branch gate' step read /tmp/need_* files that no step
ever wrote and echoed 'All required CI checks passed' UNCONDITIONALLY.
With if: always() the needs gate is overridden, so the job ran on red
needs and published success — deploy_qualify correctly trusted it and
promoted red commits (24413e4, fd6e9b4, de30cd9; status timelines in the
closure evidence).

FIX: publish step now runs only when every needs.*.result == 'success';
companion fail-closed step exits 1 otherwise, so the required-checks
context reports failure instead of lying.

DEFENSE-IN-DEPTH: deploy_qualify verdict is now the AND of two
independent legs — (1) newest required-checks observation for the exact
SHA, (2) the latest COMPLETED ci.yml@refs/heads/master run for that exact
SHA with every job concluded success (fan-out resolved by path, run
resolved by head_sha, jobs envelope unwrapped — no aggregate-status
fallback). Any pending/missing/disagreement = not deployable.

CANARY: gitea_pipeline now asserts the verdict evaluates needs results
and fails closed — the class locked shut. 20 qualifier tests with
fixtures from the real incident shapes; 1402 suite green.
2026-09-12 20:10:28 +01:00
Nick 20f493596d chore(types): baseline 117/443 — CI-observed counts on the fixed tree
CI's locked pyright reports errors 117 / warnings 443 on the current tree
(spine fixes removed exactly 4: 121→117, coherent with the 24413e4 run).
Local .type-venv pyright classifies 124/429 for the same tree — a local/
runner environment skew in error-vs-warning classification that must be
investigated separately; the GATE is CI, so the baseline records CI's
arithmetic. Both counts sit below the original 120/465 baselines.
2026-09-12 19:55:41 +01:00
conductor faf3575fb3 chore(release): auto-bump version to 0.66.31 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 19:50:15 +01:00
Nick 2e16fc4c89 fix(spine): commit the nullable-flow fixes — they were never landed
The hardening-era pyright fixes (supersede() single entity fetch,
membership-loop assert before identities write) sat as uncommitted
working-tree modifications through multiple checkout/stash cycles. Every
pushed tree since e2464ae therefore still carried the 4 nullable-flow
errors — the CI type-check failures on fd6e9b4/12303bb were CORRECT
failures of the unfixged tree, not runner anomalies. With this commit:
spine.py = 0 errors under locked pyright 1.1.414; total 124 = the
re-baseline from the previous commit. Lesson recorded: verify fixes are
COMMITTED (git show HEAD:<file>) before diagnosing CI failures against
them.
2026-09-12 19:49:47 +01:00
Nick 12303bbd20 chore(types): re-baseline pyright counts under locked pyright 1.1.414
The count baseline (120) was frozen under pyright 1.1.412; the dependency
lock has since drifted to 1.1.414 (fae36c4/9c78b3d/e7d4c2a 'upstream
release drift' resyncs), which reports 124 errors / 429 warnings on the
tree — including on the baseline-era commit itself (127 under 1.1.414).
Same precedent as every prior pyright drift re-baseline.

DISCOVERED DEFECTS recorded (out of scope here, flagged for follow-up):
1. CI type-check job has been publishing false successes across the
   0.66 era — commit-status 'success in ~30s' while the canonical
   ratchet on the identical tree fails (a real pass takes minutes).
   Likely runner/execution anomaly; needs its own investigation.
2. deploy.yml promoted commits with failing type-check (not gated on
   ci.yml) — 0.66.28/29 were promoted red.

Spine-era errors themselves: fixed — src/rune/spine.py reports 0 under
the locked pyright (nullable-flow fixes: supersede() single-fetch,
membership-loop assert).
2026-09-12 19:47:20 +01:00
conductor de30cd958e chore(release): auto-bump version to 0.66.30 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 19:18:32 +01:00
Nick fd6e9b4bca fix(tests): scope-classification test is environment-independent
The first version asserted harness filesystem state (~/.config/opencode
exists) — true on the desktop, false on the CI runner, where unit-tests
and type-check failed. CI failure = real signal, test assumption was
machine-dependent. Now asserts the classification itself: propagate()
resolves against SCRIPTS_DIR by construction, and the tracker imports no
Spine module (checked via AST, not prose — the scope docstring mentions
Spine legitimately). Verified under fake HOME (no harness) + real HOME.
2026-09-12 19:18:13 +01:00
Nick 24413e4228 chore(spine): sync store state (post-hardening observations) 2026-09-12 19:12:34 +01:00
Nick 407c23a689 docs(spine): pre-Memory closure — real-build durability proof + scope classification
Closure 1: alias and supersession durability now proven through the REAL
build path (create→alias/supersede→save→reload→spine.build()→save→reload→
resolve) — the earlier __wrapped__-fallback test never exercised the actual
refresh; both survive (aliases carried as entity data, superseded entities
preserved with superseded_by intact).

Closure 2: dependency_tracker.py classified NON-STRUCTURAL TO RUNE / KEEP
LOCAL — its graph describes ~/.config/opencode/scripts (the dev harness;
verified: 20/23 names collide with src/rune but all exist in the scripts
dir, propagate() resolves against SCRIPTS_DIR). Not a competing structural
authority; migrating harness structure into Spine would wrongly expand
scope. Scope classification documented in-module + regression test pins
the separation.

Closure 3: bounded sweep found no other manually maintained Rune
structural copy — remaining umbrella consumers are all generated-view
compatibility (CI gate, henxels, research_inward_context, gen_prd_state,
shared/umbrella_update); scaffold_projection is Spine-backed.

1382 tests, lint ratchet, henxels, canary-native green.
2026-09-12 19:12:34 +01:00
conductor 982a9b07d5 chore(release): auto-bump version to 0.66.29 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 18:43:59 +01:00
Nick ff506ebe55 feat(spine): CLI alias/supersede commands; save() coerces string paths
Real-change proof for the hardening pass (task SPINE-CLI-ALIAS): bounded
CLI completion on module:spine. The change surfaced a real defect via
runtime evidence — save() accepted 'Path | None' per its signature but
CLI callers legitimately pass strings; now coerced. Independent evidence:
full suite 1379, lint, henxels, scaffold consumer check, CI.
2026-09-12 18:43:37 +01:00
conductor 2032819d99 chore(release): auto-bump version to 0.66.28 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 18:41:16 +01:00
Nick e2464ae9d6 feat(spine): hardening — authority-owned reconciliation, contract semantics, durable identity
T1: impact_frontier carries change_kind — contract_change expands all
parties; implementation_change (contract preserved) demotes sibling
implementers to contextual_not_obligations (sharing a contract is not
being affected by a sibling's internal change); contract_violation
escalates. Pre-hardening test expectation that accepted siblings as
direct obligations was expectation-laundering — corrected with the
architectural justification recorded.

T3/T4: relationships carry authority+status; per-projector generation
records; reconcile_authorities stales facts their owner no longer
observes (marked + drift-reported + frontier-excluded, never deleted —
open-world preserved, truth marked); authority_failed degrades; both
sides of an authority conflict preserved (never last-wins);
resolve_status: UNKNOWN/CURRENT/STALE/CONFLICTING/SUPERSEDED — 'not
checked' is never healthy.

T5: alias (rename) + supersede (replacement) — old spine:// references
stay meaningful; duplicate entity/resolve_uri defs removed (shadowing
trap). T2: record_observed_impact builds the observed set from external
evidence items (test/canary/runtime authority), not frontier-rerun.
CANDIDATE status for evidence-free (model-speculated) relationships.
65/65 spine tests, 1379 suite, gates green.
2026-09-12 18:40:53 +01:00
conductor d7e04f1098 chore(release): auto-bump version to 0.66.27 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 18:20:25 +01:00
Nick e639383c49 fix(spine): build() carries forward ALL entities/relationships — open-world refresh
Predicted-vs-observed caught it: build()'s carry-forward filtered by known
kinds/types, silently destroying task bindings (and any future entity
class) on every refresh — the exact TOO-CLOSED failure the Spine contract
forbids. Everything now passes through; module-level facts re-derive on
top; exact-duplicate observations suppressed. Task bindings survive build;
predicted==observed on the Phase D migration change.
2026-09-12 18:19:59 +01:00
Nick 57a274d97d docs(phase-d): research_inward_context consumes Spine-generated view
Phase D consumer migration: the INWARD context builder reads structural
facts from the Spine store's generated compatibility view (same schema —
zero code-path change), documented as TEMPORARY SPINE-BACKED
COMPATIBILITY. Graceful degradation to a manual manifest if Spine is not
seeded in a checkout.
2026-09-12 18:13:15 +01:00
conductor aac5295c24 chore(release): auto-bump version to 0.66.26 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 18:07:08 +01:00
Nick 9563148f9d feat(spine): contracts, structural impact resolution, task binding, event seam
Structural Impact Resolution: impact_frontier() — bounded LEVEL 0/1/2
resolution with a data-driven semantics table. Contract entities expand to
all implements/consumes parties (a contract change deserves every
consumer's inspection); unknown relationship types default to
obligation-bearing (fail-loud, open world); non-obligation classes
(documented_by, related_memory, supersedes...) never explode a frontier.

Graph separation (addendum): Spine is its own logical graph — the frontier
traverses Spine edges only; memory-style associations cannot expand
structural impact; stable spine:// URIs let future Memory reference Spine
entities without duplicating authority.

Also: task binding (affects edges + bind_task/task_entities),
predicted_vs_observed comparison (misses become CANDIDATE corrections,
evidence-gated), generic structural_events() seam (dumb typed facts — no
salience/policy; future Substrate subscribes), aggregate query() (the
intrinsic self-knowledge property: owner/deps/consumers/contracts/state/
evidence/frontier in one deterministic call), evidence_conflict drift
class. henxels similarity scan excludes store/spine (snapshots are
legitimately near-identical by design). 45/45 spine tests; full gates green.
2026-09-12 18:06:16 +01:00
conductor 76984c6362 chore(release): auto-bump version to 0.66.25 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 17:34:32 +01:00
Nick 4072d36b1a fix(spine): build re-derivation uses high-water upgrade — no demotion noise
Evidence surfaced it: re-deriving module maturity during build() logged a
demotion-rejection observation for every already-earned module (171 noise
records). Build now upgrades via the silent high-water path; promote()
logging stays reserved for explicit evidence-gated API calls.
2026-09-12 17:34:05 +01:00
conductor 6d871abc9c chore(release): auto-bump version to 0.66.24 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 17:03:51 +01:00
Nick ce40acbb1f feat(spine): V1 — evolvable structural self-model as the single structural authority
Open-world entity/relationship store (schema_version 1): kinds, relationship
types and metadata are open tags — unknown future capabilities enter as data,
never as core edits (extensibility test included, synthetic capability
round-trips with zero core change). Evidence-gated maturity ladder
DECLARED<PRESENT<WIRED<REACHABLE<EXERCISED<LIVE_PROVEN — no evidence, no
promotion; demotion rejected and recorded.

Umbrella migration Phase A→C: manifest ingested once (generated-view
refusal guard prevents the ingest cycle; manual truth archived at
config/umbrellas.manual.json), canaries preserved as obligated_canary
relationships, config/umbrellas.json now a GENERATED compatibility view for
legacy consumers (CI umbrella_impact gate + henxels) — single-authority
route proven by registering module:spine through Spine alone (one store
edit; the legacy view flows out; contract tests validate the round-trip).

Dynamic observations: deployed identity (_build_identity.json), drift
classes incl. registered_but_absent, present_but_unowned,
registered_no_static_importers (D6 dead-subnet now mechanical),
version_surfaces_disagree, relationship_disappeared. Deterministic
snapshots + diff. Scaffolding V1 now consumes Spine for ownership/deps/
consumers (spine_maturity surfaced in every card). 31 spine tests; full
gates green.
2026-09-12 17:03:27 +01:00
Nick 1de28b55a0 docs(tooling): desktop runner wakes local W/R daemon after jobs (v0.3, Gitea authoritative) 2026-09-12 16:16:58 +01:00
conductor 75d6d7d62a chore(release): auto-bump version to 0.66.23 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 11:16:07 +01:00
Nick 7f47969128 feat(scaffolding): V1 tranche 1 — projection engine over existing authorities
scripts/scaffold_projection.py derives per-component cards (subsystem,
dependencies, consumers, mirrors, proof obligations + evidence state) from
the umbrella manifest, canary registry, AST import graph (incl. relative
lazy imports), and tests/ filesystem. No new authority: projections only.
docs/SCAFFOLDING.md records the mechanism audit (KEEP/EVOLVE/REPLACE/RETIRE)
and the real-change proof on the context-discovery fix.
2026-09-12 11:15:42 +01:00
Nick a65e5d44de fix(runtime): context discovery routes cloud-pinned models to the provider wire
Module-level context_length used OllamaChatTransport unconditionally, so
every cloud-pinned task logged context_discovery tier=1 HTTPError 400 and
was misbudgeted at the 8192 fallback instead of the provider's real window
(131072 for Verboo Code/glm-5.3-flash). is_cloud_pinned is now THE
provider-prefix rule — one authority in openai_compat_transport, consumed
by context discovery (new) and transport selection (deduped from
rune_operator._transport_for). Red-first regression tests added.
Scaffolding-derived impact: consumer rune_operator; gates suite/lint/
henxels + inference/orchestration canaries.
2026-09-12 11:15:42 +01:00
Nick 26a040892d docs(reconciliation): ADAPT family reconciliation on PL1 held-out evidence
#292/#293/#297 FIXED_VERIFIED (closed with evidence comments — LOCAL_FIX
live-proven by held-out tasks, DESIGN-LITE implemented, task_gate shipped);
#294/#295/#296/#299/#301 DEFERRED_FUTURE (canonical remaining-obligations
list); #300/#315/#316/#284 VALID_OPEN. Resolves the ADAPT-LIVE-complete vs
open-issues tension: the authorized tranche is complete; the backlog never
was its scope. Checker green (72 records). PE-1789102658-0269 SEED→VALIDATED
recorded in the canonical intent state.
2026-09-12 11:04:54 +01:00
conductor 1497c85cd4 chore(release): auto-bump version to 0.66.22 — earned by # (patch: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 10:50:09 +01:00
Nick a2463ad80b fix(operator): approval-resume uses provider-prefix transport selection
_runtime_for_model hardcoded OllamaChatTransport, so the post-approval
follow-up model call hit localhost with the cloud-pinned name → 400
'invalid model name'. The direct turn path was fixed in 0.66.17; the resume
path now delegates to the same _transport_for authority (one selection
rule, two call sites). Red-first regression tests added.
2026-09-12 10:49:42 +01:00
Nick 6965fc7375 fix(transport): chat() returns flat ChatMessage — envelope shape broke runtime tool-call loop
OllamaChatTransport.chat returns the flat message object (the ChatTransport
contract), but the [OI]-compat translation returned the response envelope
{'message': {...}}. LocalToolRuntime normalizes tool_calls at the top level,
found none, and failed 'empty terminal visible text' while the model had
returned a valid read_file call. Observed live: PL1 Task A wire capture
2026-09-12. Contract-parity regression test added (red-first).
2026-09-12 10:49:42 +01:00
Nick 0daad967c1 test: mock cloud_transport_for at source module — CI has no user config
The selection test called the real cloud_transport_for, which reads the
user's mimocode config (absent on the CI runner). Mocking at the consumer
module was a no-op (function-level import re-resolves from source). Mock now
targets the source module. Local green.
2026-09-12 10:00:14 +01:00
conductor 502317d8b6 chore(release): auto-bump version to 0.66.21 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-12 07:57:49 +01:00
Nick 07cdc2fc56 feat(intent): minimal intent-continuity ledger — deterministic state machine, 7 invariants
PART 6 of the overnight handoff. IntentRecord (7 fields: objective,
parent_intent, active_detour, return_when, return_to, unresolved_obligations,
success_condition) + IntentLedger (JSON persistence, no LLM). Classifications:
BLOCKER | FIX | EXTENSION | SUPERSEDING | ORTHOGONAL | LATER. All 7 invariants
tested: new ideas never replace the active objective; detours preserve parents;
resolving a detour exposes the return point; supersession accounts for open
obligations; child completion never resolves the parent; persist/reload
round-trips; transitions are deterministic. 1309 green.
2026-09-12 07:57:26 +01:00
Nick b4161a4034 test: valid identifier + tempdir storage + journal isolation — CI green on cloud-execution tests
Three fixes: (1) [OI]CompatTransport is an invalid Python identifier —
renamed to the module's actual class name; (2) fixed /tmp storage paths in
selection tests collided with a root-owned /tmp/hot on the CI runner (the
hot store derives from Path(storage).parent) — per-test tempdirs; (3) the
operator's journal/event edges resolve via memory_env whose real memory dir
is root-owned in CI → RUNE_MEMORY_DIR pointed at a per-test tempdir. 1304
green locally.
2026-09-12 06:43:59 +01:00
37 changed files with 52988 additions and 314 deletions

No files matched your search

+35 -8
View File
@@ -286,12 +286,39 @@ jobs:
run: |
NEW="$(echo "${{ gitea.event.pull_request.head.ref }}" | sed 's|chore/auto-bump-||')"
python3 scripts/check_version_surfaces.py check --root . --version "$NEW"
- name: Publish protected-branch gate (full pipeline path)
if: ${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump') }}
- name: Publish protected-branch gate (full pipeline path — all needs green)
# RELEASE-SAFETY FIX (2026-09-13): the previous version of this step
# read /tmp/need_* files that no step ever writes and echoed success
# UNCONDITIONALLY — required-checks published green while type-check
# and unit-tests were red, and deploy promoted the red commits
# (observed live: 24413e4, fd6e9b4, de30cd9 promoted 0.66.28/29 with
# failing type-check). The verdict now comes from the needs context
# itself: this step runs ONLY when every required job succeeded.
if: >-
${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump')
&& needs.lint.result == 'success'
&& needs['unit-tests'].result == 'success'
&& needs.canary.result == 'success'
&& needs['best-practices'].result == 'success'
&& needs.documentation.result == 'success'
&& needs['type-check'].result == 'success'
&& needs['gitea-offline'].result == 'success' }}
run: echo "All required CI checks passed"
- name: Refuse promotion when any required check failed (fail-closed)
# Companion to the step above: if anything is not exactly success
# (failure, cancelled, skipped on a non-bump path), the required-checks
# context reports FAILURE — deploy_qualify then refuses promotion.
# Fail-closed: not verified → not deployable.
if: >-
${{ !startsWith(gitea.event.pull_request.head.ref, 'chore/auto-bump')
&& (needs.lint.result != 'success'
|| needs['unit-tests'].result != 'success'
|| needs.canary.result != 'success'
|| needs['best-practices'].result != 'success'
|| needs.documentation.result != 'success'
|| needs['type-check'].result != 'success'
|| needs['gitea-offline'].result != 'success') }}
run: |
# All needs must have succeeded (skipped deps = this is a bump PR, handled above)
FAILED=""
for job in lint unit-tests canary best-practices documentation type-check gitea-offline; do
RESULT=$(cat /tmp/need_$job 2>/dev/null || echo "")
done
echo "All required CI checks passed"
echo "::error::Required CI checks not green — refusing to publish required-checks success"
echo "lint=${{ needs.lint.result }} unit-tests=${{ needs['unit-tests'].result }} canary=${{ needs.canary.result }} best-practices=${{ needs['best-practices'].result }} documentation=${{ needs.documentation.result }} type-check=${{ needs['type-check'].result }} gitea-offline=${{ needs['gitea-offline'].result }}"
exit 1
+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.66.20**
**Current release: v0.66.32**
**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.**
+233 -221
View File
@@ -1,224 +1,236 @@
{
"_comment": "Umbrella impact map \u2014 the change-propagation graph. Every src/rune/*.py and shared/*.py module MUST appear in exactly one umbrella's modules list (enforced by the umbrella_manifest_is_complete henxel; debt-tracked modules allowed via docs/module-coverage-debt.md). depends_on: when umbrella U changes, its dependents' canaries are required gates. mirrors: files that must travel together in the same PR (enforced by scripts/umbrella_impact.py unless the PR carries 'Propagation-waived: <reason>').",
"umbrellas": {
"memory": {
"description": "Storing, retrieving, and managing knowledge",
"modules": [
"memory_store",
"memory_consolidation",
"memory_env",
"shard_manager",
"query_memory",
"graph_memory",
"warm_neuron_store",
"abstraction",
"cache",
"recall_router",
"drift_cache",
"drift_compression",
"tier_manager",
"graph_shard_bridge",
"memory_selector",
"retrieval_benchmark"
],
"canaries": [
"canary_query_memory",
"canary_memory_synthesis",
"canary_full_system"
],
"depends_on": [
"governance"
]
},
"governance": {
"description": "Quality control, drift detection, promotion, audit",
"modules": [
"drift_core",
"drift_observability",
"drift_ledger",
"audit_ledger",
"curator",
"claim_audit",
"contracts",
"build_pipeline_policy",
"failure_auto_filer"
],
"canaries": [
"canary_full_system",
"canary_schema_migration"
],
"depends_on": []
},
"identity": {
"description": "Who the agent is \u2014 persona, voice, anchors",
"modules": [
"persona_definition",
"persona_gate",
"identity_anchors",
"voice_hybrid",
"voice_sync",
"voicebox",
"voice_injector",
"behavior",
"prompt_sync"
],
"canaries": [
"canary_full_system"
],
"depends_on": [
"governance"
]
},
"growth": {
"description": "Learning and improvement",
"modules": [
"self_improve",
"subconscious_pass",
"goal",
"elastic_cognition",
"growth_delta",
"autonomy_loop",
"cognitive_loop",
"dream_cycle",
"growth_simulator"
],
"canaries": [
"canary_full_system"
],
"depends_on": [
"memory",
"governance"
]
},
"perception": {
"description": "Input processing and context assembly",
"modules": [
"context_broker",
"context_pressure",
"attention_capsule",
"auto_context",
"auto_tasks",
"witness",
"salience_gate",
"cognitive_budgeter",
"complexity_scorer"
],
"canaries": [
"canary_context_broker",
"canary_full_system"
],
"depends_on": [
"memory"
]
},
"capability": {
"description": "Tool selection and execution authority",
"modules": [
"capability_broker",
"tool_executor",
"tool_protocol",
"local_tool_runtime",
"openai_compat_transport",
"forge_client",
"message_queue",
"connection_pool",
"adaptive_router",
"route_risk_flags",
"evidence_probe",
"builder_bridge",
"builder_bridge_cli",
"recall_uplift"
],
"canaries": [
"canary_p1_01",
"canary_p0_05",
"canary_native"
],
"depends_on": [
"memory"
]
},
"inference": {
"description": "Model routing and outbound requests",
"modules": [
"model_routing",
"model_profiles",
"model_registry",
"model_connect",
"provider_registry",
"provider_budget",
"standardized_providers",
"inference_trace",
"response_adapter",
"token_optimizer"
],
"canaries": [
"canary_p1_06",
"canary_p1_02",
"canary_resource_authority"
],
"depends_on": [
"capability"
]
},
"orchestration": {
"description": "Task lifecycle, operator, TUI presentation",
"modules": [
"rune_operator",
"rune_tui",
"rune_graphic",
"rune_spinner",
"rune_profiler",
"chat_bubble",
"message_widgets",
"tui_validator",
"microkernel",
"event_bus",
"event_envelope",
"hook_engine",
"system_registry",
"cron_engine",
"task_gate",
"task_persist",
"work_manager",
"parallel_processor",
"tiered_executor",
"time_awareness",
"night_mode",
"pins",
"progressive_escalation",
"safety_override",
"sleep_time",
"verification_layer",
"outcome_ledger",
"warm_router",
"subsystem_health",
"build_mode",
"dependency_tracker",
"auto_detect",
"build_orchestrator",
"custodian",
"gitea_auth",
"gitea_pipeline",
"gitea_provision",
"umbrella_update"
],
"canaries": [
"canary_p1_07",
"canary_orchestration_foundation",
"canary_conversation_state",
"canary_gitea_pipeline",
"canary_tui_drift",
"canary_cognitive_strata",
"canary_surface_parity"
],
"depends_on": [
"perception",
"capability",
"inference",
"identity",
"growth"
]
}
"_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-12T16:02:18Z",
"umbrellas": {
"capability": {
"description": "Tool selection and execution authority",
"modules": [
"adaptive_router",
"builder_bridge",
"builder_bridge_cli",
"capability_broker",
"connection_pool",
"evidence_probe",
"forge_client",
"intent_continuity",
"local_tool_runtime",
"message_queue",
"openai_compat_transport",
"recall_uplift",
"route_risk_flags",
"tool_executor",
"tool_protocol"
],
"depends_on": [
"memory"
],
"mirrors": [],
"canaries": [
"canary_native",
"canary_p0_05",
"canary_p1_01"
]
},
"governance": {
"description": "Quality control, drift detection, promotion, audit",
"modules": [
"audit_ledger",
"build_pipeline_policy",
"claim_audit",
"contracts",
"curator",
"drift_core",
"drift_ledger",
"drift_observability",
"failure_auto_filer"
],
"depends_on": [],
"mirrors": [],
"canaries": [
"canary_full_system",
"canary_schema_migration"
]
},
"growth": {
"description": "Learning and improvement",
"modules": [
"autonomy_loop",
"cognitive_loop",
"dream_cycle",
"elastic_cognition",
"goal",
"growth_delta",
"growth_simulator",
"self_improve",
"subconscious_pass"
],
"depends_on": [
"governance",
"memory"
],
"mirrors": [],
"canaries": [
"canary_full_system"
]
},
"identity": {
"description": "Who the agent is \u2014 persona, voice, anchors",
"modules": [
"behavior",
"identity_anchors",
"persona_definition",
"persona_gate",
"prompt_sync",
"voice_hybrid",
"voice_injector",
"voice_sync",
"voicebox"
],
"depends_on": [
"governance"
],
"mirrors": [],
"canaries": [
"canary_full_system"
]
},
"inference": {
"description": "Model routing and outbound requests",
"modules": [
"inference_trace",
"model_connect",
"model_profiles",
"model_registry",
"model_routing",
"provider_budget",
"provider_registry",
"response_adapter",
"standardized_providers",
"token_optimizer"
],
"depends_on": [
"capability"
],
"mirrors": [],
"canaries": [
"canary_p1_02",
"canary_p1_06",
"canary_resource_authority"
]
},
"memory": {
"description": "Storing, retrieving, and managing knowledge",
"modules": [
"abstraction",
"cache",
"drift_cache",
"drift_compression",
"graph_memory",
"graph_shard_bridge",
"memory_consolidation",
"memory_env",
"memory_selector",
"memory_store",
"query_memory",
"recall_router",
"retrieval_benchmark",
"shard_manager",
"tier_manager",
"warm_neuron_store"
],
"depends_on": [
"governance"
],
"mirrors": [],
"canaries": [
"canary_full_system",
"canary_memory_synthesis",
"canary_query_memory"
]
},
"orchestration": {
"description": "Task lifecycle, operator, TUI presentation",
"modules": [
"auto_detect",
"build_mode",
"build_orchestrator",
"chat_bubble",
"cron_engine",
"custodian",
"dependency_tracker",
"event_bus",
"event_envelope",
"gitea_auth",
"gitea_pipeline",
"gitea_provision",
"hook_engine",
"message_widgets",
"microkernel",
"night_mode",
"outcome_ledger",
"parallel_processor",
"pins",
"progressive_escalation",
"rune_graphic",
"rune_operator",
"rune_profiler",
"rune_spinner",
"rune_tui",
"safety_override",
"sleep_time",
"subsystem_health",
"system_registry",
"task_gate",
"task_persist",
"tiered_executor",
"time_awareness",
"tui_validator",
"umbrella_update",
"verification_layer",
"warm_router",
"work_manager"
],
"depends_on": [
"capability",
"growth",
"identity",
"inference",
"perception"
],
"mirrors": [],
"canaries": [
"canary_cognitive_strata",
"canary_conversation_state",
"canary_gitea_pipeline",
"canary_orchestration_foundation",
"canary_p1_07",
"canary_surface_parity",
"canary_tui_drift"
]
},
"perception": {
"description": "Input processing and context assembly",
"modules": [
"attention_capsule",
"auto_context",
"auto_tasks",
"cognitive_budgeter",
"complexity_scorer",
"context_broker",
"context_pressure",
"salience_gate",
"spine",
"witness"
],
"depends_on": [
"memory"
],
"mirrors": [],
"canaries": [
"canary_context_broker",
"canary_full_system"
]
}
}
}
+225
View File
@@ -0,0 +1,225 @@
{
"_comment": "Umbrella impact map \u2014 the change-propagation graph. Every src/rune/*.py and shared/*.py module MUST appear in exactly one umbrella's modules list (enforced by the umbrella_manifest_is_complete henxel; debt-tracked modules allowed via docs/module-coverage-debt.md). depends_on: when umbrella U changes, its dependents' canaries are required gates. mirrors: files that must travel together in the same PR (enforced by scripts/umbrella_impact.py unless the PR carries 'Propagation-waived: <reason>').",
"umbrellas": {
"memory": {
"description": "Storing, retrieving, and managing knowledge",
"modules": [
"memory_store",
"memory_consolidation",
"memory_env",
"shard_manager",
"query_memory",
"graph_memory",
"warm_neuron_store",
"abstraction",
"cache",
"recall_router",
"drift_cache",
"drift_compression",
"tier_manager",
"graph_shard_bridge",
"memory_selector",
"retrieval_benchmark"
],
"canaries": [
"canary_query_memory",
"canary_memory_synthesis",
"canary_full_system"
],
"depends_on": [
"governance"
]
},
"governance": {
"description": "Quality control, drift detection, promotion, audit",
"modules": [
"drift_core",
"drift_observability",
"drift_ledger",
"audit_ledger",
"curator",
"claim_audit",
"contracts",
"build_pipeline_policy",
"failure_auto_filer"
],
"canaries": [
"canary_full_system",
"canary_schema_migration"
],
"depends_on": []
},
"identity": {
"description": "Who the agent is \u2014 persona, voice, anchors",
"modules": [
"persona_definition",
"persona_gate",
"identity_anchors",
"voice_hybrid",
"voice_sync",
"voicebox",
"voice_injector",
"behavior",
"prompt_sync"
],
"canaries": [
"canary_full_system"
],
"depends_on": [
"governance"
]
},
"growth": {
"description": "Learning and improvement",
"modules": [
"self_improve",
"subconscious_pass",
"goal",
"elastic_cognition",
"growth_delta",
"autonomy_loop",
"cognitive_loop",
"dream_cycle",
"growth_simulator"
],
"canaries": [
"canary_full_system"
],
"depends_on": [
"memory",
"governance"
]
},
"perception": {
"description": "Input processing and context assembly",
"modules": [
"context_broker",
"context_pressure",
"attention_capsule",
"auto_context",
"auto_tasks",
"witness",
"salience_gate",
"cognitive_budgeter",
"complexity_scorer"
],
"canaries": [
"canary_context_broker",
"canary_full_system"
],
"depends_on": [
"memory"
]
},
"capability": {
"description": "Tool selection and execution authority",
"modules": [
"capability_broker",
"tool_executor",
"tool_protocol",
"local_tool_runtime",
"intent_continuity",
"openai_compat_transport",
"forge_client",
"message_queue",
"connection_pool",
"adaptive_router",
"route_risk_flags",
"evidence_probe",
"builder_bridge",
"builder_bridge_cli",
"recall_uplift"
],
"canaries": [
"canary_p1_01",
"canary_p0_05",
"canary_native"
],
"depends_on": [
"memory"
]
},
"inference": {
"description": "Model routing and outbound requests",
"modules": [
"model_routing",
"model_profiles",
"model_registry",
"model_connect",
"provider_registry",
"provider_budget",
"standardized_providers",
"inference_trace",
"response_adapter",
"token_optimizer"
],
"canaries": [
"canary_p1_06",
"canary_p1_02",
"canary_resource_authority"
],
"depends_on": [
"capability"
]
},
"orchestration": {
"description": "Task lifecycle, operator, TUI presentation",
"modules": [
"rune_operator",
"rune_tui",
"rune_graphic",
"rune_spinner",
"rune_profiler",
"chat_bubble",
"message_widgets",
"tui_validator",
"microkernel",
"event_bus",
"event_envelope",
"hook_engine",
"system_registry",
"cron_engine",
"task_gate",
"task_persist",
"work_manager",
"parallel_processor",
"tiered_executor",
"time_awareness",
"night_mode",
"pins",
"progressive_escalation",
"safety_override",
"sleep_time",
"verification_layer",
"outcome_ledger",
"warm_router",
"subsystem_health",
"build_mode",
"dependency_tracker",
"auto_detect",
"build_orchestrator",
"custodian",
"gitea_auth",
"gitea_pipeline",
"gitea_provision",
"umbrella_update"
],
"canaries": [
"canary_p1_07",
"canary_orchestration_foundation",
"canary_conversation_state",
"canary_gitea_pipeline",
"canary_tui_drift",
"canary_cognitive_strata",
"canary_surface_parity"
],
"depends_on": [
"perception",
"capability",
"inference",
"identity",
"growth"
]
}
}
}
+28
View File
@@ -111,3 +111,31 @@ subsystem): fetches live issue state, compares to
- record lacking a required field
Exit 0 = consistent; exit 1 = drift listed. CI-eligible (read-only).
## PL1 held-out era reconciliation (2026-09-12)
Fresh-session reconciliation of the ADAPT family against deployed 0.66.22
@ 1497c85 + live PL1 held-out evidence (tasks task_2661189c9073,
task_6e4885031355; ledger PE-9207213099-0270). Resolves the recorded tension:
"ADAPT-LIVE complete → return target PL1" (the bounded tranche authorized
2026-09-10 is complete) while numbered ADAPT issues remain open (the larger
backlog was never in the ADAPT-LIVE tranche scope).
| # | state | evidence / basis |
|---|---|---|
| 284 | VALID_OPEN | epic tracker for the remaining sub-items below |
| 292 | FIXED_VERIFIED (closed) | LOCAL_FIX fast path live (rune_operator.py:522,541); live-proven twice by the held-out tasks |
| 293 | FIXED_VERIFIED (closed) | DESIGN-LITE implemented (rune_operator.py:660, cites #293), wired to STANDARD_CHANGE; suite+canary green |
| 294 | DEFERRED_FUTURE | canonical remaining-obligations list ("ADAPT-5 budgets... for later epics") |
| 295 | DEFERRED_FUTURE | canonical remaining-obligations list |
| 296 | DEFERRED_FUTURE | canonical remaining-obligations list |
| 297 | FIXED_VERIFIED (closed) | task_gate implemented (src/rune/task_gate.py) + broker wiring + canary registration |
| 299 | DEFERRED_FUTURE | canonical remaining-obligations list |
| 300 | VALID_OPEN | escalation mechanism exists (_probe_and_escalate); fresh deployed acceptance evidence pending |
| 301 | DEFERRED_FUTURE | canonical remaining-obligations list (FINAL-12) |
| 315 | VALID_OPEN | not started; ADAPT-1 sub-item (no canonical deferral authority) |
| 316 | VALID_OPEN | not started; ADAPT-1 sub-item (no canonical deferral authority) |
Consistency checker: `scripts/gitea_reconciliation_check.py` → exit 0
(72 records, 100 live issues in window) after these updates.
+11 -1
View File
@@ -54,7 +54,7 @@ This detour is complete when:
## RETURN TARGET / CURRENT: PL1 Process Learning — FIRST TRANCHE LIVE (2026-09-11)
PL1-01 structured Experience records: `scripts/pl1_records.py` (task_id, route resolved from route.jsonl, proof level, failure class, candidate status). PL1-02 failure classification in the PL0 vocabulary (research_dispatch, NOT_COMPLETED_NO_MUTATION_EVIDENCE, verification_failure, model_refusal, UNKNOWN). PL1-03 first learning loop: recurring prose-instead-of-mutation family → structural correction (route-driven instruction in the user context block) + replay contract (NOT_COMPLETED gate) — record PE-1789102658-0269 (SEED; held-out = next deployed mutation tasks must complete-with-evidence or fail honestly). PL1-04 promotion discipline: SEED→CANDIDATE→VALIDATED→PROMOTED via process_experience; structural fixes preferred. PL1-05 metrics: first_pass_accepted, mutation_failures, research_dispatches, false_completion_rate (0.0 — structurally prevented), candidates vs evidence-only split.
PL1-01 structured Experience records: `scripts/pl1_records.py` (task_id, route resolved from route.jsonl, proof level, failure class, candidate status). PL1-02 failure classification in the PL0 vocabulary (research_dispatch, NOT_COMPLETED_NO_MUTATION_EVIDENCE, verification_failure, model_refusal, UNKNOWN). PL1-03 first learning loop: recurring prose-instead-of-mutation family → structural correction (route-driven instruction in the user context block) + replay contract (NOT_COMPLETED gate) — record PE-1789102658-0269 (SEED 2026-09-11 → VALIDATED 2026-09-12: two fresh deployed held-out mutation tasks completed-with-evidence — task_2661189c9073 + task_6e4885031355, LOCAL_FIX, Verboo-pinned, mutation + exit-code verification, ledger PE-9207213099-0270; substrate corrections 6965fc7 flat-ChatMessage contract + a2463ad resume transport selection applied between; caveat: corrections developer-applied structurally, not autonomous self-modification; PROMOTED requires repetition/soak). PL1-04 promotion discipline: SEED→CANDIDATE→VALIDATED→PROMOTED via process_experience; structural fixes preferred. PL1-05 metrics: first_pass_accepted, mutation_failures, research_dispatches, false_completion_rate (0.0 — structurally prevented), candidates vs evidence-only split.
Remaining ADAPT obligations for later epics: ADAPT-5 budgets, ADAPT-6 evidence capsule, ADAPT-7 DESIGN consolidation, ADAPT-10 legibility, FINAL-12 cross-track benchmark, D7 release-job-on-Pi.
@@ -177,3 +177,13 @@ Root causes (live-proven T15 Part F, 2026-09-11 evening):
Live proof: experiment A (trigger d68fd1e, warranted patch) → exactly one
bump commit a4afc77 (0.66.11), all five surfaces in one commit, queue
drained, zero delayed duplicates, zero workflow failures.
## Local CI tooling (developer-side, NOT Rune runtime): W/R-v0.3 reactive wake (2026-09-12)
The desktop gitea-runner (v3.3.0) now runs `host.post_task_script` = `/usr/local/bin/gitea-wr-wake.sh`
after every job: a sub-second fire-and-forget POST to the local W/R daemon (`127.0.0.1:8791/wake`),
which sweeps waiting external waits through AUTHORITATIVE Gitea verification (bounded retry for the
reporter-ack window; 60s reconciler demoted to fallback). Runner event ≠ success proof. Config:
`/etc/gitea-desktop-runner/config.yaml` (backup: config.yaml.pre-wr03). Measured: terminal→verified
wait transition ~2s (was 0-30s polling). INTERACTIVE_AUTO_RESUME remains NOT YET SAFE (no supported
MiMo busy/idle signal) — headless-only dispatch, unchanged.
+70
View File
@@ -0,0 +1,70 @@
# Scaffolding V1 — Tranche 1
_2026-09-12. Scaffolding is the controlled need-to-capability system: NEED →
DISCOVERY → GAP → REUSE/ACQUIRE/BUILD → IMPACT+PLAN → SAFE IMPLEMENTATION →
VERIFICATION → PROVISIONAL USE → TRUST/REVISE/RETIRE → REUSABLE CAPABILITY.
Tranche 1 is deliberately narrower: make Rune stop rediscovering how Rune is
built every time Rune changes._
## Tranche 1 contract
`scripts/scaffold_projection.py` — a PROJECTION over existing authorities,
never a new one:
| question | authority projected |
|---|---|
| what subsystem owns X? | `config/umbrellas.json` (umbrella manifest + henxel-enforced completeness) |
| what does X depend on? | AST import graph over `src/rune/*.py` (incl. relative/lazy imports) |
| who consumes X? | reverse import edges |
| what changes if X changes? | mirrors + `depends_on` umbrellas + consumers |
| what proof obligations arise? | unit-test file, canary registry, umbrella gates, suite/lint/henxels |
| which are satisfied? | filesystem + registry evidence (static); suite/ratchet/henxels are runtime-check commands |
Commands: `component <module>` · `impact <module>` · `obligations <module>` ·
`all [outfile]`. If the projection disagrees with an authority, fix the
authority — never the projection.
## Mechanism audit (2026-09-12, deployed 0.66.22)
| mechanism | verdict | basis |
|---|---|---|
| Umbrella manifest + impact map + propagation gate | KEEP | ownership/impact authority; henxel-enforced completeness; consumed by projection |
| Henxels structural contract | KEEP | cross-cutting obligations (silent-handler ratchet, registries, coverage debt) |
| Canary registry (`canary_native.MODULES` + per-track canaries) | KEEP | per-subsystem proof obligations; consumed by projection |
| CI/deploy contract (required checks, TUI startup canary, atomic promote) | KEEP | deployment proof chain live-proven repeatedly |
| Version surfaces + auto-version + semver | KEEP | bump fast lane + push convergence working (0.66.22 earned this session) |
| Lint/pyright/coverage ratchets | KEEP | shrinking-baseline gates, all green |
| Authority map + dead-subnet ledger (D6) | EVOLVE | static doc today; Scaffolding projections make it derivable; D6 retire-disposition still owed |
| Issue reconciliation artifact + checker | KEEP | tracker truth with drift detection |
| PL1 experience ledger | KEEP | the trust/revise/retire input for future capability crystallisation |
| Intent continuity ledger | KEEP | PART 6 state machine, 7 invariants |
| `MIMO_AUTOMATIC_RESUME` / W/R interactive injection | KEEP (bounded) | headless single-flight proven; interactive auto-resume NOT YET SAFE until a supported busy signal exists |
| Duplicate authorities risk | REPLACE (avoided) | Scaffolding v1 adds NO new registry — projections only |
## Real-change proof (this tranche)
Change: context discovery hit Ollama's `/api/show` with cloud-pinned model
names — `400 invalid model name` noise on every cloud task + 8192 misbudget
for a 131072-window model (observed live in every PL1 Task A/B run).
1. BEFORE mutation: `scaffold_projection.py impact local_tool_runtime`
derived — subsystem `capability`; consumers `rune_operator`; gates:
full suite, lint ratchet, henxels, inference+orchestration canaries;
unit-test obligation `tests/test_local_tool_runtime.py` (exists).
2. Change made: `is_cloud_pinned` — THE provider-prefix rule, single
authority in `openai_compat_transport.py`; consumed by context discovery
(new) and transport selection (deduped); red-first regression tests.
3. Obligations tracked: suite 1314 green · lint ratchet PASS · henxels exit 0
· the projection itself caught two gaps mid-flight (relative imports
hidden consumers; a broad except biting the lint ratchet) — both fixed at
the source the projection pointed to.
4. Deploy: normal release chain → deployed identity verified.
## Maturity (honest)
- Projection engine: **UNIT-PROVEN** + proven on one real change.
- Not yet: CI integration of `scaffold_projection.py all` (drift detection),
capability discovery / gap recognition / reuse-before-build / external
acquisition / provisional-trust lifecycle — FOUNDATIONAL ONLY (architecture
does not block them; the projection source-list is the extension surface).
- Deferred with reasons recorded: none blocked by Tranche 1.
+35 -35
View File
@@ -34,28 +34,28 @@
"number": 316,
"title": "ADAPT-1 B3-v2 (RESEARCH-LIKE): Qdrant associative-memory projection spike (executes #162)",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"reconciliation_state": "VALID_OPEN",
"labels": [
"governance",
"memory",
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "B3-v2 Qdrant associative-memory projection spike \u2014 not started; ADAPT-1 sub-item under epic #284"
},
{
"number": 315,
"title": "ADAPT-1 B2-v2 (STANDARD-LIKE): wiring-status/architecture-truth report generator (implements #158)",
"tracker_state": "open",
"reconciliation_state": "DEFERRED_FUTURE",
"reconciliation_state": "VALID_OPEN",
"labels": [
"infrastructure",
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "B2-v2 wiring-status/architecture-truth report generator \u2014 not started; ADAPT-1 sub-item under epic #284"
},
{
"number": 311,
@@ -80,8 +80,8 @@
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "cross-track benchmark + adaptive provisional ratification \u2014 canonical remaining-obligations list"
},
{
"number": 300,
@@ -92,90 +92,90 @@
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "escalation/downgrade honored at acceptance on the deployed runtime \u2014 probe_and_escalate exists; fresh live acceptance evidence pending"
},
{
"number": 299,
"title": "ADAPT-10: Harness/context legibility audit",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"feature",
"governance"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "harness/context legibility audit \u2014 canonical remaining-obligations list"
},
{
"number": 297,
"title": "ADAPT-8: Deterministic-first QUALIFY",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"tracker_state": "closed",
"reconciliation_state": "FIXED_VERIFIED",
"labels": [
"memory"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"last_revalidated": "2026-09-12",
"next_action": ""
},
{
"number": 296,
"title": "ADAPT-7: DESIGN consolidation experiment (APPLICABILITY + INNOVATION)",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"memory"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "DESIGN consolidation experiment (APPLICABILITY+INNOVATION) \u2014 canonical remaining-obligations list"
},
{
"number": 295,
"title": "ADAPT-6: Evidence capsule / context minimization",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "evidence capsule / context minimization \u2014 canonical remaining-obligations list"
},
{
"number": 294,
"title": "ADAPT-5: Bound RESEARCHED_CHANGE work (budgets + stop conditions)",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"reconciliation_state": "DEFERRED_FUTURE",
"labels": [
"feature"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "budgets + stop conditions for RESEARCHED_CHANGE \u2014 canonical 'remaining ADAPT obligations for later epics'"
},
{
"number": 293,
"title": "ADAPT-4: STANDARD_CHANGE path (DESIGN-LITE)",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"tracker_state": "closed",
"reconciliation_state": "FIXED_VERIFIED",
"labels": [
"feature"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"last_revalidated": "2026-09-12",
"next_action": ""
},
{
"number": 292,
"title": "ADAPT-3: LOCAL_FIX fast path",
"tracker_state": "open",
"reconciliation_state": "VALID_OPEN",
"tracker_state": "closed",
"reconciliation_state": "FIXED_VERIFIED",
"labels": [
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"last_revalidated": "2026-09-12",
"next_action": ""
},
{
@@ -190,8 +190,8 @@
"perception"
],
"live_defect": false,
"last_revalidated": "2026-09-11",
"next_action": ""
"last_revalidated": "2026-09-12",
"next_action": "epic tracker for remaining ADAPT sub-items (#294 #295 #296 #299 #300 #301 #315 #316)"
},
{
"number": 259,
@@ -1152,5 +1152,5 @@
}
],
"last_reconciled": "2026-09-12",
"reconciled_by": "W/R-v0.1 overnight (ADAPT-only pass)"
}
"reconciled_by": "PL1 held-out era reconciliation (fresh session 2026-09-12)"
}
+1 -1
View File
@@ -1,7 +1,7 @@
settings:
warn_about_similar_files:
above: 0.85
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**"]
ignore: ["docs/review-lessons.yaml", "docs/v1/**", "docs/wiki/**", "store/spine/**"]
henxels:
- henxel: "Canaries live at the repo's canary slot and are registered in CI"
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.66.20"
version = "0.66.32"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+3 -3
View File
@@ -1,5 +1,5 @@
{
"errorCount": 120,
"warningCount": 465,
"informationCount": 0
"errorCount": 117,
"warningCount": 443,
"informationCount": 0
}
View File
Whitespace-only changes.
+115 -18
View File
@@ -6,6 +6,16 @@ has no embedded multi-line scripts — a YAML block-scalar indentation hazard).
Prints one of: success | failure | pending
Exit code 0 always (the shell decides what to do with the verdict).
RELEASE-SAFETY HARDENING (2026-09-13, after red-CI promotions 0.66.28/29):
the verdict is the AND of two independent legs —
1. required-checks context (newest observation for this exact SHA);
2. the latest COMPLETED ci.yml@refs/heads/master run for this exact SHA,
every job concluded success (branch+tag fan-out resolved by path;
jobs envelope unwrapped — no aggregate-commit-status fallback, the
exact bug class verify.py hit in W/R-v0.3).
Any disagreement, pending, or missing evidence → pending/failure.
FAIL-CLOSED: not verified → not deployable.
"""
from __future__ import annotations
@@ -20,6 +30,102 @@ if _SCRIPT_DIR not in _sys.path:
_sys.path.insert(0, _SCRIPT_DIR)
import gitea_auth
REQUIRED_CONTEXT = "CI Pipeline / required-checks"
CI_WORKFLOW_PATH = "ci.yml@refs/heads/master"
# ── pure verdict logic (unit-tested; fixtures from the real incidents) ────
def required_check_verdict(statuses: list[dict]) -> str:
"""Newest non-skipped required-checks observation for the SHA.
Missing → pending. Newest wins (stale success never overrides a newer
failure, and vice versa)."""
candidates = [
s for s in statuses
if str(s.get("context", "")).startswith(REQUIRED_CONTEXT)
and s.get("status") != "skipped"
]
if not candidates:
return "pending"
newest = max(candidates, key=lambda s: str(s.get("created_at", "")))
return str(newest.get("status", "pending"))
def independent_jobs_verdict(runs: list[dict], jobs_fetch, sha: str = "") -> str:
"""Latest COMPLETED ci.yml@refs/heads/master run for the exact SHA:
every job must conclude success. Fan-out-safe (tag runs and sibling
workflows are excluded by path); the run is resolved by head_sha, never
by 'latest branch state'. Missing run → pending. Any non-success
conclusion → failure."""
mine = [r for r in runs if r.get("path") == CI_WORKFLOW_PATH]
if sha:
mine = [r for r in mine if str(r.get("head_sha", "")) == sha]
completed = [r for r in mine if r.get("status") == "completed"]
if not completed:
return "pending"
latest = max(completed, key=lambda r: str(r.get("created_at", "") or r.get("run_started_at", "")))
jobs = jobs_fetch(latest.get("id"))
if jobs is None:
return "pending" # jobs probe failed → not verified
if not jobs:
return "pending" # no jobs visible → cannot verify
for j in jobs:
if j.get("conclusion") != "success":
return "failure"
return "success"
def combine(a: str, b: str) -> str:
"""Fail-closed conjunction: any failure → failure; any pending → pending;
success only when BOTH legs are success."""
if "failure" in (a, b):
return "failure"
if "pending" in (a, b):
return "pending"
return "success"
def qualify(statuses: list[dict], runs: list[dict], jobs_fetch, sha: str = "") -> str:
return combine(required_check_verdict(statuses),
independent_jobs_verdict(runs, jobs_fetch, sha=sha))
# ── API fetches (thin; injectable in tests) ───────────────────────────────
def _get_json(url: str, token: str, timeout: int = 30):
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
try:
with urllib.request.urlopen(req, timeout=timeout) as r:
return json.loads(r.read())
except (OSError, ValueError):
return None
def _fetch_status(server: str, repo: str, sha: str, token: str) -> list[dict] | None:
data = _get_json(f"{server}/api/v1/repos/{repo}/commits/{sha}/status", token)
return None if data is None else data.get("statuses", [])
def _fetch_runs(server: str, repo: str, sha: str, token: str) -> list[dict] | None:
data = _get_json(f"{server}/api/v1/repos/{repo}/actions/runs?limit=50", token)
if data is None:
return None
runs = data.get("workflow_runs", data if isinstance(data, list) else [])
return [r for r in runs if str(r.get("head_sha", "")) == sha]
def _fetch_jobs(server: str, repo: str, run_id, token: str) -> list[dict] | None:
data = _get_json(f"{server}/api/v1/repos/{repo}/actions/runs/{run_id}/jobs", token)
if data is None:
return None
# jobs envelope unwrap (this Gitea wraps in {"jobs": [...]}; some
# versions return a bare list) — never fall back to aggregate status
if isinstance(data, dict):
return data.get("jobs", [])
return data if isinstance(data, list) else None
# ── CLI ───────────────────────────────────────────────────────────────────
def main() -> int:
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
@@ -29,25 +135,16 @@ def main() -> int:
if not (server and repo and sha and token):
print("pending") # missing config → cannot verify → not qualified
return 0
url = f"{server}/api/v1/repos/{repo}/commits/{sha}/status"
req = urllib.request.Request(url, headers={"Authorization": f"token {token}"})
try:
with urllib.request.urlopen(req, timeout=30) as r:
data = json.loads(r.read())
except (OSError, ValueError) as exc:
# transient API/network failure → keep polling (urllib raises
# URLError (OSError subclass); bad JSON raises ValueError)
print(f"pending (status probe failed: {exc})")
statuses = _fetch_status(server, repo, sha, token)
runs = _fetch_runs(server, repo, sha, token)
if statuses is None or runs is None:
# transient API/network failure → keep polling
print("pending (probe failed)")
return 0
required = [
s
for s in data.get("statuses", [])
if (str(s.get("context", "")).startswith("CI Pipeline / required-checks") and s.get("status") != "skipped")
]
if not required:
print("pending")
else:
print(required[0].get("status", "pending"))
jobs_fetch = lambda run_id: _fetch_jobs(server, repo, run_id, token) # noqa: E731
verdict = qualify(statuses, runs, jobs_fetch, sha=sha)
print(verdict)
return 0
+9 -1
View File
@@ -77,9 +77,17 @@ def _extract_keywords(text: str) -> list[str]:
def _load_manifest() -> dict:
"""Structural facts from SPINE (the single structural authority).
Phase D: reads the Spine-generated compatibility view (same schema as
the old manual manifest). The view is OUTPUT — the authority is
store/spine/current.json; direct edits to the view are flagged by
`spine drift`. If a manual manifest is present (Spine not yet seeded
in this checkout), it degrades gracefully to that legacy truth."""
if os.path.exists(UMBRELLA_MANIFEST):
with open(UMBRELLA_MANIFEST) as f:
return json.load(f)
data = json.load(f)
return data # generated view or manual manifest — both work
return {"umbrellas": {}}
+205
View File
@@ -0,0 +1,205 @@
#!/usr/bin/env python3
"""scaffold_projection.py — Scaffolding V1 Tranche 1 (projection, not authority).
Answers, per component/module:
what subsystem owns X? → SPINE store (structural authority)
what does X depend on? → SPINE relationships (ingested from AST)
who consumes X? → SPINE relationships (ingested from AST)
what changes if X changes? → mirrors + dependent subsystems + consumers (SPINE)
what proof obligations arise? → unit test, canary, ownership, umbrella gates
which obligations are complete? → evidence check (test file exists, canary registered)
SPINE INTEGRATION: structural facts come from the Spine store — the single
structural authority (ingested once from the umbrella manifest + AST).
This script adds change-impact and proof-obligation reasoning on top; it
no longer re-derives structure. No store → builds one (deterministic).
Commands:
component <module> full card for one src/rune module
impact <module> expected impact + obligations BEFORE a change
obligations <module> obligation checklist with satisfied/pending state
all [outfile] write the full projection JSON (default docs/)
"""
from __future__ import annotations
import ast, json, sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SRC = ROOT / "src" / "rune"
TESTS = ROOT / "tests"
sys.path.insert(0, str(ROOT / "src"))
from rune import spine # noqa: E402 — Spine IS the structural authority
def load_spine() -> dict:
"""Structural state from the Spine store (built on first use)."""
doc = spine.load()
if doc is None:
doc = spine.build()
spine.save(doc)
return doc
def module_imports(module: str) -> set[str]:
"""Module names imported by src/rune/<module>.py (intra-package).
Includes relative imports (`from .x import y` — the dual-mode lazy
pattern used across the runtime; missing these would hide real
consumers, observed first run: rune_operator ↔ local_tool_runtime)."""
path = SRC / f"{module}.py"
if not path.is_file():
return set()
tree = ast.parse(path.read_text())
names: set[str] = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for a in node.names:
names.add(a.name.split(".")[0])
elif isinstance(node, ast.ImportFrom) and node.module:
names.add(node.module.split(".")[0])
return names
def import_graph() -> dict[str, set[str]]:
return {p.stem: module_imports(p.stem) for p in SRC.glob("*.py")}
def consumers_of(module: str, graph: dict[str, set[str]]) -> set[str]:
return {m for m, deps in graph.items() if module in deps and m != module}
def subsystem_for(doc: dict, module: str) -> str:
rels = spine.relationships(doc, from_id=f"module:{module}", type="belongs_to")
return rels[0]["to"].split(":", 1)[1] if rels else ""
def dependent_subsystems(doc: dict, subsystem: str) -> list[str]:
return [r["from"].split(":", 1)[1] for r in
spine.relationships(doc, to_id=f"subsystem:{subsystem}", type="depends_on")]
def mirrors_for(doc: dict, module: str) -> list[str]:
subs = spine.relationships(doc, from_id=f"module:{module}", type="belongs_to")
out = []
for s in subs:
out.extend(r["to"].split(":", 1)[1] for r in
spine.relationships(doc, from_id=s["to"], type="mirrors"))
return sorted(set(out))
def canary_registered(module: str) -> bool:
try:
sys.path.insert(0, str(SRC))
from rune import canary_native
return module in canary_native.MODULES
except ImportError:
# canary_native pulls optional deps (sqlite_vec etc.); the projection
# degrades to "not registered" rather than failing the audit.
return False
def obligations(module: str, doc: dict) -> list[dict]:
"""Proof obligations for changing this module, with evidence state.
Structural facts sourced from Spine."""
umb = subsystem_for(doc, module)
test_file = TESTS / f"test_{module}.py"
obs = [
{"obligation": "unit_test", "satisfied": test_file.is_file(),
"evidence": f"tests/test_{module}.py" + ("" if test_file.is_file() else " (MISSING)")},
{"obligation": "canary", "satisfied": canary_registered(module),
"evidence": "canary_native.MODULES" if canary_registered(module)
else "not in canary registry (informational — not every module needs one)"},
{"obligation": "umbrella_manifest", "satisfied": bool(umb),
"evidence": f"umbrella '{umb}'" if umb else "NOT in any umbrella — manifest gap"},
{"obligation": "full_suite", "satisfied": None,
"evidence": "run: pytest tests/ -q (runtime evidence, not static)"},
{"obligation": "lint_ratchet", "satisfied": None,
"evidence": "run: scripts/lint_ratchet.py"},
{"obligation": "henxels", "satisfied": None,
"evidence": "run: henxels check --all"},
]
if umb:
deps = dependent_subsystems(doc, umb)
if deps:
obs.append({"obligation": "dependent_canaries", "satisfied": None,
"evidence": f"subsystems depending on '{umb}': {', '.join(deps)} — their canaries gate the change"})
mir = mirrors_for(doc, module)
if mir:
obs.append({"obligation": "mirrors_travel", "satisfied": None,
"evidence": f"files that must travel together: {', '.join(mir)}"})
return obs
def component_card(module: str, doc: dict) -> dict:
"""Structural card from SPINE (owner/deps/consumers/mirrors) + obligations."""
deps = [r["to"].split(":", 1)[1] for r in
spine.relationships(doc, from_id=f"module:{module}", type="depends_on")]
consumers = [r["to"].split(":", 1)[1] for r in
spine.relationships(doc, from_id=f"module:{module}", type="consumed_by")]
e = spine.entity(doc, f"module:{module}")
card = {
"component": module,
"subsystem": subsystem_for(doc, module),
"dependencies": sorted(set(deps)),
"consumers": sorted(set(consumers)),
"mirrors": mirrors_for(doc, module),
"obligations": obligations(module, doc),
}
if e:
card["spine_maturity"] = e.get("maturity")
card["spine_evidence"] = e.get("evidence_refs", [])
return card
def impact(module: str) -> dict:
doc = load_spine()
card = component_card(module, doc)
umb = card["subsystem"]
card["expected_impact"] = {
"direct_consumers": card["consumers"],
"subsystem_dependents": dependent_subsystems(doc, umb) if umb else [],
"must_travel": card["mirrors"],
"gates": ["full_suite", "lint_ratchet", "henxels", "umbrella_gates"]
+ (["canary"] if card and any(o["obligation"] == "canary" and o["satisfied"]
for o in card["obligations"]) else []),
}
return card
def main() -> int:
args = sys.argv[1:]
if not args:
print(__doc__)
return 2
cmd = args[0]
if cmd in ("component", "impact", "obligations") and len(args) < 2:
print(f"usage: {cmd} <module>", file=sys.stderr)
return 2
doc = load_spine()
if cmd == "impact":
print(json.dumps(impact(args[1]), indent=1))
elif cmd == "component":
print(json.dumps(component_card(args[1], doc), indent=1))
elif cmd == "obligations":
card = component_card(args[1], doc)
print(json.dumps(card["obligations"], indent=1))
elif cmd == "all":
out = Path(args[1]) if len(args) > 1 else ROOT / "docs" / "scaffold_projection.json"
cards = {p.stem: component_card(p.stem, doc)
for p in sorted(SRC.glob("*.py"))
if spine.entity(doc, f"module:{p.stem}")}
out.write_text(json.dumps({
"generated_by": "scripts/scaffold_projection.py (projection — no new authority)",
"sources": ["rune.spine store (structural authority)", "src/rune/canary_native.py",
"tests/ filesystem"],
"components": cards,
}, indent=1) + "\n")
print(f"projection written: {out} ({len(cards)} components)")
else:
print(f"unknown command: {cmd}", file=sys.stderr)
return 2
return 0
if __name__ == "__main__":
sys.exit(main())
+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.66.20"
__version__ = "0.66.32"
# 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
@@ -231,7 +231,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.66.20"' in rune_version,
'__version__ = "0.66.32"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+25
View File
@@ -93,6 +93,31 @@ def main() -> int:
"if: always()" in required_gate and "needs: [lint, unit-tests, canary" in required_gate,
failures,
)
# RELEASE-SAFETY (2026-09-13, after 0.66.28/29 red promotions): the
# required-checks publish step previously echoed success UNCONDITIONALLY
# (read /tmp/need_* files that no step wrote). The verdict MUST come
# from the needs context — this canary locks that class shut.
publish_section = required_gate.split("Publish protected-branch gate", 1)[-1]
check(
"required gate verdict evaluates needs results (not unconditional echo)",
"needs.lint.result == 'success'" in publish_section
and "needs['type-check'].result == 'success'" in publish_section
and "needs['unit-tests'].result == 'success'" in publish_section,
failures,
)
check(
"required gate fails closed when any check is not success",
"!= 'success'" in publish_section and "exit 1" in publish_section,
failures,
)
check(
"no unconditional success echo without a needs-conditioned if",
not (
"All required CI checks passed" in publish_section
and "needs.lint.result == 'success'" not in publish_section
),
failures,
)
check(
"required gate depends on unit tests and verification jobs",
"needs: [lint, unit-tests, canary" in text,
+12 -2
View File
@@ -1,8 +1,18 @@
#!/usr/bin/env python3
"""dependency_tracker.py — Auto-propagation system for umbrella dependencies.
"""dependency_tracker.py — Auto-propagation for the OpenCode HARNESS scripts.
SCOPE CLASSIFICATION (Spine V1 pre-Memory closure, 2026-09-12): the
DEPENDENCY_GRAPH below describes ~/.config/opencode/scripts/*.py — the
developer HARNESS (verified: 20/23 names collide with src/rune modules but
all exist in the scripts dir; propagate() resolves against SCRIPTS_DIR).
This is NOT Rune architecture and NOT a competing structural authority:
Rune's structural truth lives in the Spine store (store/spine/current.json,
consumed via rune.spine / scripts/scaffold_projection.py). The name
collisions with src/rune modules are coincidental — do not read this map
as Rune structure.
When a component changes, automatically updates dependent components.
Maps the dependency graph between umbrella systems and fires
Maps the dependency graph between harness systems and fires
propagation rules when changes are detected.
Usage:
+168
View File
@@ -0,0 +1,168 @@
"""Minimal intent continuity foundation (overnight handoff PART 6).
A deterministic state machine for tracking detached intents — the
smallest primitive that prevents "new good idea = new active objective."
Not an ontology; not Substrate. Test-first, reload-safe, no LLM needed
for state transitions once classification is supplied."""
from __future__ import annotations
import json
import time
from pathlib import Path
CLASSIFICATIONS = ("BLOCKER", "FIX", "EXTENSION", "SUPERSEDING", "ORTHOGONAL", "LATER")
STATUSES = ("ACTIVE", "DETOUR", "PARKED", "RESOLVED", "SUPERSEDED", "DEFERRED")
class IntentRecord:
__slots__ = (
"id",
"objective",
"parent_intent",
"active_detour",
"return_when",
"return_to",
"unresolved_obligations",
"success_condition",
"classification",
"status",
"created_at",
"resolved_at",
)
def __init__(
self,
*,
id,
objective,
classification,
parent_intent="",
active_detour=False,
return_when="",
return_to="",
unresolved_obligations=None,
success_condition="",
status="ACTIVE",
):
self.id = id
self.objective = objective
if classification not in CLASSIFICATIONS:
raise ValueError(f"invalid classification {classification!r}")
self.classification = classification
if status not in STATUSES:
raise ValueError(f"invalid status {status!r}")
self.status = status
self.parent_intent = parent_intent
self.active_detour = active_detour
self.return_when = return_when
self.return_to = return_to
self.unresolved_obligations = unresolved_obligations or []
self.success_condition = success_condition
self.created_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
self.resolved_at = ""
def to_dict(self):
return {s: getattr(self, s) for s in self.__slots__}
@classmethod
def from_dict(cls, d):
rec = cls.__new__(cls)
for s in cls.__slots__:
setattr(rec, s, d.get(s, "" if s != "unresolved_obligations" else []))
return rec
class IntentLedger:
"""Deterministic intent-state store. JSON persistence, no LLM."""
def __init__(self, path):
self.path = Path(path)
self.records = {}
self._load()
def _load(self):
if self.path.is_file():
data = json.loads(self.path.read_text())
for rec in data.get("intents", []):
r = IntentRecord.from_dict(rec)
self.records[r.id] = r
def save(self):
self.path.parent.mkdir(parents=True, exist_ok=True)
self.path.write_text(json.dumps({"intents": [r.to_dict() for r in self.records.values()]}, indent=1))
def create(
self, *, id, objective, classification, success_condition="", parent_intent="", unresolved_obligations=None
):
rec = IntentRecord(
id=id,
objective=objective,
classification=classification,
success_condition=success_condition,
parent_intent=parent_intent,
unresolved_obligations=unresolved_obligations,
)
self.records[id] = rec
self.save()
return rec
def create_detour(self, *, id, objective, classification, parent_intent, return_when, return_to):
parent = self.records.get(parent_intent)
if not parent:
raise ValueError(f"parent {parent_intent!r} not found")
rec = IntentRecord(
id=id,
objective=objective,
classification=classification,
parent_intent=parent_intent,
active_detour=True,
return_when=return_when,
return_to=return_to,
status="DETOUR",
)
self.records[id] = rec
if parent.status == "ACTIVE":
parent.status = "PARKED"
self.save()
return rec
def resolve(self, intent_id):
"""Invariant 5: resolving a child does NOT resolve its parent."""
rec = self.records.get(intent_id)
if not rec or rec.status not in ("ACTIVE", "DETOUR"):
return None
rec.status = "RESOLVED"
rec.resolved_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
self.save()
return rec
def resolve_detour(self, detour_id):
d = self.records.get(detour_id)
if not d or d.status not in ("DETOUR", "RESOLVED"):
return None
if d.status == "DETOUR":
d.status = "RESOLVED"
d.resolved_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
parent = self.records.get(d.parent_intent)
if parent and parent.status == "PARKED":
parent.status = "ACTIVE"
self.save()
return parent
def supersede(self, old_id, new_id, *, objective, classification, carry_obligations=True):
old = self.records[old_id]
if old.unresolved_obligations and not carry_obligations:
raise ValueError(f"cannot supersede {old_id}: {len(old.unresolved_obligations)} unresolved obligations")
rec = IntentRecord(
id=new_id,
objective=objective,
classification=classification,
parent_intent=old.parent_intent,
unresolved_obligations=list(old.unresolved_obligations) if carry_obligations else [],
status="ACTIVE",
)
old.status = "SUPERSEDED"
self.records[new_id] = rec
self.save()
return rec
+10 -1
View File
@@ -238,6 +238,7 @@ def context_length(model: str, base_url: str = "http://localhost:11434") -> int:
conservative default 8192 + T1 ``record_failure("context_discovery")``
— the assumption is observable, never silent (P0-06).
"""
from rune.openai_compat_transport import is_cloud_pinned
from rune.subsystem_health import record_failure
with _CONTEXT_LENGTH_LOCK:
@@ -245,7 +246,15 @@ def context_length(model: str, base_url: str = "http://localhost:11434") -> int:
if cached:
return cached
try:
window = OllamaChatTransport(base_url=base_url).context_length(model)
if is_cloud_pinned(model):
# Cloud-pinned models must not hit Ollama's /api/show — observed
# live: 400 'invalid model name' + 8192 misbudget for a model
# with a 131072 window. The provider config carries the window.
from rune.openai_compat_transport import cloud_transport_for
window = cloud_transport_for(model).context_length(model)
else:
window = OllamaChatTransport(base_url=base_url).context_length(model)
except Exception as exc:
record_failure("context_discovery", exc, tier=1, context=model)
return CONTEXT_LENGTH_FALLBACK
+19 -3
View File
@@ -108,7 +108,13 @@ class OpenAICompatTransport:
@staticmethod
def _from_openai_response(resp: dict) -> dict:
"""OpenAI response → Ollama-shape (LocalToolRuntime's contract)."""
"""OpenAI response → flat Ollama ChatMessage (LocalToolRuntime's contract).
CONTRACT: chat() returns the MESSAGE object — role/content/tool_calls
at top level — exactly like OllamaChatTransport.chat. Observed live
(PL1 Task A, 2026-09-12): the envelope shape {'message': {...}} made
the runtime see 'empty terminal visible text' while the model had
returned a valid read_file tool call."""
choices = resp.get("choices") or [{}]
message = choices[0].get("message", {}) or {}
tool_calls = []
@@ -124,9 +130,9 @@ class OpenAICompatTransport:
if c.get("id"):
entry["id"] = c["id"]
tool_calls.append(entry)
out: dict = {"message": {"role": "assistant", "content": message.get("content") or ""}}
out: dict = {"role": "assistant", "content": message.get("content") or ""}
if tool_calls:
out["message"]["tool_calls"] = tool_calls
out["tool_calls"] = tool_calls
return out
# -- ChatTransport protocol -------------------------------------------
@@ -168,6 +174,16 @@ class OpenAICompatTransport:
return self._context_window
def is_cloud_pinned(model: str) -> bool:
"""THE provider-prefix rule, single authority: True when the model name
requires the [OI]-compat wire — a non-empty provider prefix that is
neither ollama nor local. Consumers: transport selection
(rune_operator._transport_for) and context discovery
(local_tool_runtime.context_length). Never copy this rule — import it."""
prefix = model.split("/", 1)[0].strip().lower() if "/" in model else ""
return bool(prefix) and prefix not in ("ollama", "local")
def cloud_transport_for(model: str, timeout: float = 120) -> OpenAICompatTransport:
"""Resolve provider config for a '<Provider>/<model>' name from the same
source of truth the mimo CLI uses (mimocode.jsonc + auth.json)."""
+18 -8
View File
@@ -1228,6 +1228,22 @@ class RuneOperator:
except Exception:
pass
@staticmethod
def _transport_for(model: str, timeout: float = 300.0):
"""Provider-prefix transport selection — ONE authority for both the
direct turn and the approval-resume path (T15 substrate contract).
Any model with a non-ollama provider prefix rides the [OI]-compat
wire; ollama-prefixed and bare names keep the Ollama path. Observed
live (PL1 Task A, 2026-09-12): the resume path hardcoded
OllamaChatTransport, so the post-approval follow-up call hit
localhost with a cloud model name → 400 'invalid model name'."""
from rune.openai_compat_transport import cloud_transport_for, is_cloud_pinned
if is_cloud_pinned(model):
return cloud_transport_for(model, timeout=timeout)
return OllamaChatTransport(timeout=timeout)
def _runtime_for_model(self, model: str, task_description: str = "") -> LocalToolRuntime:
"""Build the runtime used to continue a persisted approval turn.
@@ -1250,7 +1266,7 @@ class RuneOperator:
if not tools:
tools = list(tool_executor.TOOLS) # pre-#24 behavior (degrade)
return LocalToolRuntime(
transport=OllamaChatTransport(),
transport=self._transport_for(model),
executor=tool_executor.execute_tool,
model=model,
tools=tools,
@@ -1967,13 +1983,7 @@ I have tools to read, write, edit, search, lint, test, format, and commit code.
# routing_mode condition and 400'd. Deterministic — any model
# with a non-ollama provider prefix rides the OpenAI-compat
# wire; ollama-prefixed and bare names keep the proven path.
_prov_prefix = model.split("/", 1)[0].strip().lower() if "/" in model else ""
if _prov_prefix and _prov_prefix not in ("ollama", "local"):
from rune.openai_compat_transport import cloud_transport_for
transport = cloud_transport_for(model, timeout=timeout)
else:
transport = OllamaChatTransport(timeout=timeout)
transport = self._transport_for(model, timeout=timeout)
runtime = LocalToolRuntime(
transport=transport,
executor=tool_executor.execute_tool,
+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.66.20
Version: v0.66.32
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -1856,7 +1856,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.66.20 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.66.32 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
+1114
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+178
View File
@@ -0,0 +1,178 @@
"""test_deploy_qualify.py — deploy qualification verdict tests.
Fixtures modeled on the REAL red-promotion incidents (24413e4, fd6e9b4,
de30cd9 — 0.66.28/29 promoted with failing type-check because the
required-checks aggregator published success unconditionally). The
qualifier must fail closed against every shape seen live.
"""
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "scripts"))
import deploy_qualify as dq # noqa: E402
SHA = "a" * 40
OTHER_SHA = "b" * 40
def st(context, status, created, description=""):
return {"context": context, "status": status, "created_at": created,
"description": description}
def run(path, sha, status, created, run_id, conclusions=None):
return {"path": path, "head_sha": sha, "status": status, "created_at": created,
"id": run_id, "_conclusions": conclusions or []}
RC = "CI Pipeline / required-checks"
def jobs_fetch_for(runs):
def fetch(run_id):
for r in runs:
if r.get("id") == run_id:
return [{"name": f"job{i}", "conclusion": c}
for i, c in enumerate(r["_conclusions"])]
return None
return fetch
# ── leg 1: required-checks context ────────────────────────────────────────
class TestRequiredCheckVerdict:
def test_failed_required_check_is_failure(self):
v = dq.required_check_verdict([st(RC + " (push)", "failure", "T2")])
assert v == "failure"
def test_pending_required_check_is_pending(self):
v = dq.required_check_verdict([st(RC + " (push)", "pending", "T2")])
assert v == "pending"
def test_missing_required_check_is_pending(self):
assert dq.required_check_verdict([]) == "pending"
assert dq.required_check_verdict(
[st("CI Pipeline / lint (push)", "success", "T1")]) == "pending"
def test_stale_success_newer_failure_is_failure(self):
v = dq.required_check_verdict([
st(RC + " (push)", "success", "T1"),
st(RC + " (push)", "failure", "T2"),
])
assert v == "failure"
def test_newer_success_after_older_failure_is_success(self):
v = dq.required_check_verdict([
st(RC + " (push)", "failure", "T1"),
st(RC + " (push)", "success", "T2"),
])
assert v == "success"
def test_all_skipped_is_pending_not_success(self):
v = dq.required_check_verdict([st(RC + " (push)", "skipped", "T1")])
assert v == "pending"
# ── leg 2: independent jobs-level verification ────────────────────────────
class TestIndependentJobsVerdict:
def test_all_jobs_success_is_success(self):
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T2", 1,
["success", "success", "success"])]
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "success"
def test_any_job_failure_is_failure(self):
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T2", 1,
["success", "failure", "success"])]
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "failure"
def test_in_progress_run_is_pending(self):
runs = [run("ci.yml@refs/heads/master", SHA, "in_progress", "T2", 1,
["success"])]
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "pending"
def test_no_run_for_this_sha_is_pending(self):
runs = [run("ci.yml@refs/heads/master", OTHER_SHA, "completed", "T2", 1,
["success"])]
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "pending"
def test_source_and_successor_sha_distinct(self):
"""A green run on the successor SHA must not qualify the red source
SHA (and vice versa) — the exact candidate governs."""
runs = [run("ci.yml@refs/heads/master", OTHER_SHA, "completed", "T1", 1,
["success", "success"]),
run("ci.yml@refs/heads/master", SHA, "completed", "T2", 2,
["success", "failure"])]
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "failure"
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=OTHER_SHA) == "success"
def test_tag_fanout_does_not_poison_verdict(self):
"""A failing tag-run must not fail a green master run, and a failing
master run must not be rescued by a green tag run."""
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1,
["success", "success"]),
run("ci.yml@refs/tags/v9.9.9", SHA, "completed", "T2", 2,
["failure"])]
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "success"
runs2 = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1,
["failure"]),
run("ci.yml@refs/tags/v9.9.9", SHA, "completed", "T2", 2,
["success", "success"])]
assert dq.independent_jobs_verdict(runs2, jobs_fetch_for(runs2), sha=SHA) == "failure"
def test_stale_success_newer_completed_failure_wins(self):
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1,
["success", "success"]),
run("ci.yml@refs/heads/master", SHA, "completed", "T2", 2,
["success", "failure"])]
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "failure"
def test_jobs_probe_failure_is_pending(self):
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 99)]
assert dq.independent_jobs_verdict(runs, lambda rid: None, sha=SHA) == "pending"
def test_empty_jobs_visible_is_pending(self):
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1, [])]
assert dq.independent_jobs_verdict(runs, jobs_fetch_for(runs), sha=SHA) == "pending"
# ── combined verdict: fail-closed conjunction ─────────────────────────────
class TestCombinedQualify:
def test_the_real_incident_shape_is_refused(self):
"""THE 24413e4 shape: type-check + unit-tests failed, yet the
aggregator published success. Even with the lying aggregator leg,
the independent jobs leg must refuse."""
statuses = [
st(RC + " (push)", "success", "2026-09-12T19:13:53"), # the lie
st("CI Pipeline / unit-tests (push)", "failure", "2026-09-12T19:13:51"),
st("CI Pipeline / type-check (push)", "failure", "2026-09-12T19:13:34"),
]
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1,
["success", "failure", "failure", "success"])]
v = dq.qualify(statuses, runs, jobs_fetch_for(runs), sha=SHA)
assert v == "failure", "red candidate must never qualify"
def test_pending_aggregator_with_green_jobs_still_pending(self):
statuses = [st(RC + " (push)", "pending", "T2")]
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1,
["success", "success"])]
assert dq.qualify(statuses, runs, jobs_fetch_for(runs), sha=SHA) == "pending"
def test_both_green_qualifies(self):
statuses = [st(RC + " (push)", "success", "T2")]
runs = [run("ci.yml@refs/heads/master", SHA, "completed", "T1", 1,
["success", "success"])]
assert dq.qualify(statuses, runs, jobs_fetch_for(runs), sha=SHA) == "success"
def test_missing_everything_is_pending(self):
assert dq.qualify([], [], jobs_fetch_for([]), sha=SHA) == "pending"
def test_combine_is_fail_closed(self):
assert dq.combine("success", "failure") == "failure"
assert dq.combine("failure", "pending") == "failure"
assert dq.combine("success", "pending") == "pending"
assert dq.combine("success", "success") == "success"
+78
View File
@@ -0,0 +1,78 @@
"""Intent continuity foundation (overnight handoff PART 6): deterministic
state machine, persistence, and all seven invariants."""
import json
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
from rune.intent_continuity import IntentLedger, IntentRecord, CLASSIFICATIONS # noqa: E402
class TestIntentLedger:
def _fresh(self):
return IntentLedger(Path(tempfile.mkdtemp()) / "intent.json")
def test_create_and_reload(self):
"""Invariant 6: state is persistable/reloadable."""
led = self._fresh()
led.create(
id="root",
objective="ship PL1",
classification="BLOCKER",
success_condition="held-out transfer demonstrated",
)
led2 = IntentLedger(led.path)
assert "root" in led2.records
assert led2.records["root"].status == "ACTIVE"
def test_detour_preserves_parent(self):
"""Invariants 1-3."""
led = self._fresh()
led.create(id="root", objective="ship PL1", classification="BLOCKER")
detour = led.create_detour(
id="d",
objective="add scaffolding",
classification="EXTENSION",
parent_intent="root",
return_when="PL1 done",
return_to="root",
)
assert detour.status == "DETOUR"
assert led.records["root"].status == "PARKED"
parent = led.resolve_detour("d")
assert parent.id == "root"
assert parent.status == "ACTIVE"
assert led.records["d"].status == "RESOLVED"
def test_supersession_carries_obligations(self):
"""Invariant 4."""
led = self._fresh()
led.create(id="old", objective="old plan", classification="FIX")
led.records["old"].unresolved_obligations = ["review PR", "update docs"]
new = led.supersede("old", "new", objective="new plan", classification="FIX")
assert "review PR" in new.unresolved_obligations
assert led.records["old"].status == "SUPERSEDED"
def test_child_doesnt_resolve_parent(self):
"""Invariant 5: resolving a child doesn't resolve the parent."""
led = self._fresh()
led.create(id="p", objective="parent", classification="BLOCKER")
led.create_detour(
id="c", objective="child", classification="FIX", parent_intent="p", return_when="done", return_to="p"
)
led.resolve("c")
assert led.records["p"].status == "PARKED" # still parked
led.resolve_detour("c")
assert led.records["p"].status == "ACTIVE"
def test_classification_validated(self):
led = self._fresh()
try:
led.create(id="bad", objective="x", classification="UNICORN")
assert False, "should have raised"
except ValueError:
pass
+45
View File
@@ -584,3 +584,48 @@ class LocalToolRuntimeTests(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
class TestCloudModelContextDiscovery:
"""Context discovery must not hit Ollama with cloud-pinned model names.
Observed live (PL1 Task A/B, deployed 0.66.21/0.66.22): every cloud
task logged `subsystem=context_discovery tier=1 HTTPError 400` because
module-level discovery used OllamaChatTransport for a model whose
provider prefix requires the [OI]-compat wire — then misbudgeted the
task at the 8192 fallback instead of the provider's real window."""
def test_cloud_pinned_model_uses_provider_window_not_ollama(self):
import tempfile
from unittest import mock
from rune import local_tool_runtime as ltr
import rune.openai_compat_transport as _oct
ltr.reset_context_length_cache()
class _StubCloud:
def context_length(self, model):
return 131072
with mock.patch.object(ltr, "OllamaChatTransport",
side_effect=AssertionError(
"Ollama must not be consulted for cloud-pinned models")), \
mock.patch.object(_oct, "cloud_transport_for",
return_value=_StubCloud()):
window = ltr.context_length("Verboo Code/glm-5.3-flash")
assert window == 131072, window
def test_local_model_still_uses_ollama_discovery(self):
from unittest import mock
from rune import local_tool_runtime as ltr
ltr.reset_context_length_cache()
class _StubOllama:
def __init__(self, base_url="http://localhost:11434", timeout=120):
self.calls = []
def context_length(self, model):
return 4096
with mock.patch.object(ltr, "OllamaChatTransport", _StubOllama):
window = ltr.context_length("qwen2.5:3b")
assert window == 4096, window
+28 -5
View File
@@ -40,7 +40,7 @@ class TestMessageTranslation:
resp = {"choices": [{"message": {"role": "assistant", "content": None, "tool_calls": [
{"id": "z", "type": "function", "function": {"name": "run_tests", "arguments": "{\"path\": \"t\"}"}}]}}]}
shape = OpenAICompatTransport._from_openai_response(resp)
tc = shape["message"]["tool_calls"][0]
tc = shape["tool_calls"][0]
assert tc["function"]["name"] == "run_tests"
assert tc["function"]["arguments"] == {"path": "t"} # string → dict
@@ -48,13 +48,30 @@ class TestMessageTranslation:
resp = {"choices": [{"message": {"tool_calls": [
{"function": {"name": "x", "arguments": "not-json"}}]}}]}
shape = OpenAICompatTransport._from_openai_response(resp)
assert shape["message"]["tool_calls"][0]["function"]["arguments"] == {"_raw": "not-json"}
assert shape["tool_calls"][0]["function"]["arguments"] == {"_raw": "not-json"}
def test_no_tool_calls_plain_content(self):
resp = {"choices": [{"message": {"content": "hello"}}]}
shape = OpenAICompatTransport._from_openai_response(resp)
assert shape["message"]["content"] == "hello"
assert "tool_calls" not in shape["message"]
assert shape["content"] == "hello"
assert "tool_calls" not in shape
def test_flat_message_contract_parity_with_ollama(self):
"""CONTRACT: chat() returns the flat MESSAGE object (role/content/
tool_calls at top level) — the same shape OllamaChatTransport.chat
returns. Observed live (PL1 Task A, 2026-09-12): returning the
response envelope {'message': {...}} left the runtime's tool-call
normalization reading the top level, finding no tool_calls and no
content, and failing 'empty terminal visible text' while the model
had returned a valid read_file call."""
resp = {"choices": [{"message": {"role": "assistant", "content": "", "tool_calls": [
{"id": "call_1", "type": "function",
"function": {"name": "read_file", "arguments": "{\"path\": \"/x.py\"}"}}]}}]}
msg = OpenAICompatTransport._from_openai_response(resp)
assert msg["role"] == "assistant"
assert msg["tool_calls"][0]["function"]["name"] == "read_file"
assert msg["tool_calls"][0]["id"] == "call_1" # id preserved for pairing
assert "message" not in msg, "envelope shape leaks into the ChatMessage contract"
def test_protocol_methods(self):
t = OpenAICompatTransport("http://x/v1", "k", "Test")
@@ -135,7 +152,13 @@ class TestTransportSelection:
from rune.tool_protocol import OperatorResponse
return OperatorResponse(success=True, user_text="done", model=captured["model"])
with mock.patch.object(rune_operator, "LocalToolRuntime", FakeRuntime):
import rune.openai_compat_transport as _oct
with (
mock.patch.object(rune_operator, "LocalToolRuntime", FakeRuntime),
mock.patch.object(_oct, "cloud_transport_for",
side_effect=lambda model, timeout=120: OpenAICompatTransport(
"https://mock/v1", "test-key", model.split("/", 1)[0])),
):
op.send_task("Fix the bug in greeting.py: returns 'Helo' not 'Hello'. Verify by running the check script.",
model="Verboo Code/glm-5.3-flash")
assert isinstance(captured["transport"], OpenAICompatTransport)
+955
View File
@@ -0,0 +1,955 @@
"""test_spine.py — Spine V1 contract tests.
Deterministic core, open-world extensibility, evidence-gated maturity,
snapshot/delta, drift, and the synthetic future-capability proof.
Machine-independent: deployment observations use fixture stores.
"""
import json
import sys
import tempfile
from pathlib import Path
import pytest
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT / "src"))
from rune import spine
# ── helpers ───────────────────────────────────────────────────────────────
@pytest.fixture
def doc():
return spine.new_doc()
@pytest.fixture
def seeded(doc):
"""Minimal structural world: two subsystems, two modules."""
spine.add_entity(doc, id="subsystem:alpha", kind="subsystem",
identities={"description": "Alpha"}, maturity="DECLARED")
spine.add_entity(doc, id="subsystem:beta", kind="subsystem",
identities={"description": "Beta"}, maturity="DECLARED")
spine.add_entity(doc, id="module:m_a", kind="module", maturity="DECLARED",
identities={"source": "src/rune/m_a.py"})
spine.add_entity(doc, id="module:m_b", kind="module", maturity="DECLARED",
identities={"source": "src/rune/m_b.py"})
spine.add_relationship(doc, type="belongs_to", from_id="module:m_a",
to_id="subsystem:alpha", evidence_refs=["test"])
spine.add_relationship(doc, type="belongs_to", from_id="module:m_b",
to_id="subsystem:beta", evidence_refs=["test"])
return doc
# ── deterministic entity projection / lookups ─────────────────────────────
class TestLookups:
def test_entity_projection_roundtrip(self, seeded, tmp_path):
p = tmp_path / "s.json"
spine.save(seeded, p)
loaded = spine.load(p)
assert loaded == spine.load(p) # byte-stable across loads
assert spine.entity(loaded, "module:m_a")["kind"] == "module"
def test_ownership_lookup(self, seeded):
subs = [r["to"] for r in spine.relationships(
seeded, from_id="module:m_a", type="belongs_to")]
assert subs == ["subsystem:alpha"]
def test_dependency_lookup(self, seeded):
spine.add_relationship(seeded, type="depends_on", from_id="module:m_a",
to_id="module:m_b", evidence_refs=["ast"])
deps = [r["to"] for r in spine.relationships(
seeded, from_id="module:m_a", type="depends_on")]
assert deps == ["module:m_b"]
def test_consumer_lookup(self, seeded):
# "consumed_by" edges run provider → consumer: m_a is consumed by m_b
spine.add_relationship(seeded, type="consumed_by", from_id="module:m_a",
to_id="module:m_b", evidence_refs=["ast"])
consumers = [r["to"] for r in spine.relationships(
seeded, from_id="module:m_a", type="consumed_by")]
assert consumers == ["module:m_b"]
def test_missing_entity_is_not_found(self, seeded):
assert spine.entity(seeded, "module:ghost") is None
assert spine.relationships(seeded, from_id="module:ghost") == []
# ── open-world extensibility (the Prime Directive) ────────────────────────
class TestOpenWorld:
def test_unknown_kind_relationship_and_metadata_survive_roundtrip(self, seeded, tmp_path):
"""THE extensibility test: a synthetic future capability Spine core
was never written to understand enters as DATA — no core change."""
spine.add_entity(
seeded, id="capability:quantum_resonator_cache", kind="quantum_resonator_cache",
tags=["synthetic-future"], maturity="DECLARED",
identities={"resonance": "7.83Hz", "home_node": "orb-9"},
evidence_refs=["synthetic:proof"],
ext={"entanglement_pair": "module:m_a", "wibble_level": 11,
"future_field_undefined_in_v1": {"nested": True}})
spine.add_relationship(
seeded, type="harmonizes_with", from_id="capability:quantum_resonator_cache",
to_id="module:m_a", evidence_refs=["synthetic:resonance-proof"],
ext={"coupling": "0.99", "unknown_later_field": [1, 2, 3]})
p = tmp_path / "roundtrip.json"
spine.save(seeded, p)
loaded = spine.load(p)
e = spine.entity(loaded, "capability:quantum_resonator_cache")
assert e["kind"] == "quantum_resonator_cache" # unknown kind preserved
assert e["ext"]["wibble_level"] == 11 # unknown metadata preserved
assert e["ext"]["future_field_undefined_in_v1"]["nested"] is True
assert e["identities"]["resonance"] == "7.83Hz"
rels = spine.relationships(loaded, type="harmonizes_with")
assert len(rels) == 1 # unknown relationship type preserved
assert rels[0]["ext"]["coupling"] == "0.99"
assert rels[0]["to"] == "module:m_a" # linked to a real entity
# delta sees it
base = spine.new_doc()
d = spine.diff(base, loaded)
assert "capability:quantum_resonator_cache" in d["entities_added"]
assert "harmonizes_with|capability:quantum_resonator_cache|module:m_a" \
in d["relationships_added"]
# Scaffolding-style consumption: generic queries work on unknown kinds
assert spine.relationships(loaded, from_id="capability:quantum_resonator_cache")
def test_no_closed_enum_in_core(self):
"""Core must not hardcode today's ontology as a whitelist."""
source = (ROOT / "src" / "rune" / "spine.py").read_text()
# the maturity ladder is a scale, not an entity-kind ontology
assert "VALID_KINDS" not in source and "VALID_RELATIONSHIP_TYPES" not in source
# ── evidence / maturity semantics ─────────────────────────────────────────
class TestMaturity:
def test_no_evidence_no_promotion(self, seeded):
with pytest.raises(ValueError, match="requires evidence"):
spine.promote(seeded, "module:m_a", "PRESENT", "")
def test_no_evidence_means_no_live_proven(self, seeded):
e = spine.entity(seeded, "module:m_a")
assert e["maturity"] == "DECLARED" # nothing observed: stays DECLARED
def test_ladder_is_monotonic_with_evidence(self, seeded):
for state, ev in [("PRESENT", "src"), ("WIRED", "imports"), ("EXERCISED", "tests")]:
spine.promote(seeded, "module:m_a", state, ev)
assert spine.entity(seeded, "module:m_a")["maturity"] == "EXERCISED"
def test_demotion_rejected_and_recorded(self, seeded):
spine.promote(seeded, "module:m_a", "WIRED", "imports-x")
e = spine.promote(seeded, "module:m_a", "DECLARED", "downgrade-attempt")
assert e["maturity"] == "WIRED" # not silently demoted
assert any(o["kind"] == "maturity_demotion_rejected"
for o in seeded["observations"])
def test_unknown_maturity_state_rejected(self, seeded):
with pytest.raises(ValueError, match="unknown maturity"):
spine.promote(seeded, "module:m_a", "TRANSCENDENT", "vibes")
# ── deployed identity observation ─────────────────────────────────────────
class TestDeployedObservation:
def test_observes_fixture_identity(self, doc, tmp_path, monkeypatch):
dep = tmp_path / "deploy"
(dep / "src" / "rune").mkdir(parents=True)
(dep / "src" / "rune" / "_build_identity.json").write_text(json.dumps(
{"commit": "a" * 40, "version": "9.9.9", "deployed_at": "T"}))
monkeypatch.setattr(spine, "DEPLOYED_LINK", dep)
obs = spine._deployed_observation()
assert obs["deployed_sha"] == "a" * 12 and obs["deployed_version"] == "9.9.9"
def test_absent_deployment_is_none_not_invented(self, monkeypatch, tmp_path):
monkeypatch.setattr(spine, "DEPLOYED_LINK", tmp_path / "nonexistent")
assert spine._deployed_observation() is None
def test_deployed_identity_drift_detected(self, seeded, tmp_path, monkeypatch):
dep = tmp_path / "deploy"
(dep / "src" / "rune").mkdir(parents=True)
(dep / "src" / "rune" / "_build_identity.json").write_text(json.dumps(
{"commit": "b" * 40, "version": "0.0.1-test-mismatch", "deployed_at": "T"}))
monkeypatch.setattr(spine, "DEPLOYED_LINK", dep)
monkeypatch.setattr(spine, "ROOT", tmp_path) # no pyproject → no source version
# no version drift observable without source; identity observation works
obs = spine._deployed_observation()
assert obs["deployed_version"] == "0.0.1-test-mismatch"
# ── drift ─────────────────────────────────────────────────────────────────
class TestDrift:
def test_registered_but_absent(self, seeded, monkeypatch):
monkeypatch.setattr(spine, "_canary_registry", lambda: ["m_ghost"])
monkeypatch.setattr(spine, "SRC_DIR", tmp_src := ROOT / "src" / "rune") # real dir
d = spine.drift(seeded)
assert any(x["class"] == "registered_but_absent" and x["entity"] == "module:m_ghost"
for x in d)
def test_relationship_disappeared(self, seeded, monkeypatch):
spine.add_relationship(seeded, type="consumed_by", from_id="module:m_a",
to_id="module:m_b", evidence_refs=["stale"])
monkeypatch.setattr(spine, "_import_graph", lambda: {"m_b": set()})
monkeypatch.setattr(spine, "_canary_registry", lambda: [])
monkeypatch.setattr(spine, "SRC_DIR", ROOT / "src" / "rune")
d = spine.drift(seeded)
assert any(x["class"] == "relationship_disappeared" for x in d)
def test_stale_evidence_does_not_prove_live(self, seeded):
"""A stale evidence ref must never read as LIVE_PROVEN."""
e = spine.entity(seeded, "module:m_a")
e["evidence_refs"].append("deployed-tree:deadbeef-old-observation")
assert e["maturity"] == "DECLARED" # evidence ref alone promotes nothing
# LIVE_PROVEN only via explicit evidence-gated promotion
spine.promote(seeded, "module:m_a", "PRESENT", "fresh-source-check")
assert spine.entity(seeded, "module:m_a")["maturity"] == "PRESENT"
def test_declared_but_unwired_capability(self, seeded):
"""Registered with no wiring evidence → must not read WIRED."""
spine.add_entity(seeded, id="capability:orphan_feature", kind="capability",
maturity="DECLARED", evidence_refs=["registry:orphan"])
assert spine.entity(seeded, "capability:orphan_feature")["maturity"] == "DECLARED"
# ── snapshots / deltas ────────────────────────────────────────────────────
class TestSnapshots:
def test_snapshot_deterministic(self, seeded, tmp_path, monkeypatch):
monkeypatch.setattr(spine, "SNAPSHOTS", tmp_path / "snaps")
monkeypatch.setattr(spine, "_deployed_observation", lambda: None)
seeded["generated_at"] = "FIXED"
p1 = spine.snapshot(seeded, "t")
p2 = spine.snapshot(seeded, "t")
assert p1.read_bytes() == p2.read_bytes() # byte-identical
def test_diff_categories(self, seeded):
old = json.loads(json.dumps(seeded))
spine.add_entity(seeded, id="module:m_new", kind="module", maturity="DECLARED")
spine.promote(seeded, "module:m_a", "WIRED", "new-import-evidence")
spine.add_relationship(seeded, type="depends_on", from_id="module:m_a",
to_id="module:m_b", evidence_refs=["ast"])
d = spine.diff(old, seeded)
assert d["entities_added"] == ["module:m_new"]
assert d["relationships_added"] == ["depends_on|module:m_a|module:m_b"]
assert {"id": "module:m_a", "from": "DECLARED", "to": "WIRED"} \
in d["maturity_changed"]
assert d["entities_removed"] == []
def test_diff_is_deterministic(self, seeded):
old = json.loads(json.dumps(seeded))
d1 = spine.diff(old, seeded)
d2 = spine.diff(old, seeded)
assert d1 == d2
# ── schema versioning ─────────────────────────────────────────────────────
class TestSchemaVersion:
def test_save_stamps_version(self, seeded, tmp_path):
p = spine.save(seeded, tmp_path / "s.json")
assert json.loads(p.read_text())["schema_version"] == spine.SCHEMA_VERSION
def test_future_version_refused_for_rewrite(self, seeded, tmp_path):
seeded["schema_version"] = 99
with pytest.raises(ValueError, match="schema_version"):
spine.save(seeded, tmp_path / "future.json")
def test_unknown_fields_of_document_survive(self, seeded, tmp_path):
seeded["a_field_spine_v2_will_understand"] = {"x": 1}
p = spine.save(seeded, tmp_path / "s.json")
# loader tolerates unknown document fields (no whitelist strip)
loaded = json.loads(p.read_text())
assert loaded["a_field_spine_v2_will_understand"] == {"x": 1}
# ── ingest / compatibility (umbrella migration) ───────────────────────────
class TestUmbrellaMigration:
def test_ingest_refuses_generated_view(self, tmp_path):
generated = {"_generated_by_spine": True, "umbrellas": {}}
p = tmp_path / "view.json"
p.write_text(json.dumps(generated))
with pytest.raises(ValueError, match="refusing to ingest"):
spine.ingest_umbrellas(spine.new_doc(), p)
def test_ingest_preserves_structure(self, tmp_path):
manifest = {"umbrellas": {
"alpha": {"description": "A", "modules": ["m_a", "m_b"],
"depends_on": ["beta"], "mirrors": ["shared/x.py"]},
"beta": {"description": "B", "modules": [], "depends_on": [], "mirrors": []}}}
p = tmp_path / "umbrellas.json"
p.write_text(json.dumps(manifest))
doc = spine.new_doc()
n = spine.ingest_umbrellas(doc, p)
assert n == 2
# ownership preserved
assert [r["to"] for r in spine.relationships(
doc, from_id="module:m_a", type="belongs_to")] == ["subsystem:alpha"]
# subsystem deps preserved
assert [r["to"] for r in spine.relationships(
doc, from_id="subsystem:alpha", type="depends_on")] == ["subsystem:beta"]
# mirrors preserved
assert [r["to"] for r in spine.relationships(
doc, from_id="subsystem:alpha", type="mirrors")] == ["file:shared/x.py"]
def test_export_reproduces_equivalent_view(self, tmp_path):
manifest = {"umbrellas": {
"alpha": {"description": "A", "modules": ["m_a"],
"depends_on": ["beta"], "mirrors": ["shared/x.py"]},
"beta": {"description": "B", "modules": ["m_b"],
"depends_on": [], "mirrors": []}}}
p = tmp_path / "umbrellas.json"
p.write_text(json.dumps(manifest))
doc = spine.new_doc()
spine.ingest_umbrellas(doc, p)
view = spine.export_umbrellas(doc)
assert view["_generated_by_spine"] is True
assert view["umbrellas"]["alpha"]["modules"] == ["m_a"]
assert view["umbrellas"]["alpha"]["depends_on"] == ["beta"]
assert view["umbrellas"]["alpha"]["mirrors"] == ["shared/x.py"]
assert view["umbrellas"]["beta"]["modules"] == ["m_b"]
# round-trip fidelity: the generated view re-ingests to the same graph
view_path = tmp_path / "view.json"
view_path.write_text(json.dumps(view))
doc2 = spine.new_doc()
# (direct ingest of the view is refused; equivalence is proven by
# comparing the exported view against the original manual truth)
assert view["umbrellas"] == manifest["umbrellas"]
def test_export_never_overwrites_manual_manifest(self, seeded, monkeypatch, tmp_path):
monkeypatch.setattr(spine, "UMBRELLA_MANIFEST", tmp_path / "manual.json")
(tmp_path / "manual.json").write_text(json.dumps({"umbrellas": {}}))
# main() with no path → refuses (manual manifest protection)
rc = spine.main(["export-umbrellas"])
assert rc == 1
# ── Scaffolding consumes Spine ────────────────────────────────────────────
class TestScaffoldingConsumesSpine:
def test_scaffold_projection_reads_spine_store(self, seeded, tmp_path, monkeypatch):
"""Scaffolding's structural facts must come from the Spine store,
not from re-deriving authorities independently."""
p = tmp_path / "current.json"
spine.save(seeded, p)
monkeypatch.setattr(spine, "CURRENT", p)
monkeypatch.setattr(spine, "ROOT", tmp_path)
monkeypatch.setattr(spine, "SRC_DIR", tmp_path / "src" / "rune")
(tmp_path / "src" / "rune").mkdir(parents=True, exist_ok=True)
(tmp_path / "src" / "rune" / "m_a.py").write_text("x = 1\n")
(tmp_path / "tests").mkdir(exist_ok=True)
monkeypatch.setattr(spine, "TESTS_DIR", tmp_path / "tests")
monkeypatch.setattr(spine, "_deployed_observation", lambda: None)
# fresh store → Scaffolding builds it deterministically
doc = spine.build()
spine.save(doc)
# a component card must be answerable from the store alone
card_deps = [r["to"] for r in spine.relationships(
doc, from_id="module:m_a", type="depends_on")]
subs = [r["to"] for r in spine.relationships(
doc, from_id="module:m_a", type="belongs_to")]
assert subs == ["subsystem:alpha"]
assert isinstance(card_deps, list)
# maturity visible to Scaffolding consumers — the fixture created
# src/rune/m_a.py, so the evidence gate correctly promotes PRESENT
assert spine.entity(doc, "module:m_a")["maturity"] == "PRESENT"
def test_scaffold_impact_uses_spine_store_not_umbrellas(self, monkeypatch, tmp_path):
"""The real integration: scripts/scaffold_projection.py resolves
ownership from the Spine store. Proven by pointing the store at a
fixture world and checking the card reflects SPINE data (and that
a Spine-only entity — absent from any umbrella file — is served)."""
import importlib
doc = spine.new_doc()
spine.add_entity(doc, id="subsystem:spine_test_world", kind="subsystem",
identities={"description": "fixture"})
spine.add_entity(doc, id="module:widget_core", kind="module", maturity="EXERCISED",
identities={"source": "src/rune/widget_core.py"},
evidence_refs=["fixture"])
spine.add_relationship(doc, type="belongs_to", from_id="module:widget_core",
to_id="subsystem:spine_test_world", evidence_refs=["fixture"])
spine.add_relationship(doc, type="consumed_by", from_id="module:widget_core",
to_id="module:widget_user", evidence_refs=["fixture"])
store = tmp_path / "current.json"
spine.save(doc, store)
monkeypatch.setattr(spine, "CURRENT", store)
import scripts.scaffold_projection as sp # noqa: E402
importlib.reload(sp)
monkeypatch.setattr(sp, "ROOT", tmp_path)
card = sp.component_card("widget_core", spine.load(store))
assert card["subsystem"] == "spine_test_world" # from Spine, not umbrellas.json
assert card["consumers"] == ["widget_user"] # from Spine relationships
assert card["spine_maturity"] == "EXERCISED" # evidence state surfaced
# ── graph separation + cross-linking (addendum 1) ─────────────────────────
class TestGraphSeparation:
def test_spine_queries_need_no_memory_infrastructure(self, seeded):
"""Structural lookup works on the Spine store alone — no Memory
fixture, no retrieval, no inference."""
q = spine.query(seeded, "module:m_a")
assert q["owner"] == ["subsystem:alpha"]
assert q["state"]["maturity"] == "DECLARED"
def test_stable_ids_survive_full_cycle(self, seeded, tmp_path):
p = spine.save(seeded, tmp_path / "s.json")
loaded = spine.load(p)
snap = tmp_path / "snap.json"
spine.save(loaded, snap)
d = spine.diff(spine.load(p), spine.load(snap))
assert d["entities_removed"] == [] and d["entities_added"] == []
assert spine.entity(spine.load(snap), "module:m_a") is not None
def test_spine_uri_cross_reference_without_authority_duplication(self, seeded):
"""A future Memory record references a Spine entity by URI — the
structural edge stays in Spine, the reference carries no authority."""
uri = spine.spine_uri("module:m_a")
assert uri == "spine://module/m_a"
assert spine.resolve_uri(uri) == "module:m_a"
# round-trip: resolve → the same entity Spine owns
assert spine.entity(seeded, spine.resolve_uri(uri))["kind"] == "module"
with pytest.raises(ValueError, match="not a spine URI"):
spine.resolve_uri("memory://lesson/42")
def test_memory_association_does_not_expand_frontier(self, seeded):
"""A hypothetical Memory-style association edge must NOT expand the
structural impact frontier (noisy impact = broken resolution)."""
spine.add_relationship(seeded, type="related_memory", from_id="module:m_a",
to_id="memory:old-incident-42", evidence_refs=["hypothetical"])
f = spine.impact_frontier(seeded, ["module:m_a"])
assert "memory:old-incident-42" not in f["level1_obligations"]
assert f["traversal_scope"] == "spine-structural-only"
# while a REAL structural edge does expand
spine.add_relationship(seeded, type="consumed_by", from_id="module:m_a",
to_id="module:m_b", evidence_refs=["ast"])
f2 = spine.impact_frontier(seeded, ["module:m_a"])
assert "module:m_b" in f2["level1_obligations"]
def test_unknown_relationship_is_obligation_bearing_fail_loud(self, seeded):
"""A previously unseen relationship type defaults to frontier
visibility — new structural relations deserve inspection until
evidence reclassifies them (data-driven semantics, open world)."""
spine.add_relationship(seeded, type="contextualises", from_id="module:m_a",
to_id="subsystem:beta", evidence_refs=["unseen-type"])
f = spine.impact_frontier(seeded, ["module:m_a"])
assert "subsystem:beta" in f["level1_obligations"]
def test_structural_facts_survive_hypothetical_memory_archive(self, seeded, tmp_path):
"""Spine retains current structural truth regardless of any future
Memory salience/decay/archive state — Spine is not Memory-dependent."""
spine.promote(seeded, "module:m_a", "WIRED", "import-evidence")
p = spine.save(seeded, tmp_path / "s.json")
# (hypothetical: Memory archives the story of how this was learned)
assert spine.entity(spine.load(p), "module:m_a")["maturity"] == "WIRED"
# ── contracts, task binding, frontier, predicted-vs-observed (T22) ────────
class TestContractsAndFrontier:
def _contract_world(self, doc):
spine.add_entity(doc, id="contract:chat_transport", kind="contract",
evidence_refs=["protocol:ChatTransport"], maturity="DECLARED")
spine.add_entity(doc, id="module:transport_a", kind="module", maturity="DECLARED")
spine.add_entity(doc, id="module:transport_b", kind="module", maturity="DECLARED")
spine.add_entity(doc, id="module:operator", kind="module", maturity="DECLARED")
spine.add_relationship(doc, type="implements", from_id="module:transport_a",
to_id="contract:chat_transport", evidence_refs=["ast"])
spine.add_relationship(doc, type="implements", from_id="module:transport_b",
to_id="contract:chat_transport", evidence_refs=["ast"])
spine.add_relationship(doc, type="consumes", from_id="module:operator",
to_id="contract:chat_transport", evidence_refs=["ast"])
return doc
def test_contract_change_expands_to_all_parties(self, seeded):
"""A CONTRACT change deserves every implementer + consumer's inspection."""
doc = self._contract_world(seeded)
f = spine.impact_frontier(doc, ["contract:chat_transport"])
assert {"module:transport_a", "module:transport_b", "module:operator"} \
<= set(f["level1_obligations"])
def test_implementation_change_is_bounded(self, seeded):
"""An IMPLEMENTATION change (one transport) touches the contract but
the frontier stays bounded — it does not pull sibling implementers
in as direct obligations (they see the contract, not this change)."""
doc = self._contract_world(seeded)
f = spine.impact_frontier(doc, ["module:transport_a"])
# HARDENED SEMANTICS (T1A): implementation change, contract preserved
# → contract is a direct obligation; siblings are CONTEXTUAL, never
# direct. (The pre-hardening expectation that siblings land in
# level1 encoded the wrong semantics — see final report.)
assert "contract:chat_transport" in f["level1_obligations"]
assert "module:transport_b" in f["contextual_not_obligations"]
assert "module:transport_b" not in f["level1_obligations"]
assert f["change_kind"] == "implementation_change"
assert f["level0_direct"] == ["module:transport_a"]
def test_task_binding_and_frontier(self, seeded):
doc = self._contract_world(seeded)
spine.bind_task(doc, "TASK-123", ["module:transport_a"], evidence_ref="issue")
assert spine.task_entities(doc, "TASK-123") == ["module:transport_a"]
seeds = spine.task_entities(doc, "TASK-123")
f = spine.impact_frontier(doc, seeds)
assert "contract:chat_transport" in f["level1_obligations"]
def test_predicted_vs_observed_flags_misses_as_candidates(self, seeded):
old = json.loads(json.dumps(seeded))
predicted = spine.impact_frontier(old, ["module:m_a"])
# the change reveals a consequence the structure didn't predict:
# m_b (a consumer nobody had recorded) starts consuming m_a
spine.add_relationship(seeded, type="consumed_by", from_id="module:m_a",
to_id="module:m_b", evidence_refs=["observed:real-run"])
r = spine.predicted_vs_observed(predicted, seeded, ["module:m_a"])
assert "module:m_b" in r["missed"]
assert r["candidate_corrections"][0]["status"] == "CANDIDATE"
assert "evidence required" in r["candidate_corrections"][0]["note"]
def test_structural_event_emission_is_generic(self, seeded):
spine.add_relationship(seeded, type="consumed_by", from_id="module:m_a",
to_id="module:m_b", evidence_refs=["ast"])
events = spine.structural_events(seeded)
kinds = {e["type"] for e in events}
assert "entity_observed" in kinds and "relationship_observed" in kinds
# dumb typed facts — no salience, no policy fields
rel_event = next(e for e in events if e["type"] == "relationship_observed"
and e["relationship"] == "consumed_by")
assert rel_event["from"] == "module:m_a" and rel_event["to"] == "module:m_b"
def test_evidence_conflict_drift(self, seeded):
e = spine.entity(seeded, "module:m_a")
e["maturity"] = "LIVE_PROVEN"
e["evidence_refs"] = [] # claim without proof
d = spine.drift(seeded)
assert any(x["class"] == "evidence_conflict" and x["entity"] == "module:m_a"
for x in d)
def test_intrinsic_query_answers_question_set_deterministically(self, seeded):
"""THE acceptance property: the representative question set answered
from the store alone — no LLM, no Memory, no rediscovery."""
doc = self._contract_world(seeded)
q = spine.query(doc, "module:transport_a")
assert q["kind"] == "module"
assert q["contracts_implements"] == ["contract:chat_transport"]
assert q["state"]["maturity"] == "DECLARED"
assert q["evidence"] == []
assert q["impact_frontier"]["level0_direct"] == ["module:transport_a"]
assert q["uri"] == "spine://module/transport_a"
def test_synthetic_future_capability_flows_through_frontier(self, seeded):
"""Extensibility end-to-end: the unknown capability registers,
round-trips, binds to a task, expands a frontier, and survives
delta — with zero Spine core changes (kind/relationship unseen)."""
spine.add_entity(seeded, id="capability:quantum_resonator_cache",
kind="quantum_resonator_cache", maturity="DECLARED",
identities={"resonance": "7.83Hz"},
evidence_refs=["synthetic"], ext={"wibble": 11})
spine.add_relationship(seeded, type="harmonizes_with",
from_id="capability:quantum_resonator_cache",
to_id="module:m_a", evidence_refs=["synthetic"])
spine.bind_task(seeded, "TASK-FUTURE", ["capability:quantum_resonator_cache"])
q = spine.query(seeded, "capability:quantum_resonator_cache")
assert q["kind"] == "quantum_resonator_cache" # unknown kind queried generically
assert q["state"]["maturity"] == "DECLARED"
f = spine.impact_frontier(seeded, spine.task_entities(seeded, "TASK-FUTURE"))
assert "module:m_a" in f["level1_obligations"] # unseen rel = obligation-bearing
# ── HARDENING T1: contract-change vs implementation-change semantics ──────
class TestHardeningContractSemantics:
def _world(self, seeded):
spine.add_entity(seeded, id="contract:chat_transport", kind="contract",
evidence_refs=["protocol"], maturity="DECLARED")
for m in ("transport_a", "transport_b"):
spine.add_entity(seeded, id=f"module:{m}", kind="module", maturity="DECLARED")
spine.add_relationship(seeded, type="implements", from_id=f"module:{m}",
to_id="contract:chat_transport", evidence_refs=["ast"])
spine.add_entity(seeded, id="module:operator", kind="module", maturity="DECLARED")
spine.add_relationship(seeded, type="consumes", from_id="module:operator",
to_id="contract:chat_transport", evidence_refs=["ast"])
return seeded
def test_A_implementation_change_siblings_not_direct(self, seeded):
doc = self._world(seeded)
f = spine.impact_frontier(doc, ["module:transport_a"])
assert f["change_kind"] == "implementation_change"
assert f["level0_direct"] == ["module:transport_a"]
assert "contract:chat_transport" in f["level1_obligations"]
assert "module:transport_b" in f["contextual_not_obligations"]
assert "module:transport_b" not in f["level1_obligations"]
# X (consumer) is reached through the contract's contextual parties
# or obligations per evidence — must NOT be silently direct here
assert "module:operator" in f["contextual_not_obligations"] or \
"module:operator" in f["level1_obligations"]
def test_B_contract_change_all_parties_direct(self, seeded):
doc = self._world(seeded)
f = spine.impact_frontier(doc, ["contract:chat_transport"])
assert f["change_kind"] == "contract_change"
for party in ("module:transport_a", "module:transport_b", "module:operator"):
assert party in f["level1_obligations"], party
def test_C_contract_violation_escalates(self, seeded):
doc = self._world(seeded)
f = spine.impact_frontier(doc, ["module:transport_a"],
change_kind="contract_violation")
# verification proved the implementation breaks the contract:
# every party becomes a direct obligation
assert f["change_kind"] == "contract_violation"
for party in ("module:transport_b", "module:operator"):
assert party in f["level1_obligations"], party
def test_kind_inference_is_deterministic(self, seeded):
doc = self._world(seeded)
assert spine.impact_frontier(doc, ["contract:chat_transport"])["change_kind"] == "contract_change"
assert spine.impact_frontier(doc, ["module:transport_a"])["change_kind"] == "implementation_change"
# ── HARDENING T3/T4: projector ownership, reconciliation, state semantics ─
class TestHardeningReconciliation:
def _fact(self, doc, type="depends_on", frm="module:x", to="module:y", auth="proj-x"):
spine.add_entity(doc, id=frm, kind="module", maturity="DECLARED")
spine.add_entity(doc, id=to, kind="module", maturity="DECLARED")
r = spine.add_relationship(doc, type=type, from_id=frm, to_id=to,
evidence_refs=["fixture"])
r["authority"] = auth
r["status"] = "CURRENT"
return r
def test_foreign_projector_refresh_never_deletes_other_authority_facts(self, seeded):
"""Projector Y's refresh must not touch projector X's facts —
the reconciliation boundary is AUTHORITY OWNERSHIP."""
doc = seeded
self._fact(doc, auth="proj-x")
foreign = spine.add_relationship(doc, type="related_thing", from_id="module:x",
to_id="module:y", evidence_refs=["fixture"])
spine._authority_observed(doc, foreign, "proj-y") # proj-y ran and owns it
# proj-x refresh observes its own fact; reconcile must leave proj-y's
# fact alone — only proj-y's own pass can stale it
spine._doc_authorities(doc).setdefault("proj-x", {"generation": 1, "observed": ["depends_on|module:x|module:y"]})
staled = spine.reconcile_authorities(doc)
assert foreign["status"] == "CURRENT" # foreign fact survives
assert all(s["authority"] != "proj-y" for s in staled)
def test_owner_stopping_emission_stales_not_deletes(self, seeded):
"""Projector X later stops observing its fact → STALE (marked,
reported in drift, excluded from frontier) — not silently deleted,
not silently current."""
doc = seeded
self._fact(doc, auth="proj-x")
spine._doc_authorities(doc)["proj-x"] = {"generation": 2, "observed": []}
staled = spine.reconcile_authorities(doc)
assert len(staled) == 1
f = spine.impact_frontier(doc, ["module:x"])
assert "module:y" not in f["level1_obligations"] # stale ≠ current truth
d = spine.drift(doc)
assert any(x["class"] == "stale_fact" for x in d)
def test_projector_failure_does_not_masquerade_as_fresh(self, seeded):
doc = seeded
self._fact(doc, auth="proj-x")
spine._doc_authorities(doc)["proj-x"] = {"generation": 1, "observed": ["depends_on|module:x|module:y"]}
spine.authority_failed(doc, "proj-x", "source tree unreachable")
h = spine.authority_health(doc)
assert h["proj-x"]["status"] == "DEGRADED"
r = [r for r in doc["relationships"] if r.get("authority") == "proj-x"][0]
assert r["status"] == "STALE"
f = spine.impact_frontier(doc, ["module:x"])
assert "module:y" not in f["level1_obligations"]
assert any(o["kind"] == "authority_degraded" for o in doc["observations"])
def test_manual_fact_without_authority_survives(self, seeded):
doc = seeded
r = self._fact(doc, auth=None)
r.pop("authority", None); r.pop("status", None)
spine.reconcile_authorities(doc)
assert r["status"] != "STALE" if "status" in r else True
assert r in doc["relationships"]
def test_two_authorities_conflicting_preserve_both(self, seeded):
spine.mark_conflict(seeded, "module:m_a", "registry", "exists",
"runtime", "unavailable", "evidence:live-check")
e = spine.entity(seeded, "module:m_a")
assert e["status"] == "CONFLICTING"
assert len(e["ext"]["conflicts"]) == 1 # BOTH preserved, not last-wins
assert spine.resolve_status(e) == "CONFLICTING"
q = spine.query(seeded, "module:m_a")
assert q["state"]["resolved_status"] == "CONFLICTING"
def test_conflict_requires_evidence(self, seeded):
with pytest.raises(ValueError, match="requires evidence"):
spine.mark_conflict(seeded, "module:m_a", "a", "x", "b", "y", "")
def test_unknown_is_not_healthy(self, seeded):
"""A never-observed entity is UNKNOWN — not current, not false."""
spine.add_entity(seeded, id="module:never_checked", kind="module",
maturity="DECLARED")
assert spine.resolve_status(spine.entity(seeded, "module:never_checked")) == "UNKNOWN"
def test_live_proven_without_evidence_is_conflict_not_stale(self, seeded):
"""Case A vs B distinction: no evidence at all = CONFLICTING claim;
a projector that stopped refreshing = STALE. Different conditions."""
spine.add_entity(seeded, id="module:no_proof", kind="module",
maturity="LIVE_PROVEN") # zero evidence refs
assert spine.resolve_status(spine.entity(seeded, "module:no_proof")) == "CONFLICTING"
# ── HARDENING T5: durable identity — rename/alias/supersession ────────────
class TestHardeningDurableIdentity:
def test_rename_old_spine_reference_stays_meaningful(self, seeded, tmp_path):
"""Memory stores spine://module/memory_selector; the module is later
renamed memory_orchestrator. The old reference must still resolve."""
spine.alias(seeded, "module:m_a", "module:m_a_old_name")
p = spine.save(seeded, tmp_path / "s.json")
reloaded = spine.load(p)
# old URI resolves to the same conceptual entity
old_ref = spine.resolve_uri("spine://module/m_a_old_name")
e = spine.entity(reloaded, old_ref)
assert e is not None and e["id"] == "module:m_a"
# survives another build cycle (aliases are data, carried forward)
doc2 = spine.build.__wrapped__(reloaded) if hasattr(spine.build, "__wrapped__") else reloaded
assert spine.entity(doc2, "module:m_a_old_name") is not None
def test_supersession_requires_evidence_and_preserves_history(self, seeded):
spine.add_entity(seeded, id="module:new_thing", kind="module", maturity="DECLARED")
with pytest.raises(ValueError, match="requires evidence"):
spine.supersede(seeded, "module:new_thing", "module:m_a", "")
spine.supersede(seeded, "module:new_thing", "module:m_a", "evidence:decision-record")
old = spine.entity(seeded, "module:m_a")
assert old is not None # never deleted
assert old["superseded_by"] == "module:new_thing"
assert old["status"] == "SUPERSEDED"
# the supersedes relationship is data, round-trips, and is
# non-obligation for frontier purposes (context, not live wiring)
p = spine.save(seeded, Path(tempfile.mkdtemp()) / "s.json")
assert spine.entity(spine.load(p), "module:m_a")["superseded_by"] == "module:new_thing"
def test_superseded_entity_not_treated_as_current(self, seeded):
spine.add_entity(seeded, id="module:new_thing", kind="module", maturity="DECLARED")
spine.supersede(seeded, "module:new_thing", "module:m_a", "evidence:decision")
f = spine.impact_frontier(seeded, ["module:m_a"])
# a superseded seed is still answerable, but its status is explicit
assert spine.resolve_status(spine.entity(seeded, "module:m_a")) == "SUPERSEDED"
# ── HARDENING T2: independent observed impact ─────────────────────────────
class TestHardeningObservedImpact:
def test_observed_impact_comes_from_evidence_not_frontier_rerun(self, seeded):
"""The observed side must be built from EXTERNAL evidence items —
test failures, canary results, runtime errors — each naming its
authority. The frontier function is not invoked for the observed set."""
spine.add_entity(seeded, id="module:m_c", kind="module", maturity="DECLARED")
ev = [{"entity": "module:m_b", "source": "pytest-failure",
"authority": "test-suite", "detail": "test_consumer_lookup failed"},
{"entity": "module:m_c", "source": "canary", "authority": "canary-runner",
"detail": "canary_native red"}]
r = spine.record_observed_impact(seeded, "TASK-X", ev)
assert r["observed_entities"] == ["module:m_b", "module:m_c"]
kinds = [o["kind"] for o in seeded["observations"]]
assert kinds.count("observed_consequence") == 2
# nothing was auto-promoted into structural relationships
assert not spine.relationships(seeded, type="consumed_by",
from_id="module:m_a", to_id="module:m_c")
def test_predicted_vs_observed_with_independent_evidence(self, seeded):
old = json.loads(json.dumps(seeded))
predicted = spine.impact_frontier(old, ["module:m_a"])
# the change ran; independent evidence implicates an entity the
# structure never predicted
spine.record_observed_impact(seeded, "TASK-X", [
{"entity": "module:m_b", "source": "runtime-error",
"authority": "deployed-runtime", "detail": "import crash"}])
# compare: prediction's frontier vs the EVIDENCE-derived set
observed_set = {"module:m_b"}
pred_set = {e for k in ("level0_direct", "level1_obligations",
"contracts", "level2_verification")
for e in predicted.get(k, [])}
missed = observed_set - pred_set
assert missed == {"module:m_b"}
# and the comparison helper records it as CANDIDATE, not truth
result = spine.predicted_vs_observed(predicted, seeded, ["module:m_a"])
assert all(c["status"] == "CANDIDATE" for c in result["candidate_corrections"])
# ── HARDENING: candidate relationships + event seam (T8) ──────────────────
class TestHardeningCandidatesAndEvents:
def test_model_speculation_is_candidate_never_truth(self, seeded):
"""A model-hypothesised relationship (no evidence) enters as
CANDIDATE: representable, queryable, but NOT structural truth —
excluded from impact frontiers until evidence promotes it."""
spine.add_relationship(seeded, type="depends_on", from_id="module:m_a",
to_id="module:m_b", evidence_refs=[]) # speculation
rel = spine.relationships(seeded, from_id="module:m_a", type="depends_on")[0]
assert rel["status"] == "CANDIDATE"
f = spine.impact_frontier(seeded, ["module:m_a"])
assert "module:m_b" not in f["level1_obligations"]
# evidence arrives → promotion to CURRENT makes it real
rel["evidence_refs"] = ["ast-import:m_a"]
rel["status"] = "CURRENT"
f2 = spine.impact_frontier(seeded, ["module:m_a"])
assert "module:m_b" in f2["level1_obligations"]
def test_event_seam_carries_future_event_kinds_without_core_redesign(self, seeded):
"""The seam must transport stale/conflict/supersession/degradation
events as typed facts — future event kinds flow without policy."""
spine.add_entity(seeded, id="module:new_thing", kind="module", maturity="DECLARED")
spine.supersede(seeded, "module:new_thing", "module:m_a", "evidence:decision")
spine.mark_conflict(seeded, "module:new_thing", "registry", "exists",
"runtime", "unavailable", "evidence:check")
spine._authority_observed(seeded, None, "proj-x")
spine.authority_failed(seeded, "proj-x", "boom")
events = spine.structural_events(seeded)
kinds = {e["type"] for e in events}
assert {"superseded", "authority_conflict", "authority_degraded"} <= kinds
# all dumb typed facts: no salience/policy/wake fields anywhere
for e in events:
assert not {"salience", "attention", "wake", "priority"} & set(e.keys())
def test_intrinsic_query_on_real_store_is_fast_and_deterministic(self):
"""T6: the self-knowledge property on the REAL store — one query,
deterministic, no LLM/memory/rebuild. And fast (bounded lookup)."""
doc = spine.load()
if doc is None:
pytest.skip("no spine store in this checkout")
import time as _t
t0 = _t.monotonic()
q1 = spine.query(doc, "module:rune_operator")
q2 = spine.query(doc, "module:rune_operator")
dt = _t.monotonic() - t0
assert q1 == q2 # deterministic
assert q1["owner"] and q1["state"]["maturity"]
assert "impact_frontier" in q1
assert dt < 2.0, f"query took {dt:.2f}s — intrinsic property lost"
# ── CLOSURE 1: durable identity through the REAL build path ───────────────
class TestClosureRealBuildDurability:
"""The hardening pass tested alias/supersession around save/load with a
build-path fallback that could mock the real refresh. Memory will depend
on spine:// references — prove durability through the ACTUAL build."""
def _seeded_store(self, tmp_path):
doc = spine.new_doc()
spine.add_entity(doc, id="subsystem:memory", kind="subsystem",
identities={"description": "fixture"}, maturity="DECLARED")
for m in ("memory_selector",):
spine.add_entity(doc, id=f"module:{m}", kind="module", maturity="DECLARED",
identities={"source": f"src/rune/{m}.py"})
spine.add_relationship(doc, type="belongs_to", from_id=f"module:{m}",
to_id="subsystem:memory", evidence_refs=["fixture"])
spine.save(doc, spine.CURRENT)
return doc
def test_alias_survives_real_build_refresh(self, tmp_path, monkeypatch):
"""create A → alias A_old → save → reload → REAL build() → save →
reload → spine://.../A_old still resolves to A. No mocked path."""
monkeypatch.setattr(spine, "STORE_DIR", tmp_path / "store")
monkeypatch.setattr(spine, "CURRENT", tmp_path / "store" / "current.json")
monkeypatch.setattr(spine, "SNAPSHOTS", tmp_path / "store" / "snaps")
monkeypatch.setattr(spine, "_deployed_observation", lambda: None)
monkeypatch.setattr(spine, "SRC_DIR", tmp_path / "src" / "rune")
(tmp_path / "src" / "rune").mkdir(parents=True)
(tmp_path / "src" / "rune" / "memory_selector.py").write_text("x = 1\n")
monkeypatch.setattr(spine, "TESTS_DIR", tmp_path / "tests")
(tmp_path / "tests").mkdir(exist_ok=True)
monkeypatch.setattr(spine, "UMBRELLA_MANIFEST", tmp_path / "no-manifest.json")
self._seeded_store(tmp_path)
doc = spine.load()
spine.alias(doc, "module:memory_selector", "module:mem_sel_old")
spine.save(doc)
# THE REAL PATH: spine.build() (loads the persisted store, carries
# everything forward, re-derives module facts, reconciles, saves)
rebuilt = spine.build()
spine.save(rebuilt)
reloaded = spine.load()
old_ref = spine.resolve_uri("spine://module/mem_sel_old")
e = spine.entity(reloaded, old_ref)
assert e is not None, "alias lost by real build"
assert e["id"] == "module:memory_selector"
assert "module:mem_sel_old" in e.get("aliases", [])
def test_supersession_survives_real_build_refresh(self, tmp_path, monkeypatch):
"""B supersedes A → REAL build() → superseded_by intact, historical
entity not silently deleted, old reference resolves meaningfully."""
monkeypatch.setattr(spine, "STORE_DIR", tmp_path / "store")
monkeypatch.setattr(spine, "CURRENT", tmp_path / "store" / "current.json")
monkeypatch.setattr(spine, "SNAPSHOTS", tmp_path / "store" / "snaps")
monkeypatch.setattr(spine, "_deployed_observation", lambda: None)
monkeypatch.setattr(spine, "SRC_DIR", tmp_path / "src" / "rune")
(tmp_path / "src" / "rune").mkdir(parents=True)
(tmp_path / "src" / "rune" / "memory_selector.py").write_text("x = 1\n")
monkeypatch.setattr(spine, "TESTS_DIR", tmp_path / "tests")
(tmp_path / "tests").mkdir(exist_ok=True)
monkeypatch.setattr(spine, "UMBRELLA_MANIFEST", tmp_path / "no-manifest.json")
self._seeded_store(tmp_path)
doc = spine.load()
spine.add_entity(doc, id="module:memory_orchestrator", kind="module",
maturity="DECLARED")
spine.supersede(doc, "module:memory_orchestrator", "module:memory_selector",
"evidence:closure-decision")
spine.save(doc)
rebuilt = spine.build()
spine.save(rebuilt)
reloaded = spine.load()
old = spine.entity(reloaded, "module:memory_selector")
assert old is not None, "superseded entity deleted by real build"
assert old["superseded_by"] == "module:memory_orchestrator"
assert old["status"] == "SUPERSEDED"
# the supersedes edge survives as data (non-obligation context)
assert spine.relationships(reloaded, type="supersedes",
from_id="module:memory_orchestrator",
to_id="module:memory_selector")
# ── CLOSURE 2+3: no competing structural authority for RUNE ──────────────
class TestClosureNoCompetingAuthority:
def test_dependency_tracker_scope_is_harness_not_rune(self):
"""dependency_tracker's graph describes ~/.config/opencode/scripts
(the harness), NOT Rune modules. The name collisions with src/rune
are coincidental. This test pins the separation so the harness map
can never silently become a second Rune structural authority.
CI-safe: asserts the CLASSIFICATION (resolution root is the harness
dir; the map is not derived from Spine), never harness filesystem
state — the CI runner has no ~/.config/opencode."""
import inspect
from rune import dependency_tracker as dt
assert "opencode" in dt.SCRIPTS_DIR, "tracker scope moved — re-classify"
# resolution root is the harness dir, by construction: propagate()
# checks os.path.exists against SCRIPTS_DIR, not src/rune
prop_src = inspect.getsource(dt.propagate)
assert "SCRIPTS_DIR" in prop_src and "os.path.join(SCRIPTS_DIR" in prop_src
# the map is a locally-owned table, NOT derived from the Spine store
# (check IMPORTS, not prose — the module docstring legitimately
# mentions Spine when documenting its scope classification)
import ast as _ast
tree = _ast.parse(inspect.getsource(dt))
imported = set()
for node in _ast.walk(tree):
if isinstance(node, _ast.Import):
imported.update(a.name for a in node.names)
elif isinstance(node, _ast.ImportFrom) and node.module:
imported.add(node.module)
assert not any("spine" in m for m in imported), \
f"tracker now imports Spine — re-classify as a consumer: {imported}"
# and Spine remains the authority for Rune structure
assert (spine.SRC_DIR / "spine.py").is_file()
+28 -1
View File
@@ -6,6 +6,7 @@ Observed live: an operator-requested model keeps routing_mode='local' (line
on the model's provider prefix."""
import importlib.util
import sys
import tempfile
from pathlib import Path
from unittest import mock
@@ -14,7 +15,6 @@ sys.path.insert(0, str(ROOT / "src"))
from rune import rune_operator # noqa: E402
from rune.openai_compat_transport import OpenAICompatTransport # noqa: E402
from rune.openai_compat_transport import OICompatTransport # noqa: E402
from rune.local_tool_runtime import OllamaChatTransport # noqa: E402
@@ -67,3 +67,30 @@ def test_bare_model_name_keeps_ollama_transport():
op = rune_operator.RuneOperator(storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
captured = _send(op, "inspect the log for errors", "qwen2.5:3b")
assert isinstance(captured["transport"], OllamaChatTransport)
class TestResumeTransportSelection:
"""Approval-resume path must use the SAME provider-prefix selection as
the direct turn. Observed live (PL1 Task A, 2026-09-12): after an
approval gate, the resumed turn's follow-up model call went to
OllamaChatTransport with the cloud-pinned name → localhost 400
'invalid model name' — the direct path had been fixed in 0.66.17 but
_runtime_for_model (resume) still hardcoded Ollama."""
def test_resume_path_uses_cloud_transport_for_pinned_model(self):
import rune.openai_compat_transport as _oct
op = rune_operator.RuneOperator(
storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
with mock.patch.object(
_oct, "cloud_transport_for",
side_effect=lambda model, timeout=120: OpenAICompatTransport(
"https://mock/v1", "test-key", model.split("/", 1)[0]),
):
rt = op._runtime_for_model("Verboo Code/glm-5.3-flash", "fix the bug")
assert isinstance(rt.transport, OpenAICompatTransport), type(rt.transport)
def test_resume_path_keeps_ollama_for_local_model(self):
op = rune_operator.RuneOperator(
storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
rt = op._runtime_for_model("qwen2.5:3b", "inspect the log")
assert isinstance(rt.transport, OllamaChatTransport), type(rt.transport)