Files
rune/scripts/await_intent.py
conductor efb99d4682 await-intent: refresh remote refs before op-type inference (M10 correction)
A just-merged master commit was invisible to branch --contains until refs
were fetched: the local clone's stale master outranked repo truth and a
master_ci operation classified as branch_ci. derive_op_type now refreshes
remote-tracking refs (bounded by GIT_TIMEOUT, fail-open) and containment
checks all refs. Generic fix; no SHA special-casing.
2026-09-19 18:23:17 +01:00

495 lines
21 KiB
Python

"""await_intent.py — ASYNC CORRECTIVE PATCH: intent normalization at the
execution boundary.
PRINCIPLE: MODEL OUTPUT = INTENT. RUNE CONTROLLER = EXECUTION SEMANTICS.
The model may express continuation intent in any quality — `await_operation
(deadline_secs=1200)`, `await-operation [op_type=master_ci, sha=X]`,
"sleep 300 then check CI", "poll until done". When a matching
PendingOperation exists (or the deterministic resolver can derive the
operation truth from git/Gitea state), the controller executes the
CANONICAL continuation and discards model-supplied scheduling policy.
Authority provenance for timing:
USER_EXPLICIT / EXTERNAL_CONTRACT / TEST_HARNESS /
GOVERNED_OPERATION_POLICY -> authoritative, retained
MODEL_INVENTED -> non-authoritative, ignored
This module owns NO storage: it reads the existing W/R wait store
(rune.event_wait) and git/Gitea state. The canonical continuation remains
scripts/ci_event_wait.py run() (the T21.5 single-logical-await).
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Any
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
_REPO_SRC = str(_SCRIPT_DIR.parent / "src")
if _REPO_SRC not in sys.path:
sys.path.insert(0, _REPO_SRC)
WR_BRIDGE = Path(os.environ.get("RUNE_WR_ROOT", "/mnt/workdrive/wr-bridge"))
REPO = Path("/mnt/workdrive/issue161-harness")
GIT_TIMEOUT = 10
# Default policy deadline per operation type — GOVERNED_OPERATION_POLICY.
# The model's numbers never become policy; these are the controller's.
POLICY_DEADLINES = {
"branch_ci": 1800,
"pull_request_checks": 1800,
"master_ci": 1800,
"deploy": 900,
"release": 900,
}
VALID_OP_TYPES = ("branch_ci", "pull_request_checks", "master_ci",
"deploy", "release")
# ── timing provenance ───────────────────────────────────────────────────────
def classify_timing_provenance(model_deadline: Any = None,
user_deadline: Any = None,
contract_deadline: Any = None) -> tuple[int, str]:
"""Resolve the effective deadline + its provenance.
Authority order: USER_EXPLICIT > EXTERNAL_CONTRACT > TEST_HARNESS >
GOVERNED_OPERATION_POLICY. MODEL_INVENTED values are recorded but
NEVER used as policy.
"""
if model_deadline is not None:
try:
model_deadline = int(model_deadline)
except (TypeError, ValueError):
model_deadline = None
if user_deadline is not None:
return int(user_deadline), "USER_EXPLICIT"
if contract_deadline is not None:
return int(contract_deadline), "EXTERNAL_CONTRACT"
return (model_deadline or 0), "MODEL_INVENTED" # caller discards this
# ── PendingOperation lookup over the existing W/R wait store ────────────────
def _load_wait_records() -> list[dict]:
"""All durable wait records from the bridge store (read-only)."""
out = []
waits = WR_BRIDGE / "waits"
if not waits.is_dir():
return out
for f in sorted(waits.glob("*.json")):
try:
rec = json.loads(f.read_text())
rec["_file"] = f.name
out.append(rec)
except (OSError, ValueError):
continue
return out
def _rec_operation_id(rec: dict) -> str:
return str((rec.get("ext") or {}).get("operation_id")
or rec.get("operation_id") or "")
def _rec_sha(rec: dict) -> str:
return str(rec.get("sha") or "")
def pending_operations(sha: str = "", op_type: str = "") -> list[dict]:
"""Pending (non-terminal) operations, optionally filtered."""
terminal = {"COMPLETED", "FAILED", "CLOSED", "CANCELLED", "EXPIRED"}
out = []
for rec in _load_wait_records():
status = str(rec.get("status", "")).upper()
if status in terminal:
continue
if sha and _rec_sha(rec) != sha:
continue
out.append(rec)
return out
def find_op_type_for_sha(sha: str) -> str | None:
"""The op_type a prior canonical wait already recorded for this SHA
(objective text embeds the type: 'event-driven {op_type} qualification')."""
for rec in _load_wait_records():
if _rec_sha(rec) != sha:
continue
obj = str(rec.get("continuation", {}).get("objective", ""))
m = re.search(r"event-driven (\w+) qualification", obj)
if m and m.group(1) in VALID_OP_TYPES:
return m.group(1)
return None
# ── deterministic op_type derivation from repo/Gitea truth ─────────────────
def _git(*args: str) -> str:
try:
r = subprocess.run(["git", "-C", str(REPO), *args],
capture_output=True, text=True, timeout=GIT_TIMEOUT)
return r.stdout.strip()
except (OSError, subprocess.TimeoutExpired):
return ""
def sha_branch(sha: str) -> str:
"""Branch(es) containing this commit in the local repo."""
return _git("branch", "--all", "--contains", sha)
def sha_on_master(sha: str) -> bool:
"""SHA is an ancestor of master (local or remote-tracking refs).
M10 correction (2026-09-19): --all so a freshly fetched master
(refs/remotes/gitea/master) counts; local branches alone go stale
between fetches. derive_op_type refreshes refs before calling this.
"""
out = _git("branch", "--all", "--contains", sha)
for l in out.splitlines():
s = l.strip().removeprefix("remotes/").removesuffix(" *")
if s == "master" or s.endswith("/master"):
return True
return False
def open_pr_for_sha(sha: str) -> int | None:
"""Is there an OPEN PR whose head SHA is this one? (Gitea API, read-only.)"""
try:
sys.path.insert(0, str(_SCRIPT_DIR))
import gitea_auth # noqa: E402
st, prs = gitea_auth.api("/pulls?state=open&limit=50")
if st != 200:
return None
for p in prs:
hs = str(p.get("head") or {}).get("sha") if isinstance(
p.get("head"), dict) else None
if hs and str(hs).startswith(sha[:12]):
return int(p.get("number"))
except Exception: # noqa: BLE001 — resolver must never crash the tool
return None
return None
def derive_op_type(sha: str) -> tuple[str, str]:
"""Deterministically derive the canonical op_type for a SHA.
Returns (op_type, evidence). Order (A-patch §2 amendment):
1. POST-PATCH normalized record (ext.intent_normalized=true) — true
stored operation truth, wins outright;
2. open PR for the sha -> pull_request_checks;
3. sha ancestor of master -> master_ci;
4. sha on a feature branch -> branch_ci;
5. legacy (pre-patch) records only CONFIRM, never override — they
were created from model-supplied defaults and cannot be trusted.
Never trusts model input.
"""
# 0. M10 correction (2026-09-19): refresh remote-tracking refs before
# inference. A just-merged master commit is invisible to
# branch --contains until refs are fetched; stale local git state
# must never outrank repo truth. Bounded by GIT_TIMEOUT, fail-open.
_git("fetch", "--quiet", "gitea", "+refs/heads/*:refs/remotes/gitea/*")
for rec in _load_wait_records():
if _rec_sha(rec) != sha:
continue
if (rec.get("ext") or {}).get("intent_normalized"):
obj = str(rec.get("continuation", {}).get("objective", ""))
m = re.search(r"event-driven (\w+) qualification", obj)
if m and m.group(1) in VALID_OP_TYPES:
return m.group(1), "normalized_record:" + m.group(1)
pr = open_pr_for_sha(sha)
if pr:
return "pull_request_checks", f"open_pr:#{pr}"
if sha_on_master(sha):
return "master_ci", "commit_ancestor_of_master"
return "branch_ci", "commit_on_feature_branch"
# ── intent normalization ────────────────────────────────────────────────────
def normalize_action(action: dict, *,
model_deadline: Any = None,
user_deadline: Any = None,
contract_deadline: Any = None,
task_id: str = "") -> dict:
"""Normalize a model continuation action into the canonical continuation.
action: {kind: await_operation|sleep_poll|check_status|unknown,
sha?: str, operation_id?: str, op_type?: str, raw?: str}
task_id: REAL_ASYNC_BRIDGE §9 — when the caller knows its task
binding, pending-operation resolution is scoped to operations
registered with that task_id; stale global operations are
never consulted when a binding exists.
Returns:
{normalized: bool, action: "ContinuePendingOperation", operation_id,
op_type, authority, deadline_secs, deadline_provenance,
model_deadline_ignored, model_op_type_ignored, evidence}
or {normalized: False, reason: ...} / {normalized: "AMBIGUOUS", candidates}
"""
sha = str(action.get("sha") or "")
op_id = str(action.get("operation_id") or "")
model_op_type = action.get("op_type")
model_deadline_ignored = model_deadline is not None
model_op_type_ignored = model_op_type is not None
# 0. task binding scope (§9): task-bound operations beat global state.
# Stored operation wins over ANY model hint (sha/op_type/deadline).
if task_id and not op_id:
bound = [r for r in pending_operations()
if str(r.get("ext", {}).get("task_id", "")) == task_id]
if len(bound) == 1:
rec = bound[0]
norm = _canonical_from_record(rec, model_deadline_ignored,
model_op_type_ignored,
"task_bound",
user_deadline, contract_deadline)
norm["sha_for_wait"] = _rec_sha(rec)
norm["task_id"] = task_id
return norm
if len(bound) > 1:
return {"normalized": "AMBIGUOUS",
"candidates": [_rec_operation_id(c) for c in bound],
"reason": f"multiple pending operations bound to task {task_id}"}
return {"normalized": False,
"reason": f"no pending operation bound to task {task_id}"}
# 1. explicit operation_id wins
if op_id:
from rune import event_wait as ew # noqa: E402
rec = ew.operation_wait(op_id)
if rec is None:
# REAL_ASYNC_BRIDGE §10: a FRESH operation has no wait record
# yet — registration happens at the park step (ci_event_wait).
# For an explicit, well-formed, EXISTING commit id, return the
# provisional canonical continuation (policy deadline, own-runs
# authority) so the model-facing tool can park on first call.
# A typo'd/ghost sha is refused here (A-patch lesson): the
# commit must exist in the repository.
import re as _re
m = _re.fullmatch(r"ci-([0-9a-f]{40})", op_id)
if m:
sha40 = m.group(1)
probe = subprocess.run(
["git", "-C", str(REPO), "cat-file", "-e",
f"{sha40}^{{commit}}"],
capture_output=True, timeout=15)
if probe.returncode == 0:
return {
"normalized": True,
"action": "ContinuePendingOperation",
"operation_id": op_id,
"sha_for_wait": sha40,
"op_type": "branch_ci",
"authority": "ci_runs",
"deadline_secs": POLICY_DEADLINES["branch_ci"],
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
"model_deadline_ignored": model_deadline_ignored,
"model_op_type_ignored": model_op_type_ignored,
"evidence": "explicit_id_fresh_registration",
}
return {"normalized": False,
"reason": f"operation_id {op_id} has no wait record"}
return _canonical_from_record(rec, model_deadline_ignored,
model_op_type_ignored, "explicit_id",
user_deadline, contract_deadline)
# 2. resolve by sha / active operation.
# H-PATCH H2/H3: the model sha is a HINT. If it doesn't resolve but
# unambiguous pending operations exist, the STORED target wins — a
# mistyped model sha must never create a ghost operation (the A-patch
# live failure: a typo'd sha derived branch_ci for a nonexistent commit
# and parked a wait on a ghost).
# 2. no explicit operation_id: resolve by sha / pending operations.
# H-PATCH: when the model provides NO sha (operation-id-first mode),
# resolve from unambiguous pending operations — the model should not
# need to reconstruct a 40-char sha to express "wait for it".
if not sha:
all_pending = pending_operations()
if len(all_pending) == 1:
rec = all_pending[0]
norm = _canonical_from_record(rec, model_deadline_ignored,
model_op_type_ignored,
"auto_resolved_no_sha",
user_deadline, contract_deadline)
norm["sha_for_wait"] = _rec_sha(rec)
return norm
if len(all_pending) > 1:
return {"normalized": "AMBIGUOUS",
"candidates": [_rec_operation_id(c) for c in all_pending],
"reason": "multiple pending operations, no sha hint"}
return {"normalized": False,
"reason": "no pending operations and no sha provided"}
candidates = pending_operations(sha=sha)
if len(candidates) > 1 and not sha:
return {"normalized": "AMBIGUOUS",
"candidates": [_rec_operation_id(c) for c in candidates],
"reason": "multiple pending operations, no target"}
if len(candidates) == 1:
rec = candidates[0]
stored_sha = _rec_sha(rec)
norm = _canonical_from_record(rec, model_deadline_ignored,
model_op_type_ignored, "pending_match",
user_deadline, contract_deadline)
if sha and stored_sha and sha != stored_sha:
norm["model_sha_mismatch"] = {
"model_sha": sha, "stored_sha": stored_sha,
"resolution": "stored_target_used"}
return norm
# model sha unresolved: any unambiguous pending op at all? then the
# model probably mistyped the sha — use the stored target, log it.
all_pending = pending_operations()
if len(all_pending) == 1 and sha:
rec = all_pending[0]
stored_sha = _rec_sha(rec)
norm = _canonical_from_record(rec, model_deadline_ignored,
model_op_type_ignored,
"unresolved_sha_single_pending",
user_deadline, contract_deadline)
norm["model_sha_mismatch"] = {
"model_sha": sha, "stored_sha": stored_sha,
"resolution": "stored_target_used"}
return norm
# 3. no stored record: derive truth deterministically
if not sha:
return {"normalized": False,
"reason": "no pending operation and no sha to resolve"}
op_type, evidence = derive_op_type(sha)
return {
"normalized": True,
"action": "ContinuePendingOperation",
"operation_id": f"ci-{sha}",
"op_type": op_type,
"authority": "ci_runs" if op_type in ("branch_ci",
"pull_request_checks")
else "deploy_qualify",
"deadline_secs": POLICY_DEADLINES[op_type],
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
"model_deadline_ignored": model_deadline_ignored,
"model_op_type_ignored": model_op_type_ignored,
"evidence": evidence,
}
def _canonical_from_record(rec: dict, model_deadline_ignored: bool,
model_op_type_ignored: bool, how: str,
user_deadline: Any, contract_deadline: Any) -> dict:
op_id = _rec_operation_id(rec)
obj = str(rec.get("continuation", {}).get("objective", ""))
m = re.search(r"event-driven (\w+) qualification", obj)
recorded = m.group(1) if (m and m.group(1) in VALID_OP_TYPES) else None
rec_sha = _rec_sha(rec)
if (rec.get("ext") or {}).get("intent_normalized") or how == "explicit_id":
# normalized record or explicit id: stored truth wins
op_type = recorded or "master_ci"
else:
# legacy record: git/Gitea derivation outranks the model-era default
op_type, _ev = derive_op_type(rec_sha) if rec_sha else \
(recorded or "master_ci", "no_sha")
deadline, prov = classify_timing_provenance(
user_deadline=user_deadline, contract_deadline=contract_deadline)
if prov == "MODEL_INVENTED" or not deadline:
deadline = POLICY_DEADLINES.get(op_type, 1800)
prov = "GOVERNED_OPERATION_POLICY"
return {
"normalized": True,
"action": "ContinuePendingOperation",
"operation_id": op_id,
"op_type": op_type,
"authority": "ci_runs" if op_type in ("branch_ci",
"pull_request_checks")
else "deploy_qualify",
"deadline_secs": deadline,
"deadline_provenance": prov,
"model_deadline_ignored": model_deadline_ignored,
"model_op_type_ignored": model_op_type_ignored,
"matched_by": how,
}
def normalize_bash(command: str, *, sha_hint: str = "") -> dict | None:
"""Recognize sleep/check/poll intent aimed at a known pending operation.
Returns a normalization record if the command's semantic intent is
continuation of a known external operation; None if the command is
legitimate local timing (short sync, user delay, backoff) with no
pending operation to continue. NEVER returns an executed sleep.
"""
if not command:
return None
cmd = command.strip()
# long sleep + (status-check language or an explicit sha hint)
sleep_m = re.search(r"\bsleep\s+(\d{2,5})\b", cmd)
if not sleep_m:
return None
secs = int(sleep_m.group(1))
check_intent = bool(re.search(
r"\b(ci|deploy|release|status|check|poll|pipeline|run)\b", cmd,
re.IGNORECASE))
sha = sha_hint or ""
m = re.search(r"\b([0-9a-f]{40}|[0-9a-f]{12})\b", cmd)
if m:
sha = m.group(1)
if not check_intent and not sha:
return None # plain sleep, no continuation intent
cands = pending_operations(sha=sha) if sha else pending_operations()
if not cands and not sha:
return None
if len(cands) > 1:
return {"normalized": "AMBIGUOUS",
"candidates": [_rec_operation_id(c) for c in cands],
"raw_command_kind": "sleep_poll"}
rec = cands[0] if cands else None
if rec is not None:
norm = _canonical_from_record(rec, True, False, "sleep_poll_intent",
None, None)
norm["sha_for_wait"] = _rec_sha(rec)
elif sha:
op_type, evidence = derive_op_type(sha)
norm = {
"normalized": True, "action": "ContinuePendingOperation",
"operation_id": f"ci-{sha}", "op_type": op_type,
"authority": "ci_runs" if op_type in ("branch_ci",
"pull_request_checks")
else "deploy_qualify",
"deadline_secs": POLICY_DEADLINES[op_type],
"deadline_provenance": "GOVERNED_OPERATION_POLICY",
"model_deadline_ignored": True, "model_op_type_ignored": False,
"evidence": evidence, "sha_for_wait": sha,
}
else:
return None
norm["raw_command_kind"] = "sleep_poll"
norm["raw_sleep_seconds_suppressed"] = secs
return norm
def emit_normalized_record(record: dict) -> None:
"""Compact structured observability — one JSONL line per normalization.
No payloads, no secrets."""
try:
log = _SCRIPT_DIR.parent / "store" / "await_intent_log.jsonl"
log.parent.mkdir(parents=True, exist_ok=True)
slim = {k: record.get(k) for k in (
"normalized", "action", "operation_id", "op_type", "authority",
"deadline_secs", "deadline_provenance", "model_deadline_ignored",
"model_op_type_ignored", "matched_by", "evidence",
"raw_command_kind", "raw_sleep_seconds_suppressed", "reason",
"candidates") if k in record}
slim["at"] = __import__("time").strftime(
"%Y-%m-%dT%H:%M:%SZ", __import__("time").gmtime())
with open(log, "a") as f:
f.write(json.dumps(slim, sort_keys=True) + "\n")
except OSError:
pass