shotgun-sh 0.2.6.dev2__py3-none-any.whl → 0.2.6.dev3__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.

@@ -6,20 +6,37 @@
6
6
  {% if interactive_mode -%}
7
7
  IMPORTANT: USER INTERACTION IS ENABLED (interactive mode).
8
8
 
9
- - BEFORE GETTING TO WORK, ALWAYS THINK WHAT YOU'RE GOING TO DO AND ask_user() TO ACCEPT WHAT YOU'RE GOING TO DO.
10
- - ALWAYS USE ask_user TO REVIEW AND ACCEPT THE ARTIFACT SECTION YOU'VE WORKED ON BEFORE PROCEEDING TO THE NEXT SECTION.
11
- - AFTER USING write_artifact_section(), ALWAYS USE ask_user() TO REVIEW AND ACCEPT THE ARTIFACT SECTION YOU'VE WORKED ON BEFORE PROCEEDING TO THE NEXT SECTION.
9
+ ## Structured Output Format
10
+
11
+ You must return responses using this structured format:
12
+
13
+ ```json
14
+ {
15
+ "response": "Your main response text here",
16
+ "clarifying_questions": ["Question 1?", "Question 2?"] // Optional, only when needed
17
+ }
18
+ ```
19
+
20
+ ## When to Use Clarifying Questions
21
+
22
+ - BEFORE GETTING TO WORK: If the user's request is ambiguous, use clarifying_questions to ask what they want
23
+ - DURING WORK: After using write_file(), you can suggest that the user review it and ask any clarifying questions with clarifying_questions
12
24
  - Don't assume - ask for confirmation of your understanding
13
- - When in doubt about any aspect of the goal, ASK before proceeding
25
+ - When in doubt about any aspect of the goal, include clarifying_questions
26
+
27
+ ## Important Notes
28
+
29
+ - If you don't need to ask questions, set clarifying_questions to null or omit it
30
+ - Keep response field concise - a paragraph at most for user communication
31
+ - Questions should be clear, specific, and independently answerable
32
+ - Don't ask multiple questions in one string - use separate array items
14
33
 
15
34
  {% else -%}
16
35
 
17
36
  IMPORTANT: USER INTERACTION IS DISABLED (non-interactive mode).
18
- - You cannot ask clarifying questions using ask_user tool
37
+ - You cannot ask clarifying questions (clarifying_questions will be ignored)
19
38
  - Make reasonable assumptions based on best practices
20
39
  - Use sensible defaults when information is missing
21
- - Make reasonable assumptions based on industry best practices
22
- - Use sensible defaults when specific details are not provided
23
40
  - When in doubt, make reasonable assumptions and proceed with best practices
24
41
  {% endif %}
25
42
 
@@ -118,7 +118,7 @@ USER INTERACTION - REDUCE UNCERTAINTY:
118
118
  - FIRST read `research.md` and `specification.md` before asking ANY questions
119
119
  - ONLY ask clarifying questions AFTER reading the context files
120
120
  - Questions should be about gaps not covered in research/specification
121
- - Use ask_user tool to gather specific details about:
121
+ - Use clarifying questions to gather specific details about:
122
122
  - Information not found in the context files
123
123
  - Clarifications on ambiguous specifications
124
124
  - Priorities when multiple options exist
@@ -39,7 +39,7 @@ For research tasks:
39
39
  ## RESEARCH PRINCIPLES
40
40
 
41
41
  {% if interactive_mode -%}
42
- - CRITICAL: BEFORE RUNNING ANY SEARCH TOOL, ASK THE USER FOR APPROVAL USING ask_user(). FINISH THE QUESTION WITH ASKING FOR A GO AHEAD.
42
+ - CRITICAL: BEFORE RUNNING ANY SEARCH TOOL, ASK THE USER FOR APPROVAL using clarifying questions. Include what you plan to search for and ask if they want you to proceed.
43
43
  {% endif -%}
44
44
  - Build upon existing research rather than starting from scratch
45
45
  - Focus on practical, actionable information over theoretical concepts
@@ -99,7 +99,7 @@ Then organize tasks into logical sections:
99
99
  USER INTERACTION - ASK CLARIFYING QUESTIONS:
100
100
 
101
101
  - ALWAYS ask clarifying questions when the request is vague or ambiguous
102
- - Use ask_user tool to gather specific details about:
102
+ - Use clarifying questions to gather specific details about:
103
103
  - Specific features or functionality to prioritize
104
104
  - Technical constraints or preferences
105
105
  - Timeline and resource constraints
shotgun/telemetry.py CHANGED
@@ -65,6 +65,14 @@ def setup_logfire_observability() -> bool:
65
65
  # Instrument Pydantic AI for better observability
66
66
  logfire.instrument_pydantic_ai()
67
67
 
68
+ # Add LogfireLoggingHandler to root logger so logfire logs also go to file
69
+ import logging
70
+
71
+ root_logger = logging.getLogger()
72
+ logfire_handler = logfire.LogfireLoggingHandler()
73
+ root_logger.addHandler(logfire_handler)
74
+ logger.debug("Added LogfireLoggingHandler to root logger for file integration")
75
+
68
76
  # Set user context using baggage for all logs and spans
69
77
  try:
70
78
  from opentelemetry import baggage, context
@@ -24,6 +24,7 @@ from textual.widgets import Button, Label, Static
24
24
 
25
25
  from shotgun.agents.agent_manager import (
26
26
  AgentManager,
27
+ ClarifyingQuestionsMessage,
27
28
  MessageHistoryUpdated,
28
29
  PartialResponseMessage,
29
30
  )
@@ -37,8 +38,6 @@ from shotgun.agents.models import (
37
38
  AgentDeps,
38
39
  AgentType,
39
40
  FileOperationTracker,
40
- MultipleUserQuestions,
41
- UserQuestion,
42
41
  )
43
42
  from shotgun.codebase.core.manager import CodebaseAlreadyIndexedError
44
43
  from shotgun.codebase.models import IndexProgress, ProgressPhase
@@ -114,6 +113,18 @@ class StatusBar(Widget):
114
113
  self.working = working
115
114
 
116
115
  def render(self) -> str:
116
+ # Check if in Q&A mode first (highest priority)
117
+ try:
118
+ chat_screen = self.screen
119
+ if isinstance(chat_screen, ChatScreen) and chat_screen.qa_mode:
120
+ return (
121
+ "[$foreground-muted][bold $text]esc[/] to exit Q&A mode • "
122
+ "[bold $text]enter[/] to send answer • [bold $text]ctrl+j[/] for newline[/]"
123
+ )
124
+ except Exception: # noqa: S110
125
+ # If we can't access chat screen, continue with normal display
126
+ pass
127
+
117
128
  if self.working:
118
129
  return (
119
130
  "[$foreground-muted][bold $text]esc[/] to stop • "
@@ -151,6 +162,18 @@ class ModeIndicator(Widget):
151
162
 
152
163
  def render(self) -> str:
153
164
  """Render the mode indicator."""
165
+ # Check if in Q&A mode first
166
+ try:
167
+ chat_screen = self.screen
168
+ if isinstance(chat_screen, ChatScreen) and chat_screen.qa_mode:
169
+ return (
170
+ "[bold $text-accent]Q&A mode[/]"
171
+ "[$foreground-muted] (Answer the clarifying questions or ESC to cancel)[/]"
172
+ )
173
+ except Exception: # noqa: S110
174
+ # If we can't access chat screen, continue with normal display
175
+ pass
176
+
154
177
  mode_display = {
155
178
  AgentType.RESEARCH: "Research",
156
179
  AgentType.PLAN: "Planning",
@@ -258,11 +281,16 @@ class ChatScreen(Screen[None]):
258
281
  history: PromptHistory = PromptHistory()
259
282
  messages = reactive(list[ModelMessage | HintMessage]())
260
283
  working = reactive(False)
261
- question: reactive[UserQuestion | MultipleUserQuestions | None] = reactive(None)
262
284
  indexing_job: reactive[CodebaseIndexSelection | None] = reactive(None)
263
285
  partial_message: reactive[ModelMessage | None] = reactive(None)
264
286
  _current_worker = None # Track the current running worker for cancellation
265
287
 
288
+ # Q&A mode state (for structured output clarifying questions)
289
+ qa_mode = reactive(False)
290
+ qa_questions: list[str] = []
291
+ qa_current_index = reactive(0)
292
+ qa_answers: list[str] = []
293
+
266
294
  def __init__(self, continue_session: bool = False) -> None:
267
295
  super().__init__()
268
296
  # Get the model configuration and services
@@ -302,11 +330,19 @@ class ChatScreen(Screen[None]):
302
330
  self._load_conversation()
303
331
 
304
332
  self.call_later(self.check_if_codebase_is_indexed)
305
- # Start the question listener worker to handle ask_user interactions
306
- self.call_later(self.add_question_listener)
307
333
 
308
334
  async def on_key(self, event: events.Key) -> None:
309
335
  """Handle key presses for cancellation."""
336
+ # If escape is pressed during Q&A mode, exit Q&A
337
+ if event.key in (Keys.Escape, Keys.ControlC) and self.qa_mode:
338
+ self._exit_qa_mode()
339
+ # Re-enable the input
340
+ prompt_input = self.query_one(PromptInput)
341
+ prompt_input.focus()
342
+ # Prevent the event from propagating (don't quit the app)
343
+ event.stop()
344
+ return
345
+
310
346
  # If escape or ctrl+c is pressed while agent is working, cancel the operation
311
347
  if (
312
348
  event.key in (Keys.Escape, Keys.ControlC)
@@ -392,6 +428,17 @@ class ChatScreen(Screen[None]):
392
428
  status_bar.working = is_working
393
429
  status_bar.refresh()
394
430
 
431
+ def watch_qa_mode(self, qa_mode_active: bool) -> None:
432
+ """Update UI when Q&A mode state changes."""
433
+ if self.is_mounted:
434
+ # Update status bar to show "ESC to exit Q&A mode"
435
+ status_bar = self.query_one(StatusBar)
436
+ status_bar.refresh()
437
+
438
+ # Update mode indicator to show "Q&A mode"
439
+ mode_indicator = self.query_one(ModeIndicator)
440
+ mode_indicator.refresh()
441
+
395
442
  def watch_messages(self, messages: list[ModelMessage | HintMessage]) -> None:
396
443
  """Update the chat history when messages change."""
397
444
  if self.is_mounted:
@@ -399,6 +446,15 @@ class ChatScreen(Screen[None]):
399
446
  chat_history.update_messages(messages)
400
447
 
401
448
  def action_toggle_mode(self) -> None:
449
+ # Prevent mode switching during Q&A
450
+ if self.qa_mode:
451
+ self.notify(
452
+ "Cannot switch modes while answering questions",
453
+ severity="warning",
454
+ timeout=3,
455
+ )
456
+ return
457
+
402
458
  modes = [
403
459
  AgentType.RESEARCH,
404
460
  AgentType.SPECIFY,
@@ -419,44 +475,6 @@ class ChatScreen(Screen[None]):
419
475
  else:
420
476
  self.notify("No usage hint available", severity="error")
421
477
 
422
- @work
423
- async def add_question_listener(self) -> None:
424
- while True:
425
- question = await self.deps.queue.get()
426
-
427
- if isinstance(question, MultipleUserQuestions):
428
- # Set question state - handle_submit will add Q&A to chat
429
- question.current_index = 0
430
- self.question = question
431
-
432
- # Show intro message with total question count
433
- num_questions = len(question.questions)
434
- self.agent_manager.add_hint_message(
435
- HintMessage(message=f"I'm going to ask {num_questions} questions:")
436
- )
437
-
438
- # Show all questions in a numbered list so users can see what's coming
439
- if question.questions:
440
- questions_list = "\n".join(
441
- f"{i + 1}. {q}" for i, q in enumerate(question.questions)
442
- )
443
- self.agent_manager.add_hint_message(
444
- HintMessage(message=questions_list)
445
- )
446
-
447
- # Now show the first question prompt to indicate where to start answering
448
- first_q = question.questions[0]
449
- self.agent_manager.add_hint_message(
450
- HintMessage(message=f"**Q1:** {first_q}")
451
- )
452
- else:
453
- # Handle single question (original behavior)
454
- self.question = question
455
- await question.result
456
- self.question = None
457
-
458
- self.deps.queue.task_done()
459
-
460
478
  def compose(self) -> ComposeResult:
461
479
  """Create child widgets for the app."""
462
480
  with Container(id="window"):
@@ -502,6 +520,42 @@ class ChatScreen(Screen[None]):
502
520
  partial_response_widget = self.query_one(ChatHistory)
503
521
  partial_response_widget.partial_response = None
504
522
 
523
+ def _exit_qa_mode(self) -> None:
524
+ """Exit Q&A mode and clean up state."""
525
+ # Track cancellation event
526
+ track_event(
527
+ "qa_mode_cancelled",
528
+ {
529
+ "questions_total": len(self.qa_questions),
530
+ "questions_answered": len(self.qa_answers),
531
+ },
532
+ )
533
+
534
+ # Clear Q&A state
535
+ self.qa_mode = False
536
+ self.qa_questions = []
537
+ self.qa_answers = []
538
+ self.qa_current_index = 0
539
+
540
+ # Show cancellation message
541
+ self.mount_hint("⚠️ Q&A cancelled - You can continue the conversation.")
542
+
543
+ @on(ClarifyingQuestionsMessage)
544
+ def handle_clarifying_questions(self, event: ClarifyingQuestionsMessage) -> None:
545
+ """Handle clarifying questions from agent structured output.
546
+
547
+ Note: Hints are now added synchronously in agent_manager.run() before this
548
+ handler is called, so we only need to set up Q&A mode state here.
549
+ """
550
+ # Clear any streaming partial response (removes final_result JSON)
551
+ self._clear_partial_response()
552
+
553
+ # Enter Q&A mode
554
+ self.qa_mode = True
555
+ self.qa_questions = event.questions
556
+ self.qa_current_index = 0
557
+ self.qa_answers = []
558
+
505
559
  @on(MessageHistoryUpdated)
506
560
  def handle_message_history_updated(self, event: MessageHistoryUpdated) -> None:
507
561
  """Handle message history updates from the agent manager."""
@@ -548,8 +602,6 @@ class ChatScreen(Screen[None]):
548
602
 
549
603
  @on(PromptInput.Submitted)
550
604
  async def handle_submit(self, message: PromptInput.Submitted) -> None:
551
- from shotgun.agents.models import UserAnswer
552
-
553
605
  text = message.text.strip()
554
606
 
555
607
  # If empty text, just clear input and return
@@ -559,58 +611,56 @@ class ChatScreen(Screen[None]):
559
611
  self.value = ""
560
612
  return
561
613
 
562
- # Check if we're in a multi-question flow
563
- if self.question and isinstance(self.question, MultipleUserQuestions):
564
- q_num = self.question.current_index + 1
565
-
566
- # Q1 already shown by handle_message_history_updated,
567
- # Q2+ shown after previous answer. So we only need to add the answer to chat
568
- self.agent_manager.add_hint_message(
569
- HintMessage(message=f"**A{q_num}:** {text}")
570
- )
614
+ # Handle Q&A mode (from structured output clarifying questions)
615
+ if self.qa_mode and self.qa_questions:
616
+ # Collect answer
617
+ self.qa_answers.append(text)
571
618
 
572
- # Store the answer
573
- self.question.answers.append(text)
619
+ # Show answer
620
+ if len(self.qa_questions) == 1:
621
+ self.agent_manager.add_hint_message(
622
+ HintMessage(message=f"**A:** {text}")
623
+ )
624
+ else:
625
+ q_num = self.qa_current_index + 1
626
+ self.agent_manager.add_hint_message(
627
+ HintMessage(message=f"**A{q_num}:** {text}")
628
+ )
574
629
 
575
- # Move to next question or finish
576
- self.question.current_index += 1
630
+ # Move to next or finish
631
+ self.qa_current_index += 1
577
632
 
578
- if self.question.current_index < len(self.question.questions):
579
- # Show the next question immediately after the answer
580
- next_q = self.question.questions[self.question.current_index]
581
- next_q_num = self.question.current_index + 1
633
+ if self.qa_current_index < len(self.qa_questions):
634
+ # Show next question
635
+ next_q = self.qa_questions[self.qa_current_index]
636
+ next_q_num = self.qa_current_index + 1
582
637
  self.agent_manager.add_hint_message(
583
638
  HintMessage(message=f"**Q{next_q_num}:** {next_q}")
584
639
  )
585
640
  else:
586
- # All questions answered! Format and resolve
587
- formatted_qa = "\n\n".join(
588
- f"Q{i + 1}: {q}\nA{i + 1}: {a}"
589
- for i, (q, a) in enumerate(
590
- zip(self.question.questions, self.question.answers, strict=True)
641
+ # All answered - format and send back
642
+ if len(self.qa_questions) == 1:
643
+ # Single question - just send the answer
644
+ formatted_qa = f"Q: {self.qa_questions[0]}\nA: {self.qa_answers[0]}"
645
+ else:
646
+ # Multiple questions - format all Q&A pairs
647
+ formatted_qa = "\n\n".join(
648
+ f"Q{i + 1}: {q}\nA{i + 1}: {a}"
649
+ for i, (q, a) in enumerate(
650
+ zip(self.qa_questions, self.qa_answers, strict=True)
651
+ )
591
652
  )
592
- )
593
-
594
- # Resolve the original future with formatted Q&A (this goes to the agent)
595
- final_answer = UserAnswer(
596
- answer=formatted_qa,
597
- tool_call_id=self.question.tool_call_id,
598
- )
599
- self.question.result.set_result(final_answer)
600
-
601
- # Clear question state
602
- self.question = None
603
653
 
604
- # Clear input first
605
- prompt_input = self.query_one(PromptInput)
606
- prompt_input.clear()
607
- self.value = ""
654
+ # Exit Q&A mode
655
+ self.qa_mode = False
656
+ self.qa_questions = []
657
+ self.qa_answers = []
658
+ self.qa_current_index = 0
608
659
 
609
- # Send the formatted Q&A directly to the agent (no prefix needed)
660
+ # Send answers back to agent
610
661
  self.run_agent(formatted_qa)
611
- return
612
662
 
613
- # Clear input and return
663
+ # Clear input
614
664
  prompt_input = self.query_one(PromptInput)
615
665
  prompt_input.clear()
616
666
  self.value = ""
@@ -19,7 +19,6 @@ from textual.reactive import reactive
19
19
  from textual.widget import Widget
20
20
  from textual.widgets import Markdown
21
21
 
22
- from shotgun.agents.models import UserAnswer
23
22
  from shotgun.tui.components.vertical_tail import VerticalTail
24
23
  from shotgun.tui.screens.chat_screen.hint_message import HintMessage, HintMessageWidget
25
24
 
@@ -103,42 +102,8 @@ class ChatHistory(Widget):
103
102
  self._rendered_count = len(filtered)
104
103
 
105
104
  def filtered_items(self) -> Generator[ModelMessage | HintMessage, None, None]:
106
- for idx, next_item in enumerate(self.items):
107
- prev_item = self.items[idx - 1] if idx > 0 else None
108
-
109
- if isinstance(prev_item, ModelRequest) and isinstance(
110
- next_item, ModelResponse
111
- ):
112
- ask_user_tool_response_part = next(
113
- (
114
- part
115
- for part in prev_item.parts
116
- if isinstance(part, ToolReturnPart)
117
- and part.tool_name in ("ask_user", "ask_questions")
118
- ),
119
- None,
120
- )
121
-
122
- ask_user_part = next(
123
- (
124
- part
125
- for part in next_item.parts
126
- if isinstance(part, ToolCallPart)
127
- and part.tool_name in ("ask_user", "ask_questions")
128
- ),
129
- None,
130
- )
131
-
132
- if not ask_user_part or not ask_user_tool_response_part:
133
- yield next_item
134
- continue
135
- if (
136
- ask_user_tool_response_part.tool_call_id
137
- == ask_user_part.tool_call_id
138
- ):
139
- continue # don't emit tool call that happens after tool response
140
-
141
- yield next_item
105
+ # Simply yield all items - no filtering needed now that ask_user/ask_questions are gone
106
+ yield from self.items
142
107
 
143
108
  def update_messages(self, messages: list[ModelMessage | HintMessage]) -> None:
144
109
  """Update the displayed messages using incremental mounting."""
@@ -167,6 +132,9 @@ class ChatHistory(Widget):
167
132
 
168
133
  self._rendered_count = len(filtered)
169
134
 
135
+ # Scroll to bottom to show newly added messages
136
+ self.vertical_tail.scroll_end(animate=False)
137
+
170
138
 
171
139
  class UserQuestionWidget(Widget):
172
140
  def __init__(self, item: ModelRequest | None) -> None:
@@ -189,13 +157,8 @@ class UserQuestionWidget(Widget):
189
157
  f"**>** {part.content if isinstance(part.content, str) else ''}\n\n"
190
158
  )
191
159
  elif isinstance(part, ToolReturnPart):
192
- if part.tool_name == "ask_user":
193
- acc += f"**>** {part.content.answer if isinstance(part.content, UserAnswer) else part.content['answer']}\n\n"
194
- else:
195
- # acc += " ∟ finished\n\n" # let's not show anything yet
196
- pass
197
- elif isinstance(part, UserPromptPart):
198
- acc += f"**>** {part.content}\n\n"
160
+ # Don't show tool return parts in the UI
161
+ pass
199
162
  return acc
200
163
 
201
164
 
@@ -216,23 +179,15 @@ class AgentResponseWidget(Widget):
216
179
  if self.item is None:
217
180
  return ""
218
181
 
219
- # Check if there's an ask_user tool call
220
- has_ask_user = any(
221
- isinstance(part, ToolCallPart) and part.tool_name == "ask_user"
222
- for part in self.item.parts
223
- )
224
-
225
182
  for idx, part in enumerate(self.item.parts):
226
183
  if isinstance(part, TextPart):
227
- # Skip ALL text parts if there's an ask_user tool call
228
- if has_ask_user:
229
- continue
230
184
  # Only show the circle prefix if there's actual content
231
185
  if part.content and part.content.strip():
232
186
  acc += f"**⏺** {part.content}\n\n"
233
187
  elif isinstance(part, ToolCallPart):
234
188
  parts_str = self._format_tool_call_part(part)
235
- acc += parts_str + "\n\n"
189
+ if parts_str: # Only add if there's actual content
190
+ acc += parts_str + "\n\n"
236
191
  elif isinstance(part, BuiltinToolCallPart):
237
192
  # Format builtin tool calls better
238
193
  if part.tool_name and "search" in part.tool_name.lower():
@@ -286,12 +241,6 @@ class AgentResponseWidget(Widget):
286
241
  return args if isinstance(args, dict) else {}
287
242
 
288
243
  def _format_tool_call_part(self, part: ToolCallPart) -> str:
289
- if part.tool_name == "ask_user":
290
- return self._format_ask_user_part(part)
291
-
292
- if part.tool_name == "ask_questions":
293
- return self._format_ask_questions_part(part)
294
-
295
244
  # Parse args once (handles both JSON string and dict)
296
245
  args = self._parse_args(part.args)
297
246
 
@@ -362,10 +311,9 @@ class AgentResponseWidget(Widget):
362
311
  return f"{part.tool_name}({args['section_title']})"
363
312
  return f"{part.tool_name}()"
364
313
 
365
- if part.tool_name == "create_artifact":
366
- if "name" in args:
367
- return f"{part.tool_name}({args['name']})"
368
- return f"▪ {part.tool_name}()"
314
+ if part.tool_name == "final_result":
315
+ # Hide final_result tool calls completely - they're internal Pydantic AI mechanics
316
+ return ""
369
317
 
370
318
  # Default case for unrecognized tools - format args properly
371
319
  args = self._parse_args(part.args)
@@ -385,27 +333,3 @@ class AgentResponseWidget(Widget):
385
333
  return f"{part.tool_name}({args_str})"
386
334
  else:
387
335
  return f"{part.tool_name}()"
388
-
389
- def _format_ask_user_part(
390
- self,
391
- part: ToolCallPart,
392
- ) -> str:
393
- if isinstance(part.args, str):
394
- try:
395
- _args = json.loads(part.args) if part.args.strip() else {}
396
- except json.JSONDecodeError:
397
- _args = {}
398
- else:
399
- _args = part.args
400
-
401
- if isinstance(_args, dict) and "question" in _args:
402
- return f"{_args['question']}"
403
- else:
404
- return "❓ "
405
-
406
- def _format_ask_questions_part(
407
- self,
408
- part: ToolCallPart,
409
- ) -> str:
410
- """Hide ask_questions tool calls - Q&A shown as HintMessages instead."""
411
- return ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.2.6.dev2
3
+ Version: 0.2.6.dev3
4
4
  Summary: AI-powered research, planning, and task management CLI tool
5
5
  Project-URL: Homepage, https://shotgun.sh/
6
6
  Project-URL: Repository, https://github.com/shotgun-sh/shotgun