claude-dev-env 8.98.8 → 8.98.10
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/AGENTS.md +1 -1
- package/bin/install.prompt-context.test.mjs +1 -1
- package/docs/agents-guidance/asks.md +13 -12
- package/docs/rule-guides/session-title.md +15 -4
- package/hooks/blocking/open_tasks_stop_gate.py +60 -259
- package/hooks/blocking/open_tasks_transcript.py +104 -90
- package/hooks/blocking/resolve_thread_reply_gate.py +203 -41
- package/hooks/blocking/session_title_turn_gate.py +142 -56
- package/hooks/blocking/test_open_tasks_stop_gate.py +203 -500
- package/hooks/blocking/test_open_tasks_transcript.py +123 -15
- package/hooks/blocking/test_resolve_thread_reply_gate.py +341 -25
- package/hooks/blocking/test_session_title_turn_gate.py +173 -90
- package/hooks/features/conduct-gates.md +12 -12
- package/hooks/hooks.json +2 -2
- package/hooks/hooks_constants/open_tasks_stop_gate_constants.py +27 -60
- package/hooks/hooks_constants/resolve_thread_reply_gate_constants.py +18 -3
- package/hooks/hooks_constants/session_title_constants.py +19 -1
- package/hooks/open_task_asks.py +6 -101
- package/hooks/session/test_tracking_nudge.py +124 -0
- package/hooks/session/tracking_nudge.py +113 -0
- package/hooks/test_open_task_asks.py +6 -13
- package/package.json +1 -1
- package/rules/index.md +1 -1
- package/scripts/policy_lint/adapter_configuration.py +6 -6
- package/hooks/session/open_task_ask_recorder.py +0 -38
- package/hooks/session/test_open_task_ask_recorder.py +0 -57
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Rebuild a session task list and its
|
|
2
|
+
"""Rebuild a session task list and what its current turn did from a transcript.
|
|
3
3
|
|
|
4
4
|
The open tasks Stop gate reads four transcript shapes, replayed in order::
|
|
5
5
|
|
|
@@ -17,15 +17,19 @@ A desktop plan call either lists items or moves the current list::
|
|
|
17
17
|
Only ``assistant`` records count for a desktop plan call, because Claude Code
|
|
18
18
|
also copies each call into ``api-request-blob`` records.
|
|
19
19
|
|
|
20
|
-
The replay also notes
|
|
20
|
+
The replay also notes what the current turn did::
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
turn_prompt_text the text of the prompt that started the turn
|
|
23
|
+
has_turn_tracker_write a TaskCreate, TaskUpdate, TodoWrite, update_plan, or desktop plan call ran
|
|
24
|
+
has_turn_work_tool any other assistant tool call ran, except the title tool,
|
|
25
|
+
AskUserQuestion, a hearthbot decision card, ToolSearch,
|
|
26
|
+
TaskList, TaskGet, and the Codex request_user_input card
|
|
24
27
|
|
|
25
28
|
A turn starts at the last user prompt record. A tool result, a record flagged
|
|
26
29
|
``isMeta``, ``isSidechain`` or ``isCompactSummary``, and a ``Stop hook feedback``
|
|
27
30
|
message do not start a turn; these are the flags session_title_turn_gate.py
|
|
28
31
|
skips, and its tests model Stop hook feedback as an ``isMeta`` user record.
|
|
32
|
+
A Codex ``response_item`` user message starts a Codex turn.
|
|
29
33
|
|
|
30
34
|
The Cursor CLI can also reach TodoWrite through ``CallDynamicTool``, with the
|
|
31
35
|
TodoWrite input under ``arguments``; that call counts as the TodoWrite call.
|
|
@@ -36,7 +40,6 @@ from __future__ import annotations
|
|
|
36
40
|
import json
|
|
37
41
|
import re
|
|
38
42
|
import sys
|
|
39
|
-
from datetime import datetime
|
|
40
43
|
from pathlib import Path
|
|
41
44
|
from typing import NamedTuple
|
|
42
45
|
|
|
@@ -47,6 +50,9 @@ if hooks_root_directory not in sys.path:
|
|
|
47
50
|
from hooks_constants.open_tasks_stop_gate_constants import (
|
|
48
51
|
ALL_SKIPPED_PROMPT_FLAGS,
|
|
49
52
|
ASSISTANT_RECORD_TYPE,
|
|
53
|
+
ALL_CODEX_TOOL_CALL_TYPES,
|
|
54
|
+
ALL_NON_WORK_TOOL_NAMES,
|
|
55
|
+
ALL_TRACKER_TOOL_NAMES,
|
|
50
56
|
ASSISTANT_ROLE,
|
|
51
57
|
CCD_PLAN_ADVANCE_KEY,
|
|
52
58
|
CCD_PLAN_CONTENT_KEY,
|
|
@@ -56,19 +62,16 @@ from hooks_constants.open_tasks_stop_gate_constants import (
|
|
|
56
62
|
CCD_PLAN_START_KEY,
|
|
57
63
|
CCD_PLAN_TOOL_NAME,
|
|
58
64
|
CODEX_ARGUMENTS_KEY,
|
|
59
|
-
CODEX_CARD_EVENT,
|
|
60
65
|
CODEX_FUNCTION_CALL_TYPE,
|
|
61
66
|
CODEX_MESSAGE_TYPE,
|
|
62
67
|
CODEX_PAYLOAD_KEY,
|
|
63
68
|
CODEX_PLAN_KEY,
|
|
64
69
|
CODEX_PLAN_TOOL_NAME,
|
|
65
|
-
CODEX_PROMPT_EVENT,
|
|
66
70
|
CODEX_RESPONSE_ITEM_TYPE,
|
|
67
71
|
CODEX_STEP_KEY,
|
|
68
72
|
COMPLETED_STATUS,
|
|
69
73
|
CONTENT_KEY,
|
|
70
74
|
CREATED_TASK_ID_GROUP_INDEX,
|
|
71
|
-
DECISION_CARD_TOOL_PATTERN,
|
|
72
75
|
DYNAMIC_TOOL_ARGUMENTS_KEY,
|
|
73
76
|
DYNAMIC_TOOL_CALL_NAME,
|
|
74
77
|
DYNAMIC_TOOL_NAME_KEY,
|
|
@@ -77,9 +80,7 @@ from hooks_constants.open_tasks_stop_gate_constants import (
|
|
|
77
80
|
IN_PROGRESS_STATUS,
|
|
78
81
|
MESSAGE_KEY,
|
|
79
82
|
PENDING_STATUS,
|
|
80
|
-
RECORD_TIMESTAMP_KEY,
|
|
81
83
|
RECORD_TYPE_KEY,
|
|
82
|
-
REPLY_TEXT_SEPARATOR,
|
|
83
84
|
ROLE_KEY,
|
|
84
85
|
STOP_HOOK_FEEDBACK_PREFIX,
|
|
85
86
|
TASK_CREATED_PATTERN,
|
|
@@ -88,6 +89,7 @@ from hooks_constants.open_tasks_stop_gate_constants import (
|
|
|
88
89
|
TASK_STATUS_KEY,
|
|
89
90
|
TASK_SUBJECT_KEY,
|
|
90
91
|
TASK_UPDATE_ID_KEY,
|
|
92
|
+
SESSION_TITLE_TOOL_SUFFIX,
|
|
91
93
|
TASK_UPDATE_TOOL_NAME,
|
|
92
94
|
TEXT_KEY,
|
|
93
95
|
TODOS_KEY,
|
|
@@ -100,8 +102,6 @@ from hooks_constants.open_tasks_stop_gate_constants import (
|
|
|
100
102
|
TOOL_RESULT_BLOCK_TYPE,
|
|
101
103
|
TOOL_USE_BLOCK_TYPE,
|
|
102
104
|
TOOL_USE_ID_KEY,
|
|
103
|
-
UTC_OFFSET_TEXT,
|
|
104
|
-
UTC_SUFFIX,
|
|
105
105
|
USER_RECORD_TYPE,
|
|
106
106
|
USER_ROLE,
|
|
107
107
|
UTF8_ENCODING,
|
|
@@ -116,15 +116,11 @@ class TaskItem(NamedTuple):
|
|
|
116
116
|
item_id: The task number or position, as text.
|
|
117
117
|
subject: The item's wording.
|
|
118
118
|
status: The item's status, such as ``pending`` or ``completed``.
|
|
119
|
-
listed_at: For a desktop plan item, seconds since the epoch of the last list that held it.
|
|
120
|
-
is_carried: True for a desktop plan item a replacing list left out.
|
|
121
119
|
"""
|
|
122
120
|
|
|
123
121
|
item_id: str
|
|
124
122
|
subject: str
|
|
125
123
|
status: str
|
|
126
|
-
listed_at: float | None = None
|
|
127
|
-
is_carried: bool = False
|
|
128
124
|
|
|
129
125
|
|
|
130
126
|
def normalized_text(text: str) -> str:
|
|
@@ -132,17 +128,6 @@ def normalized_text(text: str) -> str:
|
|
|
132
128
|
return WORD_SEPARATOR.join(text.lower().split())
|
|
133
129
|
|
|
134
130
|
|
|
135
|
-
def record_epoch(all_record_fields: dict[str, object]) -> float | None:
|
|
136
|
-
"""Return the record's ISO ``timestamp`` as seconds since the epoch, or None."""
|
|
137
|
-
timestamp_value = all_record_fields.get(RECORD_TIMESTAMP_KEY)
|
|
138
|
-
if not isinstance(timestamp_value, str):
|
|
139
|
-
return None
|
|
140
|
-
try:
|
|
141
|
-
return datetime.fromisoformat(timestamp_value.replace(UTC_SUFFIX, UTC_OFFSET_TEXT)).timestamp()
|
|
142
|
-
except ValueError:
|
|
143
|
-
return None
|
|
144
|
-
|
|
145
|
-
|
|
146
131
|
def parsed_object(json_text: str) -> dict[str, object] | None:
|
|
147
132
|
"""Return the JSON object in the text, or None for anything else."""
|
|
148
133
|
try:
|
|
@@ -182,45 +167,76 @@ def _result_text(all_block_fields: dict[str, object]) -> str:
|
|
|
182
167
|
)
|
|
183
168
|
|
|
184
169
|
|
|
185
|
-
def
|
|
186
|
-
|
|
187
|
-
|
|
170
|
+
def _text_of_blocks(all_blocks: list[dict[str, object]]) -> str:
|
|
171
|
+
return WORD_SEPARATOR.join(
|
|
172
|
+
str(each_block.get(TEXT_KEY, "")) for each_block in all_blocks if isinstance(each_block.get(TEXT_KEY), str)
|
|
173
|
+
)
|
|
188
174
|
|
|
189
175
|
|
|
190
|
-
def
|
|
191
|
-
"""Return
|
|
176
|
+
def claude_prompt_text(all_record_fields: dict[str, object]) -> str | None:
|
|
177
|
+
"""Return the prompt text of a record that starts a turn, or None for any other record."""
|
|
192
178
|
message_body = all_record_fields.get(MESSAGE_KEY)
|
|
193
179
|
message_role = message_body.get(ROLE_KEY) if isinstance(message_body, dict) else None
|
|
194
180
|
if USER_RECORD_TYPE != all_record_fields.get(RECORD_TYPE_KEY) and USER_ROLE not in (all_record_fields.get(ROLE_KEY), message_role):
|
|
195
|
-
return
|
|
181
|
+
return None
|
|
196
182
|
if any(all_record_fields.get(each_flag) for each_flag in ALL_SKIPPED_PROMPT_FLAGS):
|
|
197
|
-
return
|
|
183
|
+
return None
|
|
198
184
|
content_value = message_body.get(CONTENT_KEY) if isinstance(message_body, dict) else None
|
|
199
185
|
if isinstance(content_value, str):
|
|
200
186
|
prompt_text = content_value
|
|
201
187
|
else:
|
|
202
188
|
all_blocks = _content_blocks(all_record_fields)
|
|
203
189
|
if any(each_block.get(RECORD_TYPE_KEY) == TOOL_RESULT_BLOCK_TYPE for each_block in all_blocks):
|
|
204
|
-
return
|
|
190
|
+
return None
|
|
205
191
|
if any(each_block.get(RECORD_TYPE_KEY) != TEXT_KEY for each_block in all_blocks):
|
|
206
|
-
return
|
|
207
|
-
prompt_text =
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return
|
|
192
|
+
return _text_of_blocks(all_blocks)
|
|
193
|
+
prompt_text = _text_of_blocks(all_blocks)
|
|
194
|
+
if not prompt_text.strip() or prompt_text.lstrip().startswith(STOP_HOOK_FEEDBACK_PREFIX):
|
|
195
|
+
return None
|
|
196
|
+
return prompt_text
|
|
211
197
|
|
|
212
198
|
|
|
213
|
-
def
|
|
214
|
-
"""Return ``card`` for a Codex question card call, ``prompt`` for a Codex user message, else None."""
|
|
199
|
+
def _codex_payload(all_record_fields: dict[str, object]) -> dict[str, object] | None:
|
|
215
200
|
payload_body = all_record_fields.get(CODEX_PAYLOAD_KEY)
|
|
216
201
|
if all_record_fields.get(RECORD_TYPE_KEY) != CODEX_RESPONSE_ITEM_TYPE or not isinstance(payload_body, dict):
|
|
217
202
|
return None
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
203
|
+
return payload_body
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def codex_prompt_text(all_record_fields: dict[str, object]) -> str | None:
|
|
207
|
+
"""Return the text of a Codex user message record, or None for any other record."""
|
|
208
|
+
payload_body = _codex_payload(all_record_fields)
|
|
209
|
+
if payload_body is None or payload_body.get(RECORD_TYPE_KEY) != CODEX_MESSAGE_TYPE or payload_body.get(ROLE_KEY) != USER_ROLE:
|
|
210
|
+
return None
|
|
211
|
+
content_value = payload_body.get(CONTENT_KEY)
|
|
212
|
+
if isinstance(content_value, str):
|
|
213
|
+
return content_value
|
|
214
|
+
all_blocks = [each_block for each_block in content_value if isinstance(each_block, dict)] if isinstance(content_value, list) else []
|
|
215
|
+
return _text_of_blocks(all_blocks)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def is_codex_work_call(all_record_fields: dict[str, object]) -> bool:
|
|
219
|
+
"""Return True for a Codex tool call record other than ``update_plan`` or a question card call."""
|
|
220
|
+
payload_body = _codex_payload(all_record_fields)
|
|
221
|
+
if payload_body is None or payload_body.get(RECORD_TYPE_KEY) not in ALL_CODEX_TOOL_CALL_TYPES:
|
|
222
|
+
return False
|
|
223
|
+
tool_name = payload_body.get(TOOL_NAME_KEY)
|
|
224
|
+
return tool_name != CODEX_PLAN_TOOL_NAME and is_work_tool_name(tool_name)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def is_work_tool_name(tool_name: object) -> bool:
|
|
228
|
+
"""Return True for a tool name other than a non-work tool or the session title tool."""
|
|
229
|
+
return not (
|
|
230
|
+
isinstance(tool_name, str)
|
|
231
|
+
and (tool_name in ALL_NON_WORK_TOOL_NAMES or tool_name.endswith(SESSION_TITLE_TOOL_SUFFIX))
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def is_assistant_record(all_record_fields: dict[str, object]) -> bool:
|
|
236
|
+
"""Return True for a record the assistant wrote, by its type, its role, or its message role."""
|
|
237
|
+
message_body = all_record_fields.get(MESSAGE_KEY)
|
|
238
|
+
message_role = message_body.get(ROLE_KEY) if isinstance(message_body, dict) else None
|
|
239
|
+
return ASSISTANT_ROLE in (all_record_fields.get(ROLE_KEY), all_record_fields.get(RECORD_TYPE_KEY), message_role)
|
|
224
240
|
|
|
225
241
|
|
|
226
242
|
def _numbered_items(all_entries: object, subject_key: str) -> list[TaskItem]:
|
|
@@ -305,7 +321,7 @@ def unwrapped_tool_call(tool_name: str, all_input_fields: dict[str, object]) ->
|
|
|
305
321
|
|
|
306
322
|
|
|
307
323
|
class TranscriptReplay:
|
|
308
|
-
"""Fold transcript records into the newest task list they describe."""
|
|
324
|
+
"""Fold transcript records into the newest task list they describe and what the current turn did."""
|
|
309
325
|
|
|
310
326
|
def __init__(self) -> None:
|
|
311
327
|
self.latest_items: list[TaskItem] | None = None
|
|
@@ -313,37 +329,44 @@ class TranscriptReplay:
|
|
|
313
329
|
self.subject_by_tool_use_id: dict[str, str] = {}
|
|
314
330
|
self.plan_items: list[TaskItem] | None = None
|
|
315
331
|
self.own_plan_count = 0
|
|
316
|
-
self.
|
|
317
|
-
self.
|
|
318
|
-
self.
|
|
332
|
+
self.turn_prompt_text = ""
|
|
333
|
+
self.has_turn_tracker_write = False
|
|
334
|
+
self.has_turn_work_tool = False
|
|
335
|
+
|
|
336
|
+
def start_turn(self, prompt_text: str) -> None:
|
|
337
|
+
self.turn_prompt_text = prompt_text
|
|
338
|
+
self.has_turn_tracker_write = False
|
|
339
|
+
self.has_turn_work_tool = False
|
|
319
340
|
|
|
320
341
|
def apply_record(self, all_record_fields: dict[str, object]) -> None:
|
|
321
|
-
|
|
322
|
-
if
|
|
323
|
-
|
|
342
|
+
prompt_text = codex_prompt_text(all_record_fields)
|
|
343
|
+
if prompt_text is None:
|
|
344
|
+
prompt_text = claude_prompt_text(all_record_fields)
|
|
345
|
+
if prompt_text is not None:
|
|
346
|
+
self.start_turn(prompt_text)
|
|
324
347
|
return
|
|
325
|
-
if is_user_prompt_record(all_record_fields):
|
|
326
|
-
self.has_turn_decision_card = False
|
|
327
348
|
plan_items = codex_plan_items(all_record_fields)
|
|
328
349
|
if plan_items is not None:
|
|
329
350
|
self.latest_items = plan_items
|
|
351
|
+
self.has_turn_tracker_write = True
|
|
330
352
|
return
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
353
|
+
if is_codex_work_call(all_record_fields):
|
|
354
|
+
self.has_turn_work_tool = True
|
|
355
|
+
return
|
|
356
|
+
is_typed_assistant_record = all_record_fields.get(RECORD_TYPE_KEY) == ASSISTANT_RECORD_TYPE
|
|
357
|
+
is_from_assistant = is_assistant_record(all_record_fields)
|
|
358
|
+
for each_block in _content_blocks(all_record_fields):
|
|
334
359
|
plan_input = each_block.get(TOOL_INPUT_KEY)
|
|
335
|
-
if
|
|
336
|
-
self.
|
|
337
|
-
|
|
338
|
-
self.apply_desktop_plan(plan_input, record_epoch(all_record_fields))
|
|
360
|
+
if is_typed_assistant_record and each_block.get(TOOL_NAME_KEY) == CCD_PLAN_TOOL_NAME and isinstance(plan_input, dict):
|
|
361
|
+
self.apply_desktop_plan(plan_input)
|
|
362
|
+
self.has_turn_tracker_write = True
|
|
339
363
|
else:
|
|
340
|
-
self.apply_block(each_block)
|
|
341
|
-
self.note_assistant_text(all_record_fields, all_blocks)
|
|
364
|
+
self.apply_block(each_block, is_from_assistant)
|
|
342
365
|
|
|
343
|
-
def apply_desktop_plan(self, all_plan_fields: dict[str, object]
|
|
366
|
+
def apply_desktop_plan(self, all_plan_fields: dict[str, object]) -> None:
|
|
344
367
|
all_listed_entries = all_plan_fields.get(CCD_PLAN_ITEMS_KEY)
|
|
345
368
|
if isinstance(all_listed_entries, list):
|
|
346
|
-
self.replace_desktop_plan(all_listed_entries
|
|
369
|
+
self.replace_desktop_plan(all_listed_entries)
|
|
347
370
|
return
|
|
348
371
|
all_own_items = (self.plan_items or [])[: self.own_plan_count]
|
|
349
372
|
all_done_positions = all_plan_fields.get(CCD_PLAN_DONE_KEY)
|
|
@@ -357,41 +380,37 @@ class TranscriptReplay:
|
|
|
357
380
|
all_own_items = advanced_items(all_own_items)
|
|
358
381
|
self.plan_items = all_own_items + (self.plan_items or [])[self.own_plan_count :]
|
|
359
382
|
|
|
360
|
-
def replace_desktop_plan(self, all_listed_entries: list[object]
|
|
383
|
+
def replace_desktop_plan(self, all_listed_entries: list[object]) -> None:
|
|
361
384
|
all_own_items = [
|
|
362
|
-
each_item._replace(item_id=CCD_PLAN_ITEM_ID_TEMPLATE.format(position=each_item.item_id)
|
|
385
|
+
each_item._replace(item_id=CCD_PLAN_ITEM_ID_TEMPLATE.format(position=each_item.item_id))
|
|
363
386
|
for each_item in _numbered_items(all_listed_entries, CCD_PLAN_CONTENT_KEY)
|
|
364
387
|
]
|
|
365
388
|
all_listed_subjects = {normalized_text(each_item.subject) for each_item in all_own_items}
|
|
366
389
|
all_carried_items = [
|
|
367
|
-
each_item
|
|
390
|
+
each_item
|
|
368
391
|
for each_item in self.plan_items or []
|
|
369
392
|
if each_item.status in INCOMPLETE_STATUSES and normalized_text(each_item.subject) not in all_listed_subjects
|
|
370
393
|
]
|
|
371
394
|
self.plan_items = all_own_items + all_carried_items
|
|
372
395
|
self.own_plan_count = len(all_own_items)
|
|
373
396
|
|
|
374
|
-
def
|
|
375
|
-
message_body = all_record_fields.get(MESSAGE_KEY)
|
|
376
|
-
message_role = message_body.get(ROLE_KEY) if isinstance(message_body, dict) else None
|
|
377
|
-
if ASSISTANT_ROLE not in (all_record_fields.get(ROLE_KEY), all_record_fields.get(RECORD_TYPE_KEY), message_role):
|
|
378
|
-
return
|
|
379
|
-
record_text = REPLY_TEXT_SEPARATOR.join(
|
|
380
|
-
str(each_block.get(TEXT_KEY, "")) for each_block in all_blocks if each_block.get(RECORD_TYPE_KEY) == TEXT_KEY
|
|
381
|
-
).strip()
|
|
382
|
-
if record_text:
|
|
383
|
-
self.last_assistant_text = record_text
|
|
384
|
-
self.all_assistant_texts.append(record_text)
|
|
385
|
-
|
|
386
|
-
def apply_block(self, all_block_fields: dict[str, object]) -> None:
|
|
397
|
+
def apply_block(self, all_block_fields: dict[str, object], is_from_assistant: bool) -> None:
|
|
387
398
|
block_type = all_block_fields.get(RECORD_TYPE_KEY)
|
|
388
399
|
tool_input = all_block_fields.get(TOOL_INPUT_KEY)
|
|
389
400
|
if block_type == TOOL_USE_BLOCK_TYPE and isinstance(tool_input, dict):
|
|
390
401
|
tool_name, all_tool_arguments = unwrapped_tool_call(str(all_block_fields.get(TOOL_NAME_KEY)), tool_input)
|
|
391
402
|
self.apply_tool_use(tool_name, str(all_block_fields.get(TASK_ID_KEY)), all_tool_arguments)
|
|
403
|
+
if is_from_assistant:
|
|
404
|
+
self.note_turn_tool(tool_name)
|
|
392
405
|
elif block_type == TOOL_RESULT_BLOCK_TYPE:
|
|
393
406
|
self.apply_tool_result(all_block_fields)
|
|
394
407
|
|
|
408
|
+
def note_turn_tool(self, tool_name: str) -> None:
|
|
409
|
+
if tool_name in ALL_TRACKER_TOOL_NAMES:
|
|
410
|
+
self.has_turn_tracker_write = True
|
|
411
|
+
elif is_work_tool_name(tool_name):
|
|
412
|
+
self.has_turn_work_tool = True
|
|
413
|
+
|
|
395
414
|
def apply_tool_use(self, tool_name: str, tool_use_id: str, all_input_fields: dict[str, object]) -> None:
|
|
396
415
|
if tool_name == TODO_WRITE_TOOL_NAME:
|
|
397
416
|
all_todo_entries = all_input_fields.get(TODOS_KEY)
|
|
@@ -433,13 +452,8 @@ class TranscriptReplay:
|
|
|
433
452
|
|
|
434
453
|
|
|
435
454
|
def replayed_transcript(transcript_path: object) -> TranscriptReplay:
|
|
436
|
-
"""Fold every transcript record into one replay of the task list and the
|
|
455
|
+
"""Fold every transcript record into one replay of the task list and the current turn."""
|
|
437
456
|
replay = TranscriptReplay()
|
|
438
457
|
for each_record in _transcript_records(transcript_path):
|
|
439
458
|
replay.apply_record(each_record)
|
|
440
459
|
return replay
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
def transcript_items(transcript_path: object) -> list[TaskItem] | None:
|
|
444
|
-
"""Rebuild the task list from TaskCreate/TaskUpdate, TodoWrite, and Codex update_plan calls."""
|
|
445
|
-
return replayed_transcript(transcript_path).items()
|