klaude-code 2.10.3__py3-none-any.whl → 2.10.4__py3-none-any.whl

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.
Files changed (56) hide show
  1. klaude_code/auth/AGENTS.md +4 -24
  2. klaude_code/auth/__init__.py +1 -17
  3. klaude_code/cli/auth_cmd.py +3 -53
  4. klaude_code/cli/list_model.py +0 -50
  5. klaude_code/config/assets/builtin_config.yaml +0 -28
  6. klaude_code/config/config.py +5 -42
  7. klaude_code/const.py +5 -2
  8. klaude_code/core/agent_profile.py +2 -10
  9. klaude_code/core/backtrack/__init__.py +3 -0
  10. klaude_code/core/backtrack/manager.py +48 -0
  11. klaude_code/core/memory.py +25 -9
  12. klaude_code/core/task.py +53 -7
  13. klaude_code/core/tool/__init__.py +2 -0
  14. klaude_code/core/tool/backtrack/__init__.py +3 -0
  15. klaude_code/core/tool/backtrack/backtrack_tool.md +17 -0
  16. klaude_code/core/tool/backtrack/backtrack_tool.py +65 -0
  17. klaude_code/core/tool/context.py +5 -0
  18. klaude_code/core/turn.py +3 -0
  19. klaude_code/llm/input_common.py +70 -1
  20. klaude_code/llm/openai_compatible/input.py +5 -2
  21. klaude_code/llm/openrouter/input.py +5 -2
  22. klaude_code/llm/registry.py +0 -1
  23. klaude_code/protocol/events.py +10 -0
  24. klaude_code/protocol/llm_param.py +0 -1
  25. klaude_code/protocol/message.py +10 -1
  26. klaude_code/protocol/tools.py +1 -0
  27. klaude_code/session/session.py +111 -2
  28. klaude_code/session/store.py +2 -0
  29. klaude_code/skill/assets/executing-plans/SKILL.md +84 -0
  30. klaude_code/skill/assets/writing-plans/SKILL.md +116 -0
  31. klaude_code/tui/commands.py +15 -0
  32. klaude_code/tui/components/developer.py +1 -1
  33. klaude_code/tui/components/rich/status.py +7 -76
  34. klaude_code/tui/components/rich/theme.py +10 -0
  35. klaude_code/tui/components/tools.py +31 -18
  36. klaude_code/tui/display.py +4 -0
  37. klaude_code/tui/input/prompt_toolkit.py +15 -1
  38. klaude_code/tui/machine.py +26 -8
  39. klaude_code/tui/renderer.py +97 -0
  40. klaude_code/tui/runner.py +7 -2
  41. klaude_code/tui/terminal/image.py +28 -12
  42. klaude_code/ui/terminal/title.py +8 -3
  43. {klaude_code-2.10.3.dist-info → klaude_code-2.10.4.dist-info}/METADATA +1 -1
  44. {klaude_code-2.10.3.dist-info → klaude_code-2.10.4.dist-info}/RECORD +46 -49
  45. klaude_code/auth/antigravity/__init__.py +0 -20
  46. klaude_code/auth/antigravity/exceptions.py +0 -17
  47. klaude_code/auth/antigravity/oauth.py +0 -315
  48. klaude_code/auth/antigravity/pkce.py +0 -25
  49. klaude_code/auth/antigravity/token_manager.py +0 -27
  50. klaude_code/core/prompts/prompt-antigravity.md +0 -80
  51. klaude_code/llm/antigravity/__init__.py +0 -3
  52. klaude_code/llm/antigravity/client.py +0 -558
  53. klaude_code/llm/antigravity/input.py +0 -268
  54. klaude_code/skill/assets/create-plan/SKILL.md +0 -74
  55. {klaude_code-2.10.3.dist-info → klaude_code-2.10.4.dist-info}/WHEEL +0 -0
  56. {klaude_code-2.10.3.dist-info → klaude_code-2.10.4.dist-info}/entry_points.txt +0 -0
@@ -1,3 +1,4 @@
1
+ from .backtrack.backtrack_tool import BacktrackTool
1
2
  from .context import FileTracker, RunSubtask, SubAgentResumeClaims, TodoContext, ToolContext, build_todo_context
2
3
  from .file.apply_patch import DiffError, process_patch
3
4
  from .file.apply_patch_tool import ApplyPatchTool
@@ -19,6 +20,7 @@ from .web.web_search_tool import WebSearchTool
19
20
 
20
21
  __all__ = [
21
22
  "ApplyPatchTool",
23
+ "BacktrackTool",
22
24
  "BashTool",
23
25
  "DiffError",
24
26
  "EditTool",
@@ -0,0 +1,3 @@
1
+ from klaude_code.core.tool.backtrack.backtrack_tool import BacktrackTool
2
+
3
+ __all__ = ["BacktrackTool"]
@@ -0,0 +1,17 @@
1
+ Revert conversation history to a previous checkpoint, discarding everything after it.
2
+
3
+ Use this tool when:
4
+ - You spent many tokens on exploration that turned out unproductive
5
+ - You read large files but only need to keep key information
6
+ - A deep debugging session can be summarized before continuing
7
+ - The current approach is stuck and you want to try a different path
8
+
9
+ The note you provide will be shown to your future self at the checkpoint, so include:
10
+ - Key findings from your exploration
11
+ - What approaches did not work and why
12
+ - Important context needed to continue
13
+
14
+ IMPORTANT:
15
+ - File system changes are NOT reverted - only conversation history is affected
16
+ - Checkpoints are created automatically at the start of each turn
17
+ - Available checkpoints appear as <system>Checkpoint N</system> in the conversation
@@ -0,0 +1,65 @@
1
+ from pathlib import Path
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from klaude_code.core.tool.context import ToolContext
6
+ from klaude_code.core.tool.tool_abc import ToolABC, load_desc
7
+ from klaude_code.core.tool.tool_registry import register
8
+ from klaude_code.protocol import llm_param, message, tools
9
+
10
+
11
+ class BacktrackArguments(BaseModel):
12
+ checkpoint_id: int = Field(description="The checkpoint ID to revert to")
13
+ note: str = Field(description="A note to your future self with key findings/context to preserve")
14
+ rationale: str = Field(description="Why you are performing this backtrack")
15
+
16
+
17
+ @register(tools.BACKTRACK)
18
+ class BacktrackTool(ToolABC):
19
+ @classmethod
20
+ def schema(cls) -> llm_param.ToolSchema:
21
+ return llm_param.ToolSchema(
22
+ name=tools.BACKTRACK,
23
+ type="function",
24
+ description=load_desc(Path(__file__).parent / "backtrack_tool.md"),
25
+ parameters={
26
+ "type": "object",
27
+ "properties": {
28
+ "checkpoint_id": {
29
+ "type": "integer",
30
+ "description": "The checkpoint ID to revert to",
31
+ },
32
+ "note": {
33
+ "type": "string",
34
+ "description": "A note to your future self with key findings/context",
35
+ },
36
+ "rationale": {
37
+ "type": "string",
38
+ "description": "Why you are performing this backtrack",
39
+ },
40
+ },
41
+ "required": ["checkpoint_id", "note", "rationale"],
42
+ "additionalProperties": False,
43
+ },
44
+ )
45
+
46
+ @classmethod
47
+ async def call(cls, arguments: str, context: ToolContext) -> message.ToolResultMessage:
48
+ try:
49
+ args = BacktrackArguments.model_validate_json(arguments)
50
+ except ValueError as exc:
51
+ return message.ToolResultMessage(status="error", output_text=f"Invalid arguments: {exc}")
52
+
53
+ backtrack_manager = context.backtrack_manager
54
+ if backtrack_manager is None:
55
+ return message.ToolResultMessage(
56
+ status="error",
57
+ output_text="Backtrack is not available in this context",
58
+ )
59
+
60
+ try:
61
+ result = backtrack_manager.send_backtrack(args.checkpoint_id, args.note, args.rationale)
62
+ except ValueError as exc:
63
+ return message.ToolResultMessage(status="error", output_text=str(exc))
64
+
65
+ return message.ToolResultMessage(status="success", output_text=result)
@@ -4,6 +4,7 @@ import asyncio
4
4
  from collections.abc import Awaitable, Callable, MutableMapping
5
5
  from dataclasses import dataclass, replace
6
6
 
7
+ from klaude_code.core.backtrack import BacktrackManager
7
8
  from klaude_code.protocol import model
8
9
  from klaude_code.protocol.sub_agent import SubAgentResult
9
10
  from klaude_code.session.session import Session
@@ -85,9 +86,13 @@ class ToolContext:
85
86
  sub_agent_resume_claims: SubAgentResumeClaims | None = None
86
87
  record_sub_agent_session_id: Callable[[str], None] | None = None
87
88
  register_sub_agent_metadata_getter: Callable[[GetMetadataFn], None] | None = None
89
+ backtrack_manager: BacktrackManager | None = None
88
90
 
89
91
  def with_record_sub_agent_session_id(self, callback: Callable[[str], None] | None) -> ToolContext:
90
92
  return replace(self, record_sub_agent_session_id=callback)
91
93
 
92
94
  def with_register_sub_agent_metadata_getter(self, callback: Callable[[GetMetadataFn], None] | None) -> ToolContext:
93
95
  return replace(self, register_sub_agent_metadata_getter=callback)
96
+
97
+ def with_backtrack_manager(self, manager: BacktrackManager | None) -> ToolContext:
98
+ return replace(self, backtrack_manager=manager)
klaude_code/core/turn.py CHANGED
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from klaude_code.const import RETRY_PRESERVE_PARTIAL_MESSAGE, SUPPORTED_IMAGE_SIZES
8
+ from klaude_code.core.backtrack import BacktrackManager
8
9
  from klaude_code.core.tool import ToolABC
9
10
  from klaude_code.core.tool.context import SubAgentResumeClaims, ToolContext
10
11
 
@@ -49,6 +50,7 @@ class TurnExecutionContext:
49
50
  tools: list[llm_param.ToolSchema]
50
51
  tool_registry: dict[str, type[ToolABC]]
51
52
  sub_agent_state: model.SubAgentState | None = None
53
+ backtrack_manager: BacktrackManager | None = None
52
54
 
53
55
 
54
56
  @dataclass
@@ -404,6 +406,7 @@ class TurnExecutor:
404
406
  session_id=session_ctx.session_id,
405
407
  run_subtask=session_ctx.run_subtask,
406
408
  sub_agent_resume_claims=SubAgentResumeClaims(),
409
+ backtrack_manager=ctx.backtrack_manager,
407
410
  )
408
411
 
409
412
  executor = ToolExecutor(
@@ -108,17 +108,86 @@ def build_tool_message(
108
108
  msg: message.ToolResultMessage,
109
109
  attachment: DeveloperAttachment,
110
110
  ) -> dict[str, object]:
111
+ """Build a tool message. Note: image_url in tool message is not supported by
112
+ OpenAI Chat Completions API. Use build_tool_message_for_chat_completions instead.
113
+ """
111
114
  merged_text = merge_reminder_text(
112
115
  msg.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
113
116
  attachment.text,
114
117
  )
118
+ content: list[dict[str, object]] = [{"type": "text", "text": merged_text}]
119
+ for part in msg.parts:
120
+ if isinstance(part, message.ImageFilePart):
121
+ content.append({"type": "image_url", "image_url": {"url": image_file_to_data_url(part)}})
122
+ elif isinstance(part, message.ImageURLPart):
123
+ content.append({"type": "image_url", "image_url": {"url": part.url}})
124
+ for image in attachment.images:
125
+ if isinstance(image, message.ImageFilePart):
126
+ content.append({"type": "image_url", "image_url": {"url": image_file_to_data_url(image)}})
127
+ else:
128
+ content.append({"type": "image_url", "image_url": {"url": image.url}})
115
129
  return {
116
130
  "role": "tool",
117
- "content": [{"type": "text", "text": merged_text}],
131
+ "content": content,
118
132
  "tool_call_id": msg.call_id,
119
133
  }
120
134
 
121
135
 
136
+ def build_tool_message_for_chat_completions(
137
+ msg: message.ToolResultMessage,
138
+ attachment: DeveloperAttachment,
139
+ ) -> tuple[dict[str, object], dict[str, object] | None]:
140
+ """Build tool message for OpenAI Chat Completions API.
141
+
142
+ OpenAI Chat Completions API does not support image_url in tool messages.
143
+ Images are extracted and returned as a separate user message to be appended after the tool message.
144
+
145
+ Returns:
146
+ A tuple of (tool_message, optional_user_message_with_images).
147
+ The user_message is None if there are no images.
148
+ """
149
+ merged_text = merge_reminder_text(
150
+ msg.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
151
+ attachment.text,
152
+ )
153
+
154
+ # Collect all images
155
+ image_urls: list[dict[str, object]] = []
156
+ for part in msg.parts:
157
+ if isinstance(part, message.ImageFilePart):
158
+ image_urls.append({"type": "image_url", "image_url": {"url": image_file_to_data_url(part)}})
159
+ elif isinstance(part, message.ImageURLPart):
160
+ image_urls.append({"type": "image_url", "image_url": {"url": part.url}})
161
+ for image in attachment.images:
162
+ if isinstance(image, message.ImageFilePart):
163
+ image_urls.append({"type": "image_url", "image_url": {"url": image_file_to_data_url(image)}})
164
+ else:
165
+ image_urls.append({"type": "image_url", "image_url": {"url": image.url}})
166
+
167
+ # If only images (no text), use placeholder
168
+ has_text = bool(merged_text.strip())
169
+ tool_content = merged_text if has_text else "(see attached image)"
170
+
171
+ tool_message: dict[str, object] = {
172
+ "role": "tool",
173
+ "content": tool_content,
174
+ "tool_call_id": msg.call_id,
175
+ }
176
+
177
+ # Build user message with images if any
178
+ user_message: dict[str, object] | None = None
179
+ if image_urls:
180
+ user_message = {
181
+ "role": "user",
182
+ "content": [
183
+ {"type": "text", "text": "Attached image(s) from tool result:"},
184
+ *image_urls,
185
+ ],
186
+ }
187
+
188
+ return tool_message, user_message
189
+
190
+
122
191
  def build_assistant_common_fields(
123
192
  msg: message.AssistantMessage,
124
193
  *,
@@ -12,7 +12,7 @@ from klaude_code.llm.input_common import (
12
12
  attach_developer_messages,
13
13
  build_assistant_common_fields,
14
14
  build_chat_content_parts,
15
- build_tool_message,
15
+ build_tool_message_for_chat_completions,
16
16
  collect_text_content,
17
17
  )
18
18
  from klaude_code.protocol import llm_param, message
@@ -50,7 +50,10 @@ def convert_history_to_input(
50
50
  parts = build_chat_content_parts(msg, attachment)
51
51
  messages.append(cast(chat.ChatCompletionMessageParam, {"role": "user", "content": parts}))
52
52
  case message.ToolResultMessage():
53
- messages.append(cast(chat.ChatCompletionMessageParam, build_tool_message(msg, attachment)))
53
+ tool_msg, user_msg = build_tool_message_for_chat_completions(msg, attachment)
54
+ messages.append(cast(chat.ChatCompletionMessageParam, tool_msg))
55
+ if user_msg is not None:
56
+ messages.append(cast(chat.ChatCompletionMessageParam, user_msg))
54
57
  case message.AssistantMessage():
55
58
  messages.append(_assistant_message_to_openai(msg))
56
59
  case _:
@@ -15,7 +15,7 @@ from klaude_code.llm.input_common import (
15
15
  attach_developer_messages,
16
16
  build_assistant_common_fields,
17
17
  build_chat_content_parts,
18
- build_tool_message,
18
+ build_tool_message_for_chat_completions,
19
19
  collect_text_content,
20
20
  split_thinking_parts,
21
21
  )
@@ -153,7 +153,10 @@ def convert_history_to_input(
153
153
  parts = build_chat_content_parts(msg, attachment)
154
154
  messages.append(cast(chat.ChatCompletionMessageParam, {"role": "user", "content": parts}))
155
155
  case message.ToolResultMessage():
156
- messages.append(cast(chat.ChatCompletionMessageParam, build_tool_message(msg, attachment)))
156
+ tool_msg, user_msg = build_tool_message_for_chat_completions(msg, attachment)
157
+ messages.append(cast(chat.ChatCompletionMessageParam, tool_msg))
158
+ if user_msg is not None:
159
+ messages.append(cast(chat.ChatCompletionMessageParam, user_msg))
157
160
  case message.AssistantMessage():
158
161
  messages.append(_assistant_message_to_openrouter(msg, model_name))
159
162
  case _:
@@ -21,7 +21,6 @@ _PROTOCOL_MODULES: dict[llm_param.LLMClientProtocol, str] = {
21
21
  llm_param.LLMClientProtocol.OPENROUTER: "klaude_code.llm.openrouter",
22
22
  llm_param.LLMClientProtocol.RESPONSES: "klaude_code.llm.openai_responses",
23
23
  llm_param.LLMClientProtocol.GOOGLE: "klaude_code.llm.google",
24
- llm_param.LLMClientProtocol.ANTIGRAVITY: "klaude_code.llm.antigravity",
25
24
  }
26
25
 
27
26
 
@@ -14,6 +14,7 @@ __all__ = [
14
14
  "AssistantTextDeltaEvent",
15
15
  "AssistantTextEndEvent",
16
16
  "AssistantTextStartEvent",
17
+ "BacktrackEvent",
17
18
  "BashCommandEndEvent",
18
19
  "BashCommandOutputDeltaEvent",
19
20
  "BashCommandStartEvent",
@@ -116,6 +117,14 @@ class CompactionEndEvent(Event):
116
117
  kept_items_brief: list[message.KeptItemBrief] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
117
118
 
118
119
 
120
+ class BacktrackEvent(Event):
121
+ checkpoint_id: int
122
+ note: str
123
+ rationale: str
124
+ original_user_message: str
125
+ messages_discarded: int | None = None
126
+
127
+
119
128
  class TaskFinishEvent(Event):
120
129
  task_result: str
121
130
  has_structured_output: bool = False
@@ -220,6 +229,7 @@ type ReplayEventUnion = (
220
229
  | ErrorEvent
221
230
  | CompactionStartEvent
222
231
  | CompactionEndEvent
232
+ | BacktrackEvent
223
233
  )
224
234
 
225
235
 
@@ -16,7 +16,6 @@ class LLMClientProtocol(Enum):
16
16
  BEDROCK = "bedrock"
17
17
  CODEX_OAUTH = "codex_oauth"
18
18
  GOOGLE = "google"
19
- ANTIGRAVITY = "antigravity"
20
19
 
21
20
 
22
21
  class ToolSchema(BaseModel):
@@ -85,6 +85,15 @@ class CompactionEntry(BaseModel):
85
85
  created_at: datetime = Field(default_factory=datetime.now)
86
86
 
87
87
 
88
+ class BacktrackEntry(BaseModel):
89
+ checkpoint_id: int
90
+ note: str
91
+ rationale: str
92
+ reverted_from_index: int
93
+ original_user_message: str
94
+ created_at: datetime = Field(default_factory=datetime.now)
95
+
96
+
88
97
  # Part types
89
98
 
90
99
 
@@ -196,7 +205,7 @@ class ToolResultMessage(MessageBase):
196
205
 
197
206
  Message = SystemMessage | DeveloperMessage | UserMessage | AssistantMessage | ToolResultMessage
198
207
 
199
- HistoryEvent = Message | StreamErrorItem | TaskMetadataItem | CompactionEntry
208
+ HistoryEvent = Message | StreamErrorItem | TaskMetadataItem | CompactionEntry | BacktrackEntry
200
209
 
201
210
  StreamItem = AssistantTextDelta | AssistantImageDelta | ThinkingTextDelta | ToolCallStartDelta
202
211
 
@@ -12,6 +12,7 @@ WEB_SEARCH = "WebSearch"
12
12
  REPORT_BACK = "report_back"
13
13
  TASK = "Task"
14
14
  IMAGE_GEN = "ImageGen"
15
+ BACKTRACK = "Backtrack"
15
16
 
16
17
  # SubAgentType is just a string alias now; agent types are defined via SubAgentProfile
17
18
  SubAgentType = str
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
+ import re
4
5
  import time
5
6
  import uuid
6
7
  from collections.abc import Iterable, Sequence
@@ -15,6 +16,15 @@ from klaude_code.session.store import JsonlSessionStore, build_meta_snapshot
15
16
 
16
17
  _DEFAULT_STORES: dict[str, JsonlSessionStore] = {}
17
18
 
19
+ _CHECKPOINT_RE = re.compile(r"<system>Checkpoint (\d+)</system>")
20
+
21
+
22
+ def _extract_checkpoint_id(text: str) -> int | None:
23
+ match = _CHECKPOINT_RE.search(text)
24
+ if match is None:
25
+ return None
26
+ return int(match.group(1))
27
+
18
28
 
19
29
  def _read_json_dict(path: Path) -> dict[str, Any] | None:
20
30
  try:
@@ -51,6 +61,8 @@ class Session(BaseModel):
51
61
  todos: list[model.TodoItem] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
52
62
  model_name: str | None = None
53
63
 
64
+ next_checkpoint_id: int = 0
65
+
54
66
  model_config_name: str | None = None
55
67
  model_thinking: llm_param.Thinking | None = None
56
68
  created_at: float = Field(default_factory=lambda: time.time())
@@ -153,6 +165,8 @@ class Session(BaseModel):
153
165
  model_name = raw.get("model_name") if isinstance(raw.get("model_name"), str) else None
154
166
  model_config_name = raw.get("model_config_name") if isinstance(raw.get("model_config_name"), str) else None
155
167
 
168
+ next_checkpoint_id = int(raw.get("next_checkpoint_id", 0))
169
+
156
170
  model_thinking_raw = raw.get("model_thinking")
157
171
  model_thinking = (
158
172
  llm_param.Thinking.model_validate(model_thinking_raw) if isinstance(model_thinking_raw, dict) else None
@@ -169,6 +183,7 @@ class Session(BaseModel):
169
183
  model_name=model_name,
170
184
  model_config_name=model_config_name,
171
185
  model_thinking=model_thinking,
186
+ next_checkpoint_id=next_checkpoint_id,
172
187
  )
173
188
  session._store = store
174
189
  return session
@@ -221,19 +236,103 @@ class Session(BaseModel):
221
236
  model_name=self.model_name,
222
237
  model_config_name=self.model_config_name,
223
238
  model_thinking=self.model_thinking,
239
+ next_checkpoint_id=self.next_checkpoint_id,
224
240
  )
225
241
  self._store.append_and_flush(session_id=self.id, items=items, meta=meta)
226
242
 
243
+ @property
244
+ def n_checkpoints(self) -> int:
245
+ return self.next_checkpoint_id
246
+
247
+ def create_checkpoint(self) -> int:
248
+ checkpoint_id = self.next_checkpoint_id
249
+ self.next_checkpoint_id += 1
250
+ checkpoint_msg = message.DeveloperMessage(
251
+ parts=[message.TextPart(text=f"<system>Checkpoint {checkpoint_id}</system>")]
252
+ )
253
+ self.append_history([checkpoint_msg])
254
+ return checkpoint_id
255
+
256
+ def find_checkpoint_index(self, checkpoint_id: int) -> int | None:
257
+ target_text = f"<system>Checkpoint {checkpoint_id}</system>"
258
+ for i, item in enumerate(self.conversation_history):
259
+ if not isinstance(item, message.DeveloperMessage):
260
+ continue
261
+ text = message.join_text_parts(item.parts)
262
+ if target_text in text:
263
+ return i
264
+ return None
265
+
266
+ def get_user_message_before_checkpoint(self, checkpoint_id: int) -> str | None:
267
+ checkpoint_idx = self.find_checkpoint_index(checkpoint_id)
268
+ if checkpoint_idx is None:
269
+ return None
270
+
271
+ for i in range(checkpoint_idx - 1, -1, -1):
272
+ item = self.conversation_history[i]
273
+ if isinstance(item, message.UserMessage):
274
+ return message.join_text_parts(item.parts)
275
+ return None
276
+
277
+ def get_checkpoint_user_messages(self) -> dict[int, str]:
278
+ checkpoints: dict[int, str] = {}
279
+ last_user_message = ""
280
+ for item in self.conversation_history:
281
+ if isinstance(item, message.UserMessage):
282
+ last_user_message = message.join_text_parts(item.parts)
283
+ continue
284
+ if not isinstance(item, message.DeveloperMessage):
285
+ continue
286
+ text = message.join_text_parts(item.parts)
287
+ checkpoint_id = _extract_checkpoint_id(text)
288
+ if checkpoint_id is None:
289
+ continue
290
+ checkpoints[checkpoint_id] = last_user_message
291
+ return checkpoints
292
+
293
+ def revert_to_checkpoint(self, checkpoint_id: int, note: str, rationale: str) -> message.BacktrackEntry:
294
+ target_idx = self.find_checkpoint_index(checkpoint_id)
295
+ if target_idx is None:
296
+ raise ValueError(f"Checkpoint {checkpoint_id} not found")
297
+
298
+ user_message = self.get_user_message_before_checkpoint(checkpoint_id) or ""
299
+ reverted_from = len(self.conversation_history)
300
+ entry = message.BacktrackEntry(
301
+ checkpoint_id=checkpoint_id,
302
+ note=note,
303
+ rationale=rationale,
304
+ reverted_from_index=reverted_from,
305
+ original_user_message=user_message,
306
+ )
307
+
308
+ self.conversation_history = self.conversation_history[: target_idx + 1]
309
+ self.next_checkpoint_id = checkpoint_id + 1
310
+ self._invalidate_messages_count_cache()
311
+ self._user_messages_cache = None
312
+ return entry
313
+
227
314
  def get_llm_history(self) -> list[message.HistoryEvent]:
228
315
  """Return the LLM-facing history view with compaction summary injected."""
229
316
  history = self.conversation_history
317
+
318
+ def _convert(item: message.HistoryEvent) -> message.HistoryEvent:
319
+ if isinstance(item, message.BacktrackEntry):
320
+ return message.DeveloperMessage(
321
+ parts=[
322
+ message.TextPart(
323
+ text=f"<system>After this, some operations were performed and context was refined via Backtrack. Rationale: {item.rationale}. Summary: {item.note}. Please continue.</system>"
324
+ )
325
+ ]
326
+ )
327
+ return item
328
+
230
329
  last_compaction: message.CompactionEntry | None = None
231
330
  for item in reversed(history):
232
331
  if isinstance(item, message.CompactionEntry):
233
332
  last_compaction = item
234
333
  break
235
334
  if last_compaction is None:
236
- return [it for it in history if not isinstance(it, message.CompactionEntry)]
335
+ return [_convert(it) for it in history if not isinstance(it, message.CompactionEntry)]
237
336
 
238
337
  summary_message = message.UserMessage(parts=[message.TextPart(text=last_compaction.summary)])
239
338
  kept = [it for it in history[last_compaction.first_kept_index :] if not isinstance(it, message.CompactionEntry)]
@@ -246,7 +345,7 @@ class Session(BaseModel):
246
345
  first_non_tool += 1
247
346
  kept = kept[first_non_tool:]
248
347
 
249
- return [summary_message, *kept]
348
+ return [summary_message, *[_convert(it) for it in kept]]
250
349
 
251
350
  def fork(self, *, new_id: str | None = None, until_index: int | None = None) -> Session:
252
351
  """Create a new session as a fork of the current session.
@@ -266,6 +365,7 @@ class Session(BaseModel):
266
365
  forked.model_name = self.model_name
267
366
  forked.model_config_name = self.model_config_name
268
367
  forked.model_thinking = self.model_thinking.model_copy(deep=True) if self.model_thinking is not None else None
368
+ forked.next_checkpoint_id = self.next_checkpoint_id
269
369
  forked.file_tracker = {k: v.model_copy(deep=True) for k, v in self.file_tracker.items()}
270
370
  forked.todos = [todo.model_copy(deep=True) for todo in self.todos]
271
371
 
@@ -437,6 +537,15 @@ class Session(BaseModel):
437
537
  yield events.DeveloperMessageEvent(session_id=self.id, item=dm)
438
538
  case message.StreamErrorItem() as se:
439
539
  yield events.ErrorEvent(error_message=se.error, can_retry=False, session_id=self.id)
540
+ case message.BacktrackEntry() as be:
541
+ yield events.BacktrackEvent(
542
+ session_id=self.id,
543
+ checkpoint_id=be.checkpoint_id,
544
+ note=be.note,
545
+ rationale=be.rationale,
546
+ original_user_message=be.original_user_message,
547
+ messages_discarded=None,
548
+ )
440
549
  case message.CompactionEntry() as ce:
441
550
  yield events.CompactionStartEvent(session_id=self.id, reason="threshold")
442
551
  yield events.CompactionEndEvent(
@@ -169,6 +169,7 @@ def build_meta_snapshot(
169
169
  model_name: str | None,
170
170
  model_config_name: str | None,
171
171
  model_thinking: llm_param.Thinking | None,
172
+ next_checkpoint_id: int = 0,
172
173
  ) -> dict[str, Any]:
173
174
  return {
174
175
  "id": session_id,
@@ -186,4 +187,5 @@ def build_meta_snapshot(
186
187
  "model_thinking": model_thinking.model_dump(mode="json", exclude_defaults=True, exclude_none=True)
187
188
  if model_thinking
188
189
  else None,
190
+ "next_checkpoint_id": next_checkpoint_id,
189
191
  }
@@ -0,0 +1,84 @@
1
+ ---
2
+ name: executing-plans
3
+ description: Use when you have a written implementation plan to execute in a separate session with review checkpoints
4
+ ---
5
+
6
+ # Executing Plans
7
+
8
+ ## Overview
9
+
10
+ Load plan, review critically, execute tasks in batches, report for review between batches.
11
+
12
+ **Core principle:** Batch execution with checkpoints for architect review.
13
+
14
+ **Announce at start:** "I'm using the executing-plans skill to implement this plan."
15
+
16
+ ## The Process
17
+
18
+ ### Step 1: Load and Review Plan
19
+ 1. Read plan file
20
+ 2. Review critically - identify any questions or concerns about the plan
21
+ 3. If concerns: Raise them with your human partner before starting
22
+ 4. If no concerns: Create TodoWrite and proceed
23
+
24
+ ### Step 2: Execute Batch
25
+ **Default: First 3 tasks**
26
+
27
+ For each task:
28
+ 1. Mark as in_progress
29
+ 2. Follow each step exactly (plan has bite-sized steps)
30
+ 3. Run verifications as specified
31
+ 4. Mark as completed
32
+
33
+ ### Step 3: Report
34
+ When batch complete:
35
+ - Show what was implemented
36
+ - Show verification output
37
+ - Say: "Ready for feedback."
38
+
39
+ ### Step 4: Continue
40
+ Based on feedback:
41
+ - Apply changes if needed
42
+ - Execute next batch
43
+ - Repeat until complete
44
+
45
+ ### Step 5: Complete Development
46
+
47
+ After all tasks complete and verified:
48
+ - Announce: "I'm using the finishing-a-development-branch skill to complete this work."
49
+ - **REQUIRED SUB-SKILL:** Use superpowers:finishing-a-development-branch
50
+ - Follow that skill to verify tests, present options, execute choice
51
+
52
+ ## When to Stop and Ask for Help
53
+
54
+ **STOP executing immediately when:**
55
+ - Hit a blocker mid-batch (missing dependency, test fails, instruction unclear)
56
+ - Plan has critical gaps preventing starting
57
+ - You don't understand an instruction
58
+ - Verification fails repeatedly
59
+
60
+ **Ask for clarification rather than guessing.**
61
+
62
+ ## When to Revisit Earlier Steps
63
+
64
+ **Return to Review (Step 1) when:**
65
+ - Partner updates the plan based on your feedback
66
+ - Fundamental approach needs rethinking
67
+
68
+ **Don't force through blockers** - stop and ask.
69
+
70
+ ## Remember
71
+ - Review plan critically first
72
+ - Follow plan steps exactly
73
+ - Don't skip verifications
74
+ - Reference skills when plan says to
75
+ - Between batches: just report and wait
76
+ - Stop when blocked, don't guess
77
+ - Never start implementation on main/master branch without explicit user consent
78
+
79
+ ## Integration
80
+
81
+ **Required workflow skills:**
82
+ - **superpowers:using-git-worktrees** - REQUIRED: Set up isolated workspace before starting
83
+ - **superpowers:writing-plans** - Creates the plan this skill executes
84
+ - **superpowers:finishing-a-development-branch** - Complete development after all tasks