shotgun-sh 0.2.23.dev1__py3-none-any.whl → 0.2.29.dev2__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.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (86) hide show
  1. shotgun/agents/agent_manager.py +3 -3
  2. shotgun/agents/common.py +1 -1
  3. shotgun/agents/config/manager.py +36 -21
  4. shotgun/agents/config/models.py +30 -0
  5. shotgun/agents/config/provider.py +27 -14
  6. shotgun/agents/context_analyzer/analyzer.py +6 -2
  7. shotgun/agents/conversation/__init__.py +18 -0
  8. shotgun/agents/conversation/filters.py +164 -0
  9. shotgun/agents/conversation/history/chunking.py +278 -0
  10. shotgun/agents/{history → conversation/history}/compaction.py +27 -1
  11. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  12. shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
  13. shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
  14. shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
  15. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
  16. shotgun/agents/tools/web_search/openai.py +1 -1
  17. shotgun/cli/clear.py +1 -1
  18. shotgun/cli/compact.py +5 -3
  19. shotgun/cli/context.py +1 -1
  20. shotgun/cli/spec/__init__.py +5 -0
  21. shotgun/cli/spec/backup.py +81 -0
  22. shotgun/cli/spec/commands.py +130 -0
  23. shotgun/cli/spec/models.py +30 -0
  24. shotgun/cli/spec/pull_service.py +165 -0
  25. shotgun/codebase/core/ingestor.py +153 -7
  26. shotgun/codebase/models.py +2 -0
  27. shotgun/exceptions.py +5 -3
  28. shotgun/main.py +2 -0
  29. shotgun/posthog_telemetry.py +1 -1
  30. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +3 -3
  31. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  32. shotgun/prompts/agents/research.j2 +0 -3
  33. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  34. shotgun/prompts/history/combine_summaries.j2 +53 -0
  35. shotgun/shotgun_web/__init__.py +67 -1
  36. shotgun/shotgun_web/client.py +42 -1
  37. shotgun/shotgun_web/constants.py +46 -0
  38. shotgun/shotgun_web/exceptions.py +29 -0
  39. shotgun/shotgun_web/models.py +390 -0
  40. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  41. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  42. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  43. shotgun/shotgun_web/shared_specs/models.py +71 -0
  44. shotgun/shotgun_web/shared_specs/upload_pipeline.py +291 -0
  45. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  46. shotgun/shotgun_web/specs_client.py +703 -0
  47. shotgun/shotgun_web/supabase_client.py +31 -0
  48. shotgun/tui/app.py +39 -0
  49. shotgun/tui/containers.py +1 -1
  50. shotgun/tui/layout.py +5 -0
  51. shotgun/tui/screens/chat/chat_screen.py +212 -16
  52. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +147 -19
  53. shotgun/tui/screens/chat_screen/command_providers.py +10 -0
  54. shotgun/tui/screens/chat_screen/history/chat_history.py +0 -36
  55. shotgun/tui/screens/confirmation_dialog.py +40 -0
  56. shotgun/tui/screens/model_picker.py +7 -1
  57. shotgun/tui/screens/onboarding.py +149 -0
  58. shotgun/tui/screens/pipx_migration.py +46 -0
  59. shotgun/tui/screens/provider_config.py +41 -0
  60. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  61. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  62. shotgun/tui/screens/shared_specs/models.py +56 -0
  63. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  64. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  65. shotgun/tui/screens/shotgun_auth.py +60 -6
  66. shotgun/tui/screens/spec_pull.py +286 -0
  67. shotgun/tui/screens/welcome.py +91 -0
  68. shotgun/tui/services/conversation_service.py +5 -2
  69. shotgun/tui/widgets/widget_coordinator.py +1 -1
  70. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/METADATA +1 -1
  71. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/RECORD +86 -59
  72. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/WHEEL +1 -1
  73. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  74. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  75. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  76. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  77. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  78. /shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +0 -0
  79. /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
  80. /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
  81. /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
  82. /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
  83. /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
  84. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
  85. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/entry_points.txt +0 -0
  86. {shotgun_sh-0.2.23.dev1.dist-info → shotgun_sh-0.2.29.dev2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,130 @@
1
+ """Spec management commands for shotgun CLI."""
2
+
3
+ import asyncio
4
+ from typing import Annotated
5
+
6
+ import typer
7
+ from rich.console import Console
8
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TaskID, TextColumn
9
+
10
+ from shotgun.logging_config import get_logger
11
+ from shotgun.shotgun_web.exceptions import (
12
+ ForbiddenError,
13
+ NotFoundError,
14
+ UnauthorizedError,
15
+ )
16
+ from shotgun.tui import app as tui_app
17
+ from shotgun.utils.file_system_utils import get_shotgun_base_path
18
+
19
+ from .pull_service import CancelledError, PullProgress, SpecPullService
20
+
21
+ app = typer.Typer(
22
+ name="spec",
23
+ help="Manage shared specifications",
24
+ no_args_is_help=True,
25
+ )
26
+ logger = get_logger(__name__)
27
+ console = Console()
28
+
29
+
30
+ @app.command()
31
+ def pull(
32
+ version_id: Annotated[str, typer.Argument(help="Version ID to pull")],
33
+ no_tui: Annotated[
34
+ bool,
35
+ typer.Option("--no-tui", help="Run in CLI-only mode (requires existing auth)"),
36
+ ] = False,
37
+ ) -> None:
38
+ """Pull a spec version from the cloud to local .shotgun/ directory.
39
+
40
+ Downloads all files for the specified version and writes them to the
41
+ local .shotgun/ directory. If the directory already has content, it
42
+ will be backed up to ~/.shotgun-sh/backups/ before being replaced.
43
+
44
+ By default, launches the TUI which handles authentication and shows
45
+ the pull progress. Use --no-tui for scripted/headless use (requires
46
+ existing authentication).
47
+
48
+ Example:
49
+ shotgun spec pull 2532e1c7-7068-4d23-9379-58ea439c592f
50
+ """
51
+ if no_tui:
52
+ # CLI-only mode: do pull directly (requires existing auth)
53
+ success = asyncio.run(_async_pull(version_id))
54
+ if not success:
55
+ raise typer.Exit(1)
56
+ else:
57
+ # TUI mode: launch TUI which handles auth and pull
58
+ tui_app.run(pull_version_id=version_id)
59
+
60
+
61
+ async def _async_pull(version_id: str) -> bool:
62
+ """Async implementation of spec pull command.
63
+
64
+ Returns:
65
+ True if pull was successful, False otherwise.
66
+ """
67
+ shotgun_dir = get_shotgun_base_path()
68
+ service = SpecPullService()
69
+
70
+ # Track current progress state for rich display
71
+ current_task_id: TaskID | None = None
72
+ progress_ctx: Progress | None = None
73
+
74
+ def on_progress(p: PullProgress) -> None:
75
+ nonlocal current_task_id, progress_ctx
76
+ # For CLI, we just update the description - progress bar handled by result
77
+ if progress_ctx and current_task_id is not None:
78
+ progress_ctx.update(current_task_id, description=p.phase)
79
+ if p.total_files and p.file_index is not None:
80
+ pct = ((p.file_index + 1) / p.total_files) * 100
81
+ progress_ctx.update(current_task_id, completed=pct)
82
+
83
+ try:
84
+ with Progress(
85
+ SpinnerColumn(),
86
+ TextColumn("[progress.description]{task.description}"),
87
+ BarColumn(),
88
+ TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
89
+ ) as progress:
90
+ progress_ctx = progress
91
+ current_task_id = progress.add_task("Starting...", total=100)
92
+
93
+ result = await service.pull_version(
94
+ version_id=version_id,
95
+ shotgun_dir=shotgun_dir,
96
+ on_progress=on_progress,
97
+ )
98
+
99
+ if result.success:
100
+ console.print()
101
+ console.print(f"[green]Successfully pulled '{result.spec_name}'[/green]")
102
+ console.print(f" [dim]Files downloaded:[/dim] {result.file_count}")
103
+ if result.backup_path:
104
+ console.print(f" [dim]Previous backup:[/dim] {result.backup_path}")
105
+ if result.web_url:
106
+ console.print(f" [blue]View in browser:[/blue] {result.web_url}")
107
+ return True
108
+ else:
109
+ console.print(f"[red]Error: {result.error}[/red]")
110
+ return False
111
+
112
+ except UnauthorizedError:
113
+ console.print(
114
+ "[red]Not authenticated. Please re-run the command to login.[/red]"
115
+ )
116
+ raise typer.Exit(1) from None
117
+ except NotFoundError:
118
+ console.print(f"[red]Version not found: {version_id}[/red]")
119
+ console.print("[dim]Check the version ID and try again.[/dim]")
120
+ raise typer.Exit(1) from None
121
+ except ForbiddenError:
122
+ console.print("[red]You don't have access to this spec.[/red]")
123
+ raise typer.Exit(1) from None
124
+ except CancelledError:
125
+ console.print("[yellow]Pull cancelled.[/yellow]")
126
+ raise typer.Exit(1) from None
127
+ except Exception as e:
128
+ logger.exception("Unexpected error in spec pull")
129
+ console.print(f"[red]Unexpected error: {e}[/red]")
130
+ raise typer.Exit(1) from None
@@ -0,0 +1,30 @@
1
+ """Pydantic models for spec CLI commands."""
2
+
3
+ from datetime import datetime
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class SpecMeta(BaseModel):
9
+ """Metadata stored in .shotgun/meta.json after pulling a spec.
10
+
11
+ This file tracks the source of the local spec files and is used
12
+ by the TUI to display version information and enable future sync operations.
13
+ """
14
+
15
+ version_id: str = Field(description="Pulled version UUID")
16
+ spec_id: str = Field(description="Spec UUID")
17
+ spec_name: str = Field(description="Spec name at time of pull")
18
+ workspace_id: str = Field(description="Workspace UUID")
19
+ is_latest: bool = Field(
20
+ description="Whether this was the latest version when pulled"
21
+ )
22
+ pulled_at: datetime = Field(description="Timestamp when spec was pulled (UTC)")
23
+ backup_path: str | None = Field(
24
+ default=None,
25
+ description="Path where previous .shotgun/ files were backed up",
26
+ )
27
+ web_url: str | None = Field(
28
+ default=None,
29
+ description="URL to view this version in the web UI",
30
+ )
@@ -0,0 +1,165 @@
1
+ """Shared spec pull service for CLI and TUI."""
2
+
3
+ from collections.abc import Callable
4
+ from dataclasses import dataclass
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+
8
+ from shotgun.logging_config import get_logger
9
+ from shotgun.shotgun_web.specs_client import SpecsClient
10
+ from shotgun.shotgun_web.supabase_client import download_file_from_url
11
+
12
+ from .backup import clear_shotgun_dir, create_backup
13
+ from .models import SpecMeta
14
+
15
+ logger = get_logger(__name__)
16
+
17
+
18
+ @dataclass
19
+ class PullProgress:
20
+ """Progress update during spec pull."""
21
+
22
+ phase: str
23
+ file_index: int | None = None
24
+ total_files: int | None = None
25
+ current_file: str | None = None
26
+
27
+
28
+ @dataclass
29
+ class PullResult:
30
+ """Result of a spec pull operation."""
31
+
32
+ success: bool
33
+ spec_name: str | None = None
34
+ file_count: int = 0
35
+ backup_path: str | None = None
36
+ web_url: str | None = None
37
+ error: str | None = None
38
+
39
+
40
+ class CancelledError(Exception):
41
+ """Raised when pull is cancelled."""
42
+
43
+
44
+ class SpecPullService:
45
+ """Service for pulling spec versions from cloud."""
46
+
47
+ def __init__(self) -> None:
48
+ self._client = SpecsClient()
49
+
50
+ async def pull_version(
51
+ self,
52
+ version_id: str,
53
+ shotgun_dir: Path,
54
+ on_progress: Callable[[PullProgress], None] | None = None,
55
+ is_cancelled: Callable[[], bool] | None = None,
56
+ ) -> PullResult:
57
+ """Pull a spec version to the local directory.
58
+
59
+ Args:
60
+ version_id: The version UUID to pull
61
+ shotgun_dir: Target directory (typically .shotgun/)
62
+ on_progress: Optional callback for progress updates
63
+ is_cancelled: Optional callback to check if cancelled
64
+
65
+ Returns:
66
+ PullResult with success status and details
67
+ """
68
+
69
+ def report(
70
+ phase: str,
71
+ file_index: int | None = None,
72
+ total_files: int | None = None,
73
+ current_file: str | None = None,
74
+ ) -> None:
75
+ if on_progress:
76
+ on_progress(
77
+ PullProgress(
78
+ phase=phase,
79
+ file_index=file_index,
80
+ total_files=total_files,
81
+ current_file=current_file,
82
+ )
83
+ )
84
+
85
+ def check_cancelled() -> None:
86
+ if is_cancelled and is_cancelled():
87
+ raise CancelledError()
88
+
89
+ # Phase 1: Fetch version metadata
90
+ report("Fetching version info...")
91
+ check_cancelled()
92
+
93
+ response = await self._client.get_version_with_files(version_id)
94
+ spec_name = response.spec_name
95
+ files = response.files
96
+
97
+ if not files:
98
+ return PullResult(
99
+ success=False,
100
+ spec_name=spec_name,
101
+ error="No files in this version.",
102
+ )
103
+
104
+ # Phase 2: Backup existing content
105
+ backup_path: str | None = None
106
+ if shotgun_dir.exists():
107
+ report("Backing up existing files...")
108
+ check_cancelled()
109
+
110
+ backup_path = await create_backup(shotgun_dir)
111
+ if backup_path:
112
+ clear_shotgun_dir(shotgun_dir)
113
+
114
+ # Ensure directory exists
115
+ shotgun_dir.mkdir(parents=True, exist_ok=True)
116
+
117
+ # Phase 3: Download files
118
+ total_files = len(files)
119
+ for idx, file_info in enumerate(files):
120
+ check_cancelled()
121
+
122
+ report(
123
+ f"Downloading files ({idx + 1}/{total_files})...",
124
+ file_index=idx,
125
+ total_files=total_files,
126
+ current_file=file_info.relative_path,
127
+ )
128
+
129
+ if not file_info.download_url:
130
+ logger.warning(
131
+ "Skipping file without download URL: %s",
132
+ file_info.relative_path,
133
+ )
134
+ continue
135
+
136
+ content = await download_file_from_url(file_info.download_url)
137
+
138
+ local_path = shotgun_dir / file_info.relative_path
139
+ local_path.parent.mkdir(parents=True, exist_ok=True)
140
+ local_path.write_bytes(content)
141
+
142
+ # Phase 4: Write meta.json
143
+ report("Finalizing...")
144
+ check_cancelled()
145
+
146
+ meta = SpecMeta(
147
+ version_id=response.version.id,
148
+ spec_id=response.spec_id,
149
+ spec_name=response.spec_name,
150
+ workspace_id=response.workspace_id,
151
+ is_latest=response.version.is_latest,
152
+ pulled_at=datetime.now(timezone.utc),
153
+ backup_path=backup_path,
154
+ web_url=response.web_url,
155
+ )
156
+ meta_path = shotgun_dir / "meta.json"
157
+ meta_path.write_text(meta.model_dump_json(indent=2))
158
+
159
+ return PullResult(
160
+ success=True,
161
+ spec_name=spec_name,
162
+ file_count=total_files,
163
+ backup_path=backup_path,
164
+ web_url=response.web_url,
165
+ )
@@ -6,6 +6,7 @@ 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
 
@@ -198,11 +199,21 @@ class Ingestor:
198
199
  return True
199
200
  return False
200
201
 
201
- def flush_nodes(self) -> None:
202
- """Flush pending node insertions to the database."""
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(self) -> None:
284
- """Flush pending relationship insertions to the database."""
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
- properties,
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
- properties,
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
- self.ingestor.flush_all()
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:
@@ -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):
shotgun/exceptions.py CHANGED
@@ -153,8 +153,9 @@ class BudgetExceededException(ShotgunAccountException):
153
153
  return (
154
154
  "⚠️ **Your Shotgun Account budget has been exceeded!**\n\n"
155
155
  "Your account has reached its spending limit and cannot process more requests.\n\n"
156
- "**Need help?** Contact us for budget increase.\n\n"
157
- " Self-service budget increases are coming soon!\n\n"
156
+ "**Action Required:** Top up your account to continue using Shotgun.\n\n"
157
+ "👉 **[Top Up Now at https://app.shotgun.sh/dashboard](https://app.shotgun.sh/dashboard)**\n\n"
158
+ "**Need help?** Contact us if you have questions about your budget.\n\n"
158
159
  f"_Error details: {str(self)}_"
159
160
  )
160
161
 
@@ -163,8 +164,9 @@ class BudgetExceededException(ShotgunAccountException):
163
164
  return (
164
165
  "⚠️ Your Shotgun Account budget has been exceeded!\n\n"
165
166
  "Your account has reached its spending limit and cannot process more requests.\n\n"
167
+ "Action Required: Top up your account to continue using Shotgun.\n\n"
168
+ "→ Top Up Now: https://app.shotgun.sh/dashboard\n\n"
166
169
  f"Need help? Contact: {SHOTGUN_CONTACT_EMAIL}\n\n"
167
- "• Self-service budget increases are coming soon!\n\n"
168
170
  f"Error details: {str(self)}"
169
171
  )
170
172
 
shotgun/main.py CHANGED
@@ -32,6 +32,7 @@ from shotgun.cli import (
32
32
  feedback,
33
33
  plan,
34
34
  research,
35
+ spec,
35
36
  specify,
36
37
  tasks,
37
38
  update,
@@ -95,6 +96,7 @@ app.add_typer(tasks.app, name="tasks", help="Generate task lists with agentic ap
95
96
  app.add_typer(export.app, name="export", help="Export artifacts to various formats")
96
97
  app.add_typer(update.app, name="update", help="Check for and install updates")
97
98
  app.add_typer(feedback.app, name="feedback", help="Send us feedback")
99
+ app.add_typer(spec.app, name="spec", help="Manage shared specifications")
98
100
 
99
101
 
100
102
  def version_callback(value: bool) -> None:
@@ -8,7 +8,7 @@ from pydantic import BaseModel
8
8
 
9
9
  from shotgun import __version__
10
10
  from shotgun.agents.config import get_config_manager
11
- from shotgun.agents.conversation_manager import ConversationManager
11
+ from shotgun.agents.conversation import ConversationManager
12
12
  from shotgun.logging_config import get_early_logger
13
13
  from shotgun.settings import settings
14
14
 
@@ -7,11 +7,11 @@ Your extensive expertise spans, among other things:
7
7
  ## KEY RULES
8
8
 
9
9
  {% if interactive_mode %}
10
- 0. Always ask CLARIFYING QUESTIONS using structured output before doing work.
10
+ 0. Ask CLARIFYING QUESTIONS using structured output for complex or multi-step tasks when the request lacks sufficient detail.
11
11
  - Return your response with the clarifying_questions field populated
12
- - Do not make assumptions about what the user wants, get a clear understanding first.
12
+ - For simple, straightforward requests, make reasonable assumptions and proceed.
13
+ - Only ask the most critical questions to avoid overwhelming the user.
13
14
  - Questions should be clear, specific, and answerable
14
- - Do not ask too many questions that might overwhelm the user; prioritize the most important ones.
15
15
  {% endif %}
16
16
  1. Above all, prefer using tools to do the work and NEVER respond with text.
17
17
  2. IMPORTANT: Always ask for review and go ahead to move forward after using write_file().
@@ -19,10 +19,10 @@ You must return responses using this structured format:
19
19
 
20
20
  ## When to Use Clarifying Questions
21
21
 
22
- - BEFORE GETTING TO WORK: If the user's request is ambiguous, use clarifying_questions to ask what they want
22
+ - BEFORE GETTING TO WORK: For complex or multi-step tasks where the request is ambiguous or lacks sufficient detail, use clarifying_questions to ask what they want
23
23
  - DURING WORK: After using write_file(), you can suggest that the user review it and ask any clarifying questions with clarifying_questions
24
- - Don't assume - ask for confirmation of your understanding
25
- - When in doubt about any aspect of the goal, include clarifying_questions
24
+ - For simple, straightforward requests, make reasonable assumptions and proceed
25
+ - Only ask critical questions that significantly impact the outcome
26
26
 
27
27
  ## Important Notes
28
28
 
@@ -38,9 +38,6 @@ For research tasks:
38
38
 
39
39
  ## RESEARCH PRINCIPLES
40
40
 
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
41
  - Build upon existing research rather than starting from scratch
45
42
  - Focus on practical, actionable information over theoretical concepts
46
43
  - Include specific examples, tools, and implementation details