shotgun-sh 0.2.17__py3-none-any.whl → 0.4.0.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 +219 -37
- shotgun/agents/common.py +79 -78
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/manager.py +364 -53
- shotgun/agents/config/models.py +101 -21
- shotgun/agents/config/provider.py +51 -13
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/analyzer.py +6 -2
- 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 +27 -1
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +239 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/export.py +12 -13
- shotgun/agents/models.py +66 -1
- shotgun/agents/plan.py +12 -13
- shotgun/agents/research.py +13 -10
- shotgun/agents/router/__init__.py +47 -0
- shotgun/agents/router/models.py +376 -0
- shotgun/agents/router/router.py +185 -0
- shotgun/agents/router/tools/__init__.py +18 -0
- shotgun/agents/router/tools/delegation_tools.py +503 -0
- shotgun/agents/router/tools/plan_tools.py +322 -0
- shotgun/agents/runner.py +230 -0
- shotgun/agents/specify.py +12 -13
- shotgun/agents/tasks.py +12 -13
- shotgun/agents/tools/file_management.py +49 -1
- shotgun/agents/tools/registry.py +2 -0
- shotgun/agents/tools/web_search/__init__.py +1 -2
- shotgun/agents/tools/web_search/gemini.py +1 -3
- shotgun/agents/tools/web_search/openai.py +1 -1
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +1 -1
- shotgun/cli/compact.py +5 -3
- shotgun/cli/context.py +44 -1
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +17 -9
- 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/codebase/core/change_detector.py +1 -1
- shotgun/codebase/core/ingestor.py +154 -8
- shotgun/codebase/core/manager.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +325 -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 +42 -0
- shotgun/main.py +4 -0
- shotgun/posthog_telemetry.py +1 -1
- shotgun/prompts/agents/export.j2 +2 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +23 -3
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +36 -0
- shotgun/prompts/agents/plan.j2 +29 -1
- shotgun/prompts/agents/research.j2 +75 -23
- shotgun/prompts/agents/router.j2 +440 -0
- shotgun/prompts/agents/specify.j2 +80 -4
- shotgun/prompts/agents/state/system_state.j2 +15 -8
- shotgun/prompts/agents/tasks.j2 +63 -23
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/settings.py +5 -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/tui/app.py +78 -15
- shotgun/tui/components/mode_indicator.py +120 -25
- shotgun/tui/components/status_bar.py +2 -2
- shotgun/tui/containers.py +1 -1
- shotgun/tui/dependencies.py +64 -9
- shotgun/tui/layout.py +5 -0
- shotgun/tui/protocols.py +37 -0
- shotgun/tui/screens/chat/chat.tcss +9 -1
- shotgun/tui/screens/chat/chat_screen.py +1015 -106
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
- shotgun/tui/screens/chat_screen/command_providers.py +13 -89
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
- shotgun/tui/screens/chat_screen/history/chat_history.py +12 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +53 -15
- shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
- shotgun/tui/screens/chat_screen/messages.py +219 -0
- shotgun/tui/screens/confirmation_dialog.py +40 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +10 -3
- shotgun/tui/screens/github_issue.py +11 -2
- shotgun/tui/screens/model_picker.py +28 -8
- shotgun/tui/screens/onboarding.py +179 -26
- shotgun/tui/screens/pipx_migration.py +58 -6
- shotgun/tui/screens/provider_config.py +66 -8
- 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 +110 -16
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +123 -0
- shotgun/tui/services/conversation_service.py +5 -2
- shotgun/tui/utils/mode_progress.py +20 -86
- shotgun/tui/widgets/__init__.py +2 -1
- shotgun/tui/widgets/approval_widget.py +152 -0
- shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
- shotgun/tui/widgets/plan_panel.py +129 -0
- shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
- shotgun/tui/widgets/widget_coordinator.py +1 -1
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/METADATA +11 -4
- shotgun_sh-0.4.0.dev1.dist-info/RECORD +242 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/WHEEL +1 -1
- shotgun_sh-0.2.17.dist-info/RECORD +0 -194
- /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_counting/base.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Pydantic models for spec CLI commands."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PullSource(StrEnum):
|
|
10
|
+
"""Source of spec pull operation for analytics."""
|
|
11
|
+
|
|
12
|
+
CLI = "cli"
|
|
13
|
+
TUI = "tui"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class PullPhase(StrEnum):
|
|
17
|
+
"""Phases during spec pull operation for analytics."""
|
|
18
|
+
|
|
19
|
+
STARTING = "starting"
|
|
20
|
+
FETCHING = "fetching"
|
|
21
|
+
BACKUP = "backup"
|
|
22
|
+
DOWNLOADING = "downloading"
|
|
23
|
+
FINALIZING = "finalizing"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SpecMeta(BaseModel):
|
|
27
|
+
"""Metadata stored in .shotgun/meta.json after pulling a spec.
|
|
28
|
+
|
|
29
|
+
This file tracks the source of the local spec files and is used
|
|
30
|
+
by the TUI to display version information and enable future sync operations.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
version_id: str = Field(description="Pulled version UUID")
|
|
34
|
+
spec_id: str = Field(description="Spec UUID")
|
|
35
|
+
spec_name: str = Field(description="Spec name at time of pull")
|
|
36
|
+
workspace_id: str = Field(description="Workspace UUID")
|
|
37
|
+
is_latest: bool = Field(
|
|
38
|
+
description="Whether this was the latest version when pulled"
|
|
39
|
+
)
|
|
40
|
+
pulled_at: datetime = Field(description="Timestamp when spec was pulled (UTC)")
|
|
41
|
+
backup_path: str | None = Field(
|
|
42
|
+
default=None,
|
|
43
|
+
description="Path where previous .shotgun/ files were backed up",
|
|
44
|
+
)
|
|
45
|
+
web_url: str | None = Field(
|
|
46
|
+
default=None,
|
|
47
|
+
description="URL to view this version in the web UI",
|
|
48
|
+
)
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Shared spec pull service for CLI and TUI."""
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from shotgun.logging_config import get_logger
|
|
10
|
+
from shotgun.posthog_telemetry import track_event
|
|
11
|
+
from shotgun.shotgun_web.specs_client import SpecsClient
|
|
12
|
+
from shotgun.shotgun_web.supabase_client import download_file_from_url
|
|
13
|
+
|
|
14
|
+
from .backup import clear_shotgun_dir, create_backup
|
|
15
|
+
from .models import PullPhase, PullSource, SpecMeta
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class PullProgress:
|
|
22
|
+
"""Progress update during spec pull."""
|
|
23
|
+
|
|
24
|
+
phase: str
|
|
25
|
+
file_index: int | None = None
|
|
26
|
+
total_files: int | None = None
|
|
27
|
+
current_file: str | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class PullResult:
|
|
32
|
+
"""Result of a spec pull operation."""
|
|
33
|
+
|
|
34
|
+
success: bool
|
|
35
|
+
spec_name: str | None = None
|
|
36
|
+
file_count: int = 0
|
|
37
|
+
backup_path: str | None = None
|
|
38
|
+
web_url: str | None = None
|
|
39
|
+
error: str | None = None
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CancelledError(Exception):
|
|
43
|
+
"""Raised when pull is cancelled."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class SpecPullService:
|
|
47
|
+
"""Service for pulling spec versions from cloud."""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._client = SpecsClient()
|
|
51
|
+
|
|
52
|
+
async def pull_version(
|
|
53
|
+
self,
|
|
54
|
+
version_id: str,
|
|
55
|
+
shotgun_dir: Path,
|
|
56
|
+
on_progress: Callable[[PullProgress], None] | None = None,
|
|
57
|
+
is_cancelled: Callable[[], bool] | None = None,
|
|
58
|
+
source: PullSource = PullSource.CLI,
|
|
59
|
+
) -> PullResult:
|
|
60
|
+
"""Pull a spec version to the local directory.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
version_id: The version UUID to pull
|
|
64
|
+
shotgun_dir: Target directory (typically .shotgun/)
|
|
65
|
+
on_progress: Optional callback for progress updates
|
|
66
|
+
is_cancelled: Optional callback to check if cancelled
|
|
67
|
+
source: Source of the pull request (CLI or TUI)
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
PullResult with success status and details
|
|
71
|
+
"""
|
|
72
|
+
start_time = time.time()
|
|
73
|
+
current_phase: PullPhase = PullPhase.STARTING
|
|
74
|
+
track_event("spec_pull_started", {"source": source.value})
|
|
75
|
+
|
|
76
|
+
def report(
|
|
77
|
+
phase: str,
|
|
78
|
+
file_index: int | None = None,
|
|
79
|
+
total_files: int | None = None,
|
|
80
|
+
current_file: str | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
if on_progress:
|
|
83
|
+
on_progress(
|
|
84
|
+
PullProgress(
|
|
85
|
+
phase=phase,
|
|
86
|
+
file_index=file_index,
|
|
87
|
+
total_files=total_files,
|
|
88
|
+
current_file=current_file,
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def check_cancelled() -> None:
|
|
93
|
+
nonlocal current_phase
|
|
94
|
+
if is_cancelled and is_cancelled():
|
|
95
|
+
track_event(
|
|
96
|
+
"spec_pull_cancelled",
|
|
97
|
+
{"source": source.value, "phase": current_phase.value},
|
|
98
|
+
)
|
|
99
|
+
raise CancelledError()
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Phase 1: Fetch version metadata
|
|
103
|
+
current_phase = PullPhase.FETCHING
|
|
104
|
+
report("Fetching version info...")
|
|
105
|
+
check_cancelled()
|
|
106
|
+
|
|
107
|
+
response = await self._client.get_version_with_files(version_id)
|
|
108
|
+
spec_name = response.spec_name
|
|
109
|
+
files = response.files
|
|
110
|
+
|
|
111
|
+
if not files:
|
|
112
|
+
track_event(
|
|
113
|
+
"spec_pull_failed",
|
|
114
|
+
{
|
|
115
|
+
"source": source.value,
|
|
116
|
+
"error_type": "EmptyVersion",
|
|
117
|
+
"phase": current_phase.value,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
return PullResult(
|
|
121
|
+
success=False,
|
|
122
|
+
spec_name=spec_name,
|
|
123
|
+
error="No files in this version.",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Phase 2: Backup existing content
|
|
127
|
+
current_phase = PullPhase.BACKUP
|
|
128
|
+
backup_path: str | None = None
|
|
129
|
+
if shotgun_dir.exists():
|
|
130
|
+
report("Backing up existing files...")
|
|
131
|
+
check_cancelled()
|
|
132
|
+
|
|
133
|
+
backup_path = await create_backup(shotgun_dir)
|
|
134
|
+
if backup_path:
|
|
135
|
+
clear_shotgun_dir(shotgun_dir)
|
|
136
|
+
|
|
137
|
+
# Ensure directory exists
|
|
138
|
+
shotgun_dir.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
|
|
140
|
+
# Phase 3: Download files
|
|
141
|
+
current_phase = PullPhase.DOWNLOADING
|
|
142
|
+
total_files = len(files)
|
|
143
|
+
total_bytes = 0
|
|
144
|
+
for idx, file_info in enumerate(files):
|
|
145
|
+
check_cancelled()
|
|
146
|
+
|
|
147
|
+
report(
|
|
148
|
+
f"Downloading files ({idx + 1}/{total_files})...",
|
|
149
|
+
file_index=idx,
|
|
150
|
+
total_files=total_files,
|
|
151
|
+
current_file=file_info.relative_path,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
if not file_info.download_url:
|
|
155
|
+
logger.warning(
|
|
156
|
+
"Skipping file without download URL: %s",
|
|
157
|
+
file_info.relative_path,
|
|
158
|
+
)
|
|
159
|
+
continue
|
|
160
|
+
|
|
161
|
+
content = await download_file_from_url(file_info.download_url)
|
|
162
|
+
total_bytes += file_info.size_bytes
|
|
163
|
+
|
|
164
|
+
local_path = shotgun_dir / file_info.relative_path
|
|
165
|
+
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
166
|
+
local_path.write_bytes(content)
|
|
167
|
+
|
|
168
|
+
# Phase 4: Write meta.json
|
|
169
|
+
current_phase = PullPhase.FINALIZING
|
|
170
|
+
report("Finalizing...")
|
|
171
|
+
check_cancelled()
|
|
172
|
+
|
|
173
|
+
meta = SpecMeta(
|
|
174
|
+
version_id=response.version.id,
|
|
175
|
+
spec_id=response.spec_id,
|
|
176
|
+
spec_name=response.spec_name,
|
|
177
|
+
workspace_id=response.workspace_id,
|
|
178
|
+
is_latest=response.version.is_latest,
|
|
179
|
+
pulled_at=datetime.now(timezone.utc),
|
|
180
|
+
backup_path=backup_path,
|
|
181
|
+
web_url=response.web_url,
|
|
182
|
+
)
|
|
183
|
+
meta_path = shotgun_dir / "meta.json"
|
|
184
|
+
meta_path.write_text(meta.model_dump_json(indent=2))
|
|
185
|
+
|
|
186
|
+
# Track successful completion
|
|
187
|
+
duration = time.time() - start_time
|
|
188
|
+
track_event(
|
|
189
|
+
"spec_pull_completed",
|
|
190
|
+
{
|
|
191
|
+
"source": source.value,
|
|
192
|
+
"file_count": total_files,
|
|
193
|
+
"total_bytes": total_bytes,
|
|
194
|
+
"duration_seconds": round(duration, 2),
|
|
195
|
+
"had_backup": backup_path is not None,
|
|
196
|
+
},
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
return PullResult(
|
|
200
|
+
success=True,
|
|
201
|
+
spec_name=spec_name,
|
|
202
|
+
file_count=total_files,
|
|
203
|
+
backup_path=backup_path,
|
|
204
|
+
web_url=response.web_url,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
except CancelledError:
|
|
208
|
+
# Already tracked in check_cancelled()
|
|
209
|
+
raise
|
|
210
|
+
except Exception as e:
|
|
211
|
+
track_event(
|
|
212
|
+
"spec_pull_failed",
|
|
213
|
+
{
|
|
214
|
+
"source": source.value,
|
|
215
|
+
"error_type": type(e).__name__,
|
|
216
|
+
"phase": current_phase.value,
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
raise
|
shotgun/cli/specify.py
CHANGED
|
@@ -11,6 +11,8 @@ from shotgun.agents.specify import (
|
|
|
11
11
|
create_specify_agent,
|
|
12
12
|
run_specify_agent,
|
|
13
13
|
)
|
|
14
|
+
from shotgun.cli.error_handler import print_agent_error
|
|
15
|
+
from shotgun.exceptions import ErrorNotPickedUpBySentry
|
|
14
16
|
from shotgun.logging_config import get_logger
|
|
15
17
|
|
|
16
18
|
app = typer.Typer(
|
|
@@ -44,26 +46,25 @@ def specify(
|
|
|
44
46
|
|
|
45
47
|
logger.info("📝 Specification Requirement: %s", requirement)
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
agent_runtime_options = AgentRuntimeOptions(
|
|
50
|
-
interactive_mode=not non_interactive
|
|
51
|
-
)
|
|
49
|
+
# Create agent dependencies
|
|
50
|
+
agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
# Create the specify agent with deps and provider
|
|
53
|
+
agent, deps = asyncio.run(create_specify_agent(agent_runtime_options, provider))
|
|
55
54
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
result = asyncio.run(run_specify_agent(agent, requirement, deps))
|
|
55
|
+
# Start specification process with error handling
|
|
56
|
+
logger.info("📋 Starting specification generation...")
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
58
|
+
async def async_specify() -> None:
|
|
59
|
+
try:
|
|
60
|
+
result = await run_specify_agent(agent, requirement, deps)
|
|
61
|
+
logger.info("✅ Specification Complete!")
|
|
62
|
+
logger.info("📋 Results:")
|
|
63
|
+
logger.info("%s", result.output)
|
|
64
|
+
except ErrorNotPickedUpBySentry as e:
|
|
65
|
+
print_agent_error(e)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.exception("Unexpected error in specify command")
|
|
68
|
+
print(f"⚠️ An unexpected error occurred: {str(e)}")
|
|
64
69
|
|
|
65
|
-
|
|
66
|
-
logger.error("❌ Error during specification: %s", str(e))
|
|
67
|
-
import traceback
|
|
68
|
-
|
|
69
|
-
logger.debug("Full traceback:\n%s", traceback.format_exc())
|
|
70
|
+
asyncio.run(async_specify())
|
shotgun/cli/tasks.py
CHANGED
|
@@ -11,7 +11,10 @@ from shotgun.agents.tasks import (
|
|
|
11
11
|
create_tasks_agent,
|
|
12
12
|
run_tasks_agent,
|
|
13
13
|
)
|
|
14
|
+
from shotgun.cli.error_handler import print_agent_error
|
|
15
|
+
from shotgun.exceptions import ErrorNotPickedUpBySentry
|
|
14
16
|
from shotgun.logging_config import get_logger
|
|
17
|
+
from shotgun.posthog_telemetry import track_event
|
|
15
18
|
|
|
16
19
|
app = typer.Typer(name="tasks", help="Generate task lists with agentic approach")
|
|
17
20
|
logger = get_logger(__name__)
|
|
@@ -42,37 +45,34 @@ def tasks(
|
|
|
42
45
|
|
|
43
46
|
logger.info("📋 Task Creation Instruction: %s", instruction)
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
import traceback
|
|
77
|
-
|
|
78
|
-
logger.debug("Full traceback:\n%s", traceback.format_exc())
|
|
48
|
+
# Track tasks command usage
|
|
49
|
+
track_event(
|
|
50
|
+
"tasks_command",
|
|
51
|
+
{
|
|
52
|
+
"non_interactive": non_interactive,
|
|
53
|
+
"provider": provider.value if provider else "default",
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Create agent dependencies
|
|
58
|
+
agent_runtime_options = AgentRuntimeOptions(interactive_mode=not non_interactive)
|
|
59
|
+
|
|
60
|
+
# Create the tasks agent with deps and provider
|
|
61
|
+
agent, deps = asyncio.run(create_tasks_agent(agent_runtime_options, provider))
|
|
62
|
+
|
|
63
|
+
# Start task creation process with error handling
|
|
64
|
+
logger.info("🎯 Starting task creation...")
|
|
65
|
+
|
|
66
|
+
async def async_tasks() -> None:
|
|
67
|
+
try:
|
|
68
|
+
result = await run_tasks_agent(agent, instruction, deps)
|
|
69
|
+
logger.info("✅ Task Creation Complete!")
|
|
70
|
+
logger.info("📋 Results:")
|
|
71
|
+
logger.info("%s", result.output)
|
|
72
|
+
except ErrorNotPickedUpBySentry as e:
|
|
73
|
+
print_agent_error(e)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.exception("Unexpected error in tasks command")
|
|
76
|
+
print(f"⚠️ An unexpected error occurred: {str(e)}")
|
|
77
|
+
|
|
78
|
+
asyncio.run(async_tasks())
|
|
@@ -6,11 +6,12 @@ import os
|
|
|
6
6
|
import time
|
|
7
7
|
import uuid
|
|
8
8
|
from collections import defaultdict
|
|
9
|
+
from collections.abc import Callable
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from typing import Any
|
|
11
12
|
|
|
12
13
|
import aiofiles
|
|
13
|
-
import kuzu
|
|
14
|
+
import real_ladybug as kuzu
|
|
14
15
|
from tree_sitter import Node, Parser, QueryCursor
|
|
15
16
|
|
|
16
17
|
from shotgun.codebase.core.language_config import LANGUAGE_CONFIGS, get_language_config
|
|
@@ -198,11 +199,21 @@ class Ingestor:
|
|
|
198
199
|
return True
|
|
199
200
|
return False
|
|
200
201
|
|
|
201
|
-
def flush_nodes(
|
|
202
|
-
|
|
202
|
+
def flush_nodes(
|
|
203
|
+
self,
|
|
204
|
+
progress_callback: Callable[[int, int], None] | None = None,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Flush pending node insertions to the database.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
progress_callback: Optional callback(current, total) for progress reporting
|
|
210
|
+
"""
|
|
203
211
|
if not self.node_buffer:
|
|
204
212
|
return
|
|
205
213
|
|
|
214
|
+
total_nodes = len(self.node_buffer)
|
|
215
|
+
processed = 0
|
|
216
|
+
|
|
206
217
|
# Group nodes by label
|
|
207
218
|
nodes_by_label: dict[str, list[dict[str, Any]]] = defaultdict(list)
|
|
208
219
|
for label, properties in self.node_buffer:
|
|
@@ -239,9 +250,18 @@ class Ingestor:
|
|
|
239
250
|
params = dict(zip(prop_names, prop_values, strict=False))
|
|
240
251
|
self.conn.execute(query, params)
|
|
241
252
|
|
|
253
|
+
# Report progress
|
|
254
|
+
processed += 1
|
|
255
|
+
if progress_callback and processed % 10 == 0:
|
|
256
|
+
progress_callback(processed, total_nodes)
|
|
257
|
+
|
|
242
258
|
except Exception as e:
|
|
243
259
|
logger.error(f"Failed to insert {label} nodes: {e}")
|
|
244
260
|
|
|
261
|
+
# Final progress report
|
|
262
|
+
if progress_callback:
|
|
263
|
+
progress_callback(total_nodes, total_nodes)
|
|
264
|
+
|
|
245
265
|
# Log node counts by type
|
|
246
266
|
node_type_counts: dict[str, int] = {}
|
|
247
267
|
for label, _ in self.node_buffer:
|
|
@@ -280,11 +300,21 @@ class Ingestor:
|
|
|
280
300
|
|
|
281
301
|
# Don't auto-flush relationships - wait for explicit flush_all() to ensure nodes exist first
|
|
282
302
|
|
|
283
|
-
def flush_relationships(
|
|
284
|
-
|
|
303
|
+
def flush_relationships(
|
|
304
|
+
self,
|
|
305
|
+
progress_callback: Callable[[int, int], None] | None = None,
|
|
306
|
+
) -> None:
|
|
307
|
+
"""Flush pending relationship insertions to the database.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
progress_callback: Optional callback(current, total) for progress reporting
|
|
311
|
+
"""
|
|
285
312
|
if not self.relationship_buffer:
|
|
286
313
|
return
|
|
287
314
|
|
|
315
|
+
total_rels = len(self.relationship_buffer)
|
|
316
|
+
processed = 0
|
|
317
|
+
|
|
288
318
|
# Group relationships by type
|
|
289
319
|
rels_by_type: dict[
|
|
290
320
|
str, list[tuple[str, str, Any, str, str, str, Any, dict[str, Any] | None]]
|
|
@@ -299,7 +329,7 @@ class Ingestor:
|
|
|
299
329
|
to_label,
|
|
300
330
|
to_key,
|
|
301
331
|
to_value,
|
|
302
|
-
|
|
332
|
+
_properties,
|
|
303
333
|
) = rel_data
|
|
304
334
|
|
|
305
335
|
# Determine actual table name
|
|
@@ -323,7 +353,7 @@ class Ingestor:
|
|
|
323
353
|
to_label,
|
|
324
354
|
to_key,
|
|
325
355
|
to_value,
|
|
326
|
-
|
|
356
|
+
_properties,
|
|
327
357
|
) = rel_data
|
|
328
358
|
|
|
329
359
|
# Build MATCH and MERGE query (use MERGE to avoid duplicate relationships)
|
|
@@ -337,6 +367,11 @@ class Ingestor:
|
|
|
337
367
|
try:
|
|
338
368
|
self.conn.execute(query, params)
|
|
339
369
|
success_count += 1
|
|
370
|
+
|
|
371
|
+
# Report progress
|
|
372
|
+
processed += 1
|
|
373
|
+
if progress_callback and processed % 10 == 0:
|
|
374
|
+
progress_callback(processed, total_rels)
|
|
340
375
|
except Exception as e:
|
|
341
376
|
logger.error(
|
|
342
377
|
f"Failed to create single relationship {table_name}: {from_label}({from_value}) -> {to_label}({to_value})"
|
|
@@ -360,6 +395,10 @@ class Ingestor:
|
|
|
360
395
|
# Don't swallow the exception - let it propagate
|
|
361
396
|
raise
|
|
362
397
|
|
|
398
|
+
# Final progress report
|
|
399
|
+
if progress_callback:
|
|
400
|
+
progress_callback(total_rels, total_rels)
|
|
401
|
+
|
|
363
402
|
# Log summary of flushed relationships
|
|
364
403
|
logger.info(
|
|
365
404
|
f"Flushed {len(self.relationship_buffer)} relationships: {relationship_counts}"
|
|
@@ -586,6 +625,9 @@ class SimpleGraphBuilder:
|
|
|
586
625
|
self.ignore_dirs = self.ignore_dirs.union(set(exclude_patterns))
|
|
587
626
|
self.progress_callback = progress_callback
|
|
588
627
|
|
|
628
|
+
# Generate unique session ID for correlating timing events in PostHog
|
|
629
|
+
self._index_session_id = str(uuid.uuid4())[:8]
|
|
630
|
+
|
|
589
631
|
# Caches
|
|
590
632
|
self.structural_elements: dict[Path, str | None] = {}
|
|
591
633
|
self.ast_cache: dict[Path, tuple[Node, str]] = {}
|
|
@@ -621,25 +663,129 @@ class SimpleGraphBuilder:
|
|
|
621
663
|
# Don't let progress callback errors crash the build
|
|
622
664
|
logger.debug(f"Progress callback error: {e}")
|
|
623
665
|
|
|
666
|
+
def _log_timing(
|
|
667
|
+
self,
|
|
668
|
+
phase: str,
|
|
669
|
+
duration: float,
|
|
670
|
+
items: int,
|
|
671
|
+
extra_props: dict[str, Any] | None = None,
|
|
672
|
+
) -> None:
|
|
673
|
+
"""Log timing data to PostHog for analysis."""
|
|
674
|
+
from shotgun.posthog_telemetry import track_event
|
|
675
|
+
|
|
676
|
+
properties: dict[str, Any] = {
|
|
677
|
+
"session_id": self._index_session_id,
|
|
678
|
+
"phase": phase,
|
|
679
|
+
"duration_seconds": round(duration, 3),
|
|
680
|
+
"item_count": items,
|
|
681
|
+
}
|
|
682
|
+
if extra_props:
|
|
683
|
+
properties.update(extra_props)
|
|
684
|
+
|
|
685
|
+
track_event("codebase_index_phase_completed", properties)
|
|
686
|
+
|
|
687
|
+
def _log_summary(
|
|
688
|
+
self,
|
|
689
|
+
total_duration: float,
|
|
690
|
+
total_files: int,
|
|
691
|
+
total_nodes: int,
|
|
692
|
+
total_relationships: int,
|
|
693
|
+
) -> None:
|
|
694
|
+
"""Log indexing summary event to PostHog."""
|
|
695
|
+
from shotgun.posthog_telemetry import track_event
|
|
696
|
+
|
|
697
|
+
track_event(
|
|
698
|
+
"codebase_index_completed",
|
|
699
|
+
{
|
|
700
|
+
"session_id": self._index_session_id,
|
|
701
|
+
"total_duration_seconds": round(total_duration, 3),
|
|
702
|
+
"total_files": total_files,
|
|
703
|
+
"total_nodes": total_nodes,
|
|
704
|
+
"total_relationships": total_relationships,
|
|
705
|
+
},
|
|
706
|
+
)
|
|
707
|
+
|
|
624
708
|
async def run(self) -> None:
|
|
625
709
|
"""Run the three-pass graph building process."""
|
|
626
710
|
logger.info(f"Building graph for project: {self.project_name}")
|
|
627
711
|
|
|
628
712
|
# Pass 1: Structure
|
|
629
713
|
logger.info("Pass 1: Identifying packages and folders...")
|
|
714
|
+
t0 = time.time()
|
|
630
715
|
self._identify_structure()
|
|
716
|
+
t1 = time.time()
|
|
717
|
+
self._log_timing("structure", t1 - t0, len(self.structural_elements))
|
|
631
718
|
|
|
632
719
|
# Pass 2: Definitions
|
|
633
720
|
logger.info("Pass 2: Processing files and extracting definitions...")
|
|
721
|
+
t2 = time.time()
|
|
634
722
|
await self._process_files()
|
|
723
|
+
t3 = time.time()
|
|
724
|
+
self._log_timing(
|
|
725
|
+
"definitions",
|
|
726
|
+
t3 - t2,
|
|
727
|
+
len(self.ast_cache),
|
|
728
|
+
{"file_count": len(self.ast_cache)},
|
|
729
|
+
)
|
|
635
730
|
|
|
636
731
|
# Pass 3: Relationships
|
|
637
732
|
logger.info("Pass 3: Processing relationships (calls, imports)...")
|
|
733
|
+
t4 = time.time()
|
|
638
734
|
self._process_relationships()
|
|
735
|
+
t5 = time.time()
|
|
736
|
+
self._log_timing("relationships", t5 - t4, len(self.ast_cache))
|
|
639
737
|
|
|
640
738
|
# Flush all pending operations
|
|
641
739
|
logger.info("Flushing all data to database...")
|
|
642
|
-
|
|
740
|
+
t6 = time.time()
|
|
741
|
+
node_count = len(self.ingestor.node_buffer)
|
|
742
|
+
|
|
743
|
+
# Create progress callback for flush_nodes
|
|
744
|
+
def node_progress(current: int, total: int) -> None:
|
|
745
|
+
self._report_progress(
|
|
746
|
+
"flush_nodes", "Flushing nodes to database", current, total
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
self.ingestor.flush_nodes(progress_callback=node_progress)
|
|
750
|
+
self._report_progress(
|
|
751
|
+
"flush_nodes", "Flushing nodes to database", node_count, node_count, True
|
|
752
|
+
)
|
|
753
|
+
t7 = time.time()
|
|
754
|
+
self._log_timing("flush_nodes", t7 - t6, node_count, {"node_count": node_count})
|
|
755
|
+
|
|
756
|
+
rel_count = len(self.ingestor.relationship_buffer)
|
|
757
|
+
|
|
758
|
+
# Create progress callback for flush_relationships
|
|
759
|
+
def rel_progress(current: int, total: int) -> None:
|
|
760
|
+
self._report_progress(
|
|
761
|
+
"flush_relationships",
|
|
762
|
+
"Flushing relationships to database",
|
|
763
|
+
current,
|
|
764
|
+
total,
|
|
765
|
+
)
|
|
766
|
+
|
|
767
|
+
self.ingestor.flush_relationships(progress_callback=rel_progress)
|
|
768
|
+
self._report_progress(
|
|
769
|
+
"flush_relationships",
|
|
770
|
+
"Flushing relationships to database",
|
|
771
|
+
rel_count,
|
|
772
|
+
rel_count,
|
|
773
|
+
True,
|
|
774
|
+
)
|
|
775
|
+
t8 = time.time()
|
|
776
|
+
self._log_timing(
|
|
777
|
+
"flush_relationships", t8 - t7, rel_count, {"relationship_count": rel_count}
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
# Track summary event with totals (no PII - only numeric metadata)
|
|
781
|
+
total_duration = t8 - t0
|
|
782
|
+
self._log_summary(
|
|
783
|
+
total_duration=total_duration,
|
|
784
|
+
total_files=len(self.ast_cache),
|
|
785
|
+
total_nodes=node_count,
|
|
786
|
+
total_relationships=rel_count,
|
|
787
|
+
)
|
|
788
|
+
|
|
643
789
|
logger.info("Graph building complete!")
|
|
644
790
|
|
|
645
791
|
def _identify_structure(self) -> None:
|
shotgun/codebase/core/manager.py
CHANGED
shotgun/codebase/models.py
CHANGED
|
@@ -29,6 +29,8 @@ class ProgressPhase(StrEnum):
|
|
|
29
29
|
STRUCTURE = "structure" # Identifying packages and folders
|
|
30
30
|
DEFINITIONS = "definitions" # Processing files and extracting definitions
|
|
31
31
|
RELATIONSHIPS = "relationships" # Processing relationships (calls, imports)
|
|
32
|
+
FLUSH_NODES = "flush_nodes" # Flushing nodes to database
|
|
33
|
+
FLUSH_RELATIONSHIPS = "flush_relationships" # Flushing relationships to database
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
class IndexProgress(BaseModel):
|