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.
- code_puppy/__init__.py +7 -1
- code_puppy/agents/__init__.py +2 -0
- code_puppy/agents/agent_c_reviewer.py +59 -6
- code_puppy/agents/agent_code_puppy.py +7 -1
- code_puppy/agents/agent_code_reviewer.py +12 -2
- code_puppy/agents/agent_cpp_reviewer.py +73 -6
- code_puppy/agents/agent_creator_agent.py +45 -4
- code_puppy/agents/agent_golang_reviewer.py +92 -3
- code_puppy/agents/agent_javascript_reviewer.py +101 -8
- code_puppy/agents/agent_manager.py +81 -4
- code_puppy/agents/agent_pack_leader.py +383 -0
- code_puppy/agents/agent_planning.py +163 -0
- code_puppy/agents/agent_python_programmer.py +165 -0
- code_puppy/agents/agent_python_reviewer.py +28 -6
- code_puppy/agents/agent_qa_expert.py +98 -6
- code_puppy/agents/agent_qa_kitten.py +12 -7
- code_puppy/agents/agent_security_auditor.py +113 -3
- code_puppy/agents/agent_terminal_qa.py +323 -0
- code_puppy/agents/agent_typescript_reviewer.py +106 -7
- code_puppy/agents/base_agent.py +802 -176
- code_puppy/agents/event_stream_handler.py +350 -0
- code_puppy/agents/pack/__init__.py +34 -0
- code_puppy/agents/pack/bloodhound.py +304 -0
- code_puppy/agents/pack/husky.py +321 -0
- code_puppy/agents/pack/retriever.py +393 -0
- code_puppy/agents/pack/shepherd.py +348 -0
- code_puppy/agents/pack/terrier.py +287 -0
- code_puppy/agents/pack/watchdog.py +367 -0
- code_puppy/agents/prompt_reviewer.py +145 -0
- code_puppy/agents/subagent_stream_handler.py +276 -0
- code_puppy/api/__init__.py +13 -0
- code_puppy/api/app.py +169 -0
- code_puppy/api/main.py +21 -0
- code_puppy/api/pty_manager.py +446 -0
- code_puppy/api/routers/__init__.py +12 -0
- code_puppy/api/routers/agents.py +36 -0
- code_puppy/api/routers/commands.py +217 -0
- code_puppy/api/routers/config.py +74 -0
- code_puppy/api/routers/sessions.py +232 -0
- code_puppy/api/templates/terminal.html +361 -0
- code_puppy/api/websocket.py +154 -0
- code_puppy/callbacks.py +142 -4
- code_puppy/chatgpt_codex_client.py +283 -0
- code_puppy/claude_cache_client.py +586 -0
- code_puppy/cli_runner.py +916 -0
- code_puppy/command_line/add_model_menu.py +1079 -0
- code_puppy/command_line/agent_menu.py +395 -0
- code_puppy/command_line/attachments.py +10 -5
- code_puppy/command_line/autosave_menu.py +605 -0
- code_puppy/command_line/clipboard.py +527 -0
- code_puppy/command_line/colors_menu.py +520 -0
- code_puppy/command_line/command_handler.py +176 -738
- code_puppy/command_line/command_registry.py +150 -0
- code_puppy/command_line/config_commands.py +715 -0
- code_puppy/command_line/core_commands.py +792 -0
- code_puppy/command_line/diff_menu.py +863 -0
- code_puppy/command_line/load_context_completion.py +15 -22
- code_puppy/command_line/mcp/base.py +0 -3
- code_puppy/command_line/mcp/catalog_server_installer.py +175 -0
- code_puppy/command_line/mcp/custom_server_form.py +688 -0
- code_puppy/command_line/mcp/custom_server_installer.py +195 -0
- code_puppy/command_line/mcp/edit_command.py +148 -0
- code_puppy/command_line/mcp/handler.py +9 -4
- code_puppy/command_line/mcp/help_command.py +6 -5
- code_puppy/command_line/mcp/install_command.py +15 -26
- code_puppy/command_line/mcp/install_menu.py +685 -0
- code_puppy/command_line/mcp/list_command.py +2 -2
- code_puppy/command_line/mcp/logs_command.py +174 -65
- code_puppy/command_line/mcp/remove_command.py +2 -2
- code_puppy/command_line/mcp/restart_command.py +12 -4
- code_puppy/command_line/mcp/search_command.py +16 -10
- code_puppy/command_line/mcp/start_all_command.py +18 -6
- code_puppy/command_line/mcp/start_command.py +47 -25
- code_puppy/command_line/mcp/status_command.py +4 -5
- code_puppy/command_line/mcp/stop_all_command.py +7 -1
- code_puppy/command_line/mcp/stop_command.py +8 -4
- code_puppy/command_line/mcp/test_command.py +2 -2
- code_puppy/command_line/mcp/wizard_utils.py +20 -16
- code_puppy/command_line/mcp_completion.py +174 -0
- code_puppy/command_line/model_picker_completion.py +75 -25
- code_puppy/command_line/model_settings_menu.py +884 -0
- code_puppy/command_line/motd.py +14 -8
- code_puppy/command_line/onboarding_slides.py +179 -0
- code_puppy/command_line/onboarding_wizard.py +340 -0
- code_puppy/command_line/pin_command_completion.py +329 -0
- code_puppy/command_line/prompt_toolkit_completion.py +463 -63
- code_puppy/command_line/session_commands.py +296 -0
- code_puppy/command_line/utils.py +54 -0
- code_puppy/config.py +898 -112
- code_puppy/error_logging.py +118 -0
- code_puppy/gemini_code_assist.py +385 -0
- code_puppy/gemini_model.py +602 -0
- code_puppy/http_utils.py +210 -148
- code_puppy/keymap.py +128 -0
- code_puppy/main.py +5 -698
- code_puppy/mcp_/__init__.py +17 -0
- code_puppy/mcp_/async_lifecycle.py +35 -4
- code_puppy/mcp_/blocking_startup.py +70 -43
- code_puppy/mcp_/captured_stdio_server.py +2 -2
- code_puppy/mcp_/config_wizard.py +4 -4
- code_puppy/mcp_/dashboard.py +15 -6
- code_puppy/mcp_/managed_server.py +65 -38
- code_puppy/mcp_/manager.py +146 -52
- code_puppy/mcp_/mcp_logs.py +224 -0
- code_puppy/mcp_/registry.py +6 -6
- code_puppy/mcp_/server_registry_catalog.py +24 -5
- code_puppy/messaging/__init__.py +199 -2
- code_puppy/messaging/bus.py +610 -0
- code_puppy/messaging/commands.py +167 -0
- code_puppy/messaging/markdown_patches.py +57 -0
- code_puppy/messaging/message_queue.py +17 -48
- code_puppy/messaging/messages.py +500 -0
- code_puppy/messaging/queue_console.py +1 -24
- code_puppy/messaging/renderers.py +43 -146
- code_puppy/messaging/rich_renderer.py +1027 -0
- code_puppy/messaging/spinner/__init__.py +21 -5
- code_puppy/messaging/spinner/console_spinner.py +86 -51
- code_puppy/messaging/subagent_console.py +461 -0
- code_puppy/model_factory.py +634 -83
- code_puppy/model_utils.py +167 -0
- code_puppy/models.json +66 -68
- code_puppy/models_dev_api.json +1 -0
- code_puppy/models_dev_parser.py +592 -0
- code_puppy/plugins/__init__.py +164 -10
- code_puppy/plugins/antigravity_oauth/__init__.py +10 -0
- code_puppy/plugins/antigravity_oauth/accounts.py +406 -0
- code_puppy/plugins/antigravity_oauth/antigravity_model.py +704 -0
- code_puppy/plugins/antigravity_oauth/config.py +42 -0
- code_puppy/plugins/antigravity_oauth/constants.py +136 -0
- code_puppy/plugins/antigravity_oauth/oauth.py +478 -0
- code_puppy/plugins/antigravity_oauth/register_callbacks.py +406 -0
- code_puppy/plugins/antigravity_oauth/storage.py +271 -0
- code_puppy/plugins/antigravity_oauth/test_plugin.py +319 -0
- code_puppy/plugins/antigravity_oauth/token.py +167 -0
- code_puppy/plugins/antigravity_oauth/transport.py +767 -0
- code_puppy/plugins/antigravity_oauth/utils.py +169 -0
- code_puppy/plugins/chatgpt_oauth/__init__.py +8 -0
- code_puppy/plugins/chatgpt_oauth/config.py +52 -0
- code_puppy/plugins/chatgpt_oauth/oauth_flow.py +328 -0
- code_puppy/plugins/chatgpt_oauth/register_callbacks.py +94 -0
- code_puppy/plugins/chatgpt_oauth/test_plugin.py +293 -0
- code_puppy/plugins/chatgpt_oauth/utils.py +489 -0
- code_puppy/plugins/claude_code_oauth/README.md +167 -0
- code_puppy/plugins/claude_code_oauth/SETUP.md +93 -0
- code_puppy/plugins/claude_code_oauth/__init__.py +6 -0
- code_puppy/plugins/claude_code_oauth/config.py +50 -0
- code_puppy/plugins/claude_code_oauth/register_callbacks.py +308 -0
- code_puppy/plugins/claude_code_oauth/test_plugin.py +283 -0
- code_puppy/plugins/claude_code_oauth/utils.py +518 -0
- code_puppy/plugins/customizable_commands/__init__.py +0 -0
- code_puppy/plugins/customizable_commands/register_callbacks.py +169 -0
- code_puppy/plugins/example_custom_command/README.md +280 -0
- code_puppy/plugins/example_custom_command/register_callbacks.py +2 -2
- code_puppy/plugins/file_permission_handler/__init__.py +4 -0
- code_puppy/plugins/file_permission_handler/register_callbacks.py +523 -0
- code_puppy/plugins/frontend_emitter/__init__.py +25 -0
- code_puppy/plugins/frontend_emitter/emitter.py +121 -0
- code_puppy/plugins/frontend_emitter/register_callbacks.py +261 -0
- code_puppy/plugins/oauth_puppy_html.py +228 -0
- code_puppy/plugins/shell_safety/__init__.py +6 -0
- code_puppy/plugins/shell_safety/agent_shell_safety.py +69 -0
- code_puppy/plugins/shell_safety/command_cache.py +156 -0
- code_puppy/plugins/shell_safety/register_callbacks.py +202 -0
- code_puppy/prompts/antigravity_system_prompt.md +1 -0
- code_puppy/prompts/codex_system_prompt.md +310 -0
- code_puppy/pydantic_patches.py +131 -0
- code_puppy/reopenable_async_client.py +8 -8
- code_puppy/round_robin_model.py +9 -12
- code_puppy/session_storage.py +2 -1
- code_puppy/status_display.py +21 -4
- code_puppy/summarization_agent.py +41 -13
- code_puppy/terminal_utils.py +418 -0
- code_puppy/tools/__init__.py +37 -1
- code_puppy/tools/agent_tools.py +536 -52
- code_puppy/tools/browser/__init__.py +37 -0
- code_puppy/tools/browser/browser_control.py +19 -23
- code_puppy/tools/browser/browser_interactions.py +41 -48
- code_puppy/tools/browser/browser_locators.py +36 -38
- code_puppy/tools/browser/browser_manager.py +316 -0
- code_puppy/tools/browser/browser_navigation.py +16 -16
- code_puppy/tools/browser/browser_screenshot.py +79 -143
- code_puppy/tools/browser/browser_scripts.py +32 -42
- code_puppy/tools/browser/browser_workflows.py +44 -27
- code_puppy/tools/browser/chromium_terminal_manager.py +259 -0
- code_puppy/tools/browser/terminal_command_tools.py +521 -0
- code_puppy/tools/browser/terminal_screenshot_tools.py +556 -0
- code_puppy/tools/browser/terminal_tools.py +525 -0
- code_puppy/tools/command_runner.py +930 -147
- code_puppy/tools/common.py +1113 -5
- code_puppy/tools/display.py +84 -0
- code_puppy/tools/file_modifications.py +288 -89
- code_puppy/tools/file_operations.py +226 -154
- code_puppy/tools/subagent_context.py +158 -0
- code_puppy/uvx_detection.py +242 -0
- code_puppy/version_checker.py +30 -11
- code_puppy-0.0.366.data/data/code_puppy/models.json +110 -0
- code_puppy-0.0.366.data/data/code_puppy/models_dev_api.json +1 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/METADATA +149 -75
- code_puppy-0.0.366.dist-info/RECORD +217 -0
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/WHEEL +1 -1
- code_puppy/command_line/mcp/add_command.py +0 -183
- code_puppy/messaging/spinner/textual_spinner.py +0 -106
- code_puppy/tools/browser/camoufox_manager.py +0 -216
- code_puppy/tools/browser/vqa_agent.py +0 -70
- code_puppy/tui/__init__.py +0 -10
- code_puppy/tui/app.py +0 -1105
- code_puppy/tui/components/__init__.py +0 -21
- code_puppy/tui/components/chat_view.py +0 -551
- code_puppy/tui/components/command_history_modal.py +0 -218
- code_puppy/tui/components/copy_button.py +0 -139
- code_puppy/tui/components/custom_widgets.py +0 -63
- code_puppy/tui/components/human_input_modal.py +0 -175
- code_puppy/tui/components/input_area.py +0 -167
- code_puppy/tui/components/sidebar.py +0 -309
- code_puppy/tui/components/status_bar.py +0 -185
- code_puppy/tui/messages.py +0 -27
- code_puppy/tui/models/__init__.py +0 -8
- code_puppy/tui/models/chat_message.py +0 -25
- code_puppy/tui/models/command_history.py +0 -89
- code_puppy/tui/models/enums.py +0 -24
- code_puppy/tui/screens/__init__.py +0 -17
- code_puppy/tui/screens/autosave_picker.py +0 -175
- code_puppy/tui/screens/help.py +0 -130
- code_puppy/tui/screens/mcp_install_wizard.py +0 -803
- code_puppy/tui/screens/settings.py +0 -306
- code_puppy/tui/screens/tools.py +0 -74
- code_puppy/tui_state.py +0 -55
- code_puppy-0.0.214.data/data/code_puppy/models.json +0 -112
- code_puppy-0.0.214.dist-info/RECORD +0 -131
- {code_puppy-0.0.214.dist-info → code_puppy-0.0.366.dist-info}/entry_points.txt +0 -0
- {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
|