shotgun-sh 0.2.7.dev1__tar.gz → 0.2.8__tar.gz

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.

Files changed (158) hide show
  1. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/PKG-INFO +2 -1
  2. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/pyproject.toml +2 -1
  3. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/agent_manager.py +222 -20
  4. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/common.py +42 -17
  5. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/file_management.py +55 -9
  6. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/build_constants.py +2 -2
  7. shotgun_sh-0.2.8/src/shotgun/prompts/agents/specify.j2 +318 -0
  8. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/chat.py +6 -0
  9. shotgun_sh-0.2.7.dev1/src/shotgun/prompts/agents/specify.j2 +0 -51
  10. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/.gitignore +0 -0
  11. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/LICENSE +0 -0
  12. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/README.md +0 -0
  13. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/README_PYPI.md +0 -0
  14. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/hatch_build.py +0 -0
  15. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/__init__.py +0 -0
  16. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/__init__.py +0 -0
  17. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/config/__init__.py +0 -0
  18. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/config/constants.py +0 -0
  19. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/config/manager.py +0 -0
  20. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/config/models.py +0 -0
  21. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/config/provider.py +0 -0
  22. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/conversation_history.py +0 -0
  23. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/conversation_manager.py +0 -0
  24. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/export.py +0 -0
  25. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/__init__.py +0 -0
  26. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/compaction.py +0 -0
  27. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/constants.py +0 -0
  28. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/context_extraction.py +0 -0
  29. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/history_building.py +0 -0
  30. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/history_processors.py +0 -0
  31. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/message_utils.py +0 -0
  32. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/token_counting/__init__.py +0 -0
  33. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/token_counting/anthropic.py +0 -0
  34. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/token_counting/base.py +0 -0
  35. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/token_counting/openai.py +0 -0
  36. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/token_counting/sentencepiece_counter.py +0 -0
  37. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/token_counting/tokenizer_cache.py +0 -0
  38. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/token_counting/utils.py +0 -0
  39. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/history/token_estimation.py +0 -0
  40. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/llm.py +0 -0
  41. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/messages.py +0 -0
  42. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/models.py +0 -0
  43. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/plan.py +0 -0
  44. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/research.py +0 -0
  45. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/specify.py +0 -0
  46. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tasks.py +0 -0
  47. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/__init__.py +0 -0
  48. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/codebase/__init__.py +0 -0
  49. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/codebase/codebase_shell.py +0 -0
  50. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/codebase/directory_lister.py +0 -0
  51. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/codebase/file_read.py +0 -0
  52. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/codebase/models.py +0 -0
  53. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/codebase/query_graph.py +0 -0
  54. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/codebase/retrieve_code.py +0 -0
  55. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/web_search/__init__.py +0 -0
  56. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/web_search/anthropic.py +0 -0
  57. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/web_search/gemini.py +0 -0
  58. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/web_search/openai.py +0 -0
  59. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/tools/web_search/utils.py +0 -0
  60. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/agents/usage_manager.py +0 -0
  61. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/api_endpoints.py +0 -0
  62. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/__init__.py +0 -0
  63. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/codebase/__init__.py +0 -0
  64. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/codebase/commands.py +0 -0
  65. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/codebase/models.py +0 -0
  66. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/config.py +0 -0
  67. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/export.py +0 -0
  68. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/feedback.py +0 -0
  69. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/models.py +0 -0
  70. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/plan.py +0 -0
  71. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/research.py +0 -0
  72. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/specify.py +0 -0
  73. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/tasks.py +0 -0
  74. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/update.py +0 -0
  75. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/cli/utils.py +0 -0
  76. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/codebase/__init__.py +0 -0
  77. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/codebase/core/__init__.py +0 -0
  78. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/codebase/core/change_detector.py +0 -0
  79. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/codebase/core/code_retrieval.py +0 -0
  80. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/codebase/core/cypher_models.py +0 -0
  81. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/codebase/core/ingestor.py +0 -0
  82. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/codebase/core/language_config.py +0 -0
  83. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/codebase/core/manager.py +0 -0
  84. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/codebase/core/nl_query.py +0 -0
  85. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/codebase/core/parser_loader.py +0 -0
  86. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/codebase/models.py +0 -0
  87. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/codebase/service.py +0 -0
  88. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/llm_proxy/__init__.py +0 -0
  89. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/llm_proxy/clients.py +0 -0
  90. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/llm_proxy/constants.py +0 -0
  91. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/logging_config.py +0 -0
  92. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/main.py +0 -0
  93. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/posthog_telemetry.py +0 -0
  94. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/__init__.py +0 -0
  95. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/agents/__init__.py +0 -0
  96. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/agents/export.j2 +0 -0
  97. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/agents/partials/codebase_understanding.j2 +0 -0
  98. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +0 -0
  99. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/agents/partials/content_formatting.j2 +0 -0
  100. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/agents/partials/interactive_mode.j2 +0 -0
  101. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/agents/plan.j2 +0 -0
  102. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/agents/research.j2 +0 -0
  103. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +0 -0
  104. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/agents/state/system_state.j2 +0 -0
  105. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/agents/tasks.j2 +0 -0
  106. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/codebase/__init__.py +0 -0
  107. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/codebase/cypher_query_patterns.j2 +0 -0
  108. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/codebase/cypher_system.j2 +0 -0
  109. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/codebase/enhanced_query_context.j2 +0 -0
  110. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/codebase/partials/cypher_rules.j2 +0 -0
  111. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/codebase/partials/graph_schema.j2 +0 -0
  112. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/codebase/partials/temporal_context.j2 +0 -0
  113. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/history/__init__.py +0 -0
  114. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/history/incremental_summarization.j2 +0 -0
  115. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/history/summarization.j2 +0 -0
  116. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/loader.py +0 -0
  117. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/prompts/tools/web_search.j2 +0 -0
  118. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/py.typed +0 -0
  119. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/sdk/__init__.py +0 -0
  120. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/sdk/codebase.py +0 -0
  121. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/sdk/exceptions.py +0 -0
  122. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/sdk/models.py +0 -0
  123. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/sdk/services.py +0 -0
  124. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/sentry_telemetry.py +0 -0
  125. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/shotgun_web/__init__.py +0 -0
  126. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/shotgun_web/client.py +0 -0
  127. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/shotgun_web/constants.py +0 -0
  128. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/shotgun_web/models.py +0 -0
  129. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/telemetry.py +0 -0
  130. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/__init__.py +0 -0
  131. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/app.py +0 -0
  132. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/commands/__init__.py +0 -0
  133. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/components/prompt_input.py +0 -0
  134. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/components/spinner.py +0 -0
  135. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/components/splash.py +0 -0
  136. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/components/vertical_tail.py +0 -0
  137. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/filtered_codebase_service.py +0 -0
  138. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/chat.tcss +0 -0
  139. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/chat_screen/__init__.py +0 -0
  140. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/chat_screen/command_providers.py +0 -0
  141. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/chat_screen/hint_message.py +0 -0
  142. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/chat_screen/history.py +0 -0
  143. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/directory_setup.py +0 -0
  144. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/feedback.py +0 -0
  145. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/model_picker.py +0 -0
  146. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/provider_config.py +0 -0
  147. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/shotgun_auth.py +0 -0
  148. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/splash.py +0 -0
  149. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/screens/welcome.py +0 -0
  150. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/styles.tcss +0 -0
  151. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/utils/__init__.py +0 -0
  152. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/tui/utils/mode_progress.py +0 -0
  153. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/utils/__init__.py +0 -0
  154. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/utils/datetime_utils.py +0 -0
  155. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/utils/env_utils.py +0 -0
  156. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/utils/file_system_utils.py +0 -0
  157. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/utils/source_detection.py +0 -0
  158. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8}/src/shotgun/utils/update_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.2.7.dev1
3
+ Version: 0.2.8
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,6 +34,7 @@ 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
38
39
  Requires-Dist: textual>=6.1.0
39
40
  Requires-Dist: tiktoken>=0.7.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shotgun-sh"
3
- version = "0.2.7.dev1"
3
+ version = "0.2.8"
4
4
  description = "AI-powered research, planning, and task management CLI tool"
5
5
  readme = "README_PYPI.md"
6
6
  license = { text = "MIT" }
@@ -46,6 +46,7 @@ dependencies = [
46
46
  "sentencepiece>=0.2.0",
47
47
  "packaging>=23.0",
48
48
  "genai-prices>=0.0.27",
49
+ "tenacity>=8.0.0",
49
50
  ]
50
51
 
51
52
  [project.urls]
@@ -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
@@ -32,6 +40,7 @@ from pydantic_ai.messages import (
32
40
  SystemPromptPart,
33
41
  ToolCallPart,
34
42
  ToolCallPartDelta,
43
+ UserPromptPart,
35
44
  )
36
45
  from textual.message import Message
37
46
  from textual.widget import Widget
@@ -55,6 +64,35 @@ from .tasks import create_tasks_agent
55
64
  logger = logging.getLogger(__name__)
56
65
 
57
66
 
67
+ def _is_retryable_error(exception: BaseException) -> bool:
68
+ """Check if exception should trigger a retry.
69
+
70
+ Args:
71
+ exception: The exception to check.
72
+
73
+ Returns:
74
+ True if the exception is a transient error that should be retried.
75
+ """
76
+ # ValueError for truncated/incomplete JSON
77
+ if isinstance(exception, ValueError):
78
+ error_str = str(exception)
79
+ return "EOF while parsing" in error_str or (
80
+ "JSON" in error_str and "parsing" in error_str
81
+ )
82
+
83
+ # API errors (overload, rate limits)
84
+ exception_name = type(exception).__name__
85
+ if "APIStatusError" in exception_name:
86
+ error_str = str(exception)
87
+ return "overload" in error_str.lower() or "rate" in error_str.lower()
88
+
89
+ # Network errors
90
+ if "ConnectionError" in exception_name or "TimeoutError" in exception_name:
91
+ return True
92
+
93
+ return False
94
+
95
+
58
96
  class MessageHistoryUpdated(Message):
59
97
  """Event posted when the message history is updated."""
60
98
 
@@ -268,6 +306,49 @@ class AgentManager(Widget):
268
306
  f"Invalid agent type: {agent_type}. Must be one of: {', '.join(e.value for e in AgentType)}"
269
307
  ) from None
270
308
 
309
+ @retry(
310
+ stop=stop_after_attempt(3),
311
+ wait=wait_exponential(multiplier=1, min=1, max=8),
312
+ retry=retry_if_exception(_is_retryable_error),
313
+ before_sleep=before_sleep_log(logger, logging.WARNING),
314
+ reraise=True,
315
+ )
316
+ async def _run_agent_with_retry(
317
+ self,
318
+ agent: Agent[AgentDeps, AgentResponse],
319
+ prompt: str | None,
320
+ deps: AgentDeps,
321
+ usage_limits: UsageLimits | None,
322
+ message_history: list[ModelMessage],
323
+ event_stream_handler: Any,
324
+ **kwargs: Any,
325
+ ) -> AgentRunResult[AgentResponse]:
326
+ """Run agent with automatic retry on transient errors.
327
+
328
+ Args:
329
+ agent: The agent to run.
330
+ prompt: Optional prompt to send to the agent.
331
+ deps: Agent dependencies.
332
+ usage_limits: Optional usage limits.
333
+ message_history: Message history to provide to agent.
334
+ event_stream_handler: Event handler for streaming.
335
+ **kwargs: Additional keyword arguments.
336
+
337
+ Returns:
338
+ The agent run result.
339
+
340
+ Raises:
341
+ Various exceptions if all retries fail.
342
+ """
343
+ return await agent.run(
344
+ prompt,
345
+ deps=deps,
346
+ usage_limits=usage_limits,
347
+ message_history=message_history,
348
+ event_stream_handler=event_stream_handler,
349
+ **kwargs,
350
+ )
351
+
271
352
  async def run(
272
353
  self,
273
354
  prompt: str | None = None,
@@ -301,13 +382,12 @@ class AgentManager(Widget):
301
382
 
302
383
  # Clear file tracker before each run to track only this run's operations
303
384
  deps.file_tracker.clear()
304
- # preprocess messages; maybe we need to include the user answer in the message history
305
385
 
306
- original_messages = self.ui_message_history.copy()
386
+ # Don't manually add the user prompt - Pydantic AI will include it in result.new_messages()
387
+ # This prevents duplicates and confusion with incremental mounting
307
388
 
308
- if prompt:
309
- self.ui_message_history.append(ModelRequest.user_text_prompt(prompt))
310
- self._post_messages_updated()
389
+ # Save current message history before the run
390
+ original_messages = self.ui_message_history.copy()
311
391
 
312
392
  # Start with persistent message history
313
393
  message_history = self.message_history
@@ -394,8 +474,9 @@ class AgentManager(Widget):
394
474
  )
395
475
 
396
476
  try:
397
- result: AgentRunResult[AgentResponse] = await self.current_agent.run(
398
- prompt,
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,64 @@ class AgentManager(Widget):
427
538
 
428
539
  # Agent ALWAYS returns AgentResponse with structured output
429
540
  agent_response = result.output
430
- logger.debug("Agent returned structured AgentResponse")
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
- # Always add the agent's response messages to maintain conversation history
433
- self.ui_message_history = original_messages + cast(
558
+ # Merge agent's response messages, avoiding duplicates
559
+ # The TUI may have already added the user prompt, so check for it
560
+ new_messages = cast(
434
561
  list[ModelRequest | ModelResponse | HintMessage], result.new_messages()
435
562
  )
436
563
 
564
+ # Deduplicate: skip user prompts that are already in original_messages
565
+ deduplicated_new_messages = []
566
+ for msg in new_messages:
567
+ # Check if this is a user prompt that's already in original_messages
568
+ if isinstance(msg, ModelRequest) and any(
569
+ isinstance(part, UserPromptPart) for part in msg.parts
570
+ ):
571
+ # Check if an identical user prompt is already in original_messages
572
+ already_exists = any(
573
+ isinstance(existing, ModelRequest)
574
+ and any(isinstance(p, UserPromptPart) for p in existing.parts)
575
+ and existing.parts == msg.parts
576
+ for existing in original_messages[
577
+ -5:
578
+ ] # Check last 5 messages for efficiency
579
+ )
580
+ if already_exists:
581
+ continue # Skip this duplicate user prompt
582
+
583
+ deduplicated_new_messages.append(msg)
584
+
585
+ self.ui_message_history = original_messages + deduplicated_new_messages
586
+
587
+ # Get file operations early so we can use them for contextual messages
588
+ file_operations = deps.file_tracker.operations.copy()
589
+ self.recently_change_files = file_operations
590
+
591
+ logger.debug(
592
+ "File operations tracked",
593
+ extra={
594
+ "num_file_operations": len(file_operations),
595
+ "operation_files": [Path(op.file_path).name for op in file_operations],
596
+ },
597
+ )
598
+
437
599
  # Check if there are clarifying questions
438
600
  if agent_response.clarifying_questions:
439
601
  logger.info(
@@ -480,12 +642,50 @@ class AgentManager(Widget):
480
642
  response_text=agent_response.response,
481
643
  )
482
644
  )
645
+
646
+ # Post UI update with hint messages and file operations
647
+ logger.debug(
648
+ "Posting UI update for Q&A mode with hint messages and file operations"
649
+ )
650
+ self._post_messages_updated(file_operations)
483
651
  else:
484
- # No clarifying questions - just show the response if present
652
+ # No clarifying questions - show the response or a default success message
485
653
  if agent_response.response and agent_response.response.strip():
654
+ logger.debug(
655
+ "Adding agent response as hint",
656
+ extra={
657
+ "response_preview": agent_response.response[:100] + "..."
658
+ if len(agent_response.response) > 100
659
+ else agent_response.response,
660
+ "has_file_operations": len(file_operations) > 0,
661
+ },
662
+ )
486
663
  self.ui_message_history.append(
487
664
  HintMessage(message=agent_response.response)
488
665
  )
666
+ else:
667
+ # Fallback: response is empty or whitespace
668
+ logger.debug(
669
+ "Agent response was empty, using fallback completion message",
670
+ extra={"has_file_operations": len(file_operations) > 0},
671
+ )
672
+ # Show contextual message based on whether files were modified
673
+ if file_operations:
674
+ self.ui_message_history.append(
675
+ HintMessage(
676
+ message="✅ Task completed - files have been modified"
677
+ )
678
+ )
679
+ else:
680
+ self.ui_message_history.append(
681
+ HintMessage(message="✅ Task completed")
682
+ )
683
+
684
+ # Post UI update immediately so user sees the response without delay
685
+ logger.debug(
686
+ "Posting immediate UI update with hint message and file operations"
687
+ )
688
+ self._post_messages_updated(file_operations)
489
689
 
490
690
  # Apply compaction to persistent message history to prevent cascading growth
491
691
  all_messages = result.all_messages()
@@ -517,16 +717,18 @@ class AgentManager(Widget):
517
717
  self.message_history = all_messages
518
718
 
519
719
  usage = result.usage()
520
- deps.usage_manager.add_usage(
521
- usage, model_name=deps.llm_model.name, provider=deps.llm_model.provider
522
- )
523
-
524
- # Log file operations summary if any files were modified
525
- file_operations = deps.file_tracker.operations.copy()
526
- self.recently_change_files = file_operations
720
+ if hasattr(deps, "llm_model") and deps.llm_model is not None:
721
+ deps.usage_manager.add_usage(
722
+ usage, model_name=deps.llm_model.name, provider=deps.llm_model.provider
723
+ )
724
+ else:
725
+ logger.warning(
726
+ "llm_model is None, skipping usage tracking",
727
+ extra={"agent_mode": self._current_agent_type.value},
728
+ )
527
729
 
528
- # Post message history update (hints are now added synchronously above)
529
- self._post_messages_updated(file_operations)
730
+ # UI updates are now posted immediately in each branch (Q&A or non-Q&A)
731
+ # before compaction, so no duplicate posting needed here
530
732
 
531
733
  return result
532
734
 
@@ -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 both .md file and directory with same name
388
- allowed_file = AGENT_DIRECTORIES[agent_mode]
389
-
390
- # Check for the .md file
391
- md_file_path = base_path / allowed_file
392
- if md_file_path.exists():
393
- existing_files.append(allowed_file)
394
-
395
- # Check for directory with same base name (e.g., research/ for research.md)
396
- base_name = allowed_file.replace(".md", "")
397
- dir_path = base_path / base_name
398
- if dir_path.exists() and dir_path.is_dir():
399
- # List all files in the directory
400
- for file_path in dir_path.rglob("*"):
401
- if file_path.is_file():
402
- relative_path = file_path.relative_to(base_path)
403
- existing_files.append(str(relative_path))
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
- AGENT_DIRECTORIES = {
19
- AgentType.RESEARCH: "research.md",
20
- AgentType.SPECIFY: "specification.md",
21
- AgentType.PLAN: "plan.md",
22
- AgentType.TASKS: "tasks.md",
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, only allow writing to their specific file
64
- allowed_file = AGENT_DIRECTORIES[agent_mode]
65
- if filename != allowed_file:
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 '{allowed_file}'. "
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
@@ -12,8 +12,8 @@ POSTHOG_API_KEY = ''
12
12
  POSTHOG_PROJECT_ID = '191396'
13
13
 
14
14
  # Logfire configuration embedded at build time (only for dev builds)
15
- LOGFIRE_ENABLED = 'true'
16
- LOGFIRE_TOKEN = 'pylf_v1_us_KZ5NM1pP3NwgJkbBJt6Ftdzk8mMhmrXcGJHQQgDJ1LfK'
15
+ LOGFIRE_ENABLED = ''
16
+ LOGFIRE_TOKEN = ''
17
17
 
18
18
  # Build metadata
19
19
  BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"