From 4aa295c115476284d8b1cd4ed547383291c5e88c Mon Sep 17 00:00:00 2001 From: Invoker Bot Date: Sat, 12 Sep 2026 06:48:45 +0000 Subject: [PATCH 1/3] =?UTF-8?q?invoker:=20wf-1789195503118-10/flag-capabil?= =?UTF-8?q?ity-list-copied-from-error-text=20=E2=80=94=20Add=20a=20detecto?= =?UTF-8?q?r=20for=20a=20capability=20enumeration=20restated=20out=20of=20?= =?UTF-8?q?error-shaped=20tool=20output.=20Review=20claim:=20An=20outgoing?= =?UTF-8?q?=20message=20that=20restates=20two=20or=20more=20allowed=20valu?= =?UTF-8?q?es=20from=20error-shaped=20tool=20output=20beside=20a=20capabil?= =?UTF-8?q?ity=20verb=20is=20flagged,=20unless=20a=20non-error=20source=20?= =?UTF-8?q?for=20those=20values=20exists=20or=20the=20message=20attributes?= =?UTF-8?q?=20them.=20Review=20lane:=20behavior=20Safety=20invariant:=20Ev?= =?UTF-8?q?ery=20message=20the=20shipped=20hook=20flags=20today=20is=20sti?= =?UTF-8?q?ll=20flagged=20with=20the=20same=20text,=20and=20every=20messag?= =?UTF-8?q?e=20it=20passes=20today=20still=20passes=20unless=20it=20matche?= =?UTF-8?q?s=20this=20new=20rule.=20The=2024=20existing=20cases=20keep=20p?= =?UTF-8?q?assing=20untouched.=20Effectiveness=20measurement:=20`python3?= =?UTF-8?q?=20engine/hooks/hedge-runs-prove-it/tests/test=5Fhooks.py`=20ex?= =?UTF-8?q?its=200,=20with=20new=20positive=20cases=20that=20exit=20non-ze?= =?UTF-8?q?ro=20before=20this=20change=20and=20the=20negative=20cases=20be?= =?UTF-8?q?low=20staying=20silent=20both=20before=20and=20after.=20Slice?= =?UTF-8?q?=20rationale:=20One=20conceptual=20unit:=20one=20new=20rule=20i?= =?UTF-8?q?nside=20an=20existing=20detector,=20with=20its=20fixtures.=20Ar?= =?UTF-8?q?chitectural=20effect:=20None;=20the=20hook=20already=20parses?= =?UTF-8?q?=20transcript=20lines=20at=20Stop.=20This=20reads=20tool=20outp?= =?UTF-8?q?ut=20lines=20it=20already=20has.=20Goal:=20Stop=20a=20value=20l?= =?UTF-8?q?ifted=20out=20of=20an=20error=20string=20from=20going=20out=20a?= =?UTF-8?q?s=20the=20authority=20it=20was=20never=20sourced=20from.=20Moti?= =?UTF-8?q?vation:=20Three=20outgoing=20messages=20asserted=20a=20third=20?= =?UTF-8?q?party's=20capabilities=20using=20a=20list=20copied=20from=20a?= =?UTF-8?q?=20second=20party's=20error=20text.=20The=20operator=20believed?= =?UTF-8?q?=20them=20and=20acted.=20Alternative=20considerations:=20A=20Po?= =?UTF-8?q?stToolUse=20rule=20firing=20when=20the=20string=20enters=20was?= =?UTF-8?q?=20set=20aside=20on=20measurement=20--=20the=20value=20arrived?= =?UTF-8?q?=20334=20transcript=20rows=20before=20the=20last=20assertion,?= =?UTF-8?q?=20so=20an=20entry-time=20nudge=20fires=20far=20too=20early=20t?= =?UTF-8?q?o=20bind=20the=20decision.=20Widening=20the=20shipped=20claim?= =?UTF-8?q?=20gate=20was=20set=20aside=20--=20that=20gate=20asks=20whether?= =?UTF-8?q?=20a=20sentence=20is=20hedged;=20this=20asks=20where=20a=20valu?= =?UTF-8?q?e=20came=20from,=20a=20different=20question,=20and=20three=20op?= =?UTF-8?q?en=20pull=20requests=20are=20already=20churning=20that=20file.?= =?UTF-8?q?=20Implementation=20details:=20Capture=20enumerations=20appeari?= =?UTF-8?q?ng=20inside=20error-shaped=20tool=20output=20in=20the=20transcr?= =?UTF-8?q?ipt=20lines=20the=20hook=20already=20parses.=20At=20Stop,=20fla?= =?UTF-8?q?g=20an=20outgoing=20message=20restating=20two=20or=20more=20cap?= =?UTF-8?q?tured=20values=20within=20range=20of=20a=20capability=20verb,?= =?UTF-8?q?=20when=20no=20non-error=20occurrence=20of=20those=20values=20e?= =?UTF-8?q?xists=20in=20the=20transcript=20yet.=20Clear=20on=20an=20attrib?= =?UTF-8?q?ution=20phrase=20such=20as=20fallback,=20hardcoded,=20or=20in?= =?UTF-8?q?=20the=20error.=20Non-goals:=20No=20new=20hook=20directory.=20N?= =?UTF-8?q?o=20change=20to=20the=20shipped=20hedge=20or=20diagnosis=20rule?= =?UTF-8?q?s.=20No=20change=20to=20the=20claim=20gate=20in=20engine/hooks/?= =?UTF-8?q?diu-stop.=20No=20attempt=20to=20judge=20whether=20a=20check=20w?= =?UTF-8?q?as=20cheap=20or=20available=20--=20that=20is=20not=20computable?= =?UTF-8?q?=20and=20is=20out=20of=20scope.=20Layer:=20domain=20Feature=20s?= =?UTF-8?q?tate:=20active=20Files:=20engine/hooks/hedge-runs-prove-it/dete?= =?UTF-8?q?ct.py,=20engine/hooks/hedge-runs-prove-it/tests/test=5Fhooks.py?= =?UTF-8?q?,=20engine/hooks/hedge-runs-prove-it/README.md=20Change=20types?= =?UTF-8?q?:=20-=20engine/hooks/hedge-runs-prove-it/detect.py:=20modify=20?= =?UTF-8?q?-=20engine/hooks/hedge-runs-prove-it/tests/test=5Fhooks.py:=20m?= =?UTF-8?q?odify=20-=20engine/hooks/hedge-runs-prove-it/README.md:=20modif?= =?UTF-8?q?y=20Acceptance=20criteria:=20-=20`python3=20engine/hooks/hedge-?= =?UTF-8?q?runs-prove-it/tests/test=5Fhooks.py`=20exits=200=20after=20the?= =?UTF-8?q?=20change,=20with=20all=2024=20existing=20cases=20still=20passi?= =?UTF-8?q?ng.=20-=20A=20message=20restating=20two=20or=20more=20values=20?= =?UTF-8?q?that=20appear=20only=20inside=20error-shaped=20tool=20output,?= =?UTF-8?q?=20beside=20a=20capability=20verb,=20is=20flagged.=20-=20The=20?= =?UTF-8?q?same=20message=20is=20not=20flagged=20once=20a=20non-error=20so?= =?UTF-8?q?urce=20for=20those=20values=20exists=20in=20the=20transcript.?= =?UTF-8?q?=20-=20A=20message=20attributing=20the=20list=20as=20a=20fallba?= =?UTF-8?q?ck=20is=20not=20flagged.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Solution: Add a detector for a capability enumeration restated out of error-shaped tool output. Review claim: An outgoing message that restates two or more allowed values from error-shaped tool output beside a capability verb is flagged, unless a non-error source for those values exists or the message attributes them. Review lane: behavior Safety invariant: Every message the shipped hook flags today is still flagged with the same text, and every message it passes today still passes unless it matches this new rule. The 24 existing cases keep passing untouched. Effectiveness measurement: `python3 engine/hooks/hedge-runs-prove-it/tests/test_hooks.py` exits 0, with new positive cases that exit non-zero before this change and the negative cases below staying silent both before and after. Slice rationale: One conceptual unit: one new rule inside an existing detector, with its fixtures. Architectural effect: None; the hook already parses transcript lines at Stop. This reads tool output lines it already has. Goal: Stop a value lifted out of an error string from going out as the authority it was never sourced from. Motivation: Three outgoing messages asserted a third party's capabilities using a list copied from a second party's error text. The operator believed them and acted. Alternative considerations: A PostToolUse rule firing when the string enters was set aside on measurement -- the value arrived 334 transcript rows before the last assertion, so an entry-time nudge fires far too early to bind the decision. Widening the shipped claim gate was set aside -- that gate asks whether a sentence is hedged; this asks where a value came from, a different question, and three open pull requests are already churning that file. Implementation details: Capture enumerations appearing inside error-shaped tool output in the transcript lines the hook already parses. At Stop, flag an outgoing message restating two or more captured values within range of a capability verb, when no non-error occurrence of those values exists in the transcript yet. Clear on an attribution phrase such as fallback, hardcoded, or in the error. Non-goals: No new hook directory. No change to the shipped hedge or diagnosis rules. No change to the claim gate in engine/hooks/diu-stop. No attempt to judge whether a check was cheap or available -- that is not computable and is out of scope. Layer: domain Feature state: active Files: engine/hooks/hedge-runs-prove-it/detect.py, engine/hooks/hedge-runs-prove-it/tests/test_hooks.py, engine/hooks/hedge-runs-prove-it/README.md Change types: - engine/hooks/hedge-runs-prove-it/detect.py: modify - engine/hooks/hedge-runs-prove-it/tests/test_hooks.py: modify - engine/hooks/hedge-runs-prove-it/README.md: modify Acceptance criteria: - `python3 engine/hooks/hedge-runs-prove-it/tests/test_hooks.py` exits 0 after the change, with all 24 existing cases still passing. - A message restating two or more values that appear only inside error-shaped tool output, beside a capability verb, is flagged. - The same message is not flagged once a non-error source for those values exists in the transcript. - A message attributing the list as a fallback is not flagged. Invoker-Finalize-Id: 8156de0f-c490-468a-a9e5-0fb0dcdcc394 --- engine/hooks/hedge-runs-prove-it/README.md | 17 ++- engine/hooks/hedge-runs-prove-it/detect.py | 126 ++++++++++++++++-- .../hedge-runs-prove-it/tests/test_hooks.py | 61 +++++++++ 3 files changed, 195 insertions(+), 9 deletions(-) diff --git a/engine/hooks/hedge-runs-prove-it/README.md b/engine/hooks/hedge-runs-prove-it/README.md index 266b85f3..fd9c2436 100644 --- a/engine/hooks/hedge-runs-prove-it/README.md +++ b/engine/hooks/hedge-runs-prove-it/README.md @@ -48,6 +48,20 @@ Stays silent on a diagnosis inside a fence, a double-quoted span, a backtick span, a markdown blockquote, a hypothetical ("if it's a zombie, ..."), and past-tense narration of an old incident ("the task was a zombie"). +## Capability lists copied from errors + +An error message can expose a local fallback or hardcoded enumeration without +proving the capabilities of the system named in that error. The hook records +values in bracketed or comma-joined lists from error-shaped tool results. It +blocks an outgoing reply that repeats at least two of those values within 200 +characters of `accepts`, `supports`, `known models`, `valid`, or `only` when +those values appeared nowhere in a non-error tool result. + +One repeated value stays silent to avoid collisions on common tokens. The rule +also stays silent when a non-error tool result supplies the repeated values, or +when the reply attributes or retracts the enumeration with wording such as +`fallback`, `hardcoded`, `built-in`, `in the error`, or `retract`. + Mechanical half of `corpus/skills/cat-mode/SKILL.md`'s Verify rule: "Unhedged root-cause or fix claims about live system behavior need instrument-level proof in the same message, or `{{CAT-UNVERIFIED}}`." Four @@ -73,7 +87,8 @@ escape hatch, not a free pass). Fail-open on parse or read errors; ## Files -- `detect.py` -- hedge, code-noun, and reason patterns; turn scan; `decide()`. +- `detect.py` -- hedge, diagnosis, and error-only capability-list patterns; + turn scan; `decide()`. - `claude_stop_check.py` -- Claude Stop entrypoint. - `claude.hook.json` / `install_claude_hook.py` -- settings.json merge (idempotent). - `tests/fixtures/hedges_{fires,silent}.json` -- sanitized real replies and diff --git a/engine/hooks/hedge-runs-prove-it/detect.py b/engine/hooks/hedge-runs-prove-it/detect.py index d4ff2e66..99094d8a 100644 --- a/engine/hooks/hedge-runs-prove-it/detect.py +++ b/engine/hooks/hedge-runs-prove-it/detect.py @@ -1,6 +1,6 @@ -"""hedge-runs-prove-it: an unrun check about code or repo state is a prompt to verify. +"""hedge-runs-prove-it: unproven code, state, and capability claims must be verified. -Two shapes, two bars. +Three shapes, three bars. A hedge -- "I think", "I believe", "probably", "should work", "presumably", or a `{{CAT-UNVERIFIED}}` tag next to a code noun (a path, a backticked name, @@ -18,6 +18,11 @@ Only instrument-level proof in the same message clears it: pasted output, a `file:line`, a pid, an exit code, or an explicit `{{CAT-UNVERIFIED}}` tag. +A capability enumeration copied from error-shaped tool output is not proof of +what another system accepts or supports. A reply that repeats two or more of +those values beside a capability verb is blocked unless a non-error tool result +also supplied them, or the reply attributes the list as fallback/error data. + Hedges about things that are not code or state (a company's motive, a filing date) are out of scope, and so is either shape quoted rather than claimed -- anywhere inside a double-quoted, backticked or single-quoted run, @@ -118,6 +123,36 @@ "file:line. Otherwise tag the claim: `{tag}`." ) +ERROR_OUTPUT_RE = re.compile( + r"\b(?:error|exception|fatal|failure|failed|invalid|unsupported|traceback)\b|" + r"\bnot\s+supported\b", + re.IGNORECASE, +) +BRACKETED_ENUM_RE = re.compile(r"\[([^\[\]\n]+)\]") +COMMA_ENUM_RE = re.compile( + r"(? str: end = len(text) @@ -215,6 +250,80 @@ def _is_human_user_line(data: dict) -> bool: return bool(text.strip()) and not text.lstrip().startswith("<") +def _tool_result_text(block: dict) -> str: + content = block.get("content") + if isinstance(content, str): + return content + if not isinstance(content, list): + return "" + chunks: list[str] = [] + for item in content: + if isinstance(item, str): + chunks.append(item) + elif isinstance(item, dict) and isinstance(item.get("text"), str): + chunks.append(item["text"]) + return "\n".join(chunks) + + +def _tool_results(lines: list[dict]): + for data in lines: + message = data.get("message") + content = message.get("content") if isinstance(message, dict) else data.get("content") + if not isinstance(content, list): + continue + for block in content: + if isinstance(block, dict) and block.get("type") == "tool_result": + yield block, _tool_result_text(block) + + +def _enumerated_values(text: str) -> set[str]: + runs = [match.group(1) for match in BRACKETED_ENUM_RE.finditer(text or "")] + runs.extend(match.group(1) for match in COMMA_ENUM_RE.finditer(text or "")) + values: set[str] = set() + for run in runs: + for raw_value in run.split(","): + value = raw_value.strip().strip("'\"`") + if len(value) >= 2 and ENUM_VALUE_RE.fullmatch(value): + values.add(value.lower()) + return values + + +def _value_occurs(text: str, value: str) -> bool: + return bool(re.search( + rf"(? set[str]: + """Enumerated values whose tool-result sources are all error-shaped.""" + error_values: set[str] = set() + non_error_results: list[str] = [] + for block, text in _tool_results(lines): + error_shaped = bool(block.get("is_error") or ERROR_OUTPUT_RE.search(text)) + if error_shaped: + error_values.update(_enumerated_values(text)) + else: + non_error_results.append(text) + return { + value for value in error_values + if not any(_value_occurs(text, value) for text in non_error_results) + } + + +def _capability_feedback(message: str, lines: list[dict]) -> str | None: + if not CAPABILITY_RE.search(message or "") or CAPABILITY_ATTRIBUTION_RE.search(message or ""): + return None + values = error_only_capability_values(lines) + for verb in CAPABILITY_RE.finditer(message): + window = message[max(0, verb.start() - PROXIMITY): verb.end() + PROXIMITY] + repeated = sorted(value for value in values if _value_occurs(window, value)) + if len(repeated) >= 2: + return CAPABILITY_MESSAGE.format(values=", ".join(repeated[:4])) + return None + + def parse_lines(raw_lines) -> list[dict]: parsed: list[dict] = [] for raw in raw_lines: @@ -257,11 +366,12 @@ def decide_from_lines(message: str, lines: list[dict]) -> str | None: if diagnosis: return diagnosis hedges = code_hedges(message) - if not hedges: - return None - if verified_this_turn(lines): - return None - return MESSAGE.format(tag=markers.TAG_TEMPLATE, hedge=", ".join(f'"{h}"' for h in hedges[:3])) + if hedges and not verified_this_turn(lines): + return MESSAGE.format( + tag=markers.TAG_TEMPLATE, + hedge=", ".join(f'"{h}"' for h in hedges[:3]), + ) + return _capability_feedback(message, lines) def decide(payload: dict) -> str | None: @@ -272,7 +382,7 @@ def decide(payload: dict) -> str | None: diagnosis = _diagnosis_feedback(message) if diagnosis: return diagnosis - if not code_hedges(message): + if not code_hedges(message) and not CAPABILITY_RE.search(message): return None transcript_path = payload.get("transcript_path") or payload.get("transcriptPath") or "" lines: list[dict] = [] diff --git a/engine/hooks/hedge-runs-prove-it/tests/test_hooks.py b/engine/hooks/hedge-runs-prove-it/tests/test_hooks.py index 8f8c6ad4..314a2a89 100644 --- a/engine/hooks/hedge-runs-prove-it/tests/test_hooks.py +++ b/engine/hooks/hedge-runs-prove-it/tests/test_hooks.py @@ -43,6 +43,35 @@ def turn_lines(verified): return lines +ERROR_CAPABILITY_LIST = ( + 'Error: Execution model "gpt-5.6-luna" is not supported for execution agent ' + '"codex". Known models: [gpt-5.5, gpt-5.5-pro, gpt-5.4, gpt-5.4-pro].' +) + + +def capability_lines(with_non_error_source=False): + lines = turn_lines(False) + [ + {"type": "assistant", "message": {"role": "assistant", "content": [ + {"type": "tool_use", "id": "models-error", "name": "Task", "input": {}} + ]}}, + {"type": "user", "message": {"role": "user", "content": [ + {"type": "tool_result", "tool_use_id": "models-error", "content": ERROR_CAPABILITY_LIST} + ]}}, + ] + if with_non_error_source: + lines.extend([ + {"type": "assistant", "message": {"role": "assistant", "content": [ + {"type": "tool_use", "id": "models-answer", "name": "Task", "input": {}} + ]}}, + {"type": "user", "message": {"role": "user", "content": [ + {"type": "tool_result", "tool_use_id": "models-answer", "content": ( + "Supported execution models: gpt-5.5, gpt-5.5-pro, gpt-5.4, gpt-5.4-pro." + )} + ]}}, + ]) + return lines + + def transcript_file(lines): tmp = tempfile.NamedTemporaryFile("w", suffix=".jsonl", delete=False, encoding="utf-8") tmp.write("\n".join(json.dumps(line) for line in lines) + "\n") @@ -218,5 +247,37 @@ def test_diagnosis_gate_ignores_an_unreadable_transcript(self): })) +class TestBlocksCapabilitiesCopiedOnlyFromErrors(unittest.TestCase): + def test_hook_blocks_two_values_repeated_beside_only_accepts(self): + path = transcript_file(capability_lines()) + try: + code, err = run_hook({ + "last_assistant_message": "codex only accepts [gpt-5.5, gpt-5.5-pro].", + "transcript_path": path, + }) + finally: + os.unlink(path) + self.assertEqual(code, 2) + self.assertIn("error-shaped tool output", err) + + def test_allows_values_also_listed_by_non_error_tool_result(self): + self.assertIsNone(detect.decide_from_lines( + "codex only accepts [gpt-5.5, gpt-5.5-pro].", + capability_lines(with_non_error_source=True), + )) + + def test_allows_attributed_fallback_list(self): + self.assertIsNone(detect.decide_from_lines( + "The built-in fallback list says codex only accepts [gpt-5.5, gpt-5.5-pro].", + capability_lines(), + )) + + def test_allows_a_single_repeated_value(self): + self.assertIsNone(detect.decide_from_lines( + "codex only accepts gpt-5.5.", + capability_lines(), + )) + + if __name__ == "__main__": unittest.main() From e9aadbd8fff279b86438c1da3cc6d3794a97661d Mon Sep 17 00:00:00 2001 From: Invoker Bot Date: Sat, 12 Sep 2026 06:49:36 +0000 Subject: [PATCH 2/3] =?UTF-8?q?invoker:=20wf-1789195503118-10/verify-capab?= =?UTF-8?q?ility-list-provenance=20=E2=80=94=20Run=20the=20hook's=20own=20?= =?UTF-8?q?suite=20as=20the=20shared=20repro=20for=20this=20slice.=20Revie?= =?UTF-8?q?w=20claim:=20The=20suite=20exits=200=20only=20when=20the=20new?= =?UTF-8?q?=20rule=20flags=20the=20copied-from-error=20shape=20and=20stays?= =?UTF-8?q?=20silent=20on=20all=20four=20negative=20shapes.=20Review=20lan?= =?UTF-8?q?e:=20proof=20Safety=20invariant:=20The=20repro=20is=20identical?= =?UTF-8?q?=20before=20and=20after;=20the=20new=20positive=20cases=20exit?= =?UTF-8?q?=20non-zero=20before=20and=200=20after.=20Effectiveness=20measu?= =?UTF-8?q?rement:=20The=20exit=20status=20of=20the=20suite=20is=20the=20e?= =?UTF-8?q?ffectiveness=20signal=20for=20this=20slice.=20Slice=20rationale?= =?UTF-8?q?:=20One=20conceptual=20unit:=20the=20deterministic=20repro=20on?= =?UTF-8?q?ly.=20Architectural=20effect:=20None.=20Goal:=20Deterministical?= =?UTF-8?q?ly=20prove=20the=20new=20rule.=20Motivation:=20Nothing=20today?= =?UTF-8?q?=20asserts=20where=20a=20restated=20value=20came=20from,=20whic?= =?UTF-8?q?h=20is=20why=20three=20messages=20carried=20one=20out=20of=20an?= =?UTF-8?q?=20error=20string=20unchallenged.=20Alternative=20consideration?= =?UTF-8?q?s:=20Covering=20only=20the=20positive=20shape=20was=20set=20asi?= =?UTF-8?q?de=20--=20without=20the=20non-error-source=20and=20attributed?= =?UTF-8?q?=20cases,=20a=20rule=20that=20fires=20on=20every=20restatement?= =?UTF-8?q?=20would=20still=20exit=200.=20Implementation=20details:=20Exec?= =?UTF-8?q?ute=20the=20suite=20as=20the=20terminal=20gate.=20Non-goals:=20?= =?UTF-8?q?No=20product=20edits=20here.=20Layer:=20e2e=5Fregression=20Feat?= =?UTF-8?q?ure=20state:=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 Invoker-Finalize-Id: 5910aca8-b6ae-453c-b815-4076d8d90fca From 1965f2296cb014613f633b047f814619fd201649 Mon Sep 17 00:00:00 2001 From: Invoker Bot Date: Sat, 12 Sep 2026 06:50:24 +0000 Subject: [PATCH 3/3] =?UTF-8?q?invoker:=20wf-1789195503118-10/scrub-handof?= =?UTF-8?q?f-artifacts=20=E2=80=94=20Terminal=20read-only=20gate=20confirm?= =?UTF-8?q?ing=20no=20ephemeral=20handoff=20files=20were=20left=20behind.?= =?UTF-8?q?=20Review=20claim:=20The=20workflow=20leaves=20no=20ephemeral?= =?UTF-8?q?=20handoff=20files=20in=20the=20tree.=20Review=20lane:=20proof?= =?UTF-8?q?=20Safety=20invariant:=20Read-only=20--=20never=20deletes=20fil?= =?UTF-8?q?es,=20alters=20the=20index,=20or=20commits=20caller=20work.=20E?= =?UTF-8?q?ffectiveness=20measurement:=20A=20non-zero=20exit=20when=20ephe?= =?UTF-8?q?meral=20handoff=20files=20remain=20is=20the=20signal.=20Slice?= =?UTF-8?q?=20rationale:=20One=20conceptual=20unit:=20the=20hygiene=20gate?= =?UTF-8?q?.=20Architectural=20effect:=20None.=20Goal:=20Confirm=20no=20ep?= =?UTF-8?q?hemeral=20handoff=20files=20remain=20after=20every=20other=20ta?= =?UTF-8?q?sk=20finishes.=20Motivation:=20Ephemeral=20inter-task=20files?= =?UTF-8?q?=20leak=20into=20the=20diff=20and=20read=20as=20part=20of=20the?= =?UTF-8?q?=20change.=20Alternative=20considerations:=20Manual=20inspectio?= =?UTF-8?q?n=20was=20set=20aside=20as=20non-deterministic.=20Implementatio?= =?UTF-8?q?n=20details:=20Run=20scripts/scrub-handoff-artifacts.sh=20witho?= =?UTF-8?q?ut=20--apply.=20Non-goals:=20No=20deletion,=20no=20index=20chan?= =?UTF-8?q?ges,=20no=20commits.=20Layer:=20e2e=5Fregression=20Feature=20st?= =?UTF-8?q?ate:=20active?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Exit code: 0 Invoker-Finalize-Id: 7b20f59a-7962-415f-ad6c-6f9e3a8811b9