From d74c84d77df41d9608a7672a3fc9bd8494724d5d Mon Sep 17 00:00:00 2001 From: Invoker Date: Sat, 12 Sep 2026 07:05:35 +0000 Subject: [PATCH 1/4] Count intervention slash commands in token audit --- .../reflect/scripts/tests/test_token_audit.py | 68 ++++++++++ engine/skills/reflect/scripts/token_audit.py | 123 +++++++++++++++--- 2 files changed, 170 insertions(+), 21 deletions(-) diff --git a/engine/skills/reflect/scripts/tests/test_token_audit.py b/engine/skills/reflect/scripts/tests/test_token_audit.py index 9944f3bf..8764005a 100644 --- a/engine/skills/reflect/scripts/tests/test_token_audit.py +++ b/engine/skills/reflect/scripts/tests/test_token_audit.py @@ -12,6 +12,7 @@ import os import sys import tempfile +import time import unittest from contextlib import redirect_stdout @@ -1202,6 +1203,10 @@ def claude_user_text_line(text, ts=None): return d +def claude_command_name_line(name, ts=None): + return claude_user_text_line(f"{name}", ts=ts) + + class TestFrustrationSignals(unittest.TestCase): """Frustration-lens feed: mechanical tone-spike detection over HUMAN user messages only. Added after a real session where 13/56 user messages were @@ -1445,6 +1450,59 @@ def test_agent_blame_and_same_type_must_automate(self): finally: os.unlink(path) + def test_two_intervention_command_names_must_automate(self): + usage = {"input_tokens": 1, "output_tokens": 1} + lines = [ + claude_command_name_line("/automate-me", ts="2026-09-11T01:00:00Z"), + claude_command_name_line("/thrash", ts="2026-09-11T01:01:00Z"), + claude_assistant_line("m1", "u1", [{"type": "text", "text": "ok"}], usage), + ] + path = write_jsonl(lines) + try: + with redirect_stdout(io.StringIO()): + result = token_audit.audit_claude(path) + flags = {f["name"]: f for f in result["flags"]} + self.assertEqual(result["frustration"]["intervention_command_count"], 2) + self.assertEqual(flags["intervention-must-automate"]["value"], "yes") + self.assertEqual(flags["intervention-must-automate"]["count"], 2) + self.assertIn("intervention_commands=2", flags["frustration-signals"]["rationale"]) + finally: + os.unlink(path) + + def test_one_intervention_command_name_does_not_must_automate(self): + usage = {"input_tokens": 1, "output_tokens": 1} + lines = [ + claude_command_name_line("/automate-me", ts="2026-09-11T01:00:00Z"), + claude_assistant_line("m1", "u1", [{"type": "text", "text": "ok"}], usage), + ] + path = write_jsonl(lines) + try: + with redirect_stdout(io.StringIO()): + result = token_audit.audit_claude(path) + flags = {f["name"]: f for f in result["flags"]} + self.assertEqual(result["frustration"]["intervention_command_count"], 1) + self.assertEqual(flags["intervention-must-automate"]["value"], "no") + self.assertEqual(flags["intervention-must-automate"]["count"], 1) + finally: + os.unlink(path) + + def test_zero_intervention_command_names_does_not_must_automate(self): + usage = {"input_tokens": 1, "output_tokens": 1} + lines = [ + claude_user_text_line("please inspect the failing test", ts="2026-09-11T01:00:00Z"), + claude_assistant_line("m1", "u1", [{"type": "text", "text": "ok"}], usage), + ] + path = write_jsonl(lines) + try: + with redirect_stdout(io.StringIO()): + result = token_audit.audit_claude(path) + flags = {f["name"]: f for f in result["flags"]} + self.assertEqual(result["frustration"].get("intervention_command_count", 0), 0) + self.assertEqual(flags["intervention-must-automate"]["value"], "no") + self.assertEqual(flags["intervention-must-automate"]["count"], 0) + finally: + os.unlink(path) + def claude_queued_line(text, ts=None): d = {"type": "queue-operation", "operation": "enqueue", "content": text} @@ -1826,6 +1884,16 @@ def test_out_json_carries_subagents_section(self): finally: os.unlink(out.name) + def test_subagent_modified_after_audit_start_reports_unchecked(self): + future = time.time() + 60 + os.utime(self.agent_b, (future, future)) + res, out = self._audit() + self.assertTrue(res["subagents"]["unchecked"]) + self.assertIsNone(res["combined_total"]) + self.assertIn("subagents: unchecked", out) + flags = {f["name"]: f for f in res["flags"]} + self.assertEqual(flags["subagent-thrash"]["value"], "unchecked") + def test_opt_out_flag_skips_subagents(self): res, out = self._audit(include_subagents=False) self.assertIsNone(res["subagents"]) diff --git a/engine/skills/reflect/scripts/token_audit.py b/engine/skills/reflect/scripts/token_audit.py index 0004d8f0..bfe5563a 100644 --- a/engine/skills/reflect/scripts/token_audit.py +++ b/engine/skills/reflect/scripts/token_audit.py @@ -48,7 +48,7 @@ running an audit against a remote host is a separate, explicitly-confirmed step outside this script. """ -import bisect, json, sys, hashlib, os, re +import bisect, json, sys, hashlib, os, re, time from datetime import datetime from collections import Counter @@ -151,6 +151,8 @@ def _direct_run_targets(command): INTERVENTION_KINDS = frozenset({ "told-you", "accusation", "agent-blame", "restated-ask", "proof-challenge", }) +INTERVENTION_COMMAND_NAMES = frozenset({"/automate-me", "/thrash"}) +_CLAUDE_COMMAND_NAME_RE = re.compile(r"\s*(?P[^<]+?)\s*", re.DOTALL) # function_call_output / custom_tool_call_output payloads carry their exit # status as prose ("Process exited with code 1" for exec_command, @@ -243,6 +245,26 @@ def _is_api_error_line(row): return bool(row.get("isApiErrorMessage") or row.get("error")) +def _claude_intervention_command_counts(path, rows): + if "/subagents/" in path.replace("\\", "/"): + return Counter() + counts = Counter() + for row in rows: + if row.get("type") != "user" or row.get("agentId") or row.get("isSidechain"): + continue + message = row.get("message") + if not isinstance(message, dict) or message.get("role") != "user": + continue + text = transcript_provenance._text_from_content(message.get("content")) + for match in _CLAUDE_COMMAND_NAME_RE.finditer(text): + name = match.group("name").strip() + if name and not name.startswith("/"): + name = "/" + name + if name in INTERVENTION_COMMAND_NAMES: + counts[name] += 1 + return counts + + def _has_index_between(sorted_indices, prev_idx, curr_idx): """True if any element of sorted_indices falls strictly between prev_idx and curr_idx (exclusive on both ends). bisect keeps this O(log n) on @@ -326,10 +348,13 @@ def intervention_must_automate(frustration): the same class, two intervention kinds, or a verbatim re-send is the automate trigger. Returns (yes, count, rationale); yes and count are None when no human text could be classified.""" - if frustration.get("count") is None: + command_count = frustration.get("intervention_command_count", 0) + if frustration.get("count") is None and not command_count: return None, None, frustration["rationale"] kinds = frustration.get("kinds") or {} reasons = [] + if command_count >= 2: + reasons.append(f"intervention-commandsx{command_count}") if kinds.get("verbatim-repeat", 0): reasons.append("verbatim-repeat") for k in sorted(INTERVENTION_KINDS): @@ -340,11 +365,15 @@ def intervention_must_automate(frustration): if len(distinct) >= 2: reasons.append("+".join(distinct)) yes = bool(reasons) - count = sum(kinds.get(k, 0) for k in INTERVENTION_KINDS) + kinds.get("verbatim-repeat", 0) + count = ( + sum(kinds.get(k, 0) for k in INTERVENTION_KINDS) + + kinds.get("verbatim-repeat", 0) + + command_count + ) rationale = ( "same-type complaint / iteration: " + ", ".join(reasons) if yes else - "no repeated intervention class (one correction is not automate-me)" + f"no repeated intervention class (intervention commands={command_count}; one correction is not automate-me)" ) return yes, count, rationale @@ -380,22 +409,39 @@ def _self_retraction_flag(hits): def _frustration_flags(frustration): yes, count, rationale = intervention_must_automate(frustration) + command_count = frustration.get("intervention_command_count", 0) + command_counts = frustration.get("intervention_commands") or {} if yes is None: + unchecked_rationale = ( + f"{rationale}; intervention_commands={command_count} {command_counts}" + ) return [ - _flag(name, "unchecked", None, rationale) - for name in ("frustration-signals", "intervention-must-automate") + _flag("frustration-signals", "unchecked", None, unchecked_rationale), + _flag("intervention-must-automate", "unchecked", None, unchecked_rationale), ] + frustration_value = ( + "unchecked" if frustration["count"] is None + else ("yes" if frustration["count"] else "no") + ) + frustration_count = frustration["count"] + if frustration["count"] is None: + frustration_rationale = ( + f"{frustration['rationale']}; intervention_commands={command_count} {command_counts}" + ) + else: + frustration_rationale = ( + f"{frustration['count']}/{frustration['n_user_messages']} user messages flagged " + f"({frustration['kinds']}; intervention_commands={command_count} {command_counts}); " + f"interruptions={frustration['interruptions']}" + + (f"; peak window {frustration['peak_window'][0]} -> {frustration['peak_window'][1]}" + if frustration["peak_window"] else "") + ) return [ _flag( "frustration-signals", - "yes" if frustration["count"] else "no", - frustration["count"], - ( - f"{frustration['count']}/{frustration['n_user_messages']} user messages flagged " - f"({frustration['kinds']}); interruptions={frustration['interruptions']}" - + (f"; peak window {frustration['peak_window'][0]} -> {frustration['peak_window'][1]}" - if frustration["peak_window"] else "") - ), + frustration_value, + frustration_count, + frustration_rationale, ), _flag( "intervention-must-automate", @@ -408,15 +454,21 @@ def _frustration_flags(frustration): def _print_frustration(frustration, *, details=True): + flags = _frustration_flags(frustration) if frustration["count"] is None: - for flag in _frustration_flags(frustration): - print(f"{flag['name']}: unchecked (count=None) {flag['rationale']}") + print(f"{flags[0]['name']}: unchecked (count=None) {flags[0]['rationale']}") + print(f"{flags[1]['name']}: {flags[1]['value']} (count={flags[1]['count']}) {flags[1]['rationale']}") return if details: print("-- frustration signals (user tone spikes; feed for the Frustration lens) --") for f_ in frustration["flagged"]: print(f" [{f_['index']}] {f_['ts']} {f_['kinds']}: {f_['excerpt']!r}") - summary = f"frustration-flagged user messages: {frustration['count']}/{frustration['n_user_messages']}" + command_count = frustration.get("intervention_command_count", 0) + command_counts = frustration.get("intervention_commands") or {} + summary = ( + f"frustration-flagged user messages: {frustration['count']}/{frustration['n_user_messages']} " + f"(kinds={frustration['kinds']}; intervention_commands={command_count} {command_counts})" + ) if details: summary += f"; interruptions: {frustration['interruptions']}" if frustration["peak_window"]: @@ -479,7 +531,7 @@ def _subagent_transcripts(path): ) -def audit_subagents(path): +def audit_subagents(path, audit_started_at=None): """Audit every subagent transcript of the session at `path` and fold the numbers into one section attributed to that parent session. Human frustration / intervention flags are deliberately not aggregated: the @@ -504,6 +556,20 @@ def audit_subagents(path): } rows = [] human_messages = 0 + if audit_started_at is not None: + active_files = [f for f in files if os.path.getmtime(f) >= audit_started_at] + if active_files: + return { + "count": len(files), + "files": files, + "unchecked": True, + "rationale": "subagent transcript file was modified at or after audit start; totals may be partial", + "active_files": active_files, + "totals": totals, + "top": rows, + "thrash": thrash, + "human_messages": human_messages, + } for agent_path in files: with redirect_stdout(StringIO()): stats = audit_claude(agent_path, include_subagents=False) @@ -545,6 +611,7 @@ def audit_subagents(path): return { "count": len(rows), "files": files, + "unchecked": False, "totals": totals, "top": rows[:5], "thrash": thrash, @@ -553,6 +620,8 @@ def audit_subagents(path): def _subagent_thrash_flag(subagents): + if subagents and subagents.get("unchecked"): + return _flag("subagent-thrash", "unchecked", None, subagents["rationale"]) n = len(subagents["thrash"]["by_agent"]) if subagents else 0 return _flag( "subagent-thrash", @@ -590,8 +659,10 @@ def audit_claude(path, out_path=None, include_subagents=True): first-seen order, and msg_first_seq to the tool_use seq at which the message first appeared. """ + audit_started_at = time.time() USAGE_FIELDS = ("input_tokens", "output_tokens", "cache_read_input_tokens", "cache_creation_input_tokens") lines = read_jsonl(path) + intervention_commands = _claude_intervention_command_counts(path, lines) msg_usage = {} msg_first_seq = {} models = Counter() @@ -764,6 +835,9 @@ def audit_claude(path, out_path=None, include_subagents=True): frustration = frustration_signals( user_msgs, n_interruptions, failed_turn_indices=failed_turn_indices ) + if intervention_commands: + frustration["intervention_commands"] = dict(intervention_commands) + frustration["intervention_command_count"] = sum(intervention_commands.values()) flags = [ _flag( @@ -816,9 +890,9 @@ def audit_claude(path, out_path=None, include_subagents=True): flags.extend(_frustration_flags(frustration)) retraction_hits = self_retraction_hits(assistant_texts) flags.append(_self_retraction_flag(retraction_hits)) - subagents = audit_subagents(path) if include_subagents else None + subagents = audit_subagents(path, audit_started_at=audit_started_at) if include_subagents else None flags.append(_subagent_thrash_flag(subagents)) - combined_total = grand + (subagents["totals"]["total"] if subagents else 0) + combined_total = None if subagents and subagents.get("unchecked") else grand + (subagents["totals"]["total"] if subagents else 0) redundant_read_files = sorted({fp for fp, _, _, _ in redundant}) recurring_failure_details = [ @@ -875,7 +949,9 @@ def audit_claude(path, out_path=None, include_subagents=True): print(f"=== CLAUDE CODE token audit: {os.path.basename(path)} ===") print(f"report: {out_path}") print(f"total={grand:,} turns={n_assistant} errors={len(errors)}") - if subagents: + if subagents and subagents.get("unchecked"): + print(f"subagents=unchecked reason={subagents['rationale']}") + elif subagents: print(f"subagents={subagents['count']} subagent_total={subagents['totals']['total']:,} " f"combined_total={combined_total:,}") for fl in flags: @@ -956,6 +1032,11 @@ def audit_claude(path, out_path=None, include_subagents=True): def _print_subagents_section(subagents, combined_total): print("-- subagents (Task-tool fan-out; tokens and thrash belong to this session) --") + if subagents.get("unchecked"): + print(f"subagents: unchecked (count=None) {subagents['rationale']}") + for path in subagents.get("active_files", []): + print(f" active: {os.path.basename(path)}") + return print(f"subagents (attributed to this session): {subagents['count']}") if not subagents["count"]: return From a5398dfb38649a3e93219ebe798b122c760a149e Mon Sep 17 00:00:00 2001 From: Invoker Bot Date: Sat, 12 Sep 2026 07:09:51 +0000 Subject: [PATCH 2/4] =?UTF-8?q?invoker:=20wf-1789196449634-14/implement-in?= =?UTF-8?q?tervention-count=20=E2=80=94=20Review=20claim:=20token=5Faudit.?= =?UTF-8?q?py=20reports=20must-automate=20when=20a=20session=20carries=20t?= =?UTF-8?q?wo=20or=20more=20intervention-class=20slash=20commands,=20and?= =?UTF-8?q?=20reports=20its=20subagent=20totals=20as=20unchecked=20when=20?= =?UTF-8?q?a=20subagent=20transcript=20was=20written=20during=20the=20audi?= =?UTF-8?q?t.=20Review=20lane:=20behavior=20Safety=20invariant:=20Read-onl?= =?UTF-8?q?y=20auditor.=20Existing=20counts=20and=20flags=20keep=20their?= =?UTF-8?q?=20meaning;=20the=20new=20intervention=20count=20is=20additive,?= =?UTF-8?q?=20and=20no=20regex=20over=20prose=20is=20introduced.=20Effecti?= =?UTF-8?q?veness=20measurement:=20Fixtures=20assert=20yes=20at=20two=20co?= =?UTF-8?q?mmands,=20no=20at=20one,=20and=20unchecked=20subagent=20totals?= =?UTF-8?q?=20when=20a=20subagent=20file=20is=20newer=20than=20the=20audit?= =?UTF-8?q?=20start.=20Slice=20rationale:=20One=20claim=20about=20one=20sc?= =?UTF-8?q?ript's=20output.=20The=20dictionary=20that=20would=20let=20a=20?= =?UTF-8?q?checker=20notice=20the=20wording=20live=20is=20a=20separate=20c?= =?UTF-8?q?hange.=20Architectural=20effect:=20The=20auditor=20gains=20one?= =?UTF-8?q?=20input=20it=20already=20had=20on=20disk,=20the=20command-name?= =?UTF-8?q?=20field=20of=20user=20turns,=20and=20one=20honesty=20state=20f?= =?UTF-8?q?or=20a=20section=20it=20used=20to=20print=20as=20complete.=20Go?= =?UTF-8?q?al:=20Make=20the=20must-automate=20flag=20see=20the=20intervent?= =?UTF-8?q?ions=20this=20user=20actually=20makes.=20Motivation:=20The=20fl?= =?UTF-8?q?ag=20read=20no=20while=20the=20user=20typed=20the=20automate=20?= =?UTF-8?q?command=20twice,=20so=20the=20automatic=20pass=20never=20fired.?= =?UTF-8?q?=20Alternative=20considerations:=20Matching=20phrases=20like=20?= =?UTF-8?q?"what=20do=20you=20mean"=20out=20of=20prose=20was=20rejected:?= =?UTF-8?q?=20the=20user=20has=20ruled=20out=20regex=20for=20meaning,=20an?= =?UTF-8?q?d=20the=20typed=20command=20is=20an=20exact=20field.=20Lowering?= =?UTF-8?q?=20the=20existing=20thresholds=20was=20rejected=20as=20unrelate?= =?UTF-8?q?d=20to=20the=20blind=20spot.=20Implementation=20details:=20Read?= =?UTF-8?q?=20engine/skills/reflect/scripts/token=5Faudit.py=20and=20find?= =?UTF-8?q?=20where=20user=20turns=20are=20parsed=20and=20where=20the=20in?= =?UTF-8?q?tervention=20kinds=20and=20the=20must-automate=20flag=20are=20c?= =?UTF-8?q?omputed.=20Add=20a=20count=20of=20user=20turns=20whose=20comman?= =?UTF-8?q?d-name=20field=20names=20an=20intervention-class=20command,=20s?= =?UTF-8?q?tarting=20with=20the=20automate=20and=20thrash=20commands,=20ta?= =?UTF-8?q?ken=20from=20the=20field=20rather=20than=20from=20message=20tex?= =?UTF-8?q?t.=20Report=20two=20or=20more=20as=20must-automate,=20alongside?= =?UTF-8?q?=20the=20existing=20kinds.=20Separately,=20record=20the=20audit?= =?UTF-8?q?=20start=20time,=20and=20when=20any=20subagent=20transcript=20f?= =?UTF-8?q?ile's=20modification=20time=20is=20at=20or=20after=20that=20tim?= =?UTF-8?q?e,=20print=20the=20subagent=20section=20as=20unchecked=20with?= =?UTF-8?q?=20the=20reason,=20rather=20than=20printing=20a=20total=20that?= =?UTF-8?q?=20was=20measured=20mid-write.=20Add=20fixtures=20and=20tests?= =?UTF-8?q?=20in=20engine/skills/reflect/scripts/tests/test=5Ftoken=5Faudi?= =?UTF-8?q?t.py.=20Non-goals:=20No=20change=20to=20the=20existing=20frustr?= =?UTF-8?q?ation=20or=20thrash=20kinds,=20no=20change=20to=20pricing,=20an?= =?UTF-8?q?d=20no=20new=20phrase=20dictionary=20in=20this=20slice.=20Layer?= =?UTF-8?q?:=20domain=20Feature=20state:=20active=20Files:=20-=20engine/sk?= =?UTF-8?q?ills/reflect/scripts/token=5Faudit.py=20-=20engine/skills/refle?= =?UTF-8?q?ct/scripts/tests/test=5Ftoken=5Faudit.py=20Change=20types:=20-?= =?UTF-8?q?=20engine/skills/reflect/scripts/token=5Faudit.py:=20modify=20-?= =?UTF-8?q?=20engine/skills/reflect/scripts/tests/test=5Ftoken=5Faudit.py:?= =?UTF-8?q?=20modify=20Acceptance=20criteria:=20-=20`python3=20-m=20unitte?= =?UTF-8?q?st=20discover=20-s=20engine/skills/reflect/scripts/tests=20-v`?= =?UTF-8?q?=20exits=200.=20-=20`python3=20scripts/check=5Fno=5Fnew=5Fcomme?= =?UTF-8?q?nts.py=20--base=20origin/main`=20exits=200.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 Invoker-Finalize-Id: 49ca7364-a00f-459f-8545-1988c1672bd6 From 4ae45061df3cdc4a4bab511bb5596b38ab8423c6 Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Sat, 12 Sep 2026 00:11:24 -0700 Subject: [PATCH 3/4] =?UTF-8?q?invoker:=20wf-1789196449634-14/verify-inter?= =?UTF-8?q?vention-count=20=E2=80=94=20Review=20claim:=20the=20reflect=20s?= =?UTF-8?q?cript=20tests,=20including=20the=20new=20intervention=20and=20u?= =?UTF-8?q?nchecked-subagent=20cases,=20pass.=20Review=20lane:=20proof=20S?= =?UTF-8?q?afety=20invariant:=20Verification=20is=20read-only=20and=20chan?= =?UTF-8?q?ges=20no=20file.=20Effectiveness=20measurement:=20The=20unittes?= =?UTF-8?q?t=20run=20is=20the=20measurement.=20Slice=20rationale:=20One=20?= =?UTF-8?q?proof=20for=20one=20claim.=20Architectural=20effect:=20None;=20?= =?UTF-8?q?verification=20only.=20Goal:=20Prove=20the=20auditor's=20tests?= =?UTF-8?q?=20pass.=20Motivation:=20A=20detector=20blind=20spot=20is=20onl?= =?UTF-8?q?y=20closed=20once=20a=20test=20pins=20it.=20Alternative=20consi?= =?UTF-8?q?derations:=20The=20whole=20suite=20was=20rejected=20as=20slower?= =?UTF-8?q?=20without=20adding=20evidence=20for=20this=20claim.=20Implemen?= =?UTF-8?q?tation=20details:=20Run=20the=20reflect=20script=20tests.=20Non?= =?UTF-8?q?-goals:=20No=20edits.=20Layer:=20app=5Fregression=20Feature=20s?= =?UTF-8?q?tate:=20active=20Acceptance=20criteria:=20-=20Exits=200=20only?= =?UTF-8?q?=20when=20every=20test=20in=20that=20directory=20passes.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 From c7cf11ed8d093dd8e901a277e307b6828871c046 Mon Sep 17 00:00:00 2001 From: Edbert Chan Date: Sat, 12 Sep 2026 00:12:02 -0700 Subject: [PATCH 4/4] =?UTF-8?q?invoker:=20wf-1789196449634-14/scrub-handof?= =?UTF-8?q?f-artifacts=20=E2=80=94=20Review=20claim:=20no=20ephemeral=20ha?= =?UTF-8?q?ndoff=20files=20remain=20in=20the=20worktree.=20Review=20lane:?= =?UTF-8?q?=20cleanup=20Safety=20invariant:=20The=20scrub=20script=20only?= =?UTF-8?q?=20checks=20for=20known=20handoff=20artifact=20names=20and=20ne?= =?UTF-8?q?ver=20touches=20source=20or=20tests.=20Effectiveness=20measurem?= =?UTF-8?q?ent:=20The=20script=20exits=20non-zero=20if=20any=20handoff=20a?= =?UTF-8?q?rtifact=20remains.=20Slice=20rationale:=20Required=20terminal?= =?UTF-8?q?=20scrub=20for=20every=20implementation=20workflow.=20Architect?= =?UTF-8?q?ural=20effect:=20None;=20hygiene=20only.=20Goal:=20Leave=20the?= =?UTF-8?q?=20branch=20free=20of=20handoff=20artifacts.=20Motivation:=20Ha?= =?UTF-8?q?ndoff=20files=20must=20not=20reach=20the=20PR.=20Alternative=20?= =?UTF-8?q?considerations:=20Manual=20cleanup=20was=20rejected=20as=20non-?= =?UTF-8?q?deterministic.=20Implementation=20details:=20Run=20scripts/scru?= =?UTF-8?q?b-handoff-artifacts.sh.=20Non-goals:=20No=20product=20edits.=20?= =?UTF-8?q?Layer:=20app=5Fregression=20Feature=20state:=20active=20Accepta?= =?UTF-8?q?nce=20criteria:=20-=20`bash=20scripts/scrub-handoff-artifacts.s?= =?UTF-8?q?h`=20exits=200.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0