Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
810d0027c3 | ||
|
|
4839a62b70 | ||
|
|
15408841ac | ||
|
|
c5c69de06f | ||
|
|
fcde0b2ce3 |
No files matched your search
@@ -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.**
|
||||
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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 —
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
Reference in new issue
Block a user