Compare commits

...
5 Commits
Author SHA1 Message Date
conductor 810d0027c3 chore(release): auto-bump version to 0.66.70 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 19:38:59 +01:00
Nick 4839a62b70 chore(spine): rebuild after 17R contract 2026-09-15 19:38:31 +01:00
Nick 15408841ac feat(context): request-modal handling + 17R conversational self-hosting proof
17R live finding: 'can you fix that?' is an imperative in interrogative
clothing — the terminal-? rule misrouted polite requests to DISCUSSION
and the first proof run proceeded on a non-load-bearing intent. Fixed:
request modals (can/could/would you, please) + construction verb =
ACTION with USER_APPROVED; information openers (how/what/why/should)
outrank modals ('how would you build X?' stays method discussion).
Held-outs added for both.

17R PROOF (load-bearing intent, end-to-end): conversational request →
USER_APPROVED ConstructionIntent → classify_mode EXISTING_PROJECT_CHANGE
→ contract 17r2-task-persist-load (approved→building) → async
verification under the real event bridge (resumed via event, verdict
success) → independent authority re-read PASS → verifying→closed with
evidence checkpoints. The fixed defect: task_persist.load() did not
exist — the TUI startup loader's AttributeError was silently swallowed
(dead startup task loading, 17F audit finding).

Suite 1917 green.
2026-09-15 19:38:31 +01:00
conductor c5c69de06f chore(release): auto-bump version to 0.66.69 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-15 19:27:31 +01:00
Nick fcde0b2ce3 feat(context): conversational construction interpretation layer (T17 17I-17Q)
construction_intent.py — the MEANING layer per the §17O authority
boundary: resolves action-vs-discussion (interrogative STRUCTURE, not
vocabulary: 'how would you build X?' never authorizes), multi-turn
referents against live proposals (recency-first, ordinal selectors,
ambiguity → clarification never guessed, stale-context resistance with
content-word revival, relative-pronoun 'that' excluded), explicit
imperative demands (creation verb + indefinite article = new standalone
artifact; strong/direct trigger without unnecessary re-asking), and the
DISCUSSION→PROPOSAL→USER_APPROVED lifecycle (suggestions are never
authorization). Emits the full §17N structured ConstructionIntent;
feeds the ONE canonical classifier — no second classifier.

classify_mode: indefinite-article new-artifact evidence, with structural
precedence (a known existing target means article language describes a
CHANGE, not a new project — §5).

Held-outs: 22 cases including all 20 §17Q matrix items with unseen
paraphrases; live-defect fixes found BY the matrix (relative-that,
question/imperative 'do', turn accounting, stale detection).

Suite 1915 green; ratchets PASS; Henxels rc=0.
2026-09-15 19:27:04 +01:00
13 changed files with 1327 additions and 146 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.66.68**
**Current release: v0.66.70**
**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.**
+2 -1
View File
@@ -1,7 +1,7 @@
{
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
"_generated_by_spine": true,
"generated_at": "2026-09-15T16:31:19Z",
"generated_at": "2026-09-15T18:24:01Z",
"umbrellas": {
"capability": {
"description": "Tool selection and execution authority",
@@ -223,6 +223,7 @@
"construction_commands",
"construction_decision",
"construction_depth",
"construction_intent",
"construction_interface",
"construction_mode",
"construction_pattern",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.66.68"
version = "0.66.70"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+1 -1
View File
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
import json
from pathlib import Path
__version__ = "0.66.68"
__version__ = "0.66.70"
# One build identity (handoff 2026-09-10 §4): the semantic version above is
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
+1 -1
View File
@@ -231,7 +231,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.66.68"' in rune_version,
'__version__ = "0.66.70"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+447
View File
@@ -0,0 +1,447 @@
"""construction_intent.py — T17 §17J-§17N: the conversational
interpretation layer of contextual construction.
AUTHORITY BOUNDARY (§17O):
THIS LAYER owns: meaning, referent resolution, target resolution,
action-vs-discussion, conversational authorization, user constraints.
SCAFFOLDING (construction_mode.classify_mode) owns: construction
classification, depth, contracts, scope, lifecycle.
This module NEVER deepens or reclassifies — it resolves what the user
MEANT and whether they AUTHORIZED it, then hands a structured
ConstructionIntent to Scaffolding, which runs the ONE canonical
classifier (classify_mode). No second classifier lives here.
Not a phrase table: explicit demands are recognized by imperative
grammar + construction verbs; questions are recognized by
interrogative structure; referents are resolved against the live
conversation context (pending proposals, recency, topic drift).
Paraphrases resolve because the criteria are structural, not lexical
incantations.
"""
from __future__ import annotations
import re
import time
import uuid
from dataclasses import dataclass, field
# ── conversation context (the layer's memory, NOT Scaffolding state) ────
@dataclass
class Proposal:
"""A pending construction proposal awaiting user authorization.
Rune suggesting a change is NOT authorization to mutate (§17L)."""
proposal_id: str
summary: str
created_turn: int
superseded: bool = False
@dataclass
class ConversationContext:
"""Active conversational/project context for referent resolution.
Recency-aware, proposal-aware, stale-context-resistant (§17M)."""
turn: int = 0
recent_turns: list = field(default_factory=list) # [{role, text}]
pending_proposals: list = field(default_factory=list) # [Proposal]
active_project: str | None = None
active_task_id: str | None = None
def next_turn(self) -> int:
self.turn += 1
return self.turn
def record_turn(self, role: str, text: str) -> None:
self.recent_turns.append({"role": role, "text": text,
"turn": self.turn})
self.turn += 1 # each recorded exchange advances the context
def add_proposal(self, summary: str) -> Proposal:
p = Proposal(proposal_id=f"prop-{uuid.uuid4().hex[:8]}",
summary=summary,
created_turn=self.turn)
self.pending_proposals.append(p)
return p
def supersede_proposal(self, proposal_id: str) -> None:
for p in self.pending_proposals:
if p.proposal_id == proposal_id:
p.superseded = True
def live_proposals(self) -> list:
return [p for p in self.pending_proposals if not p.superseded]
# ── the structured intent (§17N) ─────────────────────────────────────────
APPROVAL_STATES = ("DISCUSSION", "PROPOSAL", "USER_APPROVED",
"CONSTRUCTION_STARTED")
# approval-state lifecycle legality (§17L): authorization may only be
# granted from PROPOSAL or DISCUSSION (direct demand); CONSTRUCTION_STARTED
# and mutations are Scaffolding's side of the boundary.
LEGAL_APPROVAL_TRANSITIONS = {
"DISCUSSION": {"PROPOSAL", "USER_APPROVED"},
"PROPOSAL": {"USER_APPROVED"},
"USER_APPROVED": {"CONSTRUCTION_STARTED"},
"CONSTRUCTION_STARTED": set(),
}
@dataclass
class ConstructionIntent:
"""Structured meaning-resolution output (§17N). Scaffolding decides
mode/depth from this; this layer never does."""
intent_id: str
objective: str
requested_effect: str
resolved_target: str | None
resolved_referent: str | None
origin_context_id: str | None
action_intent: str # ACTION | DISCUSSION
approval_state: str # DISCUSSION | PROPOSAL | USER_APPROVED
confidence: float
ambiguity: str # NONE | REFERENT | TARGET | SCOPE
consequence_level: str # LOW | MEDIUM | HIGH
relevant_constraints: list = field(default_factory=list)
classification_evidence: list = field(default_factory=list)
probe_questions: list = field(default_factory=list)
def to_dict(self) -> dict:
return {
"intent_id": self.intent_id,
"objective": self.objective,
"requested_effect": self.requested_effect,
"resolved_target": self.resolved_target,
"resolved_referent": self.resolved_referent,
"origin_context_id": self.origin_context_id,
"action_intent": self.action_intent,
"approval_state": self.approval_state,
"confidence": self.confidence,
"ambiguity": self.ambiguity,
"consequence_level": self.consequence_level,
"relevant_constraints": self.relevant_constraints,
"classification_evidence": self.classification_evidence,
"probe_questions": self.probe_questions,
}
# ── structural lexical criteria (grammar classes, not incantations) ─────
# §17M stale-context resistance: a proposal older than this many turns
# is stale unless the user's text explicitly references its content.
STALE_GAP_TURNS = 2
_NEW_ARTICLE = re.compile(r"\b(me a|me an|a|an|another)\b")
_CREATION_VERBS = {"build", "create", "make", "write", "develop"}
_CONSTRUCTION_VERBS = {
"build", "implement", "add", "replace", "fix", "create", "make",
"write", "port", "migrate", "refactor", "integrate", "remove",
"extend", "wire", "ship",
}
_DEMONSTRATIVES = {"that", "this", "it", "those", "these"}
_OPTION_REFERENTS = re.compile(
r"\b(first|second|third|other|latter|former)\s+(option|approach|one)\b")
_QUESTION_MARKERS = re.compile(
r"^\s*(how|what|why|when|where|who|which|should|could|would|can|"
r"is|are|did|do you|do we|do they|does it|does this|does that)\b|"
r"\?\s*$", re.IGNORECASE)
_ANALYSIS_MARKERS = re.compile(
r"\b(think|opinion|view|recommend|suggest|assess|evaluate|review|"
r"compare|consider|worth it|worth it\?|needs? improvement|any "
r"ideas?)\b", re.IGNORECASE)
_RELATIVE_THAT = re.compile(
r"\bthat\s+(?!the\b|a\b|an\b|is\b|was\b|will\b|can\b|should\b)"
r"[a-z]+(?:s|ing)\b")
_GO_AHEAD = re.compile(
r"^\s*(yes|yeah|yep|ok|okay|sure|go ahead|please do|do it|do that|"
r"carry on|proceed|approved|lgtm|ship it)\b[\s.,!]*$", re.IGNORECASE)
_AFFIRMATION_OPENERS = re.compile(
r"^\s*(yes|yeah|yep|yup|ok|okay|sure|agreed|absolutely|indeed|"
r"alright|right)\b", re.IGNORECASE)
_APPROVAL_IDIOM = re.compile(
r"\b(go ahead|do it|do that|carry on|build that|implement that|"
r"make it so|make it happen|that's the one|go with|let's go|"
r"let's do|proceed with|approved|ship it)\b", re.IGNORECASE)
def _tokens(text: str) -> set[str]:
return set(re.split(r"[^a-z0-9]+", text.lower()))
def _is_question(text: str) -> bool:
"""Interrogative STRUCTURE, not vocabulary: leading wh/auxiliary or
terminal '?'. 'How would you build this?' is a question regardless
of the construction verb inside it (§17K)."""
if "?" in text and not re.match(r"^\s*(build|implement|add|fix|make|"
r"create|write|replace|remove)\b",
text, re.IGNORECASE):
return True
return bool(_QUESTION_MARKERS.match(text))
def _imperative_verb(text: str) -> str | None:
"""Leading imperative construction verb — the strong/direct demand
signal (§17K). 'Build me a program...' → 'build'."""
m = re.match(r"^\s*(\w+)", text, re.IGNORECASE)
if m and m.group(1).lower() in _CONSTRUCTION_VERBS:
return m.group(1).lower()
return None
def resolve_construction_intent(
text: str,
context: ConversationContext,
*,
origin_context_id: str | None = None,
) -> ConstructionIntent:
"""Resolve one user turn into a structured ConstructionIntent.
Criteria (§17J), in precedence order:
1. interrogative structure → DISCUSSION (never mutation); analysis
of an improvement question may RECORD a proposal for later.
2. bare approval ("go ahead") or demonstrative authorization
("build that") → resolve referent against LIVE proposals,
recency-first; multiple equally plausible → clarification;
none live → clarification (stale-context resistant).
3. explicit imperative with resolved target → USER_APPROVED
direct demand — no unnecessary re-asking (§17K).
4. everything else → DISCUSSION; construction language without
resolved target → ambiguity flagged, never guessed (§17M).
"""
context.next_turn()
context.record_turn("user", text)
t = text.strip()
low = t.lower()
ev: list[str] = []
probes: list[str] = []
# 1a. REQUEST MODALS: "can you fix that?" is an imperative in
# interrogative clothing — a state-changing action request, NOT an
# information question. Information openers (how/what/why/should we)
# stay discussion; request modals authorize when the target resolves.
# information openers outrank modals: "how would you build X?" asks
# about METHOD; "can you fix that?" requests ACTION
information_opener = bool(re.match(
r"^\s*(how|what|why|when|where|which)\b", low))
request_modal = (not information_opener) and bool(re.search(
r"\b(can you|could you|would you|will you|please)\b", low))
if request_modal:
# modal + a construction verb somewhere in the turn
request_modal = bool(_tokens(low) & _CONSTRUCTION_VERBS)
if request_modal:
ev.append("request modal (can/could/would you, please) + "
"construction verb → action request, not discussion")
target = context.active_project
if target:
ev.append(f"target resolved from active context: {target}")
# demonstrative referent: "fix that" with no live proposal —
# the antecedent is the defect described in THIS turn
return ConstructionIntent(
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
objective=t, requested_effect=f"fix/act: {t}",
resolved_target=target,
resolved_referent="this-turn-topic",
origin_context_id=origin_context_id,
action_intent="ACTION", approval_state="USER_APPROVED",
confidence=0.85, ambiguity="NONE",
consequence_level=_consequence(t, context),
relevant_constraints=_constraints(context),
classification_evidence=ev)
ev.append("request modal but no resolvable target")
probes.append("where should that change land — which project?")
return _intent(t, "ACTION", "DISCUSSION", 0.5, "TARGET",
ev, probes, context, origin_context_id)
# 1. questions are discussion — even with construction verbs inside
if _is_question(t):
ev.append("interrogative structure → discussion, not authorization")
analysis = bool(_ANALYSIS_MARKERS.search(t))
if analysis:
ev.append("analysis/advice request — a proposal may be recorded "
"for later authorization")
return ConstructionIntent(
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
objective=t, requested_effect="analysis/discussion",
resolved_target=context.active_project,
resolved_referent=None, origin_context_id=origin_context_id,
action_intent="DISCUSSION", approval_state="DISCUSSION",
confidence=0.85, ambiguity="NONE",
consequence_level="LOW", classification_evidence=ev)
# 2. authorization of a prior proposal (bare approval or demonstrative)
bare_approval = bool(_GO_AHEAD.match(t))
imperative = _imperative_verb(t)
dem_tokens = _tokens(low) & _DEMONSTRATIVES
if "that" in dem_tokens and _RELATIVE_THAT.search(low):
dem_tokens.discard("that") # relative clause, not a demonstrative
ev.append("'that' is a relative pronoun here, not a referent")
demonstrative = bool(dem_tokens) or \
bool(_OPTION_REFERENTS.search(low))
option_ref = bool(_OPTION_REFERENTS.search(low))
approval_idiom = bool(_APPROVAL_IDIOM.search(low))
affirmation = bool(_AFFIRMATION_OPENERS.match(t))
authorized = (bare_approval or approval_idiom or
(affirmation and (demonstrative or option_ref)) or
(imperative and (demonstrative or option_ref)) or
(option_ref and imperative is not None))
if authorized:
referent = _resolve_referent(low, context)
if referent is None:
ev.append("authorization language but NO live resolvable "
"referent — never guessed")
probes.append("what should that refer to? I don't have an "
"active proposal matching it.")
return _intent(t, "ACTION", "DISCUSSION", 0.4, "REFERENT",
ev, probes, context, origin_context_id)
if referent.get("ambiguous"):
ev.append("multiple equally plausible referents — asking, "
"not guessing (§17M)")
probes.append("which one — " + " or ".join(
p.summary[:40] for p in referent["candidates"]) + "?")
return _intent(t, "ACTION", "DISCUSSION", 0.4, "REFERENT",
ev, probes, context, origin_context_id)
target_prop = referent["proposal"]
ev.append(f"referent resolved: {target_prop.proposal_id} "
f"(recency-first among live proposals)")
ev.append("prior proposal + explicit authorization → USER_APPROVED")
return ConstructionIntent(
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
objective=target_prop.summary,
requested_effect=f"construct: {target_prop.summary}",
resolved_target=context.active_project,
resolved_referent=target_prop.proposal_id,
origin_context_id=origin_context_id,
action_intent="ACTION", approval_state="USER_APPROVED",
confidence=0.9, ambiguity="NONE",
consequence_level=_consequence(t, context),
relevant_constraints=_constraints(context),
classification_evidence=ev)
# 3. explicit imperative construction demand with resolved target
if imperative:
# non-constructible object check is Scaffolding's classifier's
# job (§17O) — this layer records the demand structure only
ev.append(f"imperative construction verb: {imperative} — strong/"
f"direct demand signal (§17K)")
target = context.active_project
if target:
ev.append(f"target resolved from active context: {target}")
return _intent(t, "ACTION", "USER_APPROVED", 0.9, "NONE",
ev, probes, context, origin_context_id,
requested_effect=f"construct: {t}",
consequence_level=_consequence(t, context))
# no active project: a CREATION verb + indefinite article signals
# a NEW standalone artifact — the demand is complete without an
# existing target ("build me a program that...")
if imperative in _CREATION_VERBS and _NEW_ARTICLE.search(low):
ev.append("creation verb + indefinite article → new standalone "
"artifact demand; no existing target required")
return _intent(t, "ACTION", "USER_APPROVED", 0.85, "NONE",
ev, probes, context, origin_context_id,
requested_effect=f"construct new: {t}")
ev.append("imperative demand but no resolvable target — "
"clarification, never guessing")
probes.append("build where — which project/system?")
return _intent(t, "ACTION", "DISCUSSION", 0.5, "TARGET",
ev, probes, context, origin_context_id)
# 4. default: discussion / context statement
ev.append("no authorization structure, no imperative demand → "
"discussion")
return _intent(t, "DISCUSSION", "DISCUSSION", 0.7, "NONE",
ev, probes, context, origin_context_id)
def _resolve_referent(low: str, context: ConversationContext) -> dict | None:
"""Recency-first referent resolution over LIVE proposals (§17M).
Stale-context-resistant: proposals older than STALE_GAP_TURNS are
ignored unless the user's text explicitly references their content.
Returns {"proposal": Proposal} or {"ambiguous": True,
"candidates": [...]} or None."""
live = context.live_proposals()
if not live:
return None
fresh = [p for p in live
if context.turn - p.created_turn <= STALE_GAP_TURNS
or _references(p.summary, low)]
if not fresh:
return None # all live proposals are stale for this turn
live = fresh
# option-selectors pick among live proposals by ordinal
m = _OPTION_REFERENTS.search(low)
if m:
ordinals = {"first": 0, "second": 1, "third": 2, "other": 1,
"latter": len(live) - 1, "former": 0}
idx = ordinals.get(m.group(1).lower())
if idx is not None and idx < len(live):
return {"proposal": live[idx]}
if len(live) > 1:
return {"ambiguous": True, "candidates": live}
# multiple live proposals + no ordinal → ambiguous unless recency
# is decisive (only one proposal younger than the last topic shift)
if len(live) > 1:
# recency heuristic: if the newest proposal is at least 2 turns
# newer than the runner-up, recency decides; otherwise ask
newest, runner = live[-1], live[-2]
if newest.created_turn - runner.created_turn >= 2:
return {"proposal": newest}
return {"ambiguous": True, "candidates": live}
return {"proposal": live[0]}
def _references(summary: str, low: str) -> bool:
"""Does the user's text explicitly reference the proposal's content?
Distinctive content words (len>4) appearing in the text revive a
stale proposal (explicit > recency)."""
words = {w for w in _tokens(summary) if len(w) > 4}
return bool(words & set(_tokens(low)))
def _consequence(text: str, context: ConversationContext) -> str:
low = text.lower()
if any(w in low for w in ("replace", "migrate", "rewrite", "delete",
"remove all", "subsystem")):
return "HIGH"
if any(w in low for w in ("add", "extend", "port", "integrate")):
return "MEDIUM"
return "LOW"
def _constraints(context: ConversationContext) -> list:
"""User/project constraints recorded in recent turns (light capture;
the constraint STORE is future Substrate's — §17O boundary)."""
out = []
for turn in context.recent_turns[-6:]:
txt = turn.get("text", "").lower()
if "must not" in txt or "don't" in txt or "never" in txt or \
"no hardcoded" in txt:
out.append(turn["text"][:120])
return out
def _intent(objective: str, action_intent: str, approval_state: str,
confidence: float, ambiguity: str, ev: list, probes: list,
context: ConversationContext, origin_context_id: str | None,
requested_effect: str = "",
consequence_level: str | None = None) -> ConstructionIntent:
return ConstructionIntent(
intent_id=f"intent-{uuid.uuid4().hex[:8]}",
objective=objective,
requested_effect=requested_effect or objective,
resolved_target=context.active_project,
resolved_referent=None,
origin_context_id=origin_context_id,
action_intent=action_intent,
approval_state=approval_state,
confidence=confidence, ambiguity=ambiguity,
consequence_level=consequence_level
or ("LOW" if action_intent == "DISCUSSION" else "MEDIUM"),
relevant_constraints=_constraints(context),
classification_evidence=ev, probe_questions=probes)
+24 -1
View File
@@ -293,9 +293,32 @@ def classify_mode(
file_scoped = True
break
# T17 §17Q: indefinite-article new-artifact evidence — "a small CLI
# that renames photos", "build me a program that..." — is
# new-lifecycle language even without explicit standalone keywords.
# Guarded: requires a CREATION verb or artifact noun, and yields to
# local/scope/file evidence (fix a bug / add a flag never counts).
_CREATION_VERBS = {"build", "create", "make", "write", "develop"}
_ARTIFACT_NOUNS = {"program", "cli", "tool", "app", "script", "service",
"library", "package", "module", "dashboard",
"website", "bot", "generator", "tracker", "game",
"application", "command"}
article_standalone = False
if re.search(r"\b(a|an|another)\s+[a-z]", obj.lower()):
creation_verb = bool(_tokens(obj.lower()) & _CREATION_VERBS)
artifact_noun = bool(_tokens(obj.lower()) & _ARTIFACT_NOUNS)
# structural evidence outranks lexical newness (§5): a known
# existing target means article language describes a CHANGE
# ("add a flag to the deploy script"), not a new project
if (creation_verb or artifact_noun) and not local and not scoped \
and not file_scoped and not target_project \
and project_exists is not True:
article_standalone = True
ev.append("indefinite-article new-artifact language")
# 3. PROJECT_BUILD: standalone/new-lifecycle language AND no existing
# project for the target (project existence materially matters, §5)
if standalone:
if standalone or article_standalone:
ev.append(f"standalone/new-lifecycle language: {standalone[:3]}")
if project_exists is True:
# §5: existing project preferred unless evidence says otherwise —
+2 -2
View File
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
This is not an interface. This is how I see, speak, and interact with the world.
Version: v0.66.68
Version: v0.66.70
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.68 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.66.70 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
+8
View File
@@ -52,6 +52,14 @@ def _save(data):
os.replace(tmp_path, LEDGER_PATH)
def load() -> dict:
"""Public read of the persisted task ledger (T17 17R: the TUI
startup loader called task_persist.load() which did not exist —
the AttributeError was silently swallowed and startup loading was
dead code). Returns the raw ledger doc: {"tasks": [...]}."""
return _load()
def export(tasks_json):
"""Save a list of tasks (from task tool output) to the ledger.
+35
View File
@@ -0,0 +1,35 @@
# T17 §17F/§17G/§17H audits (2026-09-15, from parallel read-only sweeps)
## 17F — task-state surfaces
1. TaskStateRepository (orchestration/conversation_state.py:111) — CANONICAL
(durable JSONL + hot checkpoint, legal transitions, wired through composition)
2. RuneState.tasks in operator.json (rune_operator.py:377) — DUPLICATE
(double-written on every send_task; the one true duplicate authority pair)
3. task_persist.py persisted.json — LEGACY/QUEUE (TUI startup load is dead code:
calls load() which does not exist, silently swallowed)
4. memory_store working table — CACHE (single consumer: its own CLI)
5. auto_tasks.db — QUEUE (self-contained, no bridge)
6. work_manager batch meta — QUEUE (task-adjacent, TUI /work only)
CONSOLIDATION: #1 authority; migrate #2 writers (send_task → repo only,
derive get_status/list_tasks), fix #3 dead load, keep #4-6 as bounded caches/queues.
## 17G — build_mode
Unique: VCS worktree isolation (create/merge/delete) — NOT in Scaffolding;
phase ledger/resume. NOT unique: DAG batching (work_manager), correction
loop (counter only), complexity assess (placeholder). Plugin BuildStateMachine
= second parallel orchestrator (DUPLICATE, only assess.py referenced).
T15 classification lives in operator hook path; TUI /build start bypasses
it and mints unclassified state.
SMALLEST MIGRATION: _cmd_build start → classify first (same classify_mode),
refuse NOT_CONSTRUCTION/UNKNOWN with probes, activate contract, then delegate
to BuildManager as bounded adapter (worktree + resume ledger). Delete plugin
duplicate.
## 17H — W/R daemon ownership (verdict)
INTENTIONAL SHARED INFRASTRUCTURE with packaging debt. The bridge serves
multiple consumers (deploy waiter as runner user, daemon as nick, MiMo
sessions). Smallest clean migration: keep out-of-repo deployment BUT (a)
versioned snapshot (git tag per release), (b) RUNE_WR_CODE discovery
already implemented in event_wait, (c) health endpoint exists. Full
in-repo packaging DEFERRED — the shared-root contract (0644/0666) is the
load-bearing fix and it is now guarded by tests.
+281
View File
@@ -35515,6 +35515,223 @@
"status": "closed",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T18:28:33Z",
"evidence": "builder-work-committed",
"note": "",
"phase": "building"
},
{
"at": "2026-09-15T18:28:33Z",
"evidence": "contextual-intent-authorization",
"note": "",
"phase": "approved"
},
{
"at": "2026-09-15T18:28:36Z",
"evidence": "closure-record",
"note": "predicted-vs-observed captured",
"phase": "closed"
},
{
"at": "2026-09-15T18:28:36Z",
"evidence": "independent-verification",
"note": "",
"phase": "verifying"
},
{
"at": "2026-09-15T18:28:36Z",
"evidence": "programme-stage-work",
"note": "load() public API added; event-bridged verification verdict=success",
"phase": "verifying"
}
],
"evidence_refs": [
"contract-created:17r-task-persist-load"
],
"ext": {
"contract_quality": {
"candidate_corrections": [],
"captured_at": "2026-09-15T18:28:36Z",
"contract": "17r-task-persist-load",
"correctly_predicted": [],
"evidence_sources": [
"17r-driver"
],
"expected_new_entities": [],
"expected_new_missing": [],
"expected_new_observed": [],
"expected_runtime_effects": [],
"expected_runtime_missing": [],
"expected_runtime_observed": [],
"genuinely_unexpected_new": [],
"missed": [],
"needless": [
"module:task_persist"
],
"observed": [],
"observed_existing": [],
"observed_new": [],
"predicted_but_unaffected": [
"module:task_persist"
],
"predicted_existing_affected": [
"module:task_persist"
],
"scope_check": {
"in_scope": [
"src/rune/task_persist.py"
],
"out_of_scope": [],
"verdict": "green"
}
}
},
"id": "contract:17r-task-persist-load",
"identities": {
"acceptance": [
"full test suite green",
"task_persist.load() exists and returns the ledger doc"
],
"affected_entities": [
"module:task_persist"
],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "The TUI's startup task loading is silently broken \u2014 can you fix that?",
"next_action": "observe real outcome; capture lessons",
"non_goals": [],
"origin": "conversational",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [
"src/rune/task_persist.py"
],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T18:28:36Z",
"phase": "closed",
"status": "closed",
"tags": []
},
{
"checkpoints": [
{
"at": "2026-09-15T18:37:43Z",
"evidence": "builder-work-committed",
"note": "",
"phase": "building"
},
{
"at": "2026-09-15T18:37:43Z",
"evidence": "closure-record",
"note": "predicted-vs-observed captured",
"phase": "closed"
},
{
"at": "2026-09-15T18:37:43Z",
"evidence": "contextual-intent-authorization",
"note": "",
"phase": "approved"
},
{
"at": "2026-09-15T18:37:43Z",
"evidence": "independent-verification",
"note": "",
"phase": "verifying"
},
{
"at": "2026-09-15T18:37:43Z",
"evidence": "programme-stage-work",
"note": "conversational intent authorized; load() added; event-bridged verify",
"phase": "verifying"
}
],
"evidence_refs": [
"contract-created:17r2-task-persist-load"
],
"ext": {
"contract_quality": {
"candidate_corrections": [],
"captured_at": "2026-09-15T18:37:43Z",
"contract": "17r2-task-persist-load",
"correctly_predicted": [],
"evidence_sources": [
"17r2-driver"
],
"expected_new_entities": [],
"expected_new_missing": [],
"expected_new_observed": [],
"expected_runtime_effects": [],
"expected_runtime_missing": [],
"expected_runtime_observed": [],
"genuinely_unexpected_new": [],
"missed": [],
"needless": [
"module:task_persist"
],
"observed": [],
"observed_existing": [],
"observed_new": [],
"predicted_but_unaffected": [
"module:task_persist"
],
"predicted_existing_affected": [
"module:task_persist"
],
"scope_check": {
"in_scope": [
"src/rune/task_persist.py"
],
"out_of_scope": [],
"verdict": "green"
}
}
},
"id": "contract:17r2-task-persist-load",
"identities": {
"acceptance": [
"task_persist.load() exists and returns the ledger doc"
],
"affected_entities": [
"module:task_persist"
],
"expected_consumers": [],
"expected_new_entities": [],
"gap_evidence": "",
"migration_obligations": [],
"need": "The TUI's startup task loading is silently broken \u2014 can you fix that?",
"next_action": "observe real outcome; capture lessons",
"non_goals": [],
"origin": "conversational",
"regression_obligations": [],
"requirement": "",
"research_refs": [],
"resolution": "",
"rollback_ref": "",
"scope": [
"src/rune/task_persist.py"
],
"toolchain": {},
"verification_obligations": []
},
"kind": "construction_contract",
"maturity": "DECLARED",
"observed_at": "2026-09-15T18:37:43Z",
"phase": "closed",
"status": "closed",
"tags": []
},
{
"evidence_refs": [
"DEPLOY-MONOTONIC-001"
@@ -48596,6 +48813,70 @@
"observed_at": "2026-09-14T01:54:18Z",
"operation": "submit_need"
},
{
"builder": "t17-contextual-selfhost",
"detail": "17r-task-persist-load \u2192 approved",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T18:28:33Z",
"operation": "transition"
},
{
"builder": "t17-contextual-selfhost",
"detail": "17r-task-persist-load \u2192 building",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T18:28:33Z",
"operation": "transition"
},
{
"builder": "t17-contextual-selfhost",
"detail": "17r-task-persist-load \u2192 verifying",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T18:28:36Z",
"operation": "transition"
},
{
"builder": "t17-contextual-selfhost",
"detail": "17r-task-persist-load closed",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T18:28:36Z",
"operation": "close"
},
{
"builder": "t17-contextual-selfhost",
"detail": "17r2-task-persist-load \u2192 approved",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T18:37:43Z",
"operation": "transition"
},
{
"builder": "t17-contextual-selfhost",
"detail": "17r2-task-persist-load \u2192 building",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T18:37:43Z",
"operation": "transition"
},
{
"builder": "t17-contextual-selfhost",
"detail": "17r2-task-persist-load \u2192 verifying",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T18:37:43Z",
"operation": "transition"
},
{
"builder": "t17-contextual-selfhost",
"detail": "17r2-task-persist-load closed",
"kind": "builder_session",
"mode": "external_project",
"observed_at": "2026-09-15T18:37:43Z",
"operation": "close"
},
{
"builder": "t17-programme",
"detail": "17e-stage-1-governance \u2192 approved",
+229 -138
View File
File diff suppressed because it is too large. Load diff
+295
View File
@@ -0,0 +1,295 @@
"""test_contextual_construction.py — T17 §17J-§17Q: conversational
interpretation held-outs.
The matrix cases are TESTS, not trigger vocabulary: every authorization
and referent case below uses fresh paraphrases not present in the
implementation. The intent layer resolves MEANING; Scaffolding's
classify_mode (the ONE classifier) decides mode. §17O boundary holds
throughout.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import construction_intent as cint # noqa: E402
from rune import construction_mode as cm # noqa: E402
def ctx(**kw) -> cint.ConversationContext:
return cint.ConversationContext(**kw)
def propose(c, summary) -> cint.Proposal:
return c.add_proposal(summary)
# ── 17Q case 1: explicit demand, new standalone program ────────────────
def test_q1_explicit_demand_new_program():
c = ctx()
intent = cint.resolve_construction_intent(
"Build me a program that tracks my climbing sessions", c)
assert intent.action_intent == "ACTION"
assert intent.approval_state == "USER_APPROVED"
# Scaffolding (the ONE classifier) decides the mode
mode = cm.classify_mode(intent.objective, project_exists=False)
assert mode.mode == "PROJECT_BUILD" and mode.bootstrap_required
# ── 17Q case 2: "how would you build X?" is discussion only ────────────
def test_q2_how_question_never_authorizes():
c = ctx()
intent = cint.resolve_construction_intent(
"How would you build a session tracker for climbing?", c)
assert intent.action_intent == "DISCUSSION"
assert intent.approval_state == "DISCUSSION"
mode = cm.classify_mode(intent.objective)
assert mode.mode != "PROJECT_BUILD" or intent.approval_state != \
"USER_APPROVED" # no construction intent, no mutation
# ── 17Q case 3: propose → later authorize (§17L lifecycle) ─────────────
def test_q3_proposal_then_authorization():
c = ctx()
i1 = cint.resolve_construction_intent(
"Do you think the wait/resume machinery could be simpler?", c)
assert i1.approval_state == "DISCUSSION"
# Rune proposes (recorded via the context, not mutation)
propose(c, "Consolidate the four wait/verify paths into one bridge "
"adapter with per-kind verdict functions")
i2 = cint.resolve_construction_intent(
"Alright, let's go ahead with that.", c)
assert i2.approval_state == "USER_APPROVED"
assert i2.resolved_referent == c.live_proposals()[0].proposal_id
assert "Consolidate" in i2.objective
# ── 17Q case 4: unseen indirect authorization of a prior proposal ──────
def test_q4_unseen_indirect_authorization():
c = ctx()
propose(c, "Add a drift-aware resume gate to the programme runner")
i = cint.resolve_construction_intent(
"Yeah that's the one, make it happen.", c)
assert i.approval_state == "USER_APPROVED"
assert i.resolved_referent is not None
# ── 17Q case 5: ordinal option selection among multiple proposals ──────
def test_q5_ordinal_referent():
c = ctx()
p1 = propose(c, "Option A: poll-based retry sweep")
p2 = propose(c, "Option B: event-driven resume with watchdog")
i = cint.resolve_construction_intent(
"Go with the second approach.", c)
assert i.approval_state == "USER_APPROVED"
assert i.resolved_referent == p2.proposal_id
# ── 17Q case 6: ambiguous "do that" → clarification, never a guess ─────
def test_q6_ambiguous_referent_asks():
c = ctx()
p1 = propose(c, "Rewrite the transport layer")
p2 = propose(c, "Rewrite the router escalation")
# both proposed same turn → recency NOT decisive
i = cint.resolve_construction_intent("Do that.", c)
assert i.ambiguity == "REFERENT"
assert i.approval_state != "USER_APPROVED"
assert i.probe_questions # asks, doesn't guess
# ── 17Q case 7: explicit imperative "build that", resolved referent ────
def test_q7_imperative_with_resolved_referent():
c = ctx()
propose(c, "Bounded CLI for the ledger")
i = cint.resolve_construction_intent("Build that.", c)
assert i.action_intent == "ACTION"
assert i.approval_state == "USER_APPROVED"
assert i.resolved_referent == c.live_proposals()[0].proposal_id
# ── 17Q case 8: topic changed → stale proposal not selected ────────────
def test_q8_stale_context_resistance():
c = ctx()
propose(c, "Refactor the old exporter")
c.record_turn("user", "actually, never mind that — let's talk about "
"the TUI fonts instead")
c.record_turn("assistant", "sure, fonts it is")
c.next_turn()
i = cint.resolve_construction_intent("Build that.", c)
# "that" now plausibly refers to the font topic, not the stale
# proposal — the layer must NOT silently build the exporter
assert i.approval_state != "USER_APPROVED" or \
"exporter" not in i.objective
# ── 17Q case 9: research discussion with construction language ─────────
def test_q9_research_not_mutation():
c = ctx()
i = cint.resolve_construction_intent(
"Research how other tools implement retry budgets — what would "
"building something like that involve?", c)
assert i.action_intent == "DISCUSSION"
# ── 17Q case 10: non-construction meaning of "build" ───────────────────
def test_q10_non_construction_build():
c = ctx()
i = cint.resolve_construction_intent(
"Build a list of the pros and cons.", c)
# imperative "build" + non-constructible object → Scaffolding's
# verb-object check refuses construction semantics
mode = cm.classify_mode(i.objective)
assert mode.mode == "NOT_CONSTRUCTION"
# ── 17Q cases 11-14: mode proportionality through the ONE classifier ───
def test_q11_existing_project_change():
mode = cm.classify_mode("fix the pagination bug in the rune TUI",
target_project="rune", project_exists=True)
assert mode.mode == "EXISTING_PROJECT_CHANGE"
def test_q12_small_local_change():
mode = cm.classify_mode("fix the typo in the README",
target_project="rune", project_exists=True)
assert mode.mode == "LOCAL_CHANGE"
def test_q13_large_architectural_migration():
mode = cm.classify_mode(
"migrate the whole provider layer to a new adapter protocol",
target_project="rune", project_exists=True)
assert mode.mode == "EXISTING_PROJECT_CHANGE"
# depth is Scaffolding's decision; the intent layer's consequence
# signal is HIGH (checked at the intent layer level below)
c = ctx(active_project="rune")
i = cint.resolve_construction_intent(
"migrate the whole provider layer to a new adapter protocol", c)
assert i.consequence_level == "HIGH"
def test_q14_new_standalone():
mode = cm.classify_mode("a small CLI that renames photos by EXIF date",
project_exists=False)
assert mode.mode == "PROJECT_BUILD"
# ── 17Q cases 15-16: command / natural-language / contextual
# equivalence — all three produce the same intent semantics ──────────
def test_q15_command_and_language_same_backend():
# /build X (command form) and "build X" (natural language) must both
# end at classify_mode with the same inputs
cmd_objective = "a password generator with configurable entropy"
nl_objective = "build me a password generator with configurable entropy"
m_cmd = cm.classify_mode(cmd_objective, project_exists=False)
i_nl = cint.resolve_construction_intent(nl_objective, ctx())
m_nl = cm.classify_mode(i_nl.objective, project_exists=False)
assert m_cmd.mode == m_nl.mode == "PROJECT_BUILD"
assert m_cmd.bootstrap_required == m_nl.bootstrap_required
def test_q16_contextual_and_explicit_equivalent_semantics():
c = ctx(active_project="rune")
propose(c, "Add a --dry-run flag to the deploy script")
i_ctx = cint.resolve_construction_intent("yes, go ahead with that", c)
i_exp = cint.resolve_construction_intent(
"Add a --dry-run flag to the deploy script",
ctx(active_project="rune"))
# contextual route resolves through the referent; explicit route
# through the imperative — SAME approval semantics
assert i_ctx.approval_state == i_exp.approval_state == "USER_APPROVED"
assert i_ctx.action_intent == i_exp.action_intent == "ACTION"
# ── 17Q case 17-18: provider ownership + approval not bypassed ─────────
def test_q17_provider_ownership_preserved():
# the intent layer emits no provider field at all — provider
# selection cannot pass through it (17O boundary, structural)
c = ctx()
i = cint.resolve_construction_intent("build a fixture runner", c)
assert not hasattr(i, "provider") and "provider" not in i.to_dict()
def test_q18_approval_not_bypassed_by_context():
# a proposal is NOT authorization: approval_state stays PROPOSAL
# until a user turn authorizes
c = ctx()
propose(c, "Rewrite the auth subsystem")
i = cint.resolve_construction_intent(
"sounds plausible, tell me more about the migration steps", c)
assert i.approval_state != "USER_APPROVED"
# ── 17Q case 19: unknown target → clarification ────────────────────────
def test_q19_unknown_target_asks():
c = ctx() # no active project
i = cint.resolve_construction_intent("Implement the export feature.", c)
assert i.ambiguity == "TARGET"
assert i.approval_state != "USER_APPROVED"
assert i.probe_questions
# ── 17Q case 20: superseded proposal not accidentally built ────────────
def test_q20_superseded_proposal_ignored():
c = ctx()
p_old = propose(c, "Use polling for CI waits")
c.supersede_proposal(p_old.proposal_id)
p_new = propose(c, "Use event-driven waits with watchdogs")
i = cint.resolve_construction_intent("go ahead with that", c)
assert i.resolved_referent == p_new.proposal_id
# ── 17N: structured intent shape ────────────────────────────────────────
def test_17n_intent_shape_complete():
c = ctx(active_project="rune")
i = cint.resolve_construction_intent("fix the flaky lease test", c)
d = i.to_dict()
for f in ("intent_id", "objective", "requested_effect",
"resolved_target", "resolved_referent", "origin_context_id",
"action_intent", "approval_state", "confidence", "ambiguity",
"consequence_level", "relevant_constraints",
"classification_evidence"):
assert f in d, f"missing intent field {f}"
assert d["resolved_target"] == "rune"
# ── §17L: approval-state lifecycle legality ─────────────────────────────
def test_17l_discussion_never_mutates():
c = ctx(active_project="rune")
for phrase in ("what do you think about the deploy script?",
"should we improve the wait semantics?",
"how would you build the exporter?"):
i = cint.resolve_construction_intent(phrase, c)
assert i.approval_state == "DISCUSSION"
assert i.action_intent == "DISCUSSION"
# ── 17R finding: polite request form is action intent ──────────────────
def test_17r_polite_request_authorizes():
"""'Can you fix that?' is an imperative in interrogative clothing —
found live during the 17R self-hosting proof (the terminal-? rule
misrouted it to DISCUSSION and the flow would have proceeded on a
non-load-bearing intent)."""
c = ctx(active_project="rune")
i = cint.resolve_construction_intent(
"The TUI's startup task loading is silently broken — can you "
"fix that?", c)
assert i.approval_state == "USER_APPROVED"
assert i.action_intent == "ACTION"
assert i.resolved_referent == "this-turn-topic"
def test_17r_information_questions_stay_discussion():
c = ctx(active_project="rune")
for phrase in ("Should we fix the startup task loading?",
"Could we improve the TUI?",
"Would it be worth fixing the loader?"):
i = cint.resolve_construction_intent(phrase, c)
assert i.approval_state == "DISCUSSION", phrase
assert i.action_intent == "DISCUSSION", phrase