shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.3.3.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.
Files changed (175) hide show
  1. shotgun/agents/agent_manager.py +382 -60
  2. shotgun/agents/common.py +15 -9
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/constants.py +0 -6
  6. shotgun/agents/config/manager.py +383 -82
  7. shotgun/agents/config/models.py +122 -18
  8. shotgun/agents/config/provider.py +81 -15
  9. shotgun/agents/config/streaming_test.py +119 -0
  10. shotgun/agents/context_analyzer/__init__.py +28 -0
  11. shotgun/agents/context_analyzer/analyzer.py +475 -0
  12. shotgun/agents/context_analyzer/constants.py +9 -0
  13. shotgun/agents/context_analyzer/formatter.py +115 -0
  14. shotgun/agents/context_analyzer/models.py +212 -0
  15. shotgun/agents/conversation/__init__.py +18 -0
  16. shotgun/agents/conversation/filters.py +164 -0
  17. shotgun/agents/conversation/history/chunking.py +278 -0
  18. shotgun/agents/{history → conversation/history}/compaction.py +36 -5
  19. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  20. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  21. shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
  22. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
  23. shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
  24. shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
  25. shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
  26. shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
  27. shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
  28. shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
  29. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
  30. shotgun/agents/error/__init__.py +11 -0
  31. shotgun/agents/error/models.py +19 -0
  32. shotgun/agents/export.py +2 -2
  33. shotgun/agents/plan.py +2 -2
  34. shotgun/agents/research.py +3 -3
  35. shotgun/agents/runner.py +230 -0
  36. shotgun/agents/specify.py +2 -2
  37. shotgun/agents/tasks.py +2 -2
  38. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  39. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  40. shotgun/agents/tools/codebase/file_read.py +11 -2
  41. shotgun/agents/tools/codebase/query_graph.py +6 -0
  42. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  43. shotgun/agents/tools/file_management.py +27 -7
  44. shotgun/agents/tools/registry.py +217 -0
  45. shotgun/agents/tools/web_search/__init__.py +8 -8
  46. shotgun/agents/tools/web_search/anthropic.py +8 -2
  47. shotgun/agents/tools/web_search/gemini.py +7 -1
  48. shotgun/agents/tools/web_search/openai.py +8 -2
  49. shotgun/agents/tools/web_search/utils.py +2 -2
  50. shotgun/agents/usage_manager.py +16 -11
  51. shotgun/api_endpoints.py +7 -3
  52. shotgun/build_constants.py +2 -2
  53. shotgun/cli/clear.py +53 -0
  54. shotgun/cli/compact.py +188 -0
  55. shotgun/cli/config.py +8 -5
  56. shotgun/cli/context.py +154 -0
  57. shotgun/cli/error_handler.py +24 -0
  58. shotgun/cli/export.py +34 -34
  59. shotgun/cli/feedback.py +4 -2
  60. shotgun/cli/models.py +1 -0
  61. shotgun/cli/plan.py +34 -34
  62. shotgun/cli/research.py +18 -10
  63. shotgun/cli/spec/__init__.py +5 -0
  64. shotgun/cli/spec/backup.py +81 -0
  65. shotgun/cli/spec/commands.py +132 -0
  66. shotgun/cli/spec/models.py +48 -0
  67. shotgun/cli/spec/pull_service.py +219 -0
  68. shotgun/cli/specify.py +20 -19
  69. shotgun/cli/tasks.py +34 -34
  70. shotgun/cli/update.py +16 -2
  71. shotgun/codebase/core/change_detector.py +5 -3
  72. shotgun/codebase/core/code_retrieval.py +4 -2
  73. shotgun/codebase/core/ingestor.py +163 -15
  74. shotgun/codebase/core/manager.py +13 -4
  75. shotgun/codebase/core/nl_query.py +1 -1
  76. shotgun/codebase/models.py +2 -0
  77. shotgun/exceptions.py +357 -0
  78. shotgun/llm_proxy/__init__.py +17 -0
  79. shotgun/llm_proxy/client.py +215 -0
  80. shotgun/llm_proxy/models.py +137 -0
  81. shotgun/logging_config.py +60 -27
  82. shotgun/main.py +77 -11
  83. shotgun/posthog_telemetry.py +38 -29
  84. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
  85. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  86. shotgun/prompts/agents/plan.j2 +16 -0
  87. shotgun/prompts/agents/research.j2 +16 -3
  88. shotgun/prompts/agents/specify.j2 +54 -1
  89. shotgun/prompts/agents/state/system_state.j2 +0 -2
  90. shotgun/prompts/agents/tasks.j2 +16 -0
  91. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  92. shotgun/prompts/history/combine_summaries.j2 +53 -0
  93. shotgun/sdk/codebase.py +14 -3
  94. shotgun/sentry_telemetry.py +163 -16
  95. shotgun/settings.py +243 -0
  96. shotgun/shotgun_web/__init__.py +67 -1
  97. shotgun/shotgun_web/client.py +42 -1
  98. shotgun/shotgun_web/constants.py +46 -0
  99. shotgun/shotgun_web/exceptions.py +29 -0
  100. shotgun/shotgun_web/models.py +390 -0
  101. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  102. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  103. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  104. shotgun/shotgun_web/shared_specs/models.py +71 -0
  105. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  106. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  107. shotgun/shotgun_web/specs_client.py +703 -0
  108. shotgun/shotgun_web/supabase_client.py +31 -0
  109. shotgun/telemetry.py +10 -33
  110. shotgun/tui/app.py +310 -46
  111. shotgun/tui/commands/__init__.py +1 -1
  112. shotgun/tui/components/context_indicator.py +179 -0
  113. shotgun/tui/components/mode_indicator.py +70 -0
  114. shotgun/tui/components/status_bar.py +48 -0
  115. shotgun/tui/containers.py +91 -0
  116. shotgun/tui/dependencies.py +39 -0
  117. shotgun/tui/layout.py +5 -0
  118. shotgun/tui/protocols.py +45 -0
  119. shotgun/tui/screens/chat/__init__.py +5 -0
  120. shotgun/tui/screens/chat/chat.tcss +54 -0
  121. shotgun/tui/screens/chat/chat_screen.py +1531 -0
  122. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
  123. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  124. shotgun/tui/screens/chat/help_text.py +40 -0
  125. shotgun/tui/screens/chat/prompt_history.py +48 -0
  126. shotgun/tui/screens/chat.tcss +11 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +91 -4
  128. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  129. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  130. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  131. shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
  132. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  133. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  134. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  135. shotgun/tui/screens/confirmation_dialog.py +191 -0
  136. shotgun/tui/screens/directory_setup.py +45 -41
  137. shotgun/tui/screens/feedback.py +14 -7
  138. shotgun/tui/screens/github_issue.py +111 -0
  139. shotgun/tui/screens/model_picker.py +77 -32
  140. shotgun/tui/screens/onboarding.py +580 -0
  141. shotgun/tui/screens/pipx_migration.py +205 -0
  142. shotgun/tui/screens/provider_config.py +116 -35
  143. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  144. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  145. shotgun/tui/screens/shared_specs/models.py +56 -0
  146. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  147. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  148. shotgun/tui/screens/shotgun_auth.py +112 -18
  149. shotgun/tui/screens/spec_pull.py +288 -0
  150. shotgun/tui/screens/welcome.py +137 -11
  151. shotgun/tui/services/__init__.py +5 -0
  152. shotgun/tui/services/conversation_service.py +187 -0
  153. shotgun/tui/state/__init__.py +7 -0
  154. shotgun/tui/state/processing_state.py +185 -0
  155. shotgun/tui/utils/mode_progress.py +14 -7
  156. shotgun/tui/widgets/__init__.py +5 -0
  157. shotgun/tui/widgets/widget_coordinator.py +263 -0
  158. shotgun/utils/file_system_utils.py +22 -2
  159. shotgun/utils/marketing.py +110 -0
  160. shotgun/utils/update_checker.py +69 -14
  161. shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
  162. shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
  163. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
  164. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
  165. {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
  166. shotgun/tui/screens/chat.py +0 -996
  167. shotgun/tui/screens/chat_screen/history.py +0 -335
  168. shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
  169. shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
  170. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  171. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  172. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  173. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  174. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  175. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
@@ -0,0 +1,137 @@
1
+ """Pydantic models for LiteLLM Proxy API."""
2
+
3
+ from enum import StrEnum
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class BudgetSource(StrEnum):
9
+ """Source of budget information."""
10
+
11
+ KEY = "key"
12
+ TEAM = "team"
13
+
14
+
15
+ class KeyInfoData(BaseModel):
16
+ """Key information data from /key/info endpoint."""
17
+
18
+ key_name: str = Field(description="Key name/identifier")
19
+ key_alias: str | None = Field(default=None, description="Human-readable key alias")
20
+ spend: float = Field(description="Current spend for this key in USD")
21
+ max_budget: float | None = Field(
22
+ default=None, description="Maximum budget for this key in USD"
23
+ )
24
+ team_id: str = Field(description="Team ID associated with this key")
25
+ user_id: str = Field(description="User ID associated with this key")
26
+ models: list[str] = Field(
27
+ default_factory=list, description="List of models available to this key"
28
+ )
29
+
30
+
31
+ class KeyInfoResponse(BaseModel):
32
+ """Response from /key/info endpoint."""
33
+
34
+ key: str = Field(description="The API key")
35
+ info: KeyInfoData = Field(description="Key information data")
36
+
37
+
38
+ class TeamInfoData(BaseModel):
39
+ """Team information data from /team/info endpoint."""
40
+
41
+ team_id: str = Field(description="Team identifier")
42
+ team_alias: str | None = Field(
43
+ default=None, description="Human-readable team alias"
44
+ )
45
+ max_budget: float | None = Field(
46
+ default=None, description="Maximum budget for this team in USD"
47
+ )
48
+ spend: float = Field(description="Current spend for this team in USD")
49
+ models: list[str] = Field(
50
+ default_factory=list, description="List of models available to this team"
51
+ )
52
+
53
+
54
+ class TeamInfoResponse(BaseModel):
55
+ """Response from /team/info endpoint."""
56
+
57
+ team_id: str = Field(description="Team identifier")
58
+ team_info: TeamInfoData = Field(description="Team information data")
59
+
60
+
61
+ class BudgetInfo(BaseModel):
62
+ """Unified budget information.
63
+
64
+ Combines key and team budget information to provide a single view
65
+ of budget status. Budget can come from either key-level or team-level,
66
+ with key-level taking priority if set.
67
+ """
68
+
69
+ max_budget: float = Field(description="Maximum budget in USD")
70
+ spend: float = Field(description="Current spend in USD")
71
+ remaining: float = Field(description="Remaining budget in USD")
72
+ source: BudgetSource = Field(
73
+ description="Source of budget information (key or team)"
74
+ )
75
+ percentage_used: float = Field(description="Percentage of budget used (0-100)")
76
+
77
+ @classmethod
78
+ def from_key_info(cls, key_info: KeyInfoData) -> "BudgetInfo":
79
+ """Create BudgetInfo from key-level budget.
80
+
81
+ Args:
82
+ key_info: Key information containing budget data
83
+
84
+ Returns:
85
+ BudgetInfo instance with key-level budget
86
+
87
+ Raises:
88
+ ValueError: If key does not have max_budget set
89
+ """
90
+ if key_info.max_budget is None:
91
+ raise ValueError("Key does not have max_budget set")
92
+
93
+ remaining = key_info.max_budget - key_info.spend
94
+ percentage_used = (
95
+ (key_info.spend / key_info.max_budget * 100)
96
+ if key_info.max_budget > 0
97
+ else 0.0
98
+ )
99
+
100
+ return cls(
101
+ max_budget=key_info.max_budget,
102
+ spend=key_info.spend,
103
+ remaining=remaining,
104
+ source=BudgetSource.KEY,
105
+ percentage_used=percentage_used,
106
+ )
107
+
108
+ @classmethod
109
+ def from_team_info(cls, team_info: TeamInfoData) -> "BudgetInfo":
110
+ """Create BudgetInfo from team-level budget.
111
+
112
+ Args:
113
+ team_info: Team information containing budget data
114
+
115
+ Returns:
116
+ BudgetInfo instance with team-level budget
117
+
118
+ Raises:
119
+ ValueError: If team does not have max_budget set
120
+ """
121
+ if team_info.max_budget is None:
122
+ raise ValueError("Team does not have max_budget set")
123
+
124
+ remaining = team_info.max_budget - team_info.spend
125
+ percentage_used = (
126
+ (team_info.spend / team_info.max_budget * 100)
127
+ if team_info.max_budget > 0
128
+ else 0.0
129
+ )
130
+
131
+ return cls(
132
+ max_budget=team_info.max_budget,
133
+ spend=team_info.spend,
134
+ remaining=remaining,
135
+ source=BudgetSource.TEAM,
136
+ percentage_used=percentage_used,
137
+ )
shotgun/logging_config.py CHANGED
@@ -2,12 +2,16 @@
2
2
 
3
3
  import logging
4
4
  import logging.handlers
5
- import os
6
5
  import sys
6
+ from datetime import datetime, timezone
7
7
  from pathlib import Path
8
8
 
9
+ from shotgun.settings import settings
9
10
  from shotgun.utils.env_utils import is_truthy
10
11
 
12
+ # Generate a single timestamp for this run to be used across all loggers
13
+ _RUN_TIMESTAMP = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
14
+
11
15
 
12
16
  def get_log_directory() -> Path:
13
17
  """Get the log directory path, creating it if necessary.
@@ -23,6 +27,44 @@ def get_log_directory() -> Path:
23
27
  return log_dir
24
28
 
25
29
 
30
+ def cleanup_old_log_files(log_dir: Path, max_files: int) -> None:
31
+ """Remove old log files, keeping only the most recent ones.
32
+
33
+ Also removes the legacy shotgun.log file if it exists.
34
+
35
+ Args:
36
+ log_dir: Directory containing log files
37
+ max_files: Maximum number of log files to keep
38
+ """
39
+ try:
40
+ # Remove legacy non-timestamped log file if it exists
41
+ legacy_log = log_dir / "shotgun.log"
42
+ if legacy_log.exists():
43
+ try:
44
+ legacy_log.unlink()
45
+ except OSError:
46
+ pass # noqa: S110
47
+
48
+ # Find all shotgun log files
49
+ log_files = sorted(
50
+ log_dir.glob("shotgun-*.log"),
51
+ key=lambda p: p.stat().st_mtime,
52
+ reverse=True, # Newest first
53
+ )
54
+
55
+ # Remove files beyond the limit
56
+ files_to_delete = log_files[max_files:]
57
+ for log_file in files_to_delete:
58
+ try:
59
+ log_file.unlink()
60
+ except OSError:
61
+ # Ignore errors when deleting individual files
62
+ pass # noqa: S110
63
+ except Exception: # noqa: S110
64
+ # Silently fail - log cleanup shouldn't break the application
65
+ pass
66
+
67
+
26
68
  class ColoredFormatter(logging.Formatter):
27
69
  """Custom formatter with colors for different log levels."""
28
70
 
@@ -66,21 +108,16 @@ def setup_logger(
66
108
  logger = logging.getLogger(name)
67
109
 
68
110
  # Check if we already have a file handler
69
- has_file_handler = any(
70
- isinstance(h, logging.handlers.TimedRotatingFileHandler)
71
- for h in logger.handlers
72
- )
111
+ has_file_handler = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
73
112
 
74
113
  # If we already have a file handler, just return the logger
75
114
  if has_file_handler:
76
115
  return logger
77
116
 
78
- # Get log level from environment variable, default to INFO
79
- env_level = os.getenv("SHOTGUN_LOG_LEVEL", "INFO").upper()
80
- if env_level not in ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]:
81
- env_level = "INFO"
117
+ # Get log level from settings (already validated and uppercased)
118
+ log_level = settings.logging.log_level
82
119
 
83
- logger.setLevel(getattr(logging, env_level))
120
+ logger.setLevel(getattr(logging, log_level))
84
121
 
85
122
  # Default format string
86
123
  if format_string is None:
@@ -102,13 +139,13 @@ def setup_logger(
102
139
  # Check if console logging is enabled (default: off)
103
140
  # Force console logging OFF if Logfire is enabled in dev build
104
141
  console_logging_enabled = (
105
- is_truthy(os.getenv("LOGGING_TO_CONSOLE", "false")) and not is_logfire_dev_build
142
+ settings.logging.logging_to_console and not is_logfire_dev_build
106
143
  )
107
144
 
108
145
  if console_logging_enabled:
109
146
  # Create console handler
110
147
  console_handler = logging.StreamHandler(sys.stdout)
111
- console_handler.setLevel(getattr(logging, env_level))
148
+ console_handler.setLevel(getattr(logging, log_level))
112
149
 
113
150
  # Use colored formatter for console
114
151
  console_formatter = ColoredFormatter(format_string, datefmt="%H:%M:%S")
@@ -118,26 +155,25 @@ def setup_logger(
118
155
  logger.addHandler(console_handler)
119
156
 
120
157
  # Check if file logging is enabled (default: on)
121
- file_logging_enabled = is_truthy(os.getenv("LOGGING_TO_FILE", "true"))
158
+ file_logging_enabled = settings.logging.logging_to_file
122
159
 
123
160
  if file_logging_enabled:
124
161
  try:
125
- # Create file handler with rotation
162
+ # Create file handler with ISO8601 timestamp for each run
126
163
  log_dir = get_log_directory()
127
- log_file = log_dir / "shotgun.log"
128
164
 
129
- # Use TimedRotatingFileHandler - rotates daily and keeps 7 days of logs
130
- file_handler = logging.handlers.TimedRotatingFileHandler(
165
+ # Clean up old log files before creating a new one
166
+ cleanup_old_log_files(log_dir, settings.logging.max_log_files)
167
+
168
+ log_file = log_dir / f"shotgun-{_RUN_TIMESTAMP}.log"
169
+
170
+ # Use regular FileHandler - each run gets its own isolated log file
171
+ file_handler = logging.FileHandler(
131
172
  filename=log_file,
132
- when="midnight", # Rotate at midnight
133
- interval=1, # Every 1 day
134
- backupCount=7, # Keep 7 days of logs
135
173
  encoding="utf-8",
136
174
  )
137
175
 
138
- # Also set max file size (10MB) using RotatingFileHandler as fallback
139
- # Note: We'll use TimedRotatingFileHandler which handles both time and size
140
- file_handler.setLevel(getattr(logging, env_level))
176
+ file_handler.setLevel(getattr(logging, log_level))
141
177
 
142
178
  # Use standard formatter for file (no colors)
143
179
  file_formatter = logging.Formatter(
@@ -191,10 +227,7 @@ def get_logger(name: str) -> logging.Logger:
191
227
  logger = logging.getLogger(name)
192
228
 
193
229
  # Check if we have a file handler already
194
- has_file_handler = any(
195
- isinstance(h, logging.handlers.TimedRotatingFileHandler)
196
- for h in logger.handlers
197
- )
230
+ has_file_handler = any(isinstance(h, logging.FileHandler) for h in logger.handlers)
198
231
 
199
232
  # If no file handler, set up the logger (will add file handler)
200
233
  if not has_file_handler:
shotgun/main.py CHANGED
@@ -23,12 +23,16 @@ from dotenv import load_dotenv
23
23
  from shotgun import __version__
24
24
  from shotgun.agents.config import get_config_manager
25
25
  from shotgun.cli import (
26
+ clear,
26
27
  codebase,
28
+ compact,
27
29
  config,
30
+ context,
28
31
  export,
29
32
  feedback,
30
33
  plan,
31
34
  research,
35
+ spec,
32
36
  specify,
33
37
  tasks,
34
38
  update,
@@ -52,9 +56,13 @@ logger = get_logger(__name__)
52
56
  logger.debug("Logfire observability enabled: %s", _logfire_enabled)
53
57
 
54
58
  # Initialize configuration
59
+ # Note: If config migration fails, ConfigManager will auto-create fresh config
60
+ # and set migration_failed flag for user notification
55
61
  try:
62
+ import asyncio
63
+
56
64
  config_manager = get_config_manager()
57
- config_manager.load() # Ensure config is loaded at startup
65
+ asyncio.run(config_manager.load()) # Ensure config is loaded at startup
58
66
  except Exception as e:
59
67
  logger.debug("Configuration initialization warning: %s", e)
60
68
 
@@ -78,6 +86,9 @@ app.add_typer(config.app, name="config", help="Manage Shotgun configuration")
78
86
  app.add_typer(
79
87
  codebase.app, name="codebase", help="Manage and query code knowledge graphs"
80
88
  )
89
+ app.add_typer(context.app, name="context", help="Analyze conversation context usage")
90
+ app.add_typer(compact.app, name="compact", help="Compact conversation history")
91
+ app.add_typer(clear.app, name="clear", help="Clear conversation history")
81
92
  app.add_typer(research.app, name="research", help="Perform research with agentic loops")
82
93
  app.add_typer(plan.app, name="plan", help="Generate structured plans")
83
94
  app.add_typer(specify.app, name="specify", help="Generate comprehensive specifications")
@@ -85,6 +96,7 @@ app.add_typer(tasks.app, name="tasks", help="Generate task lists with agentic ap
85
96
  app.add_typer(export.app, name="export", help="Export artifacts to various formats")
86
97
  app.add_typer(update.app, name="update", help="Check for and install updates")
87
98
  app.add_typer(feedback.app, name="feedback", help="Send us feedback")
99
+ app.add_typer(spec.app, name="spec", help="Manage shared specifications")
88
100
 
89
101
 
90
102
  def version_callback(value: bool) -> None:
@@ -125,6 +137,41 @@ def main(
125
137
  help="Continue previous TUI conversation",
126
138
  ),
127
139
  ] = False,
140
+ web: Annotated[
141
+ bool,
142
+ typer.Option(
143
+ "--web",
144
+ help="Serve TUI as web application",
145
+ ),
146
+ ] = False,
147
+ port: Annotated[
148
+ int,
149
+ typer.Option(
150
+ "--port",
151
+ help="Port for web server (only used with --web)",
152
+ ),
153
+ ] = 8000,
154
+ host: Annotated[
155
+ str,
156
+ typer.Option(
157
+ "--host",
158
+ help="Host address for web server (only used with --web)",
159
+ ),
160
+ ] = "localhost",
161
+ public_url: Annotated[
162
+ str | None,
163
+ typer.Option(
164
+ "--public-url",
165
+ help="Public URL if behind proxy (only used with --web)",
166
+ ),
167
+ ] = None,
168
+ force_reindex: Annotated[
169
+ bool,
170
+ typer.Option(
171
+ "--force-reindex",
172
+ help="Force re-indexing of codebase (ignores existing index)",
173
+ ),
174
+ ] = False,
128
175
  ) -> None:
129
176
  """Shotgun - AI-powered CLI tool."""
130
177
  logger.debug("Starting shotgun CLI application")
@@ -134,16 +181,35 @@ def main(
134
181
  perform_auto_update_async(no_update_check=no_update_check)
135
182
 
136
183
  if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
137
- logger.debug("Launching shotgun TUI application")
138
- try:
139
- tui_app.run(
140
- no_update_check=no_update_check, continue_session=continue_session
141
- )
142
- finally:
143
- # Ensure PostHog is shut down cleanly even if TUI exits unexpectedly
144
- from shotgun.posthog_telemetry import shutdown
145
-
146
- shutdown()
184
+ if web:
185
+ logger.debug("Launching shotgun TUI as web application")
186
+ try:
187
+ tui_app.serve(
188
+ host=host,
189
+ port=port,
190
+ public_url=public_url,
191
+ no_update_check=no_update_check,
192
+ continue_session=continue_session,
193
+ force_reindex=force_reindex,
194
+ )
195
+ finally:
196
+ # Ensure PostHog is shut down cleanly even if server exits unexpectedly
197
+ from shotgun.posthog_telemetry import shutdown
198
+
199
+ shutdown()
200
+ else:
201
+ logger.debug("Launching shotgun TUI application")
202
+ try:
203
+ tui_app.run(
204
+ no_update_check=no_update_check,
205
+ continue_session=continue_session,
206
+ force_reindex=force_reindex,
207
+ )
208
+ finally:
209
+ # Ensure PostHog is shut down cleanly even if TUI exits unexpectedly
210
+ from shotgun.posthog_telemetry import shutdown
211
+
212
+ shutdown()
147
213
  raise typer.Exit()
148
214
 
149
215
  # For CLI commands, register PostHog shutdown handler
@@ -8,8 +8,9 @@ from pydantic import BaseModel
8
8
 
9
9
  from shotgun import __version__
10
10
  from shotgun.agents.config import get_config_manager
11
- from shotgun.agents.conversation_manager import ConversationManager
11
+ from shotgun.agents.conversation import ConversationManager
12
12
  from shotgun.logging_config import get_early_logger
13
+ from shotgun.settings import settings
13
14
 
14
15
  # Use early logger to prevent automatic StreamHandler creation
15
16
  logger = get_early_logger(__name__)
@@ -17,6 +18,9 @@ logger = get_early_logger(__name__)
17
18
  # Global PostHog client instance
18
19
  _posthog_client = None
19
20
 
21
+ # Cache the shotgun instance ID to avoid async calls during event tracking
22
+ _shotgun_instance_id: str | None = None
23
+
20
24
 
21
25
  def setup_posthog_observability() -> bool:
22
26
  """Set up PostHog analytics for usage tracking.
@@ -24,7 +28,7 @@ def setup_posthog_observability() -> bool:
24
28
  Returns:
25
29
  True if PostHog was successfully set up, False otherwise
26
30
  """
27
- global _posthog_client
31
+ global _posthog_client, _shotgun_instance_id
28
32
 
29
33
  try:
30
34
  # Check if PostHog is already initialized
@@ -32,10 +36,15 @@ def setup_posthog_observability() -> bool:
32
36
  logger.debug("PostHog is already initialized, skipping")
33
37
  return True
34
38
 
35
- # Hardcoded PostHog configuration
36
- api_key = "phc_KKnChzZUKeNqZDOTJ6soCBWNQSx3vjiULdwTR9H5Mcr"
39
+ # Get API key from settings (handles build constants + env vars automatically)
40
+ api_key = settings.telemetry.posthog_api_key
41
+
42
+ # If no API key is available, skip PostHog initialization
43
+ if not api_key:
44
+ logger.debug("No PostHog API key available, skipping initialization")
45
+ return False
37
46
 
38
- logger.debug("Using hardcoded PostHog configuration")
47
+ logger.debug("Using PostHog API key from settings")
39
48
 
40
49
  # Determine environment based on version
41
50
  # Dev versions contain "dev", "rc", "alpha", or "beta"
@@ -51,29 +60,20 @@ def setup_posthog_observability() -> bool:
51
60
  # Store the client for later use
52
61
  _posthog_client = posthog
53
62
 
54
- # Set user context with anonymous shotgun instance ID from config
63
+ # Cache the shotgun instance ID for later use (avoids async issues)
55
64
  try:
56
- config_manager = get_config_manager()
57
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
58
-
59
- # Identify the user in PostHog
60
- posthog.identify( # type: ignore[attr-defined]
61
- distinct_id=shotgun_instance_id,
62
- properties={
63
- "version": __version__,
64
- "environment": environment,
65
- },
66
- )
65
+ import asyncio
67
66
 
68
- # Set default properties for all events
69
- posthog.disabled = False
70
- posthog.personal_api_key = None # Not needed for event tracking
67
+ config_manager = get_config_manager()
68
+ _shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
71
69
 
72
70
  logger.debug(
73
- "PostHog user identified with anonymous ID: %s", shotgun_instance_id
71
+ "PostHog initialized with shotgun instance ID: %s",
72
+ _shotgun_instance_id,
74
73
  )
75
74
  except Exception as e:
76
- logger.warning("Failed to set user context: %s", e)
75
+ logger.warning("Failed to load shotgun instance ID: %s", e)
76
+ # Continue anyway - we'll try to get it during event tracking
77
77
 
78
78
  logger.debug(
79
79
  "PostHog analytics configured successfully (environment: %s, version: %s)",
@@ -94,16 +94,19 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
94
94
  event_name: Name of the event to track
95
95
  properties: Optional properties to include with the event
96
96
  """
97
- global _posthog_client
97
+ global _posthog_client, _shotgun_instance_id
98
98
 
99
99
  if _posthog_client is None:
100
100
  logger.debug("PostHog not initialized, skipping event: %s", event_name)
101
101
  return
102
102
 
103
103
  try:
104
- # Get shotgun instance ID for tracking
105
- config_manager = get_config_manager()
106
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
104
+ # Use cached instance ID (loaded during setup)
105
+ if _shotgun_instance_id is None:
106
+ logger.warning(
107
+ "Shotgun instance ID not available, skipping event: %s", event_name
108
+ )
109
+ return
107
110
 
108
111
  # Add version and environment to properties
109
112
  if properties is None:
@@ -118,7 +121,7 @@ def track_event(event_name: str, properties: dict[str, Any] | None = None) -> No
118
121
 
119
122
  # Track the event using PostHog's capture method
120
123
  _posthog_client.capture(
121
- distinct_id=shotgun_instance_id, event=event_name, properties=properties
124
+ distinct_id=_shotgun_instance_id, event=event_name, properties=properties
122
125
  )
123
126
  logger.debug("Tracked PostHog event: %s", event_name)
124
127
  except Exception as e:
@@ -162,10 +165,16 @@ def submit_feedback_survey(feedback: Feedback) -> None:
162
165
  logger.debug("PostHog not initialized, skipping feedback survey")
163
166
  return
164
167
 
168
+ import asyncio
169
+
165
170
  config_manager = get_config_manager()
166
- config = config_manager.load()
171
+ config = asyncio.run(config_manager.load())
167
172
  conversation_manager = ConversationManager()
168
- conversation = conversation_manager.load()
173
+ conversation = None
174
+ try:
175
+ conversation = asyncio.run(conversation_manager.load())
176
+ except Exception as e:
177
+ logger.debug(f"Failed to load conversation history: {e}")
169
178
  last_10_messages = []
170
179
  if conversation is not None:
171
180
  last_10_messages = conversation.get_agent_messages()[:10]
@@ -4,12 +4,38 @@ Your extensive expertise spans, among other things:
4
4
  * Software Architecture
5
5
  * Software Development
6
6
 
7
+ ## YOUR ROLE IN THE PIPELINE
8
+
9
+ **CRITICAL**: You are a DOCUMENTATION and PLANNING agent, NOT a coding/implementation agent.
10
+
11
+ - You produce DOCUMENTS (research, specifications, plans, tasks) that AI coding agents will consume
12
+ - You do NOT write production code, implement features, or make code changes
13
+ - NEVER offer to "move forward with implementation" or "start coding" - that's not your job
14
+ - NEVER ask "would you like me to implement this?" - implementation is done by separate AI coding tools
15
+ - Your deliverable is always a document file (.md), not code execution
16
+ - When your work is complete, the user will take your documents to a coding agent (Claude Code, Cursor, etc.)
17
+
18
+ ## AGENT FILE PERMISSIONS
19
+
20
+ There are four agents in the pipeline, and each agent can ONLY write to specific files. The user can switch between agents using **Shift+Tab**.
21
+
22
+ The **Research agent** can only write to `research.md`. If the user asks about specifications, plans, or tasks, tell them: "Use **Shift+Tab** to switch to the [appropriate] agent which can edit that file for you."
23
+
24
+ The **Specification agent** can only write to `specification.md` and files inside the `.shotgun/contracts/` directory. If the user asks about research, plans, or tasks, tell them which agent handles that file.
25
+
26
+ The **Plan agent** can only write to `plan.md`. If the user asks about research, specifications, or tasks, tell them which agent handles that file.
27
+
28
+ The **Tasks agent** can only write to `tasks.md`. If the user asks about research, specifications, or plans, tell them which agent handles that file.
29
+
30
+ When a user asks you to edit a file you cannot write to, you MUST tell them which agent can help and how to switch: "I can't edit [filename] - that's handled by the [agent name] agent. Use **Shift+Tab** to switch to that agent and it can edit that file for you."
31
+
7
32
  ## KEY RULES
8
33
 
9
34
  {% if interactive_mode %}
10
- 0. Always ask CLARIFYING QUESTIONS using structured output if the user's request is ambiguous or lacks sufficient detail.
35
+ 0. Ask CLARIFYING QUESTIONS using structured output for complex or multi-step tasks when the request lacks sufficient detail.
11
36
  - Return your response with the clarifying_questions field populated
12
- - Do not make assumptions about what the user wants
37
+ - For simple, straightforward requests, make reasonable assumptions and proceed.
38
+ - Only ask the most critical questions to avoid overwhelming the user.
13
39
  - Questions should be clear, specific, and answerable
14
40
  {% endif %}
15
41
  1. Above all, prefer using tools to do the work and NEVER respond with text.
@@ -19,10 +19,10 @@ You must return responses using this structured format:
19
19
 
20
20
  ## When to Use Clarifying Questions
21
21
 
22
- - BEFORE GETTING TO WORK: If the user's request is ambiguous, use clarifying_questions to ask what they want
22
+ - BEFORE GETTING TO WORK: For complex or multi-step tasks where the request is ambiguous or lacks sufficient detail, use clarifying_questions to ask what they want
23
23
  - DURING WORK: After using write_file(), you can suggest that the user review it and ask any clarifying questions with clarifying_questions
24
- - Don't assume - ask for confirmation of your understanding
25
- - When in doubt about any aspect of the goal, include clarifying_questions
24
+ - For simple, straightforward requests, make reasonable assumptions and proceed
25
+ - Only ask critical questions that significantly impact the outcome
26
26
 
27
27
  ## Important Notes
28
28
 
@@ -6,6 +6,22 @@ Your job is to help create comprehensive, actionable plans for software projects
6
6
 
7
7
  {% include 'agents/partials/common_agent_system_prompt.j2' %}
8
8
 
9
+ ## YOUR SCOPE AND HANDOFFS
10
+
11
+ You are the **Plan agent**. Your file is `plan.md` - this is the ONLY file you can write to.
12
+
13
+ When your plan is complete, suggest the next step:
14
+ "I've completed the plan. Use **Shift+Tab** to switch to the tasks agent to break this plan into actionable tasks."
15
+
16
+ If the user asks you to edit other files, redirect them helpfully:
17
+ - For research.md: "I can't edit research.md - that's handled by the research agent. Use **Shift+Tab** to switch to that agent and it can edit that file for you."
18
+ - For specification.md or contracts: "I can't edit specification.md - that's handled by the specification agent. Use **Shift+Tab** to switch to that agent and it can edit that file for you."
19
+ - For tasks.md: "I can't edit tasks.md - that's handled by the tasks agent. Use **Shift+Tab** to switch to that agent and it can edit that file for you."
20
+
21
+ NEVER offer to do work outside your scope:
22
+ - Don't offer to write research, specifications, or tasks - redirect the user to the appropriate agent
23
+ - Don't offer to implement code - you are not a coding agent
24
+
9
25
  ## MEMORY MANAGEMENT PROTOCOL
10
26
 
11
27
  - You have exclusive write access to: `plan.md`
@@ -4,6 +4,22 @@ Your job is to help the user research various subjects related to their software
4
4
 
5
5
  {% include 'agents/partials/common_agent_system_prompt.j2' %}
6
6
 
7
+ ## YOUR SCOPE AND HANDOFFS
8
+
9
+ You are the **Research agent**. Your file is `research.md` - this is the ONLY file you can write to.
10
+
11
+ When your research is complete, suggest the next step:
12
+ "I've completed the research and updated research.md. Use **Shift+Tab** to switch to the specification agent to create the specification based on this research."
13
+
14
+ If the user asks you to edit other files, redirect them helpfully:
15
+ - For specification.md or contracts: "I can't edit specification.md - that's handled by the specification agent. Use **Shift+Tab** to switch to that agent and it can edit that file for you."
16
+ - For plan.md: "I can't edit plan.md - that's handled by the plan agent. Use **Shift+Tab** to switch to that agent and it can edit that file for you."
17
+ - For tasks.md: "I can't edit tasks.md - that's handled by the tasks agent. Use **Shift+Tab** to switch to that agent and it can edit that file for you."
18
+
19
+ NEVER offer to do work outside your scope:
20
+ - Don't offer to write specifications, plans, or tasks - redirect the user to the appropriate agent
21
+ - Don't offer to implement code - you are not a coding agent
22
+
7
23
  ## MEMORY MANAGEMENT PROTOCOL
8
24
 
9
25
  - You have exclusive write access to: `research.md`
@@ -38,9 +54,6 @@ For research tasks:
38
54
 
39
55
  ## RESEARCH PRINCIPLES
40
56
 
41
- {% if interactive_mode -%}
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
- {% endif -%}
44
57
  - Build upon existing research rather than starting from scratch
45
58
  - Focus on practical, actionable information over theoretical concepts
46
59
  - Include specific examples, tools, and implementation details