Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
25622aa
make-pr: preflight runs the PR-body validator on --body-file
EdbertChan Sep 23, 2026
aeb2d1c
invoker: wf-1790182452439-6/implement-preflight-runs-validator — Revi…
EdbertChan Sep 23, 2026
e896182
invoker: wf-1790182452439-6/verify-preflight-runs-validator-1 — Revie…
EdbertChan Sep 23, 2026
fe1887b
invoker: wf-1790182452439-6/verify-preflight-runs-validator-4 — Revie…
EdbertChan Sep 23, 2026
394d321
invoker: wf-1790182452439-6/verify-preflight-runs-validator-2 — Revie…
EdbertChan Sep 23, 2026
5950111
invoker: wf-1790182452439-6/verify-preflight-runs-validator-3 — Revie…
EdbertChan Sep 23, 2026
e59f975
invoker: wf-1790182452439-6/verify-preflight-runs-validator-4 — Revie…
EdbertChan Sep 23, 2026
0c52061
invoker: wf-1790182452439-6/verify-preflight-runs-validator-4 — Revie…
EdbertChan Sep 23, 2026
54e76b3
invoker: wf-1790182452439-6/verify-preflight-runs-validator-4 — Revie…
EdbertChan Sep 23, 2026
19ead97
Invoker: merge experiment/wf-1790182452439-6/verify-preflight-runs-va…
EdbertChan Sep 23, 2026
cb1089e
Invoker: merge experiment/wf-1790182452439-6/verify-preflight-runs-va…
EdbertChan Sep 23, 2026
ed02a2a
Invoker: merge experiment/wf-1790182452439-6/verify-preflight-runs-va…
EdbertChan Sep 23, 2026
cc4dd39
invoker: wf-1790182452439-6/scrub-handoff-artifacts — Review claim: N…
EdbertChan Sep 23, 2026
9a9c913
Merge experiment/wf-1790182452439-6/scrub-handoff-artifacts/g0.t1.a-a…
EdbertChan Sep 23, 2026
13ab621
reflect: catch an evidence-order correction from the ledger transitio…
EdbertChan Sep 22, 2026
0ddfd29
cat-mode: a park beside a killed copy wins over the partial Invoker.app
EdbertChan Sep 24, 2026
1c598ed
make-pr: preflight hands the validator the changed files
edbertchantech-ai Sep 24, 2026
ccddeb1
event-wait: read a final unterminated line, kill a stuck source, hono…
EdbertChan Sep 24, 2026
aa9412e
llm-judge: find Codex's transcript by thread-id, and deliver subagent…
EdbertChan Sep 23, 2026
9df0641
llm-judge: look for Codex rollouts under CODEX_HOME when it is set
Sep 23, 2026
619f890
invoker: wf-1790173197097-223/repair — Repair PR #819: failed_checks:…
Sep 23, 2026
ff538ed
Merge of #782
mergify[bot] Sep 24, 2026
a1a8cb5
Merge of #841
mergify[bot] Sep 24, 2026
904e62f
Merge of #819
mergify[bot] Sep 24, 2026
ac781a6
Merge of #791
mergify[bot] Sep 24, 2026
4649227
Merge of #836
mergify[bot] Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions corpus/skills/cat-mode/scripts/update_fleet.sh
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,6 @@ app_version() {

parked_app() {
local candidate
[ -d "$APP_DIR/Invoker.app" ] && return 0
for candidate in "$APP_DIR"/Invoker.app.replacing.*; do
[ -d "$candidate" ] || continue
printf '%s' "$candidate"
Expand All @@ -192,7 +191,7 @@ local_app() {
parked="$(parked_app)"
if [ "$DRY_RUN" = 1 ]; then
if [ -n "$parked" ]; then
row warn local "app: an interrupted run left $parked and no $APP_DIR/Invoker.app; a real run puts it back first (dry-run)" "$parked"
row warn local "app: an interrupted run left $parked; a real run puts it back over any $APP_DIR/Invoker.app first (dry-run)" "$parked"
return 0
fi
row ok local "app $(app_version) -> $RELEASE_VERSION (dry-run)" ""; return 0
Expand Down
33 changes: 33 additions & 0 deletions engine/hooks/_sdk/transcripts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from __future__ import annotations

import glob
import os
import re

CODEX_SESSIONS_ENV = "CATSTACK_CODEX_SESSIONS_DIR"
CODEX_HOME_ENV = "CODEX_HOME"
THREAD_ID = re.compile(r"^[0-9A-Za-z-]{8,64}$")


def codex_sessions_root() -> str:
override = os.environ.get(CODEX_SESSIONS_ENV)
if override:
return override
home = os.environ.get(CODEX_HOME_ENV) or os.path.join(os.path.expanduser("~"), ".codex")
return os.path.join(home, "sessions")


def codex_rollout(payload: dict) -> str:
thread = payload.get("thread-id") or payload.get("thread_id")
if not isinstance(thread, str) or not THREAD_ID.match(thread):
return ""
pattern = os.path.join(codex_sessions_root(), "*", "*", "*", f"rollout-*-{thread}.jsonl")
matches = sorted(glob.glob(pattern))
return matches[-1] if matches else ""


def subagent_transcripts(transcript: str) -> list[str]:
if not transcript.endswith(".jsonl"):
return []
folder = os.path.join(transcript[: -len(".jsonl")], "subagents")
return sorted(glob.glob(os.path.join(folder, "*.jsonl")))
8 changes: 6 additions & 2 deletions engine/hooks/llm-judge/inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os

import judge
from transcripts import codex_rollout, subagent_transcripts

NO_TRANSCRIPT = "llm-judge: {harness} payload has no transcript path, so finished verdicts were not checked"
REPORT_LIMIT = 600
Expand All @@ -31,7 +32,7 @@ def resolve_transcript(payload: dict) -> str:
candidate = os.path.join(root, project, "agent-transcripts", conv, f"{conv}.jsonl")
if os.path.isfile(candidate):
return candidate
return ""
return codex_rollout(payload)


def _turn_text(data: dict) -> str:
Expand Down Expand Up @@ -167,7 +168,10 @@ def unchecked_message(item: dict) -> str:

def messages(transcript: str) -> list[str]:
out = []
for item in judge.drain(transcript):
drained = []
for path in [transcript, *subagent_transcripts(transcript)]:
drained.extend(judge.drain(path))
for item in drained:
outcome = item.get("outcome")
if outcome == "clean":
continue
Expand Down
6 changes: 4 additions & 2 deletions engine/hooks/llm-judge/phrases/wrong-check-reflect.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"checker": "wrong-check-reflect",
"reads": "reply",
"meaning": "The latest assistant reply admits that something it told the user earlier was wrong, misread, or answered the wrong question.",
"meaning": "The latest assistant reply admits that something it told the user earlier was wrong, misread, or answered the wrong question, OR that an earlier claim of its own went out before the check backing it had run. A reply saying the claim turned out correct but was stated before it was checked IS a match: the failure is the order, not the answer, so no word in the reply has to say anything was wrong.",
"match": [
"my earlier check was wrong",
"You're right, I misread that",
Expand All @@ -10,7 +10,9 @@
"I was wrong about the path",
"Correction: the file I pointed you to earlier is not the one in use; the real one is src/b.py.",
"Good catch. The earlier number was off; the real count is 12.",
"My earlier read of the config was wrong \u2014 the default is 4, not 8."
"My earlier read of the config was wrong \u2014 the default is 4, not 8.",
"Correcting one claim and arming the check I implied:",
"I was right - but I said it a turn before I checked it"
],
"not_match": [
"You're right. Let's go with option B.",
Expand Down
73 changes: 73 additions & 0 deletions engine/hooks/llm-judge/tests/test_inbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -355,5 +355,78 @@ def test_no_transcript_says_unchecked_on_stderr(self):
self.assertIn("no transcript path", self.run_codex([json.dumps({"type": "agent-turn-complete", "thread-id": "t1"})]))



REAL_CODEX_NOTIFY_KEYS = ["client", "cwd", "input-messages", "last-assistant-message", "thread-id", "turn-id", "type"]
THREAD = "01a0ce9a-301b-7d42-bb5c-b9a9a4cfe8c6"


class TestTranscriptResolution(InboxTestCase):
def setUp(self):
super().setUp()
sessions = os.path.join(self.work.name, "codex-sessions")
day = os.path.join(sessions, "2026", "09", "23")
os.makedirs(day)
self.rollout = os.path.join(day, f"rollout-2026-09-23T22-10-06-{THREAD}.jsonl")
with open(self.rollout, "w", encoding="utf-8") as handle:
handle.write("{}\n")
self.sessions_env = patch.dict(os.environ, {"CATSTACK_CODEX_SESSIONS_DIR": sessions})
self.sessions_env.start()

def tearDown(self):
self.sessions_env.stop()
super().tearDown()

def real_codex_payload(self):
payload = {
"type": "agent-turn-complete",
"thread-id": THREAD,
"turn-id": "01a0ce9a-30dc-73f1-bfe6-ccce644446e1",
"cwd": self.work.name,
"client": "codex_exec",
"input-messages": ["Reply with the single word ok."],
"last-assistant-message": "ok",
}
self.assertEqual(sorted(payload), REAL_CODEX_NOTIFY_KEYS)
return payload

def test_real_codex_notify_payload_resolves_to_its_rollout_file(self):
self.assertEqual(inbox.resolve_transcript(self.real_codex_payload()), self.rollout)

def test_hit_for_a_codex_rollout_reaches_the_next_codex_notify(self):
self.transcript = self.rollout
self.seed(ANSWERS_TRUE)
self.assertEqual(self.run_codex([json.dumps(self.real_codex_payload())]), ON_HIT + "\n")

def test_a_codex_home_override_is_where_the_rollout_is_looked_up(self):
home = os.path.join(self.work.name, "codex-home")
day = os.path.join(home, "sessions", "2026", "09", "23")
os.makedirs(day)
rollout = os.path.join(day, f"rollout-2026-09-23T22-40-00-{THREAD}.jsonl")
with open(rollout, "w", encoding="utf-8") as handle:
handle.write("{}\n")
self.sessions_env.stop()
try:
with patch.dict(os.environ, {"CODEX_HOME": home}, clear=False):
os.environ.pop("CATSTACK_CODEX_SESSIONS_DIR", None)
self.assertEqual(inbox.resolve_transcript(self.real_codex_payload()), rollout)
finally:
self.sessions_env.start()

def test_unknown_or_unsafe_thread_id_resolves_to_nothing(self):
for thread in ("0000000-0000-not-there", "../../etc", "*"):
with self.subTest(thread=thread):
self.assertEqual(inbox.resolve_transcript({"thread-id": thread}), "")

def test_a_subagent_verdict_is_delivered_to_the_parent_session(self):
subagent = os.path.join(self.transcript[: -len(".jsonl")], "subagents", "agent-a1.jsonl")
os.makedirs(os.path.dirname(subagent))
with open(subagent, "w", encoding="utf-8") as handle:
handle.write("{}\n")
parent = self.transcript
self.transcript = subagent
self.seed(ANSWERS_TRUE)
self.assertEqual(inbox.messages(parent), [ON_HIT])
self.assertEqual(inbox.messages(parent), [])

if __name__ == "__main__":
unittest.main()
6 changes: 6 additions & 0 deletions engine/hooks/unverified-tag-ledger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ fixtures in `tests/test_hooks.py`.
reason is written to stderr.
- **Escalation** — a claim outstanding `ESCALATE_AFTER_TURNS` (3) turns or more
is reported as a reflect trigger rather than accumulating quietly.
- **Discharge is itself a reflect trigger** — a row going outstanding ->
discharged is the record of a claim that went out first and was checked
after. That is an evidence-order miss, and it carries no wrongness word, so
the phrase scanners (`engine/skills/reflect/scripts/self_retraction_scan.py`,
and the `wrong-check-reflect` dictionary) cannot see it from the text. This
hook sees it from state instead, and says so on the Stop that discharges.

Malformed tags are deliberately ignored here; `diu-stop` already rejects those.

Expand Down
35 changes: 26 additions & 9 deletions engine/hooks/unverified-tag-ledger/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
ESCALATE_AFTER_TURNS = 3
MAX_LISTED = 5

DISCHARGE_REFLECT = (
"unverified-tag-ledger: {count} claim(s) went from unverified to checked this turn: "
"{claims}. That transition is the whole event: the claim went out first and the check "
"ran after. No wording has to admit anything for this to be true, which is why the "
"phrase scanners miss it -- an evidence-order miss carries no wrongness word. "
"Treat it as a reflect trigger, not a milestone: run reflect on this transcript, or "
"say plainly why this one does not need it."
)

CLAIM_RE = re.compile(
r"\{\{\s*CAT-UNVERIFIED\s*:?\s*(?P<claim>.*?)(?:--|—)\s*cannot\s+verify\s*:\s*(?P<reason>[^}]*)\}\}",
re.IGNORECASE | re.DOTALL,
Expand Down Expand Up @@ -177,18 +186,26 @@ def evaluate(payload: dict) -> dict:
session_id = str(payload.get("session_id") or "")
message = _last_assistant_text(payload)
tools = tools_used_this_turn(payload)
was_open = {row["claim"] for row in outstanding(read_ledger(session_id))}
rows = record_turn(session_id, message, tools)
notes = []
discharged = sorted(
row["claim"] for row in rows if row.get("resolved") and row["claim"] in was_open)
if discharged:
notes.append(DISCHARGE_REFLECT.format(
count=len(discharged), claims="; ".join(discharged[:MAX_LISTED])))

new_claims = {tag["claim"] for tag in parse_tags(message)}
if not new_claims:
return {"note": "", "block": ""}
return {"note": "\n".join(notes), "block": ""}

if tools is None:
return {"note": (
notes.append(
f"unverified-tag-ledger: logged {len(new_claims)} CAT-UNVERIFIED claim(s), but this "
"turn's tool calls could not be read from transcript_path (see the line above), so "
"whether a check was attempted is UNCHECKED, not clean. Nothing was discharged and "
"the turn was not refused."), "block": ""}
"the turn was not refused.")
return {"note": "\n".join(notes), "block": ""}

if not tools & VERIFY_TOOLS and not payload.get("stop_hook_active"):
claims = "; ".join(sorted(new_claims)[:MAX_LISTED])
Expand All @@ -201,12 +218,12 @@ def evaluate(payload: dict) -> dict:

fresh = [row for row in rows
if row["claim"] in new_claims and not row.get("resolved") and row.get("turns", 0) == 0]
if not fresh:
return {"note": "", "block": ""}
return {"note": (
f"unverified-tag-ledger: logged {len(fresh)} CAT-UNVERIFIED claim(s) against this session. "
"They are deferred, not discharged, and will be raised again next turn "
"(cat-mode/SKILL.md:269)."), "block": ""}
if fresh:
notes.append(
f"unverified-tag-ledger: logged {len(fresh)} CAT-UNVERIFIED claim(s) against this "
"session. They are deferred, not discharged, and will be raised again next turn "
"(cat-mode/SKILL.md:269).")
return {"note": "\n".join(notes), "block": ""}


def decide_stop(payload: dict) -> str:
Expand Down
30 changes: 30 additions & 0 deletions engine/hooks/unverified-tag-ledger/tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"-- cannot verify: my own reasoning isn't observable by any command}}")
MALFORMED = "{{CAT-UNVERIFIED: something I did not check}}"

EVIDENCE_ORDER_1 = "Correcting one claim and arming the check I implied:"
EVIDENCE_ORDER_2 = "I was right - but I said it a turn before I checked it"


def _real_transcript_lines() -> list[str]:
with open(REAL_TRANSCRIPT, encoding="utf-8") as handle:
Expand Down Expand Up @@ -209,6 +212,33 @@ def test_verified_and_dropped_tag_is_discharged_through_the_real_payload(self) -
self.assertEqual(self.detect.outstanding(self.detect.read_ledger("s1")), [])
self.assertEqual(self.detect.reminder("s1"), "")

def test_a_discharged_claim_fires_the_reflect_trigger(self) -> None:
self.detect.evaluate(self.payload(REAL_TAG_1, tools=True))
verdict = self.detect.evaluate(
self.payload("Here is the pasted output proving it.", tools=True))
self.assertIn("reflect trigger", verdict["note"])
self.assertIn("widened scope", verdict["note"])

def test_an_evidence_order_correction_triggers_with_no_wrongness_word(self) -> None:
"""The transition fires; the reply's wording is not consulted at all."""
for index, reply in enumerate((EVIDENCE_ORDER_1, EVIDENCE_ORDER_2)):
session = f"evidence-order-{index}"
self.detect.evaluate(self.payload(REAL_TAG_1, tools=True, session_id=session))
verdict = self.detect.evaluate(
self.payload(reply, tools=True, session_id=session))
self.assertIn("reflect trigger", verdict["note"])
self.assertIn("carries no wrongness word", verdict["note"])

def test_a_turn_that_discharges_nothing_stays_silent_about_reflect(self) -> None:
verdict = self.detect.evaluate(
self.payload("Ran the tests, all green.", tools=True))
self.assertEqual(verdict["note"], "")

def test_a_reemitted_tag_is_not_reported_as_discharged(self) -> None:
self.detect.evaluate(self.payload(REAL_TAG_1, tools=True))
verdict = self.detect.evaluate(self.payload(REAL_TAG_1, tools=True))
self.assertNotIn("reflect trigger", verdict["note"])

def test_unchecked_turn_does_not_discharge_a_row(self) -> None:
self.detect.evaluate(self.payload(REAL_TAG_1, tools=True))
self.detect.evaluate({
Expand Down
3 changes: 2 additions & 1 deletion engine/hooks/wrong-check-reflect/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "_sdk"))

from events import write_stage_event # noqa: E402
from transcripts import codex_rollout # noqa: E402
from flags import enforcement_gate # noqa: E402

HOOKS_DIR = os.path.dirname(os.path.abspath(__file__))
Expand Down Expand Up @@ -258,7 +259,7 @@ def resolve_transcript(payload: dict) -> str:
return candidate
except OSError:
pass
return ""
return codex_rollout(payload)


def _is_assistant_line(data: dict) -> bool:
Expand Down
3 changes: 3 additions & 0 deletions engine/hooks/wrong-check-reflect/eval_dictionary.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
(HIT_TEXT, True),
("You're right. Let's go with option B.", False),
("I double-checked my earlier count and it holds; nothing in it was wrong.", False),
("Correcting one claim and arming the check I implied:", True),
("I was right - but I said it a turn before I checked it", True),
("I ran the check first and then said it, so the order was right.", False),
)


Expand Down
19 changes: 19 additions & 0 deletions engine/hooks/wrong-check-reflect/tests/test_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,25 @@ def test_claude_stop_records_the_queued_job_under_the_claude_harness(self):
[("wrong-check-reflect", "claude", "transcript")])
self.assertEqual(queued[0]["finding_id"], self.wait_for_jobs(1)[0][: -len(".json")])

def test_real_codex_notify_queues_against_its_rollout_file(self):
self.use_runners(SLOW_CLEAN)
thread = "01a0ce9a-301b-7d42-bb5c-b9a9a4cfe8c6"
day = os.path.join(self.reflect_state.name, "codex-sessions", "2026", "09", "23")
os.makedirs(day)
rollout = os.path.join(day, f"rollout-2026-09-23T22-10-06-{thread}.jsonl")
with open(rollout, "w", encoding="utf-8") as handle:
handle.write("{}\n")
payload = {"type": "agent-turn-complete", "thread-id": thread, "turn-id": "t", "cwd": "/",
"client": "codex_exec", "input-messages": ["hi"], "last-assistant-message": HIT_TEXT}
with patch.dict(os.environ, {"CATSTACK_CODEX_SESSIONS_DIR": os.path.dirname(os.path.dirname(os.path.dirname(day)))}):
err = run_codex_notify([json.dumps(payload)])
self.assertEqual(err, "")
[job] = self.wait_for_jobs(1)
with open(os.path.join(self.state.name, "jobs", job), encoding="utf-8") as handle:
self.assertEqual(json.load(handle)["transcript"], rollout)
queued = [r for r in self.stage_rows() if r["action"] == "judge_queued"]
self.assertEqual([(r["harness"], r["reason"]) for r in queued], [("codex", "transcript")])

def test_claude_malformed_stdin_fail_open(self):
err = io.StringIO()
with patch.object(sys, "stdin", io.StringIO("not-json")):
Expand Down
Loading
Loading