From 1f6002f9ac6451ebdc4bd71666b6bf829dd91137 Mon Sep 17 00:00:00 2001 From: Mohab Mohie Date: Wed, 19 Aug 2026 03:49:41 +0300 Subject: [PATCH 1/3] fix(harness): map native plan surfaces for R25 receipt events Teach portable `_research_preflight_events` to record compare-proven-approaches and record-plan from enter_plan_mode, exit_plan_mode, and todo_write while keeping update_plan behavior. Track origin .grok/hooks/lifecycle.json and widen host hook matchers so PreToolUse/PostToolUse observe those tools. Closes #5195 Related to #5186 --- .claude/settings.json | 6 +- .codex/hooks.json | 4 +- .grok/hooks/lifecycle.json | 79 +++++++++++++++++++ scripts/agents/guard.py | 11 +++ .../scripts/test_agent_harness_portability.py | 16 +++- tests/scripts/test_guard_lifecycle.py | 62 ++++++++++++++- 6 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 .grok/hooks/lifecycle.json diff --git a/.claude/settings.json b/.claude/settings.json index 42d7afce140..63465d34f2b 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -27,7 +27,7 @@ "hooks": { "PreToolUse": [ { - "matcher": "Task|Agent|Read|Grep|Bash|PowerShell|shell_command|Write|Edit|NotebookEdit|apply_patch|WebSearch|WebFetch|web__run|update_plan|mcp__shaft-memory__(?:remember_memory|save_memory_patch)|mcp__mempalace__.*|mcp__graphify__.*", + "matcher": "Task|Agent|Read|Grep|Bash|PowerShell|shell_command|Write|Edit|NotebookEdit|apply_patch|WebSearch|WebFetch|web__run|update_plan|enter_plan_mode|EnterPlanMode|exit_plan_mode|ExitPlanMode|todo_write|TodoWrite|mcp__shaft-memory__(?:remember_memory|save_memory_patch)|mcp__mempalace__.*|mcp__graphify__.*", "hooks": [ { "type": "command", @@ -40,7 +40,7 @@ ], "PostToolUse": [ { - "matcher": "Read|Grep|Bash|PowerShell|shell_command|WebSearch|WebFetch|web__run|update_plan|mcp__shaft-memory__.*|mcp__mempalace__.*|mcp__graphify__.*", + "matcher": "Read|Grep|Bash|PowerShell|shell_command|WebSearch|WebFetch|web__run|update_plan|enter_plan_mode|EnterPlanMode|exit_plan_mode|ExitPlanMode|todo_write|TodoWrite|mcp__shaft-memory__.*|mcp__mempalace__.*|mcp__graphify__.*", "hooks": [ { "type": "command", @@ -53,7 +53,7 @@ ], "PostToolUseFailure": [ { - "matcher": "Read|Grep|Bash|PowerShell|shell_command|WebSearch|WebFetch|web__run|update_plan|mcp__shaft-memory__.*|mcp__mempalace__.*|mcp__graphify__.*", + "matcher": "Read|Grep|Bash|PowerShell|shell_command|WebSearch|WebFetch|web__run|update_plan|enter_plan_mode|EnterPlanMode|exit_plan_mode|ExitPlanMode|todo_write|TodoWrite|mcp__shaft-memory__.*|mcp__mempalace__.*|mcp__graphify__.*", "hooks": [ { "type": "command", diff --git a/.codex/hooks.json b/.codex/hooks.json index 8fe91820d07..8e4437f4c78 100644 --- a/.codex/hooks.json +++ b/.codex/hooks.json @@ -3,7 +3,7 @@ "hooks": { "PreToolUse": [ { - "matcher": "Task|Agent|Read|Grep|Bash|PowerShell|shell_command|exec_command|functions[.]exec|Write|Edit|NotebookEdit|apply_patch|WebSearch|WebFetch|web__run|update_plan|mcp__shaft[-_]memory__(?:remember_memory|save_memory_patch)|mcp__mempalace__.*|mcp__graphify__.*", + "matcher": "Task|Agent|Read|Grep|Bash|PowerShell|shell_command|exec_command|functions[.]exec|Write|Edit|NotebookEdit|apply_patch|WebSearch|WebFetch|web__run|update_plan|enter_plan_mode|EnterPlanMode|exit_plan_mode|ExitPlanMode|todo_write|TodoWrite|mcp__shaft[-_]memory__(?:remember_memory|save_memory_patch)|mcp__mempalace__.*|mcp__graphify__.*", "hooks": [ { "type": "command", @@ -16,7 +16,7 @@ ], "PostToolUse": [ { - "matcher": "Read|Grep|Bash|PowerShell|shell_command|exec_command|functions[.]exec|WebSearch|WebFetch|web__run|update_plan|mcp__shaft[-_]memory__.*|mcp__mempalace__.*|mcp__graphify__.*", + "matcher": "Read|Grep|Bash|PowerShell|shell_command|exec_command|functions[.]exec|WebSearch|WebFetch|web__run|update_plan|enter_plan_mode|EnterPlanMode|exit_plan_mode|ExitPlanMode|todo_write|TodoWrite|mcp__shaft[-_]memory__.*|mcp__mempalace__.*|mcp__graphify__.*", "hooks": [ { "type": "command", diff --git a/.grok/hooks/lifecycle.json b/.grok/hooks/lifecycle.json new file mode 100644 index 00000000000..8e4437f4c78 --- /dev/null +++ b/.grok/hooks/lifecycle.json @@ -0,0 +1,79 @@ +{ + "description": "Portable SHAFT lifecycle and PreToolUse safety guard.", + "hooks": { + "PreToolUse": [ + { + "matcher": "Task|Agent|Read|Grep|Bash|PowerShell|shell_command|exec_command|functions[.]exec|Write|Edit|NotebookEdit|apply_patch|WebSearch|WebFetch|web__run|update_plan|enter_plan_mode|EnterPlanMode|exit_plan_mode|ExitPlanMode|todo_write|TodoWrite|mcp__shaft[-_]memory__(?:remember_memory|save_memory_patch)|mcp__mempalace__.*|mcp__graphify__.*", + "hooks": [ + { + "type": "command", + "command": "python3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", + "commandWindows": "py -3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", + "timeout": 10 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Read|Grep|Bash|PowerShell|shell_command|exec_command|functions[.]exec|WebSearch|WebFetch|web__run|update_plan|enter_plan_mode|EnterPlanMode|exit_plan_mode|ExitPlanMode|todo_write|TodoWrite|mcp__shaft[-_]memory__.*|mcp__mempalace__.*|mcp__graphify__.*", + "hooks": [ + { + "type": "command", + "command": "python3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", + "commandWindows": "py -3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", + "timeout": 10 + } + ] + } + ], + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", + "commandWindows": "py -3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", + "timeout": 30 + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", + "commandWindows": "py -3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", + "timeout": 10 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", + "commandWindows": "py -3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", + "timeout": 30 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "python3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", + "commandWindows": "py -3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", + "timeout": 30 + } + ] + } + ] + } +} diff --git a/scripts/agents/guard.py b/scripts/agents/guard.py index 0e94aa8f68f..195086f0035 100644 --- a/scripts/agents/guard.py +++ b/scripts/agents/guard.py @@ -2125,6 +2125,17 @@ def _research_preflight_events( plan = details.get("plan") if isinstance(plan, list) and plan: events.append("record-plan") + plan_surface = lowered_name.replace("_", "").replace("-", "") + if plan_surface == "enterplanmode": + events.append("compare-proven-approaches") + if plan_surface == "exitplanmode": + events.append("record-plan") + if plan_surface == "todowrite": + if "compare proven approaches" in rendered: + events.append("compare-proven-approaches") + todos = details.get("todos") + if isinstance(todos, list) and todos: + events.append("record-plan") if tool_name in _SHELL_TOOLS and _is_plan_receipt_command( str(details.get("command") or details.get("cmd") or "") ): diff --git a/tests/scripts/test_agent_harness_portability.py b/tests/scripts/test_agent_harness_portability.py index e49cc61a5eb..ba57c3212c1 100644 --- a/tests/scripts/test_agent_harness_portability.py +++ b/tests/scripts/test_agent_harness_portability.py @@ -679,14 +679,26 @@ def test_hook_configs_share_one_cwd_independent_lifecycle_contract(self): def test_hook_configs_are_tracked_for_host_local_trust(self): tracked = subprocess.run( # nosec B603 B607 - fixed read-only git command. - ["git", "ls-files", "--error-unmatch", ".claude/settings.json", ".codex/hooks.json", "scripts/agents/guard.py"], + [ + "git", + "ls-files", + "--error-unmatch", + ".claude/settings.json", + ".codex/hooks.json", + ".grok/hooks/lifecycle.json", + "scripts/agents/guard.py", + ], cwd=ROOT, capture_output=True, text=True, check=False, ) self.assertEqual(tracked.returncode, 0, tracked.stderr) - for path in (ROOT / ".claude/settings.json", ROOT / ".codex/hooks.json"): + for path in ( + ROOT / ".claude/settings.json", + ROOT / ".codex/hooks.json", + ROOT / ".grok/hooks/lifecycle.json", + ): self.assertNotIn("bypass-hook-trust", path.read_text(encoding="utf-8")) def test_equivalent_host_hook_events_produce_equivalent_outcomes(self): diff --git a/tests/scripts/test_guard_lifecycle.py b/tests/scripts/test_guard_lifecycle.py index 84e121bccf3..0ac57a8e9f8 100644 --- a/tests/scripts/test_guard_lifecycle.py +++ b/tests/scripts/test_guard_lifecycle.py @@ -1362,6 +1362,34 @@ def test_live_tool_events_map_to_the_receipt_vocabulary(self): ("WebSearch", {"query": "official hook documentation"}, {"url": "https://docs.github.com/en/actions"}, "authoritative-online-research"), ("update_plan", {"explanation": "Compare proven approaches", "plan": []}, None, "compare-proven-approaches"), ("update_plan", {"explanation": "Compare proven approaches", "plan": [{"step": "Implement", "status": "pending"}]}, None, "record-plan"), + ("enter_plan_mode", {}, None, "compare-proven-approaches"), + ("EnterPlanMode", {}, None, "compare-proven-approaches"), + ("exit_plan_mode", {}, None, "record-plan"), + ("ExitPlanMode", {}, None, "record-plan"), + ( + "todo_write", + { + "todos": [ + { + "id": "1", + "content": "Compare proven approaches then implement", + "status": "in_progress", + } + ] + }, + None, + "compare-proven-approaches", + ), + ( + "todo_write", + { + "todos": [ + {"id": "1", "content": "Implement mapping", "status": "pending"} + ] + }, + None, + "record-plan", + ), ) for tool_name, tool_input, tool_result, expected in fixtures: with self.subTest(tool_name=tool_name, expected=expected): @@ -1388,6 +1416,16 @@ def test_live_tool_events_map_to_the_receipt_vocabulary(self): (), ) + def test_todo_write_without_compare_marker_does_not_emit_compare(self): + events = guard._research_preflight_events( + "todo_write", + {"todos": [{"id": "1", "content": "Implement mapping", "status": "pending"}]}, + ) + self.assertIn("record-plan", events) + self.assertNotIn("compare-proven-approaches", events) + self.assertEqual(guard._research_preflight_events("todo_write", {"todos": []}), ()) + self.assertEqual(guard._research_preflight_events("todo_write", {}), ()) + def test_shell_command_maps_research_clis_in_command_order(self): self.assertEqual( guard._research_preflight_events( @@ -1607,10 +1645,25 @@ def test_failed_current_host_research_calls_do_not_certify_success(self): self.assertEqual(observed, []) def test_portable_hook_matchers_observe_receipt_and_mutation_tools(self): - for relative in (".claude/settings.json", ".codex/hooks.json"): + plan_surfaces = ( + "update_plan", + "enter_plan_mode", + "EnterPlanMode", + "exit_plan_mode", + "ExitPlanMode", + "todo_write", + "TodoWrite", + ) + for relative in ( + ".claude/settings.json", + ".codex/hooks.json", + ".grok/hooks/lifecycle.json", + ): with self.subTest(relative=relative): - text = (Path(__file__).resolve().parents[2] / relative).read_text(encoding="utf-8") - for tool in ("Read", "WebSearch", "WebFetch", "update_plan", "apply_patch"): + text = (Path(__file__).resolve().parents[2] / relative).read_text( + encoding="utf-8" + ) + for tool in ("Read", "WebSearch", "WebFetch", "apply_patch", *plan_surfaces): self.assertIn(tool, text) self.assertIn("PostToolUse", text) codex = (Path(__file__).resolve().parents[2] / ".codex/hooks.json").read_text( @@ -1622,6 +1675,9 @@ def test_portable_hook_matchers_observe_receipt_and_mutation_tools(self): hooks = json.loads(codex)["hooks"] self.assertIn("functions[.]exec", hooks["PreToolUse"][0]["matcher"]) self.assertIn("functions[.]exec", hooks["PostToolUse"][0]["matcher"]) + for tool in plan_surfaces: + self.assertIn(tool, hooks["PreToolUse"][0]["matcher"]) + self.assertIn(tool, hooks["PostToolUse"][0]["matcher"]) def test_every_live_mutation_lane_requires_the_receipt(self): fixtures = ( From 8935f2dfab4ff164742eb39039c83dcdef8c650a Mon Sep 17 00:00:00 2001 From: Mohab Mohie Date: Wed, 19 Aug 2026 03:51:23 +0300 Subject: [PATCH 2/3] docs(memory): record plan-surface R25 mapping gotcha Capture snake_case/PascalCase normalization and hook-matcher allowlist coupling from #5195 / PR #5203 for reuse. --- .memory/events.jsonl | 1 + ...-snake-case-and-pascalcase-tool-names.json | 48 +++++++++++++++++++ ...ze-snake-case-and-pascalcase-tool-names.md | 1 + 3 files changed, 50 insertions(+) create mode 100644 .memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.json create mode 100644 .memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.md diff --git a/.memory/events.jsonl b/.memory/events.jsonl index 625a7fbb73f..47957fc9597 100644 --- a/.memory/events.jsonl +++ b/.memory/events.jsonl @@ -1173,3 +1173,4 @@ {"actor":"agent","event":"memory.created","id":"gotcha.failed-locator-by-must-not-run-before-getby-on-the-same-line","timestamp":"2026-08-18T19:17:30+03:00"} {"actor":"agent","event":"memory.created","id":"gotcha.empty-git-status-porcelain-piped-to-set-content-skips-git-status-txt","timestamp":"2026-08-18T20:03:45+03:00"} {"actor":"agent","event":"memory.updated","id":"gotcha.tiny-1-5b-soup-sft-is-not-enough-for-aider-whole-format","timestamp":"2026-08-18T20:21:32+03:00"} +{"actor":"agent","event":"memory.created","id":"gotcha.plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names","timestamp":"2026-08-19T03:51:15+03:00"} diff --git a/.memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.json b/.memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.json new file mode 100644 index 00000000000..54768aeccae --- /dev/null +++ b/.memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.json @@ -0,0 +1,48 @@ +{ + "body_path": "memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.md", + "content_hash": "sha256:aaa14fc7d0548c958458a11d157a25d3a41aafa09a86eb04c3c078bc7755f36d", + "created_at": "2026-08-19T03:51:15+03:00", + "evidence": [ + { + "id": "5195", + "kind": "task" + }, + { + "id": "5203", + "kind": "task" + } + ], + "facets": { + "applies_to": [ + "scripts/agents/guard.py", + ".codex/hooks.json", + ".claude/settings.json", + ".grok/hooks/lifecycle.json" + ], + "category": "gotcha", + "load_modes": [ + "coding", + "debugging" + ] + }, + "id": "gotcha.plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names", + "scope": { + "branch": null, + "kind": "project", + "project": "project.shaft-engine", + "task": null + }, + "source": { + "kind": "agent", + "task": "#5195 R25 plan surfaces" + }, + "status": "active", + "tags": [ + "r25", + "hooks", + "plan-surfaces" + ], + "title": "Plan-surface receipt mapping must normalize snake_case and PascalCase tool names", + "type": "gotcha", + "updated_at": "2026-08-19T03:51:15+03:00" +} diff --git a/.memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.md b/.memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.md new file mode 100644 index 00000000000..354a567f39d --- /dev/null +++ b/.memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.md @@ -0,0 +1 @@ +R25 `_research_preflight_events` originally mapped only `update_plan`. Hosts also emit `enter_plan_mode` / `EnterPlanMode`, `exit_plan_mode` / `ExitPlanMode`, and `todo_write` / `TodoWrite`. Lowercasing alone is not enough: `EnterPlanMode`.lower() is `enterplanmode`, not `enter_plan_mode`. Strip `_` and `-` before comparing plan-surface names. Separately, origin Claude/Codex/Grok PreToolUse and PostToolUse matchers are allowlists: mapping a tool in portable guard without widening those matchers leaves the session ledger blind even when the mapping is correct. Fixed in PR #5203 / #5195; keep both layers in sync when adding receipt tools. \ No newline at end of file From e5d0ec235f2f9f088e3339a609d5e77d8aaa70f5 Mon Sep 17 00:00:00 2001 From: Mohab Mohie Date: Wed, 19 Aug 2026 04:25:21 +0300 Subject: [PATCH 3/3] chore(harness): drop committed grok hooks and memory gotcha from R25 mapping PR --- .grok/hooks/lifecycle.json | 79 ------------------- .memory/events.jsonl | 1 - ...-snake-case-and-pascalcase-tool-names.json | 48 ----------- ...ze-snake-case-and-pascalcase-tool-names.md | 1 - .../scripts/test_agent_harness_portability.py | 16 +--- tests/scripts/test_guard_lifecycle.py | 6 +- 6 files changed, 3 insertions(+), 148 deletions(-) delete mode 100644 .grok/hooks/lifecycle.json delete mode 100644 .memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.json delete mode 100644 .memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.md diff --git a/.grok/hooks/lifecycle.json b/.grok/hooks/lifecycle.json deleted file mode 100644 index 8e4437f4c78..00000000000 --- a/.grok/hooks/lifecycle.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "description": "Portable SHAFT lifecycle and PreToolUse safety guard.", - "hooks": { - "PreToolUse": [ - { - "matcher": "Task|Agent|Read|Grep|Bash|PowerShell|shell_command|exec_command|functions[.]exec|Write|Edit|NotebookEdit|apply_patch|WebSearch|WebFetch|web__run|update_plan|enter_plan_mode|EnterPlanMode|exit_plan_mode|ExitPlanMode|todo_write|TodoWrite|mcp__shaft[-_]memory__(?:remember_memory|save_memory_patch)|mcp__mempalace__.*|mcp__graphify__.*", - "hooks": [ - { - "type": "command", - "command": "python3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", - "commandWindows": "py -3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", - "timeout": 10 - } - ] - } - ], - "PostToolUse": [ - { - "matcher": "Read|Grep|Bash|PowerShell|shell_command|exec_command|functions[.]exec|WebSearch|WebFetch|web__run|update_plan|enter_plan_mode|EnterPlanMode|exit_plan_mode|ExitPlanMode|todo_write|TodoWrite|mcp__shaft[-_]memory__.*|mcp__mempalace__.*|mcp__graphify__.*", - "hooks": [ - { - "type": "command", - "command": "python3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", - "commandWindows": "py -3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", - "timeout": 10 - } - ] - } - ], - "SessionStart": [ - { - "hooks": [ - { - "type": "command", - "command": "python3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", - "commandWindows": "py -3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", - "timeout": 30 - } - ] - } - ], - "UserPromptSubmit": [ - { - "hooks": [ - { - "type": "command", - "command": "python3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", - "commandWindows": "py -3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", - "timeout": 10 - } - ] - } - ], - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "python3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", - "commandWindows": "py -3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", - "timeout": 30 - } - ] - } - ], - "SubagentStop": [ - { - "hooks": [ - { - "type": "command", - "command": "python3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", - "commandWindows": "py -3 -c \"import pathlib,runpy;p=next(p/'scripts/agents/guard.py' for p in (pathlib.Path.cwd(),*pathlib.Path.cwd().parents) if (p/'scripts/agents/guard.py').is_file());runpy.run_path(str(p),run_name='__main__')\"", - "timeout": 30 - } - ] - } - ] - } -} diff --git a/.memory/events.jsonl b/.memory/events.jsonl index 47957fc9597..625a7fbb73f 100644 --- a/.memory/events.jsonl +++ b/.memory/events.jsonl @@ -1173,4 +1173,3 @@ {"actor":"agent","event":"memory.created","id":"gotcha.failed-locator-by-must-not-run-before-getby-on-the-same-line","timestamp":"2026-08-18T19:17:30+03:00"} {"actor":"agent","event":"memory.created","id":"gotcha.empty-git-status-porcelain-piped-to-set-content-skips-git-status-txt","timestamp":"2026-08-18T20:03:45+03:00"} {"actor":"agent","event":"memory.updated","id":"gotcha.tiny-1-5b-soup-sft-is-not-enough-for-aider-whole-format","timestamp":"2026-08-18T20:21:32+03:00"} -{"actor":"agent","event":"memory.created","id":"gotcha.plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names","timestamp":"2026-08-19T03:51:15+03:00"} diff --git a/.memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.json b/.memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.json deleted file mode 100644 index 54768aeccae..00000000000 --- a/.memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "body_path": "memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.md", - "content_hash": "sha256:aaa14fc7d0548c958458a11d157a25d3a41aafa09a86eb04c3c078bc7755f36d", - "created_at": "2026-08-19T03:51:15+03:00", - "evidence": [ - { - "id": "5195", - "kind": "task" - }, - { - "id": "5203", - "kind": "task" - } - ], - "facets": { - "applies_to": [ - "scripts/agents/guard.py", - ".codex/hooks.json", - ".claude/settings.json", - ".grok/hooks/lifecycle.json" - ], - "category": "gotcha", - "load_modes": [ - "coding", - "debugging" - ] - }, - "id": "gotcha.plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names", - "scope": { - "branch": null, - "kind": "project", - "project": "project.shaft-engine", - "task": null - }, - "source": { - "kind": "agent", - "task": "#5195 R25 plan surfaces" - }, - "status": "active", - "tags": [ - "r25", - "hooks", - "plan-surfaces" - ], - "title": "Plan-surface receipt mapping must normalize snake_case and PascalCase tool names", - "type": "gotcha", - "updated_at": "2026-08-19T03:51:15+03:00" -} diff --git a/.memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.md b/.memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.md deleted file mode 100644 index 354a567f39d..00000000000 --- a/.memory/memory/gotchas/plan-surface-receipt-mapping-must-normalize-snake-case-and-pascalcase-tool-names.md +++ /dev/null @@ -1 +0,0 @@ -R25 `_research_preflight_events` originally mapped only `update_plan`. Hosts also emit `enter_plan_mode` / `EnterPlanMode`, `exit_plan_mode` / `ExitPlanMode`, and `todo_write` / `TodoWrite`. Lowercasing alone is not enough: `EnterPlanMode`.lower() is `enterplanmode`, not `enter_plan_mode`. Strip `_` and `-` before comparing plan-surface names. Separately, origin Claude/Codex/Grok PreToolUse and PostToolUse matchers are allowlists: mapping a tool in portable guard without widening those matchers leaves the session ledger blind even when the mapping is correct. Fixed in PR #5203 / #5195; keep both layers in sync when adding receipt tools. \ No newline at end of file diff --git a/tests/scripts/test_agent_harness_portability.py b/tests/scripts/test_agent_harness_portability.py index ba57c3212c1..e49cc61a5eb 100644 --- a/tests/scripts/test_agent_harness_portability.py +++ b/tests/scripts/test_agent_harness_portability.py @@ -679,26 +679,14 @@ def test_hook_configs_share_one_cwd_independent_lifecycle_contract(self): def test_hook_configs_are_tracked_for_host_local_trust(self): tracked = subprocess.run( # nosec B603 B607 - fixed read-only git command. - [ - "git", - "ls-files", - "--error-unmatch", - ".claude/settings.json", - ".codex/hooks.json", - ".grok/hooks/lifecycle.json", - "scripts/agents/guard.py", - ], + ["git", "ls-files", "--error-unmatch", ".claude/settings.json", ".codex/hooks.json", "scripts/agents/guard.py"], cwd=ROOT, capture_output=True, text=True, check=False, ) self.assertEqual(tracked.returncode, 0, tracked.stderr) - for path in ( - ROOT / ".claude/settings.json", - ROOT / ".codex/hooks.json", - ROOT / ".grok/hooks/lifecycle.json", - ): + for path in (ROOT / ".claude/settings.json", ROOT / ".codex/hooks.json"): self.assertNotIn("bypass-hook-trust", path.read_text(encoding="utf-8")) def test_equivalent_host_hook_events_produce_equivalent_outcomes(self): diff --git a/tests/scripts/test_guard_lifecycle.py b/tests/scripts/test_guard_lifecycle.py index 0ac57a8e9f8..a648d7ae54b 100644 --- a/tests/scripts/test_guard_lifecycle.py +++ b/tests/scripts/test_guard_lifecycle.py @@ -1654,11 +1654,7 @@ def test_portable_hook_matchers_observe_receipt_and_mutation_tools(self): "todo_write", "TodoWrite", ) - for relative in ( - ".claude/settings.json", - ".codex/hooks.json", - ".grok/hooks/lifecycle.json", - ): + for relative in (".claude/settings.json", ".codex/hooks.json"): with self.subTest(relative=relative): text = (Path(__file__).resolve().parents[2] / relative).read_text( encoding="utf-8"