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.
- shotgun/agents/agent_manager.py +382 -60
- shotgun/agents/common.py +15 -9
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +383 -82
- shotgun/agents/config/models.py +122 -18
- shotgun/agents/config/provider.py +81 -15
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +475 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +36 -5
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
- shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
- shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
- shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/export.py +2 -2
- shotgun/agents/plan.py +2 -2
- shotgun/agents/research.py +3 -3
- shotgun/agents/runner.py +230 -0
- shotgun/agents/specify.py +2 -2
- shotgun/agents/tasks.py +2 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +27 -7
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +8 -2
- shotgun/agents/tools/web_search/gemini.py +7 -1
- shotgun/agents/tools/web_search/openai.py +8 -2
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +188 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +154 -0
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +18 -10
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +163 -15
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +357 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +60 -27
- shotgun/main.py +77 -11
- shotgun/posthog_telemetry.py +38 -29
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/plan.j2 +16 -0
- shotgun/prompts/agents/research.j2 +16 -3
- shotgun/prompts/agents/specify.j2 +54 -1
- shotgun/prompts/agents/state/system_state.j2 +0 -2
- shotgun/prompts/agents/tasks.j2 +16 -0
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +243 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/telemetry.py +10 -33
- shotgun/tui/app.py +310 -46
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/layout.py +5 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1531 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +91 -4
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +191 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +14 -7
- shotgun/tui/screens/github_issue.py +111 -0
- shotgun/tui/screens/model_picker.py +77 -32
- shotgun/tui/screens/onboarding.py +580 -0
- shotgun/tui/screens/pipx_migration.py +205 -0
- shotgun/tui/screens/provider_config.py +116 -35
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +112 -18
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +137 -11
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +187 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +263 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
- shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
- shotgun/tui/screens/chat.py +0 -996
- shotgun/tui/screens/chat_screen/history.py +0 -335
- shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
- shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /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
|
|
79
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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
|
|
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
|
-
#
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
shotgun/posthog_telemetry.py
CHANGED
|
@@ -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.
|
|
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
|
-
#
|
|
36
|
-
api_key =
|
|
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
|
|
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
|
-
#
|
|
63
|
+
# Cache the shotgun instance ID for later use (avoids async issues)
|
|
55
64
|
try:
|
|
56
|
-
|
|
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
|
-
|
|
69
|
-
|
|
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
|
|
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
|
|
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
|
-
#
|
|
105
|
-
|
|
106
|
-
|
|
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=
|
|
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 =
|
|
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.
|
|
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
|
-
-
|
|
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:
|
|
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
|
-
-
|
|
25
|
-
-
|
|
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
|
|
shotgun/prompts/agents/plan.j2
CHANGED
|
@@ -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
|