claude-dev-env 8.106.1 → 8.106.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -174,11 +174,6 @@ export const WINDOWS_PYTHON_LAUNCHER_COMMAND = 'py -3';
174
174
  export const PYTHON_PROBE_TIMEOUT_MILLISECONDS = 10_000;
175
175
  export const ADDITIONAL_SETTINGS_BLOB_PREFIX = 'settings-';
176
176
  export const CODEX_NATIVE_ROUTING_MATCHER = 'multi_agent_v1__spawn_agent';
177
- export const CODEX_SUBAGENT_PROMPT_MATCHER = 'Agent|Task';
178
- export const ALL_CODEX_NATIVE_PRE_TOOL_USE_MATCHERS = new Set([
179
- CODEX_NATIVE_ROUTING_MATCHER,
180
- CODEX_SUBAGENT_PROMPT_MATCHER,
181
- ]);
182
177
  export const ALL_CODEX_NATIVE_STOP_HOOK_SCRIPT_NAMES = new Set([
183
178
  'open_tasks_stop_gate.py',
184
179
  'owned_pr_merge_stop_gate.py',
@@ -130,11 +130,10 @@ test('seeds one editable policy and one native Codex routing hook', () => {
130
130
  assert.match(firstRoutingGroups[0].hooks[0].command, /subagent_model_routing\.mjs/);
131
131
  assert.match(firstRoutingGroups[0].hooks[1].command, /spawn_readiness_hook\.py/);
132
132
  assert.match(firstRoutingGroups[0].hooks[2].command, /spawn_oversight_hook\.py/);
133
- const firstSpawnPromptGroups = firstCodexHooks.hooks.PreToolUse.filter(
134
- group => group.matcher === 'Agent|Task',
133
+ assert.deepEqual(
134
+ firstCodexHooks.hooks.PreToolUse.map(group => group.matcher),
135
+ ['multi_agent_v1__spawn_agent'],
135
136
  );
136
- assert.equal(firstSpawnPromptGroups.length, 1);
137
- assert.match(firstSpawnPromptGroups[0].hooks[0].command, /skill_loaded_reminder\.py/);
138
137
  assert.deepEqual(Object.keys(firstCodexHooks.hooks), ['PreToolUse', 'UserPromptSubmit', 'PostToolUse', 'Stop', 'SessionStart']);
139
138
  const allCodexPostToolUseCommands = firstCodexHooks.hooks.PostToolUse.flatMap(
140
139
  group => group.hooks.map(hook => hook.command),
@@ -276,6 +275,7 @@ test('Codex reinstall removes omitted package hooks and preserves user configura
276
275
  const allLegacyRegistrations = [
277
276
  ['PreToolUse', 'Edit', '.claude', 'blocking/pre_tool_use_dispatcher.py'],
278
277
  ['PreToolUse', 'Bash', '.agents', 'blocking/bash_pre_tool_use_dispatcher.py'],
278
+ ['PreToolUse', 'Agent|Task', 'custom-codex', 'session/skill_loaded_reminder.py'],
279
279
  ['SessionStart', '*', 'custom-codex', 'session/issue_tracker_session_starter.py'],
280
280
  ['Stop', '*', '.claude', 'session/skill_loaded_reminder.py'],
281
281
  ['SessionEnd', '*', 'custom-codex', 'lifecycle/session_end_cleanup.py'],
@@ -311,8 +311,13 @@ test('Codex reinstall removes omitted package hooks and preserves user configura
311
311
 
312
312
  const migratedHooks = JSON.parse(readFileSync(codexHooksPath, 'utf8'));
313
313
  const allSpawnGroups = migratedHooks.hooks.PreToolUse.filter(eachGroup =>
314
- ['Agent|Task', 'multi_agent_v1__spawn_agent'].includes(eachGroup.matcher));
315
- assert.equal(allSpawnGroups.length, 2);
314
+ eachGroup.matcher === 'multi_agent_v1__spawn_agent');
315
+ assert.equal(allSpawnGroups.length, 1);
316
+ assert.equal(
317
+ migratedHooks.hooks.PreToolUse.flatMap(eachGroup => eachGroup.hooks)
318
+ .some(eachHook => String(eachHook.command).includes('skill_loaded_reminder.py')),
319
+ false,
320
+ );
316
321
  migratedHooks.hooks.PreToolUse = migratedHooks.hooks.PreToolUse.filter(eachGroup => !allSpawnGroups.includes(eachGroup));
317
322
  const allPackagePromptCommands = (migratedHooks.hooks.UserPromptSubmit ?? [])
318
323
  .flatMap(eachGroup => eachGroup.hooks)
package/bin/install.mjs CHANGED
@@ -50,7 +50,7 @@ import {
50
50
  SUBAGENT_MODEL_POLICY_FILE_NAME,
51
51
  WINDOWS_PYTHON_LAUNCHER_COMMAND,
52
52
  PYTHON_PROBE_TIMEOUT_MILLISECONDS,
53
- ALL_CODEX_NATIVE_PRE_TOOL_USE_MATCHERS,
53
+ CODEX_NATIVE_ROUTING_MATCHER,
54
54
  ALL_CODEX_NATIVE_SESSION_START_HOOK_SCRIPT_NAMES,
55
55
  ALL_CODEX_NATIVE_STOP_HOOK_SCRIPT_NAMES,
56
56
  ALL_CODEX_NATIVE_POST_TOOL_USE_HOOK_SCRIPT_NAMES,
@@ -1315,6 +1315,7 @@ export const RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS = new Set([
1315
1315
  'blocking/session_title_stop_gate.py',
1316
1316
  'session/issue_tracker_session_starter.py',
1317
1317
  'session/deepen_first_prompt.py',
1318
+ 'workflow/investigation_tracker_reset.py',
1318
1319
  ]);
1319
1320
 
1320
1321
  export const MOVED_HOOK_RELATIVE_PATHS = new Set([
@@ -2283,7 +2284,7 @@ function selectCodexNativeHooksConfig(hooksConfig) {
2283
2284
  ...hooksConfig,
2284
2285
  hooks: {
2285
2286
  PreToolUse: Array.isArray(preToolUseGroups)
2286
- ? preToolUseGroups.filter(group => ALL_CODEX_NATIVE_PRE_TOOL_USE_MATCHERS.has(group?.matcher))
2287
+ ? preToolUseGroups.filter(group => group?.matcher === CODEX_NATIVE_ROUTING_MATCHER)
2287
2288
  : [],
2288
2289
  UserPromptSubmit: Array.isArray(userPromptSubmitGroups) ? userPromptSubmitGroups : [],
2289
2290
  PostToolUse: selectCodexNativeGroupsByScriptName(
@@ -1245,6 +1245,7 @@ test('retired hook registrations stay managed so reinstall removes them', () =>
1245
1245
  'blocking/session_title_stop_gate.py',
1246
1246
  'session/issue_tracker_session_starter.py',
1247
1247
  'session/deepen_first_prompt.py',
1248
+ 'workflow/investigation_tracker_reset.py',
1248
1249
  ]);
1249
1250
  const shippedHooks = JSON.parse(
1250
1251
  readFileSync(new URL('../hooks/hooks.json', import.meta.url), 'utf8')
@@ -26,8 +26,8 @@ Run `codex-compat bridge --surface <name> --payload '<json-object>'`. The bridge
26
26
 
27
27
  The package installer seeds `subagent-model-policy.json` in the agents-home
28
28
  `rules/` directory when the file is absent. It registers one `PreToolUse` group
29
- for `multi_agent_v1__spawn_agent` and one for `Agent|Task` in `$CODEX_HOME/hooks.json`.
30
- Before adding these groups, the installer removes obsolete package-owned hook
29
+ for `multi_agent_v1__spawn_agent` in `$CODEX_HOME/hooks.json`.
30
+ Before adding this group, the installer removes obsolete package-owned hook
31
31
  registrations from every event. Custom hook entries and Codex's enabled or
32
32
  disabled hook settings stay unchanged. Claude-only hook groups do not enter the
33
33
  Codex file.
@@ -28,6 +28,6 @@ Each family page starts with a title and an agent-facing behavior summary. Its f
28
28
  - [Conduct gates](./conduct-gates.md) covers chat replies, edit markers, step notes, checked claims, and the pull request lifecycle skill.
29
29
  - [Session context](./session-context.md) covers skill reminders, startup guidance, and the auto mode denial quick fix.
30
30
  - [Spawn routing](./spawn-routing.md) covers spawn readiness, pacing, and model selection.
31
- - [Observability](./observability.md) covers instruction loads, edited files, and investigation resets.
31
+ - [Observability](./observability.md) covers instruction loads and edited files.
32
32
  - [Read gates](./read-gates.md) covers the image size cap on Read.
33
33
  - [Lifecycle cleanup](./lifecycle-cleanup.md) covers nested checkouts, worktree setup, and session cleanup.
@@ -1,18 +1,16 @@
1
1
  # Observability
2
2
 
3
- This family records instruction loads and file edits for later checks. Delegation also clears the lead session's investigation timer so the agent can continue its investigation.
3
+ This family records instruction loads and file edits for later checks.
4
4
 
5
5
  ## Checks
6
6
 
7
7
  - `observability/instructions_loaded_logger.py` writes selected instruction-load fields to a JSONL log.
8
8
  - `observability/session_file_edit_tracker.py` records edited file paths in a per-session tracker for the stage gate.
9
- - `workflow/investigation_tracker_reset.py` clears the investigation tracker after delegation.
10
9
 
11
10
  ## When it fires
12
11
 
13
12
  - `observability/instructions_loaded_logger.py` runs on `InstructionsLoaded`, matcher `session_start|nested_traversal|path_glob_match|include|compact`, timeout `10` seconds in `hooks.json`.
14
13
  - `observability/session_file_edit_tracker.py` runs on `PostToolUse`, matcher `Write|Edit|MultiEdit|apply_patch`, timeout `30` seconds in `hooks.json`.
15
- - `workflow/investigation_tracker_reset.py` runs on `PostToolUse`, matcher `Agent|Task|TeamCreate`, timeout `30` seconds in `hooks.json`.
16
14
 
17
15
  ## Proving it
18
16
 
@@ -22,10 +20,8 @@ Preconditions:
22
20
 
23
21
  - **Instruction log.** Input is an `InstructionsLoaded` payload with a file path and load reason. Run `python -m pytest packages/claude-dev-env/hooks/observability/test_instructions_loaded_logger.py -q`. The adjacent test observes one JSONL record with the selected fields.
24
22
  - **Edit tracker.** Input is a Write payload with a file path and session ID. Run `python -m pytest packages/claude-dev-env/hooks/observability/test_session_file_edit_tracker.py -q`. The adjacent test observes the resolved path in that session's tracker.
25
- - **Investigation reset.** Input is a completed Agent, Task, or TeamCreate call. Run `python -m pytest packages/claude-dev-env/hooks/workflow/test_investigation_tracker_reset.py -q`. The adjacent test observes removal of the tracker file.
26
23
 
27
24
  ## Gotchas
28
25
 
29
26
  - The instruction logger keeps only selected payload fields. It exits zero if logging fails.
30
27
  - The edit tracker never blocks a tool call. Its per-session file is read later by the stage gate.
31
- - The investigation reset applies to delegation tools. Its companion investigation gate lives outside this Claude hook registration map.
@@ -1,6 +1,6 @@
1
1
  # Session context
2
2
 
3
- This family injects reminders and task guidance when an agent starts, resumes, submits a prompt, spawns a helper, starts an artifact page, or hits an auto mode denial. The startup scripts leave the session quiet when their opt-in conditions fail.
3
+ This family injects reminders and task guidance when an agent starts, resumes, submits a prompt, starts a workflow helper, starts an artifact page, or hits an auto mode denial. The startup scripts leave the session quiet when their opt-in conditions fail.
4
4
 
5
5
  ## Checks
6
6
 
@@ -17,7 +17,7 @@ This family injects reminders and task guidance when an agent starts, resumes, s
17
17
 
18
18
  ## When it fires
19
19
 
20
- - `session/skill_loaded_reminder.py` runs on `PreToolUse` matcher `Agent|Task` at `10` seconds, `UserPromptSubmit` matcher empty at `10` seconds, `SessionStart` matcher `compact` at `10` seconds, and `SubagentStart` matcher `workflow-subagent` at `10` seconds in `hooks.json`.
20
+ - `session/skill_loaded_reminder.py` runs on `UserPromptSubmit` matcher empty at `10` seconds, `SessionStart` matcher `compact` at `10` seconds, and `SubagentStart` matcher `workflow-subagent` at `10` seconds in `hooks.json`.
21
21
  - `session/compaction_recall_prompt.py` runs on `SessionStart` matcher `compact` at `10` seconds in `hooks.json`. The installer also writes it to the Codex `hooks.json`, the only `SessionStart` hook Codex gets.
22
22
  - `session/task_tool_prompt.py`, `session/working_style_prompt.py`, `session/advisor_rules_prompt.py`, and `session/orchestrator_auto_starter.py` run on `SessionStart`, matcher empty, timeout `10` seconds each in `hooks.json`.
23
23
  - `session/plugin_session_baseline_writer.py` runs on `SessionStart`, matcher empty, at `10` seconds, so it sees every source. `session/plugin_change_warning.py` runs on `UserPromptSubmit`, matcher empty, at `10` seconds in `hooks.json`.
@@ -30,7 +30,7 @@ Preconditions:
30
30
 
31
31
  - Run each command from the repository root with Python and pytest available. Enable opt-in flags only inside the tests.
32
32
 
33
- - **Skill reminder.** Input is a helper spawn whose prompt lacks poteto-mode. Run `python -m pytest packages/claude-dev-env/hooks/session/test_skill_loaded_reminder.py -q`. The adjacent test observes the reminder and leaves an already prepared prompt unchanged.
33
+ - **Skill reminder.** Input is a compaction, a user turn, or a workflow helper start. Run `python -m pytest packages/claude-dev-env/hooks/session/test_skill_loaded_reminder.py -q`. The adjacent test observes the reminder, silence on a spawn payload, and no `PreToolUse` registration in `hooks.json`.
34
34
  - **Compaction recall.** Input is a Claude Code transcript or a Codex rollout with typed, meta, sidechain, peer, and harness entries across a compaction. Run `python -m pytest packages/claude-dev-env/hooks/session/test_compaction_user_messages.py packages/claude-dev-env/hooks/session/test_compaction_trim.py packages/claude-dev-env/hooks/session/test_compaction_recall_prompt.py packages/claude-dev-env/hooks/session/test_compaction_trim_eval.py -q`. The tests observe only typed messages, a stable result across a second compaction, the trim rules, the recall budget, and the trimmed lines in the hook's `additionalContext`. `python packages/claude-dev-env/hooks/session/compaction_trim_eval.py` trims each fixture transcript with planted facts, prints the share of facts that survive, the reversals marked, and the compression ratio, and exits 1 on any loss.
35
35
  - **Task tracking.** Input is a SessionStart event. Run `python -m pytest packages/claude-dev-env/hooks/session/test_task_tool_prompt.py -q`. The adjacent test observes a `SessionStart` context directive that names the task tool.
36
36
  - **Working style.** Input is a SessionStart event. Run `python -m pytest packages/claude-dev-env/hooks/session/test_working_style_prompt.py -q`. The adjacent test observes the fixed context text.
@@ -44,7 +44,7 @@ Preconditions:
44
44
 
45
45
  - The recall keeps the first message and the newest that fit in 6,000 characters, under the 2,500 token preview limit Codex applies to hook context. A message over 1,500 characters is cut, and the line says how much was left out.
46
46
  - The overruled mark comes from cue phrases such as `instead` or `change of plan` and shared words. A reversal with no cue phrase gets no mark, and the later line still overrides by its place. A cue in a message that agrees with an earlier one, such as `dont use haiku` after `use Python trim instead of Haiku`, marks that earlier line too, so the context tells the agent to follow the later message where the two differ.
47
- - `session/skill_loaded_reminder.py` has four registration points. Its tests cover helper prompt rewriting and repeated prompt suppression.
47
+ - `session/skill_loaded_reminder.py` has three registration points. An `Agent` or `Task` helper gets no poteto-mode line from it; a brief that needs the skill names it, or spawns `pstack:poteto-agent`. Its tests cover the compaction restore, the workflow helper reminder, silence on a user turn while the skill stays loaded, silence on a spawn payload, and no `PreToolUse` registration in `hooks.json`.
48
48
  - The advisor prompt depends on settings and an environment disable flag. The two starter scripts each use separate opt-in checks.
49
49
  - The issue tracker starter requires the checkout to appear in the project path registry.
50
50
  - `advisory/auto_mode_denial_quick_fix.py` lives under `advisory/` but belongs here because its only output is agent context and a one-line user message.
package/hooks/hooks.json CHANGED
@@ -5,11 +5,6 @@
5
5
  {
6
6
  "matcher": "Agent|Task",
7
7
  "hooks": [
8
- {
9
- "type": "command",
10
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/skill_loaded_reminder.py",
11
- "timeout": 10
12
- },
13
8
  {
14
9
  "type": "command",
15
10
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/haiku_spawn_slim_gate.py",
@@ -526,16 +521,6 @@
526
521
  }
527
522
  ]
528
523
  },
529
- {
530
- "matcher": "Agent|Task|TeamCreate",
531
- "hooks": [
532
- {
533
- "type": "command",
534
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/workflow/investigation_tracker_reset.py",
535
- "timeout": 30
536
- }
537
- ]
538
- },
539
524
  {
540
525
  "matcher": "Bash|PowerShell",
541
526
  "hooks": [
@@ -3,52 +3,29 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  __all__ = [
6
- "ALL_SELF_LOADING_SUBAGENT_TYPES",
7
6
  "COMPACTION_REMINDER",
8
7
  "COMPACTION_SOURCE",
9
8
  "NOT_LOADED_REMINDER",
10
9
  "ALL_POTETO_MODE_SKILL_NAMES",
11
- "PRE_TOOL_USE_EVENT_NAME",
12
- "PROMPT_SEPARATOR",
13
10
  "SESSION_START_EVENT_NAME",
14
11
  "ALL_SLASH_COMMAND_MARKERS",
15
12
  "SUBAGENT_START_EVENT_NAME",
16
- "CLAUDE_SUBAGENT_PROMPT_PREFIX",
17
- "CODEX_SUBAGENT_PROMPT_PREFIX",
18
- "ALL_SPAWN_PROMPT_FIELDS_AND_PREFIXES_BY_TOOL_NAME",
19
13
  "USER_PROMPT_SUBMIT_EVENT_NAME",
20
14
  "WORKFLOW_SUBAGENT_TYPE",
21
15
  ]
22
16
 
23
- PRE_TOOL_USE_EVENT_NAME = "PreToolUse"
24
17
  SESSION_START_EVENT_NAME = "SessionStart"
25
18
  USER_PROMPT_SUBMIT_EVENT_NAME = "UserPromptSubmit"
26
19
  COMPACTION_SOURCE = "compact"
27
20
  SUBAGENT_START_EVENT_NAME = "SubagentStart"
28
21
  WORKFLOW_SUBAGENT_TYPE = "workflow-subagent"
29
22
 
30
- ALL_SELF_LOADING_SUBAGENT_TYPES = frozenset({"pstack:poteto-agent", "poteto-agent"})
31
-
32
23
  ALL_POTETO_MODE_SKILL_NAMES = ("poteto-mode", "pstack:poteto-mode")
33
24
  SKILL_TOOL_NAME = "Skill"
34
25
  ALL_SLASH_COMMAND_MARKERS = (
35
26
  "<command-name>/poteto-mode</command-name>",
36
27
  "<command-name>/pstack:poteto-mode</command-name>",
37
28
  )
38
- PROMPT_SEPARATOR = "\n\n"
39
-
40
- CLAUDE_SUBAGENT_PROMPT_PREFIX = (
41
- "Before any other work, invoke the poteto-mode skill with the Skill tool. "
42
- "Every prompt you write for a subagent opens with the same instruction."
43
- )
44
-
45
- CODEX_SUBAGENT_PROMPT_PREFIX = "$poteto-mode"
46
-
47
- ALL_SPAWN_PROMPT_FIELDS_AND_PREFIXES_BY_TOOL_NAME = {
48
- "Agent": ("prompt", CLAUDE_SUBAGENT_PROMPT_PREFIX),
49
- "Task": ("prompt", CLAUDE_SUBAGENT_PROMPT_PREFIX),
50
- "spawn_agent": ("message", CODEX_SUBAGENT_PROMPT_PREFIX),
51
- }
52
29
 
53
30
  NOT_LOADED_REMINDER = (
54
31
  "The poteto-mode skill is not loaded in this context. "
@@ -1,9 +1,8 @@
1
1
  #!/usr/bin/env python3
2
- """Hook that restores poteto-mode after compaction and loads it for helpers.
2
+ """Hook that restores poteto-mode after compaction and loads it for workflow helpers.
3
3
 
4
4
  ::
5
5
 
6
- Agent or Codex spawn_agent, skill not named -> prompt opens with "invoke poteto-mode"
7
6
  Workflow script helper starts -> "invoke poteto-mode now"
8
7
  compact SessionStart, skill invoked before -> "invoke the skill again"
9
8
  compact SessionStart, no prior invocation
@@ -28,55 +27,21 @@ if _hooks_dir not in sys.path:
28
27
  sys.path.insert(0, _hooks_dir)
29
28
 
30
29
  from hooks_constants.skill_loaded_reminder_constants import (
31
- ALL_SELF_LOADING_SUBAGENT_TYPES,
32
30
  COMPACTION_REMINDER,
33
31
  COMPACTION_SOURCE,
34
32
  NOT_LOADED_REMINDER,
35
33
  ALL_POTETO_MODE_SKILL_NAMES,
36
- PRE_TOOL_USE_EVENT_NAME,
37
- PROMPT_SEPARATOR,
38
34
  SESSION_START_EVENT_NAME,
39
35
  ALL_SLASH_COMMAND_MARKERS,
40
36
  SUBAGENT_START_EVENT_NAME,
41
- ALL_SPAWN_PROMPT_FIELDS_AND_PREFIXES_BY_TOOL_NAME,
42
37
  USER_PROMPT_SUBMIT_EVENT_NAME,
43
38
  WORKFLOW_SUBAGENT_TYPE,
44
39
  )
45
- from hooks_constants.pre_tool_use_allow_output import write_pre_tool_use_allow_to_stdout
46
40
  from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
47
41
  from hooks_constants.setup_project_paths_constants import DECODE_ERRORS_POLICY, UTF8_ENCODING
48
42
  from transcript_skill_scan import skill_invocation_status
49
43
 
50
44
 
51
- def subagent_input_with_poteto_mode(
52
- tool_name: str,
53
- all_tool_input_fields: dict[str, object],
54
- ) -> dict[str, object] | None:
55
- """Return the spawn input with the poteto-mode invocation first, or None to leave it.
56
-
57
- ::
58
-
59
- Agent {"prompt": "Reply leaf."} -> {"prompt": "Before any ...\\n\\nReply leaf."}
60
- spawn_agent {"message": "Fix it."} -> {"message": "$poteto-mode\\n\\nFix it."}
61
- Agent {"prompt": "Invoke poteto-mode, then ..."} -> None
62
- Agent {"subagent_type": "poteto-agent", ...} -> None, it loads the skill
63
-
64
- Claude Code spawns through Agent or Task and Codex through spawn_agent, so
65
- each tool name carries its own prompt field and invocation text.
66
-
67
- Args:
68
- tool_name: The spawn tool the session called.
69
- all_tool_input_fields: The spawn input the session is about to send.
70
- """
71
- field_name, invocation_prefix = ALL_SPAWN_PROMPT_FIELDS_AND_PREFIXES_BY_TOOL_NAME[tool_name]
72
- prompt = all_tool_input_fields.get(field_name)
73
- if not isinstance(prompt, str) or ALL_POTETO_MODE_SKILL_NAMES[0] in prompt:
74
- return None
75
- if all_tool_input_fields.get("subagent_type") in ALL_SELF_LOADING_SUBAGENT_TYPES:
76
- return None
77
- return {**all_tool_input_fields, field_name: invocation_prefix + PROMPT_SEPARATOR + prompt}
78
-
79
-
80
45
  def poteto_mode_status(all_transcript_lines: Iterable[str]) -> tuple[bool, bool]:
81
46
  """Report whether the skill was invoked and whether it remains loaded.
82
47
 
@@ -160,26 +125,11 @@ def reminder_for(all_hook_fields: dict[str, object]) -> str | None:
160
125
  return NOT_LOADED_REMINDER if was_invoked and not is_loaded else None
161
126
 
162
127
 
163
- def _is_subagent_spawn(all_hook_fields: dict[str, object]) -> bool:
164
- return (
165
- all_hook_fields.get("hook_event_name") == PRE_TOOL_USE_EVENT_NAME
166
- and all_hook_fields.get("tool_name") in ALL_SPAWN_PROMPT_FIELDS_AND_PREFIXES_BY_TOOL_NAME
167
- and isinstance(all_hook_fields.get("tool_input"), dict)
168
- )
169
-
170
-
171
128
  def main() -> None:
172
- """Rewrite a subagent prompt, or print the reminder its event needs, or stay quiet."""
129
+ """Print the reminder its event needs, or stay quiet."""
173
130
  hook_payload = read_hook_input_dictionary_from_stdin()
174
131
  if hook_payload is None:
175
132
  return
176
- if _is_subagent_spawn(hook_payload):
177
- rewritten_input = subagent_input_with_poteto_mode(
178
- hook_payload["tool_name"], hook_payload["tool_input"]
179
- )
180
- if rewritten_input is not None:
181
- write_pre_tool_use_allow_to_stdout(rewritten_input)
182
- return
183
133
  reminder = reminder_for(hook_payload)
184
134
  if reminder is None:
185
135
  return
@@ -1,4 +1,4 @@
1
- """Tests for skill_loaded_reminder. Checks restoration after compaction and helper prompts."""
1
+ """Tests for skill_loaded_reminder. Checks restoration after compaction and spawn pass-through."""
2
2
 
3
3
  import json
4
4
  import sys
@@ -76,10 +76,6 @@ def _run_main(hook_payload: object) -> str:
76
76
  return captured_stdout.getvalue()
77
77
 
78
78
 
79
- def _agent_call(tool_input: dict[str, object], tool_name: str = "Agent") -> dict[str, object]:
80
- return {"hook_event_name": "PreToolUse", "tool_name": tool_name, "tool_input": tool_input}
81
-
82
-
83
79
  def _user_turn(transcript_path: Path) -> dict[str, object]:
84
80
  return {"hook_event_name": "UserPromptSubmit", "transcript_path": str(transcript_path)}
85
81
 
@@ -90,85 +86,28 @@ def _write_transcript(tmp_path: Path, all_lines: list[str]) -> Path:
90
86
  return transcript_path
91
87
 
92
88
 
93
- class TestSubagentSpawn:
94
- def test_a_plain_subagent_prompt_opens_with_the_poteto_mode_invocation(self) -> None:
95
- emitted = json.loads(
96
- _run_main(
97
- _agent_call(
98
- {
99
- "description": "Find callers",
100
- "subagent_type": "Explore",
101
- "prompt": "List every caller of main.",
102
- }
103
- )
104
- )
105
- )
106
- hook_output = emitted["hookSpecificOutput"]
107
- rewritten_input = hook_output["updatedInput"]
108
- assert hook_output["permissionDecision"] == "allow"
109
- assert rewritten_input["prompt"].startswith(
110
- "Before any other work, invoke the poteto-mode skill with the Skill tool."
111
- )
112
- assert rewritten_input["prompt"].endswith("\n\nList every caller of main.")
113
- assert rewritten_input["subagent_type"] == "Explore"
114
- assert rewritten_input["description"] == "Find callers"
115
-
116
- def test_the_legacy_task_tool_name_is_rewritten_too(self) -> None:
117
- emitted = json.loads(_run_main(_agent_call({"prompt": "Go."}, tool_name="Task")))
118
- assert "poteto-mode" in emitted["hookSpecificOutput"]["updatedInput"]["prompt"]
119
-
120
- def test_a_prompt_that_already_names_the_skill_is_left_alone(self) -> None:
121
- assert _run_main(_agent_call({"prompt": "Invoke poteto-mode first. Then go."})) == ""
89
+ class TestRegistration:
90
+ def test_a_spawn_payload_from_a_stale_registration_prints_nothing(self) -> None:
91
+ stale_spawn_payload = {
92
+ "hook_event_name": "PreToolUse",
93
+ "tool_name": "Agent",
94
+ "tool_input": {"prompt": "Reply leaf."},
95
+ }
96
+ assert _run_main(stale_spawn_payload) == ""
122
97
 
123
- def test_a_poteto_agent_is_left_alone_because_it_loads_the_skill_itself(self) -> None:
124
- assert (
125
- _run_main(_agent_call({"subagent_type": "pstack:poteto-agent", "prompt": "Go."})) == ""
98
+ def test_no_pre_tool_use_group_runs_the_reminder(self) -> None:
99
+ shipped_hooks = json.loads(
100
+ (Path(__file__).resolve().parents[1] / "hooks.json").read_text(encoding="utf-8")
126
101
  )
127
-
128
- def test_another_tool_is_left_alone(self) -> None:
129
- assert _run_main(_agent_call({"prompt": "Go."}, tool_name="Bash")) == ""
130
-
131
-
132
- class TestSubagentInputWithPotetoMode:
133
- def test_every_other_input_field_is_kept(self) -> None:
134
- rewritten_input = reminder.subagent_input_with_poteto_mode(
135
- "Agent", {"prompt": "Go.", "model": "sonnet", "run_in_background": True}
136
- )
137
- assert rewritten_input is not None
138
- assert rewritten_input["model"] == "sonnet"
139
- assert rewritten_input["run_in_background"] is True
140
-
141
- def test_an_input_with_no_prompt_is_left_alone(self) -> None:
142
- assert reminder.subagent_input_with_poteto_mode("Agent", {"description": "Go"}) is None
143
-
144
- def test_a_codex_spawn_input_with_only_items_is_left_alone(self) -> None:
145
- assert (
146
- reminder.subagent_input_with_poteto_mode(
147
- "spawn_agent", {"items": [{"type": "text", "text": "Go."}]}
148
- )
149
- is None
150
- )
151
-
152
-
153
- class TestCodexSpawn:
154
- def test_a_codex_spawn_message_opens_with_the_skill_mention(self) -> None:
155
- emitted = json.loads(
156
- _run_main(
157
- _agent_call(
158
- {"message": "Fix the failing test.", "model": "gpt-6-luna"},
159
- tool_name="spawn_agent",
160
- )
161
- )
162
- )
163
- rewritten_input = emitted["hookSpecificOutput"]["updatedInput"]
164
- assert rewritten_input["message"] == "$poteto-mode\n\nFix the failing test."
165
- assert rewritten_input["model"] == "gpt-6-luna"
166
- assert "prompt" not in rewritten_input
167
-
168
- def test_a_codex_spawn_message_that_already_mentions_the_skill_is_left_alone(self) -> None:
169
- assert (
170
- _run_main(_agent_call({"message": "$poteto-mode\n\nFix it."}, tool_name="spawn_agent"))
171
- == ""
102
+ all_pre_tool_use_commands = [
103
+ each_hook.get("command", "")
104
+ for each_group in shipped_hooks["hooks"]["PreToolUse"]
105
+ for each_hook in each_group["hooks"]
106
+ ]
107
+ assert all_pre_tool_use_commands
108
+ assert not any(
109
+ "skill_loaded_reminder.py" in each_command
110
+ for each_command in all_pre_tool_use_commands
172
111
  )
173
112
 
174
113
 
@@ -353,9 +292,7 @@ class TestReminderFor:
353
292
  assert hook_output["hookEventName"] == "SubagentStart"
354
293
  assert hook_output["additionalContext"] == NOT_LOADED_REMINDER
355
294
 
356
- def test_an_agent_tool_helper_starting_prints_nothing_since_its_prompt_carries_the_line(
357
- self,
358
- ) -> None:
295
+ def test_an_agent_tool_helper_starting_prints_nothing(self) -> None:
359
296
  assert (
360
297
  _run_main({"hook_event_name": "SubagentStart", "agent_type": "general-purpose"}) == ""
361
298
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.106.1",
3
+ "version": "8.106.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,48 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PostToolUse hook — resets the serial investigation tracker when delegation occurs.
3
-
4
- Clears the investigation timestamp tracker when the lead session delegates work
5
- via Agent, Task, or TeamCreate tools. This allows the lead to resume limited
6
- diagnostic calls after properly delegating.
7
-
8
- Companion: blocking/serial-investigation-blocker.py (PreToolUse on Read|Bash|Grep)
9
- """
10
-
11
- import json
12
- import os
13
- import sys
14
-
15
- TRACKER_STATE_PATH = os.path.join(
16
- os.path.expanduser("~"), ".claude", "runtime", "investigation-tracker.json"
17
- )
18
-
19
- DELEGATION_TOOLS = frozenset({"Agent", "Task", "TeamCreate"})
20
-
21
-
22
- def clear_investigation_tracker() -> None:
23
- try:
24
- os.remove(TRACKER_STATE_PATH)
25
- except FileNotFoundError:
26
- pass
27
- except OSError:
28
- pass
29
-
30
-
31
- def main() -> None:
32
- try:
33
- hook_input = json.load(sys.stdin)
34
- except json.JSONDecodeError:
35
- sys.exit(0)
36
- if not isinstance(hook_input, dict):
37
- sys.exit(0)
38
-
39
- tool_name = hook_input.get("tool_name", "")
40
- if tool_name not in DELEGATION_TOOLS:
41
- sys.exit(0)
42
-
43
- clear_investigation_tracker()
44
- sys.exit(0)
45
-
46
-
47
- if __name__ == "__main__":
48
- main()
@@ -1,90 +0,0 @@
1
- """Behavior tests for investigation tracker reset."""
2
-
3
- from __future__ import annotations
4
-
5
- import importlib.util
6
- import io
7
- import json
8
- from pathlib import Path
9
- from unittest import mock
10
-
11
- import pytest
12
-
13
-
14
- HOOK_DIRECTORY = Path(__file__).parent
15
- HOOK_SPEC = importlib.util.spec_from_file_location(
16
- "investigation_tracker_reset",
17
- HOOK_DIRECTORY / "investigation_tracker_reset.py",
18
- )
19
- assert HOOK_SPEC is not None
20
- assert HOOK_SPEC.loader is not None
21
- HOOK_MODULE = importlib.util.module_from_spec(HOOK_SPEC)
22
- HOOK_SPEC.loader.exec_module(HOOK_MODULE)
23
-
24
-
25
- def _run_main(input_text: str) -> int | None:
26
- with mock.patch("sys.stdin", io.StringIO(input_text)):
27
- try:
28
- HOOK_MODULE.main()
29
- except SystemExit as system_exit:
30
- if system_exit.code not in (None, 0):
31
- raise
32
- return system_exit.code if isinstance(system_exit.code, int) else None
33
- return None
34
-
35
-
36
- @pytest.mark.parametrize("delegation_tool", ["Agent", "Task", "TeamCreate"])
37
- def test_main_removes_tracker_after_delegation(
38
- tmp_path: Path,
39
- delegation_tool: str,
40
- ) -> None:
41
- tracker_path = tmp_path / "investigation-tracker.json"
42
- tracker_path.write_text("{}", encoding="utf-8")
43
- HOOK_MODULE.__dict__["TRACKER_STATE_PATH"] = str(tracker_path)
44
-
45
- exit_status = _run_main(json.dumps({"tool_name": delegation_tool}))
46
-
47
- assert exit_status in (None, 0)
48
- assert not tracker_path.exists()
49
-
50
-
51
- @pytest.mark.parametrize("unrelated_tool", ["Read", "Bash", "Grep"])
52
- def test_main_keeps_tracker_for_unrelated_tool(
53
- tmp_path: Path,
54
- unrelated_tool: str,
55
- ) -> None:
56
- tracker_path = tmp_path / "investigation-tracker.json"
57
- tracker_path.write_text("{}", encoding="utf-8")
58
- HOOK_MODULE.__dict__["TRACKER_STATE_PATH"] = str(tracker_path)
59
-
60
- exit_status = _run_main(json.dumps({"tool_name": unrelated_tool}))
61
-
62
- assert exit_status in (None, 0)
63
- assert tracker_path.exists()
64
-
65
-
66
- @pytest.mark.parametrize("invalid_input", ["not json", json.dumps([])])
67
- def test_main_ignores_invalid_input(
68
- tmp_path: Path,
69
- invalid_input: str,
70
- ) -> None:
71
- tracker_path = tmp_path / "investigation-tracker.json"
72
- tracker_path.write_text("{}", encoding="utf-8")
73
- HOOK_MODULE.__dict__["TRACKER_STATE_PATH"] = str(tracker_path)
74
-
75
- exit_status = _run_main(invalid_input)
76
-
77
- assert exit_status in (None, 0)
78
- assert tracker_path.exists()
79
-
80
-
81
- def test_main_keeps_running_when_tracker_removal_fails(tmp_path: Path) -> None:
82
- tracker_path = tmp_path / "investigation-tracker.json"
83
- tracker_path.write_text("{}", encoding="utf-8")
84
- HOOK_MODULE.__dict__["TRACKER_STATE_PATH"] = str(tracker_path)
85
-
86
- with mock.patch.object(HOOK_MODULE.os, "remove", side_effect=OSError("locked")):
87
- exit_status = _run_main(json.dumps({"tool_name": "Agent"}))
88
-
89
- assert exit_status in (None, 0)
90
- assert tracker_path.exists()