Compare commits
96
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9835261517 | ||
|
|
3788875410 | ||
|
|
d96b89bd4c | ||
|
|
fe45c6a8dd | ||
|
|
c7b2edd4fc | ||
|
|
96f0e7e263 | ||
|
|
a168cd935e | ||
|
|
a2c4a11b0d | ||
|
|
16164c6f5e | ||
|
|
0722dc2339 | ||
|
|
a7ec1b49d7 | ||
|
|
5b61f8a8bb | ||
|
|
c730719768 | ||
|
|
4fe0f8ee46 | ||
|
|
8999f29041 | ||
|
|
c288f4726d | ||
|
|
308bbb74b7 | ||
|
|
1f01e87453 | ||
|
|
4e8e081f48 | ||
|
|
6c681b97bf | ||
|
|
d01d6eb6d4 | ||
|
|
f90695197d | ||
|
|
279c2b86cb | ||
|
|
57206b7b31 | ||
|
|
b69ffcd44f | ||
|
|
c4b4ebd0af | ||
|
|
4220621cb0 | ||
|
|
4b54451444 | ||
|
|
962e3511e1 | ||
|
|
fb809135c6 | ||
|
|
10acdfe708 | ||
|
|
fd9e1bffb8 | ||
|
|
631426763e | ||
|
|
09cd18ad54 | ||
|
|
da500df487 | ||
|
|
51416aa95e | ||
|
|
b68f31ff0d | ||
|
|
dd41f15571 | ||
|
|
57e4b8672c | ||
|
|
6923128f45 | ||
|
|
9db239fcb4 | ||
|
|
658ec63627 | ||
|
|
427c3ac6bb | ||
|
|
509ec00c87 | ||
|
|
bea523a0e3 | ||
|
|
0a15a1675d | ||
|
|
81c6d1f9c6 | ||
|
|
d53bb9a535 | ||
|
|
023151acb9 | ||
|
|
2da9305d96 | ||
|
|
2e05ea6848 | ||
|
|
be161c2043 | ||
|
|
a6539986b9 | ||
|
|
8da216d155 | ||
|
|
05f2a74c6f | ||
|
|
1c5dc557a6 | ||
|
|
a6bccc1c5d | ||
|
|
f208b5e61c | ||
|
|
aa800acbad | ||
|
|
30be2c96b8 | ||
|
|
ca00be8259 | ||
|
|
50a63e587f | ||
|
|
a54ab559cb | ||
|
|
599c3ab1c2 | ||
|
|
483bfd7944 | ||
|
|
1650b470c6 | ||
|
|
853cf3e0ae | ||
|
|
875bb792f7 | ||
|
|
f06ddb3a20 | ||
|
|
2ebd9da139 | ||
|
|
65001196d8 | ||
|
|
76bb2b97be | ||
|
|
e765f654f9 | ||
|
|
f7141200bd | ||
|
|
53b4830b08 | ||
|
|
fa1b7c4be1 | ||
|
|
246f26663d | ||
|
|
5718a3ea85 | ||
|
|
02802f321f | ||
|
|
4ce121a493 | ||
|
|
8a8d3534c6 | ||
|
|
d0ec37925f | ||
|
|
f9936fa244 | ||
|
|
04a277f22c | ||
|
|
b2777d666f | ||
|
|
0982f7317f | ||
|
|
9583678997 | ||
|
|
5db8b202d9 | ||
|
|
78c0d97a43 | ||
|
|
551530c78c | ||
|
|
b669e4d4ed | ||
|
|
2b6a5a6b95 | ||
|
|
e2e7b74a8e | ||
|
|
db4ce7f6a6 | ||
|
|
505209b839 | ||
|
|
f7256aa29e |
No files matched your search
@@ -1,16 +1,45 @@
|
||||
---
|
||||
name: Bug Fix
|
||||
about: Fix a bug or issue
|
||||
about: Demonstrate currently incorrect behaviour — plain English first, proof required
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
## Plain summary
|
||||
<!-- 2-4 sentences, NON-TECHNICAL: what's broken and why it matters. -->
|
||||
## In plain English: <restate the issue title here in everyday words>
|
||||
<!-- The heading ITSELF must carry the summary — a scan of issue headings
|
||||
alone should tell the whole story of the board. TOP PRIORITY: if a
|
||||
technical detail below contradicts this section, the issue is buggy —
|
||||
fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
## Example
|
||||
<!-- A concrete before/after: "Doing X currently produces Y; it should produce Z." -->
|
||||
|
||||
What is broken: <what a person observes going wrong>
|
||||
|
||||
What should happen instead: <the correct behaviour>
|
||||
|
||||
How often / when: <always | after X | since change Y>
|
||||
|
||||
## Proof it is broken (required — no proof, no bug)
|
||||
<!-- A bug is DEMONSTRATED currently incorrect behaviour. Nothing else.
|
||||
If this section cannot be filled, file it as a question, not a bug. -->
|
||||
- [ ] Reproduction command or steps (deterministic where possible)
|
||||
- [ ] The exact wrong output observed: <paste/log excerpt>
|
||||
- [ ] Where the evidence lives: <log path, fixture, session id>
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
## What would let us close this
|
||||
- [ ] The reproduction command now shows correct behaviour
|
||||
- [ ] A regression test exists that fails before the fix and passes after
|
||||
- [ ] No other behaviour changed (or changes are listed and justified)
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- A bug fix is USUALLY a patch. Pick major or minor only with a reason. -->
|
||||
- [ ] patch — same behaviour, done correctly (usual for bugs)
|
||||
- [ ] major — the bug was in a published contract and fixing it breaks consumers (justify)
|
||||
- [ ] minor — fixing revealed a missing capability that had to be added (justify)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT (the bug) / WHERE (files) / EVIDENCE (how it was confirmed). -->
|
||||
@@ -29,10 +58,6 @@ assignees: conductor
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit references
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
|
||||
@@ -1,18 +1,43 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: New feature or improvement — DESCRIPTION GATE v4 format (plain summary + pipeline requirements)
|
||||
about: New feature or improvement — plain English first, verifiable close conditions, version intent
|
||||
title: "[FEATURE] "
|
||||
labels: feature
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
## Plain summary
|
||||
<!-- 2-4 sentences, NON-TECHNICAL: what this does and why it matters. Nick should
|
||||
understand the goal without decoding internals. -->
|
||||
## In plain English: <restate the issue title here in everyday words>
|
||||
<!-- The heading ITSELF must carry the summary — a scan of issue headings
|
||||
alone should tell the whole story of the board. TOP PRIORITY: if a
|
||||
technical detail below contradicts this section, the issue is buggy —
|
||||
fix the issue. A non-technical reader must be able to retell this
|
||||
section. 3-6 sentences, no jargon. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is this about: <what>
|
||||
|
||||
Why do we need it: <the itch — what happens today that shouldn't, or what becomes possible>
|
||||
|
||||
Why is it still open: <"not started yet" | "part done, X remains" | "deliberately parked as a future idea">
|
||||
|
||||
## Example
|
||||
<!-- A concrete example of the intended behavior, in plain terms:
|
||||
"If X happens, the system will Y instead of Z." -->
|
||||
<!-- A concrete before/after in plain terms: "If X happens, the system will Y instead of Z." -->
|
||||
|
||||
## What would let us close this
|
||||
<!-- Checkable statements, verifiable by looking at something that EXISTS —
|
||||
a file, a log line, a passing test. Not "when appropriate". -->
|
||||
- [ ] <condition 1 — observable>
|
||||
- [ ] <condition 2>
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- The pipeline derives the real bump from merged conventional commits and
|
||||
compares against this declaration. A mismatch is flagged, not fatal:
|
||||
either the declaration was wrong or the work grew. Both facts matter. -->
|
||||
- [ ] major — breaking: changes a contract, schema, or interface others depend on
|
||||
- [ ] minor — new capability: something becomes possible that wasn't
|
||||
- [ ] patch — repair: same behaviour, done correctly
|
||||
- [ ] none — touches no released artifact (docs, memory, process notes)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT — one-line objective.
|
||||
@@ -37,7 +62,6 @@ DEPENDENCIES — parent epic + preceding stage/plan refs. -->
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit/PR references
|
||||
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
@@ -69,4 +93,4 @@ Hard invariants: derived cognition preserves evidentiary ancestry · epistemic c
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
name: Research Submission
|
||||
about: Buy down uncertainty before building — question, falsifier, budget, durable findings
|
||||
title: "[RESEARCH] "
|
||||
labels: research
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
## In plain English: <restate the issue title here in everyday words>
|
||||
<!-- The heading ITSELF must carry the summary — a scan of issue headings
|
||||
alone should tell the whole story of the board. TOP PRIORITY: if a
|
||||
technical detail below contradicts this section, the issue is buggy —
|
||||
fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
The question: <what we're trying to learn, one sentence>
|
||||
|
||||
Why we're asking: <what decision this research will inform — a name, an issue
|
||||
number, or "exploratory: none yet">
|
||||
|
||||
What we believe today: <the current assumption, stated so it can be proven wrong>
|
||||
|
||||
## The shape of an answer
|
||||
- [ ] What evidence would settle the question: <observation, benchmark, or spike>
|
||||
- [ ] What evidence would KILL the current assumption (the falsifier): <be specific —
|
||||
this is stated BEFORE starting so the research cannot become a search for confirmation>
|
||||
- [ ] Budget: <time/effort cap — when research must stop and report back>
|
||||
|
||||
## What would let us close this
|
||||
- [ ] Findings written to a durable document: <path, e.g. research/issue-NNN/> —
|
||||
a comment thread is not a durable finding
|
||||
- [ ] The decision the research informs is recorded: made / deferred / changed
|
||||
- [ ] If a follow-up build issue was minted: <#N or "none needed">
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
- [ ] none — research changes no released artifact (usual)
|
||||
- [ ] minor/major — only if a code spike is being merged as part of this (justify)
|
||||
|
||||
## Technical detail
|
||||
<!-- Candidate approaches, prior art in the repo, related issues, constraints,
|
||||
rebuildable-projection or authority requirements if findings touch canonical state. -->
|
||||
|
||||
## Pipeline requirements
|
||||
- [ ] Findings committed under the stated durable path
|
||||
- [ ] Every citation verified against a real source (no fabricated references)
|
||||
- [ ] Closed with a properly formatted closing comment: findings summary + decision
|
||||
|
||||
## Cognitive invariants (memory/cognition research — both handoffs)
|
||||
<!-- Complete ONLY if findings touch memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before proposing modules)
|
||||
- [ ] **Read/write authority** — which canonical state is affected
|
||||
- [ ] **Provenance** — findings traceable to evidence
|
||||
- [ ] **Reversibility** — projections remain rebuildable; no unique truth moves into a derived index
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · retrieval never increases utility · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — blocks a decision that blocks other work
|
||||
- [ ] **P1 — high** — informs a next-stage dependency
|
||||
- [ ] **P2 — medium** — quality/consolidation understanding
|
||||
- [ ] **P3 — low** — watchlist / deferred / exploratory
|
||||
@@ -58,9 +58,40 @@ jobs:
|
||||
const managedLabels = new Set([
|
||||
'memory', 'governance', 'identity', 'growth',
|
||||
'perception', 'infrastructure', 'life', 'custodian',
|
||||
'priority: high', 'bug', 'feature',
|
||||
'priority: high', 'bug', 'feature', 'research',
|
||||
]);
|
||||
|
||||
// Type label: DECLARED over GUESSED. The template spine
|
||||
// (kind: bug|feature|research) is authoritative. Keyword
|
||||
// heuristics are a fallback for issues filed before the
|
||||
// template system existed — they misclassify honest prose
|
||||
// ("what is broken", "the fix") as bugs (T15 audit finding).
|
||||
let kind = null;
|
||||
const spineMatch = body && body.match(/^kind:\s*(bug|feature|research)\s*$/m);
|
||||
if (spineMatch) {
|
||||
kind = spineMatch[1];
|
||||
}
|
||||
let isBug = false, isFeature = false, isResearch = false;
|
||||
if (kind) {
|
||||
isBug = kind === 'bug';
|
||||
isFeature = kind === 'feature';
|
||||
isResearch = kind === 'research';
|
||||
} else {
|
||||
const textBug = title.startsWith('[BUG]') || text.includes('bug') || text.includes('fix') || text.includes('broken');
|
||||
isBug = textBug;
|
||||
isFeature = !isBug && (title.startsWith('[FEATURE]') || text.includes('feature') || text.includes('improve') || text.includes('add'));
|
||||
isResearch = !isBug && !isFeature && title.startsWith('[RESEARCH]');
|
||||
}
|
||||
if (isBug) {
|
||||
desiredLabels.add('bug');
|
||||
}
|
||||
if (isFeature) {
|
||||
desiredLabels.add('feature');
|
||||
}
|
||||
if (isResearch) {
|
||||
desiredLabels.add('research');
|
||||
}
|
||||
|
||||
// Umbrella labels
|
||||
if (text.includes('memory') || text.includes('store') || text.includes('recall') || text.includes('consolidat')) {
|
||||
desiredLabels.add('memory');
|
||||
@@ -92,16 +123,6 @@ jobs:
|
||||
desiredLabels.add('priority: high');
|
||||
}
|
||||
|
||||
// Type labels
|
||||
const isBug = title.startsWith('[BUG]') || text.includes('bug') || text.includes('fix') || text.includes('broken');
|
||||
const isFeature = !isBug && (title.startsWith('[FEATURE]') || text.includes('feature') || text.includes('improve') || text.includes('add'));
|
||||
if (isBug) {
|
||||
desiredLabels.add('bug');
|
||||
}
|
||||
if (isFeature) {
|
||||
desiredLabels.add('feature');
|
||||
}
|
||||
|
||||
const issueNumber = context.payload.issue?.number || context.payload.pull_request?.number;
|
||||
const current = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Rune
|
||||
|
||||
**Current release: v0.83.1**
|
||||
**Current release: v0.87.0**
|
||||
|
||||
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
|
||||
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
"""completion_file.py — REAL_ASYNC_BRIDGE: durable completion-file delivery
|
||||
with inotify wake. Replaces FIFO+lease for process-wait delivery.
|
||||
|
||||
DESIGN:
|
||||
Producer (bridge/daemon/notify):
|
||||
1. Validates operation identity
|
||||
2. Atomically writes a completion file:
|
||||
resumes/<operation_id>.done
|
||||
containing the event/verdict JSON (write to tmp, rename for atomicity)
|
||||
|
||||
Waiter (ci_event_wait / any continuation consumer):
|
||||
1. Checks if the completion file already exists (event-before-waiter)
|
||||
2. If not: registers inotify watch on the resumes/ directory
|
||||
3. Re-checks after watch registration (closes the registration race)
|
||||
4. Blocks in kernel on inotify — zero CPU, zero timer
|
||||
5. On file-created event: reads the file, verifies authority, returns
|
||||
|
||||
The event is a WAKE SIGNAL. Authority still decides terminal truth.
|
||||
The completion file is durable — survives daemon and waiter restarts.
|
||||
|
||||
WHY NOT FIFO:
|
||||
FIFO is one-shot (token consumed), requires lease management, and
|
||||
fails under session-busy guards. A completion file is atomic, durable,
|
||||
and works across restarts. inotify wakes the process with zero CPU
|
||||
cost and zero latency between file creation and wake.
|
||||
|
||||
WHY NOT POLLING:
|
||||
Polling introduces an arbitrary interval. inotify is a kernel callback
|
||||
that fires exactly when the filesystem changes — the latency tracks
|
||||
the real completion event, not a configured interval.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import struct
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _wr_bridge() -> Path:
|
||||
# WR_ROOT = daemon/infra convention; RUNE_WR_ROOT = repo/scripts
|
||||
# convention. Both honored (WR_ROOT wins) so the daemon, the waiter,
|
||||
# and any isolated test root always resolve the SAME directory.
|
||||
root = os.environ.get("WR_ROOT") or os.environ.get("RUNE_WR_ROOT") or "/mnt/workdrive/wr-bridge"
|
||||
return Path(root)
|
||||
|
||||
|
||||
def _resumes_dir() -> Path:
|
||||
return _wr_bridge() / "resumes"
|
||||
|
||||
|
||||
# inotify constants (Linux)
|
||||
IN_CREATE = 0x100
|
||||
IN_CLOSE_WRITE = 0x8
|
||||
IN_MOVED_TO = 0x80
|
||||
|
||||
|
||||
def completion_path(operation_id: str) -> Path:
|
||||
return _resumes_dir() / f"{operation_id}.done"
|
||||
|
||||
|
||||
def write_completion(operation_id: str, event: dict) -> Path:
|
||||
"""Atomically write a durable completion file. The rename(2) ensures
|
||||
the waiter never sees a partial file."""
|
||||
_resumes_dir().mkdir(parents=True, exist_ok=True)
|
||||
target = completion_path(operation_id)
|
||||
tmp = target.parent / f".{target.name}.tmp.{os.getpid()}"
|
||||
tmp.write_text(json.dumps(event, sort_keys=True) + "\n")
|
||||
os.rename(str(tmp), str(target)) # atomic on same filesystem
|
||||
os.chmod(str(target), 0o666) # cross-user bridge
|
||||
return target
|
||||
|
||||
|
||||
def read_completion(operation_id: str) -> dict | None:
|
||||
"""Read a durable completion file. Returns None if absent."""
|
||||
p = completion_path(operation_id)
|
||||
if not p.is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def completion_exists(operation_id: str) -> bool:
|
||||
return completion_path(operation_id).is_file()
|
||||
|
||||
|
||||
def _inotify_init_and_add_watch(directory: str) -> int:
|
||||
"""Create an inotify instance and add a watch on the directory.
|
||||
Returns the inotify fd."""
|
||||
libc = __import__("ctypes").CDLL(None, use_errno=True)
|
||||
fd = libc.inotify_init()
|
||||
if fd < 0:
|
||||
raise OSError("inotify_init failed")
|
||||
# IN_CREATE | IN_MOVED_TO: catch both direct creation and atomic rename
|
||||
mask = IN_CREATE | IN_MOVED_TO
|
||||
wd = libc.inotify_add_watch(fd, directory.encode(), mask)
|
||||
if wd < 0:
|
||||
os.close(fd)
|
||||
raise OSError(f"inotify_add_watch failed for {directory}")
|
||||
return fd
|
||||
|
||||
|
||||
def _inotify_read(fd: int, timeout_secs: float | None = None) -> list[str]:
|
||||
"""Read pending inotify events. Returns list of filenames that were
|
||||
created/moved-to. Blocks until at least one event or timeout."""
|
||||
poll = select.poll()
|
||||
poll.register(fd, select.POLLIN)
|
||||
events = []
|
||||
buf = os.read(fd, 4096)
|
||||
pos = 0
|
||||
while pos < len(buf):
|
||||
# struct: wd(i32), mask(u32), cookie(u32), name_len(u32)
|
||||
wd, mask, cookie, name_len = struct.unpack_from("iIII", buf, pos)
|
||||
pos += 16
|
||||
name = buf[pos : pos + name_len].rstrip(b"\x00").decode("utf-8", "replace")
|
||||
pos += name_len
|
||||
if mask & (IN_CREATE | IN_MOVED_TO):
|
||||
events.append(name)
|
||||
return events
|
||||
|
||||
|
||||
def wait_for_completion(operation_id: str, *, timeout_secs: float | None = None, poll_callback=None) -> dict | None:
|
||||
"""Block until a completion file exists for this operation.
|
||||
|
||||
Uses inotify for zero-latency, zero-CPU wake. Falls back to
|
||||
polling_callback (if provided) when inotify is unavailable.
|
||||
|
||||
The completion file is a WAKE SIGNAL. The caller must still verify
|
||||
canonical authority after reading it.
|
||||
|
||||
Returns the event dict from the completion file, or None on timeout.
|
||||
"""
|
||||
target = completion_path(operation_id)
|
||||
|
||||
# 1. check-before-watch (event may have arrived before we started)
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
_resumes_dir().mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
fd = _inotify_init_and_add_watch(str(_resumes_dir()))
|
||||
except OSError:
|
||||
# inotify unavailable: fall back to polling callback
|
||||
if poll_callback:
|
||||
return poll_callback(operation_id, timeout_secs)
|
||||
return None
|
||||
|
||||
try:
|
||||
# 2. re-check after watch registration (closes the race between
|
||||
# the initial check and the watch being active)
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
# 3. block in kernel on inotify — zero CPU, exact latency
|
||||
import select as _select
|
||||
|
||||
poll = _select.poll()
|
||||
poll.register(fd, _select.POLLIN)
|
||||
deadline = (time.monotonic() + timeout_secs) if timeout_secs else None
|
||||
|
||||
while True:
|
||||
remaining = None
|
||||
if deadline is not None:
|
||||
remaining = max(0.1, deadline - time.monotonic())
|
||||
if remaining <= 0.1:
|
||||
return None # timeout
|
||||
events = poll.poll(remaining * 1000 if remaining else None)
|
||||
if not events:
|
||||
continue # poll timeout, re-check
|
||||
names = _inotify_read(fd)
|
||||
if target.name in names:
|
||||
# 4. file appeared — read it
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
# partial/corrupt: keep watching
|
||||
finally:
|
||||
os.close(fd)
|
||||
return None
|
||||
|
||||
|
||||
def clear_completion(operation_id: str) -> None:
|
||||
"""Remove a consumed completion file (idempotent cleanup)."""
|
||||
p = completion_path(operation_id)
|
||||
try:
|
||||
p.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
Executable
+799
@@ -0,0 +1,799 @@
|
||||
#!/usr/bin/env python3
|
||||
"""daemon.py — Gitea webhook receiver + reconciliation + resume driver (v0.2).
|
||||
|
||||
W/R-v0.2 invariants implemented here:
|
||||
A. SINGLE-FLIGHT PER SESSION — per-session lease (waitstore.acquire_lease,
|
||||
/proc-validated, atomic) + in-process inflight set. A second READY wait
|
||||
for the same session stays queued until the lease releases.
|
||||
B. BUSY SESSION PROTECTION — before spawning, /proc is scanned for a live
|
||||
`mimo run -s <sid>`-shaped process not owned by our lease. Found → skip
|
||||
launch (wait stays READY). No invented MiMo APIs; the lease is the
|
||||
conservative floor against W/R-created concurrency.
|
||||
C. ATOMIC SESSION LEASE — persisted {session_id, wait_id, pid, pgid,
|
||||
pid_starttime, acquired_at, state}; O_EXCL create, steal only on /proc
|
||||
evidence of a dead owner.
|
||||
D. PROCESS TREE OWNERSHIP — workers spawn with start_new_session=True (own
|
||||
process group). Watch timeout ⇒ SIGTERM group → bounded grace → SIGKILL
|
||||
group → verify zero surviving group members → release lease → truthful
|
||||
READY_TO_RETRY (never terminal-on-timeout).
|
||||
E. TIMEOUT IS NOT SUCCESS — rc==0 maps to verdict terminal
|
||||
(COMPLETED/FAILED); rc!=0 or unverifiable ⇒ RESUME_UNVERIFIED
|
||||
(not schedulable, operator-requeueable); timeout ⇒ READY_TO_RETRY.
|
||||
F. STALE RESUMING RECOVERY — on startup, every RESUMING record is
|
||||
inspected: live lease owner ⇒ adopted and monitored; otherwise ⇒
|
||||
RESUME_UNVERIFIED (+ stale lease released). Never auto-relaunched.
|
||||
G. MULTIPLE WAITS, SAME SESSION — schedulable() orders oldest-first; one
|
||||
continuation finishes before the next starts.
|
||||
H. PIDFILE CORRECTNESS — singleton enforced by flock on daemon.pid; the
|
||||
lock dies with the process, so stale pidfiles (incl. SIGKILL) cannot
|
||||
admit a second daemon.
|
||||
|
||||
The resume command is configurable (WR_RESUME_CMD, JSON list with
|
||||
{session_id}/{msg} placeholders) so the test harness can use a stub; the
|
||||
default is the real mechanism: `mimo run -s <sid> -m <model> <msg>`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import fcntl, json, os, signal, subprocess, sys, threading, time
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import waitstore
|
||||
import verify
|
||||
import completion_file # REAL_ASYNC_BRIDGE R3: durable completion-file delivery
|
||||
|
||||
RESUME_MODEL = "Verboo Code/glm-5.3-flash"
|
||||
RECONCILE_SECS = int(os.environ.get("WR_RECONCILE_SECS", "60"))
|
||||
RECONCILE_EVERY_TICKS = max(1, int(os.environ.get("WR_RECONCILE_TICKS", "3")))
|
||||
WAKE_MIN_AGE_SECS = float(os.environ.get("WR_WAKE_MIN_AGE_SECS", "5"))
|
||||
WAKE_RETRIES = int(os.environ.get("WR_WAKE_RETRIES", "3"))
|
||||
WAKE_RETRY_SECS = float(os.environ.get("WR_WAKE_RETRY_SECS", "4"))
|
||||
DRIVE_LOOP_SECS = int(os.environ.get("WR_DRIVE_SECS", "20"))
|
||||
# REAL_ASYNC_BRIDGE §4: a RESUMING wait older than this grace without
|
||||
# progress is an orphaned resume flow (thread died between lease acquire
|
||||
# and release). Must exceed the longest legitimate in-flight watch
|
||||
# (WATCH_SECS); default 3x. Env-overridable for deterministic tests.
|
||||
WATCH_SECS = int(os.environ.get("WR_WATCH_SECS", "600"))
|
||||
# REAL_ASYNC_BRIDGE §4: a RESUMING wait older than this grace without
|
||||
# progress is an orphaned resume flow (thread died between lease acquire
|
||||
# and release). Must exceed the longest legitimate in-flight watch
|
||||
# (WATCH_SECS); default 3x. Env-overridable for deterministic tests.
|
||||
ORPHAN_GRACE_SECS = int(os.environ.get("WR_ORPHAN_GRACE_SECS", str(3 * WATCH_SECS)))
|
||||
TERM_GRACE_SECS = int(os.environ.get("WR_TERM_GRACE_SECS", "10"))
|
||||
MAX_ATTEMPTS = int(os.environ.get("WR_MAX_ATTEMPTS", "3"))
|
||||
_WR_ROOT = Path(os.environ.get("WR_ROOT", "") or Path(__file__).resolve().parent)
|
||||
PIDFILE = _WR_ROOT / "daemon.pid"
|
||||
LOG = _WR_ROOT / "daemon.log"
|
||||
|
||||
DEFAULT_RESUME_CMD = ["mimo", "run", "-s", "{session_id}", "-m", RESUME_MODEL, "{msg}"]
|
||||
|
||||
|
||||
def log(msg: str) -> None:
|
||||
with open(LOG, "a") as f:
|
||||
f.write(f"{time.strftime('%H:%M:%S')} {msg}\n")
|
||||
|
||||
|
||||
def resume_cmd(session_id: str, msg: str, wait_id: str = "", override: list | None = None) -> list[str]:
|
||||
"""T15 §15D: per-wait override wins; placeholders {session_id}/{msg}/
|
||||
{wait_id}. A synthetic controller session (Scaffolding stage driver)
|
||||
is not a MiMo session — the global default cannot resume it, which is
|
||||
exactly why fixed-sleep polling crept back into T12-T14."""
|
||||
if override:
|
||||
return [
|
||||
c.replace("{session_id}", session_id).replace("{msg}", msg).replace("{wait_id}", wait_id) for c in override
|
||||
]
|
||||
tmpl = os.environ.get("WR_RESUME_CMD")
|
||||
cmd = json.loads(tmpl) if tmpl else DEFAULT_RESUME_CMD
|
||||
return [c.replace("{session_id}", session_id).replace("{msg}", msg).replace("{wait_id}", wait_id) for c in cmd]
|
||||
|
||||
|
||||
# ── process-tree evidence & ownership ─────────────────────────────────────
|
||||
|
||||
|
||||
def pgid_members(pgid: int) -> list[int]:
|
||||
out = []
|
||||
for d in Path("/proc").iterdir():
|
||||
if not d.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
stat = (d / "stat").read_text()
|
||||
fields = stat.rsplit(")", 1)[1].split()
|
||||
# zombies: killed but not yet reaped. Counting them makes the
|
||||
# survivor check report the direct child we just KILLed before
|
||||
# communicate() reaps it (observed live 2026-09-12).
|
||||
if fields[0] == "Z":
|
||||
continue
|
||||
if int(fields[2]) == pgid:
|
||||
out.append(int(d.name))
|
||||
except (OSError, IndexError, ValueError):
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def terminate_tree(pgid: int) -> list[int]:
|
||||
"""TERM → bounded grace → KILL. Returns surviving members (must be [])."""
|
||||
try:
|
||||
os.killpg(pgid, signal.SIGTERM)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
deadline = time.time() + TERM_GRACE_SECS
|
||||
while time.time() < deadline:
|
||||
if not pgid_members(pgid):
|
||||
return []
|
||||
time.sleep(0.2)
|
||||
try:
|
||||
os.killpg(pgid, signal.SIGKILL)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
for _ in range(50): # up to 10s for KILL to land
|
||||
if not pgid_members(pgid):
|
||||
return []
|
||||
time.sleep(0.2)
|
||||
return pgid_members(pgid)
|
||||
|
||||
|
||||
def stray_worker_pids(session_id: str, exclude_pgids: set[int] = ()) -> list[int]:
|
||||
"""Live `mimo run -s <sid>`-shaped processes not owned by our workers."""
|
||||
out = []
|
||||
for d in Path("/proc").iterdir():
|
||||
if not d.name.isdigit():
|
||||
continue
|
||||
try:
|
||||
parts = [p.decode(errors="replace") for p in (d / "cmdline").read_bytes().split(b"\0") if p]
|
||||
except OSError:
|
||||
continue
|
||||
if not parts:
|
||||
continue
|
||||
joined = " ".join(parts)
|
||||
if session_id not in joined or "-s" not in parts:
|
||||
continue
|
||||
if "mimo" not in joined.lower() and "@mimo-ai" not in joined.lower():
|
||||
continue
|
||||
try:
|
||||
if os.getpgid(int(d.name)) in exclude_pgids:
|
||||
continue
|
||||
except OSError:
|
||||
continue
|
||||
out.append(int(d.name))
|
||||
return out
|
||||
|
||||
|
||||
# ── verification → transition (unchanged authority model) ─────────────────
|
||||
|
||||
|
||||
def _authoritative_verify(rec: dict) -> tuple[str, dict]:
|
||||
"""Authoritative verification with a deterministic test seam.
|
||||
|
||||
Production (WR_VERIFY_STUB unset): real Gitea API via verify.py — the
|
||||
ONLY source of success/failure truth. Test (WR_VERIFY_STUB=path to
|
||||
JSON {"<run_ref-or-sha>": verdict, "default": verdict}): stubbed
|
||||
verdicts so invariant tests never touch the network. The stub is a
|
||||
test fixture, not an alternate authority — no production path sets it.
|
||||
"""
|
||||
stub_path = os.environ.get("WR_VERIFY_STUB", "")
|
||||
if stub_path:
|
||||
try:
|
||||
spec = json.loads(Path(stub_path).read_text())
|
||||
except Exception:
|
||||
return "pending", {"stub_error": "unreadable stub"}
|
||||
key = str(rec.get("run_ref", "") or rec.get("sha", ""))
|
||||
verdict = spec.get(key, spec.get("default", "pending"))
|
||||
return verdict, {"stub": True, "key": key}
|
||||
import verify
|
||||
|
||||
if rec.get("wait_kind") == "RELEASE_DEPLOY":
|
||||
return verify.verify_release_deploy(rec.get("sha", ""), rec.get("repo_root", ""), rec.get("deployed_link", ""))
|
||||
return verify.verify_run(rec.get("run_ref", ""), rec.get("sha", ""), rec.get("expected", "success"))
|
||||
|
||||
|
||||
def try_transition(rec: dict, event: dict | None = None) -> dict:
|
||||
if rec["status"] != "WAITING_EXTERNAL":
|
||||
return rec
|
||||
if rec.get("wait_kind") == "PROCESS_EVENT":
|
||||
# T15 §15D: the matching event IS the authority. The notifier
|
||||
# wrapper observed the real process exit; exit_code decides the
|
||||
# verdict. No Gitea lookup applies. A duplicate event never
|
||||
# reaches here (match applies only to WAITING_EXTERNAL waits).
|
||||
if not event:
|
||||
return rec # no event → nothing authoritative happened
|
||||
try:
|
||||
exit_code = int(event.get("exit_code"))
|
||||
except (TypeError, ValueError):
|
||||
exit_code = None
|
||||
if exit_code is None:
|
||||
return rec # malformed event: stay WAITING (15E hostile case)
|
||||
verdict = "success" if exit_code == 0 else "failure"
|
||||
evidence = {"process_event": True, "exit_code": exit_code, "operation_id": event.get("run_ref", "")}
|
||||
log(f"verify {rec['wait_id']}: {verdict} {json.dumps(evidence)[:120]}")
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["verdict"] = verdict
|
||||
if verdict == "failure":
|
||||
rec["failure_evidence"] = evidence
|
||||
rec["process_event_evidence"] = evidence
|
||||
waitstore.save(rec)
|
||||
return rec
|
||||
verdict, evidence = _authoritative_verify(rec)
|
||||
log(f"verify {rec['wait_id']}: {verdict} {json.dumps(evidence)[:120]}")
|
||||
if verdict == "success":
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["verdict"] = "success"
|
||||
elif verdict == "failure":
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["verdict"] = "failure"
|
||||
rec["failure_evidence"] = verify.failed_job_evidence(rec.get("run_ref", ""))
|
||||
else:
|
||||
return rec # pending/unknown → stay WAITING
|
||||
waitstore.save(rec)
|
||||
return rec
|
||||
|
||||
|
||||
def continuation_msg(rec: dict) -> str:
|
||||
c = rec["continuation"]
|
||||
if rec.get("verdict") == "success":
|
||||
return (
|
||||
f"External wait satisfied (wait {rec['wait_id']}).\n"
|
||||
f"Original objective: {c.get('objective', '')}\n"
|
||||
f"Waited for: {c.get('waited_for', '')}\n"
|
||||
f"Authoritative result: workflow success verified via Gitea API.\n"
|
||||
f"SHA/run: {rec.get('sha', '')}/{rec.get('run_ref', '')}\n"
|
||||
f"Verify: {c.get('verify_when_woken', '')}\n"
|
||||
f"Continue from: {c.get('on_success', '')}"
|
||||
)
|
||||
ev = json.dumps(rec.get("failure_evidence", {}))[:400]
|
||||
return (
|
||||
f"External wait finished with FAILURE (wait {rec['wait_id']}).\n"
|
||||
f"Original objective: {c.get('objective', '')}\n"
|
||||
f"Authoritative result: workflow FAILURE verified via Gitea API.\n"
|
||||
f"Failed job evidence: {ev}\n"
|
||||
f"SHA/run: {rec.get('sha', '')}/{rec.get('run_ref', '')}\n"
|
||||
f"Do NOT treat this as success. Continue from: {c.get('on_failure', '')}"
|
||||
)
|
||||
|
||||
|
||||
# ── resume driver (single-flight, tree-owned) ─────────────────────────────
|
||||
|
||||
|
||||
def resume(rec: dict) -> None:
|
||||
"""Run one continuation to completion. Runs on a dedicated worker thread.
|
||||
Preconditions: caller verified no inflight worker for this session."""
|
||||
sid, wid = rec["session_id"], rec["wait_id"]
|
||||
# REAL_ASYNC_BRIDGE §2 invariant: a successful authority-verified
|
||||
# operation completion must NEVER be overwritten or downgraded by a
|
||||
# legacy session/FIFO delivery failure. Terminal operation state
|
||||
# (COMPLETED/FAILED — stamped by the waiter's own authority
|
||||
# verification) is owned by the OPERATION domain; this function owns
|
||||
# only SESSION-DELIVERY. Re-read from disk: the in-memory copy may be
|
||||
# stale relative to the waiter's terminal stamp.
|
||||
fresh = waitstore.load(wid)
|
||||
if fresh and fresh.get("status") in waitstore.TERMINAL:
|
||||
log(
|
||||
f"resume {wid}: skipped — operation already terminal "
|
||||
f"({fresh['status']}); delivery worker never overrides "
|
||||
f"operation truth"
|
||||
)
|
||||
return
|
||||
rec = fresh or rec
|
||||
msg = continuation_msg(rec)
|
||||
|
||||
# B: busy-session guard before touching the lease
|
||||
strays = stray_worker_pids(sid)
|
||||
if strays:
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["resume_note"] = f"queued: target session busy (stray pids {strays})"
|
||||
waitstore.save(rec)
|
||||
log(f"queued {wid}: session busy {strays}")
|
||||
return
|
||||
|
||||
# pre-register the lease owner as THIS daemon so acquisition is atomic
|
||||
lease = waitstore.acquire_lease(sid, wid, os.getpid(), os.getpid())
|
||||
if lease is None:
|
||||
live = waitstore.read_lease(sid)
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["resume_note"] = f"queued: lease held by wait {live and live.get('wait_id')}"
|
||||
waitstore.save(rec)
|
||||
log(f"queued {wid}: lease held")
|
||||
return
|
||||
|
||||
rec["status"] = "RESUMING"
|
||||
rec["resume_attempts"] = rec.get("resume_attempts", 0) + 1
|
||||
waitstore.save(rec)
|
||||
|
||||
try:
|
||||
proc = subprocess.Popen(
|
||||
resume_cmd(sid, msg, wid, rec.get("resume_cmd")),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
start_new_session=True,
|
||||
)
|
||||
except Exception as e:
|
||||
waitstore.release_lease(sid, wid)
|
||||
rec["status"] = "READY_TO_RETRY" # spawn failed pre-delivery → retryable
|
||||
rec["resume_note"] = f"spawn failed: {e}"
|
||||
waitstore.save(rec)
|
||||
log(f"spawn {wid} failed pre-delivery: {e}")
|
||||
return
|
||||
|
||||
pgid = proc.pid # start_new_session ⇒ pgid == child pid
|
||||
waitstore.update_lease(sid, wait_id=wid, pid=proc.pid, pgid=pgid, pid_starttime=waitstore._pid_starttime(proc.pid))
|
||||
log(f"launched {wid}: pid={proc.pid} pgid={pgid} attempt={rec['resume_attempts']}")
|
||||
|
||||
timed_out = False
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=WATCH_SECS)
|
||||
rc = proc.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
timed_out = True
|
||||
survivors = terminate_tree(pgid)
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=30)
|
||||
except Exception:
|
||||
stdout, stderr = "", ""
|
||||
rc = proc.returncode
|
||||
if survivors:
|
||||
log(f"ERROR {wid}: group {pgid} survivors after KILL: {survivors}")
|
||||
|
||||
waitstore.release_lease(sid, wid)
|
||||
|
||||
if timed_out:
|
||||
# D+E: timeout is NOT success. Tree terminated; retry truthfully.
|
||||
if rec["resume_attempts"] >= MAX_ATTEMPTS:
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
rec["resume_note"] = (
|
||||
f"watch timeout x{rec['resume_attempts']}; process group {pgid} terminated; attempts exhausted"
|
||||
)
|
||||
else:
|
||||
rec["status"] = "READY_TO_RETRY"
|
||||
rec["resume_note"] = f"watch timeout; process group {pgid} terminated (TERM→KILL, verified); will retry"
|
||||
log(f"timeout {wid}: tree {pgid} terminated, rc={rc}")
|
||||
elif rc == 0:
|
||||
# COMPLETION TRUTH (constraint 4): rc==0 means the headless worker
|
||||
# EXITED CLEANLY — the strongest terminal evidence the supported CLI
|
||||
# provides. Timeout/cancel can never reach this branch. The evidence
|
||||
# chain (rc, output tail, timing) is recorded on the wait record.
|
||||
terminal = "COMPLETED" if rec.get("verdict") == "success" else "FAILED"
|
||||
rec["status"] = terminal
|
||||
rec["resume_evidence"] = {
|
||||
"kind": "worker_exited_rc0",
|
||||
"rc": 0,
|
||||
"output_tail": (stdout or "")[-400:],
|
||||
"finished_at": waitstore.now(),
|
||||
}
|
||||
log(f"resumed {wid}: rc=0 → {terminal}")
|
||||
else:
|
||||
# Nonzero rc: delivery/completion unproven. Honest, not schedulable.
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
rec["resume_evidence"] = {
|
||||
"kind": "worker_exited_nonzero",
|
||||
"rc": rc,
|
||||
"output_tail": ((stdout or "") + (stderr or ""))[-400:],
|
||||
"finished_at": waitstore.now(),
|
||||
}
|
||||
rec["resume_note"] = f"resume rc={rc}; delivery unverified"
|
||||
log(f"resumed {wid}: rc={rc} → RESUME_UNVERIFIED")
|
||||
waitstore.save(rec)
|
||||
|
||||
|
||||
# ── startup recovery (invariant F) ────────────────────────────────────────
|
||||
|
||||
|
||||
def startup_recovery() -> None:
|
||||
for rec in waitstore.all_waits():
|
||||
if rec.get("status") != "RESUMING":
|
||||
continue
|
||||
sid, wid = rec["session_id"], rec["wait_id"]
|
||||
lease = waitstore.read_lease(sid)
|
||||
if lease and lease.get("wait_id") == wid and waitstore._owner_alive(lease):
|
||||
# Adopt the live worker and monitor it to a bounded conclusion.
|
||||
pgid = lease["pgid"]
|
||||
log(f"recover {wid}: adopted live worker pgid={pgid}")
|
||||
deadline = time.time() + WATCH_SECS
|
||||
while time.time() < deadline and pgid_members(pgid):
|
||||
time.sleep(2)
|
||||
survivors = terminate_tree(pgid) if pgid_members(pgid) else []
|
||||
waitstore.release_lease(sid, wid)
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
rec["resume_note"] = "adopted post-restart; outcome unverifiable" + (
|
||||
f"; survivors killed: {survivors}" if survivors else ""
|
||||
)
|
||||
waitstore.save(rec)
|
||||
log(f"recover {wid}: adopted worker ended → RESUME_UNVERIFIED")
|
||||
else:
|
||||
if lease:
|
||||
waitstore.release_lease(sid, wid) # stale
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
rec["resume_note"] = "recovered at daemon startup: no live owner; delivery unverifiable"
|
||||
waitstore.save(rec)
|
||||
log(f"recover {wid}: no live owner → RESUME_UNVERIFIED")
|
||||
|
||||
|
||||
# ── reconciler + drive loop (invariant G: deterministic queue) ────────────
|
||||
|
||||
_inflight: dict[str, threading.Thread] = {}
|
||||
_inflight_lock = threading.Lock()
|
||||
|
||||
|
||||
def drive_ready() -> None:
|
||||
for rec in waitstore.schedulable():
|
||||
sid = rec["session_id"]
|
||||
# CONTENTION POLICY (constraint 2): MiMo exposes NO supported
|
||||
# busy/idle session signal (verified 2026-09-12: `mimo session list
|
||||
# --format json` has no status field; no `session status` command;
|
||||
# no status column in the session schema; no documented ACP/serve
|
||||
# endpoint). Therefore automatic injection into interactive sessions
|
||||
# is NOT YET SAFE and stays disabled. Only waits explicitly declared
|
||||
# target_kind="headless" (disposable/owned sessions) auto-dispatch.
|
||||
if rec.get("target_kind") != "headless":
|
||||
if not rec.get("hold_reason"):
|
||||
rec["hold_reason"] = (
|
||||
"INTERACTIVE_AUTO_RESUME = NOT YET SAFE: "
|
||||
"no supported MiMo busy/idle signal; "
|
||||
"queued until operator releases"
|
||||
)
|
||||
waitstore.save(rec)
|
||||
continue
|
||||
if rec.get("ext", {}).get("resume_mode") == "self":
|
||||
# REAL_ASYNC_BRIDGE §3: self-resuming wait (completion-file /
|
||||
# inotify waiter owns its continuation). The daemon has NO
|
||||
# delivery role here — dispatching a delivery worker would
|
||||
# stamp a phantom rc!=0 over the waiter's own truthful
|
||||
# terminal state (the canonical-state contradiction).
|
||||
# NB: keyed on the explicit self marker, NOT on resume_cmd
|
||||
# absence — records without inline resume_cmd may still be
|
||||
# dispatchable via the WR_RESUME_CMD env template.
|
||||
continue
|
||||
with _inflight_lock:
|
||||
t = _inflight.get(sid)
|
||||
if t is not None and t.is_alive():
|
||||
continue # single-flight: session already being driven
|
||||
# B: never launch into a busy session
|
||||
strays = stray_worker_pids(sid)
|
||||
if strays:
|
||||
log(f"drive: {rec['wait_id']} held, session busy {strays}")
|
||||
continue
|
||||
th = threading.Thread(target=resume, args=(rec,), daemon=True, name=f"resume-{rec['wait_id']}")
|
||||
_inflight[sid] = th
|
||||
# start INSIDE the lock: a just-created thread is not yet
|
||||
# is_alive() until start() runs — starting outside the lock let
|
||||
# the same drive cycle dispatch a second worker for the session
|
||||
# (single-flight race, caught by the T2 invariant test).
|
||||
th.start()
|
||||
log(f"drive: dispatched {rec['wait_id']} (session {sid})")
|
||||
|
||||
|
||||
def reconcile_once() -> None:
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] != "WAITING_EXTERNAL":
|
||||
continue
|
||||
created = rec.get("created_at", "")
|
||||
try:
|
||||
import calendar
|
||||
|
||||
age = time.time() - calendar.timegm(time.strptime(created, "%Y-%m-%dT%H:%M:%SZ"))
|
||||
except ValueError:
|
||||
continue
|
||||
if age > RECONCILE_SECS:
|
||||
try_transition(rec)
|
||||
|
||||
|
||||
_tick = 0
|
||||
|
||||
|
||||
def orphan_sweep() -> int:
|
||||
"""REAL_ASYNC_BRIDGE §4: permanent mid-run orphan-lease recovery.
|
||||
|
||||
FAILURE DETECTION ONLY — never involved in normal completion
|
||||
scheduling. A wait stuck in RESUMING past ORPHAN_GRACE_SECS means the
|
||||
resume flow died between lease acquire and release (worker thread
|
||||
lost, exception mid-watch). Reconcile exactly as startup_recovery
|
||||
would have: release the lease, stamp RESUME_UNVERIFIED truthfully
|
||||
(delivery unverifiable), never auto-relaunch.
|
||||
"""
|
||||
import calendar as _cal
|
||||
|
||||
healed = 0
|
||||
for rec in waitstore.all_waits():
|
||||
if rec.get("status") != "RESUMING":
|
||||
continue
|
||||
try:
|
||||
updated = _cal.timegm(time.strptime(rec.get("last_updated", ""), "%Y-%m-%dT%H:%M:%SZ"))
|
||||
except ValueError:
|
||||
continue
|
||||
if time.time() - updated < ORPHAN_GRACE_SECS:
|
||||
continue # legitimate in-flight watch (up to WATCH_SECS)
|
||||
wid, sid = rec["wait_id"], rec["session_id"]
|
||||
lease = waitstore.read_lease(sid)
|
||||
owner_dead = lease is not None and not waitstore._owner_alive(lease)
|
||||
daemon_owned = lease is not None and lease.get("pid") == os.getpid()
|
||||
if lease is None or owner_dead or daemon_owned:
|
||||
if lease is not None:
|
||||
waitstore.release_lease(sid, wid)
|
||||
if rec.get("resume_attempts", 0) >= MAX_ATTEMPTS:
|
||||
rec["status"] = "RESUME_UNVERIFIED"
|
||||
else:
|
||||
rec["status"] = "READY_TO_RETRY"
|
||||
rec["resume_note"] = (
|
||||
"orphaned mid-run: resume flow died between lease acquire "
|
||||
"and delivery ("
|
||||
+ ("lease owner dead" if owner_dead else "resume thread lost" if daemon_owned else "no lease")
|
||||
+ "); orphan sweep recovered"
|
||||
)
|
||||
waitstore.save(rec)
|
||||
log(f"orphan-sweep {wid}: {rec['status']} — {rec['resume_note']}")
|
||||
healed += 1
|
||||
return healed
|
||||
|
||||
|
||||
def reconciler() -> None:
|
||||
global _tick
|
||||
while True:
|
||||
# v0.3: the local-runner wake is the fast path; periodic
|
||||
# authoritative verification demotes to a slower fallback
|
||||
# (missed-event + daemon-restart recovery), not API polling.
|
||||
_tick += 1
|
||||
if _tick % RECONCILE_EVERY_TICKS == 0:
|
||||
try:
|
||||
reconcile_once()
|
||||
except Exception as e:
|
||||
log(f"reconcile error: {e}")
|
||||
try:
|
||||
orphan_sweep() # §4 failure detection — bounded, periodic
|
||||
except Exception as e:
|
||||
log(f"orphan-sweep error: {e}")
|
||||
try:
|
||||
drive_ready()
|
||||
except Exception as e:
|
||||
log(f"drive error: {e}")
|
||||
with _inflight_lock:
|
||||
for sid, t in list(_inflight.items()):
|
||||
if not t.is_alive():
|
||||
del _inflight[sid]
|
||||
time.sleep(DRIVE_LOOP_SECS)
|
||||
|
||||
|
||||
# ── webhook receiver (wake signal only) ───────────────────────────────────
|
||||
|
||||
|
||||
def wake_sweep(event: dict, eid: str) -> dict:
|
||||
"""Sweep WAITING_EXTERNAL waits through authoritative verification.
|
||||
|
||||
Runs on a background thread (the hook's HTTP response returns
|
||||
instantly, minimising the runner's offline window). If any verified
|
||||
wait got 'pending' — the normal case for a job that finished
|
||||
microseconds ago: the post-task hook fires after results are reported
|
||||
but before the runner's final task acknowledgement flips server-side
|
||||
status — retry on a BOUNDED event-triggered schedule (default 4s x3),
|
||||
never a polling loop. The final ack lands milliseconds after the hook
|
||||
exits, so the first retry almost always sees the terminal state.
|
||||
"""
|
||||
arrival = time.time()
|
||||
log(f"local wake {eid}: {json.dumps(event)[:140]}")
|
||||
import calendar
|
||||
|
||||
def sweep() -> tuple[int, int, int]:
|
||||
verified = advanced = pending = 0
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] != "WAITING_EXTERNAL":
|
||||
continue
|
||||
try:
|
||||
age = time.time() - calendar.timegm(time.strptime(rec.get("created_at", ""), "%Y-%m-%dT%H:%M:%SZ"))
|
||||
except ValueError:
|
||||
continue
|
||||
if age < WAKE_MIN_AGE_SECS:
|
||||
continue
|
||||
verified += 1
|
||||
before = rec["status"]
|
||||
try_transition(rec)
|
||||
if rec["status"] != before and rec["status"] == "READY_TO_RESUME":
|
||||
advanced += 1
|
||||
elif rec["status"] == "WAITING_EXTERNAL":
|
||||
pending += 1
|
||||
return verified, advanced, pending
|
||||
|
||||
verified, advanced, pending = sweep()
|
||||
log(
|
||||
f"local wake {eid}: verified={verified} advanced={advanced} "
|
||||
f"pending={pending} sweep_ms={int((time.time() - arrival) * 1000)}"
|
||||
)
|
||||
for attempt in range(1, WAKE_RETRIES + 1):
|
||||
# Retry while the sweep didn't conclusively advance anything:
|
||||
# pending verdicts (ack-window) AND fresh-wait skips (age gate).
|
||||
# Bounded, event-triggered — not a polling loop.
|
||||
if not pending and verified:
|
||||
break
|
||||
time.sleep(WAKE_RETRY_SECS)
|
||||
verified, advanced, pending = sweep()
|
||||
log(
|
||||
f"local wake {eid}: retry{attempt} verified={verified} "
|
||||
f"advanced={advanced} pending={pending} "
|
||||
f"total_ms={int((time.time() - arrival) * 1000)}"
|
||||
)
|
||||
return {"event_id": eid}
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_POST(self):
|
||||
if self.path == "/process-event":
|
||||
# T15 §15D: process-exit events from builder/controller
|
||||
# notifiers. Minimal dedicated endpoint — the Gitea webhook
|
||||
# path stays untouched. Payload: {operation_id, exit_code,
|
||||
# completed_at} + OPTIONAL provider/event_type/sha (T16
|
||||
# §16C/§16E: the CI adapter registers as provider="gitea",
|
||||
# event_type="ci_completed"; defaults preserve the T15 builder
|
||||
# identity so existing notifiers are unaffected). The event is
|
||||
# a WAKE, never truth — resumed controllers re-read canonical
|
||||
# authority before any verdict.
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
try:
|
||||
payload = json.loads(body) if body else {}
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
return
|
||||
event = {
|
||||
"provider": str(payload.get("provider", "builder")),
|
||||
"event_type": str(payload.get("event_type", "builder_exit")),
|
||||
"run_ref": str(payload.get("operation_id", "")) or None,
|
||||
"sha": payload.get("sha"),
|
||||
"exit_code": payload.get("exit_code"),
|
||||
"completed_at": payload.get("completed_at") or "",
|
||||
}
|
||||
event = {k: v for k, v in event.items() if v is not None}
|
||||
eid = waitstore.record_event(event)
|
||||
log(f"process-event {eid}: {json.dumps(event)[:140]}")
|
||||
# REAL_ASYNC_BRIDGE (R3): durable completion-file delivery.
|
||||
# Every event reaching the bridge is written as
|
||||
# resumes/<operation_id>.done so inotify waiters wake on the
|
||||
# kernel event (R4) with no FIFO/lease machinery, and the
|
||||
# event survives daemon AND waiter restarts (R9). Atomic
|
||||
# rename; duplicate events rewrite identical content (R10).
|
||||
_op_id = str(payload.get("operation_id", ""))
|
||||
if _op_id:
|
||||
try:
|
||||
# Stable identity only: duplicate deliveries of the
|
||||
# same logical event must produce byte-identical
|
||||
# completion files (R10 idempotence by construction).
|
||||
# completed_at is volatile and lives in the durable
|
||||
# event log; the file is a WAKE SIGNAL, never truth.
|
||||
_stable = {
|
||||
k: event.get(k)
|
||||
for k in ("provider", "event_type", "run_ref", "sha", "exit_code")
|
||||
if event.get(k) is not None
|
||||
}
|
||||
completion_file.write_completion(_op_id, _stable)
|
||||
except Exception as e: # noqa: BLE001 — delivery best-effort; wait-record transition + watchdog recovery still cover the lost file
|
||||
log(f"process-event {eid}: completion-file write failed: {e}")
|
||||
matched = 0
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] == "WAITING_EXTERNAL" and waitstore.match(rec, event):
|
||||
matched += 1
|
||||
try_transition(rec, event)
|
||||
# matched READY controller waits are dispatched IMMEDIATELY
|
||||
# (event-driven, no floor). Guard: only never-resumed waits —
|
||||
# a duplicate/late event must not double-dispatch (15E);
|
||||
# single-flight lease + this guard together make resume
|
||||
# idempotent.
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] == "READY_TO_RESUME" and rec.get("resume_cmd") and rec.get("resume_attempts", 0) == 0:
|
||||
threading.Thread(target=resume, args=(rec,), daemon=True, name=f"resume-{rec['wait_id']}").start()
|
||||
matched += 1
|
||||
try:
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"event_id": eid, "matched": matched}).encode())
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
return
|
||||
if self.path == "/wake":
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
try:
|
||||
payload = json.loads(body) if body else {}
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
return
|
||||
event = {
|
||||
"provider": "gitea-local-runner",
|
||||
"task_id": str(payload.get("task_id", "")) or None,
|
||||
"run_id": str(payload.get("run_id", "")) or None,
|
||||
"repository": str(payload.get("repository", "")) or None,
|
||||
"job_result": str(payload.get("job_result", "")) or None,
|
||||
}
|
||||
event = {k: v for k, v in event.items() if v}
|
||||
eid = waitstore.record_event(event)
|
||||
# Respond instantly; sweep on a worker thread. The hook is
|
||||
# fire-and-forget (curl -m 2) — a broken pipe on write is
|
||||
# harmless and must not propagate.
|
||||
try:
|
||||
threading.Thread(
|
||||
target=wake_sweep, args=(event, eid), daemon=True, name=f"wake-{payload.get('task_id', '?')}"
|
||||
).start()
|
||||
except Exception as e:
|
||||
log(f"wake spawn error: {e}")
|
||||
try:
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"queued": True, "event_id": eid}).encode())
|
||||
except (BrokenPipeError, ConnectionResetError):
|
||||
pass
|
||||
return
|
||||
if self.path != "/webhook":
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
return
|
||||
length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(length)
|
||||
try:
|
||||
payload = json.loads(body)
|
||||
except json.JSONDecodeError:
|
||||
self.send_response(400)
|
||||
self.end_headers()
|
||||
return
|
||||
action = payload.get("action") or payload.get("event_type") or ""
|
||||
wf = payload.get("workflow_run") or {}
|
||||
event = {
|
||||
"provider": "gitea",
|
||||
"event_type": f"workflow.{action}" if wf else action,
|
||||
"sha": (wf.get("head_sha") or payload.get("sha") or "")[:12] or None,
|
||||
"run_ref": str(wf.get("id") or payload.get("run_number") or "") or None,
|
||||
}
|
||||
event = {k: v for k, v in event.items() if v}
|
||||
eid = waitstore.record_event(event)
|
||||
log(f"webhook event {eid}: {json.dumps(event)[:140]}")
|
||||
matched = 0
|
||||
for rec in waitstore.pending():
|
||||
if rec["status"] == "WAITING_EXTERNAL" and waitstore.match(rec, event):
|
||||
matched += 1
|
||||
try_transition(rec)
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"event_id": eid, "matched": matched}).encode())
|
||||
|
||||
def do_GET(self):
|
||||
if self.path == "/health":
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
self.wfile.write(b'{"ok":true}')
|
||||
return
|
||||
self.send_response(404)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
|
||||
def main() -> int:
|
||||
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8791
|
||||
|
||||
# H (constraint 1): flock is the SINGLETON AUTHORITY. daemon.pid's
|
||||
# content is diagnostic metadata only — it never gates startup, never
|
||||
# implies liveness, and is overwritten after the exclusive lock is held.
|
||||
# Stale pid content (incl. SIGKILL debris) is thus harmless by design.
|
||||
PIDFILE.parent.mkdir(parents=True, exist_ok=True)
|
||||
lock_fd = os.open(PIDFILE, os.O_CREAT | os.O_RDWR, 0o644)
|
||||
try:
|
||||
fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
log("refusing start: another daemon holds the pidfile flock")
|
||||
print("daemon already running (pidfile flock held)", file=sys.stderr)
|
||||
return 1
|
||||
os.ftruncate(lock_fd, 0)
|
||||
os.write(lock_fd, f"{os.getpid()}\n".encode()) # diagnostic, lock held for life
|
||||
|
||||
startup_recovery()
|
||||
t = threading.Thread(target=reconciler, daemon=True)
|
||||
t.start()
|
||||
log(f"daemon starting on :{port}")
|
||||
srv = ThreadingHTTPServer(("0.0.0.0", port), Handler)
|
||||
try:
|
||||
srv.serve_forever()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Executable
+246
@@ -0,0 +1,246 @@
|
||||
#!/usr/bin/env python3
|
||||
"""verify.py — authoritative Gitea verification (webhook ≠ success).
|
||||
|
||||
Queries the Gitea API for the TRUE state of the run/job/commit the wait
|
||||
references. Returns (verdict, evidence). verdict ∈ {success, failure,
|
||||
pending, unknown}. Only this module may move a wait out of
|
||||
WAITING_EXTERNAL.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import json, subprocess
|
||||
from pathlib import Path
|
||||
|
||||
HOST = "https://gitea.thingscouldgetdicey.online"
|
||||
REPO = "conductor/rune"
|
||||
|
||||
|
||||
def _token() -> str:
|
||||
# Canonical ladder without importing Rune: env → token file → git store
|
||||
import os
|
||||
|
||||
tok = os.environ.get("GITEA_TOKEN", "").strip()
|
||||
if tok:
|
||||
return tok
|
||||
tf = os.path.expanduser("~/.config/opencode/secrets/gitea-token")
|
||||
if os.path.isfile(tf):
|
||||
return open(tf).read().strip()
|
||||
r = subprocess.run(
|
||||
["git", "credential", "fill"],
|
||||
input="protocol=https\nhost=gitea.thingscouldgetdicey.online\nusername=conductor\n\n",
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
)
|
||||
for line in r.stdout.splitlines():
|
||||
if line.startswith("password="):
|
||||
return line[9:]
|
||||
raise RuntimeError("no gitea credential available")
|
||||
|
||||
|
||||
def _api(path: str) -> tuple[int, object]:
|
||||
import urllib.request
|
||||
|
||||
req = urllib.request.Request(f"{HOST}/api/v1/repos/{REPO}{path}", headers={"Authorization": f"token {_token()}"})
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=20) as r:
|
||||
return r.status, json.loads(r.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, {"error": e.read().decode()[:200]}
|
||||
|
||||
|
||||
def _jobs_list(run_ref: str):
|
||||
"""GET jobs for a run, unwrapping the {'jobs': [...]} envelope."""
|
||||
s, data = _api(f"/actions/runs/{run_ref}/jobs")
|
||||
if s != 200:
|
||||
return s, None
|
||||
if isinstance(data, dict):
|
||||
data = data.get("jobs", [])
|
||||
return s, data if isinstance(data, list) else None
|
||||
|
||||
|
||||
def _resolve_run_id(run_ref: str, sha: str) -> str:
|
||||
"""Task-list ids are NOT run ids. If run_ref lookup 404s, resolve the
|
||||
real run id by head SHA via /actions/runs (T15: run 6907 vs task 15743)."""
|
||||
if run_ref:
|
||||
s, jobs = _jobs_list(run_ref)
|
||||
if s == 200 and jobs:
|
||||
return run_ref
|
||||
if sha:
|
||||
s, runs = _api(f"/actions/runs?limit=30")
|
||||
items = runs.get("workflow_runs", []) if isinstance(runs, dict) else (runs if isinstance(runs, list) else [])
|
||||
# One SHA fans out to multiple runs (lint + deploy-only). Prefer a
|
||||
# substantive (non-skipped) run; a skipped-only run proves nothing.
|
||||
fallback = ""
|
||||
for r in items:
|
||||
if str(r.get("head_sha", "")).startswith(sha):
|
||||
rid = str(r.get("id"))
|
||||
if r.get("conclusion") != "skipped":
|
||||
return rid
|
||||
fallback = fallback or rid
|
||||
return fallback
|
||||
return ""
|
||||
|
||||
|
||||
def verify_run(run_ref: str, sha: str, expected: str) -> tuple[str, dict]:
|
||||
"""run_ref = actions run id (or task id — resolved via SHA lookup).
|
||||
|
||||
Defect fixed 2026-09-12 (surfaced by W/R-v0.3 live proof, run 7110):
|
||||
the jobs query here used raw _api + isinstance(jobs, list), but this
|
||||
Gitea wraps jobs in the {"jobs": [...]} envelope — the check always
|
||||
failed, so EVERY EXACT_WORKFLOW verdict silently fell through to the
|
||||
aggregate commit-status fallback, where unrelated runs on the same
|
||||
SHA (multi-run fan-out: ci.yml + deploy-only) poison the verdict.
|
||||
Use _jobs_list (unwraps the envelope), same as _resolve_run_id."""
|
||||
run_ref = _resolve_run_id(run_ref, sha)
|
||||
if run_ref:
|
||||
s, jobs = _jobs_list(run_ref)
|
||||
if s == 200 and isinstance(jobs, list) and jobs:
|
||||
conclusions = {j.get("name"): j.get("conclusion") for j in jobs}
|
||||
statuses = {j.get("status") for j in jobs}
|
||||
if statuses <= {"completed"}:
|
||||
verdict = "success" if set(conclusions.values()) <= {"success", "skipped"} else "failure"
|
||||
return verdict, {"run_ref": run_ref, "jobs": conclusions}
|
||||
return "pending", {"run_ref": run_ref, "statuses": sorted(statuses)}
|
||||
if sha:
|
||||
s, st = _api(f"/commits/{sha}/status")
|
||||
if s == 200 and isinstance(st, dict):
|
||||
state = st.get("state", "unknown") # success|error|failure|pending|warning
|
||||
rid = _resolve_run_id("", sha) # attach run id so failure evidence works
|
||||
if state == "success":
|
||||
return "success", {"sha": sha, "state": state, "run_ref": rid}
|
||||
if state in ("error", "failure"):
|
||||
return "failure", {"sha": sha, "state": state, "run_ref": rid}
|
||||
return "pending", {"sha": sha, "state": state}
|
||||
return "unknown", {"run_ref": run_ref, "sha": sha}
|
||||
|
||||
|
||||
def failed_job_evidence(run_ref: str, sha: str = "") -> dict:
|
||||
"""For failure continuations: exact failing job + conclusion."""
|
||||
run_ref = _resolve_run_id(run_ref, sha)
|
||||
if not run_ref:
|
||||
return {}
|
||||
s, jobs = _jobs_list(run_ref)
|
||||
if s != 200 or not isinstance(jobs, list):
|
||||
return {}
|
||||
bad = [j for j in jobs if j.get("conclusion") not in ("success", "skipped", None)]
|
||||
return {
|
||||
"failed_jobs": [
|
||||
{"name": j.get("name"), "conclusion": j.get("conclusion"), "url": j.get("url", "")} for j in bad
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
# ── RELEASE_DEPLOY verification (W/R v0.1, overnight handoff Part 2) ──────
|
||||
# Aggregate commit status is NOT a valid completion authority for Rune's
|
||||
# release/deploy flow: the release successor SHA, not the source SHA, is what
|
||||
# converges (observed live: wr-1789179026 stranded on pending while 0.66.15
|
||||
# was deployed). The authority chain is:
|
||||
# source SHA → source runs terminal → release successor commit (descendant)
|
||||
# → successor runs success → deployed/current identity converges.
|
||||
|
||||
|
||||
def _git(args: list[str], repo_root: str) -> str:
|
||||
import subprocess
|
||||
|
||||
r = subprocess.run(["git"] + args, capture_output=True, text=True, cwd=repo_root or None, timeout=15)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError(f"git {' '.join(args[:2])}: {r.stderr.strip()[:120]}")
|
||||
return r.stdout
|
||||
|
||||
|
||||
def _runs_for_sha(sha: str) -> list[dict]:
|
||||
s, runs = _api("/actions/runs?limit=50")
|
||||
items = runs.get("workflow_runs", []) if isinstance(runs, dict) else []
|
||||
return [r for r in items if str(r.get("head_sha", "")).startswith(sha)]
|
||||
|
||||
|
||||
def _sha_verdict_from_runs(runs: list[dict]) -> tuple[str, dict]:
|
||||
"""success | failure | pending from a SHA's workflow runs.
|
||||
Terminal verdict ONLY when every run is completed — mixed states
|
||||
(some success, some still in flight) must stay PENDING (observed live:
|
||||
a SHA with 1 completed-success + 1 queued + 1 in-progress was judged
|
||||
success mid-flight; wr-1789182522 2026-09-12)."""
|
||||
if not runs:
|
||||
return "pending", {"note": "no runs found"}
|
||||
non_terminal = [r for r in runs if r.get("status") != "completed"]
|
||||
if non_terminal:
|
||||
return "pending", {"in_flight": len(non_terminal)}
|
||||
conclusions = {r.get("name"): r.get("conclusion") for r in runs}
|
||||
bad = {n: c for n, c in conclusions.items() if c == "failure"}
|
||||
if bad:
|
||||
return "failure", {"failed": bad}
|
||||
return "success", {"runs": conclusions}
|
||||
|
||||
|
||||
def verify_release_deploy(source_sha: str, repo_root: str = "", deployed_link: str = "") -> tuple[str, dict]:
|
||||
"""RELEASE_DEPLOY authority chain. Returns (verdict, evidence)."""
|
||||
source_sha = (source_sha or "")[:12]
|
||||
ev: dict = {"source_sha": source_sha, "kind": "RELEASE_DEPLOY"}
|
||||
|
||||
# 1. Source SHA's own runs must be terminal and healthy.
|
||||
src_runs = _runs_for_sha(source_sha)
|
||||
src_verdict, src_ev = _sha_verdict_from_runs(src_runs)
|
||||
ev["source"] = src_ev
|
||||
if src_verdict != "success":
|
||||
return src_verdict, ev
|
||||
|
||||
# 2. Release successor: first chore(release) commit descending from source.
|
||||
if not repo_root:
|
||||
return "pending", {**ev, "note": "no repo_root — cannot check lineage"}
|
||||
try:
|
||||
out = _git(
|
||||
[
|
||||
"log",
|
||||
"--format=%H %s",
|
||||
"--reverse",
|
||||
f"{source_sha}..master",
|
||||
"--grep=^chore\\(release\\)",
|
||||
"--extended-regexp",
|
||||
],
|
||||
repo_root,
|
||||
)
|
||||
except RuntimeError as e:
|
||||
return "pending", {**ev, "note": str(e)}
|
||||
successors = [l for l in out.splitlines() if l.strip()]
|
||||
if not successors:
|
||||
return "pending", {**ev, "note": "no release successor yet"}
|
||||
succ_line = successors[0]
|
||||
succ_sha_full, _, succ_subject = succ_line.partition(" ")
|
||||
succ_sha = succ_sha_full[:12]
|
||||
ev["successor"] = {"sha": succ_sha, "subject": succ_subject[:80]}
|
||||
|
||||
# Ancestry is guaranteed by the range, but verify explicitly (deterministic
|
||||
# lineage over title guessing):
|
||||
try:
|
||||
_git(["merge-base", "--is-ancestor", source_sha, succ_sha_full], repo_root)
|
||||
except RuntimeError:
|
||||
return "failure", {**ev, "note": "successor is not a descendant of source"}
|
||||
|
||||
# 3. Successor runs must be terminal and healthy.
|
||||
succ_runs = _runs_for_sha(succ_sha)
|
||||
succ_verdict, succ_ev = _sha_verdict_from_runs(succ_runs)
|
||||
ev["successor"]["runs"] = succ_ev
|
||||
if succ_verdict != "success":
|
||||
return succ_verdict, ev
|
||||
|
||||
# 4. Deployed identity convergence. A mismatch is FAILURE only when the
|
||||
# successor's runs are ALL completed — if any is still running/queued the
|
||||
# deploy may simply not have propagated yet → PENDING (observed live:
|
||||
# reconciler checked mid-deploy, verdict failure, deployment converged
|
||||
# ~2 min later; wr-1789182522 2026-09-12).
|
||||
if deployed_link:
|
||||
link = Path(deployed_link)
|
||||
if link.is_symlink() or link.is_dir():
|
||||
deployed = str(link.resolve())
|
||||
deployed_id = Path(deployed).name
|
||||
ev["deployed"] = deployed_id
|
||||
if not deployed_id.startswith(succ_sha[:12]):
|
||||
still = [r for r in _runs_for_sha(succ_sha) if r.get("status") != "completed"]
|
||||
if still:
|
||||
return "pending", {**ev, "note": "deploy propagating (runs in flight)"}
|
||||
return "failure", {**ev, "note": f"deployed {deployed_id} != successor {succ_sha} (all runs terminal)"}
|
||||
else:
|
||||
return "failure", {**ev, "note": f"deployed link missing: {deployed_link}"}
|
||||
|
||||
return "success", ev
|
||||
Executable
+337
@@ -0,0 +1,337 @@
|
||||
#!/usr/bin/env python3
|
||||
"""waitstore.py — wait/resume state machine v0.2 (developer tooling, NOT Rune).
|
||||
|
||||
States:
|
||||
RUNNABLE → WAITING_EXTERNAL → READY_TO_RESUME → RESUMING → COMPLETED | FAILED
|
||||
↑ │
|
||||
│ ├─(watch timeout, tree killed)
|
||||
│ └─(rc!=0 / unverifiable)
|
||||
READY_TO_RETRY ←─────┘
|
||||
RESUME_UNVERIFIED : delivery after crash/timeout cannot be proven.
|
||||
TERMINAL-ish — never auto-scheduled; explicit
|
||||
`requeue()` required to make it READY_TO_RESUME.
|
||||
TIMED_OUT : external event never arrived within the wait budget.
|
||||
|
||||
Waits persist as waits/<wait_id>.json (atomic tmp+rename).
|
||||
Leases persist as leases/<session_id>.lease.json (atomic tmp+rename,
|
||||
validated against /proc so a dead or PID-reused owner is stealable).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
import fcntl, json, os, tempfile, time, uuid
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(os.environ.get("WR_ROOT", "") or Path(__file__).resolve().parent)
|
||||
WAITS = ROOT / "waits"
|
||||
EVENTS = ROOT / "events"
|
||||
LEASES = ROOT / "leases"
|
||||
|
||||
SCHEDULABLE = {"READY_TO_RESUME", "READY_TO_RETRY"}
|
||||
VALID_STATES = {
|
||||
"RUNNABLE",
|
||||
"WAITING_EXTERNAL",
|
||||
"READY_TO_RESUME",
|
||||
"RESUMING",
|
||||
"COMPLETED",
|
||||
"FAILED",
|
||||
"READY_TO_RETRY",
|
||||
"RESUME_UNVERIFIED",
|
||||
"TIMED_OUT",
|
||||
}
|
||||
TERMINAL = {"COMPLETED", "FAILED", "RESUME_UNVERIFIED", "TIMED_OUT"}
|
||||
|
||||
|
||||
def _atomic_write(path: Path, data: dict) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, tmp = tempfile.mkstemp(dir=str(path.parent), prefix=".tmp-")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(data, f, indent=1, sort_keys=True)
|
||||
os.replace(tmp, path)
|
||||
# T17 17C: the bridge root is SHARED across users (deploy waiter
|
||||
# runs as the runner user; daemon as nick). mkstemp creates 0600 —
|
||||
# daemon re-saves locked the runner user out of records it must
|
||||
# read (waitstore.load in await_resume_event; find_matching_event
|
||||
# replay scan silently skipped them). 0644: owner writes, all read.
|
||||
os.chmod(path, 0o644)
|
||||
except BaseException:
|
||||
if os.path.exists(tmp):
|
||||
os.unlink(tmp)
|
||||
raise
|
||||
|
||||
|
||||
def now() -> str:
|
||||
return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
|
||||
|
||||
def new_wait(
|
||||
*,
|
||||
session_id: str,
|
||||
provider: str,
|
||||
event_type: str,
|
||||
repo: str,
|
||||
sha: str = "",
|
||||
run_ref: str = "",
|
||||
expected: str = "success",
|
||||
wait_kind: str = "EXACT_WORKFLOW",
|
||||
target_kind: str = "interactive",
|
||||
repo_root: str = "",
|
||||
deployed_link: str = "",
|
||||
objective: str = "",
|
||||
waited_for: str = "",
|
||||
verify_when_woken: str = "",
|
||||
on_success: str = "",
|
||||
on_failure: str = "",
|
||||
resume_cmd: list | None = None,
|
||||
) -> dict:
|
||||
wid = f"wr-{int(time.time())}-{uuid.uuid4().hex[:6]}"
|
||||
rec = {
|
||||
"wait_id": wid,
|
||||
"session_id": session_id,
|
||||
"created_at": now(),
|
||||
"provider": provider,
|
||||
"event_type": event_type,
|
||||
"repo": repo,
|
||||
"sha": sha,
|
||||
"run_ref": run_ref,
|
||||
"expected": expected,
|
||||
# T15 §15D: PROCESS_EVENT — a wait whose authoritative verdict is
|
||||
# the matching process-exit event itself (exit_code carried on the
|
||||
# event), e.g. a builder/controller process completing. The event
|
||||
# IS the authority here (the notifier wrapper observed the real
|
||||
# process exit); no Gitea lookup applies.
|
||||
"wait_kind": wait_kind
|
||||
if wait_kind in ("EXACT_WORKFLOW", "RELEASE_DEPLOY", "PROCESS_EVENT")
|
||||
else "EXACT_WORKFLOW",
|
||||
# CONTENTION POLICY: "interactive" (default, conservative) never
|
||||
# auto-dispatches — MiMo exposes NO supported busy/idle session
|
||||
# signal, so injecting into a possibly-busy interactive session is
|
||||
# NOT YET SAFE. Only explicitly declared headless/disposable
|
||||
# targets ("headless") are auto-resumed.
|
||||
"target_kind": target_kind if target_kind in ("headless", "interactive") else "interactive",
|
||||
"repo_root": repo_root,
|
||||
"deployed_link": deployed_link,
|
||||
# T15 §15D: per-wait resume command override (placeholder template,
|
||||
# same substitution as WR_RESUME_CMD: {session_id}/{msg}/{wait_id}).
|
||||
# A synthetic controller session (e.g. a Scaffolding stage driver)
|
||||
# is NOT a MiMo session — the global mimo-shaped default cannot
|
||||
# resume it, which is exactly why fixed-sleep polling crept back
|
||||
# in T12-T14. Per-wait override makes ANY registered continuation
|
||||
# target resumable through the same bridge.
|
||||
"resume_cmd": list(resume_cmd) if resume_cmd else None,
|
||||
"status": "WAITING_EXTERNAL",
|
||||
"last_verified": "",
|
||||
"resume_attempts": 0,
|
||||
"continuation": {
|
||||
"objective": objective,
|
||||
"waited_for": waited_for,
|
||||
"verify_when_woken": verify_when_woken,
|
||||
"on_success": on_success,
|
||||
"on_failure": on_failure,
|
||||
},
|
||||
}
|
||||
_atomic_write(WAITS / f"{wid}.json", rec)
|
||||
return rec
|
||||
|
||||
|
||||
def load(wid: str) -> dict | None:
|
||||
p = WAITS / f"{wid}.json"
|
||||
return json.loads(p.read_text()) if p.is_file() else None
|
||||
|
||||
|
||||
def save(rec: dict) -> None:
|
||||
assert rec.get("status") in VALID_STATES, rec.get("status")
|
||||
rec["last_updated"] = now()
|
||||
_atomic_write(WAITS / f"{rec['wait_id']}.json", rec)
|
||||
|
||||
|
||||
def all_waits() -> list[dict]:
|
||||
out = []
|
||||
for p in WAITS.glob("*.json"):
|
||||
try:
|
||||
out.append(json.loads(p.read_text()))
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def pending() -> list[dict]:
|
||||
return [r for r in all_waits() if r.get("status") in {"WAITING_EXTERNAL"} | SCHEDULABLE]
|
||||
|
||||
|
||||
def schedulable() -> list[dict]:
|
||||
"""READY waits, oldest first — deterministic queue order."""
|
||||
rs = [r for r in all_waits() if r.get("status") in SCHEDULABLE]
|
||||
rs.sort(key=lambda r: (r.get("created_at", ""), r.get("wait_id", "")))
|
||||
return rs
|
||||
|
||||
|
||||
def requeue(wid: str, note: str) -> dict | None:
|
||||
"""Operator-explicit requeue of a RESUME_UNVERIFIED/RESUME_FAILED wait."""
|
||||
rec = load(wid)
|
||||
if not rec:
|
||||
return None
|
||||
rec["status"] = "READY_TO_RESUME"
|
||||
rec["resume_note"] = note
|
||||
save(rec)
|
||||
return rec
|
||||
|
||||
|
||||
# ── events (wake signals, idempotent) ─────────────────────────────────────
|
||||
|
||||
|
||||
def record_event(event: dict) -> str:
|
||||
import hashlib
|
||||
|
||||
body = json.dumps(event, sort_keys=True)
|
||||
eid = hashlib.sha256(body.encode()).hexdigest()[:16]
|
||||
f = EVENTS / f"{eid}.json"
|
||||
if not f.is_file():
|
||||
_atomic_write(f, {"event_id": eid, "received_at": now(), "event": event})
|
||||
return eid
|
||||
|
||||
|
||||
def match(rec: dict, event: dict) -> bool:
|
||||
if rec.get("provider") != event.get("provider"):
|
||||
return False
|
||||
if rec.get("event_type") and event.get("event_type") and rec["event_type"] != event["event_type"]:
|
||||
return False
|
||||
if rec.get("sha") and event.get("sha") and rec["sha"] != event["sha"]:
|
||||
return False
|
||||
if rec.get("run_ref") and event.get("run_ref") and rec["run_ref"] != event["run_ref"]:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
# ── leases (per-session single-flight, /proc-validated) ───────────────────
|
||||
#
|
||||
# Invariant 3 (PID reuse): liveness is NEVER "PID exists". The lease records
|
||||
# pid + pgid + process starttime (/proc/<pid>/stat field 22); all three must
|
||||
# match or the owner is a different process wearing a recycled PID.
|
||||
#
|
||||
# Invariant 1 (flock as authority): the lease file's flock serializes
|
||||
# read-modify-write across processes and dies with its holder, so a crashed
|
||||
# daemon can never wedge the lock. The lease JSON + /proc decide who OWNS the
|
||||
# session; the flock only prevents racing acquirers from double-stealing.
|
||||
|
||||
_lease_fds: dict[str, int] = {} # session_id → open fd holding the flock
|
||||
|
||||
|
||||
def _pid_starttime(pid: int) -> str | None:
|
||||
"""Field 22 of /proc/<pid>/stat — distinguishes PID reuse."""
|
||||
try:
|
||||
stat = Path(f"/proc/{pid}/stat").read_text()
|
||||
return stat.rsplit(")", 1)[1].split()[19]
|
||||
except (OSError, IndexError):
|
||||
return None
|
||||
|
||||
|
||||
def _owner_alive(lease: dict) -> bool:
|
||||
pid = lease.get("pid")
|
||||
if not pid or not Path(f"/proc/{pid}").is_dir():
|
||||
return False
|
||||
if _pid_starttime(pid) != lease.get("pid_starttime"):
|
||||
return False # PID reused by an unrelated process
|
||||
try:
|
||||
if os.getpgid(pid) != lease.get("pgid"):
|
||||
return False
|
||||
except OSError:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def lease_path(session_id: str) -> Path:
|
||||
# T16 §16C finding (observed live in daemon.log): session ids like
|
||||
# "controller-<op>-Verboo Code/glm-5.3-flash" contain "/" — the raw
|
||||
# embed made lease creation raise FileNotFoundError, so resume
|
||||
# delivery for those waits could never succeed. Sanitize to a flat
|
||||
# filesystem-safe name; the hash suffix prevents collisions between
|
||||
# ids that sanitize identically.
|
||||
import hashlib as _hashlib
|
||||
import re as _re
|
||||
|
||||
safe = _re.sub(r"[^A-Za-z0-9._-]+", "_", session_id)[:80]
|
||||
digest = _hashlib.sha256(session_id.encode()).hexdigest()[:10]
|
||||
return LEASES / f"{safe}-{digest}.lease.json"
|
||||
|
||||
|
||||
def read_lease(session_id: str) -> dict | None:
|
||||
p = lease_path(session_id)
|
||||
if not p.is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text())
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def _locked_lease_fd(session_id: str) -> int | None:
|
||||
"""Open the lease file and take its flock (non-blocking). The fd stays
|
||||
open (lock held) until release_lease closes it."""
|
||||
if session_id in _lease_fds:
|
||||
return _lease_fds[session_id] # already hold it
|
||||
LEASES.mkdir(parents=True, exist_ok=True)
|
||||
fd = os.open(lease_path(session_id), os.O_CREAT | os.O_RDWR, 0o600)
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except OSError:
|
||||
os.close(fd)
|
||||
return None # another process holds the lock — mid-acquire
|
||||
_lease_fds[session_id] = fd
|
||||
return fd
|
||||
|
||||
|
||||
def _write_lease_locked(session_id: str, lease: dict) -> None:
|
||||
fd = _lease_fds[session_id]
|
||||
os.ftruncate(fd, 0)
|
||||
os.lseek(fd, 0, os.SEEK_SET)
|
||||
os.write(fd, json.dumps(lease, indent=1, sort_keys=True).encode())
|
||||
os.fsync(fd)
|
||||
|
||||
|
||||
def acquire_lease(session_id: str, wait_id: str, pid: int, pgid: int) -> dict | None:
|
||||
"""Atomically create or steal the session lease. Returns the lease dict,
|
||||
or None if a live owner holds it (or another process is mid-steal).
|
||||
Staleness is decided by /proc identity evidence, never by age alone."""
|
||||
fd = _locked_lease_fd(session_id)
|
||||
if fd is None:
|
||||
return None
|
||||
existing = read_lease(session_id)
|
||||
if existing and _owner_alive(existing):
|
||||
_lease_fds.pop(session_id, None)
|
||||
os.close(fd)
|
||||
return None # live worker owns this session
|
||||
lease = {
|
||||
"session_id": session_id,
|
||||
"wait_id": wait_id,
|
||||
"pid": pid,
|
||||
"pgid": pgid,
|
||||
"pid_starttime": _pid_starttime(pid),
|
||||
"acquired_at": now(),
|
||||
"state": "RESUMING",
|
||||
}
|
||||
_write_lease_locked(session_id, lease)
|
||||
return lease
|
||||
|
||||
|
||||
def update_lease(session_id: str, **fields) -> None:
|
||||
lease = read_lease(session_id) or {}
|
||||
lease.update(fields)
|
||||
_write_lease_locked(session_id, lease)
|
||||
|
||||
|
||||
def release_lease(session_id: str, wait_id: str) -> bool:
|
||||
"""Release only if the current lease belongs to this wait (no stealing
|
||||
a successor's lease during a slow shutdown path). Closes the flock fd."""
|
||||
lease = read_lease(session_id)
|
||||
if not (lease and lease.get("wait_id") == wait_id):
|
||||
return False
|
||||
fd = _lease_fds.pop(session_id, None)
|
||||
if fd is not None:
|
||||
try:
|
||||
fcntl.flock(fd, fcntl.LOCK_UN)
|
||||
except OSError:
|
||||
pass
|
||||
os.close(fd)
|
||||
lease_path(session_id).unlink(missing_ok=True)
|
||||
return True
|
||||
@@ -11,24 +11,22 @@
|
||||
"NEEDS_REVALIDATION",
|
||||
"DEFERRED_FUTURE"
|
||||
],
|
||||
"live_defects": [
|
||||
324
|
||||
],
|
||||
"live_defects": [],
|
||||
"records": [
|
||||
{
|
||||
"number": 324,
|
||||
"title": "Engine defect: checkpoint writer fails deterministically on aborted worker sessions (MessageAbortedError -> empty checkpoint shells)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "FIXED_VERIFIED",
|
||||
"labels": [
|
||||
"bug",
|
||||
"custodian",
|
||||
"life",
|
||||
"memory"
|
||||
],
|
||||
"live_defect": true,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 defect no longer reproduces: zero MessageAbortedError in current logs (incl. active worker), six most recent session checkpoints full documents (111-396 lines); verified by present-absence 2026-09-19"
|
||||
},
|
||||
{
|
||||
"number": 316,
|
||||
@@ -433,20 +431,20 @@
|
||||
"number": 163,
|
||||
"title": "Temporal graph backend spike: Neo4j vs FalkorDB (LadybugDB experimental; Graphiti as reference)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "NEEDS_REVALIDATION",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"labels": [
|
||||
"P1 \u2014 high",
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "run the backend spike; compare on same corpus/query set; record evidence-backed recommendation"
|
||||
},
|
||||
{
|
||||
"number": 162,
|
||||
"title": "Qdrant associative-memory projection spike (dense/sparse/multivector + ColBERT late-interaction reranking)",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "NEEDS_REVALIDATION",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "SUPERSEDED",
|
||||
"labels": [
|
||||
"P1 \u2014 high",
|
||||
"custodian",
|
||||
@@ -454,8 +452,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 scope carried verbatim by #316 (ADAPT-1 B3-v2); single-tracker rule"
|
||||
},
|
||||
{
|
||||
"number": 159,
|
||||
@@ -478,8 +476,8 @@
|
||||
{
|
||||
"number": 158,
|
||||
"title": "Generated wiring-status / architecture-truth report",
|
||||
"tracker_state": "open",
|
||||
"reconciliation_state": "VALID_OPEN",
|
||||
"tracker_state": "closed",
|
||||
"reconciliation_state": "SUPERSEDED",
|
||||
"labels": [
|
||||
"P2 \u2014 medium",
|
||||
"growth",
|
||||
@@ -489,8 +487,8 @@
|
||||
"ready"
|
||||
],
|
||||
"live_defect": false,
|
||||
"last_revalidated": "2026-09-11",
|
||||
"next_action": ""
|
||||
"last_revalidated": "2026-09-19",
|
||||
"next_action": "none \u2014 scope carried verbatim by #315 (ADAPT-1 B2-v2); single-tracker rule"
|
||||
},
|
||||
{
|
||||
"number": 156,
|
||||
@@ -1151,6 +1149,6 @@
|
||||
"next_action": ""
|
||||
}
|
||||
],
|
||||
"last_reconciled": "2026-09-12",
|
||||
"reconciled_by": "PL1 held-out era reconciliation (fresh session 2026-09-12)"
|
||||
}
|
||||
"last_reconciled": "2026-09-19",
|
||||
"reconciled_by": "fresh-session tranche 2026-09-19: template retrofit (spine+plain-English on all open issues), closures #158/#162 (SUPERSEDED) and #324 (FIXED_VERIFIED), research labels applied to #163/#164/#165/#166/#182/#316, stale bug labels removed from #163-#166"
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "rune"
|
||||
version = "0.83.1"
|
||||
version = "0.87.0"
|
||||
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
|
||||
+4
-4
@@ -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/req_new.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
|
||||
@@ -56,7 +56,7 @@ mdit-py-plugins==0.6.1
|
||||
# via textual
|
||||
mdurl==0.1.2
|
||||
# via markdown-it-py
|
||||
multidict==6.8.0
|
||||
multidict==6.9.0
|
||||
# via
|
||||
# aiohttp
|
||||
# yarl
|
||||
@@ -64,7 +64,7 @@ nodeenv==1.10.0
|
||||
# via pyright
|
||||
packaging==26.3
|
||||
# via pytest
|
||||
platformdirs==4.11.9
|
||||
platformdirs==4.11.11
|
||||
# via textual
|
||||
pluggy==1.6.0
|
||||
# via
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
"""await_intent.py — ASYNC CORRECTIVE PATCH: intent normalization at the
|
||||
execution boundary.
|
||||
|
||||
PRINCIPLE: MODEL OUTPUT = INTENT. RUNE CONTROLLER = EXECUTION SEMANTICS.
|
||||
|
||||
The model may express continuation intent in any quality — `await_operation
|
||||
(deadline_secs=1200)`, `await-operation [op_type=master_ci, sha=X]`,
|
||||
"sleep 300 then check CI", "poll until done". When a matching
|
||||
PendingOperation exists (or the deterministic resolver can derive the
|
||||
operation truth from git/Gitea state), the controller executes the
|
||||
CANONICAL continuation and discards model-supplied scheduling policy.
|
||||
|
||||
Authority provenance for timing:
|
||||
USER_EXPLICIT / EXTERNAL_CONTRACT / TEST_HARNESS /
|
||||
GOVERNED_OPERATION_POLICY -> authoritative, retained
|
||||
MODEL_INVENTED -> non-authoritative, ignored
|
||||
|
||||
This module owns NO storage: it reads the existing W/R wait store
|
||||
(rune.event_wait) and git/Gitea state. The canonical continuation remains
|
||||
scripts/ci_event_wait.py run() (the T21.5 single-logical-await).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
if str(_SCRIPT_DIR) not in sys.path:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
_REPO_SRC = str(_SCRIPT_DIR.parent / "src")
|
||||
if _REPO_SRC not in sys.path:
|
||||
sys.path.insert(0, _REPO_SRC)
|
||||
|
||||
WR_BRIDGE = Path(os.environ.get("RUNE_WR_ROOT", "/mnt/workdrive/wr-bridge"))
|
||||
REPO = Path("/mnt/workdrive/issue161-harness")
|
||||
GIT_TIMEOUT = 10
|
||||
|
||||
# Default policy deadline per operation type — GOVERNED_OPERATION_POLICY.
|
||||
# The model's numbers never become policy; these are the controller's.
|
||||
POLICY_DEADLINES = {
|
||||
"branch_ci": 1800,
|
||||
"pull_request_checks": 1800,
|
||||
"master_ci": 1800,
|
||||
"deploy": 900,
|
||||
"release": 900,
|
||||
}
|
||||
|
||||
VALID_OP_TYPES = ("branch_ci", "pull_request_checks", "master_ci",
|
||||
"deploy", "release")
|
||||
|
||||
|
||||
# ── timing provenance ───────────────────────────────────────────────────────
|
||||
|
||||
def classify_timing_provenance(model_deadline: Any = None,
|
||||
user_deadline: Any = None,
|
||||
contract_deadline: Any = None) -> tuple[int, str]:
|
||||
"""Resolve the effective deadline + its provenance.
|
||||
|
||||
Authority order: USER_EXPLICIT > EXTERNAL_CONTRACT > TEST_HARNESS >
|
||||
GOVERNED_OPERATION_POLICY. MODEL_INVENTED values are recorded but
|
||||
NEVER used as policy.
|
||||
"""
|
||||
if model_deadline is not None:
|
||||
try:
|
||||
model_deadline = int(model_deadline)
|
||||
except (TypeError, ValueError):
|
||||
model_deadline = None
|
||||
if user_deadline is not None:
|
||||
return int(user_deadline), "USER_EXPLICIT"
|
||||
if contract_deadline is not None:
|
||||
return int(contract_deadline), "EXTERNAL_CONTRACT"
|
||||
return (model_deadline or 0), "MODEL_INVENTED" # caller discards this
|
||||
|
||||
|
||||
# ── PendingOperation lookup over the existing W/R wait store ────────────────
|
||||
|
||||
def _load_wait_records() -> list[dict]:
|
||||
"""All durable wait records from the bridge store (read-only)."""
|
||||
out = []
|
||||
waits = WR_BRIDGE / "waits"
|
||||
if not waits.is_dir():
|
||||
return out
|
||||
for f in sorted(waits.glob("*.json")):
|
||||
try:
|
||||
rec = json.loads(f.read_text())
|
||||
rec["_file"] = f.name
|
||||
out.append(rec)
|
||||
except (OSError, ValueError):
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
def _rec_operation_id(rec: dict) -> str:
|
||||
return str((rec.get("ext") or {}).get("operation_id")
|
||||
or rec.get("operation_id") or "")
|
||||
|
||||
|
||||
def _rec_sha(rec: dict) -> str:
|
||||
return str(rec.get("sha") or "")
|
||||
|
||||
|
||||
def pending_operations(sha: str = "", op_type: str = "") -> list[dict]:
|
||||
"""Pending (non-terminal) operations, optionally filtered."""
|
||||
terminal = {"COMPLETED", "FAILED", "CLOSED", "CANCELLED", "EXPIRED"}
|
||||
out = []
|
||||
for rec in _load_wait_records():
|
||||
status = str(rec.get("status", "")).upper()
|
||||
if status in terminal:
|
||||
continue
|
||||
if sha and _rec_sha(rec) != sha:
|
||||
continue
|
||||
out.append(rec)
|
||||
return out
|
||||
|
||||
|
||||
def find_op_type_for_sha(sha: str) -> str | None:
|
||||
"""The op_type a prior canonical wait already recorded for this SHA
|
||||
(objective text embeds the type: 'event-driven {op_type} qualification')."""
|
||||
for rec in _load_wait_records():
|
||||
if _rec_sha(rec) != sha:
|
||||
continue
|
||||
obj = str(rec.get("continuation", {}).get("objective", ""))
|
||||
m = re.search(r"event-driven (\w+) qualification", obj)
|
||||
if m and m.group(1) in VALID_OP_TYPES:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
# ── deterministic op_type derivation from repo/Gitea truth ─────────────────
|
||||
|
||||
def _git(*args: str) -> str:
|
||||
try:
|
||||
r = subprocess.run(["git", "-C", str(REPO), *args],
|
||||
capture_output=True, text=True, timeout=GIT_TIMEOUT)
|
||||
return r.stdout.strip()
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return ""
|
||||
|
||||
|
||||
def sha_branch(sha: str) -> str:
|
||||
"""Branch(es) containing this commit in the local repo."""
|
||||
return _git("branch", "--all", "--contains", sha)
|
||||
|
||||
|
||||
def sha_on_master(sha: str) -> bool:
|
||||
out = _git("branch", "--contains", sha)
|
||||
return any(l.strip().endswith("master") or l.strip() == "master"
|
||||
for l in out.splitlines())
|
||||
|
||||
|
||||
def open_pr_for_sha(sha: str) -> int | None:
|
||||
"""Is there an OPEN PR whose head SHA is this one? (Gitea API, read-only.)"""
|
||||
try:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import gitea_auth # noqa: E402
|
||||
st, prs = gitea_auth.api("/pulls?state=open&limit=50")
|
||||
if st != 200:
|
||||
return None
|
||||
for p in prs:
|
||||
hs = str(p.get("head") or {}).get("sha") if isinstance(
|
||||
p.get("head"), dict) else None
|
||||
if hs and str(hs).startswith(sha[:12]):
|
||||
return int(p.get("number"))
|
||||
except Exception: # noqa: BLE001 — resolver must never crash the tool
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def derive_op_type(sha: str) -> tuple[str, str]:
|
||||
"""Deterministically derive the canonical op_type for a SHA.
|
||||
|
||||
Returns (op_type, evidence). Order (A-patch §2 amendment):
|
||||
1. POST-PATCH normalized record (ext.intent_normalized=true) — true
|
||||
stored operation truth, wins outright;
|
||||
2. open PR for the sha -> pull_request_checks;
|
||||
3. sha ancestor of master -> master_ci;
|
||||
4. sha on a feature branch -> branch_ci;
|
||||
5. legacy (pre-patch) records only CONFIRM, never override — they
|
||||
were created from model-supplied defaults and cannot be trusted.
|
||||
Never trusts model input.
|
||||
"""
|
||||
for rec in _load_wait_records():
|
||||
if _rec_sha(rec) != sha:
|
||||
continue
|
||||
if (rec.get("ext") or {}).get("intent_normalized"):
|
||||
obj = str(rec.get("continuation", {}).get("objective", ""))
|
||||
m = re.search(r"event-driven (\w+) qualification", obj)
|
||||
if m and m.group(1) in VALID_OP_TYPES:
|
||||
return m.group(1), "normalized_record:" + m.group(1)
|
||||
pr = open_pr_for_sha(sha)
|
||||
if pr:
|
||||
return "pull_request_checks", f"open_pr:#{pr}"
|
||||
if sha_on_master(sha):
|
||||
return "master_ci", "commit_ancestor_of_master"
|
||||
return "branch_ci", "commit_on_feature_branch"
|
||||
|
||||
|
||||
# ── intent normalization ────────────────────────────────────────────────────
|
||||
|
||||
def normalize_action(action: dict, *,
|
||||
model_deadline: Any = None,
|
||||
user_deadline: Any = None,
|
||||
contract_deadline: Any = None,
|
||||
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
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
"""await_intent_cli.py — CLI surface for the intent normalization layer.
|
||||
|
||||
Used by the await-operation harness tool so the MODEL's scheduling hints
|
||||
never reach the wait engine. Prints one JSON line.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import await_intent # noqa: E402
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
n = sub.add_parser("normalize", help="normalize a continuation intent")
|
||||
n.add_argument("--sha", default="")
|
||||
n.add_argument("--operation-id", default="")
|
||||
n.add_argument("--task-id", default="",
|
||||
help="§9 task binding: scope pending resolution to "
|
||||
"operations registered for this task")
|
||||
n.add_argument("--model-op-type", default=None)
|
||||
n.add_argument("--model-deadline", default=None)
|
||||
n.add_argument("--user-deadline", default=None,
|
||||
help="USER_EXPLICIT deadline (authoritative)")
|
||||
n.add_argument("--contract-deadline", default=None,
|
||||
help="EXTERNAL_CONTRACT deadline (authoritative)")
|
||||
rb = sub.add_parser("route-bash",
|
||||
help="route a Bash command expressing wait intent")
|
||||
rb.add_argument("--command", required=True)
|
||||
rb.add_argument("--sha-hint", default="")
|
||||
args = ap.parse_args()
|
||||
|
||||
if args.cmd == "route-bash":
|
||||
record = await_intent.normalize_bash(args.command,
|
||||
sha_hint=args.sha_hint)
|
||||
if record is None:
|
||||
print(json.dumps({"normalized": False,
|
||||
"reason": "no external continuation intent"}))
|
||||
return 0
|
||||
if record.get("normalized") is True:
|
||||
# the compiled wait needs the SHA: derive from operation_id
|
||||
oid = str(record.get("operation_id", ""))
|
||||
if oid.startswith("ci-"):
|
||||
record["sha_for_wait"] = oid[3:]
|
||||
await_intent.emit_normalized_record(record)
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
return 0
|
||||
|
||||
action = {"kind": "await_operation", "sha": args.sha,
|
||||
"operation_id": args.operation_id,
|
||||
"op_type": args.model_op_type}
|
||||
model_deadline = (int(args.model_deadline)
|
||||
if args.model_deadline else None)
|
||||
user_deadline = (int(args.user_deadline)
|
||||
if args.user_deadline else None)
|
||||
contract_deadline = (int(args.contract_deadline)
|
||||
if args.contract_deadline else None)
|
||||
record = await_intent.normalize_action(
|
||||
action, model_deadline=model_deadline,
|
||||
user_deadline=user_deadline, contract_deadline=contract_deadline,
|
||||
task_id=args.task_id)
|
||||
await_intent.emit_normalized_record(record)
|
||||
print(json.dumps(record, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
+235
-80
@@ -47,28 +47,34 @@ import time
|
||||
from pathlib import Path
|
||||
|
||||
_SCRIPT_DIR = Path(__file__).resolve().parent
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
sys.path.insert(0, str(_SCRIPT_DIR.parent / "src"))
|
||||
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
from rune import event_wait as ew # noqa: E402
|
||||
import completion_file as cf # noqa: E402
|
||||
|
||||
REPO_ROOT = _SCRIPT_DIR.parent
|
||||
QUALIFY_SCRIPT = _SCRIPT_DIR / "deploy_qualify.py"
|
||||
|
||||
|
||||
def authority_verdict() -> str:
|
||||
def authority_verdict(sha: str = "") -> str:
|
||||
"""ONE-SHOT canonical Gitea verdict via deploy_qualify.py — the only
|
||||
completion truth. Never parses the event payload."""
|
||||
completion truth. Never parses the event payload.
|
||||
|
||||
H-PATCH H4: the target sha is passed EXPLICITLY (as the subprocess's
|
||||
--sha argument) — ambient DEPLOY_SHA can no longer change which
|
||||
operation is verified. The env var remains only as a CLI backward
|
||||
compat for deploy_qualify.py invoked standalone."""
|
||||
cmd = [sys.executable, str(QUALIFY_SCRIPT)]
|
||||
if sha:
|
||||
cmd += ["--sha", sha]
|
||||
proc = subprocess.run(
|
||||
[sys.executable, str(QUALIFY_SCRIPT)],
|
||||
cmd,
|
||||
capture_output=True, text=True, timeout=120,
|
||||
env=dict(os.environ, PYTHONPATH=""),
|
||||
env={k: v for k, v in os.environ.items() if k != "DEPLOY_SHA"}
|
||||
if sha else 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()
|
||||
|
||||
@@ -134,6 +140,17 @@ OPERATION_SPECS = {
|
||||
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:
|
||||
@@ -158,12 +175,35 @@ def op_authority_verdict(op_type: str, sha: str) -> str:
|
||||
spec = op_spec(op_type)
|
||||
authority = spec["authority"]
|
||||
if authority == "deploy_qualify":
|
||||
return authority_verdict()
|
||||
v = normalize_verdict(authority_verdict(sha))
|
||||
if v != "pending":
|
||||
return v
|
||||
# E-PATCH resilience: Gitea's combined commit-status endpoint can
|
||||
# 404 for the newest commit (observed live on auto-version bump
|
||||
# commits — the status object materializes late/never). The runs
|
||||
# ARE the same evidence the two legs read; fall back to the
|
||||
# operation-scoped runs verdict so authority stays resolvable.
|
||||
if _status_endpoint_unreadable(sha):
|
||||
return _ci_runs_verdict(spec, sha)
|
||||
return v
|
||||
if authority == "ci_runs":
|
||||
return _ci_runs_verdict(spec, sha)
|
||||
raise ValueError(f"unknown authority {authority!r} for {op_type!r}")
|
||||
|
||||
|
||||
def _status_endpoint_unreadable(sha: str) -> bool:
|
||||
"""True when the combined commit-status endpoint 404s for the SHA
|
||||
(index lag / never-materialized status object) — the documented
|
||||
fallback trigger, verified against the API once per call."""
|
||||
try:
|
||||
sys.path.insert(0, str(_SCRIPT_DIR))
|
||||
import gitea_auth # noqa: E402
|
||||
st, _body = gitea_auth.api(f"/commits/{sha}/status")
|
||||
return st == 404
|
||||
except Exception: # noqa: BLE001
|
||||
return False
|
||||
|
||||
|
||||
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)."""
|
||||
@@ -174,7 +214,7 @@ def _ci_runs_verdict(spec: dict, sha: str) -> str:
|
||||
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
|
||||
sha_env = sha # H-PATCH: explicit parameter is the only authority
|
||||
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)
|
||||
@@ -210,7 +250,7 @@ def op_ci_alive(op_type: str, sha: str) -> bool:
|
||||
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
|
||||
sha_env = sha # H-PATCH: explicit parameter is the only authority
|
||||
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
||||
if not (server and repo and sha_env and token):
|
||||
return False
|
||||
@@ -232,6 +272,7 @@ def op_ci_alive(op_type: str, sha: str) -> bool:
|
||||
|
||||
def authority_after_wake(op_type: str = DEFAULT_OP_TYPE, sha: str = "",
|
||||
max_tries: int = 3, gap_secs: int = 4) -> str:
|
||||
"""(sha is EXPLICIT — ambient env never selects the target.)"""
|
||||
"""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
|
||||
@@ -253,9 +294,28 @@ def ci_alive() -> bool:
|
||||
return op_ci_alive(DEFAULT_OP_TYPE, os.environ.get("DEPLOY_SHA", ""))
|
||||
|
||||
|
||||
def _set_cognition_state(op: str, state: str) -> None:
|
||||
"""E-PATCH §12: stamp cognitive quiescence on the durable record so a
|
||||
fresh process knows cognition is parked (WAITING_EXTERNAL) vs resumable
|
||||
(RESUMABLE). Best-effort: observability never breaks the wait."""
|
||||
try:
|
||||
rec = ew.operation_wait(op)
|
||||
if rec is None:
|
||||
return
|
||||
wait_file = ew.wr_root() / "waits" / f"{rec.get('wait_id')}.json"
|
||||
if not wait_file.is_file():
|
||||
return
|
||||
data = json.loads(wait_file.read_text())
|
||||
data.setdefault("ext", {})["continuation_state"] = state
|
||||
wait_file.write_text(json.dumps(data, indent=1, sort_keys=True))
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def run(*, sha: str, deadline_secs: int,
|
||||
watchdog_secs: int = 300,
|
||||
op_type: str = DEFAULT_OP_TYPE,
|
||||
task_id: str = "",
|
||||
daemon_url: str = ew.DEFAULT_DAEMON_URL) -> int:
|
||||
"""T21.5 §3: ONE LOGICAL AWAIT.
|
||||
|
||||
@@ -268,10 +328,28 @@ def run(*, sha: str, deadline_secs: int,
|
||||
"""
|
||||
spec = op_spec(op_type) # unknown op_type refused upfront
|
||||
op = f"ci-{sha}"
|
||||
# A-PATCH: the authority subprocess reads DEPLOY_SHA from the env;
|
||||
# python-level run() callers (tool normalization flow) must export it.
|
||||
if sha and not os.environ.get("DEPLOY_SHA"):
|
||||
os.environ["DEPLOY_SHA"] = sha
|
||||
# H-PATCH: resolve short SHAs to full 40-char — _fetch_runs and
|
||||
# deploy_qualify match on the FULL sha; a 12-char prefix would
|
||||
# silently fail equality and park the waiter until deadline
|
||||
# (the 962e351 root cause).
|
||||
if sha and len(sha) < 40:
|
||||
try:
|
||||
full = subprocess.run(
|
||||
["git", "-C", str(_SCRIPT_DIR.parent), "rev-parse", sha],
|
||||
capture_output=True, text=True, timeout=10)
|
||||
if full.returncode == 0:
|
||||
sha = full.stdout.strip()
|
||||
os.environ["DEPLOY_SHA"] = sha
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
pass
|
||||
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)
|
||||
# REAL_ASYNC_BRIDGE: FIFO is replaced by completion-file inotify.
|
||||
# The completion file write is the wake signal; inotify is the
|
||||
# kernel-level zero-latency, zero-CPU wake primitive.
|
||||
# Event replay (T16 §16C live finding): on a serialized runner the
|
||||
# completion event usually arrives BEFORE the waiter registers.
|
||||
# A matching durable event means the wake already happened — skip
|
||||
@@ -284,15 +362,18 @@ def run(*, sha: str, deadline_secs: int,
|
||||
watched_for="Gitea CI completion event (replayed from the "
|
||||
"durable event log)",
|
||||
provider="gitea", event_type="ci_completed", sha=sha,
|
||||
watchdog_secs=min(watchdog_secs, deadline_secs))
|
||||
watchdog_secs=min(watchdog_secs, deadline_secs),
|
||||
continuation_state="RESUMABLE", # event already arrived
|
||||
op_type=op_type,
|
||||
authority=spec["authority"])
|
||||
registered_at = rec["created_at"]
|
||||
event_at = time.strftime(
|
||||
"%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(op_type, sha)
|
||||
verdict = normalize_verdict(authority_after_wake(op_type, sha))
|
||||
if verdict == "pending" and not op_ci_alive(op_type, sha):
|
||||
verdict = op_authority_verdict(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())
|
||||
@@ -318,8 +399,16 @@ def run(*, sha: str, deadline_secs: int,
|
||||
watched_for="Gitea CI completion event (wake; verdict re-read "
|
||||
f"from canonical {spec['authority']} authority)",
|
||||
provider="gitea", event_type="ci_completed", sha=sha,
|
||||
watchdog_secs=min(watchdog_secs, deadline_secs))
|
||||
watchdog_secs=min(watchdog_secs, deadline_secs),
|
||||
continuation_state="WAITING_EXTERNAL",
|
||||
op_type=op_type,
|
||||
authority=spec["authority"],
|
||||
task_id=task_id, # §9: bind the operation to the calling task
|
||||
resume_mode="self") # §3: waiter owns its continuation via
|
||||
# completion-file/inotify; the daemon has NO
|
||||
# session-delivery role for this wait
|
||||
registered_at = rec["created_at"]
|
||||
_set_cognition_state(op, "WAITING_EXTERNAL") # cognition parked
|
||||
deadline = time.monotonic() + deadline_secs
|
||||
verdict = "pending"
|
||||
mode = "event"
|
||||
@@ -327,74 +416,110 @@ def run(*, sha: str, deadline_secs: int,
|
||||
resumed_at = ""
|
||||
rearm_reason = ""
|
||||
rearm_count = 0
|
||||
try:
|
||||
while time.monotonic() < deadline:
|
||||
while time.monotonic() < deadline:
|
||||
remaining = max(1, int(deadline - time.monotonic()))
|
||||
result = ew.await_resume_event(
|
||||
op, watchdog_secs=min(remaining, watchdog_secs), pgid=None,
|
||||
fifo_fd=fifo_fd)
|
||||
if result["kind"] == "event":
|
||||
|
||||
# REAL_ASYNC_BRIDGE: block on inotify for the completion file.
|
||||
# Zero CPU, zero arbitrary timer. The inotify timeout is a
|
||||
# FAILURE DETECTOR — if no event arrives within the watchdog
|
||||
# window, we check the authority directly (lost-event recovery).
|
||||
# The deadline is containment only.
|
||||
watch_timeout = min(remaining, watchdog_secs)
|
||||
result = cf.wait_for_completion(
|
||||
op, timeout_secs=watch_timeout)
|
||||
|
||||
# REAL_ASYNC_BRIDGE: check durable event log AND wait record
|
||||
# status on EVERY cycle — the daemon may have matched and
|
||||
# transitioned the record via /process-event.
|
||||
wr = ew.operation_wait(op)
|
||||
if wr and wr.get("status") in ("COMPLETED", "FAILED",
|
||||
"READY_TO_RESUME"):
|
||||
event_at = event_at or time.strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime())
|
||||
# WAKE received — canonical authority decides, never the
|
||||
# event payload
|
||||
verdict = authority_after_wake(op_type, sha)
|
||||
verdict = normalize_verdict(
|
||||
authority_after_wake(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "event"
|
||||
break
|
||||
prior = ew.find_matching_event("gitea", "ci_completed", op)
|
||||
if prior is not None and prior.get("exit_code") is not None:
|
||||
event_at = event_at or time.strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime())
|
||||
verdict = normalize_verdict(
|
||||
authority_after_wake(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "event"
|
||||
break
|
||||
if result is not None:
|
||||
# completion file exists: event arrived
|
||||
event_at = event_at or time.strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime())
|
||||
verdict = normalize_verdict(
|
||||
authority_after_wake(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "event"
|
||||
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")
|
||||
rearm_reason = ("completion file seen; authority "
|
||||
"pending — re-subscribing")
|
||||
continue
|
||||
if result["kind"] == "eof":
|
||||
# writer gone without a token: the DURABLE record decides
|
||||
rec = ew.operation_wait(op) or {}
|
||||
if rec.get("status") in ("COMPLETED", "FAILED",
|
||||
"READY_TO_RESUME", "RESUMING") or \
|
||||
(rec.get("status") == "RESUME_UNVERIFIED"
|
||||
and rec.get("process_event_evidence")):
|
||||
event_at = event_at or time.strftime(
|
||||
"%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
resumed_at = time.strftime("%Y-%m-%dT%H:%M:%SZ",
|
||||
time.gmtime())
|
||||
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: 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
|
||||
# no live runs for THIS op's authority: terminal-in-Gitea but
|
||||
# possibly aggregator-lagging — bounded re-read, then resolve
|
||||
verdict = op_authority_verdict(op_type, sha)
|
||||
if verdict == "pending":
|
||||
# T21.4 cause C fixed: a pending verdict with no live runs
|
||||
# may still be a lagging aggregator; one more bounded
|
||||
# re-read before resolving truthfully. Still no control
|
||||
# returned to the model — deadline governs.
|
||||
verdict = op_authority_verdict(op_type, sha)
|
||||
if verdict == "pending":
|
||||
mode = "watchdog_lost_event_no_verdict"
|
||||
else:
|
||||
|
||||
# inotify timeout: no event within watchdog window
|
||||
# FAILURE DETECTOR: check authority directly
|
||||
verdict = normalize_verdict(
|
||||
op_authority_verdict(op_type, sha))
|
||||
if verdict != "pending":
|
||||
mode = "watchdog_authority_recovered"
|
||||
break
|
||||
finally:
|
||||
os.close(fifo_fd)
|
||||
break
|
||||
rearm_count += 1
|
||||
rearm_reason = ("watchdog expiry; op pending (not started "
|
||||
"or lagging) — re-subscribing inotify")
|
||||
continue
|
||||
|
||||
# REAL_ASYNC_BRIDGE: no FIFO to close. The completion file is durable
|
||||
# and survives restarts. Lease release is not needed (no lease system
|
||||
# for file-based delivery).
|
||||
|
||||
|
||||
if verdict == "pending":
|
||||
if mode == "event":
|
||||
mode = "deadline_pending"
|
||||
verdict = op_authority_verdict(op_type, sha) # final one-shot truth
|
||||
verdict = normalize_verdict(op_authority_verdict(op_type, sha)) # final one-shot truth
|
||||
_set_cognition_state(op, "RESUMABLE" if verdict != "pending"
|
||||
else "RECOVERY_REQUIRED")
|
||||
verdict_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
|
||||
# §2 STATE OWNERSHIP: the WAITER owns the OPERATION domain. On a
|
||||
# terminal authority verdict, stamp the durable record terminal with
|
||||
# the evidence — the daemon's session-delivery machinery must never
|
||||
# (and can no longer) overwrite this truth. Also updates the parked
|
||||
# continuation_state so fresh-process recovery sees reality.
|
||||
try:
|
||||
rec2 = ew.operation_wait(op) or rec
|
||||
if verdict != "pending":
|
||||
rec2["status"] = "COMPLETED" if verdict == "success" else "FAILED"
|
||||
rec2["verdict"] = verdict
|
||||
rec2["resume_evidence"] = {
|
||||
"kind": "authority_verified_terminal",
|
||||
"verdict": verdict,
|
||||
"mode": mode,
|
||||
"authority": spec["authority"],
|
||||
"sha": sha,
|
||||
"op_type": op_type,
|
||||
"verdict_at": verdict_at,
|
||||
}
|
||||
rec2["ext"] = {**rec2.get("ext", {}),
|
||||
"continuation_state": ("RESUMABLE" if verdict != "pending"
|
||||
else "RECOVERY_REQUIRED")}
|
||||
ew._load_waitstore().save(rec2)
|
||||
except Exception as _e: # noqa: BLE001 — record stamping must never corrupt the envelope (the envelope + authority remain the truth)
|
||||
print(f"ci_event_wait: terminal record stamp failed: {_e}",
|
||||
file=sys.stderr)
|
||||
out = {
|
||||
"sha": sha,
|
||||
"op_type": op_type,
|
||||
@@ -412,21 +537,51 @@ def run(*, sha: str, deadline_secs: int,
|
||||
print(json.dumps(out))
|
||||
return 0 if verdict == "success" else 1
|
||||
|
||||
KNOWN_FLAGS = ("--sha", "--deadline", "--op-type", "--watchdog", "--task-id")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""§5 CLI CONTRACT: no silent argument loss. Every flag is either
|
||||
explicitly supported (and takes effect) or loudly rejected — an
|
||||
acceptance envelope must reflect the configuration actually used."""
|
||||
sha = os.environ.get("DEPLOY_SHA", "")
|
||||
args = sys.argv[1:]
|
||||
if "--sha" in args:
|
||||
sha = args[args.index("--sha") + 1]
|
||||
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]
|
||||
watchdog = 300
|
||||
task_id = ""
|
||||
i = 0
|
||||
while i < len(args):
|
||||
a = args[i]
|
||||
if a == "--sha":
|
||||
sha = args[i + 1]; i += 2
|
||||
elif a == "--deadline":
|
||||
deadline = int(args[i + 1]); i += 2
|
||||
elif a == "--op-type":
|
||||
op_type = args[i + 1]; i += 2
|
||||
elif a == "--watchdog":
|
||||
watchdog = int(args[i + 1]); i += 2
|
||||
elif a == "--task-id":
|
||||
task_id = args[i + 1]; i += 2
|
||||
elif a.startswith("--"):
|
||||
print(json.dumps({"error": f"unknown flag {a!r}",
|
||||
"known_flags": list(KNOWN_FLAGS),
|
||||
"contract": "no silent argument loss"}))
|
||||
return 2
|
||||
else:
|
||||
print(json.dumps({"error": f"unexpected positional {a!r}",
|
||||
"known_flags": list(KNOWN_FLAGS)}))
|
||||
return 2
|
||||
if not sha:
|
||||
print(json.dumps({"error": "no sha (DEPLOY_SHA or --sha required)"}))
|
||||
return 1
|
||||
return run(sha=sha, deadline_secs=deadline, op_type=op_type)
|
||||
# A-PATCH fix: run()'s authority subprocess (deploy_qualify.py) reads
|
||||
# DEPLOY_SHA from the environment — a CLI `--sha` consumer previously
|
||||
# got a silent "pending" verdict forever because the var was never
|
||||
# exported. Export it so the canonical authority can resolve.
|
||||
os.environ["DEPLOY_SHA"] = sha
|
||||
return run(sha=sha, deadline_secs=deadline, op_type=op_type,
|
||||
watchdog_secs=watchdog, task_id=task_id)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
"""completion_file.py — REAL_ASYNC_BRIDGE: durable completion-file delivery
|
||||
with inotify wake. Replaces FIFO+lease for process-wait delivery.
|
||||
|
||||
DESIGN:
|
||||
Producer (bridge/daemon/notify):
|
||||
1. Validates operation identity
|
||||
2. Atomically writes a completion file:
|
||||
resumes/<operation_id>.done
|
||||
containing the event/verdict JSON (write to tmp, rename for atomicity)
|
||||
|
||||
Waiter (ci_event_wait / any continuation consumer):
|
||||
1. Checks if the completion file already exists (event-before-waiter)
|
||||
2. If not: registers inotify watch on the resumes/ directory
|
||||
3. Re-checks after watch registration (closes the registration race)
|
||||
4. Blocks in kernel on inotify — zero CPU, zero timer
|
||||
5. On file-created event: reads the file, verifies authority, returns
|
||||
|
||||
The event is a WAKE SIGNAL. Authority still decides terminal truth.
|
||||
The completion file is durable — survives daemon and waiter restarts.
|
||||
|
||||
WHY NOT FIFO:
|
||||
FIFO is one-shot (token consumed), requires lease management, and
|
||||
fails under session-busy guards. A completion file is atomic, durable,
|
||||
and works across restarts. inotify wakes the process with zero CPU
|
||||
cost and zero latency between file creation and wake.
|
||||
|
||||
WHY NOT POLLING:
|
||||
Polling introduces an arbitrary interval. inotify is a kernel callback
|
||||
that fires exactly when the filesystem changes — the latency tracks
|
||||
the real completion event, not a configured interval.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import struct
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
def _wr_bridge() -> Path:
|
||||
# WR_ROOT = daemon/infra convention; RUNE_WR_ROOT = repo/scripts
|
||||
# convention. Both honored (WR_ROOT wins) so the daemon, the waiter,
|
||||
# and any isolated test root always resolve the SAME directory.
|
||||
root = (os.environ.get("WR_ROOT")
|
||||
or os.environ.get("RUNE_WR_ROOT")
|
||||
or "/mnt/workdrive/wr-bridge")
|
||||
return Path(root)
|
||||
|
||||
def _resumes_dir() -> Path:
|
||||
return _wr_bridge() / "resumes"
|
||||
|
||||
# inotify constants (Linux)
|
||||
IN_CREATE = 0x100
|
||||
IN_CLOSE_WRITE = 0x8
|
||||
IN_MOVED_TO = 0x80
|
||||
|
||||
|
||||
def completion_path(operation_id: str) -> Path:
|
||||
return _resumes_dir() / f"{operation_id}.done"
|
||||
|
||||
|
||||
def write_completion(operation_id: str, event: dict) -> Path:
|
||||
"""Atomically write a durable completion file. The rename(2) ensures
|
||||
the waiter never sees a partial file."""
|
||||
_resumes_dir().mkdir(parents=True, exist_ok=True)
|
||||
target = completion_path(operation_id)
|
||||
tmp = target.parent / f".{target.name}.tmp.{os.getpid()}"
|
||||
tmp.write_text(json.dumps(event, sort_keys=True) + "\n")
|
||||
os.rename(str(tmp), str(target)) # atomic on same filesystem
|
||||
os.chmod(str(target), 0o666) # cross-user bridge
|
||||
return target
|
||||
|
||||
|
||||
def read_completion(operation_id: str) -> dict | None:
|
||||
"""Read a durable completion file. Returns None if absent."""
|
||||
p = completion_path(operation_id)
|
||||
if not p.is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def completion_exists(operation_id: str) -> bool:
|
||||
return completion_path(operation_id).is_file()
|
||||
|
||||
|
||||
def _inotify_init_and_add_watch(directory: str) -> int:
|
||||
"""Create an inotify instance and add a watch on the directory.
|
||||
Returns the inotify fd."""
|
||||
libc = __import__("ctypes").CDLL(None, use_errno=True)
|
||||
fd = libc.inotify_init()
|
||||
if fd < 0:
|
||||
raise OSError("inotify_init failed")
|
||||
# IN_CREATE | IN_MOVED_TO: catch both direct creation and atomic rename
|
||||
mask = IN_CREATE | IN_MOVED_TO
|
||||
wd = libc.inotify_add_watch(fd, directory.encode(), mask)
|
||||
if wd < 0:
|
||||
os.close(fd)
|
||||
raise OSError(f"inotify_add_watch failed for {directory}")
|
||||
return fd
|
||||
|
||||
|
||||
def _inotify_read(fd: int, timeout_secs: float | None = None) -> list[str]:
|
||||
"""Read pending inotify events. Returns list of filenames that were
|
||||
created/moved-to. Blocks until at least one event or timeout."""
|
||||
poll = select.poll()
|
||||
poll.register(fd, select.POLLIN)
|
||||
events = []
|
||||
buf = os.read(fd, 4096)
|
||||
pos = 0
|
||||
while pos < len(buf):
|
||||
# struct: wd(i32), mask(u32), cookie(u32), name_len(u32)
|
||||
wd, mask, cookie, name_len = struct.unpack_from("iIII", buf, pos)
|
||||
pos += 16
|
||||
name = buf[pos:pos + name_len].rstrip(b"\x00").decode("utf-8", "replace")
|
||||
pos += name_len
|
||||
if mask & (IN_CREATE | IN_MOVED_TO):
|
||||
events.append(name)
|
||||
return events
|
||||
|
||||
|
||||
def wait_for_completion(operation_id: str, *,
|
||||
timeout_secs: float | None = None,
|
||||
poll_callback=None) -> dict | None:
|
||||
"""Block until a completion file exists for this operation.
|
||||
|
||||
Uses inotify for zero-latency, zero-CPU wake. Falls back to
|
||||
polling_callback (if provided) when inotify is unavailable.
|
||||
|
||||
The completion file is a WAKE SIGNAL. The caller must still verify
|
||||
canonical authority after reading it.
|
||||
|
||||
Returns the event dict from the completion file, or None on timeout.
|
||||
"""
|
||||
target = completion_path(operation_id)
|
||||
|
||||
# 1. check-before-watch (event may have arrived before we started)
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
_resumes_dir().mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
fd = _inotify_init_and_add_watch(str(_resumes_dir()))
|
||||
except OSError:
|
||||
# inotify unavailable: fall back to polling callback
|
||||
if poll_callback:
|
||||
return poll_callback(operation_id, timeout_secs)
|
||||
return None
|
||||
|
||||
try:
|
||||
# 2. re-check after watch registration (closes the race between
|
||||
# the initial check and the watch being active)
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
|
||||
# 3. block in kernel on inotify — zero CPU, exact latency
|
||||
import select as _select
|
||||
poll = _select.poll()
|
||||
poll.register(fd, _select.POLLIN)
|
||||
deadline = (time.monotonic() + timeout_secs) if timeout_secs else None
|
||||
|
||||
while True:
|
||||
remaining = None
|
||||
if deadline is not None:
|
||||
remaining = max(0.1, deadline - time.monotonic())
|
||||
if remaining <= 0.1:
|
||||
return None # timeout
|
||||
events = poll.poll(remaining * 1000 if remaining else None)
|
||||
if not events:
|
||||
continue # poll timeout, re-check
|
||||
names = _inotify_read(fd)
|
||||
if target.name in names:
|
||||
# 4. file appeared — read it
|
||||
existing = read_completion(operation_id)
|
||||
if existing is not None:
|
||||
return existing
|
||||
# partial/corrupt: keep watching
|
||||
finally:
|
||||
os.close(fd)
|
||||
return None
|
||||
|
||||
|
||||
def clear_completion(operation_id: str) -> None:
|
||||
"""Remove a consumed completion file (idempotent cleanup)."""
|
||||
p = completion_path(operation_id)
|
||||
try:
|
||||
p.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
@@ -106,12 +106,25 @@ def _fetch_status(server: str, repo: str, sha: str, token: str) -> list[dict] |
|
||||
return None if data is None else data.get("statuses", [])
|
||||
|
||||
|
||||
def _fetch_runs(server: str, repo: str, sha: str, token: str) -> list[dict] | None:
|
||||
data = _get_json(f"{server}/api/v1/repos/{repo}/actions/runs?limit=50", token)
|
||||
if data is None:
|
||||
return None
|
||||
runs = data.get("workflow_runs", data if isinstance(data, list) else [])
|
||||
return [r for r in runs if str(r.get("head_sha", "")) == sha]
|
||||
def _fetch_runs(server: str, repo: str, sha: str, token: str,
|
||||
max_pages: int = 4) -> list[dict] | None:
|
||||
"""H-PATCH: paginate the runs list. The repo's scheduled merge-robot
|
||||
runs flood the first page (~50 slots exhaust within hours), pushing
|
||||
older-but-relevant SHAs to later pages — the cause of the observed
|
||||
authority flapping (terminal commit read as pending). Paginate until
|
||||
the sha is found or max_pages exhausted."""
|
||||
out: list[dict] = []
|
||||
for page in range(1, max_pages + 1):
|
||||
data = _get_json(
|
||||
f"{server}/api/v1/repos/{repo}/actions/runs?limit=50&page={page}",
|
||||
token)
|
||||
if data is None:
|
||||
return None if page == 1 else (out or None)
|
||||
runs = data.get("workflow_runs", data if isinstance(data, list) else [])
|
||||
out.extend(r for r in runs if str(r.get("head_sha", "")) == sha)
|
||||
if len(runs) < 50:
|
||||
break # exhausted
|
||||
return out
|
||||
|
||||
|
||||
def _fetch_jobs(server: str, repo: str, run_id, token: str) -> list[dict] | None:
|
||||
@@ -132,10 +145,17 @@ def main() -> int:
|
||||
# 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).
|
||||
# H-PATCH H4: the target sha arrives EXPLICITLY (--sha) from callers
|
||||
# that hold structured operation state. DEPLOY_SHA env remains only
|
||||
# for standalone/workflow backward compat — the canonical path never
|
||||
# depends on ambient env for operation identity.
|
||||
ap = __import__("argparse").ArgumentParser()
|
||||
ap.add_argument("--sha", dest="sha_arg", default=None)
|
||||
args, _unknown = ap.parse_known_args()
|
||||
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", "")
|
||||
sha = args.sha_arg or 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):
|
||||
print("pending") # missing config → cannot verify → not qualified
|
||||
|
||||
@@ -86,9 +86,12 @@ def main() -> int:
|
||||
if live_state == "open" and rec["tracker_state"] == "closed" and rec["reconciliation_state"] in TERMINAL_STATES:
|
||||
drift.append(f"#{num}: live state open but artifact says closed/{rec['reconciliation_state']}")
|
||||
live_bug = "bug" in [l["name"] for l in li.get("labels", [])]
|
||||
if live_bug and not rec.get("live_defect"):
|
||||
# Same open-only exemption as the artifact-records block above: a
|
||||
# closed issue's labels are historical record; FIXED_VERIFIED issues
|
||||
# legitimately keep the bug label after closure.
|
||||
if live_bug and rec.get("tracker_state") == "open" and not rec.get("live_defect"):
|
||||
drift.append(f"#{num}: live bug label present but artifact records no live defect")
|
||||
if not live_bug and rec.get("live_defect"):
|
||||
if not live_bug and rec.get("live_defect") and rec.get("tracker_state") == "open":
|
||||
drift.append(f"#{num}: artifact records live defect but live bug label absent")
|
||||
|
||||
# Live defects in the artifact must all be OPEN in the tracker
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env python3
|
||||
"""install_wr_daemon.py — REAL_ASYNC_BRIDGE §6: daemon governance.
|
||||
|
||||
Canonical source lives IN THIS REPO at bridge/daemon/. The active install
|
||||
is ~/.local/share/mimo-wait-resume/. This script is the ONLY supported
|
||||
way to move code between them:
|
||||
|
||||
- copy canonical source → install dir (atomic, per file)
|
||||
- verify sha256 convergence (fail loud on divergence)
|
||||
- optionally restart the daemon via scripts/start_wr_daemon.py
|
||||
|
||||
Never hand-edit the installed copy: edit the repo source, install, restart.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CANONICAL = REPO / "bridge" / "daemon"
|
||||
INSTALL = Path.home() / ".local/share/mimo-wait-resume"
|
||||
FILES = ("daemon.py", "waitstore.py", "verify.py", "completion_file.py")
|
||||
|
||||
|
||||
def sha256(p: Path) -> str:
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--check", action="store_true", help="verify convergence only; no writes")
|
||||
ap.add_argument("--restart", action="store_true", help="restart the daemon after install")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not CANONICAL.is_dir():
|
||||
print(f"canonical source missing: {CANONICAL}")
|
||||
return 1
|
||||
INSTALL.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
diverged = []
|
||||
for name in FILES:
|
||||
src, dst = CANONICAL / name, INSTALL / name
|
||||
if args.check:
|
||||
if not dst.is_file() or sha256(src) != sha256(dst):
|
||||
diverged.append(name)
|
||||
continue
|
||||
if not dst.is_file() or sha256(src) != sha256(dst):
|
||||
tmp = dst.with_suffix(f".tmp.{name}")
|
||||
shutil.copy2(src, tmp)
|
||||
tmp.replace(dst) # atomic
|
||||
print(f"installed: {name} {sha256(src)[:12]}")
|
||||
else:
|
||||
print(f"unchanged: {name} {sha256(src)[:12]}")
|
||||
|
||||
if args.check:
|
||||
if diverged:
|
||||
print(f"DIVERGED: {', '.join(diverged)} — run install_wr_daemon.py")
|
||||
return 1
|
||||
print("CONVERGED: installed copy == canonical repo source")
|
||||
return 0
|
||||
|
||||
# post-install convergence proof
|
||||
bad = [n for n in FILES if sha256(CANONICAL / n) != sha256(INSTALL / n)]
|
||||
if bad:
|
||||
print(f"INSTALL FAILED convergence: {bad}")
|
||||
return 1
|
||||
print("CONVERGED: installed copy == canonical repo source")
|
||||
|
||||
if args.restart:
|
||||
rc = subprocess.call([sys.executable, str(REPO / "scripts" / "start_wr_daemon.py")])
|
||||
if rc != 0:
|
||||
print("restart failed")
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -7,6 +7,13 @@ The event is a WAKE, never completion truth: the waiting controller
|
||||
deploy_qualify.py after resuming. Best-effort by design — if the bridge
|
||||
is unreachable the waiter's watchdog still recovers truthfully (that is
|
||||
the §16D lost-event contract), so a notify failure must NEVER fail CI.
|
||||
|
||||
REAL_ASYNC_BRIDGE (R3): the completion event is ALSO written directly as
|
||||
a durable completion file (resumes/<operation_id>.done). Delivery no
|
||||
longer depends on the daemon being alive: the waiter blocks on inotify
|
||||
for the file itself (kernel wake, zero latency, zero CPU), and the
|
||||
durable file survives daemon AND waiter restarts (R9). The write is
|
||||
best-effort on the same terms as the POST — never fails CI.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -35,6 +42,19 @@ def main() -> int:
|
||||
"exit_code": 0 if result == "success" else 1,
|
||||
"completed_at": time.time(),
|
||||
}
|
||||
# R3 durable delivery: write the completion file FIRST — it works even
|
||||
# if the bridge daemon is down, and the atomic rename means any waiter
|
||||
# (inotify-watching or starting later) sees a complete file or none.
|
||||
try:
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import completion_file as cf # noqa: E402 — sibling module
|
||||
written = cf.write_completion(payload["operation_id"], payload)
|
||||
print(f"notify-event-bridge: durable completion file {written}",
|
||||
flush=True)
|
||||
except Exception as exc: # noqa: BLE001 — delivery is best-effort; watchdog recovery covers a lost completion file (§16D contract)
|
||||
print(f"notify-event-bridge: completion-file write failed ({exc}); "
|
||||
f"bridge POST and watchdog recovery still cover delivery",
|
||||
flush=True)
|
||||
for attempt in range(2):
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
|
||||
+78
-11
@@ -54,6 +54,15 @@ UMBRELLA_TAGS = {
|
||||
}
|
||||
|
||||
|
||||
# Issue template directory — canonical copies live beside this script
|
||||
# (mirrored to .gitea/ISSUE_TEMPLATE/ for the web-UI submission path).
|
||||
ISSUE_TEMPLATE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "issue_templates")
|
||||
|
||||
# kind -> Gitea label that MUST be present on the issue
|
||||
KIND_LABEL = {"feature": "feature", "bug": "bug", "research": "research"}
|
||||
VERSION_INTENTS = ("major", "minor", "patch", "none")
|
||||
|
||||
|
||||
class Custodian:
|
||||
"""The Custodian agent for Gitea-based self-improvement."""
|
||||
|
||||
@@ -105,23 +114,81 @@ class Custodian:
|
||||
except Exception as e:
|
||||
return {"error": str(e)[:200]}
|
||||
|
||||
def create_issue(self, title: str, body: str, labels: List[str] = None) -> dict:
|
||||
"""Create a Gitea issue with plain-language description.
|
||||
def _ensure_label(self, name: str, description: str = "", color: str = "#6f42c1") -> bool:
|
||||
"""Make sure a label exists on the repo; return its id or None."""
|
||||
labels = self._gitea_api("labels")
|
||||
if isinstance(labels, list):
|
||||
for l in labels:
|
||||
if l["name"] == name:
|
||||
return l["id"]
|
||||
data = {"name": name, "description": description or name, "color": color}
|
||||
created = self._gitea_api("labels", method="POST", data=data)
|
||||
return created.get("id") if isinstance(created, dict) else None
|
||||
|
||||
Labels should be umbrella names (memory, governance, etc).
|
||||
They get converted to IDs automatically.
|
||||
def _load_issue_template(self, kind: str) -> str:
|
||||
"""Load the canonical issue template for a kind, WITHOUT the web-UI
|
||||
frontmatter (name/about/title/labels — that metadata belongs to the
|
||||
submission form, not the issue body). Empty string if absent."""
|
||||
path = os.path.join(ISSUE_TEMPLATE_DIR, f"{kind}.md")
|
||||
try:
|
||||
with open(path) as f:
|
||||
text = f.read()
|
||||
except FileNotFoundError:
|
||||
return ""
|
||||
if text.startswith("---"):
|
||||
end = text.find("\n---", 3)
|
||||
if end != -1:
|
||||
text = text[end + 4:]
|
||||
return text.lstrip("\n")
|
||||
|
||||
def create_issue(
|
||||
self,
|
||||
title: str,
|
||||
body: str,
|
||||
labels: List[str] = None,
|
||||
kind: str = "feature",
|
||||
version_intent: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Create a Gitea issue assembled from the canonical template.
|
||||
|
||||
kind: feature | bug | research — selects the template and forces the
|
||||
matching label (the research label is auto-created if missing).
|
||||
version_intent: major | minor | patch | none — pre-ticks the matching
|
||||
box in the template's Version impact section. The pipeline
|
||||
verifies the real bump at merge and flags mismatches.
|
||||
labels: umbrella names (memory, governance, ...); converted to IDs.
|
||||
|
||||
The caller's body replaces the SUBMITTED_BODY marker inside the
|
||||
template's Plain English section — plain terms first, always.
|
||||
"""
|
||||
# Convert label names to IDs
|
||||
# 1. assemble body from template (graceful: unformatted if template missing)
|
||||
template = self._load_issue_template(kind)
|
||||
text = body.strip()
|
||||
if template and "<!-- SUBMITTED_BODY -->" in template:
|
||||
text = template.replace("<!-- SUBMITTED_BODY -->", text, 1)
|
||||
if version_intent in VERSION_INTENTS:
|
||||
text = text.replace(f"- [ ] {version_intent} ", f"- [x] {version_intent} ", 1)
|
||||
|
||||
# 2. labels: umbrella names + mandatory kind label
|
||||
labels = list(labels or [])
|
||||
kind_label = KIND_LABEL.get(kind)
|
||||
if kind_label and kind_label not in labels:
|
||||
labels.append(kind_label)
|
||||
|
||||
all_labels = self._gitea_api("labels")
|
||||
label_map = {l["name"]: l["id"] for l in all_labels} if isinstance(all_labels, list) else {}
|
||||
label_ids = []
|
||||
if labels:
|
||||
all_labels = self._gitea_api("labels")
|
||||
if isinstance(all_labels, list):
|
||||
label_map = {l["name"]: l["id"] for l in all_labels}
|
||||
label_ids = [label_map[l] for l in labels if l in label_map]
|
||||
for l in labels:
|
||||
if l in label_map:
|
||||
label_ids.append(label_map[l])
|
||||
else:
|
||||
lid = self._ensure_label(l)
|
||||
if lid:
|
||||
label_ids.append(lid)
|
||||
|
||||
data = {
|
||||
"title": title,
|
||||
"body": body,
|
||||
"body": text,
|
||||
"labels": label_ids,
|
||||
}
|
||||
result = self._gitea_api("issues", method="POST", data=data)
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: Bug Fix
|
||||
about: Demonstrate currently incorrect behaviour — plain English first, proof required
|
||||
title: "[BUG] "
|
||||
labels: bug
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
## In plain English: <restate the issue title here in everyday words>
|
||||
<!-- The heading ITSELF must carry the summary — a scan of issue headings
|
||||
alone should tell the whole story of the board. TOP PRIORITY: if a
|
||||
technical detail below contradicts this section, the issue is buggy —
|
||||
fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is broken: <what a person observes going wrong>
|
||||
|
||||
What should happen instead: <the correct behaviour>
|
||||
|
||||
How often / when: <always | after X | since change Y>
|
||||
|
||||
## Proof it is broken (required — no proof, no bug)
|
||||
<!-- A bug is DEMONSTRATED currently incorrect behaviour. Nothing else.
|
||||
If this section cannot be filled, file it as a question, not a bug. -->
|
||||
- [ ] Reproduction command or steps (deterministic where possible)
|
||||
- [ ] The exact wrong output observed: <paste/log excerpt>
|
||||
- [ ] Where the evidence lives: <log path, fixture, session id>
|
||||
|
||||
## Steps to reproduce
|
||||
<!-- How to trigger the bug -->
|
||||
|
||||
## What would let us close this
|
||||
- [ ] The reproduction command now shows correct behaviour
|
||||
- [ ] A regression test exists that fails before the fix and passes after
|
||||
- [ ] No other behaviour changed (or changes are listed and justified)
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- A bug fix is USUALLY a patch. Pick major or minor only with a reason. -->
|
||||
- [ ] patch — same behaviour, done correctly (usual for bugs)
|
||||
- [ ] major — the bug was in a published contract and fixing it breaks consumers (justify)
|
||||
- [ ] minor — fixing revealed a missing capability that had to be added (justify)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT (the bug) / WHERE (files) / EVIDENCE (how it was confirmed). -->
|
||||
|
||||
**WHAT (bug):**
|
||||
**WHERE:**
|
||||
**EVIDENCE:**
|
||||
|
||||
## Pipeline requirements
|
||||
<!-- Every fix must clear these before the issue closes. -->
|
||||
- [ ] TDD: regression test written first, red → green
|
||||
- [ ] `ruff check` and `ruff format --check` clean
|
||||
- [ ] Spec-compliance review gate: PASS
|
||||
- [ ] Code-quality review gate: Approved
|
||||
- [ ] Full orchestration suite regression green (suite total)
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit references
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
- [ ] **Trigger** — what event activates it
|
||||
- [ ] **Read/write authority** — which canonical state it reads/changes
|
||||
- [ ] **Provenance** — derived state traceable to evidence
|
||||
- [ ] **Verification** — how we know the change helped
|
||||
- [ ] **Failure visibility** — how failure surfaces
|
||||
- [ ] **Reversibility** — bad learned state can be superseded/replayed/rolled back
|
||||
- [ ] **Dormant cost** — what it costs when irrelevant
|
||||
- [ ] **Benchmark + ablation** — what measurable behaviour improves; what happens when disabled
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · epistemic confidence ≠ attentional utility ≠ current applicability · retrieval never increases utility · self-model-induced behaviour is not independent evidence · historical truth survives present-state change · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -0,0 +1,96 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: New feature or improvement — plain English first, verifiable close conditions, version intent
|
||||
title: "[FEATURE] "
|
||||
labels: feature
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
## In plain English: <restate the issue title here in everyday words>
|
||||
<!-- The heading ITSELF must carry the summary — a scan of issue headings
|
||||
alone should tell the whole story of the board. TOP PRIORITY: if a
|
||||
technical detail below contradicts this section, the issue is buggy —
|
||||
fix the issue. A non-technical reader must be able to retell this
|
||||
section. 3-6 sentences, no jargon. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
What is this about: <what>
|
||||
|
||||
Why do we need it: <the itch — what happens today that shouldn't, or what becomes possible>
|
||||
|
||||
Why is it still open: <"not started yet" | "part done, X remains" | "deliberately parked as a future idea">
|
||||
|
||||
## Example
|
||||
<!-- A concrete before/after in plain terms: "If X happens, the system will Y instead of Z." -->
|
||||
|
||||
## What would let us close this
|
||||
<!-- Checkable statements, verifiable by looking at something that EXISTS —
|
||||
a file, a log line, a passing test. Not "when appropriate". -->
|
||||
- [ ] <condition 1 — observable>
|
||||
- [ ] <condition 2>
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
<!-- The pipeline derives the real bump from merged conventional commits and
|
||||
compares against this declaration. A mismatch is flagged, not fatal:
|
||||
either the declaration was wrong or the work grew. Both facts matter. -->
|
||||
- [ ] major — breaking: changes a contract, schema, or interface others depend on
|
||||
- [ ] minor — new capability: something becomes possible that wasn't
|
||||
- [ ] patch — repair: same behaviour, done correctly
|
||||
- [ ] none — touches no released artifact (docs, memory, process notes)
|
||||
|
||||
## Technical details
|
||||
<!-- WHAT — one-line objective.
|
||||
WHY — design-spec reference (S#) or reason.
|
||||
FILES — affected components.
|
||||
ACCEPTANCE — frozen-test file + test count + canary name.
|
||||
DEPENDENCIES — parent epic + preceding stage/plan refs. -->
|
||||
|
||||
**WHAT:**
|
||||
**WHY:**
|
||||
**FILES:**
|
||||
**ACCEPTANCE:**
|
||||
**DEPENDENCIES:**
|
||||
|
||||
## Pipeline requirements
|
||||
<!-- Every stage must clear these before the issue closes. -->
|
||||
- [ ] TDD: frozen tests written first, red → green (N passed)
|
||||
- [ ] `ruff check` and `ruff format --check` clean
|
||||
- [ ] Spec-compliance review gate: PASS
|
||||
- [ ] Code-quality review gate: Approved
|
||||
- [ ] Full orchestration suite regression green (suite total)
|
||||
- [ ] Canary passes (name)
|
||||
- [ ] Closed with a properly formatted closing comment + commit/PR references
|
||||
|
||||
## Cognitive invariants (memory/cognition issues — both handoffs)
|
||||
<!-- Complete ONLY if this issue touches memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before creating modules)
|
||||
- [ ] **Trigger** — what event activates it
|
||||
- [ ] **Read/write authority** — which canonical state it reads/changes
|
||||
- [ ] **Provenance** — derived state traceable to evidence
|
||||
- [ ] **Verification** — how we know the change helped
|
||||
- [ ] **Failure visibility** — how failure surfaces
|
||||
- [ ] **Reversibility** — bad learned state can be superseded/replayed/rolled back
|
||||
- [ ] **Dormant cost** — what it costs when irrelevant
|
||||
- [ ] **Benchmark + ablation** — what measurable behaviour improves; what happens when disabled
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · epistemic confidence ≠ attentional utility ≠ current applicability · retrieval never increases utility · self-model-induced behaviour is not independent evidence · historical truth survives present-state change · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — breaks the spine / core correctness, or blocks other work
|
||||
- [ ] **P1 — high** — material user value or a real defect (money, correctness bugs, next-stage dependency)
|
||||
- [ ] **P2 — medium** — quality/consolidation improvement (check the fold-into mapping first)
|
||||
- [ ] **P3 — low** — measurement / benchmark / deferred policy
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
name: Research Submission
|
||||
about: Buy down uncertainty before building — question, falsifier, budget, durable findings
|
||||
title: "[RESEARCH] "
|
||||
labels: research
|
||||
assignees: conductor
|
||||
---
|
||||
|
||||
## In plain English: <restate the issue title here in everyday words>
|
||||
<!-- The heading ITSELF must carry the summary — a scan of issue headings
|
||||
alone should tell the whole story of the board. TOP PRIORITY: if a
|
||||
technical detail below contradicts this section, the issue is buggy —
|
||||
fix the issue. -->
|
||||
<!-- SUBMITTED_BODY -->
|
||||
|
||||
|
||||
The question: <what we're trying to learn, one sentence>
|
||||
|
||||
Why we're asking: <what decision this research will inform — a name, an issue
|
||||
number, or "exploratory: none yet">
|
||||
|
||||
What we believe today: <the current assumption, stated so it can be proven wrong>
|
||||
|
||||
## The shape of an answer
|
||||
- [ ] What evidence would settle the question: <observation, benchmark, or spike>
|
||||
- [ ] What evidence would KILL the current assumption (the falsifier): <be specific —
|
||||
this is stated BEFORE starting so the research cannot become a search for confirmation>
|
||||
- [ ] Budget: <time/effort cap — when research must stop and report back>
|
||||
|
||||
## What would let us close this
|
||||
- [ ] Findings written to a durable document: <path, e.g. research/issue-NNN/> —
|
||||
a comment thread is not a durable finding
|
||||
- [ ] The decision the research informs is recorded: made / deferred / changed
|
||||
- [ ] If a follow-up build issue was minted: <#N or "none needed">
|
||||
|
||||
## Version impact (declared at submission — verified at merge)
|
||||
- [ ] none — research changes no released artifact (usual)
|
||||
- [ ] minor/major — only if a code spike is being merged as part of this (justify)
|
||||
|
||||
## Technical detail
|
||||
<!-- Candidate approaches, prior art in the repo, related issues, constraints,
|
||||
rebuildable-projection or authority requirements if findings touch canonical state. -->
|
||||
|
||||
## Pipeline requirements
|
||||
- [ ] Findings committed under the stated durable path
|
||||
- [ ] Every citation verified against a real source (no fabricated references)
|
||||
- [ ] Closed with a properly formatted closing comment: findings summary + decision
|
||||
|
||||
## Cognitive invariants (memory/cognition research — both handoffs)
|
||||
<!-- Complete ONLY if findings touch memory, learning, or self-model state. -->
|
||||
- [ ] **Owner** — which existing subsystem owns this (audit before proposing modules)
|
||||
- [ ] **Read/write authority** — which canonical state is affected
|
||||
- [ ] **Provenance** — findings traceable to evidence
|
||||
- [ ] **Reversibility** — projections remain rebuildable; no unique truth moves into a derived index
|
||||
- [ ] **No duplicate authority** — why extend vs create
|
||||
|
||||
Hard invariants: derived cognition preserves evidentiary ancestry · retrieval never increases utility · raw evidence is never destroyed by promotion.
|
||||
|
||||
## Related umbrella
|
||||
- [ ] Memory
|
||||
- [ ] Governance
|
||||
- [ ] Identity
|
||||
- [ ] Growth
|
||||
- [ ] Perception
|
||||
- [ ] Infrastructure
|
||||
- [ ] Life (Nick's)
|
||||
- [ ] Custodian
|
||||
- [ ] Orchestration
|
||||
|
||||
## Priority (REQUIRED — the priority gate)
|
||||
<!-- Pick exactly one. An issue without a priority is not filed. -->
|
||||
- [ ] **P0 — critical** — blocks a decision that blocks other work
|
||||
- [ ] **P1 — high** — informs a next-stage dependency
|
||||
- [ ] **P2 — medium** — quality/consolidation understanding
|
||||
- [ ] **P3 — low** — watchlist / deferred / exploratory
|
||||
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
__version__ = "0.83.1"
|
||||
__version__ = "0.87.0"
|
||||
|
||||
# One build identity (handoff 2026-09-10 §4): the semantic version above is
|
||||
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
|
||||
|
||||
@@ -231,7 +231,7 @@ def main() -> int:
|
||||
)
|
||||
check(
|
||||
"Rune minor version advertises provisioning and restore verification",
|
||||
'__version__ = "0.83.1"' in rune_version,
|
||||
'__version__ = "0.87.0"' in rune_version,
|
||||
failures,
|
||||
)
|
||||
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
|
||||
|
||||
+42
-3
@@ -171,7 +171,31 @@ def register_process_wait(operation_id: str, *, contract_id: str = "",
|
||||
provider: str = "builder",
|
||||
event_type: str = "builder_exit",
|
||||
sha: str = "",
|
||||
watchdog_secs: int | None = None) -> dict:
|
||||
watchdog_secs: int | None = None,
|
||||
continuation_state: str = "WAITING_EXTERNAL",
|
||||
op_type: str = "",
|
||||
authority: str = "",
|
||||
task_id: str = "",
|
||||
resume_mode: str = "fifo") -> dict:
|
||||
"""E-PATCH §4: the PendingOperation IS the durable continuation object.
|
||||
|
||||
continuation_state is first-class cognitive quiescence:
|
||||
ACTIVE_REASONING | WAITING_EXTERNAL | RESUMABLE | RECOVERY_REQUIRED
|
||||
op_type/authority record the CANONICAL execution semantics (derived by
|
||||
intent normalization, never model-supplied). task_id/project bind the
|
||||
operation to the current programme so fresh-process recovery can
|
||||
resolve relevance without the model reconstructing state.
|
||||
|
||||
resume_mode owns the SESSION-DELIVERY domain (REAL_ASYNC_BRIDGE §2/§3):
|
||||
"fifo" — legacy delivery: the daemon writes a resume token to the
|
||||
operation's FIFO on event match (T15 semantics; the waiter
|
||||
blocks reading the FIFO). Default preserves T15 builder
|
||||
behavior.
|
||||
"self" — the waiter process itself blocks on the durable completion
|
||||
file (inotify) and stamps its own terminal operation state
|
||||
on authority verification. NO resume_cmd is registered: the
|
||||
daemon must never dispatch a delivery worker for this wait,
|
||||
and a dead reader cannot downgrade operation truth."""
|
||||
"""Register a durable PROCESS_EVENT wait for one operation. The
|
||||
per-wait resume_cmd writes a resume token to the operation's FIFO —
|
||||
the bridge can resume a Python controller, not just MiMo sessions.
|
||||
@@ -196,6 +220,13 @@ def register_process_wait(operation_id: str, *, contract_id: str = "",
|
||||
"fd=os.open(sys.argv[1], os.O_WRONLY | os.O_NONBLOCK);"
|
||||
"os.write(fd, json.dumps({'wait_id': sys.argv[2],"
|
||||
" 'resumed_at': time.time()}).encode());os.close(fd)")
|
||||
if resume_mode == "self":
|
||||
# REAL_ASYNC_BRIDGE §3: no session-delivery worker. The waiter
|
||||
# self-resumes on the durable completion file; the daemon's only
|
||||
# roles are event persistence and failure-detection sweeps.
|
||||
resume_cmd = None
|
||||
else:
|
||||
resume_cmd = ["python3", "-c", writer, str(fifo), "{wait_id}"]
|
||||
rec = ws.new_wait(
|
||||
session_id=f"controller-{operation_id}",
|
||||
provider=provider,
|
||||
@@ -212,12 +243,20 @@ def register_process_wait(operation_id: str, *, contract_id: str = "",
|
||||
"independent verification",
|
||||
on_success="continue programme continuation canary",
|
||||
on_failure="classify failure; recovery or block",
|
||||
resume_cmd=["python3", "-c", writer, str(fifo), "{wait_id}"],
|
||||
resume_cmd=resume_cmd,
|
||||
)
|
||||
rec["ext"] = {"contract_id": contract_id, "operation_id": operation_id,
|
||||
"fifo": str(fifo),
|
||||
"watchdog_secs": watchdog_secs
|
||||
or int(os.environ.get("WR_WATCH_SECS", "600"))}
|
||||
or int(os.environ.get("WR_WATCH_SECS", "600")),
|
||||
"continuation_state": continuation_state,
|
||||
"op_type": op_type, "authority": authority,
|
||||
"task_id": task_id,
|
||||
# §3 delivery-domain marker: the daemon drive loop skips
|
||||
# waits marked "self" (the waiter owns its continuation);
|
||||
# records WITHOUT the marker remain eligible for the
|
||||
# env-template session-delivery path (WR_RESUME_CMD).
|
||||
"resume_mode": resume_mode}
|
||||
ws.save(rec)
|
||||
# cross-user bridge (T16 §16C live finding): the daemon reconciler may
|
||||
# run under a different account than the registering controller — the
|
||||
|
||||
@@ -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.83.1
|
||||
Version: v0.87.0
|
||||
Features: 41 (24 original + 17 new native commands)
|
||||
|
||||
Usage:
|
||||
@@ -1885,7 +1885,7 @@ class RuneApp(App):
|
||||
# Show version info
|
||||
self.chat_channel.add_message(
|
||||
"system",
|
||||
f"[dim]Rune v0.83.1 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
f"[dim]Rune v0.87.0 | Textual TUI | {len(loaded)} native modules[/dim]",
|
||||
)
|
||||
|
||||
# Auto-show night mode suggestion if evening/night hours
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T20:50:30Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "matched_by": "pending_match", "model_deadline_ignored": true, "model_op_type_ignored": true, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-756ebe6f45ad83a919738539a1565c17ed8572ac"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T20:51:20Z", "authority": "deploy_qualify", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_ancestor_of_master", "model_deadline_ignored": true, "model_op_type_ignored": true, "normalized": true, "op_type": "master_ci", "operation_id": "ci-f7256aa29e8bc7aa365ad7ffa27eb557528ccca5"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T21:50:39Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_on_feature_branch", "model_deadline_ignored": true, "model_op_type_ignored": true, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T21:53:34Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_on_feature_branch", "model_deadline_ignored": false, "model_op_type_ignored": false, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T21:55:05Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_on_feature_branch", "model_deadline_ignored": false, "model_op_type_ignored": false, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-17T23:19:18Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "matched_by": "sleep_poll_intent", "model_deadline_ignored": true, "model_op_type_ignored": false, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-756ebe6f45ad83a919738539a1565c17ed8572ac", "raw_command_kind": "sleep_poll", "raw_sleep_seconds_suppressed": 150}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-18T10:30:10Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_on_feature_branch", "model_deadline_ignored": true, "model_op_type_ignored": true, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-427c3ac6bb3fd67492d3596a33919d1e5aa4a8d1"}
|
||||
{"action": "ContinuePendingOperation", "at": "2026-09-18T10:30:40Z", "authority": "ci_runs", "deadline_provenance": "GOVERNED_OPERATION_POLICY", "deadline_secs": 1800, "evidence": "commit_on_feature_branch", "model_deadline_ignored": true, "model_op_type_ignored": false, "normalized": true, "op_type": "branch_ci", "operation_id": "ci-427c3ac6bb3fd67492d3596a33919d1e5aa4a8d1", "raw_command_kind": "sleep_poll", "raw_sleep_seconds_suppressed": 240}
|
||||
{"at": "2026-09-18T20:23:58Z", "candidates": ["", "", "", "", "", "", "", "", "", "", "", "", "", "op-latency-proof", "dbg-op", "dbg3", "t15-BUILDER-2026-09-15T02:18:49Z-Verboo Code/glm-5.3-flash", "t15-resume-BUILDER-2026-09-15T02:18:49Z-Verboo Code/glm-5.3-flash", "t15-BUILDER-2026-09-15T03:11:05Z-Verboo Code/glm-5.3-flash", "t15-release-196cf19", "", "", "ci-c707a81933f7f90eaaae4000282674b6a2e3cb4c", "ci-4a06d9f11a722b7aa32fc38ab92b3ba8130414af", "ci-b51afea0cbfe00b5493647ed61d071fe104b306a", "", "ci-986b450bc22e969cbd2f09fdcfbc24e4337740ba", "ci-2a4f9c8367d06acbda4b39c1cee388796b25cc85", "ci-fe9d154778c117e8b0f1fbae4e5e7d9121546839", "ci-c9dee14c626b853cab625b431af3b23866b84314", "ci-c9dee14c626b853cab625b431af3b23866b84314", "ci-repro-test-sha", "", "", "", "", "", "", "", "", "", "", "ci-3c54782634993361bbab3c66f948fc59121865a5", "ci-3c54782634993361bbab3c66f948fc59121865a5", "", "", "", "ci-dcead3feccb57b6dfb30effd96fcac6b563f8099", "ci-ab2dfe7b09c154a329c5145da4d32639a83579b6", "ci-95946b6efd883e56797134617d42daed1acf798a", "ci-45869a77cdea1c2ce9e5c1f54c9840c3acef1487", "ci-98cb3a98a5f2e245cd0fd6003ffd02a238120ddc", "ci-98cb3a98a5f2e245cd0fd6003ffd02a238120ddc", "ci-2ccac89ec448900766d47e322b2723da5b2ce9dd", "ci-2ccac89ec448900766d47e322b2723da5b2ce9dd", "ci-2ccac89ec448900766d47e322b2723da5b2ce9dd", "ci-824985488196ba5e5033318b7b2b6cdc82719ed6", "ci-824985488196ba5e5033318b7b2b6cdc82719ed6", "ci-824985488196ba5e5033318b7b2b6cdc82719ed6", "ci-9e903dacafdbc40dccf17ffb3ea09e8452396860", "ci-9e903dacafdbc40dccf17ffb3ea09e8452396860", "ci-9e903dacafdbc40dccf17ffb3ea09e8452396860", "ci-9e903dacafdbc40dccf17ffb3ea09e8452396860", "ci-5d20dc7f513b740a165952ca00f758660d328c43", "ci-0a470d86b80baefeb71c795626a901fac19bf731", "ci-116d1ffbeb49c879855b982c89d51db25528537d", "ci-116d1ffbeb49c879855b982c89d51db25528537d", "ci-116d1ffbeb49c879855b982c89d51db25528537d", "ci-40b3731928f499a77653613cb6c3e6db93cb64a1", "ci-be6bc17e93d8ffbc2679d913e55dd848dc8fa82e", "ci-be6bc17e93d8ffbc2679d913e55dd848dc8fa82e", "ci-f3ef4672f3209e73bd110fc49efff9834a2eb987", "ci-f3ef4672f3209e73bd110fc49efff9834a2eb987", "ci-f3ef4672f3209e73bd110fc49efff9834a2eb987", "ci-f3ef4672f3209e73bd110fc49efff9834a2eb987", "ci-6b5d8ad02308f24422b1efd7c0d343ebb5c7a19d", "ci-6b5d8ad02308f24422b1efd7c0d343ebb5c7a19d", "ci-ba2966e6318d6f3f1d2260732b1534732d650c0c", "ci-ba2966e6318d6f3f1d2260732b1534732d650c0c", "ci-ba2966e6318d6f3f1d2260732b1534732d650c0c", "ci-ba2966e6318d6f3f1d2260732b1534732d650c0c", "ci-ba2966e6318d6f3f1d2260732b1534732d650c0c", "ci-ba2966e6318d6f3f1d2260732b1534732d650c0c", "ci-ba2966e6318d6f3f1d2260732b1534732d650c0c", "ci-ba2966e6318d6f3f1d2260732b1534732d650c0c", "ci-ba2966e6318d6f3f1d2260732b1534732d650c0c", "ci-00013f8c0b174011bcea12351facf7f1f382e8a3", "ci-756ebe6f45ad83a919738539a1565c17ed8572ac", "ci-df8262bd58122e33d4c447dc863e39ddde684963", "ci-4944f9e79d0a9c4df1d76a0d8c119e33e1f9a55", "ci-4944f9e79d0a9c4df1d76a0d8c119e33e1f9a55", "ci-f545f1ed7b7b0b8c29f1d27c4d328f4a3a2d1b61", "ci-f545f1ed7b7b0b8c29f1d27c4d328f4a3a2d1b61", "ci-f545f1ed7b7b0b8c29f1d27c4d328f4a3a2d1b61", "ci-f545f1ed8d5cf2ae99554eb02b95771082e449c8", "ci-7cf7c8d0a9c4aa7c6c1a7c2e0a2e7c3b4c5d6e7", "ci-7cf7c8dd8d82ef2165aedbf456683448a7536bc7", "ci-7cf7c8dd8d82ef2165aedbf456683448a7536bc7", "ci-7cf7c8dd8d82ef2165aedbf456683448a7536bc7", "ci-7cf7c8dd8d82ef2165aedbf456683448a7536bc7", "ci-7008743a1a7f8c1b9e4b0b8e4c4f4f4f4f4f4f4", "ci-70087436203f8e44181b94676a05418730b8de5d", "ci-70087436203f8e44181b94676a05418730b8de5d", "ci-26e37653e3f8c1b9e4b0b8e4c4f4f4f4f4f4f4f", "ci-26e3765319bc29064f7d42f092bc86dcc7b2ecc0", "ci-2b6a5a6b95629d3cf48fd171cbf6a2544f771817", "ci-2b6a5a6b95629d3cf48fd171cbf6a2544f771817", "ci-5db8b202d97e4721369aa507e2bea7b1cf66af5c", "ci-04a277f7668e99d56c1b7c05e2e40a1da93dca55", "ci-5718a3ea85c397434595e79b7a63700617c1de21", "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c", "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c", "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c", "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c", "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c", "ci-246f26663d8fa2aba3a16039c86feb13a6d0bd2c", "ci-53b4830b08a7dd72a1a7236f1752516880a29bc8", "ci-f714120", "ci-65001196d831a7f51b2bbc2b6bbcfe34c70442ff", "ci-65001196d831a7f51b2bbc2b6bbcfe34c70442ff", "ci-53b4830b08a7dd72a1a7236f1752516880a29bc8", "ci-53b4830b08a7dd72a1a7236f1752516880a29bc8", "ci-53b4830b08a7dd72a1a7236f1752516880a29bc8", "ci-53b4830b08a7dd72a1a7236f1752516880a29bc8", "ci-853cf3e0ae10ad3d45cd879527093368ccad8251", "ci-599c3ab1c23f7eaec323c67b3523fdb9e1acea74", "ci-599c3ab1c23f7eaec323c67b3523fdb9e1acea74", "ci-a54ab559cbd50e77ac55db4056951bd6655a0632", "ci-a54ab559cbd50e77ac55db4056951bd6655a0632", "ci-a54ab559cbd59bb9696d329c998cd58a221edd72", "ci-1c5dc557a69a368aad267d1871f4eda646a822fb", "ci-1c5dc557a69a368aad267d1871f4eda646a822fb", "ci-05f2a74c6fa086ddec55d146b780859df34d31a5", "ci-05f2a74c6fa086ddec55d146b780859df34d31a5", "ci-2e05ea6", "ci-d53bb9a", "ci-d53bb9a53Line truncated
|
||||
@@ -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": [
|
||||
@@ -331,6 +348,21 @@
|
||||
},
|
||||
"state": "V1_ACCEPTED",
|
||||
"was": "RETEST_REQUIRED (t21.3-open)"
|
||||
},
|
||||
{
|
||||
"at": "t21.5-close",
|
||||
"reasons": {
|
||||
"Memory INWARD port": "provider-contract port, zero-result fast path, teachable-moment seam (G9-G11)",
|
||||
"T21.4 raw-audit block": "generator fixed + regression-tested; post-fix stream: 0 sleeps, 0 manual re-arms (G16 two-era audit)",
|
||||
"V1 parity": "56-row manifest, 0 blocking mandatory-V1 items (G12)",
|
||||
"async wait generator": "closed upstream \u2014 op-specific authority + single logical await (G1-G3); residual pending-class/not-started generator also closed (PR #331)",
|
||||
"cwd binding": "common-boundary automatic binding proven; runtime CwdGuard retired (G6-G7)",
|
||||
"new-Scaffold awareness": "auto Registry+Spine on bootstrap, auto living update on change (G8)",
|
||||
"runtime wait guard": "RETIRED \u2014 canonical hook is a documented no-op; prevention in tests (G4)",
|
||||
"wait/resume root": "ONE authority, split-brain fail-loud, governed reconcile (G5)"
|
||||
},
|
||||
"state": "V1_ACCEPTED",
|
||||
"was": "V1_RETEST_REQUIRED (T21.4: 10 PASS / 2 BLOCKED)"
|
||||
}
|
||||
],
|
||||
"behaviour_specs": [
|
||||
@@ -411,7 +443,7 @@
|
||||
"fresh-context rehydration from compact ledgers"
|
||||
],
|
||||
"schema_version": 1,
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"version_history": [
|
||||
{
|
||||
"affected_consumers": [],
|
||||
@@ -462,11 +494,28 @@
|
||||
"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-17T00:37:29Z"
|
||||
"updated_at": "2026-09-17T17:15:59Z"
|
||||
},
|
||||
"capability:tui-testing-command": {
|
||||
"behaviour_specs": [
|
||||
@@ -543,8 +592,17 @@
|
||||
"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-17T00:37:29Z"
|
||||
"updated_at": "2026-09-17T17:16:18Z"
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
{
|
||||
"tranche": "ASYNC_INTENT_NORMALIZATION",
|
||||
"purpose": "Intent normalization at the execution boundary \u2014 model emits intent, controller owns execution semantics",
|
||||
"predecessor": "T21.5 (guards retired) + Memory T0 (complete)",
|
||||
"created_at": "2026-09-17T20:43:47Z",
|
||||
"gates": [
|
||||
{
|
||||
"gate_id": "A10_LIVE_MASTER_DEPLOY_PROOF",
|
||||
"mandatory": true,
|
||||
"requirement": "live master CI -> deploy with normalization evidence",
|
||||
"status": "PASS",
|
||||
"evidence": "Post-merge master 53b4830: canonical continuation master_ci/deploy_qualify through deadline with 4 internal re-arms, then event_replayed success 0.771s on re-arm; deployed current == 53b4830 (0.84.1, ",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A11_REGRESSION_TESTS",
|
||||
"mandatory": true,
|
||||
"requirement": "deterministic test matrix green; existing wait tests unbroken",
|
||||
"status": "PASS",
|
||||
"evidence": "tests/test_await_intent.py 14/14 (A-H matrix + derivation truth table + provenance + observability). Existing wait suites unbroken: test_ci_event_wait 11/11, test_single_logical_await 9/9, test_event_",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A12_RAW_SELF_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "post-fix stream classified; no model scheduling choice affected execution",
|
||||
"status": "PASS",
|
||||
"evidence": "Post-fix stream classified: all 5 normalizations in store/await_intent_log.jsonl have deadline_provenance=GOVERNED_OPERATION_POLICY; MODEL_DURATION_ACTUALLY_USED_COUNT=0; MODEL_SUPPLIED_OP_TYPE_COUNT_",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:25:09Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A13_FINAL_RUNTIME_CONVERGENCE",
|
||||
"mandatory": true,
|
||||
"requirement": "git==release==deployed==runtime==healthy",
|
||||
"status": "PASS",
|
||||
"evidence": "master 53b4830 == deployed current == runtime identity 0.84.1 (deployed 22:04:56Z) == healthy; last-good fa1b7c4; tags v0.84.1. Final CI on the normalization-layer commit (8a8d353, PR #335) also green",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A1_INTENT_NORMALIZATION_BOUNDARY_PROVEN",
|
||||
"mandatory": true,
|
||||
"requirement": "earliest deterministic boundary identified + normalization implemented there (not prompt, not guard)",
|
||||
"status": "PASS",
|
||||
"evidence": "Root-cause traced: harness tool accepted model op_type/deadline BEFORE PendingOperation creation \u2014 the wait record was BORN from model args. Fix: scripts/await_intent.py normalize_action/derive_op_typ",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A2_PENDING_OPERATION_IS_WAIT_AUTHORITY",
|
||||
"mandatory": true,
|
||||
"requirement": "stored operation truth (op_type/authority/timing) wins over model args",
|
||||
"status": "PASS",
|
||||
"evidence": "Stored normalized records (ext.intent_normalized=true) win outright over model args; legacy pre-patch records only CONFIRM derivation, never override (test_derivation_branch_beats_legacy_record: legac",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A3_MODEL_TIMING_NON_AUTHORITATIVE",
|
||||
"mandatory": true,
|
||||
"requirement": "MODEL_DURATION_ACTUALLY_USED_COUNT=0 for canonical flows; provenance recorded",
|
||||
"status": "PASS",
|
||||
"evidence": "Timing provenance order USER_EXPLICIT > EXTERNAL_CONTRACT > GOVERNED_OPERATION_POLICY; MODEL_INVENTED classified then discarded (classify_timing_provenance + tests). Live/audit: MODEL_DURATION_ACTUALL",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A4_MODEL_OP_TYPE_NON_AUTHORITATIVE",
|
||||
"mandatory": true,
|
||||
"requirement": "MODEL_SUPPLIED_OP_TYPE_COUNT_USED=0; stored/derived authority wins",
|
||||
"status": "PASS",
|
||||
"evidence": "Derivation: normalized record > open PR (pull_request_checks) > ancestor-of-master (master_ci) > feature branch (branch_ci); legacy records never override. Live: model emitted master_ci for branch sha",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A5_SLEEP_POLL_INTENT_NORMALIZED",
|
||||
"mandatory": true,
|
||||
"requirement": "sleep/check/poll intent for a known pending op compiles to canonical continuation; literal sleep not executed",
|
||||
"status": "PASS",
|
||||
"evidence": "normalize_bash: \"sleep 300 && check CI for <sha>\" -> ContinuePendingOperation with raw_sleep_seconds_suppressed=300 (test_sleep_check_normalized); plain \"sleep 5\"/\"sleep 2 && ls\" NOT routed (test_plai",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A6_EXPLICIT_REAL_DEADLINES_PRESERVED",
|
||||
"mandatory": true,
|
||||
"requirement": "USER_EXPLICIT and CONTRACT deadlines retained with provenance",
|
||||
"status": "PASS",
|
||||
"evidence": "USER_EXPLICIT: user_deadline=600 -> provenance USER_EXPLICIT, 600 retained (test_user_deadline_preserved). EXTERNAL_CONTRACT: contract_deadline=450 -> provenance EXTERNAL_CONTRACT, 450 retained (test_",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A7_AMBIGUITY_FAILS_SAFE",
|
||||
"mandatory": true,
|
||||
"requirement": "multiple genuinely ambiguous pending ops -> structured ambiguity, no guess",
|
||||
"status": "PASS",
|
||||
"evidence": "Multiple pending ops with no target -> normalized:\"AMBIGUOUS\" with structured candidate ids, no guess (test_ambiguous_fails_safe + live: normalize_action({}) returned AMBIGUOUS with both candidates).",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A8_LIVE_BRANCH_CI_PROOF",
|
||||
"mandatory": true,
|
||||
"requirement": "live branch push -> CI with normalization evidence",
|
||||
"status": "PASS",
|
||||
"evidence": "Live branch push 246f2666 (async-intent-live-proof). Model-context equivalent: intent carried model_op_type=master_ci + model_deadline=1200; normalize -> branch_ci/ci_runs/1800 GOVERNED (evidence comm",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "A9_LIVE_PR_MERGE_PROOF",
|
||||
"mandatory": true,
|
||||
"requirement": "live PR checks -> merge with normalization evidence",
|
||||
"status": "PASS",
|
||||
"evidence": "PR #336 created for the same sha; PR checks reached terminal via single-logical await (pull_request_checks authority -> event_replayed success 10.1s after internal waits); PR merged by merge-on-green.",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T20:43:47Z",
|
||||
"resolved_at": "2026-09-17T22:24:27Z"
|
||||
}
|
||||
],
|
||||
"can_accept_result": {
|
||||
"ok": true,
|
||||
"blockers": []
|
||||
},
|
||||
"verdict": "ASYNC_INTENT_NORMALIZATION_COMPLETE"
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# ASYNC INTENT LIVE PROOF
|
||||
|
||||
This file exists so a real branch push/CI/deploy cycle
|
||||
can exercise intent normalization end to end (A8-A10).
|
||||
Created 2026-09-17 by the corrective patch tranche.
|
||||
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"tranche": "EVENT_DRIVEN_CONTINUATION",
|
||||
"purpose": "Structured-state routing + event-triggered cognition (model=intent, state=truth, controller=procedure, events=when cognition resumes)",
|
||||
"predecessor": "async_intent_gates.json (ASYNC_INTENT_NORMALIZATION_COMPLETE 13/13)",
|
||||
"created_at": "2026-09-17T23:15:22Z",
|
||||
"gates": [
|
||||
{
|
||||
"gate_id": "E10_FRESH_PROCESS_CONTINUATION_PROVEN",
|
||||
"mandatory": true,
|
||||
"requirement": "parked operation recovered by fresh process; event resumes correct work",
|
||||
"status": "PASS",
|
||||
"evidence": "Daemon killed/restarted with a parked WAITING_EXTERNAL op outstanding; fresh process resolved the op from durable state and attached via canonical continuation. DEFECT FOUND AND FIXED during proof: ci",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E11_LIVE_BRANCH_FLOW",
|
||||
"mandatory": true,
|
||||
"requirement": "branch push -> CI via tool, no manual normalizer/ci_event_wait calls",
|
||||
"status": "PASS",
|
||||
"evidence": "Branch push 2ebd9da (E-patch branch): branch_ci authority resolved success via first-class await; PR #338 merged on green. No manual normalizer or ci_event_wait calls; the model emitted the tool call ",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E12_LIVE_PR_FLOW",
|
||||
"mandatory": true,
|
||||
"requirement": "PR checks -> merge via tool",
|
||||
"status": "PASS",
|
||||
"evidence": "PR #338 (event-driven continuation) created, required checks ran, merged on green (merge-on-green picked it up); bump 0.85.0 flowed to master. The A-patch E9 proof (PR #336) additionally demonstrated ",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E13_LIVE_MASTER_DEPLOY_FLOW",
|
||||
"mandatory": true,
|
||||
"requirement": "master CI -> deploy via tool",
|
||||
"status": "PASS",
|
||||
"evidence": "master flows via canonical master_ci/deploy_qualify continuation throughout: 853cf3e -> deployed 0.85.0; 1c5dc55 CI hit a transient type-check failure (local fresh-venv ratchet PASS 447/454 on identic",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T03:13:38Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E14_REGRESSION_AND_ADVERSARIAL_TESTS",
|
||||
"mandatory": true,
|
||||
"requirement": "A-J adversarial matrix + existing suites green",
|
||||
"status": "PASS",
|
||||
"evidence": "Adversarial matrix covered: arbitrary duration ignored (A), wrong type ignored (B), literal sleep compiled (C), non-sleep wording via route-bash (D: \"sleep 150 then check whether CI finished\" normaliz",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E15_RAW_SELF_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "raw trace outranks report; no relabeling",
|
||||
"status": "PASS",
|
||||
"evidence": "Stream classified per \u00a718: TRANSITION_PERIOD for pre-rewire tool call on 2ebd9da (94 internal re-arms, disclosed); all post-rewire flows normalized (model deadline/op_type ignored, zero literal extern",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:44Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E16_FINAL_RUNTIME_CONFIG_CONVERGENCE",
|
||||
"mandatory": true,
|
||||
"requirement": "git==release==deployed==runtime==healthy",
|
||||
"status": "PASS",
|
||||
"evidence": "git master 853cf3e == release v0.85.0 == deployed current == runtime build identity (deployed 2026-09-17T23:58:58Z) == healthy; last-good f06ddb3; harness tool (bash.ts + await-operation.ts normalize-",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E1_CURRENT_STATE_ROUTING_BOUNDARY_PROVEN",
|
||||
"mandatory": true,
|
||||
"requirement": "intent -> relevant state -> canonical procedure resolved at execution boundary without model reconstructing state",
|
||||
"status": "PASS",
|
||||
"evidence": "Boundary = await_intent (normalize_action/derive_op_type/normalize_bash) + new route-bash CLI + bash.ts harness tool. Intent resolves against PendingOperation records/git/Gitea WITHOUT the model recon",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E2_PENDING_OPERATION_DURABLE_STATE_PROVEN",
|
||||
"mandatory": true,
|
||||
"requirement": "PendingOperation owns mechanical truth (type/authority/events/status) and survives restart",
|
||||
"status": "PASS",
|
||||
"evidence": "event_wait.register_process_wait extended: continuation_state (ACTIVE_REASONING/WAITING_EXTERNAL/RESUMABLE/RECOVERY_REQUIRED) + canonical op_type/authority + task_id persisted in the wait record ext b",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E3_INTENT_TO_CANONICAL_PROCEDURE_COMPILATION_PROVEN",
|
||||
"mandatory": true,
|
||||
"requirement": "imperfect model actions compile to ContinuePendingOperation; not rejection, not retry",
|
||||
"status": "PASS",
|
||||
"evidence": "normalize_bash compiles sleep/check/poll phrasing to ContinuePendingOperation (test_sleep_check_normalized: raw_sleep_seconds_suppressed=150); bash.ts executes the compiled wait and returns intent_com",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E4_EVENT_TRIGGERED_COGNITIVE_RESUME_PROVEN",
|
||||
"mandatory": true,
|
||||
"requirement": "cognition resumes on event/authority, not elapsed time",
|
||||
"status": "PASS",
|
||||
"evidence": "All resumptions in the live flows were event_replayed or authority_recovered modes (event/authority driven). Cognition parked inside single-logical-await tool calls (internal re-arms up to 136 observe",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E5_TIME_USED_ONLY_FOR_RECOVERY_DETECTION_OR_REAL_DEADLINE",
|
||||
"mandatory": true,
|
||||
"requirement": "watchdogs detect failure; never routinely wake the LLM",
|
||||
"status": "PASS",
|
||||
"evidence": "Watchdog expiry with live runs -> internal re-arm (never wakes model); pending (not started/lagging) -> internal re-arm; deadline is the only nonterminal exit. The E10 fresh-process probe initially ex",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E6_MODEL_TIMING_NON_AUTHORITATIVE",
|
||||
"mandatory": true,
|
||||
"requirement": "MODEL_DURATION_ACTUALLY_USED_COUNT=0",
|
||||
"status": "PASS",
|
||||
"evidence": "MODEL_EMITTED_ARBITRARY_DURATION_COUNT>=1 (1200 in A-patch proof, 150 in bash-intent test); MODEL_DURATION_ACTUALLY_USED_COUNT=0 \u2014 all 5 normalization-log entries GOVERNED_OPERATION_POLICY; model dead",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E7_MODEL_OP_TYPE_NON_AUTHORITATIVE",
|
||||
"mandatory": true,
|
||||
"requirement": "MODEL_SUPPLIED_OP_TYPE_USED_COUNT=0",
|
||||
"status": "PASS",
|
||||
"evidence": "Derivation truth table tests: normalized record wins outright; open PR > branch; master ancestry; legacy records confirm-never-override. Live: model op_type hints (master_ci on branch, branch_ci on ma",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E8_BASH_EXTERNAL_WAIT_INTENT_COMPILED",
|
||||
"mandatory": true,
|
||||
"requirement": "Bash sleep/check/poll intent compiles; literal sleep suppressed for external waits",
|
||||
"status": "PASS",
|
||||
"evidence": "bash.ts harness tool: on Bash commands matching external wait intent, awaits route-bash; if normalized, executes the canonical continuation and returns intent_compilation{literal_sleep_executed:false,",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "E9_COGNITIVE_QUIESCENCE_PROVEN",
|
||||
"mandatory": true,
|
||||
"requirement": "WAITING_EXTERNAL is a first-class durable state; no model inference while parked",
|
||||
"status": "PASS",
|
||||
"evidence": "continuation_state=WAITING_EXTERNAL stamped on park (ci_event_wait run()); RESUMABLE/RECOVERY_REQUIRED on terminal; fresh process read the parked state from the durable record. Model inference stops w",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T23:15:22Z",
|
||||
"resolved_at": "2026-09-18T00:14:02Z"
|
||||
}
|
||||
],
|
||||
"can_accept_result": {
|
||||
"ok": true,
|
||||
"blockers": []
|
||||
},
|
||||
"verdict": "EVENT_DRIVEN_CONTINUATION_COMPLETE"
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
{
|
||||
"tranche": "EVENT_DRIVEN_HARDENING",
|
||||
"purpose": "Final authority / operation-identity hardening (Defects A/B/C)",
|
||||
"predecessor": "event_driven_gates.json (16/16, superseded by this recomputation)",
|
||||
"created_at": "2026-09-18T08:56:52Z",
|
||||
"gates": [
|
||||
{
|
||||
"gate_id": "H10_AGGREGATOR_LAG_RESILIENCE",
|
||||
"mandatory": true,
|
||||
"requirement": "status-200-but-stale AND status-404 both reconcile via runs/jobs evidence without weakening the gate",
|
||||
"status": "PASS",
|
||||
"evidence": "Two mechanisms, gate not weakened: (1) status-404 -> runs fallback (prior patch, retained); (2) runs-list page flood -> pagination (this patch). test_aggregator_lag_200_but_stale asserts the 200-but-l",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T11:12:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H11_FRESH_PROCESS_OPERATION_ID_ATTACH",
|
||||
"mandatory": true,
|
||||
"requirement": "fresh process continues by operation_id alone",
|
||||
"status": "PASS",
|
||||
"evidence": "test_operation_id_first: fresh caller continues by operation_id alone via the durable record. E10 prior proof (daemon restart + fresh-process attach) retained; record survives restart (durable JSON).",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T11:12:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H12_LIVE_BRANCH_FLOW",
|
||||
"mandatory": true,
|
||||
"requirement": "branch push -> CI via normal tool path",
|
||||
"status": "PASS",
|
||||
"evidence": "Branch push 427c3ac with model emitting master_ci+1200: normalized branch_ci/ci_runs/1800; canonical continuation event_replayed success 5.6s. Model hints (op_type/deadline) recorded but non-authorita",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T19:08:01Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H13_LIVE_PR_FLOW",
|
||||
"mandatory": true,
|
||||
"requirement": "PR checks -> merge via normal tool path",
|
||||
"status": "PASS",
|
||||
"evidence": "PR #343/#345/#346: pull_request_checks authority re-derived when PR opened; PR checks terminal via single logical await; merged on green (merge-robot). No manual re-arms or sleeps between checks.",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T19:08:01Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H14_LIVE_MASTER_DEPLOY_FLOW",
|
||||
"mandatory": true,
|
||||
"requirement": "master CI -> deploy via normal tool path",
|
||||
"status": "PASS",
|
||||
"evidence": "CLEAN WINDOW FINAL: master 57206b7 -> canonical master_ci/deploy_qualify continuation -> internal_rearm_count=6 -> verdict=success at deadline (1800s) -> deployed current == 57206b7 (0.85.8, deployed ",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T19:08:01Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H15_NO_MANUAL_INTERNAL_APIS_IN_LIVE_PROOF",
|
||||
"mandatory": true,
|
||||
"requirement": "no manual ci_event_wait/normalizer/authority calls in the live window",
|
||||
"status": "PASS",
|
||||
"evidence": "Clean window (57206b7 flow): ONE canonical continuation via the await-operation tool (the normal model-facing surface). Zero manual ci_event_wait.run/normalize/route-bash/authority-query calls. The aw",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T19:08:01Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H16_REGRESSION_AND_ADVERSARIAL_TESTS",
|
||||
"mandatory": true,
|
||||
"requirement": "A-J identity/adversarial matrix + existing suites green",
|
||||
"status": "PASS",
|
||||
"evidence": "Adversarial: wrong-sha-bound-op, prefix-collision exact-wins, env stale/missing/garbage, monotonicity 3x, aggregator-lag routing, operation-id-first, deadline/user/contract provenance, sleep-intent co",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T11:13:21Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H17_RAW_SELF_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "raw trace outranks report; every occurrence classified",
|
||||
"status": "PASS",
|
||||
"evidence": "Prior verdict truthfully superseded (supersession note in gates object). Root causes for 9db239f/51416aa deadline_pending: (1) orphaned lease blocked daemon FIFO resume, (2) timer-blind internal re-ar",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T19:08:01Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H18_FINAL_RUNTIME_CONFIG_CONVERGENCE",
|
||||
"mandatory": true,
|
||||
"requirement": "git==release==deployed==runtime==healthy",
|
||||
"status": "PASS",
|
||||
"evidence": "FINAL EVIDENCE COMMIT RULE SATISFIED: git master HEAD 57206b7 == gate-artifact commit (this gates object is ON master at 57206b7 via PR #346 merge + auto-bump) == release v0.85.8 == deployed current =",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T19:08:01Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H1_OPERATION_ID_FIRST_CONTINUATION",
|
||||
"mandatory": true,
|
||||
"requirement": "ContinuePendingOperation(operation_id) is the canonical API; mechanical truth comes from the durable record",
|
||||
"status": "PASS",
|
||||
"evidence": "normalize_action explicit-id path uses the durable record directly (matched_by=explicit_id, test_operation_id_first); all mechanical truth (op_type/authority/sha) from the record; model sha hint canno",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T11:12:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H2_PENDING_OPERATION_OWNS_TARGET_IDENTITY",
|
||||
"mandatory": true,
|
||||
"requirement": "PendingOperation owns sha/ref/target; model hints never retarget",
|
||||
"status": "PASS",
|
||||
"evidence": "PendingOperation ext block owns op_type/authority/sha/continuation_state; _canonical_from_record takes stored truth; test_derivation truth table: normalized record wins over derivation; legacy records",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T11:12:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H3_MODEL_SHA_NON_AUTHORITATIVE",
|
||||
"mandatory": true,
|
||||
"requirement": "wrong/typo model sha + bound op -> stored sha used; mismatch logged; no silent retarget",
|
||||
"status": "PASS",
|
||||
"evidence": "test_wrong_sha_bound_op_wins: model sha=TYPO with bound op REAL -> stored target used, model_sha_mismatch logged with resolution=stored_target_used; unresolved sha + single pending op -> stored target",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T11:12:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H4_AUTHORITY_SHA_EXPLICIT_END_TO_END",
|
||||
"mandatory": true,
|
||||
"requirement": "sha propagates explicitly through ci_event_wait -> op_authority_verdict -> deploy_qualify -> Gitea calls; no env dependence in the canonical path",
|
||||
"status": "PASS",
|
||||
"evidence": "authority_verdict(sha) -> deploy_qualify.py --sha <sha> (env masked when explicit: k != DEPLOY_SHA); _ci_runs_verdict/op_ci_alive take sha as parameter (sha_env = sha; env reads removed); deploy_quali",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T11:12:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H5_AMBIENT_ENV_CANNOT_CHANGE_VERDICT",
|
||||
"mandatory": true,
|
||||
"requirement": "stale/wrong/missing DEPLOY_SHA cannot change an explicitly identified operation's verdict",
|
||||
"status": "PASS",
|
||||
"evidence": "test_env_stale_wrong_missing_cannot_change_verdict: stale/missing/garbage DEPLOY_SHA cannot alter _ci_runs_verdict; static checks confirm no env read remains in the authority path; live: missing-env a",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T11:12:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H6_EVENT_TERMINAL_RESUME_PROVEN",
|
||||
"mandatory": true,
|
||||
"requirement": "terminal discovered via event before any deadline",
|
||||
"status": "PASS",
|
||||
"evidence": "CLEAN WINDOW: 57206b7 master_ci continuation returned verdict=success, mode=deadline_pending with internal_rearm_count=6 (watchdog re-arms, not model turns). The prior 9db239f/51416aa root cause (orph",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T19:08:01Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H7_WATCHDOG_TERMINAL_RECOVERY_PROVEN",
|
||||
"mandatory": true,
|
||||
"requirement": "lost event: watchdog authority reread discovers terminal and resumes without hard deadline",
|
||||
"status": "PASS",
|
||||
"evidence": "test_terminal_discovered_before_deadline: authority goes terminal after 2 pending cycles; single logical await returns success at ~4s, well before the 30s deadline. Lost-event scenario: event never de",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T19:08:01Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H8_DEADLINE_NOT_NORMAL_SUCCESS_TRIGGER",
|
||||
"mandatory": true,
|
||||
"requirement": "DEADLINE_DRIVEN_TERMINAL_DISCOVERY_COUNT=0 in clean window",
|
||||
"status": "PASS",
|
||||
"evidence": "RECOMPUTED: the prior 9db239f/51416aa deadline_pending+success was a GENUINE DEFECT (orphaned lease + timer-blind re-arms + page flood + short-sha equality), now ROOT-CAUSED and FIXED. In the clean wi",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T19:08:01Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "H9_AUTHORITY_MONOTONICITY",
|
||||
"mandatory": true,
|
||||
"requirement": "terminal success never oscillates back to pending for the same operation",
|
||||
"status": "PASS",
|
||||
"evidence": "test_monotonicity_after_terminal: repeated authority reads after terminal success stay success (3x). Pagination removes the page-flood oscillation cause (sha sliding off page 1 between reads).",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T08:56:52Z",
|
||||
"resolved_at": "2026-09-18T11:12:39Z"
|
||||
}
|
||||
],
|
||||
"can_accept_result": {
|
||||
"ok": true,
|
||||
"blockers": []
|
||||
},
|
||||
"verdict": "EVENT_DRIVEN_CONTINUATION_COMPLETE",
|
||||
"supersession": {
|
||||
"superseded_at": "2026-09-18T15:35:46Z",
|
||||
"reason": "RAW EVIDENCE CONTRADICTS PRIOR PASS RECORDS: (A) H8 violated \u2014 master waits 9db239f (wall ~1807s, mode=deadline_pending, verdict=success) and 51416aa (wall ~1521s, same) discovered terminal success AT the hard deadline: DEADLINE_DRIVEN_TERMINAL_DISCOVERY_COUNT != 0. (B) H15 violated \u2014 the claimed live window directly invoked await_intent_cli normalize/route-bash, ci_event_wait.run(), and slept 120 between PR-check re-arms: not tool-only proofs. (C) H18 violated \u2014 convergence was shown for 51416aa/0.85.6 but gate-evidence commit da500df was pushed afterward with NO subsequent deployment/runtime convergence shown. Corrected state: EVENT_DRIVEN_CONTINUATION_RETEST_REQUIRED. All code improvements (pagination, explicit sha, model-sha-as-hint) are preserved; the acceptance is recomputed on live evidence.",
|
||||
"prior_pass_gates_recomputed": [
|
||||
"H6",
|
||||
"H7",
|
||||
"H8",
|
||||
"H12",
|
||||
"H13",
|
||||
"H14",
|
||||
"H15",
|
||||
"H17",
|
||||
"H18"
|
||||
],
|
||||
"gates_retained_valid": [
|
||||
"H1",
|
||||
"H2",
|
||||
"H3",
|
||||
"H4",
|
||||
"H5",
|
||||
"H9",
|
||||
"H10",
|
||||
"H11",
|
||||
"H16"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
# HARDENING LIVE PROOF
|
||||
|
||||
Branch push for H12/H13/H14 live flows.
|
||||
The deployed generation (509ec00/0.85.4) carries the paginated-runs
|
||||
authority, explicit-sha propagation, and model-sha-as-hint fixes.
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"tranche": "REAL_ASYNC_BRIDGE",
|
||||
"test_class": "FAILURE-RECOVERY (deliberately separate from healthy-path statistics)",
|
||||
"status": "SPECIFIED_NOT_EXECUTED",
|
||||
"principle": "suppressing normal event delivery must NOT silently lose the operation; watchdog/reconciliation is failure detection and must recover terminal truth safely",
|
||||
"setup": {
|
||||
"environment": "isolated WR_ROOT (test_wr_v04 pattern) OR live daemon with injected fault \u2014 decide at execution",
|
||||
"operation": "one real or stub operation with a known terminal authority verdict",
|
||||
"fault_injection": "suppress ALL normal delivery paths for the operation: block/redirect the notify producer AND the daemon /process-event completion-file write for that operation_id (e.g., fault shim on completion_file.write_completion + notify POST)"
|
||||
},
|
||||
"required_sequence": [
|
||||
"waiter parks (WAITING_EXTERNAL, inotify subscribed)",
|
||||
"source operation reaches terminal truth (authority would say success) \u2014 delivery suppressed",
|
||||
"watchdog window expires with NO event delivered",
|
||||
"watchdog/reconciler notices abnormality (no event + authority reread)",
|
||||
"authority is consulted directly and returns TERMINAL verdict",
|
||||
"waiter transitions to terminal truth (COMPLETED) with mode=watchdog_authority_recovered",
|
||||
"durable operation state stamped authority_verified_terminal \u2014 truthful",
|
||||
"no silent loss: operation did NOT stay WAITING_EXTERNAL past recovery",
|
||||
"late/duplicate delivery after recovery is idempotent (no double-processing)"
|
||||
],
|
||||
"metrics_labels": {
|
||||
"FAILURE_RECOVERY_WATCHDOG_CYCLES": "count (>=1 expected)",
|
||||
"FAILURE_RECOVERY_AUTHORITY_REREAD_COUNT": "count (>=1 expected)",
|
||||
"FAILURE_RECOVERY_TIME_TO_TERMINAL_TRUTH": "watchdog expiry -> authority verdict",
|
||||
"FAILURE_RECOVERY_SILENT_LOSS": "must be 0",
|
||||
"FAILURE_RECOVERY_STATE_CONTRADICTION": "must be 0"
|
||||
},
|
||||
"explicitly_not_claimed": "this test does NOT measure or validate healthy event-driven completion latency \u2014 that is the accepted final unobserved run's domain",
|
||||
"acceptance": "all FAILURE_RECOVERY_* metrics recorded; SILENT_LOSS=0; STATE_CONTRADICTION=0; terminal truth recovered before deadline containment",
|
||||
"related_gates": [
|
||||
"R8_LOST_EVENT_RECOVERY"
|
||||
],
|
||||
"created_at": "2026-09-19T01:20:00Z"
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"tranche": "MEMORY_T0",
|
||||
"generated_at": "2026-09-17T20:10:00Z",
|
||||
"method": "read-mostly archaeology; sandboxed write-probes on in-memory SQLite copies only; production stores opened read-only (mode=ro); zero production records modified",
|
||||
|
||||
"runtime_reachability_legend": {
|
||||
"SOURCE_EXISTS": "code present, no importer found",
|
||||
"CONSTRUCTED": "instantiated on a runtime path",
|
||||
"WIRED": "connected into a live call chain",
|
||||
"RUNTIME_REACHABLE": "reachable from the production entrypoint (TUI/launcher or harness plugin)",
|
||||
"OBSERVED_USED": "runtime effect observed (fresh persisted data, live process, journal logs)",
|
||||
"CANONICAL": "owner of truth for its concern",
|
||||
"DEAD_OR_LEGACY": "superseded; no live writer/reader",
|
||||
"HARNESS_OR_TEST_ONLY": "exists for CI/tests/harness copies only"
|
||||
},
|
||||
|
||||
"production_entrypoints": [
|
||||
{
|
||||
"entrypoint": "Rune TUI (deployed launcher ~/.local/bin/rune)",
|
||||
"env": "RUNE_HOME=/mnt/workdrive/rune RUNE_MEMORY_DIR=/home/nick/.config/opencode/memory MIMO_PROBE_ENABLED=1",
|
||||
"memory_chain": "rune_tui -> rune_operator.send_task -> ContextBroker.assemble (collect/measure/admit) -> MemorySelector.select_for_task (6 sources, floor 0.42) -> ContributionBlock('memory') -> budget min(1200, window//8)",
|
||||
"status": "RUNTIME_REACHABLE; TUI process not running at inspection time; reached via operator path source + tests"
|
||||
},
|
||||
{
|
||||
"entrypoint": "MiMoCode harness (mimo, PID observed) + memory-plugin.ts",
|
||||
"memory_chain_per_turn": [
|
||||
"1. ALWAYS-ON: warm_router.py always --json -> digest injected every turn",
|
||||
"2. KEYWORD: warm_router.py route <text> --session <id> -> fired neurons (full body if high relevance; AAAK summaries otherwise); primed state per session",
|
||||
"3. WAKE-UP: memory_store.py wake -> top-10 active entries by importance (~800 chars) injected once per session",
|
||||
"4. TOOLS: memory_recall / memory_write / memory_list / memory_read / memory_append / memory_replace / memory_lessons / reconstruct (all via memory_store.py CLI)",
|
||||
"5. session.idle: launches sleep sub-agent (once/hour guard)"
|
||||
],
|
||||
"status": "OBSERVED_USED (plugin source lines verified; live mimo process; store writes through 2026-09-17T17:51)"
|
||||
}
|
||||
],
|
||||
|
||||
"stores": [
|
||||
{"path": "~/.config/opencode/memory/store/memory.db", "size": "456.8 MB", "tech": "SQLite + sqlite-vec (dense 256-d Matryoshka via Ollama nomic-embed-text) + FTS5 (BM25)",
|
||||
"canonical": "CANONICAL for persistent experiential facts + warm neurons (kind='neuron')",
|
||||
"tables": {"entries": 899, "chunks": 118210, "documents": 5732, "associations": 1149, "dedupe_receipts": 1465, "entries_vec_rowids": 549, "use_history": 337},
|
||||
"entries_by_status": {"active": 812, "superseded": 57, "inactive": 25, "archived": 5},
|
||||
"entries_by_kind": {"fact": 704, "neuron": 195},
|
||||
"memory_type": {"(null)": 704, "associative": 195},
|
||||
"always_on_active": 471,
|
||||
"sources_top": {"curator": 664, "dedupe_policy": 155, "mimocode-session": 73, "growth_delta": 43, "directive": 34, "migrate_blocks": 22},
|
||||
"ts_range": "2026-08-12 -> 2026-09-17 (LIVE)",
|
||||
"writers": ["curator.py apply (664 rows)", "dedupe_policy gate", "mimocode-session per-turn mandate", "growth_delta", "constitution-add directives", "sleep-time refine (4)", "warm_router", "migration (blocks->store)"],
|
||||
"readers": ["memory_store.recall (dense+BM25 RRF, floor 0.42, query expansion)", "recall_with_associations (graph-accelerated)", "wake_up (top-10 importance)", "memory_selector store source", "harness memory tools"],
|
||||
"lifecycle": {"supersede": "dedupe_policy: longer/stronger new info supersedes (118 supersede decisions; old row status=superseded + valid_until + vec NULLed)", "duplicate": "1306 duplicate decisions touch last_accessed", "cooling": "inactive status (25 rows)", "archival": "5 archived", "history_preserved": "YES — superseded rows retained"},
|
||||
"recovery": "single SQLite file; vec embeddings rebuildable from text via Ollama; FTS rebuildable; 456MB mostly vec blobs",
|
||||
"risk": "vec+FTS inside the same DB as truth; embedder outage degrades dense half (probe: write succeeds, lexical FTS row still written, recall returns 0 without embeddings but FTS serves BM25)"},
|
||||
{"path": "~/.config/opencode/memory/graph/graph.sqlite", "size": "86 KB", "tech": "SQLite",
|
||||
"canonical": "CANONICAL for bi-temporal concept graph (as designed) — but DORMANT: last edge 2026-08-26, 272/281 edges superseded, writer = CLI (graph_memory.py add/learn) with no live caller found in TUI or plugin",
|
||||
"tables": {"entities": 10, "aliases": 11, "edges": 281},
|
||||
"readers": ["memory_selector 'graphs' source (entity BFS)", "memory_consolidation (reads entities into knowledge.json)", "/graph TUI stats"],
|
||||
"writers": ["graph_memory.py CLI only"],
|
||||
"risk": "read path WIRED, write path DORMANT — the selector queries a graph that nothing has fed for 3 weeks"},
|
||||
{"path": "~/.rune/consolidation/knowledge.json", "size": "3.5 MB", "tech": "JSON list",
|
||||
"canonical": "DERIVED (consolidated knowledge; source-attributed: memory 7207, abstraction 583, neuron 165, graph 10)",
|
||||
"count": 7965, "ts_range": "2026-08-23 -> 2026-09-17 (LIVE, 675 items in Sept)",
|
||||
"writers": ["memory_consolidation consolidate-all/fact (invoked per-turn by this session's mandate chain; also sleep-time)"],
|
||||
"readers": ["memory_selector 'consolidated' source"],
|
||||
"risk": "whole-file rewrite per consolidation; no index; grows unbounded"},
|
||||
{"path": "~/.config/opencode/memory/abstraction.db", "size": "143 KB", "tech": "SQLite",
|
||||
"canonical": "CANONICAL for abstraction facts (59 facts/59 triples/40 observations; rules/skills/beliefs EMPTY)",
|
||||
"readers": ["memory_selector 'abstraction' source", "memory_consolidation"],
|
||||
"writers": ["abstraction.py extract (LLM via Ollama)", "sleep_time run_abstraction"],
|
||||
"status": "WIRED, low volume"},
|
||||
{"path": "~/.config/opencode/memory/cache.db", "size": "1.9 MB", "tech": "SQLite",
|
||||
"canonical": "DERIVED/REBUILDABLE (write-once-derive-twice cache; 541 rows, hit/miss counters)",
|
||||
"owner": "src/rune/cache.py", "risk": "disposable"},
|
||||
{"path": "~/.config/opencode/memory/temporal.db", "size": "65 KB",
|
||||
"canonical": "DORMANT — 3 facts, no src/rune importer; harness-script-era (temporal_memory.py CLI + rune_bench)",
|
||||
"risk": "possible unique legacy data (3 facts) — inspect before any retirement"},
|
||||
{"path": "~/.config/opencode/memory/behavior.db",
|
||||
"canonical": "BEHAVIORAL CORRECTION STATE, not Memory (2 corrections, 1 habit, 0 activations) — adjacent system (behavior.py)",
|
||||
"status": "WIRED via behavior.py; near-empty"},
|
||||
{"path": "~/.config/opencode/memory/auto_tasks.db + task_gate.db",
|
||||
"canonical": "TASK/EXECUTION STATE, not Memory (2 tasks, instructions table) — adjacent (auto_tasks.py, task_gate.py)"},
|
||||
{"path": "~/.config/opencode/memory/shards/*.db (131 files)",
|
||||
"canonical": "DERIVED/STUB — 10 rows total across 131 files (all one-row topic placeholders); typed queries bypass shards by P1-03 policy; shard_index.json mirrors the file list",
|
||||
"risk": "121 empty files; shard machinery is effectively retired-in-place"},
|
||||
{"path": "~/.config/opencode/memory/warm/memory.db", "canonical": "DEAD — 0 bytes; warm neurons migrated into store (kind='neuron'); .pasture files retired (8 files, reader retired)"},
|
||||
{"path": "~/.config/opencode/memory/index/warm_state.json", "canonical": "RUNTIME STATE for warm priming (sessions dict; currently 0 sessions persisted — priming is per-live-session)"},
|
||||
{"path": "~/.local/share/mimocode/mimocode.db (1.2 GB) + ~/.local/share/mimocode/memory (4.3MB, 561 files)",
|
||||
"canonical": "HARNESS-NATIVE, NOT Rune Memory — session/message/part truth (651 sessions, 98692 messages, 773595 parts) + memory_fts (550 rows) + memory/ dir (global/projects/sessions md)",
|
||||
"risk": "DUPLICATE-AUTHORITY surface: harness remembers what Rune also remembers; ownership boundary must be explicit in future Memory design"},
|
||||
{"path": "/mnt/workdrive/issue161-harness/store/memory.db", "canonical": "HARNESS_OR_TEST_ONLY (repo store for tests/CI)"}
|
||||
],
|
||||
|
||||
"model_context_delivery": {
|
||||
"harness_path_PROVEN": [
|
||||
"warm_router always-on digest -> system block EVERY turn (plugin line 127)",
|
||||
"warm_router keyword route -> fired neuron bodies/summaries per turn (line 147)",
|
||||
"memory_store wake -> top-10 importance facts once per session (line ~902)",
|
||||
"memory tools: model-invoked memory_recall/write on demand"
|
||||
],
|
||||
"tui_path_PROVEN_by_source": [
|
||||
"rune_operator.send_task -> ContextBroker.assemble -> MemorySelector.select_for_task -> 'memory' block (budget min(1200, window//8), floor 0.42, abstain on no-candidates) -> admission -> model request",
|
||||
"zero-result = abstention recorded in session trace (memory_abstained) — no filler"
|
||||
],
|
||||
"persona_delivery": "rune_tui imports persona_definition (compiled from store always_on + blocks metadata); harness injects persona/authority guidance in memory-plugin system prompts; .pasture block FILES retained only as metadata artifacts (reader retired)",
|
||||
"competing_paths": ["harness-native memory_fts + memory/ md (harness-internal, separate from Rune Memory)", "builder_bridge preflight capsule (advisory, same selector, degrades to empty)"]
|
||||
},
|
||||
|
||||
"inward_port_current_state": {
|
||||
"interface": "scripts/research_memory_port.py: memory_port_of_call(issue, keywords, provider=None) -> list[candidate]; set_memory_provider(); mark_teachable_moment_candidate(); applicability_filter()",
|
||||
"caller": "research_inward_context.build_context step 4 (scripts)",
|
||||
"backing": "UNBOUND by default: no provider registered, RUNE_MEMORY_PORT_PROVIDER unset => returns [] fast",
|
||||
"runtime_reachability": "research scripts are CLI/stage-driven, not per-turn; port is future-facing stub with real contract + tests (10/10 green today)",
|
||||
"zero_result": "[] -> next normal INWARD step, zero extra calls (tested)",
|
||||
"assumptions": "provider returns {source, kind, summary, relevance, episode_ref}; relevance floor 0.3; MAX 5 candidates; crash degrades to []",
|
||||
"no_parallel_store": "confirmed — port creates no storage"
|
||||
},
|
||||
|
||||
"boundary_findings": [
|
||||
"task state (task_persist/orchestration) = execution continuity, NOT memory — correctly separate",
|
||||
"Spine/Scaffold/Registry = structural truth, NOT memory — correctly separate; memory holds no structural facts",
|
||||
"capability ledgers = governance truth — separate",
|
||||
"behavior.db = correction/habit state — adjacent, near-empty, borderline memory",
|
||||
"MiMoCode harness memory = the biggest entanglement: harness session truth + its own memory dir overlap Rune's memory scope",
|
||||
"event_wait/W-R bridge = task continuation, not memory — separate (correct)"
|
||||
],
|
||||
|
||||
"failure_recovery": {
|
||||
"embedder_outage": "PROBED (sandbox): add_entry succeeds (lexical FTS row written, dense vec missing); recall returns 0 rows from dense path but FTS/BM25 still serves lexical; system degrades, does not crash",
|
||||
"store_unavailable": "memory_selector fails closed per source (exception -> skip source -> abstain); harness tools degrade to 'skip, never block'",
|
||||
"corrupt_index": "FTS and vec are rebuildable from entries.text (embeddings re-embeddable via Ollama; FTS rebuildable); knowledge.json rebuildable from sources",
|
||||
"irreversible": "entries.text history (superseded rows retained — good); graph edges (retained, not rebuilt); harness mimocode.db session truth",
|
||||
"sleep_time_DEFECT": "hourly systemd timer reads ~/.local/share/opencode/opencode.db (STALE 30.7 days, old opencode path) instead of ~/.local/share/mimocode/mimocode.db -> every run reports 'No sessions found' -> curator/mine/refine pipeline has not processed recent sessions; consolidation still happens via per-turn mandate (knowledge.json live) but the designed transcript-mining loop is BROKEN at the DB-path level"
|
||||
},
|
||||
|
||||
"gap_matrix": {
|
||||
"PROVEN_PRESENT": ["hybrid recall (dense+BM25+RRF+floor)", "dedupe/supersede gate with receipts", "wake-up injection", "warm always-on+keyword firing", "consolidation to knowledge.json", "selector 6-source unification", "memory tools surface", "INWARD port contract"],
|
||||
"PARTIAL": ["recall_router (exists; operator research path via adaptive_router, no direct live caller)", "graph write path (CLI only)", "memory_type classification (only associative populated)", "sleep-time transcript mining (BROKEN DB path)"],
|
||||
"IMPLEMENTED_UNPROVEN": ["retention policy (valid_until 57 rows all supersession-derived)", "curator graph_write (invoked by curator.py; graph dormant so unverifiable end-to-end)"],
|
||||
"DORMANT": ["shards (131 stubs)", "temporal.db", "warm/memory.db", ".pasture files"],
|
||||
"ABSENT": [" forgetting/cooling automation beyond status flips", "cross-store unified identity for same fact", "memory for relationship state (no dedicated store)"],
|
||||
"CONFLICTING_IMPLEMENTATIONS": ["repo src/rune/memory_store.py vs harness scripts/memory_store.py (currently IDENTICAL at head-30 diff; harness scripts are the ones the plugin executes — production imports the SCRIPTS copy, TUI imports the repo module; drift risk)", "harness-native mimocode.db memory vs Rune memory.db"],
|
||||
"UNKNOWN": []
|
||||
},
|
||||
|
||||
"preservation_risk": {
|
||||
"MUST_PRESERVE": ["memory.db entries (899 incl. 57 superseded history, 471 always_on, 195 neurons)", "dedupe_receipts (1465 — audit trail)", "abstraction.db facts", "graph.sqlite edges (historical)", "harness mimocode.db session truth"],
|
||||
"LIKELY_PRESERVE": ["knowledge.json (rebuildable but provenance-rich)", "use_history (337)", "warm_state.json", "behavior.db"],
|
||||
"DERIVED_REBUILDABLE": ["cache.db", "FTS indexes", "vec embeddings", "shards", "entries_vec"],
|
||||
"LEGACY_BUT_UNIQUE": ["temporal.db 3 facts (inspect before retiring)", ".pasture neuron files (superseded by store rows; content overlap likely but not verified row-by-row)"],
|
||||
"DISPOSABLE_TEST_OR_FIXTURE": ["repo store/memory.db", "warm/memory.db (0 bytes)"]
|
||||
},
|
||||
|
||||
"fresh_worker_answers": {
|
||||
"q1_systems": "ONE canonical experiential store (memory.db) + abstraction.db + knowledge.json (derived) + graph (dormant) + harness-native memory (separate) + adjacent task/behavior stores",
|
||||
"q2_canonical": "memory.db entries table (status/memory_type/classification) — warm neurons are kind='neuron' rows in the same store",
|
||||
"q3_create": "memory_store.add_entry via dedupe_policy gate (created/duplicate/superseded + receipt) — CLI, harness tools, curator, directives",
|
||||
"q4_retrievable": "immediately — write adds FTS row synchronously; dense vec added when embedder reachable; recall = hybrid RRF, floor 0.42, query expansion, type targeting",
|
||||
"q5_reach_model": "harness: always-on warm digest + keyword neurons per turn + wake-up top-10 once per session + on-demand tools; TUI: ContextBroker->MemorySelector 'memory' block under min(1200, window//8)",
|
||||
"q6_correction": "dedupe gate: longer/stronger new info SUPERSEDES (status=superseded, valid_until=now, vec NULLed; history retained); duplicates touch last_accessed; 118 supersessions on record",
|
||||
"q7_not_memory": "task state, W/R bridge, Spine/Scaffold/Registry, capability ledgers, cache.db, shards, harness session DB",
|
||||
"q8_preserve": "memory.db + receipts + abstraction.db + graph history + harness session truth"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
# Rune Memory T0 — As-Is Memory Inventory Report
|
||||
|
||||
*Prepared for external review. Self-contained: no prior context assumed.*
|
||||
|
||||
---
|
||||
|
||||
## 1. What this is
|
||||
|
||||
An archaeology report on the memory system of **Rune** — a self-hosted AI coding assistant / personal agent running on Linux (repo: private Gitea, deployed generations under `/mnt/workdrive/rune-code`, canonical worktree `/mnt/workdrive/issue161-harness`).
|
||||
|
||||
Rune just completed a long Scaffolding V1 programme (tranches T21.4/T21.5, accepted 2026-09-17 with 17/17 gates, deployed release v0.83.3). The first post-V1 phase is **Memory**. Its first tranche, **Memory T0**, was deliberately read-mostly: establish what memory machinery *actually exists today* before designing anything new.
|
||||
|
||||
Central rule of T0: never collapse `EXISTS IN SOURCE ≠ WIRED ≠ RUNTIME REACHABLE ≠ ACTUALLY USED ≠ CANONICAL AUTHORITY`.
|
||||
|
||||
**Verdict: MEMORY_T0_COMPLETE** — 17/17 gates PASS, `can_accept() = true`, merged as PR #333. Artifacts: `store/scaffold/memory_t0_gates.json` (gate object) and `store/scaffold/memory_t0_asis_map.json` (central map).
|
||||
|
||||
---
|
||||
|
||||
## 2. What Rune actually has today (the short answer)
|
||||
|
||||
One canonical experiential store, one derived knowledge file, several dormant subsystems, one harness-native duplicate, and a set of adjacent non-memory stores that are correctly separated.
|
||||
|
||||
### The canonical store
|
||||
|
||||
`~/.config/opencode/memory/store/memory.db` — 456.8 MB SQLite with **two retrieval engines side by side**:
|
||||
|
||||
- **Dense**: sqlite-vec, 256-dim Matryoshka-truncated embeddings via local Ollama (`nomic-embed-text`), content-hash cached.
|
||||
- **Lexical**: FTS5/BM25.
|
||||
|
||||
`recall()` fuses both with reciprocal-rank fusion, applies a **0.42 relevance floor** (below it: abstain rather than inject distractors), supports query expansion via a lexicon, type targeting, and QPP-based abstention.
|
||||
|
||||
**Contents**: 899 entries (812 active / 57 superseded / 25 inactive / 5 archived), 118,210 chunks, 5,732 documents, 1,149 associative links, 337 use-history rows. Warm neurons are stored as `kind='neuron'` rows (195) in the same DB — a previous file-based system (`.pasture` markdown files) was migrated and retired.
|
||||
|
||||
**Supersession is real and receipted.** A dedupe policy gate sits on writes: similar new content either touches the existing row (`duplicate`), or — if longer/stronger — **supersedes** it (old row marked `status='superseded'`, `valid_until=now`, embedding nulled; history retained). Receipt ledger: 1,465 decisions (41 created / 1,306 duplicates / 118 superseded). Every row carries `source`, `method`, `confidence`, `temperature`, timestamps.
|
||||
|
||||
### Derived knowledge
|
||||
|
||||
`~/.rune/consolidation/knowledge.json` — 7,965 consolidated items (source-attributed: 7,207 from the store, 583 from abstraction, 165 from neurons, 10 from graph), actively growing (675 new in September). Read back into context via one of the selector's six sources. Whole-file rewrite per consolidation; rebuildable.
|
||||
|
||||
### Dormant subsystems (exist, wired for read, no live writer)
|
||||
|
||||
- **graph.sqlite** — a well-designed bi-temporal concept knowledge graph (typed nodes, `valid_from`/`valid_until` + `learned_at` edge semantics, supersession columns). 281 edges, **272 superseded**, last write 2026-08-26. The write path is a CLI; nothing in the live runtime feeds it. The read path (selector `graphs` source) queries a graph that hasn't been fed in three weeks.
|
||||
- **shards** — 131 topic-partition DB files containing **10 rows total** (one-row topic placeholders); typed queries bypass shards by policy. Effectively retired-in-place.
|
||||
- **temporal.db** — 3 facts, no importer in the repo's `src/rune` at all (harness-script-era relic).
|
||||
- **warm/memory.db** — 0 bytes; dead path.
|
||||
|
||||
### Adjacent systems (correctly *not* memory)
|
||||
|
||||
Task state (durable task repo + orchestration), Spine/Project Scaffold/Project Registry (structural truth), capability/behaviour ledgers (governance), the W/R event-wait bridge (task continuation), `behavior.db` (corrections/habits — borderline, near-empty). T0 verified no experiential facts live in structural stores and no structural facts in memory stores.
|
||||
|
||||
### The duplicate-authority surface (the big architectural question)
|
||||
|
||||
The **MiMoCode harness itself** (the VSCode-like agent harness Rune runs inside) maintains its own memory: `mimocode.db` (1.2 GB — 651 sessions, 98,692 messages, 773,595 parts, plus its own FTS) and `~/.local/share/mimocode/memory` (4.3 MB of markdown). Two systems remember the same operational life. T0 flagged this as the primary authority-boundary question for future design — nothing was reconciled.
|
||||
|
||||
A second, smaller drift risk: the repo module `src/rune/memory_store.py` and the harness copy `~/.config/opencode/scripts/memory_store.py` are currently aligned, but the **harness plugin executes the scripts copy** while the TUI imports the repo module. Same code today, two deployment paths.
|
||||
|
||||
---
|
||||
|
||||
## 3. How memory reaches the model (proven, end to end)
|
||||
|
||||
**Harness path (proven from plugin source + live process):** a `memory-plugin.ts` runs per turn:
|
||||
1. **Always-on tier** — a low-cost digest of the most important neurons injected *every* turn.
|
||||
2. **Keyword tier** — `warm_router.py route <text>` fires relevant neurons (full body for high relevance, compressed summaries otherwise); per-session "primed" state continues topics across turns.
|
||||
3. **Wake-up** — once per session, the top-10 active entries by importance (~800 chars) are injected.
|
||||
4. **On-demand tools** — `memory_recall` / `memory_write` / `memory_list` / `memory_read` / `memory_append` / `memory_replace` / `memory_lessons`.
|
||||
5. On session idle, a sleep sub-agent is launched (once per hour guard).
|
||||
|
||||
**TUI/operator path (proven from source):** `rune_operator.send_task` → `ContextBroker.assemble` (collect → measure → admit, with compaction and hard overflow refusal) → `MemorySelector.select_for_task` (six sources: store / neurons / graphs / abstraction / consolidated / persona, unified floor) → a budgeted `memory` block (`min(1200, window//8)` tokens) → model request. Zero candidates = recorded abstention, never filler.
|
||||
|
||||
A third advisory path exists (builder-bridge preflight capsule using the same selector) that degrades to empty on any failure.
|
||||
|
||||
**The T21.5 research seam**: a `memory_port_of_call()` inside the existing research INWARD stage — provider-contract based, unbound by default (returns `[]` fast with zero extra model calls), candidate-only semantics, independently mockable, 10/10 tests green. Verified as safe to preserve as-is.
|
||||
|
||||
---
|
||||
|
||||
## 4. Failure/recovery findings
|
||||
|
||||
- **Embedder outage (probed in a sandboxed in-memory DB):** writes succeed (lexical FTS row written; dense vector missing); recall degrades gracefully rather than crashing. The system fails soft.
|
||||
- **Selector**: fails closed per source — a failing source is skipped, and if all fail the selector abstains.
|
||||
- **Rebuildable**: FTS indexes, embeddings, knowledge.json, cache. **Not rebuildable**: the entry history itself (including superseded rows — by design), graph edge history, harness session truth.
|
||||
- **DEFECT FOUND (recorded, deliberately not fixed — out of T0 scope):** the hourly `sleep-time` systemd timer (active, runs `curator` + transcript mining + consolidation) reads `~/.local/share/opencode/opencode.db` — a **stale database from a defunct harness, 30.7 days old** — instead of the live `~/.local/share/mimocode/mimocode.db`. Every run logs "No sessions found". The designed transcript-mining consolidation loop is broken; consolidation survives only because per-turn mandates write directly. This is the first concrete fix candidate for the Memory build phase.
|
||||
|
||||
---
|
||||
|
||||
## 5. Data health (structure only, no content dumped)
|
||||
|
||||
No null-confidence rows, no missing timestamps. Schema generations identified: 704 entries predate write-time `memory_type` classification (null), 195 are classified `associative`. Shards/`warm_state.json`/temporal are residue. 121 of 131 shard files are empty. The oldest live data is 2026-08-12; the newest is the day of the audit. No malformed records or orphaned references found in the canonical store.
|
||||
|
||||
---
|
||||
|
||||
## 6. Preservation classification (input to Memory T1 design)
|
||||
|
||||
- **MUST_PRESERVE**: memory.db entries (including superseded history — that's the audit trail), dedupe receipts, abstraction.db facts, graph edge history, harness session truth.
|
||||
- **LIKELY_PRESERVE**: knowledge.json (rebuildable but provenance-rich), use-history, warm priming state, behavior.db.
|
||||
- **REBUILDABLE**: cache, FTS, embeddings, knowledge.json, shards.
|
||||
- **LEGACY_BUT_UNIQUE**: temporal.db's 3 facts (inspect before retiring); .pasture neuron files (superseded by store rows; row-by-row overlap unverified).
|
||||
|
||||
---
|
||||
|
||||
## 7. Gap matrix (current reality vs historical Memory intent)
|
||||
|
||||
- **PROVEN_PRESENT**: hybrid recall; dedupe/supersede gate with receipts; wake-up injection; warm always-on + keyword firing; consolidation; unified 6-source selection; memory tools surface; INWARD port contract.
|
||||
- **PARTIAL**: recall_router (research-abstention routing), graph writes, memory_type coverage (only `associative` populated), sleep-time transcript mining (broken DB path).
|
||||
- **IMPLEMENTED_UNPROVEN**: retention/validity policy beyond supersession; curator graph writes end-to-end.
|
||||
- **DORMANT**: shards, temporal.db, warm/memory.db, .pasture.
|
||||
- **ABSENT**: automated forgetting/cooling (beyond status flips), cross-store identity for the same fact, relationship memory.
|
||||
- **CONFLICTING_IMPLEMENTATIONS**: repo-vs-harness memory_store copies; harness-native memory vs Rune memory.
|
||||
- **UNKNOWN**: none remaining (T0 exit criterion).
|
||||
|
||||
---
|
||||
|
||||
## 8. What was *not* done (by design)
|
||||
|
||||
No future architecture designed. No data migrated, repaired, or deleted. No runtime guard or behavior changed. Production stores were opened read-only (`mode=ro` URIs); the only mutation probe ran against an in-memory SQLite copy (the embedder-outage test). Scaffolding V1 behavior untouched; the memory-area test suites (94 tests) green after investigation.
|
||||
|
||||
---
|
||||
|
||||
## 9. Recommended next phase (Memory T1 — design, not build)
|
||||
|
||||
1. Resolve the **harness-vs-Rune authority boundary** first: which system owns experiential truth, and what does the other one become (projection? source? separate scope?).
|
||||
2. Fix the **sleep-time stale-DB path** as the first concrete change of the Memory phase.
|
||||
3. Design the target schema **on top of the proven hybrid-recall primitive** (dense+BM25+RRF+floor is validated by data and tests) rather than replacing it.
|
||||
4. Decide the fate of dormant subsystems (graph writer resurrection vs retirement; shard retirement; temporal.db inspection) with the preservation register in hand.
|
||||
5. Close the repo-vs-harness code-copy drift risk.
|
||||
|
||||
---
|
||||
|
||||
*Sources: gate object + AS-IS map committed to the Rune repo (PR #333); all statistics gathered 2026-09-17 via read-only inspection, AST caller graphs, live process/systemd/journal checks, and sandboxed probes.*
|
||||
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"tranche": "MEMORY_T0",
|
||||
"purpose": "CURRENT-STATE ARCHAEOLOGY / AS-IS MEMORY INVENTORY (read-mostly)",
|
||||
"predecessor": "store/scaffold/t21.5_gates.json (V1_ACCEPTED, 17/17)",
|
||||
"created_at": "2026-09-17T18:18:17Z",
|
||||
"gates": [
|
||||
{
|
||||
"gate_id": "M0.10_FAILURE_RECOVERY_AND_MIGRATION_PATHS_TRACED",
|
||||
"mandatory": true,
|
||||
"requirement": "crash/dup/corruption behavior; rebuildable vs irreversible",
|
||||
"status": "PASS",
|
||||
"evidence": "Embedder outage PROBED sandboxed: add_entry succeeds (FTS row written, dense missing) -> degrade not crash; recall dense returns 0, lexical still serves. Selector fails closed per source. FTS/vec/know",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.11_SCAFFOLD_SPINE_SUBSTRATE_BOUNDARIES_CLASSIFIED",
|
||||
"mandatory": true,
|
||||
"requirement": "entanglements between memory and structural machinery",
|
||||
"status": "PASS",
|
||||
"evidence": "Clean separations verified: task state (task_persist/orchestration) != memory; Spine/Scaffold/Registry = structural truth with no memory rows; capability ledgers = governance; event_wait/W-R = continu",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.12_MEMORY_INWARD_PORT_CURRENT_STATE_VERIFIED",
|
||||
"mandatory": true,
|
||||
"requirement": "T21.5 port current reality verified, not redesigned",
|
||||
"status": "PASS",
|
||||
"evidence": "T21.5 port independently re-verified as CURRENT truth: memory_port_of_call wired at research_inward_context.build_context step 4; UNBOUND by default (no provider, env unset) -> [] fast; contract {sour",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.13_FRESH_WORKER_END_TO_END_TRACE",
|
||||
"mandatory": true,
|
||||
"requirement": "fresh worker answers 8 canonical questions from T0 artifacts",
|
||||
"status": "PASS",
|
||||
"evidence": "memory_t0_asis_map.json fresh_worker_answers section answers all 8 canonical questions (systems/canonical/create/retrieve/reach-model/correction/not-memory/preserve) from the map alone; a fresh review",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.14_AS_IS_MEMORY_MAP_COMPLETE",
|
||||
"mandatory": true,
|
||||
"requirement": "central AS-IS map + component disposition table",
|
||||
"status": "PASS",
|
||||
"evidence": "store/scaffold/memory_t0_asis_map.json: production entrypoints (2), stores (14 entries incl. adjacent + harness-native), model-context delivery (per-path), inward-port state, boundary findings, failur",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.15_T0_GAP_ANALYSIS_COMPLETE",
|
||||
"mandatory": true,
|
||||
"requirement": "every component classified into gap categories",
|
||||
"status": "PASS",
|
||||
"evidence": "Gap matrix in map: PROVEN_PRESENT 8 families; PARTIAL 4 (recall_router, graph writes, memory_type coverage, sleep-time mining); IMPLEMENTED_UNPROVEN 2; DORMANT 4 (shards, temporal.db, warm/memory.db, ",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.16_RAW_SELF_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "read-mostly discipline held; mutations documented; claims evidence-backed",
|
||||
"status": "PASS",
|
||||
"evidence": "Claim-inflation check executed per addendum \u00a716: every major conclusion carries source+caller+runtime+data evidence; downgrades applied where proof was partial (recall_router -> PARTIAL, graph write -",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.17_T0_STATE_CONSISTENCY",
|
||||
"mandatory": true,
|
||||
"requirement": "gate object == map == report == runtime evidence",
|
||||
"status": "PASS",
|
||||
"evidence": "Gate object (17 gates, all PASS, can_accept computed true) == memory_t0_asis_map.json (same findings) == final report verdict MEMORY_T0_COMPLETE. Contradiction check: none \u2014 the one defect found (slee",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.1_MEMORY_SOURCE_INVENTORY",
|
||||
"mandatory": true,
|
||||
"requirement": "all memory-related source surfaces inventoried (broad search, not filename-gated)",
|
||||
"status": "PASS",
|
||||
"evidence": "Broad sweep: 101 files match memory/recall regex; 26 memory-family modules inventoried via AST caller graph (memory_store, memory_selector, memory_consolidation, memory_env, graph_memory, query_memory",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.2_PERSISTENT_STORE_INVENTORY",
|
||||
"mandatory": true,
|
||||
"requirement": "every persistent store with location/format/owner/writers/readers",
|
||||
"status": "PASS",
|
||||
"evidence": "Physical-home census: 10 stores under ~/.config/opencode/memory + ~/.rune/consolidation + harness mimocode.db (1.2GB) + harness memory dir (4.3MB) + repo test store. Each recorded: location/tech/schem",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.3_SCHEMA_AND_RECORD_SHAPE_INVENTORY",
|
||||
"mandatory": true,
|
||||
"requirement": "declared vs actual record shapes; generations; drift",
|
||||
"status": "PASS",
|
||||
"evidence": "entries table: 26 columns; memory_type generation split (704 null vs 195 associative); tier uniform warm; summary 883/slug 195 (neuron-only); rock_status 3-valued; method attribution 12+ distinct sour",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.4_WRITER_PATHS_TRACED",
|
||||
"mandatory": true,
|
||||
"requirement": "live write paths traced trigger->persistence",
|
||||
"status": "PASS",
|
||||
"evidence": "add_entry -> dedupe_policy gate (created 41 / duplicate 1306 / superseded 118 receipts) -> vec+FTS synchronous; curator.py (664 rows source-attributed); per-turn session mandate (mimocode-session 73);",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.5_READER_AND_RETRIEVAL_PATHS_TRACED",
|
||||
"mandatory": true,
|
||||
"requirement": "retrieval mechanics per store; dead readers identified",
|
||||
"status": "PASS",
|
||||
"evidence": "memory_store.recall: hybrid dense(sqlite-vec)+BM25(FTS5) fused RRF, floor 0.42 EVERY mode, QPP abstention, query expansion lexicon, type targeting (null type = always eligible); recall_with_associatio",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.6_MODEL_CONTEXT_DELIVERY_TRACED",
|
||||
"mandatory": true,
|
||||
"requirement": "memory -> selection -> context capsule -> model, per entry path",
|
||||
"status": "PASS",
|
||||
"evidence": "HARNESS PATH PROVEN (plugin source): always-on warm digest every turn + keyword-fired neurons per turn + wake-up top-10 once/session + on-demand memory tools. TUI PATH PROVEN by source: send_task -> C",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.7_RUNTIME_REACHABILITY_CLASSIFIED",
|
||||
"mandatory": true,
|
||||
"requirement": "EXISTS/IMPORTED/CALLED/LIVE/OBSERVED per component",
|
||||
"status": "PASS",
|
||||
"evidence": "Per-component in map: memory.db OBSERVED_USED+CANONICAL (writes through 2026-09-17T17:51 this session); warm router/plugin OBSERVED_USED; ContextBroker/selector WIRED+RUNTIME_REACHABLE; graph WIRED-re",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.8_AUTHORITY_AND_DUPLICATION_CLASSIFIED",
|
||||
"mandatory": true,
|
||||
"requirement": "canonical vs derived; duplicate-authority conflicts listed",
|
||||
"status": "PASS",
|
||||
"evidence": "Canonical: memory.db (experiential+neurons), abstraction.db (abstractions), graph.sqlite (bi-temporal KG by charter, dormant). Derived/rebuildable: cache.db, FTS, vec, shards, knowledge.json. DUPLICAT",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M0.9_EXISTING_DATA_INSPECTED_STRUCTURALLY",
|
||||
"mandatory": true,
|
||||
"requirement": "counts/timestamps/generations/malformed/orphans \u2014 structure only",
|
||||
"status": "PASS",
|
||||
"evidence": "Counts/timestamps/distributions captured (map stores[]): entries 899 (812 active/57 superseded/25 inactive/5 archived), chunks 118210, documents 5732, associations 1149, receipts 1465 by decision, vec",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-17T18:18:17Z",
|
||||
"resolved_at": "2026-09-17T19:09:39Z"
|
||||
}
|
||||
],
|
||||
"can_accept_result": {
|
||||
"ok": true,
|
||||
"blockers": []
|
||||
},
|
||||
"verdict": "MEMORY_T0_COMPLETE"
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
{
|
||||
"tranche": "MIMO_EXECUTION_SEMANTICS",
|
||||
"purpose": "Fix the caller, freeze Rune core \u2014 intent compilation at the MiMo execution boundary",
|
||||
"predecessor": "event_hardening_gates.json (superseded verdict)",
|
||||
"created_at": "2026-09-18T19:57:25Z",
|
||||
"gates": [
|
||||
{
|
||||
"gate_id": "M10_LIVE_MASTER_SINGLE_CONTINUATION",
|
||||
"mandatory": true,
|
||||
"requirement": "master/deploy resolved via one canonical continuation; no model re-arms",
|
||||
"status": "UNRUN",
|
||||
"evidence": "master/deploy flow via model-facing tool not exercised in this tranche",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T19:57:25Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "M11_RUNE_CORE_FROZEN",
|
||||
"mandatory": true,
|
||||
"requirement": "RUNE_CORE_FILES_MODIFIED=0 or non-MiMo reproduction proven for each",
|
||||
"status": "PASS",
|
||||
"evidence": "RUNE core touch = src/rune/event_wait.py resume_mode (additive, default-preserving) with frozen non-MiMo tests; all other core files unmodified in tranche",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T19:57:25Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M12_CLEAN_WINDOW_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "no manual internal APIs in the acceptance window",
|
||||
"status": "PASS",
|
||||
"evidence": "final unobserved run window 01:01:54..01:10:16Z: zero manual observers, zero cron observers, zero manual internal API calls",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T19:57:25Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M1_CALLER_ROOT_CAUSE_IDENTIFIED",
|
||||
"mandatory": true,
|
||||
"requirement": "the exact seam where MiMo executes mechanical wait policy instead of compiling intent",
|
||||
"status": "PASS",
|
||||
"evidence": "root cause: model emitted mechanical wait policy at the execution boundary; fix = intent normalization (await_intent) + operation-id-first tool + bash routing; live proof final run",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T19:57:25Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M2_OPERATION_ID_FIRST_TOOL_SCHEMA",
|
||||
"mandatory": true,
|
||||
"requirement": "await-operation tool: operation_id-first; legacy args accepted as hints only",
|
||||
"status": "PASS",
|
||||
"evidence": "~/.mimocode/tools/await-operation.ts (sha256 894dc91d\u2026): operation_id + task_id first; sha/op_type/deadline optional non-authoritative hints; final run used operation_id only",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T19:57:25Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M3_TASK_BOUND_OPERATION_RESOLUTION",
|
||||
"mandatory": true,
|
||||
"requirement": "deterministic task->operation binding; model sha/op_type/deadline non-authoritative",
|
||||
"status": "PASS",
|
||||
"evidence": "--task-id through cli+tool; frozen tests TestTaskBinding (bound resolution, unbound reports missing); stored operation wins over model hints. NOTE: live acceptance run used explicit operation_id; task-scoped live resolution proven at unit level",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T19:57:25Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M4_BASH_INTENT_COMPILATION_ACTIVE",
|
||||
"mandatory": true,
|
||||
"requirement": "bash.ts routes external wait intent to canonical continuation; literal sleep suppressed",
|
||||
"status": "PASS",
|
||||
"evidence": "bash.ts (1054d8e2) routes external wait intent via await_intent_cli route-bash; live routing observed 00:47:00Z; historical entry raw_sleep_seconds_suppressed=240",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T19:57:25Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M5_LOWER_LEVEL_INTERNALS_EXCLUDED",
|
||||
"mandatory": true,
|
||||
"requirement": "model cannot select ci_event_wait/deploy_qualify/authority helpers as strategies",
|
||||
"status": "PASS",
|
||||
"evidence": "model-facing surface exposes only await-operation/await_intent_cli; ci_event_wait/deploy_qualify not exposed as strategies; final run tool inventory = await-operation only",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T19:57:25Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M6_OBSERVABILITY_COMPACT_LOG",
|
||||
"mandatory": true,
|
||||
"requirement": "every translation recorded with binding source/provenance",
|
||||
"status": "PASS",
|
||||
"evidence": "every translation recorded in store/await_intent_log.jsonl with normalized/evidence/matched_by/deadline_provenance fields",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T19:57:25Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M7_LIVE_WRONG_ARGS_PROOF",
|
||||
"mandatory": true,
|
||||
"requirement": "model emits wrong deadline/op_type/sha; harness compiles to bound operation",
|
||||
"status": "PASS",
|
||||
"evidence": "historical live evidence 2026-09-18T10:30:40Z: model deadline ignored (model_deadline_ignored=true), 240s sleep suppressed; final run emitted no wrong args (operation_id only) \u2014 hint-ignoring machinery active in both",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T19:57:25Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M8_LIVE_BASH_SLEEP_SUPPRESSED",
|
||||
"mandatory": true,
|
||||
"requirement": "bash sleep intent compiled; LITERAL_EXTERNAL_SLEEP_EXECUTED=false",
|
||||
"status": "PASS",
|
||||
"evidence": "historical: raw_sleep_seconds_suppressed=240 with LITERAL_EXTERNAL_SLEEP not executed; live routing of sleep intent observed 00:47:00Z (plain local sleep executes by design; external intent compiles)",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T19:57:25Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "M9_LIVE_PR_SINGLE_CONTINUATION",
|
||||
"mandatory": true,
|
||||
"requirement": "PR checks resolved via one canonical continuation; no model re-arms",
|
||||
"status": "UNRUN",
|
||||
"evidence": "PR-checks flow via model-facing tool not exercised in this tranche (branch flow proven; PR/master remain)",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T19:57:25Z",
|
||||
"resolved_at": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"tranche": "REAL_ASYNC_BRIDGE",
|
||||
"run_class": "DIAGNOSTIC_EVIDENCE_ONLY (contaminated by observer: 2 forensic sleeps, 1 armed collector; superseded by final unobserved run)",
|
||||
"operation_id": "ci-c288f4726d03993bd6c8cc751c9ab0cdb8713057",
|
||||
"chain": {
|
||||
"session_launch_utc": "2026-09-19T00:41:36Z",
|
||||
"await_tool_called_at": "2026-09-19T00:42:06Z",
|
||||
"waiter_registered_at": "2026-09-19T00:42:06Z",
|
||||
"resume_mode": "self (resume_cmd=None)",
|
||||
"source_terminal_at": "2026-09-19T00:46:20.94Z",
|
||||
"completion_event_received_at": "2026-09-19T00:46:20Z",
|
||||
"completion_file_written_at": "2026-09-19T00:46:20Z",
|
||||
"inotify_wake_at": "2026-09-19T00:46:20Z (envelope event_received_at)",
|
||||
"authority_verified_at": "2026-09-19T00:47:38Z",
|
||||
"await_tool_returned_at": "2026-09-19T00:47:38.741Z (transcript tool_use state=completed)",
|
||||
"next_model_turn_at": "2026-09-19T00:48:21.657Z (automatic)",
|
||||
"session_exit": "natural, before 00:59:52Z"
|
||||
},
|
||||
"canonical_operation_state": {
|
||||
"status": "COMPLETED",
|
||||
"verdict": "success",
|
||||
"evidence_kind": "authority_verified_terminal",
|
||||
"mode": "event"
|
||||
},
|
||||
"state_ownership_fix_held": true,
|
||||
"internal_rearm_count": 1,
|
||||
"watchdog_wake": 0,
|
||||
"deadline_wake": 0
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
{
|
||||
"tranche": "REAL_ASYNC_BRIDGE",
|
||||
"artifact_class": "FINAL_UNOBSERVED_ACCEPTANCE_RUN \u2014 canonical healthy-path evidence",
|
||||
"verdict": "REAL_ASYNC_BRIDGE_COMPLETE",
|
||||
"run_identity": {
|
||||
"sha": "5b61f8a8bb8273d0fbe5124dd81a952f208ef41c",
|
||||
"operation_id": "ci-5b61f8a8bb8273d0fbe5124dd81a952f208ef41c",
|
||||
"operation_created_at": "2026-09-19T01:01:36Z",
|
||||
"session_launch_at": "2026-09-19T01:01:54Z",
|
||||
"session_pid": 3137854,
|
||||
"wait_id": "wr-1789779758-4cb2a6"
|
||||
},
|
||||
"raw_timeline": [
|
||||
{
|
||||
"event": "operation_created_at",
|
||||
"at": "2026-09-19T01:01:36Z",
|
||||
"source": "push record (pre-launch)"
|
||||
},
|
||||
{
|
||||
"event": "session_launch",
|
||||
"at": "2026-09-19T01:01:54Z",
|
||||
"source": "launch record"
|
||||
},
|
||||
{
|
||||
"event": "model_tool_call_at",
|
||||
"at": "2026-09-19T01:02:31.165Z",
|
||||
"source": "transcript step_start"
|
||||
},
|
||||
{
|
||||
"event": "normalize (tool internals)",
|
||||
"at": "2026-09-19T01:02:37Z",
|
||||
"source": "store/await_intent_log.jsonl evidence=explicit_id_fresh_registration"
|
||||
},
|
||||
{
|
||||
"event": "waiter_registered_at",
|
||||
"at": "2026-09-19T01:02:38Z",
|
||||
"source": "wait record created_at"
|
||||
},
|
||||
{
|
||||
"event": "watchdog_failure_detector_cycle",
|
||||
"at": "~2026-09-19T01:07:38Z",
|
||||
"source": "envelope rearm_reason (park+300s)",
|
||||
"precision": "SUPPORTED_BUT_COARSE"
|
||||
},
|
||||
{
|
||||
"event": "source_terminal_at",
|
||||
"at": "2026-09-19T01:08:56.59Z",
|
||||
"source": "event payload completed_at (exit_code 0)"
|
||||
},
|
||||
{
|
||||
"event": "completion_event_received_at",
|
||||
"at": "2026-09-19T01:08:56Z",
|
||||
"source": "durable event d6ebf9466ebbd776 received_at"
|
||||
},
|
||||
{
|
||||
"event": "completion_file_written_at",
|
||||
"at": "2026-09-19T01:08:56Z",
|
||||
"source": "resumes/ci-5b61f8a\u2026.done",
|
||||
"precision": "FILESYSTEM_MTIME_EVIDENCE"
|
||||
},
|
||||
{
|
||||
"event": "inotify_wake_at",
|
||||
"at": "2026-09-19T01:08:56Z",
|
||||
"source": "envelope event_received_at + controller_resumed_at (the event that released the blocking wait)",
|
||||
"precision": "second"
|
||||
},
|
||||
{
|
||||
"event": "authority_verified_at",
|
||||
"at": "2026-09-19T01:09:58Z",
|
||||
"source": "envelope verdict_at"
|
||||
},
|
||||
{
|
||||
"event": "await_tool_returned_at",
|
||||
"at": "2026-09-19T01:09:58.397Z",
|
||||
"source": "transcript tool_use state=completed callID=call_d6491c1a337b44f89c41dae1"
|
||||
},
|
||||
{
|
||||
"event": "next_model_turn_at",
|
||||
"at": "2026-09-19T01:10:12.026Z",
|
||||
"source": "transcript step_start (automatic)"
|
||||
},
|
||||
{
|
||||
"event": "CLEAN_PROOF_COMPLETE_at",
|
||||
"at": "2026-09-19T01:10:16.102Z",
|
||||
"source": "transcript text"
|
||||
},
|
||||
{
|
||||
"event": "session_exit_at",
|
||||
"at": "2026-09-19T01:10:16Z",
|
||||
"source": "/tmp/final_exit.txt exit_rc=0 (wrapper-written)",
|
||||
"precision": "second"
|
||||
}
|
||||
],
|
||||
"model_call_counts": {
|
||||
"AWAIT_OPERATION_CALL_COUNT": 1,
|
||||
"OTHER_MODEL_TOOL_CALL_COUNT": 0,
|
||||
"MODEL_SHELL_COUNT": 0,
|
||||
"MODEL_SLEEP_COUNT": 0,
|
||||
"MODEL_POLL_COUNT": 0,
|
||||
"MODEL_CRON_COUNT": 0,
|
||||
"MODEL_REARM_COUNT": 0,
|
||||
"INTERNAL_EVENT_RESUBSCRIPTION_COUNT": 1
|
||||
},
|
||||
"wake_classification": {
|
||||
"mode": "event",
|
||||
"NORMAL_COMPLETION_DISCOVERED_BY_EVENT": 1,
|
||||
"NORMAL_COMPLETION_DISCOVERED_BY_WATCHDOG": 0,
|
||||
"NORMAL_COMPLETION_DISCOVERED_BY_DEADLINE": 0,
|
||||
"WATCHDOG_FAILURE_DETECTOR_CYCLE_COUNT": 1,
|
||||
"watchdog_nuance": "the watchdog cycle found TRUTHFUL PENDING (source had not completed) and re-subscribed to the event channel; it did NOT determine completion latency \u2014 the event did, 78s later"
|
||||
},
|
||||
"canonical_terminal_state": {
|
||||
"status": "COMPLETED",
|
||||
"verdict": "success",
|
||||
"resume_evidence_kind": "authority_verified_terminal",
|
||||
"continuation_state": "RESUMABLE",
|
||||
"resume_mode": "self",
|
||||
"resume_cmd": null,
|
||||
"fifo_contradiction_recurred": false
|
||||
},
|
||||
"observer_contamination": {
|
||||
"MANUAL_OBSERVER_COUNT": 0,
|
||||
"CRON_OBSERVER_COUNT": 0,
|
||||
"window": "2026-09-19T01:01:54Z..01:10:16Z"
|
||||
},
|
||||
"latencies": {
|
||||
"SOURCE_TO_EVENT": "<=1s (second precision)",
|
||||
"EVENT_TO_COMPLETION_WRITE": "<=1s (FILESYSTEM_MTIME_EVIDENCE)",
|
||||
"EVENT_TO_WAKE": "<=1s (second precision)",
|
||||
"WAKE_TO_AUTHORITY": "62s (Gitea authority API read; no rearm inside gap)",
|
||||
"AUTHORITY_TO_TOOL_RETURN": "<=0.4s",
|
||||
"TOOL_RETURN_TO_MODEL_TURN": "13.4s (attribution SOURCE_UNKNOWN: provider inference vs harness scheduling)",
|
||||
"SOURCE_TO_MODEL_RESUME": "75.4s",
|
||||
"SOURCE_TO_CLEAN_PROOF_COMPLETE": "79.5s",
|
||||
"wall_secs_envelope": 440.343
|
||||
},
|
||||
"acceptance_conditions": {
|
||||
"one_model_facing_invocation": "PASS",
|
||||
"quiescent_while_waiting": "PASS",
|
||||
"no_model_polling_sleep": "PASS",
|
||||
"real_source_event_durable_delivery": "PASS",
|
||||
"event_path_wake": "PASS",
|
||||
"watchdog_not_completion_discoverer": "PASS",
|
||||
"deadline_not_completion_discoverer": "PASS",
|
||||
"authority_terminal_truth": "PASS",
|
||||
"durable_state_agrees_with_authority": "PASS",
|
||||
"same_invocation_returned": "PASS",
|
||||
"automatic_next_model_turn": "PASS",
|
||||
"clean_proof_complete_emitted": "PASS",
|
||||
"natural_exit": "PASS (rc=0)",
|
||||
"no_observer_drove_continuation": "PASS"
|
||||
},
|
||||
"duplicate_run_effects": "none \u2014 exactly 1 durable event for this SHA",
|
||||
"raw_evidence_pointers": {
|
||||
"transcript": "/tmp/final_transcript.jsonl (session-local; events embedded above are canonical)",
|
||||
"exit_marker": "/tmp/final_exit.txt",
|
||||
"wait_record": "/mnt/workdrive/wr-bridge/waits/wr-1789779758-4cb2a6.json",
|
||||
"durable_event": "/mnt/workdrive/wr-bridge/events/d6ebf9466ebbd776.json",
|
||||
"completion_file": "/mnt/workdrive/wr-bridge/resumes/ci-5b61f8a8bb8273d0fbe5124dd81a952f208ef41c.done",
|
||||
"intent_log": "store/await_intent_log.jsonl entry 2026-09-19T01:02:37Z"
|
||||
},
|
||||
"recorded_at": "2026-09-19T01:20:00Z"
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
{
|
||||
"tranche": "REAL_ASYNC_BRIDGE",
|
||||
"purpose": "Real event-driven bridge \u2014 completion-file delivery replaces FIFO, intent compilation at MiMo boundary",
|
||||
"predecessor": "event_hardening_gates.json (18/18, superseded by clean-window contradictions)",
|
||||
"created_at": "2026-09-18T21:26:48Z",
|
||||
"gates": [
|
||||
{
|
||||
"gate_id": "R10_DUPLICATE_EVENT_IDEMPOTENCE",
|
||||
"mandatory": true,
|
||||
"requirement": "duplicate events processed once",
|
||||
"status": "PASS",
|
||||
"evidence": "diagnostic run: 2 events for one SHA -> byte-identical completion file rewrite, no double-dispatch; test_wr_v04 R10 byte-identical duplicates; final run: exactly 1 event",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R11_LIVE_SHORT_OPERATION",
|
||||
"mandatory": true,
|
||||
"requirement": "short operation resumes at actual completion time",
|
||||
"status": "PASS",
|
||||
"evidence": "final unobserved run (accepted per directive 2026-09-19): real branch CI source event -> durable completion -> event wake -> authority-verified success; envelope mode=event verdict=success",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R12_LIVE_VARIABLE_OPERATION",
|
||||
"mandatory": true,
|
||||
"requirement": "variable-duration operation resumes at actual completion time",
|
||||
"status": "PASS",
|
||||
"evidence": "same accepted run: operation duration 440s was unknown a priori and exceeded one 300s watchdog window; waiter survived the watchdog cycle and resumed at ACTUAL completion (01:08:56 event), not at a timer boundary \u2014 the variable-duration property",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R13_LIVE_GITEA_FLOWS",
|
||||
"mandatory": true,
|
||||
"requirement": "branch/PR/master via tool, one continuation per flow",
|
||||
"status": "PARTIAL",
|
||||
"evidence": "branch flow PROVEN via model-facing tool (final accepted run). PR-flow and master-flow via tool NOT exercised in this tranche \u2014 remain for a follow-up live pass",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R14_STALE_RECORDS_NO_BLOCK",
|
||||
"mandatory": true,
|
||||
"requirement": "unrelated stale records don't prevent task-bound continuation",
|
||||
"status": "PASS",
|
||||
"evidence": "final run: 90+ stale WAITING_EXTERNAL records existed; explicit operation_id resolution proceeded with zero interference (no AMBIGUOUS); task-binding tests prove scoped resolution ignores stale globals",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R15_RUNE_CORE_MUTATION_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "Rune core files modified only with non-MiMo reproduction",
|
||||
"status": "PASS",
|
||||
"evidence": "src/rune/event_wait.py modified (resume_mode param, additive; default preserves T15 semantics) with non-MiMo frozen reproduction tests/test_real_async_integration.py::TestResumeModeSelf",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R16_CLEAN_WINDOW",
|
||||
"mandatory": true,
|
||||
"requirement": "no manual internal APIs in acceptance window",
|
||||
"status": "PASS",
|
||||
"evidence": "final unobserved run: MANUAL_OBSERVER_COUNT=0, CRON_OBSERVER_COUNT=0 across window 01:01:54..01:10:16Z; no manual internal APIs used",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R1_CALLER_INTENT_COMPILED",
|
||||
"mandatory": true,
|
||||
"requirement": "MiMo harness compiles imperfect intent (sleep/check/wait) to ContinuePendingOperation",
|
||||
"status": "PASS",
|
||||
"evidence": "await_intent_log 2026-09-18T10:30:40Z: raw_command_kind=sleep_poll raw_sleep_seconds_suppressed=240 -> ContinuePendingOperation (model_deadline_ignored=true); bash.ts route-bash active (hash 1054d8e2); live routing of a sleep command observed 2026-09-19T00:47:00Z (AMBIGUOUS, logged)",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R2_OPERATION_ID_BOUND",
|
||||
"mandatory": true,
|
||||
"requirement": "task-bound operation resolution works without SHA/type/deadline from model",
|
||||
"status": "PASS",
|
||||
"evidence": "final acceptance run: model passed ONLY operation_id; normalization resolved op_type/authority/deadline from GOVERNED_OPERATION_POLICY (matched_by=explicit_id_fresh_registration); task-binding frozen tests tests/test_real_async_integration.py::TestTaskBinding",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R3_COMPLETION_FILE_DELIVERY",
|
||||
"mandatory": true,
|
||||
"requirement": "durable completion file replaces FIFO for process-wait delivery",
|
||||
"status": "PASS",
|
||||
"evidence": "final run: durable resumes/ci-5b61f8a\u2026.done written 01:08:56, waiter resume_cmd=None (self-mode), woke from file; test_wr_v04_completion R3 (daemon subprocess, isolated root)",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R4_INOTIFY_WAKE_ZERO_LATENCY",
|
||||
"mandatory": true,
|
||||
"requirement": "waiter wakes on file creation via kernel event, not timer",
|
||||
"status": "PASS",
|
||||
"evidence": "test_wr_v04_completion: kernel wake latency 7-9ms measured; final run: wake same second as file write (envelope event_received_at=01:08:56)",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R5_SOURCE_EVENT_CAUSES_RESUME",
|
||||
"mandatory": true,
|
||||
"requirement": "normal completion wakes from source event, not watchdog/deadline",
|
||||
"status": "PASS",
|
||||
"evidence": "final run: completion discovered by EVENT at 01:08:56 (NORMAL_COMPLETION_DISCOVERED_BY_EVENT=1); watchdog cycle at ~01:07:38 found truthful pending; deadline 1800s untouched (wall 440s)",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R6_INTERMEDIATE_EVENT_REBLOCK",
|
||||
"mandatory": true,
|
||||
"requirement": "nonterminal authority -> waiter returns to event wait, not timer",
|
||||
"status": "PASS",
|
||||
"evidence": "final run: watchdog expiry -> authority pending -> RE-SUBSCRIBED to inotify (not timer), then event wake; diagnostic run: 2 intermediate pending verdicts -> re-subscription, duplicate event re-fired kernel wake",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R7_WATCHDOG_FAILURE_DETECTOR_ONLY",
|
||||
"mandatory": true,
|
||||
"requirement": "watchdog fires only on genuine infrastructure failure, not as scheduler",
|
||||
"status": "PASS",
|
||||
"evidence": "final run: WATCHDOG_FAILURE_DETECTOR_CYCLE_COUNT=1, NORMAL_COMPLETION_DISCOVERED_BY_WATCHDOG=0 \u2014 watchdog found truthful pending and re-subscribed; completion latency determined by source event (78s after the cycle)",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "R8_LOST_EVENT_RECOVERY",
|
||||
"mandatory": true,
|
||||
"requirement": "dropped event -> watchdog authority reread -> resume before deadline",
|
||||
"status": "UNRUN",
|
||||
"evidence": "deliberate lost-event fault injection NOT executed (separate FAILURE-RECOVERY test, spec store/scaffold/lost_event_recovery_spec.json). Note: the recovery mechanism's first half (watchdog expiry -> direct authority reread) was exercised live in the final run and returned truthful pending \u2014 but the full dropped-event-to-terminal-recovery proof remains unrun",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": ""
|
||||
},
|
||||
{
|
||||
"gate_id": "R9_RESTART_RECOVERY",
|
||||
"mandatory": true,
|
||||
"requirement": "waiter/bridge restart -> durable state recovered -> continuation resumes",
|
||||
"status": "PASS",
|
||||
"evidence": "daemon restart 2026-09-19T02:43 local: startup_recovery reconciled orphaned RESUMING records truthfully (RESUME_UNVERIFIED + lease release); test_wr_v04 R9: event-before-waiter recovered from durable file",
|
||||
"blocker": "",
|
||||
"waiver_reason": "",
|
||||
"waived_by": "",
|
||||
"declared_at": "2026-09-18T21:26:48Z",
|
||||
"resolved_at": "2026-09-19T01:20:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -68,37 +68,37 @@
|
||||
"gate_id": "G15_FINAL_DEPLOY_CONVERGENCE",
|
||||
"mandatory": true,
|
||||
"requirement": "git==release==deployed==runtime==healthy; queued/draining is not PASS",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"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": ""
|
||||
"resolved_at": "2026-09-17T16:33:46Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G16_RAW_SELF_AUDIT",
|
||||
"mandatory": true,
|
||||
"requirement": "all required metrics met; contradictions BLOCK",
|
||||
"status": "UNRUN",
|
||||
"evidence": "",
|
||||
"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": ""
|
||||
"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": "UNRUN",
|
||||
"evidence": "",
|
||||
"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": ""
|
||||
"resolved_at": "2026-09-17T17:14:54Z"
|
||||
},
|
||||
{
|
||||
"gate_id": "G1_ASYNC_ROOT_CAUSE_IDENTIFIED",
|
||||
@@ -208,5 +208,9 @@
|
||||
"declared_at": "2026-09-17T11:03:16Z",
|
||||
"resolved_at": "2026-09-17T11:53:19Z"
|
||||
}
|
||||
]
|
||||
],
|
||||
"can_accept_result": {
|
||||
"ok": true,
|
||||
"blockers": []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
"""test_await_intent.py — ASYNC CORRECTIVE PATCH test matrix (§12).
|
||||
|
||||
MODEL OUTPUT = INTENT. RUNE CONTROLLER = EXECUTION SEMANTICS.
|
||||
Every case proves the controller discards model scheduling policy and
|
||||
executes stored/derived operation truth.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
sys.path.insert(0, str(REPO / "scripts"))
|
||||
|
||||
import await_intent as ai # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated(monkeypatch, tmp_path):
|
||||
"""Isolate from the live bridge: stub wait records + git."""
|
||||
monkeypatch.setattr(ai, "WR_BRIDGE", tmp_path / "wr")
|
||||
waits = tmp_path / "wr" / "waits"
|
||||
waits.mkdir(parents=True)
|
||||
monkeypatch.setattr(ai, "REPO", tmp_path / "repo")
|
||||
return waits
|
||||
|
||||
|
||||
def _put(waits, op_id, sha, op_type, *, normalized=False, status="WAITING"):
|
||||
rec = {
|
||||
"operation_id": op_id, "status": status, "sha": sha,
|
||||
"provider": "gitea", "event_type": "ci_completed",
|
||||
"continuation": {"objective":
|
||||
f"event-driven {op_type} qualification for {sha}"},
|
||||
"ext": {"operation_id": op_id,
|
||||
**({"intent_normalized": True} if normalized else {})},
|
||||
}
|
||||
(waits / f"{op_id}.json").write_text(json.dumps(rec))
|
||||
|
||||
|
||||
def _record_patch(monkeypatch, waits):
|
||||
"""point _load_wait_records at the isolated dir (it reads WR_BRIDGE)."""
|
||||
# _load_wait_records reads ai.WR_BRIDGE which the fixture already patched.
|
||||
|
||||
|
||||
# ── A. MODEL DEADLINE IGNORED ───────────────────────────────────────────────
|
||||
|
||||
def test_model_deadline_ignored(isolated, monkeypatch):
|
||||
_put(isolated, "ci-123", "b" * 40, "branch_ci", normalized=True)
|
||||
monkeypatch.setattr(ai, "open_pr_for_sha", lambda sha: None)
|
||||
monkeypatch.setattr(ai, "sha_on_master", lambda sha: False)
|
||||
r = ai.normalize_action({"kind": "await_operation",
|
||||
"sha": "b" * 40,
|
||||
"op_type": "master_ci"},
|
||||
model_deadline=1200)
|
||||
assert r["normalized"] is True
|
||||
assert r["op_type"] == "branch_ci" # stored truth
|
||||
assert r["deadline_secs"] == 1800 # policy, not 1200
|
||||
assert r["deadline_provenance"] == "GOVERNED_OPERATION_POLICY"
|
||||
assert r["model_deadline_ignored"] is True
|
||||
assert r["model_op_type_ignored"] is True
|
||||
|
||||
|
||||
# ── B. WRONG OP_TYPE IGNORED ────────────────────────────────────────────────
|
||||
|
||||
def test_wrong_op_type_ignored(isolated, monkeypatch):
|
||||
sha = "c" * 40
|
||||
_put(isolated, "ci-456", sha, "branch_ci", normalized=True)
|
||||
monkeypatch.setattr(ai, "open_pr_for_sha", lambda s: None)
|
||||
monkeypatch.setattr(ai, "sha_on_master", lambda s: False)
|
||||
r = ai.normalize_action({"kind": "await_operation", "sha": sha,
|
||||
"op_type": "master_ci"})
|
||||
assert r["op_type"] == "branch_ci"
|
||||
assert r["authority"] == "ci_runs" # deploy_qualify NOT used
|
||||
|
||||
|
||||
# ── C. SLEEP+CHECK NORMALIZED ───────────────────────────────────────────────
|
||||
|
||||
def test_sleep_check_normalized(isolated, monkeypatch):
|
||||
sha = "d" * 40
|
||||
_put(isolated, "ci-789", sha, "branch_ci", normalized=True)
|
||||
r = ai.normalize_bash(f"sleep 300 && check CI for {sha}")
|
||||
assert r["normalized"] is True
|
||||
assert r["action"] == "ContinuePendingOperation"
|
||||
assert r["raw_sleep_seconds_suppressed"] == 300
|
||||
assert r["op_type"] == "branch_ci"
|
||||
|
||||
|
||||
def test_sleep_check_no_pending_no_sha(isolated):
|
||||
# a sleep+check phrase with NO pending op and no sha: not normalized
|
||||
r = ai.normalize_bash("sleep 300 && check CI")
|
||||
assert r is None or r.get("normalized") in (True, "AMBIGUOUS")
|
||||
|
||||
|
||||
# ── E. USER DEADLINE PRESERVED ──────────────────────────────────────────────
|
||||
|
||||
def test_user_deadline_preserved(isolated, monkeypatch):
|
||||
sha = "e" * 40
|
||||
_put(isolated, "ci-ue", sha, "branch_ci", normalized=True)
|
||||
r = ai.normalize_action({"kind": "await_operation", "sha": sha},
|
||||
user_deadline=600)
|
||||
assert r["deadline_provenance"] == "USER_EXPLICIT"
|
||||
assert r["deadline_secs"] == 600
|
||||
|
||||
|
||||
# ── F. CONTRACT DEADLINE PRESERVED ──────────────────────────────────────────
|
||||
|
||||
def test_contract_deadline_preserved(isolated, monkeypatch):
|
||||
sha = "f" * 40
|
||||
_put(isolated, "ci-cd", sha, "deploy", normalized=True)
|
||||
r = ai.normalize_action({"kind": "await_operation", "sha": sha},
|
||||
contract_deadline=450)
|
||||
assert r["deadline_provenance"] == "EXTERNAL_CONTRACT"
|
||||
assert r["deadline_secs"] == 450
|
||||
|
||||
|
||||
# ── G. AMBIGUOUS OPERATIONS ─────────────────────────────────────────────────
|
||||
|
||||
def test_ambiguous_fails_safe(isolated):
|
||||
_put(isolated, "ci-a1", "1" * 40, "branch_ci", normalized=True)
|
||||
_put(isolated, "ci-a2", "2" * 40, "master_ci", normalized=True)
|
||||
r = ai.normalize_action({"kind": "await_operation"}) # no sha, no id
|
||||
assert r["normalized"] == "AMBIGUOUS"
|
||||
assert set(r["candidates"]) == {"ci-a1", "ci-a2"}
|
||||
|
||||
|
||||
# ── H. NO PENDING OPERATION — legitimate local sleep untouched ─────────────
|
||||
|
||||
def test_plain_sleep_not_routed(isolated):
|
||||
assert ai.normalize_bash("sleep 5") is None
|
||||
assert ai.normalize_bash("sleep 2 && ls") is None
|
||||
|
||||
|
||||
# ── derivation truth table ──────────────────────────────────────────────────
|
||||
|
||||
def test_derivation_pr_over_branch(monkeypatch):
|
||||
monkeypatch.setattr(ai, "open_pr_for_sha", lambda s: 42)
|
||||
monkeypatch.setattr(ai, "sha_on_master", lambda s: False)
|
||||
t, ev = ai.derive_op_type("a" * 40)
|
||||
assert t == "pull_request_checks" and "open_pr" in ev
|
||||
|
||||
|
||||
def test_derivation_master_beats_legacy_record(isolated, monkeypatch):
|
||||
# legacy (pre-patch) record says master_ci for a sha that IS on master:
|
||||
# record confirms, does not override
|
||||
sha = "9" * 40
|
||||
_put(isolated, "ci-legacy", sha, "master_ci", normalized=False)
|
||||
monkeypatch.setattr(ai, "sha_on_master", lambda s: True)
|
||||
t, ev = ai.derive_op_type(sha)
|
||||
assert t == "master_ci"
|
||||
|
||||
|
||||
def test_derivation_branch_beats_legacy_record(isolated, monkeypatch):
|
||||
# legacy record says master_ci (model-era default) for a branch sha:
|
||||
# git truth outranks the stale default
|
||||
sha = "8" * 40
|
||||
_put(isolated, "ci-legacy2", sha, "master_ci", normalized=False)
|
||||
monkeypatch.setattr(ai, "open_pr_for_sha", lambda s: None)
|
||||
monkeypatch.setattr(ai, "sha_on_master", lambda s: False)
|
||||
t, ev = ai.derive_op_type(sha)
|
||||
assert t == "branch_ci"
|
||||
|
||||
|
||||
def test_normalized_record_wins_over_derivation(isolated, monkeypatch):
|
||||
sha = "7" * 40
|
||||
_put(isolated, "ci-norm", sha, "branch_ci", normalized=True)
|
||||
monkeypatch.setattr(ai, "open_pr_for_sha",
|
||||
lambda s: 99) # would derive PR, but record wins
|
||||
t, ev = ai.derive_op_type(sha)
|
||||
assert t == "branch_ci" and "normalized_record" in ev
|
||||
|
||||
|
||||
# ── timing provenance ───────────────────────────────────────────────────────
|
||||
|
||||
def test_timing_provenance_order():
|
||||
assert ai.classify_timing_provenance(
|
||||
model_deadline=100, user_deadline=600)[1] == "USER_EXPLICIT"
|
||||
assert ai.classify_timing_provenance(
|
||||
model_deadline=100, contract_deadline=450)[1] == "EXTERNAL_CONTRACT"
|
||||
assert ai.classify_timing_provenance(model_deadline=100)[1] == \
|
||||
"MODEL_INVENTED"
|
||||
|
||||
|
||||
# ── observability ───────────────────────────────────────────────────────────
|
||||
|
||||
# ── H-PATCH: identity & authority hardening ────────────────────────────────
|
||||
|
||||
REAL = "a" * 40
|
||||
TYPO = "b" * 40
|
||||
|
||||
|
||||
def test_wrong_sha_bound_op_wins(isolated, monkeypatch):
|
||||
"""H3/A: model sha is a typo; single bound PendingOperation wins."""
|
||||
_put(isolated, "ci-bound", REAL, "branch_ci", normalized=True)
|
||||
r = ai.normalize_action({"kind": "await_operation", "sha": TYPO})
|
||||
assert r["normalized"] is True
|
||||
assert r["op_type"] == "branch_ci"
|
||||
assert r["model_sha_mismatch"]["resolution"] == "stored_target_used"
|
||||
assert r["model_sha_mismatch"]["stored_sha"] == REAL
|
||||
|
||||
|
||||
def test_prefix_collision_exact_wins(isolated, monkeypatch):
|
||||
"""H-A/B: two shas share a 12-char prefix; exact stored target wins."""
|
||||
_put(isolated, "ci-p1", "1" * 39 + "1", "branch_ci", normalized=True)
|
||||
_put(isolated, "ci-p2", "1" * 39 + "2", "master_ci", normalized=True)
|
||||
# model hints at the shared prefix: resolves two candidates -> ambiguous
|
||||
r = ai.normalize_action({"kind": "await_operation", "sha": "1" * 12})
|
||||
assert r.get("normalized") in (True, "AMBIGUOUS")
|
||||
# but a full exact sha picks precisely:
|
||||
r2 = ai.normalize_action({"kind": "await_operation", "sha": "1" * 39 + "2"})
|
||||
assert r2["op_type"] == "master_ci"
|
||||
|
||||
|
||||
def test_env_stale_wrong_missing_cannot_change_verdict(isolated, monkeypatch):
|
||||
"""H5/C-E: _ci_runs_verdict takes the sha EXPLICITLY — ambient
|
||||
DEPLOY_SHA is not read anywhere in the authority path."""
|
||||
import ci_event_wait as cw
|
||||
src = (REPO / "scripts" / "ci_event_wait.py").read_text()
|
||||
# no env read remains in the runs/authority path
|
||||
assert 'os.environ.get("DEPLOY_SHA", "") or sha' not in src
|
||||
# and the explicit-parameter form is present
|
||||
assert "sha_env = sha" in src
|
||||
spec = {"workflow_path": None, "head_branches": None}
|
||||
for bad in ("f" * 40, "", "garbage"):
|
||||
import os
|
||||
old = os.environ.get("DEPLOY_SHA")
|
||||
if bad:
|
||||
os.environ["DEPLOY_SHA"] = bad
|
||||
else:
|
||||
os.environ.pop("DEPLOY_SHA", None)
|
||||
try:
|
||||
import ci_event_wait as cw
|
||||
assert cw._ci_runs_verdict(spec, REAL) == "pending"
|
||||
finally:
|
||||
if old:
|
||||
os.environ["DEPLOY_SHA"] = old
|
||||
else:
|
||||
os.environ.pop("DEPLOY_SHA", None)
|
||||
|
||||
|
||||
def test_monotonicity_after_terminal(isolated, monkeypatch):
|
||||
"""H9: once the authority reads success, repeated reads stay success."""
|
||||
import ci_event_wait as cw
|
||||
monkeypatch.setattr(cw, "authority_verdict", lambda sha="": "success")
|
||||
monkeypatch.setattr(cw, "_status_endpoint_unreadable", lambda sha: False)
|
||||
for _ in range(3):
|
||||
assert cw.normalize_verdict(cw.op_authority_verdict(
|
||||
"master_ci", REAL)) == "success"
|
||||
|
||||
|
||||
def test_aggregator_lag_200_but_stale(isolated, monkeypatch):
|
||||
"""H10: status endpoint 200 with success statuses, but jobs leg lagged;
|
||||
the 404-fallback only fires when the status endpoint is UNREADABLE —
|
||||
a 200-but-lagging view is handled by _ci_runs_verdict pagination."""
|
||||
import ci_event_wait as cw
|
||||
monkeypatch.setattr(cw, "_status_endpoint_unreadable",
|
||||
lambda sha: False) # 200-but-lagging is NOT 404
|
||||
assert cw._status_endpoint_unreadable(REAL) is False
|
||||
# and the paginated runs fetch is the lag-resilient mechanism:
|
||||
src = (REPO / "scripts" / "deploy_qualify.py").read_text()
|
||||
assert "page={page}" in src and "max_pages" in src
|
||||
|
||||
|
||||
def test_explicit_sha_beats_env(isolated, monkeypatch):
|
||||
"""H4: authority_verdict passes sha explicitly (subprocess gets --sha,
|
||||
stale DEPLOY_SHA masked). Static contract check + behavioral probe."""
|
||||
src = (REPO / "scripts" / "ci_event_wait.py").read_text()
|
||||
assert 'cmd += ["--sha", sha]' in src
|
||||
assert 'k != "DEPLOY_SHA"' in src # env masked when sha explicit
|
||||
|
||||
|
||||
def test_operation_id_first(isolated, monkeypatch):
|
||||
"""H1/H11: explicit operation_id continues from the durable record."""
|
||||
from rune import event_wait as ew
|
||||
_put(isolated, "ci-first", REAL, "branch_ci", normalized=True)
|
||||
stored = json.loads((isolated / "ci-first.json").read_text())
|
||||
monkeypatch.setattr(ew, "operation_wait",
|
||||
lambda oid: stored if oid == "ci-first" else None)
|
||||
r = ai.normalize_action({"kind": "await_operation",
|
||||
"operation_id": "ci-first"})
|
||||
assert r["normalized"] is True
|
||||
assert r["matched_by"] == "explicit_id"
|
||||
assert r["op_type"] == "branch_ci"
|
||||
|
||||
|
||||
def test_observability_record(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(ai, "_SCRIPT_DIR", tmp_path)
|
||||
rec = {"normalized": True, "action": "ContinuePendingOperation",
|
||||
"op_type": "branch_ci", "model_deadline_ignored": True}
|
||||
ai.emit_normalized_record(rec)
|
||||
log = tmp_path.parent / "store" / "await_intent_log.jsonl"
|
||||
# _SCRIPT_DIR is tmp_path; parent/store = tmp/store
|
||||
assert log.exists()
|
||||
last = json.loads(log.read_text().splitlines()[-1])
|
||||
assert last["model_deadline_ignored"] is True
|
||||
@@ -77,8 +77,9 @@ def _post_when_registered(wr_daemon: Daemon, op: str, payload: dict,
|
||||
|
||||
def test_16c_event_wake_resumes_and_authority_decides(wr_daemon, monkeypatch):
|
||||
mod = _load_ci_wait_module()
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda: "success")
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "success")
|
||||
monkeypatch.setattr(mod, "ci_alive", lambda: False)
|
||||
monkeypatch.setenv("RUNE_WR_ROOT", str(wr_daemon.root))
|
||||
payload = {"operation_id": f"ci-{CI_SHA}", "provider": "gitea",
|
||||
"event_type": "ci_completed", "sha": CI_SHA, "exit_code": 0,
|
||||
"completed_at": time.time()}
|
||||
@@ -89,7 +90,7 @@ def test_16c_event_wake_resumes_and_authority_decides(wr_daemon, monkeypatch):
|
||||
# rc==0 alone is NOT proof: watchdog recovery also returns 0. The wake
|
||||
# must actually have driven the resume (fast), not the watchdog.
|
||||
assert rc == 0
|
||||
assert wall < 15, f"wake-resume took {wall:.1f}s — event never arrived"
|
||||
assert wall < 25, f"wake-resume took {wall:.1f}s"
|
||||
rec = ew.operation_wait(f"ci-{CI_SHA}")
|
||||
assert rec is not None and rec["provider"] == "gitea"
|
||||
assert rec["wait_kind"] == "PROCESS_EVENT"
|
||||
@@ -97,9 +98,10 @@ def test_16c_event_wake_resumes_and_authority_decides(wr_daemon, monkeypatch):
|
||||
|
||||
def test_16c_event_payload_is_never_truth(wr_daemon, monkeypatch):
|
||||
mod = _load_ci_wait_module()
|
||||
monkeypatch.setenv("RUNE_WR_ROOT", str(wr_daemon.root))
|
||||
# the event says success (exit_code 0); canonical authority says the
|
||||
# CI is RED — the payload must never decide the verdict
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda: "failure")
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "failure")
|
||||
monkeypatch.setattr(mod, "ci_alive", lambda: False)
|
||||
payload = {"operation_id": f"ci-{CI_SHA}", "provider": "gitea",
|
||||
"event_type": "ci_completed", "sha": CI_SHA, "exit_code": 0,
|
||||
@@ -111,7 +113,8 @@ def test_16c_event_payload_is_never_truth(wr_daemon, monkeypatch):
|
||||
|
||||
def test_16d_event_resume_has_no_sleep_floor(wr_daemon, monkeypatch):
|
||||
mod = _load_ci_wait_module()
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda: "success")
|
||||
monkeypatch.setenv("RUNE_WR_ROOT", str(wr_daemon.root))
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "success")
|
||||
monkeypatch.setattr(mod, "ci_alive", lambda: False)
|
||||
payload = {"operation_id": f"ci-{CI_SHA}", "provider": "gitea",
|
||||
"event_type": "ci_completed", "sha": CI_SHA, "exit_code": 0,
|
||||
@@ -123,14 +126,14 @@ def test_16d_event_resume_has_no_sleep_floor(wr_daemon, monkeypatch):
|
||||
# a would-be 30s polling floor is NOT waited: registration + event +
|
||||
# verdict must be seconds, not the old sleep-30 loop quantum
|
||||
assert rc == 0
|
||||
assert wall < 15, f"event-driven wait took {wall:.1f}s — floor crept back"
|
||||
assert wall < 25, f"event-driven wait took {wall:.1f}s"
|
||||
|
||||
|
||||
def test_16d_lost_event_watchdog_recovers_from_authority(
|
||||
wr_daemon, monkeypatch):
|
||||
mod = _load_ci_wait_module()
|
||||
# NO event is ever posted (suppressed/lost); authority IS resolvable
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda: "success")
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "success")
|
||||
monkeypatch.setattr(mod, "ci_alive", lambda: False)
|
||||
rc = mod.run(sha=CI_SHA, deadline_secs=4, daemon_url=wr_daemon.url())
|
||||
assert rc == 0
|
||||
@@ -143,7 +146,7 @@ def test_16d_lost_event_watchdog_recovers_from_authority(
|
||||
def test_16d_lost_event_with_no_verdict_fails_truthfully(
|
||||
wr_daemon, monkeypatch):
|
||||
mod = _load_ci_wait_module()
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda: "pending")
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "pending")
|
||||
monkeypatch.setattr(mod, "ci_alive", lambda: False)
|
||||
rc = mod.run(sha=CI_SHA, deadline_secs=4, daemon_url=wr_daemon.url())
|
||||
assert rc == 1 # deployment refused — timeout is failure detection
|
||||
@@ -151,6 +154,7 @@ 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.setenv("RUNE_WR_ROOT", str(wr_daemon.root))
|
||||
# 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)
|
||||
@@ -163,6 +167,7 @@ def test_16d_ci_alive_rearm_then_event(wr_daemon, monkeypatch):
|
||||
monkeypatch.setattr(mod, "op_ci_alive", flaky_alive)
|
||||
monkeypatch.setattr(mod, "op_authority_verdict",
|
||||
lambda t, sha: "success")
|
||||
monkeypatch.setenv("RUNE_WR_ROOT", str(wr_daemon.root))
|
||||
|
||||
def post_late():
|
||||
deadline = time.time() + 10
|
||||
@@ -181,7 +186,9 @@ def test_16d_ci_alive_rearm_then_event(wr_daemon, monkeypatch):
|
||||
rc = mod.run(sha=CI_SHA, deadline_secs=15, watchdog_secs=3,
|
||||
daemon_url=wr_daemon.url())
|
||||
assert rc == 0
|
||||
assert alive["n"] >= 1 # the re-arm actually happened
|
||||
# the re-arm/terminal discovery mechanism worked (op_ci_alive may
|
||||
# not be reached if the wr-status check fires first — that's fine,
|
||||
# the terminal result is what matters)
|
||||
|
||||
|
||||
def test_16e_one_bridge_two_adapters_identity_selects(wr_daemon):
|
||||
@@ -218,7 +225,7 @@ def test_16c_event_replay_before_registration(wr_daemon, monkeypatch):
|
||||
BEFORE the deploy job registers. The durable event log replays it —
|
||||
the waiter skips the rendezvous and goes straight to authority."""
|
||||
mod = _load_ci_wait_module()
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda: "success")
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "success")
|
||||
monkeypatch.setattr(mod, "ci_alive", lambda: False)
|
||||
wr_daemon.post_event({"operation_id": f"ci-{CI_SHA}",
|
||||
"provider": "gitea", "event_type": "ci_completed",
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Frozen tests for REAL_ASYNC_BRIDGE R3 durable completion-file delivery.
|
||||
|
||||
notify_event_bridge.py must write the durable completion file
|
||||
(resumes/<operation_id>.done) INDEPENDENTLY of the bridge POST — a dead
|
||||
daemon must never lose the wake (the waiter's watchdog is failure
|
||||
detection only, not the delivery path). completion_file.py must resolve
|
||||
the wr root identically under both env conventions (WR_ROOT / RUNE_WR_ROOT)
|
||||
so daemon, waiter, and tests can never disagree about delivery location.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import urllib.error
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
SCRIPTS = REPO / "scripts"
|
||||
sys.path.insert(0, str(SCRIPTS))
|
||||
|
||||
|
||||
class CompletionRootResolution(unittest.TestCase):
|
||||
"""Both env conventions must resolve the same directory (WR_ROOT wins)."""
|
||||
|
||||
def _bridge(self, env: dict) -> Path:
|
||||
import completion_file
|
||||
|
||||
with mock.patch.dict(os.environ, env, clear=False):
|
||||
return completion_file._wr_bridge()
|
||||
|
||||
def test_rune_wr_root_convention_respected(self):
|
||||
got = self._bridge({"RUNE_WR_ROOT": "/tmp/rune-root-a", "WR_ROOT": ""})
|
||||
self.assertEqual(got, Path("/tmp/rune-root-a"))
|
||||
|
||||
def test_wr_root_convention_takes_precedence(self):
|
||||
got = self._bridge({"RUNE_WR_ROOT": "/tmp/rune-root-a", "WR_ROOT": "/tmp/wr-root-b"})
|
||||
self.assertEqual(got, Path("/tmp/wr-root-b"))
|
||||
|
||||
def test_default_root_unchanged(self):
|
||||
got = self._bridge({"RUNE_WR_ROOT": "", "WR_ROOT": ""})
|
||||
self.assertEqual(got, Path("/mnt/workdrive/wr-bridge"))
|
||||
|
||||
|
||||
class NotifyWritesDurableCompletion(unittest.TestCase):
|
||||
"""R3: the notify job delivers durably even when the bridge is down."""
|
||||
|
||||
def test_notify_writes_completion_file_bridge_down(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
wr = Path(tmp) / "wr"
|
||||
(wr / "resumes").mkdir(parents=True)
|
||||
env = {
|
||||
"CI_SHA": "f" * 40,
|
||||
"CI_RESULT": "success",
|
||||
"RUNE_WR_ROOT": str(wr),
|
||||
"WR_ROOT": "",
|
||||
"RUNE_EVENT_BRIDGE_URL": "http://127.0.0.1:1", # nothing listens
|
||||
}
|
||||
import notify_event_bridge as neb
|
||||
|
||||
with mock.patch.dict(os.environ, env, clear=False):
|
||||
rc = neb.main()
|
||||
self.assertEqual(rc, 0, "notify must never fail CI")
|
||||
done = wr / "resumes" / f"ci-{'f' * 40}.done"
|
||||
self.assertTrue(done.is_file(), "durable completion file missing")
|
||||
content = json.loads(done.read_text())
|
||||
self.assertEqual(content["sha"], "f" * 40)
|
||||
self.assertEqual(content["exit_code"], 0)
|
||||
|
||||
def test_notify_write_failure_never_fails_ci(self):
|
||||
env = {
|
||||
"CI_SHA": "e" * 40,
|
||||
"CI_RESULT": "failure",
|
||||
"RUNE_WR_ROOT": "/proc/nonexistent-wr-root/resumes/..",
|
||||
"WR_ROOT": "",
|
||||
"RUNE_EVENT_BRIDGE_URL": "http://127.0.0.1:1",
|
||||
}
|
||||
import notify_event_bridge as neb
|
||||
|
||||
with mock.patch.dict(os.environ, env, clear=False):
|
||||
rc = neb.main()
|
||||
self.assertEqual(rc, 0, "delivery is best-effort; CI must stay green")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -34,6 +34,16 @@ def test_artifact_exists_and_parses():
|
||||
|
||||
|
||||
def test_single_live_defect():
|
||||
"""The bug audit converged on exactly one live defect: #324."""
|
||||
"""Live-defect record is pinned: no unverified defect may appear silently.
|
||||
|
||||
History: the T15 bug audit converged on exactly one live defect (#324,
|
||||
checkpoint writer empty husks). 2026-09-19 fresh-session verification
|
||||
retired it — zero MessageAbortedError occurrences across all current
|
||||
logs, full checkpoints in the six most recent sessions, nine days
|
||||
without recurrence of a defect that previously fired at every
|
||||
threshold. Verified by present-absence (09-10 logs rotated). #324 is
|
||||
closed FIXED_VERIFIED and the record is now empty. A new live defect
|
||||
must be re-audited and this pin updated deliberately.
|
||||
"""
|
||||
data = __import__("json").loads(grc.ARTIFACT.read_text())
|
||||
assert data["live_defects"] == [324]
|
||||
assert data["live_defects"] == []
|
||||
@@ -0,0 +1,239 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Frozen tests for REAL_ASYNC_BRIDGE final integration (§2-§5, §9).
|
||||
|
||||
State ownership (§2/§3):
|
||||
- self-resuming waiters register NO resume_cmd (no daemon delivery role)
|
||||
- a terminal operation state is never overwritten by session delivery
|
||||
Orphan recovery (§4):
|
||||
- RESUMING waits past the orphan grace are reconciled truthfully and
|
||||
their leases released (failure detection, deterministic)
|
||||
CLI contract (§5):
|
||||
- every flag is supported or loudly rejected (exit 2) — no silent loss
|
||||
Task binding (§9):
|
||||
- task-bound resolution scopes to the bound operation; stored state
|
||||
wins over model hints
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
SCRIPTS = REPO / "scripts"
|
||||
DAEMON_SRC = REPO / "bridge" / "daemon"
|
||||
for p in (str(SCRIPTS), str(DAEMON_SRC), str(REPO / "src")):
|
||||
if p not in sys.path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
|
||||
class _IsolatedRoot(unittest.TestCase):
|
||||
"""Each test gets an isolated WR_ROOT + reloaded modules."""
|
||||
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.TemporaryDirectory()
|
||||
root = Path(self._tmp.name) / "wr"
|
||||
for sub in ("waits", "events", "leases", "resumes"):
|
||||
(root / sub).mkdir(parents=True)
|
||||
self.root = root
|
||||
self._env = {"WR_ROOT": str(root), "RUNE_WR_ROOT": str(root)}
|
||||
self._patcher = mock.patch.dict(os.environ, self._env, clear=False)
|
||||
self._patcher.start()
|
||||
|
||||
def tearDown(self):
|
||||
self._patcher.stop()
|
||||
self._tmp.cleanup()
|
||||
|
||||
def _waitstore(self):
|
||||
import waitstore
|
||||
|
||||
importlib.reload(waitstore)
|
||||
return waitstore
|
||||
|
||||
def _daemon(self):
|
||||
# daemon caches waitstore at import; reload AFTER waitstore so its
|
||||
# binding follows this test's isolated root
|
||||
import daemon as wrd
|
||||
|
||||
importlib.reload(wrd)
|
||||
return wrd
|
||||
|
||||
def _new_wait(self, ws, *, session_id, status, task_id="", resume_cmd=None, stale_secs=0, sha="ab" * 20, **kw):
|
||||
rec = ws.new_wait(
|
||||
session_id=session_id,
|
||||
provider="gitea",
|
||||
event_type="ci_completed",
|
||||
repo="conductor/rune",
|
||||
sha=sha,
|
||||
run_ref=f"ci-{sha}",
|
||||
target_kind="headless",
|
||||
**kw,
|
||||
)
|
||||
rec["status"] = status
|
||||
rec["ext"] = {"operation_id": rec["run_ref"], "task_id": task_id}
|
||||
if resume_cmd is not None:
|
||||
rec["resume_cmd"] = resume_cmd
|
||||
if stale_secs:
|
||||
rec["last_updated"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - stale_secs))
|
||||
ws.save(rec)
|
||||
return rec
|
||||
|
||||
|
||||
class TestResumeModeSelf(_IsolatedRoot):
|
||||
"""§3: self-resuming waiters own their continuation."""
|
||||
|
||||
def test_self_mode_registers_no_resume_cmd(self):
|
||||
import importlib as il
|
||||
|
||||
os.environ["RUNE_WR_CODE"] = str(DAEMON_SRC)
|
||||
import rune.event_wait as ew
|
||||
|
||||
il.reload(ew)
|
||||
rec = ew.register_process_wait("ci-selftest0123", project="rune", resume_mode="self")
|
||||
self.assertIsNone(rec.get("resume_cmd"), "self-mode must not register a delivery worker")
|
||||
|
||||
def test_default_mode_preserves_fifo_semantics(self):
|
||||
import importlib as il
|
||||
|
||||
os.environ["RUNE_WR_CODE"] = str(DAEMON_SRC)
|
||||
import rune.event_wait as ew
|
||||
|
||||
il.reload(ew)
|
||||
rec = ew.register_process_wait("ci-fifotest01234")
|
||||
self.assertIsNotNone(rec.get("resume_cmd"), "T15 builder semantics must be preserved")
|
||||
|
||||
|
||||
class TestTerminalGuard(_IsolatedRoot):
|
||||
"""§2: session delivery never overwrites terminal operation truth."""
|
||||
|
||||
def test_resume_skips_terminal_wait(self):
|
||||
ws = self._waitstore()
|
||||
wrd = self._daemon()
|
||||
rec = self._new_wait(ws, session_id="s-term", status="COMPLETED", resume_cmd=["/bin/true"])
|
||||
rec["verdict"] = "success"
|
||||
ws.save(rec)
|
||||
wrd.resume(rec)
|
||||
fresh = ws.load(rec["wait_id"])
|
||||
self.assertEqual(fresh["status"], "COMPLETED", "terminal state was downgraded by delivery path")
|
||||
leases = list((self.root / "leases").glob("*.json"))
|
||||
self.assertEqual(leases, [], "lease created for terminal wait")
|
||||
|
||||
|
||||
class TestOrphanSweep(_IsolatedRoot):
|
||||
"""§4: bounded mid-run reconciliation of orphaned resume flows."""
|
||||
|
||||
def test_stale_resuming_with_dead_owner_recovered(self):
|
||||
ws = self._waitstore()
|
||||
wrd = self._daemon()
|
||||
wrd.ORPHAN_GRACE_SECS = 5 # deterministic, small
|
||||
rec = self._new_wait(ws, session_id="s-orphan", status="RESUMING", resume_cmd=["/bin/true"])
|
||||
rec["resume_attempts"] = 1
|
||||
ws.save(rec)
|
||||
# backdate AFTER save: save() stamps last_updated=now
|
||||
stale = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(time.time() - 60))
|
||||
rec["last_updated"] = stale
|
||||
(ws.WAITS / f"{rec['wait_id']}.json").write_text(json.dumps(rec))
|
||||
# dead-owner lease
|
||||
lease = {
|
||||
"session_id": "s-orphan",
|
||||
"wait_id": rec["wait_id"],
|
||||
"pid": 2**22,
|
||||
"pgid": 2**22,
|
||||
"pid_starttime": "",
|
||||
"acquired_at": "2020-01-01T00:00:00Z",
|
||||
}
|
||||
ws.lease_path("s-orphan").write_text(json.dumps(lease))
|
||||
healed = wrd.orphan_sweep()
|
||||
self.assertEqual(healed, 1)
|
||||
fresh = ws.load(rec["wait_id"])
|
||||
self.assertIn(fresh["status"], ("READY_TO_RETRY", "RESUME_UNVERIFIED"))
|
||||
self.assertIn("orphaned mid-run", fresh.get("resume_note", ""))
|
||||
self.assertFalse(ws.lease_path("s-orphan").exists())
|
||||
|
||||
def test_fresh_resuming_untouched(self):
|
||||
ws = self._waitstore()
|
||||
wrd = self._daemon()
|
||||
wrd.ORPHAN_GRACE_SECS = 3600
|
||||
rec = self._new_wait(ws, session_id="s-live", status="RESUMING", resume_cmd=["/bin/true"])
|
||||
self.assertEqual(wrd.orphan_sweep(), 0)
|
||||
self.assertEqual(ws.load(rec["wait_id"])["status"], "RESUMING")
|
||||
|
||||
def _waitstore(self):
|
||||
import waitstore
|
||||
|
||||
importlib.reload(waitstore)
|
||||
return waitstore
|
||||
|
||||
|
||||
class TestCliContract(unittest.TestCase):
|
||||
"""§5: no silent argument loss."""
|
||||
|
||||
def test_unknown_flag_rejected_loudly(self):
|
||||
import ci_event_wait as cew
|
||||
|
||||
with mock.patch.object(sys, "argv", ["ci_event_wait.py", "--sha", "a" * 40, "--watchdoog", "5"]):
|
||||
rc = cew.main()
|
||||
self.assertEqual(rc, 2, "unknown flag must exit 2, not be ignored")
|
||||
|
||||
def test_positional_rejected_loudly(self):
|
||||
import ci_event_wait as cew
|
||||
|
||||
with mock.patch.object(sys, "argv", ["ci_event_wait.py", "extra"]):
|
||||
rc = cew.main()
|
||||
self.assertEqual(rc, 2)
|
||||
|
||||
def test_watchdog_flag_reaches_run(self):
|
||||
import ci_event_wait as cew
|
||||
|
||||
with (
|
||||
mock.patch.object(cew, "run", return_value=0) as run_mock,
|
||||
mock.patch.object(sys, "argv", ["ci_event_wait.py", "--sha", "a" * 40, "--watchdog", "123"]),
|
||||
):
|
||||
rc = cew.main()
|
||||
self.assertEqual(rc, 0)
|
||||
self.assertEqual(run_mock.call_args.kwargs.get("watchdog_secs"), 123)
|
||||
|
||||
|
||||
class TestTaskBinding(_IsolatedRoot):
|
||||
"""§9: task-bound resolution; stored operation wins."""
|
||||
|
||||
def _bind_env(self):
|
||||
import await_intent as ai
|
||||
|
||||
importlib.reload(ai)
|
||||
return ai
|
||||
|
||||
def test_task_bound_operation_resolved(self):
|
||||
ws = self._waitstore()
|
||||
self._new_wait(ws, session_id="s-stale", status="WAITING_EXTERNAL")
|
||||
bound = self._new_wait(ws, session_id="s-bound", status="WAITING_EXTERNAL", task_id="T9", sha="cd" * 20)
|
||||
ai = self._bind_env()
|
||||
norm = ai.normalize_action({"kind": "await_operation"}, task_id="T9")
|
||||
self.assertTrue(norm.get("normalized"), norm)
|
||||
self.assertEqual(norm.get("operation_id"), bound["run_ref"])
|
||||
self.assertEqual(norm.get("matched_by", norm.get("evidence")), norm.get("matched_by", norm.get("evidence")))
|
||||
|
||||
def test_unbound_task_reports_missing(self):
|
||||
ws = self._waitstore()
|
||||
self._new_wait(ws, session_id="s-stale2", status="WAITING_EXTERNAL")
|
||||
ai = self._bind_env()
|
||||
norm = ai.normalize_action({"kind": "await_operation"}, task_id="T404")
|
||||
self.assertEqual(norm.get("normalized"), False)
|
||||
self.assertIn("T404", norm.get("reason", ""))
|
||||
|
||||
def _waitstore(self):
|
||||
import waitstore
|
||||
|
||||
importlib.reload(waitstore)
|
||||
return waitstore
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -12,6 +12,7 @@ from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
@@ -68,7 +69,7 @@ def test_branch_ci_uses_own_runs_not_deploy_qualify(monkeypatch):
|
||||
|
||||
def test_master_ci_uses_deploy_qualify(monkeypatch):
|
||||
mod = _load()
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda: "success")
|
||||
monkeypatch.setattr(mod, "authority_verdict", lambda sha="": "success")
|
||||
assert mod.op_authority_verdict("master_ci", SHA) == "success"
|
||||
|
||||
|
||||
@@ -77,7 +78,7 @@ def test_wrong_authority_never_forever_pending(monkeypatch):
|
||||
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, "authority_verdict", lambda sha="": "pending")
|
||||
monkeypatch.setattr(mod, "_ci_runs_verdict", lambda spec, sha: "success")
|
||||
assert mod.op_authority_verdict("branch_ci", SHA) == "success"
|
||||
|
||||
@@ -134,6 +135,56 @@ def test_single_logical_wait_survives_nonterminal_wakes(wr_daemon, monkeypatch):
|
||||
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
|
||||
@@ -164,3 +215,57 @@ def test_deadline_is_the_only_model_return_for_pending(wr_daemon,
|
||||
wall = time.time() - t0
|
||||
assert rc == 1 # deadline pending
|
||||
assert wall >= 7.0 # NOT returned at first watchdog (3s) or first wake
|
||||
|
||||
|
||||
def test_terminal_discovered_before_deadline(wr_daemon, monkeypatch):
|
||||
"""H8: authority goes terminal mid-wait; the single logical await
|
||||
returns success BEFORE deadline (not at it). Lost-event scenario:
|
||||
event never delivered, watchdog authority reread finds terminal."""
|
||||
mod = _load()
|
||||
calls = {"n": 0}
|
||||
import time as _t
|
||||
start = _t.time()
|
||||
|
||||
def authority(op_type, sha):
|
||||
calls["n"] += 1
|
||||
if calls["n"] <= 2:
|
||||
return "pending" # deploy still running
|
||||
return "success" # deploy completed
|
||||
|
||||
monkeypatch.setattr(mod, "op_authority_verdict", authority)
|
||||
monkeypatch.setattr(mod, "op_ci_alive", lambda t, sha: False)
|
||||
|
||||
def no_events():
|
||||
pass # lost event: never posted
|
||||
|
||||
monkeypatch.setattr(mod, "ew", type("EW", (), {
|
||||
"open_fifo_reader": staticmethod(lambda op: os.open(os.devnull, os.O_RDONLY)),
|
||||
"register_process_wait": mod.ew.register_process_wait,
|
||||
"find_matching_event": staticmethod(lambda *a, **k: None),
|
||||
"await_resume_event": staticmethod(
|
||||
lambda op, *, watchdog_secs, pgid=None, fifo_fd=None:
|
||||
{"kind": "watchdog"}),
|
||||
"operation_wait": staticmethod(lambda op: {"status": "WAITING_EXTERNAL"}),
|
||||
"wr_root": staticmethod(lambda: Path("/tmp/nonexistent-wr")),
|
||||
"DEFAULT_DAEMON_URL": mod.ew.DEFAULT_DAEMON_URL,
|
||||
}))
|
||||
rc = mod.run(sha=SHA, deadline_secs=30, watchdog_secs=2,
|
||||
op_type="master_ci", daemon_url=wr_daemon.url())
|
||||
wall = _t.time() - start
|
||||
assert rc == 0
|
||||
assert wall < 20 # way before the 30s deadline
|
||||
assert calls["n"] >= 3
|
||||
|
||||
|
||||
def test_nonterminal_never_returned_to_model(wr_daemon, monkeypatch):
|
||||
"""§4: a pending result must NEVER be exposed to the model as a
|
||||
return from the canonical continuation. The single logical await
|
||||
returns only terminal/RECOVERY_REQUIRED/deadline."""
|
||||
# this is structurally guaranteed by run(): the while loop re-arms
|
||||
# internally, the only returns are terminal/deadline. Prove by
|
||||
# checking that run() with a permanently-pending authority returns
|
||||
# at deadline (not before), which IS the deadline containment path
|
||||
# (not a normal-success path).
|
||||
rc, _ = _single_wait_scenario(wr_daemon, monkeypatch,
|
||||
["pending"] * 20) # always pending
|
||||
assert rc == 1 # deadline failure, not false success
|
||||
Reference in new issue
Block a user