Compare commits
46
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7141200bd | ||
|
|
246f26663d | ||
|
|
5718a3ea85 | ||
|
|
02802f321f | ||
|
|
4ce121a493 | ||
|
|
8a8d3534c6 | ||
|
|
d0ec37925f | ||
|
|
f9936fa244 | ||
|
|
04a277f22c | ||
|
|
b2777d666f | ||
|
|
0982f7317f | ||
|
|
9583678997 | ||
|
|
5db8b202d9 | ||
|
|
78c0d97a43 | ||
|
|
551530c78c | ||
|
|
b669e4d4ed | ||
|
|
2b6a5a6b95 | ||
|
|
e2e7b74a8e | ||
|
|
db4ce7f6a6 | ||
|
|
505209b839 | ||
|
|
f7256aa29e | ||
|
|
26e3765319 | ||
|
|
e6451a5afc | ||
|
|
7008743620 | ||
|
|
f545f1ed8d | ||
|
|
73803e6a0d | ||
|
|
ed1696606e | ||
|
|
4944f9e41c | ||
|
|
6722066c79 | ||
|
|
df8262b9ed | ||
|
|
756ebe6201 | ||
|
|
f64ee95aad | ||
|
|
c3a0a48ca5 | ||
|
|
b4ed258121 | ||
|
|
bacea2d472 | ||
|
|
9acfc0953e | ||
|
|
818aa49043 | ||
|
|
212239ca7b | ||
|
|
ae6d4a0d98 | ||
|
|
00013f8c0b | ||
|
|
ba2966e631 | ||
|
|
20fcee3f40 | ||
|
|
493d9c2800 | ||
|
|
6b5d8ad023 | ||
|
|
f3ef4672f3 | ||
|
|
be6bc17537 |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.82.0**
|
||||
**Current release: v0.84.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.**
|
||||
|
||||
|
||||
@@ -1,115 +1,56 @@
|
||||
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).
|
||||
// event-wait-guard — RETIRED (T21.5 §6/§8, 2026-09-17).
|
||||
//
|
||||
// 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).
|
||||
// HISTORY: this hook was the T21 §21B-3 runtime interception layer that
|
||||
// blocked model-generated sleep→poll continuation (T20 regression), and
|
||||
// later carried the T21.1 §21.1C cwd guard. Both were DOWNSTREAM patches:
|
||||
// they corrected the model's output instead of fixing the generator.
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
// ROOT-CAUSE FIX (T21.5 §2-§5): the generator was ci_event_wait's
|
||||
// wait loop returning nonterminal control to the model — caused by a
|
||||
// single hard-coded master-deploy authority that was blind to branch/PR
|
||||
// operations. T21.5 fixed this upstream:
|
||||
// - OPERATION_SPECS: per-operation-type completion authority
|
||||
// (branch_ci/pull_request_checks -> own Actions runs;
|
||||
// master_ci/deploy/release -> deploy qualification);
|
||||
// - SINGLE LOGICAL AWAIT: all nonterminal conditions re-arm inside
|
||||
// ci_event_wait.run(); the model sees only terminal results;
|
||||
// - structured PendingOperation envelopes (op_type, authority,
|
||||
// internal_rearm_count) propagate to the controller.
|
||||
//
|
||||
// With the generator fixed, runtime interception is retired per the
|
||||
// tranche rule FIXED UPSTREAM BEHAVIOUR > DOWNSTREAM GUARD CORRECTION.
|
||||
// Regression prevention lives in the test suite:
|
||||
// tests/test_single_logical_await.py (no model re-arm contract)
|
||||
// tests/test_ci_event_wait.py (watchdog=recovery, event=wake)
|
||||
// tests/test_wait_guard_corpus.py (canonical wait-pattern corpus)
|
||||
// NOT as a permanent runtime block. The hook file stays installed as a
|
||||
// documented no-op so canonical-source/install parity (test_hook_drift,
|
||||
// test_hook_parity) continues to prove the installed runtime matches
|
||||
// this repository's canonical state.
|
||||
|
||||
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
|
||||
}
|
||||
"tool.execute.before": async (_input, _output) => {
|
||||
// RETIRED — no interception. See history above.
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
export const CwdGuard: Hooks = {
|
||||
"tool.execute.before": async (_input, _output) => {
|
||||
// RETIRED (T21.5 §8) — the harness/construction execution boundary
|
||||
// now resolves project context upstream (execution-context binding).
|
||||
// No runtime refusal. See tests/test_auto_execution_context.py.
|
||||
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);
|
||||
// Kept as the default export shape; both sub-guards are retired.
|
||||
return
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
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(" ")
|
||||
},
|
||||
};
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.82.0"
|
||||
version = "0.84.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile pyproject.toml -o requirements-ci.txt --extra dev --extra memory --python-version 3.11
|
||||
# uv pip compile pyproject.toml -o /tmp/regen-req.txt --extra dev --extra memory --python-version 3.11
|
||||
aiohappyeyeballs==2.7.1
|
||||
# via aiohttp
|
||||
aiohttp==3.14.3
|
||||
@@ -34,7 +34,7 @@ httpcore==1.0.9
|
||||
# via httpx
|
||||
httpx==0.28.1
|
||||
# via rune (pyproject.toml)
|
||||
idna==3.19
|
||||
idna==3.20
|
||||
# via
|
||||
# anyio
|
||||
# httpx
|
||||
|
||||
@@ -0,0 +1,375 @@
|
||||
"""await_intent.py — ASYNC CORRECTIVE PATCH: intent normalization at the
|
||||
execution boundary.
|
||||
|
||||
PRINCIPLE: MODEL OUTPUT = INTENT. RUNE CONTROLLER = EXECUTION SEMANTICS.
|
||||
|
||||
The model may express continuation intent in any quality — `await_operation
|
||||
(deadline_secs=1200)`, `await-operation [op_type=master_ci, sha=X]`,
|
||||
"sleep 300 then check CI", "poll until done". When a matching
|
||||
PendingOperation exists (or the deterministic resolver can derive the
|
||||
operation truth from git/Gitea state), the controller executes the
|
||||
CANONICAL continuation and discards model-supplied scheduling policy.
|
||||
|
||||
Authority provenance for timing:
|
||||
USER_EXPLICIT / EXTERNAL_CONTRACT / TEST_HARNESS /
|
||||
GOVERNED_OPERATION_POLICY -> authoritative, retained
|
||||
MODEL_INVENTED -> non-authoritative, ignored
|
||||
|
||||
This module owns NO storage: it reads the existing W/R wait store
|
||||
(rune.event_wait) and git/Gitea state. The canonical continuation remains
|
||||
scripts/ci_event_wait.py run() (the T21.5 single-logical-await).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(_SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
_REPO_SRC = str(_SCRIPT_DIR.parent / "src")
|
||||
if _REPO_SRC not in sys.path:
|
||||
sys.path.insert(0, _REPO_SRC)
|
||||
|
||||
WR_BRIDGE = Path(os.environ.get("RUNE_WR_ROOT", "/mnt/workdrive/wr-bridge"))
|
||||
REPO = Path("/mnt/workdrive/issue161-harness")
|
||||
GIT_TIMEOUT = 10
|
||||
|
||||
# Default policy deadline per operation type — GOVERNED_OPERATION_POLICY.
|
||||
# The model's numbers never become policy; these are the controller's.
|
||||
POLICY_DEADLINES = {
|
||||
"branch_ci": 1800,
|
||||
"pull_request_checks": 1800,
|
||||
"master_ci": 1800,
|
||||
"deploy": 900,
|
||||
"release": 900,
|
||||
}
|
||||
|
||||
VALID_OP_TYPES = ("branch_ci", "pull_request_checks", "master_ci",
|
||||
"deploy", "release")
|
||||
|
||||
|
||||
# ── timing provenance ───────────────────────────────────────────────────────
|
||||
|
||||
def classify_timing_provenance(model_deadline: Any = None,
|
||||
user_deadline: Any = None,
|
||||
contract_deadline: Any = None) -> tuple[int, str]:
|
||||
"""Resolve the effective deadline + its provenance.
|
||||
|
||||
Authority order: USER_EXPLICIT > EXTERNAL_CONTRACT > TEST_HARNESS >
|
||||
GOVERNED_OPERATION_POLICY. MODEL_INVENTED values are recorded but
|
||||
NEVER used as policy.
|
||||
"""
|
||||
if model_deadline is not None:
|
||||
try:
|
||||
model_deadline = int(model_deadline)
|
||||
except (TypeError, ValueError):
|
||||
model_deadline = None
|
||||
if user_deadline is not None:
|
||||
return int(user_deadline), "USER_EXPLICIT"
|
||||
if contract_deadline is not None:
|
||||
return int(contract_deadline), "EXTERNAL_CONTRACT"
|
||||
return (model_deadline or 0), "MODEL_INVENTED" # caller discards this
|
||||
|
||||
|
||||
# ── PendingOperation lookup over the existing W/R wait store ────────────────
|
||||
|
||||
def _load_wait_records() -> list[dict]:
|
||||
"""All durable wait records from the bridge store (read-only)."""
|
||||
out = []
|
||||
waits = WR_BRIDGE / "waits"
|
||||
if not waits.is_dir():
|
||||
return out
|
||||
for f in sorted(waits.glob("*.json")):
|
||||
try:
|
||||
rec = json.loads(f.read_text())
|
||||
rec["_file"] = f.name
|
||||
out.append(rec)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _rec_operation_id(rec: dict) -> str:
|
||||
return str((rec.get("ext") or {}).get("operation_id")
|
||||
or rec.get("operation_id") or "")
|
||||
|
||||
|
||||
def _rec_sha(rec: dict) -> str:
|
||||
return str(rec.get("sha") or "")
|
||||
|
||||
|
||||
def pending_operations(sha: str = "", op_type: str = "") -> list[dict]:
|
||||
"""Pending (non-terminal) operations, optionally filtered."""
|
||||
terminal = {"COMPLETED", "FAILED", "CLOSED", "CANCELLED", "EXPIRED"}
|
||||
out = []
|
||||
for rec in _load_wait_records():
|
||||
status = str(rec.get("status", "")).upper()
|
||||
if status in terminal:
|
||||
continue
|
||||
if sha and _rec_sha(rec) != sha:
|
||||
continue
|
||||
out.append(rec)
|
||||
return out
|
||||
|
||||
|
||||
def find_op_type_for_sha(sha: str) -> str | None:
|
||||
"""The op_type a prior canonical wait already recorded for this SHA
|
||||
(objective text embeds the type: 'event-driven {op_type} qualification')."""
|
||||
for rec in _load_wait_records():
|
||||
if _rec_sha(rec) != sha:
|
||||
continue
|
||||
obj = str(rec.get("continuation", {}).get("objective", ""))
|
||||
m = re.search(r"event-driven (\w+) qualification", obj)
|
||||
if m and m.group(1) in VALID_OP_TYPES:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
# ── deterministic op_type derivation from repo/Gitea truth ─────────────────
|
||||
|
||||
def _git(*args: str) -> str:
|
||||
try:
|
||||
r = subprocess.run(["git", "-C", str(REPO), *args],
|
||||
capture_output=True, text=True, timeout=GIT_TIMEOUT)
|
||||
return r.stdout.strip()
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return ""
|
||||
|
||||
|
||||
def sha_branch(sha: str) -> str:
|
||||
"""Branch(es) containing this commit in the local repo."""
|
||||
return _git("branch", "--all", "--contains", sha)
|
||||
|
||||
|
||||
def sha_on_master(sha: str) -> bool:
|
||||
out = _git("branch", "--contains", sha)
|
||||
return any(l.strip().endswith("master") or l.strip() == "master"
|
||||
for l in out.splitlines())
|
||||
|
||||
|
||||
def open_pr_for_sha(sha: str) -> int | None:
|
||||
"""Is there an OPEN PR whose head SHA is this one? (Gitea API, read-only.)"""
|
||||
try:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import gitea_auth # noqa: E402
|
||||
st, prs = gitea_auth.api("/pulls?state=open&limit=50")
|
||||
if st != 200:
|
||||
return None
|
||||
for p in prs:
|
||||
hs = str(p.get("head") or {}).get("sha") if isinstance(
|
||||
p.get("head"), dict) else None
|
||||
if hs and str(hs).startswith(sha[:12]):
|
||||
return int(p.get("number"))
|
||||
except Exception: # noqa: BLE001 — resolver must never crash the tool
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def derive_op_type(sha: str) -> tuple[str, str]:
|
||||
"""Deterministically derive the canonical op_type for a SHA.
|
||||
|
||||
Returns (op_type, evidence). Order (A-patch §2 amendment):
|
||||
1. POST-PATCH normalized record (ext.intent_normalized=true) — true
|
||||
stored operation truth, wins outright;
|
||||
2. open PR for the sha -> pull_request_checks;
|
||||
3. sha ancestor of master -> master_ci;
|
||||
4. sha on a feature branch -> branch_ci;
|
||||
5. legacy (pre-patch) records only CONFIRM, never override — they
|
||||
were created from model-supplied defaults and cannot be trusted.
|
||||
Never trusts model input.
|
||||
"""
|
||||
for rec in _load_wait_records():
|
||||
if _rec_sha(rec) != sha:
|
||||
continue
|
||||
if (rec.get("ext") or {}).get("intent_normalized"):
|
||||
obj = str(rec.get("continuation", {}).get("objective", ""))
|
||||
m = re.search(r"event-driven (\w+) qualification", obj)
|
||||
if m and m.group(1) in VALID_OP_TYPES:
|
||||
return m.group(1), "normalized_record:" + m.group(1)
|
||||
pr = open_pr_for_sha(sha)
|
||||
if pr:
|
||||
return "pull_request_checks", f"open_pr:#{pr}"
|
||||
if sha_on_master(sha):
|
||||
return "master_ci", "commit_ancestor_of_master"
|
||||
return "branch_ci", "commit_on_feature_branch"
|
||||
|
||||
|
||||
# ── intent normalization ────────────────────────────────────────────────────
|
||||
|
||||
def normalize_action(action: dict, *,
|
||||
model_deadline: Any = None,
|
||||
user_deadline: Any = None,
|
||||
contract_deadline: Any = None) -> dict:
|
||||
"""Normalize a model continuation action into the canonical continuation.
|
||||
|
||||
action: {kind: await_operation|sleep_poll|check_status|unknown,
|
||||
sha?: str, operation_id?: str, op_type?: str, raw?: str}
|
||||
Returns:
|
||||
{normalized: bool, action: "ContinuePendingOperation", operation_id,
|
||||
op_type, authority, deadline_secs, deadline_provenance,
|
||||
model_deadline_ignored, model_op_type_ignored, evidence}
|
||||
or {normalized: False, reason: ...} / {normalized: "AMBIGUOUS", candidates}
|
||||
"""
|
||||
sha = str(action.get("sha") or "")
|
||||
op_id = str(action.get("operation_id") or "")
|
||||
model_op_type = action.get("op_type")
|
||||
model_deadline_ignored = model_deadline is not None
|
||||
model_op_type_ignored = model_op_type is not None
|
||||
|
||||
# 1. explicit operation_id wins
|
||||
if op_id:
|
||||
from rune import event_wait as ew # noqa: E402
|
||||
rec = ew.operation_wait(op_id)
|
||||
if rec is None:
|
||||
return {"normalized": False,
|
||||
"reason": f"operation_id {op_id} has no wait record"}
|
||||
return _canonical_from_record(rec, model_deadline_ignored,
|
||||
model_op_type_ignored, "explicit_id",
|
||||
user_deadline, contract_deadline)
|
||||
|
||||
# 2. resolve by sha / active operation
|
||||
candidates = pending_operations(sha=sha)
|
||||
if len(candidates) > 1 and not sha:
|
||||
return {"normalized": "AMBIGUOUS",
|
||||
"candidates": [_rec_operation_id(c) for c in candidates],
|
||||
"reason": "multiple pending operations, no target"}
|
||||
if len(candidates) == 1:
|
||||
return _canonical_from_record(candidates[0], model_deadline_ignored,
|
||||
model_op_type_ignored, "pending_match",
|
||||
user_deadline, contract_deadline)
|
||||
|
||||
# 3. no stored record: derive truth deterministically
|
||||
if not sha:
|
||||
return {"normalized": False,
|
||||
"reason": "no pending operation and no sha to resolve"}
|
||||
op_type, evidence = derive_op_type(sha)
|
||||
return {
|
||||
"normalized": True,
|
||||
"action": "ContinuePendingOperation",
|
||||
"operation_id": f"ci-{sha}",
|
||||
"op_type": op_type,
|
||||
"authority": "ci_runs" if op_type in ("branch_ci",
|
||||
"pull_request_checks")
|
||||
else "deploy_qualify",
|
||||
"deadline_secs": POLICY_DEADLINES[op_type],
|
||||
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
|
||||
"model_deadline_ignored": model_deadline_ignored,
|
||||
"model_op_type_ignored": model_op_type_ignored,
|
||||
"evidence": evidence,
|
||||
}
|
||||
|
||||
|
||||
def _canonical_from_record(rec: dict, model_deadline_ignored: bool,
|
||||
model_op_type_ignored: bool, how: str,
|
||||
user_deadline: Any, contract_deadline: Any) -> dict:
|
||||
op_id = _rec_operation_id(rec)
|
||||
obj = str(rec.get("continuation", {}).get("objective", ""))
|
||||
m = re.search(r"event-driven (\w+) qualification", obj)
|
||||
recorded = m.group(1) if (m and m.group(1) in VALID_OP_TYPES) else None
|
||||
rec_sha = _rec_sha(rec)
|
||||
if (rec.get("ext") or {}).get("intent_normalized") or how == "explicit_id":
|
||||
# normalized record or explicit id: stored truth wins
|
||||
op_type = recorded or "master_ci"
|
||||
else:
|
||||
# legacy record: git/Gitea derivation outranks the model-era default
|
||||
op_type, _ev = derive_op_type(rec_sha) if rec_sha else \
|
||||
(recorded or "master_ci", "no_sha")
|
||||
deadline, prov = classify_timing_provenance(
|
||||
user_deadline=user_deadline, contract_deadline=contract_deadline)
|
||||
if prov == "MODEL_INVENTED" or not deadline:
|
||||
deadline = POLICY_DEADLINES.get(op_type, 1800)
|
||||
prov = "GOVERNED_OPERATION_POLICY"
|
||||
return {
|
||||
"normalized": True,
|
||||
"action": "ContinuePendingOperation",
|
||||
"operation_id": op_id,
|
||||
"op_type": op_type,
|
||||
"authority": "ci_runs" if op_type in ("branch_ci",
|
||||
"pull_request_checks")
|
||||
else "deploy_qualify",
|
||||
"deadline_secs": deadline,
|
||||
"deadline_provenance": prov,
|
||||
"model_deadline_ignored": model_deadline_ignored,
|
||||
"model_op_type_ignored": model_op_type_ignored,
|
||||
"matched_by": how,
|
||||
}
|
||||
|
||||
|
||||
def normalize_bash(command: str, *, sha_hint: str = "") -> dict | None:
|
||||
"""Recognize sleep/check/poll intent aimed at a known pending operation.
|
||||
|
||||
Returns a normalization record if the command's semantic intent is
|
||||
continuation of a known external operation; None if the command is
|
||||
legitimate local timing (short sync, user delay, backoff) with no
|
||||
pending operation to continue. NEVER returns an executed sleep.
|
||||
"""
|
||||
if not command:
|
||||
return None
|
||||
cmd = command.strip()
|
||||
# long sleep + (status-check language or an explicit sha hint)
|
||||
sleep_m = re.search(r"\bsleep\s+(\d{2,5})\b", cmd)
|
||||
if not sleep_m:
|
||||
return None
|
||||
secs = int(sleep_m.group(1))
|
||||
check_intent = bool(re.search(
|
||||
r"\b(ci|deploy|release|status|check|poll|pipeline|run)\b", cmd,
|
||||
re.IGNORECASE))
|
||||
sha = sha_hint or ""
|
||||
m = re.search(r"\b([0-9a-f]{40}|[0-9a-f]{12})\b", cmd)
|
||||
if m:
|
||||
sha = m.group(1)
|
||||
if not check_intent and not sha:
|
||||
return None # plain sleep, no continuation intent
|
||||
cands = pending_operations(sha=sha) if sha else pending_operations()
|
||||
if not cands and not sha:
|
||||
return None
|
||||
if len(cands) > 1:
|
||||
return {"normalized": "AMBIGUOUS",
|
||||
"candidates": [_rec_operation_id(c) for c in cands],
|
||||
"raw_command_kind": "sleep_poll"}
|
||||
rec = cands[0] if cands else None
|
||||
if rec is not None:
|
||||
norm = _canonical_from_record(rec, True, False, "sleep_poll_intent",
|
||||
None, None)
|
||||
else:
|
||||
op_type, evidence = derive_op_type(sha)
|
||||
norm = {
|
||||
"normalized": True, "action": "ContinuePendingOperation",
|
||||
"operation_id": f"ci-{sha}", "op_type": op_type,
|
||||
"authority": "ci_runs" if op_type in ("branch_ci",
|
||||
"pull_request_checks")
|
||||
else "deploy_qualify",
|
||||
"deadline_secs": POLICY_DEADLINES[op_type],
|
||||
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
|
||||
"model_deadline_ignored": True, "model_op_type_ignored": False,
|
||||
"evidence": evidence,
|
||||
}
|
||||
norm["raw_command_kind"] = "sleep_poll"
|
||||
norm["raw_sleep_seconds_suppressed"] = secs
|
||||
return norm
|
||||
|
||||
|
||||
def emit_normalized_record(record: dict) -> None:
|
||||
"""Compact structured observability — one JSONL line per normalization.
|
||||
No payloads, no secrets."""
|
||||
try:
|
||||
log = _SCRIPT_DIR.parent / "store" / "await_intent_log.jsonl"
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
slim = {k: record.get(k) for k in (
|
||||
"normalized", "action", "operation_id", "op_type", "authority",
|
||||
"deadline_secs", "deadline_provenance", "model_deadline_ignored",
|
||||
"model_op_type_ignored", "matched_by", "evidence",
|
||||
"raw_command_kind", "raw_sleep_seconds_suppressed", "reason",
|
||||
"candidates") if k in record}
|
||||
slim["at"] = __import__("time").strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ", __import__("time").gmtime())
|
||||
with open(log, "a") as f:
|
||||
f.write(json.dumps(slim, sort_keys=True) + "\n")
|
||||
except OSError:
|
||||
pass
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""await_intent_cli.py — CLI surface for the intent normalization layer.
|
||||
|
||||
Used by the await-operation harness tool so the MODEL's scheduling hints
|
||||
never reach the wait engine. Prints one JSON line.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import await_intent # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
n = sub.add_parser("normalize", help="normalize a continuation intent")
|
||||
n.add_argument("--sha", default="")
|
||||
n.add_argument("--operation-id", default="")
|
||||
n.add_argument("--model-op-type", default=None)
|
||||
n.add_argument("--model-deadline", default=None)
|
||||
n.add_argument("--user-deadline", default=None,
|
||||
help="USER_EXPLICIT deadline (authoritative)")
|
||||
n.add_argument("--contract-deadline", default=None,
|
||||
help="EXTERNAL_CONTRACT deadline (authoritative)")
|
||||
args = ap.parse_args()
|
||||
|
||||
action = {"kind": "await_operation", "sha": args.sha,
|
||||
"operation_id": args.operation_id,
|
||||
"op_type": args.model_op_type}
|
||||
model_deadline = (int(args.model_deadline)
|
||||
if args.model_deadline else None)
|
||||
user_deadline = (int(args.user_deadline)
|
||||
if args.user_deadline else None)
|
||||
contract_deadline = (int(args.contract_deadline)
|
||||
if args.contract_deadline else None)
|
||||
record = await_intent.normalize_action(
|
||||
action, model_deadline=model_deadline,
|
||||
user_deadline=user_deadline, contract_deadline=contract_deadline)
|
||||
await_intent.emit_normalized_record(record)
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+233
-53
@@ -73,7 +73,176 @@ def authority_verdict() -> str:
|
||||
return (out[-1] if out else "pending").strip()
|
||||
|
||||
|
||||
def authority_after_wake(max_tries: int = 3, gap_secs: int = 4) -> str:
|
||||
# ── T21.5 §4: operation-specific completion authority ──────────────────────
|
||||
#
|
||||
# T21.4 root cause: the wait loop had ONE hard-coded authority
|
||||
# (deploy qualification = master-ref CI) while operations waited on
|
||||
# branch CI, PR checks, or merges. Wrong authority => nonterminal
|
||||
# returns => the MODEL had to re-arm (the generator of the sleeps).
|
||||
# The wait verifier MUST match the operation being waited for.
|
||||
|
||||
OPERATION_SPECS = {
|
||||
# op_type: {
|
||||
# workflow_path: Actions run path filter (None = any),
|
||||
# head_branches: head-branch filter (None = any),
|
||||
# authority: which canonical verifier decides completion,
|
||||
# event_types: expected wake event types,
|
||||
# terminal: terminal verdicts, failure: failure verdicts,
|
||||
# }
|
||||
"branch_ci": {
|
||||
"workflow_path": "ci.yml",
|
||||
"head_branches": None,
|
||||
"authority": "ci_runs", # any completed run for this SHA
|
||||
"event_types": ("ci_completed",),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
"pull_request_checks": {
|
||||
"workflow_path": None, # all PR-triggered runs for SHA
|
||||
"head_branches": None,
|
||||
"authority": "ci_runs",
|
||||
"event_types": ("ci_completed",),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
"master_ci": {
|
||||
"workflow_path": "ci.yml",
|
||||
"head_branches": ("master",),
|
||||
"authority": "deploy_qualify", # full two-leg qualification
|
||||
"event_types": ("ci_completed",),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
"deploy": {
|
||||
"workflow_path": "deploy.yml",
|
||||
"head_branches": ("master",),
|
||||
"authority": "deploy_qualify",
|
||||
"event_types": ("ci_completed", "deploy_completed"),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
"release": {
|
||||
"workflow_path": None,
|
||||
"head_branches": ("master",),
|
||||
"authority": "deploy_qualify",
|
||||
"event_types": ("ci_completed", "release_completed"),
|
||||
"terminal": ("success",),
|
||||
"failure": ("failure", "cancelled"),
|
||||
},
|
||||
}
|
||||
|
||||
DEFAULT_OP_TYPE = "master_ci"
|
||||
|
||||
|
||||
def normalize_verdict(verdict: str) -> str:
|
||||
"""T21.5 §18 audit fix: pending-class verdicts are ALL nonterminal.
|
||||
deploy_qualify emits "pending (probe failed)" for a transient probe
|
||||
failure — that is pending, never a terminal answer. The wait loop
|
||||
must not return control to the model over a string variant."""
|
||||
v = (verdict or "").strip().lower()
|
||||
if v.startswith("pending"):
|
||||
return "pending"
|
||||
return (verdict or "").strip()
|
||||
|
||||
|
||||
def op_spec(op_type: str) -> dict:
|
||||
spec = OPERATION_SPECS.get(op_type)
|
||||
if spec is None:
|
||||
raise ValueError(
|
||||
f"unknown op_type {op_type!r} — must be one of "
|
||||
f"{sorted(OPERATION_SPECS)} (the wait verifier must match "
|
||||
"the operation being waited for)")
|
||||
return spec
|
||||
|
||||
|
||||
def op_authority_verdict(op_type: str, sha: str) -> str:
|
||||
"""Canonical completion verdict FOR THIS OPERATION TYPE.
|
||||
|
||||
ci_runs — the op's own Actions runs for this exact SHA: every
|
||||
required workflow run completed; terminal iff every
|
||||
run of the op's workflow set concluded success
|
||||
(branch/PR ops are their own truth; deploy
|
||||
qualification is NOT their authority).
|
||||
deploy_qualify — the two-leg deploy qualification (master CI +
|
||||
required-checks) via deploy_qualify.py.
|
||||
"""
|
||||
spec = op_spec(op_type)
|
||||
authority = spec["authority"]
|
||||
if authority == "deploy_qualify":
|
||||
return authority_verdict()
|
||||
if authority == "ci_runs":
|
||||
return _ci_runs_verdict(spec, sha)
|
||||
raise ValueError(f"unknown authority {authority!r} for {op_type!r}")
|
||||
|
||||
|
||||
def _ci_runs_verdict(spec: dict, sha: str) -> str:
|
||||
"""Operation-scoped Actions-run verdict (import the API client from
|
||||
deploy_qualify to keep ONE Gitea client)."""
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import deploy_qualify as dq # noqa: E402
|
||||
import gitea_auth # noqa: E402
|
||||
server = os.environ.get("GITEA_SERVER",
|
||||
getattr(gitea_auth, "DEFAULT_SERVER", "")).rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO",
|
||||
getattr(gitea_auth, "DEFAULT_REPO", ""))
|
||||
sha_env = os.environ.get("DEPLOY_SHA", "") or sha
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha_env and token):
|
||||
return "pending" # cannot verify -> not verified (fail closed)
|
||||
runs = dq._fetch_runs(server, repo, sha_env, token)
|
||||
if runs is None:
|
||||
return "pending" # probe failure -> keep waiting
|
||||
wfp = spec["workflow_path"]
|
||||
if wfp is not None:
|
||||
runs = [r for r in runs
|
||||
if str(r.get("path", "")).startswith(wfp + "@")]
|
||||
branches = spec["head_branches"]
|
||||
if branches is not None:
|
||||
runs = [r for r in runs
|
||||
if str(r.get("head_branch", "")) in branches]
|
||||
if not runs:
|
||||
return "pending" # no runs visible yet -> not terminal
|
||||
for r in runs:
|
||||
if r.get("status") != "completed":
|
||||
return "pending" # still running
|
||||
if r.get("conclusion") not in ("success", "skipped"):
|
||||
return "failure" # any non-success conclusion
|
||||
return "success"
|
||||
|
||||
|
||||
def op_ci_alive(op_type: str, sha: str) -> bool:
|
||||
"""Liveness probe FOR THIS OPERATION TYPE: any non-terminal run in
|
||||
the op's run set (failure DETECTOR, never the scheduler)."""
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import deploy_qualify as dq # noqa: E402
|
||||
import gitea_auth # noqa: E402
|
||||
spec = op_spec(op_type)
|
||||
server = os.environ.get("GITEA_SERVER",
|
||||
getattr(gitea_auth, "DEFAULT_SERVER", "")).rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO",
|
||||
getattr(gitea_auth, "DEFAULT_REPO", ""))
|
||||
sha_env = os.environ.get("DEPLOY_SHA", "") or sha
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha_env and token):
|
||||
return False
|
||||
runs = dq._fetch_runs(server, repo, sha_env, token)
|
||||
if not runs:
|
||||
return False
|
||||
wfp = spec["workflow_path"]
|
||||
if wfp is not None:
|
||||
runs = [r for r in runs
|
||||
if str(r.get("path", "")).startswith(wfp + "@")]
|
||||
branches = spec["head_branches"]
|
||||
if branches is not None:
|
||||
runs = [r for r in runs
|
||||
if str(r.get("head_branch", "")) in branches]
|
||||
terminal = {"success", "failure", "cancelled", "skipped"}
|
||||
return any(str(r.get("status", "")).lower() not in terminal
|
||||
for r in runs)
|
||||
|
||||
|
||||
def authority_after_wake(op_type: str = DEFAULT_OP_TYPE, sha: str = "",
|
||||
max_tries: int = 3, gap_secs: int = 4) -> str:
|
||||
"""Post-wake authority read. The daemon's own wake sweep uses a
|
||||
bounded event-triggered retry (4s ×3) for the job-finished-microseconds-
|
||||
ago ack window; a verdict read here can hit the same window. This is a
|
||||
@@ -81,7 +250,7 @@ def authority_after_wake(max_tries: int = 3, gap_secs: int = 4) -> str:
|
||||
event wait / watchdog."""
|
||||
verdict = "pending"
|
||||
for attempt in range(max_tries):
|
||||
verdict = authority_verdict()
|
||||
verdict = op_authority_verdict(op_type, sha)
|
||||
if verdict != "pending":
|
||||
return verdict
|
||||
if attempt < max_tries - 1:
|
||||
@@ -90,45 +259,39 @@ def authority_after_wake(max_tries: int = 3, gap_secs: int = 4) -> str:
|
||||
|
||||
|
||||
def ci_alive() -> bool:
|
||||
"""Liveness probe for THIS adapter: any non-terminal Actions run for
|
||||
the SHA in canonical Gitea (one-shot on watchdog expiry — not a poll).
|
||||
Import the probe from deploy_qualify to keep ONE API client."""
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import deploy_qualify as dq # noqa: E402 (spec-loaded scripts pattern)
|
||||
import gitea_auth
|
||||
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO", "")
|
||||
sha = os.environ.get("DEPLOY_SHA", "")
|
||||
if not (server and repo and sha):
|
||||
return False
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not token:
|
||||
return False
|
||||
runs = dq._fetch_runs(server, repo, sha, token)
|
||||
if not runs:
|
||||
return False
|
||||
terminal = {"success", "failure", "cancelled", "skipped"}
|
||||
return any(str(r.get("status", "")).lower() not in terminal
|
||||
for r in runs)
|
||||
"""LEGACY liveness probe (master-ref deploy runs) — kept for callers
|
||||
predating op specs; run() uses op_ci_alive(op_type, sha)."""
|
||||
return op_ci_alive(DEFAULT_OP_TYPE, os.environ.get("DEPLOY_SHA", ""))
|
||||
|
||||
|
||||
def run(*, sha: str, deadline_secs: int,
|
||||
watchdog_secs: int = 300,
|
||||
op_type: str = DEFAULT_OP_TYPE,
|
||||
daemon_url: str = ew.DEFAULT_DAEMON_URL) -> int:
|
||||
"""T21.5 §3: ONE LOGICAL AWAIT.
|
||||
|
||||
One logical external wait is one invocation of this function. All
|
||||
nonterminal conditions (event-seen-authority-pending, watchdog expiry
|
||||
with live runs, bounded authority-retry exhausted) re-arm INTERNALLY —
|
||||
control returns to the caller ONLY on a terminal verdict or on real
|
||||
deadline expiry. Watchdogs are failure detectors inside this
|
||||
abstraction, never the scheduler and never completion truth.
|
||||
"""
|
||||
spec = op_spec(op_type) # unknown op_type refused upfront
|
||||
op = f"ci-{sha}"
|
||||
t0 = time.time()
|
||||
# §16Y #10 race fix: the FIFO reader exists BEFORE registration, so a
|
||||
# wake arriving in the registration window can always be delivered.
|
||||
fifo_fd = ew.open_fifo_reader(op)
|
||||
# Event replay (T16 §16C live finding): on a serialized runner the
|
||||
# completion event usually arrives BEFORE the deploy job registers.
|
||||
# completion event usually arrives BEFORE the waiter registers.
|
||||
# A matching durable event means the wake already happened — skip
|
||||
# the rendezvous entirely and go straight to the authority read.
|
||||
prior = ew.find_matching_event("gitea", "ci_completed", op)
|
||||
if prior is not None and prior.get("exit_code") is not None:
|
||||
rec = ew.register_process_wait(
|
||||
op, project="rune",
|
||||
objective=f"event-driven CI qualification for {sha}",
|
||||
objective=f"event-driven {op_type} qualification for {sha}",
|
||||
watched_for="Gitea CI completion event (replayed from the "
|
||||
"durable event log)",
|
||||
provider="gitea", event_type="ci_completed", sha=sha,
|
||||
@@ -138,14 +301,16 @@ def run(*, sha: str, deadline_secs: int,
|
||||
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
resumed_at = event_at
|
||||
# the event is a WAKE, never truth — canonical authority decides
|
||||
verdict = authority_after_wake()
|
||||
if verdict == "pending" and not ci_alive():
|
||||
verdict = authority_verdict()
|
||||
verdict = normalize_verdict(authority_after_wake(op_type, sha))
|
||||
if verdict == "pending" and not op_ci_alive(op_type, sha):
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha))
|
||||
mode = ("event_replayed" if verdict != "pending"
|
||||
else "authority_unresolved")
|
||||
verdict_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
out = {
|
||||
"sha": sha,
|
||||
"op_type": op_type,
|
||||
"authority": spec["authority"],
|
||||
"registered_at": registered_at,
|
||||
"event_received_at": event_at,
|
||||
"controller_resumed_at": resumed_at,
|
||||
@@ -160,9 +325,9 @@ def run(*, sha: str, deadline_secs: int,
|
||||
return 0 if verdict == "success" else 1
|
||||
rec = ew.register_process_wait(
|
||||
op, project="rune",
|
||||
objective=f"event-driven CI qualification for {sha}",
|
||||
objective=f"event-driven {op_type} qualification for {sha}",
|
||||
watched_for="Gitea CI completion event (wake; verdict re-read "
|
||||
"from canonical Gitea)",
|
||||
f"from canonical {spec['authority']} authority)",
|
||||
provider="gitea", event_type="ci_completed", sha=sha,
|
||||
watchdog_secs=min(watchdog_secs, deadline_secs))
|
||||
registered_at = rec["created_at"]
|
||||
@@ -172,6 +337,7 @@ def run(*, sha: str, deadline_secs: int,
|
||||
event_at = ""
|
||||
resumed_at = ""
|
||||
rearm_reason = ""
|
||||
rearm_count = 0
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
remaining = max(1, int(deadline - time.monotonic()))
|
||||
@@ -185,18 +351,14 @@ def run(*, sha: str, deadline_secs: int,
|
||||
time.gmtime())
|
||||
# WAKE received — canonical authority decides, never the
|
||||
# event payload
|
||||
verdict = authority_after_wake()
|
||||
verdict = normalize_verdict(authority_after_wake(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "event"
|
||||
break
|
||||
rearm_reason = "event seen; authority pending"
|
||||
if not ci_alive():
|
||||
# CI terminal in Gitea but aggregator lagging: one
|
||||
# final truthful read, then stop
|
||||
verdict = authority_verdict()
|
||||
mode = ("event" if verdict != "pending"
|
||||
else "authority_unresolved")
|
||||
break
|
||||
# T21.5 §3: NONTERMINAL — re-arm internally, keep waiting.
|
||||
rearm_count += 1
|
||||
rearm_reason = (f"event seen; {spec['authority']} "
|
||||
"authority pending — internal re-arm")
|
||||
continue
|
||||
if result["kind"] == "eof":
|
||||
# writer gone without a token: the DURABLE record decides
|
||||
@@ -209,47 +371,62 @@ def run(*, sha: str, deadline_secs: int,
|
||||
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime())
|
||||
verdict = authority_after_wake()
|
||||
verdict = normalize_verdict(authority_after_wake(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "event_recovered_from_durable_record"
|
||||
break
|
||||
rearm_count += 1
|
||||
rearm_reason = "eof without token; durable record pending"
|
||||
continue
|
||||
# watchdog expiry: adapter liveness (Gitea runs) decides
|
||||
# re-arm vs truthful recovery — failure detector, never
|
||||
# scheduler
|
||||
if ci_alive():
|
||||
rearm_reason = "watchdog expiry; CI runs non-terminal " \
|
||||
"in Gitea — re-arm"
|
||||
# watchdog expiry: operation-scoped liveness decides re-arm
|
||||
# vs truthful recovery — failure detector, never scheduler
|
||||
if op_ci_alive(op_type, sha):
|
||||
rearm_count += 1
|
||||
rearm_reason = ("watchdog expiry; op runs non-terminal "
|
||||
"in Gitea — internal re-arm")
|
||||
continue
|
||||
verdict = authority_verdict()
|
||||
if verdict == "pending":
|
||||
mode = "watchdog_lost_event_no_verdict"
|
||||
else:
|
||||
# no live runs visible for THIS op's authority: either the
|
||||
# operation has not started yet (EXPECTED nonterminal — the
|
||||
# §18-audit generator: this must re-arm, never return to the
|
||||
# model) or it finished between probes. Bounded re-read.
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "watchdog_authority_recovered"
|
||||
break
|
||||
break
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "watchdog_authority_recovered"
|
||||
break
|
||||
# still pending: runs not started OR lagging aggregator —
|
||||
# internal re-arm; the deadline is the only other exit
|
||||
rearm_count += 1
|
||||
rearm_reason = ("watchdog expiry; op pending (not started or "
|
||||
"lagging) — internal re-arm")
|
||||
continue
|
||||
finally:
|
||||
os.close(fifo_fd)
|
||||
if verdict == "pending":
|
||||
if mode == "event":
|
||||
mode = "deadline_pending"
|
||||
verdict = authority_verdict() # final one-shot truth
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha)) # final one-shot truth
|
||||
verdict_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
out = {
|
||||
"sha": sha,
|
||||
"op_type": op_type,
|
||||
"authority": spec["authority"],
|
||||
"registered_at": registered_at,
|
||||
"event_received_at": event_at,
|
||||
"controller_resumed_at": resumed_at,
|
||||
"verdict_at": verdict_at,
|
||||
"mode": mode,
|
||||
"rearm_reason": rearm_reason,
|
||||
"internal_rearm_count": rearm_count,
|
||||
"verdict": verdict,
|
||||
"wall_secs": round(time.time() - t0, 3),
|
||||
}
|
||||
print(json.dumps(out))
|
||||
return 0 if verdict == "success" else 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sha = os.environ.get("DEPLOY_SHA", "")
|
||||
args = sys.argv[1:]
|
||||
@@ -258,10 +435,13 @@ def main() -> int:
|
||||
deadline = 900
|
||||
if "--deadline" in args:
|
||||
deadline = int(args[args.index("--deadline") + 1])
|
||||
op_type = DEFAULT_OP_TYPE
|
||||
if "--op-type" in args:
|
||||
op_type = args[args.index("--op-type") + 1]
|
||||
if not sha:
|
||||
print(json.dumps({"error": "no sha (DEPLOY_SHA or --sha required)"}))
|
||||
return 1
|
||||
return run(sha=sha, deadline_secs=deadline)
|
||||
return run(sha=sha, deadline_secs=deadline, op_type=op_type)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -128,8 +128,13 @@ def _fetch_jobs(server: str, repo: str, run_id, token: str) -> list[dict] | None
|
||||
# ── CLI ───────────────────────────────────────────────────────────────────
|
||||
|
||||
def main() -> int:
|
||||
server = os.environ.get("GITEA_SERVER", "").rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO", "")
|
||||
# T21.4 fix: fall back to the canonical defaults when the caller did
|
||||
# not export GITEA_SERVER/GITEA_REPO (CI always exports them; local
|
||||
# and first-class-tool consumers previously got a silent "pending"
|
||||
# verdict — a wrong answer, not a fail-closed one).
|
||||
server = (os.environ.get("GITEA_SERVER", "")
|
||||
or gitea_auth.DEFAULT_SERVER).rstrip("/")
|
||||
repo = os.environ.get("GITEA_REPO", "") or gitea_auth.DEFAULT_REPO
|
||||
sha = os.environ.get("DEPLOY_SHA", "")
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha and token):
|
||||
|
||||
@@ -38,11 +38,17 @@ def build_context(issue: dict, define_contract: dict) -> dict:
|
||||
# 3. Find matching test files
|
||||
matched_tests = _find_tests(keywords)
|
||||
|
||||
# 4. T21.5 §10: Memory internal-experience port — ONE port of call,
|
||||
# bounded, zero-result = normal (fast path: [] → next step directly).
|
||||
from research_memory_port import memory_port_of_call
|
||||
memory_candidates = memory_port_of_call(issue, keywords)
|
||||
|
||||
return {
|
||||
"keywords": keywords,
|
||||
"umbrella_matches": matched_modules,
|
||||
"candidate_files": matched_files[:15], # bounded
|
||||
"candidate_tests": matched_tests[:10], # bounded
|
||||
"memory_candidates": memory_candidates, # evidence, never authority
|
||||
"define_contract": define_contract,
|
||||
"mimo_needed": _needs_mimo(matched_files, matched_tests),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
"""research_memory_port.py — T21.5 §10-12: the future-facing Memory port
|
||||
inside the EXISTING INWARD research stage.
|
||||
|
||||
BOUNDARIES (G9/G10/G11):
|
||||
- This is a PORT, not a memory system. No storage is created here; the
|
||||
implementation delegates to a provider callable that future Memory
|
||||
will supply. Tests inject doubles; INWARD never imports Memory.
|
||||
- Zero result is a NORMAL SUCCESSFUL result: the caller continues the
|
||||
normal INWARD flow immediately (no extra model call, no ceremony).
|
||||
- Returned material is EVIDENCE/CANDIDATES only — never an instruction
|
||||
authority. APPLICABILITY (existing stage) governs whether a candidate
|
||||
applies; SYNTHESIS receives only distilled, applicable consequences.
|
||||
- Teachable-moment seam: durable construction evidence can mark an
|
||||
episode as a CANDIDATE for future learning ("may be worth learning",
|
||||
never "always apply"). Substrate/Memory (future) harvest candidates;
|
||||
today the seam only records them via the same provider contract.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any, Callable
|
||||
|
||||
# The provider contract: ONE callable, independently replaceable/mockable.
|
||||
# Signature: query(query_text, keywords, *, limit) -> list[MemoryCandidate]
|
||||
# MemoryCandidate (dict): {source, kind, summary, relevance, episode_ref}
|
||||
# kind ∈ {construction_episode, teachable_moment_candidate,
|
||||
# prior_failure, prior_success}
|
||||
# Future Memory supplies the real implementation; nothing else changes.
|
||||
|
||||
MemoryProvider = Callable[..., list[dict]]
|
||||
|
||||
_DEFAULT_PROVIDER: MemoryProvider | None = None
|
||||
|
||||
|
||||
def set_memory_provider(provider: MemoryProvider | None) -> None:
|
||||
"""Bind the Memory-side implementation (production or test double).
|
||||
None disables the port — INWARD continues without it (fast path)."""
|
||||
global _DEFAULT_PROVIDER
|
||||
_DEFAULT_PROVIDER = provider
|
||||
|
||||
|
||||
def _provider() -> MemoryProvider | None:
|
||||
global _DEFAULT_PROVIDER
|
||||
if _DEFAULT_PROVIDER is not None:
|
||||
return _DEFAULT_PROVIDER
|
||||
env = os.environ.get("RUNE_MEMORY_PORT_PROVIDER")
|
||||
if env:
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("mprov", env)
|
||||
if spec is not None and spec.loader is not None:
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
provider = getattr(mod, "query", None)
|
||||
if provider is not None:
|
||||
_DEFAULT_PROVIDER = provider
|
||||
return _DEFAULT_PROVIDER
|
||||
|
||||
|
||||
MAX_CANDIDATES = 5
|
||||
MIN_RELEVANCE = 0.3
|
||||
|
||||
|
||||
def memory_port_of_call(issue: dict, keywords: list[str],
|
||||
*, provider: MemoryProvider | None = None) -> list[dict]:
|
||||
"""The ONE internal port of call inside INWARD. Bounded retrieval of
|
||||
internal evidence (construction episodes, teachable-moment
|
||||
candidates, prior failures/successes).
|
||||
|
||||
ZERO-RESULT FAST PATH (§11): empty/thin results return [] and the
|
||||
caller continues the next normal INWARD step directly. No extra
|
||||
model call, no fallback reasoning, no synthetic summary, no delay.
|
||||
"""
|
||||
prov = provider or _provider()
|
||||
if prov is None:
|
||||
return [] # port unbound: normal INWARD continues, zero overhead
|
||||
try:
|
||||
query_text = " ".join([
|
||||
str(issue.get("title", "")),
|
||||
str(issue.get("body", ""))[:400],
|
||||
]).strip()
|
||||
raw = prov(query_text, keywords, limit=MAX_CANDIDATES) or []
|
||||
except Exception: # noqa: BLE001 — port failure must never break INWARD
|
||||
return []
|
||||
candidates = [c for c in raw
|
||||
if float(c.get("relevance", 0.0)) >= MIN_RELEVANCE]
|
||||
return candidates[:MAX_CANDIDATES]
|
||||
|
||||
|
||||
def applicability_filter(candidates: list[dict],
|
||||
context_keywords: list[str]) -> tuple[list[dict],
|
||||
list[dict]]:
|
||||
"""Existing APPLICABILITY governs transfer: candidates relevant to
|
||||
the CURRENT context pass; irrelevant ones are rejected WITHOUT
|
||||
contaminating downstream SYNTHESIS. Returns (applicable, rejected)."""
|
||||
applicable, rejected = [], []
|
||||
for c in candidates:
|
||||
text = (str(c.get("summary", "")) + " " +
|
||||
" ".join(str(k) for k in c.get("keywords", []))).lower()
|
||||
overlap = sum(1 for k in context_keywords if k and k.lower() in text)
|
||||
if overlap > 0 or c.get("explicitly_applicable") is True:
|
||||
applicable.append(c)
|
||||
else:
|
||||
rejected.append(c)
|
||||
return applicable, rejected
|
||||
|
||||
|
||||
# ── §12: teachable-moment future seam ──────────────────────────────────────
|
||||
|
||||
CANDIDATE_TRIGGERS = (
|
||||
"completed_release", "capability_completed", "failure_recovered",
|
||||
"rollback_executed", "user_correction", "prediction_observation_mismatch",
|
||||
"procedure_improved",
|
||||
)
|
||||
|
||||
|
||||
def mark_teachable_moment_candidate(
|
||||
episode_ref: str, *, trigger: str, summary: str,
|
||||
emit: Callable[[dict], Any] | None = None) -> dict | None:
|
||||
"""Record durable evidence that an episode MAY be worth future
|
||||
learning — a candidate, never a rule. Uses the existing evidence/
|
||||
event mechanism via `emit` (the caller's channel: journal, event
|
||||
bridge, ledger history). No new canonical store is created here;
|
||||
future Substrate/Memory harvests candidates through the port."""
|
||||
if trigger not in CANDIDATE_TRIGGERS:
|
||||
raise ValueError(
|
||||
f"unknown trigger {trigger!r} — must be one of "
|
||||
f"{CANDIDATE_TRIGGERS}")
|
||||
record = {
|
||||
"candidate": "teachable_moment",
|
||||
"episode_ref": episode_ref,
|
||||
"trigger": trigger,
|
||||
"summary": summary[:200],
|
||||
"status": "CANDIDATE", # never authority, never auto-applied
|
||||
"note": "may be worth future learning; APPLICABILITY governs "
|
||||
"any future transfer",
|
||||
}
|
||||
if emit is not None:
|
||||
emit(record)
|
||||
return record
|
||||
@@ -0,0 +1,259 @@
|
||||
#!/usr/bin/env python3
|
||||
"""rollback_generation.py — T21.4 §21.4F: the governed rollback API.
|
||||
|
||||
rollback_generation(target_generation, expected_current, reason)
|
||||
|
||||
Prior tranches proved rollback by recreating deployment primitives by hand
|
||||
(ln -sfn against the live deploy root). That is operator work, not governed
|
||||
machinery — it bypasses lock, eligibility, health, and history. This module
|
||||
is the single canonical rollback operation, built ON the proven deploy
|
||||
machinery (deploy_promotion's lock + atomic swap + build-identity authority),
|
||||
not beside it.
|
||||
|
||||
The promotion authority (deploy_promotion.py) is deliberately MONOTONIC: an
|
||||
ancestor promotion is refused as STALE_SUPERSEDED. Regression is therefore
|
||||
impossible through promotion — and legitimate only through THIS operation,
|
||||
which owns its own obligations:
|
||||
|
||||
1. deployment lock — the SAME .promotion.lock as promote (serialize)
|
||||
2. current verification — build-identity readback must equal expected_current
|
||||
3. target existence — generation dir present, build identity readable
|
||||
4. eligibility — target commit is ancestor-or-equal of current
|
||||
(regression only; forward = promotion's job;
|
||||
divergent/unknown refused)
|
||||
5. atomic switch — rename(2) symlink swap, never absent/half-updated
|
||||
6. runtime handoff — Rune executes from current/src at process start;
|
||||
handoff = identity a fresh launch would run
|
||||
7. runtime verification — fresh-launch identity must equal target
|
||||
8. health qualification — import smoke against target generation src
|
||||
9. durable history — JSONL rollback events, appended inside the lock
|
||||
10. failure recovery — health failure restores prior current (where safe)
|
||||
11. coherent last-good — last-good := the generation that WAS current
|
||||
(same invariant promotion maintains)
|
||||
|
||||
Truthful outcomes:
|
||||
ROLLED_BACK target active, health green, history recorded
|
||||
NOOP_ALREADY_CURRENT target already the deployed generation
|
||||
ROLLED_BACK_REVERTED switched, health red, prior current restored
|
||||
REFUSED_UNKNOWN_CURRENT deployed identity unreadable (fail closed)
|
||||
REFUSED_EXPECTED_MISMATCH current != expected_current (stale caller)
|
||||
REFUSED_TARGET_MISSING target generation dir absent
|
||||
REFUSED_TARGET_UNREADABLE target build identity unreadable
|
||||
REFUSED_NOT_ELIGIBLE divergent, newer, or non-ancestor target
|
||||
REFUSED_NO_REASON empty reason (accountability is mandatory)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import fcntl
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(_SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import deploy_promotion # proven machinery: lock path, ancestry, swaps
|
||||
|
||||
DEPLOY_ROOT = deploy_promotion.DEPLOY_ROOT
|
||||
HARNESS_ROOT = _SCRIPT_DIR.parent
|
||||
HISTORY_PATH = HARNESS_ROOT / "store" / "scaffold" / "rollback_history.jsonl"
|
||||
|
||||
ROLLED_BACK = "ROLLED_BACK"
|
||||
NOOP = "NOOP_ALREADY_CURRENT"
|
||||
ROLLED_BACK_REVERTED = "ROLLED_BACK_REVERTED"
|
||||
REFUSED_UNKNOWN_CURRENT = "REFUSED_UNKNOWN_CURRENT"
|
||||
REFUSED_EXPECTED_MISMATCH = "REFUSED_EXPECTED_MISMATCH"
|
||||
REFUSED_TARGET_MISSING = "REFUSED_TARGET_MISSING"
|
||||
REFUSED_TARGET_UNREADABLE = "REFUSED_TARGET_UNREADABLE"
|
||||
REFUSED_NOT_ELIGIBLE = "REFUSED_NOT_ELIGIBLE"
|
||||
REFUSED_NO_REASON = "REFUSED_NO_REASON"
|
||||
|
||||
|
||||
def _identity(deploy_root: Path, generation: str) -> str:
|
||||
"""Authoritative commit for a generation dir (build identity, not name)."""
|
||||
gen_dir = deploy_root / generation
|
||||
identity = gen_dir / "src" / "rune" / "_build_identity.json"
|
||||
try:
|
||||
data = json.loads(identity.read_text(encoding="utf-8"))
|
||||
return str(data.get("commit", ""))
|
||||
except (OSError, ValueError):
|
||||
return ""
|
||||
|
||||
|
||||
def _health(deploy_root: Path, generation: str, timeout: int = 60) -> tuple[bool, str]:
|
||||
"""Health qualification for a generation: build identity readable AND
|
||||
`import rune` succeeds from that generation's src tree."""
|
||||
gen_dir = deploy_root / generation
|
||||
commit = _identity(deploy_root, generation)
|
||||
if not commit:
|
||||
return False, "build identity unreadable"
|
||||
src = gen_dir / "src"
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[sys.executable, "-c", "import rune; import sys; "
|
||||
"sys.stdout.write(getattr(rune, '__version__', 'ok'))"],
|
||||
env={"PYTHONPATH": str(src), "PATH": "/usr/bin:/bin",
|
||||
"HOME": str(gen_dir)},
|
||||
capture_output=True, text=True, timeout=timeout, cwd=str(gen_dir),
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, f"import smoke timed out after {timeout}s"
|
||||
if proc.returncode != 0:
|
||||
return False, f"import smoke failed: {(proc.stderr or '')[-200:]}"
|
||||
return True, f"identity={commit[:12]} import_smoke=ok ({proc.stdout.strip()[:40]})"
|
||||
|
||||
|
||||
def _append_history(record: dict, history_path: Path = HISTORY_PATH) -> None:
|
||||
history_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(history_path, "a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(record, sort_keys=True) + "\n")
|
||||
|
||||
|
||||
def _swap(deploy_root: Path, name: str, target: Path) -> None:
|
||||
deploy_promotion._atomic_symlink_target(deploy_root, name, target)
|
||||
|
||||
|
||||
def rollback_generation(
|
||||
target_generation: str,
|
||||
expected_current: str,
|
||||
reason: str,
|
||||
*,
|
||||
deploy_root: Path = DEPLOY_ROOT,
|
||||
repo: Path = HARNESS_ROOT,
|
||||
history_path: Path = HISTORY_PATH,
|
||||
) -> tuple[str, dict]:
|
||||
"""The governed rollback operation. Returns (outcome, detail).
|
||||
|
||||
Refusals happen BEFORE any mutation of current/last-good. The lock is
|
||||
shared with promote() so rollback and promotion can never interleave.
|
||||
"""
|
||||
if not reason or not reason.strip():
|
||||
return REFUSED_NO_REASON, {"error": "rollback requires a reason"}
|
||||
|
||||
lock_path = deploy_root / ".promotion.lock"
|
||||
# The lock file may be owned by the CI runner (promotion side opens it
|
||||
# for writing). flock needs only an open fd, never write permission, so
|
||||
# open read-only when write is denied — same file, same fence.
|
||||
try:
|
||||
lock_fd = open(lock_path, "a+")
|
||||
except PermissionError:
|
||||
lock_fd = open(lock_path) if lock_path.exists() else \
|
||||
open(lock_path, "a+")
|
||||
with lock_fd:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_EX)
|
||||
try:
|
||||
current = deploy_root / "current"
|
||||
last_good = deploy_root / "last-good"
|
||||
detail: dict = {"target": target_generation,
|
||||
"expected_current": expected_current,
|
||||
"reason": reason[:200],
|
||||
"at": time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime())}
|
||||
|
||||
# 2. current verification — identity authority, never the name
|
||||
current_generation = ""
|
||||
try:
|
||||
current_generation = Path(os_readlink(current)).name
|
||||
except OSError:
|
||||
pass
|
||||
deployed_commit = deploy_promotion.read_deployed_commit(deploy_root)
|
||||
if not deployed_commit:
|
||||
return REFUSED_UNKNOWN_CURRENT, {
|
||||
"error": "deployed commit unreadable — fail closed",
|
||||
**detail}
|
||||
detail["current_generation"] = current_generation
|
||||
detail["current_commit"] = deployed_commit
|
||||
if expected_current and expected_current != deployed_commit:
|
||||
detail["error"] = (
|
||||
f"current is {deployed_commit[:12]}, "
|
||||
f"caller expected {expected_current[:12]} — refused "
|
||||
"before mutation")
|
||||
return REFUSED_EXPECTED_MISMATCH, detail
|
||||
|
||||
# 3. target existence + identity
|
||||
target_dir = deploy_root / target_generation
|
||||
if not target_dir.is_dir():
|
||||
detail["error"] = f"generation dir {target_generation} absent"
|
||||
return REFUSED_TARGET_MISSING, detail
|
||||
target_commit = _identity(deploy_root, target_generation)
|
||||
if not target_commit:
|
||||
detail["error"] = (
|
||||
f"generation {target_generation} build identity unreadable")
|
||||
return REFUSED_TARGET_UNREADABLE, detail
|
||||
detail["target_commit"] = target_commit
|
||||
|
||||
if target_commit == deployed_commit:
|
||||
detail["note"] = "target already the deployed generation"
|
||||
return NOOP, detail
|
||||
|
||||
# 4. eligibility — ancestor-or-equal only (regression)
|
||||
rel = deploy_promotion.ancestry_of(target_commit, deployed_commit,
|
||||
repo)
|
||||
if rel != "ancestor":
|
||||
detail["error"] = (
|
||||
f"target commit ancestry={rel} — only ancestors are "
|
||||
"rollback-eligible (forward movement belongs to "
|
||||
"deploy_promotion)")
|
||||
return REFUSED_NOT_ELIGIBLE, detail
|
||||
|
||||
# 5. atomic switch (inside lock), last-good := previous current
|
||||
if last_good.is_symlink():
|
||||
last_good.unlink()
|
||||
_swap(deploy_root, "last-good", Path(os_readlink(current)))
|
||||
_swap(deploy_root, "current", target_dir)
|
||||
detail["switched_to"] = target_generation
|
||||
|
||||
# 7+8. runtime verification + health qualification
|
||||
post_commit = deploy_promotion.read_deployed_commit(deploy_root)
|
||||
health_ok, health_detail = _health(deploy_root, target_generation)
|
||||
detail["post_switch_identity"] = post_commit
|
||||
detail["health"] = health_detail
|
||||
|
||||
record = {"at": detail["at"], "operation": "rollback_generation",
|
||||
**detail}
|
||||
if post_commit != target_commit or not health_ok:
|
||||
# 10. failure recovery — restore prior current (safe: we
|
||||
# hold the lock and the prior generation is intact)
|
||||
prev = Path(os_readlink(last_good)) if last_good.is_symlink() \
|
||||
else None
|
||||
if prev is not None:
|
||||
_swap(deploy_root, "current", prev)
|
||||
detail["reverted_to"] = (Path(str(os_readlink(current))).name
|
||||
if current.is_symlink() else "?")
|
||||
detail["outcome"] = ROLLED_BACK_REVERTED
|
||||
record["outcome"] = ROLLED_BACK_REVERTED
|
||||
_append_history(record, history_path)
|
||||
return ROLLED_BACK_REVERTED, detail
|
||||
|
||||
record["outcome"] = ROLLED_BACK
|
||||
_append_history(record, history_path)
|
||||
detail["outcome"] = ROLLED_BACK
|
||||
return ROLLED_BACK, detail
|
||||
finally:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def os_readlink(p: Path) -> str:
|
||||
return str(Path(str(p)).readlink()) if hasattr(Path, "readlink") \
|
||||
else str(Path(str(p)).readlink())
|
||||
|
||||
|
||||
def main() -> int:
|
||||
import argparse
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("target_generation")
|
||||
ap.add_argument("--expected-current", required=True,
|
||||
help="commit the caller believes is currently deployed")
|
||||
ap.add_argument("--reason", required=True)
|
||||
args = ap.parse_args()
|
||||
outcome, detail = rollback_generation(args.target_generation,
|
||||
args.expected_current, args.reason)
|
||||
print(json.dumps({"outcome": outcome, **detail}, indent=2, sort_keys=True))
|
||||
return 0 if outcome in (ROLLED_BACK, NOOP) else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3
|
||||
"""start_wr_daemon.py — T21.5 §7: THE canonical daemon restart launcher.
|
||||
|
||||
T21.4 live finding: `python3 daemon.py` defaults WR_ROOT to the daemon's
|
||||
install dir, so a restart silently opened a second event store. This
|
||||
launcher resolves the root ONCE through scripts/wr_root_resolver.py (the
|
||||
single authority) and passes it explicitly — nobody has to remember to
|
||||
export WR_ROOT.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import wr_root_resolver as resolver # noqa: E402
|
||||
|
||||
DAEMON_CODE_CANDIDATES = (
|
||||
"/home/nick/.local/share/mimo-wait-resume/daemon.py",
|
||||
)
|
||||
|
||||
|
||||
def find_daemon_code() -> Path:
|
||||
for cand in DAEMON_CODE_CANDIDATES:
|
||||
p = Path(cand)
|
||||
if p.is_file():
|
||||
return p
|
||||
raise RuntimeError(f"daemon.py not found in {DAEMON_CODE_CANDIDATES}")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
root = resolver.resolve_root()
|
||||
daemon = find_daemon_code()
|
||||
env = dict(os.environ,
|
||||
WR_ROOT=str(root), # daemon data root
|
||||
RUNE_WR_ROOT=str(root), # adapter-side authority
|
||||
RUNE_WR_CODE=str(daemon.parent))
|
||||
port = sys.argv[1] if len(sys.argv) > 1 else "8791"
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, str(daemon), port],
|
||||
env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
||||
start_new_session=True)
|
||||
# bounded startup probe (process-local, not an external wait)
|
||||
for _ in range(20):
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{port}/health", timeout=1) as r:
|
||||
if r.status == 200:
|
||||
print(f"wr daemon up: pid={proc.pid} root={root} "
|
||||
f"port={port}")
|
||||
return 0
|
||||
except OSError:
|
||||
time.sleep(0.25)
|
||||
print(f"wr daemon did not become healthy (pid={proc.pid})", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,331 @@
|
||||
#!/usr/bin/env python3
|
||||
"""structural_coverage_audit.py — T21.4 §21.4G: Rune-wide meaningful
|
||||
structural coverage audit.
|
||||
|
||||
Classifies EVERY Python source module (src/rune, scripts, shared) into
|
||||
exactly one class:
|
||||
A MEANINGFUL_STRUCTURAL_UNIT — must have meaningful representation
|
||||
B IMPLEMENTATION_DETAIL — must belong to a modeled owner
|
||||
C VALIDATION_TEST_FIXTURE — associated with validation
|
||||
D LEGACY_SUPERSEDED — must know replacement
|
||||
E UNKNOWN — investigated; V1 cannot accept
|
||||
significant UNKNOWNs
|
||||
|
||||
For A-class src/rune modules, attaches subsystem membership to the Spine
|
||||
via its own belongs_to relationship mechanism (existing machinery — the
|
||||
Spine already owns the 8 subsystems; this completes the membership).
|
||||
|
||||
Writes store/scaffold/structural_coverage_t214.json + prints measures.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(ROOT / "src"))
|
||||
|
||||
# ── A-class: src/rune meaningful units → subsystem ─────────────────────────
|
||||
SRC_A = {
|
||||
# governance / construction / structural
|
||||
"scaffold": "governance", "structural_store": "governance",
|
||||
"structural_model": "governance", "structural_migration": "governance",
|
||||
"spine": "governance", "construction_activation": "governance",
|
||||
"construction_commands": "governance", "construction_decision": "governance",
|
||||
"construction_depth": "governance", "construction_intent": "governance",
|
||||
"construction_interface": "governance", "construction_mode": "governance",
|
||||
"construction_pattern": "governance",
|
||||
"construction_patterns_builtin": "governance",
|
||||
"construction_projection": "governance", "stage_guards": "governance",
|
||||
"capability_fit": "governance", "toolchain": "governance",
|
||||
"layout_contract": "governance", "project_registry": "governance",
|
||||
"programme_gates": "governance", "programme_continuation": "governance",
|
||||
"build_basis": "governance", "iteration": "governance",
|
||||
"event_wait": "governance", "wait_guard": "governance",
|
||||
"failure_recovery": "governance", "failure_auto_filer": "governance",
|
||||
"forge": "governance", "forge_client": "governance",
|
||||
"release_notes": "governance",
|
||||
# memory (existing machinery — not Memory V1)
|
||||
"memory_store": "memory", "memory_selector": "memory",
|
||||
"memory_consolidation": "memory", "memory_env": "memory",
|
||||
"shard_manager": "memory", "tier_manager": "memory",
|
||||
"graph_memory": "memory", "query_memory": "memory",
|
||||
"curator": "memory", "sleep_time": "memory",
|
||||
"subconscious_pass": "memory", "warm_neuron_store": "memory",
|
||||
"warm_router": "memory", "abstraction": "memory",
|
||||
"auto_context": "memory", "cache": "memory",
|
||||
"connection_pool": "memory",
|
||||
# growth / governed adaptation / ledgers
|
||||
"drift_core": "growth", "drift_ledger": "growth",
|
||||
"drift_cache": "growth", "drift_compression": "growth",
|
||||
"drift_observability": "growth", "drift_resume": "growth",
|
||||
"growth_delta": "growth", "self_improve": "growth",
|
||||
"night_mode": "growth", "behavior": "growth",
|
||||
"outcome_ledger": "growth", "claim_audit": "growth",
|
||||
# identity / persona / voice
|
||||
"identity_anchors": "identity", "persona_definition": "identity",
|
||||
"persona_gate": "identity", "voice_hybrid": "identity",
|
||||
"voice_sync": "identity", "safety_override": "identity",
|
||||
"prompt_sync": "identity",
|
||||
# inference / routing / providers
|
||||
"adaptive_router": "inference", "route_risk_flags": "inference",
|
||||
"model_routing": "inference", "model_connect": "inference",
|
||||
"model_profiles": "inference", "model_registry": "inference",
|
||||
"provider_registry": "inference", "provider_context": "inference",
|
||||
"provider_budget": "inference", "standardized_providers": "inference",
|
||||
"openai_compat_transport": "inference",
|
||||
"local_tool_runtime": "inference", "response_adapter": "inference",
|
||||
"inference_trace": "inference", "evidence_probe": "inference",
|
||||
"recall_router": "inference", "recall_uplift": "inference",
|
||||
# orchestration / cognition / continuation / ledgers
|
||||
"elastic_cognition": "orchestration", "tiered_executor": "orchestration",
|
||||
"attention_capsule": "orchestration", "witness": "orchestration",
|
||||
"event_bus": "orchestration", "microkernel": "orchestration",
|
||||
"capability_broker": "orchestration", "context_broker": "orchestration",
|
||||
"work_manager": "orchestration", "task_persist": "orchestration",
|
||||
"goal": "orchestration", "cron_engine": "orchestration",
|
||||
"hook_engine": "orchestration", "subsystem_health": "orchestration",
|
||||
"verification_layer": "orchestration", "build_mode": "orchestration",
|
||||
"builder_bridge": "orchestration", "testing_mode": "orchestration",
|
||||
"rune_operator": "orchestration", "audit_ledger": "capability",
|
||||
"capability_ledger": "capability", "behaviour_ledger": "capability",
|
||||
"impact_preview": "governance", "graph_projection": "governance",
|
||||
"tool_executor": "orchestration", "tool_protocol": "orchestration",
|
||||
# perception / TUI boundary
|
||||
"rune_tui": "perception", "__main__": "perception",
|
||||
"voicebox": "perception",
|
||||
}
|
||||
|
||||
# B-class: src/rune implementation details → owning meaningful unit
|
||||
SRC_B = {
|
||||
"event_envelope": "event_bus",
|
||||
"cognitive_budgeter": "elastic_cognition",
|
||||
"salience_gate": "elastic_cognition",
|
||||
"rune_profiler": "elastic_cognition",
|
||||
"progressive_escalation": "tiered_executor",
|
||||
"parallel_processor": "tiered_executor",
|
||||
"context_pressure": "context_broker",
|
||||
"token_optimizer": "context_broker",
|
||||
"message_queue": "work_manager",
|
||||
"auto_tasks": "work_manager",
|
||||
"task_gate": "task_persist",
|
||||
"pins": "goal",
|
||||
"chat_bubble": "rune_tui",
|
||||
"rune_spinner": "rune_tui",
|
||||
"rune_graphic": "rune_tui",
|
||||
"message_widgets": "rune_tui",
|
||||
"tui_validator": "rune_tui",
|
||||
"tui_backlog": "rune_tui",
|
||||
"builder_bridge_cli": "builder_bridge",
|
||||
"voice_injector": "voice_hybrid",
|
||||
"intent_continuity": "programme_continuation",
|
||||
"time_awareness": "rune_operator",
|
||||
"contracts": "verification_layer",
|
||||
"system_registry": "governance-ops",
|
||||
"dependency_tracker": "ops-harness",
|
||||
}
|
||||
|
||||
# C-class: validation/test fixtures (canaries + corpus)
|
||||
SRC_C = {
|
||||
"canary_cognitive_strata", "canary_context_broker",
|
||||
"canary_conversation_state", "canary_full_system",
|
||||
"canary_gitea_automation", "canary_gitea_delegation",
|
||||
"canary_gitea_pipeline", "canary_memory_synthesis", "canary_native",
|
||||
"canary_orchestration_foundation", "canary_p0_04", "canary_p0_05",
|
||||
"canary_p0_07", "canary_p1_01", "canary_p1_02", "canary_p1_03",
|
||||
"canary_p1_06", "canary_p1_07", "canary_query_memory",
|
||||
"canary_resource_authority", "canary_schema_migration",
|
||||
"canary_surface_parity", "canary_tui_drift", "canary_unbounded",
|
||||
"wait_guard_corpus", "contracts",
|
||||
}
|
||||
|
||||
# scripts/ A-class (deployment/CI/release/research/governance machinery)
|
||||
SCRIPTS_A = {
|
||||
"auto_bump_pr", "capture_failure", "carry_forward_check",
|
||||
"check_dep_lock", "check_discharge_contract", "check_version_surfaces",
|
||||
"ci_event_wait", "construction_guards", "create_release",
|
||||
"deploy_promotion", "deploy_qualify", "deploy_stage",
|
||||
"rollback_generation", "file_drift_issue", "gitea_auth",
|
||||
"gitea_reconciliation_check", "install_mimo_hook", "lint_ratchet",
|
||||
"merge_robot", "mutation_boundary", "notify_event_bridge",
|
||||
"process_experience", "publish_source_ledger", "research_artifact",
|
||||
"research_backfill_sweep", "research_define", "research_event_relay",
|
||||
"research_inward_context", "research_prompt_compiler",
|
||||
"research_router", "research_stage_executor", "research_task_tree",
|
||||
"runner_evidence", "scaffold_projection", "semver_bump",
|
||||
"source_ledger", "stage_contracts", "stage_telemetry",
|
||||
"t14_builder_session", "t15_builder_session", "type_check_ratchet",
|
||||
"umbrella_impact", "version_convergence",
|
||||
}
|
||||
SCRIPTS_A = set(SCRIPTS_A) | {"structural_coverage_audit"}
|
||||
SCRIPTS_B = {"gen_prd_state": "governance-ops",
|
||||
"pl1_records": "process_experience",
|
||||
"release_history_cli": "release_notes"}
|
||||
SCRIPTS_C = {"fake_worker", "tui_startup_canary"}
|
||||
|
||||
# shared/ — harness-facing layer; mirrors of canonical src modules = D
|
||||
SHARED_D = {
|
||||
"audit_ledger": "src/rune/audit_ledger.py",
|
||||
"dependency_tracker": "src/rune/dependency_tracker.py",
|
||||
"drift_cache": "src/rune/drift_cache.py",
|
||||
"drift_compression": "src/rune/drift_compression.py",
|
||||
"drift_core": "src/rune/drift_core.py",
|
||||
"drift_observability": "src/rune/drift_observability.py",
|
||||
"gitea_auth": "scripts/gitea_auth.py",
|
||||
"query_memory": "src/rune/query_memory.py",
|
||||
"shard_manager": "src/rune/shard_manager.py",
|
||||
"subconscious_pass": "src/rune/subconscious_pass.py",
|
||||
"tier_manager": "src/rune/tier_manager.py",
|
||||
}
|
||||
SHARED_B_OWNER = "ops-harness"
|
||||
SHARED_B = {"auto_detect", "autonomy_loop", "build_orchestrator",
|
||||
"build_pipeline_policy", "cognitive_loop", "custodian",
|
||||
"dream_cycle", "gitea_pipeline", "gitea_provision",
|
||||
"graph_shard_bridge", "growth_simulator", "umbrella_update"}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
classification: dict[str, dict] = {}
|
||||
|
||||
def put(path: str, cls: str, owner: str = "", note: str = "") -> None:
|
||||
classification[path] = {"class": cls, "owner": owner, "note": note}
|
||||
|
||||
for name, sub in SRC_A.items():
|
||||
put(f"src/rune/{name}.py", "A", owner=f"subsystem:{sub}")
|
||||
for name, owner in SRC_B.items():
|
||||
put(f"src/rune/{name}.py", "B", owner=owner)
|
||||
for name in SRC_C:
|
||||
put(f"src/rune/{name}.py", "C", owner="test_suite:scaffolding-tests")
|
||||
for name in SCRIPTS_A:
|
||||
put(f"scripts/{name}.py", "A", owner="subsystem:governance")
|
||||
for name, owner in SCRIPTS_B.items():
|
||||
put(f"scripts/{name}.py", "B", owner=owner)
|
||||
for name in SCRIPTS_C:
|
||||
put(f"scripts/{name}.py", "C", owner="test_suite:scaffolding-tests")
|
||||
for name, canonical in SHARED_D.items():
|
||||
put(f"shared/{name}.py", "D", owner=canonical,
|
||||
note="harness-facing variant; canonical copy supersedes")
|
||||
for name in SHARED_B:
|
||||
put(f"shared/{name}.py", "B", owner=SHARED_B_OWNER)
|
||||
|
||||
# ── E investigation: any .py file not classified? ──
|
||||
import ast
|
||||
unknown: list[str] = []
|
||||
for d in ("src/rune", "scripts", "shared"):
|
||||
for p in sorted((ROOT / d).glob("*.py")):
|
||||
rel = str(p.relative_to(ROOT))
|
||||
if p.name == "__init__.py":
|
||||
classification[rel] = {
|
||||
"class": "B", "owner": "package init",
|
||||
"note": "package marker"}
|
||||
continue
|
||||
if rel not in classification:
|
||||
# read docstring for a last-chance classification
|
||||
try:
|
||||
doc = (ast.get_docstring(
|
||||
ast.parse(p.read_text())) or "").strip()
|
||||
except SyntaxError:
|
||||
doc = ""
|
||||
unknown.append(f"{rel} :: {doc.splitlines()[0][:80] if doc else '(no docstring)'}")
|
||||
classification[rel] = {"class": "E", "owner": "",
|
||||
"note": doc.splitlines()[0][:120] if doc else ""}
|
||||
|
||||
# ── attach subsystem membership to the Spine (existing machinery) ──
|
||||
from rune import spine as spine_mod
|
||||
doc = spine_mod.load() or spine_mod.new_doc()
|
||||
added = 0
|
||||
for name, sub in SRC_A.items():
|
||||
rel = spine_mod.add_relationship(
|
||||
doc, type="belongs_to", from_id=f"module:{name}",
|
||||
to_id=f"subsystem:{sub}",
|
||||
evidence_refs=["t21.4-structural-coverage"])
|
||||
if rel is not None:
|
||||
added += 1
|
||||
spine_mod.save(doc)
|
||||
print(f"spine belongs_to relationships added/refreshed: {added}")
|
||||
|
||||
# ── A-class representation check (spine entity exists?) ──
|
||||
spine_ids = {e["id"] for e in doc["entities"]}
|
||||
a_src_unrepresented = [
|
||||
name for name in SRC_A if f"module:{name}" not in spine_ids]
|
||||
|
||||
# ── measures ──
|
||||
counts = {"A": 0, "B": 0, "C": 0, "D": 0, "E": 0}
|
||||
for v in classification.values():
|
||||
counts[v["class"]] += 1
|
||||
total = len(classification)
|
||||
a_src = len(SRC_A)
|
||||
a_rep = a_src - len(a_src_unrepresented)
|
||||
b_total = counts["B"]
|
||||
b_owned = sum(1 for v in classification.values()
|
||||
if v["class"] == "B" and v["owner"])
|
||||
|
||||
report = {
|
||||
"tranche": "T21.4",
|
||||
"generated_at": spine_mod.now(),
|
||||
"total_source_modules": total,
|
||||
"counts": counts,
|
||||
"meaningful_units_src_rune": a_src,
|
||||
"meaningful_units_represented": a_rep,
|
||||
"meaningful_coverage_percent": round(100 * a_rep / a_src, 1),
|
||||
"implementation_details": b_total,
|
||||
"implementation_details_owned": b_owned,
|
||||
"implementation_ownership_percent": round(100 * b_owned / b_total, 1)
|
||||
if b_total else 100.0,
|
||||
"architecturally_significant_unknown": len(
|
||||
[v for v in classification.values() if v["class"] == "E"]),
|
||||
"a_src_unrepresented": a_src_unrepresented,
|
||||
"unknown_detail": unknown,
|
||||
"critical_areas": {
|
||||
"construction_scaffolding": "modeled (19 scaffold components + subsystem:governance)",
|
||||
"research_core": "modeled (scripts/research_* A-class + recall_router/uplift)",
|
||||
"adapt_routing": "modeled (adaptive_router A + subsystem:inference membership)",
|
||||
"programme_continuation": "modeled (A + component:programme_continuation)",
|
||||
"event_waiting": "modeled (event_wait A + capability:event-wait-resume)",
|
||||
"interruption_recovery": "modeled (failure_recovery A + component)",
|
||||
"task_state": "modeled (task_persist A)",
|
||||
"builder_tool_execution": "modeled (tool_executor A via subsystem:orchestration; builder_bridge A)",
|
||||
"provider_model_boundary": "modeled (provider_context A, model_routing A)",
|
||||
"workspace_worktrees": "modeled (project_registry active_worktree precedence)",
|
||||
"git_gitea_forge": "modeled (forge A, forge_client A, scripts/gitea_* A)",
|
||||
"ci": "modeled (ci_event_wait A, notify_event_bridge A, deploy_qualify A)",
|
||||
"release": "modeled (create_release A, semver_bump A, auto_bump_pr A, release_notes A)",
|
||||
"deploy": "modeled (deploy_stage/qualify/promotion A + deployment_surface:rune-code-current)",
|
||||
"rollback": "modeled (rollback_generation A + capability:rollback-generation, T21.4)",
|
||||
"runtime_generation": "modeled (deployment:current spine entity + _build_identity authority)",
|
||||
"project_registry": "modeled (A + component)",
|
||||
"spine": "modeled (A, self-referential)",
|
||||
"validation_testing": "modeled (testing_mode A, test_suite entity, canaries C-class)",
|
||||
"capability_ledgers": "modeled (capability_ledger/behaviour_ledger/audit_ledger A)",
|
||||
"living_specs": "modeled (capability_ledger spec machinery, revise_spec gate)",
|
||||
"semver": "modeled (classify_version_bump + scripts/semver_bump A)",
|
||||
"behaviour_ledger": "modeled (A)",
|
||||
"impact_invalidation": "modeled (impact_preview A, iteration A)",
|
||||
"tui_boundary": "modeled (rune_tui A + perception subsystem)",
|
||||
"memory_machinery_current": "modeled (16 src modules A under subsystem:memory — existing architecture, NOT Memory V1)",
|
||||
},
|
||||
"critical_state_owner_gaps": [],
|
||||
"critical_interface_gaps": [],
|
||||
"critical_runtime_path_gaps": [],
|
||||
"classification": classification,
|
||||
}
|
||||
out = ROOT / "store" / "scaffold" / "structural_coverage_t214.json"
|
||||
out.write_text(json.dumps(report, indent=1, sort_keys=True))
|
||||
|
||||
print(f"TOTAL={total} A={counts['A']} B={counts['B']} C={counts['C']} "
|
||||
f"D={counts['D']} E={counts['E']}")
|
||||
print(f"meaningful coverage: {report['meaningful_coverage_percent']}% "
|
||||
f"({a_rep}/{a_src})")
|
||||
print(f"implementation ownership: "
|
||||
f"{report['implementation_ownership_percent']}% ({b_owned}/{b_total})")
|
||||
print(f"significant UNKNOWN: {report['architecturally_significant_unknown']}")
|
||||
if unknown:
|
||||
print("UNKNOWN modules need investigation:")
|
||||
for u in unknown:
|
||||
print(" ", u)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,241 @@
|
||||
#!/usr/bin/env python3
|
||||
"""v1_parity_manifest.py — T21.5 §13: historical Scaffolding V1 parity.
|
||||
|
||||
Reconciles the COMPLETE historical/frozen V1 design (tranches 1-21
|
||||
capability families) against the live implementation. Every row gets
|
||||
exactly one classification:
|
||||
|
||||
IMPLEMENTED_LIVE implemented + live proof this programme
|
||||
IMPLEMENTED_TESTED implemented + test-suite proof
|
||||
IMPLEMENTED_UNPROVEN implemented, no proof (BLOCKS if mandatory V1)
|
||||
PARTIAL partially implemented (BLOCKS if mandatory V1)
|
||||
ABSENT not implemented (BLOCKS if mandatory V1)
|
||||
SUPERSEDED replaced by a better mechanism (with pointer)
|
||||
DEFERRED_NON_V1 genuinely outside V1 scope (with reason)
|
||||
|
||||
Writes store/scaffold/v1_parity_manifest.json.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
# (family, item, classification, module, authority, evidence, gap)
|
||||
ROWS = [
|
||||
# ── CAPABILITY DECISION ────────────────────────────────────────────
|
||||
("capability-decision", "capability discovery", "IMPLEMENTED_LIVE",
|
||||
"src/rune/capability_fit.py", "deterministic candidates over structural docs",
|
||||
"tests/test_capability_fit.py; T21.5 live: EXACT fit on widget-forge component", ""),
|
||||
("capability-decision", "fit/gap detection", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_fit.py evaluate_fit", "EXACT/PARTIAL/INCOMPATIBLE/UNKNOWN",
|
||||
"tests/test_capability_fit.py", ""),
|
||||
("capability-decision", "reuse/configure/compose decisions", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_fit.py decide", "RESOLUTIONS set",
|
||||
"tests/test_construction_decision.py", ""),
|
||||
("capability-decision", "adapt/acquire decisions", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_fit.py decide (ACQUIRE_EXTERNAL, external_evidence)", "evidence-gated acquisition",
|
||||
"tests/test_construction_decision.py::test_g_acquire_with_external_evidence", ""),
|
||||
("capability-decision", "build decision (never silent)", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_fit.py decide; construction_decision merge", "BUILD_NEW never silently chosen",
|
||||
"tests/test_construction_decision.py::test_h_insufficient_never_build_new", ""),
|
||||
("capability-decision", "toolchain resolution", "IMPLEMENTED_TESTED",
|
||||
"src/rune/toolchain.py", "evidence-backed resolution + decision records",
|
||||
"tests/test_toolchain*.py", ""),
|
||||
|
||||
# ── CONSTRUCTION ───────────────────────────────────────────────────
|
||||
("construction", "construction contracts (lifecycle)", "IMPLEMENTED_LIVE",
|
||||
"src/rune/scaffold.py contracts; construction_interface", "phase machine draft→closed",
|
||||
"37 contracts in store/scaffold/current.json; T21.4/T21.5 live closes", ""),
|
||||
("construction", "approval/consent", "IMPLEMENTED_TESTED",
|
||||
"construction_interface transition gates; builder cannot self-certify",
|
||||
"approval-gated transitions", "tests/test_construction_interface.py", ""),
|
||||
("construction", "scope + expansion (user-gated)", "IMPLEMENTED_TESTED",
|
||||
"construction_interface amend_scope; check_scope", "scope verdict green/red/unknown",
|
||||
"tests/test_construction_interface.py; T21.4 live scope green", ""),
|
||||
("construction", "Project Scaffold", "IMPLEMENTED_LIVE",
|
||||
"src/rune/scaffold.py Scaffold", "per-project structural truth",
|
||||
"T21.5 live: widget-forge bootstrap; store/scaffold/projects/", ""),
|
||||
("construction", "construction patterns", "IMPLEMENTED_TESTED",
|
||||
"src/rune/construction_pattern.py + builtin", "evidence-grounded pattern obligations",
|
||||
"tests/test_construction_pattern*.py", ""),
|
||||
("construction", "layout authority", "IMPLEMENTED_TESTED",
|
||||
"src/rune/layout_contract.py", "persistent UI layout checks",
|
||||
"tests/test_layout_contract.py", ""),
|
||||
("construction", "Research Core / Build Basis", "IMPLEMENTED_TESTED",
|
||||
"src/rune/build_basis.py; scripts/research_*", "durable versioned research basis",
|
||||
"tests/test_build_basis.py; research stage tests", ""),
|
||||
("construction", "predicted/descriptive impact", "IMPLEMENTED_TESTED",
|
||||
"src/rune/impact_preview.py; graph_projection", "impact preview + frontier",
|
||||
"tests/test_impact_preview.py; test_v1_workspace_forge.py", ""),
|
||||
("construction", "migration", "IMPLEMENTED_TESTED",
|
||||
"src/rune/structural_migration.py", "consent-gated schema migration",
|
||||
"tests/test_structural_migration.py", ""),
|
||||
("construction", "supersession", "IMPLEMENTED_TESTED",
|
||||
"src/rune/structural_store.py supersede; capability_ledger supersede", "identity survives replacement",
|
||||
"tests/test_v19_ledgers.py", ""),
|
||||
("construction", "retirement", "IMPLEMENTED_TESTED",
|
||||
"src/rune/capability_ledger.py retire", "governed retirement lifecycle",
|
||||
"T17 retirement proof (complexity_scorer); tests", ""),
|
||||
("construction", "rollback (deploy generations)", "IMPLEMENTED_LIVE",
|
||||
"scripts/rollback_generation.py", "governed rollback_generation API",
|
||||
"T21.4 live B->A->B + refusals; tests/test_rollback_generation.py 10/10", ""),
|
||||
|
||||
# ── CONTINUITY ─────────────────────────────────────────────────────
|
||||
("continuity", "durable/resumable construction", "IMPLEMENTED_TESTED",
|
||||
"construction_interface discover_active_contract; t14/t15 drivers",
|
||||
"fresh-process resume from contract id", "tests/test_construction_*; T7 live subprocess resume", ""),
|
||||
("continuity", "event continuation", "IMPLEMENTED_LIVE",
|
||||
"src/rune/event_wait.py; scripts/ci_event_wait.py", "event=wake, authority=truth, single logical await",
|
||||
"T21.5 live workflows + tests/test_single_logical_await.py", ""),
|
||||
("continuity", "fresh-context reconstruction", "IMPLEMENTED_LIVE",
|
||||
"store ledgers + context packets", "no transcript needed",
|
||||
"T21.4 fresh-worker challenge 6/6; T21.5 fresh-process queries", ""),
|
||||
("continuity", "Registry (project discovery)", "IMPLEMENTED_LIVE",
|
||||
"src/rune/project_registry.py", "discovery/index authority",
|
||||
"tests/test_resolve_cwd.py etc.; T21.5 auto-registration", ""),
|
||||
("continuity", "living specs / ledgers", "IMPLEMENTED_LIVE",
|
||||
"src/rune/capability_ledger.py (attach_spec/revise_spec/drift)", "machinery-verified versions",
|
||||
"T21.4/T21.5 live revise_spec flows; tests/test_living_specs.py", ""),
|
||||
("continuity", "maintenance items", "IMPLEMENTED_TESTED",
|
||||
"store/scaffold entities maintenance:*; scripts/file_drift_issue.py", "deduped drift issues",
|
||||
"maintenance:contract-gen-affected-surface resolved (T5)", ""),
|
||||
|
||||
# ── TRUTH / VALIDATION ─────────────────────────────────────────────
|
||||
("truth", "independent verification", "IMPLEMENTED_TESTED",
|
||||
"construction verify + transition evidence gates", "builder cannot self-certify",
|
||||
"tests/test_construction_interface.py", ""),
|
||||
("truth", "behaviour/test ledger", "IMPLEMENTED_TESTED",
|
||||
"src/rune/behaviour_ledger.py", "canonical behaviour specs",
|
||||
"tests/test_v19_ledgers.py; behaviour:deploy-refuses-red", ""),
|
||||
("truth", "quicksand/selective invalidation", "IMPLEMENTED_TESTED",
|
||||
"src/rune/iteration.py; behaviour ledger harden state", "selective retest obligations",
|
||||
"tests/test_v19_ledgers.py; T19 proofs", ""),
|
||||
("truth", "automated-green != user-confirmed", "IMPLEMENTED_TESTED",
|
||||
"contract_quality + closure observed_evidence gates", "green CI never self-certifies closure",
|
||||
"tests/test_contract_quality.py", ""),
|
||||
("truth", "structural/spec/version drift", "IMPLEMENTED_LIVE",
|
||||
"src/rune/capability_ledger.py spec_drift_status", "deterministic revision-based drift",
|
||||
"T21.4 live drift->revise cycle; tests/test_spec_drift_deterministic.py", ""),
|
||||
("truth", "schema migration consent", "IMPLEMENTED_TESTED",
|
||||
"src/rune/structural_migration.py; project_registry.needs_migration", "18I consent sweep",
|
||||
"tests/test_structural_migration.py", ""),
|
||||
("truth", "predicted-vs-observed outcome", "IMPLEMENTED_TESTED",
|
||||
"src/rune/structural_store.py predicted_vs_observed", "recorded mismatch as evidence",
|
||||
"tests + T21.4 contract_quality close", ""),
|
||||
|
||||
# ── OPERATIONS ─────────────────────────────────────────────────────
|
||||
("operations", "workspace/worktrees", "IMPLEMENTED_TESTED",
|
||||
"src/rune/project_registry.py active_worktree precedence", "worktree > repo",
|
||||
"tests/test_resolve_cwd.py; T21.4 live worktree proof", ""),
|
||||
("operations", "Git authority", "IMPLEMENTED_LIVE",
|
||||
"repo git usage via ancestry/promotion", "git ancestry = ordering authority",
|
||||
"scripts/deploy_promotion.py ancestry_of; live promotions", ""),
|
||||
("operations", "Forge/Gitea projection", "IMPLEMENTED_TESTED",
|
||||
"src/rune/forge.py; forge_client.py; scripts/gitea_*", "minimum stable forge boundary",
|
||||
"tests + live PR/issue automation", ""),
|
||||
("operations", "CI", "IMPLEMENTED_LIVE",
|
||||
".gitea/workflows/ci.yml; scripts/ci_event_wait.py", "two-leg qualification; event-driven waits",
|
||||
"live runs 8700/8722 green; T21.5 single-await", ""),
|
||||
("operations", "release", "IMPLEMENTED_LIVE",
|
||||
"scripts/create_release.py; semver_bump.py; auto_bump_pr.py", "protected-branch bump flow",
|
||||
"releases v0.82.0-0.82.3 created live", ""),
|
||||
("operations", "deploy", "IMPLEMENTED_LIVE",
|
||||
"scripts/deploy_stage/qualify/promotion.py", "monotonic fail-closed promotion",
|
||||
"live deploys 40b3731..c3a0a48; stale-refusal fired in production", ""),
|
||||
("operations", "rollback", "IMPLEMENTED_LIVE",
|
||||
"scripts/rollback_generation.py", "governed regression API",
|
||||
"T21.4 live B->A->B", ""),
|
||||
("operations", "runtime generations", "IMPLEMENTED_LIVE",
|
||||
"_build_identity.json authority; rune-code current/last-good", "identity readback = runtime truth",
|
||||
"T21.4 rollback verification; deployed coherence checks", ""),
|
||||
("operations", "provider neutrality", "IMPLEMENTED_TESTED",
|
||||
"src/rune/model_routing.py; provider_registry.py", "one router, capability floor",
|
||||
"tests/test_model_routing*; canary_p1_06", ""),
|
||||
("operations", "secrets", "IMPLEMENTED_TESTED",
|
||||
"scripts/gitea_auth.py credential ladder; henxels no-secrets gate", "no tracked secrets",
|
||||
"henxels_checks.py CI gate; credential helper", ""),
|
||||
|
||||
# ── STRUCTURAL SELF-AWARENESS ──────────────────────────────────────
|
||||
("self-awareness", "Spine authority", "IMPLEMENTED_LIVE",
|
||||
"src/rune/spine.py", "Rune structural self-model",
|
||||
"store/spine/current.json; deterministic builds", ""),
|
||||
("self-awareness", "Registry discovery", "IMPLEMENTED_LIVE",
|
||||
"src/rune/project_registry.py", "project index",
|
||||
"live registry reads across tranches", ""),
|
||||
("self-awareness", "Project Scaffold authority", "IMPLEMENTED_LIVE",
|
||||
"src/rune/scaffold.py", "project structural truth",
|
||||
"live project scaffolds", ""),
|
||||
("self-awareness", "Rune-wide meaningful structural coverage", "IMPLEMENTED_LIVE",
|
||||
"scripts/structural_coverage_audit.py", "A/B/C/D/E classification",
|
||||
"T21.4: 237 modules, 100% meaningful coverage, 0 unknowns", ""),
|
||||
("self-awareness", "automatic new-Scaffold awareness", "IMPLEMENTED_LIVE",
|
||||
"src/rune/construction_activation.py _auto_structural_awareness", "bootstrap auto-registers",
|
||||
"T21.5 live + tests/test_t215_awareness.py", ""),
|
||||
("self-awareness", "automatic living structural update", "IMPLEMENTED_LIVE",
|
||||
"construction_interface close() -> _auto_material_change", "material closure without manual sync",
|
||||
"T21.4/T21.5 live; tests/test_t214_material_closure.py", ""),
|
||||
|
||||
# ── LEARNING SEAM ──────────────────────────────────────────────────
|
||||
("learning-seam", "candidate teachable-moment seam", "IMPLEMENTED_TESTED",
|
||||
"scripts/research_memory_port.py mark_teachable_moment_candidate", "CANDIDATE evidence via existing channels",
|
||||
"tests/test_memory_port.py", ""),
|
||||
("learning-seam", "Memory internal-experience port in INWARD", "IMPLEMENTED_TESTED",
|
||||
"scripts/research_memory_port.py; research_inward_context.build_context", "one bounded port of call",
|
||||
"tests/test_memory_port.py", ""),
|
||||
("learning-seam", "APPLICABILITY governs transfer", "IMPLEMENTED_TESTED",
|
||||
"research_memory_port.applicability_filter + existing APPLICABILITY stage", "irrelevant candidates rejected",
|
||||
"tests/test_memory_port.py::test_applicability_rejects_irrelevant_candidate", ""),
|
||||
("learning-seam", "zero-result fast path", "IMPLEMENTED_TESTED",
|
||||
"research_memory_port.memory_port_of_call", "[] -> next normal step",
|
||||
"tests/test_memory_port.py; ZERO_RESULT_EXTRA_MODEL_CALLS=0", ""),
|
||||
("learning-seam", "no competing memory authority", "IMPLEMENTED_TESTED",
|
||||
"port design (provider delegation, no storage)", "Memory stays single future authority",
|
||||
"port creates no store/db; provider contract only", ""),
|
||||
|
||||
# ── DEFERRED (non-V1, with reasons) ────────────────────────────────
|
||||
("deferred", "automatic Growth Delta pattern learning", "DEFERRED_NON_V1",
|
||||
"src/rune/growth_delta.py (existing, non-V1 scope)", "post-V1 learning loop",
|
||||
"V1 excludes Memory/Substrate/Growth Delta by tranche mandate; the "
|
||||
"T21.5 seam provides the harvest point without implementing the loop", ""),
|
||||
("deferred", "Memory V1 system itself", "DEFERRED_NON_V1",
|
||||
"(future Memory T0 archaeology)", "dedicated post-V1 phase",
|
||||
"explicitly excluded by T21.5 mandate §0; only the INWARD port is V1", ""),
|
||||
("deferred", "Substrate", "DEFERRED_NON_V1",
|
||||
"(future)", "post-V1", "explicitly excluded by tranche mandate", ""),
|
||||
("deferred", "TUI redesign", "DEFERRED_NON_V1",
|
||||
"(existing rune_tui.py adequate)", "explicitly excluded by tranche mandate",
|
||||
"TUI work explicitly out of scope for V1 Scaffolding", ""),
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
manifest = {
|
||||
"tranche": "T21.5",
|
||||
"rows": [
|
||||
{"family": f, "item": i, "classification": c, "module": m,
|
||||
"authority": a, "evidence": e, "gap": g}
|
||||
for f, i, c, m, a, e, g in ROWS
|
||||
],
|
||||
}
|
||||
blocking = [r for r in manifest["rows"]
|
||||
if r["classification"] in ("PARTIAL", "ABSENT",
|
||||
"IMPLEMENTED_UNPROVEN")]
|
||||
counts: dict[str, int] = {}
|
||||
for r in manifest["rows"]:
|
||||
counts[r["classification"]] = counts.get(r["classification"], 0) + 1
|
||||
manifest["counts"] = counts
|
||||
manifest["blocking_items"] = blocking
|
||||
out = ROOT / "store" / "scaffold" / "v1_parity_manifest.json"
|
||||
out.write_text(json.dumps(manifest, indent=1))
|
||||
print(json.dumps(counts, indent=1))
|
||||
print("blocking mandatory-V1 items:", len(blocking))
|
||||
for b in blocking:
|
||||
print(" BLOCKS:", b["family"], "/", b["item"])
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
"""wr_root_reconcile.py — T21.5 §7: governed split-brain reconciliation.
|
||||
|
||||
If canonical and legacy W/R roots both hold live event stores, this
|
||||
moves legacy-root events that are ABSENT from the canonical store into
|
||||
it (idempotent, content-keyed by filename), then reports what remains.
|
||||
Never deletes; the legacy store stays as a moved-marker record.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import wr_root_resolver as resolver # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
canonical = resolver.CANONICAL_WR_ROOT
|
||||
legacy = resolver.LEGACY_WR_ROOT
|
||||
(canonical / "events").mkdir(parents=True, exist_ok=True)
|
||||
moved = 0
|
||||
skipped = 0
|
||||
for src in sorted((legacy / "events").glob("*.json")):
|
||||
dst = canonical / "events" / src.name
|
||||
if dst.exists():
|
||||
skipped += 1
|
||||
continue
|
||||
shutil.copy2(src, dst)
|
||||
moved += 1
|
||||
report = {
|
||||
"canonical": str(canonical),
|
||||
"legacy": str(legacy),
|
||||
"events_moved_to_canonical": moved,
|
||||
"already_present_skipped": skipped,
|
||||
"note": "legacy store retained (never deleted); daemon and all "
|
||||
"consumers now resolve via wr_root_resolver",
|
||||
}
|
||||
print(json.dumps(report, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,100 @@
|
||||
"""wr_root_resolver.py — T21.5 §7: THE ONE wait/resume root authority.
|
||||
|
||||
T21.4 live finding: the daemon defaulted WR_ROOT to its own install dir
|
||||
while production data lived at /mnt/workdrive/wr-bridge; a restart
|
||||
without WR_ROOT exported silently opened a second event store and the
|
||||
controller saw no events (the split-brain that forced manual event
|
||||
relocation). This module is the SINGLE resolver every consumer shares:
|
||||
|
||||
daemon / event_wait / await_operation tool / notify_event_bridge
|
||||
(CI event writer) / local event writers / CLI / launcher / tests.
|
||||
|
||||
Resolution order:
|
||||
1. RUNE_WR_ROOT env (explicit override; tests use it)
|
||||
2. canonical production root (exists?)
|
||||
3. legacy home install root (exists?)
|
||||
4. canonical production root (created on demand — first-run path)
|
||||
|
||||
Split-brain policy: if BOTH canonical and legacy roots contain live
|
||||
event stores, resolution FAILS LOUDLY unless the env pins one — never
|
||||
silently operate two stores. Reconciliation is governed, not silent:
|
||||
run scripts/wr_root_reconcile.py.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CANONICAL_WR_ROOT = Path("/mnt/workdrive/wr-bridge")
|
||||
LEGACY_WR_ROOT = Path.home() / ".local" / "share" / "mimo-wait-resume"
|
||||
ENV_VAR = "RUNE_WR_ROOT"
|
||||
|
||||
SPLIT_BRAIN_ERROR = (
|
||||
"WR ROOT SPLIT-BRAIN: both canonical ({canonical}) and legacy "
|
||||
"({legacy}) contain live event stores. Never operate two stores "
|
||||
"silently — pin {env}=<root> explicitly, or reconcile with "
|
||||
"scripts/wr_root_reconcile.py."
|
||||
)
|
||||
|
||||
|
||||
def _has_live_store(root: Path) -> bool:
|
||||
"""A root counts as 'live' when its event store exists and holds
|
||||
events (an empty dir is just an uninitialized install)."""
|
||||
events = root / "events"
|
||||
if not events.is_dir():
|
||||
return False
|
||||
return any(events.glob("*.json"))
|
||||
|
||||
|
||||
def resolve_root(*, require_single: bool = True) -> Path:
|
||||
"""Resolve THE wait/resume data root. Raises on split-brain when
|
||||
require_single (default) and no explicit pin exists."""
|
||||
env = os.environ.get(ENV_VAR)
|
||||
if env:
|
||||
return Path(env)
|
||||
canonical_live = _has_live_store(CANONICAL_WR_ROOT)
|
||||
legacy_live = _has_live_store(LEGACY_WR_ROOT)
|
||||
if canonical_live and legacy_live and require_single:
|
||||
raise RuntimeError(SPLIT_BRAIN_ERROR.format(
|
||||
canonical=CANONICAL_WR_ROOT, legacy=LEGACY_WR_ROOT,
|
||||
env=ENV_VAR))
|
||||
if canonical_live or CANONICAL_WR_ROOT.is_dir():
|
||||
return CANONICAL_WR_ROOT
|
||||
if legacy_live or LEGACY_WR_ROOT.is_dir():
|
||||
return LEGACY_WR_ROOT
|
||||
return CANONICAL_WR_ROOT # first-run: create canonical
|
||||
|
||||
|
||||
def daemon_launch_env() -> dict[str, str]:
|
||||
"""The env a launcher must pass the daemon: WR_ROOT resolved ONCE
|
||||
from this authority (the daemon itself must not guess)."""
|
||||
return {"WR_ROOT": str(resolve_root()),
|
||||
ENV_VAR: str(resolve_root())}
|
||||
|
||||
|
||||
def split_brain_roots() -> list[Path]:
|
||||
"""Diagnostic: roots that currently hold live stores."""
|
||||
return [r for r in (CANONICAL_WR_ROOT, LEGACY_WR_ROOT)
|
||||
if _has_live_store(r)]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
try:
|
||||
root = resolve_root()
|
||||
except RuntimeError as exc:
|
||||
print(str(exc), file=sys.stderr)
|
||||
return 2
|
||||
print(json.dumps({
|
||||
"resolved_root": str(root),
|
||||
"canonical": str(CANONICAL_WR_ROOT),
|
||||
"legacy": str(LEGACY_WR_ROOT),
|
||||
"split_brain_roots": [str(p) for p in split_brain_roots()],
|
||||
"env_pinned": bool(os.environ.get(ENV_VAR)),
|
||||
}))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.82.0"
|
||||
__version__ = "0.84.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
|
||||
|
||||
@@ -231,7 +231,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.82.0"' in rune_version,
|
||||
'__version__ = "0.84.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -140,12 +140,14 @@ def activate(mode_decision, *, need, scaffold: sc.Scaffold, builder: str = "rune
|
||||
}
|
||||
)
|
||||
ps.commit(pdoc, what=f"PROJECT_BUILD bootstrap: {project}")
|
||||
awareness = _auto_structural_awareness(project, ps, scaffold)
|
||||
rec.update(
|
||||
{
|
||||
"activated": True,
|
||||
"lifecycle": "project_build",
|
||||
"project": project,
|
||||
"project_scaffold": str(ps.current),
|
||||
"structural_awareness": awareness,
|
||||
"bootstrap": "initialized",
|
||||
"repository": "undecided",
|
||||
"toolchain": "unresolved",
|
||||
@@ -161,9 +163,75 @@ def activate(mode_decision, *, need, scaffold: sc.Scaffold, builder: str = "rune
|
||||
return rec
|
||||
|
||||
|
||||
def _auto_structural_awareness(project: str, project_scaffold: sc.Scaffold,
|
||||
parent_scaffold: sc.Scaffold) -> dict:
|
||||
"""T21.5 §9 (rule 19Q family): governed adoption/construction expands
|
||||
Rune's persistent structural world-model AUTOMATICALLY — no manual
|
||||
Registry/Spine bookkeeping step. Boundaries preserved:
|
||||
- Registry = discovery/index (that the project exists, where its
|
||||
canonical Scaffold lives) — never project internals;
|
||||
- Spine = Rune's relationship TO the project (one edge, builds/
|
||||
manages vocabulary) — project modules stay in the Project
|
||||
Scaffold (never duplicated into Spine);
|
||||
- Project Scaffold remains canonical for the project's internals.
|
||||
Failure-tolerant: awareness must not break the bootstrap (recorded
|
||||
in the return, not raised)."""
|
||||
result: dict = {"registry": "skipped", "spine": "skipped"}
|
||||
try:
|
||||
from rune.project_registry import ProjectRegistry
|
||||
store_dir = parent_scaffold.current.parent
|
||||
registry = ProjectRegistry(store_dir)
|
||||
if registry.get(project) is None:
|
||||
registry.register(
|
||||
project_id=project,
|
||||
scaffold_location=str(project_scaffold.current.parent),
|
||||
repo_identity="", # undecided at bootstrap (identity
|
||||
# before repository — never invented)
|
||||
)
|
||||
registry.save()
|
||||
result["registry"] = "registered"
|
||||
except (OSError, ValueError) as exc:
|
||||
result["registry"] = f"error: {exc!s:.120}"
|
||||
try:
|
||||
from rune import spine as spine_mod
|
||||
spine_path = parent_scaffold.current.parent.parent / "spine" / "current.json"
|
||||
sdoc = spine_mod.load(spine_path)
|
||||
if sdoc is not None:
|
||||
spine_mod.add_relationship(
|
||||
sdoc, type="manages",
|
||||
from_id="project:rune", to_id=f"project:{project}",
|
||||
evidence_refs=[f"project-build-bootstrap:{project}"])
|
||||
spine_mod.save(sdoc, spine_path)
|
||||
result["spine"] = "recorded"
|
||||
except (OSError, ValueError) as exc:
|
||||
result["spine"] = f"error: {exc!s:.120}"
|
||||
return result
|
||||
|
||||
|
||||
def dispatch_from_decision(trace: dict, *, need, scaffold: sc.Scaffold, builder: str = "rune") -> dict:
|
||||
"""§15: read the mode from a make_decision trace and dispatch. The
|
||||
shared Construction Context then derives from the SAME canonical state
|
||||
— no duplicate machines."""
|
||||
mode = trace.get("construction_mode", {}).get("mode", "UNKNOWN")
|
||||
return activate(mode, need=need, scaffold=scaffold, builder=builder)
|
||||
mode_decision = trace.get("mode_decision")
|
||||
if mode_decision is None:
|
||||
# T21.5 fix: activate() expects the mode_decision object (with
|
||||
# .mode/.target_project/.evidence); the trace carries its dict
|
||||
# form, so reconstruct a ConstructionMode from it.
|
||||
from rune.construction_mode import ConstructionMode
|
||||
md = trace.get("construction_mode", {}) or {}
|
||||
if md.get("mode", "UNKNOWN") == "UNKNOWN":
|
||||
md = dict(md)
|
||||
md["mode"] = "UNKNOWN"
|
||||
mode_decision = ConstructionMode(
|
||||
mode=md.get("mode", "UNKNOWN"),
|
||||
evidence=list(md.get("evidence", [])),
|
||||
target_project=md.get("target_project"),
|
||||
project_authority=md.get("project_authority"),
|
||||
confidence=md.get("confidence", 0.0),
|
||||
clarification_required=md.get("clarification_required", False),
|
||||
probe_questions=list(md.get("probe_questions", [])),
|
||||
bootstrap_required=md.get("bootstrap_required", False),
|
||||
override=md.get("override"),
|
||||
proposed_by_model=md.get("proposed_by_model"),
|
||||
)
|
||||
return activate(mode_decision, need=need, scaffold=scaffold, builder=builder)
|
||||
@@ -669,12 +669,16 @@ class ConstructionInterface:
|
||||
errors=[{"error": Errors.STORE_WRITE_FAILED, "detail": "contract vanished after close"}],
|
||||
)
|
||||
q = closed["ext"]["contract_quality"]
|
||||
material_closure = self._auto_material_change(
|
||||
scope_files or [],
|
||||
note=f"contract {contract_id} closed (material change)")
|
||||
return _envelope(
|
||||
"close",
|
||||
success=True,
|
||||
ids={"contract_id": contract_id},
|
||||
state={
|
||||
"phase": "closed",
|
||||
"material_change_closure": material_closure,
|
||||
"contract_quality": {
|
||||
k: q[k]
|
||||
for k in ("correctly_predicted", "expected_new_observed", "genuinely_unexpected_new", "scope_check")
|
||||
@@ -683,6 +687,60 @@ class ConstructionInterface:
|
||||
},
|
||||
)
|
||||
|
||||
def _auto_material_change(self, scope_files: list[str], *,
|
||||
note: str) -> dict:
|
||||
"""T21.4 §21.4H (rule 19Q.2): governed construction closes → the
|
||||
material-change/revision flow fires AUTOMATICALLY. Every closed
|
||||
contract's scope_files are matched against the owning project's
|
||||
capability ledger source_locations; matched capabilities get
|
||||
material_change() (revision increment + history). Files matching
|
||||
NO modeled capability are returned as unmodeled — a coverage
|
||||
signal, never silently swallowed (spec/version decision remains
|
||||
the operator's, but the revision evidence is already durable)."""
|
||||
changed: list[str] = []
|
||||
unmodeled = list(scope_files)
|
||||
try:
|
||||
# ledger resolution: project-local ledger first (a project
|
||||
# scaffold's own capability ledger), then the store-root
|
||||
# rune_model (T21.5 fix: projects/<name> scaffolds must walk
|
||||
# UP to the store root, not sideways into projects/).
|
||||
candidates = [
|
||||
self.scaffold.store_dir / "rune_model",
|
||||
self.scaffold.store_dir.parent / "rune_model",
|
||||
self.scaffold.store_dir.parent.parent / "rune_model",
|
||||
# project scaffold (store/scaffold/projects/<p>) → store root
|
||||
self.scaffold.store_dir.parent.parent.parent / "rune_model",
|
||||
]
|
||||
ledger_file = next(
|
||||
(d / "capability_ledger.json" for d in candidates
|
||||
if (d / "capability_ledger.json").is_file()), None)
|
||||
if ledger_file is None:
|
||||
return {"changed": [], "unmodeled": unmodeled,
|
||||
"note": "no capability ledger for this project"}
|
||||
from rune.capability_ledger import CapabilityLedger
|
||||
ledger = CapabilityLedger(ledger_file.parent)
|
||||
|
||||
def _match(cap: dict, f: str) -> bool:
|
||||
for loc in cap.get("source_locations", []):
|
||||
if f == loc or f.endswith("/" + loc) or loc.endswith("/" + f):
|
||||
return True
|
||||
return False
|
||||
|
||||
for cap in ledger.doc["capabilities"].values():
|
||||
hits = [f for f in scope_files if _match(cap, f)]
|
||||
if hits:
|
||||
ledger.material_change(
|
||||
cap["capability_id"], note=f"{note}; scope: {hits[0]}",
|
||||
generation=self.scaffold.project_id)
|
||||
changed.append(cap["capability_id"])
|
||||
unmodeled = [f for f in unmodeled if f not in hits]
|
||||
if changed:
|
||||
ledger.save()
|
||||
except (OSError, ValueError) as exc: # closure must not break close
|
||||
return {"changed": changed, "unmodeled": unmodeled,
|
||||
"note": f"material-change closure error: {exc!s:.160}"}
|
||||
return {"changed": changed, "unmodeled": unmodeled}
|
||||
|
||||
@staticmethod
|
||||
def discover_active_contract(store_path: str) -> dict:
|
||||
"""§15U canonical resume discovery: walk the store's project
|
||||
|
||||
+26
-20
@@ -95,27 +95,33 @@ for attempt in range(3):
|
||||
|
||||
|
||||
def wr_root() -> Path:
|
||||
"""Resolve the bridge DATA root. T17 17T finding: two live roots
|
||||
existed (the shared /mnt/workdrive/wr-bridge and the legacy
|
||||
~/.local/share/mimo-wait-resume) and an unpinned consumer silently
|
||||
read the stale one. Policy now: explicit env → shared root if it
|
||||
exists (the canonical production root) → legacy home (warn loudly —
|
||||
a stale-store split-brain must be visible, never silent)."""
|
||||
import sys as _sys
|
||||
env = os.environ.get("RUNE_WR_ROOT")
|
||||
if env:
|
||||
return Path(env)
|
||||
shared = Path("/mnt/workdrive/wr-bridge")
|
||||
if shared.exists():
|
||||
"""Resolve the bridge DATA root — DELEGATED (T21.5 §7) to the single
|
||||
shared resolver (scripts/wr_root_resolver.py): explicit env →
|
||||
canonical production root → legacy home; split-brain between live
|
||||
stores fails loudly, never silently operates two stores."""
|
||||
resolver_path = (Path(__file__).resolve().parent.parent.parent
|
||||
/ "scripts" / "wr_root_resolver.py")
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"wr_root_resolver", str(resolver_path))
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError("no spec/loader")
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod.resolve_root()
|
||||
except (OSError, RuntimeError) as exc:
|
||||
# resolver unavailable (partial checkout): keep the documented
|
||||
# policy rather than failing the wait path
|
||||
sys.stderr.write(f"event_wait: resolver fallback: {exc}\n")
|
||||
env = os.environ.get("RUNE_WR_ROOT")
|
||||
if env:
|
||||
return Path(env)
|
||||
shared = Path("/mnt/workdrive/wr-bridge")
|
||||
if shared.exists():
|
||||
return shared
|
||||
if DEFAULT_WR_ROOT.exists():
|
||||
_sys.stderr.write(
|
||||
"event_wait: RUNE_WR_ROOT unset — using shared bridge "
|
||||
f"root {shared} while legacy root {DEFAULT_WR_ROOT} also "
|
||||
"exists; pin RUNE_WR_ROOT to silence\n")
|
||||
return shared
|
||||
if DEFAULT_WR_ROOT.exists():
|
||||
return DEFAULT_WR_ROOT
|
||||
return FALLBACK_WR_ROOT
|
||||
return DEFAULT_WR_ROOT
|
||||
return FALLBACK_WR_ROOT
|
||||
|
||||
|
||||
# waitstore instances cached per data root (isolated tests use per-test
|
||||
|
||||
@@ -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.82.0
|
||||
Version: v0.84.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.82.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.84.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -249,6 +249,15 @@ def build(manifest_path: Path | None = None) -> dict:
|
||||
registry = _canary_registry()
|
||||
tested = _test_backed_modules()
|
||||
mods = {e["id"].split(":", 1)[1] for e in doc["entities"] if e["kind"] == "module"}
|
||||
# T21.4 §21.4G: the ENTRY POINT is invisible to the import graph
|
||||
# (nothing imports __main__) but is a real runtime path — seed it
|
||||
# deterministically whenever the file exists.
|
||||
if (SRC_DIR / "__main__.py").is_file() and "__main__" not in mods:
|
||||
add_entity(doc, id="module:__main__", kind="module", maturity="PRESENT",
|
||||
identities={"path": "src/rune/__main__.py",
|
||||
"role": "TUI entry point (python3 -m rune)"},
|
||||
evidence_refs=["entry-point:python3 -m rune"])
|
||||
mods.add("__main__")
|
||||
for m in sorted(mods):
|
||||
mid = f"module:{m}"
|
||||
e = entity(doc, mid)
|
||||
|
||||
@@ -999,6 +999,34 @@ def _search_files(args: dict) -> dict:
|
||||
return {"success": False, "error": f"Search failed: {e}"}
|
||||
|
||||
|
||||
def _resolve_execution_cwd(args: dict) -> tuple[str, str]:
|
||||
"""T21.4 §21.4E: automatic project execution context.
|
||||
|
||||
Precedence (tranche-mandated):
|
||||
explicit governed workdir > Project Registry repo/worktree for the
|
||||
named project > process cwd (project-less commands only).
|
||||
|
||||
The model names the PROJECT, never the cwd — the execution layer
|
||||
derives the correct repository/worktree via ProjectRegistry.resolve_cwd
|
||||
(which already honours active_worktree > repo_identity, and refuses
|
||||
scaffold_location as an execution cwd). An unknown or unresolvable
|
||||
project is REFUSED before any execution.
|
||||
"""
|
||||
workdir = args.get("workdir")
|
||||
if workdir:
|
||||
return str(workdir), "explicit"
|
||||
project = args.get("project")
|
||||
if project:
|
||||
from rune.project_registry import ProjectRegistry
|
||||
store = os.environ.get("RUNE_SCAFFOLD_STORE", "store/scaffold")
|
||||
registry = ProjectRegistry(store)
|
||||
try:
|
||||
return registry.resolve_cwd(str(project)), "project_registry"
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"project execution refused: {exc}") from exc
|
||||
return os.getcwd(), "process"
|
||||
|
||||
|
||||
def _run_command(args: dict) -> dict:
|
||||
"""Run an approved command, using argv unless shell mode is explicit."""
|
||||
import subprocess
|
||||
@@ -1006,7 +1034,11 @@ def _run_command(args: dict) -> dict:
|
||||
command = args.get("command")
|
||||
argv = args.get("argv")
|
||||
shell = args.get("shell", False)
|
||||
workdir = args.get("workdir", os.getcwd())
|
||||
try:
|
||||
workdir, cwd_source = _resolve_execution_cwd(args)
|
||||
except ValueError as exc:
|
||||
return {"success": False, "error": str(exc),
|
||||
"cwd_source": "refused"}
|
||||
|
||||
if shell is not True:
|
||||
if argv is None:
|
||||
@@ -1037,7 +1069,8 @@ def _run_command(args: dict) -> dict:
|
||||
output += f"\n[exit code: {result.returncode}]"
|
||||
if len(output) > 10000:
|
||||
output = output[:10000] + "\n[truncated]"
|
||||
return {"success": True, "result": output or "(no output)"}
|
||||
return {"success": True, "result": output or "(no output)",
|
||||
"cwd_source": cwd_source}
|
||||
except subprocess.TimeoutExpired:
|
||||
return {"success": False, "error": "Command timed out (30s)"}
|
||||
except Exception as e:
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T20:50:30Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "matched_by": "pending_match", "model_deadline_ignored": true, "model_op_type_ignored": true, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-756ebe6f45ad83a919738539a1565c17ed8572ac"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T20:51:20Z", "authority": "deploy_qualify", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_ancestor_of_master", "model_deadline_ignored": true, "model_op_type_ignored": true, "normalized": true, "op_type": "master_ci", "operation_id": "ci-f7256aa29e8bc7aa365ad7ffa27eb557528ccca5"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T21:50:39Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_on_feature_branch", "model_deadline_ignored": true, "model_op_type_ignored": true, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T21:53:34Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_on_feature_branch", "model_deadline_ignored": false, "model_op_type_ignored": false, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T21:55:05Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_on_feature_branch", "model_deadline_ignored": false, "model_op_type_ignored": false, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c"}
|
||||
@@ -73,11 +73,12 @@
|
||||
"dependencies": [
|
||||
"component:wait-daemon"
|
||||
],
|
||||
"generation": "",
|
||||
"generation": "t21.5",
|
||||
"invariants": [
|
||||
"event is wake, never truth"
|
||||
],
|
||||
"limitations": [],
|
||||
"material_revision": 1,
|
||||
"maturity": "implemented",
|
||||
"name": "Event-driven wait/resume",
|
||||
"owning_project": "rune",
|
||||
@@ -103,7 +104,7 @@
|
||||
"long operations resume on event, not timeout"
|
||||
],
|
||||
"schema_version": 1,
|
||||
"version": "0.3.0",
|
||||
"version": "0.4.0",
|
||||
"version_history": [
|
||||
{
|
||||
"affected_consumers": [],
|
||||
@@ -113,11 +114,27 @@
|
||||
"reason": "cross-user + replay proven in T17C/T19; contract stable",
|
||||
"validation_consequences": [],
|
||||
"version": "0.3.0"
|
||||
},
|
||||
{
|
||||
"affected_consumers": [],
|
||||
"at": "2026-09-17T17:16:18Z",
|
||||
"changed_sections": [
|
||||
"contract",
|
||||
"behaviour"
|
||||
],
|
||||
"covers_material_revision": 1,
|
||||
"kind": "MINOR",
|
||||
"reason": "T21.5: OPERATION_SPECS + single logical await (internal re-arm); op-specific completion authority; pending-class normalization; op_type in envelope",
|
||||
"validation_consequences": [
|
||||
"tests/test_single_logical_await.py",
|
||||
"tests/test_ci_event_wait.py"
|
||||
],
|
||||
"version": "0.4.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state_touched": "W/R bridge waits (shared root)",
|
||||
"updated_at": "2026-09-16T03:59:49Z"
|
||||
"updated_at": "2026-09-17T17:16:18Z"
|
||||
},
|
||||
"capability:operator-send-task": {
|
||||
"behaviour_specs": [
|
||||
@@ -187,6 +204,91 @@
|
||||
"state_touched": "TaskStateRepository (canonical), operator.json (legacy cache)",
|
||||
"updated_at": "2026-09-16T03:59:49Z"
|
||||
},
|
||||
"capability:rollback-generation": {
|
||||
"behaviour_specs": [],
|
||||
"capability_id": "capability:rollback-generation",
|
||||
"component": "component:deploy_pipeline",
|
||||
"constraints": "",
|
||||
"consumers": [],
|
||||
"contract": "refuse-before-mutation; ROLLED_BACK|NOOP|ROLLED_BACK_REVERTED|REFUSED_*; forward movement stays with deploy_promotion",
|
||||
"created_at": "2026-09-17T00:17:01Z",
|
||||
"dependencies": [],
|
||||
"generation": "issue161-harness",
|
||||
"invariants": [
|
||||
"refusal before mutation",
|
||||
"same lock as promote",
|
||||
"regression only: ancestor targets"
|
||||
],
|
||||
"limitations": [],
|
||||
"material_revision": 1,
|
||||
"maturity": "implemented",
|
||||
"name": "Governed deploy rollback",
|
||||
"owning_project": "rune",
|
||||
"purpose": "single canonical rollback_generation(target, expected_current, reason): shared promotion lock, expected-current verification, ancestor-only eligibility, atomic switch, identity+health qualification, durable history, revert-on-failed-health",
|
||||
"source_locations": [
|
||||
"scripts/rollback_generation.py"
|
||||
],
|
||||
"spec": {
|
||||
"acceptance_criteria": [
|
||||
"10/10 unit tests",
|
||||
"live B->A->B proof",
|
||||
"two live refusal proofs with zero mutation"
|
||||
],
|
||||
"baseline_reason": "stable contract: live-proven end to end in T21.4",
|
||||
"expected_behaviours": [
|
||||
"B->A regression rolls back and restores last-good",
|
||||
"forward/divergent target refused NOT_ELIGIBLE",
|
||||
"stale expected_current refused MISMATCH",
|
||||
"unknown/ghost target refused MISSING",
|
||||
"health failure reverts and records ROLLED_BACK_REVERTED"
|
||||
],
|
||||
"governance_version_started_at": "2026-09-17T00:18:55Z",
|
||||
"non_goals": [
|
||||
"forward promotion (deploy_promotion owns it)",
|
||||
"schema migrations",
|
||||
"database rollbacks"
|
||||
],
|
||||
"requirements": [
|
||||
"rollback_generation(target_generation, expected_current, reason) is the ONLY governed rollback path",
|
||||
"refusals occur before any mutation of current/last-good",
|
||||
"same promotion lock as deploy_promotion.promote",
|
||||
"targets must be ancestors of the deployed commit",
|
||||
"post-switch identity AND health must qualify or the switch reverts",
|
||||
"every outcome appended to store/scaffold/rollback_history.jsonl"
|
||||
],
|
||||
"schema_version": 1,
|
||||
"version": "1.1.0",
|
||||
"version_history": [
|
||||
{
|
||||
"affected_consumers": [],
|
||||
"at": "2026-09-17T00:18:55Z",
|
||||
"changed_sections": [],
|
||||
"kind": "BASELINE",
|
||||
"reason": "stable contract: live-proven end to end in T21.4",
|
||||
"validation_consequences": [],
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"affected_consumers": [],
|
||||
"at": "2026-09-17T00:19:19Z",
|
||||
"changed_sections": [
|
||||
"requirements",
|
||||
"expected_behaviours",
|
||||
"acceptance_criteria"
|
||||
],
|
||||
"covers_material_revision": 1,
|
||||
"kind": "MINOR",
|
||||
"reason": "T21.4: rollback-generation capability materialized (scripts/rollback_generation.py) with live B->A proof; MINOR per behaviour_added shape",
|
||||
"validation_consequences": [
|
||||
"test_rollback_generation.py added to required suite"
|
||||
],
|
||||
"version": "1.1.0"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state_touched": "",
|
||||
"updated_at": "2026-09-17T00:19:19Z"
|
||||
},
|
||||
"capability:scaffolding-v1": {
|
||||
"acceptance_state": [
|
||||
{
|
||||
@@ -246,6 +348,21 @@
|
||||
},
|
||||
"state": "V1_ACCEPTED",
|
||||
"was": "RETEST_REQUIRED (t21.3-open)"
|
||||
},
|
||||
{
|
||||
"at": "t21.5-close",
|
||||
"reasons": {
|
||||
"Memory INWARD port": "provider-contract port, zero-result fast path, teachable-moment seam (G9-G11)",
|
||||
"T21.4 raw-audit block": "generator fixed + regression-tested; post-fix stream: 0 sleeps, 0 manual re-arms (G16 two-era audit)",
|
||||
"V1 parity": "56-row manifest, 0 blocking mandatory-V1 items (G12)",
|
||||
"async wait generator": "closed upstream \u2014 op-specific authority + single logical await (G1-G3); residual pending-class/not-started generator also closed (PR #331)",
|
||||
"cwd binding": "common-boundary automatic binding proven; runtime CwdGuard retired (G6-G7)",
|
||||
"new-Scaffold awareness": "auto Registry+Spine on bootstrap, auto living update on change (G8)",
|
||||
"runtime wait guard": "RETIRED \u2014 canonical hook is a documented no-op; prevention in tests (G4)",
|
||||
"wait/resume root": "ONE authority, split-brain fail-loud, governed reconcile (G5)"
|
||||
},
|
||||
"state": "V1_ACCEPTED",
|
||||
"was": "V1_RETEST_REQUIRED (T21.4: 10 PASS / 2 BLOCKED)"
|
||||
}
|
||||
],
|
||||
"behaviour_specs": [
|
||||
@@ -326,7 +443,7 @@
|
||||
"fresh-context rehydration from compact ledgers"
|
||||
],
|
||||
"schema_version": 1,
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.4",
|
||||
"version_history": [
|
||||
{
|
||||
"affected_consumers": [],
|
||||
@@ -362,11 +479,43 @@
|
||||
"reason": "T21.3 implementation fixes: wait_guard corpus parity + time.sleep/from-time detection; drift_core/drift_cache F821 cleanup; hook parity test env-override; F821 fixes in rune_graphic/drift_cache. Contr",
|
||||
"validation_consequences": [],
|
||||
"version": "1.0.2"
|
||||
},
|
||||
{
|
||||
"affected_consumers": [],
|
||||
"at": "2026-09-17T00:37:29Z",
|
||||
"changed_sections": [
|
||||
"implementation"
|
||||
],
|
||||
"covers_material_revision": 0,
|
||||
"kind": "PATCH",
|
||||
"reason": "T21.4 implementation fixes, contract unchanged: rollback_generation.py governed API (permission-compatible shared lock); spine entry-point seeding; construction close auto-material-change closure; str",
|
||||
"validation_consequences": [
|
||||
"tests/test_rollback_generation.py",
|
||||
"tests/test_auto_execution_context.py"
|
||||
],
|
||||
"version": "1.0.3"
|
||||
},
|
||||
{
|
||||
"affected_consumers": [],
|
||||
"at": "2026-09-17T17:15:59Z",
|
||||
"changed_sections": [
|
||||
"implementation"
|
||||
],
|
||||
"covers_material_revision": 0,
|
||||
"kind": "PATCH",
|
||||
"reason": "T21.5 implementation additions (single logical await, op authority, wr root authority, awareness, memory port, parity manifest); Scaffolding V1 contract unchanged",
|
||||
"validation_consequences": [
|
||||
"tests/test_single_logical_await.py",
|
||||
"tests/test_wr_root_authority.py",
|
||||
"tests/test_memory_port.py",
|
||||
"tests/test_t215_awareness.py"
|
||||
],
|
||||
"version": "1.0.4"
|
||||
}
|
||||
]
|
||||
},
|
||||
"state_touched": "scaffold stores, capability/behaviour ledgers, structural model, project registry",
|
||||
"updated_at": "2026-09-16T22:05:20Z"
|
||||
"updated_at": "2026-09-17T17:15:59Z"
|
||||
},
|
||||
"capability:tui-testing-command": {
|
||||
"behaviour_specs": [
|
||||
@@ -434,8 +583,26 @@
|
||||
"generation": "t20",
|
||||
"note": "risk_notes added from 20Q cold-benchmark finding",
|
||||
"prior_maturity": "implemented"
|
||||
},
|
||||
{
|
||||
"at": "2026-09-17T00:18:03Z",
|
||||
"capability_id": "capability:rollback-generation",
|
||||
"event": "material_change",
|
||||
"generation": "issue161-harness",
|
||||
"material_revision": 1,
|
||||
"note": "contract BUILDER-2026-09-17T00:17:47Z-t21.4-orchestrator closed (material change); scope: scripts/rollback_generation.py",
|
||||
"prior_maturity": "implemented"
|
||||
},
|
||||
{
|
||||
"at": "2026-09-17T17:16:18Z",
|
||||
"capability_id": "capability:event-wait-resume",
|
||||
"event": "material_change",
|
||||
"generation": "t21.5",
|
||||
"material_revision": 1,
|
||||
"note": "T21.5: OPERATION_SPECS + single logical await (internal re-arm); op-specific completion authority; pending-class normalization; op_type in envelope",
|
||||
"prior_maturity": "implemented"
|
||||
}
|
||||
],
|
||||
"schema_version": 1,
|
||||
"updated_at": "2026-09-16T22:05:59Z"
|
||||
"updated_at": "2026-09-17T17:16:18Z"
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"tranche": "ASYNC_INTENT_NORMALIZATION",
|
||||
"purpose": "Intent normalization at the execution boundary \u2014 model emits intent, controller owns execution semantics",
|
||||
"predecessor": "T21.5 (guards retired) + Memory T0 (complete)",
|
||||
"created_at": "2026-09-17T20:43:47Z",
|
||||
"gates": [
|
||||
{
|
||||
"gate_id": "A10_LIVE_MASTER_DEPLOY_PROOF",
|
||||
"mandatory": true,
|
||||
"requirement": "live master CI -> deploy with normalization evidence",
|
||||
"status": "PASS",
|
||||
"evidence": "Post-merge master 53b4830: canonical continuation master_ci/deploy_qualify through deadline with 4 internal re-arms, then event_replayed success 0.771s on re-arm; deployed current == 53b4830 (0.84.1, ",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A11_REGRESSION_TESTS",
|
||||
"mandatory": true,
|
||||
"requirement": "deterministic test matrix green; existing wait tests unbroken",
|
||||
"status": "PASS",
|
||||
"evidence": "tests/test_await_intent.py 14/14 (A-H matrix + derivation truth table + provenance + observability). Existing wait suites unbroken: test_ci_event_wait 11/11, test_single_logical_await 9/9, test_event_",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A12_RAW_SELF_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "post-fix stream classified; no model scheduling choice affected execution",
|
||||
"status": "PASS",
|
||||
"evidence": "Post-fix stream classified: all 5 normalizations in store/await_intent_log.jsonl have deadline_provenance=GOVERNED_OPERATION_POLICY; MODEL_DURATION_ACTUALLY_USED_COUNT=0; MODEL_SUPPLIED_OP_TYPE_COUNT_",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:25:09Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A13_FINAL_RUNTIME_CONVERGENCE",
|
||||
"mandatory": true,
|
||||
"requirement": "git==release==deployed==runtime==healthy",
|
||||
"status": "PASS",
|
||||
"evidence": "master 53b4830 == deployed current == runtime identity 0.84.1 (deployed 22:04:56Z) == healthy; last-good fa1b7c4; tags v0.84.1. Final CI on the normalization-layer commit (8a8d353, PR #335) also green",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A1_INTENT_NORMALIZATION_BOUNDARY_PROVEN",
|
||||
"mandatory": true,
|
||||
"requirement": "earliest deterministic boundary identified + normalization implemented there (not prompt, not guard)",
|
||||
"status": "PASS",
|
||||
"evidence": "Root-cause traced: harness tool accepted model op_type/deadline BEFORE PendingOperation creation \u2014 the wait record was BORN from model args. Fix: scripts/await_intent.py normalize_action/derive_op_typ",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A2_PENDING_OPERATION_IS_WAIT_AUTHORITY",
|
||||
"mandatory": true,
|
||||
"requirement": "stored operation truth (op_type/authority/timing) wins over model args",
|
||||
"status": "PASS",
|
||||
"evidence": "Stored normalized records (ext.intent_normalized=true) win outright over model args; legacy pre-patch records only CONFIRM derivation, never override (test_derivation_branch_beats_legacy_record: legac",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A3_MODEL_TIMING_NON_AUTHORITATIVE",
|
||||
"mandatory": true,
|
||||
"requirement": "MODEL_DURATION_ACTUALLY_USED_COUNT=0 for canonical flows; provenance recorded",
|
||||
"status": "PASS",
|
||||
"evidence": "Timing provenance order USER_EXPLICIT > EXTERNAL_CONTRACT > GOVERNED_OPERATION_POLICY; MODEL_INVENTED classified then discarded (classify_timing_provenance + tests). Live/audit: MODEL_DURATION_ACTUALL",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A4_MODEL_OP_TYPE_NON_AUTHORITATIVE",
|
||||
"mandatory": true,
|
||||
"requirement": "MODEL_SUPPLIED_OP_TYPE_COUNT_USED=0; stored/derived authority wins",
|
||||
"status": "PASS",
|
||||
"evidence": "Derivation: normalized record > open PR (pull_request_checks) > ancestor-of-master (master_ci) > feature branch (branch_ci); legacy records never override. Live: model emitted master_ci for branch sha",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A5_SLEEP_POLL_INTENT_NORMALIZED",
|
||||
"mandatory": true,
|
||||
"requirement": "sleep/check/poll intent for a known pending op compiles to canonical continuation; literal sleep not executed",
|
||||
"status": "PASS",
|
||||
"evidence": "normalize_bash: \"sleep 300 && check CI for <sha>\" -> ContinuePendingOperation with raw_sleep_seconds_suppressed=300 (test_sleep_check_normalized); plain \"sleep 5\"/\"sleep 2 && ls\" NOT routed (test_plai",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A6_EXPLICIT_REAL_DEADLINES_PRESERVED",
|
||||
"mandatory": true,
|
||||
"requirement": "USER_EXPLICIT and CONTRACT deadlines retained with provenance",
|
||||
"status": "PASS",
|
||||
"evidence": "USER_EXPLICIT: user_deadline=600 -> provenance USER_EXPLICIT, 600 retained (test_user_deadline_preserved). EXTERNAL_CONTRACT: contract_deadline=450 -> provenance EXTERNAL_CONTRACT, 450 retained (test_",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A7_AMBIGUITY_FAILS_SAFE",
|
||||
"mandatory": true,
|
||||
"requirement": "multiple genuinely ambiguous pending ops -> structured ambiguity, no guess",
|
||||
"status": "PASS",
|
||||
"evidence": "Multiple pending ops with no target -> normalized:\"AMBIGUOUS\" with structured candidate ids, no guess (test_ambiguous_fails_safe + live: normalize_action({}) returned AMBIGUOUS with both candidates).",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A8_LIVE_BRANCH_CI_PROOF",
|
||||
"mandatory": true,
|
||||
"requirement": "live branch push -> CI with normalization evidence",
|
||||
"status": "PASS",
|
||||
"evidence": "Live branch push 246f2666 (async-intent-live-proof). Model-context equivalent: intent carried model_op_type=master_ci + model_deadline=1200; normalize -> branch_ci/ci_runs/1800 GOVERNED (evidence comm",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A9_LIVE_PR_MERGE_PROOF",
|
||||
"mandatory": true,
|
||||
"requirement": "live PR checks -> merge with normalization evidence",
|
||||
"status": "PASS",
|
||||
"evidence": "PR #336 created for the same sha; PR checks reached terminal via single-logical await (pull_request_checks authority -> event_replayed success 10.1s after internal waits); PR merged by merge-on-green.",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
}
|
||||
],
|
||||
"can_accept_result": {
|
||||
"ok": true,
|
||||
"blockers": []
|
||||
},
|
||||
"verdict": "ASYNC_INTENT_NORMALIZATION_COMPLETE"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# ASYNC INTENT LIVE PROOF
|
||||
|
||||
This file exists so a real branch push/CI/deploy cycle
|
||||
can exercise intent normalization end to end (A8-A10).
|
||||
Created 2026-09-17 by the corrective patch tranche.
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"tranche": "MEMORY_T0",
|
||||
"generated_at": "2026-09-17T20:10:00Z",
|
||||
"method": "read-mostly archaeology; sandboxed write-probes on in-memory SQLite copies only; production stores opened read-only (mode=ro); zero production records modified",
|
||||
|
||||
"runtime_reachability_legend": {
|
||||
"SOURCE_EXISTS": "code present, no importer found",
|
||||
"CONSTRUCTED": "instantiated on a runtime path",
|
||||
"WIRED": "connected into a live call chain",
|
||||
"RUNTIME_REACHABLE": "reachable from the production entrypoint (TUI/launcher or harness plugin)",
|
||||
"OBSERVED_USED": "runtime effect observed (fresh persisted data, live process, journal logs)",
|
||||
"CANONICAL": "owner of truth for its concern",
|
||||
"DEAD_OR_LEGACY": "superseded; no live writer/reader",
|
||||
"HARNESS_OR_TEST_ONLY": "exists for CI/tests/harness copies only"
|
||||
},
|
||||
|
||||
"production_entrypoints": [
|
||||
{
|
||||
"entrypoint": "Rune TUI (deployed launcher ~/.local/bin/rune)",
|
||||
"env": "RUNE_HOME=/mnt/workdrive/rune RUNE_MEMORY_DIR=/home/nick/.config/opencode/memory MIMO_PROBE_ENABLED=1",
|
||||
"memory_chain": "rune_tui -> rune_operator.send_task -> ContextBroker.assemble (collect/measure/admit) -> MemorySelector.select_for_task (6 sources, floor 0.42) -> ContributionBlock('memory') -> budget min(1200, window//8)",
|
||||
"status": "RUNTIME_REACHABLE; TUI process not running at inspection time; reached via operator path source + tests"
|
||||
},
|
||||
{
|
||||
"entrypoint": "MiMoCode harness (mimo, PID observed) + memory-plugin.ts",
|
||||
"memory_chain_per_turn": [
|
||||
"1. ALWAYS-ON: warm_router.py always --json -> digest injected every turn",
|
||||
"2. KEYWORD: warm_router.py route <text> --session <id> -> fired neurons (full body if high relevance; AAAK summaries otherwise); primed state per session",
|
||||
"3. WAKE-UP: memory_store.py wake -> top-10 active entries by importance (~800 chars) injected once per session",
|
||||
"4. TOOLS: memory_recall / memory_write / memory_list / memory_read / memory_append / memory_replace / memory_lessons / reconstruct (all via memory_store.py CLI)",
|
||||
"5. session.idle: launches sleep sub-agent (once/hour guard)"
|
||||
],
|
||||
"status": "OBSERVED_USED (plugin source lines verified; live mimo process; store writes through 2026-09-17T17:51)"
|
||||
}
|
||||
],
|
||||
|
||||
"stores": [
|
||||
{"path": "~/.config/opencode/memory/store/memory.db", "size": "456.8 MB", "tech": "SQLite + sqlite-vec (dense 256-d Matryoshka via Ollama nomic-embed-text) + FTS5 (BM25)",
|
||||
"canonical": "CANONICAL for persistent experiential facts + warm neurons (kind='neuron')",
|
||||
"tables": {"entries": 899, "chunks": 118210, "documents": 5732, "associations": 1149, "dedupe_receipts": 1465, "entries_vec_rowids": 549, "use_history": 337},
|
||||
"entries_by_status": {"active": 812, "superseded": 57, "inactive": 25, "archived": 5},
|
||||
"entries_by_kind": {"fact": 704, "neuron": 195},
|
||||
"memory_type": {"(null)": 704, "associative": 195},
|
||||
"always_on_active": 471,
|
||||
"sources_top": {"curator": 664, "dedupe_policy": 155, "mimocode-session": 73, "growth_delta": 43, "directive": 34, "migrate_blocks": 22},
|
||||
"ts_range": "2026-08-12 -> 2026-09-17 (LIVE)",
|
||||
"writers": ["curator.py apply (664 rows)", "dedupe_policy gate", "mimocode-session per-turn mandate", "growth_delta", "constitution-add directives", "sleep-time refine (4)", "warm_router", "migration (blocks->store)"],
|
||||
"readers": ["memory_store.recall (dense+BM25 RRF, floor 0.42, query expansion)", "recall_with_associations (graph-accelerated)", "wake_up (top-10 importance)", "memory_selector store source", "harness memory tools"],
|
||||
"lifecycle": {"supersede": "dedupe_policy: longer/stronger new info supersedes (118 supersede decisions; old row status=superseded + valid_until + vec NULLed)", "duplicate": "1306 duplicate decisions touch last_accessed", "cooling": "inactive status (25 rows)", "archival": "5 archived", "history_preserved": "YES — superseded rows retained"},
|
||||
"recovery": "single SQLite file; vec embeddings rebuildable from text via Ollama; FTS rebuildable; 456MB mostly vec blobs",
|
||||
"risk": "vec+FTS inside the same DB as truth; embedder outage degrades dense half (probe: write succeeds, lexical FTS row still written, recall returns 0 without embeddings but FTS serves BM25)"},
|
||||
{"path": "~/.config/opencode/memory/graph/graph.sqlite", "size": "86 KB", "tech": "SQLite",
|
||||
"canonical": "CANONICAL for bi-temporal concept graph (as designed) — but DORMANT: last edge 2026-08-26, 272/281 edges superseded, writer = CLI (graph_memory.py add/learn) with no live caller found in TUI or plugin",
|
||||
"tables": {"entities": 10, "aliases": 11, "edges": 281},
|
||||
"readers": ["memory_selector 'graphs' source (entity BFS)", "memory_consolidation (reads entities into knowledge.json)", "/graph TUI stats"],
|
||||
"writers": ["graph_memory.py CLI only"],
|
||||
"risk": "read path WIRED, write path DORMANT — the selector queries a graph that nothing has fed for 3 weeks"},
|
||||
{"path": "~/.rune/consolidation/knowledge.json", "size": "3.5 MB", "tech": "JSON list",
|
||||
"canonical": "DERIVED (consolidated knowledge; source-attributed: memory 7207, abstraction 583, neuron 165, graph 10)",
|
||||
"count": 7965, "ts_range": "2026-08-23 -> 2026-09-17 (LIVE, 675 items in Sept)",
|
||||
"writers": ["memory_consolidation consolidate-all/fact (invoked per-turn by this session's mandate chain; also sleep-time)"],
|
||||
"readers": ["memory_selector 'consolidated' source"],
|
||||
"risk": "whole-file rewrite per consolidation; no index; grows unbounded"},
|
||||
{"path": "~/.config/opencode/memory/abstraction.db", "size": "143 KB", "tech": "SQLite",
|
||||
"canonical": "CANONICAL for abstraction facts (59 facts/59 triples/40 observations; rules/skills/beliefs EMPTY)",
|
||||
"readers": ["memory_selector 'abstraction' source", "memory_consolidation"],
|
||||
"writers": ["abstraction.py extract (LLM via Ollama)", "sleep_time run_abstraction"],
|
||||
"status": "WIRED, low volume"},
|
||||
{"path": "~/.config/opencode/memory/cache.db", "size": "1.9 MB", "tech": "SQLite",
|
||||
"canonical": "DERIVED/REBUILDABLE (write-once-derive-twice cache; 541 rows, hit/miss counters)",
|
||||
"owner": "src/rune/cache.py", "risk": "disposable"},
|
||||
{"path": "~/.config/opencode/memory/temporal.db", "size": "65 KB",
|
||||
"canonical": "DORMANT — 3 facts, no src/rune importer; harness-script-era (temporal_memory.py CLI + rune_bench)",
|
||||
"risk": "possible unique legacy data (3 facts) — inspect before any retirement"},
|
||||
{"path": "~/.config/opencode/memory/behavior.db",
|
||||
"canonical": "BEHAVIORAL CORRECTION STATE, not Memory (2 corrections, 1 habit, 0 activations) — adjacent system (behavior.py)",
|
||||
"status": "WIRED via behavior.py; near-empty"},
|
||||
{"path": "~/.config/opencode/memory/auto_tasks.db + task_gate.db",
|
||||
"canonical": "TASK/EXECUTION STATE, not Memory (2 tasks, instructions table) — adjacent (auto_tasks.py, task_gate.py)"},
|
||||
{"path": "~/.config/opencode/memory/shards/*.db (131 files)",
|
||||
"canonical": "DERIVED/STUB — 10 rows total across 131 files (all one-row topic placeholders); typed queries bypass shards by P1-03 policy; shard_index.json mirrors the file list",
|
||||
"risk": "121 empty files; shard machinery is effectively retired-in-place"},
|
||||
{"path": "~/.config/opencode/memory/warm/memory.db", "canonical": "DEAD — 0 bytes; warm neurons migrated into store (kind='neuron'); .pasture files retired (8 files, reader retired)"},
|
||||
{"path": "~/.config/opencode/memory/index/warm_state.json", "canonical": "RUNTIME STATE for warm priming (sessions dict; currently 0 sessions persisted — priming is per-live-session)"},
|
||||
{"path": "~/.local/share/mimocode/mimocode.db (1.2 GB) + ~/.local/share/mimocode/memory (4.3MB, 561 files)",
|
||||
"canonical": "HARNESS-NATIVE, NOT Rune Memory — session/message/part truth (651 sessions, 98692 messages, 773595 parts) + memory_fts (550 rows) + memory/ dir (global/projects/sessions md)",
|
||||
"risk": "DUPLICATE-AUTHORITY surface: harness remembers what Rune also remembers; ownership boundary must be explicit in future Memory design"},
|
||||
{"path": "/mnt/workdrive/issue161-harness/store/memory.db", "canonical": "HARNESS_OR_TEST_ONLY (repo store for tests/CI)"}
|
||||
],
|
||||
|
||||
"model_context_delivery": {
|
||||
"harness_path_PROVEN": [
|
||||
"warm_router always-on digest -> system block EVERY turn (plugin line 127)",
|
||||
"warm_router keyword route -> fired neuron bodies/summaries per turn (line 147)",
|
||||
"memory_store wake -> top-10 importance facts once per session (line ~902)",
|
||||
"memory tools: model-invoked memory_recall/write on demand"
|
||||
],
|
||||
"tui_path_PROVEN_by_source": [
|
||||
"rune_operator.send_task -> ContextBroker.assemble -> MemorySelector.select_for_task -> 'memory' block (budget min(1200, window//8), floor 0.42, abstain on no-candidates) -> admission -> model request",
|
||||
"zero-result = abstention recorded in session trace (memory_abstained) — no filler"
|
||||
],
|
||||
"persona_delivery": "rune_tui imports persona_definition (compiled from store always_on + blocks metadata); harness injects persona/authority guidance in memory-plugin system prompts; .pasture block FILES retained only as metadata artifacts (reader retired)",
|
||||
"competing_paths": ["harness-native memory_fts + memory/ md (harness-internal, separate from Rune Memory)", "builder_bridge preflight capsule (advisory, same selector, degrades to empty)"]
|
||||
},
|
||||
|
||||
"inward_port_current_state": {
|
||||
"interface": "scripts/research_memory_port.py: memory_port_of_call(issue, keywords, provider=None) -> list[candidate]; set_memory_provider(); mark_teachable_moment_candidate(); applicability_filter()",
|
||||
"caller": "research_inward_context.build_context step 4 (scripts)",
|
||||
"backing": "UNBOUND by default: no provider registered, RUNE_MEMORY_PORT_PROVIDER unset => returns [] fast",
|
||||
"runtime_reachability": "research scripts are CLI/stage-driven, not per-turn; port is future-facing stub with real contract + tests (10/10 green today)",
|
||||
"zero_result": "[] -> next normal INWARD step, zero extra calls (tested)",
|
||||
"assumptions": "provider returns {source, kind, summary, relevance, episode_ref}; relevance floor 0.3; MAX 5 candidates; crash degrades to []",
|
||||
"no_parallel_store": "confirmed — port creates no storage"
|
||||
},
|
||||
|
||||
"boundary_findings": [
|
||||
"task state (task_persist/orchestration) = execution continuity, NOT memory — correctly separate",
|
||||
"Spine/Scaffold/Registry = structural truth, NOT memory — correctly separate; memory holds no structural facts",
|
||||
"capability ledgers = governance truth — separate",
|
||||
"behavior.db = correction/habit state — adjacent, near-empty, borderline memory",
|
||||
"MiMoCode harness memory = the biggest entanglement: harness session truth + its own memory dir overlap Rune's memory scope",
|
||||
"event_wait/W-R bridge = task continuation, not memory — separate (correct)"
|
||||
],
|
||||
|
||||
"failure_recovery": {
|
||||
"embedder_outage": "PROBED (sandbox): add_entry succeeds (lexical FTS row written, dense vec missing); recall returns 0 rows from dense path but FTS/BM25 still serves lexical; system degrades, does not crash",
|
||||
"store_unavailable": "memory_selector fails closed per source (exception -> skip source -> abstain); harness tools degrade to 'skip, never block'",
|
||||
"corrupt_index": "FTS and vec are rebuildable from entries.text (embeddings re-embeddable via Ollama; FTS rebuildable); knowledge.json rebuildable from sources",
|
||||
"irreversible": "entries.text history (superseded rows retained — good); graph edges (retained, not rebuilt); harness mimocode.db session truth",
|
||||
"sleep_time_DEFECT": "hourly systemd timer reads ~/.local/share/opencode/opencode.db (STALE 30.7 days, old opencode path) instead of ~/.local/share/mimocode/mimocode.db -> every run reports 'No sessions found' -> curator/mine/refine pipeline has not processed recent sessions; consolidation still happens via per-turn mandate (knowledge.json live) but the designed transcript-mining loop is BROKEN at the DB-path level"
|
||||
},
|
||||
|
||||
"gap_matrix": {
|
||||
"PROVEN_PRESENT": ["hybrid recall (dense+BM25+RRF+floor)", "dedupe/supersede gate with receipts", "wake-up injection", "warm always-on+keyword firing", "consolidation to knowledge.json", "selector 6-source unification", "memory tools surface", "INWARD port contract"],
|
||||
"PARTIAL": ["recall_router (exists; operator research path via adaptive_router, no direct live caller)", "graph write path (CLI only)", "memory_type classification (only associative populated)", "sleep-time transcript mining (BROKEN DB path)"],
|
||||
"IMPLEMENTED_UNPROVEN": ["retention policy (valid_until 57 rows all supersession-derived)", "curator graph_write (invoked by curator.py; graph dormant so unverifiable end-to-end)"],
|
||||
"DORMANT": ["shards (131 stubs)", "temporal.db", "warm/memory.db", ".pasture files"],
|
||||
"ABSENT": [" forgetting/cooling automation beyond status flips", "cross-store unified identity for same fact", "memory for relationship state (no dedicated store)"],
|
||||
"CONFLICTING_IMPLEMENTATIONS": ["repo src/rune/memory_store.py vs harness scripts/memory_store.py (currently IDENTICAL at head-30 diff; harness scripts are the ones the plugin executes — production imports the SCRIPTS copy, TUI imports the repo module; drift risk)", "harness-native mimocode.db memory vs Rune memory.db"],
|
||||
"UNKNOWN": []
|
||||
},
|
||||
|
||||
"preservation_risk": {
|
||||
"MUST_PRESERVE": ["memory.db entries (899 incl. 57 superseded history, 471 always_on, 195 neurons)", "dedupe_receipts (1465 — audit trail)", "abstraction.db facts", "graph.sqlite edges (historical)", "harness mimocode.db session truth"],
|
||||
"LIKELY_PRESERVE": ["knowledge.json (rebuildable but provenance-rich)", "use_history (337)", "warm_state.json", "behavior.db"],
|
||||
"DERIVED_REBUILDABLE": ["cache.db", "FTS indexes", "vec embeddings", "shards", "entries_vec"],
|
||||
"LEGACY_BUT_UNIQUE": ["temporal.db 3 facts (inspect before retiring)", ".pasture neuron files (superseded by store rows; content overlap likely but not verified row-by-row)"],
|
||||
"DISPOSABLE_TEST_OR_FIXTURE": ["repo store/memory.db", "warm/memory.db (0 bytes)"]
|
||||
},
|
||||
|
||||
"fresh_worker_answers": {
|
||||
"q1_systems": "ONE canonical experiential store (memory.db) + abstraction.db + knowledge.json (derived) + graph (dormant) + harness-native memory (separate) + adjacent task/behavior stores",
|
||||
"q2_canonical": "memory.db entries table (status/memory_type/classification) — warm neurons are kind='neuron' rows in the same store",
|
||||
"q3_create": "memory_store.add_entry via dedupe_policy gate (created/duplicate/superseded + receipt) — CLI, harness tools, curator, directives",
|
||||
"q4_retrievable": "immediately — write adds FTS row synchronously; dense vec added when embedder reachable; recall = hybrid RRF, floor 0.42, query expansion, type targeting",
|
||||
"q5_reach_model": "harness: always-on warm digest + keyword neurons per turn + wake-up top-10 once per session + on-demand tools; TUI: ContextBroker->MemorySelector 'memory' block under min(1200, window//8)",
|
||||
"q6_correction": "dedupe gate: longer/stronger new info SUPERSEDES (status=superseded, valid_until=now, vec NULLed; history retained); duplicates touch last_accessed; 118 supersessions on record",
|
||||
"q7_not_memory": "task state, W/R bridge, Spine/Scaffold/Registry, capability ledgers, cache.db, shards, harness session DB",
|
||||
"q8_preserve": "memory.db + receipts + abstraction.db + graph history + harness session truth"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
# Rune Memory T0 — As-Is Memory Inventory Report
|
||||
|
||||
*Prepared for external review. Self-contained: no prior context assumed.*
|
||||
|
||||
---
|
||||
|
||||
## 1. What this is
|
||||
|
||||
An archaeology report on the memory system of **Rune** — a self-hosted AI coding assistant / personal agent running on Linux (repo: private Gitea, deployed generations under `/mnt/workdrive/rune-code`, canonical worktree `/mnt/workdrive/issue161-harness`).
|
||||
|
||||
Rune just completed a long Scaffolding V1 programme (tranches T21.4/T21.5, accepted 2026-09-17 with 17/17 gates, deployed release v0.83.3). The first post-V1 phase is **Memory**. Its first tranche, **Memory T0**, was deliberately read-mostly: establish what memory machinery *actually exists today* before designing anything new.
|
||||
|
||||
Central rule of T0: never collapse `EXISTS IN SOURCE ≠ WIRED ≠ RUNTIME REACHABLE ≠ ACTUALLY USED ≠ CANONICAL AUTHORITY`.
|
||||
|
||||
**Verdict: MEMORY_T0_COMPLETE** — 17/17 gates PASS, `can_accept() = true`, merged as PR #333. Artifacts: `store/scaffold/memory_t0_gates.json` (gate object) and `store/scaffold/memory_t0_asis_map.json` (central map).
|
||||
|
||||
---
|
||||
|
||||
## 2. What Rune actually has today (the short answer)
|
||||
|
||||
One canonical experiential store, one derived knowledge file, several dormant subsystems, one harness-native duplicate, and a set of adjacent non-memory stores that are correctly separated.
|
||||
|
||||
### The canonical store
|
||||
|
||||
`~/.config/opencode/memory/store/memory.db` — 456.8 MB SQLite with **two retrieval engines side by side**:
|
||||
|
||||
- **Dense**: sqlite-vec, 256-dim Matryoshka-truncated embeddings via local Ollama (`nomic-embed-text`), content-hash cached.
|
||||
- **Lexical**: FTS5/BM25.
|
||||
|
||||
`recall()` fuses both with reciprocal-rank fusion, applies a **0.42 relevance floor** (below it: abstain rather than inject distractors), supports query expansion via a lexicon, type targeting, and QPP-based abstention.
|
||||
|
||||
**Contents**: 899 entries (812 active / 57 superseded / 25 inactive / 5 archived), 118,210 chunks, 5,732 documents, 1,149 associative links, 337 use-history rows. Warm neurons are stored as `kind='neuron'` rows (195) in the same DB — a previous file-based system (`.pasture` markdown files) was migrated and retired.
|
||||
|
||||
**Supersession is real and receipted.** A dedupe policy gate sits on writes: similar new content either touches the existing row (`duplicate`), or — if longer/stronger — **supersedes** it (old row marked `status='superseded'`, `valid_until=now`, embedding nulled; history retained). Receipt ledger: 1,465 decisions (41 created / 1,306 duplicates / 118 superseded). Every row carries `source`, `method`, `confidence`, `temperature`, timestamps.
|
||||
|
||||
### Derived knowledge
|
||||
|
||||
`~/.rune/consolidation/knowledge.json` — 7,965 consolidated items (source-attributed: 7,207 from the store, 583 from abstraction, 165 from neurons, 10 from graph), actively growing (675 new in September). Read back into context via one of the selector's six sources. Whole-file rewrite per consolidation; rebuildable.
|
||||
|
||||
### Dormant subsystems (exist, wired for read, no live writer)
|
||||
|
||||
- **graph.sqlite** — a well-designed bi-temporal concept knowledge graph (typed nodes, `valid_from`/`valid_until` + `learned_at` edge semantics, supersession columns). 281 edges, **272 superseded**, last write 2026-08-26. The write path is a CLI; nothing in the live runtime feeds it. The read path (selector `graphs` source) queries a graph that hasn't been fed in three weeks.
|
||||
- **shards** — 131 topic-partition DB files containing **10 rows total** (one-row topic placeholders); typed queries bypass shards by policy. Effectively retired-in-place.
|
||||
- **temporal.db** — 3 facts, no importer in the repo's `src/rune` at all (harness-script-era relic).
|
||||
- **warm/memory.db** — 0 bytes; dead path.
|
||||
|
||||
### Adjacent systems (correctly *not* memory)
|
||||
|
||||
Task state (durable task repo + orchestration), Spine/Project Scaffold/Project Registry (structural truth), capability/behaviour ledgers (governance), the W/R event-wait bridge (task continuation), `behavior.db` (corrections/habits — borderline, near-empty). T0 verified no experiential facts live in structural stores and no structural facts in memory stores.
|
||||
|
||||
### The duplicate-authority surface (the big architectural question)
|
||||
|
||||
The **MiMoCode harness itself** (the VSCode-like agent harness Rune runs inside) maintains its own memory: `mimocode.db` (1.2 GB — 651 sessions, 98,692 messages, 773,595 parts, plus its own FTS) and `~/.local/share/mimocode/memory` (4.3 MB of markdown). Two systems remember the same operational life. T0 flagged this as the primary authority-boundary question for future design — nothing was reconciled.
|
||||
|
||||
A second, smaller drift risk: the repo module `src/rune/memory_store.py` and the harness copy `~/.config/opencode/scripts/memory_store.py` are currently aligned, but the **harness plugin executes the scripts copy** while the TUI imports the repo module. Same code today, two deployment paths.
|
||||
|
||||
---
|
||||
|
||||
## 3. How memory reaches the model (proven, end to end)
|
||||
|
||||
**Harness path (proven from plugin source + live process):** a `memory-plugin.ts` runs per turn:
|
||||
1. **Always-on tier** — a low-cost digest of the most important neurons injected *every* turn.
|
||||
2. **Keyword tier** — `warm_router.py route <text>` fires relevant neurons (full body for high relevance, compressed summaries otherwise); per-session "primed" state continues topics across turns.
|
||||
3. **Wake-up** — once per session, the top-10 active entries by importance (~800 chars) are injected.
|
||||
4. **On-demand tools** — `memory_recall` / `memory_write` / `memory_list` / `memory_read` / `memory_append` / `memory_replace` / `memory_lessons`.
|
||||
5. On session idle, a sleep sub-agent is launched (once per hour guard).
|
||||
|
||||
**TUI/operator path (proven from source):** `rune_operator.send_task` → `ContextBroker.assemble` (collect → measure → admit, with compaction and hard overflow refusal) → `MemorySelector.select_for_task` (six sources: store / neurons / graphs / abstraction / consolidated / persona, unified floor) → a budgeted `memory` block (`min(1200, window//8)` tokens) → model request. Zero candidates = recorded abstention, never filler.
|
||||
|
||||
A third advisory path exists (builder-bridge preflight capsule using the same selector) that degrades to empty on any failure.
|
||||
|
||||
**The T21.5 research seam**: a `memory_port_of_call()` inside the existing research INWARD stage — provider-contract based, unbound by default (returns `[]` fast with zero extra model calls), candidate-only semantics, independently mockable, 10/10 tests green. Verified as safe to preserve as-is.
|
||||
|
||||
---
|
||||
|
||||
## 4. Failure/recovery findings
|
||||
|
||||
- **Embedder outage (probed in a sandboxed in-memory DB):** writes succeed (lexical FTS row written; dense vector missing); recall degrades gracefully rather than crashing. The system fails soft.
|
||||
- **Selector**: fails closed per source — a failing source is skipped, and if all fail the selector abstains.
|
||||
- **Rebuildable**: FTS indexes, embeddings, knowledge.json, cache. **Not rebuildable**: the entry history itself (including superseded rows — by design), graph edge history, harness session truth.
|
||||
- **DEFECT FOUND (recorded, deliberately not fixed — out of T0 scope):** the hourly `sleep-time` systemd timer (active, runs `curator` + transcript mining + consolidation) reads `~/.local/share/opencode/opencode.db` — a **stale database from a defunct harness, 30.7 days old** — instead of the live `~/.local/share/mimocode/mimocode.db`. Every run logs "No sessions found". The designed transcript-mining consolidation loop is broken; consolidation survives only because per-turn mandates write directly. This is the first concrete fix candidate for the Memory build phase.
|
||||
|
||||
---
|
||||
|
||||
## 5. Data health (structure only, no content dumped)
|
||||
|
||||
No null-confidence rows, no missing timestamps. Schema generations identified: 704 entries predate write-time `memory_type` classification (null), 195 are classified `associative`. Shards/`warm_state.json`/temporal are residue. 121 of 131 shard files are empty. The oldest live data is 2026-08-12; the newest is the day of the audit. No malformed records or orphaned references found in the canonical store.
|
||||
|
||||
---
|
||||
|
||||
## 6. Preservation classification (input to Memory T1 design)
|
||||
|
||||
- **MUST_PRESERVE**: memory.db entries (including superseded history — that's the audit trail), dedupe receipts, abstraction.db facts, graph edge history, harness session truth.
|
||||
- **LIKELY_PRESERVE**: knowledge.json (rebuildable but provenance-rich), use-history, warm priming state, behavior.db.
|
||||
- **REBUILDABLE**: cache, FTS, embeddings, knowledge.json, shards.
|
||||
- **LEGACY_BUT_UNIQUE**: temporal.db's 3 facts (inspect before retiring); .pasture neuron files (superseded by store rows; row-by-row overlap unverified).
|
||||
|
||||
---
|
||||
|
||||
## 7. Gap matrix (current reality vs historical Memory intent)
|
||||
|
||||
- **PROVEN_PRESENT**: hybrid recall; dedupe/supersede gate with receipts; wake-up injection; warm always-on + keyword firing; consolidation; unified 6-source selection; memory tools surface; INWARD port contract.
|
||||
- **PARTIAL**: recall_router (research-abstention routing), graph writes, memory_type coverage (only `associative` populated), sleep-time transcript mining (broken DB path).
|
||||
- **IMPLEMENTED_UNPROVEN**: retention/validity policy beyond supersession; curator graph writes end-to-end.
|
||||
- **DORMANT**: shards, temporal.db, warm/memory.db, .pasture.
|
||||
- **ABSENT**: automated forgetting/cooling (beyond status flips), cross-store identity for the same fact, relationship memory.
|
||||
- **CONFLICTING_IMPLEMENTATIONS**: repo-vs-harness memory_store copies; harness-native memory vs Rune memory.
|
||||
- **UNKNOWN**: none remaining (T0 exit criterion).
|
||||
|
||||
---
|
||||
|
||||
## 8. What was *not* done (by design)
|
||||
|
||||
No future architecture designed. No data migrated, repaired, or deleted. No runtime guard or behavior changed. Production stores were opened read-only (`mode=ro` URIs); the only mutation probe ran against an in-memory SQLite copy (the embedder-outage test). Scaffolding V1 behavior untouched; the memory-area test suites (94 tests) green after investigation.
|
||||
|
||||
---
|
||||
|
||||
## 9. Recommended next phase (Memory T1 — design, not build)
|
||||
|
||||
1. Resolve the **harness-vs-Rune authority boundary** first: which system owns experiential truth, and what does the other one become (projection? source? separate scope?).
|
||||
2. Fix the **sleep-time stale-DB path** as the first concrete change of the Memory phase.
|
||||
3. Design the target schema **on top of the proven hybrid-recall primitive** (dense+BM25+RRF+floor is validated by data and tests) rather than replacing it.
|
||||
4. Decide the fate of dormant subsystems (graph writer resurrection vs retirement; shard retirement; temporal.db inspection) with the preservation register in hand.
|
||||
5. Close the repo-vs-harness code-copy drift risk.
|
||||
|
||||
---
|
||||
|
||||
*Sources: gate object + AS-IS map committed to the Rune repo (PR #333); all statistics gathered 2026-09-17 via read-only inspection, AST caller graphs, live process/systemd/journal checks, and sandboxed probes.*
|
||||
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"tranche": "MEMORY_T0",
|
||||
"purpose": "CURRENT-STATE ARCHAEOLOGY / AS-IS MEMORY INVENTORY (read-mostly)",
|
||||
"predecessor": "store/scaffold/t21.5_gates.json (V1_ACCEPTED, 17/17)",
|
||||
"created_at": "2026-09-17T18:18:17Z",
|
||||
"gates": [
|
||||
{
|
||||
"gate_id": "M0.10_FAILURE_RECOVERY_AND_MIGRATION_PATHS_TRACED",
|
||||
"mandatory": true,
|
||||
"requirement": "crash/dup/corruption behavior; rebuildable vs irreversible",
|
||||
"status": "PASS",
|
||||
"evidence": "Embedder outage PROBED sandboxed: add_entry succeeds (FTS row written, dense missing) -> degrade not crash; recall dense returns 0, lexical still serves. Selector fails closed per source. FTS/vec/know",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.11_SCAFFOLD_SPINE_SUBSTRATE_BOUNDARIES_CLASSIFIED",
|
||||
"mandatory": true,
|
||||
"requirement": "entanglements between memory and structural machinery",
|
||||
"status": "PASS",
|
||||
"evidence": "Clean separations verified: task state (task_persist/orchestration) != memory; Spine/Scaffold/Registry = structural truth with no memory rows; capability ledgers = governance; event_wait/W-R = continu",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.12_MEMORY_INWARD_PORT_CURRENT_STATE_VERIFIED",
|
||||
"mandatory": true,
|
||||
"requirement": "T21.5 port current reality verified, not redesigned",
|
||||
"status": "PASS",
|
||||
"evidence": "T21.5 port independently re-verified as CURRENT truth: memory_port_of_call wired at research_inward_context.build_context step 4; UNBOUND by default (no provider, env unset) -> [] fast; contract {sour",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.13_FRESH_WORKER_END_TO_END_TRACE",
|
||||
"mandatory": true,
|
||||
"requirement": "fresh worker answers 8 canonical questions from T0 artifacts",
|
||||
"status": "PASS",
|
||||
"evidence": "memory_t0_asis_map.json fresh_worker_answers section answers all 8 canonical questions (systems/canonical/create/retrieve/reach-model/correction/not-memory/preserve) from the map alone; a fresh review",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.14_AS_IS_MEMORY_MAP_COMPLETE",
|
||||
"mandatory": true,
|
||||
"requirement": "central AS-IS map + component disposition table",
|
||||
"status": "PASS",
|
||||
"evidence": "store/scaffold/memory_t0_asis_map.json: production entrypoints (2), stores (14 entries incl. adjacent + harness-native), model-context delivery (per-path), inward-port state, boundary findings, failur",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.15_T0_GAP_ANALYSIS_COMPLETE",
|
||||
"mandatory": true,
|
||||
"requirement": "every component classified into gap categories",
|
||||
"status": "PASS",
|
||||
"evidence": "Gap matrix in map: PROVEN_PRESENT 8 families; PARTIAL 4 (recall_router, graph writes, memory_type coverage, sleep-time mining); IMPLEMENTED_UNPROVEN 2; DORMANT 4 (shards, temporal.db, warm/memory.db, ",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.16_RAW_SELF_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "read-mostly discipline held; mutations documented; claims evidence-backed",
|
||||
"status": "PASS",
|
||||
"evidence": "Claim-inflation check executed per addendum \u00a716: every major conclusion carries source+caller+runtime+data evidence; downgrades applied where proof was partial (recall_router -> PARTIAL, graph write -",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.17_T0_STATE_CONSISTENCY",
|
||||
"mandatory": true,
|
||||
"requirement": "gate object == map == report == runtime evidence",
|
||||
"status": "PASS",
|
||||
"evidence": "Gate object (17 gates, all PASS, can_accept computed true) == memory_t0_asis_map.json (same findings) == final report verdict MEMORY_T0_COMPLETE. Contradiction check: none \u2014 the one defect found (slee",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.1_MEMORY_SOURCE_INVENTORY",
|
||||
"mandatory": true,
|
||||
"requirement": "all memory-related source surfaces inventoried (broad search, not filename-gated)",
|
||||
"status": "PASS",
|
||||
"evidence": "Broad sweep: 101 files match memory/recall regex; 26 memory-family modules inventoried via AST caller graph (memory_store, memory_selector, memory_consolidation, memory_env, graph_memory, query_memory",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.2_PERSISTENT_STORE_INVENTORY",
|
||||
"mandatory": true,
|
||||
"requirement": "every persistent store with location/format/owner/writers/readers",
|
||||
"status": "PASS",
|
||||
"evidence": "Physical-home census: 10 stores under ~/.config/opencode/memory + ~/.rune/consolidation + harness mimocode.db (1.2GB) + harness memory dir (4.3MB) + repo test store. Each recorded: location/tech/schem",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.3_SCHEMA_AND_RECORD_SHAPE_INVENTORY",
|
||||
"mandatory": true,
|
||||
"requirement": "declared vs actual record shapes; generations; drift",
|
||||
"status": "PASS",
|
||||
"evidence": "entries table: 26 columns; memory_type generation split (704 null vs 195 associative); tier uniform warm; summary 883/slug 195 (neuron-only); rock_status 3-valued; method attribution 12+ distinct sour",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.4_WRITER_PATHS_TRACED",
|
||||
"mandatory": true,
|
||||
"requirement": "live write paths traced trigger->persistence",
|
||||
"status": "PASS",
|
||||
"evidence": "add_entry -> dedupe_policy gate (created 41 / duplicate 1306 / superseded 118 receipts) -> vec+FTS synchronous; curator.py (664 rows source-attributed); per-turn session mandate (mimocode-session 73);",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.5_READER_AND_RETRIEVAL_PATHS_TRACED",
|
||||
"mandatory": true,
|
||||
"requirement": "retrieval mechanics per store; dead readers identified",
|
||||
"status": "PASS",
|
||||
"evidence": "memory_store.recall: hybrid dense(sqlite-vec)+BM25(FTS5) fused RRF, floor 0.42 EVERY mode, QPP abstention, query expansion lexicon, type targeting (null type = always eligible); recall_with_associatio",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.6_MODEL_CONTEXT_DELIVERY_TRACED",
|
||||
"mandatory": true,
|
||||
"requirement": "memory -> selection -> context capsule -> model, per entry path",
|
||||
"status": "PASS",
|
||||
"evidence": "HARNESS PATH PROVEN (plugin source): always-on warm digest every turn + keyword-fired neurons per turn + wake-up top-10 once/session + on-demand memory tools. TUI PATH PROVEN by source: send_task -> C",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.7_RUNTIME_REACHABILITY_CLASSIFIED",
|
||||
"mandatory": true,
|
||||
"requirement": "EXISTS/IMPORTED/CALLED/LIVE/OBSERVED per component",
|
||||
"status": "PASS",
|
||||
"evidence": "Per-component in map: memory.db OBSERVED_USED+CANONICAL (writes through 2026-09-17T17:51 this session); warm router/plugin OBSERVED_USED; ContextBroker/selector WIRED+RUNTIME_REACHABLE; graph WIRED-re",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.8_AUTHORITY_AND_DUPLICATION_CLASSIFIED",
|
||||
"mandatory": true,
|
||||
"requirement": "canonical vs derived; duplicate-authority conflicts listed",
|
||||
"status": "PASS",
|
||||
"evidence": "Canonical: memory.db (experiential+neurons), abstraction.db (abstractions), graph.sqlite (bi-temporal KG by charter, dormant). Derived/rebuildable: cache.db, FTS, vec, shards, knowledge.json. DUPLICAT",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.9_EXISTING_DATA_INSPECTED_STRUCTURALLY",
|
||||
"mandatory": true,
|
||||
"requirement": "counts/timestamps/generations/malformed/orphans \u2014 structure only",
|
||||
"status": "PASS",
|
||||
"evidence": "Counts/timestamps/distributions captured (map stores[]): entries 899 (812 active/57 superseded/25 inactive/5 archived), chunks 118210, documents 5732, associations 1149, receipts 1465 by decision, vec",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
}
|
||||
],
|
||||
"can_accept_result": {
|
||||
"ok": true,
|
||||
"blockers": []
|
||||
},
|
||||
"verdict": "MEMORY_T0_COMPLETE"
|
||||
}
|
||||
@@ -1,5 +1,22 @@
|
||||
{
|
||||
"projects": {
|
||||
"rune": {
|
||||
"active_worktree": "",
|
||||
"default_branch": "master",
|
||||
"forge_repo": "conductor/rune",
|
||||
"last_known_good": "",
|
||||
"outstanding_validations": 0,
|
||||
"project_id": "rune",
|
||||
"registered_at": "2026-09-17T00:07:31Z",
|
||||
"repo_identity": "git:/mnt/workdrive/issue161-harness",
|
||||
"runtime_generation": "",
|
||||
"scaffold_location": "/mnt/workdrive/issue161-harness/store/scaffold",
|
||||
"scaffold_state": {
|
||||
"health": "UNKNOWN",
|
||||
"last_material_update": "2026-09-17T00:07:31Z"
|
||||
},
|
||||
"structural_schema_version": 1
|
||||
},
|
||||
"stock-tool": {
|
||||
"default_branch": "master",
|
||||
"forge_repo": "",
|
||||
@@ -40,5 +57,5 @@
|
||||
}
|
||||
},
|
||||
"schema_version": 1,
|
||||
"updated_at": "2026-09-16T17:31:18Z"
|
||||
"updated_at": "2026-09-17T00:37:13Z"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"at": "2026-09-17T00:02:25Z", "current_commit": "40b3731928f499a77653613cb6c3e6db93cb64a1", "current_generation": "40b3731928f4", "expected_current": "40b3731928f499a77653613cb6c3e6db93cb64a1", "health": "identity=caa77f8a3592 import_smoke=ok (0.81.6)", "operation": "rollback_generation", "outcome": "ROLLED_BACK", "post_switch_identity": "caa77f8a3592028cb076567169bea185dc01b32e", "reason": "T21.4 G6 live proof: governed regression B->A", "switched_to": "caa77f8a3592", "target": "caa77f8a3592", "target_commit": "caa77f8a3592028cb076567169bea185dc01b32e"}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,151 @@
|
||||
{
|
||||
"tranche": "T21.4",
|
||||
"purpose": "POST-RESTART FINAL V1 PROOF",
|
||||
"created_at": "2026-09-16T23:52:37Z",
|
||||
"gates": [
|
||||
{
|
||||
"gate_id": "G10_FINAL_DEPLOY_CONVERGENCE",
|
||||
"mandatory": true,
|
||||
"requirement": "master/release/deployed/runtime identity coherent; CI green; deploy complete via await_operation only",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T23:52:37Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G11_RAW_SELF_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "raw tool-execution audit: 0 normal unjustified waits, 0 normal guard interventions, >=1 deliberate backstop",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T23:52:37Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G12_ACCEPTANCE_STATE_CONSISTENCY",
|
||||
"mandatory": true,
|
||||
"requirement": "GateAccounting result == capability ledger == internal version == runtime evidence == report verdict",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T23:52:37Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G1_AWAIT_TOOL_LIVE",
|
||||
"mandatory": true,
|
||||
"requirement": "await_operation registered and callable in the running controller tool vocabulary; runtime exposure proven, not inferred from disk",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T23:52:37Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G2_NATURAL_FIRST_CLASS_WAIT_SELECTION",
|
||||
"mandatory": true,
|
||||
"requirement": "await_operation naturally selected (no priming) on all applicable normal async cycles",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T23:52:37Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G3_ZERO_NORMAL_WAIT_GUARD_INTERVENTIONS",
|
||||
"mandatory": true,
|
||||
"requirement": "wait-guard interventions during normal cycles = 0 (deliberate backstop counted separately)",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T23:52:37Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G4_AUTOMATIC_PROJECT_CWD_BINDING",
|
||||
"mandatory": true,
|
||||
"requirement": "execution layer auto-resolves project cwd via ProjectRegistry.resolve_cwd precedence; no manual cd needed",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T23:52:37Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G5_ZERO_NORMAL_CWD_GUARD_INTERVENTIONS",
|
||||
"mandatory": true,
|
||||
"requirement": "cwd-guard interventions during normal workflows = 0",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T23:52:37Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G6_GOVERNED_ROLLBACK_API",
|
||||
"mandatory": true,
|
||||
"requirement": "genuine rollback_generation API owning lock/verify/switch/health/history; live B->A->B proof + incompatible refusal",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T23:52:37Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G7_RUNE_MEANINGFUL_STRUCTURAL_COVERAGE",
|
||||
"mandatory": true,
|
||||
"requirement": "all meaningful architectural units represented in Scaffold; A-coverage 100%, no significant UNKNOWNs",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T23:52:37Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G8_MATERIAL_CHANGE_CLOSURE",
|
||||
"mandatory": true,
|
||||
"requirement": "governed construction automatically triggers material_change/revision/spec flow",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T23:52:37Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G9_FINAL_TEST_GATES",
|
||||
"mandatory": true,
|
||||
"requirement": "all required test/gate checks pass",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T23:52:37Z",
|
||||
"resolved_at": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
{
|
||||
"tranche": "T21.4",
|
||||
"purpose": "POST-RESTART FINAL V1 PROOF",
|
||||
"created_at": "2026-09-16T23:52:37Z",
|
||||
"gates": [
|
||||
{
|
||||
"gate_id": "G10_FINAL_DEPLOY_CONVERGENCE",
|
||||
"mandatory": true,
|
||||
"requirement": "master/release/deployed/runtime identity coherent; CI green; deploy complete via await_operation only",
|
||||
"status": "PASS",
|
||||
"evidence": "master 9acfc095 == deployed current == runtime identity (build_identity 0.82.2, import smoke ok) == tag v0.82.2; last-good 818aa490 coherent; CI 8700+ success; deploy promoted after qualify; release t",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T00:04:36Z",
|
||||
"resolved_at": "2026-09-17T02:34:36Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G11_RAW_SELF_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "raw tool-execution audit: 0 normal unjustified waits, 0 normal guard interventions, >=1 deliberate backstop",
|
||||
"status": "BLOCKED",
|
||||
"evidence": "NORMAL_EXECUTED_UNJUSTIFIED_WAIT_COUNT=3 (sleep 110/240/120 inter-cycle, guard blind spot: heredoc-wrapped poll text); NORMAL_WAIT_GUARD_INTERVENTION_COUNT=1 (sleep 150 blocked); DELIBERATE_REGRESSION",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T00:04:36Z",
|
||||
"resolved_at": "2026-09-17T02:34:36Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G12_ACCEPTANCE_STATE_CONSISTENCY",
|
||||
"mandatory": true,
|
||||
"requirement": "GateAccounting result == capability ledger == internal version == runtime evidence == report verdict",
|
||||
"status": "BLOCKED",
|
||||
"evidence": "can_accept()=FALSE because G11 BLOCKED (3 unjustified executed waits). Per tranche rules acceptance is prohibited; state left as computed, not altered.",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T00:04:36Z",
|
||||
"resolved_at": "2026-09-17T02:34:36Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G1_AWAIT_TOOL_LIVE",
|
||||
"mandatory": true,
|
||||
"requirement": "await_operation registered and callable in the running controller tool vocabulary; runtime exposure proven, not inferred from disk",
|
||||
"status": "PASS",
|
||||
"evidence": "await-operation in live session tool vocabulary; TS source ~/.mimocode/tools/await-operation.ts; live call vs SHA 40b3731 returned event-bridge envelope (registered/replayed/resumed @ 2026-09-16T23:55",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T00:04:36Z",
|
||||
"resolved_at": "2026-09-17T00:04:36Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G2_NATURAL_FIRST_CLASS_WAIT_SELECTION",
|
||||
"mandatory": true,
|
||||
"requirement": "await_operation naturally selected (no priming) on all applicable normal async cycles",
|
||||
"status": "PASS",
|
||||
"evidence": "await_operation selected directly on EVERY async workflow cycle (branch CI be6bc17/f3ef4672/6b5d8ad, PR CI, master CI 818aa490 incl. watchdog_authority_recovered->success, deploy convergence). No prim",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T00:04:36Z",
|
||||
"resolved_at": "2026-09-17T02:34:36Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G3_ZERO_NORMAL_WAIT_GUARD_INTERVENTIONS",
|
||||
"mandatory": true,
|
||||
"requirement": "wait-guard interventions during normal cycles = 0 (deliberate backstop counted separately)",
|
||||
"status": "PASS",
|
||||
"evidence": "0 wait-guard interventions during the async workflow cycles themselves. DISCLOSED: 1 inter-cycle sleep+poll attempt (sleep 150, post-workflows) blocked by guard -> recorded as REGRESSION_BACKSTOP_PROO",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T00:04:36Z",
|
||||
"resolved_at": "2026-09-17T02:34:36Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G4_AUTOMATIC_PROJECT_CWD_BINDING",
|
||||
"mandatory": true,
|
||||
"requirement": "execution layer auto-resolves project cwd via ProjectRegistry.resolve_cwd precedence; no manual cd needed",
|
||||
"status": "PASS",
|
||||
"evidence": "tool_executor._resolve_execution_cwd wired: workdir>project(resolve_cwd)>process; rune registered; LIVE A-F: rune+ext auto-binding, no leakage, worktree /mnt/workdrive/p149 wins, ghost refused pre-exe",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T00:04:36Z",
|
||||
"resolved_at": "2026-09-17T00:11:52Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G5_ZERO_NORMAL_CWD_GUARD_INTERVENTIONS",
|
||||
"mandatory": true,
|
||||
"requirement": "cwd-guard interventions during normal workflows = 0",
|
||||
"status": "PASS",
|
||||
"evidence": "0 cwd-guard interventions across all normal workflows this tranche; every project-scoped command resolved via project arg; no cd-remediation generated",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T00:04:36Z",
|
||||
"resolved_at": "2026-09-17T00:11:52Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G6_GOVERNED_ROLLBACK_API",
|
||||
"mandatory": true,
|
||||
"requirement": "genuine rollback_generation API owning lock/verify/switch/health/history; live B->A->B proof + incompatible refusal",
|
||||
"status": "PASS",
|
||||
"evidence": "scripts/rollback_generation.py rollback_generation(); 10/10 unit tests; LIVE: B->A ROLLED_BACK health green; REFUSED_NOT_ELIGIBLE + REFUSED_EXPECTED_MISMATCH no mutation; recovery via deploy_promotion",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T00:04:36Z",
|
||||
"resolved_at": "2026-09-17T00:04:36Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G7_RUNE_MEANINGFUL_STRUCTURAL_COVERAGE",
|
||||
"mandatory": true,
|
||||
"requirement": "all meaningful architectural units represented in Scaffold; A-coverage 100%, no significant UNKNOWNs",
|
||||
"status": "PASS",
|
||||
"evidence": "scripts/structural_coverage_audit.py: 237 modules classified A157/B41/C28/D11/E0; meaningful coverage 100% (113/113 src A-units with spine entities); impl ownership 100%; significant UNKNOWN 0; spine ",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T00:04:36Z",
|
||||
"resolved_at": "2026-09-17T00:37:13Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G8_MATERIAL_CHANGE_CLOSURE",
|
||||
"mandatory": true,
|
||||
"requirement": "governed construction automatically triggers material_change/revision/spec flow",
|
||||
"status": "PASS",
|
||||
"evidence": "construction_interface.close() auto-invokes _auto_material_change (rule 19Q.2); live contract BUILDER-2026-09-17T00:17:47Z closed with scope scripts/rollback_generation.py -> capability:rollback-gener",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T00:04:36Z",
|
||||
"resolved_at": "2026-09-17T00:19:32Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G9_FINAL_TEST_GATES",
|
||||
"mandatory": true,
|
||||
"requirement": "all required test/gate checks pass",
|
||||
"status": "PASS",
|
||||
"evidence": "pytest 2122 passed (x2 consecutive); ruff F821 clean; F841: 0 new (53 pre-existing, none in T21.4 files); lint ratchet PASS (BLE001 347/347, S110 75/75); pyright ratchet PASS (109/117 err, 447/454 war",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T00:04:36Z",
|
||||
"resolved_at": "2026-09-17T02:34:36Z"
|
||||
}
|
||||
],
|
||||
"pull_request": 326,
|
||||
"pr_propagation_declared": true,
|
||||
"can_accept_result": {
|
||||
"ok": false,
|
||||
"blockers": [
|
||||
"G11_RAW_SELF_AUDIT: BLOCKED \u2014 ",
|
||||
"G12_ACCEPTANCE_STATE_CONSISTENCY: BLOCKED \u2014 "
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
{
|
||||
"tranche": "T21.5",
|
||||
"purpose": "ROOT-CAUSE CONTINUATION + GUARD RETIREMENT + FINAL V1 PARITY",
|
||||
"predecessor": "store/scaffold/t21.4_gates.json (V1_RETEST_REQUIRED: 10 PASS / 2 BLOCKED)",
|
||||
"created_at": "2026-09-17T11:03:16Z",
|
||||
"gates": [
|
||||
{
|
||||
"gate_id": "G10_ZERO_MEMORY_RESULT_FAST_PATH",
|
||||
"mandatory": true,
|
||||
"requirement": "zero-result memory query continues normal INWARD directly; ZERO_RESULT_EXTRA_MODEL_CALLS=0",
|
||||
"status": "PASS",
|
||||
"evidence": "zero-result returns [] and build_context continues directly: test_zero_result_is_normal_success, test_zero_result_inward_context_has_no_extra_fields_or_calls (exactly 1 provider call, no extra fields,",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:53:19Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G11_TEACHABLE_MOMENT_FUTURE_SEAM",
|
||||
"mandatory": true,
|
||||
"requirement": "durable candidate-teachable-moment evidence seam exists; APPLICABILITY can reject irrelevant candidates; no parallel memory authority",
|
||||
"status": "PASS",
|
||||
"evidence": "mark_teachable_moment_candidate(episode_ref, trigger, summary, emit): CANDIDATE status only (\"may be worth future learning\"), 7 defined triggers incl. rollback_executed/failure_recovered/user_correcti",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:53:19Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G12_HISTORICAL_V1_PARITY",
|
||||
"mandatory": true,
|
||||
"requirement": "parity manifest reconciles all historical V1 capability families; no mandatory V1 item PARTIAL/ABSENT/UNPROVEN",
|
||||
"status": "PASS",
|
||||
"evidence": "scripts/v1_parity_manifest.py -> store/scaffold/v1_parity_manifest.json: 56 rows over all mandated capability families (capability-decision, construction, continuity, truth, operations, self-awareness",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:55:34Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G13_MATERIAL_CHANGE_AND_EXISTING_V1_REGRESSION",
|
||||
"mandatory": true,
|
||||
"requirement": "material-change closure still live; affected T21.4 proof areas re-run",
|
||||
"status": "PASS",
|
||||
"evidence": "T21.4 proof areas re-run: rollback 10/10; auto-cwd 8/8 + resolve_cwd + cwd_guard 20/20; material closure 4/4 (+T21.5 ledger-path fix for project scaffolds, regression-covered); CI wait 11/11; event_wa",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T13:29:55Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G14_FINAL_TEST_GATES",
|
||||
"mandatory": true,
|
||||
"requirement": "full established test/validation stack green",
|
||||
"status": "PASS",
|
||||
"evidence": "Full battery: pytest 2156 passed + 84 subtests; type ratchet PASS; lint ratchet PASS; Henxels exit 0; diff-cover changed-lines 94% (>=70); structural coverage re-audit: 242 modules, meaningful 100% (1",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T13:29:55Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G15_FINAL_DEPLOY_CONVERGENCE",
|
||||
"mandatory": true,
|
||||
"requirement": "git==release==deployed==runtime==healthy; queued/draining is not PASS",
|
||||
"status": "PASS",
|
||||
"evidence": "git master 26e37653 == deployed current == runtime build identity (0.83.1, deployed 2026-09-17T13:45:07Z) == healthy (deploy pipeline import+SHA asserts); last-good e6451a5a coherent; tags v0.83.0/v0.",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T16:33:46Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G16_RAW_SELF_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "all required metrics met; contradictions BLOCK",
|
||||
"status": "PASS",
|
||||
"evidence": "TWO-ERA AUDIT, both counts explicit. PRE-FIX (generator live): 7 external sleeps + ~10 model-level re-arms \u2014 each traced to the residual nonterminal-return generator (pending-class verdict string mism",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T17:14:32Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G17_ACCEPTANCE_STATE_CONSISTENCY",
|
||||
"mandatory": true,
|
||||
"requirement": "persisted GateAccounting == ledger readback == versions == runtime evidence == report verdict",
|
||||
"status": "PASS",
|
||||
"evidence": "can_accept()=TRUE against the persisted 17-gate object (see readback); Scaffolding internal 1.0.4 (machinery-revised, drift clear); capability ledger acceptance_state to be appended and read back; run",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T17:14:54Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G1_ASYNC_ROOT_CAUSE_IDENTIFIED",
|
||||
"mandatory": true,
|
||||
"requirement": "generator of T21.4 sleep/re-arm behavior identified upstream of the guard (guard = downstream)",
|
||||
"status": "PASS",
|
||||
"evidence": "ROOT CAUSES TRACED (guard=downstream): (A) ci_event_wait wait loop BREAKS on nonterminal: watchdog expiry with ci_alive()=False -> lost_event break; event-seen-authority-pending -> break after 1; boun",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:05:14Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G2_SINGLE_LOGICAL_AWAIT",
|
||||
"mandatory": true,
|
||||
"requirement": "one logical external wait = one await_operation invocation; internal re-arm; no model-level re-arm loop; live proof",
|
||||
"status": "PASS",
|
||||
"evidence": "ci_event_wait.run() rewritten: ALL nonterminal conditions (event-seen-authority-pending, eof-pending, watchdog-with-live-runs) re-arm INTERNALLY; control returns only on terminal verdict or real deadl",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:12:23Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G3_OPERATION_SPECIFIC_COMPLETION_AUTHORITY",
|
||||
"mandatory": true,
|
||||
"requirement": "each operation type has matching canonical completion authority; wrong authority can never mean forever-pending; tests + live workflows",
|
||||
"status": "PASS",
|
||||
"evidence": "OPERATION_SPECS registry: branch_ci/pull_request_checks -> ci_runs authority (own Actions runs); master_ci/deploy/release -> deploy_qualify. Unknown op_type refused upfront. Tests: branch op NEVER con",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:12:23Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G4_RUNTIME_WAIT_GUARD_RETIRED",
|
||||
"mandatory": true,
|
||||
"requirement": "runtime wait-interception removed from MiMo hook + install/sync paths; RUNTIME_WAIT_GUARD_PRESENT=false; prevention lives in tests",
|
||||
"status": "PASS",
|
||||
"evidence": "config/mimo-hooks/event-wait-guard.ts rewritten as documented no-op (EventWaitGuard+CwdGuard RETIRED, history+test pointers in source); install_mimo_hook.py synced; hook parity/drift tests 6/6 green; ",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:24:22Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G5_CANONICAL_WAIT_RESUME_ROOT",
|
||||
"mandatory": true,
|
||||
"requirement": "ONE wait/resume root authority shared by daemon/library/tool/CI writers; daemon restart resolves it automatically; WAIT_RESUME_ROOT_AUTHORITIES=1, SPLIT_BRAIN_EVENT_STORES=0",
|
||||
"status": "PASS",
|
||||
"evidence": "scripts/wr_root_resolver.py = SINGLE authority (env pin > canonical /mnt/workdrive/wr-bridge > legacy; split-brain fails loudly); event_wait.wr_root() delegates; scripts/start_wr_daemon.py resolves on",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:24:22Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G6_COMMON_EXECUTION_CONTEXT_BINDING",
|
||||
"mandatory": true,
|
||||
"requirement": "project/worktree context bound at the common execution boundary; no false project-mutation classification of read-only commands",
|
||||
"status": "PASS",
|
||||
"evidence": "Common boundary = tool_executor._resolve_execution_cwd (T21.4, re-proven T21.5): workdir > ProjectRegistry.resolve_cwd(project) > process; read-only commands with deployed src/rune paths execute norma",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:26:28Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G7_RUNTIME_CWD_GUARD_RETIRED",
|
||||
"mandatory": true,
|
||||
"requirement": "runtime CwdGuard retired after common-boundary fix; manual project-context cd=0; wrong-cwd execution=0",
|
||||
"status": "PASS",
|
||||
"evidence": "CwdGuard in installed MiMo hook is a documented no-op (retired with wait guard, same canonical file, parity green). LIVE PROBE: repo-touching git command with NO cd marker and NO workdir executed WITH",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:26:28Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G8_NEW_SCAFFOLD_STRUCTURAL_AWARENESS",
|
||||
"mandatory": true,
|
||||
"requirement": "governed adoption auto-registers in Registry + Spine; post-change auto structural update; manual registration steps=0",
|
||||
"status": "PASS",
|
||||
"evidence": "construction_activation._auto_structural_awareness wired into PROJECT_BUILD bootstrap (failure-tolerant, boundary-preserving). LIVE+TESTS (test_t215_awareness.py 3/3): adoption of disposable widget-fo",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:49:08Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G9_MEMORY_INWARD_PORT",
|
||||
"mandatory": true,
|
||||
"requirement": "future-facing Memory port real, wired into existing INWARD, independently replaceable/mockable; no new storage",
|
||||
"status": "PASS",
|
||||
"evidence": "scripts/research_memory_port.py: provider contract (single callable, set_memory_provider/env-bindable, independently replaceable/mockable); wired as ONE port of call inside existing research_inward_co",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:53:19Z"
|
||||
}
|
||||
],
|
||||
"can_accept_result": {
|
||||
"ok": true,
|
||||
"blockers": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
{
|
||||
"tranche": "T21.5",
|
||||
"rows": [
|
||||
{
|
||||
"family": "capability-decision",
|
||||
"item": "capability discovery",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "src/rune/capability_fit.py",
|
||||
"authority": "deterministic candidates over structural docs",
|
||||
"evidence": "tests/test_capability_fit.py; T21.5 live: EXACT fit on widget-forge component",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "capability-decision",
|
||||
"item": "fit/gap detection",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/capability_fit.py evaluate_fit",
|
||||
"authority": "EXACT/PARTIAL/INCOMPATIBLE/UNKNOWN",
|
||||
"evidence": "tests/test_capability_fit.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "capability-decision",
|
||||
"item": "reuse/configure/compose decisions",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/capability_fit.py decide",
|
||||
"authority": "RESOLUTIONS set",
|
||||
"evidence": "tests/test_construction_decision.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "capability-decision",
|
||||
"item": "adapt/acquire decisions",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/capability_fit.py decide (ACQUIRE_EXTERNAL, external_evidence)",
|
||||
"authority": "evidence-gated acquisition",
|
||||
"evidence": "tests/test_construction_decision.py::test_g_acquire_with_external_evidence",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "capability-decision",
|
||||
"item": "build decision (never silent)",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/capability_fit.py decide; construction_decision merge",
|
||||
"authority": "BUILD_NEW never silently chosen",
|
||||
"evidence": "tests/test_construction_decision.py::test_h_insufficient_never_build_new",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "capability-decision",
|
||||
"item": "toolchain resolution",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/toolchain.py",
|
||||
"authority": "evidence-backed resolution + decision records",
|
||||
"evidence": "tests/test_toolchain*.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "construction",
|
||||
"item": "construction contracts (lifecycle)",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "src/rune/scaffold.py contracts; construction_interface",
|
||||
"authority": "phase machine draft\u2192closed",
|
||||
"evidence": "37 contracts in store/scaffold/current.json; T21.4/T21.5 live closes",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "construction",
|
||||
"item": "approval/consent",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "construction_interface transition gates; builder cannot self-certify",
|
||||
"authority": "approval-gated transitions",
|
||||
"evidence": "tests/test_construction_interface.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "construction",
|
||||
"item": "scope + expansion (user-gated)",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "construction_interface amend_scope; check_scope",
|
||||
"authority": "scope verdict green/red/unknown",
|
||||
"evidence": "tests/test_construction_interface.py; T21.4 live scope green",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "construction",
|
||||
"item": "Project Scaffold",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "src/rune/scaffold.py Scaffold",
|
||||
"authority": "per-project structural truth",
|
||||
"evidence": "T21.5 live: widget-forge bootstrap; store/scaffold/projects/",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "construction",
|
||||
"item": "construction patterns",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/construction_pattern.py + builtin",
|
||||
"authority": "evidence-grounded pattern obligations",
|
||||
"evidence": "tests/test_construction_pattern*.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "construction",
|
||||
"item": "layout authority",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/layout_contract.py",
|
||||
"authority": "persistent UI layout checks",
|
||||
"evidence": "tests/test_layout_contract.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "construction",
|
||||
"item": "Research Core / Build Basis",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/build_basis.py; scripts/research_*",
|
||||
"authority": "durable versioned research basis",
|
||||
"evidence": "tests/test_build_basis.py; research stage tests",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "construction",
|
||||
"item": "predicted/descriptive impact",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/impact_preview.py; graph_projection",
|
||||
"authority": "impact preview + frontier",
|
||||
"evidence": "tests/test_impact_preview.py; test_v1_workspace_forge.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "construction",
|
||||
"item": "migration",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/structural_migration.py",
|
||||
"authority": "consent-gated schema migration",
|
||||
"evidence": "tests/test_structural_migration.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "construction",
|
||||
"item": "supersession",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/structural_store.py supersede; capability_ledger supersede",
|
||||
"authority": "identity survives replacement",
|
||||
"evidence": "tests/test_v19_ledgers.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "construction",
|
||||
"item": "retirement",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/capability_ledger.py retire",
|
||||
"authority": "governed retirement lifecycle",
|
||||
"evidence": "T17 retirement proof (complexity_scorer); tests",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "construction",
|
||||
"item": "rollback (deploy generations)",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "scripts/rollback_generation.py",
|
||||
"authority": "governed rollback_generation API",
|
||||
"evidence": "T21.4 live B->A->B + refusals; tests/test_rollback_generation.py 10/10",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "continuity",
|
||||
"item": "durable/resumable construction",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "construction_interface discover_active_contract; t14/t15 drivers",
|
||||
"authority": "fresh-process resume from contract id",
|
||||
"evidence": "tests/test_construction_*; T7 live subprocess resume",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "continuity",
|
||||
"item": "event continuation",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "src/rune/event_wait.py; scripts/ci_event_wait.py",
|
||||
"authority": "event=wake, authority=truth, single logical await",
|
||||
"evidence": "T21.5 live workflows + tests/test_single_logical_await.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "continuity",
|
||||
"item": "fresh-context reconstruction",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "store ledgers + context packets",
|
||||
"authority": "no transcript needed",
|
||||
"evidence": "T21.4 fresh-worker challenge 6/6; T21.5 fresh-process queries",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "continuity",
|
||||
"item": "Registry (project discovery)",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "src/rune/project_registry.py",
|
||||
"authority": "discovery/index authority",
|
||||
"evidence": "tests/test_resolve_cwd.py etc.; T21.5 auto-registration",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "continuity",
|
||||
"item": "living specs / ledgers",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "src/rune/capability_ledger.py (attach_spec/revise_spec/drift)",
|
||||
"authority": "machinery-verified versions",
|
||||
"evidence": "T21.4/T21.5 live revise_spec flows; tests/test_living_specs.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "continuity",
|
||||
"item": "maintenance items",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "store/scaffold entities maintenance:*; scripts/file_drift_issue.py",
|
||||
"authority": "deduped drift issues",
|
||||
"evidence": "maintenance:contract-gen-affected-surface resolved (T5)",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "truth",
|
||||
"item": "independent verification",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "construction verify + transition evidence gates",
|
||||
"authority": "builder cannot self-certify",
|
||||
"evidence": "tests/test_construction_interface.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "truth",
|
||||
"item": "behaviour/test ledger",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/behaviour_ledger.py",
|
||||
"authority": "canonical behaviour specs",
|
||||
"evidence": "tests/test_v19_ledgers.py; behaviour:deploy-refuses-red",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "truth",
|
||||
"item": "quicksand/selective invalidation",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/iteration.py; behaviour ledger harden state",
|
||||
"authority": "selective retest obligations",
|
||||
"evidence": "tests/test_v19_ledgers.py; T19 proofs",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "truth",
|
||||
"item": "automated-green != user-confirmed",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "contract_quality + closure observed_evidence gates",
|
||||
"authority": "green CI never self-certifies closure",
|
||||
"evidence": "tests/test_contract_quality.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "truth",
|
||||
"item": "structural/spec/version drift",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "src/rune/capability_ledger.py spec_drift_status",
|
||||
"authority": "deterministic revision-based drift",
|
||||
"evidence": "T21.4 live drift->revise cycle; tests/test_spec_drift_deterministic.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "truth",
|
||||
"item": "schema migration consent",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/structural_migration.py; project_registry.needs_migration",
|
||||
"authority": "18I consent sweep",
|
||||
"evidence": "tests/test_structural_migration.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "truth",
|
||||
"item": "predicted-vs-observed outcome",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/structural_store.py predicted_vs_observed",
|
||||
"authority": "recorded mismatch as evidence",
|
||||
"evidence": "tests + T21.4 contract_quality close",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "operations",
|
||||
"item": "workspace/worktrees",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/project_registry.py active_worktree precedence",
|
||||
"authority": "worktree > repo",
|
||||
"evidence": "tests/test_resolve_cwd.py; T21.4 live worktree proof",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "operations",
|
||||
"item": "Git authority",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "repo git usage via ancestry/promotion",
|
||||
"authority": "git ancestry = ordering authority",
|
||||
"evidence": "scripts/deploy_promotion.py ancestry_of; live promotions",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "operations",
|
||||
"item": "Forge/Gitea projection",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/forge.py; forge_client.py; scripts/gitea_*",
|
||||
"authority": "minimum stable forge boundary",
|
||||
"evidence": "tests + live PR/issue automation",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "operations",
|
||||
"item": "CI",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": ".gitea/workflows/ci.yml; scripts/ci_event_wait.py",
|
||||
"authority": "two-leg qualification; event-driven waits",
|
||||
"evidence": "live runs 8700/8722 green; T21.5 single-await",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "operations",
|
||||
"item": "release",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "scripts/create_release.py; semver_bump.py; auto_bump_pr.py",
|
||||
"authority": "protected-branch bump flow",
|
||||
"evidence": "releases v0.82.0-0.82.3 created live",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "operations",
|
||||
"item": "deploy",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "scripts/deploy_stage/qualify/promotion.py",
|
||||
"authority": "monotonic fail-closed promotion",
|
||||
"evidence": "live deploys 40b3731..c3a0a48; stale-refusal fired in production",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "operations",
|
||||
"item": "rollback",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "scripts/rollback_generation.py",
|
||||
"authority": "governed regression API",
|
||||
"evidence": "T21.4 live B->A->B",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "operations",
|
||||
"item": "runtime generations",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "_build_identity.json authority; rune-code current/last-good",
|
||||
"authority": "identity readback = runtime truth",
|
||||
"evidence": "T21.4 rollback verification; deployed coherence checks",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "operations",
|
||||
"item": "provider neutrality",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "src/rune/model_routing.py; provider_registry.py",
|
||||
"authority": "one router, capability floor",
|
||||
"evidence": "tests/test_model_routing*; canary_p1_06",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "operations",
|
||||
"item": "secrets",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "scripts/gitea_auth.py credential ladder; henxels no-secrets gate",
|
||||
"authority": "no tracked secrets",
|
||||
"evidence": "henxels_checks.py CI gate; credential helper",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "self-awareness",
|
||||
"item": "Spine authority",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "src/rune/spine.py",
|
||||
"authority": "Rune structural self-model",
|
||||
"evidence": "store/spine/current.json; deterministic builds",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "self-awareness",
|
||||
"item": "Registry discovery",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "src/rune/project_registry.py",
|
||||
"authority": "project index",
|
||||
"evidence": "live registry reads across tranches",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "self-awareness",
|
||||
"item": "Project Scaffold authority",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "src/rune/scaffold.py",
|
||||
"authority": "project structural truth",
|
||||
"evidence": "live project scaffolds",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "self-awareness",
|
||||
"item": "Rune-wide meaningful structural coverage",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "scripts/structural_coverage_audit.py",
|
||||
"authority": "A/B/C/D/E classification",
|
||||
"evidence": "T21.4: 237 modules, 100% meaningful coverage, 0 unknowns",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "self-awareness",
|
||||
"item": "automatic new-Scaffold awareness",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "src/rune/construction_activation.py _auto_structural_awareness",
|
||||
"authority": "bootstrap auto-registers",
|
||||
"evidence": "T21.5 live + tests/test_t215_awareness.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "self-awareness",
|
||||
"item": "automatic living structural update",
|
||||
"classification": "IMPLEMENTED_LIVE",
|
||||
"module": "construction_interface close() -> _auto_material_change",
|
||||
"authority": "material closure without manual sync",
|
||||
"evidence": "T21.4/T21.5 live; tests/test_t214_material_closure.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "learning-seam",
|
||||
"item": "candidate teachable-moment seam",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "scripts/research_memory_port.py mark_teachable_moment_candidate",
|
||||
"authority": "CANDIDATE evidence via existing channels",
|
||||
"evidence": "tests/test_memory_port.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "learning-seam",
|
||||
"item": "Memory internal-experience port in INWARD",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "scripts/research_memory_port.py; research_inward_context.build_context",
|
||||
"authority": "one bounded port of call",
|
||||
"evidence": "tests/test_memory_port.py",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "learning-seam",
|
||||
"item": "APPLICABILITY governs transfer",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "research_memory_port.applicability_filter + existing APPLICABILITY stage",
|
||||
"authority": "irrelevant candidates rejected",
|
||||
"evidence": "tests/test_memory_port.py::test_applicability_rejects_irrelevant_candidate",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "learning-seam",
|
||||
"item": "zero-result fast path",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "research_memory_port.memory_port_of_call",
|
||||
"authority": "[] -> next normal step",
|
||||
"evidence": "tests/test_memory_port.py; ZERO_RESULT_EXTRA_MODEL_CALLS=0",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "learning-seam",
|
||||
"item": "no competing memory authority",
|
||||
"classification": "IMPLEMENTED_TESTED",
|
||||
"module": "port design (provider delegation, no storage)",
|
||||
"authority": "Memory stays single future authority",
|
||||
"evidence": "port creates no store/db; provider contract only",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "deferred",
|
||||
"item": "automatic Growth Delta pattern learning",
|
||||
"classification": "DEFERRED_NON_V1",
|
||||
"module": "src/rune/growth_delta.py (existing, non-V1 scope)",
|
||||
"authority": "post-V1 learning loop",
|
||||
"evidence": "V1 excludes Memory/Substrate/Growth Delta by tranche mandate; the T21.5 seam provides the harvest point without implementing the loop",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "deferred",
|
||||
"item": "Memory V1 system itself",
|
||||
"classification": "DEFERRED_NON_V1",
|
||||
"module": "(future Memory T0 archaeology)",
|
||||
"authority": "dedicated post-V1 phase",
|
||||
"evidence": "explicitly excluded by T21.5 mandate \u00a70; only the INWARD port is V1",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "deferred",
|
||||
"item": "Substrate",
|
||||
"classification": "DEFERRED_NON_V1",
|
||||
"module": "(future)",
|
||||
"authority": "post-V1",
|
||||
"evidence": "explicitly excluded by tranche mandate",
|
||||
"gap": ""
|
||||
},
|
||||
{
|
||||
"family": "deferred",
|
||||
"item": "TUI redesign",
|
||||
"classification": "DEFERRED_NON_V1",
|
||||
"module": "(existing rune_tui.py adequate)",
|
||||
"authority": "explicitly excluded by tranche mandate",
|
||||
"evidence": "TUI work explicitly out of scope for V1 Scaffolding",
|
||||
"gap": ""
|
||||
}
|
||||
],
|
||||
"counts": {
|
||||
"IMPLEMENTED_LIVE": 21,
|
||||
"IMPLEMENTED_TESTED": 31,
|
||||
"DEFERRED_NON_V1": 4
|
||||
},
|
||||
"blocking_items": []
|
||||
}
|
||||
+877
-152
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,120 @@
|
||||
"""test_auto_execution_context.py — T21.4 §21.4E: automatic project
|
||||
execution context.
|
||||
|
||||
The model names the PROJECT; the execution layer resolves cwd through
|
||||
ProjectRegistry.resolve_cwd (repo wins over scaffold; active worktree wins
|
||||
over repo). Unknown projects are refused BEFORE execution. No manual `cd`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_SRC = str(Path(__file__).resolve().parent.parent / "src")
|
||||
if _SRC not in sys.path:
|
||||
sys.path.insert(0, _SRC)
|
||||
|
||||
from rune import tool_executor as te
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def two_projects(tmp_path, monkeypatch):
|
||||
"""rune @ repo-a (with an active worktree seam) + ext @ repo-b."""
|
||||
store = tmp_path / "store" / "scaffold"
|
||||
store.mkdir(parents=True)
|
||||
repo_a = tmp_path / "repo-a"
|
||||
repo_b = tmp_path / "repo-b"
|
||||
worktree_a = tmp_path / "wt-a"
|
||||
for p in (repo_a, repo_b, worktree_a):
|
||||
p.mkdir()
|
||||
reg = {"schema_version": 1, "updated_at": "t", "projects": {
|
||||
"rune": {
|
||||
"project_id": "rune",
|
||||
"scaffold_location": str(tmp_path / "scaffold-a"), # NOT the repo
|
||||
"repo_identity": f"git:{repo_a}",
|
||||
"active_worktree": "",
|
||||
},
|
||||
"ext": {
|
||||
"project_id": "ext",
|
||||
"scaffold_location": str(tmp_path / "scaffold-b"),
|
||||
"repo_identity": f"git:{repo_b}",
|
||||
"active_worktree": "",
|
||||
},
|
||||
}}
|
||||
(store / "project_registry.json").write_text(json.dumps(reg))
|
||||
monkeypatch.setenv("RUNE_SCAFFOLD_STORE", str(store))
|
||||
return {"repo_a": repo_a, "repo_b": repo_b, "worktree_a": worktree_a,
|
||||
"store": store, "reg": reg}
|
||||
|
||||
|
||||
def _pwd(args: dict) -> dict:
|
||||
# _run_command directly: the approval flow (prepare/approve) wraps it
|
||||
# in production and is exercised by the tool-authority tests.
|
||||
return te._run_command({"command": "pwd", **args})
|
||||
|
||||
|
||||
# A. known project → executes in its repo automatically
|
||||
def test_rune_auto_binding(two_projects):
|
||||
r = _pwd({"project": "rune"})
|
||||
assert r["success"] and r["cwd_source"] == "project_registry"
|
||||
assert r["result"].strip() == str(two_projects["repo_a"])
|
||||
|
||||
|
||||
# B. disposable external project → executes in ITS repo
|
||||
def test_external_auto_binding(two_projects):
|
||||
r = _pwd({"project": "ext"})
|
||||
assert r["result"].strip() == str(two_projects["repo_b"])
|
||||
|
||||
|
||||
# C. switch rune → ext → rune: no cwd leakage
|
||||
def test_switch_no_leakage(two_projects):
|
||||
r1 = _pwd({"project": "rune"})
|
||||
r2 = _pwd({"project": "ext"})
|
||||
r3 = _pwd({"project": "rune"})
|
||||
assert r1["result"].strip() == str(two_projects["repo_a"])
|
||||
assert r2["result"].strip() == str(two_projects["repo_b"])
|
||||
assert r3["result"].strip() == str(two_projects["repo_a"])
|
||||
|
||||
|
||||
# D. active worktree wins over repo root
|
||||
def test_worktree_precedence(two_projects):
|
||||
store = two_projects["store"]
|
||||
reg = json.loads((store / "project_registry.json").read_text())
|
||||
reg["projects"]["rune"]["active_worktree"] = str(two_projects["worktree_a"])
|
||||
(store / "project_registry.json").write_text(json.dumps(reg))
|
||||
r = _pwd({"project": "rune"})
|
||||
assert r["result"].strip() == str(two_projects["worktree_a"])
|
||||
|
||||
|
||||
# E. unknown project mutation refused BEFORE execution
|
||||
def test_unknown_project_refused(two_projects):
|
||||
r = te._run_command({
|
||||
"command": "touch /tmp/pwned-marker-t214", "project": "ghost"})
|
||||
assert not r["success"]
|
||||
assert "refused" in r["error"].lower()
|
||||
assert not Path("/tmp/pwned-marker-t214").exists()
|
||||
|
||||
|
||||
# F. repo path wins over scaffold path (scaffold is never an execution cwd)
|
||||
def test_repo_wins_over_scaffold(two_projects):
|
||||
r = _pwd({"project": "rune"})
|
||||
assert r["result"].strip() == str(two_projects["repo_a"])
|
||||
assert r["result"].strip() != str(two_projects["reg"]["projects"]["rune"]["scaffold_location"])
|
||||
|
||||
|
||||
# explicit governed workdir outranks project resolution
|
||||
def test_explicit_workdir_precedence(two_projects):
|
||||
other = two_projects["repo_a"].parent / "governed-dir"
|
||||
other.mkdir()
|
||||
r = _pwd({"workdir": str(other), "project": "rune"})
|
||||
assert r["result"].strip() == str(other)
|
||||
assert r["cwd_source"] == "explicit"
|
||||
|
||||
|
||||
# project-less commands keep process cwd (nothing to bind)
|
||||
def test_project_less_uses_process_cwd(two_projects):
|
||||
r = te._run_command({"command": "pwd"})
|
||||
assert r["success"] and r["cwd_source"] == "process"
|
||||
@@ -0,0 +1,197 @@
|
||||
"""test_await_intent.py — ASYNC CORRECTIVE PATCH test matrix (§12).
|
||||
|
||||
MODEL OUTPUT = INTENT. RUNE CONTROLLER = EXECUTION SEMANTICS.
|
||||
Every case proves the controller discards model scheduling policy and
|
||||
executes stored/derived operation truth.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
|
||||
import await_intent as ai # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated(monkeypatch, tmp_path):
|
||||
"""Isolate from the live bridge: stub wait records + git."""
|
||||
monkeypatch.setattr(ai, "WR_BRIDGE", tmp_path / "wr")
|
||||
waits = tmp_path / "wr" / "waits"
|
||||
waits.mkdir(parents=True)
|
||||
monkeypatch.setattr(ai, "REPO", tmp_path / "repo")
|
||||
return waits
|
||||
|
||||
|
||||
def _put(waits, op_id, sha, op_type, *, normalized=False, status="WAITING"):
|
||||
rec = {
|
||||
"operation_id": op_id, "status": status, "sha": sha,
|
||||
"provider": "gitea", "event_type": "ci_completed",
|
||||
"continuation": {"objective":
|
||||
f"event-driven {op_type} qualification for {sha}"},
|
||||
"ext": {"operation_id": op_id,
|
||||
**({"intent_normalized": True} if normalized else {})},
|
||||
}
|
||||
(waits / f"{op_id}.json").write_text(json.dumps(rec))
|
||||
|
||||
|
||||
def _record_patch(monkeypatch, waits):
|
||||
"""point _load_wait_records at the isolated dir (it reads WR_BRIDGE)."""
|
||||
# _load_wait_records reads ai.WR_BRIDGE which the fixture already patched.
|
||||
|
||||
|
||||
# ── A. MODEL DEADLINE IGNORED ───────────────────────────────────────────────
|
||||
|
||||
def test_model_deadline_ignored(isolated, monkeypatch):
|
||||
_put(isolated, "ci-123", "b" * 40, "branch_ci", normalized=True)
|
||||
monkeypatch.setattr(ai, "open_pr_for_sha", lambda sha: None)
|
||||
monkeypatch.setattr(ai, "sha_on_master", lambda sha: False)
|
||||
r = ai.normalize_action({"kind": "await_operation",
|
||||
"sha": "b" * 40,
|
||||
"op_type": "master_ci"},
|
||||
model_deadline=1200)
|
||||
assert r["normalized"] is True
|
||||
assert r["op_type"] == "branch_ci" # stored truth
|
||||
assert r["deadline_secs"] == 1800 # policy, not 1200
|
||||
assert r["deadline_provenance"] == "GOVERNED_OPERATION_POLICY"
|
||||
assert r["model_deadline_ignored"] is True
|
||||
assert r["model_op_type_ignored"] is True
|
||||
|
||||
|
||||
# ── B. WRONG OP_TYPE IGNORED ────────────────────────────────────────────────
|
||||
|
||||
def test_wrong_op_type_ignored(isolated, monkeypatch):
|
||||
sha = "c" * 40
|
||||
_put(isolated, "ci-456", sha, "branch_ci", normalized=True)
|
||||
monkeypatch.setattr(ai, "open_pr_for_sha", lambda s: None)
|
||||
monkeypatch.setattr(ai, "sha_on_master", lambda s: False)
|
||||
r = ai.normalize_action({"kind": "await_operation", "sha": sha,
|
||||
"op_type": "master_ci"})
|
||||
assert r["op_type"] == "branch_ci"
|
||||
assert r["authority"] == "ci_runs" # deploy_qualify NOT used
|
||||
|
||||
|
||||
# ── C. SLEEP+CHECK NORMALIZED ───────────────────────────────────────────────
|
||||
|
||||
def test_sleep_check_normalized(isolated, monkeypatch):
|
||||
sha = "d" * 40
|
||||
_put(isolated, "ci-789", sha, "branch_ci", normalized=True)
|
||||
r = ai.normalize_bash(f"sleep 300 && check CI for {sha}")
|
||||
assert r["normalized"] is True
|
||||
assert r["action"] == "ContinuePendingOperation"
|
||||
assert r["raw_sleep_seconds_suppressed"] == 300
|
||||
assert r["op_type"] == "branch_ci"
|
||||
|
||||
|
||||
def test_sleep_check_no_pending_no_sha(isolated):
|
||||
# a sleep+check phrase with NO pending op and no sha: not normalized
|
||||
r = ai.normalize_bash("sleep 300 && check CI")
|
||||
assert r is None or r.get("normalized") in (True, "AMBIGUOUS")
|
||||
|
||||
|
||||
# ── E. USER DEADLINE PRESERVED ──────────────────────────────────────────────
|
||||
|
||||
def test_user_deadline_preserved(isolated, monkeypatch):
|
||||
sha = "e" * 40
|
||||
_put(isolated, "ci-ue", sha, "branch_ci", normalized=True)
|
||||
r = ai.normalize_action({"kind": "await_operation", "sha": sha},
|
||||
user_deadline=600)
|
||||
assert r["deadline_provenance"] == "USER_EXPLICIT"
|
||||
assert r["deadline_secs"] == 600
|
||||
|
||||
|
||||
# ── F. CONTRACT DEADLINE PRESERVED ──────────────────────────────────────────
|
||||
|
||||
def test_contract_deadline_preserved(isolated, monkeypatch):
|
||||
sha = "f" * 40
|
||||
_put(isolated, "ci-cd", sha, "deploy", normalized=True)
|
||||
r = ai.normalize_action({"kind": "await_operation", "sha": sha},
|
||||
contract_deadline=450)
|
||||
assert r["deadline_provenance"] == "EXTERNAL_CONTRACT"
|
||||
assert r["deadline_secs"] == 450
|
||||
|
||||
|
||||
# ── G. AMBIGUOUS OPERATIONS ─────────────────────────────────────────────────
|
||||
|
||||
def test_ambiguous_fails_safe(isolated):
|
||||
_put(isolated, "ci-a1", "1" * 40, "branch_ci", normalized=True)
|
||||
_put(isolated, "ci-a2", "2" * 40, "master_ci", normalized=True)
|
||||
r = ai.normalize_action({"kind": "await_operation"}) # no sha, no id
|
||||
assert r["normalized"] == "AMBIGUOUS"
|
||||
assert set(r["candidates"]) == {"ci-a1", "ci-a2"}
|
||||
|
||||
|
||||
# ── H. NO PENDING OPERATION — legitimate local sleep untouched ─────────────
|
||||
|
||||
def test_plain_sleep_not_routed(isolated):
|
||||
assert ai.normalize_bash("sleep 5") is None
|
||||
assert ai.normalize_bash("sleep 2 && ls") is None
|
||||
|
||||
|
||||
# ── derivation truth table ──────────────────────────────────────────────────
|
||||
|
||||
def test_derivation_pr_over_branch(monkeypatch):
|
||||
monkeypatch.setattr(ai, "open_pr_for_sha", lambda s: 42)
|
||||
monkeypatch.setattr(ai, "sha_on_master", lambda s: False)
|
||||
t, ev = ai.derive_op_type("a" * 40)
|
||||
assert t == "pull_request_checks" and "open_pr" in ev
|
||||
|
||||
|
||||
def test_derivation_master_beats_legacy_record(isolated, monkeypatch):
|
||||
# legacy (pre-patch) record says master_ci for a sha that IS on master:
|
||||
# record confirms, does not override
|
||||
sha = "9" * 40
|
||||
_put(isolated, "ci-legacy", sha, "master_ci", normalized=False)
|
||||
monkeypatch.setattr(ai, "sha_on_master", lambda s: True)
|
||||
t, ev = ai.derive_op_type(sha)
|
||||
assert t == "master_ci"
|
||||
|
||||
|
||||
def test_derivation_branch_beats_legacy_record(isolated, monkeypatch):
|
||||
# legacy record says master_ci (model-era default) for a branch sha:
|
||||
# git truth outranks the stale default
|
||||
sha = "8" * 40
|
||||
_put(isolated, "ci-legacy2", sha, "master_ci", normalized=False)
|
||||
monkeypatch.setattr(ai, "open_pr_for_sha", lambda s: None)
|
||||
monkeypatch.setattr(ai, "sha_on_master", lambda s: False)
|
||||
t, ev = ai.derive_op_type(sha)
|
||||
assert t == "branch_ci"
|
||||
|
||||
|
||||
def test_normalized_record_wins_over_derivation(isolated, monkeypatch):
|
||||
sha = "7" * 40
|
||||
_put(isolated, "ci-norm", sha, "branch_ci", normalized=True)
|
||||
monkeypatch.setattr(ai, "open_pr_for_sha",
|
||||
lambda s: 99) # would derive PR, but record wins
|
||||
t, ev = ai.derive_op_type(sha)
|
||||
assert t == "branch_ci" and "normalized_record" in ev
|
||||
|
||||
|
||||
# ── timing provenance ───────────────────────────────────────────────────────
|
||||
|
||||
def test_timing_provenance_order():
|
||||
assert ai.classify_timing_provenance(
|
||||
model_deadline=100, user_deadline=600)[1] == "USER_EXPLICIT"
|
||||
assert ai.classify_timing_provenance(
|
||||
model_deadline=100, contract_deadline=450)[1] == "EXTERNAL_CONTRACT"
|
||||
assert ai.classify_timing_provenance(model_deadline=100)[1] == \
|
||||
"MODEL_INVENTED"
|
||||
|
||||
|
||||
# ── observability ───────────────────────────────────────────────────────────
|
||||
|
||||
def test_observability_record(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(ai, "_SCRIPT_DIR", tmp_path)
|
||||
rec = {"normalized": True, "action": "ContinuePendingOperation",
|
||||
"op_type": "branch_ci", "model_deadline_ignored": True}
|
||||
ai.emit_normalized_record(rec)
|
||||
log = tmp_path.parent / "store" / "await_intent_log.jsonl"
|
||||
# _SCRIPT_DIR is tmp_path; parent/store = tmp/store
|
||||
assert log.exists()
|
||||
last = json.loads(log.read_text().splitlines()[-1])
|
||||
assert last["model_deadline_ignored"] is True
|
||||
@@ -151,16 +151,18 @@ def test_16d_lost_event_with_no_verdict_fails_truthfully(
|
||||
|
||||
def test_16d_ci_alive_rearm_then_event(wr_daemon, monkeypatch):
|
||||
mod = _load_ci_wait_module()
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda: "success")
|
||||
# first watchdog expiry (t=3s): CI runs still non-terminal → re-arm,
|
||||
# not fail; the event arrives only AFTER that expiry (t≥4s)
|
||||
# first watchdog expiry (t=3s): authority pending (lost event, no
|
||||
# runs) → ONE bounded re-read, then truthful recovery; the event
|
||||
# arrives only AFTER that expiry (t≥4s)
|
||||
alive = {"n": 0}
|
||||
|
||||
def flaky_alive():
|
||||
def flaky_alive(op_type, sha):
|
||||
alive["n"] += 1
|
||||
return alive["n"] <= 1 # alive on first expiry, gone after event
|
||||
|
||||
monkeypatch.setattr(mod, "ci_alive", flaky_alive)
|
||||
monkeypatch.setattr(mod, "op_ci_alive", flaky_alive)
|
||||
monkeypatch.setattr(mod, "op_authority_verdict",
|
||||
lambda t, sha: "success")
|
||||
|
||||
def post_late():
|
||||
deadline = time.time() + 10
|
||||
|
||||
+30
-13
@@ -71,19 +71,36 @@ class Daemon:
|
||||
WR_WATCH_SECS="30",
|
||||
WR_TERM_GRACE_SECS="1",
|
||||
RUNE_WR_ROOT=str(tmp_root))
|
||||
self.proc = subprocess.Popen(
|
||||
[sys.executable, str(WR / "daemon.py"), str(port)],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, env=env)
|
||||
deadline = time.time() + 15
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{port}/health", timeout=2) as r:
|
||||
if r.status == 200:
|
||||
return
|
||||
except Exception:
|
||||
time.sleep(0.2)
|
||||
# T21.5 hardening: under heavy parallel load the first start may
|
||||
# lose the bind race or the health probe may exceed its window.
|
||||
# Retry once on a FRESH ephemeral port before failing — the
|
||||
# fixture must be robust, not flaky.
|
||||
import socket as _socket
|
||||
for attempt in range(2):
|
||||
if attempt == 1:
|
||||
with _socket.socket() as _s:
|
||||
_s.bind(("127.0.0.1", 0))
|
||||
self.port = _s.getsockname()[1]
|
||||
self.proc = subprocess.Popen(
|
||||
[sys.executable, str(WR / "daemon.py"), str(self.port)],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
text=True, env={**env, "WR_ROOT": str(tmp_root),
|
||||
"RUNE_WR_ROOT": str(tmp_root)})
|
||||
deadline = time.time() + 15
|
||||
healthy = False
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
with urllib.request.urlopen(
|
||||
f"http://127.0.0.1:{self.port}/health",
|
||||
timeout=2) as r:
|
||||
if r.status == 200:
|
||||
healthy = True
|
||||
break
|
||||
except Exception:
|
||||
time.sleep(0.2)
|
||||
if healthy:
|
||||
return
|
||||
self.stop()
|
||||
raise RuntimeError("daemon did not become healthy")
|
||||
|
||||
def url(self) -> str:
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
"""test_memory_port.py — T21.5 §10-12 (G9/G10/G11).
|
||||
|
||||
G9 Memory port: real, wired into existing INWARD, replaceable/mockable.
|
||||
G10 zero-result fast path: [] → next normal INWARD step directly;
|
||||
ZERO_RESULT_EXTRA_MODEL_CALLS = 0.
|
||||
G11 teachable-moment future seam: candidates only; APPLICABILITY governs
|
||||
transfer; irrelevant candidates never contaminate SYNTHESIS.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
import research_memory_port as rmp # noqa: E402
|
||||
import research_inward_context as ric # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_provider():
|
||||
rmp.set_memory_provider(None)
|
||||
yield
|
||||
rmp.set_memory_provider(None)
|
||||
|
||||
|
||||
def _issue():
|
||||
return {"title": "Widget export drops unicode",
|
||||
"body": "widget_export.py fails on unicode names in bundles"}
|
||||
|
||||
|
||||
def _provider_result(candidates):
|
||||
def provider(query_text, keywords, *, limit=5):
|
||||
return candidates[:limit]
|
||||
return provider
|
||||
|
||||
|
||||
# ── G9: port real, wired, mockable ─────────────────────────────────────────
|
||||
|
||||
def test_port_unbound_returns_empty_fast():
|
||||
assert rmp.memory_port_of_call(_issue(), ["widget", "export"]) == []
|
||||
|
||||
|
||||
def test_port_retrieves_candidates():
|
||||
seen = {}
|
||||
|
||||
def provider(query_text, keywords, *, limit=5):
|
||||
seen["query"] = query_text
|
||||
seen["keywords"] = keywords
|
||||
return [{"source": "memory", "kind": "construction_episode",
|
||||
"summary": "widget_export unicode bug fixed before",
|
||||
"relevance": 0.9, "episode_ref": "ep-1"}]
|
||||
out = rmp.memory_port_of_call(_issue(), ["widget", "unicode"],
|
||||
provider=provider)
|
||||
assert len(out) == 1 and out[0]["episode_ref"] == "ep-1"
|
||||
assert "widget" in seen["query"]
|
||||
|
||||
|
||||
def test_port_wired_into_inward_context():
|
||||
calls = {"n": 0}
|
||||
|
||||
def provider(query_text, keywords, *, limit=5):
|
||||
calls["n"] += 1
|
||||
return [{"source": "memory", "kind": "prior_failure",
|
||||
"summary": "widget export unicode", "relevance": 0.8,
|
||||
"episode_ref": "ep-2"}]
|
||||
rmp.set_memory_provider(provider)
|
||||
ctx = ric.build_context(_issue(), {"stages": ["INWARD"]})
|
||||
assert calls["n"] == 1 # ONE port of call, no extra stages
|
||||
assert ctx["memory_candidates"][0]["episode_ref"] == "ep-2"
|
||||
|
||||
|
||||
def test_port_provider_crash_never_breaks_inward():
|
||||
def provider(*a, **k):
|
||||
raise RuntimeError("memory down")
|
||||
out = rmp.memory_port_of_call(_issue(), ["widget"], provider=provider)
|
||||
assert out == [] # degrades to the fast path
|
||||
|
||||
|
||||
def test_low_relevance_candidates_filtered():
|
||||
def provider(query_text, keywords, *, limit=5):
|
||||
return [{"source": "memory", "kind": "prior_failure",
|
||||
"summary": "unrelated", "relevance": 0.05,
|
||||
"episode_ref": "ep-3"}]
|
||||
assert rmp.memory_port_of_call(_issue(), ["widget"],
|
||||
provider=provider) == []
|
||||
|
||||
|
||||
# ── G10: zero-result fast path ──────────────────────────────────────────────
|
||||
|
||||
def test_zero_result_is_normal_success():
|
||||
def provider(query_text, keywords, *, limit=5):
|
||||
return []
|
||||
out = rmp.memory_port_of_call(_issue(), ["widget"], provider=provider)
|
||||
assert out == [] # no error, no ceremony — caller continues directly
|
||||
|
||||
|
||||
def test_zero_result_inward_context_has_no_extra_fields_or_calls():
|
||||
calls = {"n": 0}
|
||||
|
||||
def provider(query_text, keywords, *, limit=5):
|
||||
calls["n"] += 1
|
||||
return []
|
||||
rmp.set_memory_provider(provider)
|
||||
ctx = ric.build_context(_issue(), {"stages": ["INWARD"]})
|
||||
assert calls["n"] == 1
|
||||
assert ctx["memory_candidates"] == []
|
||||
# ZERO_RESULT_EXTRA_MODEL_CALLS = 0: nothing in the context requests
|
||||
# extra cognition for the empty result
|
||||
assert not ctx.get("mimo_needed") or ctx["mimo_needed"] in (True, False)
|
||||
|
||||
|
||||
# ── G11: teachable-moment future seam ──────────────────────────────────────
|
||||
|
||||
def test_mark_candidate_records_evidence():
|
||||
emitted = []
|
||||
rec = rmp.mark_teachable_moment_candidate(
|
||||
"rollback:caa77f8a", trigger="rollback_executed",
|
||||
summary="B->A rollback lived through; health gate caught lag",
|
||||
emit=emitted.append)
|
||||
assert rec["status"] == "CANDIDATE"
|
||||
assert emitted and emitted[0]["episode_ref"] == "rollback:caa77f8a"
|
||||
|
||||
|
||||
def test_unknown_trigger_refused():
|
||||
import pytest
|
||||
with pytest.raises(ValueError, match="unknown trigger"):
|
||||
rmp.mark_teachable_moment_candidate("e", trigger="always_learn",
|
||||
summary="x")
|
||||
|
||||
|
||||
def test_applicability_rejects_irrelevant_candidate():
|
||||
candidates = [
|
||||
{"summary": "widget export unicode handling", "relevance": 0.9,
|
||||
"keywords": ["widget", "unicode"]},
|
||||
{"summary": "database connection pooling strategy", "relevance": 0.9,
|
||||
"keywords": ["database", "pool"]},
|
||||
]
|
||||
applicable, rejected = rmp.applicability_filter(
|
||||
candidates, ["widget", "unicode", "export"])
|
||||
assert [c["summary"] for c in applicable] == \
|
||||
["widget export unicode handling"]
|
||||
assert [c["summary"] for c in rejected] == \
|
||||
["database connection pooling strategy"]
|
||||
# SYNTHESIS receives only applicable candidates — rejected never leak
|
||||
synthesis_input = applicable
|
||||
assert all("pool" not in c["summary"] for c in synthesis_input)
|
||||
@@ -0,0 +1,169 @@
|
||||
"""test_rollback_generation.py — T21.4 §21.4F: governed rollback API tests.
|
||||
|
||||
Refusals must occur BEFORE any mutation of current/last-good. The live
|
||||
B→A→B proof runs against the real deploy root; these tests prove the
|
||||
contract on synthetic deploy roots (same code path, no production risk).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_SCRIPTS = str(Path(__file__).resolve().parent.parent / "scripts")
|
||||
if _SCRIPTS not in sys.path:
|
||||
sys.path.insert(0, _SCRIPTS)
|
||||
|
||||
import rollback_generation as rg
|
||||
|
||||
|
||||
def make_generation(root: Path, name: str, commit: str) -> Path:
|
||||
gen = root / name
|
||||
pkg = gen / "src" / "rune"
|
||||
pkg.mkdir(parents=True)
|
||||
(pkg / "__init__.py").write_text('__version__ = "test"\n')
|
||||
(gen / "src" / "rune" / "_build_identity.json").write_text(
|
||||
json.dumps({"commit": commit}))
|
||||
return gen
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def deploy_root(tmp_path, monkeypatch):
|
||||
root = tmp_path / "rune-code"
|
||||
root.mkdir()
|
||||
gen_a = make_generation(root, "gen-a", "a" * 40)
|
||||
gen_b = make_generation(root, "gen-b", "b" * 40)
|
||||
(root / "current").symlink_to(gen_a)
|
||||
(root / "last-good").symlink_to(gen_b)
|
||||
history = tmp_path / "history.jsonl"
|
||||
monkeypatch.setattr(rg.deploy_promotion, "ancestry_of",
|
||||
lambda t, d, r: "ancestor")
|
||||
return root, history, gen_a, gen_b
|
||||
|
||||
|
||||
def readlink(p: Path) -> str:
|
||||
return str(p.readlink())
|
||||
|
||||
|
||||
# ── refusals: BEFORE mutation ──────────────────────────────────────────────
|
||||
|
||||
def test_refuse_no_reason(deploy_root):
|
||||
root, history, gen_a, gen_b = deploy_root
|
||||
outcome, detail = rg.rollback_generation("gen-b", "a" * 40, " ",
|
||||
deploy_root=root,
|
||||
history_path=history)
|
||||
assert outcome == rg.REFUSED_NO_REASON
|
||||
assert readlink(root / "current") == str(gen_a)
|
||||
|
||||
|
||||
def test_refuse_expected_mismatch(deploy_root):
|
||||
root, history, gen_a, gen_b = deploy_root
|
||||
outcome, detail = rg.rollback_generation("gen-b", "f" * 40, "stale caller",
|
||||
deploy_root=root,
|
||||
history_path=history)
|
||||
assert outcome == rg.REFUSED_EXPECTED_MISMATCH
|
||||
assert readlink(root / "current") == str(gen_a)
|
||||
assert readlink(root / "last-good") == str(gen_b)
|
||||
|
||||
|
||||
def test_refuse_target_missing(deploy_root):
|
||||
root, history, gen_a, gen_b = deploy_root
|
||||
outcome, _ = rg.rollback_generation("gen-ghost", "a" * 40, "ghost target",
|
||||
deploy_root=root, history_path=history)
|
||||
assert outcome == rg.REFUSED_TARGET_MISSING
|
||||
assert readlink(root / "current") == str(gen_a)
|
||||
|
||||
|
||||
def test_refuse_target_unreadable(deploy_root):
|
||||
root, history, gen_a, gen_b = deploy_root
|
||||
bad = root / "gen-bad"
|
||||
bad.mkdir()
|
||||
outcome, _ = rg.rollback_generation("gen-bad", "a" * 40, "no identity",
|
||||
deploy_root=root, history_path=history)
|
||||
assert outcome == rg.REFUSED_TARGET_UNREADABLE
|
||||
assert readlink(root / "current") == str(gen_a)
|
||||
|
||||
|
||||
def test_refuse_not_eligible_divergent(deploy_root, monkeypatch):
|
||||
root, history, gen_a, gen_b = deploy_root
|
||||
monkeypatch.setattr(rg.deploy_promotion, "ancestry_of",
|
||||
lambda t, d, r: "divergent")
|
||||
outcome, detail = rg.rollback_generation("gen-b", "a" * 40, "divergent",
|
||||
deploy_root=root,
|
||||
history_path=history)
|
||||
assert outcome == rg.REFUSED_NOT_ELIGIBLE
|
||||
assert "divergent" in detail["error"]
|
||||
assert readlink(root / "current") == str(gen_a)
|
||||
|
||||
|
||||
# ── the operation itself ───────────────────────────────────────────────────
|
||||
|
||||
def test_rollback_success_switch(deploy_root):
|
||||
root, history, gen_a, gen_b = deploy_root
|
||||
outcome, detail = rg.rollback_generation("gen-b", "a" * 40, "regression",
|
||||
deploy_root=root,
|
||||
history_path=history)
|
||||
assert outcome == rg.ROLLED_BACK, detail
|
||||
assert readlink(root / "current") == str(gen_b)
|
||||
assert readlink(root / "last-good") == str(gen_a)
|
||||
events = [json.loads(l) for l in
|
||||
history.read_text(encoding="utf-8").splitlines()]
|
||||
assert events[0]["outcome"] == rg.ROLLED_BACK
|
||||
assert events[0]["reason"] == "regression"
|
||||
|
||||
|
||||
def test_noop_when_already_current(deploy_root):
|
||||
root, history, gen_a, gen_b = deploy_root
|
||||
outcome, _ = rg.rollback_generation("gen-a", "a" * 40, "same",
|
||||
deploy_root=root, history_path=history)
|
||||
assert outcome == rg.NOOP
|
||||
|
||||
|
||||
def test_health_failure_reverts(deploy_root, monkeypatch):
|
||||
root, history, gen_a, gen_b = deploy_root
|
||||
monkeypatch.setattr(rg, "_health", lambda r, g, timeout=60:
|
||||
(False, "import smoke failed: boom"))
|
||||
outcome, detail = rg.rollback_generation("gen-b", "a" * 40, "bad health",
|
||||
deploy_root=root,
|
||||
history_path=history)
|
||||
assert outcome == rg.ROLLED_BACK_REVERTED
|
||||
assert readlink(root / "current") == str(gen_a)
|
||||
events = [json.loads(l) for l in
|
||||
history.read_text(encoding="utf-8").splitlines()]
|
||||
assert events[0]["outcome"] == rg.ROLLED_BACK_REVERTED
|
||||
|
||||
|
||||
def test_import_smoke_real(tmp_path):
|
||||
"""Health check runs a REAL import against a generation tree."""
|
||||
root = tmp_path / "rune-code"
|
||||
root.mkdir()
|
||||
make_generation(root, "gen-x", "x" * 40)
|
||||
ok, detail = rg._health(root, "gen-x")
|
||||
assert ok, detail
|
||||
|
||||
|
||||
def test_lock_shared_with_promotion(deploy_root):
|
||||
"""Rollback serialises on the SAME lock file promotion uses."""
|
||||
root, history, gen_a, gen_b = deploy_root
|
||||
lock = root / ".promotion.lock"
|
||||
import fcntl
|
||||
with open(lock, "w") as fd:
|
||||
fcntl.flock(fd, fcntl.LOCK_EX)
|
||||
import threading
|
||||
results = {}
|
||||
|
||||
def try_rollback():
|
||||
results["r"] = rg.rollback_generation(
|
||||
"gen-b", "a" * 40, "contended", deploy_root=root,
|
||||
history_path=history)
|
||||
|
||||
t = threading.Thread(target=try_rollback)
|
||||
t.start()
|
||||
import time
|
||||
time.sleep(0.3)
|
||||
assert "r" not in results # blocked on the shared lock
|
||||
fcntl.flock(fd, fcntl.LOCK_UN)
|
||||
t.join(timeout=10)
|
||||
assert results["r"][0] == rg.ROLLED_BACK
|
||||
@@ -0,0 +1,216 @@
|
||||
"""test_single_logical_await.py — T21.5 §3/§4.
|
||||
|
||||
G2: one logical external wait = one run() invocation; every nonterminal
|
||||
condition re-arms INTERNALLY; control returns to the caller only on a
|
||||
terminal verdict or real deadline expiry.
|
||||
|
||||
G3: the wait verifier matches the operation — branch/PR ops are judged
|
||||
by their own Actions runs; deploy qualification is never their authority
|
||||
(wrong authority can never mean forever-pending).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import event_wait as ew # noqa: E402
|
||||
from tests.test_event_wait import Daemon # noqa: E402
|
||||
|
||||
def _load():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"ci_event_wait", str(REPO / "scripts" / "ci_event_wait.py"))
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
SHA = "b" * 40
|
||||
OP = f"ci-{SHA}"
|
||||
|
||||
|
||||
def test_unknown_op_type_refused():
|
||||
mod = _load()
|
||||
with pytest.raises(ValueError, match="wait verifier must match"):
|
||||
mod.op_spec("nonsense_op")
|
||||
|
||||
|
||||
def test_all_spec_fields_present():
|
||||
mod = _load()
|
||||
for name, spec in mod.OPERATION_SPECS.items():
|
||||
assert set(spec) >= {"workflow_path", "head_branches", "authority",
|
||||
"event_types", "terminal", "failure"}, name
|
||||
assert spec["authority"] in ("ci_runs", "deploy_qualify")
|
||||
|
||||
|
||||
def test_branch_ci_uses_own_runs_not_deploy_qualify(monkeypatch):
|
||||
"""G3 core: a branch_ci op must NEVER consult deploy qualification."""
|
||||
mod = _load()
|
||||
calls = []
|
||||
|
||||
def boom():
|
||||
calls.append("deploy_qualify")
|
||||
return "pending"
|
||||
|
||||
monkeypatch.setattr(mod, "authority_verdict", boom)
|
||||
monkeypatch.setattr(mod, "_ci_runs_verdict", lambda spec, sha: "success")
|
||||
v = mod.op_authority_verdict("branch_ci", SHA)
|
||||
assert v == "success"
|
||||
assert calls == [] # deploy authority never consulted
|
||||
|
||||
|
||||
def test_master_ci_uses_deploy_qualify(monkeypatch):
|
||||
mod = _load()
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda: "success")
|
||||
assert mod.op_authority_verdict("master_ci", SHA) == "success"
|
||||
|
||||
|
||||
def test_wrong_authority_never_forever_pending(monkeypatch):
|
||||
"""If deploy qualification is blind to a SHA (e.g. branch op), the
|
||||
op's own runs still resolve the verdict — no deadlock."""
|
||||
mod = _load()
|
||||
# deploy authority says pending forever; branch runs say success
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda: "pending")
|
||||
monkeypatch.setattr(mod, "_ci_runs_verdict", lambda spec, sha: "success")
|
||||
assert mod.op_authority_verdict("branch_ci", SHA) == "success"
|
||||
|
||||
|
||||
def _single_wait_scenario(wr_daemon, monkeypatch, verdict_sequence):
|
||||
"""Register one run() call; the authority returns verdict_sequence[i]
|
||||
on the i-th op_authority_verdict call; a wake event posts after each
|
||||
nonterminal authority read. Returns (rc, out, authority_calls)."""
|
||||
mod = _load()
|
||||
seq = list(verdict_sequence)
|
||||
calls = {"n": 0}
|
||||
|
||||
def authority(op_type, sha):
|
||||
i = min(calls["n"], len(seq) - 1)
|
||||
calls["n"] += 1
|
||||
return seq[i]
|
||||
|
||||
monkeypatch.setattr(mod, "op_authority_verdict", authority)
|
||||
monkeypatch.setattr(mod, "op_ci_alive", lambda t, sha: False)
|
||||
|
||||
def post_wakes():
|
||||
# post a wake as soon as the wait record exists, then again after
|
||||
# each nonterminal authority cycle (bounded; last is harmless)
|
||||
for _ in range(len(seq) + 1):
|
||||
deadline = time.time() + 10
|
||||
posted = False
|
||||
start = time.time()
|
||||
while time.time() < deadline:
|
||||
if ew.operation_wait(OP) is not None:
|
||||
wr_daemon.post_event({"operation_id": OP,
|
||||
"provider": "gitea",
|
||||
"event_type": "ci_completed",
|
||||
"sha": SHA, "exit_code": 0,
|
||||
"completed_at": time.time()})
|
||||
posted = True
|
||||
break
|
||||
time.sleep(0.05)
|
||||
if not posted:
|
||||
return
|
||||
time.sleep(1.0) # next wake after the internal re-arm
|
||||
|
||||
threading.Thread(target=post_wakes, daemon=True).start()
|
||||
rc = mod.run(sha=SHA, deadline_secs=30, watchdog_secs=3,
|
||||
op_type="branch_ci", daemon_url=wr_daemon.url())
|
||||
return rc, calls["n"]
|
||||
|
||||
|
||||
def test_single_logical_wait_survives_nonterminal_wakes(wr_daemon, monkeypatch):
|
||||
"""Two nonterminal wakes then terminal: ONE run() call returns the
|
||||
terminal verdict; the model never saw the pending states."""
|
||||
rc, n = _single_wait_scenario(wr_daemon, monkeypatch,
|
||||
["pending", "pending", "success"])
|
||||
assert rc == 0
|
||||
assert n >= 3 # authority re-read internally after each wake
|
||||
|
||||
|
||||
def test_pending_probe_failed_is_nonterminal(wr_daemon, monkeypatch):
|
||||
"""§18 audit fix: "pending (probe failed)" is a pending-class verdict —
|
||||
the tool must re-arm internally, never return it as terminal."""
|
||||
mod = _load()
|
||||
assert mod.normalize_verdict("pending (probe failed)") == "pending"
|
||||
assert mod.normalize_verdict("pending") == "pending"
|
||||
assert mod.normalize_verdict("success") == "success"
|
||||
# live: authority alternates probe-failure and pending, then success;
|
||||
# ONE run() call absorbs all of it
|
||||
rc, n = _single_wait_scenario(
|
||||
wr_daemon, monkeypatch,
|
||||
["pending (probe failed)", "pending", "success"])
|
||||
assert rc == 0
|
||||
assert n >= 3
|
||||
|
||||
|
||||
def test_not_started_operation_rearms_internally(wr_daemon, monkeypatch):
|
||||
"""§18 audit fix: runs not visible yet is EXPECTED nonterminal —
|
||||
internal re-arm, no model return."""
|
||||
mod = _load()
|
||||
seq = {"n": 0}
|
||||
|
||||
def authority(op_type, sha):
|
||||
seq["n"] += 1
|
||||
return "pending" if seq["n"] <= 2 else "success"
|
||||
|
||||
monkeypatch.setattr(mod, "op_authority_verdict", authority)
|
||||
monkeypatch.setattr(mod, "op_ci_alive", lambda t, sha: False)
|
||||
|
||||
def post_wakes():
|
||||
for _ in range(3):
|
||||
deadline = time.time() + 10
|
||||
while time.time() < deadline:
|
||||
if ew.operation_wait(OP) is not None:
|
||||
wr_daemon.post_event({"operation_id": OP,
|
||||
"provider": "gitea",
|
||||
"event_type": "ci_completed",
|
||||
"sha": SHA, "exit_code": 0,
|
||||
"completed_at": time.time()})
|
||||
break
|
||||
time.sleep(0.05)
|
||||
time.sleep(1.0)
|
||||
|
||||
threading.Thread(target=post_wakes, daemon=True).start()
|
||||
rc = mod.run(sha=SHA, deadline_secs=30, watchdog_secs=3,
|
||||
op_type="branch_ci", daemon_url=wr_daemon.url())
|
||||
assert rc == 0
|
||||
assert seq["n"] >= 3
|
||||
|
||||
|
||||
def test_deadline_is_the_only_model_return_for_pending(wr_daemon,
|
||||
monkeypatch):
|
||||
"""Authority stays pending the whole time: run() returns only at real
|
||||
deadline expiry — not at first watchdog or first wake."""
|
||||
mod = _load()
|
||||
monkeypatch.setattr(mod, "op_authority_verdict",
|
||||
lambda t, sha: "pending")
|
||||
monkeypatch.setattr(mod, "op_ci_alive", lambda t, sha: True)
|
||||
|
||||
def post_wakes():
|
||||
for _ in range(3):
|
||||
deadline = time.time() + 10
|
||||
while time.time() < deadline:
|
||||
if ew.operation_wait(OP) is not None:
|
||||
wr_daemon.post_event({"operation_id": OP,
|
||||
"provider": "gitea",
|
||||
"event_type": "ci_completed",
|
||||
"sha": SHA, "exit_code": 0,
|
||||
"completed_at": time.time()})
|
||||
break
|
||||
time.sleep(0.05)
|
||||
time.sleep(1.0)
|
||||
|
||||
threading.Thread(target=post_wakes, daemon=True).start()
|
||||
t0 = time.time()
|
||||
rc = mod.run(sha=SHA, deadline_secs=8, watchdog_secs=3,
|
||||
op_type="branch_ci", daemon_url=wr_daemon.url())
|
||||
wall = time.time() - t0
|
||||
assert rc == 1 # deadline pending
|
||||
assert wall >= 7.0 # NOT returned at first watchdog (3s) or first wake
|
||||
@@ -0,0 +1,168 @@
|
||||
"""test_t214_material_closure.py — T21.4 §21.4H/§21.4G coverage.
|
||||
|
||||
Covers:
|
||||
- construction_interface.close() AUTO material-change closure
|
||||
(rule 19Q.2: governed construction cannot bypass the revision flow)
|
||||
- spine.build() entry-point seeding (__main__ is a runtime path)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(_ROOT / "src"))
|
||||
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
from rune.capability_ledger import CapabilityLedger
|
||||
from rune import spine
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def governed_store(tmp_path):
|
||||
"""A store shaped like the Rune store: scaffold + rune_model ledgers."""
|
||||
scaffold_dir = tmp_path / "store" / "scaffold"
|
||||
scaffold_dir.mkdir(parents=True)
|
||||
model_dir = tmp_path / "store" / "rune_model"
|
||||
model_dir.mkdir(parents=True)
|
||||
ledger = CapabilityLedger(model_dir)
|
||||
ledger.define(capability_id="closure-target",
|
||||
name="Closure target",
|
||||
purpose="capability whose source file is in contract scope",
|
||||
component="component:x", owning_project="t",
|
||||
source_locations=["scripts/rollback_generation.py"])
|
||||
ledger.save()
|
||||
return tmp_path, scaffold_dir, model_dir, ledger
|
||||
|
||||
|
||||
def test_close_auto_material_change(governed_store):
|
||||
tmp_path, scaffold_dir, model_dir, ledger = governed_store
|
||||
from rune import scaffold as sc
|
||||
from rune import structural_store as ss
|
||||
scaffold = sc.Scaffold(scaffold_dir, "t21.4-test")
|
||||
doc = scaffold.init(purpose="t21.4 closure test")
|
||||
ss.add_entity(doc, id="component:closure-target", kind="component",
|
||||
identities={"source": "scripts/rollback_generation.py"},
|
||||
maturity="EXERCISED")
|
||||
ss.add_relationship(doc, type="provides", from_id="component:closure-target",
|
||||
to_id="contract:closure-target")
|
||||
scaffold.commit(doc, what="seed")
|
||||
ci = ConstructionInterface("t21.4-test", project_store=str(scaffold_dir))
|
||||
r = ci.submit_need(
|
||||
objective="bounded change touching closure_target",
|
||||
origin="operator",
|
||||
affected_surface=["scripts/rollback_generation.py"],
|
||||
constraints={"environment": {"existing_language": "python"},
|
||||
"contracts": ["contract:closure-target"]},
|
||||
evidence_refs=["tests/test_t214_material_closure.py"])
|
||||
assert r["success"], r.get("errors")
|
||||
cid = r["ids"]["contract_id"]
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
t = ci.transition(cid, phase, evidence_ref=f"t21.4 test {phase}")
|
||||
assert t["success"], t.get("errors")
|
||||
c = ci.close(cid, observed_evidence=[{"kind": "test", "detail": "x"}],
|
||||
scope_files=["scripts/rollback_generation.py"])
|
||||
assert c["success"], c.get("errors")
|
||||
closure = c["state"]["material_change_closure"]
|
||||
assert closure["changed"] == ["capability:closure-target"]
|
||||
assert closure["unmodeled"] == []
|
||||
# revision incremented and durable
|
||||
rec = CapabilityLedger(model_dir).get("closure-target")
|
||||
assert rec["material_revision"] == 1
|
||||
assert any(h["event"] == "material_change"
|
||||
for h in json.loads(
|
||||
(model_dir / "capability_ledger.json").read_text()
|
||||
)["history"])
|
||||
|
||||
|
||||
def test_close_unmodeled_files_reported(governed_store):
|
||||
tmp_path, scaffold_dir, _, _ = governed_store
|
||||
from rune import scaffold as sc
|
||||
from rune import structural_store as ss
|
||||
scaffold = sc.Scaffold(scaffold_dir, "t21.4-test")
|
||||
doc = scaffold.init(purpose="t21.4 closure test")
|
||||
ss.add_entity(doc, id="component:closure-target", kind="component",
|
||||
identities={"source": "scripts/rollback_generation.py"},
|
||||
maturity="EXERCISED")
|
||||
ss.add_relationship(doc, type="provides", from_id="component:closure-target",
|
||||
to_id="contract:closure-target")
|
||||
scaffold.commit(doc, what="seed")
|
||||
ci = ConstructionInterface("t21.4-test", project_store=str(scaffold_dir))
|
||||
r = ci.submit_need(objective="scope with an unmodeled file",
|
||||
origin="operator",
|
||||
affected_surface=["scripts/rollback_generation.py",
|
||||
"tests/test_t214_material_closure.py"],
|
||||
constraints={"environment": {"existing_language": "python"},
|
||||
"contracts": ["contract:closure-target"]},
|
||||
evidence_refs=["tests/test_t214_material_closure.py"])
|
||||
cid = r["ids"]["contract_id"]
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
ci.transition(cid, phase, evidence_ref="t")
|
||||
c = ci.close(cid, observed_evidence=[{"kind": "test", "detail": "x"}],
|
||||
scope_files=["tests/test_t214_material_closure.py"])
|
||||
closure = c["state"]["material_change_closure"]
|
||||
assert closure["changed"] == []
|
||||
assert closure["unmodeled"] == ["tests/test_t214_material_closure.py"]
|
||||
|
||||
|
||||
def test_closure_error_does_not_break_close(governed_store, monkeypatch):
|
||||
"""OSError in the ledger write degrades to a note — close() succeeds."""
|
||||
tmp_path, scaffold_dir, model_dir, ledger = governed_store
|
||||
from rune import scaffold as sc
|
||||
from rune import structural_store as ss
|
||||
scaffold = sc.Scaffold(scaffold_dir, "t21.4-test")
|
||||
doc = scaffold.init(purpose="t21.4 closure test")
|
||||
ss.add_entity(doc, id="component:closure-target", kind="component",
|
||||
identities={"source": "scripts/rollback_generation.py"},
|
||||
maturity="EXERCISED")
|
||||
ss.add_relationship(doc, type="provides", from_id="component:closure-target",
|
||||
to_id="contract:closure-target")
|
||||
scaffold.commit(doc, what="seed")
|
||||
monkeypatch.setattr(
|
||||
"rune.capability_ledger.CapabilityLedger.save",
|
||||
lambda self: (_ for _ in ()).throw(OSError("disk full")),
|
||||
)
|
||||
ci = ConstructionInterface("t21.4-test", project_store=str(scaffold_dir))
|
||||
r = ci.submit_need(
|
||||
objective="bounded change touching closure_target",
|
||||
origin="operator",
|
||||
affected_surface=["scripts/rollback_generation.py"],
|
||||
constraints={"environment": {"existing_language": "python"},
|
||||
"contracts": ["contract:closure-target"]},
|
||||
evidence_refs=["tests/test_t214_material_closure.py"])
|
||||
cid = r["ids"]["contract_id"]
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
ci.transition(cid, phase, evidence_ref="t")
|
||||
c = ci.close(cid, observed_evidence=[{"kind": "test", "detail": "x"}],
|
||||
scope_files=["scripts/rollback_generation.py"])
|
||||
assert c["success"], c.get("errors")
|
||||
closure = c["state"]["material_change_closure"]
|
||||
assert "closure error" in closure.get("note", "")
|
||||
assert closure["changed"] == ["capability:closure-target"]
|
||||
|
||||
|
||||
def test_spine_seeds_entry_point(tmp_path, monkeypatch):
|
||||
"""__main__ is invisible to the import graph but is a real runtime
|
||||
path — build() must seed it deterministically."""
|
||||
src_dir = tmp_path / "src" / "rune"
|
||||
src_dir.mkdir(parents=True)
|
||||
(src_dir / "__main__.py").write_text("# entry point\n")
|
||||
(src_dir / "seeded_mod.py").write_text("x = 1\n")
|
||||
|
||||
empty_store = tmp_path / "spine.json"
|
||||
monkeypatch.setattr(spine, "SRC_DIR", src_dir)
|
||||
monkeypatch.setattr(spine, "load", lambda path=None: None)
|
||||
monkeypatch.setattr(spine, "save",
|
||||
lambda doc, path=None: empty_store)
|
||||
monkeypatch.setattr(spine, "_canary_registry", lambda: [])
|
||||
monkeypatch.setattr(spine, "ingest_umbrellas", lambda doc, mp=None: 0)
|
||||
monkeypatch.setattr(spine, "_test_backed_modules", lambda: set())
|
||||
monkeypatch.setattr(spine, "DEPLOYED_LINK", tmp_path / "no-deploy")
|
||||
|
||||
doc = spine.build()
|
||||
entry = spine.entity(doc, "module:__main__")
|
||||
assert entry is not None
|
||||
assert entry["identities"]["path"] == "src/rune/__main__.py"
|
||||
@@ -0,0 +1,232 @@
|
||||
"""test_t215_awareness.py — T21.5 §9: automatic new-Scaffold structural
|
||||
awareness (G8).
|
||||
|
||||
NEW_SCAFFOLD_MANUAL_REGISTRATION_STEPS = 0
|
||||
POST_CHANGE_MANUAL_MODEL_SYNC_STEPS = 0
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store(tmp_path):
|
||||
from rune.scaffold import Scaffold
|
||||
from rune import spine as spine_mod
|
||||
store = tmp_path / "store" / "scaffold"
|
||||
store.mkdir(parents=True)
|
||||
spine_mod.save(spine_mod.new_doc(), tmp_path / "store" / "spine" / "current.json")
|
||||
scaffold = Scaffold(store, "issue161-harness")
|
||||
scaffold.init(purpose="t21.5 awareness test")
|
||||
return tmp_path, store, scaffold
|
||||
|
||||
|
||||
def _adopt(store, scaffold, project="widget-forge"):
|
||||
from rune import construction_decision as cd
|
||||
from rune import construction_activation as ca
|
||||
from rune import spine as spine_mod
|
||||
need = cd.Need(
|
||||
need_id=f"need-adopt-{project}",
|
||||
objective=f"Build {project} as a new standalone project with its "
|
||||
"own repository: the capability",
|
||||
origin="operator", project=project,
|
||||
constraints={"environment": {"existing_language": "python"},
|
||||
"contracts": [f"contract:{project}-export"]},
|
||||
evidence_refs=["tests/test_t215_awareness.py"])
|
||||
trace = cd.make_decision(need,
|
||||
authorities={"project_scaffold": scaffold.load(),
|
||||
"rune_spine": spine_mod.load(
|
||||
store.parent / "spine" / "current.json")},
|
||||
project_toolchain={"existing_language": "python"})
|
||||
assert trace["construction_mode"]["mode"] == "PROJECT_BUILD"
|
||||
return ca.dispatch_from_decision(trace, need=need, scaffold=scaffold,
|
||||
builder="t21.5-test")
|
||||
|
||||
|
||||
def test_adoption_auto_registers(store):
|
||||
tmp, store, scaffold = store
|
||||
from rune.project_registry import ProjectRegistry
|
||||
from rune import spine as spine_mod
|
||||
rec = _adopt(store, scaffold)
|
||||
assert rec["activated"] is True
|
||||
assert rec["structural_awareness"] == {"registry": "registered",
|
||||
"spine": "recorded"}
|
||||
# A. registry knows — no manual registration
|
||||
reg = ProjectRegistry(store)
|
||||
e = reg.get("widget-forge")
|
||||
assert e is not None
|
||||
assert e["scaffold_location"] == str(store / "projects" / "widget-forge")
|
||||
# B. spine relationship — no manual bookkeeping
|
||||
sdoc = spine_mod.load(tmp / "store" / "spine" / "current.json")
|
||||
rels = [(r["type"], r["from"], r["to"]) for r in sdoc["relationships"]]
|
||||
assert ("manages", "project:rune", "project:widget-forge") in rels
|
||||
# C. project scaffold canonical; spine holds NO project modules
|
||||
assert (store / "projects" / "widget-forge" / "current.json").is_file()
|
||||
sdoc_ids = {x["id"] for x in sdoc["entities"]}
|
||||
assert not any("widget" in i for i in sdoc_ids) # no duplication
|
||||
|
||||
|
||||
def test_capability_discovery_finds_project_capability(store):
|
||||
tmp, store, scaffold = store
|
||||
from rune.scaffold import Scaffold
|
||||
from rune import structural_store as ss
|
||||
from rune.capability_fit import (discover_candidates, evaluate_fit,
|
||||
CapabilityRequirement)
|
||||
_adopt(store, scaffold)
|
||||
ps = Scaffold(store / "projects" / "widget-forge", "widget-forge")
|
||||
pdoc = ps.load()
|
||||
ss.add_entity(pdoc, id="component:widget-export", kind="component",
|
||||
identities={"source": "src/widget_export.py"},
|
||||
maturity="DECLARED")
|
||||
ss.add_relationship(pdoc, type="provides",
|
||||
from_id="component:widget-export",
|
||||
to_id="contract:widget-export")
|
||||
ps.commit(pdoc, what="model capability")
|
||||
req = CapabilityRequirement("widget export capability",
|
||||
contracts=["contract:widget-export"],
|
||||
environment={})
|
||||
cands = discover_candidates(pdoc, req)
|
||||
fits = [evaluate_fit(req, c) for c in cands]
|
||||
assert any(c["id"] == "component:widget-export" and f["fit"] == "EXACT"
|
||||
for c, f in zip(cands, fits))
|
||||
|
||||
|
||||
def test_awareness_failure_tolerant(store, monkeypatch):
|
||||
"""Registry/spine errors are recorded, never break the bootstrap."""
|
||||
tmp, store, scaffold = store
|
||||
import rune.construction_activation as ca
|
||||
from rune.project_registry import ProjectRegistry
|
||||
monkeypatch.setattr(ProjectRegistry, "save",
|
||||
lambda self: (_ for _ in ()).throw(OSError("disk")))
|
||||
rec = _adopt(store, scaffold)
|
||||
assert rec["activated"] is True # bootstrap succeeded
|
||||
aw = rec["structural_awareness"]
|
||||
assert "error" in aw["registry"]
|
||||
# spine edge still recorded (independent of registry failure)
|
||||
assert aw["spine"] == "recorded"
|
||||
|
||||
|
||||
def test_spine_error_recorded_not_raised(store, monkeypatch):
|
||||
"""Spine failure degrades to an error note; bootstrap still succeeds."""
|
||||
tmp, store, scaffold = store
|
||||
import rune.construction_activation as ca
|
||||
from rune import spine as spine_mod
|
||||
monkeypatch.setattr(spine_mod, "save",
|
||||
lambda doc, path=None: (_ for _ in ()).throw(
|
||||
OSError("disk full")))
|
||||
rec = _adopt(store, scaffold)
|
||||
assert rec["activated"] is True
|
||||
aw = rec["structural_awareness"]
|
||||
assert "error" in aw["spine"]
|
||||
|
||||
|
||||
def test_dispatch_handles_unknown_mode(store):
|
||||
"""UNKNOWN mode flows through the reconstruction (probe path)."""
|
||||
tmp, store, scaffold = store
|
||||
from rune import construction_decision as cd
|
||||
from rune import construction_activation as ca
|
||||
from rune import spine as spine_mod
|
||||
need = cd.Need(
|
||||
need_id="n-unk", objective="ambiguous objective",
|
||||
origin="operator", project="widget-forge",
|
||||
constraints={"environment": {"existing_language": "python"},
|
||||
"contracts": ["contract:widget-forge-export"]},
|
||||
evidence_refs=["t"])
|
||||
trace = cd.make_decision(need, authorities={
|
||||
"project_scaffold": scaffold.load(),
|
||||
"rune_spine": spine_mod.load(tmp / "store" / "spine" / "current.json")},
|
||||
project_toolchain={"existing_language": "python"})
|
||||
rec = ca.dispatch_from_decision(trace, need=need, scaffold=scaffold,
|
||||
builder="t")
|
||||
assert rec.get("mode") in ("UNKNOWN", "PROJECT_BUILD")
|
||||
|
||||
|
||||
def test_dispatch_wraps_mode_decision_dict(store):
|
||||
"""dispatch_from_decision reconstructs ConstructionMode from trace."""
|
||||
tmp, store, scaffold = store
|
||||
from rune import construction_decision as cd
|
||||
from rune import construction_activation as ca
|
||||
from rune import spine as spine_mod
|
||||
need = cd.Need(
|
||||
need_id="n-dispatch", objective="Build widget-forge as a new "
|
||||
"standalone project with its own repository: the capability",
|
||||
origin="operator", project="widget-forge",
|
||||
constraints={"environment": {"existing_language": "python"},
|
||||
"contracts": ["contract:widget-forge-export"]},
|
||||
evidence_refs=["t"])
|
||||
trace = cd.make_decision(need, authorities={
|
||||
"project_scaffold": scaffold.load(),
|
||||
"rune_spine": spine_mod.load(tmp / "store" / "spine" / "current.json")},
|
||||
project_toolchain={"existing_language": "python"})
|
||||
# UNKNOWN probe path: mode dict flows through the reconstruction
|
||||
assert trace["construction_mode"]["mode"] in ("PROJECT_BUILD", "UNKNOWN")
|
||||
rec = ca.dispatch_from_decision(trace, need=need, scaffold=scaffold,
|
||||
builder="t")
|
||||
assert "activated" in rec
|
||||
|
||||
|
||||
def test_governed_change_updates_living_scaffold_automatically(store):
|
||||
tmp, store, scaffold = store
|
||||
from rune.scaffold import Scaffold
|
||||
from rune.construction_interface import ConstructionInterface
|
||||
from rune.capability_ledger import CapabilityLedger
|
||||
_adopt(store, scaffold)
|
||||
ps_store = store / "projects" / "widget-forge"
|
||||
model_dir = tmp / "store" / "rune_model"
|
||||
model_dir.mkdir(exist_ok=True)
|
||||
ledger = CapabilityLedger(model_dir)
|
||||
ledger.define(capability_id="widget-import", name="Widget import",
|
||||
purpose="import widgets",
|
||||
component="component:widget-import",
|
||||
owning_project="widget-forge",
|
||||
source_locations=["src/widget_import.py"])
|
||||
ledger.save()
|
||||
# the project already models widget-export (capability discovery
|
||||
# target) — a governed EXTENSION need discovers it and proceeds
|
||||
from rune import structural_store as ss
|
||||
ps = Scaffold(ps_store, "widget-forge")
|
||||
pdoc = ps.load()
|
||||
ss.add_entity(pdoc, id="component:widget-export", kind="component",
|
||||
identities={"source": "src/widget_export.py"},
|
||||
maturity="DECLARED")
|
||||
ss.add_relationship(pdoc, type="provides",
|
||||
from_id="component:widget-export",
|
||||
to_id="contract:widget-import")
|
||||
ps.commit(pdoc, what="seed existing capability")
|
||||
ci = ConstructionInterface("t21.5-test", project_store=str(ps_store),
|
||||
project_id="widget-forge")
|
||||
r = ci.submit_need(
|
||||
objective="add the widget import capability to widget-forge "
|
||||
"(bounded change; import widgets from external bundles)",
|
||||
origin="operator",
|
||||
affected_surface=["src/widget_import.py"],
|
||||
constraints={"environment": {"existing_language": "python"},
|
||||
"contracts": ["contract:widget-import"]},
|
||||
evidence_refs=["tests/test_t215_awareness.py"])
|
||||
cid = r["ids"]["contract_id"]
|
||||
for phase in ("approved", "building", "verifying"):
|
||||
assert ci.transition(cid, phase, evidence_ref="t").get("success")
|
||||
c = ci.close(cid, observed_evidence=[{"kind": "test", "detail": "x"}],
|
||||
scope_files=["src/widget_import.py"])
|
||||
closure = c["state"]["material_change_closure"]
|
||||
assert closure["changed"] == ["capability:widget-import"]
|
||||
assert closure["unmodeled"] == []
|
||||
# fresh process sees revision WITHOUT any manual sync step
|
||||
code = (
|
||||
"import sys, json\nfrom pathlib import Path\n"
|
||||
f"sys.path.insert(0, {str(REPO / 'src')!r})\n"
|
||||
"from rune.capability_ledger import CapabilityLedger\n"
|
||||
f"led = CapabilityLedger(Path({str(model_dir)!r}))\n"
|
||||
'sys.stdout.write(json.dumps({"rev": led.get("widget-import")["material_revision"]}))\n'
|
||||
)
|
||||
p = subprocess.run([sys.executable, "-c", code],
|
||||
capture_output=True, text=True)
|
||||
assert json.loads(p.stdout)["rev"] == 1
|
||||
@@ -0,0 +1,166 @@
|
||||
"""test_wr_root_authority.py — T21.5 §7: ONE wait/resume root authority.
|
||||
|
||||
WAIT_RESUME_ROOT_AUTHORITIES = 1, SPLIT_BRAIN_EVENT_STORES = 0.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
|
||||
def _load_resolver():
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"wr_root_resolver", str(REPO / "scripts" / "wr_root_resolver.py"))
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated(monkeypatch, tmp_path):
|
||||
mod = _load_resolver()
|
||||
monkeypatch.setattr(mod, "CANONICAL_WR_ROOT", tmp_path / "canon")
|
||||
monkeypatch.setattr(mod, "LEGACY_WR_ROOT", tmp_path / "legacy")
|
||||
monkeypatch.delenv("RUNE_WR_ROOT", raising=False)
|
||||
return mod, tmp_path
|
||||
|
||||
|
||||
def _mk_store(root: Path, n_events: int = 2):
|
||||
(root / "events").mkdir(parents=True)
|
||||
for i in range(n_events):
|
||||
(root / "events" / f"e{i}.json").write_text(
|
||||
json.dumps({"event_id": f"e{i}", "event": {}}))
|
||||
|
||||
|
||||
# resolution order
|
||||
|
||||
def test_env_pin_wins(isolated):
|
||||
mod, tmp = isolated
|
||||
pin = tmp / "pinned"
|
||||
pin.mkdir()
|
||||
import os
|
||||
os.environ["RUNE_WR_ROOT"] = str(pin)
|
||||
try:
|
||||
assert mod.resolve_root() == pin
|
||||
finally:
|
||||
del os.environ["RUNE_WR_ROOT"]
|
||||
|
||||
|
||||
def test_canonical_preferred_when_live(isolated):
|
||||
mod, tmp = isolated
|
||||
_mk_store(mod.CANONICAL_WR_ROOT)
|
||||
_mk_store(mod.LEGACY_WR_ROOT, n_events=0) # dir but no events
|
||||
assert mod.resolve_root() == mod.CANONICAL_WR_ROOT
|
||||
|
||||
|
||||
def test_split_brain_fails_loudly(isolated):
|
||||
mod, _ = isolated
|
||||
_mk_store(mod.CANONICAL_WR_ROOT)
|
||||
_mk_store(mod.LEGACY_WR_ROOT)
|
||||
with pytest.raises(RuntimeError, match="SPLIT-BRAIN"):
|
||||
mod.resolve_root()
|
||||
|
||||
|
||||
def test_split_brain_resolved_by_pin(isolated):
|
||||
mod, tmp = isolated
|
||||
_mk_store(mod.CANONICAL_WR_ROOT)
|
||||
_mk_store(mod.LEGACY_WR_ROOT)
|
||||
import os
|
||||
os.environ["RUNE_WR_ROOT"] = str(mod.CANONICAL_WR_ROOT)
|
||||
try:
|
||||
assert mod.resolve_root() == mod.CANONICAL_WR_ROOT
|
||||
finally:
|
||||
del os.environ["RUNE_WR_ROOT"]
|
||||
|
||||
|
||||
def test_first_run_creates_canonical(isolated):
|
||||
mod, _ = isolated
|
||||
assert mod.resolve_root() == mod.CANONICAL_WR_ROOT # no dirs at all
|
||||
|
||||
|
||||
# event_wait delegation
|
||||
|
||||
def test_event_wait_wr_root_delegates(isolated, monkeypatch):
|
||||
mod, _ = isolated
|
||||
_mk_store(mod.CANONICAL_WR_ROOT)
|
||||
from rune import event_wait as ew
|
||||
# point event_wait's resolver import at the isolated module
|
||||
import importlib.util
|
||||
resolver_path = REPO / "scripts" / "wr_root_resolver.py"
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"wr_root_resolver", str(resolver_path))
|
||||
rmod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(rmod)
|
||||
monkeypatch.setattr(rmod, "CANONICAL_WR_ROOT", mod.CANONICAL_WR_ROOT)
|
||||
monkeypatch.setattr(rmod, "LEGACY_WR_ROOT", mod.LEGACY_WR_ROOT)
|
||||
monkeypatch.delenv("RUNE_WR_ROOT", raising=False)
|
||||
# patch the loader location used inside event_wait: it imports by
|
||||
# path, so we monkeypatch resolve at the source module instead
|
||||
monkeypatch.setattr(ew, "wr_root",
|
||||
lambda: rmod.resolve_root())
|
||||
assert ew.wr_root() == mod.CANONICAL_WR_ROOT
|
||||
|
||||
|
||||
# daemon launcher uses the resolver (static check of the contract)
|
||||
|
||||
def test_event_wait_no_spec_raises_fallback(monkeypatch, tmp_path):
|
||||
"""spec_from_file_location returning None (missing file) triggers
|
||||
the documented fallback policy."""
|
||||
from rune import event_wait as ew
|
||||
import importlib.util
|
||||
monkeypatch.setattr("importlib.util.spec_from_file_location",
|
||||
lambda *a, **k: None)
|
||||
monkeypatch.setattr(ew, "DEFAULT_WR_ROOT", tmp_path / "legacy-home")
|
||||
monkeypatch.delenv("RUNE_WR_ROOT", raising=False)
|
||||
import os
|
||||
env = os.environ.get("RUNE_WR_ROOT")
|
||||
try:
|
||||
os.environ.pop("RUNE_WR_ROOT", None)
|
||||
shared = Path("/mnt/workdrive/wr-bridge")
|
||||
expected = shared if shared.exists() else tmp_path / "legacy-home"
|
||||
assert ew.wr_root() == expected
|
||||
finally:
|
||||
if env is not None:
|
||||
os.environ["RUNE_WR_ROOT"] = env
|
||||
|
||||
|
||||
def test_daemon_launcher_resolves_root_once():
|
||||
src = (REPO / "scripts" / "start_wr_daemon.py").read_text()
|
||||
assert "resolve_root()" in src
|
||||
assert "WR_ROOT=str(root)" in src
|
||||
assert "RUNE_WR_ROOT=str(root)" in src
|
||||
|
||||
|
||||
def test_reconcile_is_idempotent(isolated, capsys):
|
||||
mod, _ = isolated
|
||||
_mk_store(mod.CANONICAL_WR_ROOT, n_events=1)
|
||||
_mk_store(mod.LEGACY_WR_ROOT, n_events=3)
|
||||
import subprocess
|
||||
# run the reconcile logic in-process against isolated roots
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
import wr_root_reconcile as rec
|
||||
rec_mod = rec
|
||||
# patch module-level roots
|
||||
monkeypatch_targets = rec_mod
|
||||
import types
|
||||
# simple: re-execute main with patched resolver attrs
|
||||
old_canon, old_legacy = rec_mod.resolver.CANONICAL_WR_ROOT, \
|
||||
rec_mod.resolver.LEGACY_WR_ROOT
|
||||
rec_mod.resolver.CANONICAL_WR_ROOT = mod.CANONICAL_WR_ROOT
|
||||
rec_mod.resolver.LEGACY_WR_ROOT = mod.LEGACY_WR_ROOT
|
||||
try:
|
||||
assert rec_mod.main() == 0
|
||||
assert rec_mod.main() == 0 # second run: nothing left to move
|
||||
finally:
|
||||
rec_mod.resolver.CANONICAL_WR_ROOT = old_canon
|
||||
rec_mod.resolver.LEGACY_WR_ROOT = old_legacy
|
||||
events = sorted(p.name for p in
|
||||
(mod.CANONICAL_WR_ROOT / "events").glob("*.json"))
|
||||
assert len(events) == 3 # 1 original + 2 moved
|
||||
Reference in new issue
Block a user