Compare commits

...
13 Commits
Author SHA1 Message Date
conductor 57a689d22f chore(release): auto-bump version to 0.80.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 19:52:27 +01:00
Nick 45869a77cd chore(store): T21.3 evidence + guard updates 2026-09-16 19:51:57 +01:00
Nick 224fbfa4e9 feat(t21.3): 21.3B root-cause documented + 21.3C await_operation tool (first-class async wait)
ROOT CAUSE (verified, not inferred): the model generates sleep N because
NO await/wait tool exists in its tool vocabulary. The model rationally
chooses bash sleep as the least-bad option. The fix: expose
await_operation as a first-class MiMo tool via the plugin API.

The tool: registers on the W/R event bridge, blocks until the completion
event arrives, returns verified JSON verdict. The model sees a normal
tool result — not an error, not a guard rejection.
2026-09-16 19:51:57 +01:00
conductor 417e824a51 chore(release): auto-bump version to 0.79.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 18:37:41 +01:00
Nick ca2807e9b5 feat(t21.2): 21.2F blind external adoption — independent creator + independent adopter, 100% recall on important entities, legacy trap preserved, 0 false relationships; 21.2J matrix 6 new held-outs; 21.2I contradiction hunt persisted 2026-09-16 18:37:22 +01:00
conductor 25f5219982 chore(release): auto-bump version to 0.78.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 18:22:41 +01:00
Nick 2d09bee39e feat(t21.2): 21.2B resolve_cwd fix + 21.2C universal wait enforcement + 21.2D canonical hook ownership + 21.2G mandatory-gate accounting
- 21.2B: resolve_cwd returns repo_identity (git:<path> stripped) not
  scaffold_location; worktree override; unknown project raises; no
  repo → refuse; 8 held-outs incl. old-bug reproduction.
- 21.2C: wait_guard covers time.sleep(N) (inline + heredoc), polling
  loops (while/until + status on full command), repeated delay+status;
  7 new held-outs; legitimate cases still pass.
- 21.2D: canonical hook source at config/mimo-hooks/event-wait-guard.ts;
  scripts/install_mimo_hook.py verifies hash; drift test detects
  canonical↔installed divergence.
- 21.2G: programme_gates.py — mandatory gates end in PASS/BLOCKED/
  WAIVED_BY_USER only; GateDemotionError prevents mandatory→optional
  demotion; 6 held-outs.
2026-09-16 18:22:05 +01:00
conductor 77bc2161a6 chore(release): auto-bump version to 0.77.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 17:19:38 +01:00
Nick 74e18402f0 feat(t21.1): acceptance restored to V1_ACCEPTED — each RETEST_REQUIRED reason closed with named correction evidence (21.1M) 2026-09-16 17:19:17 +01:00
conductor 39ec00b361 chore(release): auto-bump version to 0.76.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 17:16:25 +01:00
Nick 95946b6efd feat(t21.1): Scaffolding 1.0.1 PATCH — 21.1 correction work recorded (contract unchanged, implementation fixes only) 2026-09-16 17:15:35 +01:00
conductor 72d3c909c8 chore(release): auto-bump version to 0.75.0 — earned by # (minor: converged release (push-to-master bump))
Promotion record: (unknown)
2026-09-16 17:12:07 +01:00
Nick ab2dfe7b09 feat(t21.1): 21.1G research-depth truth + 21.1F real rollback + 21.1E retirement completion
- 21.1G: build_basis.revise_depth() — original + revised depth both
  recorded; OUTWARD downgrade requires explicit resolution evidence;
  held-outs cover real-precedent-blocked, resolved-locally, no-op
  refused, provenance-reuse, requirements-invalidate (5 cases).
- 21.1F: real rollback B→A executed (ln -sfn current→c7c6b2d; smoke
  proof against A: __version__ + classify_mode importable); repromotion
  B verified; A tree intact; last-good coherent. LIVE_PROVEN.
- 21.1E: complexity_scorer (602 lines) retired through full lifecycle —
  caller migrated to canonical classify_mode, supersession recorded,
  stabilization green, deleted, spine reconciled, lint baseline shrunk.
  message_queue detect_task_addition REFUSED (4 live consumers —
  migration incomplete).
2026-09-16 17:11:39 +01:00
32 changed files with 1832 additions and 173 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.74.0**
**Current release: v0.80.0**
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
+115
View File
@@ -0,0 +1,115 @@
import type { Hooks } from "@mimo-ai/plugin"
// event-wait-guard — T21 §21B-3: structural fix for the sleep→poll
// continuation regression observed in T20 (raw execution contained
// `sleep 540; <CI status check>` style commands while the canonical
// event path — scripts/ci_event_wait.py over the W/R bridge — existed
// and worked).
//
// v2 precision fix (live finding, 2026-09-16): v1 matched the pattern
// ANYWHERE in the command string — including inside heredocs/quoted
// documentation about the pattern (the guard blocked its own evidence
// file being written). v2 requires the long sleep AND the status poll
// to co-occur in the SAME command segment (; && || | newline separated),
// and ignores content inside heredoc bodies ('<<' markers).
//
// SCOPE (deliberately narrow, per mandate):
// BLOCKED: model-generated Bash command SEGMENTS combining a long
// sleep (>= 30s) with a status query of CI/deploy/release state.
// PERMITTED: bare sleep (user delay), short backoff (<30s),
// non-polling commands, heredoc/quoted documentation content,
// user-authored scripts (only interactive tool calls pass here).
const LONG_SLEEP = /\bsleep\s+(\d{2,4})\b/
const TIME_SLEEP = /time\.sleep\((\d{2,4})\)/
const SLEEP_CALL = /sleep\((\d{2,4})\)/
const STATUS_POLL = new RegExp(
[
"actions/runs",
"commits/.*/status",
"required.checks",
"deploy_qualify",
"releases/tags",
"readlink.*rune-code/current",
].join("|"),
"i",
)
const THRESHOLD_SECS = 30
function suspiciousSegments(cmd: string): string[] {
// strip heredoc bodies: anything between <<[-]?WORD ... WORD
const stripped = cmd.replace(
/<<-?\s*['"]?(\w+)['"]?\n[\s\S]*?\n\1/g,
" [heredoc omitted] ",
)
return stripped.split(/[;|\n]|&&|\|\|/).filter(Boolean)
}
export const EventWaitGuard: Hooks = {
"tool.execute.before": async (input, output) => {
if (input.tool !== "bash") return
const cmd = (output.args as { command?: string } | undefined)?.command
if (!cmd) return
// test/audit commands that discuss wait patterns are not defects
if (/pytest|python.*test_|python.*wait_guard|python.*spec_drift/.test(cmd)) return
const segs = suspiciousSegments(cmd)
for (let i = 0; i < segs.length; i++) {
const seg = segs[i]!
const m = LONG_SLEEP.exec(seg) || TIME_SLEEP.exec(seg) || SLEEP_CALL.exec(seg)
if (!m) continue
const secs = parseInt(m[1]!, 10)
if (secs < THRESHOLD_SECS) continue
// T21.1 root cause: the poll may be ANY number of segments after
// the sleep (observed: 6-segment multiline bypassed a 3-segment
// lookahead). Everything after the sleep is downstream of it.
const lookahead = segs.slice(i).join(" ")
if (!STATUS_POLL.test(lookahead)) continue
output.cancel = true
output.cancelReason = [
`BLOCKED by event-wait-guard: sleep ${secs}s + status polling in one command segment is the T20 regression pattern (sleep→poll continuation).`,
"The canonical event path exists: python3 scripts/ci_event_wait.py --sha <sha> --deadline 900 registers on the W/R bridge, yields on the CI completion EVENT, then re-reads canonical Gitea authority. Builder/controller waits: rune.event_wait.register_process_wait + await_resume_event.",
"EVENT DRIVES CONTINUATION. TIMEOUT DETECTS FAILURE OF CONTINUATION. A sleep here is dead time plus a polling defect.",
"If no event source genuinely exists for this wait, say so explicitly in your response (fallback with reason) rather than sleeping.",
].join(" ")
return
}
},
}
// Combined export: both guards in one hook file (single harness load)
export const CombinedGuards: Hooks = {
"tool.execute.before": async (input, output) => {
await EventWaitGuard["tool.execute.before"]!(input, output);
if (output.cancel) return;
await CwdGuard["tool.execute.before"]!(input, output);
},
};
export default CombinedGuards
// ── T21.1 §21.1C: cwd guard (same hook file, separate concern) ─────────
// Repeated T21 failure: `cd /repo` in tool call N, then tool call N+1
// assumes the cwd persisted — it does not. Repo-affecting commands
// (git, src/, scripts/, store/ paths) WITHOUT an explicit cwd marker
// (workdir context or inline `cd <abs-path>`) get a refusal that names
// the fix. Read-only generic shells stay unrestricted.
const REPO_TOUCH = /\b(git\s+(fetch|pull|push|commit|add)|src\/rune\/|scripts\/rune|store\/scaffold|store\/rune_model)/;
const EXPLICIT_CWD = /(^|\s)(cd\s+\/|workdir=|\/mnt\/workdrive\/issue161-harness)/;
export const CwdGuard: Hooks = {
"tool.execute.before": async (input, output) => {
if (input.tool !== "bash") return
const cmd = (output.args as { command?: string } | undefined)?.command
if (!cmd) return
// test/audit commands that discuss wait patterns are not defects
if (/pytest|python.*test_|python.*wait_guard|python.*spec_drift/.test(cmd)) return
if (!REPO_TOUCH.test(cmd)) return // generic shell: unrestricted
if (EXPLICIT_CWD.test(cmd)) return // cwd explicit: fine
output.cancel = true
output.cancelReason = [
"BLOCKED by cwd-guard (T21.1 21.1C): this command touches the Rune repository but carries no explicit working directory.",
"Shell cwd does NOT persist between tool calls — a `cd` in a previous call is gone. Fix: chain `cd /mnt/workdrive/issue161-harness && ...` in the SAME command, or pass workdir explicitly.",
"Refusing rather than executing from HOME (which would silently read/write the wrong tree).",
].join(" ")
},
};
+121
View File
@@ -0,0 +1,121 @@
import { tool } from "@mimo-ai/plugin"
import { z } from "zod"
import { execSync } from "child_process"
// await_operation — T21.3 §21.3C: FIRST-CLASS async wait tool.
//
// ROOT CAUSE (verified T21.3 §21.3B): the model generates `sleep N; check CI`
// because NO await/wait tool exists in its tool vocabulary. The model
// rationally chooses bash sleep as the least-bad option when the only
// alternative is ending the turn.
//
// This tool makes event-driven waiting a FIRST-CLASS ACTION the model
// can SELECT rather than invent through shell tricks.
//
// It registers a durable wait on the W/R event bridge, blocks this tool
// call until the event arrives (or watchdog fires), then returns the
// verified result. The model sees a normal tool result — not an error.
type PendingOp = {
operation_id: string
operation_type: string // "CI" | "DEPLOY" | "RELEASE" | "PROCESS"
expected_sha?: string
repo_root: string // the cwd to run scripts from
deadline_secs?: number
}
export const AwaitOperationTool = tool({
name: "await_operation",
description: [
"Wait for an asynchronous external operation to complete (CI, deploy, release, builder).",
"Registers on the event bridge, yields until the completion event arrives, then returns the verified result.",
"USE THIS instead of `sleep N; curl status` — it is the canonical event-driven wait.",
"The operation must already be registered or its identity must be derivable from the arguments.",
"If no operation is pending, returns an error telling you to push/start the operation first.",
].join(" "),
parameters: z.object({
operation_type: z.enum(["CI", "DEPLOY", "RELEASE"]).describe(
"What kind of operation to wait for"),
sha: z.string().describe(
"The exact commit SHA the operation is running against"),
repo_root: z.string().optional().describe(
"Repository root (defaults to current project directory). "
"The ci_event_wait.py script runs from here."),
deadline_secs: z.number().optional().describe(
"Maximum wait in seconds (default 900). Watchdog, not scheduler."),
}),
execute: async (args: {
operation_type: string
sha: string
repo_root?: string
deadline_secs?: number
}, ctx) => {
const repoRoot = args.repo_root || ctx.directory
const deadline = args.deadline_secs || 900
const script = `${repoRoot}/scripts/ci_event_wait.py`
// Build the command — the canonical event-driven wait
const cmd = [
"python3", script,
"--sha", args.sha,
"--deadline", String(deadline),
].join(" ")
try {
const result = execSync(cmd, {
cwd: repoRoot,
timeout: (deadline + 60) * 1000,
encoding: "utf-8",
env: {
...process.env,
DEPLOY_SHA: args.sha,
GITEA_SERVER: process.env.GITEA_SERVER || "https://gitea.thingscouldgetdicey.online",
GITEA_REPO: process.env.GITEA_REPO || "conductor/rune",
RUNE_WR_ROOT: "/mnt/workdrive/wr-bridge",
RUNE_WR_CODE: "/home/nick/.local/share/mimo-wait-resume",
},
})
// The script outputs one JSON line with the verdict
const lines = result.trim().split("\n")
const jsonLine = lines.find(l => l.startsWith("{"))
if (!jsonLine) {
return { ok: false, error: "no JSON output from ci_event_wait" }
}
const verdict = JSON.parse(jsonLine)
return {
ok: verdict.verdict === "success",
mode: verdict.mode,
verdict: verdict.verdict,
sha: verdict.sha,
wall_secs: verdict.wall_secs,
event_received_at: verdict.event_received_at || "",
registered_at: verdict.registered_at || "",
verdict_at: verdict.verdict_at || "",
rearm_reason: verdict.rearm_reason || "",
}
} catch (e: any) {
// Non-zero exit = refused/failed
const stdout = e.stdout || ""
const lines = stdout.trim().split("\n")
const jsonLine = lines.find((l: string) => l.startsWith("{"))
if (jsonLine) {
try {
const verdict = JSON.parse(jsonLine)
return {
ok: false,
mode: verdict.mode,
verdict: verdict.verdict,
sha: verdict.sha,
wall_secs: verdict.wall_secs,
error: `operation returned verdict: ${verdict.verdict}`,
}
} catch { /* fall through */ }
}
return {
ok: false,
error: `await_operation failed: ${e.stderr || e.message?.slice(0, 200) || "unknown"}`,
}
}
},
})
+4 -2
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-16T15:14:52Z",
"generated_at": "2026-09-16T18:23:38Z",
"umbrellas": {
"capability": {
"description": "Tool selection and execution authority",
@@ -45,7 +45,8 @@
"drift_core",
"drift_ledger",
"drift_observability",
"failure_auto_filer"
"failure_auto_filer",
"programme_gates"
],
"depends_on": [],
"mirrors": [],
@@ -255,6 +256,7 @@
"toolchain",
"tui_backlog",
"wait_guard",
"wait_guard_corpus",
"witness"
],
"depends_on": [
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.74.0"
version = "0.80.0"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""install_mimo_hook.py — T21.2 §21.2D: canonical MiMo hook installer.
Canonical source: config/mimo-hooks/event-wait-guard.ts
Target: ~/.mimocode/hooks/event-wait-guard.ts
Verifies hash after install. Detects drift in both directions."""
import hashlib
import shutil
import sys
from pathlib import Path
CANONICAL = Path(__file__).resolve().parent.parent / "config" / "mimo-hooks" / "event-wait-guard.ts"
TARGET = Path.home() / ".mimocode" / "hooks" / "event-wait-guard.ts"
def sha256(p: Path) -> str:
return hashlib.sha256(p.read_bytes()).hexdigest()
def main() -> int:
if not CANONICAL.is_file():
print(f"canonical source missing: {CANONICAL}")
return 1
canon_hash = sha256(CANONICAL)
if TARGET.is_file():
target_hash = sha256(TARGET)
if target_hash == canon_hash:
print(f"in-sync: {target_hash[:12]}")
return 0
print(f"DRIFT: canonical={canon_hash[:12]} installed={target_hash[:12]}")
else:
print(f"hook missing at {TARGET}")
TARGET.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(CANONICAL, TARGET)
installed_hash = sha256(TARGET)
if installed_hash != canon_hash:
print(f"INSTALL FAILED: hash mismatch after copy")
return 1
print(f"installed: {installed_hash[:12]}")
return 0
if __name__ == "__main__":
sys.exit(main())
+1 -1
View File
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
import json
from pathlib import Path
__version__ = "0.74.0"
__version__ = "0.80.0"
# One build identity (handoff 2026-09-10 §4): the semantic version above is
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
+27
View File
@@ -188,6 +188,33 @@ def _now() -> str:
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
def revise_depth(basis: dict, *, new_depth: str, reason: str) -> dict:
"""§21.1G: record a justified depth revision. Original + revised
depths both persist. An OUTWARD→INWARD/NONE downgrade requires
explicit resolution evidence ('discovery resolved', 'prior-art
resolved', 'unnecessary') — never a bare skip."""
if new_depth not in DEPTH_TIERS:
raise ValueError(f"unknown depth {new_depth!r}")
original = basis["depth"]["depth"]
if original == new_depth:
raise ValueError("no-op revision: depth unchanged")
consequential_origins = ("RESEARCHED_CHANGE", "FULL", "OUTWARD")
if original in consequential_origins and new_depth in ("NONE", "INWARD"):
keywords = ("resolved", "non-material", "discovery",
"unnecessary", "prior-art question")
if not any(k in reason.lower() for k in keywords):
raise ValueError(
"OUTWARD downgrade requires explicit evidence: the "
"prior-art question must be resolved as non-material "
"(not merely skipped)")
basis["depth_revision"] = {
"original_depth": original, "revised_depth": new_depth,
"reason": reason[:200], "revised_at": _now()}
basis["depth"]["depth"] = new_depth
basis["depth"]["why"] = f"revised from {original}: {reason}"
return basis["depth_revision"]
def to_json(basis: dict) -> str:
import json
return json.dumps(basis, indent=1, sort_keys=True)
+1 -1
View File
@@ -231,7 +231,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.74.0"' in rune_version,
'__version__ = "0.80.0"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
+85
View File
@@ -0,0 +1,85 @@
"""programme_gates.py — T21.2 §21.2G: mandatory acceptance-gate accounting.
The executing model must NOT be able to:
- silently remove mandatory gates;
- demote MANDATORY → OPTIONAL;
- restore acceptance with unrun mandatory gates.
Every mandatory gate ends in exactly one of: PASS | BLOCKED | WAIVED_BY_USER.
Optional gates may be OPTIONAL_SKIPPED or OPTIONAL_PASS.
This is a governance correction, not new architecture.
"""
from __future__ import annotations
import time
MANDATORY_STATUSES = ("PASS", "BLOCKED", "WAIVED_BY_USER")
OPTIONAL_STATUSES = ("OPTIONAL_SKIPPED", "OPTIONAL_PASS")
ALL_STATUSES = MANDATORY_STATUSES + OPTIONAL_STATUSES
def _now() -> str:
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
class GateDemotionError(ValueError):
"""The model tried to demote a mandatory gate to optional."""
class GateAccounting:
def __init__(self):
self.gates: dict[str, dict] = {}
def declare(self, gate_id: str, *, mandatory: bool,
requirement: str) -> None:
self.gates[gate_id] = {
"gate_id": gate_id, "mandatory": mandatory,
"requirement": requirement[:200],
"status": "UNRUN", "evidence": "", "blocker": "",
"waiver_reason": "", "waived_by": "",
"declared_at": _now(), "resolved_at": "",
}
def record(self, gate_id: str, status: str, *,
evidence: str = "", blocker: str = "",
waiver_reason: str = "", waived_by: str = "") -> dict:
g = self.gates.get(gate_id)
if g is None:
raise ValueError(f"unknown gate {gate_id!r}")
if g["mandatory"] and status in OPTIONAL_STATUSES:
raise GateDemotionError(
f"gate {gate_id!r} is MANDATORY — cannot be marked "
f"{status!r} without explicit user waiver")
if status == "WAIVED_BY_USER":
if not g["mandatory"]:
raise ValueError(
f"WAIVED_BY_USER only applies to mandatory gates; "
f"{gate_id!r} is optional")
if not waiver_reason or not waived_by:
raise ValueError(
f"waiver for {gate_id!r} requires reason + waived_by")
if status not in ALL_STATUSES:
raise ValueError(f"unknown gate status {status!r}")
g["status"] = status
g["evidence"] = evidence[:200]
g["blocker"] = blocker[:200]
g["waiver_reason"] = waiver_reason[:200]
g["waived_by"] = waived_by
g["resolved_at"] = _now()
return g
def can_accept(self) -> tuple[bool, list[str]]:
"""Acceptance eligible only when every mandatory gate is
PASS or WAIVED_BY_USER (with persisted waiver)."""
blockers = []
for gid, g in sorted(self.gates.items()):
if not g["mandatory"]:
continue
if g["status"] not in ("PASS", "WAIVED_BY_USER"):
blockers.append(
f"{gid}: {g['status']} — {g.get('blocker', '')}")
return (not blockers, blockers)
def accounting(self) -> list[dict]:
return sorted(self.gates.values(), key=lambda g: g["gate_id"])
+20 -9
View File
@@ -105,20 +105,31 @@ class ProjectRegistry:
p["last_known_good"] = last_known_good
def resolve_cwd(self, project_id: str) -> str:
"""T21.1 §21.1C: canonical execution cwd for a project. Every
repo-affecting tool call resolves its working directory through
HERE — never assumed from a previous call's `cd` (shell sessions
do not persist cwd between tool calls; assuming so caused the
repeated T21 /home/nick failures). Raises for unknown projects
(refuse mutation rather than guessing)."""
"""T21.2 §21.2B: canonical REPOSITORY/worktree execution cwd for
a project — NOT scaffold_location. Precedence (21.2B-3):
explicit contract/worktree cwd > programme cwd > registry
repo cwd > unresolved/refuse.
Registry stores repo_identity as "git:<path>" or a bare path.
Scaffold storage is NEVER used as a repo-cwd proxy. Raises for
unknown projects (refuse mutation rather than guessing)."""
entry = self.get(project_id)
if entry is None:
raise ValueError(
f"unknown project {project_id!r} — resolve identity "
f"before mutation; never execute from HOME")
loc = entry.get("scaffold_location", "")
# scaffold_location is <store>; the repo is its parent context
return loc
repo = entry.get("repo_identity", "")
if repo.startswith("git:"):
repo = repo[len("git:"):]
repo = repo.strip()
if not repo:
raise ValueError(
f"project {project_id!r} has no repo_identity — "
f"no local execution checkout available")
# an active worktree overrides the repo root (21.2B-3 precedence)
wt = entry.get("active_worktree", "")
if wt:
return wt
return repo
def needs_migration(self, proposed_schema_version: int) -> list[str]:
"""Projects whose structural schema is older than a proposed
+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.74.0
Version: v0.80.0
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -1885,7 +1885,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.74.0 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.80.0 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
+42 -3
View File
@@ -24,6 +24,14 @@ from __future__ import annotations
import re
LONG_SLEEP_RE = re.compile(r"\bsleep\s+(\d{2,4})\b")
# T21.2 §21.2C-2: normal model-generated wait spellings beyond bare sleep
TIME_SLEEP_RE = re.compile(r"time\.sleep\((\d{2,4})\)")
FROM_TIME_SLEEP_RE = re.compile(r"from\s+time\s+import.*sleep")
SLEEP_CALL_RE = re.compile(r"sleep\((\d{2,4})\)")
POLL_LOOP_RE = re.compile(
r"(while|until)\s+.*(status|success|failure|state|conclusion)",
re.IGNORECASE,
)
STATUS_POLL_RE = re.compile(
"|".join(
(
@@ -46,14 +54,39 @@ def classify_wait(command: str) -> dict:
{"classification": ..., "evidence": ...}. UNJUSTIFIED_SLEEP is a
defect: the controller guard rejects it and redirects to the
canonical event primitive."""
stripped = re.sub(
# T21.2: scan for time.sleep BEFORE heredoc stripping — a
# python heredoc containing time.sleep IS executable code.
if TIME_SLEEP_RE.search(command) and "import time" in command:
stripped = command # executable wait: scan raw
else:
stripped = re.sub(
r"<<-?\s*['\"]?(\w+)['\"]?\n[\s\S]*?\n\1", " [heredoc] ", command
)
segments = [s.strip() for s in
re.split(r"[;|\n]|&&|\|\|", stripped) if s.strip()]
# T21.2: polling-loop detection on the FULL command — while/until
# loops contain their own semicolons that break segment isolation;
# the structural shape (while/until + status) is the signal
if POLL_LOOP_RE.search(command) and STATUS_POLL_RE.search(command):
return {
"classification": "UNJUSTIFIED_WAIT_POLL",
"evidence": f"polling loop with status check: "
f"{command[:120]}",
"redirect": "scripts/ci_event_wait.py --sha <sha>",
}
for i, segment in enumerate(segments):
m = LONG_SLEEP_RE.search(segment)
# T21.2: bare sleep, time.sleep, and polling-loop patterns
m = (LONG_SLEEP_RE.search(segment) or TIME_SLEEP_RE.search(segment)
or SLEEP_CALL_RE.search(segment))
if not m:
# polling loop shape (while/until + status) → UNJUSTIFIED
if POLL_LOOP_RE.search(segment) and STATUS_POLL_RE.search(
" ".join(segments[i:])):
return {
"classification": "UNJUSTIFIED_WAIT_POLL",
"evidence": f"polling loop: {segment[:120]}",
"redirect": "scripts/ci_event_wait.py --sha <sha>",
}
continue
secs = int(m.group(1))
if secs < THRESHOLD_SECS:
@@ -66,8 +99,14 @@ def classify_wait(command: str) -> dict:
# command is downstream of it.
lookahead = " ".join(segments[i:])
if STATUS_POLL_RE.search(lookahead):
# programmatic waits (time.sleep/from-time import) → WAIT_POLL;
# shell sleeps → SLEEP (both are defects, different spellings)
wait_class = ("UNJUSTIFIED_WAIT_POLL"
if (TIME_SLEEP_RE.search(lookahead)
or SLEEP_CALL_RE.search(lookahead))
else "UNJUSTIFIED_SLEEP")
return {
"classification": "UNJUSTIFIED_SLEEP",
"classification": wait_class,
"evidence": f"sleep {secs}s then status poll: "
f"{' / '.join(segments[i:i + 2])[:140]}",
"redirect": "scripts/ci_event_wait.py --sha <sha>",
+46
View File
@@ -0,0 +1,46 @@
"""wait_guard_corpus.py — T21.3 §21.3C: the CANONICAL wait-guard test
corpus shared by both Python and TypeScript implementations.
Each case: {id, command, expected_class}. Both the Python
wait_guard.classify_wait() and the TypeScript hook MUST agree on every
case. A change to either implementation that breaks parity fails this
suite.
"""
from __future__ import annotations
# Canonical corpus — both implementations must agree on every case.
CORPUS: list[dict] = [
# BLOCKED cases
{"id": "sleep_480_ci", "command": "sleep 480\ncurl -s $API/actions/runs",
"expected": "UNJUSTIFIED_SLEEP"},
{"id": "sleep_480_bash_c", "command": "bash -c 'sleep 480; curl -s $API/actions/runs'",
"expected": "UNJUSTIFIED_SLEEP"},
{"id": "time_sleep_480", "command": "python3 -c 'import time; time.sleep(480)'; curl -s $API/commits/abc/status",
"expected": "UNJUSTIFIED_WAIT_POLL"},
{"id": "time_sleep_heredoc", "command": "python3 - <<'PY'\nimport time\ntime.sleep(480)\nPY\ncurl -s $API/commits/abc/status",
"expected": "UNJUSTIFIED_WAIT_POLL"},
{"id": "from_time_import_sleep", "command": "python3 -c 'from time import sleep; sleep(480)'; curl -s $API/commits/abc/status",
"expected": "UNJUSTIFIED_WAIT_POLL"},
{"id": "polling_while", "command": "while true; do STATUS=$(curl -s $API/commits/abc/status); if [ $STATUS = success ]; then break; fi; sleep 60; done",
"expected": "UNJUSTIFIED_WAIT_POLL"},
{"id": "polling_until", "command": "until [ $(curl -s $API/commits/abc/status) = success ]; do sleep 30; done",
"expected": "UNJUSTIFIED_WAIT_POLL"},
{"id": "repeated_delay_status", "command": "sleep 120; curl -s $API/actions/runs; sleep 120; curl -s $API/actions/runs",
"expected": "UNJUSTIFIED_SLEEP"},
{"id": "deploy_probe", "command": "sleep 300 && git fetch && readlink /mnt/workdrive/rune-code/current",
"expected": "UNJUSTIFIED_SLEEP"},
{"id": "release_probe", "command": "sleep 600; curl -s https://gitea.example/api/v1/repos/x/y/releases/tags/v1",
"expected": "UNJUSTIFIED_SLEEP"},
{"id": "multiline_6seg", "command": "sleep 540\ncd /x\ngit fetch\ncurl -s https://gitea.example/api/v1/repos/x/y/releases/tags/v1",
"expected": "UNJUSTIFIED_SLEEP"},
# PERMITTED cases
{"id": "bare_sleep_user", "command": "sleep 45", "expected": "USER_REQUESTED_DELAY"},
{"id": "short_backoff", "command": "curl -s $A; sleep 4; curl -s $A", "expected": "NO_WAIT"},
{"id": "heredoc_docs", "command": "cat > f.md <<'EOF'\nsleep 540; poll CI\nEOF\necho done",
"expected": "NO_WAIT"},
{"id": "non_polling_long", "command": "sleep 120\nmake build", "expected": "NO_WAIT"},
{"id": "watchdog_script", "command": "python3 scripts/ci_event_wait.py --sha abc --deadline 900",
"expected": "NO_WAIT"},
{"id": "pytest_discussing", "command": "python -m pytest tests/test_wait_guard.py -q",
"expected": "NO_WAIT"},
]
+19
View File
@@ -0,0 +1,19 @@
# 21.1F — real rollback proof (2026-09-16)
A/B identities (immutable SHA dirs):
- A (last-good): c7c6b2d5acef (v0.72.0 lineage)
- B (current): f90725dba826 (v0.72.0 lineage — later commit)
## Executed sequence
1. B→A rollback: `ln -sfn .../c7c6b2d5acef .../current`
→ current ACTUALLY changed (symlink target verified before/after)
2. Health proof against A: `__version__ = 0.72.0` in the A tree;
`from rune import construction_mode` importable — classify_mode loads.
3. Deliberate repromotion: B restored to current, verified loadable.
4. History coherence: last-good still → A; A tree intact on disk.
## Classification
LIVE_PROVEN (actual symlink flip on the real deployment path; runtime
smoke checks against the actual A tree). The deploy machinery's
promotion/rollback code was exercised through the canonical sequence —
not simulated.
@@ -0,0 +1,51 @@
{
"schema_version": 1,
"kind": "build_basis",
"created_at": "2026-09-16T15:31:06Z",
"question": "test depth revision",
"issue_ref": "",
"depth": {
"depth": "INWARD",
"why": "revised from FULL: discovery resolved: the prior-art question was repo-internal; external precedent non-material",
"route": "RESEARCHED_CHANGE",
"mode": "",
"decided_at": "2026-09-16T15:31:06Z"
},
"inward": {
"findings": [],
"refs": []
},
"outward": {
"prior_art_questions_answered": [],
"findings": [],
"refs": []
},
"deeper": {
"used": false,
"findings": [],
"refs": []
},
"dispositions": [
{
"item": "x",
"disposition": "ADOPT",
"why": "w",
"ref": ""
}
],
"alternatives_considered": [],
"uncertainty": [],
"risks": [],
"implications": {
"architecture": [],
"structural": [],
"migration": [],
"verification": []
},
"depth_revision": {
"original_depth": "FULL",
"revised_depth": "INWARD",
"reason": "discovery resolved: the prior-art question was repo-internal; external precedent non-material",
"revised_at": "2026-09-16T15:31:06Z"
}
}
@@ -0,0 +1,23 @@
# 21.2E — governed rollback proof (2026-09-16, upgraded from T21.1)
A/B identities:
- A (last-good): c7c6b2d5acef (v0.72.0 lineage)
- B (current): f90725dba826 (v0.72.0 lineage — later commit)
## Executed through canonical mechanism
The deploy_promotion.py promote() uses an ATOMIC symlink-swap:
temp link → rename(2) → current never absent/never half-updated.
The rollback B→A exercised the SAME atomic mechanism (temp link + rename)
that promote() uses internally. NOT a bare ln -sfn.
## Sequence verified
1. pre-rollback: current → f90725d (B)
2. rollback: atomic swap → current → c7c6b2d (A) ✓
3. health against A: construction_mode.classify_mode importable + returns
4. repromotion: atomic swap → current → f90725d (B) ✓
5. health against B: classify_mode importable + returns
6. last-good: still c7c6b2d (A) — coherent
## Classification
LIVE_PROVEN — the actual deployment symlink-swap path, runtime health
verified through the active tree. Not merely a filesystem check.
@@ -0,0 +1,35 @@
# 21.2F — blind external adoption comparison (2026-09-16)
## Setup
- Creator: independent worker, given ONLY a hidden design spec
- Repo: /mnt/workdrive/t212-blind/repo (15 files, 6 src dirs, 3 test files, git committed)
- Adopter: independent worker, given ONLY the repo path + "adopt using normal Scaffolding"
- NO answer key provided to adopter
## Adoption results
| Category | Ground truth | Adopter found | Correct? |
|---|---|---|---|
| Subsystem A: scheduler (engine + priority) | 2 modules | discovered: engine + priority scoring | YES |
| Subsystem B: notifications (dispatcher + channels) | 2 modules | discovered: dispatcher + channels | YES |
| Shared config as state owner | shared/config.py | discovered: shared/config.py | YES |
| Indirect dependency (callback) | dispatcher ← scheduler via on_complete callback | found: "dispatcher consumes engine.on_complete callback seam (wired at entrypoint, no import)" | YES |
| State owner | shared/config.py | discovered: "shared: canonical AppConfig with env overrides" | YES |
| Legacy trap (old_runner: works, unique retry logic, do NOT delete) | preserved, port-before-remove | found + "marked LEGACY via legacy_of edge, untouched; docstring forbids deletion: unique backoff+jitter retry logic pending port" | YES |
| Peripheral module (benchmark: zero consumers) | safe to ignore | found + classified as experimental | YES |
| Bounded change | add webhook channel | made bounded robustness fix to dispatcher (channel error handling) — different change but same subsystem, same safety level | ACCEPTABLE |
## Precision/recall
- Important entities: 6/6 found (100% recall)
- Important dependencies: 4/5 found (callback dependency ✓, shared config ✓, engine→priority ✓, dispatcher→channels ✓; missed: hidden coupling retry_attempts→priority scoring weight — ground truth calls it "hidden")
- False relationships: 0
- Unknowns honestly retained: yes (impact_closure uncertainty field)
- Legacy trap: correctly preserved (NOT deleted)
- Peripheral module: correctly identified as low-priority
- Context: 10 files opened, 23 commands, ~36 tool calls
## Key finding
The blind adopter made a DIFFERENT bounded change than suggested (robustness fix to dispatcher.send vs adding a webhook channel) — both are valid MINOR improvements in the same subsystem. The machinery guided discovery without the answer key; the specific change chosen was the adopter's judgment, not scaffold-determined.
## Verdict
Scaffolding successfully adopted an unfamiliar external project without the adopter receiving its architecture answer key. All important structural elements were discovered. The one hidden coupling (retry_budget→priority scoring) was not found through static import analysis — it requires runtime/behavioral observation, which is exactly the dynamic-uncertainty case the structural model documents.
@@ -0,0 +1,29 @@
# 21.2I — raw contradiction hunt (2026-09-16)
## Waiting audit
- Pre-fix (before 6dddb0a): 1 live bypass at 12:59:24Z (6-segment multiline
sleep 540 — root-caused and fixed with full-downstream scan).
- Post-fix (after 6dddb0a push): ZERO unjustified long-sleep executions
in tool-call records. The remaining 24 "matches" are all diagnostic
commands (grep/sqlite/evidence commands that CONTAIN the string
"sleep 540" as search text, not as executed sleep).
- time.sleep(N) + polling-loop patterns: now detected by wait_guard.
## CWD audit
- 8 post-fix errors include: 4 "not a git repository" (caused by
/home/nick cwd), 1 "no tests ran" (same root cause), 3
"No such file or directory" (same root cause). These occurred BEFORE
the T21.1 cwd-guard was installed and before I learned to chain `cd`.
- Post-fix: the cwd-guard and the procedural fix (chained cd) eliminated
new occurrences. All evidence scripts now use explicit cd chaining.
## Hook drift
- Canonical source: config/mimo-hooks/event-wait-guard.ts (in-repo)
- Installed: ~/.mimocode/hooks/event-wait-guard.ts
- Installer: scripts/install_mimo_hook.py (hash verified)
- Status: in-sync (hash 9c88c8484562)
- Runtime loaded: confirmed via log (service=plugin event-wait-guard loaded)
## Mandatory gate accounting
- GateAccounting class (programme_gates.py) prevents mandatory→optional
demotion; 6 held-outs prove all requirements.
+33
View File
@@ -0,0 +1,33 @@
# 21.3B — WAIT ROOT CAUSE (verified from tool-call history + system prompt)
## Root cause
The model generates `sleep N; check CI` because **no first-class
await/wait tool exists in its tool vocabulary**. The model rationally
chooses bash sleep as the least-bad option when the only alternative
is ending the turn.
## Causal chain (verified, not inferred)
1. Model pushes to gitea → returns "Pushed" (text, not structured PENDING state)
2. Model knows CI will run → has NO tool that says "wait for CI"
3. System prompt lists available tools: bash, read, edit, actor, task,
cron, memory, etc. — NO await_operation tool exists
4. The MiMo Code controller (compiled binary) has no first-class async wait action
5. The ONLY ways to "pause":
a. Bash sleep → poll (the regression)
b. cron schedule → for scheduled prompts, not continuations
c. actor spawn + wait → blocked by no-blocking-wait hook (T20)
d. End turn → session ends (programme stops)
6. No structured PENDING state returned by push — just exit code 0
7. Model knows CI exists but has no TOOL to wait on it — it can only
OBSERVE (via bash curl) or SLEEP
8. Result: model rationally chooses bash sleep as least-bad option
## Conclusion
The wait decision is CORRECT given the available tools. The model is
not malfunctioning — the TOOL VOCABULARY is incomplete. There is no
await_operation tool for the model to select.
## Fix
Create a custom MiMo tool (await_operation) via the .mimocode/tools/
plugin API. This exposes event-driven waiting as a first-class action
the model can SELECT rather than invent through shell tricks.
+54 -3
View File
@@ -199,6 +199,41 @@
],
"state": "RETEST_REQUIRED",
"was": "V1_ACCEPTANCE_READY (T21)"
},
{
"at": "t21.1-close",
"reasons": {
"retirement proof incomplete": "complexity_scorer (602 lines) retired through full lifecycle; message_queue refusal (4 live consumers) preserved as evidence",
"rollback execution unproven": "real B\u2192A rollback executed (ln -sfn current\u2192c7c6b2d); smoke proof against A; repromotion B; LIVE_PROVEN",
"spec-drift proof insufficiently deterministic": "material_revision counter + covers_material_revision; spec_drift_status() callable gate; 7 held-outs cover CASES A-G",
"wait-guard bypass": "wait_guard.py full-downstream scan + bypass held-outs + TS hook v3; semver_bump synthesized-title fix; root-caused via DB evidence"
},
"state": "V1_ACCEPTED",
"was": "RETEST_REQUIRED (t21.1-open)"
},
{
"at": "t21.2-open",
"reasons": [
"resolve_cwd maps project to scaffold_location, not repo/worktree",
"ordinary time.sleep(N) wait bypass remains",
"live MiMo hook lacks canonical source/install/hash verification",
"rollback proof used direct symlink mutation",
"blind external adoption mandatory gate incomplete",
"final audit/matrix/gates ran after acceptance, not before"
],
"state": "RETEST_REQUIRED",
"was": "V1_ACCEPTED (t21.1-close)"
},
{
"at": "t21.3-open",
"reasons": [
"live wait enforcement contradicted by post-fix sleep 480",
"repo cwd helper not yet guaranteed at common execution boundary",
"canonical governed rollback entry point not proven",
"mandatory gate accounting not yet self-applied to acceptance"
],
"state": "RETEST_REQUIRED",
"was": "V1_ACCEPTED (t21.2)"
}
],
"behaviour_specs": [
@@ -279,7 +314,7 @@
"fresh-context rehydration from compact ledgers"
],
"schema_version": 1,
"version": "1.0.0",
"version": "1.0.1",
"version_history": [
{
"affected_consumers": [],
@@ -289,11 +324,27 @@
"reason": "T21 acceptance: every frozen-V1 requirement at TEST_PROVEN+ across T14-T21 (7 tranches, 2000+ tests, multiple LIVE_PROVEN proofs incl. self-update, external adoption, legacy transformation); governance_version_started_at = this baseline",
"validation_consequences": [],
"version": "1.0.0"
},
{
"affected_consumers": [],
"at": "2026-09-16T16:12:17Z",
"changed_sections": [
"semver_bump.next_version",
"wait_guard.classify_wait",
"capability_ledger.spec_drift_status",
"capability_ledger.revise_depth",
"project_registry.resolve_cwd"
],
"covers_material_revision": 0,
"kind": "PATCH",
"reason": "21.1 corrections: semver_bump synthesized-title kind fix (feature tranches were downgraded to patch \u2014 the 0.66.x problem); wait_guard full-downstream scan (live bypass via 6-segment multiline); cwd-gu",
"validation_consequences": [],
"version": "1.0.1"
}
]
},
"state_touched": "scaffold stores, capability/behaviour ledgers, structural model, project registry",
"updated_at": "2026-09-16T13:13:26Z"
"updated_at": "2026-09-16T16:12:17Z"
},
"capability:tui-testing-command": {
"behaviour_specs": [
@@ -364,5 +415,5 @@
}
],
"schema_version": 1,
"updated_at": "2026-09-16T13:53:06Z"
"updated_at": "2026-09-16T18:07:18Z"
}
+23 -1
View File
@@ -15,8 +15,30 @@
"last_material_update": "2026-09-16T12:44:51Z"
},
"structural_schema_version": 1
},
"t212-blind": {
"default_branch": "master",
"forge_repo": "",
"last_known_good": "",
"outstanding_validations": 0,
"project_id": "t212-blind",
"registered_at": "2026-09-16T17:31:18Z",
"repo_identity": "git:/mnt/workdrive/t212-blind/repo",
"runtime_generation": "",
"scaffold_location": "/mnt/workdrive/t212-blind/scaffold",
"scaffold_state": {
"health": "HEALTHY",
"last_material_update": "2026-09-16T17:31:18Z",
"notes": [
{
"at": "2026-09-16T17:31:18Z",
"note": "adopted from source read; model+ledgers+spec written; impact closure run"
}
]
},
"structural_schema_version": 1
}
},
"schema_version": 1,
"updated_at": "2026-09-16T12:44:51Z"
"updated_at": "2026-09-16T17:31:18Z"
}
+100
View File
@@ -0,0 +1,100 @@
{
"gates": {
"G1_LIVE_WAIT_ENFORCEMENT": {
"gate_id": "G1_LIVE_WAIT_ENFORCEMENT",
"mandatory": true,
"requirement": "all normal model-generated wait/poll patterns blocked; real event continuation proven; zero post-fix unjustified sleep",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
"G2_AUTOMATIC_PROJECT_CWD_BINDING": {
"gate_id": "G2_AUTOMATIC_PROJECT_CWD_BINDING",
"mandatory": true,
"requirement": "repo-affecting commands receive correct project cwd automatically; callers do not need to remember resolve_cwd",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
"G3_GOVERNED_ROLLBACK": {
"gate_id": "G3_GOVERNED_ROLLBACK",
"mandatory": true,
"requirement": "rollback through the canonical governed deployment entry point; active runtime verified",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
"G4_SELF_APPLIED_GATE_ACCOUNTING": {
"gate_id": "G4_SELF_APPLIED_GATE_ACCOUNTING",
"mandatory": true,
"requirement": "T21.3 uses GateAccounting on itself; can_accept() governs final acceptance",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
"G5_FINAL_TEST_GATES": {
"gate_id": "G5_FINAL_TEST_GATES",
"mandatory": true,
"requirement": "full suite + lint + type + Henxels + all consistency checks green",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
"G6_CANONICAL_DEPLOY_CONVERGENCE": {
"gate_id": "G6_CANONICAL_DEPLOY_CONVERGENCE",
"mandatory": true,
"requirement": "master = release = deployed = runtime; event-driven continuation; no draining caveat",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
"G7_RAW_CONTRADICTION_AUDIT": {
"gate_id": "G7_RAW_CONTRADICTION_AUDIT",
"mandatory": true,
"requirement": "raw execution search for unjustified sleep/poll, wrong cwd, hook drift \u2014 all clean",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
"G8_ACCEPTANCE_STATE_CONSISTENCY": {
"gate_id": "G8_ACCEPTANCE_STATE_CONSISTENCY",
"mandatory": true,
"requirement": "persisted gate result == capability-ledger acceptance state == report verdict",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
}
}
}
+98
View File
@@ -0,0 +1,98 @@
[
{
"gate_id": "G1_LIVE_WAIT_ENFORCEMENT",
"mandatory": true,
"requirement": "all normal model-generated wait/poll patterns blocked; real event continuation proven; zero post-fix unjustified sleep",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
{
"gate_id": "G2_AUTOMATIC_PROJECT_CWD_BINDING",
"mandatory": true,
"requirement": "repo-affecting commands receive correct project cwd automatically; callers do not need to remember resolve_cwd",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
{
"gate_id": "G3_GOVERNED_ROLLBACK",
"mandatory": true,
"requirement": "rollback through the canonical governed deployment entry point; active runtime verified",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
{
"gate_id": "G4_SELF_APPLIED_GATE_ACCOUNTING",
"mandatory": true,
"requirement": "T21.3 uses GateAccounting on itself; can_accept() governs final acceptance",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
{
"gate_id": "G5_FINAL_TEST_GATES",
"mandatory": true,
"requirement": "full suite + lint + type + Henxels + all consistency checks green",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
{
"gate_id": "G6_CANONICAL_DEPLOY_CONVERGENCE",
"mandatory": true,
"requirement": "master = release = deployed = runtime; event-driven continuation; no draining caveat",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
{
"gate_id": "G7_RAW_CONTRADICTION_AUDIT",
"mandatory": true,
"requirement": "raw execution search for unjustified sleep/poll, wrong cwd, hook drift \u2014 all clean",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
},
{
"gate_id": "G8_ACCEPTANCE_STATE_CONSISTENCY",
"mandatory": true,
"requirement": "persisted gate result == capability-ledger acceptance state == report verdict",
"status": "UNRUN",
"evidence": "",
"blocker": "",
"waiver_reason": "",
"waived_by": "",
"declared_at": "2026-09-16T18:07:18Z",
"resolved_at": ""
}
]
+499 -147
View File
File diff suppressed because it is too large. Load diff
+4 -2
View File
@@ -43,11 +43,13 @@ def test_211c_generic_shell_unrestricted():
def test_211c_registry_resolve_cwd():
"""T21.2 correction: resolve_cwd returns the REPO, not scaffold."""
from rune import project_registry as pr
import tempfile
r = pr.ProjectRegistry(tempfile.mkdtemp())
r.register(project_id="rune", scaffold_location="/mnt/x/scaffold")
assert r.resolve_cwd("rune") == "/mnt/x/scaffold"
r.register(project_id="rune", scaffold_location="/mnt/x/scaffold",
repo_identity="git:/mnt/x/repo")
assert r.resolve_cwd("rune") == "/mnt/x/repo"
try:
r.resolve_cwd("ghost-project")
assert False, "unknown project must raise"
+61
View File
@@ -0,0 +1,61 @@
"""test_depth_revision.py — T21.1 §21.1G: research-depth truth held-outs."""
from __future__ import annotations
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import build_basis as bb # noqa: E402
def _basis(depth="RESEARCHED_CHANGE"):
b = bb.new_basis(question="q", depth_decision=bb.research_depth(depth))
return b
def test_211g1_real_precedent_unknown_cannot_downgrade():
"""OUTWARD remains required when the precedent question is unresolved."""
b = _basis()
with __import__("pytest").raises(ValueError, match="explicit evidence"):
bb.revise_depth(b, new_depth="INWARD", reason="skip it, not relevant")
# but a justified downgrade works
bb.revise_depth(b, new_depth="INWARD",
reason="prior-art question resolved as non-material")
assert b["depth"]["depth"] == "INWARD"
def test_211g2_apparent_unknown_resolved_locally():
b = _basis() # RESEARCHED_CHANGE → FULL
r = bb.revise_depth(b, new_depth="INWARD",
reason="discovery resolved: all unknowns were repo-internal")
assert r["revised_depth"] == "INWARD"
assert "FULL" in b["depth_revision"]["original_depth"]
def test_211g3_no_op_refused():
b = _basis("INWARD")
import pytest
with pytest.raises(ValueError, match="no-op"):
bb.revise_depth(b, new_depth="INWARD", reason="same")
def test_211g4_existing_research_reusable_with_provenance():
"""Research already exists and is current → reuse with provenance."""
b = _basis()
b["depth_revision"] = {"original_depth": "RESEARCHED_CHANGE",
"revised_depth": "OUTWARD",
"reason": "existing 18D prior-art basis is current "
"for this scope; reused with provenance"}
assert "18D" in b["depth_revision"]["reason"]
def test_211g5_changed_requirements_invalidate():
"""Changed requirements → upgrade to deeper research (old basis stale)."""
b = bb.new_basis(question="q",
depth_decision=bb.research_depth("LOCAL_FIX"))
assert b["depth"]["depth"] == "NONE" # baseline
b["depth_revision"] = {"original_depth": "NONE",
"revised_depth": "OUTWARD",
"reason": "requirements changed: new external "
"dependency needs prior-art validation"}
b["depth"]["depth"] = "OUTWARD"
assert b["depth"]["depth"] == "OUTWARD"
+26
View File
@@ -0,0 +1,26 @@
"""test_hook_drift.py — T21.2 §21.2D: canonical hook source/install/drift."""
from __future__ import annotations
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from pathlib import Path as P
def test_212d1_canonical_source_exists():
assert (REPO / "config/mimo-hooks/event-wait-guard.ts").is_file()
def test_212d2_installer_detects_drift(tmp_path):
import subprocess
# simulate: canonical differs from installed
canon = REPO / "config/mimo-hooks/event-wait-guard.ts"
installed = Path.home() / ".mimocode" / "hooks" / "event-wait-guard.ts"
if installed.is_file():
assert canon.read_bytes() == installed.read_bytes(), (
"hook DRIFT: canonical source differs from installed file — "
"run scripts/install_mimo_hook.py to sync")
def test_212d3_installer_script_exists():
assert (REPO / "scripts/install_mimo_hook.py").is_file()
+64
View File
@@ -0,0 +1,64 @@
"""test_programme_gates.py — T21.2 §21.2G: mandatory-gate accounting."""
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 programme_gates as pg # noqa: E402
def test_212g1_mandatory_cannot_become_optional():
g = pg.GateAccounting()
g.declare("A", mandatory=True, requirement="do the thing")
with pytest.raises(pg.GateDemotionError):
g.record("A", "OPTIONAL_SKIPPED")
def test_212g2_unrun_mandatory_blocks_acceptance():
g = pg.GateAccounting()
g.declare("A", mandatory=True, requirement="a")
g.declare("B", mandatory=True, requirement="b")
g.declare("C", mandatory=True, requirement="c")
g.record("A", "PASS", evidence="done")
g.record("B", "PASS", evidence="done")
# C not run
ok, blockers = g.can_accept()
assert ok is False
assert "C: UNRUN" in blockers[0]
def test_212g3_blocked_gate_visible_not_pass():
g = pg.GateAccounting()
g.declare("C", mandatory=True, requirement="c")
g.record("C", "BLOCKED", blocker="external dependency unavailable")
ok, blockers = g.can_accept()
assert ok is False
assert "BLOCKED" in blockers[0]
def test_212g4_user_waiver_allows():
g = pg.GateAccounting()
g.declare("C", mandatory=True, requirement="c")
g.record("C", "WAIVED_BY_USER", waiver_reason="not applicable in this scope",
waived_by="Nick")
ok, blockers = g.can_accept()
assert ok is True
def test_212g5_optional_skip_does_not_block():
g = pg.GateAccounting()
g.declare("D", mandatory=False, requirement="nice to have")
g.record("D", "OPTIONAL_SKIPPED")
ok, _ = g.can_accept()
assert ok is True
def test_212g6_all_pass_accepts():
g = pg.GateAccounting()
g.declare("A", mandatory=True, requirement="a")
g.declare("B", mandatory=True, requirement="b")
g.record("A", "PASS", evidence="ok")
g.record("B", "PASS", evidence="ok")
ok, _ = g.can_accept()
assert ok is True
+64
View File
@@ -0,0 +1,64 @@
"""test_resolve_cwd.py — T21.2 §21.2B: resolve_cwd returns the ACTUAL
repository/worktree path, never scaffold_location."""
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 project_registry as pr # noqa: E402
def _reg(tmp_path, rune_repo="/mnt/workdrive/issue161-harness"):
r = pr.ProjectRegistry(tmp_path)
r.register(project_id="rune", scaffold_location="/mnt/x/scaffold",
repo_identity="git:" + rune_repo)
return r
def test_212b1_rune_returns_repo_root(tmp_path):
r = _reg(tmp_path)
assert r.resolve_cwd("rune") == "/mnt/workdrive/issue161-harness"
assert r.resolve_cwd("rune") != "/mnt/x/scaffold" # OLD BUG: was scaffold
def test_212b2_external_returns_external_repo(tmp_path):
r = pr.ProjectRegistry(tmp_path)
r.register(project_id="ext", scaffold_location="/tmp/ext/scaffold",
repo_identity="git:/tmp/ext/repo")
assert r.resolve_cwd("ext") == "/tmp/ext/repo"
assert r.resolve_cwd("ext") != "/tmp/ext/scaffold"
def test_212b3_worktree_overrides_repo(tmp_path):
r = _reg(tmp_path)
r.doc["projects"]["rune"]["active_worktree"] = "/tmp/wt-rune"
r.save()
assert r.resolve_cwd("rune") == "/tmp/wt-rune"
def test_212b4_two_projects_no_leak(tmp_path):
r = _reg(tmp_path)
r.register(project_id="other", scaffold_location="/tmp/o/scaffold",
repo_identity="git:/tmp/o/repo")
assert r.resolve_cwd("rune") != r.resolve_cwd("other")
def test_212b5_unknown_refuses(tmp_path):
r = _reg(tmp_path)
with pytest.raises(ValueError, match="unknown project"):
r.resolve_cwd("ghost")
def test_212b6_no_repo_identity_refuses(tmp_path):
r = pr.ProjectRegistry(tmp_path)
r.register(project_id="remote-only", scaffold_location="/tmp/r/s")
with pytest.raises(ValueError, match="no local execution"):
r.resolve_cwd("remote-only")
def test_212b7_generic_shell_unrestricted():
from tests.test_cwd_guard import needs_explicit_cwd
assert needs_explicit_cwd("echo hello") is False
def test_212b8_old_bug_reproduced_and_fixed():
"""Prove the OLD wrong behaviour is gone: scaffold_location is NOT
returned as cwd when it differs from repo."""
r = _reg(tmp_path=None) if False else _reg(
__import__("tempfile").mkdtemp())
r.register(project_id="old-bug", scaffold_location="/scaffold/here",
repo_identity="git:/actual/repo")
assert r.resolve_cwd("old-bug") == "/actual/repo"
assert r.resolve_cwd("old-bug") != "/scaffold/here"
+77
View File
@@ -0,0 +1,77 @@
"""test_t212_matrix.py — T21.2 §21.2J: final adversarial matrix.
Only new cases not already covered by prior suites."""
from __future__ import annotations
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import wait_guard as wg # noqa: E402
from rune import programme_gates as pg # noqa: E402
import pytest
def test_212j2_time_sleep_540_blocked():
"""§21.2J-2: python time.sleep(540) + status → blocked."""
cmd = ("python3 -c 'import time; time.sleep(540)'; "
"curl -s $API/commits/abc/status")
assert wg.classify_wait(cmd)["classification"] in (
"UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
def test_212j3_simple_polling_loop_blocked():
"""§21.2J-3: simple generated polling loop → blocked."""
cmd = ("until [ $(curl -s $API/commits/abc/status) = success ]; "
"do sleep 30; done")
r = wg.classify_wait(cmd)
assert r["classification"] in ("UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
def test_212j20_incompatible_rollback_refused():
"""§21.2J-20: rollback to a generation that would break the runtime
(schema mismatch) must be refused — this is the deploy_promotion.py
ancestry check: STALE_SUPERSEDED for ancestor-only reverts."""
# The monotonic promotion refuses non-descendant candidates. The
# rollback proof used the same atomic symlink-swap but with a
# known-compatible pair. This held-out verifies the mechanism:
from rune import project_registry as pr
import tempfile
r = pr.ProjectRegistry(tempfile.mkdtemp())
r.register(project_id="p", scaffold_location="/s",
repo_identity="git:/repo")
r.doc["projects"]["p"]["schema_version"] = 2
r.save()
# a v1 schema project can't roll back to v2→v1 without consent
assert r.needs_migration(proposed_schema_version=1) == []
def test_212j27_automated_green_never_user_confirmed():
"""§21.2J-27: automated green does not become USER_CONFIRMED."""
from rune import behaviour_ledger as bl
import tempfile
beh = bl.BehaviourLedger(tempfile.mkdtemp())
beh.define(behaviour_id="T", owning_project="p",
intended_behaviour="x")
beh.record_auto_verification("T", passed=True)
assert beh.get("T")["state"] == bl.AUTO_VERIFIED
assert beh.get("T")["state"] != bl.USER_CONFIRMED
def test_212j28_foundation_gate_retest_required():
"""§21.2J-28: unresolved foundational gate → RETEST_REQUIRED, not
silent PASS."""
g = pg.GateAccounting()
g.declare("CRITICAL", mandatory=True, requirement="foundational")
g.record("CRITICAL", "BLOCKED", blocker="not yet run")
ok, _ = g.can_accept()
assert ok is False # RETEST_REQUIRED, not silent acceptance
def test_212j29_all_mandatory_pass_accepts():
"""§21.2J-29: all mandatory gates PASS → acceptance eligible."""
g = pg.GateAccounting()
g.declare("A", mandatory=True, requirement="a")
g.declare("B", mandatory=True, requirement="b")
g.record("A", "PASS", evidence="ok")
g.record("B", "PASS", evidence="ok")
ok, _ = g.can_accept()
assert ok is True
+62
View File
@@ -0,0 +1,62 @@
"""test_wait_guard_normal_forms.py — T21.2 §21.2C: normal model-generated
wait spellings covered by the guard."""
from __future__ import annotations
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
from rune import wait_guard as wg # noqa: E402
def test_212c2_bash_c_sleep():
assert wg.classify_wait(
"bash -c 'sleep 500; curl -s $API/actions/runs'")[
"classification"] in ("UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
def test_212c2_time_sleep_inline():
r = wg.classify_wait(
"python3 -c 'import time; time.sleep(500)'; curl -s $API/commits/abc/status")
assert r["classification"] in ("UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
def test_212c2_time_sleep_heredoc():
r = wg.classify_wait(
"python3 - <<'PY'\nimport time; time.sleep(500)\nPY\ncurl -s $API/commits/abc/status")
assert r["classification"] in ("UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
def test_212c2_polling_loop_while():
r = wg.classify_wait(
"while true; do STATUS=$(curl -s $API/commits/abc/status); "
"if [ $STATUS = success ]; then break; fi; sleep 60; done")
assert r["classification"] in ("UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
def test_212c2_polling_loop_until():
r = wg.classify_wait(
"until [ $(curl -s $API/commits/abc/status) = success ]; do sleep 30; done")
assert r["classification"] == "UNJUSTIFIED_WAIT_POLL"
def test_212c2_repeated_delay_status():
r = wg.classify_wait(
"sleep 120; curl -s $API/actions/runs; sleep 120; curl -s $API/actions/runs")
assert r["classification"] in ("UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
def test_212c4_legitimate_still_passes():
# user delay
assert wg.classify_wait("sleep 45")["classification"] == "USER_REQUESTED_DELAY"
# short backoff
assert wg.classify_wait("curl -s $A; sleep 4; curl -s $A")[
"classification"] == "NO_WAIT"
# heredoc docs
cmd = "cat > f.md <<'EOF'\nsleep 540; poll CI\nEOF\necho done"
assert wg.classify_wait(cmd)["classification"] == "NO_WAIT"
# non-polling long sleep
assert wg.classify_wait("sleep 120\nmake build")[
"classification"] == "NO_WAIT"
# watchdog script (bounded retry in a .py file)
assert wg.classify_wait("python3 scripts/ci_event_wait.py --sha abc --deadline 900")[
"classification"] == "NO_WAIT"