shotgun-sh 0.2.17__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 +28 -14
- shotgun/agents/common.py +1 -1
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/manager.py +323 -53
- shotgun/agents/config/models.py +85 -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 +216 -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/runner.py +230 -0
- 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/ingestor.py +153 -7
- 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/partials/common_agent_system_prompt.j2 +28 -3
- 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/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 +73 -9
- shotgun/tui/containers.py +1 -1
- shotgun/tui/layout.py +5 -0
- shotgun/tui/screens/chat/chat_screen.py +372 -95
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
- shotgun/tui/screens/chat_screen/command_providers.py +13 -2
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- 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 +149 -0
- 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/widgets/widget_coordinator.py +1 -1
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/METADATA +9 -2
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/RECORD +112 -77
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
- /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.3.3.dev1.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,132 @@
|
|
|
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 .models import PullSource
|
|
20
|
+
from .pull_service import CancelledError, PullProgress, SpecPullService
|
|
21
|
+
|
|
22
|
+
app = typer.Typer(
|
|
23
|
+
name="spec",
|
|
24
|
+
help="Manage shared specifications",
|
|
25
|
+
no_args_is_help=True,
|
|
26
|
+
)
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command()
|
|
32
|
+
def pull(
|
|
33
|
+
version_id: Annotated[str, typer.Argument(help="Version ID to pull")],
|
|
34
|
+
no_tui: Annotated[
|
|
35
|
+
bool,
|
|
36
|
+
typer.Option("--no-tui", help="Run in CLI-only mode (requires existing auth)"),
|
|
37
|
+
] = False,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Pull a spec version from the cloud to local .shotgun/ directory.
|
|
40
|
+
|
|
41
|
+
Downloads all files for the specified version and writes them to the
|
|
42
|
+
local .shotgun/ directory. If the directory already has content, it
|
|
43
|
+
will be backed up to ~/.shotgun-sh/backups/ before being replaced.
|
|
44
|
+
|
|
45
|
+
By default, launches the TUI which handles authentication and shows
|
|
46
|
+
the pull progress. Use --no-tui for scripted/headless use (requires
|
|
47
|
+
existing authentication).
|
|
48
|
+
|
|
49
|
+
Example:
|
|
50
|
+
shotgun spec pull 2532e1c7-7068-4d23-9379-58ea439c592f
|
|
51
|
+
"""
|
|
52
|
+
if no_tui:
|
|
53
|
+
# CLI-only mode: do pull directly (requires existing auth)
|
|
54
|
+
success = asyncio.run(_async_pull(version_id))
|
|
55
|
+
if not success:
|
|
56
|
+
raise typer.Exit(1)
|
|
57
|
+
else:
|
|
58
|
+
# TUI mode: launch TUI which handles auth and pull
|
|
59
|
+
tui_app.run(pull_version_id=version_id)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
async def _async_pull(version_id: str) -> bool:
|
|
63
|
+
"""Async implementation of spec pull command.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
True if pull was successful, False otherwise.
|
|
67
|
+
"""
|
|
68
|
+
shotgun_dir = get_shotgun_base_path()
|
|
69
|
+
service = SpecPullService()
|
|
70
|
+
|
|
71
|
+
# Track current progress state for rich display
|
|
72
|
+
current_task_id: TaskID | None = None
|
|
73
|
+
progress_ctx: Progress | None = None
|
|
74
|
+
|
|
75
|
+
def on_progress(p: PullProgress) -> None:
|
|
76
|
+
nonlocal current_task_id, progress_ctx
|
|
77
|
+
# For CLI, we just update the description - progress bar handled by result
|
|
78
|
+
if progress_ctx and current_task_id is not None:
|
|
79
|
+
progress_ctx.update(current_task_id, description=p.phase)
|
|
80
|
+
if p.total_files and p.file_index is not None:
|
|
81
|
+
pct = ((p.file_index + 1) / p.total_files) * 100
|
|
82
|
+
progress_ctx.update(current_task_id, completed=pct)
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
with Progress(
|
|
86
|
+
SpinnerColumn(),
|
|
87
|
+
TextColumn("[progress.description]{task.description}"),
|
|
88
|
+
BarColumn(),
|
|
89
|
+
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
|
|
90
|
+
) as progress:
|
|
91
|
+
progress_ctx = progress
|
|
92
|
+
current_task_id = progress.add_task("Starting...", total=100)
|
|
93
|
+
|
|
94
|
+
result = await service.pull_version(
|
|
95
|
+
version_id=version_id,
|
|
96
|
+
shotgun_dir=shotgun_dir,
|
|
97
|
+
on_progress=on_progress,
|
|
98
|
+
source=PullSource.CLI,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
if result.success:
|
|
102
|
+
console.print()
|
|
103
|
+
console.print(f"[green]Successfully pulled '{result.spec_name}'[/green]")
|
|
104
|
+
console.print(f" [dim]Files downloaded:[/dim] {result.file_count}")
|
|
105
|
+
if result.backup_path:
|
|
106
|
+
console.print(f" [dim]Previous backup:[/dim] {result.backup_path}")
|
|
107
|
+
if result.web_url:
|
|
108
|
+
console.print(f" [blue]View in browser:[/blue] {result.web_url}")
|
|
109
|
+
return True
|
|
110
|
+
else:
|
|
111
|
+
console.print(f"[red]Error: {result.error}[/red]")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
except UnauthorizedError:
|
|
115
|
+
console.print(
|
|
116
|
+
"[red]Not authenticated. Please re-run the command to login.[/red]"
|
|
117
|
+
)
|
|
118
|
+
raise typer.Exit(1) from None
|
|
119
|
+
except NotFoundError:
|
|
120
|
+
console.print(f"[red]Version not found: {version_id}[/red]")
|
|
121
|
+
console.print("[dim]Check the version ID and try again.[/dim]")
|
|
122
|
+
raise typer.Exit(1) from None
|
|
123
|
+
except ForbiddenError:
|
|
124
|
+
console.print("[red]You don't have access to this spec.[/red]")
|
|
125
|
+
raise typer.Exit(1) from None
|
|
126
|
+
except CancelledError:
|
|
127
|
+
console.print("[yellow]Pull cancelled.[/yellow]")
|
|
128
|
+
raise typer.Exit(1) from None
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.exception("Unexpected error in spec pull")
|
|
131
|
+
console.print(f"[red]Unexpected error: {e}[/red]")
|
|
132
|
+
raise typer.Exit(1) from None
|
|
@@ -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())
|