claude-dev-env 8.38.2 → 8.38.3
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.
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Hook that
|
|
2
|
+
"""Hook that restores poteto-mode after compaction and loads it for helpers.
|
|
3
3
|
|
|
4
4
|
::
|
|
5
5
|
|
|
6
6
|
Agent or Codex spawn_agent, skill not named -> prompt opens with "invoke poteto-mode"
|
|
7
7
|
Workflow script helper starts -> "invoke poteto-mode now"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
8
|
+
compact SessionStart, skill invoked before -> "invoke the skill again"
|
|
9
|
+
compact SessionStart, no prior invocation
|
|
10
|
+
or transcript unavailable -> nothing
|
|
11
|
+
user turn, skill invoked then compacted -> "invoke poteto-mode now"
|
|
12
|
+
user turn, skill loaded, never invoked,
|
|
13
|
+
or transcript unavailable -> nothing
|
|
14
|
+
|
|
15
|
+
A session that invoked the skill hears nothing more until a compaction drops it.
|
|
13
16
|
The hook prints its output and stops.
|
|
14
17
|
"""
|
|
15
18
|
|
|
@@ -116,66 +119,92 @@ def _is_loaded_after(all_entry_fields: dict[str, object], was_loaded: bool) -> b
|
|
|
116
119
|
return was_loaded or _invokes_poteto_mode(all_entry_fields)
|
|
117
120
|
|
|
118
121
|
|
|
119
|
-
def
|
|
120
|
-
"""
|
|
122
|
+
def poteto_mode_status(all_transcript_lines: Iterable[str]) -> tuple[bool, bool]:
|
|
123
|
+
"""Report whether the skill was invoked and whether it remains loaded.
|
|
121
124
|
|
|
122
125
|
::
|
|
123
126
|
|
|
124
|
-
Skill(poteto-mode) ... Read
|
|
125
|
-
/poteto-mode
|
|
126
|
-
Skill(poteto-mode) ... compact_boundary ...
|
|
127
|
-
|
|
127
|
+
Skill(poteto-mode) ... Read -> (True, True)
|
|
128
|
+
/poteto-mode ... compact_boundary -> (True, False)
|
|
129
|
+
Skill(poteto-mode) ... compact_boundary ... Skill(poteto-mode)
|
|
130
|
+
-> (True, True)
|
|
131
|
+
Read ... Edit -> (False, False)
|
|
128
132
|
|
|
129
133
|
Only lines naming the skill or a compaction are parsed, so a long transcript
|
|
130
134
|
reads fast.
|
|
131
135
|
|
|
132
136
|
Args:
|
|
133
137
|
all_transcript_lines: The session transcript, one JSON entry per line.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Whether the skill was invoked at any point and whether it is loaded now.
|
|
134
141
|
"""
|
|
142
|
+
was_invoked = False
|
|
135
143
|
is_loaded = False
|
|
136
144
|
for each_line in all_transcript_lines:
|
|
137
145
|
all_entry_fields = _marker_entry(each_line)
|
|
138
146
|
if all_entry_fields is not None:
|
|
139
147
|
is_loaded = _is_loaded_after(all_entry_fields, is_loaded)
|
|
140
|
-
|
|
148
|
+
was_invoked = was_invoked or is_loaded
|
|
149
|
+
return was_invoked, is_loaded
|
|
150
|
+
|
|
141
151
|
|
|
152
|
+
def is_poteto_mode_loaded(all_transcript_lines: Iterable[str]) -> bool:
|
|
153
|
+
"""Return whether the transcript invoked the skill after its last compaction.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
all_transcript_lines: The session transcript, one JSON entry per line.
|
|
142
157
|
|
|
143
|
-
|
|
158
|
+
Returns:
|
|
159
|
+
Whether the skill is loaded now.
|
|
160
|
+
"""
|
|
161
|
+
return poteto_mode_status(all_transcript_lines)[1]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _status_in_transcript(transcript_path: object) -> tuple[bool, bool]:
|
|
144
165
|
if not isinstance(transcript_path, str):
|
|
145
|
-
return False
|
|
166
|
+
return False, False
|
|
146
167
|
try:
|
|
147
168
|
with open(
|
|
148
169
|
transcript_path, encoding=UTF8_ENCODING, errors=DECODE_ERRORS_POLICY
|
|
149
170
|
) as transcript:
|
|
150
|
-
return
|
|
171
|
+
return poteto_mode_status(transcript)
|
|
151
172
|
except OSError:
|
|
152
|
-
return False
|
|
173
|
+
return False, False
|
|
153
174
|
|
|
154
175
|
|
|
155
176
|
def reminder_for(all_hook_fields: dict[str, object]) -> str | None:
|
|
156
|
-
"""Return
|
|
177
|
+
"""Return a reminder only for helpers or sessions that need the skill restored.
|
|
157
178
|
|
|
158
179
|
::
|
|
159
180
|
|
|
160
|
-
SessionStart,
|
|
181
|
+
SessionStart, compact, skill invoked before -> COMPACTION_REMINDER
|
|
182
|
+
SessionStart, compact, no prior invocation
|
|
183
|
+
or transcript unavailable -> None
|
|
161
184
|
SubagentStart, workflow-subagent -> NOT_LOADED_REMINDER
|
|
162
|
-
UserPromptSubmit,
|
|
163
|
-
UserPromptSubmit,
|
|
185
|
+
UserPromptSubmit, skill invoked then compacted -> NOT_LOADED_REMINDER
|
|
186
|
+
UserPromptSubmit, skill loaded, never invoked,
|
|
187
|
+
or transcript unavailable -> None
|
|
164
188
|
|
|
165
189
|
Args:
|
|
166
190
|
all_hook_fields: The parsed hook input.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
The event's reminder, or None when no reminder is needed.
|
|
167
194
|
"""
|
|
168
195
|
event_name = all_hook_fields.get("hook_event_name")
|
|
169
196
|
if event_name == SESSION_START_EVENT_NAME:
|
|
170
|
-
|
|
197
|
+
if all_hook_fields.get("source") != COMPACTION_SOURCE:
|
|
198
|
+
return None
|
|
199
|
+
was_invoked, _ = _status_in_transcript(all_hook_fields.get("transcript_path"))
|
|
200
|
+
return COMPACTION_REMINDER if was_invoked else None
|
|
171
201
|
if event_name == SUBAGENT_START_EVENT_NAME:
|
|
172
202
|
is_workflow_helper = all_hook_fields.get("agent_type") == WORKFLOW_SUBAGENT_TYPE
|
|
173
203
|
return NOT_LOADED_REMINDER if is_workflow_helper else None
|
|
174
204
|
if event_name != USER_PROMPT_SUBMIT_EVENT_NAME:
|
|
175
205
|
return None
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
return NOT_LOADED_REMINDER
|
|
206
|
+
was_invoked, is_loaded = _status_in_transcript(all_hook_fields.get("transcript_path"))
|
|
207
|
+
return NOT_LOADED_REMINDER if was_invoked and not is_loaded else None
|
|
179
208
|
|
|
180
209
|
|
|
181
210
|
def _is_subagent_spawn(all_hook_fields: dict[str, object]) -> bool:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Tests for skill_loaded_reminder. Checks
|
|
1
|
+
"""Tests for skill_loaded_reminder. Checks restoration after compaction and helper prompts."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import sys
|
|
@@ -167,14 +167,25 @@ class TestCodexSpawn:
|
|
|
167
167
|
|
|
168
168
|
def test_a_codex_spawn_message_that_already_mentions_the_skill_is_left_alone(self) -> None:
|
|
169
169
|
assert (
|
|
170
|
-
_run_main(
|
|
171
|
-
_agent_call({"message": "$poteto-mode\n\nFix it."}, tool_name="spawn_agent")
|
|
172
|
-
)
|
|
170
|
+
_run_main(_agent_call({"message": "$poteto-mode\n\nFix it."}, tool_name="spawn_agent"))
|
|
173
171
|
== ""
|
|
174
172
|
)
|
|
175
173
|
|
|
176
174
|
|
|
177
175
|
class TestIsPotetoModeLoaded:
|
|
176
|
+
def test_status_without_a_skill_call_reports_no_invocation_or_load(self) -> None:
|
|
177
|
+
assert reminder.poteto_mode_status(iter([READ_CALL_LINE])) == (False, False)
|
|
178
|
+
|
|
179
|
+
def test_status_keeps_invocation_history_after_compaction(self) -> None:
|
|
180
|
+
assert reminder.poteto_mode_status(
|
|
181
|
+
iter([SKILL_CALL_LINE, COMPACT_BOUNDARY_LINE, READ_CALL_LINE])
|
|
182
|
+
) == (True, False)
|
|
183
|
+
|
|
184
|
+
def test_status_reports_the_skill_loaded_again_after_compaction(self) -> None:
|
|
185
|
+
assert reminder.poteto_mode_status(
|
|
186
|
+
iter([SKILL_CALL_LINE, COMPACT_BOUNDARY_LINE, SKILL_CALL_LINE])
|
|
187
|
+
) == (True, True)
|
|
188
|
+
|
|
178
189
|
def test_a_skill_call_loads_it(self) -> None:
|
|
179
190
|
assert reminder.is_poteto_mode_loaded([READ_CALL_LINE, SKILL_CALL_LINE, READ_CALL_LINE])
|
|
180
191
|
|
|
@@ -210,35 +221,106 @@ class TestIsPotetoModeLoaded:
|
|
|
210
221
|
|
|
211
222
|
|
|
212
223
|
class TestReminderFor:
|
|
213
|
-
def
|
|
224
|
+
def test_a_user_turn_in_a_session_that_never_loaded_the_skill_prints_nothing(
|
|
214
225
|
self, tmp_path: Path
|
|
215
226
|
) -> None:
|
|
216
227
|
transcript_path = _write_transcript(tmp_path, [READ_CALL_LINE])
|
|
228
|
+
assert _run_main(_user_turn(transcript_path)) == ""
|
|
229
|
+
|
|
230
|
+
def test_a_user_turn_after_the_skill_and_compaction_gets_the_reminder(
|
|
231
|
+
self, tmp_path: Path
|
|
232
|
+
) -> None:
|
|
233
|
+
transcript_path = _write_transcript(
|
|
234
|
+
tmp_path, [SKILL_CALL_LINE, COMPACT_BOUNDARY_LINE, READ_CALL_LINE]
|
|
235
|
+
)
|
|
217
236
|
emitted = json.loads(_run_main(_user_turn(transcript_path)))
|
|
218
237
|
hook_output = emitted["hookSpecificOutput"]
|
|
219
238
|
assert hook_output["hookEventName"] == "UserPromptSubmit"
|
|
220
239
|
assert hook_output["additionalContext"] == NOT_LOADED_REMINDER
|
|
221
240
|
assert "poteto-mode" in NOT_LOADED_REMINDER
|
|
222
241
|
|
|
242
|
+
def test_a_user_turn_after_the_skill_is_reloaded_prints_nothing(self, tmp_path: Path) -> None:
|
|
243
|
+
transcript_path = _write_transcript(
|
|
244
|
+
tmp_path, [SKILL_CALL_LINE, COMPACT_BOUNDARY_LINE, SKILL_CALL_LINE]
|
|
245
|
+
)
|
|
246
|
+
assert _run_main(_user_turn(transcript_path)) == ""
|
|
247
|
+
|
|
223
248
|
def test_a_user_turn_in_a_session_that_loaded_the_skill_prints_nothing(
|
|
224
249
|
self, tmp_path: Path
|
|
225
250
|
) -> None:
|
|
226
251
|
transcript_path = _write_transcript(tmp_path, [SKILL_CALL_LINE, READ_CALL_LINE])
|
|
227
252
|
assert _run_main(_user_turn(transcript_path)) == ""
|
|
228
253
|
|
|
229
|
-
def
|
|
230
|
-
assert (
|
|
231
|
-
reminder.reminder_for(_user_turn(tmp_path / "not-written-yet.jsonl"))
|
|
232
|
-
== NOT_LOADED_REMINDER
|
|
233
|
-
)
|
|
254
|
+
def test_a_user_turn_with_no_transcript_yet_prints_nothing(self, tmp_path: Path) -> None:
|
|
255
|
+
assert _run_main(_user_turn(tmp_path / "not-written-yet.jsonl")) == ""
|
|
234
256
|
|
|
235
|
-
def
|
|
236
|
-
|
|
257
|
+
def test_session_start_after_compaction_tells_a_prior_user_to_invoke_it_again(
|
|
258
|
+
self, tmp_path: Path
|
|
259
|
+
) -> None:
|
|
260
|
+
transcript_path = _write_transcript(tmp_path, [SKILL_CALL_LINE, READ_CALL_LINE])
|
|
261
|
+
emitted = json.loads(
|
|
262
|
+
_run_main(
|
|
263
|
+
{
|
|
264
|
+
"hook_event_name": "SessionStart",
|
|
265
|
+
"source": "compact",
|
|
266
|
+
"transcript_path": str(transcript_path),
|
|
267
|
+
}
|
|
268
|
+
)
|
|
269
|
+
)
|
|
237
270
|
hook_output = emitted["hookSpecificOutput"]
|
|
238
271
|
assert hook_output["hookEventName"] == "SessionStart"
|
|
239
272
|
assert hook_output["additionalContext"] == COMPACTION_REMINDER
|
|
240
273
|
assert "poteto-mode" in COMPACTION_REMINDER
|
|
241
274
|
|
|
275
|
+
def test_session_start_after_a_written_boundary_reminds_a_prior_user(
|
|
276
|
+
self, tmp_path: Path
|
|
277
|
+
) -> None:
|
|
278
|
+
transcript_path = _write_transcript(tmp_path, [SKILL_CALL_LINE, COMPACT_BOUNDARY_LINE])
|
|
279
|
+
assert (
|
|
280
|
+
reminder.reminder_for(
|
|
281
|
+
{
|
|
282
|
+
"hook_event_name": "SessionStart",
|
|
283
|
+
"source": "compact",
|
|
284
|
+
"transcript_path": str(transcript_path),
|
|
285
|
+
}
|
|
286
|
+
)
|
|
287
|
+
== COMPACTION_REMINDER
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
def test_session_start_after_compaction_without_a_prior_skill_call_prints_nothing(
|
|
291
|
+
self, tmp_path: Path
|
|
292
|
+
) -> None:
|
|
293
|
+
transcript_path = _write_transcript(tmp_path, [READ_CALL_LINE])
|
|
294
|
+
assert (
|
|
295
|
+
_run_main(
|
|
296
|
+
{
|
|
297
|
+
"hook_event_name": "SessionStart",
|
|
298
|
+
"source": "compact",
|
|
299
|
+
"transcript_path": str(transcript_path),
|
|
300
|
+
}
|
|
301
|
+
)
|
|
302
|
+
== ""
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def test_session_start_after_compaction_without_a_transcript_path_prints_nothing(
|
|
306
|
+
self,
|
|
307
|
+
) -> None:
|
|
308
|
+
assert _run_main({"hook_event_name": "SessionStart", "source": "compact"}) == ""
|
|
309
|
+
|
|
310
|
+
def test_session_start_after_compaction_with_an_unreadable_transcript_prints_nothing(
|
|
311
|
+
self, tmp_path: Path
|
|
312
|
+
) -> None:
|
|
313
|
+
assert (
|
|
314
|
+
_run_main(
|
|
315
|
+
{
|
|
316
|
+
"hook_event_name": "SessionStart",
|
|
317
|
+
"source": "compact",
|
|
318
|
+
"transcript_path": str(tmp_path / "not-written-yet.jsonl"),
|
|
319
|
+
}
|
|
320
|
+
)
|
|
321
|
+
== ""
|
|
322
|
+
)
|
|
323
|
+
|
|
242
324
|
def test_a_resumed_session_keeps_its_context_and_prints_nothing(self) -> None:
|
|
243
325
|
assert _run_main({"hook_event_name": "SessionStart", "source": "resume"}) == ""
|
|
244
326
|
|