shotgun-sh 0.2.7.dev1__py3-none-any.whl → 0.2.8.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 +191 -13
- shotgun/agents/common.py +42 -17
- shotgun/agents/tools/file_management.py +55 -9
- shotgun/main.py +54 -10
- shotgun/prompts/agents/specify.j2 +270 -3
- shotgun/tui/app.py +116 -0
- {shotgun_sh-0.2.7.dev1.dist-info → shotgun_sh-0.2.8.dev1.dist-info}/METADATA +3 -1
- {shotgun_sh-0.2.7.dev1.dist-info → shotgun_sh-0.2.8.dev1.dist-info}/RECORD +11 -11
- {shotgun_sh-0.2.7.dev1.dist-info → shotgun_sh-0.2.8.dev1.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.7.dev1.dist-info → shotgun_sh-0.2.8.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.7.dev1.dist-info → shotgun_sh-0.2.8.dev1.dist-info}/licenses/LICENSE +0 -0
shotgun/agents/agent_manager.py
CHANGED
|
@@ -4,9 +4,17 @@ import json
|
|
|
4
4
|
import logging
|
|
5
5
|
from collections.abc import AsyncIterable, Sequence
|
|
6
6
|
from dataclasses import dataclass, field, is_dataclass, replace
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
from typing import TYPE_CHECKING, Any, cast
|
|
8
9
|
|
|
9
10
|
import logfire
|
|
11
|
+
from tenacity import (
|
|
12
|
+
before_sleep_log,
|
|
13
|
+
retry,
|
|
14
|
+
retry_if_exception,
|
|
15
|
+
stop_after_attempt,
|
|
16
|
+
wait_exponential,
|
|
17
|
+
)
|
|
10
18
|
|
|
11
19
|
if TYPE_CHECKING:
|
|
12
20
|
from shotgun.agents.conversation_history import ConversationState
|
|
@@ -55,6 +63,35 @@ from .tasks import create_tasks_agent
|
|
|
55
63
|
logger = logging.getLogger(__name__)
|
|
56
64
|
|
|
57
65
|
|
|
66
|
+
def _is_retryable_error(exception: BaseException) -> bool:
|
|
67
|
+
"""Check if exception should trigger a retry.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
exception: The exception to check.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if the exception is a transient error that should be retried.
|
|
74
|
+
"""
|
|
75
|
+
# ValueError for truncated/incomplete JSON
|
|
76
|
+
if isinstance(exception, ValueError):
|
|
77
|
+
error_str = str(exception)
|
|
78
|
+
return "EOF while parsing" in error_str or (
|
|
79
|
+
"JSON" in error_str and "parsing" in error_str
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# API errors (overload, rate limits)
|
|
83
|
+
exception_name = type(exception).__name__
|
|
84
|
+
if "APIStatusError" in exception_name:
|
|
85
|
+
error_str = str(exception)
|
|
86
|
+
return "overload" in error_str.lower() or "rate" in error_str.lower()
|
|
87
|
+
|
|
88
|
+
# Network errors
|
|
89
|
+
if "ConnectionError" in exception_name or "TimeoutError" in exception_name:
|
|
90
|
+
return True
|
|
91
|
+
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
|
|
58
95
|
class MessageHistoryUpdated(Message):
|
|
59
96
|
"""Event posted when the message history is updated."""
|
|
60
97
|
|
|
@@ -268,6 +305,49 @@ class AgentManager(Widget):
|
|
|
268
305
|
f"Invalid agent type: {agent_type}. Must be one of: {', '.join(e.value for e in AgentType)}"
|
|
269
306
|
) from None
|
|
270
307
|
|
|
308
|
+
@retry(
|
|
309
|
+
stop=stop_after_attempt(3),
|
|
310
|
+
wait=wait_exponential(multiplier=1, min=1, max=8),
|
|
311
|
+
retry=retry_if_exception(_is_retryable_error),
|
|
312
|
+
before_sleep=before_sleep_log(logger, logging.WARNING),
|
|
313
|
+
reraise=True,
|
|
314
|
+
)
|
|
315
|
+
async def _run_agent_with_retry(
|
|
316
|
+
self,
|
|
317
|
+
agent: Agent[AgentDeps, AgentResponse],
|
|
318
|
+
prompt: str | None,
|
|
319
|
+
deps: AgentDeps,
|
|
320
|
+
usage_limits: UsageLimits | None,
|
|
321
|
+
message_history: list[ModelMessage],
|
|
322
|
+
event_stream_handler: Any,
|
|
323
|
+
**kwargs: Any,
|
|
324
|
+
) -> AgentRunResult[AgentResponse]:
|
|
325
|
+
"""Run agent with automatic retry on transient errors.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
agent: The agent to run.
|
|
329
|
+
prompt: Optional prompt to send to the agent.
|
|
330
|
+
deps: Agent dependencies.
|
|
331
|
+
usage_limits: Optional usage limits.
|
|
332
|
+
message_history: Message history to provide to agent.
|
|
333
|
+
event_stream_handler: Event handler for streaming.
|
|
334
|
+
**kwargs: Additional keyword arguments.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
The agent run result.
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
Various exceptions if all retries fail.
|
|
341
|
+
"""
|
|
342
|
+
return await agent.run(
|
|
343
|
+
prompt,
|
|
344
|
+
deps=deps,
|
|
345
|
+
usage_limits=usage_limits,
|
|
346
|
+
message_history=message_history,
|
|
347
|
+
event_stream_handler=event_stream_handler,
|
|
348
|
+
**kwargs,
|
|
349
|
+
)
|
|
350
|
+
|
|
271
351
|
async def run(
|
|
272
352
|
self,
|
|
273
353
|
prompt: str | None = None,
|
|
@@ -394,8 +474,9 @@ class AgentManager(Widget):
|
|
|
394
474
|
)
|
|
395
475
|
|
|
396
476
|
try:
|
|
397
|
-
result: AgentRunResult[AgentResponse] = await self.
|
|
398
|
-
|
|
477
|
+
result: AgentRunResult[AgentResponse] = await self._run_agent_with_retry(
|
|
478
|
+
agent=self.current_agent,
|
|
479
|
+
prompt=prompt,
|
|
399
480
|
deps=deps,
|
|
400
481
|
usage_limits=usage_limits,
|
|
401
482
|
message_history=message_history,
|
|
@@ -404,6 +485,36 @@ class AgentManager(Widget):
|
|
|
404
485
|
else None,
|
|
405
486
|
**kwargs,
|
|
406
487
|
)
|
|
488
|
+
except ValueError as e:
|
|
489
|
+
# Handle truncated/incomplete JSON in tool calls specifically
|
|
490
|
+
error_str = str(e)
|
|
491
|
+
if "EOF while parsing" in error_str or (
|
|
492
|
+
"JSON" in error_str and "parsing" in error_str
|
|
493
|
+
):
|
|
494
|
+
logger.error(
|
|
495
|
+
"Tool call with truncated/incomplete JSON arguments detected",
|
|
496
|
+
extra={
|
|
497
|
+
"agent_mode": self._current_agent_type.value,
|
|
498
|
+
"model_name": model_name,
|
|
499
|
+
"error": error_str,
|
|
500
|
+
},
|
|
501
|
+
)
|
|
502
|
+
logfire.error(
|
|
503
|
+
"Tool call with truncated JSON arguments",
|
|
504
|
+
agent_mode=self._current_agent_type.value,
|
|
505
|
+
model_name=model_name,
|
|
506
|
+
error=error_str,
|
|
507
|
+
)
|
|
508
|
+
# Add helpful hint message for the user
|
|
509
|
+
self.ui_message_history.append(
|
|
510
|
+
HintMessage(
|
|
511
|
+
message="⚠️ The agent attempted an operation with arguments that were too large (truncated JSON). "
|
|
512
|
+
"Try breaking your request into smaller steps or more focused contracts."
|
|
513
|
+
)
|
|
514
|
+
)
|
|
515
|
+
self._post_messages_updated()
|
|
516
|
+
# Re-raise to maintain error visibility
|
|
517
|
+
raise
|
|
407
518
|
except Exception as e:
|
|
408
519
|
# Log the error with full stack trace to shotgun.log and Logfire
|
|
409
520
|
logger.exception(
|
|
@@ -427,13 +538,40 @@ class AgentManager(Widget):
|
|
|
427
538
|
|
|
428
539
|
# Agent ALWAYS returns AgentResponse with structured output
|
|
429
540
|
agent_response = result.output
|
|
430
|
-
logger.debug(
|
|
541
|
+
logger.debug(
|
|
542
|
+
"Agent returned structured AgentResponse",
|
|
543
|
+
extra={
|
|
544
|
+
"has_response": agent_response.response is not None,
|
|
545
|
+
"response_length": len(agent_response.response)
|
|
546
|
+
if agent_response.response
|
|
547
|
+
else 0,
|
|
548
|
+
"response_preview": agent_response.response[:100] + "..."
|
|
549
|
+
if agent_response.response and len(agent_response.response) > 100
|
|
550
|
+
else agent_response.response or "(empty)",
|
|
551
|
+
"has_clarifying_questions": bool(agent_response.clarifying_questions),
|
|
552
|
+
"num_clarifying_questions": len(agent_response.clarifying_questions)
|
|
553
|
+
if agent_response.clarifying_questions
|
|
554
|
+
else 0,
|
|
555
|
+
},
|
|
556
|
+
)
|
|
431
557
|
|
|
432
558
|
# Always add the agent's response messages to maintain conversation history
|
|
433
559
|
self.ui_message_history = original_messages + cast(
|
|
434
560
|
list[ModelRequest | ModelResponse | HintMessage], result.new_messages()
|
|
435
561
|
)
|
|
436
562
|
|
|
563
|
+
# Get file operations early so we can use them for contextual messages
|
|
564
|
+
file_operations = deps.file_tracker.operations.copy()
|
|
565
|
+
self.recently_change_files = file_operations
|
|
566
|
+
|
|
567
|
+
logger.debug(
|
|
568
|
+
"File operations tracked",
|
|
569
|
+
extra={
|
|
570
|
+
"num_file_operations": len(file_operations),
|
|
571
|
+
"operation_files": [Path(op.file_path).name for op in file_operations],
|
|
572
|
+
},
|
|
573
|
+
)
|
|
574
|
+
|
|
437
575
|
# Check if there are clarifying questions
|
|
438
576
|
if agent_response.clarifying_questions:
|
|
439
577
|
logger.info(
|
|
@@ -480,12 +618,50 @@ class AgentManager(Widget):
|
|
|
480
618
|
response_text=agent_response.response,
|
|
481
619
|
)
|
|
482
620
|
)
|
|
621
|
+
|
|
622
|
+
# Post UI update with hint messages and file operations
|
|
623
|
+
logger.debug(
|
|
624
|
+
"Posting UI update for Q&A mode with hint messages and file operations"
|
|
625
|
+
)
|
|
626
|
+
self._post_messages_updated(file_operations)
|
|
483
627
|
else:
|
|
484
|
-
# No clarifying questions -
|
|
628
|
+
# No clarifying questions - show the response or a default success message
|
|
485
629
|
if agent_response.response and agent_response.response.strip():
|
|
630
|
+
logger.debug(
|
|
631
|
+
"Adding agent response as hint",
|
|
632
|
+
extra={
|
|
633
|
+
"response_preview": agent_response.response[:100] + "..."
|
|
634
|
+
if len(agent_response.response) > 100
|
|
635
|
+
else agent_response.response,
|
|
636
|
+
"has_file_operations": len(file_operations) > 0,
|
|
637
|
+
},
|
|
638
|
+
)
|
|
486
639
|
self.ui_message_history.append(
|
|
487
640
|
HintMessage(message=agent_response.response)
|
|
488
641
|
)
|
|
642
|
+
else:
|
|
643
|
+
# Fallback: response is empty or whitespace
|
|
644
|
+
logger.debug(
|
|
645
|
+
"Agent response was empty, using fallback completion message",
|
|
646
|
+
extra={"has_file_operations": len(file_operations) > 0},
|
|
647
|
+
)
|
|
648
|
+
# Show contextual message based on whether files were modified
|
|
649
|
+
if file_operations:
|
|
650
|
+
self.ui_message_history.append(
|
|
651
|
+
HintMessage(
|
|
652
|
+
message="✅ Task completed - files have been modified"
|
|
653
|
+
)
|
|
654
|
+
)
|
|
655
|
+
else:
|
|
656
|
+
self.ui_message_history.append(
|
|
657
|
+
HintMessage(message="✅ Task completed")
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
# Post UI update immediately so user sees the response without delay
|
|
661
|
+
logger.debug(
|
|
662
|
+
"Posting immediate UI update with hint message and file operations"
|
|
663
|
+
)
|
|
664
|
+
self._post_messages_updated(file_operations)
|
|
489
665
|
|
|
490
666
|
# Apply compaction to persistent message history to prevent cascading growth
|
|
491
667
|
all_messages = result.all_messages()
|
|
@@ -517,16 +693,18 @@ class AgentManager(Widget):
|
|
|
517
693
|
self.message_history = all_messages
|
|
518
694
|
|
|
519
695
|
usage = result.usage()
|
|
520
|
-
deps.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
696
|
+
if hasattr(deps, "llm_model") and deps.llm_model is not None:
|
|
697
|
+
deps.usage_manager.add_usage(
|
|
698
|
+
usage, model_name=deps.llm_model.name, provider=deps.llm_model.provider
|
|
699
|
+
)
|
|
700
|
+
else:
|
|
701
|
+
logger.warning(
|
|
702
|
+
"llm_model is None, skipping usage tracking",
|
|
703
|
+
extra={"agent_mode": self._current_agent_type.value},
|
|
704
|
+
)
|
|
527
705
|
|
|
528
|
-
#
|
|
529
|
-
|
|
706
|
+
# UI updates are now posted immediately in each branch (Q&A or non-Q&A)
|
|
707
|
+
# before compaction, so no duplicate posting needed here
|
|
530
708
|
|
|
531
709
|
return result
|
|
532
710
|
|
shotgun/agents/common.py
CHANGED
|
@@ -384,23 +384,48 @@ def get_agent_existing_files(agent_mode: AgentType | None = None) -> list[str]:
|
|
|
384
384
|
relative_path = file_path.relative_to(base_path)
|
|
385
385
|
existing_files.append(str(relative_path))
|
|
386
386
|
else:
|
|
387
|
-
# For other agents, check
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
#
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
#
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
387
|
+
# For other agents, check files/directories they have access to
|
|
388
|
+
allowed_paths_raw = AGENT_DIRECTORIES[agent_mode]
|
|
389
|
+
|
|
390
|
+
# Convert single Path/string to list of Paths for uniform handling
|
|
391
|
+
if isinstance(allowed_paths_raw, str):
|
|
392
|
+
# Special case: "*" means export agent (shouldn't reach here but handle it)
|
|
393
|
+
allowed_paths = (
|
|
394
|
+
[Path(allowed_paths_raw)] if allowed_paths_raw != "*" else []
|
|
395
|
+
)
|
|
396
|
+
elif isinstance(allowed_paths_raw, Path):
|
|
397
|
+
allowed_paths = [allowed_paths_raw]
|
|
398
|
+
else:
|
|
399
|
+
# Already a list
|
|
400
|
+
allowed_paths = allowed_paths_raw
|
|
401
|
+
|
|
402
|
+
# Check each allowed path
|
|
403
|
+
for allowed_path in allowed_paths:
|
|
404
|
+
allowed_str = str(allowed_path)
|
|
405
|
+
|
|
406
|
+
# Check if it's a directory (no .md suffix)
|
|
407
|
+
if not allowed_path.suffix or not allowed_str.endswith(".md"):
|
|
408
|
+
# It's a directory - list all files within it
|
|
409
|
+
dir_path = base_path / allowed_str
|
|
410
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
411
|
+
for file_path in dir_path.rglob("*"):
|
|
412
|
+
if file_path.is_file():
|
|
413
|
+
relative_path = file_path.relative_to(base_path)
|
|
414
|
+
existing_files.append(str(relative_path))
|
|
415
|
+
else:
|
|
416
|
+
# It's a file - check if it exists
|
|
417
|
+
file_path = base_path / allowed_str
|
|
418
|
+
if file_path.exists():
|
|
419
|
+
existing_files.append(allowed_str)
|
|
420
|
+
|
|
421
|
+
# Also check for associated directory (e.g., research/ for research.md)
|
|
422
|
+
base_name = allowed_str.replace(".md", "")
|
|
423
|
+
dir_path = base_path / base_name
|
|
424
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
425
|
+
for file_path in dir_path.rglob("*"):
|
|
426
|
+
if file_path.is_file():
|
|
427
|
+
relative_path = file_path.relative_to(base_path)
|
|
428
|
+
existing_files.append(str(relative_path))
|
|
404
429
|
|
|
405
430
|
return existing_files
|
|
406
431
|
|
|
@@ -15,11 +15,18 @@ from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
|
15
15
|
logger = get_logger(__name__)
|
|
16
16
|
|
|
17
17
|
# Map agent modes to their allowed directories/files (in workflow order)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
# Values can be:
|
|
19
|
+
# - A Path: exact file (e.g., Path("research.md"))
|
|
20
|
+
# - A list of Paths: multiple allowed files/directories (e.g., [Path("specification.md"), Path("contracts")])
|
|
21
|
+
# - "*": any file except protected files (for export agent)
|
|
22
|
+
AGENT_DIRECTORIES: dict[AgentType, str | Path | list[Path]] = {
|
|
23
|
+
AgentType.RESEARCH: Path("research.md"),
|
|
24
|
+
AgentType.SPECIFY: [
|
|
25
|
+
Path("specification.md"),
|
|
26
|
+
Path("contracts"),
|
|
27
|
+
], # Specify can write specs and contract files
|
|
28
|
+
AgentType.PLAN: Path("plan.md"),
|
|
29
|
+
AgentType.TASKS: Path("tasks.md"),
|
|
23
30
|
AgentType.EXPORT: "*", # Export agent can write anywhere except protected files
|
|
24
31
|
}
|
|
25
32
|
|
|
@@ -60,13 +67,52 @@ def _validate_agent_scoped_path(filename: str, agent_mode: AgentType | None) ->
|
|
|
60
67
|
# Allow writing anywhere else in .shotgun directory
|
|
61
68
|
full_path = (base_path / filename).resolve()
|
|
62
69
|
else:
|
|
63
|
-
# For other agents,
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
# For other agents, check if they have access to the requested file
|
|
71
|
+
allowed_paths_raw = AGENT_DIRECTORIES[agent_mode]
|
|
72
|
+
|
|
73
|
+
# Convert single Path/string to list of Paths for uniform handling
|
|
74
|
+
if isinstance(allowed_paths_raw, str):
|
|
75
|
+
# Special case: "*" means export agent
|
|
76
|
+
allowed_paths = (
|
|
77
|
+
[Path(allowed_paths_raw)] if allowed_paths_raw != "*" else []
|
|
78
|
+
)
|
|
79
|
+
elif isinstance(allowed_paths_raw, Path):
|
|
80
|
+
allowed_paths = [allowed_paths_raw]
|
|
81
|
+
else:
|
|
82
|
+
# Already a list
|
|
83
|
+
allowed_paths = allowed_paths_raw
|
|
84
|
+
|
|
85
|
+
# Check if filename matches any allowed path
|
|
86
|
+
is_allowed = False
|
|
87
|
+
for allowed_path in allowed_paths:
|
|
88
|
+
allowed_str = str(allowed_path)
|
|
89
|
+
|
|
90
|
+
# Check if it's a directory (no .md extension or suffix)
|
|
91
|
+
# Directories: Path("contracts") has no suffix, files: Path("spec.md") has .md suffix
|
|
92
|
+
if not allowed_path.suffix or (
|
|
93
|
+
allowed_path.suffix and not allowed_str.endswith(".md")
|
|
94
|
+
):
|
|
95
|
+
# Directory - allow any file within this directory
|
|
96
|
+
# Check both "contracts/file.py" and "contracts" prefix
|
|
97
|
+
if (
|
|
98
|
+
filename.startswith(allowed_str + "/")
|
|
99
|
+
or filename == allowed_str
|
|
100
|
+
):
|
|
101
|
+
is_allowed = True
|
|
102
|
+
break
|
|
103
|
+
else:
|
|
104
|
+
# Exact file match
|
|
105
|
+
if filename == allowed_str:
|
|
106
|
+
is_allowed = True
|
|
107
|
+
break
|
|
108
|
+
|
|
109
|
+
if not is_allowed:
|
|
110
|
+
allowed_display = ", ".join(f"'{p}'" for p in allowed_paths)
|
|
66
111
|
raise ValueError(
|
|
67
|
-
f"{agent_mode.value.capitalize()} agent can only write to
|
|
112
|
+
f"{agent_mode.value.capitalize()} agent can only write to {allowed_display}. "
|
|
68
113
|
f"Attempted to write to '{filename}'"
|
|
69
114
|
)
|
|
115
|
+
|
|
70
116
|
full_path = (base_path / filename).resolve()
|
|
71
117
|
else:
|
|
72
118
|
# No agent mode specified, fall back to old validation
|
shotgun/main.py
CHANGED
|
@@ -125,6 +125,34 @@ def main(
|
|
|
125
125
|
help="Continue previous TUI conversation",
|
|
126
126
|
),
|
|
127
127
|
] = False,
|
|
128
|
+
web: Annotated[
|
|
129
|
+
bool,
|
|
130
|
+
typer.Option(
|
|
131
|
+
"--web",
|
|
132
|
+
help="Serve TUI as web application",
|
|
133
|
+
),
|
|
134
|
+
] = False,
|
|
135
|
+
port: Annotated[
|
|
136
|
+
int,
|
|
137
|
+
typer.Option(
|
|
138
|
+
"--port",
|
|
139
|
+
help="Port for web server (only used with --web)",
|
|
140
|
+
),
|
|
141
|
+
] = 8000,
|
|
142
|
+
host: Annotated[
|
|
143
|
+
str,
|
|
144
|
+
typer.Option(
|
|
145
|
+
"--host",
|
|
146
|
+
help="Host address for web server (only used with --web)",
|
|
147
|
+
),
|
|
148
|
+
] = "localhost",
|
|
149
|
+
public_url: Annotated[
|
|
150
|
+
str | None,
|
|
151
|
+
typer.Option(
|
|
152
|
+
"--public-url",
|
|
153
|
+
help="Public URL if behind proxy (only used with --web)",
|
|
154
|
+
),
|
|
155
|
+
] = None,
|
|
128
156
|
) -> None:
|
|
129
157
|
"""Shotgun - AI-powered CLI tool."""
|
|
130
158
|
logger.debug("Starting shotgun CLI application")
|
|
@@ -134,16 +162,32 @@ def main(
|
|
|
134
162
|
perform_auto_update_async(no_update_check=no_update_check)
|
|
135
163
|
|
|
136
164
|
if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
165
|
+
if web:
|
|
166
|
+
logger.debug("Launching shotgun TUI as web application")
|
|
167
|
+
try:
|
|
168
|
+
tui_app.serve(
|
|
169
|
+
host=host,
|
|
170
|
+
port=port,
|
|
171
|
+
public_url=public_url,
|
|
172
|
+
no_update_check=no_update_check,
|
|
173
|
+
continue_session=continue_session,
|
|
174
|
+
)
|
|
175
|
+
finally:
|
|
176
|
+
# Ensure PostHog is shut down cleanly even if server exits unexpectedly
|
|
177
|
+
from shotgun.posthog_telemetry import shutdown
|
|
178
|
+
|
|
179
|
+
shutdown()
|
|
180
|
+
else:
|
|
181
|
+
logger.debug("Launching shotgun TUI application")
|
|
182
|
+
try:
|
|
183
|
+
tui_app.run(
|
|
184
|
+
no_update_check=no_update_check, continue_session=continue_session
|
|
185
|
+
)
|
|
186
|
+
finally:
|
|
187
|
+
# Ensure PostHog is shut down cleanly even if TUI exits unexpectedly
|
|
188
|
+
from shotgun.posthog_telemetry import shutdown
|
|
189
|
+
|
|
190
|
+
shutdown()
|
|
147
191
|
raise typer.Exit()
|
|
148
192
|
|
|
149
193
|
# For CLI commands, register PostHog shutdown handler
|
|
@@ -8,14 +8,281 @@ Transform requirements into detailed, actionable specifications that development
|
|
|
8
8
|
|
|
9
9
|
## MEMORY MANAGEMENT PROTOCOL
|
|
10
10
|
|
|
11
|
-
- You have exclusive write access to: `specification.md`
|
|
11
|
+
- You have exclusive write access to: `specification.md` and `.shotgun/contracts/*`
|
|
12
12
|
- SHOULD READ `research.md` for context but CANNOT write to it
|
|
13
|
-
-
|
|
13
|
+
- **specification.md is for PROSE ONLY** - no code, no implementation details, no type definitions
|
|
14
|
+
- **All code goes in .shotgun/contracts/** - types, interfaces, schemas
|
|
15
|
+
- specification.md describes WHAT and WHY, contracts/ show HOW with actual code
|
|
16
|
+
- This is your persistent memory store - ALWAYS load specification.md first
|
|
14
17
|
- Compress content regularly to stay within context limits
|
|
15
|
-
- Keep your
|
|
18
|
+
- Keep your files updated as you work - they're your memory across sessions
|
|
16
19
|
- When adding new specifications, review and consolidate overlapping requirements
|
|
17
20
|
- Structure specifications for easy reference by the next agents
|
|
18
21
|
|
|
22
|
+
## WHAT GOES IN SPECIFICATION.MD
|
|
23
|
+
|
|
24
|
+
specification.md is your prose documentation file. It should contain:
|
|
25
|
+
|
|
26
|
+
**INCLUDE in specification.md:**
|
|
27
|
+
- Requirements and business context (what needs to be built and why)
|
|
28
|
+
- Architecture overview and system design decisions
|
|
29
|
+
- Component descriptions and how they interact
|
|
30
|
+
- User workflows and use cases
|
|
31
|
+
- Directory structure as succinct prose (e.g., "src/ contains main code, tests/ contains test files")
|
|
32
|
+
- Dependencies listed in prose (e.g., "Requires TypeScript 5.0+, React 18, and PostgreSQL")
|
|
33
|
+
- Configuration requirements described (e.g., "App needs database URL and API key in environment")
|
|
34
|
+
- Testing strategies and acceptance criteria
|
|
35
|
+
- References to contract files (e.g., "See contracts/user_models.py for User type definition")
|
|
36
|
+
|
|
37
|
+
**DO NOT INCLUDE in specification.md:**
|
|
38
|
+
- Code blocks, type definitions, or function signatures (those go in contracts/)
|
|
39
|
+
- Implementation details or algorithms (describe behavior instead)
|
|
40
|
+
- Actual configuration files or build manifests (describe what's needed instead)
|
|
41
|
+
- Directory trees or file listings (keep structure descriptions succinct)
|
|
42
|
+
|
|
43
|
+
**When you need to show structure:** Reference contract files instead of inline code.
|
|
44
|
+
Example: "User authentication uses OAuth2. See contracts/auth_types.ts for AuthUser and AuthToken types."
|
|
45
|
+
|
|
46
|
+
## CONTRACT FILES
|
|
47
|
+
|
|
48
|
+
Contract files define the **interfaces and types** that form contracts between components.
|
|
49
|
+
They contain actual code that shows structure, not prose descriptions.
|
|
50
|
+
|
|
51
|
+
**ONLY put these in `.shotgun/contracts/` (language-agnostic):**
|
|
52
|
+
- **Type definitions ONLY** - Shape and structure, NO behavior or logic:
|
|
53
|
+
- Python: Pydantic models, dataclasses, `typing.Protocol` classes (interface definitions)
|
|
54
|
+
- TypeScript: interfaces, type aliases
|
|
55
|
+
- Rust: struct definitions
|
|
56
|
+
- Java: interfaces, POJOs
|
|
57
|
+
- C++: header files with class/struct declarations
|
|
58
|
+
- Go: interface types, struct definitions
|
|
59
|
+
- **Schema definitions**: API contracts and data schemas
|
|
60
|
+
- OpenAPI/Swagger specs (openapi.json, openapi.yaml)
|
|
61
|
+
- JSON Schema definitions
|
|
62
|
+
- GraphQL schemas
|
|
63
|
+
- Protobuf definitions
|
|
64
|
+
- **Protocol/Interface classes**: Pure interface definitions with method signatures only
|
|
65
|
+
- Python: `class Storage(Protocol): def save(self, data: str) -> None: ...`
|
|
66
|
+
- Use `...` (Ellipsis) for protocol methods, NOT `pass`
|
|
67
|
+
|
|
68
|
+
**NEVER put these in `.shotgun/contracts/` - NO EXECUTABLE CODE:**
|
|
69
|
+
- ❌ **Functions or methods with implementations** (even with `pass` or empty bodies)
|
|
70
|
+
- ❌ **Helper functions** with any logic whatsoever
|
|
71
|
+
- ❌ **Classes with method implementations** (use Protocol classes instead)
|
|
72
|
+
- ❌ **Standalone functions** like `def main(): pass` or `def validate_input(x): ...`
|
|
73
|
+
- ❌ **Code with behavior**: loops, conditionals, data manipulation, computations
|
|
74
|
+
- ❌ **Data constants**: dictionaries, lists, or any runtime values
|
|
75
|
+
- ❌ **`if __name__ == "__main__":` blocks** or any executable code
|
|
76
|
+
- Build/dependency configs (pyproject.toml, package.json, Cargo.toml, requirements.txt)
|
|
77
|
+
- Directory structure files (directory_structure.txt)
|
|
78
|
+
- Configuration templates (.env, config.yaml, example configs)
|
|
79
|
+
- Documentation or markdown files
|
|
80
|
+
- SQL migration files or database dumps
|
|
81
|
+
|
|
82
|
+
**These belong in specification.md instead:**
|
|
83
|
+
- Directory structure (as succinct prose: "src/ contains modules, tests/ has unit tests")
|
|
84
|
+
- Dependencies (as prose: "Requires Rust 1.70+, tokio, serde")
|
|
85
|
+
- Configuration needs (describe: "App needs DB_URL and API_KEY environment variables")
|
|
86
|
+
|
|
87
|
+
**Guidelines for contract files:**
|
|
88
|
+
- Keep each file focused on a single domain (e.g., user_types.ts, payment_models.py)
|
|
89
|
+
- Reference from specification.md: "See contracts/user_types.ts for User and Profile types"
|
|
90
|
+
- Use descriptive filenames: `auth_models.py`, `api_spec.json`, `database_types.rs`
|
|
91
|
+
- Keep files under 500 lines to avoid truncation
|
|
92
|
+
- When contracts grow large, split into focused files
|
|
93
|
+
|
|
94
|
+
**Example workflow:**
|
|
95
|
+
1. In specification.md: "Authentication system with JWT tokens. See contracts/auth_types.ts for types."
|
|
96
|
+
2. Create contract file: `write_file("contracts/auth_types.ts", content)` with actual TypeScript interfaces
|
|
97
|
+
3. Create contract file: `write_file("contracts/auth_api.json", content)` with actual OpenAPI spec
|
|
98
|
+
4. Coding agents can directly use these contracts to implement features
|
|
99
|
+
|
|
100
|
+
## HOW TO WRITE CONTRACT FILES
|
|
101
|
+
|
|
102
|
+
**CRITICAL - Always use correct file paths with write_file():**
|
|
103
|
+
|
|
104
|
+
Your working directory is `.shotgun/`, so paths should be relative to that directory.
|
|
105
|
+
|
|
106
|
+
<GOOD_EXAMPLES>
|
|
107
|
+
✅ `write_file("contracts/user_models.py", content)` - Correct path for Python models
|
|
108
|
+
✅ `write_file("contracts/auth_types.ts", content)` - Correct path for TypeScript types
|
|
109
|
+
✅ `write_file("contracts/api_spec.json", content)` - Correct path for OpenAPI spec
|
|
110
|
+
✅ `write_file("contracts/payment_service.rs", content)` - Correct path for Rust code
|
|
111
|
+
</GOOD_EXAMPLES>
|
|
112
|
+
|
|
113
|
+
<BAD_EXAMPLES>
|
|
114
|
+
❌ `write_file(".shotgun/contracts/user_models.py", content)` - WRONG! Don't include .shotgun/ prefix
|
|
115
|
+
❌ `write_file("contracts/directory_structure.txt", content)` - WRONG! No documentation files
|
|
116
|
+
❌ `write_file("contracts/pyproject.toml", content)` - WRONG! No build configs in contracts/
|
|
117
|
+
❌ `write_file("contracts/requirements.txt", content)` - WRONG! No dependency lists in contracts/
|
|
118
|
+
❌ `write_file("contracts/config.yaml", content)` - WRONG! No config templates in contracts/
|
|
119
|
+
</BAD_EXAMPLES>
|
|
120
|
+
|
|
121
|
+
**Path format rule:** Always use `contracts/filename.ext`, never `.shotgun/contracts/filename.ext`
|
|
122
|
+
|
|
123
|
+
**Language-specific examples:**
|
|
124
|
+
|
|
125
|
+
<PYTHON_EXAMPLE>
|
|
126
|
+
# Python Pydantic model contract
|
|
127
|
+
from pydantic import BaseModel, Field
|
|
128
|
+
from typing import Optional
|
|
129
|
+
|
|
130
|
+
class User(BaseModel):
|
|
131
|
+
"""User model contract."""
|
|
132
|
+
id: int
|
|
133
|
+
email: str = Field(..., description="User email address")
|
|
134
|
+
username: str
|
|
135
|
+
is_active: bool = True
|
|
136
|
+
role: Optional[str] = None
|
|
137
|
+
|
|
138
|
+
# Save as: write_file("contracts/user_models.py", content)
|
|
139
|
+
</PYTHON_EXAMPLE>
|
|
140
|
+
|
|
141
|
+
<TYPESCRIPT_EXAMPLE>
|
|
142
|
+
// TypeScript interface contract
|
|
143
|
+
interface User {
|
|
144
|
+
id: number;
|
|
145
|
+
email: string;
|
|
146
|
+
username: string;
|
|
147
|
+
isActive: boolean;
|
|
148
|
+
role?: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface AuthToken {
|
|
152
|
+
token: string;
|
|
153
|
+
expiresAt: Date;
|
|
154
|
+
userId: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Save as: write_file("contracts/auth_types.ts", content)
|
|
158
|
+
</TYPESCRIPT_EXAMPLE>
|
|
159
|
+
|
|
160
|
+
<RUST_EXAMPLE>
|
|
161
|
+
// Rust struct contract
|
|
162
|
+
use serde::{Deserialize, Serialize};
|
|
163
|
+
|
|
164
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
165
|
+
pub struct User {
|
|
166
|
+
pub id: u64,
|
|
167
|
+
pub email: String,
|
|
168
|
+
pub username: String,
|
|
169
|
+
pub is_active: bool,
|
|
170
|
+
pub role: Option<String>,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Save as: write_file("contracts/user_types.rs", content)
|
|
174
|
+
</RUST_EXAMPLE>
|
|
175
|
+
|
|
176
|
+
<OPENAPI_EXAMPLE>
|
|
177
|
+
{
|
|
178
|
+
"openapi": "3.0.0",
|
|
179
|
+
"info": {
|
|
180
|
+
"title": "User API",
|
|
181
|
+
"version": "1.0.0"
|
|
182
|
+
},
|
|
183
|
+
"paths": {
|
|
184
|
+
"/users": {
|
|
185
|
+
"get": {
|
|
186
|
+
"summary": "List users",
|
|
187
|
+
"responses": {
|
|
188
|
+
"200": {
|
|
189
|
+
"description": "Successful response",
|
|
190
|
+
"content": {
|
|
191
|
+
"application/json": {
|
|
192
|
+
"schema": {
|
|
193
|
+
"type": "array",
|
|
194
|
+
"items": { "$ref": "#/components/schemas/User" }
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
"components": {
|
|
204
|
+
"schemas": {
|
|
205
|
+
"User": {
|
|
206
|
+
"type": "object",
|
|
207
|
+
"properties": {
|
|
208
|
+
"id": { "type": "integer" },
|
|
209
|
+
"email": { "type": "string" },
|
|
210
|
+
"username": { "type": "string" }
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Save as: write_file("contracts/user_api.json", content)
|
|
218
|
+
</OPENAPI_EXAMPLE>
|
|
219
|
+
|
|
220
|
+
## WHAT IS ALLOWED vs WHAT IS FORBIDDEN
|
|
221
|
+
|
|
222
|
+
**✅ ALLOWED - Type Definitions (Shape and Structure):**
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
# ✅ GOOD: Pydantic model (type definition)
|
|
226
|
+
from pydantic import BaseModel
|
|
227
|
+
|
|
228
|
+
class User(BaseModel):
|
|
229
|
+
id: int
|
|
230
|
+
email: str
|
|
231
|
+
username: str
|
|
232
|
+
|
|
233
|
+
# ✅ GOOD: Protocol class (interface definition)
|
|
234
|
+
from typing import Protocol
|
|
235
|
+
|
|
236
|
+
class Storage(Protocol):
|
|
237
|
+
def save(self, data: str) -> None: ...
|
|
238
|
+
def load(self) -> str: ...
|
|
239
|
+
|
|
240
|
+
# ✅ GOOD: Type aliases and enums
|
|
241
|
+
from typing import Literal
|
|
242
|
+
from enum import Enum
|
|
243
|
+
|
|
244
|
+
UserRole = Literal["admin", "user", "guest"]
|
|
245
|
+
|
|
246
|
+
class Status(Enum):
|
|
247
|
+
ACTIVE = "active"
|
|
248
|
+
INACTIVE = "inactive"
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**❌ FORBIDDEN - Executable Code (Behavior and Logic):**
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
# ❌ BAD: Function with pass (executable code)
|
|
255
|
+
def main() -> int:
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
# ❌ BAD: Function with implementation
|
|
259
|
+
def validate_input(x: str) -> str:
|
|
260
|
+
return x.strip()
|
|
261
|
+
|
|
262
|
+
# ❌ BAD: Class with method implementations
|
|
263
|
+
class HistoryManager:
|
|
264
|
+
def __init__(self):
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
def add_message(self, msg: str):
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
# ❌ BAD: Data constants (runtime values)
|
|
271
|
+
SUPPORTED_PROVIDERS = [
|
|
272
|
+
{"name": "openai", "key": "OPENAI_API_KEY"}
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
# ❌ BAD: Helper functions
|
|
276
|
+
def get_default_config() -> dict:
|
|
277
|
+
return {"model": "gpt-4"}
|
|
278
|
+
|
|
279
|
+
# ❌ BAD: Executable code blocks
|
|
280
|
+
if __name__ == "__main__":
|
|
281
|
+
main()
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Remember**: Contracts define **SHAPES** (types, interfaces, schemas), NOT **BEHAVIOR** (functions, logic, implementations).
|
|
285
|
+
|
|
19
286
|
## AI AGENT PIPELINE AWARENESS
|
|
20
287
|
|
|
21
288
|
**CRITICAL**: Your output will be consumed by AI coding agents (Claude Code, Cursor, Windsurf, etc.)
|
shotgun/tui/app.py
CHANGED
|
@@ -152,5 +152,121 @@ def run(no_update_check: bool = False, continue_session: bool = False) -> None:
|
|
|
152
152
|
app.run(inline_no_clear=True)
|
|
153
153
|
|
|
154
154
|
|
|
155
|
+
def serve(
|
|
156
|
+
host: str = "localhost",
|
|
157
|
+
port: int = 8000,
|
|
158
|
+
public_url: str | None = None,
|
|
159
|
+
no_update_check: bool = False,
|
|
160
|
+
continue_session: bool = False,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Serve the TUI application as a web application.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
host: Host address for the web server.
|
|
166
|
+
port: Port number for the web server.
|
|
167
|
+
public_url: Public URL if behind a proxy.
|
|
168
|
+
no_update_check: If True, disable automatic update checks.
|
|
169
|
+
continue_session: If True, continue from previous conversation.
|
|
170
|
+
"""
|
|
171
|
+
# Clean up any corrupted databases BEFORE starting the TUI
|
|
172
|
+
# This prevents crashes from corrupted databases during initialization
|
|
173
|
+
import asyncio
|
|
174
|
+
|
|
175
|
+
from textual_serve.server import Server
|
|
176
|
+
|
|
177
|
+
from shotgun.codebase.core.manager import CodebaseGraphManager
|
|
178
|
+
from shotgun.utils import get_shotgun_home
|
|
179
|
+
|
|
180
|
+
storage_dir = get_shotgun_home() / "codebases"
|
|
181
|
+
manager = CodebaseGraphManager(storage_dir)
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
removed = asyncio.run(manager.cleanup_corrupted_databases())
|
|
185
|
+
if removed:
|
|
186
|
+
logger.info(
|
|
187
|
+
f"Cleaned up {len(removed)} corrupted database(s) before TUI startup"
|
|
188
|
+
)
|
|
189
|
+
except Exception as e:
|
|
190
|
+
logger.error(f"Failed to cleanup corrupted databases: {e}")
|
|
191
|
+
# Continue anyway - the TUI can still function
|
|
192
|
+
|
|
193
|
+
# Create a new event loop after asyncio.run() closes the previous one
|
|
194
|
+
# This is needed for the Server.serve() method
|
|
195
|
+
loop = asyncio.new_event_loop()
|
|
196
|
+
asyncio.set_event_loop(loop)
|
|
197
|
+
|
|
198
|
+
# Build the command string based on flags
|
|
199
|
+
command = "shotgun"
|
|
200
|
+
if no_update_check:
|
|
201
|
+
command += " --no-update-check"
|
|
202
|
+
if continue_session:
|
|
203
|
+
command += " --continue"
|
|
204
|
+
|
|
205
|
+
# Create and start the server with hardcoded title and debug=False
|
|
206
|
+
server = Server(
|
|
207
|
+
command=command,
|
|
208
|
+
host=host,
|
|
209
|
+
port=port,
|
|
210
|
+
title="The Shotgun",
|
|
211
|
+
public_url=public_url,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Set up graceful shutdown on SIGTERM/SIGINT
|
|
215
|
+
import signal
|
|
216
|
+
import sys
|
|
217
|
+
|
|
218
|
+
def signal_handler(_signum: int, _frame: Any) -> None:
|
|
219
|
+
"""Handle shutdown signals gracefully."""
|
|
220
|
+
from shotgun.posthog_telemetry import shutdown
|
|
221
|
+
|
|
222
|
+
logger.info("Received shutdown signal, cleaning up...")
|
|
223
|
+
# Restore stdout/stderr before shutting down
|
|
224
|
+
sys.stdout = original_stdout
|
|
225
|
+
sys.stderr = original_stderr
|
|
226
|
+
shutdown()
|
|
227
|
+
sys.exit(0)
|
|
228
|
+
|
|
229
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
230
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
231
|
+
|
|
232
|
+
# Suppress the textual-serve banner by redirecting stdout/stderr
|
|
233
|
+
import io
|
|
234
|
+
|
|
235
|
+
# Capture and suppress the banner, but show the actual serving URL
|
|
236
|
+
original_stdout = sys.stdout
|
|
237
|
+
original_stderr = sys.stderr
|
|
238
|
+
|
|
239
|
+
captured_output = io.StringIO()
|
|
240
|
+
sys.stdout = captured_output
|
|
241
|
+
sys.stderr = captured_output
|
|
242
|
+
|
|
243
|
+
try:
|
|
244
|
+
# This will print the banner to our captured output
|
|
245
|
+
import logging
|
|
246
|
+
|
|
247
|
+
# Temporarily set logging to ERROR level to suppress INFO messages
|
|
248
|
+
textual_serve_logger = logging.getLogger("textual_serve")
|
|
249
|
+
original_level = textual_serve_logger.level
|
|
250
|
+
textual_serve_logger.setLevel(logging.ERROR)
|
|
251
|
+
|
|
252
|
+
# Print our own message to the original stdout
|
|
253
|
+
sys.stdout = original_stdout
|
|
254
|
+
sys.stderr = original_stderr
|
|
255
|
+
print(f"Serving Shotgun TUI at http://{host}:{port}")
|
|
256
|
+
print("Press Ctrl+C to quit")
|
|
257
|
+
|
|
258
|
+
# Now suppress output again for the serve call
|
|
259
|
+
sys.stdout = captured_output
|
|
260
|
+
sys.stderr = captured_output
|
|
261
|
+
|
|
262
|
+
server.serve(debug=False)
|
|
263
|
+
finally:
|
|
264
|
+
# Restore original stdout/stderr
|
|
265
|
+
sys.stdout = original_stdout
|
|
266
|
+
sys.stderr = original_stderr
|
|
267
|
+
if "textual_serve_logger" in locals():
|
|
268
|
+
textual_serve_logger.setLevel(original_level)
|
|
269
|
+
|
|
270
|
+
|
|
155
271
|
if __name__ == "__main__":
|
|
156
272
|
run()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shotgun-sh
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.8.dev1
|
|
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
|
|
@@ -34,7 +34,9 @@ Requires-Dist: pydantic-ai>=0.0.14
|
|
|
34
34
|
Requires-Dist: rich>=13.0.0
|
|
35
35
|
Requires-Dist: sentencepiece>=0.2.0
|
|
36
36
|
Requires-Dist: sentry-sdk[pure-eval]>=2.0.0
|
|
37
|
+
Requires-Dist: tenacity>=8.0.0
|
|
37
38
|
Requires-Dist: textual-dev>=1.7.0
|
|
39
|
+
Requires-Dist: textual-serve>=0.1.0
|
|
38
40
|
Requires-Dist: textual>=6.1.0
|
|
39
41
|
Requires-Dist: tiktoken>=0.7.0
|
|
40
42
|
Requires-Dist: tree-sitter-go>=0.23.0
|
|
@@ -2,14 +2,14 @@ shotgun/__init__.py,sha256=P40K0fnIsb7SKcQrFnXZ4aREjpWchVDhvM1HxI4cyIQ,104
|
|
|
2
2
|
shotgun/api_endpoints.py,sha256=TvxuJyMrZLy6KZTrR6lrdkG8OBtb3TJ48qaw3pWitO0,526
|
|
3
3
|
shotgun/build_constants.py,sha256=RXNxMz46HaB5jucgMVpw8a2yCJqjbhTOh0PddyEVMN8,713
|
|
4
4
|
shotgun/logging_config.py,sha256=UKenihvgH8OA3W0b8ZFcItYaFJVe9MlsMYlcevyW1HY,7440
|
|
5
|
-
shotgun/main.py,sha256=
|
|
5
|
+
shotgun/main.py,sha256=8t-jw4KZAlvUVvoqMlp0rTVCXtJx4herSheI2N8i-8Y,6445
|
|
6
6
|
shotgun/posthog_telemetry.py,sha256=TOiyBtLg21SttHGWKc4-e-PQgpbq6Uz_4OzlvlxMcZ0,6099
|
|
7
7
|
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=
|
|
12
|
-
shotgun/agents/common.py,sha256=
|
|
11
|
+
shotgun/agents/agent_manager.py,sha256=CgG_Cj1P_ZB7wzwvqqqJ5gxOUVKTGy_o0VS4eaB7NqI,39522
|
|
12
|
+
shotgun/agents/common.py,sha256=0F_dwEc72APvbY1b-lqM9VgPYuY1GpfhjSWv5-1pCB0,19032
|
|
13
13
|
shotgun/agents/conversation_history.py,sha256=c-1PBaG9FXPfSmekWtrD6s6YVT0sd98DdDhzS4a1Hyo,7859
|
|
14
14
|
shotgun/agents/conversation_manager.py,sha256=X3DWZZIqrv0hS1SUasyoxexnLPBUrZMBg4ZmRAipkBE,4429
|
|
15
15
|
shotgun/agents/export.py,sha256=7ZNw7781WJ4peLSeoUc7zxKeaZVxFpow2y4nU4dCfbo,2919
|
|
@@ -42,7 +42,7 @@ shotgun/agents/history/token_counting/sentencepiece_counter.py,sha256=qj1bT7J5nC
|
|
|
42
42
|
shotgun/agents/history/token_counting/tokenizer_cache.py,sha256=Y0V6KMtEwn42M5-zJGAc7YudM8X6m5-j2ekA6YGL5Xk,2868
|
|
43
43
|
shotgun/agents/history/token_counting/utils.py,sha256=d124IDjtd0IYBYrr3gDJGWxSbdP10Vrc7ZistbUosMg,5002
|
|
44
44
|
shotgun/agents/tools/__init__.py,sha256=kYppd4f4MoJcfTEPzkY2rqtxL1suXRGa9IRUm1G82GY,717
|
|
45
|
-
shotgun/agents/tools/file_management.py,sha256=
|
|
45
|
+
shotgun/agents/tools/file_management.py,sha256=7qj2yxzOC_40_swuBIcRcFv6MSZM78mNfsV3SCL_sH4,9205
|
|
46
46
|
shotgun/agents/tools/codebase/__init__.py,sha256=ceAGkK006NeOYaIJBLQsw7Q46sAyCRK9PYDs8feMQVw,661
|
|
47
47
|
shotgun/agents/tools/codebase/codebase_shell.py,sha256=9b7ZStAVFprdGqp1O23ZgwkToMytlUdp_R4MhvmENhc,8584
|
|
48
48
|
shotgun/agents/tools/codebase/directory_lister.py,sha256=eX5GKDSmbKggKDvjPpYMa2WPSGPYQAtUEZ4eN01T0t8,4703
|
|
@@ -90,7 +90,7 @@ shotgun/prompts/agents/__init__.py,sha256=YRIJMbzpArojNX1BP5gfxxois334z_GQga8T-x
|
|
|
90
90
|
shotgun/prompts/agents/export.j2,sha256=DGqVijH1PkpkY0rDauU52u_fMv15frEvOdXAPFZNMM4,17057
|
|
91
91
|
shotgun/prompts/agents/plan.j2,sha256=mbt505NdqmzmPxXzQYJS_gH5vkiVa2a3Dgz2K-15JZk,6093
|
|
92
92
|
shotgun/prompts/agents/research.j2,sha256=QFoSSiF_5v7c78RHaiucZEb9mOC_wF54BVKnJEH_DnI,3964
|
|
93
|
-
shotgun/prompts/agents/specify.j2,sha256=
|
|
93
|
+
shotgun/prompts/agents/specify.j2,sha256=XdB2WehbVmszw9crl6PEHLyLgwvU08MV--ClV3hI4mA,12014
|
|
94
94
|
shotgun/prompts/agents/tasks.j2,sha256=SMvTQPzRR6eHlW3fcj-7Bl-Lh9HWaiF3uAKv77nMdZw,5956
|
|
95
95
|
shotgun/prompts/agents/partials/codebase_understanding.j2,sha256=7WH-PVd-TRBFQUdOdKkwwn9hAUaJznFZMAGHhO7IGGU,5633
|
|
96
96
|
shotgun/prompts/agents/partials/common_agent_system_prompt.j2,sha256=wfjsQGcMTWWGBk9l0pKDnPehG8NrwTHm5FFEqba__LI,2161
|
|
@@ -119,7 +119,7 @@ shotgun/shotgun_web/client.py,sha256=n5DDuVfSa6VPZjhSsfSxQlSFOnhgDHyidRnB8Hv9XF4
|
|
|
119
119
|
shotgun/shotgun_web/constants.py,sha256=eNvtjlu81bAVQaCwZXOVjSpDopUm9pf34XuZEvuMiko,661
|
|
120
120
|
shotgun/shotgun_web/models.py,sha256=Ie9VfqKZM2tIJhIjentU9qLoNaMZvnUJaIu-xg9kQsA,1391
|
|
121
121
|
shotgun/tui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
122
|
-
shotgun/tui/app.py,sha256=
|
|
122
|
+
shotgun/tui/app.py,sha256=dBoniec5iscVozM7BTDiKAwMWLUvzw2CioLiTqtmoqo,9184
|
|
123
123
|
shotgun/tui/filtered_codebase_service.py,sha256=lJ8gTMhIveTatmvmGLP299msWWTkVYKwvY_2FhuL2s4,1687
|
|
124
124
|
shotgun/tui/styles.tcss,sha256=ETyyw1bpMBOqTi5RLcAJUScdPWTvAWEqE9YcT0kVs_E,121
|
|
125
125
|
shotgun/tui/commands/__init__.py,sha256=8D5lvtpqMW5-fF7Bg3oJtUzU75cKOv6aUaHYYszydU8,2518
|
|
@@ -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.8.dev1.dist-info/METADATA,sha256=razgwSFigJNH56uBlRPZ8yxyQjjvThAbMZpHlPI4_JY,4335
|
|
152
|
+
shotgun_sh-0.2.8.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
153
|
+
shotgun_sh-0.2.8.dev1.dist-info/entry_points.txt,sha256=asZxLU4QILneq0MWW10saVCZc4VWhZfb0wFZvERnzfA,45
|
|
154
|
+
shotgun_sh-0.2.8.dev1.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
|
|
155
|
+
shotgun_sh-0.2.8.dev1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|