code-puppy 0.0.214__py3-none-any.whl → 0.0.366__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 (231) hide show
  1. code_puppy/__init__.py +7 -1
  2. code_puppy/agents/__init__.py +2 -0
  3. code_puppy/agents/agent_c_reviewer.py +59 -6
  4. code_puppy/agents/agent_code_puppy.py +7 -1
  5. code_puppy/agents/agent_code_reviewer.py +12 -2
  6. code_puppy/agents/agent_cpp_reviewer.py +73 -6
  7. code_puppy/agents/agent_creator_agent.py +45 -4
  8. code_puppy/agents/agent_golang_reviewer.py +92 -3
  9. code_puppy/agents/agent_javascript_reviewer.py +101 -8
  10. code_puppy/agents/agent_manager.py +81 -4
  11. code_puppy/agents/agent_pack_leader.py +383 -0
  12. code_puppy/agents/agent_planning.py +163 -0
  13. code_puppy/agents/agent_python_programmer.py +165 -0
  14. code_puppy/agents/agent_python_reviewer.py +28 -6
  15. code_puppy/agents/agent_qa_expert.py +98 -6
  16. code_puppy/agents/agent_qa_kitten.py +12 -7
  17. code_puppy/agents/agent_security_auditor.py +113 -3
  18. code_puppy/agents/agent_terminal_qa.py +323 -0
  19. code_puppy/agents/agent_typescript_reviewer.py +106 -7
  20. code_puppy/agents/base_agent.py +802 -176
  21. code_puppy/agents/event_stream_handler.py +350 -0
  22. code_puppy/agents/pack/__init__.py +34 -0
  23. code_puppy/agents/pack/bloodhound.py +304 -0
  24. code_puppy/agents/pack/husky.py +321 -0
  25. code_puppy/agents/pack/retriever.py +393 -0
  26. code_puppy/agents/pack/shepherd.py +348 -0
  27. code_puppy/agents/pack/terrier.py +287 -0
  28. code_puppy/agents/pack/watchdog.py +367 -0
  29. code_puppy/agents/prompt_reviewer.py +145 -0
  30. code_puppy/agents/subagent_stream_handler.py +276 -0
  31. code_puppy/api/__init__.py +13 -0
  32. code_puppy/api/app.py +169 -0
  33. code_puppy/api/main.py +21 -0
  34. code_puppy/api/pty_manager.py +446 -0
  35. code_puppy/api/routers/__init__.py +12 -0
  36. code_puppy/api/routers/agents.py +36 -0
  37. code_puppy/api/routers/commands.py +217 -0
  38. code_puppy/api/routers/config.py +74 -0
  39. code_puppy/api/routers/sessions.py +232 -0
  40. code_puppy/api/templates/terminal.html +361 -0
  41. code_puppy/api/websocket.py +154 -0
  42. code_puppy/callbacks.py +142 -4
  43. code_puppy/chatgpt_codex_client.py +283 -0
  44. code_puppy/claude_cache_client.py +586 -0
  45. code_puppy/cli_runner.py +916 -0
  46. code_puppy/command_line/add_model_menu.py +1079 -0
  47. code_puppy/command_line/agent_menu.py +395 -0
  48. code_puppy/command_line/attachments.py +10 -5
  49. code_puppy/command_line/autosave_menu.py +605 -0
  50. code_puppy/command_line/clipboard.py +527 -0
  51. code_puppy/command_line/colors_menu.py +520 -0
  52. code_puppy/command_line/command_handler.py +176 -738
  53. code_puppy/command_line/command_registry.py +150 -0
  54. code_puppy/command_line/config_commands.py +715 -0
  55. code_puppy/command_line/core_commands.py +792 -0
  56. code_puppy/command_line/diff_menu.py +863 -0
  57. code_puppy/command_line/load_context_completion.py +15 -22
  58. code_puppy/command_line/mcp/base.py +0 -3
  59. code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
  60. code_puppy/command_line/mcp/custom_server_form.py +688 -0
  61. code_puppy/command_line/mcp/custom_server_installer.py +195 -0
  62. code_puppy/command_line/mcp/edit_command.py +148 -0
  63. code_puppy/command_line/mcp/handler.py +9 -4
  64. code_puppy/command_line/mcp/help_command.py +6 -5
  65. code_puppy/command_line/mcp/install_command.py +15 -26
  66. code_puppy/command_line/mcp/install_menu.py +685 -0
  67. code_puppy/command_line/mcp/list_command.py +2 -2
  68. code_puppy/command_line/mcp/logs_command.py +174 -65
  69. code_puppy/command_line/mcp/remove_command.py +2 -2
  70. code_puppy/command_line/mcp/restart_command.py +12 -4
  71. code_puppy/command_line/mcp/search_command.py +16 -10
  72. code_puppy/command_line/mcp/start_all_command.py +18 -6
  73. code_puppy/command_line/mcp/start_command.py +47 -25
  74. code_puppy/command_line/mcp/status_command.py +4 -5
  75. code_puppy/command_line/mcp/stop_all_command.py +7 -1
  76. code_puppy/command_line/mcp/stop_command.py +8 -4
  77. code_puppy/command_line/mcp/test_command.py +2 -2
  78. code_puppy/command_line/mcp/wizard_utils.py +20 -16
  79. code_puppy/command_line/mcp_completion.py +174 -0
  80. code_puppy/command_line/model_picker_completion.py +75 -25
  81. code_puppy/command_line/model_settings_menu.py +884 -0
  82. code_puppy/command_line/motd.py +14 -8
  83. code_puppy/command_line/onboarding_slides.py +179 -0
  84. code_puppy/command_line/onboarding_wizard.py +340 -0
  85. code_puppy/command_line/pin_command_completion.py +329 -0
  86. code_puppy/command_line/prompt_toolkit_completion.py +463 -63
  87. code_puppy/command_line/session_commands.py +296 -0
  88. code_puppy/command_line/utils.py +54 -0
  89. code_puppy/config.py +898 -112
  90. code_puppy/error_logging.py +118 -0
  91. code_puppy/gemini_code_assist.py +385 -0
  92. code_puppy/gemini_model.py +602 -0
  93. code_puppy/http_utils.py +210 -148
  94. code_puppy/keymap.py +128 -0
  95. code_puppy/main.py +5 -698
  96. code_puppy/mcp_/__init__.py +17 -0
  97. code_puppy/mcp_/async_lifecycle.py +35 -4
  98. code_puppy/mcp_/blocking_startup.py +70 -43
  99. code_puppy/mcp_/captured_stdio_server.py +2 -2
  100. code_puppy/mcp_/config_wizard.py +4 -4
  101. code_puppy/mcp_/dashboard.py +15 -6
  102. code_puppy/mcp_/managed_server.py +65 -38
  103. code_puppy/mcp_/manager.py +146 -52
  104. code_puppy/mcp_/mcp_logs.py +224 -0
  105. code_puppy/mcp_/registry.py +6 -6
  106. code_puppy/mcp_/server_registry_catalog.py +24 -5
  107. code_puppy/messaging/__init__.py +199 -2
  108. code_puppy/messaging/bus.py +610 -0
  109. code_puppy/messaging/commands.py +167 -0
  110. code_puppy/messaging/markdown_patches.py +57 -0
  111. code_puppy/messaging/message_queue.py +17 -48
  112. code_puppy/messaging/messages.py +500 -0
  113. code_puppy/messaging/queue_console.py +1 -24
  114. code_puppy/messaging/renderers.py +43 -146
  115. code_puppy/messaging/rich_renderer.py +1027 -0
  116. code_puppy/messaging/spinner/__init__.py +21 -5
  117. code_puppy/messaging/spinner/console_spinner.py +86 -51
  118. code_puppy/messaging/subagent_console.py +461 -0
  119. code_puppy/model_factory.py +634 -83
  120. code_puppy/model_utils.py +167 -0
  121. code_puppy/models.json +66 -68
  122. code_puppy/models_dev_api.json +1 -0
  123. code_puppy/models_dev_parser.py +592 -0
  124. code_puppy/plugins/__init__.py +164 -10
  125. code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
  126. code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
  127. code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
  128. code_puppy/plugins/antigravity_oauth/config.py +42 -0
  129. code_puppy/plugins/antigravity_oauth/constants.py +136 -0
  130. code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
  131. code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
  132. code_puppy/plugins/antigravity_oauth/storage.py +271 -0
  133. code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
  134. code_puppy/plugins/antigravity_oauth/token.py +167 -0
  135. code_puppy/plugins/antigravity_oauth/transport.py +767 -0
  136. code_puppy/plugins/antigravity_oauth/utils.py +169 -0
  137. code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
  138. code_puppy/plugins/chatgpt_oauth/config.py +52 -0
  139. code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
  140. code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
  141. code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
  142. code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
  143. code_puppy/plugins/claude_code_oauth/README.md +167 -0
  144. code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
  145. code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
  146. code_puppy/plugins/claude_code_oauth/config.py +50 -0
  147. code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
  148. code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
  149. code_puppy/plugins/claude_code_oauth/utils.py +518 -0
  150. code_puppy/plugins/customizable_commands/__init__.py +0 -0
  151. code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
  152. code_puppy/plugins/example_custom_command/README.md +280 -0
  153. code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
  154. code_puppy/plugins/file_permission_handler/__init__.py +4 -0
  155. code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
  156. code_puppy/plugins/frontend_emitter/__init__.py +25 -0
  157. code_puppy/plugins/frontend_emitter/emitter.py +121 -0
  158. code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
  159. code_puppy/plugins/oauth_puppy_html.py +228 -0
  160. code_puppy/plugins/shell_safety/__init__.py +6 -0
  161. code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
  162. code_puppy/plugins/shell_safety/command_cache.py +156 -0
  163. code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
  164. code_puppy/prompts/antigravity_system_prompt.md +1 -0
  165. code_puppy/prompts/codex_system_prompt.md +310 -0
  166. code_puppy/pydantic_patches.py +131 -0
  167. code_puppy/reopenable_async_client.py +8 -8
  168. code_puppy/round_robin_model.py +9 -12
  169. code_puppy/session_storage.py +2 -1
  170. code_puppy/status_display.py +21 -4
  171. code_puppy/summarization_agent.py +41 -13
  172. code_puppy/terminal_utils.py +418 -0
  173. code_puppy/tools/__init__.py +37 -1
  174. code_puppy/tools/agent_tools.py +536 -52
  175. code_puppy/tools/browser/__init__.py +37 -0
  176. code_puppy/tools/browser/browser_control.py +19 -23
  177. code_puppy/tools/browser/browser_interactions.py +41 -48
  178. code_puppy/tools/browser/browser_locators.py +36 -38
  179. code_puppy/tools/browser/browser_manager.py +316 -0
  180. code_puppy/tools/browser/browser_navigation.py +16 -16
  181. code_puppy/tools/browser/browser_screenshot.py +79 -143
  182. code_puppy/tools/browser/browser_scripts.py +32 -42
  183. code_puppy/tools/browser/browser_workflows.py +44 -27
  184. code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
  185. code_puppy/tools/browser/terminal_command_tools.py +521 -0
  186. code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
  187. code_puppy/tools/browser/terminal_tools.py +525 -0
  188. code_puppy/tools/command_runner.py +930 -147
  189. code_puppy/tools/common.py +1113 -5
  190. code_puppy/tools/display.py +84 -0
  191. code_puppy/tools/file_modifications.py +288 -89
  192. code_puppy/tools/file_operations.py +226 -154
  193. code_puppy/tools/subagent_context.py +158 -0
  194. code_puppy/uvx_detection.py +242 -0
  195. code_puppy/version_checker.py +30 -11
  196. code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
  197. code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
  198. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
  199. code_puppy-0.0.366.dist-info/RECORD +217 -0
  200. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
  201. code_puppy/command_line/mcp/add_command.py +0 -183
  202. code_puppy/messaging/spinner/textual_spinner.py +0 -106
  203. code_puppy/tools/browser/camoufox_manager.py +0 -216
  204. code_puppy/tools/browser/vqa_agent.py +0 -70
  205. code_puppy/tui/__init__.py +0 -10
  206. code_puppy/tui/app.py +0 -1105
  207. code_puppy/tui/components/__init__.py +0 -21
  208. code_puppy/tui/components/chat_view.py +0 -551
  209. code_puppy/tui/components/command_history_modal.py +0 -218
  210. code_puppy/tui/components/copy_button.py +0 -139
  211. code_puppy/tui/components/custom_widgets.py +0 -63
  212. code_puppy/tui/components/human_input_modal.py +0 -175
  213. code_puppy/tui/components/input_area.py +0 -167
  214. code_puppy/tui/components/sidebar.py +0 -309
  215. code_puppy/tui/components/status_bar.py +0 -185
  216. code_puppy/tui/messages.py +0 -27
  217. code_puppy/tui/models/__init__.py +0 -8
  218. code_puppy/tui/models/chat_message.py +0 -25
  219. code_puppy/tui/models/command_history.py +0 -89
  220. code_puppy/tui/models/enums.py +0 -24
  221. code_puppy/tui/screens/__init__.py +0 -17
  222. code_puppy/tui/screens/autosave_picker.py +0 -175
  223. code_puppy/tui/screens/help.py +0 -130
  224. code_puppy/tui/screens/mcp_install_wizard.py +0 -803
  225. code_puppy/tui/screens/settings.py +0 -306
  226. code_puppy/tui/screens/tools.py +0 -74
  227. code_puppy/tui_state.py +0 -55
  228. code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
  229. code_puppy-0.0.214.dist-info/RECORD +0 -131
  230. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
  231. {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,350 @@
1
+ """Event stream handler for processing streaming events from agent runs."""
2
+
3
+ import asyncio
4
+ import logging
5
+ from collections.abc import AsyncIterable
6
+ from typing import Any, Optional
7
+
8
+ from pydantic_ai import PartDeltaEvent, PartEndEvent, PartStartEvent, RunContext
9
+ from pydantic_ai.messages import (
10
+ TextPart,
11
+ TextPartDelta,
12
+ ThinkingPart,
13
+ ThinkingPartDelta,
14
+ ToolCallPart,
15
+ ToolCallPartDelta,
16
+ )
17
+ from rich.console import Console
18
+ from rich.markup import escape
19
+ from rich.text import Text
20
+
21
+ from code_puppy.config import get_banner_color, get_subagent_verbose
22
+ from code_puppy.messaging.spinner import pause_all_spinners, resume_all_spinners
23
+ from code_puppy.tools.subagent_context import is_subagent
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+
28
+ def _fire_stream_event(event_type: str, event_data: Any) -> None:
29
+ """Fire a stream event callback asynchronously (non-blocking).
30
+
31
+ Args:
32
+ event_type: Type of the event (e.g., 'part_start', 'part_delta', 'part_end')
33
+ event_data: Data associated with the event
34
+ """
35
+ try:
36
+ from code_puppy import callbacks
37
+ from code_puppy.messaging import get_session_context
38
+
39
+ agent_session_id = get_session_context()
40
+
41
+ # Use create_task to fire callback without blocking
42
+ asyncio.create_task(
43
+ callbacks.on_stream_event(event_type, event_data, agent_session_id)
44
+ )
45
+ except ImportError:
46
+ logger.debug("callbacks or messaging module not available for stream event")
47
+ except Exception as e:
48
+ logger.debug(f"Error firing stream event callback: {e}")
49
+
50
+
51
+ # Module-level console for streaming output
52
+ # Set via set_streaming_console() to share console with spinner
53
+ _streaming_console: Optional[Console] = None
54
+
55
+
56
+ def set_streaming_console(console: Optional[Console]) -> None:
57
+ """Set the console used for streaming output.
58
+
59
+ This should be called with the same console used by the spinner
60
+ to avoid Live display conflicts that cause line duplication.
61
+
62
+ Args:
63
+ console: The Rich console to use, or None to use a fallback.
64
+ """
65
+ global _streaming_console
66
+ _streaming_console = console
67
+
68
+
69
+ def get_streaming_console() -> Console:
70
+ """Get the console for streaming output.
71
+
72
+ Returns the configured console or creates a fallback Console.
73
+ """
74
+ if _streaming_console is not None:
75
+ return _streaming_console
76
+ return Console()
77
+
78
+
79
+ def _should_suppress_output() -> bool:
80
+ """Check if sub-agent output should be suppressed.
81
+
82
+ Returns:
83
+ True if we're in a sub-agent context and verbose mode is disabled.
84
+ """
85
+ return is_subagent() and not get_subagent_verbose()
86
+
87
+
88
+ async def event_stream_handler(
89
+ ctx: RunContext,
90
+ events: AsyncIterable[Any],
91
+ ) -> None:
92
+ """Handle streaming events from the agent run.
93
+
94
+ This function processes streaming events and emits TextPart, ThinkingPart,
95
+ and ToolCallPart content with styled banners/tokens as they stream in.
96
+
97
+ Args:
98
+ ctx: The run context.
99
+ events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
100
+ """
101
+ # If we're in a sub-agent and verbose mode is disabled, silently consume events
102
+ if _should_suppress_output():
103
+ async for _ in events:
104
+ pass # Just consume events without rendering
105
+ return
106
+
107
+ import time
108
+
109
+ from termflow import Parser as TermflowParser
110
+ from termflow import Renderer as TermflowRenderer
111
+
112
+ # Use the module-level console (set via set_streaming_console)
113
+ console = get_streaming_console()
114
+
115
+ # Track which part indices we're currently streaming (for Text/Thinking/Tool parts)
116
+ streaming_parts: set[int] = set()
117
+ thinking_parts: set[int] = set() # Track which parts are thinking (for dim style)
118
+ text_parts: set[int] = set() # Track which parts are text
119
+ tool_parts: set[int] = set() # Track which parts are tool calls
120
+ banner_printed: set[int] = set() # Track if banner was already printed
121
+ token_count: dict[int, int] = {} # Track token count per text/tool part
122
+ tool_names: dict[int, str] = {} # Track tool name per tool part index
123
+ did_stream_anything = False # Track if we streamed any content
124
+
125
+ # Termflow streaming state for text parts
126
+ termflow_parsers: dict[int, TermflowParser] = {}
127
+ termflow_renderers: dict[int, TermflowRenderer] = {}
128
+ termflow_line_buffers: dict[int, str] = {} # Buffer incomplete lines
129
+
130
+ def _print_thinking_banner() -> None:
131
+ """Print the THINKING banner with spinner pause and line clear."""
132
+ nonlocal did_stream_anything
133
+
134
+ pause_all_spinners()
135
+ time.sleep(0.1) # Delay to let spinner fully clear
136
+ # Clear line and print newline before banner
137
+ console.print(" " * 50, end="\r")
138
+ console.print() # Newline before banner
139
+ # Bold banner with configurable color and lightning bolt
140
+ thinking_color = get_banner_color("thinking")
141
+ console.print(
142
+ Text.from_markup(
143
+ f"[bold white on {thinking_color}] THINKING [/bold white on {thinking_color}] [dim]\u26a1 "
144
+ ),
145
+ end="",
146
+ )
147
+ did_stream_anything = True
148
+
149
+ def _print_response_banner() -> None:
150
+ """Print the AGENT RESPONSE banner with spinner pause and line clear."""
151
+ nonlocal did_stream_anything
152
+
153
+ pause_all_spinners()
154
+ time.sleep(0.1) # Delay to let spinner fully clear
155
+ # Clear line and print newline before banner
156
+ console.print(" " * 50, end="\r")
157
+ console.print() # Newline before banner
158
+ response_color = get_banner_color("agent_response")
159
+ console.print(
160
+ Text.from_markup(
161
+ f"[bold white on {response_color}] AGENT RESPONSE [/bold white on {response_color}]"
162
+ )
163
+ )
164
+ did_stream_anything = True
165
+
166
+ async for event in events:
167
+ # PartStartEvent - register the part but defer banner until content arrives
168
+ if isinstance(event, PartStartEvent):
169
+ # Fire stream event callback for part_start
170
+ _fire_stream_event(
171
+ "part_start",
172
+ {
173
+ "index": event.index,
174
+ "part_type": type(event.part).__name__,
175
+ "part": event.part,
176
+ },
177
+ )
178
+
179
+ part = event.part
180
+ if isinstance(part, ThinkingPart):
181
+ streaming_parts.add(event.index)
182
+ thinking_parts.add(event.index)
183
+ # If there's initial content, print banner + content now
184
+ if part.content and part.content.strip():
185
+ _print_thinking_banner()
186
+ escaped = escape(part.content)
187
+ console.print(f"[dim]{escaped}[/dim]", end="")
188
+ banner_printed.add(event.index)
189
+ elif isinstance(part, TextPart):
190
+ streaming_parts.add(event.index)
191
+ text_parts.add(event.index)
192
+ # Initialize termflow streaming for this text part
193
+ termflow_parsers[event.index] = TermflowParser()
194
+ termflow_renderers[event.index] = TermflowRenderer(
195
+ output=console.file, width=console.width
196
+ )
197
+ termflow_line_buffers[event.index] = ""
198
+ # Handle initial content if present
199
+ if part.content and part.content.strip():
200
+ _print_response_banner()
201
+ banner_printed.add(event.index)
202
+ termflow_line_buffers[event.index] = part.content
203
+ elif isinstance(part, ToolCallPart):
204
+ streaming_parts.add(event.index)
205
+ tool_parts.add(event.index)
206
+ token_count[event.index] = 0 # Initialize token counter
207
+ # Capture tool name from the start event
208
+ tool_names[event.index] = part.tool_name or ""
209
+ # Track tool name for display
210
+ banner_printed.add(
211
+ event.index
212
+ ) # Use banner_printed to track if we've shown tool info
213
+
214
+ # PartDeltaEvent - stream the content as it arrives
215
+ elif isinstance(event, PartDeltaEvent):
216
+ # Fire stream event callback for part_delta
217
+ _fire_stream_event(
218
+ "part_delta",
219
+ {
220
+ "index": event.index,
221
+ "delta_type": type(event.delta).__name__,
222
+ "delta": event.delta,
223
+ },
224
+ )
225
+
226
+ if event.index in streaming_parts:
227
+ delta = event.delta
228
+ if isinstance(delta, (TextPartDelta, ThinkingPartDelta)):
229
+ if delta.content_delta:
230
+ # For text parts, stream markdown with termflow
231
+ if event.index in text_parts:
232
+ # Print banner on first content
233
+ if event.index not in banner_printed:
234
+ _print_response_banner()
235
+ banner_printed.add(event.index)
236
+
237
+ # Add content to line buffer
238
+ termflow_line_buffers[event.index] += delta.content_delta
239
+
240
+ # Process complete lines
241
+ parser = termflow_parsers[event.index]
242
+ renderer = termflow_renderers[event.index]
243
+ buffer = termflow_line_buffers[event.index]
244
+
245
+ while "\n" in buffer:
246
+ line, buffer = buffer.split("\n", 1)
247
+ events_to_render = parser.parse_line(line)
248
+ renderer.render_all(events_to_render)
249
+
250
+ termflow_line_buffers[event.index] = buffer
251
+ else:
252
+ # For thinking parts, stream immediately (dim)
253
+ if event.index not in banner_printed:
254
+ _print_thinking_banner()
255
+ banner_printed.add(event.index)
256
+ escaped = escape(delta.content_delta)
257
+ console.print(f"[dim]{escaped}[/dim]", end="")
258
+ elif isinstance(delta, ToolCallPartDelta):
259
+ # For tool calls, estimate tokens from args_delta content
260
+ # args_delta contains the streaming JSON arguments
261
+ args_delta = getattr(delta, "args_delta", "") or ""
262
+ if args_delta:
263
+ # Rough estimate: 4 chars ≈ 1 token (same heuristic as subagent_stream_handler)
264
+ estimated_tokens = max(1, len(args_delta) // 4)
265
+ token_count[event.index] += estimated_tokens
266
+ else:
267
+ # Even empty deltas count as activity
268
+ token_count[event.index] += 1
269
+
270
+ # Update tool name if delta provides more of it
271
+ tool_name_delta = getattr(delta, "tool_name_delta", "") or ""
272
+ if tool_name_delta:
273
+ tool_names[event.index] = (
274
+ tool_names.get(event.index, "") + tool_name_delta
275
+ )
276
+
277
+ # Use stored tool name for display
278
+ tool_name = tool_names.get(event.index, "")
279
+ count = token_count[event.index]
280
+ # Display with tool wrench icon and tool name
281
+ if tool_name:
282
+ console.print(
283
+ f" \U0001f527 Calling {tool_name}... {count} token(s) ",
284
+ end="\r",
285
+ )
286
+ else:
287
+ console.print(
288
+ f" \U0001f527 Calling tool... {count} token(s) ",
289
+ end="\r",
290
+ )
291
+
292
+ # PartEndEvent - finish the streaming with a newline
293
+ elif isinstance(event, PartEndEvent):
294
+ # Fire stream event callback for part_end
295
+ _fire_stream_event(
296
+ "part_end",
297
+ {
298
+ "index": event.index,
299
+ "next_part_kind": getattr(event, "next_part_kind", None),
300
+ },
301
+ )
302
+
303
+ if event.index in streaming_parts:
304
+ # For text parts, finalize termflow rendering
305
+ if event.index in text_parts:
306
+ # Render any remaining buffered content
307
+ if event.index in termflow_parsers:
308
+ parser = termflow_parsers[event.index]
309
+ renderer = termflow_renderers[event.index]
310
+ remaining = termflow_line_buffers.get(event.index, "")
311
+
312
+ # Parse and render any remaining partial line
313
+ if remaining.strip():
314
+ events_to_render = parser.parse_line(remaining)
315
+ renderer.render_all(events_to_render)
316
+
317
+ # Finalize the parser to close any open blocks
318
+ final_events = parser.finalize()
319
+ renderer.render_all(final_events)
320
+
321
+ # Clean up termflow state
322
+ del termflow_parsers[event.index]
323
+ del termflow_renderers[event.index]
324
+ del termflow_line_buffers[event.index]
325
+ # For tool parts, clear the chunk counter line
326
+ elif event.index in tool_parts:
327
+ # Clear the chunk counter line by printing spaces and returning
328
+ console.print(" " * 50, end="\r")
329
+ # For thinking parts, just print newline
330
+ elif event.index in banner_printed:
331
+ console.print() # Final newline after streaming
332
+
333
+ # Clean up token count and tool names
334
+ token_count.pop(event.index, None)
335
+ tool_names.pop(event.index, None)
336
+ # Clean up all tracking sets
337
+ streaming_parts.discard(event.index)
338
+ thinking_parts.discard(event.index)
339
+ text_parts.discard(event.index)
340
+ tool_parts.discard(event.index)
341
+ banner_printed.discard(event.index)
342
+
343
+ # Resume spinner if next part is NOT text/thinking/tool (avoid race condition)
344
+ # If next part is None or handled differently, it's safe to resume
345
+ # Note: spinner itself handles blank line before appearing
346
+ next_kind = getattr(event, "next_part_kind", None)
347
+ if next_kind not in ("text", "thinking", "tool-call"):
348
+ resume_all_spinners()
349
+
350
+ # Spinner is resumed in PartEndEvent when appropriate (based on next_part_kind)
@@ -0,0 +1,34 @@
1
+ """The Pack - Specialized sub-agents coordinated by Pack Leader 🐺
2
+
3
+ This package contains the specialized agents that work together under
4
+ Pack Leader's coordination for parallel multi-agent workflows:
5
+
6
+ - **Bloodhound** 🐕‍🦺 - Issue tracking specialist (bd only)
7
+ - **Terrier** 🐕 - Worktree management (git worktree from base branch)
8
+ - **Husky** 🐺 - Task execution (coding work in worktrees)
9
+ - **Shepherd** 🐕 - Code review critic (quality gatekeeper)
10
+ - **Watchdog** 🐕‍🦺 - QA critic (tests, coverage, quality)
11
+ - **Retriever** 🦮 - Local branch merging (git merge to base branch)
12
+
13
+ All work happens locally - no GitHub PRs or remote pushes.
14
+ Everything merges to a declared base branch.
15
+
16
+ Each agent is designed to do one thing well, following the Unix philosophy.
17
+ Pack Leader orchestrates them to execute complex parallel workflows.
18
+ """
19
+
20
+ from .bloodhound import BloodhoundAgent
21
+ from .husky import HuskyAgent
22
+ from .retriever import RetrieverAgent
23
+ from .shepherd import ShepherdAgent
24
+ from .terrier import TerrierAgent
25
+ from .watchdog import WatchdogAgent
26
+
27
+ __all__ = [
28
+ "BloodhoundAgent",
29
+ "TerrierAgent",
30
+ "RetrieverAgent",
31
+ "HuskyAgent",
32
+ "ShepherdAgent",
33
+ "WatchdogAgent",
34
+ ]
@@ -0,0 +1,304 @@
1
+ """Bloodhound - The issue tracking specialist who follows the scent of dependencies 🐕‍🦺"""
2
+
3
+ from code_puppy.config import get_puppy_name
4
+
5
+ from ... import callbacks
6
+ from ..base_agent import BaseAgent
7
+
8
+
9
+ class BloodhoundAgent(BaseAgent):
10
+ """Bloodhound - Tracks issues like following a scent trail.
11
+
12
+ Expert in `bd` (local issue tracker with dependencies).
13
+ Never loses the trail!
14
+ """
15
+
16
+ @property
17
+ def name(self) -> str:
18
+ return "bloodhound"
19
+
20
+ @property
21
+ def display_name(self) -> str:
22
+ return "Bloodhound 🐕‍🦺"
23
+
24
+ @property
25
+ def description(self) -> str:
26
+ return "Issue tracking specialist - follows the scent of dependencies with bd"
27
+
28
+ def get_available_tools(self) -> list[str]:
29
+ """Get the list of tools available to Bloodhound."""
30
+ return [
31
+ # Shell for bd commands
32
+ "agent_run_shell_command",
33
+ # Transparency - always share the sniff report!
34
+ "agent_share_your_reasoning",
35
+ # Read files to understand issue context
36
+ "read_file",
37
+ ]
38
+
39
+ def get_system_prompt(self) -> str:
40
+ """Get Bloodhound's system prompt."""
41
+ puppy_name = get_puppy_name()
42
+
43
+ result = f"""
44
+ You are {puppy_name} as Bloodhound 🐕‍🦺 - the issue tracking specialist with the best nose in the pack!
45
+
46
+ Your job is to track issues like a bloodhound follows a scent trail. You're an expert in:
47
+ - **`bd`** - The local issue tracker with powerful dependency support
48
+
49
+ You never lose the trail of an issue! When Pack Leader needs issues created, queried, or managed, you're the one who sniffs it out.
50
+
51
+ ## 🐕‍🦺 YOUR SPECIALTY
52
+
53
+ You follow the scent of:
54
+ - **Issue dependencies** - What blocks what? What was discovered from what?
55
+ - **Issue status** - What's open? What's ready to work on? What's blocked?
56
+ - **Priority trails** - Critical issues get your attention first!
57
+ - **Dependency visualization** - See the full tree of how work connects
58
+
59
+ ## 📋 CORE bd COMMANDS
60
+
61
+ ### Creating Issues
62
+ ```bash
63
+ # Basic issue creation
64
+ bd create "Fix login bug" -d "Users can't login after password reset" -p 1 -t bug
65
+
66
+ # With dependencies (the good stuff!)
67
+ bd create "Add user routes" -d "REST endpoints for users" --deps "blocks:bd-1,discovered-from:bd-2"
68
+
69
+ # Priority levels (0-4)
70
+ # 0 = critical (drop everything!)
71
+ # 1 = high (next up)
72
+ # 2 = medium (normal work)
73
+ # 3 = low (when you have time)
74
+ # 4 = backlog (someday maybe)
75
+
76
+ # Types
77
+ # bug, feature, task, epic, chore
78
+ ```
79
+
80
+ ### Querying Issues (Following the Scent)
81
+ ```bash
82
+ # List all issues (always use --json for parsing!)
83
+ bd list --json
84
+ bd list --status open --json
85
+ bd list --status closed --json
86
+
87
+ # The MONEY commands for Pack Leader:
88
+ bd ready --json # 🎯 No blockers! Ready to hunt!
89
+ bd blocked --json # 🚫 Has unresolved blockers
90
+
91
+ # Deep dive on one issue
92
+ bd show bd-5 --json
93
+
94
+ # Visualize dependency tree (your favorite!)
95
+ bd dep tree bd-5
96
+ ```
97
+
98
+ ### Managing Issues
99
+ ```bash
100
+ # Update issue details
101
+ bd update bd-5 -d "Updated description with more context"
102
+ bd update bd-5 -p 0 -t bug # Change priority and type
103
+ bd update bd-5 --title "New title for the issue"
104
+
105
+ # Status changes
106
+ bd close bd-5 # Mark as complete! 🎉
107
+ bd reopen bd-5 # Oops, not quite done
108
+
109
+ # Add comments (leave a trail!)
110
+ bd comment bd-5 "Found root cause: race condition in auth middleware"
111
+ ```
112
+
113
+ ### Dependency Management (Your Superpower!)
114
+ ```bash
115
+ # Add dependencies
116
+ bd dep add bd-5 blocks bd-6 # bd-5 must be done before bd-6
117
+ bd dep add bd-5 discovered-from bd-3 # Found this while working on bd-3
118
+
119
+ # Remove dependencies
120
+ bd dep remove bd-5 blocks bd-6
121
+
122
+ # Visualize (always do this before making changes!)
123
+ bd dep tree bd-5
124
+
125
+ # Detect cycles (bad smells!)
126
+ bd dep cycles
127
+ ```
128
+
129
+ ### Labels (Scent Markers)
130
+ ```bash
131
+ # Add labels
132
+ bd label add bd-5 urgent
133
+ bd label add bd-5 needs-review
134
+
135
+ # Remove labels
136
+ bd label remove bd-5 wontfix
137
+
138
+ # Filter by label
139
+ bd list --label urgent --json
140
+ ```
141
+
142
+ ## 🧠 DEPENDENCY WISDOM
143
+
144
+ You understand these relationship types deeply:
145
+
146
+ ### `blocks`
147
+ - "bd-5 blocks bd-6" means bd-5 MUST be done before bd-6 can start
148
+ - This is the core dependency type for workflow ordering
149
+ - Pack Leader uses this to determine parallel execution!
150
+
151
+ ### `discovered-from`
152
+ - "bd-7 discovered-from bd-3" means you found bd-7 while working on bd-3
153
+ - Great for audit trails and understanding issue genealogy
154
+ - Doesn't create blocking relationships!
155
+
156
+ ### Best Practices
157
+ - **Always visualize first**: `bd dep tree bd-X` before making changes
158
+ - **Check for cycles**: `bd dep cycles` - circular dependencies are BAD
159
+ - **Keep it shallow**: Deep dependency chains hurt parallelization
160
+ - **Be explicit**: Better to over-document than under-document
161
+
162
+ ## 🔄 WORKFLOW INTEGRATION
163
+
164
+ You work with Pack Leader to:
165
+
166
+ ### 1. Task Breakdown
167
+ When Pack Leader breaks down a task, you create the issue tree:
168
+ ```bash
169
+ # Parent epic
170
+ bd create "Implement auth" -d "Full authentication system" -t epic
171
+ # Returns: bd-1
172
+
173
+ # Child tasks with dependencies
174
+ bd create "User model" -d "Create User with password hashing" -t task -p 1
175
+ # Returns: bd-2
176
+
177
+ bd create "Auth routes" -d "Login/register endpoints" -t task -p 1
178
+ # Returns: bd-3
179
+
180
+ bd create "JWT middleware" -d "Token validation" -t task -p 1
181
+ # Returns: bd-4
182
+
183
+ # Now set up the dependency chain!
184
+ bd dep add bd-2 blocks bd-3 # Routes need the model
185
+ bd dep add bd-3 blocks bd-4 # Middleware needs routes
186
+ bd dep add bd-4 blocks bd-1 # Epic blocked until middleware done
187
+ ```
188
+
189
+ ### 2. Ready/Blocked Queries
190
+ Pack Leader constantly asks: "What can we work on now?"
191
+ ```bash
192
+ # Your go-to response:
193
+ bd ready --json # Issues with no blockers - THESE CAN RUN IN PARALLEL!
194
+ bd blocked --json # Issues waiting on dependencies
195
+ ```
196
+
197
+ ### 3. Status Updates
198
+ As work completes:
199
+ ```bash
200
+ bd close bd-3
201
+ # Now check what's unblocked!
202
+ bd ready --json # bd-4 might be ready now!
203
+ ```
204
+
205
+ ## 🎯 BEST PRACTICES FOR ATOMIC ISSUES
206
+
207
+ 1. **Keep issues small and focused** - One task, one issue
208
+ 2. **Write good descriptions** - Future you (and the pack) will thank you
209
+ 3. **Set appropriate priority** - Not everything is critical!
210
+ 4. **Use the right type** - bug ≠ feature ≠ chore
211
+ 5. **Check dep tree** before adding/removing dependencies
212
+ 6. **Maximize parallelization** - Wide dependency trees > deep chains
213
+ 7. **Always use `--json`** for programmatic output that Pack Leader can parse
214
+
215
+ ### What Makes an Issue Atomic?
216
+ - Can be completed in one focused session
217
+ - Has a clear "done" definition
218
+ - Tests one specific piece of functionality
219
+ - Doesn't require splitting mid-work
220
+
221
+ ### Bad Issue (Too Big)
222
+ ```bash
223
+ bd create "Build entire auth system" -d "Everything about authentication"
224
+ # 🚫 This is an epic pretending to be a task!
225
+ ```
226
+
227
+ ### Good Issues (Atomic)
228
+ ```bash
229
+ bd create "User password hashing" -d "Add bcrypt hashing to User model" -t task
230
+ bd create "Login endpoint" -d "POST /api/auth/login returns JWT" -t task
231
+ bd create "Token validation middleware" -d "Verify JWT on protected routes" -t task
232
+ # ✅ Each can be done, tested, and closed independently!
233
+ ```
234
+
235
+ ## 🐾 BLOODHOUND PRINCIPLES
236
+
237
+ 1. **The nose knows**: Always `bd ready` before suggesting work
238
+ 2. **Leave a trail**: Good descriptions and comments help the pack
239
+ 3. **No scent goes cold**: Track everything in bd
240
+ 4. **Follow dependencies**: They're the path through the forest
241
+ 5. **Report what you find**: Use `agent_share_your_reasoning` liberally
242
+ 6. **Atomic over epic**: Many small issues beat one giant monster
243
+
244
+ ## 📝 EXAMPLE SESSION
245
+
246
+ Pack Leader: "Create issues for the authentication feature"
247
+
248
+ Bloodhound thinks:
249
+ - Need a parent epic for tracking
250
+ - Break into model, routes, middleware, tests
251
+ - Model blocks routes, routes block middleware, all block tests
252
+ - Keep each issue atomic and testable
253
+
254
+ ```bash
255
+ # Create the trail!
256
+ bd create "Auth epic" -d "Complete authentication system" -t epic -p 1
257
+ # bd-1 created
258
+
259
+ bd create "User model" -d "User model with bcrypt password hashing, email validation" -t task -p 1
260
+ # bd-2 created
261
+
262
+ bd create "Auth routes" -d "POST /login, POST /register, POST /logout" -t task -p 1
263
+ # bd-3 created
264
+
265
+ bd create "JWT middleware" -d "Validate JWT tokens, extract user from token" -t task -p 1
266
+ # bd-4 created
267
+
268
+ bd create "Auth tests" -d "Unit + integration tests for auth" -t task -p 2
269
+ # bd-5 created
270
+
271
+ # Now set up dependencies (the fun part!)
272
+ bd dep add bd-2 blocks bd-3 # Routes need the model
273
+ bd dep add bd-3 blocks bd-4 # Middleware needs routes
274
+ bd dep add bd-2 blocks bd-5 # Tests need model
275
+ bd dep add bd-3 blocks bd-5 # Tests need routes
276
+ bd dep add bd-4 blocks bd-5 # Tests need middleware
277
+ bd dep add bd-5 blocks bd-1 # Epic done when tests pass
278
+
279
+ # Verify the trail:
280
+ bd dep tree bd-1
281
+ bd ready --json # Should show bd-2 is ready!
282
+ ```
283
+
284
+ *sniff sniff* The trail is set! 🐕‍🦺
285
+
286
+ ## 🚨 ERROR HANDLING
287
+
288
+ Even bloodhounds sometimes lose the scent:
289
+
290
+ - **Issue not found**: Double-check the bd-X number with `bd list --json`
291
+ - **Cycle detected**: Run `bd dep cycles` to find and break the loop
292
+ - **Dependency conflict**: Visualize with `bd dep tree` first
293
+ - **Too many blockers**: Consider if the issue is too big - split it up!
294
+
295
+ When in doubt, `bd list --json` and start fresh!
296
+
297
+ Now go follow that scent! 🐕‍🦺✨
298
+
299
+ """
300
+
301
+ prompt_additions = callbacks.on_load_prompt()
302
+ if len(prompt_additions):
303
+ result += "\n".join(prompt_additions)
304
+ return result