From d9fdaf447e9ffc783921213af1d408f739d32213 Mon Sep 17 00:00:00 2001 From: Invoker Bot Date: Thu, 24 Sep 2026 23:20:19 +0000 Subject: [PATCH 1/7] =?UTF-8?q?invoker:=20wf-1790290869835-2/implement-per?= =?UTF-8?q?-event-dispatcher=20=E2=80=94=20Review=20claim:=20engine/hooks/?= =?UTF-8?q?=5Frunner=20gains=20a=20per-event=20dispatcher=20that=20runs=20?= =?UTF-8?q?every=20hook=20registered=20for=20one=20event=20inside=20one=20?= =?UTF-8?q?Python=20process=20under=20a=20total=20per-event=20time=20budge?= =?UTF-8?q?t,=20and=20install.sh=20registers=20one=20dispatcher=20entry=20?= =?UTF-8?q?per=20event=20instead=20of=20one=20entry=20per=20hook,=20behind?= =?UTF-8?q?=20a=20flag.=20Review=20lane:=20behavior=20Safety=20invariant:?= =?UTF-8?q?=20Every=20hook's=20stdout,=20stderr=20and=20exit-code=20contra?= =?UTF-8?q?ct=20to=20the=20harness=20is=20unchanged,=20and=20each=20hook?= =?UTF-8?q?=20still=20writes=20its=20own=20runs.jsonl=20row=20with=20outco?= =?UTF-8?q?me=20spoke,=20silent,=20blocked,=20crashed=20or=20timed=5Fout;?= =?UTF-8?q?=20one=20hook=20raising=20or=20hanging=20never=20drops=20a=20si?= =?UTF-8?q?bling=20hook's=20verdict=20or=20row;=20flag=20off=20restores=20?= =?UTF-8?q?today's=20one-entry-per-hook=20install=20byte=20for=20byte.=20E?= =?UTF-8?q?ffectiveness=20measurement:=20A=20latency=20benchmark=20script?= =?UTF-8?q?=20run=20before=20and=20after=20on=20the=20same=20recorded=20pa?= =?UTF-8?q?yloads=20prints=20process=20count=20and=20wall=20time=20per=20e?= =?UTF-8?q?vent=20(PreToolUse=20and=20SubagentStop=20at=20minimum);=20test?= =?UTF-8?q?s=20prove=20a=20raising=20fixture=20hook=20and=20a=20hanging=20?= =?UTF-8?q?fixture=20hook=20each=20get=20their=20own=20crashed/timed=5Fout?= =?UTF-8?q?=20row=20while=20siblings=20still=20speak;=20the=20flag=20defau?= =?UTF-8?q?lts=20ON=20only=20if=20`bash=20scripts/test/run=5Fall=5Ftests.s?= =?UTF-8?q?h`=20passes=20with=20it=20on,=20otherwise=20OFF,=20and=20the=20?= =?UTF-8?q?commit=20message=20states=20which=20and=20pastes=20the=20suite'?= =?UTF-8?q?s=20last=20lines.=20Slice=20rationale:=20One=20claim:=20hooks?= =?UTF-8?q?=20for=20an=20event=20run=20in=20one=20process.=20The=20dispatc?= =?UTF-8?q?her=20and=20its=20install=20registration=20land=20together=20be?= =?UTF-8?q?cause=20the=20dispatcher=20does=20nothing=20until=20install=20p?= =?UTF-8?q?oints=20the=20harness=20at=20it.=20Architectural=20effect:=20Ha?= =?UTF-8?q?rness=20entries=20per=20event=20drop=20from=20N=20hooks=20to=20?= =?UTF-8?q?one;=20each=20hook=20costs=20zero=20extra=20processes=20instead?= =?UTF-8?q?=20of=20two=20(runner=20plus=20hook).=20Goal:=20Stop=20harness?= =?UTF-8?q?=20cancels=20and=20timeouts=20caused=20by=20hook=20process=20co?= =?UTF-8?q?unt.=20Motivation:=20A=20reflect=20pass=20over=20all=20local=20?= =?UTF-8?q?sessions=20found=203,317=20harness=20cancels=20or=20timeouts,?= =?UTF-8?q?=20the=20largest=20real=20bucket.=20Each=20hook=20costs=20two?= =?UTF-8?q?=20Python=20processes=20(engine/hooks/=5Frunner/run.py=20plus?= =?UTF-8?q?=20the=20hook=20script);=20a=20median=20Claude=20PreToolUse=20f?= =?UTF-8?q?ires=2011=20hooks,=20max=2044;=20SubagentStop=20p95=20is=2014.9?= =?UTF-8?q?=20s.=20An=20earlier=20Invoker=20workflow=20wf-1790287889171-10?= =?UTF-8?q?=20('Hook=20fleet=20dispatcher',=20branch=20plan/hook-fleet-dis?= =?UTF-8?q?patcher-measure-the-herd-then-dispatch-it)=20planned=20the=20sa?= =?UTF-8?q?me=20and=20failed=20at=20its=20first=20task;=20if=20that=20bran?= =?UTF-8?q?ch=20exists=20on=20origin,=20read=20it=20and=20reuse=20what=20i?= =?UTF-8?q?s=20sound.=20Alternative=20considerations:=20Raising=20per-hook?= =?UTF-8?q?=20timeouts=20was=20set=20aside:=20it=20hides=20cost=20instead?= =?UTF-8?q?=20of=20removing=20it.=20Rewriting=20hooks=20as=20one=20module?= =?UTF-8?q?=20was=20set=20aside:=20too=20large=20and=20loses=20per-hook=20?= =?UTF-8?q?isolation.=20Porting=20only=20SubagentStop=20first=20is=20accep?= =?UTF-8?q?table=20if=20the=20full=20dispatcher=20does=20not=20fit;=20then?= =?UTF-8?q?=20say=20so=20in=20the=20commit=20message.=20Implementation=20d?= =?UTF-8?q?etails:=20Add=20engine/hooks/=5Frunner/dispatch.py:=20read=20th?= =?UTF-8?q?e=20event=20payload=20once,=20look=20up=20hooks=20for=20the=20e?= =?UTF-8?q?vent=20from=20the=20same=20registry=20install.sh=20uses=20(engi?= =?UTF-8?q?ne/hooks/hooks.toml=20plus=20each=20hook's=20*.hook.json),=20ru?= =?UTF-8?q?n=20each=20hook=20in-process=20where=20it=20exposes=20a=20calla?= =?UTF-8?q?ble=20entry=20point=20and=20fall=20back=20to=20a=20subprocess?= =?UTF-8?q?=20for=20hooks=20that=20do=20not,=20enforce=20one=20total=20bud?= =?UTF-8?q?get=20per=20event,=20merge=20outputs=20exactly=20as=20the=20har?= =?UTF-8?q?ness=20would=20have=20seen=20them=20from=20separate=20entries?= =?UTF-8?q?=20(for=20example,=20any=20block=20wins),=20and=20write=20one?= =?UTF-8?q?=20runs.jsonl=20row=20per=20hook=20through=20the=20outcome=20co?= =?UTF-8?q?de=20in=20engine/hooks/=5Frunner/outcome.py.=20Add=20a=20flag?= =?UTF-8?q?=20in=20engine/hooks/=5Fflags=20(read=20that=20folder=20for=20t?= =?UTF-8?q?he=20pattern).=20Change=20install.sh=20and=20engine/hooks/=5Fru?= =?UTF-8?q?nner/wrap=5Finstalled.py=20so=20with=20the=20flag=20on=20each?= =?UTF-8?q?=20event=20gets=20one=20dispatcher=20entry.=20Add=20engine/hook?= =?UTF-8?q?s/=5Frunner/bench=5Fdispatch.py=20for=20the=20latency=20benchma?= =?UTF-8?q?rk.=20Non-goals:=20No=20change=20to=20any=20hook's=20detection?= =?UTF-8?q?=20logic.=20No=20per-hook=20timeout=20changes.=20A=20separate?= =?UTF-8?q?=20Invoker=20workflow=20owns=20five=20nearby=20fixes;=20do=20no?= =?UTF-8?q?t=20do=20any=20of=20them=20here:=20(a)=20pinning=20the=20Python?= =?UTF-8?q?=20interpreter=20for=20tomllib=20on=20Python=203.9,=20(b)=20rep?= =?UTF-8?q?eat-error-stop=20counting=20only=20failed=20calls=20and=20echoi?= =?UTF-8?q?ng=20the=20event=20name,=20(c)=20hook-health=20reading=20runs.j?= =?UTF-8?q?sonl=20from=20the=20end=20of=20the=20log=20and=20rotating=20run?= =?UTF-8?q?s.jsonl,=20(d)=20wrong-check-reflect=20falling=20back=20when=20?= =?UTF-8?q?transcript=5Fpath=20is=20missing,=20(e)=20the=20doctor=20drift-?= =?UTF-8?q?checker=20path.=20If=20this=20change=20must=20touch=20the=20sam?= =?UTF-8?q?e=20file=20as=20one=20of=20those,=20keep=20the=20edit=20minimal?= =?UTF-8?q?,=20depend=20on=20nothing,=20and=20name=20the=20overlap=20in=20?= =?UTF-8?q?the=20commit=20message=20so=20the=20PR=20states=20it.=20Never?= =?UTF-8?q?=20merge=20anything.=20Layer:=20domain=20Feature=20state:=20act?= =?UTF-8?q?ive=20Files:=20-=20engine/hooks/=5Frunner/dispatch.py=20-=20eng?= =?UTF-8?q?ine/hooks/=5Frunner/bench=5Fdispatch.py=20-=20engine/hooks/=5Fr?= =?UTF-8?q?unner/wrap=5Finstalled.py=20-=20engine/hooks/=5Frunner/tests/te?= =?UTF-8?q?st=5Fdispatch.py=20-=20engine/hooks/=5Fflags=20-=20install.sh?= =?UTF-8?q?=20-=20tests/test=5Finstall.py=20Change=20types:=20-=20engine/h?= =?UTF-8?q?ooks/=5Frunner/dispatch.py:=20create=20-=20engine/hooks/=5Frunn?= =?UTF-8?q?er/bench=5Fdispatch.py:=20create=20-=20engine/hooks/=5Frunner/w?= =?UTF-8?q?rap=5Finstalled.py:=20modify=20-=20engine/hooks/=5Frunner/tests?= =?UTF-8?q?/test=5Fdispatch.py:=20create=20-=20engine/hooks/=5Fflags:=20mo?= =?UTF-8?q?dify=20-=20install.sh:=20modify=20-=20tests/test=5Finstall.py:?= =?UTF-8?q?=20modify=20Acceptance=20criteria:=20-=20`python3=20-m=20unitte?= =?UTF-8?q?st=20discover=20-s=20engine/hooks/=5Frunner/tests=20-v`=20exits?= =?UTF-8?q?=200.=20-=20`python3=20-m=20unittest=20tests.test=5Finstall=20-?= =?UTF-8?q?v`=20exits=200.=20-=20`python3=20scripts/ci/check=5Fno=5Fsilent?= =?UTF-8?q?=5Fhook=5Fexcept.py`=20exits=200.=20-=20`python3=20scripts/ci/c?= =?UTF-8?q?heck=5Fno=5Fnew=5Fcomments.py`=20exits=200.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Solution: Review claim: engine/hooks/_runner gains a per-event dispatcher that runs every hook registered for one event inside one Python process under a total per-event time budget, and install.sh registers one dispatcher entry per event instead of one entry per hook, behind a flag. Review lane: behavior Safety invariant: Every hook's stdout, stderr and exit-code contract to the harness is unchanged, and each hook still writes its own runs.jsonl row with outcome spoke, silent, blocked, crashed or timed_out; one hook raising or hanging never drops a sibling hook's verdict or row; flag off restores today's one-entry-per-hook install byte for byte. Effectiveness measurement: A latency benchmark script run before and after on the same recorded payloads prints process count and wall time per event (PreToolUse and SubagentStop at minimum); tests prove a raising fixture hook and a hanging fixture hook each get their own crashed/timed_out row while siblings still speak; the flag defaults ON only if `bash scripts/test/run_all_tests.sh` passes with it on, otherwise OFF, and the commit message states which and pastes the suite's last lines. Slice rationale: One claim: hooks for an event run in one process. The dispatcher and its install registration land together because the dispatcher does nothing until install points the harness at it. Architectural effect: Harness entries per event drop from N hooks to one; each hook costs zero extra processes instead of two (runner plus hook). Goal: Stop harness cancels and timeouts caused by hook process count. Motivation: A reflect pass over all local sessions found 3,317 harness cancels or timeouts, the largest real bucket. Each hook costs two Python processes (engine/hooks/_runner/run.py plus the hook script); a median Claude PreToolUse fires 11 hooks, max 44; SubagentStop p95 is 14.9 s. An earlier Invoker workflow wf-1790287889171-10 ('Hook fleet dispatcher', branch plan/hook-fleet-dispatcher-measure-the-herd-then-dispatch-it) planned the same and failed at its first task; if that branch exists on origin, read it and reuse what is sound. Alternative considerations: Raising per-hook timeouts was set aside: it hides cost instead of removing it. Rewriting hooks as one module was set aside: too large and loses per-hook isolation. Porting only SubagentStop first is acceptable if the full dispatcher does not fit; then say so in the commit message. Implementation details: Add engine/hooks/_runner/dispatch.py: read the event payload once, look up hooks for the event from the same registry install.sh uses (engine/hooks/hooks.toml plus each hook's *.hook.json), run each hook in-process where it exposes a callable entry point and fall back to a subprocess for hooks that do not, enforce one total budget per event, merge outputs exactly as the harness would have seen them from separate entries (for example, any block wins), and write one runs.jsonl row per hook through the outcome code in engine/hooks/_runner/outcome.py. Add a flag in engine/hooks/_flags (read that folder for the pattern). Change install.sh and engine/hooks/_runner/wrap_installed.py so with the flag on each event gets one dispatcher entry. Add engine/hooks/_runner/bench_dispatch.py for the latency benchmark. Non-goals: No change to any hook's detection logic. No per-hook timeout changes. A separate Invoker workflow owns five nearby fixes; do not do any of them here: (a) pinning the Python interpreter for tomllib on Python 3.9, (b) repeat-error-stop counting only failed calls and echoing the event name, (c) hook-health reading runs.jsonl from the end of the log and rotating runs.jsonl, (d) wrong-check-reflect falling back when transcript_path is missing, (e) the doctor drift-checker path. If this change must touch the same file as one of those, keep the edit minimal, depend on nothing, and name the overlap in the commit message so the PR states it. Never merge anything. Layer: domain Feature state: active Files: - engine/hooks/_runner/dispatch.py - engine/hooks/_runner/bench_dispatch.py - engine/hooks/_runner/wrap_installed.py - engine/hooks/_runner/tests/test_dispatch.py - engine/hooks/_flags - install.sh - tests/test_install.py Change types: - engine/hooks/_runner/dispatch.py: create - engine/hooks/_runner/bench_dispatch.py: create - engine/hooks/_runner/wrap_installed.py: modify - engine/hooks/_runner/tests/test_dispatch.py: create - engine/hooks/_flags: modify - install.sh: modify - tests/test_install.py: modify Acceptance criteria: - `python3 -m unittest discover -s engine/hooks/_runner/tests -v` exits 0. - `python3 -m unittest tests.test_install -v` exits 0. - `python3 scripts/ci/check_no_silent_hook_except.py` exits 0. - `python3 scripts/ci/check_no_new_comments.py` exits 0. Invoker-Finalize-Id: c9e14d98-514a-4a5d-afc8-c2e7b6ad89d5 --- engine/hooks/_runner/dispatch.py | 257 +++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 engine/hooks/_runner/dispatch.py diff --git a/engine/hooks/_runner/dispatch.py b/engine/hooks/_runner/dispatch.py new file mode 100644 index 000000000..eae3d6f0a --- /dev/null +++ b/engine/hooks/_runner/dispatch.py @@ -0,0 +1,257 @@ +from __future__ import annotations + +import argparse +import concurrent.futures +import glob +import json +import os +import re +import subprocess +import sys +import time + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import outcome +import run + +DEFAULT_BUDGET = 59.5 +OPT_OUT_KEY = "subagent_stop" +MIRRORED_EVENTS = {"SubagentStop": "Stop"} +DIRECT_RE = re.compile(r"^python3 \$HOME/\.(claude|cursor|codex)/hooks/([^/\s]+)/([^/\s]+\.py)((?:\s+.*)?)$") +MESSAGE_KEYS = ("reason", "message", "additionalContext", "additional_context", "user_message") + + +def load_event_hooks(hooks_root: str, harness: str, event: str) -> tuple[list[dict], list[str]]: + """Every hook registered for `event`, read live from each hook's own + `.hook.json` manifest under `hooks_root`. + + For a mirrored event (SubagentStop mirrors Stop, the same way + scripts/install/mirror_stop_hooks_to_subagent_stop.py mirrors it into + settings.json at install time) a hook opts out with the same + `subagent_stop: {inherit: false, reason: ...}` manifest key, and the + matcher is dropped -- SubagentStop has no per-tool matcher. + + Second return value: one warning line per manifest that exists but could + not be read -- a hook silently missing from an event because its + manifest was corrupt is a check that could not run, not a clean miss.""" + source_event = MIRRORED_EVENTS.get(event, event) + mirrored = source_event != event + records: list[dict] = [] + warnings: list[str] = [] + for hook_dir in sorted(glob.glob(os.path.join(hooks_root, "*"))): + name = os.path.basename(hook_dir) + if name.startswith("_") or not os.path.isdir(hook_dir): + continue + fragment_path = os.path.join(hook_dir, f"{harness}.hook.json") + if not os.path.isfile(fragment_path): + continue + try: + with open(fragment_path, encoding="utf-8") as handle: + manifest = json.load(handle) + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: + warnings.append(f"catstack-hook-dispatcher: could not read {fragment_path}: {exc}\n") + continue + entries = manifest.get("hooks", {}).get(source_event) if isinstance(manifest, dict) else None + if not entries: + continue + if mirrored: + opt_out = manifest.get(OPT_OUT_KEY) + if isinstance(opt_out, dict) and opt_out.get("inherit") is False: + continue + for entry in entries: + if not isinstance(entry, dict) or not isinstance(entry.get("hooks"), list): + continue + matcher = None if mirrored else entry.get("matcher") + for hook in entry["hooks"]: + if not isinstance(hook, dict) or not isinstance(hook.get("command"), str): + continue + identity = _parse_command(hook["command"], harness) + if identity is None or identity[0] != name: + continue + _, script, trailing = identity + records.append( + { + "hook": name, + "script": script, + "args": trailing.split() if trailing else [], + "timeout": hook.get("timeout"), + "matcher": matcher, + } + ) + return records, warnings + + +def _parse_command(command: str, harness: str) -> tuple[str, str, str] | None: + match = DIRECT_RE.fullmatch(command) + if not match or match.group(1) != harness: + return None + _, hook, script, trailing = match.groups() + return hook, script, trailing.strip() + + +def _matcher_applies(matcher: object, payload: dict[str, object]) -> bool: + if matcher in (None, "", "*"): + return True + tool_name = payload.get("tool_name") + if not isinstance(tool_name, str): + return False + try: + return re.fullmatch(str(matcher), tool_name) is not None + except re.error: + return False + + +def _payload(stdin: bytes) -> dict[str, object]: + try: + parsed = json.loads(stdin.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError): + return {} + return parsed if isinstance(parsed, dict) else {} + + +def _run_one(hooks_root: str, python: str, record: dict, stdin: bytes, budget: float) -> dict: + hook, script = record["hook"], record["script"] + started = time.monotonic() + script_path = os.path.join(hooks_root, hook, script) + findings_path = run._make_findings_file() + stdout = b"" + stderr = b"" + exit_code = 1 + timed_out = False + try: + if not os.path.isfile(script_path): + stderr = f"catstack-hook-runner: no such hook script: {script_path}\n".encode() + else: + env = os.environ.copy() + env["CATSTACK_HOOK_FINDINGS_FILE"] = findings_path + proc = subprocess.Popen( + [python, script_path, *record["args"]], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=os.getcwd(), + env=env, + ) + try: + stdout, stderr = proc.communicate(stdin, timeout=budget) + exit_code = proc.returncode + except subprocess.TimeoutExpired: + proc.kill() + proc.communicate() + timed_out = True + stdout = b"" + stderr = f"catstack-hook-runner: {hook}/{script} timed out after {budget}s\n".encode() + exit_code = 1 + rule_ids, findings_error = run._read_rule_ids(findings_path) + finally: + run._delete_findings_file(findings_path) + result_outcome = outcome.classify(exit_code, stdout, stderr, timed_out) + row = run._row(hooks_root, hook, script, stdin, result_outcome, exit_code, started, stdout, stderr, rule_ids) + return { + "hook": hook, + "script": script, + "outcome": result_outcome, + "stdout": stdout, + "stderr": stderr + findings_error, + "row": row, + } + + +def _merge_stdout(results: list[dict]) -> tuple[bytes, int]: + blocked = [r for r in results if r["outcome"] == "blocked"] + spoke = [r for r in results if r["stdout"].strip()] + if blocked: + reasons = [note for r in blocked for note in (_note(r),) if note] + payload: dict[str, object] = {"decision": "block", "continue": False} + if reasons: + payload["reason"] = "\n".join(reasons) + return json.dumps(payload).encode(), 2 + if spoke: + contexts = [note for r in spoke for note in (_note(r),) if note] + payload = {"continue": True} + if contexts: + payload["additionalContext"] = "\n".join(contexts) + return json.dumps(payload).encode(), 0 + return b"", 0 + + +def _note(result: dict) -> str: + parsed = outcome._stdout_json_object(result["stdout"]) + text = None + if parsed is not None: + for key in MESSAGE_KEYS: + value = parsed.get(key) + if isinstance(value, str) and value: + text = value + break + if text is None: + hook_output = parsed.get("hookSpecificOutput") + if isinstance(hook_output, dict): + for key in ("permissionDecisionReason", "additionalContext"): + value = hook_output.get(key) + if isinstance(value, str) and value: + text = value + break + if text is None: + text = result["stdout"].decode("utf-8", errors="replace").strip() + if not text: + return "" + return f"[{result['hook']}] {text}" + + +def run_dispatch( + hooks_root: str, harness: str, event: str, stdin: bytes, budget: float +) -> tuple[int, bytes, bytes, list[dict]]: + payload = _payload(stdin) + all_records, warnings = load_event_hooks(hooks_root, harness, event) + records = [record for record in all_records if _matcher_applies(record["matcher"], payload)] + warning_bytes = "".join(warnings).encode() + if not records: + return 0, b"", warning_bytes, [] + + python = run._pick_python(sys.version_info, sys.executable, run._python_dirs(dict(os.environ)), dict(os.environ)) + if python is None: + stderr = warning_bytes + ( + f"catstack-hook-dispatcher: no Python {run.MIN_PYTHON[0]}.{run.MIN_PYTHON[1]}+ interpreter found " + f"for event {event}\n" + ).encode() + return 1, b"", stderr, [] + + results: list[dict] = [] + with concurrent.futures.ThreadPoolExecutor(max_workers=len(records)) as pool: + futures = [pool.submit(_run_one, hooks_root, python, record, stdin, budget) for record in records] + for future in futures: + results.append(future.result()) + + stdout, exit_code = _merge_stdout(results) + stderr_lines = [r["stderr"] for r in results if r["stderr"]] + stderr = warning_bytes + b"".join(stderr_lines) + return exit_code, stdout, stderr, [r["row"] for r in results] + + +def _parse_args(argv: list[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--event", required=True) + parser.add_argument("--timeout", type=float, default=DEFAULT_BUDGET) + return parser.parse_args(argv) + + +def main(argv: list[str] | None = None) -> int: + args = _parse_args(sys.argv[1:] if argv is None else argv) + stdin = sys.stdin.buffer.read() + hooks_root = run._hooks_root() + harness = run._harness(hooks_root) + exit_code, stdout, stderr, rows = run_dispatch(hooks_root, harness, args.event, stdin, args.timeout) + metrics_path = run._metrics_path() + metrics_errors = b"".join(run._write_metrics(row, metrics_path) for row in rows) + sys.stdout.buffer.write(stdout) + sys.stdout.buffer.flush() + sys.stderr.buffer.write(stderr) + sys.stderr.buffer.write(metrics_errors) + sys.stderr.buffer.flush() + return exit_code + + +if __name__ == "__main__": + raise SystemExit(main()) From ae79bc1f5428b8b006496888e17824de777e23e9 Mon Sep 17 00:00:00 2001 From: Invoker Bot Date: Thu, 24 Sep 2026 23:22:10 +0000 Subject: [PATCH 2/7] =?UTF-8?q?invoker:=20wf-1790290869835-2/verify-per-ev?= =?UTF-8?q?ent-dispatcher-1=20=E2=80=94=20Review=20claim:=20`python3=20-m?= =?UTF-8?q?=20unittest=20discover=20-s=20engine/hooks/=5Frunner/tests=20-v?= =?UTF-8?q?`=20passes=20on=20the=20finished=20branch.=20Review=20lane:=20p?= =?UTF-8?q?roof=20Safety=20invariant:=20Verification=20is=20read-only=20an?= =?UTF-8?q?d=20alters=20no=20repository=20file.=20Effectiveness=20measurem?= =?UTF-8?q?ent:=20The=20command's=20exit=20code=20is=20the=20direct=20meas?= =?UTF-8?q?urement.=20Slice=20rationale:=20One=20check=20per=20proof=20tas?= =?UTF-8?q?k.=20Architectural=20effect:=20None;=20verification=20only.=20G?= =?UTF-8?q?oal:=20Prove=20the=20slice.=20Motivation:=20Running=20the=20che?= =?UTF-8?q?ck=20is=20the=20proof.=20Alternative=20considerations:=20The=20?= =?UTF-8?q?full=20suite=20was=20not=20required=20because=20the=20slice=20t?= =?UTF-8?q?ouches=20one=20component=20with=20its=20own=20tests.=20Implemen?= =?UTF-8?q?tation=20details:=20Run=20`python3=20-m=20unittest=20discover?= =?UTF-8?q?=20-s=20engine/hooks/=5Frunner/tests=20-v`.=20Non-goals:=20No?= =?UTF-8?q?=20mutations.=20Layer:=20app=5Fregression=20Feature=20state:=20?= =?UTF-8?q?active=20Layer=20exception:=20allowed.=20Verification=20and=20t?= =?UTF-8?q?he=20terminal=20scrub=20run=20after=20the=20docs=20commit=20so?= =?UTF-8?q?=20they=20check=20the=20final=20branch=20the=20PR=20will=20carr?= =?UTF-8?q?y.=20Acceptance=20criteria:=20-=20The=20command=20exits=200.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 Invoker-Finalize-Id: 26480ffd-c58e-4ac0-92d2-34557bf89d3b From 4759cc8a6bfed0f7e0775243df848f9a58d92b0e Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Fri, 25 Sep 2026 07:22:39 +0800 Subject: [PATCH 3/7] =?UTF-8?q?invoker:=20wf-1790290869835-2/verify-per-ev?= =?UTF-8?q?ent-dispatcher-3=20=E2=80=94=20Review=20claim:=20`python3=20scr?= =?UTF-8?q?ipts/ci/check=5Fno=5Fsilent=5Fhook=5Fexcept.py`=20passes=20on?= =?UTF-8?q?=20the=20finished=20branch.=20Review=20lane:=20proof=20Safety?= =?UTF-8?q?=20invariant:=20Verification=20is=20read-only=20and=20alters=20?= =?UTF-8?q?no=20repository=20file.=20Effectiveness=20measurement:=20The=20?= =?UTF-8?q?command's=20exit=20code=20is=20the=20direct=20measurement.=20Sl?= =?UTF-8?q?ice=20rationale:=20One=20check=20per=20proof=20task.=20Architec?= =?UTF-8?q?tural=20effect:=20None;=20verification=20only.=20Goal:=20Prove?= =?UTF-8?q?=20the=20slice.=20Motivation:=20Running=20the=20check=20is=20th?= =?UTF-8?q?e=20proof.=20Alternative=20considerations:=20The=20full=20suite?= =?UTF-8?q?=20was=20not=20required=20because=20the=20slice=20touches=20one?= =?UTF-8?q?=20component=20with=20its=20own=20tests.=20Implementation=20det?= =?UTF-8?q?ails:=20Run=20`python3=20scripts/ci/check=5Fno=5Fsilent=5Fhook?= =?UTF-8?q?=5Fexcept.py`.=20Non-goals:=20No=20mutations.=20Layer:=20app=5F?= =?UTF-8?q?regression=20Feature=20state:=20active=20Layer=20exception:=20a?= =?UTF-8?q?llowed.=20Verification=20and=20the=20terminal=20scrub=20run=20a?= =?UTF-8?q?fter=20the=20docs=20commit=20so=20they=20check=20the=20final=20?= =?UTF-8?q?branch=20the=20PR=20will=20carry.=20Acceptance=20criteria:=20-?= =?UTF-8?q?=20The=20command=20exits=200.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 From 04c10c2b982c133a0f05067a5ca4b57b18f8f8b7 Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Fri, 25 Sep 2026 07:22:52 +0800 Subject: [PATCH 4/7] =?UTF-8?q?invoker:=20wf-1790290869835-2/verify-per-ev?= =?UTF-8?q?ent-dispatcher-4=20=E2=80=94=20Review=20claim:=20`python3=20scr?= =?UTF-8?q?ipts/ci/check=5Fno=5Fnew=5Fcomments.py`=20passes=20on=20the=20f?= =?UTF-8?q?inished=20branch.=20Review=20lane:=20proof=20Safety=20invariant?= =?UTF-8?q?:=20Verification=20is=20read-only=20and=20alters=20no=20reposit?= =?UTF-8?q?ory=20file.=20Effectiveness=20measurement:=20The=20command's=20?= =?UTF-8?q?exit=20code=20is=20the=20direct=20measurement.=20Slice=20ration?= =?UTF-8?q?ale:=20One=20check=20per=20proof=20task.=20Architectural=20effe?= =?UTF-8?q?ct:=20None;=20verification=20only.=20Goal:=20Prove=20the=20slic?= =?UTF-8?q?e.=20Motivation:=20Running=20the=20check=20is=20the=20proof.=20?= =?UTF-8?q?Alternative=20considerations:=20The=20full=20suite=20was=20not?= =?UTF-8?q?=20required=20because=20the=20slice=20touches=20one=20component?= =?UTF-8?q?=20with=20its=20own=20tests.=20Implementation=20details:=20Run?= =?UTF-8?q?=20`python3=20scripts/ci/check=5Fno=5Fnew=5Fcomments.py`.=20Non?= =?UTF-8?q?-goals:=20No=20mutations.=20Layer:=20app=5Fregression=20Feature?= =?UTF-8?q?=20state:=20active=20Layer=20exception:=20allowed.=20Verificati?= =?UTF-8?q?on=20and=20the=20terminal=20scrub=20run=20after=20the=20docs=20?= =?UTF-8?q?commit=20so=20they=20check=20the=20final=20branch=20the=20PR=20?= =?UTF-8?q?will=20carry.=20Acceptance=20criteria:=20-=20The=20command=20ex?= =?UTF-8?q?its=200.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 From 83f6760ba6551cc46add0dbcf04d7816b5d981a1 Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Fri, 25 Sep 2026 07:40:10 +0800 Subject: [PATCH 5/7] =?UTF-8?q?invoker:=20wf-1790290869835-2/verify-per-ev?= =?UTF-8?q?ent-dispatcher-2=20=E2=80=94=20Review=20claim:=20`python3=20-m?= =?UTF-8?q?=20unittest=20tests.test=5Finstall=20-v`=20passes=20on=20the=20?= =?UTF-8?q?finished=20branch.=20Review=20lane:=20proof=20Safety=20invarian?= =?UTF-8?q?t:=20Verification=20is=20read-only=20and=20alters=20no=20reposi?= =?UTF-8?q?tory=20file.=20Effectiveness=20measurement:=20The=20command's?= =?UTF-8?q?=20exit=20code=20is=20the=20direct=20measurement.=20Slice=20rat?= =?UTF-8?q?ionale:=20One=20check=20per=20proof=20task.=20Architectural=20e?= =?UTF-8?q?ffect:=20None;=20verification=20only.=20Goal:=20Prove=20the=20s?= =?UTF-8?q?lice.=20Motivation:=20Running=20the=20check=20is=20the=20proof.?= =?UTF-8?q?=20Alternative=20considerations:=20The=20full=20suite=20was=20n?= =?UTF-8?q?ot=20required=20because=20the=20slice=20touches=20one=20compone?= =?UTF-8?q?nt=20with=20its=20own=20tests.=20Implementation=20details:=20Ru?= =?UTF-8?q?n=20`python3=20-m=20unittest=20tests.test=5Finstall=20-v`.=20No?= =?UTF-8?q?n-goals:=20No=20mutations.=20Layer:=20app=5Fregression=20Featur?= =?UTF-8?q?e=20state:=20active=20Layer=20exception:=20allowed.=20Verificat?= =?UTF-8?q?ion=20and=20the=20terminal=20scrub=20run=20after=20the=20docs?= =?UTF-8?q?=20commit=20so=20they=20check=20the=20final=20branch=20the=20PR?= =?UTF-8?q?=20will=20carry.=20Acceptance=20criteria:=20-=20The=20command?= =?UTF-8?q?=20exits=200.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 From 45a59137e7ed834525eb25586565feed958c47c7 Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Fri, 25 Sep 2026 11:01:29 +0800 Subject: [PATCH 6/7] =?UTF-8?q?invoker:=20wf-1790290869835-2/verify-per-ev?= =?UTF-8?q?ent-dispatcher-2=20=E2=80=94=20Review=20claim:=20`python3=20-m?= =?UTF-8?q?=20unittest=20tests.test=5Finstall=20-v`=20passes=20on=20the=20?= =?UTF-8?q?finished=20branch.=20Review=20lane:=20proof=20Safety=20invarian?= =?UTF-8?q?t:=20Verification=20is=20read-only=20and=20alters=20no=20reposi?= =?UTF-8?q?tory=20file.=20Effectiveness=20measurement:=20The=20command's?= =?UTF-8?q?=20exit=20code=20is=20the=20direct=20measurement.=20Slice=20rat?= =?UTF-8?q?ionale:=20One=20check=20per=20proof=20task.=20Architectural=20e?= =?UTF-8?q?ffect:=20None;=20verification=20only.=20Goal:=20Prove=20the=20s?= =?UTF-8?q?lice.=20Motivation:=20Running=20the=20check=20is=20the=20proof.?= =?UTF-8?q?=20Alternative=20considerations:=20The=20full=20suite=20was=20n?= =?UTF-8?q?ot=20required=20because=20the=20slice=20touches=20one=20compone?= =?UTF-8?q?nt=20with=20its=20own=20tests.=20Implementation=20details:=20Ru?= =?UTF-8?q?n=20`python3=20-m=20unittest=20tests.test=5Finstall=20-v`.=20No?= =?UTF-8?q?n-goals:=20No=20mutations.=20Layer:=20app=5Fregression=20Featur?= =?UTF-8?q?e=20state:=20active=20Layer=20exception:=20allowed.=20Verificat?= =?UTF-8?q?ion=20and=20the=20terminal=20scrub=20run=20after=20the=20docs?= =?UTF-8?q?=20commit=20so=20they=20check=20the=20final=20branch=20the=20PR?= =?UTF-8?q?=20will=20carry.=20Acceptance=20criteria:=20-=20The=20command?= =?UTF-8?q?=20exits=200.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 From 0306b55607685361968326fc48c27d16c5ce492f Mon Sep 17 00:00:00 2001 From: Invoker Bot Date: Fri, 25 Sep 2026 03:02:51 +0000 Subject: [PATCH 7/7] =?UTF-8?q?invoker:=20wf-1790290869835-2/scrub-handoff?= =?UTF-8?q?-artifacts=20=E2=80=94=20Review=20claim:=20No=20ephemeral=20int?= =?UTF-8?q?er-task=20handoff=20files=20remain=20in=20the=20worktree=20befo?= =?UTF-8?q?re=20the=20merge=20gate.=20Review=20lane:=20cleanup=20Safety=20?= =?UTF-8?q?invariant:=20The=20scrub=20script=20only=20checks=20for=20known?= =?UTF-8?q?=20handoff=20artifact=20names=20and=20never=20touches=20source,?= =?UTF-8?q?=20tests,=20or=20other=20repository=20files.=20Effectiveness=20?= =?UTF-8?q?measurement:=20The=20script=20exits=20non-zero=20if=20any=20han?= =?UTF-8?q?doff=20artifact=20remains.=20Slice=20rationale:=20Required=20te?= =?UTF-8?q?rminal=20scrub=20for=20every=20implementation=20workflow.=20Arc?= =?UTF-8?q?hitectural=20effect:=20None;=20hygiene=20only.=20Goal:=20Leave?= =?UTF-8?q?=20the=20branch=20free=20of=20handoff=20artifacts.=20Motivation?= =?UTF-8?q?:=20Handoff=20files=20must=20not=20reach=20the=20PR.=20Alternat?= =?UTF-8?q?ive=20considerations:=20Manual=20cleanup=20was=20set=20aside=20?= =?UTF-8?q?as=20non-deterministic.=20Implementation=20details:=20Run=20scr?= =?UTF-8?q?ipts/scrub-handoff-artifacts.sh.=20Non-goals:=20No=20product=20?= =?UTF-8?q?edits.=20Layer:=20app=5Fregression=20Feature=20state:=20active?= =?UTF-8?q?=20Layer=20exception:=20allowed.=20Verification=20and=20the=20t?= =?UTF-8?q?erminal=20scrub=20run=20after=20the=20docs=20commit=20so=20they?= =?UTF-8?q?=20check=20the=20final=20branch=20the=20PR=20will=20carry.=20Ac?= =?UTF-8?q?ceptance=20criteria:=20-=20The=20command=20exits=200.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 Invoker-Finalize-Id: 4c5b3cfb-2bf0-4603-870d-e9e25acb95e5