shotgun-sh 0.2.6.dev1__py3-none-any.whl → 0.2.17__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 (127) hide show
  1. shotgun/agents/agent_manager.py +694 -73
  2. shotgun/agents/common.py +69 -70
  3. shotgun/agents/config/constants.py +0 -6
  4. shotgun/agents/config/manager.py +70 -35
  5. shotgun/agents/config/models.py +41 -1
  6. shotgun/agents/config/provider.py +33 -5
  7. shotgun/agents/context_analyzer/__init__.py +28 -0
  8. shotgun/agents/context_analyzer/analyzer.py +471 -0
  9. shotgun/agents/context_analyzer/constants.py +9 -0
  10. shotgun/agents/context_analyzer/formatter.py +115 -0
  11. shotgun/agents/context_analyzer/models.py +212 -0
  12. shotgun/agents/conversation_history.py +125 -2
  13. shotgun/agents/conversation_manager.py +57 -19
  14. shotgun/agents/export.py +6 -7
  15. shotgun/agents/history/compaction.py +9 -4
  16. shotgun/agents/history/context_extraction.py +93 -6
  17. shotgun/agents/history/history_processors.py +113 -5
  18. shotgun/agents/history/token_counting/anthropic.py +39 -3
  19. shotgun/agents/history/token_counting/base.py +14 -3
  20. shotgun/agents/history/token_counting/openai.py +11 -1
  21. shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
  22. shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
  23. shotgun/agents/history/token_counting/utils.py +0 -3
  24. shotgun/agents/models.py +50 -2
  25. shotgun/agents/plan.py +6 -7
  26. shotgun/agents/research.py +7 -8
  27. shotgun/agents/specify.py +6 -7
  28. shotgun/agents/tasks.py +6 -7
  29. shotgun/agents/tools/__init__.py +0 -2
  30. shotgun/agents/tools/codebase/codebase_shell.py +6 -0
  31. shotgun/agents/tools/codebase/directory_lister.py +6 -0
  32. shotgun/agents/tools/codebase/file_read.py +11 -2
  33. shotgun/agents/tools/codebase/query_graph.py +6 -0
  34. shotgun/agents/tools/codebase/retrieve_code.py +6 -0
  35. shotgun/agents/tools/file_management.py +82 -16
  36. shotgun/agents/tools/registry.py +217 -0
  37. shotgun/agents/tools/web_search/__init__.py +8 -8
  38. shotgun/agents/tools/web_search/anthropic.py +8 -2
  39. shotgun/agents/tools/web_search/gemini.py +7 -1
  40. shotgun/agents/tools/web_search/openai.py +7 -1
  41. shotgun/agents/tools/web_search/utils.py +2 -2
  42. shotgun/agents/usage_manager.py +16 -11
  43. shotgun/api_endpoints.py +7 -3
  44. shotgun/build_constants.py +3 -3
  45. shotgun/cli/clear.py +53 -0
  46. shotgun/cli/compact.py +186 -0
  47. shotgun/cli/config.py +8 -5
  48. shotgun/cli/context.py +111 -0
  49. shotgun/cli/export.py +1 -1
  50. shotgun/cli/feedback.py +4 -2
  51. shotgun/cli/models.py +1 -0
  52. shotgun/cli/plan.py +1 -1
  53. shotgun/cli/research.py +1 -1
  54. shotgun/cli/specify.py +1 -1
  55. shotgun/cli/tasks.py +1 -1
  56. shotgun/cli/update.py +16 -2
  57. shotgun/codebase/core/change_detector.py +5 -3
  58. shotgun/codebase/core/code_retrieval.py +4 -2
  59. shotgun/codebase/core/ingestor.py +10 -8
  60. shotgun/codebase/core/manager.py +13 -4
  61. shotgun/codebase/core/nl_query.py +1 -1
  62. shotgun/exceptions.py +32 -0
  63. shotgun/logging_config.py +18 -27
  64. shotgun/main.py +73 -11
  65. shotgun/posthog_telemetry.py +37 -28
  66. shotgun/prompts/agents/export.j2 +18 -1
  67. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +5 -1
  68. shotgun/prompts/agents/partials/interactive_mode.j2 +24 -7
  69. shotgun/prompts/agents/plan.j2 +1 -1
  70. shotgun/prompts/agents/research.j2 +1 -1
  71. shotgun/prompts/agents/specify.j2 +270 -3
  72. shotgun/prompts/agents/tasks.j2 +1 -1
  73. shotgun/sentry_telemetry.py +163 -16
  74. shotgun/settings.py +238 -0
  75. shotgun/telemetry.py +18 -33
  76. shotgun/tui/app.py +243 -43
  77. shotgun/tui/commands/__init__.py +1 -1
  78. shotgun/tui/components/context_indicator.py +179 -0
  79. shotgun/tui/components/mode_indicator.py +70 -0
  80. shotgun/tui/components/status_bar.py +48 -0
  81. shotgun/tui/containers.py +91 -0
  82. shotgun/tui/dependencies.py +39 -0
  83. shotgun/tui/protocols.py +45 -0
  84. shotgun/tui/screens/chat/__init__.py +5 -0
  85. shotgun/tui/screens/chat/chat.tcss +54 -0
  86. shotgun/tui/screens/chat/chat_screen.py +1254 -0
  87. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +64 -0
  88. shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
  89. shotgun/tui/screens/chat/help_text.py +40 -0
  90. shotgun/tui/screens/chat/prompt_history.py +48 -0
  91. shotgun/tui/screens/chat.tcss +11 -0
  92. shotgun/tui/screens/chat_screen/command_providers.py +78 -2
  93. shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
  94. shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
  95. shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
  96. shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
  97. shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
  98. shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
  99. shotgun/tui/screens/confirmation_dialog.py +151 -0
  100. shotgun/tui/screens/feedback.py +4 -4
  101. shotgun/tui/screens/github_issue.py +102 -0
  102. shotgun/tui/screens/model_picker.py +49 -24
  103. shotgun/tui/screens/onboarding.py +431 -0
  104. shotgun/tui/screens/pipx_migration.py +153 -0
  105. shotgun/tui/screens/provider_config.py +50 -27
  106. shotgun/tui/screens/shotgun_auth.py +2 -2
  107. shotgun/tui/screens/welcome.py +23 -12
  108. shotgun/tui/services/__init__.py +5 -0
  109. shotgun/tui/services/conversation_service.py +184 -0
  110. shotgun/tui/state/__init__.py +7 -0
  111. shotgun/tui/state/processing_state.py +185 -0
  112. shotgun/tui/utils/mode_progress.py +14 -7
  113. shotgun/tui/widgets/__init__.py +5 -0
  114. shotgun/tui/widgets/widget_coordinator.py +263 -0
  115. shotgun/utils/file_system_utils.py +22 -2
  116. shotgun/utils/marketing.py +110 -0
  117. shotgun/utils/update_checker.py +69 -14
  118. shotgun_sh-0.2.17.dist-info/METADATA +465 -0
  119. shotgun_sh-0.2.17.dist-info/RECORD +194 -0
  120. {shotgun_sh-0.2.6.dev1.dist-info → shotgun_sh-0.2.17.dist-info}/entry_points.txt +1 -0
  121. {shotgun_sh-0.2.6.dev1.dist-info → shotgun_sh-0.2.17.dist-info}/licenses/LICENSE +1 -1
  122. shotgun/agents/tools/user_interaction.py +0 -37
  123. shotgun/tui/screens/chat.py +0 -804
  124. shotgun/tui/screens/chat_screen/history.py +0 -401
  125. shotgun_sh-0.2.6.dev1.dist-info/METADATA +0 -467
  126. shotgun_sh-0.2.6.dev1.dist-info/RECORD +0 -156
  127. {shotgun_sh-0.2.6.dev1.dist-info → shotgun_sh-0.2.17.dist-info}/WHEEL +0 -0
@@ -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
- - This is your persistent memory store - ALWAYS load it first
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 file updated as you work - it's your memory across sessions
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.)
@@ -99,7 +99,7 @@ Then organize tasks into logical sections:
99
99
  USER INTERACTION - ASK CLARIFYING QUESTIONS:
100
100
 
101
101
  - ALWAYS ask clarifying questions when the request is vague or ambiguous
102
- - Use ask_user tool to gather specific details about:
102
+ - Use clarifying questions to gather specific details about:
103
103
  - Specific features or functionality to prioritize
104
104
  - Technical constraints or preferences
105
105
  - Timeline and resource constraints
@@ -1,14 +1,132 @@
1
1
  """Sentry observability setup for Shotgun."""
2
2
 
3
- import os
3
+ from pathlib import Path
4
+ from typing import Any
4
5
 
5
6
  from shotgun import __version__
6
7
  from shotgun.logging_config import get_early_logger
8
+ from shotgun.settings import settings
7
9
 
8
10
  # Use early logger to prevent automatic StreamHandler creation
9
11
  logger = get_early_logger(__name__)
10
12
 
11
13
 
14
+ def _scrub_path(path: str) -> str:
15
+ """Scrub sensitive information from file paths.
16
+
17
+ Removes home directory and current working directory prefixes to prevent
18
+ leaking usernames that might be part of the path.
19
+
20
+ Args:
21
+ path: The file path to scrub
22
+
23
+ Returns:
24
+ The scrubbed path with sensitive prefixes removed
25
+ """
26
+ if not path:
27
+ return path
28
+
29
+ try:
30
+ # Get home and cwd as Path objects for comparison
31
+ home = Path.home()
32
+ cwd = Path.cwd()
33
+
34
+ # Convert path to Path object
35
+ path_obj = Path(path)
36
+
37
+ # Try to make path relative to cwd first (most common case)
38
+ try:
39
+ relative_to_cwd = path_obj.relative_to(cwd)
40
+ return str(relative_to_cwd)
41
+ except ValueError:
42
+ pass
43
+
44
+ # Try to replace home directory with ~
45
+ try:
46
+ relative_to_home = path_obj.relative_to(home)
47
+ return f"~/{relative_to_home}"
48
+ except ValueError:
49
+ pass
50
+
51
+ # If path is absolute but not under cwd or home, just return filename
52
+ if path_obj.is_absolute():
53
+ return path_obj.name
54
+
55
+ # Return as-is if already relative
56
+ return path
57
+
58
+ except Exception:
59
+ # If anything goes wrong, return the original path
60
+ # Better to leak a path than break error reporting
61
+ return path
62
+
63
+
64
+ def _scrub_sensitive_paths(event: dict[str, Any]) -> None:
65
+ """Scrub sensitive paths from Sentry event data.
66
+
67
+ Modifies the event in-place to remove:
68
+ - Home directory paths (might contain usernames)
69
+ - Current working directory paths (might contain usernames)
70
+ - Server name/hostname
71
+ - Paths in sys.argv
72
+
73
+ Args:
74
+ event: The Sentry event dictionary to scrub
75
+ """
76
+ extra = event.get("extra", {})
77
+ if "sys.argv" in extra:
78
+ argv = extra["sys.argv"]
79
+ if isinstance(argv, list):
80
+ extra["sys.argv"] = [
81
+ _scrub_path(arg) if isinstance(arg, str) else arg for arg in argv
82
+ ]
83
+
84
+ # Scrub server name if present
85
+ if "server_name" in event:
86
+ event["server_name"] = ""
87
+
88
+ # Scrub contexts that might contain paths
89
+ if "contexts" in event:
90
+ contexts = event["contexts"]
91
+ # Remove runtime context if it has CWD
92
+ if "runtime" in contexts:
93
+ if "cwd" in contexts["runtime"]:
94
+ del contexts["runtime"]["cwd"]
95
+ # Scrub sys.argv to remove paths
96
+ if "sys.argv" in contexts["runtime"]:
97
+ argv = contexts["runtime"]["sys.argv"]
98
+ if isinstance(argv, list):
99
+ contexts["runtime"]["sys.argv"] = [
100
+ _scrub_path(arg) if isinstance(arg, str) else arg
101
+ for arg in argv
102
+ ]
103
+
104
+ # Scrub exception stack traces
105
+ if "exception" in event and "values" in event["exception"]:
106
+ for exception in event["exception"]["values"]:
107
+ if "stacktrace" in exception and "frames" in exception["stacktrace"]:
108
+ for frame in exception["stacktrace"]["frames"]:
109
+ # Scrub file paths
110
+ if "abs_path" in frame:
111
+ frame["abs_path"] = _scrub_path(frame["abs_path"])
112
+ if "filename" in frame:
113
+ frame["filename"] = _scrub_path(frame["filename"])
114
+
115
+ # Scrub local variables that might contain paths
116
+ if "vars" in frame:
117
+ for var_name, var_value in frame["vars"].items():
118
+ if isinstance(var_value, str):
119
+ frame["vars"][var_name] = _scrub_path(var_value)
120
+
121
+ # Scrub breadcrumbs that might contain paths
122
+ if "breadcrumbs" in event and "values" in event["breadcrumbs"]:
123
+ for breadcrumb in event["breadcrumbs"]["values"]:
124
+ if "data" in breadcrumb:
125
+ for key, value in breadcrumb["data"].items():
126
+ if isinstance(value, str):
127
+ breadcrumb["data"][key] = _scrub_path(value)
128
+
129
+
12
130
  def setup_sentry_observability() -> bool:
13
131
  """Set up Sentry observability for error tracking.
14
132
 
@@ -23,48 +141,77 @@ def setup_sentry_observability() -> bool:
23
141
  logger.debug("Sentry is already initialized, skipping")
24
142
  return True
25
143
 
26
- # Try to get DSN from build constants first (production builds)
27
- dsn = None
28
- try:
29
- from shotgun import build_constants
30
-
31
- dsn = build_constants.SENTRY_DSN
32
- logger.debug("Using Sentry DSN from build constants")
33
- except ImportError:
34
- # Fallback to environment variable (development)
35
- dsn = os.getenv("SENTRY_DSN", "")
36
- if dsn:
37
- logger.debug("Using Sentry DSN from environment variable")
144
+ # Get DSN from settings (handles build constants + env vars automatically)
145
+ dsn = settings.telemetry.sentry_dsn
38
146
 
39
147
  if not dsn:
40
148
  logger.debug("No Sentry DSN configured, skipping Sentry initialization")
41
149
  return False
42
150
 
43
- logger.debug("Found DSN, proceeding with Sentry setup")
151
+ logger.debug("Using Sentry DSN from settings, proceeding with setup")
44
152
 
45
153
  # Determine environment based on version
46
- # Dev versions contain "dev", "rc", "alpha", or "beta"
154
+ # Dev versions contain "dev", "rc", "alpha", "beta"
47
155
  if any(marker in __version__ for marker in ["dev", "rc", "alpha", "beta"]):
48
156
  environment = "development"
49
157
  else:
50
158
  environment = "production"
51
159
 
160
+ def before_send(event: Any, hint: dict[str, Any]) -> Any:
161
+ """Filter out user-actionable errors and scrub sensitive paths.
162
+
163
+ User-actionable errors (like context size limits) are expected conditions
164
+ that users need to resolve, not bugs that need tracking.
165
+
166
+ Also scrubs sensitive information like usernames from file paths and
167
+ working directories to protect user privacy.
168
+ """
169
+
170
+ log_record = hint.get("log_record")
171
+ if log_record:
172
+ # Scrub pathname using the helper function
173
+ log_record.pathname = _scrub_path(log_record.pathname)
174
+
175
+ # Scrub traceback text if it exists
176
+ if hasattr(log_record, "exc_text") and isinstance(
177
+ log_record.exc_text, str
178
+ ):
179
+ # Replace home directory in traceback text
180
+ home = Path.home()
181
+ log_record.exc_text = log_record.exc_text.replace(str(home), "~")
182
+
183
+ if "exc_info" in hint:
184
+ _, exc_value, _ = hint["exc_info"]
185
+ from shotgun.exceptions import ErrorNotPickedUpBySentry
186
+
187
+ if isinstance(exc_value, ErrorNotPickedUpBySentry):
188
+ # Don't send to Sentry - this is user-actionable, not a bug
189
+ return None
190
+
191
+ # Scrub sensitive paths from the event
192
+ _scrub_sensitive_paths(event)
193
+ return event
194
+
52
195
  # Initialize Sentry
53
196
  sentry_sdk.init(
54
197
  dsn=dsn,
55
198
  release=f"shotgun-sh@{__version__}",
56
199
  environment=environment,
57
200
  send_default_pii=False, # Privacy-first: never send PII
201
+ server_name="", # Privacy: don't send hostname (may contain username)
58
202
  traces_sample_rate=0.1 if environment == "production" else 1.0,
59
203
  profiles_sample_rate=0.1 if environment == "production" else 1.0,
204
+ before_send=before_send,
60
205
  )
61
206
 
62
207
  # Set user context with anonymous shotgun instance ID from config
63
208
  try:
209
+ import asyncio
210
+
64
211
  from shotgun.agents.config import get_config_manager
65
212
 
66
213
  config_manager = get_config_manager()
67
- shotgun_instance_id = config_manager.get_shotgun_instance_id()
214
+ shotgun_instance_id = asyncio.run(config_manager.get_shotgun_instance_id())
68
215
  sentry_sdk.set_user({"id": shotgun_instance_id})
69
216
  logger.debug("Sentry user context set with anonymous ID")
70
217
  except Exception as e: