shotgun-sh 0.2.6.dev5__py3-none-any.whl → 0.2.7.dev1__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.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- shotgun/agents/agent_manager.py +81 -1
- shotgun/agents/conversation_history.py +123 -2
- shotgun/agents/conversation_manager.py +24 -2
- shotgun/agents/history/context_extraction.py +93 -6
- shotgun/tui/screens/chat.py +54 -13
- shotgun_sh-0.2.7.dev1.dist-info/METADATA +125 -0
- {shotgun_sh-0.2.6.dev5.dist-info → shotgun_sh-0.2.7.dev1.dist-info}/RECORD +10 -10
- shotgun_sh-0.2.6.dev5.dist-info/METADATA +0 -467
- {shotgun_sh-0.2.6.dev5.dist-info → shotgun_sh-0.2.7.dev1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.6.dev5.dist-info → shotgun_sh-0.2.7.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.6.dev5.dist-info → shotgun_sh-0.2.7.dev1.dist-info}/licenses/LICENSE +0 -0
shotgun/agents/agent_manager.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Agent manager for coordinating multiple AI agents with shared message history."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import logging
|
|
4
5
|
from collections.abc import AsyncIterable, Sequence
|
|
5
6
|
from dataclasses import dataclass, field, is_dataclass, replace
|
|
6
7
|
from typing import TYPE_CHECKING, Any, cast
|
|
7
8
|
|
|
9
|
+
import logfire
|
|
10
|
+
|
|
8
11
|
if TYPE_CHECKING:
|
|
9
12
|
from shotgun.agents.conversation_history import ConversationState
|
|
10
13
|
|
|
@@ -401,6 +404,24 @@ class AgentManager(Widget):
|
|
|
401
404
|
else None,
|
|
402
405
|
**kwargs,
|
|
403
406
|
)
|
|
407
|
+
except Exception as e:
|
|
408
|
+
# Log the error with full stack trace to shotgun.log and Logfire
|
|
409
|
+
logger.exception(
|
|
410
|
+
"Agent execution failed",
|
|
411
|
+
extra={
|
|
412
|
+
"agent_mode": self._current_agent_type.value,
|
|
413
|
+
"model_name": model_name,
|
|
414
|
+
"error_type": type(e).__name__,
|
|
415
|
+
},
|
|
416
|
+
)
|
|
417
|
+
logfire.exception(
|
|
418
|
+
"Agent execution failed",
|
|
419
|
+
agent_mode=self._current_agent_type.value,
|
|
420
|
+
model_name=model_name,
|
|
421
|
+
error_type=type(e).__name__,
|
|
422
|
+
)
|
|
423
|
+
# Re-raise to let TUI handle user messaging
|
|
424
|
+
raise
|
|
404
425
|
finally:
|
|
405
426
|
self._stream_state = None
|
|
406
427
|
|
|
@@ -468,7 +489,33 @@ class AgentManager(Widget):
|
|
|
468
489
|
|
|
469
490
|
# Apply compaction to persistent message history to prevent cascading growth
|
|
470
491
|
all_messages = result.all_messages()
|
|
471
|
-
|
|
492
|
+
try:
|
|
493
|
+
logger.debug(
|
|
494
|
+
"Starting message history compaction",
|
|
495
|
+
extra={"message_count": len(all_messages)},
|
|
496
|
+
)
|
|
497
|
+
self.message_history = await apply_persistent_compaction(all_messages, deps)
|
|
498
|
+
logger.debug(
|
|
499
|
+
"Completed message history compaction",
|
|
500
|
+
extra={
|
|
501
|
+
"original_count": len(all_messages),
|
|
502
|
+
"compacted_count": len(self.message_history),
|
|
503
|
+
},
|
|
504
|
+
)
|
|
505
|
+
except Exception as e:
|
|
506
|
+
# If compaction fails, log full error with stack trace and use uncompacted messages
|
|
507
|
+
logger.error(
|
|
508
|
+
"Failed to compact message history - using uncompacted messages",
|
|
509
|
+
exc_info=True,
|
|
510
|
+
extra={
|
|
511
|
+
"error": str(e),
|
|
512
|
+
"message_count": len(all_messages),
|
|
513
|
+
"agent_mode": self._current_agent_type.value,
|
|
514
|
+
},
|
|
515
|
+
)
|
|
516
|
+
# Fallback: use uncompacted messages to prevent data loss
|
|
517
|
+
self.message_history = all_messages
|
|
518
|
+
|
|
472
519
|
usage = result.usage()
|
|
473
520
|
deps.usage_manager.add_usage(
|
|
474
521
|
usage, model_name=deps.llm_model.name, provider=deps.llm_model.provider
|
|
@@ -554,6 +601,39 @@ class AgentManager(Widget):
|
|
|
554
601
|
# Detect source from call stack
|
|
555
602
|
source = detect_source()
|
|
556
603
|
|
|
604
|
+
# Log if tool call has incomplete args (for debugging truncated JSON)
|
|
605
|
+
if isinstance(event.part.args, str):
|
|
606
|
+
try:
|
|
607
|
+
json.loads(event.part.args)
|
|
608
|
+
except (json.JSONDecodeError, ValueError):
|
|
609
|
+
args_preview = (
|
|
610
|
+
event.part.args[:100] + "..."
|
|
611
|
+
if len(event.part.args) > 100
|
|
612
|
+
else event.part.args
|
|
613
|
+
)
|
|
614
|
+
logger.warning(
|
|
615
|
+
"FunctionToolCallEvent received with incomplete JSON args",
|
|
616
|
+
extra={
|
|
617
|
+
"tool_name": event.part.tool_name,
|
|
618
|
+
"tool_call_id": event.part.tool_call_id,
|
|
619
|
+
"args_preview": args_preview,
|
|
620
|
+
"args_length": len(event.part.args)
|
|
621
|
+
if event.part.args
|
|
622
|
+
else 0,
|
|
623
|
+
"agent_mode": self._current_agent_type.value,
|
|
624
|
+
},
|
|
625
|
+
)
|
|
626
|
+
logfire.warn(
|
|
627
|
+
"FunctionToolCallEvent received with incomplete JSON args",
|
|
628
|
+
tool_name=event.part.tool_name,
|
|
629
|
+
tool_call_id=event.part.tool_call_id,
|
|
630
|
+
args_preview=args_preview,
|
|
631
|
+
args_length=len(event.part.args)
|
|
632
|
+
if event.part.args
|
|
633
|
+
else 0,
|
|
634
|
+
agent_mode=self._current_agent_type.value,
|
|
635
|
+
)
|
|
636
|
+
|
|
557
637
|
track_event(
|
|
558
638
|
"tool_called",
|
|
559
639
|
{
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"""Models and utilities for persisting TUI conversation history."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
3
5
|
from datetime import datetime
|
|
4
6
|
from typing import Any, cast
|
|
5
7
|
|
|
@@ -7,14 +9,106 @@ from pydantic import BaseModel, ConfigDict, Field
|
|
|
7
9
|
from pydantic_ai.messages import (
|
|
8
10
|
ModelMessage,
|
|
9
11
|
ModelMessagesTypeAdapter,
|
|
12
|
+
ModelResponse,
|
|
13
|
+
ToolCallPart,
|
|
10
14
|
)
|
|
11
15
|
from pydantic_core import to_jsonable_python
|
|
12
16
|
|
|
13
17
|
from shotgun.tui.screens.chat_screen.hint_message import HintMessage
|
|
14
18
|
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
15
21
|
SerializedMessage = dict[str, Any]
|
|
16
22
|
|
|
17
23
|
|
|
24
|
+
def is_tool_call_complete(tool_call: ToolCallPart) -> bool:
|
|
25
|
+
"""Check if a tool call has valid, complete JSON arguments.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
tool_call: The tool call part to validate
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
True if the tool call args are valid JSON, False otherwise
|
|
32
|
+
"""
|
|
33
|
+
if tool_call.args is None:
|
|
34
|
+
return True # No args is valid
|
|
35
|
+
|
|
36
|
+
if isinstance(tool_call.args, dict):
|
|
37
|
+
return True # Already parsed dict is valid
|
|
38
|
+
|
|
39
|
+
if not isinstance(tool_call.args, str):
|
|
40
|
+
return False
|
|
41
|
+
|
|
42
|
+
# Try to parse the JSON string
|
|
43
|
+
try:
|
|
44
|
+
json.loads(tool_call.args)
|
|
45
|
+
return True
|
|
46
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
47
|
+
# Log incomplete tool call detection
|
|
48
|
+
args_preview = (
|
|
49
|
+
tool_call.args[:100] + "..."
|
|
50
|
+
if len(tool_call.args) > 100
|
|
51
|
+
else tool_call.args
|
|
52
|
+
)
|
|
53
|
+
logger.info(
|
|
54
|
+
"Detected incomplete tool call in validation",
|
|
55
|
+
extra={
|
|
56
|
+
"tool_name": tool_call.tool_name,
|
|
57
|
+
"tool_call_id": tool_call.tool_call_id,
|
|
58
|
+
"args_preview": args_preview,
|
|
59
|
+
"error": str(e),
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def filter_incomplete_messages(messages: list[ModelMessage]) -> list[ModelMessage]:
|
|
66
|
+
"""Filter out messages with incomplete tool calls.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
messages: List of messages to filter
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
List of messages with only complete tool calls
|
|
73
|
+
"""
|
|
74
|
+
filtered: list[ModelMessage] = []
|
|
75
|
+
filtered_count = 0
|
|
76
|
+
filtered_tool_names: list[str] = []
|
|
77
|
+
|
|
78
|
+
for message in messages:
|
|
79
|
+
# Only check ModelResponse messages for tool calls
|
|
80
|
+
if not isinstance(message, ModelResponse):
|
|
81
|
+
filtered.append(message)
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
# Check if any tool calls are incomplete
|
|
85
|
+
has_incomplete_tool_call = False
|
|
86
|
+
for part in message.parts:
|
|
87
|
+
if isinstance(part, ToolCallPart) and not is_tool_call_complete(part):
|
|
88
|
+
has_incomplete_tool_call = True
|
|
89
|
+
filtered_tool_names.append(part.tool_name)
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
# Only include messages without incomplete tool calls
|
|
93
|
+
if not has_incomplete_tool_call:
|
|
94
|
+
filtered.append(message)
|
|
95
|
+
else:
|
|
96
|
+
filtered_count += 1
|
|
97
|
+
|
|
98
|
+
# Log if any messages were filtered
|
|
99
|
+
if filtered_count > 0:
|
|
100
|
+
logger.info(
|
|
101
|
+
"Filtered incomplete messages before saving",
|
|
102
|
+
extra={
|
|
103
|
+
"filtered_count": filtered_count,
|
|
104
|
+
"total_messages": len(messages),
|
|
105
|
+
"filtered_tool_names": filtered_tool_names,
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return filtered
|
|
110
|
+
|
|
111
|
+
|
|
18
112
|
class ConversationState(BaseModel):
|
|
19
113
|
"""Represents the complete state of a conversation in memory."""
|
|
20
114
|
|
|
@@ -46,14 +140,41 @@ class ConversationHistory(BaseModel):
|
|
|
46
140
|
Args:
|
|
47
141
|
messages: List of ModelMessage objects to serialize and store
|
|
48
142
|
"""
|
|
143
|
+
# Filter out messages with incomplete tool calls to prevent corruption
|
|
144
|
+
filtered_messages = filter_incomplete_messages(messages)
|
|
145
|
+
|
|
49
146
|
# Serialize ModelMessage list to JSON-serializable format
|
|
50
147
|
self.agent_history = to_jsonable_python(
|
|
51
|
-
|
|
148
|
+
filtered_messages, fallback=lambda x: str(x), exclude_none=True
|
|
52
149
|
)
|
|
53
150
|
|
|
54
151
|
def set_ui_messages(self, messages: list[ModelMessage | HintMessage]) -> None:
|
|
55
152
|
"""Set ui_history from a list of UI messages."""
|
|
56
153
|
|
|
154
|
+
# Filter out ModelMessages with incomplete tool calls (keep all HintMessages)
|
|
155
|
+
# We need to maintain message order, so we'll check each message individually
|
|
156
|
+
filtered_messages: list[ModelMessage | HintMessage] = []
|
|
157
|
+
|
|
158
|
+
for msg in messages:
|
|
159
|
+
if isinstance(msg, HintMessage):
|
|
160
|
+
# Always keep hint messages
|
|
161
|
+
filtered_messages.append(msg)
|
|
162
|
+
elif isinstance(msg, ModelResponse):
|
|
163
|
+
# Check if this ModelResponse has incomplete tool calls
|
|
164
|
+
has_incomplete = False
|
|
165
|
+
for part in msg.parts:
|
|
166
|
+
if isinstance(part, ToolCallPart) and not is_tool_call_complete(
|
|
167
|
+
part
|
|
168
|
+
):
|
|
169
|
+
has_incomplete = True
|
|
170
|
+
break
|
|
171
|
+
|
|
172
|
+
if not has_incomplete:
|
|
173
|
+
filtered_messages.append(msg)
|
|
174
|
+
else:
|
|
175
|
+
# Keep all other ModelMessage types (ModelRequest, etc.)
|
|
176
|
+
filtered_messages.append(msg)
|
|
177
|
+
|
|
57
178
|
def _serialize_message(
|
|
58
179
|
message: ModelMessage | HintMessage,
|
|
59
180
|
) -> Any:
|
|
@@ -68,7 +189,7 @@ class ConversationHistory(BaseModel):
|
|
|
68
189
|
payload.setdefault("message_type", "model")
|
|
69
190
|
return payload
|
|
70
191
|
|
|
71
|
-
self.ui_history = [_serialize_message(msg) for msg in
|
|
192
|
+
self.ui_history = [_serialize_message(msg) for msg in filtered_messages]
|
|
72
193
|
|
|
73
194
|
def get_agent_messages(self) -> list[ModelMessage]:
|
|
74
195
|
"""Get agent_history as a list of ModelMessage objects.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Manager for handling conversation persistence operations."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import shutil
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
7
|
from shotgun.logging_config import get_logger
|
|
@@ -77,9 +78,30 @@ class ConversationManager:
|
|
|
77
78
|
)
|
|
78
79
|
return conversation
|
|
79
80
|
|
|
80
|
-
except
|
|
81
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
82
|
+
# Handle corrupted JSON or validation errors
|
|
83
|
+
logger.error(
|
|
84
|
+
"Corrupted conversation file at %s: %s. Creating backup and starting fresh.",
|
|
85
|
+
self.conversation_path,
|
|
86
|
+
e,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Create a backup of the corrupted file for debugging
|
|
90
|
+
backup_path = self.conversation_path.with_suffix(".json.backup")
|
|
91
|
+
try:
|
|
92
|
+
shutil.copy2(self.conversation_path, backup_path)
|
|
93
|
+
logger.info("Backed up corrupted conversation to %s", backup_path)
|
|
94
|
+
except Exception as backup_error: # pragma: no cover
|
|
95
|
+
logger.warning("Failed to backup corrupted file: %s", backup_error)
|
|
96
|
+
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
except Exception as e: # pragma: no cover
|
|
100
|
+
# Catch-all for unexpected errors
|
|
81
101
|
logger.error(
|
|
82
|
-
"
|
|
102
|
+
"Unexpected error loading conversation from %s: %s",
|
|
103
|
+
self.conversation_path,
|
|
104
|
+
e,
|
|
83
105
|
)
|
|
84
106
|
return None
|
|
85
107
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
"""Context extraction utilities for history processing."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import traceback
|
|
6
|
+
|
|
3
7
|
from pydantic_ai.messages import (
|
|
4
8
|
BuiltinToolCallPart,
|
|
5
9
|
BuiltinToolReturnPart,
|
|
@@ -16,6 +20,46 @@ from pydantic_ai.messages import (
|
|
|
16
20
|
UserPromptPart,
|
|
17
21
|
)
|
|
18
22
|
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _safely_parse_tool_args(args: dict[str, object] | str | None) -> dict[str, object]:
|
|
27
|
+
"""Safely parse tool call arguments, handling incomplete/invalid JSON.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
args: Tool call arguments (dict, JSON string, or None)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Parsed args dict, or empty dict if parsing fails
|
|
34
|
+
"""
|
|
35
|
+
if args is None:
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
if isinstance(args, dict):
|
|
39
|
+
return args
|
|
40
|
+
|
|
41
|
+
if not isinstance(args, str):
|
|
42
|
+
return {}
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
parsed = json.loads(args)
|
|
46
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
47
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
48
|
+
# Only log warning if it looks like JSON (starts with { or [) - incomplete JSON
|
|
49
|
+
# Plain strings are valid args and shouldn't trigger warnings
|
|
50
|
+
stripped_args = args.strip()
|
|
51
|
+
if stripped_args.startswith(("{", "[")):
|
|
52
|
+
args_preview = args[:100] + "..." if len(args) > 100 else args
|
|
53
|
+
logger.warning(
|
|
54
|
+
"Detected incomplete/invalid JSON in tool call args during parsing",
|
|
55
|
+
extra={
|
|
56
|
+
"args_preview": args_preview,
|
|
57
|
+
"error": str(e),
|
|
58
|
+
"args_length": len(args),
|
|
59
|
+
},
|
|
60
|
+
)
|
|
61
|
+
return {}
|
|
62
|
+
|
|
19
63
|
|
|
20
64
|
def extract_context_from_messages(messages: list[ModelMessage]) -> str:
|
|
21
65
|
"""Extract context from a list of messages for summarization."""
|
|
@@ -87,12 +131,55 @@ def extract_context_from_part(
|
|
|
87
131
|
return f"<ASSISTANT_TEXT>\n{message_part.content}\n</ASSISTANT_TEXT>"
|
|
88
132
|
|
|
89
133
|
elif isinstance(message_part, ToolCallPart):
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
134
|
+
# Safely parse args to avoid crashes from incomplete JSON during streaming
|
|
135
|
+
try:
|
|
136
|
+
parsed_args = _safely_parse_tool_args(message_part.args)
|
|
137
|
+
if parsed_args:
|
|
138
|
+
# Successfully parsed as dict - format nicely
|
|
139
|
+
args_str = ", ".join(f"{k}={repr(v)}" for k, v in parsed_args.items())
|
|
140
|
+
tool_call_str = f"{message_part.tool_name}({args_str})"
|
|
141
|
+
elif isinstance(message_part.args, str) and message_part.args:
|
|
142
|
+
# Non-empty string that didn't parse as JSON
|
|
143
|
+
# Check if it looks like JSON (starts with { or [) - if so, it's incomplete
|
|
144
|
+
stripped_args = message_part.args.strip()
|
|
145
|
+
if stripped_args.startswith(("{", "[")):
|
|
146
|
+
# Looks like incomplete JSON - log warning and show empty parens
|
|
147
|
+
args_preview = (
|
|
148
|
+
stripped_args[:100] + "..."
|
|
149
|
+
if len(stripped_args) > 100
|
|
150
|
+
else stripped_args
|
|
151
|
+
)
|
|
152
|
+
stack_trace = "".join(traceback.format_stack())
|
|
153
|
+
logger.warning(
|
|
154
|
+
"ToolCallPart with unparseable args encountered during context extraction",
|
|
155
|
+
extra={
|
|
156
|
+
"tool_name": message_part.tool_name,
|
|
157
|
+
"tool_call_id": message_part.tool_call_id,
|
|
158
|
+
"args_preview": args_preview,
|
|
159
|
+
"args_type": type(message_part.args).__name__,
|
|
160
|
+
"stack_trace": stack_trace,
|
|
161
|
+
},
|
|
162
|
+
)
|
|
163
|
+
tool_call_str = f"{message_part.tool_name}()"
|
|
164
|
+
else:
|
|
165
|
+
# Plain string arg - display as-is
|
|
166
|
+
tool_call_str = f"{message_part.tool_name}({message_part.args})"
|
|
167
|
+
else:
|
|
168
|
+
# No args
|
|
169
|
+
tool_call_str = f"{message_part.tool_name}()"
|
|
170
|
+
return f"<TOOL_CALL>\n{tool_call_str}\n</TOOL_CALL>"
|
|
171
|
+
except Exception as e: # pragma: no cover - defensive catch-all
|
|
172
|
+
# If anything goes wrong, log full exception with stack trace
|
|
173
|
+
logger.error(
|
|
174
|
+
"Unexpected error processing ToolCallPart",
|
|
175
|
+
exc_info=True,
|
|
176
|
+
extra={
|
|
177
|
+
"tool_name": message_part.tool_name,
|
|
178
|
+
"tool_call_id": message_part.tool_call_id,
|
|
179
|
+
"error": str(e),
|
|
180
|
+
},
|
|
181
|
+
)
|
|
182
|
+
return f"<TOOL_CALL>\n{message_part.tool_name}()\n</TOOL_CALL>"
|
|
96
183
|
|
|
97
184
|
elif isinstance(message_part, BuiltinToolCallPart):
|
|
98
185
|
return f"<BUILTIN_TOOL_CALL>\n{message_part.tool_name}\n</BUILTIN_TOOL_CALL>"
|
shotgun/tui/screens/chat.py
CHANGED
|
@@ -881,6 +881,30 @@ class ChatScreen(Screen[None]):
|
|
|
881
881
|
except asyncio.CancelledError:
|
|
882
882
|
# Handle cancellation gracefully - DO NOT re-raise
|
|
883
883
|
self.mount_hint("⚠️ Operation cancelled by user")
|
|
884
|
+
except Exception as e:
|
|
885
|
+
# Log with full stack trace to shotgun.log
|
|
886
|
+
logger.exception(
|
|
887
|
+
"Agent run failed",
|
|
888
|
+
extra={
|
|
889
|
+
"agent_mode": self.mode.value,
|
|
890
|
+
"error_type": type(e).__name__,
|
|
891
|
+
},
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
# Determine user-friendly message based on error type
|
|
895
|
+
error_name = type(e).__name__
|
|
896
|
+
error_message = str(e)
|
|
897
|
+
|
|
898
|
+
if "APIStatusError" in error_name and "overload" in error_message.lower():
|
|
899
|
+
hint = "⚠️ The AI service is temporarily overloaded. Please wait a moment and try again."
|
|
900
|
+
elif "APIStatusError" in error_name and "rate" in error_message.lower():
|
|
901
|
+
hint = "⚠️ Rate limit reached. Please wait before trying again."
|
|
902
|
+
elif "APIStatusError" in error_name:
|
|
903
|
+
hint = f"⚠️ AI service error: {error_message}"
|
|
904
|
+
else:
|
|
905
|
+
hint = f"⚠️ An error occurred: {error_message}\n\nCheck logs at ~/.shotgun-sh/logs/shotgun.log"
|
|
906
|
+
|
|
907
|
+
self.mount_hint(hint)
|
|
884
908
|
finally:
|
|
885
909
|
self.working = False
|
|
886
910
|
self._current_worker = None
|
|
@@ -910,24 +934,41 @@ class ChatScreen(Screen[None]):
|
|
|
910
934
|
"""Load conversation from persistent storage."""
|
|
911
935
|
conversation = self.conversation_manager.load()
|
|
912
936
|
if conversation is None:
|
|
937
|
+
# Check if file existed but was corrupted (backup was created)
|
|
938
|
+
backup_path = self.conversation_manager.conversation_path.with_suffix(
|
|
939
|
+
".json.backup"
|
|
940
|
+
)
|
|
941
|
+
if backup_path.exists():
|
|
942
|
+
# File was corrupted - show friendly notification
|
|
943
|
+
self.mount_hint(
|
|
944
|
+
"⚠️ Previous session was corrupted and has been backed up. Starting fresh conversation."
|
|
945
|
+
)
|
|
913
946
|
return
|
|
914
947
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
948
|
+
try:
|
|
949
|
+
# Restore agent state
|
|
950
|
+
agent_messages = conversation.get_agent_messages()
|
|
951
|
+
ui_messages = conversation.get_ui_messages()
|
|
952
|
+
|
|
953
|
+
# Create ConversationState for restoration
|
|
954
|
+
state = ConversationState(
|
|
955
|
+
agent_messages=agent_messages,
|
|
956
|
+
ui_messages=ui_messages,
|
|
957
|
+
agent_type=conversation.last_agent_model,
|
|
958
|
+
)
|
|
918
959
|
|
|
919
|
-
|
|
920
|
-
state = ConversationState(
|
|
921
|
-
agent_messages=agent_messages,
|
|
922
|
-
ui_messages=ui_messages,
|
|
923
|
-
agent_type=conversation.last_agent_model,
|
|
924
|
-
)
|
|
960
|
+
self.agent_manager.restore_conversation_state(state)
|
|
925
961
|
|
|
926
|
-
|
|
962
|
+
# Update the current mode
|
|
963
|
+
self.mode = AgentType(conversation.last_agent_model)
|
|
964
|
+
self.deps.usage_manager.restore_usage_state()
|
|
927
965
|
|
|
928
|
-
#
|
|
929
|
-
|
|
930
|
-
|
|
966
|
+
except Exception as e: # pragma: no cover
|
|
967
|
+
# If anything goes wrong during restoration, log it and continue
|
|
968
|
+
logger.error("Failed to restore conversation state: %s", e)
|
|
969
|
+
self.mount_hint(
|
|
970
|
+
"⚠️ Could not restore previous session. Starting fresh conversation."
|
|
971
|
+
)
|
|
931
972
|
|
|
932
973
|
|
|
933
974
|
def help_text_with_codebase(already_indexed: bool = False) -> str:
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: shotgun-sh
|
|
3
|
+
Version: 0.2.7.dev1
|
|
4
|
+
Summary: AI-powered research, planning, and task management CLI tool
|
|
5
|
+
Project-URL: Homepage, https://shotgun.sh/
|
|
6
|
+
Project-URL: Repository, https://github.com/shotgun-sh/shotgun
|
|
7
|
+
Project-URL: Issues, https://github.com/shotgun-sh/shotgun-alpha/issues
|
|
8
|
+
Project-URL: Discord, https://discord.gg/5RmY6J2N7s
|
|
9
|
+
Author-email: "Proofs.io" <hello@proofs.io>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,ai,cli,llm,planning,productivity,pydantic-ai,research,task-management
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Requires-Dist: anthropic>=0.39.0
|
|
25
|
+
Requires-Dist: genai-prices>=0.0.27
|
|
26
|
+
Requires-Dist: httpx>=0.27.0
|
|
27
|
+
Requires-Dist: jinja2>=3.1.0
|
|
28
|
+
Requires-Dist: kuzu>=0.7.0
|
|
29
|
+
Requires-Dist: logfire[pydantic-ai]>=2.0.0
|
|
30
|
+
Requires-Dist: openai>=1.0.0
|
|
31
|
+
Requires-Dist: packaging>=23.0
|
|
32
|
+
Requires-Dist: posthog>=3.0.0
|
|
33
|
+
Requires-Dist: pydantic-ai>=0.0.14
|
|
34
|
+
Requires-Dist: rich>=13.0.0
|
|
35
|
+
Requires-Dist: sentencepiece>=0.2.0
|
|
36
|
+
Requires-Dist: sentry-sdk[pure-eval]>=2.0.0
|
|
37
|
+
Requires-Dist: textual-dev>=1.7.0
|
|
38
|
+
Requires-Dist: textual>=6.1.0
|
|
39
|
+
Requires-Dist: tiktoken>=0.7.0
|
|
40
|
+
Requires-Dist: tree-sitter-go>=0.23.0
|
|
41
|
+
Requires-Dist: tree-sitter-javascript>=0.23.0
|
|
42
|
+
Requires-Dist: tree-sitter-python>=0.23.0
|
|
43
|
+
Requires-Dist: tree-sitter-rust>=0.23.0
|
|
44
|
+
Requires-Dist: tree-sitter-typescript>=0.23.0
|
|
45
|
+
Requires-Dist: tree-sitter>=0.21.0
|
|
46
|
+
Requires-Dist: typer>=0.12.0
|
|
47
|
+
Requires-Dist: watchdog>=4.0.0
|
|
48
|
+
Provides-Extra: dev
|
|
49
|
+
Requires-Dist: commitizen>=3.13.0; extra == 'dev'
|
|
50
|
+
Requires-Dist: lefthook>=1.12.0; extra == 'dev'
|
|
51
|
+
Requires-Dist: mypy>=1.11.0; extra == 'dev'
|
|
52
|
+
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
53
|
+
Description-Content-Type: text/markdown
|
|
54
|
+
|
|
55
|
+
# Shotgun
|
|
56
|
+
|
|
57
|
+
**Spec-Driven Development for AI Code Generation**
|
|
58
|
+
|
|
59
|
+
Shotgun is a CLI tool that turns work with AI code-gen tools from "I want to build X" into: **research → specs → plans → tasks → implementation**. It reads your entire codebase, coordinates AI agents to do the heavy lifting, and exports clean artifacts in the agents.md format so your code-gen tools actually know what they're building.
|
|
60
|
+
|
|
61
|
+
🌐 **Learn more at [shotgun.sh](https://shotgun.sh/)**
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
### 📊 Complete Codebase Understanding
|
|
66
|
+
|
|
67
|
+
Before writing a single line, Shotgun reads all of it. Your patterns. Your dependencies. Your technical debt. Whether you're adding features, onboarding devs, planning migrations, or refactoring - Shotgun knows what you're working with.
|
|
68
|
+
|
|
69
|
+
### 🔄 Five Modes. One Journey. Zero Gaps.
|
|
70
|
+
|
|
71
|
+
**Research** (what exists) → **Specify** (what to build) → **Plan** (how to build) → **Tasks** (break it down) → **Export** (to any tool)
|
|
72
|
+
|
|
73
|
+
Not another chatbot. A complete workflow where each mode feeds the next.
|
|
74
|
+
|
|
75
|
+
### ➡️ Export to agents.md
|
|
76
|
+
|
|
77
|
+
Outputs plug into many code-generation tools including Codex, Cursor, Warp, Devin, opencode, Jules, and more.
|
|
78
|
+
|
|
79
|
+
### 📝 Specs That Don't Die in Slack
|
|
80
|
+
|
|
81
|
+
Every research finding, every architectural decision, every "here's why we didn't use that library" - captured as markdown in your repo. Version controlled. Searchable.
|
|
82
|
+
|
|
83
|
+
## Installation
|
|
84
|
+
|
|
85
|
+
### Using pipx (Recommended)
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pipx install shotgun-sh
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Why pipx?** It installs Shotgun in an isolated environment, preventing dependency conflicts with your other Python projects.
|
|
92
|
+
|
|
93
|
+
### Using pip
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
pip install shotgun-sh
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Quick Start
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Research your codebase or a topic
|
|
103
|
+
shotgun research "What is our authentication flow?"
|
|
104
|
+
|
|
105
|
+
# Generate specifications
|
|
106
|
+
shotgun spec "Add OAuth2 authentication"
|
|
107
|
+
|
|
108
|
+
# Create an implementation plan
|
|
109
|
+
shotgun plan "Build user dashboard"
|
|
110
|
+
|
|
111
|
+
# Break down into tasks
|
|
112
|
+
shotgun tasks "Implement payment system"
|
|
113
|
+
|
|
114
|
+
# Export to agents.md format for your code-gen tools
|
|
115
|
+
shotgun export
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Support
|
|
119
|
+
|
|
120
|
+
Have questions? Join our community on **[Discord](https://discord.gg/5RmY6J2N7s)**
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
**License:** MIT
|
|
125
|
+
**Python:** 3.11+
|
|
@@ -8,10 +8,10 @@ shotgun/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
8
8
|
shotgun/sentry_telemetry.py,sha256=VD8es-tREfgtRKhDsEVvqpo0_kM_ab6iVm2lkOEmTlI,2950
|
|
9
9
|
shotgun/telemetry.py,sha256=C8dM7Feo1OxJMwDvgAMaA2RyRDO2rYPvC_8kLBuRUS8,3683
|
|
10
10
|
shotgun/agents/__init__.py,sha256=8Jzv1YsDuLyNPFJyckSr_qI4ehTVeDyIMDW4omsfPGc,25
|
|
11
|
-
shotgun/agents/agent_manager.py,sha256=
|
|
11
|
+
shotgun/agents/agent_manager.py,sha256=Fjm_7fDqmaqWgjAgGiiIdMNnK0ahanLLZ4bxJIbrMlE,32611
|
|
12
12
|
shotgun/agents/common.py,sha256=g8QW782XZfZHjAJPC6k2uXqoX4UZufvbGaejzWgya8E,17768
|
|
13
|
-
shotgun/agents/conversation_history.py,sha256=
|
|
14
|
-
shotgun/agents/conversation_manager.py,sha256=
|
|
13
|
+
shotgun/agents/conversation_history.py,sha256=c-1PBaG9FXPfSmekWtrD6s6YVT0sd98DdDhzS4a1Hyo,7859
|
|
14
|
+
shotgun/agents/conversation_manager.py,sha256=X3DWZZIqrv0hS1SUasyoxexnLPBUrZMBg4ZmRAipkBE,4429
|
|
15
15
|
shotgun/agents/export.py,sha256=7ZNw7781WJ4peLSeoUc7zxKeaZVxFpow2y4nU4dCfbo,2919
|
|
16
16
|
shotgun/agents/llm.py,sha256=hs8j1wwTczGtehzahL1Z_5D4qus5QUx4-h9-m5ZPzm4,2209
|
|
17
17
|
shotgun/agents/messages.py,sha256=wNn0qC5AqASM8LMaSGFOerZEJPn5FsIOmaJs1bdosuU,1036
|
|
@@ -29,7 +29,7 @@ shotgun/agents/config/provider.py,sha256=K2uW7DkdAhZfoDJcWV7NxOrSoEq1rQzArtZnxIg
|
|
|
29
29
|
shotgun/agents/history/__init__.py,sha256=XFQj2a6fxDqVg0Q3juvN9RjV_RJbgvFZtQOCOjVJyp4,147
|
|
30
30
|
shotgun/agents/history/compaction.py,sha256=9RMpG0aY_7L4TecbgwHSOkGtbd9W5XZTg-MbzZmNl00,3515
|
|
31
31
|
shotgun/agents/history/constants.py,sha256=yWY8rrTZarLA3flCCMB_hS2NMvUDRDTwP4D4j7MIh1w,446
|
|
32
|
-
shotgun/agents/history/context_extraction.py,sha256=
|
|
32
|
+
shotgun/agents/history/context_extraction.py,sha256=yPF3oYpv5GFsFQT5y53ORKdADtrkGH4u8LwPdO0YVzU,7157
|
|
33
33
|
shotgun/agents/history/history_building.py,sha256=6LFDZ60MTPDoGAcmu_mjlnjVYu8YYWdIi-cGbF3jm7A,3532
|
|
34
34
|
shotgun/agents/history/history_processors.py,sha256=D3z-hzrXHxE7OAZaVX4_YAKN_nyxSF5iYMIYO24V_CI,17943
|
|
35
35
|
shotgun/agents/history/message_utils.py,sha256=aPusAl2RYKbjc7lBxPaNprRHmZEG6fe97q7DQUlhlzU,2918
|
|
@@ -127,7 +127,7 @@ shotgun/tui/components/prompt_input.py,sha256=Ss-htqraHZAPaehGE4x86ij0veMjc4Ugad
|
|
|
127
127
|
shotgun/tui/components/spinner.py,sha256=ovTDeaJ6FD6chZx_Aepia6R3UkPOVJ77EKHfRmn39MY,2427
|
|
128
128
|
shotgun/tui/components/splash.py,sha256=vppy9vEIEvywuUKRXn2y11HwXSRkQZHLYoVjhDVdJeU,1267
|
|
129
129
|
shotgun/tui/components/vertical_tail.py,sha256=kROwTaRjUwVB7H35dtmNcUVPQqNYvvfq7K2tXBKEb6c,638
|
|
130
|
-
shotgun/tui/screens/chat.py,sha256=
|
|
130
|
+
shotgun/tui/screens/chat.py,sha256=VAsgX7YiXTXAH7pAwcWaWr2hGu_Ft0i8VpoUETTPsTM,37424
|
|
131
131
|
shotgun/tui/screens/chat.tcss,sha256=2Yq3E23jxsySYsgZf4G1AYrYVcpX0UDW6kNNI0tDmtM,437
|
|
132
132
|
shotgun/tui/screens/directory_setup.py,sha256=lIZ1J4A6g5Q2ZBX8epW7BhR96Dmdcg22CyiM5S-I5WU,3237
|
|
133
133
|
shotgun/tui/screens/feedback.py,sha256=VxpW0PVxMp22ZvSfQkTtgixNrpEOlfWtekjqlVfYEjA,5708
|
|
@@ -148,8 +148,8 @@ shotgun/utils/env_utils.py,sha256=ulM3BRi9ZhS7uC-zorGeDQm4SHvsyFuuU9BtVPqdrHY,14
|
|
|
148
148
|
shotgun/utils/file_system_utils.py,sha256=l-0p1bEHF34OU19MahnRFdClHufThfGAjQ431teAIp0,1004
|
|
149
149
|
shotgun/utils/source_detection.py,sha256=Co6Q03R3fT771TF3RzB-70stfjNP2S4F_ArZKibwzm8,454
|
|
150
150
|
shotgun/utils/update_checker.py,sha256=IgzPHRhS1ETH7PnJR_dIx6lxgr1qHpCkMTgzUxvGjhI,7586
|
|
151
|
-
shotgun_sh-0.2.
|
|
152
|
-
shotgun_sh-0.2.
|
|
153
|
-
shotgun_sh-0.2.
|
|
154
|
-
shotgun_sh-0.2.
|
|
155
|
-
shotgun_sh-0.2.
|
|
151
|
+
shotgun_sh-0.2.7.dev1.dist-info/METADATA,sha256=MEaQlu9HJSSTi9_WuCgVgC8nX5J34O8PzyP8b8mCj_4,4268
|
|
152
|
+
shotgun_sh-0.2.7.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
153
|
+
shotgun_sh-0.2.7.dev1.dist-info/entry_points.txt,sha256=asZxLU4QILneq0MWW10saVCZc4VWhZfb0wFZvERnzfA,45
|
|
154
|
+
shotgun_sh-0.2.7.dev1.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
|
|
155
|
+
shotgun_sh-0.2.7.dev1.dist-info/RECORD,,
|
|
@@ -1,467 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: shotgun-sh
|
|
3
|
-
Version: 0.2.6.dev5
|
|
4
|
-
Summary: AI-powered research, planning, and task management CLI tool
|
|
5
|
-
Project-URL: Homepage, https://shotgun.sh/
|
|
6
|
-
Project-URL: Repository, https://github.com/shotgun-sh/shotgun
|
|
7
|
-
Project-URL: Issues, https://github.com/shotgun-sh/shotgun-alpha/issues
|
|
8
|
-
Project-URL: Discord, https://discord.gg/5RmY6J2N7s
|
|
9
|
-
Author-email: "Proofs.io" <hello@proofs.io>
|
|
10
|
-
License: MIT
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Keywords: agent,ai,cli,llm,planning,productivity,pydantic-ai,research,task-management
|
|
13
|
-
Classifier: Development Status :: 3 - Alpha
|
|
14
|
-
Classifier: Environment :: Console
|
|
15
|
-
Classifier: Intended Audience :: Developers
|
|
16
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
-
Classifier: Operating System :: OS Independent
|
|
18
|
-
Classifier: Programming Language :: Python :: 3
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
-
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
-
Classifier: Topic :: Utilities
|
|
23
|
-
Requires-Python: >=3.11
|
|
24
|
-
Requires-Dist: anthropic>=0.39.0
|
|
25
|
-
Requires-Dist: genai-prices>=0.0.27
|
|
26
|
-
Requires-Dist: httpx>=0.27.0
|
|
27
|
-
Requires-Dist: jinja2>=3.1.0
|
|
28
|
-
Requires-Dist: kuzu>=0.7.0
|
|
29
|
-
Requires-Dist: logfire[pydantic-ai]>=2.0.0
|
|
30
|
-
Requires-Dist: openai>=1.0.0
|
|
31
|
-
Requires-Dist: packaging>=23.0
|
|
32
|
-
Requires-Dist: posthog>=3.0.0
|
|
33
|
-
Requires-Dist: pydantic-ai>=0.0.14
|
|
34
|
-
Requires-Dist: rich>=13.0.0
|
|
35
|
-
Requires-Dist: sentencepiece>=0.2.0
|
|
36
|
-
Requires-Dist: sentry-sdk[pure-eval]>=2.0.0
|
|
37
|
-
Requires-Dist: textual-dev>=1.7.0
|
|
38
|
-
Requires-Dist: textual>=6.1.0
|
|
39
|
-
Requires-Dist: tiktoken>=0.7.0
|
|
40
|
-
Requires-Dist: tree-sitter-go>=0.23.0
|
|
41
|
-
Requires-Dist: tree-sitter-javascript>=0.23.0
|
|
42
|
-
Requires-Dist: tree-sitter-python>=0.23.0
|
|
43
|
-
Requires-Dist: tree-sitter-rust>=0.23.0
|
|
44
|
-
Requires-Dist: tree-sitter-typescript>=0.23.0
|
|
45
|
-
Requires-Dist: tree-sitter>=0.21.0
|
|
46
|
-
Requires-Dist: typer>=0.12.0
|
|
47
|
-
Requires-Dist: watchdog>=4.0.0
|
|
48
|
-
Provides-Extra: dev
|
|
49
|
-
Requires-Dist: commitizen>=3.13.0; extra == 'dev'
|
|
50
|
-
Requires-Dist: lefthook>=1.12.0; extra == 'dev'
|
|
51
|
-
Requires-Dist: mypy>=1.11.0; extra == 'dev'
|
|
52
|
-
Requires-Dist: ruff>=0.6.0; extra == 'dev'
|
|
53
|
-
Description-Content-Type: text/markdown
|
|
54
|
-
|
|
55
|
-
# Shotgun
|
|
56
|
-
|
|
57
|
-
A Python CLI tool for research, planning, and task management powered by AI agents.
|
|
58
|
-
|
|
59
|
-
## Features
|
|
60
|
-
|
|
61
|
-
- **Research**: Perform research with agentic loops
|
|
62
|
-
- **Planning**: Generate structured plans for achieving goals
|
|
63
|
-
- **Tasks**: Generate prioritized task lists with agentic approaches
|
|
64
|
-
|
|
65
|
-
## Installation
|
|
66
|
-
|
|
67
|
-
### From PyPI (Recommended)
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
pip install shotgun-sh
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### From Source
|
|
74
|
-
|
|
75
|
-
```bash
|
|
76
|
-
git clone https://github.com/shotgun-sh/shotgun.git
|
|
77
|
-
cd shotgun
|
|
78
|
-
uv sync --all-extras
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
After installation from source, you can use either method:
|
|
82
|
-
|
|
83
|
-
**Method 1: Direct command (after uv sync)**
|
|
84
|
-
```bash
|
|
85
|
-
shotgun --help
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
**Method 2: Via uv run**
|
|
89
|
-
```bash
|
|
90
|
-
uv run shotgun --help
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
If installed from PyPI, simply use:
|
|
94
|
-
```bash
|
|
95
|
-
shotgun --help
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
### Virtual Environment Setup (Optional)
|
|
99
|
-
|
|
100
|
-
If you prefer using a local virtual environment:
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
uv venv
|
|
104
|
-
source .venv/bin/activate # On Windows: .venv\Scripts\activate
|
|
105
|
-
uv sync --all-extras
|
|
106
|
-
shotgun --help
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
## Usage
|
|
110
|
-
|
|
111
|
-
### Using Direct Commands (after uv sync)
|
|
112
|
-
|
|
113
|
-
```bash
|
|
114
|
-
# Research a topic
|
|
115
|
-
shotgun research "What is quantum computing?"
|
|
116
|
-
|
|
117
|
-
# Generate a plan
|
|
118
|
-
shotgun plan "Build a web application"
|
|
119
|
-
shotgun plan "build me a house"
|
|
120
|
-
|
|
121
|
-
# Generate tasks for a project
|
|
122
|
-
shotgun tasks "Create a machine learning model"
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
### Using uv run
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
# Research a topic
|
|
129
|
-
uv run shotgun research "What is quantum computing?"
|
|
130
|
-
|
|
131
|
-
# Generate a plan
|
|
132
|
-
uv run shotgun plan "Build a web application"
|
|
133
|
-
|
|
134
|
-
# Generate tasks for a project
|
|
135
|
-
uv run shotgun tasks "Create a machine learning model"
|
|
136
|
-
```
|
|
137
|
-
|
|
138
|
-
## Auto-Updates
|
|
139
|
-
|
|
140
|
-
Shotgun automatically checks for updates to keep you on the latest version.
|
|
141
|
-
|
|
142
|
-
### How it works
|
|
143
|
-
|
|
144
|
-
- Checks for updates on startup (runs in background, non-blocking)
|
|
145
|
-
- Caches results for 24 hours to minimize API calls
|
|
146
|
-
- Shows notification after command execution if an update is available
|
|
147
|
-
- Never auto-updates development versions
|
|
148
|
-
|
|
149
|
-
### Update Commands
|
|
150
|
-
|
|
151
|
-
```bash
|
|
152
|
-
# Check for available updates
|
|
153
|
-
shotgun update --check
|
|
154
|
-
|
|
155
|
-
# Install available updates
|
|
156
|
-
shotgun update
|
|
157
|
-
|
|
158
|
-
# Force update (even for dev versions with confirmation)
|
|
159
|
-
shotgun update --force
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
### Disable Update Checks
|
|
163
|
-
|
|
164
|
-
```bash
|
|
165
|
-
# Disable for a single command
|
|
166
|
-
shotgun --no-update-check research "topic"
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
### Installation Methods
|
|
170
|
-
|
|
171
|
-
The update command automatically detects and uses the appropriate method:
|
|
172
|
-
- **pipx**: `pipx upgrade shotgun-sh`
|
|
173
|
-
- **pip**: `pip install --upgrade shotgun-sh`
|
|
174
|
-
- **venv**: Updates within the virtual environment
|
|
175
|
-
|
|
176
|
-
## Development Setup
|
|
177
|
-
|
|
178
|
-
### Requirements
|
|
179
|
-
|
|
180
|
-
- **Python 3.11+** (3.13 recommended)
|
|
181
|
-
- **uv** - Fast Python package installer and resolver
|
|
182
|
-
- **actionlint** (optional) - For GitHub Actions workflow validation
|
|
183
|
-
|
|
184
|
-
### Quick Start
|
|
185
|
-
|
|
186
|
-
1. **Clone and setup**:
|
|
187
|
-
```bash
|
|
188
|
-
git clone https://github.com/shotgun-sh/shotgun.git
|
|
189
|
-
cd shotgun
|
|
190
|
-
```
|
|
191
|
-
|
|
192
|
-
2. **Install uv** (if not already installed):
|
|
193
|
-
```bash
|
|
194
|
-
# macOS/Linux
|
|
195
|
-
curl -LsSf https://astral.sh/uv/install.sh | sh
|
|
196
|
-
|
|
197
|
-
# Or via brew
|
|
198
|
-
brew install uv
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
3. **Install dependencies**:
|
|
202
|
-
```bash
|
|
203
|
-
uv sync --all-extras
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
4. **Install git hooks**:
|
|
207
|
-
```bash
|
|
208
|
-
uv run lefthook install
|
|
209
|
-
```
|
|
210
|
-
|
|
211
|
-
5. **Verify setup**:
|
|
212
|
-
```bash
|
|
213
|
-
uv run shotgun --version
|
|
214
|
-
```
|
|
215
|
-
|
|
216
|
-
### Development Commands
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
# Run the CLI
|
|
220
|
-
uv run shotgun --help
|
|
221
|
-
|
|
222
|
-
# Run the TUI
|
|
223
|
-
uv run tui
|
|
224
|
-
|
|
225
|
-
# Run tests
|
|
226
|
-
uv run pytest
|
|
227
|
-
|
|
228
|
-
# Run tests with coverage
|
|
229
|
-
uv run pytest --cov=src --cov-report=term-missing --cov-report=html
|
|
230
|
-
|
|
231
|
-
# Run linting
|
|
232
|
-
uv run ruff check .
|
|
233
|
-
|
|
234
|
-
# Run formatting
|
|
235
|
-
uv run ruff format .
|
|
236
|
-
|
|
237
|
-
# Run type checking
|
|
238
|
-
uv run mypy src/
|
|
239
|
-
|
|
240
|
-
# Run all pre-commit hooks manually
|
|
241
|
-
uv run lefthook run pre-commit
|
|
242
|
-
```
|
|
243
|
-
|
|
244
|
-
### Code Coverage
|
|
245
|
-
|
|
246
|
-
To analyze test coverage and identify areas that need testing:
|
|
247
|
-
|
|
248
|
-
```bash
|
|
249
|
-
# Run tests with coverage analysis
|
|
250
|
-
uv run pytest --cov=src --cov-report=term-missing --cov-report=html
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
This will:
|
|
254
|
-
- Display coverage summary in the terminal
|
|
255
|
-
- Generate a detailed HTML coverage report
|
|
256
|
-
|
|
257
|
-
**Viewing the coverage report:**
|
|
258
|
-
Open `htmlcov/index.html` in your browser to see:
|
|
259
|
-
- Overall coverage percentage
|
|
260
|
-
- File-by-file coverage breakdown
|
|
261
|
-
- Line-by-line coverage highlighting
|
|
262
|
-
- Missing coverage areas
|
|
263
|
-
|
|
264
|
-
The coverage configuration is in `pyproject.toml` and will automatically run when you use `uv run pytest`.
|
|
265
|
-
|
|
266
|
-
### Git Hooks (Lefthook)
|
|
267
|
-
|
|
268
|
-
This project uses [lefthook](https://github.com/evilmartians/lefthook) for git hooks. The hooks automatically run:
|
|
269
|
-
|
|
270
|
-
- **ruff** - Python linting with auto-fix
|
|
271
|
-
- **ruff-format** - Code formatting
|
|
272
|
-
- **mypy** - Type checking
|
|
273
|
-
- **commitizen** - Commit message validation
|
|
274
|
-
- **actionlint** - GitHub Actions workflow validation (if installed)
|
|
275
|
-
|
|
276
|
-
#### Installing actionlint (recommended)
|
|
277
|
-
|
|
278
|
-
```bash
|
|
279
|
-
# macOS
|
|
280
|
-
brew install actionlint
|
|
281
|
-
|
|
282
|
-
# Linux/macOS (direct download)
|
|
283
|
-
curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash | bash
|
|
284
|
-
|
|
285
|
-
# Go install
|
|
286
|
-
go install github.com/rhysd/actionlint/cmd/actionlint@latest
|
|
287
|
-
```
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
### Python Version Management
|
|
291
|
-
|
|
292
|
-
The project supports **Python 3.11+**. The `.python-version` file specifies Python 3.11 to ensure development against the minimum supported version.
|
|
293
|
-
|
|
294
|
-
If using **pyenv**:
|
|
295
|
-
```bash
|
|
296
|
-
pyenv install 3.11
|
|
297
|
-
```
|
|
298
|
-
|
|
299
|
-
If using **uv** (recommended):
|
|
300
|
-
```bash
|
|
301
|
-
uv python install 3.11
|
|
302
|
-
uv sync --python 3.11
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### Commit Message Convention
|
|
306
|
-
|
|
307
|
-
This project enforces **Conventional Commits** specification. All commit messages must follow this format:
|
|
308
|
-
|
|
309
|
-
```
|
|
310
|
-
<type>[optional scope]: <description>
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
**Required commit types:**
|
|
314
|
-
- `feat`: New feature
|
|
315
|
-
- `fix`: Bug fix
|
|
316
|
-
- `docs`: Documentation changes
|
|
317
|
-
- `style`: Code formatting changes
|
|
318
|
-
- `refactor`: Code restructuring without feature changes
|
|
319
|
-
- `perf`: Performance improvements
|
|
320
|
-
- `test`: Adding or updating tests
|
|
321
|
-
- `build`: Build system changes
|
|
322
|
-
- `ci`: CI/CD changes
|
|
323
|
-
- `chore`: Maintenance tasks
|
|
324
|
-
- `revert`: Reverting previous commits
|
|
325
|
-
|
|
326
|
-
**Examples:**
|
|
327
|
-
```bash
|
|
328
|
-
feat: add user authentication system
|
|
329
|
-
fix: resolve memory leak in data processing
|
|
330
|
-
docs: update API documentation
|
|
331
|
-
refactor: simplify user validation logic
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
**For interactive commit creation:**
|
|
335
|
-
```bash
|
|
336
|
-
uv run cz commit
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
### Contributing
|
|
340
|
-
|
|
341
|
-
1. Fork the repository
|
|
342
|
-
2. Create a feature branch: `git checkout -b feat/feature-name`
|
|
343
|
-
3. Make your changes
|
|
344
|
-
4. Run the pre-commit hooks: `uv run lefthook run pre-commit`
|
|
345
|
-
5. Commit with conventional format: `git commit -m "feat: add new feature"`
|
|
346
|
-
6. Push to your fork: `git push origin feat/feature-name`
|
|
347
|
-
7. Create a Pull Request with conventional title format
|
|
348
|
-
|
|
349
|
-
### CI/CD
|
|
350
|
-
|
|
351
|
-
GitHub Actions automatically:
|
|
352
|
-
- Runs on pull requests and pushes to main
|
|
353
|
-
- Tests with Python 3.11
|
|
354
|
-
- Validates code with ruff, ruff-format, and mypy
|
|
355
|
-
- Ensures all checks pass before merge
|
|
356
|
-
|
|
357
|
-
## Observability & Telemetry
|
|
358
|
-
|
|
359
|
-
Shotgun includes built-in observability with Sentry for error tracking and Logfire for logging and tracing. Both services track users anonymously using a UUID generated on first run.
|
|
360
|
-
|
|
361
|
-
### Anonymous User Tracking
|
|
362
|
-
|
|
363
|
-
Each user gets a unique anonymous ID stored in their config:
|
|
364
|
-
```bash
|
|
365
|
-
# Get your anonymous user ID
|
|
366
|
-
shotgun config get-user-id
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
This ID is automatically included in:
|
|
370
|
-
- **Sentry**: Error reports and exceptions
|
|
371
|
-
- **Logfire**: All logs, traces, and spans
|
|
372
|
-
|
|
373
|
-
### Logfire Queries
|
|
374
|
-
|
|
375
|
-
Logfire uses SQL for querying logs. Here are helpful queries for debugging and analysis:
|
|
376
|
-
|
|
377
|
-
#### Find all logs for a specific user
|
|
378
|
-
```sql
|
|
379
|
-
SELECT * FROM records
|
|
380
|
-
WHERE attributes->>'user_id' = 'your-user-id-here'
|
|
381
|
-
ORDER BY timestamp DESC;
|
|
382
|
-
```
|
|
383
|
-
|
|
384
|
-
#### Track user actions
|
|
385
|
-
```sql
|
|
386
|
-
SELECT
|
|
387
|
-
timestamp,
|
|
388
|
-
span_name,
|
|
389
|
-
message,
|
|
390
|
-
attributes
|
|
391
|
-
FROM records
|
|
392
|
-
WHERE attributes->>'user_id' = 'your-user-id-here'
|
|
393
|
-
AND span_name LIKE '%research%'
|
|
394
|
-
ORDER BY timestamp DESC;
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
#### Find slow operations for a user
|
|
398
|
-
```sql
|
|
399
|
-
SELECT
|
|
400
|
-
span_name,
|
|
401
|
-
duration_ms,
|
|
402
|
-
attributes
|
|
403
|
-
FROM records
|
|
404
|
-
WHERE attributes->>'user_id' = 'your-user-id-here'
|
|
405
|
-
AND duration_ms > 1000
|
|
406
|
-
ORDER BY duration_ms DESC;
|
|
407
|
-
```
|
|
408
|
-
|
|
409
|
-
#### Find errors for a user
|
|
410
|
-
```sql
|
|
411
|
-
SELECT * FROM records
|
|
412
|
-
WHERE attributes->>'user_id' = 'your-user-id-here'
|
|
413
|
-
AND level = 'error'
|
|
414
|
-
ORDER BY timestamp DESC;
|
|
415
|
-
```
|
|
416
|
-
|
|
417
|
-
#### Analyze user's AI provider usage
|
|
418
|
-
```sql
|
|
419
|
-
SELECT
|
|
420
|
-
attributes->>'provider' as provider,
|
|
421
|
-
COUNT(*) as usage_count,
|
|
422
|
-
AVG(duration_ms) as avg_duration
|
|
423
|
-
FROM records
|
|
424
|
-
WHERE attributes->>'user_id' = 'your-user-id-here'
|
|
425
|
-
AND attributes->>'provider' IS NOT NULL
|
|
426
|
-
GROUP BY provider;
|
|
427
|
-
```
|
|
428
|
-
|
|
429
|
-
#### Track feature usage by user
|
|
430
|
-
```sql
|
|
431
|
-
SELECT
|
|
432
|
-
span_name,
|
|
433
|
-
COUNT(*) as usage_count
|
|
434
|
-
FROM records
|
|
435
|
-
WHERE attributes->>'user_id' = 'your-user-id-here'
|
|
436
|
-
AND span_name IN ('research', 'plan', 'tasks')
|
|
437
|
-
GROUP BY span_name
|
|
438
|
-
ORDER BY usage_count DESC;
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
### Setting Up Observability (Optional)
|
|
442
|
-
|
|
443
|
-
For local development with Logfire:
|
|
444
|
-
```bash
|
|
445
|
-
# Set environment variables
|
|
446
|
-
export LOGFIRE_ENABLED=true
|
|
447
|
-
export LOGFIRE_TOKEN=your-logfire-token
|
|
448
|
-
|
|
449
|
-
# Run shotgun - will now send logs to Logfire
|
|
450
|
-
shotgun research "topic"
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
For Sentry (automatically configured in production builds):
|
|
454
|
-
```bash
|
|
455
|
-
# Set for local development
|
|
456
|
-
export SENTRY_DSN=your-sentry-dsn
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
### Privacy
|
|
460
|
-
|
|
461
|
-
- **No PII collected**: Only anonymous UUIDs are used for identification
|
|
462
|
-
- **Opt-in for development**: Telemetry requires explicit environment variables
|
|
463
|
-
- **Automatic in production**: Production builds include telemetry for error tracking
|
|
464
|
-
|
|
465
|
-
## Support
|
|
466
|
-
|
|
467
|
-
Join our discord https://discord.gg/5RmY6J2N7s
|
|
File without changes
|
|
File without changes
|
|
File without changes
|