Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
725c779ac7 | ||
|
|
edd6d06300 | ||
|
|
98cb3a98a5 | ||
|
|
7881e78e7f |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.80.0**
|
||||
**Current release: v0.81.1**
|
||||
|
||||
**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,121 +1,54 @@
|
||||
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.
|
||||
// Uses the plugin tool() API with args (not parameters).
|
||||
|
||||
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",
|
||||
export const AwaitOperation = tool({
|
||||
description: [
|
||||
"Wait for an asynchronous external operation to complete (CI, deploy, release, builder).",
|
||||
"Wait for an asynchronous external operation (CI, deploy, release) to complete.",
|
||||
"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(
|
||||
args: {
|
||||
sha: tool.schema.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
|
||||
deadline_secs: tool.schema.number().optional().describe(
|
||||
"Maximum wait in seconds (default 900)"),
|
||||
},
|
||||
async execute(args, ctx) {
|
||||
const repoRoot = "/mnt/workdrive/issue161-harness"
|
||||
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,
|
||||
"python3", `${repoRoot}/scripts/ci_event_wait.py`,
|
||||
"--sha", args.sha,
|
||||
"--deadline", String(deadline),
|
||||
].join(" ")
|
||||
|
||||
try {
|
||||
const result = execSync(cmd, {
|
||||
cwd: repoRoot,
|
||||
timeout: (deadline + 60) * 1000,
|
||||
encoding: "utf-8",
|
||||
encoding: "utf-8" as const,
|
||||
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 || "",
|
||||
}
|
||||
const jsonLine = lines.find((l: string) => l.startsWith("{"))
|
||||
if (jsonLine) return jsonLine
|
||||
return "await_operation: no JSON output"
|
||||
} 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 {
|
||||
if (jsonLine) return jsonLine
|
||||
return JSON.stringify({
|
||||
ok: false,
|
||||
error: `await_operation failed: ${e.stderr || e.message?.slice(0, 200) || "unknown"}`,
|
||||
}
|
||||
error: `await_operation failed: ${(e.stderr || e.message || "?").slice(0, 200)}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.80.0"
|
||||
version = "0.81.1"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
@@ -63,6 +63,12 @@ def authority_verdict() -> str:
|
||||
capture_output=True, text=True, timeout=120,
|
||||
env=dict(os.environ, PYTHONPATH=""),
|
||||
)
|
||||
# T21.3: GITEA_TOKEN from deploy.yml secrets must propagate to the
|
||||
# subprocess — gitea_auth reads it from env, but the env may not
|
||||
# carry it through the bash wrapper in CI
|
||||
if not os.environ.get("GITEA_TOKEN"):
|
||||
# fallback: gitea_auth's own credential ladder handles it
|
||||
pass
|
||||
out = (proc.stdout or "").strip().splitlines()
|
||||
return (out[-1] if out else "pending").strip()
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.80.0"
|
||||
__version__ = "0.81.1"
|
||||
|
||||
# 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.80.0"' in rune_version,
|
||||
'__version__ = "0.81.1"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -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.80.0
|
||||
Version: v0.81.1
|
||||
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.80.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.81.1 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
+157
-149
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,28 @@
|
||||
"""test_hook_parity.py — T21.3 §21.3C-2: canonical hook hash parity.
|
||||
The installed hook MUST match the canonical source."""
|
||||
from __future__ import annotations
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CANONICAL = REPO / "config" / "mimo-hooks" / "event-wait-guard.ts"
|
||||
INSTALLED = Path.home() / ".mimocode" / "hooks" / "event-wait-guard.ts"
|
||||
|
||||
|
||||
def _sha(p: Path) -> str:
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def test_213c_canonical_exists():
|
||||
assert CANONICAL.is_file()
|
||||
|
||||
|
||||
def test_213c_installed_matches_canonical():
|
||||
assert INSTALLED.is_file(), f"hook missing at {INSTALLED}"
|
||||
assert _sha(CANONICAL) == _sha(INSTALLED), (
|
||||
f"HOOK DRIFT: canonical={_sha(CANONICAL)[:12]} "
|
||||
f"installed={_sha(INSTALLED)[:12]} — run "
|
||||
f"python3 scripts/install_mimo_hook.py to sync")
|
||||
|
||||
|
||||
def test_213c_installer_exists():
|
||||
assert (REPO / "scripts/install_mimo_hook.py").is_file()
|
||||
@@ -0,0 +1,25 @@
|
||||
"""test_wait_guard_corpus.py — T21.3 §21.3C-1: parity corpus.
|
||||
Python side: verify classify_wait against the canonical corpus."""
|
||||
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.wait_guard_corpus import CORPUS # noqa: E402
|
||||
|
||||
|
||||
def test_corpus_python_parity():
|
||||
"""Python classify_wait must agree with every corpus case."""
|
||||
failures = []
|
||||
for case in CORPUS:
|
||||
r = wg.classify_wait(case["command"])
|
||||
if r["classification"] != case["expected"]:
|
||||
failures.append(
|
||||
f"{case['id']}: expected {case['expected']}, "
|
||||
f"got {r['classification']}")
|
||||
assert not failures, f"parity failures: {failures}"
|
||||
|
||||
|
||||
def test_corpus_size():
|
||||
assert len(CORPUS) >= 15 # enough to cover the mandate's shapes
|
||||
Reference in new issue
Block a user