claude-dev-env 8.9.0 → 8.10.0
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.
- package/bin/install-constants.mjs +5 -0
- package/bin/install.cursor-rules.test.mjs +6 -0
- package/bin/install.mjs +2 -2
- package/hooks/hooks.json +12 -0
- package/hooks/hooks_constants/skill_loaded_reminder_constants.py +16 -4
- package/hooks/session/skill_loaded_reminder.py +28 -13
- package/hooks/session/test_skill_loaded_reminder.py +49 -2
- package/package.json +1 -1
|
@@ -179,3 +179,8 @@ export const WINDOWS_PYTHON_LAUNCHER_COMMAND = 'py -3';
|
|
|
179
179
|
export const PYTHON_PROBE_TIMEOUT_MILLISECONDS = 10_000;
|
|
180
180
|
export const ADDITIONAL_SETTINGS_BLOB_PREFIX = 'settings-';
|
|
181
181
|
export const CODEX_NATIVE_ROUTING_MATCHER = 'multi_agent_v1__spawn_agent';
|
|
182
|
+
export const CODEX_SUBAGENT_PROMPT_MATCHER = 'Agent|Task';
|
|
183
|
+
export const ALL_CODEX_NATIVE_PRE_TOOL_USE_MATCHERS = new Set([
|
|
184
|
+
CODEX_NATIVE_ROUTING_MATCHER,
|
|
185
|
+
CODEX_SUBAGENT_PROMPT_MATCHER,
|
|
186
|
+
]);
|
|
@@ -148,6 +148,12 @@ test('seeds one editable policy and one native Codex routing hook', () => {
|
|
|
148
148
|
assert.equal(firstRoutingGroups.length, 1);
|
|
149
149
|
assert.equal(firstRoutingGroups[0].hooks.length, 1);
|
|
150
150
|
assert.match(firstRoutingGroups[0].hooks[0].command, /subagent_model_routing\.mjs/);
|
|
151
|
+
const firstSpawnPromptGroups = firstCodexHooks.hooks.PreToolUse.filter(
|
|
152
|
+
group => group.matcher === 'Agent|Task',
|
|
153
|
+
);
|
|
154
|
+
assert.equal(firstSpawnPromptGroups.length, 1);
|
|
155
|
+
assert.match(firstSpawnPromptGroups[0].hooks[0].command, /skill_loaded_reminder\.py/);
|
|
156
|
+
assert.deepEqual(Object.keys(firstCodexHooks.hooks), ['PreToolUse']);
|
|
151
157
|
assert.equal(
|
|
152
158
|
firstCodexHooks.hooks.PreToolUse.some(group => group.matcher === 'Write|Edit|MultiEdit|apply_patch'),
|
|
153
159
|
false,
|
package/bin/install.mjs
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
SUBAGENT_MODEL_POLICY_FILE_NAME,
|
|
39
39
|
WINDOWS_PYTHON_LAUNCHER_COMMAND,
|
|
40
40
|
PYTHON_PROBE_TIMEOUT_MILLISECONDS,
|
|
41
|
-
|
|
41
|
+
ALL_CODEX_NATIVE_PRE_TOOL_USE_MATCHERS,
|
|
42
42
|
} from './install-constants.mjs';
|
|
43
43
|
import {
|
|
44
44
|
installPstackPlugin,
|
|
@@ -2102,7 +2102,7 @@ function selectCodexNativeHooksConfig(hooksConfig) {
|
|
|
2102
2102
|
...hooksConfig,
|
|
2103
2103
|
hooks: {
|
|
2104
2104
|
PreToolUse: Array.isArray(preToolUseGroups)
|
|
2105
|
-
? preToolUseGroups.filter(group => group?.matcher
|
|
2105
|
+
? preToolUseGroups.filter(group => ALL_CODEX_NATIVE_PRE_TOOL_USE_MATCHERS.has(group?.matcher))
|
|
2106
2106
|
: [],
|
|
2107
2107
|
},
|
|
2108
2108
|
};
|
package/hooks/hooks.json
CHANGED
|
@@ -122,6 +122,18 @@
|
|
|
122
122
|
]
|
|
123
123
|
}
|
|
124
124
|
],
|
|
125
|
+
"SubagentStart": [
|
|
126
|
+
{
|
|
127
|
+
"matcher": "workflow-subagent",
|
|
128
|
+
"hooks": [
|
|
129
|
+
{
|
|
130
|
+
"type": "command",
|
|
131
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/skill_loaded_reminder.py",
|
|
132
|
+
"timeout": 10
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
}
|
|
136
|
+
],
|
|
125
137
|
"SessionEnd": [
|
|
126
138
|
{
|
|
127
139
|
"matcher": "",
|
|
@@ -4,7 +4,6 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
__all__ = [
|
|
6
6
|
"ALL_SELF_LOADING_SUBAGENT_TYPES",
|
|
7
|
-
"ALL_SUBAGENT_TOOL_NAMES",
|
|
8
7
|
"ASSISTANT_ENTRY_TYPE",
|
|
9
8
|
"COMPACT_BOUNDARY_SUBTYPE",
|
|
10
9
|
"COMPACTION_REMINDER",
|
|
@@ -16,18 +15,23 @@ __all__ = [
|
|
|
16
15
|
"SESSION_START_EVENT_NAME",
|
|
17
16
|
"SKILL_TOOL_NAME",
|
|
18
17
|
"SLASH_COMMAND_MARKER",
|
|
19
|
-
"
|
|
18
|
+
"SUBAGENT_START_EVENT_NAME",
|
|
19
|
+
"CLAUDE_SUBAGENT_PROMPT_PREFIX",
|
|
20
|
+
"CODEX_SUBAGENT_PROMPT_PREFIX",
|
|
21
|
+
"ALL_SPAWN_PROMPT_FIELDS_AND_PREFIXES_BY_TOOL_NAME",
|
|
20
22
|
"TOOL_USE_BLOCK_TYPE",
|
|
21
23
|
"USER_ENTRY_TYPE",
|
|
22
24
|
"USER_PROMPT_SUBMIT_EVENT_NAME",
|
|
25
|
+
"WORKFLOW_SUBAGENT_TYPE",
|
|
23
26
|
]
|
|
24
27
|
|
|
25
28
|
PRE_TOOL_USE_EVENT_NAME = "PreToolUse"
|
|
26
29
|
SESSION_START_EVENT_NAME = "SessionStart"
|
|
27
30
|
USER_PROMPT_SUBMIT_EVENT_NAME = "UserPromptSubmit"
|
|
28
31
|
COMPACTION_SOURCE = "compact"
|
|
32
|
+
SUBAGENT_START_EVENT_NAME = "SubagentStart"
|
|
33
|
+
WORKFLOW_SUBAGENT_TYPE = "workflow-subagent"
|
|
29
34
|
|
|
30
|
-
ALL_SUBAGENT_TOOL_NAMES = frozenset({"Agent", "Task"})
|
|
31
35
|
ALL_SELF_LOADING_SUBAGENT_TYPES = frozenset({"pstack:poteto-agent", "poteto-agent"})
|
|
32
36
|
|
|
33
37
|
POTETO_MODE_SKILL_NAME = "pstack:poteto-mode"
|
|
@@ -40,11 +44,19 @@ USER_ENTRY_TYPE = "user"
|
|
|
40
44
|
TOOL_USE_BLOCK_TYPE = "tool_use"
|
|
41
45
|
COMPACT_BOUNDARY_SUBTYPE = "compact_boundary"
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
CLAUDE_SUBAGENT_PROMPT_PREFIX = (
|
|
44
48
|
"Before any other work, invoke the pstack:poteto-mode skill with the Skill tool. "
|
|
45
49
|
"Every prompt you write for a subagent opens with the same instruction."
|
|
46
50
|
)
|
|
47
51
|
|
|
52
|
+
CODEX_SUBAGENT_PROMPT_PREFIX = "$pstack:poteto-mode"
|
|
53
|
+
|
|
54
|
+
ALL_SPAWN_PROMPT_FIELDS_AND_PREFIXES_BY_TOOL_NAME = {
|
|
55
|
+
"Agent": ("prompt", CLAUDE_SUBAGENT_PROMPT_PREFIX),
|
|
56
|
+
"Task": ("prompt", CLAUDE_SUBAGENT_PROMPT_PREFIX),
|
|
57
|
+
"spawn_agent": ("message", CODEX_SUBAGENT_PROMPT_PREFIX),
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
NOT_LOADED_REMINDER = (
|
|
49
61
|
"The pstack:poteto-mode skill is not loaded in this context. "
|
|
50
62
|
"Your next tool call is the Skill tool with skill pstack:poteto-mode, before any other "
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Hook that keeps poteto-mode loaded in every session, subagent and workflow helper.
|
|
3
3
|
|
|
4
4
|
::
|
|
5
5
|
|
|
6
|
-
Agent
|
|
6
|
+
Agent or Codex spawn_agent, skill not named -> prompt opens with "invoke pstack:poteto-mode"
|
|
7
|
+
Workflow script helper starts -> "invoke pstack:poteto-mode now"
|
|
7
8
|
context compacted mid-run -> "invoke pstack:poteto-mode again"
|
|
8
9
|
user turn, skill not loaded since compacting -> "invoke pstack:poteto-mode now"
|
|
9
10
|
user turn, skill already loaded -> nothing
|
|
@@ -25,7 +26,6 @@ if _hooks_dir not in sys.path:
|
|
|
25
26
|
|
|
26
27
|
from hooks_constants.skill_loaded_reminder_constants import (
|
|
27
28
|
ALL_SELF_LOADING_SUBAGENT_TYPES,
|
|
28
|
-
ALL_SUBAGENT_TOOL_NAMES,
|
|
29
29
|
ASSISTANT_ENTRY_TYPE,
|
|
30
30
|
COMPACT_BOUNDARY_SUBTYPE,
|
|
31
31
|
COMPACTION_REMINDER,
|
|
@@ -37,10 +37,12 @@ from hooks_constants.skill_loaded_reminder_constants import (
|
|
|
37
37
|
SESSION_START_EVENT_NAME,
|
|
38
38
|
SKILL_TOOL_NAME,
|
|
39
39
|
SLASH_COMMAND_MARKER,
|
|
40
|
-
|
|
40
|
+
SUBAGENT_START_EVENT_NAME,
|
|
41
|
+
ALL_SPAWN_PROMPT_FIELDS_AND_PREFIXES_BY_TOOL_NAME,
|
|
41
42
|
TOOL_USE_BLOCK_TYPE,
|
|
42
43
|
USER_ENTRY_TYPE,
|
|
43
44
|
USER_PROMPT_SUBMIT_EVENT_NAME,
|
|
45
|
+
WORKFLOW_SUBAGENT_TYPE,
|
|
44
46
|
)
|
|
45
47
|
from hooks_constants.pre_tool_use_allow_output import write_pre_tool_use_allow_to_stdout
|
|
46
48
|
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
|
|
@@ -48,25 +50,32 @@ from hooks_constants.setup_project_paths_constants import DECODE_ERRORS_POLICY,
|
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
def subagent_input_with_poteto_mode(
|
|
53
|
+
tool_name: str,
|
|
51
54
|
all_tool_input_fields: dict[str, object],
|
|
52
55
|
) -> dict[str, object] | None:
|
|
53
|
-
"""Return the
|
|
56
|
+
"""Return the spawn input with the poteto-mode invocation first, or None to leave it.
|
|
54
57
|
|
|
55
58
|
::
|
|
56
59
|
|
|
57
|
-
{"prompt": "Reply leaf."}
|
|
58
|
-
{"
|
|
59
|
-
{"
|
|
60
|
+
Agent {"prompt": "Reply leaf."} -> {"prompt": "Before any ...\\n\\nReply leaf."}
|
|
61
|
+
spawn_agent {"message": "Fix it."} -> {"message": "$pstack:poteto-mode\\n\\nFix it."}
|
|
62
|
+
Agent {"prompt": "Invoke pstack:poteto-mode, then ..."} -> None
|
|
63
|
+
Agent {"subagent_type": "pstack:poteto-agent", ...} -> None, it loads the skill
|
|
64
|
+
|
|
65
|
+
Claude Code spawns through Agent or Task and Codex through spawn_agent, so
|
|
66
|
+
each tool name carries its own prompt field and invocation text.
|
|
60
67
|
|
|
61
68
|
Args:
|
|
62
|
-
|
|
69
|
+
tool_name: The spawn tool the session called.
|
|
70
|
+
all_tool_input_fields: The spawn input the session is about to send.
|
|
63
71
|
"""
|
|
64
|
-
|
|
72
|
+
field_name, invocation_prefix = ALL_SPAWN_PROMPT_FIELDS_AND_PREFIXES_BY_TOOL_NAME[tool_name]
|
|
73
|
+
prompt = all_tool_input_fields.get(field_name)
|
|
65
74
|
if not isinstance(prompt, str) or POTETO_MODE_SKILL_NAME in prompt:
|
|
66
75
|
return None
|
|
67
76
|
if all_tool_input_fields.get("subagent_type") in ALL_SELF_LOADING_SUBAGENT_TYPES:
|
|
68
77
|
return None
|
|
69
|
-
return {**all_tool_input_fields,
|
|
78
|
+
return {**all_tool_input_fields, field_name: invocation_prefix + PROMPT_SEPARATOR + prompt}
|
|
70
79
|
|
|
71
80
|
|
|
72
81
|
def _invokes_poteto_mode(all_entry_fields: dict[str, object]) -> bool:
|
|
@@ -149,6 +158,7 @@ def reminder_for(all_hook_fields: dict[str, object]) -> str | None:
|
|
|
149
158
|
::
|
|
150
159
|
|
|
151
160
|
SessionStart, source compact -> COMPACTION_REMINDER
|
|
161
|
+
SubagentStart, workflow-subagent -> NOT_LOADED_REMINDER
|
|
152
162
|
UserPromptSubmit, transcript lacks the skill -> NOT_LOADED_REMINDER
|
|
153
163
|
UserPromptSubmit, transcript loaded the skill -> None
|
|
154
164
|
|
|
@@ -158,6 +168,9 @@ def reminder_for(all_hook_fields: dict[str, object]) -> str | None:
|
|
|
158
168
|
event_name = all_hook_fields.get("hook_event_name")
|
|
159
169
|
if event_name == SESSION_START_EVENT_NAME:
|
|
160
170
|
return COMPACTION_REMINDER if all_hook_fields.get("source") == COMPACTION_SOURCE else None
|
|
171
|
+
if event_name == SUBAGENT_START_EVENT_NAME:
|
|
172
|
+
is_workflow_helper = all_hook_fields.get("agent_type") == WORKFLOW_SUBAGENT_TYPE
|
|
173
|
+
return NOT_LOADED_REMINDER if is_workflow_helper else None
|
|
161
174
|
if event_name != USER_PROMPT_SUBMIT_EVENT_NAME:
|
|
162
175
|
return None
|
|
163
176
|
if _is_loaded_in_transcript(all_hook_fields.get("transcript_path")):
|
|
@@ -168,7 +181,7 @@ def reminder_for(all_hook_fields: dict[str, object]) -> str | None:
|
|
|
168
181
|
def _is_subagent_spawn(all_hook_fields: dict[str, object]) -> bool:
|
|
169
182
|
return (
|
|
170
183
|
all_hook_fields.get("hook_event_name") == PRE_TOOL_USE_EVENT_NAME
|
|
171
|
-
and all_hook_fields.get("tool_name") in
|
|
184
|
+
and all_hook_fields.get("tool_name") in ALL_SPAWN_PROMPT_FIELDS_AND_PREFIXES_BY_TOOL_NAME
|
|
172
185
|
and isinstance(all_hook_fields.get("tool_input"), dict)
|
|
173
186
|
)
|
|
174
187
|
|
|
@@ -179,7 +192,9 @@ def main() -> None:
|
|
|
179
192
|
if hook_payload is None:
|
|
180
193
|
return
|
|
181
194
|
if _is_subagent_spawn(hook_payload):
|
|
182
|
-
rewritten_input = subagent_input_with_poteto_mode(
|
|
195
|
+
rewritten_input = subagent_input_with_poteto_mode(
|
|
196
|
+
hook_payload["tool_name"], hook_payload["tool_input"]
|
|
197
|
+
)
|
|
183
198
|
if rewritten_input is not None:
|
|
184
199
|
write_pre_tool_use_allow_to_stdout(rewritten_input)
|
|
185
200
|
return
|
|
@@ -132,14 +132,46 @@ class TestSubagentSpawn:
|
|
|
132
132
|
class TestSubagentInputWithPotetoMode:
|
|
133
133
|
def test_every_other_input_field_is_kept(self) -> None:
|
|
134
134
|
rewritten_input = reminder.subagent_input_with_poteto_mode(
|
|
135
|
-
{"prompt": "Go.", "model": "sonnet", "run_in_background": True}
|
|
135
|
+
"Agent", {"prompt": "Go.", "model": "sonnet", "run_in_background": True}
|
|
136
136
|
)
|
|
137
137
|
assert rewritten_input is not None
|
|
138
138
|
assert rewritten_input["model"] == "sonnet"
|
|
139
139
|
assert rewritten_input["run_in_background"] is True
|
|
140
140
|
|
|
141
141
|
def test_an_input_with_no_prompt_is_left_alone(self) -> None:
|
|
142
|
-
assert reminder.subagent_input_with_poteto_mode({"description": "Go"}) is 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"] == "$pstack: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(
|
|
171
|
+
_agent_call({"message": "$pstack:poteto-mode\n\nFix it."}, tool_name="spawn_agent")
|
|
172
|
+
)
|
|
173
|
+
== ""
|
|
174
|
+
)
|
|
143
175
|
|
|
144
176
|
|
|
145
177
|
class TestIsPotetoModeLoaded:
|
|
@@ -202,5 +234,20 @@ class TestReminderFor:
|
|
|
202
234
|
def test_a_resumed_session_keeps_its_context_and_prints_nothing(self) -> None:
|
|
203
235
|
assert _run_main({"hook_event_name": "SessionStart", "source": "resume"}) == ""
|
|
204
236
|
|
|
237
|
+
def test_a_workflow_helper_starting_is_told_to_invoke_the_skill(self) -> None:
|
|
238
|
+
emitted = json.loads(
|
|
239
|
+
_run_main({"hook_event_name": "SubagentStart", "agent_type": "workflow-subagent"})
|
|
240
|
+
)
|
|
241
|
+
hook_output = emitted["hookSpecificOutput"]
|
|
242
|
+
assert hook_output["hookEventName"] == "SubagentStart"
|
|
243
|
+
assert hook_output["additionalContext"] == NOT_LOADED_REMINDER
|
|
244
|
+
|
|
245
|
+
def test_an_agent_tool_helper_starting_prints_nothing_since_its_prompt_carries_the_line(
|
|
246
|
+
self,
|
|
247
|
+
) -> None:
|
|
248
|
+
assert (
|
|
249
|
+
_run_main({"hook_event_name": "SubagentStart", "agent_type": "general-purpose"}) == ""
|
|
250
|
+
)
|
|
251
|
+
|
|
205
252
|
def test_a_payload_that_is_not_json_prints_nothing(self) -> None:
|
|
206
253
|
assert _run_main("not json") == ""
|