Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d871abc9c | ||
|
|
ce40acbb1f | ||
|
|
1de28b55a0 | ||
|
|
75d6d7d62a | ||
|
|
7f47969128 | ||
|
|
a65e5d44de | ||
|
|
26a040892d | ||
|
|
1497c85cd4 | ||
|
|
a2463ad80b | ||
|
|
6965fc7375 | ||
|
|
0daad967c1 | ||
|
|
502317d8b6 | ||
|
|
07cdc2fc56 | ||
|
|
b4161a4034 | ||
|
|
e036f5f545 | ||
|
|
ab6c4f4e38 | ||
|
|
c520b024d1 | ||
|
|
1c73d809b9 | ||
|
|
34fc38d4ed | ||
|
|
a67a9c8520 | ||
|
|
b0efaffca1 | ||
|
|
a087a0b5e7 | ||
|
|
acb3ce0157 | ||
|
|
58af0b0f78 | ||
|
|
95f9e15ed9 | ||
|
|
b083b0c4cf | ||
|
|
46e74110ad | ||
|
|
c669e37a3d | ||
|
|
7f4a6262ae | ||
|
|
34d5ab74b7 | ||
|
|
7b268e17fe |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.66.14**
|
||||
**Current release: v0.66.24**
|
||||
|
||||
**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
-220
@@ -1,223 +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",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -41,8 +41,8 @@
|
||||
"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,
|
||||
@@ -54,14 +54,14 @@
|
||||
"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,
|
||||
"title": "ADAPT-1 B2 (STANDARD-LIKE): wiring-status/architecture-truth report generator (#158)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "SUPERSEDED",
|
||||
"labels": [
|
||||
"infrastructure",
|
||||
"perception"
|
||||
@@ -74,14 +74,14 @@
|
||||
"number": 301,
|
||||
"title": "FINAL-12: Cross-track benchmark + adaptive provisional ratification",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "NEEDS_REVALIDATION",
|
||||
"reconciliation_state": "DEFERRED_FUTURE",
|
||||
"labels": [
|
||||
"growth",
|
||||
"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,
|
||||
@@ -1150,5 +1150,7 @@
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"last_reconciled": "2026-09-12",
|
||||
"reconciled_by": "PL1 held-out era reconciliation (fresh session 2026-09-12)"
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
def broken(:
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.66.14"
|
||||
version = "0.66.24"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
Whitespace-only changes.
Executable
+205
@@ -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())
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.66.14"
|
||||
__version__ = "0.66.24"
|
||||
|
||||
# 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
|
||||
|
||||
@@ -231,7 +231,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.66.14"' in rune_version,
|
||||
'__version__ = "0.66.24"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
"""OpenAI-compatible chat transport (T15 PL1 held-out substrate fix).
|
||||
|
||||
The operator's execution substrate had exactly one transport —
|
||||
OllamaChatTransport (localhost:11434) — so send_task(model=<cloud>) posted a
|
||||
cloud model name to Ollama's /api/chat and died with HTTP 400 (observed live
|
||||
2026-09-12: 'Verboo Code/glm-5.3-flash' → 400 in 0.2s). The supported
|
||||
model= override could not execute ANY cloud provider.
|
||||
|
||||
This transport speaks the OpenAI wire format (/chat/completions) and returns
|
||||
Ollama-SHAPED responses, so LocalToolRuntime's loop is unchanged. Selection
|
||||
is deterministic (routing_mode == 'cloud') — no router, ladder, or escalation
|
||||
logic is touched.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import time
|
||||
import urllib.request
|
||||
|
||||
|
||||
class OpenAICompatTransport:
|
||||
"""ChatTransport for OpenAI-compatible providers (Verboo Code, Xioami...)."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
provider_name: str,
|
||||
timeout: float = 120,
|
||||
context_window: int = 131072,
|
||||
):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.api_key = api_key
|
||||
self.provider = provider_name
|
||||
# Cloud round trips with a full tool schema routinely exceed the local
|
||||
# tier timeout (30s): observed live, glm-5.3-flash read-timeout at
|
||||
# 30.6s mid-iteration. Clamp the WIRE timeout to >=120s — the
|
||||
# escalation ladder still governs retries/tiers; this is one round
|
||||
# trip's rope, not policy.
|
||||
self.timeout = max(timeout, 120)
|
||||
self._context_window = context_window
|
||||
# Usage accumulators — same contract as OllamaChatTransport (I2).
|
||||
self.usage_calls = 0
|
||||
self.usage_prompt_tokens = 0
|
||||
self.usage_output_tokens = 0
|
||||
self.usage_total_duration_ns = 0
|
||||
|
||||
# -- wire -----------------------------------------------------------
|
||||
|
||||
def _post(self, path: str, payload: dict) -> dict:
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}{path}",
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
},
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=self.timeout) as response:
|
||||
decoded = json.loads(response.read())
|
||||
if not isinstance(decoded, dict):
|
||||
raise ValueError("provider returned a non-object response")
|
||||
return decoded
|
||||
|
||||
# -- message translation --------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _to_openai_messages(messages: list[dict]) -> list[dict]:
|
||||
"""Ollama-shape → OpenAI-shape. Pairing: the runtime's _tool_message
|
||||
sets tool_call_id from request.call_id (minted by
|
||||
normalize_ollama_tool_calls from the response's own id). The assistant
|
||||
message must use THE SAME ids for its tool_calls — a mismatch makes
|
||||
the provider treat the tool result as orphaned and the model re-issues
|
||||
the identical call forever (observed live: glm-5.3-flash looped the
|
||||
same edit_file twice, empty terminal text both turns)."""
|
||||
out: list[dict] = []
|
||||
i = 0
|
||||
while i < len(messages):
|
||||
m = messages[i]
|
||||
role = m.get("role")
|
||||
tool_calls = m.get("tool_calls") or []
|
||||
if role == "assistant" and tool_calls:
|
||||
# Look ahead: pair each tool_call with the tool messages that
|
||||
# immediately follow, reusing their tool_call_ids.
|
||||
paired_ids = [messages[j].get("tool_call_id")
|
||||
for j in range(i + 1, len(messages))
|
||||
if messages[j].get("role") == "tool"][:len(tool_calls)]
|
||||
tc = []
|
||||
for k, c in enumerate(tool_calls):
|
||||
fn = c.get("function", {})
|
||||
args = fn.get("arguments", {})
|
||||
args_str = json.dumps(args) if isinstance(args, dict) else str(args)
|
||||
call_id = paired_ids[k] if k < len(paired_ids) else f"call_{i}_{k}"
|
||||
tc.append({"id": call_id or f"call_{i}_{k}", "type": "function",
|
||||
"function": {"name": fn.get("name"), "arguments": args_str}})
|
||||
out.append({"role": "assistant", "content": m.get("content") or None, "tool_calls": tc})
|
||||
i += 1
|
||||
continue
|
||||
if role == "tool":
|
||||
out.append({"role": "tool",
|
||||
"tool_call_id": m.get("tool_call_id") or f"call_{i}",
|
||||
"content": m.get("content", "")})
|
||||
i += 1
|
||||
continue
|
||||
out.append({"role": role, "content": m.get("content", "")})
|
||||
i += 1
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _from_openai_response(resp: dict) -> dict:
|
||||
"""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 = []
|
||||
for c in message.get("tool_calls") or []:
|
||||
fn = c.get("function", {}) or {}
|
||||
args = fn.get("arguments", {})
|
||||
if isinstance(args, str):
|
||||
try:
|
||||
args = json.loads(args)
|
||||
except json.JSONDecodeError:
|
||||
args = {"_raw": args}
|
||||
entry = {"function": {"name": fn.get("name"), "arguments": args or {}}}
|
||||
if c.get("id"):
|
||||
entry["id"] = c["id"]
|
||||
tool_calls.append(entry)
|
||||
out: dict = {"role": "assistant", "content": message.get("content") or ""}
|
||||
if tool_calls:
|
||||
out["tool_calls"] = tool_calls
|
||||
return out
|
||||
|
||||
# -- ChatTransport protocol -------------------------------------------
|
||||
|
||||
def chat(self, *, model: str, messages: list[dict], tools: list[dict] | None,
|
||||
options: dict | None = None, think: bool | str | None = None) -> dict:
|
||||
# The runtime passes the qualified name ('<Provider>/<model>'); the
|
||||
# provider API wants the bare model id ('glm-5.3-flash'). Observed
|
||||
# live: qualified name → 400 on every call (PL1 held-out attempt).
|
||||
wire_model = model.split("/", 1)[-1] if "/" in model else model
|
||||
payload: dict = {
|
||||
"model": wire_model,
|
||||
"messages": self._to_openai_messages(messages),
|
||||
"stream": False,
|
||||
}
|
||||
if tools:
|
||||
payload["tools"] = tools # Ollama tool schema == OpenAI tool schema
|
||||
if options:
|
||||
for k in ("temperature", "top_p", "max_tokens"):
|
||||
if k in options:
|
||||
payload[k] = options[k]
|
||||
started = time.monotonic()
|
||||
resp = self._post("/chat/completions", payload)
|
||||
elapsed_ns = int((time.monotonic() - started) * 1e9)
|
||||
usage = resp.get("usage", {}) or {}
|
||||
self.usage_calls += 1
|
||||
self.usage_prompt_tokens += int(usage.get("prompt_tokens") or 0)
|
||||
self.usage_output_tokens += int(usage.get("completion_tokens") or 0)
|
||||
self.usage_total_duration_ns += elapsed_ns
|
||||
return self._from_openai_response(resp)
|
||||
|
||||
def supports_tools(self, model: str) -> bool:
|
||||
return True
|
||||
|
||||
def supports_thinking(self, model: str) -> bool:
|
||||
return False
|
||||
|
||||
def context_length(self, model: str) -> int:
|
||||
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)."""
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
if "/" not in model:
|
||||
raise ValueError(f"cloud model must be '<Provider>/<model>': {model!r}")
|
||||
provider_name, short_model = model.split("/", 1)
|
||||
base_url = api_key = None
|
||||
context_window = 131072
|
||||
cfg_path = Path(os.path.expanduser("~/.config/mimocode/mimocode.jsonc"))
|
||||
if cfg_path.is_file():
|
||||
text = cfg_path.read_text()
|
||||
m = re.search(rf'"{re.escape(provider_name)}"\s*:\s*\{{', text)
|
||||
if m:
|
||||
block = text[m.start():m.start() + 2000]
|
||||
bm = re.search(r'"baseURL":\s*"([^"]+)"', block)
|
||||
if bm:
|
||||
base_url = bm.group(1)
|
||||
wm = re.search(rf'"{re.escape(short_model)}"\s*:\s*\{{[^}}]*?"context_window"\s*:\s*(\d+)', block)
|
||||
if wm:
|
||||
context_window = int(wm.group(1))
|
||||
if not base_url:
|
||||
raise ValueError(f"no baseURL configured for provider {provider_name!r}")
|
||||
key = os.environ.get(f"{provider_name.upper().replace(' ', '_')}_API_KEY", "")
|
||||
if not key:
|
||||
auth_path = Path(os.path.expanduser("~/.local/share/mimocode/auth.json"))
|
||||
if auth_path.is_file():
|
||||
auth = json.loads(auth_path.read_text())
|
||||
key = (auth.get(provider_name) or {}).get("key", "")
|
||||
if not key:
|
||||
raise ValueError(f"no API key for provider {provider_name!r}")
|
||||
# Cloud routers queue and batch — a tool-loop turn can legitimately exceed
|
||||
# the local-model timeout (observed live: glm-5.3-flash read-timeout at
|
||||
# 120s twice). Floor cloud transports at 300s; local keeps its own.
|
||||
return OpenAICompatTransport(
|
||||
base_url, key, provider_name, timeout=max(timeout, 300),
|
||||
context_window=context_window,
|
||||
)
|
||||
@@ -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,
|
||||
@@ -1953,8 +1969,23 @@ I have tools to read, write, edit, search, lint, test, format, and commit code.
|
||||
if hasattr(capsule, "capture_messages"):
|
||||
capsule.capture_messages(messages)
|
||||
|
||||
# Transport selection is deterministic substrate (T15 PL1
|
||||
# substrate fix): the pinned provider override
|
||||
# (send_task(model='<Provider>/<model>')) requires an
|
||||
# OpenAI-compatible wire for cloud models — OllamaChatTransport
|
||||
# is localhost-only and 400s on any cloud model name. Router,
|
||||
# ladder, and escalation logic are untouched.
|
||||
# Provider-keyed selection (NOT routing_mode): an
|
||||
# operator-requested model keeps routing_mode="local" (line
|
||||
# ~887) even for cloud providers, so the selection must key on
|
||||
# the model's provider prefix. Observed live: pinned
|
||||
# 'Verboo Code/glm-5.3-flash' still hit Ollama under the
|
||||
# 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.
|
||||
transport = self._transport_for(model, timeout=timeout)
|
||||
runtime = LocalToolRuntime(
|
||||
transport=OllamaChatTransport(timeout=timeout),
|
||||
transport=transport,
|
||||
executor=tool_executor.execute_tool,
|
||||
model=model,
|
||||
tools=capability_selection["tools"],
|
||||
|
||||
@@ -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.14
|
||||
Version: v0.66.24
|
||||
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.14 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.66.24 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -0,0 +1,608 @@
|
||||
"""spine.py — Rune Spine V1: evolvable structural self-model.
|
||||
|
||||
Spine answers, deterministically and without an LLM:
|
||||
what exists, how it relates, what is wired/live, what version is
|
||||
deployed, what evidence supports each claim, what drifted, what changed.
|
||||
|
||||
PRIME DIRECTIVE: model Rune's architecture without freezing it. Entity
|
||||
kinds, relationship types and metadata are OPEN TAGS — a previously unseen
|
||||
capability enters as data, never as a core edit. Unknown fields survive
|
||||
load/projection/serialize/reload untouched.
|
||||
|
||||
AUTHORITY MODEL: Spine is THE structural authority (subsystems, membership,
|
||||
dependencies, consumers, mirrors). Specialised authorities stay authoritative
|
||||
for their own domains and are OBSERVED, not copied: deployment identity
|
||||
(_build_identity.json), canary registry (canary_native.MODULES), provider
|
||||
config, CI outputs. Adding a domain = adding an adapter/projector.
|
||||
|
||||
MATURITY LADDER (evidence-gated, no evidence = no promotion):
|
||||
DECLARED < PRESENT < WIRED < REACHABLE < EXERCISED < LIVE_PROVEN
|
||||
DECLARED named in a registry/manifest
|
||||
PRESENT source artifact exists
|
||||
WIRED structural consumers/imports exist (or registry wiring)
|
||||
REACHABLE reachable from the composition root / deployed tree contains it
|
||||
EXERCISED test or canary exercises it
|
||||
LIVE_PROVEN runtime observation on the deployed identity proves it live
|
||||
|
||||
SNAPSHOTS: ingest is a pure function of authorities (sorted output), so
|
||||
snapshots are byte-deterministic; diff(a, b) answers "what changed" without
|
||||
interpretation.
|
||||
|
||||
Schema version: 1. Unknown metadata survives by construction; a future
|
||||
reader migrates by adding a projector, never by editing old records.
|
||||
|
||||
CLI: PYTHONPATH=src python3 -m rune.spine <command> (JSON canonical).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
MATURITY_ORDER = ["DECLARED", "PRESENT", "WIRED", "REACHABLE", "EXERCISED", "LIVE_PROVEN"]
|
||||
_MATURITY_RANK = {name: i for i, name in enumerate(MATURITY_ORDER)}
|
||||
|
||||
GENERATED_VIEW_MARKER = "_generated_by_spine"
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2] # repo root (src/rune/spine.py → up 2)
|
||||
SRC_DIR = ROOT / "src" / "rune"
|
||||
TESTS_DIR = ROOT / "tests"
|
||||
STORE_DIR = ROOT / "store" / "spine"
|
||||
CURRENT = STORE_DIR / "current.json"
|
||||
SNAPSHOTS = STORE_DIR / "snapshots"
|
||||
# Deployment observation point (this machine's promoted build). Configurable:
|
||||
# never an identity, only an observation surface.
|
||||
DEPLOYED_LINK = Path(os.environ.get("RUNE_DEPLOYED_LINK", "/mnt/workdrive/rune-code/current"))
|
||||
UMBRELLA_MANIFEST = ROOT / "config" / "umbrellas.json"
|
||||
|
||||
|
||||
def now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def _sorted(obj):
|
||||
"""Deterministic normalization: sort everything sortable, recursively."""
|
||||
if isinstance(obj, dict):
|
||||
return {k: _sorted(obj[k]) for k in sorted(obj)}
|
||||
if isinstance(obj, list):
|
||||
return sorted((_sorted(v) for v in obj), key=lambda v: json.dumps(v, sort_keys=True))
|
||||
return obj
|
||||
|
||||
|
||||
# ── store ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def new_doc() -> dict:
|
||||
return {"schema_version": SCHEMA_VERSION, "generated_at": now(),
|
||||
"entities": [], "relationships": [], "observations": []}
|
||||
|
||||
|
||||
def load(path: Path | None = None) -> dict | None:
|
||||
p = path or CURRENT
|
||||
if not p.is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text())
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def save(doc: dict, path: Path | None = None) -> Path:
|
||||
incoming = doc.get("schema_version", SCHEMA_VERSION)
|
||||
if incoming != SCHEMA_VERSION:
|
||||
raise ValueError(f"schema_version {incoming} != {SCHEMA_VERSION}")
|
||||
doc["schema_version"] = SCHEMA_VERSION
|
||||
p = path or CURRENT
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_text(json.dumps(_sorted(doc), indent=1, sort_keys=True) + "\n")
|
||||
return p
|
||||
|
||||
|
||||
def add_entity(doc: dict, *, id: str, kind: str, tags=None, identities=None,
|
||||
evidence_refs=None, maturity="DECLARED", ext=None) -> dict:
|
||||
"""Register an entity. kind/tags/ext are open — unknown values are data.
|
||||
|
||||
Refuses to downgrade an existing entity's maturity: promotion requires
|
||||
evidence (via observe/promote); re-registration with a lower claimed
|
||||
maturity keeps the higher evidenced state."""
|
||||
for e in doc["entities"]:
|
||||
if e["id"] == id:
|
||||
if maturity and _MATURITY_RANK.get(maturity, 0) > _MATURITY_RANK.get(e.get("maturity", "DECLARED"), 0):
|
||||
e["maturity"] = maturity # only upgrades here; promotion gate below
|
||||
if tags:
|
||||
e["tags"] = sorted(set(e.get("tags", [])) | set(tags))
|
||||
if identities:
|
||||
e.setdefault("identities", {}).update(identities)
|
||||
if evidence_refs:
|
||||
e["evidence_refs"] = sorted(set(e.get("evidence_refs", [])) | set(evidence_refs))
|
||||
if ext:
|
||||
e.setdefault("ext", {}).update(ext)
|
||||
e["observed_at"] = now()
|
||||
return e
|
||||
ent = {"id": id, "kind": kind, "maturity": maturity,
|
||||
"tags": sorted(tags or []), "identities": dict(identities or {}),
|
||||
"evidence_refs": sorted(evidence_refs or []),
|
||||
"observed_at": now()}
|
||||
if ext:
|
||||
ent["ext"] = dict(ext)
|
||||
doc["entities"].append(ent)
|
||||
return ent
|
||||
|
||||
|
||||
def add_relationship(doc: dict, *, type: str, from_id: str, to_id: str,
|
||||
evidence_refs=None, ext=None) -> dict:
|
||||
rel = {"type": type, "from": from_id, "to": to_id,
|
||||
"evidence_refs": sorted(evidence_refs or []), "observed_at": now()}
|
||||
if ext:
|
||||
rel["ext"] = dict(ext)
|
||||
if not any(r["type"] == type and r["from"] == from_id and r["to"] == to_id
|
||||
for r in doc["relationships"]):
|
||||
doc["relationships"].append(rel)
|
||||
return rel
|
||||
|
||||
|
||||
def entity(doc: dict, id: str) -> dict | None:
|
||||
return next((e for e in doc["entities"] if e["id"] == id), None)
|
||||
|
||||
|
||||
def relationships(doc: dict, *, from_id=None, to_id=None, type=None) -> list[dict]:
|
||||
out = []
|
||||
for r in doc["relationships"]:
|
||||
if from_id and r["from"] != from_id:
|
||||
continue
|
||||
if to_id and r["to"] != to_id:
|
||||
continue
|
||||
if type and r["type"] != type:
|
||||
continue
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
|
||||
def promote(doc: dict, id: str, state: str, evidence_ref: str) -> dict | None:
|
||||
"""Evidence-gated maturity promotion. No evidence ref = no promotion.
|
||||
Demotion is never silent: it records an observation, it does not rewrite."""
|
||||
e = entity(doc, id)
|
||||
if e is None:
|
||||
return None
|
||||
if not evidence_ref:
|
||||
raise ValueError(f"promotion of {id} to {state} requires evidence")
|
||||
if state not in _MATURITY_RANK:
|
||||
raise ValueError(f"unknown maturity state: {state}")
|
||||
current = _MATURITY_RANK.get(e.get("maturity", "DECLARED"), 0)
|
||||
if _MATURITY_RANK[state] < current:
|
||||
doc["observations"].append({
|
||||
"kind": "maturity_demotion_rejected", "entity": id,
|
||||
"requested": state, "current": e["maturity"], "observed_at": now()})
|
||||
return e
|
||||
e["maturity"] = state
|
||||
e["evidence_refs"] = sorted(set(e.get("evidence_refs", [])) | {evidence_ref})
|
||||
e["observed_at"] = now()
|
||||
return e
|
||||
|
||||
|
||||
# ── static projections (deterministic, from authorities) ─────────────────
|
||||
|
||||
def _module_imports(module: str) -> set[str]:
|
||||
path = SRC_DIR / f"{module}.py"
|
||||
try:
|
||||
tree = ast.parse(path.read_text())
|
||||
except (OSError, SyntaxError):
|
||||
return set()
|
||||
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_DIR.glob("*.py")}
|
||||
|
||||
|
||||
def _canary_registry() -> list[str]:
|
||||
try:
|
||||
if str(SRC_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(SRC_DIR))
|
||||
from rune import canary_native
|
||||
return list(canary_native.MODULES)
|
||||
except ImportError:
|
||||
# canary_native pulls optional deps (sqlite_vec etc.); degrade to an
|
||||
# empty registry — drift will note absent registrations honestly.
|
||||
return []
|
||||
|
||||
|
||||
def _deployed_observation() -> dict | None:
|
||||
"""Observe the deployment authority — never invent it."""
|
||||
ident = DEPLOYED_LINK / "src" / "rune" / "_build_identity.json"
|
||||
if not ident.is_file():
|
||||
return None
|
||||
try:
|
||||
data = json.loads(ident.read_text())
|
||||
return {"deployed_sha": data.get("commit", "")[:12],
|
||||
"deployed_version": data.get("version", ""),
|
||||
"deployed_at": data.get("deployed_at", ""),
|
||||
"observed_from": str(DEPLOYED_LINK)}
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def _test_backed_modules() -> set[str]:
|
||||
out = set()
|
||||
for p in TESTS_DIR.glob("test_*.py"):
|
||||
out.add(p.stem[5:])
|
||||
return out
|
||||
|
||||
|
||||
def ingest_umbrellas(doc: dict, manifest_path: Path | None = None) -> int:
|
||||
"""Phase A: translate the (last manually maintained) umbrella manifest
|
||||
into Spine entities + relationships. Refuses a Spine-GENERATED view —
|
||||
that guards the ingest cycle after Phase C makes the view output-only.
|
||||
Returns count of subsystems ingested."""
|
||||
path = manifest_path or UMBRELLA_MANIFEST
|
||||
text = path.read_text()
|
||||
data = json.loads(text)
|
||||
if GENERATED_VIEW_MARKER in data:
|
||||
raise ValueError(
|
||||
f"{path} is a Spine-generated compatibility view; refusing to "
|
||||
"ingest it (Phase A reads the manual authority exactly once). "
|
||||
"Structural membership now lives in the Spine store.")
|
||||
count = 0
|
||||
KNOWN_FIELDS = {"description", "modules", "depends_on", "mirrors", "canaries"}
|
||||
for name, u in data.get("umbrellas", {}).items():
|
||||
sid = f"subsystem:{name}"
|
||||
# canaries ride as ext data (obligation metadata, not structure);
|
||||
# unknown future manifest fields ride too — open-world preservation
|
||||
ext = {k: u[k] for k in u if k in ("canaries",) or k not in KNOWN_FIELDS}
|
||||
add_entity(doc, id=sid, kind="subsystem",
|
||||
identities={"description": u.get("description", "")},
|
||||
evidence_refs=[f"umbrella-manifest#{name}"],
|
||||
maturity="DECLARED", ext=ext or None)
|
||||
count += 1
|
||||
for dep in u.get("depends_on", []):
|
||||
add_relationship(doc, type="depends_on", from_id=sid,
|
||||
to_id=f"subsystem:{dep}",
|
||||
evidence_refs=[f"umbrella-manifest#{name}"])
|
||||
for mirror in u.get("mirrors", []):
|
||||
add_relationship(doc, type="mirrors", from_id=sid, to_id=f"file:{mirror}",
|
||||
evidence_refs=[f"umbrella-manifest#{name}"])
|
||||
for canary in u.get("canaries", []):
|
||||
add_relationship(doc, type="obligated_canary", from_id=sid,
|
||||
to_id=f"module:{canary}",
|
||||
evidence_refs=[f"umbrella-manifest#{name}"])
|
||||
for mod in u.get("modules", []):
|
||||
mid = f"module:{mod}"
|
||||
add_entity(doc, id=mid, kind="module", tags=["code"],
|
||||
identities={"source": f"src/rune/{mod}.py"},
|
||||
evidence_refs=[f"umbrella-manifest#{name}"],
|
||||
maturity="DECLARED")
|
||||
add_relationship(doc, type="belongs_to", from_id=mid, to_id=sid,
|
||||
evidence_refs=[f"umbrella-manifest#{name}"])
|
||||
return count
|
||||
|
||||
|
||||
def build(manifest_path: Path | None = None) -> dict:
|
||||
"""Deterministic full projection from authorities. Same inputs →
|
||||
byte-identical doc (modulo generated_at)."""
|
||||
doc = new_doc()
|
||||
|
||||
# 1. subsystem structure — from the Spine store if it already owns
|
||||
# membership (Phase B+), else from the manual manifest (Phase A).
|
||||
existing = load()
|
||||
seeded = False
|
||||
if existing:
|
||||
rels = [r for r in existing["relationships"] if r["type"] == "belongs_to"]
|
||||
if rels:
|
||||
seeded = True
|
||||
doc["entities"].extend(json.loads(json.dumps(
|
||||
[e for e in existing["entities"]
|
||||
if e["kind"] in ("subsystem", "module")])))
|
||||
doc["relationships"].extend(json.loads(json.dumps(
|
||||
[r for r in existing["relationships"]
|
||||
if r["type"] in ("belongs_to", "depends_on", "mirrors")
|
||||
and (r["from"].startswith(("subsystem:",)) or r["type"] == "belongs_to")])))
|
||||
if not seeded:
|
||||
ingest_umbrellas(doc, manifest_path)
|
||||
|
||||
# 2. module-level structural facts from actual code
|
||||
graph = _import_graph()
|
||||
registry = _canary_registry()
|
||||
tested = _test_backed_modules()
|
||||
mods = {e["id"].split(":", 1)[1] for e in doc["entities"] if e["kind"] == "module"}
|
||||
for m in sorted(mods):
|
||||
mid = f"module:{m}"
|
||||
e = entity(doc, mid)
|
||||
present = (SRC_DIR / f"{m}.py").is_file()
|
||||
if present:
|
||||
e["identities"]["source"] = f"src/rune/{m}.py"
|
||||
_ = promote(doc, mid, "PRESENT", f"source:{m}.py")
|
||||
consumers = sorted(c for c, deps in graph.items() if m in deps and c != m)
|
||||
deps = sorted(d for d in graph.get(m, set()) if d in mods and d != m)
|
||||
for c in consumers:
|
||||
add_relationship(doc, type="consumed_by", from_id=mid,
|
||||
to_id=f"module:{c}", evidence_refs=[f"ast-import:{m}"])
|
||||
for d in deps:
|
||||
add_relationship(doc, type="depends_on", from_id=mid,
|
||||
to_id=f"module:{d}", evidence_refs=[f"ast-import:{m}"])
|
||||
# WIRED: importers exist, or registry wiring (canary registry)
|
||||
if consumers or m in registry:
|
||||
_ = promote(doc, mid, "WIRED", f"ast-imports:{len(consumers)}" if consumers
|
||||
else "canary-registry")
|
||||
# EXERCISED: dedicated test or canary exercise
|
||||
if m in tested:
|
||||
_ = promote(doc, mid, "EXERCISED", f"tests/test_{m}.py")
|
||||
elif m in registry:
|
||||
_ = promote(doc, mid, "EXERCISED", "canary-registry-exercise")
|
||||
|
||||
# 3. runtime/deployment observation (dynamic, bounded)
|
||||
obs = _deployed_observation()
|
||||
if obs:
|
||||
dep = add_entity(doc, id="deployment:current", kind="deployment_surface",
|
||||
identities=obs, evidence_refs=["_build_identity.json"],
|
||||
maturity="LIVE_PROVEN")
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] == "module" and (DEPLOYED_LINK / e["identities"].get("source", "")).is_file():
|
||||
add_relationship(doc, type="deployed_as", from_id=e["id"],
|
||||
to_id="deployment:current",
|
||||
evidence_refs=[f"deployed-tree:{obs['deployed_sha']}"])
|
||||
doc["observations"].append({"kind": "deployed_identity", **obs, "observed_at": now()})
|
||||
|
||||
doc["generated_at"] = now()
|
||||
return doc
|
||||
|
||||
|
||||
# ── drift ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def drift(doc: dict) -> list[dict]:
|
||||
"""Architectural drift classes. Reuses authorities' verdicts; never
|
||||
duplicates their logic. Deterministic output."""
|
||||
out = []
|
||||
graph = _import_graph()
|
||||
registry = _canary_registry()
|
||||
tested = _test_backed_modules()
|
||||
mods = {e["id"].split(":", 1)[1] for e in doc["entities"] if e["kind"] == "module"}
|
||||
|
||||
# 1. registered but absent (canary registry names a non-existent module)
|
||||
for m in registry:
|
||||
if m not in mods:
|
||||
out.append({"class": "registered_but_absent", "entity": f"module:{m}",
|
||||
"detail": "in canary registry, no source file"})
|
||||
|
||||
# 2. present but unowned (runtime source file outside every subsystem).
|
||||
# Excludes canary files (governed by the canary contract, not umbrella
|
||||
# membership) and package plumbing (__init__/__main__).
|
||||
for p in SRC_DIR.glob("*.py"):
|
||||
if p.stem.startswith(("canary_", "__")):
|
||||
continue
|
||||
if p.stem not in mods:
|
||||
out.append({"class": "present_but_unowned", "entity": f"module:{p.stem}",
|
||||
"detail": "src/rune runtime module with no subsystem membership"})
|
||||
|
||||
# 3. registered but no static importers — OBSERVATION, not a death
|
||||
# verdict: registry dispatch and entry points wire dynamically
|
||||
# (task_gate via capability_broker, rune_tui via entry point).
|
||||
# Interpretation belongs to cognition; Spine reports the fact.
|
||||
for m in sorted(mods & set(registry)):
|
||||
importers = [c for c, deps in graph.items() if m in deps and c != m]
|
||||
if not importers:
|
||||
dyn = "no dedicated test either" if m not in tested else "dedicated test exists"
|
||||
out.append({"class": "registered_no_static_importers", "entity": f"module:{m}",
|
||||
"detail": f"zero static importers; dynamic wiring possible; {dyn}"})
|
||||
|
||||
# 4. deployed identity drift (source version vs deployment authority)
|
||||
obs = _deployed_observation()
|
||||
try:
|
||||
src_version = (ROOT / "pyproject.toml").read_text()
|
||||
import re
|
||||
m = re.search(r'^version\s*=\s*"([^"]+)"', src_version, re.M)
|
||||
src_version = m.group(1) if m else ""
|
||||
except OSError:
|
||||
src_version = ""
|
||||
if obs and src_version and obs["deployed_version"] != src_version:
|
||||
out.append({"class": "version_surfaces_disagree",
|
||||
"detail": f"source {src_version} != deployed {obs['deployed_version']}"})
|
||||
|
||||
# 5. relationship disappeared (store claims consumers that no longer import)
|
||||
for r in relationships(doc, type="consumed_by"):
|
||||
m = r["from"].split(":", 1)[1]
|
||||
c = r["to"].split(":", 1)[1]
|
||||
if m not in graph.get(c, set()):
|
||||
out.append({"class": "relationship_disappeared",
|
||||
"detail": f"{c} no longer imports {m} (stale store edge)"})
|
||||
return out
|
||||
|
||||
|
||||
# ── snapshots / deltas ────────────────────────────────────────────────────
|
||||
|
||||
def snapshot(doc: dict, label: str = "") -> Path:
|
||||
SNAPSHOTS.mkdir(parents=True, exist_ok=True)
|
||||
obs = _deployed_observation() or {}
|
||||
name = f"snap-{time.strftime('%Y%m%dT%H%M%SZ', time.gmtime())}-{obs.get('deployed_sha', 'noident')[:8]}"
|
||||
if label:
|
||||
name += f"-{label}"
|
||||
p = SNAPSHOTS / f"{name}.json"
|
||||
save(doc, p)
|
||||
return p
|
||||
|
||||
|
||||
def diff(old: dict, new: dict) -> dict:
|
||||
"""Deterministic structural delta. No interpretation."""
|
||||
oe = {e["id"]: e for e in old.get("entities", [])}
|
||||
ne = {e["id"]: e for e in new.get("entities", [])}
|
||||
ors = {(r["type"], r["from"], r["to"]) for r in old.get("relationships", [])}
|
||||
nrs = {(r["type"], r["from"], r["to"]) for r in new.get("relationships", [])}
|
||||
out = {"entities_added": sorted(set(ne) - set(oe)),
|
||||
"entities_removed": sorted(set(oe) - set(ne)),
|
||||
"relationships_added": sorted({"|".join(t) for t in nrs - ors}),
|
||||
"relationships_removed": sorted({"|".join(t) for t in ors - nrs}),
|
||||
"maturity_changed": [], "identity_changed": [], "drift_new": []}
|
||||
for eid in sorted(set(oe) & set(ne)):
|
||||
if oe[eid].get("maturity") != ne[eid].get("maturity"):
|
||||
out["maturity_changed"].append(
|
||||
{"id": eid, "from": oe[eid].get("maturity"), "to": ne[eid].get("maturity")})
|
||||
if oe[eid].get("identities") != ne[eid].get("identities"):
|
||||
out["identity_changed"].append(eid)
|
||||
return out
|
||||
|
||||
|
||||
# ── compatibility projection (Phase C) ────────────────────────────────────
|
||||
|
||||
def export_umbrellas(doc: dict) -> dict:
|
||||
"""Generate the legacy umbrella view FROM Spine. Output only — the old
|
||||
consumers (CI gate, henxels) read this during migration. Never ingest."""
|
||||
umbrellas: dict = {}
|
||||
for e in doc["entities"]:
|
||||
if e["kind"] != "subsystem":
|
||||
continue
|
||||
name = e["id"].split(":", 1)[1]
|
||||
umbrellas[name] = {"description": e["identities"].get("description", ""),
|
||||
"modules": [], "depends_on": [], "mirrors": []}
|
||||
# faithful reproduction: emit canaries only when the ingested
|
||||
# authority carried them (the legacy contract omits empty lists)
|
||||
if e.get("ext", {}).get("canaries"):
|
||||
umbrellas[name]["canaries"] = sorted(e["ext"]["canaries"])
|
||||
for r in doc["relationships"]:
|
||||
if r["type"] == "belongs_to" and r["from"].startswith("module:"):
|
||||
sub = r["to"].split(":", 1)[1]
|
||||
if sub in umbrellas:
|
||||
umbrellas[sub]["modules"].append(r["from"].split(":", 1)[1])
|
||||
elif r["type"] == "depends_on" and r["from"].startswith("subsystem:"):
|
||||
sub = r["from"].split(":", 1)[1]
|
||||
if sub in umbrellas:
|
||||
umbrellas[sub]["depends_on"].append(r["to"].split(":", 1)[1])
|
||||
elif r["type"] == "mirrors" and r["from"].startswith("subsystem:"):
|
||||
sub = r["from"].split(":", 1)[1]
|
||||
if sub in umbrellas:
|
||||
umbrellas[sub]["mirrors"].append(r["to"].split(":", 1)[1])
|
||||
elif r["type"] == "obligated_canary" and r["from"].startswith("subsystem:"):
|
||||
sub = r["from"].split(":", 1)[1]
|
||||
if sub in umbrellas:
|
||||
umbrellas[sub]["canaries"].append(r["to"].split(":", 1)[1])
|
||||
for u in umbrellas.values():
|
||||
u["modules"].sort(); u["depends_on"].sort(); u["mirrors"].sort()
|
||||
if "canaries" in u:
|
||||
u["canaries"].sort()
|
||||
return {"_comment": "GENERATED by rune.spine — 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_VIEW_MARKER: True,
|
||||
"generated_at": now(),
|
||||
"umbrellas": dict(sorted(umbrellas.items()))}
|
||||
|
||||
|
||||
# ── CLI (deterministic, JSON canonical) ───────────────────────────────────
|
||||
|
||||
def _fresh_doc() -> dict:
|
||||
doc = load()
|
||||
if doc is None:
|
||||
doc = build()
|
||||
save(doc)
|
||||
return doc
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
argv = list(sys.argv[1:] if argv is None else argv)
|
||||
if not argv or argv[0] in ("-h", "--help"):
|
||||
print(__doc__)
|
||||
return 0
|
||||
cmd = argv[0]
|
||||
args = argv[1:]
|
||||
|
||||
if cmd == "ingest": # Phase A: seed from manual manifest (explicit)
|
||||
doc = new_doc()
|
||||
n = ingest_umbrellas(doc, Path(args[0]) if args else None)
|
||||
save(doc)
|
||||
print(json.dumps({"ingested_subsystems": n, "store": str(CURRENT)}))
|
||||
return 0
|
||||
if cmd == "build": # full deterministic projection
|
||||
doc = build()
|
||||
save(doc)
|
||||
print(json.dumps({"entities": len(doc["entities"]),
|
||||
"relationships": len(doc["relationships"]),
|
||||
"observations": len(doc["observations"]),
|
||||
"store": str(CURRENT)}))
|
||||
return 0
|
||||
|
||||
doc = _fresh_doc()
|
||||
|
||||
if cmd == "entity":
|
||||
e = entity(doc, args[0]) if args else None
|
||||
print(json.dumps(e) if e else json.dumps({"error": "not found", "id": args[0] if args else ""}))
|
||||
return 0 if e else 1
|
||||
if cmd == "relationships":
|
||||
out = relationships(doc, from_id=args[0] if args else None,
|
||||
type=args[1] if len(args) > 1 else None)
|
||||
print(json.dumps(out, indent=1))
|
||||
return 0
|
||||
if cmd == "owner":
|
||||
e = entity(doc, args[0]) if args else None
|
||||
if not e:
|
||||
print(json.dumps({"error": "not found"})); return 1
|
||||
subs = [r["to"] for r in relationships(doc, from_id=e["id"], type="belongs_to")]
|
||||
print(json.dumps({"id": e["id"], "subsystem": subs}))
|
||||
return 0
|
||||
if cmd == "dependencies":
|
||||
out = [r["to"] for r in relationships(doc, from_id=args[0], type="depends_on")] if args else []
|
||||
print(json.dumps({"id": args[0] if args else "", "dependencies": sorted(out)}))
|
||||
return 0
|
||||
if cmd == "consumers":
|
||||
# "consumed_by" edges run provider → consumer; consumers of X are
|
||||
# the "to" ends of edges leaving X.
|
||||
out = [r["to"] for r in relationships(doc, from_id=args[0], type="consumed_by")] if args else []
|
||||
print(json.dumps({"id": args[0] if args else "", "consumers": sorted(set(out))}))
|
||||
return 0
|
||||
if cmd == "status":
|
||||
e = entity(doc, args[0]) if args else None
|
||||
if not e:
|
||||
print(json.dumps({"error": "not found"})); return 1
|
||||
print(json.dumps({"id": e["id"], "kind": e["kind"], "maturity": e["maturity"],
|
||||
"evidence_refs": e["evidence_refs"], "observed_at": e["observed_at"]}))
|
||||
return 0
|
||||
if cmd == "drift":
|
||||
print(json.dumps(drift(doc), indent=1))
|
||||
return 0
|
||||
if cmd == "deployed":
|
||||
obs = _deployed_observation()
|
||||
print(json.dumps(obs if obs else {"error": "no deployment observation"}))
|
||||
return 0 if obs else 1
|
||||
if cmd == "snapshot":
|
||||
p = snapshot(doc, args[0] if args else "")
|
||||
print(json.dumps({"snapshot": str(p)}))
|
||||
return 0
|
||||
if cmd == "diff":
|
||||
a, b = load(Path(args[0])), load(Path(args[1]))
|
||||
if not a or not b:
|
||||
print(json.dumps({"error": "snapshot path not found"})); return 1
|
||||
print(json.dumps(diff(a, b), indent=1))
|
||||
return 0
|
||||
if cmd == "export-umbrellas":
|
||||
view = export_umbrellas(doc)
|
||||
target = Path(args[0]) if args else UMBRELLA_MANIFEST
|
||||
if target == UMBRELLA_MANIFEST and target.is_file() and GENERATED_VIEW_MARKER not in json.loads(target.read_text()):
|
||||
print(json.dumps({"error": "refusing to overwrite a MANUAL manifest — "
|
||||
"pass an explicit output path for the generated view"}))
|
||||
return 1
|
||||
target.write_text(json.dumps(view, indent=1) + "\n")
|
||||
print(json.dumps({"written": str(target), "subsystems": len(view["umbrellas"])}))
|
||||
return 0
|
||||
if cmd == "promote":
|
||||
e = promote(doc, args[0], args[1], args[2]) if len(args) > 2 else None
|
||||
save(doc)
|
||||
print(json.dumps(e or {"error": "not found or missing evidence"}))
|
||||
return 0 if e else 1
|
||||
if cmd == "maturity":
|
||||
print(json.dumps(MATURITY_ORDER))
|
||||
return 0
|
||||
|
||||
print(f"unknown command: {cmd}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
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
@@ -385,7 +385,8 @@ def test_thread_safety_concurrent_assemble():
|
||||
|
||||
def test_operator_characterization_parity():
|
||||
"""Existing role-message assertions still hold via the broker path."""
|
||||
operator = rune_operator.RuneOperator("/tmp/context-broker-parity-operator.json")
|
||||
import tempfile
|
||||
operator = rune_operator.RuneOperator(str(Path(tempfile.mkdtemp()) / "parity-operator.json"))
|
||||
runtime = Mock()
|
||||
runtime.run.return_value = OperatorResponse(success=True, user_text="Final answer", model="qwen3:8b")
|
||||
profile = SimpleNamespace(safety_tier=2)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,192 @@
|
||||
"""OpenAI-compatible transport (T15 PL1 substrate fix).
|
||||
|
||||
The operator had exactly one transport (Ollama) — send_task(model=<cloud>)
|
||||
posted cloud model names to Ollama's /api/chat and 400'd. These tests prove
|
||||
the translation layer both directions; the live wire is smoke-tested
|
||||
separately (provider credentials are not test fixtures)."""
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
from unittest import mock
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
from rune.openai_compat_transport import OpenAICompatTransport # noqa: E402
|
||||
|
||||
|
||||
class TestMessageTranslation:
|
||||
def test_ollama_to_openai_roundtrip(self):
|
||||
msgs = [
|
||||
{"role": "system", "content": "sys"},
|
||||
{"role": "user", "content": "task"},
|
||||
{"role": "assistant", "content": "", "tool_calls": [
|
||||
{"function": {"name": "edit_file", "arguments": {"path": "/x.py", "old": "a", "new": "b"}}}]},
|
||||
{"role": "tool", "tool_call_id": "call_2_0", "content": "{\"success\": true}"},
|
||||
]
|
||||
out = OpenAICompatTransport._to_openai_messages(msgs)
|
||||
# assistant tool_calls: dict args → JSON string, ids assigned
|
||||
tc = out[2]["tool_calls"][0]
|
||||
assert tc["function"]["arguments"] == json.dumps({"path": "/x.py", "old": "a", "new": "b"})
|
||||
assert tc["id"] == "call_2_0"
|
||||
# tool result keeps its call id
|
||||
assert out[3]["tool_call_id"] == "call_2_0"
|
||||
# plain roles pass through
|
||||
assert out[0] == {"role": "system", "content": "sys"}
|
||||
|
||||
def test_openai_response_to_ollama_shape(self):
|
||||
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["tool_calls"][0]
|
||||
assert tc["function"]["name"] == "run_tests"
|
||||
assert tc["function"]["arguments"] == {"path": "t"} # string → dict
|
||||
|
||||
def test_malformed_arguments_become_raw(self):
|
||||
resp = {"choices": [{"message": {"tool_calls": [
|
||||
{"function": {"name": "x", "arguments": "not-json"}}]}}]}
|
||||
shape = OpenAICompatTransport._from_openai_response(resp)
|
||||
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["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")
|
||||
assert t.supports_tools("m") is True
|
||||
assert t.supports_thinking("m") is False
|
||||
assert t.context_length("m") == 131072
|
||||
|
||||
def test_usage_accumulators(self):
|
||||
t = OpenAICompatTransport("http://x/v1", "k", "Test")
|
||||
t.chat = lambda **kw: ( # simulate the accumulator path
|
||||
setattr(t, "usage_calls", t.usage_calls + 1),
|
||||
setattr(t, "usage_prompt_tokens", t.usage_prompt_tokens + 10),
|
||||
setattr(t, "usage_output_tokens", t.usage_output_tokens + 5),
|
||||
)[-1]
|
||||
t.chat(model="m", messages=[])
|
||||
assert t.usage_calls == 1 and t.usage_prompt_tokens == 10 and t.usage_output_tokens == 5
|
||||
|
||||
|
||||
def test_qualified_model_name_sent_as_bare_id(self):
|
||||
"""Runtime passes '<Provider>/<model>'; the wire must carry the bare
|
||||
model id (observed live: qualified name → 400 on every call)."""
|
||||
captured = {}
|
||||
|
||||
def fake_post(path, payload):
|
||||
captured.update(payload)
|
||||
return {"choices": [{"message": {"role": "assistant", "content": "ok"}}], "usage": {}}
|
||||
|
||||
t = OpenAICompatTransport("http://x/v1", "k", "Verboo Code")
|
||||
t._post = fake_post
|
||||
t.chat(model="Verboo Code/glm-5.3-flash", messages=[{"role": "user", "content": "hi"}], tools=None)
|
||||
assert captured["model"] == "glm-5.3-flash"
|
||||
|
||||
|
||||
def test_cloud_transport_for_floors_timeout(self):
|
||||
"""cloud_transport_for floors the timeout at 300s for cloud routers."""
|
||||
import rune.openai_compat_transport as mod
|
||||
captured = {}
|
||||
real_init = mod.OpenAICompatTransport.__init__
|
||||
|
||||
def spy_init(self, base_url, api_key, provider_name, timeout=120, context_window=131072):
|
||||
captured["timeout"] = timeout
|
||||
|
||||
with mock.patch.object(mod.OpenAICompatTransport, "__init__", spy_init), \
|
||||
mock.patch.object(Path, "is_file", return_value=True), \
|
||||
mock.patch.object(Path, "read_text", return_value='{"Verboo Code": {"options": {"baseURL": "https://x/v1"}}}'), \
|
||||
mock.patch.dict(sys.modules, {}):
|
||||
import json as _json
|
||||
with mock.patch.object(mod.json, "loads", return_value={"Verboo Code": {"key": "k"}}):
|
||||
mod.cloud_transport_for("Verboo Code/glm-5.3-flash", timeout=120)
|
||||
assert captured["timeout"] == 300
|
||||
|
||||
|
||||
class TestTransportSelection:
|
||||
"""Provider-keyed transport selection (not routing_mode): an
|
||||
operator-requested cloud model keeps routing_mode='local', so the
|
||||
selection must key on the provider prefix (T15 held-out finding)."""
|
||||
|
||||
def test_pinned_cloud_selects_openai_compat(self):
|
||||
import os
|
||||
from unittest import mock
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
os.environ["MIMO_PROBE_ENABLED"] = "0"
|
||||
# Journal edges resolve via memory_env; on CI the real memory dir is
|
||||
# root-owned (PermissionError on events.jsonl). Isolate first.
|
||||
os.environ["RUNE_MEMORY_DIR"] = os.path.join(tempfile.mkdtemp(), "memory")
|
||||
from rune import rune_operator
|
||||
from rune.local_tool_runtime import OllamaChatTransport
|
||||
|
||||
op = rune_operator.RuneOperator(storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
|
||||
captured = {}
|
||||
|
||||
class FakeRuntime:
|
||||
def __init__(self, transport, executor, model, tools):
|
||||
captured["transport"] = transport
|
||||
captured["model"] = model
|
||||
|
||||
def run(self, messages, options=None):
|
||||
from rune.tool_protocol import OperatorResponse
|
||||
return OperatorResponse(success=True, user_text="done", model=captured["model"])
|
||||
|
||||
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)
|
||||
|
||||
captured.clear()
|
||||
with mock.patch.object(rune_operator, "LocalToolRuntime", FakeRuntime):
|
||||
op.send_task("inspect the log for errors", model="qwen2.5:3b")
|
||||
assert isinstance(captured["transport"], OllamaChatTransport)
|
||||
|
||||
|
||||
def test_constructor_clamps_timeout(self):
|
||||
"""The transport clamps the wire timeout to >=120s (cloud round trips
|
||||
exceed the local tier timeout — observed live)."""
|
||||
t = OpenAICompatTransport("http://x/v1", "k", "T", timeout=45)
|
||||
assert t.timeout == 120, "below-floor must clamp to 120"
|
||||
t2 = OpenAICompatTransport("http://x/v1", "k", "T", timeout=300)
|
||||
assert t2.timeout == 300, "above-floor must be preserved"
|
||||
|
||||
def test_timeout_clamped_to_120(self):
|
||||
t = OpenAICompatTransport("http://x/v1", "k", "Test", timeout=30)
|
||||
assert t.timeout == 120
|
||||
t2 = OpenAICompatTransport("http://x/v1", "k", "Test", timeout=300)
|
||||
assert t2.timeout == 300
|
||||
def test_timeout_clamped_to_120():
|
||||
"""Cloud round trips with a full tool schema exceed the local tier timeout
|
||||
(30s): observed live, glm-5.3-flash read-timeout at 30.6s mid-iteration.
|
||||
The transport clamps the wire timeout to >=120s."""
|
||||
t = OpenAICompatTransport("http://x/v1", "k", "Test", timeout=30)
|
||||
assert t.timeout == 120
|
||||
t2 = OpenAICompatTransport("http://x/v1", "k", "Test", timeout=300)
|
||||
assert t2.timeout == 300
|
||||
@@ -0,0 +1,388 @@
|
||||
"""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
|
||||
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
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Transport selection (T15 PL1 substrate): provider-prefix keyed, not routing_mode.
|
||||
|
||||
Observed live: an operator-requested model keeps routing_mode='local' (line
|
||||
~887), so keying selection on routing_mode=='cloud' still sent pinned
|
||||
'Verboo Code/glm-5.3-flash' to OllamaChatTransport → 400. Selection must key
|
||||
on the model's provider prefix."""
|
||||
import importlib.util
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
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.local_tool_runtime import OllamaChatTransport # noqa: E402
|
||||
|
||||
|
||||
class _FakeRuntime:
|
||||
def __init__(self, transport, executor, model, tools):
|
||||
self.transport = transport
|
||||
self.model = model
|
||||
|
||||
def run(self, messages, options=None):
|
||||
from rune.tool_protocol import OperatorResponse
|
||||
return OperatorResponse(success=True, user_text="done", model=self.model)
|
||||
|
||||
|
||||
def _send(op, description, model):
|
||||
captured = {}
|
||||
class _RT(_FakeRuntime):
|
||||
def __init__(self, transport, executor, model, tools):
|
||||
super().__init__(transport, executor, model, tools)
|
||||
captured["transport"] = transport
|
||||
captured["model"] = model
|
||||
with mock.patch.object(rune_operator, "LocalToolRuntime", _RT):
|
||||
op.send_task(description, model=model)
|
||||
return captured
|
||||
|
||||
|
||||
def test_pinned_cloud_selects_openai_compat_transport():
|
||||
"""Selection is the unit under test — config resolution (cloud_transport_for
|
||||
reads the user's mimocode config, absent in CI) is mocked."""
|
||||
from unittest import mock as _mock
|
||||
import tempfile
|
||||
op = rune_operator.RuneOperator(storage_path=str(Path(tempfile.mkdtemp()) / "op.json"))
|
||||
import rune.openai_compat_transport as _oct
|
||||
with _mock.patch.object(_oct, "cloud_transport_for",
|
||||
side_effect=lambda model, timeout=120: OpenAICompatTransport(
|
||||
"https://mock/v1", "test-key", model.split("/", 1)[0])):
|
||||
captured = _send(op, "Fix the bug in /mnt/workdrive/pl1_heldout/greeting.py: returns 'Helo' not 'Hello'. Verify by running python3 /mnt/workdrive/pl1_heldout/check_greeting.py", "Verboo Code/glm-5.3-flash")
|
||||
assert isinstance(captured["transport"], OpenAICompatTransport)
|
||||
assert captured["model"] == "Verboo Code/glm-5.3-flash"
|
||||
|
||||
|
||||
def test_local_model_keeps_ollama_transport():
|
||||
import tempfile
|
||||
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)
|
||||
|
||||
|
||||
def test_bare_model_name_keeps_ollama_transport():
|
||||
import tempfile
|
||||
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)
|
||||
@@ -1 +0,0 @@
|
||||
def broken(:
|
||||
Reference in new issue
Block a user