shotgun-sh 0.1.0.dev22__py3-none-any.whl → 0.1.0.dev24__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 (56) hide show
  1. shotgun/agents/agent_manager.py +95 -15
  2. shotgun/agents/common.py +143 -25
  3. shotgun/agents/conversation_history.py +56 -0
  4. shotgun/agents/conversation_manager.py +105 -0
  5. shotgun/agents/export.py +5 -2
  6. shotgun/agents/models.py +16 -7
  7. shotgun/agents/plan.py +2 -1
  8. shotgun/agents/research.py +2 -1
  9. shotgun/agents/specify.py +2 -1
  10. shotgun/agents/tasks.py +5 -2
  11. shotgun/agents/tools/file_management.py +67 -2
  12. shotgun/codebase/core/ingestor.py +1 -1
  13. shotgun/codebase/core/manager.py +106 -4
  14. shotgun/codebase/models.py +4 -0
  15. shotgun/codebase/service.py +60 -2
  16. shotgun/main.py +9 -1
  17. shotgun/prompts/agents/export.j2 +14 -11
  18. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +6 -9
  19. shotgun/prompts/agents/plan.j2 +9 -13
  20. shotgun/prompts/agents/research.j2 +11 -14
  21. shotgun/prompts/agents/specify.j2 +9 -12
  22. shotgun/prompts/agents/state/system_state.j2 +27 -5
  23. shotgun/prompts/agents/tasks.j2 +12 -12
  24. shotgun/sdk/codebase.py +26 -2
  25. shotgun/sdk/services.py +0 -14
  26. shotgun/tui/app.py +9 -4
  27. shotgun/tui/screens/chat.py +80 -19
  28. shotgun/tui/screens/chat_screen/command_providers.py +1 -1
  29. shotgun/tui/screens/chat_screen/history.py +6 -0
  30. shotgun/tui/utils/mode_progress.py +111 -78
  31. {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev24.dist-info}/METADATA +8 -9
  32. {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev24.dist-info}/RECORD +35 -54
  33. shotgun/agents/artifact_state.py +0 -58
  34. shotgun/agents/tools/artifact_management.py +0 -481
  35. shotgun/artifacts/__init__.py +0 -17
  36. shotgun/artifacts/exceptions.py +0 -89
  37. shotgun/artifacts/manager.py +0 -530
  38. shotgun/artifacts/models.py +0 -334
  39. shotgun/artifacts/service.py +0 -463
  40. shotgun/artifacts/templates/__init__.py +0 -10
  41. shotgun/artifacts/templates/loader.py +0 -252
  42. shotgun/artifacts/templates/models.py +0 -136
  43. shotgun/artifacts/templates/plan/delivery_and_release_plan.yaml +0 -66
  44. shotgun/artifacts/templates/research/market_research.yaml +0 -585
  45. shotgun/artifacts/templates/research/sdk_comparison.yaml +0 -257
  46. shotgun/artifacts/templates/specify/prd.yaml +0 -331
  47. shotgun/artifacts/templates/specify/product_spec.yaml +0 -301
  48. shotgun/artifacts/utils.py +0 -76
  49. shotgun/prompts/agents/partials/artifact_system.j2 +0 -32
  50. shotgun/prompts/agents/state/artifact_templates_available.j2 +0 -20
  51. shotgun/prompts/agents/state/existing_artifacts_available.j2 +0 -25
  52. shotgun/sdk/artifact_models.py +0 -186
  53. shotgun/sdk/artifacts.py +0 -448
  54. {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev24.dist-info}/WHEEL +0 -0
  55. {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev24.dist-info}/entry_points.txt +0 -0
  56. {shotgun_sh-0.1.0.dev22.dist-info → shotgun_sh-0.1.0.dev24.dist-info}/licenses/LICENSE +0 -0
@@ -1,334 +0,0 @@
1
- """Pydantic models for the artifact system."""
2
-
3
- import re
4
- from datetime import datetime
5
- from enum import Enum
6
- from pathlib import Path
7
- from typing import Any
8
-
9
- from pydantic import BaseModel, Field, field_validator, model_validator
10
-
11
- from shotgun.utils.file_system_utils import get_shotgun_base_path
12
-
13
-
14
- class AgentMode(str, Enum):
15
- """Supported agent modes for artifacts."""
16
-
17
- RESEARCH = "research"
18
- PLAN = "plan"
19
- TASKS = "tasks"
20
- SPECIFY = "specify"
21
-
22
-
23
- class ArtifactSection(BaseModel):
24
- """A section within an artifact."""
25
-
26
- number: int = Field(..., ge=1, description="Section number for ordering")
27
- slug: str = Field(..., min_length=1, description="URL-friendly section identifier")
28
- title: str = Field(..., min_length=1, description="Human-readable section title")
29
- content: str = Field(default="", description="Markdown content of the section")
30
-
31
- @field_validator("slug")
32
- @classmethod
33
- def validate_slug(cls, v: str) -> str:
34
- """Validate that slug contains only valid characters."""
35
- if not re.match(r"^[a-z0-9._-]+$", v):
36
- raise ValueError(
37
- "Slug must contain only lowercase letters, numbers, hyphens, dots, and underscores"
38
- )
39
- if v.startswith("-") or v.endswith("-"):
40
- raise ValueError("Slug cannot start or end with hyphen")
41
- if v.startswith(".") or v.endswith("."):
42
- raise ValueError("Slug cannot start or end with dot")
43
- if v.startswith("_") or v.endswith("_"):
44
- raise ValueError("Slug cannot start or end with underscore")
45
- if "--" in v:
46
- raise ValueError("Slug cannot contain consecutive hyphens")
47
- if ".." in v:
48
- raise ValueError("Slug cannot contain consecutive dots")
49
- if "__" in v:
50
- raise ValueError("Slug cannot contain consecutive underscores")
51
- return v
52
-
53
- @property
54
- def filename(self) -> str:
55
- """Generate filename for this section."""
56
- return f"{self.number:03d}-{self.slug}.md"
57
-
58
- def __str__(self) -> str:
59
- """String representation for debugging."""
60
- return f"Section({self.number:03d}-{self.slug})"
61
-
62
-
63
- class Artifact(BaseModel):
64
- """Main artifact model containing sections."""
65
-
66
- artifact_id: str = Field(
67
- ..., min_length=1, description="Unique artifact identifier"
68
- )
69
- agent_mode: AgentMode = Field(
70
- ..., description="Agent mode this artifact belongs to"
71
- )
72
- name: str = Field(..., min_length=1, description="Human-readable artifact name")
73
- sections: list[ArtifactSection] = Field(default_factory=list)
74
-
75
- @field_validator("artifact_id")
76
- @classmethod
77
- def validate_artifact_id(cls, v: str) -> str:
78
- """Validate that artifact_id is a valid slug."""
79
- if not re.match(r"^[a-z0-9-]+$", v):
80
- raise ValueError(
81
- "Artifact ID must contain only lowercase letters, numbers, and hyphens"
82
- )
83
- if v.startswith("-") or v.endswith("-"):
84
- raise ValueError("Artifact ID cannot start or end with hyphen")
85
- if "--" in v:
86
- raise ValueError("Artifact ID cannot contain consecutive hyphens")
87
- return v
88
-
89
- @model_validator(mode="after")
90
- def validate_section_numbers(self) -> "Artifact":
91
- """Ensure section numbers are unique and sequential."""
92
- if not self.sections:
93
- return self
94
-
95
- numbers = [section.number for section in self.sections]
96
- if len(numbers) != len(set(numbers)):
97
- raise ValueError("Section numbers must be unique")
98
-
99
- # Check for reasonable numbering (starting from 1, no big gaps)
100
- sorted_numbers = sorted(numbers)
101
- if sorted_numbers[0] != 1:
102
- raise ValueError("Section numbers should start from 1")
103
-
104
- # Allow gaps but warn about unreasonable ones
105
- max_gap = (
106
- max(
107
- sorted_numbers[i + 1] - sorted_numbers[i]
108
- for i in range(len(sorted_numbers) - 1)
109
- )
110
- if len(sorted_numbers) > 1
111
- else 0
112
- )
113
-
114
- if max_gap > 10:
115
- # This is just a warning, not an error
116
- pass
117
-
118
- return self
119
-
120
- @property
121
- def directory_path(self) -> str:
122
- """Get the relative directory path for this artifact."""
123
- return f"{self.agent_mode.value}/{self.artifact_id}"
124
-
125
- def get_section_by_number(self, number: int) -> ArtifactSection | None:
126
- """Get section by number."""
127
- for section in self.sections:
128
- if section.number == number:
129
- return section
130
- return None
131
-
132
- def get_section_by_slug(self, slug: str) -> ArtifactSection | None:
133
- """Get section by slug."""
134
- for section in self.sections:
135
- if section.slug == slug:
136
- return section
137
- return None
138
-
139
- def add_section(self, section: ArtifactSection) -> None:
140
- """Add a section to the artifact."""
141
- # Check for conflicts
142
- if self.get_section_by_number(section.number):
143
- raise ValueError(f"Section number {section.number} already exists")
144
- if self.get_section_by_slug(section.slug):
145
- raise ValueError(f"Section slug '{section.slug}' already exists")
146
-
147
- self.sections.append(section)
148
- self.sections.sort(key=lambda s: s.number)
149
-
150
- def remove_section(self, number: int) -> bool:
151
- """Remove a section by number. Returns True if section was removed."""
152
- original_count = len(self.sections)
153
- self.sections = [s for s in self.sections if s.number != number]
154
- removed = len(self.sections) < original_count
155
- return removed
156
-
157
- def update_section(self, number: int, **kwargs: Any) -> bool:
158
- """Update a section's fields. Returns True if section was found and updated."""
159
- section = self.get_section_by_number(number)
160
- if not section:
161
- return False
162
-
163
- # Handle special case of changing number
164
- if "number" in kwargs and kwargs["number"] != section.number:
165
- new_number = kwargs["number"]
166
- if self.get_section_by_number(new_number):
167
- raise ValueError(f"Section number {new_number} already exists")
168
-
169
- # Update fields
170
- for field, value in kwargs.items():
171
- setattr(section, field, value)
172
-
173
- # Re-sort if number changed
174
- if "number" in kwargs:
175
- self.sections.sort(key=lambda s: s.number)
176
-
177
- return True
178
-
179
- def get_ordered_sections(self) -> list[ArtifactSection]:
180
- """Get sections ordered by number."""
181
- return sorted(self.sections, key=lambda s: s.number)
182
-
183
- def has_template(self, base_path: Path | None = None) -> bool:
184
- """Check if this artifact was created from a template."""
185
- if base_path is None:
186
- base_path = get_shotgun_base_path()
187
- elif isinstance(base_path, str):
188
- base_path = Path(base_path)
189
-
190
- template_path = (
191
- base_path / self.agent_mode.value / self.artifact_id / ".template.yaml"
192
- )
193
- return template_path.exists()
194
-
195
- def get_template_id(self, base_path: Path | None = None) -> str | None:
196
- """Get the template ID from the template file."""
197
- template_content = self.load_template_from_file(base_path)
198
- if template_content and "template_id" in template_content:
199
- template_id = template_content["template_id"]
200
- return str(template_id) if template_id is not None else None
201
- return None
202
-
203
- def load_template_from_file(
204
- self, base_path: Path | None = None
205
- ) -> dict[str, Any] | None:
206
- """Load template content from the artifact's .template.yaml file.
207
-
208
- Args:
209
- base_path: Base path for artifacts. Defaults to .shotgun in current directory.
210
-
211
- Returns:
212
- Template content as dict or None if no template file exists.
213
- """
214
- import yaml
215
-
216
- if base_path is None:
217
- base_path = get_shotgun_base_path()
218
- elif isinstance(base_path, str):
219
- base_path = Path(base_path)
220
-
221
- template_path = (
222
- base_path / self.agent_mode.value / self.artifact_id / ".template.yaml"
223
- )
224
-
225
- if not template_path.exists():
226
- return None
227
-
228
- try:
229
- with open(template_path, encoding="utf-8") as f:
230
- data = yaml.safe_load(f)
231
- return data if isinstance(data, dict) else None
232
- except Exception:
233
- return None
234
-
235
- def get_section_count(self) -> int:
236
- """Get section count based on current sections."""
237
- return len(self.sections)
238
-
239
- def get_total_content_length(self) -> int:
240
- """Calculate total content length from all sections."""
241
- return sum(len(section.content) for section in self.sections)
242
-
243
- def get_created_at(self, base_path: Path | None = None) -> datetime:
244
- """Get artifact creation time from filesystem.
245
-
246
- Args:
247
- base_path: Base path for artifacts. Defaults to .shotgun in current directory.
248
-
249
- Returns:
250
- Creation timestamp based on artifact directory creation time.
251
- """
252
- if base_path is None:
253
- base_path = get_shotgun_base_path()
254
- elif isinstance(base_path, str):
255
- base_path = Path(base_path)
256
-
257
- artifact_path = base_path / self.agent_mode.value / self.artifact_id
258
- if artifact_path.exists():
259
- return datetime.fromtimestamp(artifact_path.stat().st_ctime)
260
- return datetime.now()
261
-
262
- def get_updated_at(self, base_path: Path | None = None) -> datetime:
263
- """Get artifact last updated time from filesystem.
264
-
265
- Args:
266
- base_path: Base path for artifacts. Defaults to .shotgun in current directory.
267
-
268
- Returns:
269
- Last modified timestamp based on most recently modified file in artifact directory.
270
- """
271
- if base_path is None:
272
- base_path = get_shotgun_base_path()
273
- elif isinstance(base_path, str):
274
- base_path = Path(base_path)
275
-
276
- artifact_path = base_path / self.agent_mode.value / self.artifact_id
277
- if not artifact_path.exists():
278
- return datetime.now()
279
-
280
- # Find the most recently modified file in the artifact directory
281
- most_recent = artifact_path.stat().st_mtime
282
- for file_path in artifact_path.rglob("*"):
283
- if file_path.is_file():
284
- file_mtime = file_path.stat().st_mtime
285
- most_recent = max(most_recent, file_mtime)
286
-
287
- return datetime.fromtimestamp(most_recent)
288
-
289
- def __str__(self) -> str:
290
- """String representation for debugging."""
291
- template_info = " (template)" if self.has_template() else ""
292
- return f"Artifact({self.agent_mode.value}/{self.artifact_id}, {len(self.sections)} sections{template_info})"
293
-
294
-
295
- class ArtifactSummary(BaseModel):
296
- """Summary information about an artifact without full content."""
297
-
298
- artifact_id: str
299
- agent_mode: AgentMode
300
- name: str
301
- section_count: int
302
- created_at: datetime
303
- updated_at: datetime
304
- section_titles: list[str] = Field(default_factory=list)
305
- template_id: str | None = Field(
306
- default=None, description="ID of template used to create this artifact"
307
- )
308
-
309
- @classmethod
310
- def from_artifact(
311
- cls, artifact: Artifact, base_path: Path | None = None
312
- ) -> "ArtifactSummary":
313
- """Create summary from full artifact.
314
-
315
- Args:
316
- artifact: The artifact to create summary from
317
- base_path: Base path for artifacts. Used for filesystem-based timestamps.
318
- """
319
- return cls(
320
- artifact_id=artifact.artifact_id,
321
- agent_mode=artifact.agent_mode,
322
- name=artifact.name,
323
- section_count=artifact.get_section_count(),
324
- created_at=artifact.get_created_at(base_path),
325
- updated_at=artifact.get_updated_at(base_path),
326
- section_titles=[
327
- section.title for section in artifact.get_ordered_sections()
328
- ],
329
- template_id=artifact.get_template_id(base_path),
330
- )
331
-
332
- def __str__(self) -> str:
333
- """String representation for debugging."""
334
- return f"ArtifactSummary({self.agent_mode.value}/{self.artifact_id})"