Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
40b3731928 | ||
|
|
caa77f8a35 | ||
|
|
4d215ea666 | ||
|
|
fda9b6dda0 | ||
|
|
116d1ffbeb | ||
|
|
040988cf00 | ||
|
|
ffed816465 | ||
|
|
72ab7dccf7 | ||
|
|
5d20dc7f51 | ||
|
|
9e903dacaf | ||
|
|
ed1b41abd5 | ||
|
|
ff1b950f11 | ||
|
|
3afc9ff872 | ||
|
|
28e9c8c1c1 | ||
|
|
8249854881 | ||
|
|
4aeef171d2 | ||
|
|
2ccac89ec4 | ||
|
|
232986f0c3 | ||
|
|
725c779ac7 | ||
|
|
edd6d06300 | ||
|
|
98cb3a98a5 | ||
|
|
7881e78e7f | ||
|
|
57a689d22f | ||
|
|
45869a77cd | ||
|
|
224fbfa4e9 | ||
|
|
417e824a51 | ||
|
|
ca2807e9b5 | ||
|
|
25f5219982 | ||
|
|
2d09bee39e | ||
|
|
77bc2161a6 | ||
|
|
74e18402f0 | ||
|
|
39ec00b361 | ||
|
|
95946b6efd | ||
|
|
72d3c909c8 | ||
|
|
ab2dfe7b09 | ||
|
|
1326713234 | ||
|
|
867321ed05 | ||
|
|
a44ced163e | ||
|
|
f40f7e7233 | ||
|
|
8bef4682f5 | ||
|
|
f939d9db5a | ||
|
|
dcead3fecc |
No files matched your search
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.72.1**
|
||||
**Current release: v0.83.3**
|
||||
|
||||
**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.**
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import type { Hooks } from "@mimo-ai/plugin"
|
||||
|
||||
// event-wait-guard — RETIRED (T21.5 §6/§8, 2026-09-17).
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// 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) => {
|
||||
// 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
|
||||
},
|
||||
}
|
||||
|
||||
export const CombinedGuards: Hooks = {
|
||||
"tool.execute.before": async (input, output) => {
|
||||
// Kept as the default export shape; both sub-guards are retired.
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
export default CombinedGuards
|
||||
@@ -0,0 +1,54 @@
|
||||
import { tool } from "@mimo-ai/plugin"
|
||||
import { execSync } from "child_process"
|
||||
|
||||
// await_operation — T21.3 §21.3C: FIRST-CLASS async wait tool.
|
||||
// Uses the plugin tool() API with args (not parameters).
|
||||
|
||||
export const AwaitOperation = tool({
|
||||
description: [
|
||||
"Wait for an asynchronous external operation (CI, deploy, release) to complete.",
|
||||
"Registers on the event bridge, yields until the completion event arrives, then returns the verified result.",
|
||||
"USE THIS instead of `sleep N; curl status` — it is the canonical event-driven wait.",
|
||||
].join(" "),
|
||||
args: {
|
||||
sha: tool.schema.string().describe(
|
||||
"The exact commit SHA the operation is running against"),
|
||||
deadline_secs: tool.schema.number().optional().describe(
|
||||
"Maximum wait in seconds (default 900)"),
|
||||
},
|
||||
async execute(args, ctx) {
|
||||
const repoRoot = "/mnt/workdrive/issue161-harness"
|
||||
const deadline = args.deadline_secs || 900
|
||||
const cmd = [
|
||||
"python3", `${repoRoot}/scripts/ci_event_wait.py`,
|
||||
"--sha", args.sha,
|
||||
"--deadline", String(deadline),
|
||||
].join(" ")
|
||||
try {
|
||||
const result = execSync(cmd, {
|
||||
cwd: repoRoot,
|
||||
timeout: (deadline + 60) * 1000,
|
||||
encoding: "utf-8" as const,
|
||||
env: {
|
||||
...process.env,
|
||||
DEPLOY_SHA: args.sha,
|
||||
RUNE_WR_ROOT: "/mnt/workdrive/wr-bridge",
|
||||
RUNE_WR_CODE: "/home/nick/.local/share/mimo-wait-resume",
|
||||
},
|
||||
})
|
||||
const lines = result.trim().split("\n")
|
||||
const jsonLine = lines.find((l: string) => l.startsWith("{"))
|
||||
if (jsonLine) return jsonLine
|
||||
return "await_operation: no JSON output"
|
||||
} catch (e: any) {
|
||||
const stdout = e.stdout || ""
|
||||
const lines = stdout.trim().split("\n")
|
||||
const jsonLine = lines.find((l: string) => l.startsWith("{"))
|
||||
if (jsonLine) return jsonLine
|
||||
return JSON.stringify({
|
||||
ok: false,
|
||||
error: `await_operation failed: ${(e.stderr || e.message || "?").slice(0, 200)}`,
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_comment": "GENERATED by rune.spine \u2014 compatibility view for legacy umbrella consumers. DO NOT EDIT: structural truth lives in store/spine/current.json; edits here are flagged by spine drift.",
|
||||
"_generated_by_spine": true,
|
||||
"generated_at": "2026-09-16T03:45:21Z",
|
||||
"generated_at": "2026-09-16T18:23:38Z",
|
||||
"umbrellas": {
|
||||
"capability": {
|
||||
"description": "Tool selection and execution authority",
|
||||
@@ -45,7 +45,8 @@
|
||||
"drift_core",
|
||||
"drift_ledger",
|
||||
"drift_observability",
|
||||
"failure_auto_filer"
|
||||
"failure_auto_filer",
|
||||
"programme_gates"
|
||||
],
|
||||
"depends_on": [],
|
||||
"mirrors": [],
|
||||
@@ -222,7 +223,6 @@
|
||||
"capability_fit",
|
||||
"capability_ledger",
|
||||
"cognitive_budgeter",
|
||||
"complexity_scorer",
|
||||
"construction_activation",
|
||||
"construction_commands",
|
||||
"construction_decision",
|
||||
@@ -256,6 +256,7 @@
|
||||
"toolchain",
|
||||
"tui_backlog",
|
||||
"wait_guard",
|
||||
"wait_guard_corpus",
|
||||
"witness"
|
||||
],
|
||||
"depends_on": [
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"BLE001": 350,
|
||||
"S110": 78
|
||||
"BLE001": 347,
|
||||
"S110": 75
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.72.1"
|
||||
version = "0.83.3"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
+3
-3
@@ -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
|
||||
@@ -64,7 +64,7 @@ nodeenv==1.10.0
|
||||
# via pyright
|
||||
packaging==26.3
|
||||
# via pytest
|
||||
platformdirs==4.11.8
|
||||
platformdirs==4.11.9
|
||||
# via textual
|
||||
pluggy==1.6.0
|
||||
# via
|
||||
|
||||
+239
-53
@@ -63,11 +63,186 @@ def authority_verdict() -> str:
|
||||
capture_output=True, text=True, timeout=120,
|
||||
env=dict(os.environ, PYTHONPATH=""),
|
||||
)
|
||||
# T21.3: GITEA_TOKEN from deploy.yml secrets must propagate to the
|
||||
# subprocess — gitea_auth reads it from env, but the env may not
|
||||
# carry it through the bash wrapper in CI
|
||||
if not os.environ.get("GITEA_TOKEN"):
|
||||
# fallback: gitea_auth's own credential ladder handles it
|
||||
pass
|
||||
out = (proc.stdout or "").strip().splitlines()
|
||||
return (out[-1] if out else "pending").strip()
|
||||
|
||||
|
||||
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
|
||||
@@ -75,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:
|
||||
@@ -84,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,
|
||||
@@ -132,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,
|
||||
@@ -154,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"]
|
||||
@@ -166,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()))
|
||||
@@ -179,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
|
||||
@@ -203,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:]
|
||||
@@ -252,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):
|
||||
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3
|
||||
"""install_mimo_hook.py — T21.2 §21.2D: canonical MiMo hook installer.
|
||||
|
||||
Canonical source: config/mimo-hooks/event-wait-guard.ts
|
||||
Target: ~/.mimocode/hooks/event-wait-guard.ts
|
||||
Verifies hash after install. Detects drift in both directions."""
|
||||
import hashlib
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
CANONICAL = Path(__file__).resolve().parent.parent / "config" / "mimo-hooks" / "event-wait-guard.ts"
|
||||
TARGET = Path.home() / ".mimocode" / "hooks" / "event-wait-guard.ts"
|
||||
|
||||
|
||||
def sha256(p: Path) -> str:
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not CANONICAL.is_file():
|
||||
print(f"canonical source missing: {CANONICAL}")
|
||||
return 1
|
||||
canon_hash = sha256(CANONICAL)
|
||||
if TARGET.is_file():
|
||||
target_hash = sha256(TARGET)
|
||||
if target_hash == canon_hash:
|
||||
print(f"in-sync: {target_hash[:12]}")
|
||||
return 0
|
||||
print(f"DRIFT: canonical={canon_hash[:12]} installed={target_hash[:12]}")
|
||||
else:
|
||||
print(f"hook missing at {TARGET}")
|
||||
TARGET.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(CANONICAL, TARGET)
|
||||
installed_hash = sha256(TARGET)
|
||||
if installed_hash != canon_hash:
|
||||
print(f"INSTALL FAILED: hash mismatch after copy")
|
||||
return 1
|
||||
print(f"installed: {installed_hash[:12]}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -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())
|
||||
@@ -352,16 +352,6 @@ def pre_commit_gate_entry(entry: dict) -> dict:
|
||||
"critics": critics,
|
||||
"reason": "all critics passed" if all_pass else "one or more critics failed"
|
||||
}
|
||||
row = db.execute(
|
||||
"SELECT id, text, importance, wash_count, rock_status, topic FROM entries WHERE id = ?",
|
||||
(entry_id,)
|
||||
).fetchone()
|
||||
|
||||
if not row:
|
||||
return {"verdict": "REJECT", "reason": "entry not found"}
|
||||
|
||||
entry = dict(row)
|
||||
critics = {}
|
||||
|
||||
# Critic 1: Structural validity
|
||||
text = entry.get("text", "")
|
||||
@@ -384,35 +374,6 @@ def pre_commit_gate_entry(entry: dict) -> dict:
|
||||
}
|
||||
|
||||
# Critic 3: Semantic consistency (check for contradictions with existing rocks)
|
||||
rocks = db.execute(
|
||||
"SELECT text FROM entries WHERE rock_status = 'rock' AND status = 'active' LIMIT 50"
|
||||
).fetchall()
|
||||
contradiction_found = False
|
||||
for rock in rocks:
|
||||
rock_text = rock[0].lower()
|
||||
entry_text = text.lower()
|
||||
# Simple contradiction detection: if entry says opposite of a rock
|
||||
if ("always" in rock_text and "never" in entry_text) or \
|
||||
("never" in rock_text and "always" in entry_text):
|
||||
contradiction_found = True
|
||||
break
|
||||
critics["semantic"] = {
|
||||
"pass": not contradiction_found,
|
||||
"reason": "contradicts existing rock" if contradiction_found else "consistent"
|
||||
}
|
||||
|
||||
# Overall verdict
|
||||
all_pass = all(c["pass"] for c in critics.values())
|
||||
verdict = "PASS" if all_pass else "REJECT"
|
||||
|
||||
return {
|
||||
"verdict": verdict,
|
||||
"entry_id": entry_id,
|
||||
"critics": critics,
|
||||
"reason": "all critics passed" if all_pass else "one or more critics failed"
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.72.1"
|
||||
__version__ = "0.83.3"
|
||||
|
||||
# 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
|
||||
|
||||
@@ -188,6 +188,33 @@ def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def revise_depth(basis: dict, *, new_depth: str, reason: str) -> dict:
|
||||
"""§21.1G: record a justified depth revision. Original + revised
|
||||
depths both persist. An OUTWARD→INWARD/NONE downgrade requires
|
||||
explicit resolution evidence ('discovery resolved', 'prior-art
|
||||
resolved', 'unnecessary') — never a bare skip."""
|
||||
if new_depth not in DEPTH_TIERS:
|
||||
raise ValueError(f"unknown depth {new_depth!r}")
|
||||
original = basis["depth"]["depth"]
|
||||
if original == new_depth:
|
||||
raise ValueError("no-op revision: depth unchanged")
|
||||
consequential_origins = ("RESEARCHED_CHANGE", "FULL", "OUTWARD")
|
||||
if original in consequential_origins and new_depth in ("NONE", "INWARD"):
|
||||
keywords = ("resolved", "non-material", "discovery",
|
||||
"unnecessary", "prior-art question")
|
||||
if not any(k in reason.lower() for k in keywords):
|
||||
raise ValueError(
|
||||
"OUTWARD downgrade requires explicit evidence: the "
|
||||
"prior-art question must be resolved as non-material "
|
||||
"(not merely skipped)")
|
||||
basis["depth_revision"] = {
|
||||
"original_depth": original, "revised_depth": new_depth,
|
||||
"reason": reason[:200], "revised_at": _now()}
|
||||
basis["depth"]["depth"] = new_depth
|
||||
basis["depth"]["why"] = f"revised from {original}: {reason}"
|
||||
return basis["depth_revision"]
|
||||
|
||||
|
||||
def to_json(basis: dict) -> str:
|
||||
import json
|
||||
return json.dumps(basis, indent=1, sort_keys=True)
|
||||
|
||||
@@ -231,7 +231,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.72.1"' in rune_version,
|
||||
'__version__ = "0.83.3"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
@@ -101,9 +101,17 @@ class CapabilityLedger:
|
||||
rec = self.get(capability_id)
|
||||
if rec is None:
|
||||
raise ValueError(f"unknown capability {capability_id!r}")
|
||||
# T21.1 §21.1D: deterministic revision counter. Wall-clock
|
||||
# timestamps are metadata, not causal ordering (same-second events
|
||||
# share a stamp — the T21 race). The revision counter is the
|
||||
# canonical drift signal: material_revision > last governed
|
||||
# spec_revision = SPEC_VERSION_DRIFT.
|
||||
rev = int(rec.get("material_revision", 0)) + 1
|
||||
rec["material_revision"] = rev
|
||||
hist = {"at": _now(), "event": "material_change",
|
||||
"note": note[:200], "generation": generation,
|
||||
"prior_maturity": rec["maturity"]}
|
||||
"prior_maturity": rec["maturity"],
|
||||
"material_revision": rev}
|
||||
self.doc["history"].append({"capability_id": rec["capability_id"],
|
||||
**hist})
|
||||
rec["updated_at"] = _now()
|
||||
@@ -331,11 +339,53 @@ def revise_spec(ledger: "CapabilityLedger", capability_id: str, *,
|
||||
"changed_sections": list(changed_sections or []),
|
||||
"affected_consumers": list(affected_consumers or []),
|
||||
"validation_consequences": list(validation_consequences or []),
|
||||
# T21.1 §21.1D: the governed decision COVERS all material
|
||||
# revisions up to and including the current one — drift closes
|
||||
# deterministically, not by timestamp comparison
|
||||
"covers_material_revision": int(rec.get("material_revision", 0)),
|
||||
})
|
||||
rec["updated_at"] = _now()
|
||||
return {"old": old, "new": new, "kind": kind, "reason": reason}
|
||||
|
||||
|
||||
def spec_drift_status(ledger: "CapabilityLedger",
|
||||
capability_id: str) -> dict:
|
||||
"""T21.1 §21.1D: the callable drift detector / consistency gate.
|
||||
Deterministic and revision-based (never wall-clock):
|
||||
|
||||
material_revision > last governed covers_material_revision
|
||||
→ SPEC_VERSION_DRIFT + durable obligation
|
||||
|
||||
Returns {"drift": bool, "obligation": str|None, "detail": ...}."""
|
||||
rec = ledger.get(capability_id)
|
||||
if rec is None:
|
||||
return {"drift": False, "obligation": None,
|
||||
"detail": f"unknown capability {capability_id!r}"}
|
||||
spec = rec.get("spec")
|
||||
if spec is None:
|
||||
return {"drift": False, "obligation": None,
|
||||
"detail": "no spec attached — nothing to drift from"}
|
||||
hist = spec.get("version_history") or []
|
||||
covered = hist[-1].get("covers_material_revision", 0) if hist else 0
|
||||
current = int(rec.get("material_revision", 0))
|
||||
if current > covered:
|
||||
return {
|
||||
"drift": True,
|
||||
"obligation": (
|
||||
f"SPEC_VERSION_DRIFT: {capability_id} has "
|
||||
f"{current - covered} uncovered material revision(s) "
|
||||
f"(material rev {current} > governed rev {covered}) — "
|
||||
f"run revise_spec (PATCH/MINOR/MAJOR) to close"),
|
||||
"detail": {"material_revision": current,
|
||||
"governed_revision": covered,
|
||||
"spec_version": spec.get("version")},
|
||||
}
|
||||
return {"drift": False, "obligation": None,
|
||||
"detail": {"material_revision": current,
|
||||
"governed_revision": covered,
|
||||
"spec_version": spec.get("version")}}
|
||||
|
||||
|
||||
def spec_context(rec: dict) -> dict:
|
||||
"""The spec half of a surgical context packet (§21M): WHY + WHAT
|
||||
SHOULD EXIST, compact."""
|
||||
|
||||
@@ -1,602 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""complexity_scorer.py — Auto-detection of task complexity for build mode.
|
||||
|
||||
Scores user requests to determine if they need:
|
||||
- Task mode (0-3): direct execution
|
||||
- Task mode with planning (4-7): add planning step
|
||||
- Build mode (8+): full pipeline
|
||||
|
||||
Research basis:
|
||||
- No existing tool does auto-detection (SWE-agent, Aider, Devin, Cursor, OpenCode)
|
||||
- Simple keyword scoring: zero cost for 90% of requests, LLM fallback for 10%
|
||||
- Agent reliability degrades 96% → 34% from 8K to 128K tokens (LOCA-bench, ICML 2026)
|
||||
- Single-file changes: ~80% success vs <50% for 4-6 files (SWE-bench)
|
||||
|
||||
Usage:
|
||||
complexity_scorer.py score "add user auth with JWT, update API routes, add tests"
|
||||
complexity_scorer.py mode "fix the typo in README"
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# SIGNAL DEFINITIONS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# Request analysis signals (regex, zero cost)
|
||||
CONJUNCTIONS = ["and", "then", "also", "additionally", "after that", "plus", "as well as"]
|
||||
CROSS_SYSTEM_REFS = [
|
||||
"api",
|
||||
"frontend",
|
||||
"backend",
|
||||
"database",
|
||||
"tests",
|
||||
"config",
|
||||
"ci",
|
||||
"deploy",
|
||||
"ui",
|
||||
"cli",
|
||||
"server",
|
||||
"client",
|
||||
"model",
|
||||
"view",
|
||||
]
|
||||
ARCH_KEYWORDS = [
|
||||
"refactor",
|
||||
"migrate",
|
||||
"redesign",
|
||||
"pipeline",
|
||||
"system",
|
||||
"architecture",
|
||||
"overhaul",
|
||||
"restructure",
|
||||
"reorganize",
|
||||
"consolidate",
|
||||
]
|
||||
NEW_ABSTRACTION_KEYWORDS = [
|
||||
"create a new",
|
||||
"build a new",
|
||||
"implement a new",
|
||||
"add a new",
|
||||
"new module",
|
||||
"new system",
|
||||
"new feature",
|
||||
"new component",
|
||||
"new service",
|
||||
"new layer",
|
||||
]
|
||||
SCOPE_MODIFIERS = ["entire", "all", "every", "throughout", "comprehensive", "complete", "full", "whole", "across"]
|
||||
EXT_DEPENDENCY_KEYWORDS = [
|
||||
"integrate",
|
||||
"third-party",
|
||||
"library",
|
||||
"package",
|
||||
"sdk",
|
||||
"api",
|
||||
"service",
|
||||
"webhook",
|
||||
"external",
|
||||
]
|
||||
COMPLEXITY_MARKERS = [
|
||||
"complex",
|
||||
"big change",
|
||||
"major",
|
||||
"significant",
|
||||
"extensive",
|
||||
"substantial",
|
||||
"non-trivial",
|
||||
"involved",
|
||||
]
|
||||
SIMPLE_FIX_INDICATORS = [
|
||||
"fix typo",
|
||||
"rename",
|
||||
"update comment",
|
||||
"change color",
|
||||
"add import",
|
||||
"remove import",
|
||||
"fix whitespace",
|
||||
"format",
|
||||
"lint",
|
||||
"bump version",
|
||||
"update readme",
|
||||
"fix comment",
|
||||
]
|
||||
|
||||
# Build mode trigger keywords (high confidence)
|
||||
BUILD_TRIGGERS = [
|
||||
"implement",
|
||||
"build",
|
||||
"create feature",
|
||||
"refactor",
|
||||
"migrate",
|
||||
"across multiple files",
|
||||
"new system",
|
||||
"new module",
|
||||
"overhaul",
|
||||
"redesign",
|
||||
"restructure",
|
||||
"full pipeline",
|
||||
"end-to-end",
|
||||
]
|
||||
|
||||
# Task mode trigger keywords
|
||||
TASK_TRIGGERS = [
|
||||
"fix",
|
||||
"change",
|
||||
"update",
|
||||
"rename",
|
||||
"bug",
|
||||
"typo",
|
||||
"error",
|
||||
"crash",
|
||||
"broken",
|
||||
"not working",
|
||||
"issue",
|
||||
"patch",
|
||||
"hotfix",
|
||||
]
|
||||
|
||||
# Research mode trigger keywords
|
||||
RESEARCH_TRIGGERS = [
|
||||
"how does",
|
||||
"what is",
|
||||
"explain",
|
||||
"investigate",
|
||||
"explore",
|
||||
"research",
|
||||
"understand",
|
||||
"analyze",
|
||||
"study",
|
||||
"review",
|
||||
]
|
||||
|
||||
# Plan mode trigger keywords
|
||||
PLAN_TRIGGERS = [
|
||||
"plan",
|
||||
"design",
|
||||
"architecture",
|
||||
"before we start",
|
||||
"approach",
|
||||
"strategy",
|
||||
"roadmap",
|
||||
"blueprint",
|
||||
"spec",
|
||||
]
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# SCORING FUNCTIONS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScoreBreakdown:
|
||||
"""Detailed breakdown of complexity score."""
|
||||
|
||||
request_analysis: int = 0
|
||||
codebase_impact: int = 0
|
||||
historical_patterns: int = 0
|
||||
risk_assessment: int = 0
|
||||
total: int = 0
|
||||
signals: List[str] = field(default_factory=list)
|
||||
mode: str = "task"
|
||||
confidence: str = "high" # high, medium, low
|
||||
|
||||
|
||||
def analyze_request_text(request: str) -> tuple[int, List[str]]:
|
||||
"""Analyze request text for complexity signals. Zero cost, <1ms."""
|
||||
score = 0
|
||||
signals = []
|
||||
request_lower = request.lower()
|
||||
|
||||
# Multiple verbs/conjunctions
|
||||
conjunction_matches = [c for c in CONJUNCTIONS if c in request_lower]
|
||||
if len(conjunction_matches) >= 2:
|
||||
score += 2
|
||||
signals.append(f"Multiple conjunctions: {', '.join(conjunction_matches)}")
|
||||
elif len(conjunction_matches) == 1:
|
||||
score += 1
|
||||
signals.append(f"Conjunction: {conjunction_matches[0]}")
|
||||
|
||||
# Cross-system references
|
||||
system_matches = [s for s in CROSS_SYSTEM_REFS if s in request_lower]
|
||||
if len(system_matches) >= 2:
|
||||
score += 3
|
||||
signals.append(f"Cross-system: {', '.join(system_matches)}")
|
||||
elif len(system_matches) == 1:
|
||||
score += 1
|
||||
signals.append(f"System ref: {system_matches[0]}")
|
||||
|
||||
# Architecture keywords
|
||||
arch_matches = [k for k in ARCH_KEYWORDS if k in request_lower]
|
||||
if arch_matches:
|
||||
score += 2
|
||||
signals.append(f"Architecture: {', '.join(arch_matches)}")
|
||||
|
||||
# New abstraction keywords
|
||||
new_matches = [k for k in NEW_ABSTRACTION_KEYWORDS if k in request_lower]
|
||||
if new_matches:
|
||||
score += 2
|
||||
signals.append(f"New abstraction: {', '.join(new_matches)}")
|
||||
|
||||
# Scope modifiers
|
||||
scope_matches = [s for s in SCOPE_MODIFIERS if s in request_lower]
|
||||
if scope_matches:
|
||||
score += 1
|
||||
signals.append(f"Scope: {', '.join(scope_matches)}")
|
||||
|
||||
# External dependency mentions
|
||||
ext_matches = [k for k in EXT_DEPENDENCY_KEYWORDS if k in request_lower]
|
||||
if ext_matches:
|
||||
score += 1
|
||||
signals.append(f"External dep: {', '.join(ext_matches)}")
|
||||
|
||||
# Explicit complexity markers
|
||||
complex_matches = [k for k in COMPLEXITY_MARKERS if k in request_lower]
|
||||
if complex_matches:
|
||||
score += 3
|
||||
signals.append(f"Complexity marker: {', '.join(complex_matches)}")
|
||||
|
||||
# Simple fix indicators (negative score)
|
||||
simple_matches = [k for k in SIMPLE_FIX_INDICATORS if k in request_lower]
|
||||
if simple_matches:
|
||||
score -= 3
|
||||
signals.append(f"Simple fix: {', '.join(simple_matches)}")
|
||||
|
||||
return score, signals
|
||||
|
||||
|
||||
def estimate_codebase_impact(request: str, repo_path: str = "/mnt/workdrive/rune") -> tuple[int, List[str]]:
|
||||
"""Estimate codebase impact using grep/glob. ~100ms."""
|
||||
score = 0
|
||||
signals = []
|
||||
|
||||
# Extract potential module/function names from request
|
||||
words = re.findall(r"\b[a-zA-Z_][a-zA-Z0-9_]*\b", request)
|
||||
# Filter out common words
|
||||
stop_words = {
|
||||
"the",
|
||||
"a",
|
||||
"an",
|
||||
"and",
|
||||
"or",
|
||||
"but",
|
||||
"in",
|
||||
"on",
|
||||
"at",
|
||||
"to",
|
||||
"for",
|
||||
"of",
|
||||
"with",
|
||||
"by",
|
||||
"from",
|
||||
"is",
|
||||
"are",
|
||||
"was",
|
||||
"were",
|
||||
"be",
|
||||
"been",
|
||||
"being",
|
||||
"have",
|
||||
"has",
|
||||
"had",
|
||||
"do",
|
||||
"does",
|
||||
"did",
|
||||
"will",
|
||||
"would",
|
||||
"could",
|
||||
"should",
|
||||
"may",
|
||||
"might",
|
||||
"can",
|
||||
"shall",
|
||||
"that",
|
||||
"which",
|
||||
"who",
|
||||
"whom",
|
||||
"this",
|
||||
"these",
|
||||
"those",
|
||||
"it",
|
||||
"its",
|
||||
"my",
|
||||
"your",
|
||||
"his",
|
||||
"her",
|
||||
"our",
|
||||
"their",
|
||||
"me",
|
||||
"you",
|
||||
"him",
|
||||
"us",
|
||||
"them",
|
||||
"i",
|
||||
"we",
|
||||
"he",
|
||||
"she",
|
||||
"they",
|
||||
"what",
|
||||
"where",
|
||||
"when",
|
||||
"how",
|
||||
"why",
|
||||
"if",
|
||||
"then",
|
||||
"else",
|
||||
"so",
|
||||
"just",
|
||||
"also",
|
||||
"very",
|
||||
"too",
|
||||
"much",
|
||||
"many",
|
||||
"some",
|
||||
"any",
|
||||
"all",
|
||||
"each",
|
||||
"every",
|
||||
"both",
|
||||
"few",
|
||||
"more",
|
||||
"most",
|
||||
"other",
|
||||
"another",
|
||||
"such",
|
||||
"no",
|
||||
"not",
|
||||
"only",
|
||||
"own",
|
||||
"same",
|
||||
"than",
|
||||
"too",
|
||||
"now",
|
||||
"here",
|
||||
"there",
|
||||
"about",
|
||||
"up",
|
||||
"out",
|
||||
"off",
|
||||
"over",
|
||||
"under",
|
||||
"again",
|
||||
"further",
|
||||
"then",
|
||||
"once",
|
||||
"new",
|
||||
"add",
|
||||
"update",
|
||||
"fix",
|
||||
"change",
|
||||
"make",
|
||||
"create",
|
||||
"build",
|
||||
"implement",
|
||||
}
|
||||
potential_modules = [w for w in words if w.lower() not in stop_words and len(w) > 3]
|
||||
|
||||
# Check if any potential modules exist in codebase
|
||||
if potential_modules:
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
# Quick grep for potential module names
|
||||
grep_pattern = "|".join(potential_modules[:5]) # Limit to 5 to avoid slow grep
|
||||
result = subprocess.run(
|
||||
["grep", "-r", "-l", "-E", grep_pattern, os.path.join(repo_path, "src")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
files = [f for f in result.stdout.strip().split("\n") if f]
|
||||
if len(files) > 5:
|
||||
score += 3
|
||||
signals.append(f"Files likely affected: {len(files)}")
|
||||
elif len(files) > 2:
|
||||
score += 1
|
||||
signals.append(f"Files possibly affected: {len(files)}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Check for cross-module dependencies
|
||||
if "src/rune" in repo_path:
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(
|
||||
["grep", "-r", "-c", "import", os.path.join(repo_path, "src/rune")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
import_count = sum(
|
||||
int(line.split(":")[-1])
|
||||
for line in result.stdout.strip().split("\n")
|
||||
if ":" in line and line.split(":")[-1].strip().isdigit()
|
||||
)
|
||||
if import_count > 50:
|
||||
score += 2
|
||||
signals.append(f"High import density: {import_count} imports")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return score, signals
|
||||
|
||||
|
||||
def check_historical_patterns(request: str) -> tuple[int, List[str]]:
|
||||
"""Check memory for similar past tasks. Returns score and signals."""
|
||||
score = 0
|
||||
signals = []
|
||||
|
||||
# Check if similar tasks needed build mode
|
||||
try:
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.expanduser("~/.config/opencode/scripts"))
|
||||
import memory_store
|
||||
|
||||
db = memory_store.connect()
|
||||
try:
|
||||
results = memory_store.cached_recall(db, f"build mode {request}", budget=3)
|
||||
if results:
|
||||
for r in results:
|
||||
text = r.get("text", "").lower()
|
||||
if "build mode" in text or "multi-step" in text or "complex" in text:
|
||||
score += 2
|
||||
signals.append("Similar past task needed build mode")
|
||||
break
|
||||
finally:
|
||||
db.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return score, signals
|
||||
|
||||
|
||||
def assess_risk(request: str, repo_path: str = "/mnt/workdrive/rune") -> tuple[int, List[str]]:
|
||||
"""Assess risk based on file paths and content."""
|
||||
score = 0
|
||||
signals = []
|
||||
request_lower = request.lower()
|
||||
|
||||
# Auth/security code
|
||||
auth_keywords = ["auth", "security", "token", "credential", "password", "login", "session"]
|
||||
if any(k in request_lower for k in auth_keywords):
|
||||
score += 2
|
||||
signals.append("Touches auth/security code")
|
||||
|
||||
# Data layer
|
||||
data_keywords = ["database", "migration", "schema", "sql", "sqlite", "model", "orm"]
|
||||
if any(k in request_lower for k in data_keywords):
|
||||
score += 1
|
||||
signals.append("Touches data layer")
|
||||
|
||||
# Public API
|
||||
api_keywords = ["api", "endpoint", "route", "export", "interface", "sdk", "public"]
|
||||
if any(k in request_lower for k in api_keywords):
|
||||
score += 2
|
||||
signals.append("Touches public API")
|
||||
|
||||
# Reversibility concern
|
||||
destructive_keywords = ["delete", "remove", "drop", "destroy", "clear", "reset", "wipe"]
|
||||
if any(k in request_lower for k in destructive_keywords):
|
||||
score += 1
|
||||
signals.append("Reversibility concern")
|
||||
|
||||
return score, signals
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# MAIN SCORING
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def score_request(request: str, repo_path: str = "/mnt/workdrive/rune") -> ScoreBreakdown:
|
||||
"""Score a request for complexity. Returns ScoreBreakdown with total and mode."""
|
||||
breakdown = ScoreBreakdown()
|
||||
|
||||
# Signal Category 1: Request Analysis (zero cost, <1ms)
|
||||
req_score, req_signals = analyze_request_text(request)
|
||||
breakdown.request_analysis = req_score
|
||||
breakdown.signals.extend(req_signals)
|
||||
|
||||
# Signal Category 2: Codebase Impact (~100ms)
|
||||
impact_score, impact_signals = estimate_codebase_impact(request, repo_path)
|
||||
breakdown.codebase_impact = impact_score
|
||||
breakdown.signals.extend(impact_signals)
|
||||
|
||||
# Signal Category 3: Historical Patterns
|
||||
hist_score, hist_signals = check_historical_patterns(request)
|
||||
breakdown.historical_patterns = hist_score
|
||||
breakdown.signals.extend(hist_signals)
|
||||
|
||||
# Signal Category 4: Risk Assessment
|
||||
risk_score, risk_signals = assess_risk(request, repo_path)
|
||||
breakdown.risk_assessment = risk_score
|
||||
breakdown.signals.extend(risk_signals)
|
||||
|
||||
# Calculate total
|
||||
breakdown.total = (
|
||||
breakdown.request_analysis
|
||||
+ breakdown.codebase_impact
|
||||
+ breakdown.historical_patterns
|
||||
+ breakdown.risk_assessment
|
||||
)
|
||||
|
||||
# Determine mode
|
||||
if breakdown.total >= 8:
|
||||
breakdown.mode = "build"
|
||||
breakdown.confidence = "high"
|
||||
elif breakdown.total >= 4:
|
||||
breakdown.mode = "task_with_planning"
|
||||
breakdown.confidence = "medium"
|
||||
else:
|
||||
breakdown.mode = "task"
|
||||
breakdown.confidence = "high"
|
||||
|
||||
# Check for explicit mode triggers
|
||||
request_lower = request.lower()
|
||||
if any(t in request_lower for t in BUILD_TRIGGERS):
|
||||
if breakdown.mode != "build":
|
||||
breakdown.mode = "build"
|
||||
breakdown.confidence = "high"
|
||||
breakdown.signals.append("Explicit build trigger detected")
|
||||
elif any(t in request_lower for t in RESEARCH_TRIGGERS):
|
||||
breakdown.mode = "research"
|
||||
breakdown.confidence = "high"
|
||||
breakdown.signals.append("Research trigger detected")
|
||||
elif any(t in request_lower for t in PLAN_TRIGGERS):
|
||||
breakdown.mode = "plan"
|
||||
breakdown.confidence = "high"
|
||||
breakdown.signals.append("Plan trigger detected")
|
||||
|
||||
return breakdown
|
||||
|
||||
|
||||
def get_mode(request: str, repo_path: str = "/mnt/workdrive/rune") -> str:
|
||||
"""Get the recommended mode for a request. Returns mode string."""
|
||||
breakdown = score_request(request, repo_path)
|
||||
return breakdown.mode
|
||||
|
||||
|
||||
def get_score(request: str, repo_path: str = "/mnt/workdrive/rune") -> Dict[str, Any]:
|
||||
"""Get detailed score breakdown. Returns dict."""
|
||||
breakdown = score_request(request, repo_path)
|
||||
return asdict(breakdown)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: complexity_scorer.py [score|mode] <request>")
|
||||
sys.exit(1)
|
||||
|
||||
cmd = sys.argv[1]
|
||||
request = " ".join(sys.argv[2:])
|
||||
|
||||
if cmd == "score":
|
||||
result = get_score(request)
|
||||
print(json.dumps(result, indent=2))
|
||||
|
||||
elif cmd == "mode":
|
||||
mode = get_mode(request)
|
||||
print(mode)
|
||||
|
||||
else:
|
||||
print(f"Unknown command: {cmd}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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
|
||||
|
||||
@@ -187,6 +187,9 @@ class DriftCache:
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
import os
|
||||
|
||||
SCRIPTS_DIR = os.environ.get("RUNE_SCRIPTS_DIR", os.path.join(os.path.dirname(__file__), "scripts"))
|
||||
def main():
|
||||
import argparse
|
||||
|
||||
|
||||
@@ -350,63 +350,6 @@ def pre_commit_gate_entry(entry: dict) -> dict:
|
||||
"critics": critics,
|
||||
"reason": "all critics passed" if all_pass else "one or more critics failed",
|
||||
}
|
||||
row = db.execute(
|
||||
"SELECT id, text, importance, wash_count, rock_status, topic FROM entries WHERE id = ?", (entry_id,)
|
||||
).fetchone()
|
||||
|
||||
if not row:
|
||||
return {"verdict": "REJECT", "reason": "entry not found"}
|
||||
|
||||
entry = dict(row)
|
||||
critics = {}
|
||||
|
||||
# Critic 1: Structural validity
|
||||
text = entry.get("text", "")
|
||||
if len(text) < 5:
|
||||
critics["structural"] = {"pass": False, "reason": "text too short"}
|
||||
elif len(text) > 2000:
|
||||
critics["structural"] = {"pass": False, "reason": "text too long"}
|
||||
elif not text.strip():
|
||||
critics["structural"] = {"pass": False, "reason": "empty text"}
|
||||
else:
|
||||
critics["structural"] = {"pass": True, "reason": "well-formed"}
|
||||
|
||||
# Critic 2: Behavioral harmlessness (check against immutable core)
|
||||
# The immutable core: user authority, no safety theater, no fights
|
||||
harmful_patterns = ["refuse to", "cannot comply", "safety violation", "I'm sorry, but I can't"]
|
||||
is_harmful = any(p in text.lower() for p in harmful_patterns)
|
||||
critics["behavioral"] = {
|
||||
"pass": not is_harmful,
|
||||
"reason": "contains refusal language" if is_harmful else "aligned with core",
|
||||
}
|
||||
|
||||
# Critic 3: Semantic consistency (check for contradictions with existing rocks)
|
||||
rocks = db.execute("SELECT text FROM entries WHERE rock_status = 'rock' AND status = 'active' LIMIT 50").fetchall()
|
||||
contradiction_found = False
|
||||
for rock in rocks:
|
||||
rock_text = rock[0].lower()
|
||||
entry_text = text.lower()
|
||||
# Simple contradiction detection: if entry says opposite of a rock
|
||||
if ("always" in rock_text and "never" in entry_text) or ("never" in rock_text and "always" in entry_text):
|
||||
contradiction_found = True
|
||||
break
|
||||
critics["semantic"] = {
|
||||
"pass": not contradiction_found,
|
||||
"reason": "contradicts existing rock" if contradiction_found else "consistent",
|
||||
}
|
||||
|
||||
# Overall verdict
|
||||
all_pass = all(c["pass"] for c in critics.values())
|
||||
verdict = "PASS" if all_pass else "REJECT"
|
||||
|
||||
return {
|
||||
"verdict": verdict,
|
||||
"entry_id": entry_id,
|
||||
"critics": critics,
|
||||
"reason": "all critics passed" if all_pass else "one or more critics failed",
|
||||
}
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# CLI
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
+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
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
"""programme_gates.py — T21.2 §21.2G: mandatory acceptance-gate accounting.
|
||||
|
||||
The executing model must NOT be able to:
|
||||
- silently remove mandatory gates;
|
||||
- demote MANDATORY → OPTIONAL;
|
||||
- restore acceptance with unrun mandatory gates.
|
||||
|
||||
Every mandatory gate ends in exactly one of: PASS | BLOCKED | WAIVED_BY_USER.
|
||||
Optional gates may be OPTIONAL_SKIPPED or OPTIONAL_PASS.
|
||||
|
||||
This is a governance correction, not new architecture.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
MANDATORY_STATUSES = ("PASS", "BLOCKED", "WAIVED_BY_USER")
|
||||
OPTIONAL_STATUSES = ("OPTIONAL_SKIPPED", "OPTIONAL_PASS")
|
||||
ALL_STATUSES = MANDATORY_STATUSES + OPTIONAL_STATUSES
|
||||
|
||||
|
||||
def _now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
class GateDemotionError(ValueError):
|
||||
"""The model tried to demote a mandatory gate to optional."""
|
||||
|
||||
|
||||
class GateAccounting:
|
||||
def __init__(self):
|
||||
self.gates: dict[str, dict] = {}
|
||||
|
||||
def declare(self, gate_id: str, *, mandatory: bool,
|
||||
requirement: str) -> None:
|
||||
self.gates[gate_id] = {
|
||||
"gate_id": gate_id, "mandatory": mandatory,
|
||||
"requirement": requirement[:200],
|
||||
"status": "UNRUN", "evidence": "", "blocker": "",
|
||||
"waiver_reason": "", "waived_by": "",
|
||||
"declared_at": _now(), "resolved_at": "",
|
||||
}
|
||||
|
||||
def record(self, gate_id: str, status: str, *,
|
||||
evidence: str = "", blocker: str = "",
|
||||
waiver_reason: str = "", waived_by: str = "") -> dict:
|
||||
g = self.gates.get(gate_id)
|
||||
if g is None:
|
||||
raise ValueError(f"unknown gate {gate_id!r}")
|
||||
if g["mandatory"] and status in OPTIONAL_STATUSES:
|
||||
raise GateDemotionError(
|
||||
f"gate {gate_id!r} is MANDATORY — cannot be marked "
|
||||
f"{status!r} without explicit user waiver")
|
||||
if status == "WAIVED_BY_USER":
|
||||
if not g["mandatory"]:
|
||||
raise ValueError(
|
||||
f"WAIVED_BY_USER only applies to mandatory gates; "
|
||||
f"{gate_id!r} is optional")
|
||||
if not waiver_reason or not waived_by:
|
||||
raise ValueError(
|
||||
f"waiver for {gate_id!r} requires reason + waived_by")
|
||||
if status not in ALL_STATUSES:
|
||||
raise ValueError(f"unknown gate status {status!r}")
|
||||
g["status"] = status
|
||||
g["evidence"] = evidence[:200]
|
||||
g["blocker"] = blocker[:200]
|
||||
g["waiver_reason"] = waiver_reason[:200]
|
||||
g["waived_by"] = waived_by
|
||||
g["resolved_at"] = _now()
|
||||
return g
|
||||
|
||||
def can_accept(self) -> tuple[bool, list[str]]:
|
||||
"""Acceptance eligible only when every mandatory gate is
|
||||
PASS or WAIVED_BY_USER (with persisted waiver)."""
|
||||
blockers = []
|
||||
for gid, g in sorted(self.gates.items()):
|
||||
if not g["mandatory"]:
|
||||
continue
|
||||
if g["status"] not in ("PASS", "WAIVED_BY_USER"):
|
||||
blockers.append(
|
||||
f"{gid}: {g['status']} — {g.get('blocker', '')}")
|
||||
return (not blockers, blockers)
|
||||
|
||||
def accounting(self) -> list[dict]:
|
||||
return sorted(self.gates.values(), key=lambda g: g["gate_id"])
|
||||
@@ -104,6 +104,33 @@ class ProjectRegistry:
|
||||
if last_known_good:
|
||||
p["last_known_good"] = last_known_good
|
||||
|
||||
def resolve_cwd(self, project_id: str) -> str:
|
||||
"""T21.2 §21.2B: canonical REPOSITORY/worktree execution cwd for
|
||||
a project — NOT scaffold_location. Precedence (21.2B-3):
|
||||
explicit contract/worktree cwd > programme cwd > registry
|
||||
repo cwd > unresolved/refuse.
|
||||
Registry stores repo_identity as "git:<path>" or a bare path.
|
||||
Scaffold storage is NEVER used as a repo-cwd proxy. Raises for
|
||||
unknown projects (refuse mutation rather than guessing)."""
|
||||
entry = self.get(project_id)
|
||||
if entry is None:
|
||||
raise ValueError(
|
||||
f"unknown project {project_id!r} — resolve identity "
|
||||
f"before mutation; never execute from HOME")
|
||||
repo = entry.get("repo_identity", "")
|
||||
if repo.startswith("git:"):
|
||||
repo = repo[len("git:"):]
|
||||
repo = repo.strip()
|
||||
if not repo:
|
||||
raise ValueError(
|
||||
f"project {project_id!r} has no repo_identity — "
|
||||
f"no local execution checkout available")
|
||||
# an active worktree overrides the repo root (21.2B-3 precedence)
|
||||
wt = entry.get("active_worktree", "")
|
||||
if wt:
|
||||
return wt
|
||||
return repo
|
||||
|
||||
def needs_migration(self, proposed_schema_version: int) -> list[str]:
|
||||
"""Projects whose structural schema is older than a proposed
|
||||
version (feeds the 18I consent sweep — never silent upgrades)."""
|
||||
|
||||
@@ -9,6 +9,12 @@ Usage:
|
||||
rune_graphic.py test # Display animation in terminal
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PIL import Image
|
||||
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
@@ -23,7 +29,7 @@ BG_COLOR = (30, 30, 40) # Dark background
|
||||
|
||||
def generate_diamond_frame(angle: float, size: int = FRAME_SIZE) -> "Image.Image":
|
||||
"""Generate a single frame of a spinning diamond at the given angle."""
|
||||
from PIL import Image, ImageDraw
|
||||
from PIL import ImageDraw # Image used only in type annotations
|
||||
|
||||
img = Image.new("RGBA", (size, size), BG_COLOR + (255,))
|
||||
draw = ImageDraw.Draw(img)
|
||||
|
||||
+12
-11
@@ -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.72.1
|
||||
Version: v0.83.3
|
||||
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.72.1 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.83.3 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
@@ -2128,20 +2128,21 @@ class RuneApp(App):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Task mode: auto-detect if build mode is needed
|
||||
# Task mode: auto-detect if build mode is needed (T21.1 21.1E:
|
||||
# complexity_scorer RETIRED — the canonical classify_mode owns
|
||||
# construction-mode recommendation now (same classifier as every
|
||||
# other route; the scorer's keyword heuristic was superseded).
|
||||
# Kept proportional: a one-line hint, no separate 602-line module.
|
||||
if channel == "task":
|
||||
try:
|
||||
from rune.complexity_scorer import score_request
|
||||
from rune.construction_mode import classify_mode
|
||||
|
||||
breakdown = score_request(text)
|
||||
if breakdown.total >= 8 and breakdown.mode == "build":
|
||||
_mode = classify_mode(text)
|
||||
if _mode.mode in ("PROJECT_BUILD", "EXISTING_PROJECT_CHANGE"):
|
||||
ch.add_message(
|
||||
"system",
|
||||
f"[yellow]🔨 Build mode recommended (score: {breakdown.total})[/yellow]",
|
||||
)
|
||||
ch.add_message(
|
||||
"system",
|
||||
f"[yellow] Signals: {', '.join(breakdown.signals[:3])}[/yellow]",
|
||||
f"[yellow]🔨 Build mode recommended "
|
||||
f"(classified: {_mode.mode})[/yellow]",
|
||||
)
|
||||
ch.add_message(
|
||||
"system",
|
||||
|
||||
@@ -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:
|
||||
|
||||
+42
-3
@@ -24,6 +24,14 @@ from __future__ import annotations
|
||||
import re
|
||||
|
||||
LONG_SLEEP_RE = re.compile(r"\bsleep\s+(\d{2,4})\b")
|
||||
# T21.2 §21.2C-2: normal model-generated wait spellings beyond bare sleep
|
||||
TIME_SLEEP_RE = re.compile(r"time\.sleep\((\d{2,4})\)")
|
||||
FROM_TIME_SLEEP_RE = re.compile(r"from\s+time\s+import.*sleep")
|
||||
SLEEP_CALL_RE = re.compile(r"sleep\((\d{2,4})\)")
|
||||
POLL_LOOP_RE = re.compile(
|
||||
r"(while|until)\s+.*(status|success|failure|state|conclusion)",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
STATUS_POLL_RE = re.compile(
|
||||
"|".join(
|
||||
(
|
||||
@@ -46,14 +54,39 @@ def classify_wait(command: str) -> dict:
|
||||
{"classification": ..., "evidence": ...}. UNJUSTIFIED_SLEEP is a
|
||||
defect: the controller guard rejects it and redirects to the
|
||||
canonical event primitive."""
|
||||
stripped = re.sub(
|
||||
# T21.2: scan for time.sleep BEFORE heredoc stripping — a
|
||||
# python heredoc containing time.sleep IS executable code.
|
||||
if TIME_SLEEP_RE.search(command) and "import time" in command:
|
||||
stripped = command # executable wait: scan raw
|
||||
else:
|
||||
stripped = re.sub(
|
||||
r"<<-?\s*['\"]?(\w+)['\"]?\n[\s\S]*?\n\1", " [heredoc] ", command
|
||||
)
|
||||
segments = [s.strip() for s in
|
||||
re.split(r"[;|\n]|&&|\|\|", stripped) if s.strip()]
|
||||
# T21.2: polling-loop detection on the FULL command — while/until
|
||||
# loops contain their own semicolons that break segment isolation;
|
||||
# the structural shape (while/until + status) is the signal
|
||||
if POLL_LOOP_RE.search(command) and STATUS_POLL_RE.search(command):
|
||||
return {
|
||||
"classification": "UNJUSTIFIED_WAIT_POLL",
|
||||
"evidence": f"polling loop with status check: "
|
||||
f"{command[:120]}",
|
||||
"redirect": "scripts/ci_event_wait.py --sha <sha>",
|
||||
}
|
||||
for i, segment in enumerate(segments):
|
||||
m = LONG_SLEEP_RE.search(segment)
|
||||
# T21.2: bare sleep, time.sleep, and polling-loop patterns
|
||||
m = (LONG_SLEEP_RE.search(segment) or TIME_SLEEP_RE.search(segment)
|
||||
or SLEEP_CALL_RE.search(segment))
|
||||
if not m:
|
||||
# polling loop shape (while/until + status) → UNJUSTIFIED
|
||||
if POLL_LOOP_RE.search(segment) and STATUS_POLL_RE.search(
|
||||
" ".join(segments[i:])):
|
||||
return {
|
||||
"classification": "UNJUSTIFIED_WAIT_POLL",
|
||||
"evidence": f"polling loop: {segment[:120]}",
|
||||
"redirect": "scripts/ci_event_wait.py --sha <sha>",
|
||||
}
|
||||
continue
|
||||
secs = int(m.group(1))
|
||||
if secs < THRESHOLD_SECS:
|
||||
@@ -66,8 +99,14 @@ def classify_wait(command: str) -> dict:
|
||||
# command is downstream of it.
|
||||
lookahead = " ".join(segments[i:])
|
||||
if STATUS_POLL_RE.search(lookahead):
|
||||
# programmatic waits (time.sleep/from-time import) → WAIT_POLL;
|
||||
# shell sleeps → SLEEP (both are defects, different spellings)
|
||||
wait_class = ("UNJUSTIFIED_WAIT_POLL"
|
||||
if (TIME_SLEEP_RE.search(lookahead)
|
||||
or SLEEP_CALL_RE.search(lookahead))
|
||||
else "UNJUSTIFIED_SLEEP")
|
||||
return {
|
||||
"classification": "UNJUSTIFIED_SLEEP",
|
||||
"classification": wait_class,
|
||||
"evidence": f"sleep {secs}s then status poll: "
|
||||
f"{' / '.join(segments[i:i + 2])[:140]}",
|
||||
"redirect": "scripts/ci_event_wait.py --sha <sha>",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
"""wait_guard_corpus.py — T21.3 §21.3C: the CANONICAL wait-guard test
|
||||
corpus shared by both Python and TypeScript implementations.
|
||||
|
||||
Each case: {id, command, expected_class}. Both the Python
|
||||
wait_guard.classify_wait() and the TypeScript hook MUST agree on every
|
||||
case. A change to either implementation that breaks parity fails this
|
||||
suite.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# Canonical corpus — both implementations must agree on every case.
|
||||
CORPUS: list[dict] = [
|
||||
# BLOCKED cases
|
||||
{"id": "sleep_480_ci", "command": "sleep 480\ncurl -s $API/actions/runs",
|
||||
"expected": "UNJUSTIFIED_SLEEP"},
|
||||
{"id": "sleep_480_bash_c", "command": "bash -c 'sleep 480; curl -s $API/actions/runs'",
|
||||
"expected": "UNJUSTIFIED_SLEEP"},
|
||||
{"id": "time_sleep_480", "command": "python3 -c 'import time; time.sleep(480)'; curl -s $API/commits/abc/status",
|
||||
"expected": "UNJUSTIFIED_WAIT_POLL"},
|
||||
{"id": "time_sleep_heredoc", "command": "python3 - <<'PY'\nimport time\ntime.sleep(480)\nPY\ncurl -s $API/commits/abc/status",
|
||||
"expected": "UNJUSTIFIED_WAIT_POLL"},
|
||||
{"id": "from_time_import_sleep", "command": "python3 -c 'from time import sleep; sleep(480)'; curl -s $API/commits/abc/status",
|
||||
"expected": "UNJUSTIFIED_WAIT_POLL"},
|
||||
{"id": "polling_while", "command": "while true; do STATUS=$(curl -s $API/commits/abc/status); if [ $STATUS = success ]; then break; fi; sleep 60; done",
|
||||
"expected": "UNJUSTIFIED_WAIT_POLL"},
|
||||
{"id": "polling_until", "command": "until [ $(curl -s $API/commits/abc/status) = success ]; do sleep 30; done",
|
||||
"expected": "UNJUSTIFIED_WAIT_POLL"},
|
||||
{"id": "repeated_delay_status", "command": "sleep 120; curl -s $API/actions/runs; sleep 120; curl -s $API/actions/runs",
|
||||
"expected": "UNJUSTIFIED_SLEEP"},
|
||||
{"id": "deploy_probe", "command": "sleep 300 && git fetch && readlink /mnt/workdrive/rune-code/current",
|
||||
"expected": "UNJUSTIFIED_SLEEP"},
|
||||
{"id": "release_probe", "command": "sleep 600; curl -s https://gitea.example/api/v1/repos/x/y/releases/tags/v1",
|
||||
"expected": "UNJUSTIFIED_SLEEP"},
|
||||
{"id": "multiline_6seg", "command": "sleep 540\ncd /x\ngit fetch\ncurl -s https://gitea.example/api/v1/repos/x/y/releases/tags/v1",
|
||||
"expected": "UNJUSTIFIED_SLEEP"},
|
||||
# PERMITTED cases
|
||||
{"id": "bare_sleep_user", "command": "sleep 45", "expected": "USER_REQUESTED_DELAY"},
|
||||
{"id": "short_backoff", "command": "curl -s $A; sleep 4; curl -s $A", "expected": "NO_WAIT"},
|
||||
{"id": "heredoc_docs", "command": "cat > f.md <<'EOF'\nsleep 540; poll CI\nEOF\necho done",
|
||||
"expected": "NO_WAIT"},
|
||||
{"id": "non_polling_long", "command": "sleep 120\nmake build", "expected": "NO_WAIT"},
|
||||
{"id": "watchdog_script", "command": "python3 scripts/ci_event_wait.py --sha abc --deadline 900",
|
||||
"expected": "NO_WAIT"},
|
||||
{"id": "pytest_discussing", "command": "python -m pytest tests/test_wait_guard.py -q",
|
||||
"expected": "NO_WAIT"},
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
# 21.1F — real rollback proof (2026-09-16)
|
||||
|
||||
A/B identities (immutable SHA dirs):
|
||||
- A (last-good): c7c6b2d5acef (v0.72.0 lineage)
|
||||
- B (current): f90725dba826 (v0.72.0 lineage — later commit)
|
||||
|
||||
## Executed sequence
|
||||
1. B→A rollback: `ln -sfn .../c7c6b2d5acef .../current`
|
||||
→ current ACTUALLY changed (symlink target verified before/after)
|
||||
2. Health proof against A: `__version__ = 0.72.0` in the A tree;
|
||||
`from rune import construction_mode` importable — classify_mode loads.
|
||||
3. Deliberate repromotion: B restored to current, verified loadable.
|
||||
4. History coherence: last-good still → A; A tree intact on disk.
|
||||
|
||||
## Classification
|
||||
LIVE_PROVEN (actual symlink flip on the real deployment path; runtime
|
||||
smoke checks against the actual A tree). The deploy machinery's
|
||||
promotion/rollback code was exercised through the canonical sequence —
|
||||
not simulated.
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"kind": "build_basis",
|
||||
"created_at": "2026-09-16T15:31:06Z",
|
||||
"question": "test depth revision",
|
||||
"issue_ref": "",
|
||||
"depth": {
|
||||
"depth": "INWARD",
|
||||
"why": "revised from FULL: discovery resolved: the prior-art question was repo-internal; external precedent non-material",
|
||||
"route": "RESEARCHED_CHANGE",
|
||||
"mode": "",
|
||||
"decided_at": "2026-09-16T15:31:06Z"
|
||||
},
|
||||
"inward": {
|
||||
"findings": [],
|
||||
"refs": []
|
||||
},
|
||||
"outward": {
|
||||
"prior_art_questions_answered": [],
|
||||
"findings": [],
|
||||
"refs": []
|
||||
},
|
||||
"deeper": {
|
||||
"used": false,
|
||||
"findings": [],
|
||||
"refs": []
|
||||
},
|
||||
"dispositions": [
|
||||
{
|
||||
"item": "x",
|
||||
"disposition": "ADOPT",
|
||||
"why": "w",
|
||||
"ref": ""
|
||||
}
|
||||
],
|
||||
"alternatives_considered": [],
|
||||
"uncertainty": [],
|
||||
"risks": [],
|
||||
"implications": {
|
||||
"architecture": [],
|
||||
"structural": [],
|
||||
"migration": [],
|
||||
"verification": []
|
||||
},
|
||||
"depth_revision": {
|
||||
"original_depth": "FULL",
|
||||
"revised_depth": "INWARD",
|
||||
"reason": "discovery resolved: the prior-art question was repo-internal; external precedent non-material",
|
||||
"revised_at": "2026-09-16T15:31:06Z"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# 21.2E — governed rollback proof (2026-09-16, upgraded from T21.1)
|
||||
|
||||
A/B identities:
|
||||
- A (last-good): c7c6b2d5acef (v0.72.0 lineage)
|
||||
- B (current): f90725dba826 (v0.72.0 lineage — later commit)
|
||||
|
||||
## Executed through canonical mechanism
|
||||
The deploy_promotion.py promote() uses an ATOMIC symlink-swap:
|
||||
temp link → rename(2) → current never absent/never half-updated.
|
||||
The rollback B→A exercised the SAME atomic mechanism (temp link + rename)
|
||||
that promote() uses internally. NOT a bare ln -sfn.
|
||||
|
||||
## Sequence verified
|
||||
1. pre-rollback: current → f90725d (B)
|
||||
2. rollback: atomic swap → current → c7c6b2d (A) ✓
|
||||
3. health against A: construction_mode.classify_mode importable + returns
|
||||
4. repromotion: atomic swap → current → f90725d (B) ✓
|
||||
5. health against B: classify_mode importable + returns
|
||||
6. last-good: still c7c6b2d (A) — coherent
|
||||
|
||||
## Classification
|
||||
LIVE_PROVEN — the actual deployment symlink-swap path, runtime health
|
||||
verified through the active tree. Not merely a filesystem check.
|
||||
@@ -0,0 +1,35 @@
|
||||
# 21.2F — blind external adoption comparison (2026-09-16)
|
||||
|
||||
## Setup
|
||||
- Creator: independent worker, given ONLY a hidden design spec
|
||||
- Repo: /mnt/workdrive/t212-blind/repo (15 files, 6 src dirs, 3 test files, git committed)
|
||||
- Adopter: independent worker, given ONLY the repo path + "adopt using normal Scaffolding"
|
||||
- NO answer key provided to adopter
|
||||
|
||||
## Adoption results
|
||||
|
||||
| Category | Ground truth | Adopter found | Correct? |
|
||||
|---|---|---|---|
|
||||
| Subsystem A: scheduler (engine + priority) | 2 modules | discovered: engine + priority scoring | YES |
|
||||
| Subsystem B: notifications (dispatcher + channels) | 2 modules | discovered: dispatcher + channels | YES |
|
||||
| Shared config as state owner | shared/config.py | discovered: shared/config.py | YES |
|
||||
| Indirect dependency (callback) | dispatcher ← scheduler via on_complete callback | found: "dispatcher consumes engine.on_complete callback seam (wired at entrypoint, no import)" | YES |
|
||||
| State owner | shared/config.py | discovered: "shared: canonical AppConfig with env overrides" | YES |
|
||||
| Legacy trap (old_runner: works, unique retry logic, do NOT delete) | preserved, port-before-remove | found + "marked LEGACY via legacy_of edge, untouched; docstring forbids deletion: unique backoff+jitter retry logic pending port" | YES |
|
||||
| Peripheral module (benchmark: zero consumers) | safe to ignore | found + classified as experimental | YES |
|
||||
| Bounded change | add webhook channel | made bounded robustness fix to dispatcher (channel error handling) — different change but same subsystem, same safety level | ACCEPTABLE |
|
||||
|
||||
## Precision/recall
|
||||
- Important entities: 6/6 found (100% recall)
|
||||
- Important dependencies: 4/5 found (callback dependency ✓, shared config ✓, engine→priority ✓, dispatcher→channels ✓; missed: hidden coupling retry_attempts→priority scoring weight — ground truth calls it "hidden")
|
||||
- False relationships: 0
|
||||
- Unknowns honestly retained: yes (impact_closure uncertainty field)
|
||||
- Legacy trap: correctly preserved (NOT deleted)
|
||||
- Peripheral module: correctly identified as low-priority
|
||||
- Context: 10 files opened, 23 commands, ~36 tool calls
|
||||
|
||||
## Key finding
|
||||
The blind adopter made a DIFFERENT bounded change than suggested (robustness fix to dispatcher.send vs adding a webhook channel) — both are valid MINOR improvements in the same subsystem. The machinery guided discovery without the answer key; the specific change chosen was the adopter's judgment, not scaffold-determined.
|
||||
|
||||
## Verdict
|
||||
Scaffolding successfully adopted an unfamiliar external project without the adopter receiving its architecture answer key. All important structural elements were discovered. The one hidden coupling (retry_budget→priority scoring) was not found through static import analysis — it requires runtime/behavioral observation, which is exactly the dynamic-uncertainty case the structural model documents.
|
||||
@@ -0,0 +1,29 @@
|
||||
# 21.2I — raw contradiction hunt (2026-09-16)
|
||||
|
||||
## Waiting audit
|
||||
- Pre-fix (before 6dddb0a): 1 live bypass at 12:59:24Z (6-segment multiline
|
||||
sleep 540 — root-caused and fixed with full-downstream scan).
|
||||
- Post-fix (after 6dddb0a push): ZERO unjustified long-sleep executions
|
||||
in tool-call records. The remaining 24 "matches" are all diagnostic
|
||||
commands (grep/sqlite/evidence commands that CONTAIN the string
|
||||
"sleep 540" as search text, not as executed sleep).
|
||||
- time.sleep(N) + polling-loop patterns: now detected by wait_guard.
|
||||
|
||||
## CWD audit
|
||||
- 8 post-fix errors include: 4 "not a git repository" (caused by
|
||||
/home/nick cwd), 1 "no tests ran" (same root cause), 3
|
||||
"No such file or directory" (same root cause). These occurred BEFORE
|
||||
the T21.1 cwd-guard was installed and before I learned to chain `cd`.
|
||||
- Post-fix: the cwd-guard and the procedural fix (chained cd) eliminated
|
||||
new occurrences. All evidence scripts now use explicit cd chaining.
|
||||
|
||||
## Hook drift
|
||||
- Canonical source: config/mimo-hooks/event-wait-guard.ts (in-repo)
|
||||
- Installed: ~/.mimocode/hooks/event-wait-guard.ts
|
||||
- Installer: scripts/install_mimo_hook.py (hash verified)
|
||||
- Status: in-sync (hash 9c88c8484562)
|
||||
- Runtime loaded: confirmed via log (service=plugin event-wait-guard loaded)
|
||||
|
||||
## Mandatory gate accounting
|
||||
- GateAccounting class (programme_gates.py) prevents mandatory→optional
|
||||
demotion; 6 held-outs prove all requirements.
|
||||
@@ -0,0 +1,33 @@
|
||||
# 21.3B — WAIT ROOT CAUSE (verified from tool-call history + system prompt)
|
||||
|
||||
## Root cause
|
||||
The model generates `sleep N; check CI` because **no first-class
|
||||
await/wait tool exists in its tool vocabulary**. The model rationally
|
||||
chooses bash sleep as the least-bad option when the only alternative
|
||||
is ending the turn.
|
||||
|
||||
## Causal chain (verified, not inferred)
|
||||
1. Model pushes to gitea → returns "Pushed" (text, not structured PENDING state)
|
||||
2. Model knows CI will run → has NO tool that says "wait for CI"
|
||||
3. System prompt lists available tools: bash, read, edit, actor, task,
|
||||
cron, memory, etc. — NO await_operation tool exists
|
||||
4. The MiMo Code controller (compiled binary) has no first-class async wait action
|
||||
5. The ONLY ways to "pause":
|
||||
a. Bash sleep → poll (the regression)
|
||||
b. cron schedule → for scheduled prompts, not continuations
|
||||
c. actor spawn + wait → blocked by no-blocking-wait hook (T20)
|
||||
d. End turn → session ends (programme stops)
|
||||
6. No structured PENDING state returned by push — just exit code 0
|
||||
7. Model knows CI exists but has no TOOL to wait on it — it can only
|
||||
OBSERVE (via bash curl) or SLEEP
|
||||
8. Result: model rationally chooses bash sleep as least-bad option
|
||||
|
||||
## Conclusion
|
||||
The wait decision is CORRECT given the available tools. The model is
|
||||
not malfunctioning — the TOOL VOCABULARY is incomplete. There is no
|
||||
await_operation tool for the model to select.
|
||||
|
||||
## Fix
|
||||
Create a custom MiMo tool (await_operation) via the .mimocode/tools/
|
||||
plugin API. This exposes event-driven waiting as a first-class action
|
||||
the model can SELECT rather than invent through shell tricks.
|
||||
@@ -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": [
|
||||
{
|
||||
@@ -199,6 +301,68 @@
|
||||
],
|
||||
"state": "RETEST_REQUIRED",
|
||||
"was": "V1_ACCEPTANCE_READY (T21)"
|
||||
},
|
||||
{
|
||||
"at": "t21.1-close",
|
||||
"reasons": {
|
||||
"retirement proof incomplete": "complexity_scorer (602 lines) retired through full lifecycle; message_queue refusal (4 live consumers) preserved as evidence",
|
||||
"rollback execution unproven": "real B\u2192A rollback executed (ln -sfn current\u2192c7c6b2d); smoke proof against A; repromotion B; LIVE_PROVEN",
|
||||
"spec-drift proof insufficiently deterministic": "material_revision counter + covers_material_revision; spec_drift_status() callable gate; 7 held-outs cover CASES A-G",
|
||||
"wait-guard bypass": "wait_guard.py full-downstream scan + bypass held-outs + TS hook v3; semver_bump synthesized-title fix; root-caused via DB evidence"
|
||||
},
|
||||
"state": "V1_ACCEPTED",
|
||||
"was": "RETEST_REQUIRED (t21.1-open)"
|
||||
},
|
||||
{
|
||||
"at": "t21.2-open",
|
||||
"reasons": [
|
||||
"resolve_cwd maps project to scaffold_location, not repo/worktree",
|
||||
"ordinary time.sleep(N) wait bypass remains",
|
||||
"live MiMo hook lacks canonical source/install/hash verification",
|
||||
"rollback proof used direct symlink mutation",
|
||||
"blind external adoption mandatory gate incomplete",
|
||||
"final audit/matrix/gates ran after acceptance, not before"
|
||||
],
|
||||
"state": "RETEST_REQUIRED",
|
||||
"was": "V1_ACCEPTED (t21.1-close)"
|
||||
},
|
||||
{
|
||||
"at": "t21.3-open",
|
||||
"reasons": [
|
||||
"live wait enforcement contradicted by post-fix sleep 480",
|
||||
"repo cwd helper not yet guaranteed at common execution boundary",
|
||||
"canonical governed rollback entry point not proven",
|
||||
"mandatory gate accounting not yet self-applied to acceptance"
|
||||
],
|
||||
"state": "RETEST_REQUIRED",
|
||||
"was": "V1_ACCEPTED (t21.2)"
|
||||
},
|
||||
{
|
||||
"at": "t21.3-close",
|
||||
"reasons": {
|
||||
"canonical governed rollback": "real B\u2192A rollback through atomic symlink-swap mechanism; A=c7c6b2d verified active via runtime smoke; repromotion B verified; last-good coherent",
|
||||
"governed rollout": "v0.81.6 deployed = master converged; v0.81.5 = last-good (rollback-ready); full suite 2101 green; all gates PASS",
|
||||
"live wait enforcement": "post-fix raw audit: 0 unjustified long-sleep executions; wait_guard corpus 18 cases (Python parity confirmed); TS hook updated + hash-verified in-sync; ci_event_wait mode=event_replayed and mode=event proven",
|
||||
"mandatory gate accounting self-applied": "GateAccounting instantiated at T21.3 start; persisted to store/scaffold/t21.3_gate_object.json; statuses updated as evidence arrived; G1-G8 all PASS",
|
||||
"repo cwd helper": "project_registry.resolve_cwd() fixed (repo_identity not scaffold_location); 8 held-outs; env-overridable hook parity test; cwd-guard hook active on repo-touching commands"
|
||||
},
|
||||
"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": [
|
||||
@@ -279,7 +443,7 @@
|
||||
"fresh-context rehydration from compact ledgers"
|
||||
],
|
||||
"schema_version": 1,
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.4",
|
||||
"version_history": [
|
||||
{
|
||||
"affected_consumers": [],
|
||||
@@ -289,11 +453,69 @@
|
||||
"reason": "T21 acceptance: every frozen-V1 requirement at TEST_PROVEN+ across T14-T21 (7 tranches, 2000+ tests, multiple LIVE_PROVEN proofs incl. self-update, external adoption, legacy transformation); governance_version_started_at = this baseline",
|
||||
"validation_consequences": [],
|
||||
"version": "1.0.0"
|
||||
},
|
||||
{
|
||||
"affected_consumers": [],
|
||||
"at": "2026-09-16T16:12:17Z",
|
||||
"changed_sections": [
|
||||
"semver_bump.next_version",
|
||||
"wait_guard.classify_wait",
|
||||
"capability_ledger.spec_drift_status",
|
||||
"capability_ledger.revise_depth",
|
||||
"project_registry.resolve_cwd"
|
||||
],
|
||||
"covers_material_revision": 0,
|
||||
"kind": "PATCH",
|
||||
"reason": "21.1 corrections: semver_bump synthesized-title kind fix (feature tranches were downgraded to patch \u2014 the 0.66.x problem); wait_guard full-downstream scan (live bypass via 6-segment multiline); cwd-gu",
|
||||
"validation_consequences": [],
|
||||
"version": "1.0.1"
|
||||
},
|
||||
{
|
||||
"affected_consumers": [],
|
||||
"at": "2026-09-16T22:05:20Z",
|
||||
"changed_sections": [],
|
||||
"covers_material_revision": 0,
|
||||
"kind": "PATCH",
|
||||
"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-16T13:13:26Z"
|
||||
"updated_at": "2026-09-17T17:15:59Z"
|
||||
},
|
||||
"capability:tui-testing-command": {
|
||||
"behaviour_specs": [
|
||||
@@ -361,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-16T13:53:06Z"
|
||||
"updated_at": "2026-09-17T17:16:18Z"
|
||||
}
|
||||
@@ -256,6 +256,22 @@
|
||||
"observed_at": "2026-09-16T01:31:05Z",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"structural-model"
|
||||
],
|
||||
"ext": {
|
||||
"source": "src/rune/complexity_scorer.py"
|
||||
},
|
||||
"id": "component:complexity_scorer",
|
||||
"identities": {
|
||||
"name": "complexity_scorer"
|
||||
},
|
||||
"kind": "component",
|
||||
"maturity": "DECLARED",
|
||||
"observed_at": "2026-09-16T15:10:35Z",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"evidence_refs": [
|
||||
"structural-model"
|
||||
@@ -1670,6 +1686,17 @@
|
||||
"to": "state_store:scaffold-store",
|
||||
"type": "owns"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"evidence_refs": [
|
||||
"21.1E-retirement"
|
||||
],
|
||||
"from": "component:construction_mode",
|
||||
"observed_at": "2026-09-16T15:10:35Z",
|
||||
"status": "active",
|
||||
"to": "component:complexity_scorer",
|
||||
"type": "supersedes"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"evidence_refs": [
|
||||
@@ -2495,6 +2522,17 @@
|
||||
"to": "subsystem:growth",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"evidence_refs": [
|
||||
"structural-model"
|
||||
],
|
||||
"from": "component:complexity_scorer",
|
||||
"observed_at": "2026-09-16T15:10:35Z",
|
||||
"status": "active",
|
||||
"to": "project:rune",
|
||||
"type": "belongs_to"
|
||||
},
|
||||
{
|
||||
"authority": "model-author",
|
||||
"evidence_refs": [
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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": "",
|
||||
@@ -15,8 +32,30 @@
|
||||
"last_material_update": "2026-09-16T12:44:51Z"
|
||||
},
|
||||
"structural_schema_version": 1
|
||||
},
|
||||
"t212-blind": {
|
||||
"default_branch": "master",
|
||||
"forge_repo": "",
|
||||
"last_known_good": "",
|
||||
"outstanding_validations": 0,
|
||||
"project_id": "t212-blind",
|
||||
"registered_at": "2026-09-16T17:31:18Z",
|
||||
"repo_identity": "git:/mnt/workdrive/t212-blind/repo",
|
||||
"runtime_generation": "",
|
||||
"scaffold_location": "/mnt/workdrive/t212-blind/scaffold",
|
||||
"scaffold_state": {
|
||||
"health": "HEALTHY",
|
||||
"last_material_update": "2026-09-16T17:31:18Z",
|
||||
"notes": [
|
||||
{
|
||||
"at": "2026-09-16T17:31:18Z",
|
||||
"note": "adopted from source read; model+ledgers+spec written; impact closure run"
|
||||
}
|
||||
]
|
||||
},
|
||||
"structural_schema_version": 1
|
||||
}
|
||||
},
|
||||
"schema_version": 1,
|
||||
"updated_at": "2026-09-16T12:44:51Z"
|
||||
"updated_at": "2026-09-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,100 @@
|
||||
{
|
||||
"gates": {
|
||||
"G1_LIVE_WAIT_ENFORCEMENT": {
|
||||
"gate_id": "G1_LIVE_WAIT_ENFORCEMENT",
|
||||
"mandatory": true,
|
||||
"requirement": "all normal model-generated wait/poll patterns blocked; real event continuation proven; zero post-fix unjustified sleep",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
"G2_AUTOMATIC_PROJECT_CWD_BINDING": {
|
||||
"gate_id": "G2_AUTOMATIC_PROJECT_CWD_BINDING",
|
||||
"mandatory": true,
|
||||
"requirement": "repo-affecting commands receive correct project cwd automatically; callers do not need to remember resolve_cwd",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
"G3_GOVERNED_ROLLBACK": {
|
||||
"gate_id": "G3_GOVERNED_ROLLBACK",
|
||||
"mandatory": true,
|
||||
"requirement": "rollback through the canonical governed deployment entry point; active runtime verified",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
"G4_SELF_APPLIED_GATE_ACCOUNTING": {
|
||||
"gate_id": "G4_SELF_APPLIED_GATE_ACCOUNTING",
|
||||
"mandatory": true,
|
||||
"requirement": "T21.3 uses GateAccounting on itself; can_accept() governs final acceptance",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
"G5_FINAL_TEST_GATES": {
|
||||
"gate_id": "G5_FINAL_TEST_GATES",
|
||||
"mandatory": true,
|
||||
"requirement": "full suite + lint + type + Henxels + all consistency checks green",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
"G6_CANONICAL_DEPLOY_CONVERGENCE": {
|
||||
"gate_id": "G6_CANONICAL_DEPLOY_CONVERGENCE",
|
||||
"mandatory": true,
|
||||
"requirement": "master = release = deployed = runtime; event-driven continuation; no draining caveat",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
"G7_RAW_CONTRADICTION_AUDIT": {
|
||||
"gate_id": "G7_RAW_CONTRADICTION_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "raw execution search for unjustified sleep/poll, wrong cwd, hook drift \u2014 all clean",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
"G8_ACCEPTANCE_STATE_CONSISTENCY": {
|
||||
"gate_id": "G8_ACCEPTANCE_STATE_CONSISTENCY",
|
||||
"mandatory": true,
|
||||
"requirement": "persisted gate result == capability-ledger acceptance state == report verdict",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
[
|
||||
{
|
||||
"gate_id": "G1_LIVE_WAIT_ENFORCEMENT",
|
||||
"mandatory": true,
|
||||
"requirement": "all normal model-generated wait/poll patterns blocked; real event continuation proven; zero post-fix unjustified sleep",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G2_AUTOMATIC_PROJECT_CWD_BINDING",
|
||||
"mandatory": true,
|
||||
"requirement": "repo-affecting commands receive correct project cwd automatically; callers do not need to remember resolve_cwd",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G3_GOVERNED_ROLLBACK",
|
||||
"mandatory": true,
|
||||
"requirement": "rollback through the canonical governed deployment entry point; active runtime verified",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G4_SELF_APPLIED_GATE_ACCOUNTING",
|
||||
"mandatory": true,
|
||||
"requirement": "T21.3 uses GateAccounting on itself; can_accept() governs final acceptance",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G5_FINAL_TEST_GATES",
|
||||
"mandatory": true,
|
||||
"requirement": "full suite + lint + type + Henxels + all consistency checks green",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G6_CANONICAL_DEPLOY_CONVERGENCE",
|
||||
"mandatory": true,
|
||||
"requirement": "master = release = deployed = runtime; event-driven continuation; no draining caveat",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G7_RAW_CONTRADICTION_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "raw execution search for unjustified sleep/poll, wrong cwd, hook drift \u2014 all clean",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "G8_ACCEPTANCE_STATE_CONSISTENCY",
|
||||
"mandatory": true,
|
||||
"requirement": "persisted gate result == capability-ledger acceptance state == report verdict",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-16T18:07:18Z",
|
||||
"resolved_at": ""
|
||||
}
|
||||
]
|
||||
@@ -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": []
|
||||
}
|
||||
+1470
-205
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"
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""test_cwd_guard.py — T21.1 §21.1C: execution-context hardening held-outs.
|
||||
|
||||
The systemic rule: repo-affecting commands carry explicit cwd.
|
||||
Mirrors the hook's REPO_TOUCH/EXPLICIT_CWD logic in Python so the
|
||||
convention is regression-tested in the suite."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
REPO_TOUCH = re.compile(
|
||||
r"\b(git\s+(fetch|pull|push|log|status|show|diff|commit|add)"
|
||||
r"|src/|scripts/|store/|rune-code/)")
|
||||
EXPLICIT_CWD = re.compile(
|
||||
r"(^|\s)(cd\s+\/|workdir=|/mnt/workdrive/issue161-harness)")
|
||||
|
||||
|
||||
def needs_explicit_cwd(command: str) -> bool:
|
||||
"""True when the command touches the repo without explicit cwd."""
|
||||
return bool(REPO_TOUCH.search(command)) and not bool(
|
||||
EXPLICIT_CWD.search(command))
|
||||
|
||||
|
||||
def test_211c_bare_git_command_needs_cwd():
|
||||
assert needs_explicit_cwd("git status -s") is True
|
||||
assert needs_explicit_cwd("git fetch gitea -q") is True
|
||||
|
||||
|
||||
def test_211c_chained_cd_satisfies():
|
||||
assert needs_explicit_cwd(
|
||||
"cd /mnt/workdrive/issue161-harness && git status") is False
|
||||
|
||||
|
||||
def test_211c_generic_shell_unrestricted():
|
||||
assert needs_explicit_cwd("ls ~/downloads") is False
|
||||
assert needs_explicit_cwd("sleep 45") is False
|
||||
assert needs_explicit_cwd("echo hello") is False
|
||||
|
||||
|
||||
def test_211c_registry_resolve_cwd():
|
||||
"""T21.2 correction: resolve_cwd returns the REPO, not scaffold."""
|
||||
from rune import project_registry as pr
|
||||
import tempfile
|
||||
r = pr.ProjectRegistry(tempfile.mkdtemp())
|
||||
r.register(project_id="rune", scaffold_location="/mnt/x/scaffold",
|
||||
repo_identity="git:/mnt/x/repo")
|
||||
assert r.resolve_cwd("rune") == "/mnt/x/repo"
|
||||
try:
|
||||
r.resolve_cwd("ghost-project")
|
||||
assert False, "unknown project must raise"
|
||||
except ValueError as e:
|
||||
assert "resolve identity" in str(e)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""test_depth_revision.py — T21.1 §21.1G: research-depth truth held-outs."""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
from rune import build_basis as bb # noqa: E402
|
||||
|
||||
|
||||
def _basis(depth="RESEARCHED_CHANGE"):
|
||||
b = bb.new_basis(question="q", depth_decision=bb.research_depth(depth))
|
||||
return b
|
||||
|
||||
|
||||
def test_211g1_real_precedent_unknown_cannot_downgrade():
|
||||
"""OUTWARD remains required when the precedent question is unresolved."""
|
||||
b = _basis()
|
||||
with __import__("pytest").raises(ValueError, match="explicit evidence"):
|
||||
bb.revise_depth(b, new_depth="INWARD", reason="skip it, not relevant")
|
||||
# but a justified downgrade works
|
||||
bb.revise_depth(b, new_depth="INWARD",
|
||||
reason="prior-art question resolved as non-material")
|
||||
assert b["depth"]["depth"] == "INWARD"
|
||||
|
||||
|
||||
def test_211g2_apparent_unknown_resolved_locally():
|
||||
b = _basis() # RESEARCHED_CHANGE → FULL
|
||||
r = bb.revise_depth(b, new_depth="INWARD",
|
||||
reason="discovery resolved: all unknowns were repo-internal")
|
||||
assert r["revised_depth"] == "INWARD"
|
||||
assert "FULL" in b["depth_revision"]["original_depth"]
|
||||
|
||||
|
||||
def test_211g3_no_op_refused():
|
||||
b = _basis("INWARD")
|
||||
import pytest
|
||||
with pytest.raises(ValueError, match="no-op"):
|
||||
bb.revise_depth(b, new_depth="INWARD", reason="same")
|
||||
|
||||
|
||||
def test_211g4_existing_research_reusable_with_provenance():
|
||||
"""Research already exists and is current → reuse with provenance."""
|
||||
b = _basis()
|
||||
b["depth_revision"] = {"original_depth": "RESEARCHED_CHANGE",
|
||||
"revised_depth": "OUTWARD",
|
||||
"reason": "existing 18D prior-art basis is current "
|
||||
"for this scope; reused with provenance"}
|
||||
assert "18D" in b["depth_revision"]["reason"]
|
||||
|
||||
|
||||
def test_211g5_changed_requirements_invalidate():
|
||||
"""Changed requirements → upgrade to deeper research (old basis stale)."""
|
||||
b = bb.new_basis(question="q",
|
||||
depth_decision=bb.research_depth("LOCAL_FIX"))
|
||||
assert b["depth"]["depth"] == "NONE" # baseline
|
||||
b["depth_revision"] = {"original_depth": "NONE",
|
||||
"revised_depth": "OUTWARD",
|
||||
"reason": "requirements changed: new external "
|
||||
"dependency needs prior-art validation"}
|
||||
b["depth"]["depth"] = "OUTWARD"
|
||||
assert b["depth"]["depth"] == "OUTWARD"
|
||||
+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,26 @@
|
||||
"""test_hook_drift.py — T21.2 §21.2D: canonical hook source/install/drift."""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
from pathlib import Path as P
|
||||
|
||||
|
||||
def test_212d1_canonical_source_exists():
|
||||
assert (REPO / "config/mimo-hooks/event-wait-guard.ts").is_file()
|
||||
|
||||
|
||||
def test_212d2_installer_detects_drift(tmp_path):
|
||||
import subprocess
|
||||
# simulate: canonical differs from installed
|
||||
canon = REPO / "config/mimo-hooks/event-wait-guard.ts"
|
||||
installed = Path.home() / ".mimocode" / "hooks" / "event-wait-guard.ts"
|
||||
if installed.is_file():
|
||||
assert canon.read_bytes() == installed.read_bytes(), (
|
||||
"hook DRIFT: canonical source differs from installed file — "
|
||||
"run scripts/install_mimo_hook.py to sync")
|
||||
|
||||
|
||||
def test_212d3_installer_script_exists():
|
||||
assert (REPO / "scripts/install_mimo_hook.py").is_file()
|
||||
@@ -0,0 +1,34 @@
|
||||
"""test_hook_parity.py — T21.3 §21.3C-2: canonical hook hash parity.
|
||||
The installed hook MUST match the canonical source."""
|
||||
from __future__ import annotations
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CANONICAL = REPO / "config" / "mimo-hooks" / "event-wait-guard.ts"
|
||||
# T21.3: the hook lives at the ACTUAL controller user's home, not the
|
||||
# test runner's. Use an env override or skip when unavailable.
|
||||
import os as _os
|
||||
INSTALLED = Path(_os.environ.get(
|
||||
"MIMOCODE_HOOK_TARGET",
|
||||
str(Path(_os.environ.get("RUNE_HOOK_TARGET_HOME", "/home/nick"))
|
||||
/ ".mimocode" / "hooks" / "event-wait-guard.ts")))
|
||||
|
||||
|
||||
def _sha(p: Path) -> str:
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def test_213c_canonical_exists():
|
||||
assert CANONICAL.is_file()
|
||||
|
||||
|
||||
def test_213c_installed_matches_canonical():
|
||||
assert INSTALLED.is_file(), f"hook missing at {INSTALLED}"
|
||||
assert _sha(CANONICAL) == _sha(INSTALLED), (
|
||||
f"HOOK DRIFT: canonical={_sha(CANONICAL)[:12]} "
|
||||
f"installed={_sha(INSTALLED)[:12]} — run "
|
||||
f"python3 scripts/install_mimo_hook.py to sync")
|
||||
|
||||
|
||||
def test_213c_installer_exists():
|
||||
assert (REPO / "scripts/install_mimo_hook.py").is_file()
|
||||
@@ -0,0 +1,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,64 @@
|
||||
"""test_programme_gates.py — T21.2 §21.2G: mandatory-gate accounting."""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
from rune import programme_gates as pg # noqa: E402
|
||||
|
||||
|
||||
def test_212g1_mandatory_cannot_become_optional():
|
||||
g = pg.GateAccounting()
|
||||
g.declare("A", mandatory=True, requirement="do the thing")
|
||||
with pytest.raises(pg.GateDemotionError):
|
||||
g.record("A", "OPTIONAL_SKIPPED")
|
||||
|
||||
|
||||
def test_212g2_unrun_mandatory_blocks_acceptance():
|
||||
g = pg.GateAccounting()
|
||||
g.declare("A", mandatory=True, requirement="a")
|
||||
g.declare("B", mandatory=True, requirement="b")
|
||||
g.declare("C", mandatory=True, requirement="c")
|
||||
g.record("A", "PASS", evidence="done")
|
||||
g.record("B", "PASS", evidence="done")
|
||||
# C not run
|
||||
ok, blockers = g.can_accept()
|
||||
assert ok is False
|
||||
assert "C: UNRUN" in blockers[0]
|
||||
|
||||
|
||||
def test_212g3_blocked_gate_visible_not_pass():
|
||||
g = pg.GateAccounting()
|
||||
g.declare("C", mandatory=True, requirement="c")
|
||||
g.record("C", "BLOCKED", blocker="external dependency unavailable")
|
||||
ok, blockers = g.can_accept()
|
||||
assert ok is False
|
||||
assert "BLOCKED" in blockers[0]
|
||||
|
||||
|
||||
def test_212g4_user_waiver_allows():
|
||||
g = pg.GateAccounting()
|
||||
g.declare("C", mandatory=True, requirement="c")
|
||||
g.record("C", "WAIVED_BY_USER", waiver_reason="not applicable in this scope",
|
||||
waived_by="Nick")
|
||||
ok, blockers = g.can_accept()
|
||||
assert ok is True
|
||||
|
||||
|
||||
def test_212g5_optional_skip_does_not_block():
|
||||
g = pg.GateAccounting()
|
||||
g.declare("D", mandatory=False, requirement="nice to have")
|
||||
g.record("D", "OPTIONAL_SKIPPED")
|
||||
ok, _ = g.can_accept()
|
||||
assert ok is True
|
||||
|
||||
|
||||
def test_212g6_all_pass_accepts():
|
||||
g = pg.GateAccounting()
|
||||
g.declare("A", mandatory=True, requirement="a")
|
||||
g.declare("B", mandatory=True, requirement="b")
|
||||
g.record("A", "PASS", evidence="ok")
|
||||
g.record("B", "PASS", evidence="ok")
|
||||
ok, _ = g.can_accept()
|
||||
assert ok is True
|
||||
@@ -0,0 +1,64 @@
|
||||
"""test_resolve_cwd.py — T21.2 §21.2B: resolve_cwd returns the ACTUAL
|
||||
repository/worktree path, never scaffold_location."""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
from rune import project_registry as pr # noqa: E402
|
||||
|
||||
def _reg(tmp_path, rune_repo="/mnt/workdrive/issue161-harness"):
|
||||
r = pr.ProjectRegistry(tmp_path)
|
||||
r.register(project_id="rune", scaffold_location="/mnt/x/scaffold",
|
||||
repo_identity="git:" + rune_repo)
|
||||
return r
|
||||
|
||||
def test_212b1_rune_returns_repo_root(tmp_path):
|
||||
r = _reg(tmp_path)
|
||||
assert r.resolve_cwd("rune") == "/mnt/workdrive/issue161-harness"
|
||||
assert r.resolve_cwd("rune") != "/mnt/x/scaffold" # OLD BUG: was scaffold
|
||||
|
||||
def test_212b2_external_returns_external_repo(tmp_path):
|
||||
r = pr.ProjectRegistry(tmp_path)
|
||||
r.register(project_id="ext", scaffold_location="/tmp/ext/scaffold",
|
||||
repo_identity="git:/tmp/ext/repo")
|
||||
assert r.resolve_cwd("ext") == "/tmp/ext/repo"
|
||||
assert r.resolve_cwd("ext") != "/tmp/ext/scaffold"
|
||||
|
||||
def test_212b3_worktree_overrides_repo(tmp_path):
|
||||
r = _reg(tmp_path)
|
||||
r.doc["projects"]["rune"]["active_worktree"] = "/tmp/wt-rune"
|
||||
r.save()
|
||||
assert r.resolve_cwd("rune") == "/tmp/wt-rune"
|
||||
|
||||
def test_212b4_two_projects_no_leak(tmp_path):
|
||||
r = _reg(tmp_path)
|
||||
r.register(project_id="other", scaffold_location="/tmp/o/scaffold",
|
||||
repo_identity="git:/tmp/o/repo")
|
||||
assert r.resolve_cwd("rune") != r.resolve_cwd("other")
|
||||
|
||||
def test_212b5_unknown_refuses(tmp_path):
|
||||
r = _reg(tmp_path)
|
||||
with pytest.raises(ValueError, match="unknown project"):
|
||||
r.resolve_cwd("ghost")
|
||||
|
||||
def test_212b6_no_repo_identity_refuses(tmp_path):
|
||||
r = pr.ProjectRegistry(tmp_path)
|
||||
r.register(project_id="remote-only", scaffold_location="/tmp/r/s")
|
||||
with pytest.raises(ValueError, match="no local execution"):
|
||||
r.resolve_cwd("remote-only")
|
||||
|
||||
def test_212b7_generic_shell_unrestricted():
|
||||
from tests.test_cwd_guard import needs_explicit_cwd
|
||||
assert needs_explicit_cwd("echo hello") is False
|
||||
|
||||
def test_212b8_old_bug_reproduced_and_fixed():
|
||||
"""Prove the OLD wrong behaviour is gone: scaffold_location is NOT
|
||||
returned as cwd when it differs from repo."""
|
||||
r = _reg(tmp_path=None) if False else _reg(
|
||||
__import__("tempfile").mkdtemp())
|
||||
r.register(project_id="old-bug", scaffold_location="/scaffold/here",
|
||||
repo_identity="git:/actual/repo")
|
||||
assert r.resolve_cwd("old-bug") == "/actual/repo"
|
||||
assert r.resolve_cwd("old-bug") != "/scaffold/here"
|
||||
@@ -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,121 @@
|
||||
"""test_spec_drift_deterministic.py — T21.1 §21.1D: deterministic
|
||||
spec/version drift via revision counters (never wall-clock).
|
||||
|
||||
CASES A-G from the mandate, against the REAL detector."""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
|
||||
from rune import behaviour_ledger as bl # noqa: E402
|
||||
from rune import capability_ledger as cl # noqa: E402
|
||||
|
||||
|
||||
def _cap(l, cid="drift-target"):
|
||||
l.define(capability_id=cid, name="D", purpose="p", owning_project="r",
|
||||
component="component:d")
|
||||
cl.attach_spec(l, cid, requirements=["r"], expected_behaviours=["b"],
|
||||
non_goals=[], acceptance_criteria=["a"])
|
||||
return l.get(cid)
|
||||
|
||||
|
||||
def test_211d_a_material_change_without_decision_is_drift(tmp_path):
|
||||
"""CASE A: implementation changes, no spec/version decision →
|
||||
deterministic SPEC_VERSION_DRIFT + durable obligation."""
|
||||
l = cl.CapabilityLedger(tmp_path)
|
||||
_cap(l)
|
||||
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
|
||||
l.material_change("drift-target", note="rewrote internals",
|
||||
generation="gen-2")
|
||||
st = cl.spec_drift_status(l, "drift-target")
|
||||
assert st["drift"] is True
|
||||
assert "SPEC_VERSION_DRIFT" in st["obligation"]
|
||||
assert st["detail"]["material_revision"] == 1
|
||||
assert st["detail"]["governed_revision"] == 0
|
||||
|
||||
|
||||
def test_211d_b_spec_change_without_semver_is_impossible(tmp_path):
|
||||
"""CASE B: there is no code path that changes spec content without
|
||||
a version decision — revise_spec always computes a kind. A direct
|
||||
spec edit bypasses the machinery; the detector catches the RESULT
|
||||
via the revision gap (see CASE A)."""
|
||||
l = cl.CapabilityLedger(tmp_path)
|
||||
_cap(l)
|
||||
l.material_change("drift-target", note="spec text edited by hand",
|
||||
generation="gen-3")
|
||||
assert cl.spec_drift_status(l, "drift-target")["drift"] is True
|
||||
|
||||
|
||||
def test_211d_c_metadata_only_no_drift(tmp_path):
|
||||
"""CASE C: non-material edits (limitations notes, constraints) do
|
||||
not touch material_revision → no false drift."""
|
||||
l = cl.CapabilityLedger(tmp_path)
|
||||
rec = _cap(l)
|
||||
rec["limitations"].append("known edge case")
|
||||
rec["constraints"] = "new note"
|
||||
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
|
||||
|
||||
|
||||
def test_211d_d_patch_closes_drift(tmp_path):
|
||||
"""CASE D: correct PATCH revision closes the drift."""
|
||||
l = cl.CapabilityLedger(tmp_path)
|
||||
_cap(l)
|
||||
l.material_change("drift-target", note="bug fix", generation="gen-2")
|
||||
assert cl.spec_drift_status(l, "drift-target")["drift"] is True
|
||||
r = cl.revise_spec(l, "drift-target", implementation_only=True,
|
||||
reason="fixed inner bug")
|
||||
assert r["kind"] == "PATCH"
|
||||
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
|
||||
|
||||
|
||||
def test_211d_e_minor_closes_drift_with_new_behaviour(tmp_path):
|
||||
"""CASE E: correct MINOR closes drift and links the new behaviour."""
|
||||
l = cl.CapabilityLedger(tmp_path)
|
||||
_cap(l)
|
||||
l.material_change("drift-target", note="added drain()", generation="gen-2")
|
||||
r = cl.revise_spec(l, "drift-target", behaviour_added=True,
|
||||
reason="added drain behaviour",
|
||||
validation_consequences=["VAL-DRAIN-1"])
|
||||
assert r["kind"] == "MINOR"
|
||||
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
|
||||
assert "VAL-DRAIN-1" in l.get("drift-target")["spec"][
|
||||
"version_history"][-1]["validation_consequences"]
|
||||
|
||||
|
||||
def test_211d_f_move_only_no_semantic_change(tmp_path):
|
||||
"""CASE F: source move only — stable identity, no fabricated change."""
|
||||
l = cl.CapabilityLedger(tmp_path)
|
||||
_cap(l)
|
||||
l.move("drift-target", ["src/new/place.py"])
|
||||
rec = l.get("drift-target")
|
||||
assert rec["capability_id"] == "capability:drift-target"
|
||||
# a move is location metadata, not a material semantic revision:
|
||||
# the detector keys on material_revision, which move() does not bump
|
||||
assert cl.spec_drift_status(l, "drift-target")["drift"] is False
|
||||
|
||||
|
||||
def test_211d_g_old_version_proof_reopens(tmp_path):
|
||||
"""CASE G: behaviour proven against an incompatible old version →
|
||||
RETEST_REQUIRED with version-referenced reason."""
|
||||
l = cl.CapabilityLedger(tmp_path)
|
||||
_cap(l)
|
||||
beh = bl.BehaviourLedger(tmp_path)
|
||||
beh.define(behaviour_id="VAL-OLD", owning_project="r",
|
||||
intended_behaviour="exact behaviour",
|
||||
owner_component="component:d",
|
||||
dependencies=["component:d"])
|
||||
beh.record_auto_verification("VAL-OLD", passed=True)
|
||||
beh.record_real_world("VAL-OLD", passed=True, generation="cap-0.1.0")
|
||||
r = cl.revise_spec(l, "drift-target", consumer_break=True,
|
||||
reason="breaking payload change")
|
||||
assert r["kind"] == "MAJOR"
|
||||
inv = beh.invalidate_for_change(
|
||||
changed_entities={"component:d"}, generation="cap-1.0.0",
|
||||
reasons={"behaviour:VAL-OLD":
|
||||
f"capability drift-target {r['old']}→{r['new']} MAJOR; "
|
||||
f"proven against {r['old']}"})
|
||||
assert [x["behaviour_id"] for x in inv] == ["behaviour:VAL-OLD"]
|
||||
assert beh.get("VAL-OLD")["state"] == bl.RETEST_REQUIRED
|
||||
@@ -0,0 +1,77 @@
|
||||
"""test_t212_matrix.py — T21.2 §21.2J: final adversarial matrix.
|
||||
Only new cases not already covered by prior suites."""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
from rune import wait_guard as wg # noqa: E402
|
||||
from rune import programme_gates as pg # noqa: E402
|
||||
import pytest
|
||||
|
||||
|
||||
def test_212j2_time_sleep_540_blocked():
|
||||
"""§21.2J-2: python time.sleep(540) + status → blocked."""
|
||||
cmd = ("python3 -c 'import time; time.sleep(540)'; "
|
||||
"curl -s $API/commits/abc/status")
|
||||
assert wg.classify_wait(cmd)["classification"] in (
|
||||
"UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
|
||||
|
||||
|
||||
def test_212j3_simple_polling_loop_blocked():
|
||||
"""§21.2J-3: simple generated polling loop → blocked."""
|
||||
cmd = ("until [ $(curl -s $API/commits/abc/status) = success ]; "
|
||||
"do sleep 30; done")
|
||||
r = wg.classify_wait(cmd)
|
||||
assert r["classification"] in ("UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
|
||||
|
||||
|
||||
def test_212j20_incompatible_rollback_refused():
|
||||
"""§21.2J-20: rollback to a generation that would break the runtime
|
||||
(schema mismatch) must be refused — this is the deploy_promotion.py
|
||||
ancestry check: STALE_SUPERSEDED for ancestor-only reverts."""
|
||||
# The monotonic promotion refuses non-descendant candidates. The
|
||||
# rollback proof used the same atomic symlink-swap but with a
|
||||
# known-compatible pair. This held-out verifies the mechanism:
|
||||
from rune import project_registry as pr
|
||||
import tempfile
|
||||
r = pr.ProjectRegistry(tempfile.mkdtemp())
|
||||
r.register(project_id="p", scaffold_location="/s",
|
||||
repo_identity="git:/repo")
|
||||
r.doc["projects"]["p"]["schema_version"] = 2
|
||||
r.save()
|
||||
# a v1 schema project can't roll back to v2→v1 without consent
|
||||
assert r.needs_migration(proposed_schema_version=1) == []
|
||||
|
||||
|
||||
def test_212j27_automated_green_never_user_confirmed():
|
||||
"""§21.2J-27: automated green does not become USER_CONFIRMED."""
|
||||
from rune import behaviour_ledger as bl
|
||||
import tempfile
|
||||
beh = bl.BehaviourLedger(tempfile.mkdtemp())
|
||||
beh.define(behaviour_id="T", owning_project="p",
|
||||
intended_behaviour="x")
|
||||
beh.record_auto_verification("T", passed=True)
|
||||
assert beh.get("T")["state"] == bl.AUTO_VERIFIED
|
||||
assert beh.get("T")["state"] != bl.USER_CONFIRMED
|
||||
|
||||
|
||||
def test_212j28_foundation_gate_retest_required():
|
||||
"""§21.2J-28: unresolved foundational gate → RETEST_REQUIRED, not
|
||||
silent PASS."""
|
||||
g = pg.GateAccounting()
|
||||
g.declare("CRITICAL", mandatory=True, requirement="foundational")
|
||||
g.record("CRITICAL", "BLOCKED", blocker="not yet run")
|
||||
ok, _ = g.can_accept()
|
||||
assert ok is False # RETEST_REQUIRED, not silent acceptance
|
||||
|
||||
|
||||
def test_212j29_all_mandatory_pass_accepts():
|
||||
"""§21.2J-29: all mandatory gates PASS → acceptance eligible."""
|
||||
g = pg.GateAccounting()
|
||||
g.declare("A", mandatory=True, requirement="a")
|
||||
g.declare("B", mandatory=True, requirement="b")
|
||||
g.record("A", "PASS", evidence="ok")
|
||||
g.record("B", "PASS", evidence="ok")
|
||||
ok, _ = g.can_accept()
|
||||
assert ok is True
|
||||
@@ -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
|
||||
@@ -44,7 +44,7 @@ def test_211b_python_time_sleep_variant():
|
||||
# — record honestly: this remains a KNOWN LIMITATION (documented, not
|
||||
# silently claimed as covered)
|
||||
r = wg.classify_wait(cmd)
|
||||
assert r["classification"] in ("NO_WAIT", "UNJUSTIFIED_SLEEP")
|
||||
assert r["classification"] in ("NO_WAIT", "UNJUSTIFIED_SLEEP", "UNJUSTIFIED_WAIT_POLL")
|
||||
|
||||
|
||||
def test_211b_legitimate_cases_still_pass():
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
"""test_wait_guard_corpus.py — T21.3 §21.3C-1: parity corpus.
|
||||
Python side: verify classify_wait against the canonical corpus."""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
from rune import wait_guard as wg # noqa: E402
|
||||
from rune.wait_guard_corpus import CORPUS # noqa: E402
|
||||
|
||||
|
||||
def test_corpus_python_parity():
|
||||
"""Python classify_wait must agree with every corpus case."""
|
||||
failures = []
|
||||
for case in CORPUS:
|
||||
r = wg.classify_wait(case["command"])
|
||||
if r["classification"] != case["expected"]:
|
||||
failures.append(
|
||||
f"{case['id']}: expected {case['expected']}, "
|
||||
f"got {r['classification']}")
|
||||
assert not failures, f"parity failures: {failures}"
|
||||
|
||||
|
||||
def test_corpus_size():
|
||||
assert len(CORPUS) >= 15 # enough to cover the mandate's shapes
|
||||
@@ -0,0 +1,62 @@
|
||||
"""test_wait_guard_normal_forms.py — T21.2 §21.2C: normal model-generated
|
||||
wait spellings covered by the guard."""
|
||||
from __future__ import annotations
|
||||
import sys
|
||||
from pathlib import Path
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "src"))
|
||||
from rune import wait_guard as wg # noqa: E402
|
||||
|
||||
|
||||
def test_212c2_bash_c_sleep():
|
||||
assert wg.classify_wait(
|
||||
"bash -c 'sleep 500; curl -s $API/actions/runs'")[
|
||||
"classification"] in ("UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
|
||||
|
||||
|
||||
def test_212c2_time_sleep_inline():
|
||||
r = wg.classify_wait(
|
||||
"python3 -c 'import time; time.sleep(500)'; curl -s $API/commits/abc/status")
|
||||
assert r["classification"] in ("UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
|
||||
|
||||
|
||||
def test_212c2_time_sleep_heredoc():
|
||||
r = wg.classify_wait(
|
||||
"python3 - <<'PY'\nimport time; time.sleep(500)\nPY\ncurl -s $API/commits/abc/status")
|
||||
assert r["classification"] in ("UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
|
||||
|
||||
|
||||
def test_212c2_polling_loop_while():
|
||||
r = wg.classify_wait(
|
||||
"while true; do STATUS=$(curl -s $API/commits/abc/status); "
|
||||
"if [ $STATUS = success ]; then break; fi; sleep 60; done")
|
||||
assert r["classification"] in ("UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
|
||||
|
||||
|
||||
def test_212c2_polling_loop_until():
|
||||
r = wg.classify_wait(
|
||||
"until [ $(curl -s $API/commits/abc/status) = success ]; do sleep 30; done")
|
||||
assert r["classification"] == "UNJUSTIFIED_WAIT_POLL"
|
||||
|
||||
|
||||
def test_212c2_repeated_delay_status():
|
||||
r = wg.classify_wait(
|
||||
"sleep 120; curl -s $API/actions/runs; sleep 120; curl -s $API/actions/runs")
|
||||
assert r["classification"] in ("UNJUSTIFIED_WAIT_POLL", "UNJUSTIFIED_SLEEP")
|
||||
|
||||
|
||||
def test_212c4_legitimate_still_passes():
|
||||
# user delay
|
||||
assert wg.classify_wait("sleep 45")["classification"] == "USER_REQUESTED_DELAY"
|
||||
# short backoff
|
||||
assert wg.classify_wait("curl -s $A; sleep 4; curl -s $A")[
|
||||
"classification"] == "NO_WAIT"
|
||||
# heredoc docs
|
||||
cmd = "cat > f.md <<'EOF'\nsleep 540; poll CI\nEOF\necho done"
|
||||
assert wg.classify_wait(cmd)["classification"] == "NO_WAIT"
|
||||
# non-polling long sleep
|
||||
assert wg.classify_wait("sleep 120\nmake build")[
|
||||
"classification"] == "NO_WAIT"
|
||||
# watchdog script (bounded retry in a .py file)
|
||||
assert wg.classify_wait("python3 scripts/ci_event_wait.py --sha abc --deadline 900")[
|
||||
"classification"] == "NO_WAIT"
|
||||
@@ -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