steerdev 0.4.27__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.
- steerdev-0.4.27.dist-info/METADATA +224 -0
- steerdev-0.4.27.dist-info/RECORD +57 -0
- steerdev-0.4.27.dist-info/WHEEL +4 -0
- steerdev-0.4.27.dist-info/entry_points.txt +2 -0
- steerdev_agent/__init__.py +10 -0
- steerdev_agent/api/__init__.py +32 -0
- steerdev_agent/api/activity.py +278 -0
- steerdev_agent/api/agents.py +145 -0
- steerdev_agent/api/client.py +158 -0
- steerdev_agent/api/commands.py +399 -0
- steerdev_agent/api/configs.py +238 -0
- steerdev_agent/api/context.py +306 -0
- steerdev_agent/api/events.py +294 -0
- steerdev_agent/api/hooks.py +178 -0
- steerdev_agent/api/implementation_plan.py +408 -0
- steerdev_agent/api/messages.py +231 -0
- steerdev_agent/api/prd.py +281 -0
- steerdev_agent/api/runs.py +526 -0
- steerdev_agent/api/sessions.py +403 -0
- steerdev_agent/api/specs.py +321 -0
- steerdev_agent/api/tasks.py +659 -0
- steerdev_agent/api/workflow_runs.py +351 -0
- steerdev_agent/api/workflows.py +191 -0
- steerdev_agent/cli.py +2254 -0
- steerdev_agent/config/__init__.py +19 -0
- steerdev_agent/config/models.py +236 -0
- steerdev_agent/config/platform.py +272 -0
- steerdev_agent/config/settings.py +62 -0
- steerdev_agent/daemon.py +675 -0
- steerdev_agent/executor/__init__.py +64 -0
- steerdev_agent/executor/base.py +121 -0
- steerdev_agent/executor/claude.py +328 -0
- steerdev_agent/executor/stream.py +163 -0
- steerdev_agent/git/__init__.py +1 -0
- steerdev_agent/handlers/__init__.py +5 -0
- steerdev_agent/handlers/prd.py +533 -0
- steerdev_agent/integration.py +334 -0
- steerdev_agent/prompt/__init__.py +10 -0
- steerdev_agent/prompt/builder.py +263 -0
- steerdev_agent/prompt/templates.py +422 -0
- steerdev_agent/py.typed +0 -0
- steerdev_agent/runner.py +829 -0
- steerdev_agent/setup/__init__.py +5 -0
- steerdev_agent/setup/claude_setup.py +560 -0
- steerdev_agent/setup/templates/claude_md_section.md +140 -0
- steerdev_agent/setup/templates/settings.json +69 -0
- steerdev_agent/setup/templates/skills/activity/SKILL.md +160 -0
- steerdev_agent/setup/templates/skills/context/SKILL.md +122 -0
- steerdev_agent/setup/templates/skills/git-workflow/SKILL.md +218 -0
- steerdev_agent/setup/templates/skills/progress-logging/SKILL.md +211 -0
- steerdev_agent/setup/templates/skills/specs-management/SKILL.md +161 -0
- steerdev_agent/setup/templates/skills/task-management/SKILL.md +343 -0
- steerdev_agent/setup/templates/steerdev.yaml +51 -0
- steerdev_agent/version.py +149 -0
- steerdev_agent/workflow/__init__.py +10 -0
- steerdev_agent/workflow/executor.py +494 -0
- steerdev_agent/workflow/memory.py +185 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Integration test harness for steerdev.
|
|
2
|
+
|
|
3
|
+
Creates a fresh Python project and adds tasks for building a todo-list API with SQLite.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
import typer
|
|
14
|
+
from rich import print as rprint
|
|
15
|
+
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
|
|
19
|
+
console = Console()
|
|
20
|
+
|
|
21
|
+
# Default test tasks for todo-list API
|
|
22
|
+
DEFAULT_TASKS = [
|
|
23
|
+
{
|
|
24
|
+
"title": "Set up project with FastAPI and SQLite dependencies",
|
|
25
|
+
"prompt": """Set up the Python project for a todo-list API:
|
|
26
|
+
1. Add dependencies to pyproject.toml: fastapi, uvicorn, sqlalchemy, pydantic
|
|
27
|
+
2. Run `uv sync` to install them
|
|
28
|
+
3. Create a basic src/todo_api/__init__.py file""",
|
|
29
|
+
"priority": 3,
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"title": "Create SQLite database models",
|
|
33
|
+
"prompt": """Create the database layer in src/todo_api/database.py:
|
|
34
|
+
1. Set up SQLAlchemy with SQLite (use 'todos.db' as the database file)
|
|
35
|
+
2. Create a Todo model with fields: id (int, primary key), title (str), description (str, optional), completed (bool, default False), created_at (datetime)
|
|
36
|
+
3. Add a function to initialize the database and create tables""",
|
|
37
|
+
"priority": 2,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"title": "Create Pydantic schemas",
|
|
41
|
+
"prompt": """Create Pydantic schemas in src/todo_api/schemas.py:
|
|
42
|
+
1. TodoCreate schema for creating todos (title required, description optional)
|
|
43
|
+
2. TodoUpdate schema for updating todos (all fields optional)
|
|
44
|
+
3. TodoResponse schema for API responses (includes id, created_at, completed)""",
|
|
45
|
+
"priority": 2,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"title": "Implement CRUD API endpoints",
|
|
49
|
+
"prompt": """Create the FastAPI application in src/todo_api/main.py:
|
|
50
|
+
1. Initialize FastAPI app with title "Todo API"
|
|
51
|
+
2. Add startup event to initialize database
|
|
52
|
+
3. Implement endpoints:
|
|
53
|
+
- GET /todos - list all todos
|
|
54
|
+
- POST /todos - create a todo
|
|
55
|
+
- GET /todos/{id} - get a single todo
|
|
56
|
+
- PATCH /todos/{id} - update a todo
|
|
57
|
+
- DELETE /todos/{id} - delete a todo
|
|
58
|
+
4. Add proper error handling (404 for not found)""",
|
|
59
|
+
"priority": 2,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"title": "Add tests for the API",
|
|
63
|
+
"prompt": """Create tests in tests/test_api.py:
|
|
64
|
+
1. Add pytest and httpx as dev dependencies
|
|
65
|
+
2. Use FastAPI TestClient to test all endpoints
|
|
66
|
+
3. Test cases: create todo, list todos, get single todo, update todo, delete todo, 404 handling
|
|
67
|
+
4. Make sure tests are isolated (clean database between tests)""",
|
|
68
|
+
"priority": 1,
|
|
69
|
+
},
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def log_info(msg: str) -> None:
|
|
74
|
+
rprint(f"[blue][INFO][/blue] {msg}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def log_success(msg: str) -> None:
|
|
78
|
+
rprint(f"[green][OK][/green] {msg}")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def log_warn(msg: str) -> None:
|
|
82
|
+
rprint(f"[yellow][WARN][/yellow] {msg}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def log_error(msg: str) -> None:
|
|
86
|
+
rprint(f"[red][ERROR][/red] {msg}")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def run_cmd(
|
|
90
|
+
cmd: list[str],
|
|
91
|
+
cwd: Path | None = None,
|
|
92
|
+
check: bool = True,
|
|
93
|
+
capture: bool = True,
|
|
94
|
+
) -> subprocess.CompletedProcess:
|
|
95
|
+
"""Run a command and return the result."""
|
|
96
|
+
return subprocess.run(
|
|
97
|
+
cmd,
|
|
98
|
+
cwd=cwd,
|
|
99
|
+
check=check,
|
|
100
|
+
capture_output=capture,
|
|
101
|
+
text=True,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def create_project(project_dir: Path) -> None:
|
|
106
|
+
"""Create a new Python project using uv init."""
|
|
107
|
+
log_info(f"Creating project at {project_dir}...")
|
|
108
|
+
|
|
109
|
+
if project_dir.exists():
|
|
110
|
+
log_warn(f"Directory exists, removing: {project_dir}")
|
|
111
|
+
shutil.rmtree(project_dir)
|
|
112
|
+
|
|
113
|
+
# Create with uv init
|
|
114
|
+
project_dir.parent.mkdir(parents=True, exist_ok=True)
|
|
115
|
+
run_cmd(["uv", "init", "--lib", str(project_dir)])
|
|
116
|
+
|
|
117
|
+
# Rename the default package to todo_api
|
|
118
|
+
src_dir = project_dir / "src"
|
|
119
|
+
default_pkg = src_dir / project_dir.name.replace("-", "_")
|
|
120
|
+
todo_pkg = src_dir / "todo_api"
|
|
121
|
+
|
|
122
|
+
if default_pkg.exists() and default_pkg != todo_pkg:
|
|
123
|
+
default_pkg.rename(todo_pkg)
|
|
124
|
+
|
|
125
|
+
# Update pyproject.toml to use todo_api
|
|
126
|
+
pyproject = project_dir / "pyproject.toml"
|
|
127
|
+
content = pyproject.read_text()
|
|
128
|
+
content = content.replace(
|
|
129
|
+
f'"{project_dir.name.replace("-", "_")}"',
|
|
130
|
+
'"todo_api"',
|
|
131
|
+
)
|
|
132
|
+
pyproject.write_text(content)
|
|
133
|
+
|
|
134
|
+
# Create tests directory
|
|
135
|
+
(project_dir / "tests").mkdir(exist_ok=True)
|
|
136
|
+
(project_dir / "tests" / "__init__.py").touch()
|
|
137
|
+
|
|
138
|
+
# Create .env.example
|
|
139
|
+
env_example = """\
|
|
140
|
+
# SteerDev Agent Configuration
|
|
141
|
+
# Copy this file to .env and fill in your values
|
|
142
|
+
|
|
143
|
+
# Required: Your SteerDev API key (get from steerdev.com dashboard)
|
|
144
|
+
STEERDEV_API_KEY=
|
|
145
|
+
|
|
146
|
+
# Required: Project ID to fetch tasks from
|
|
147
|
+
STEERDEV_PROJECT_ID=
|
|
148
|
+
|
|
149
|
+
# Optional: API endpoint (defaults to production)
|
|
150
|
+
# STEERDEV_API_ENDPOINT=https://steerdev.com/api/v1
|
|
151
|
+
|
|
152
|
+
# Optional: Agent identification
|
|
153
|
+
# STEERDEV_AGENT_ID=
|
|
154
|
+
# STEERDEV_AGENT_NAME=
|
|
155
|
+
|
|
156
|
+
# Optional: LLM API keys (for Claude Code / other agents)
|
|
157
|
+
# ANTHROPIC_API_KEY=
|
|
158
|
+
# OPENAI_API_KEY=
|
|
159
|
+
|
|
160
|
+
# Optional: Notifications
|
|
161
|
+
# DISCORD_BOT_TOKEN=
|
|
162
|
+
# DISCORD_CHANNEL_ID=
|
|
163
|
+
# SLACK_WEBHOOK_URL=
|
|
164
|
+
"""
|
|
165
|
+
(project_dir / ".env.steerdev.example").write_text(env_example)
|
|
166
|
+
|
|
167
|
+
# Create .gitignore with common patterns
|
|
168
|
+
gitignore = """\
|
|
169
|
+
# Python
|
|
170
|
+
__pycache__/
|
|
171
|
+
*.py[cod]
|
|
172
|
+
*$py.class
|
|
173
|
+
.venv/
|
|
174
|
+
venv/
|
|
175
|
+
*.egg-info/
|
|
176
|
+
dist/
|
|
177
|
+
build/
|
|
178
|
+
|
|
179
|
+
# Environment
|
|
180
|
+
.env
|
|
181
|
+
.env.local
|
|
182
|
+
|
|
183
|
+
# Database
|
|
184
|
+
*.db
|
|
185
|
+
*.sqlite
|
|
186
|
+
*.sqlite3
|
|
187
|
+
|
|
188
|
+
# IDE
|
|
189
|
+
.idea/
|
|
190
|
+
.vscode/
|
|
191
|
+
*.swp
|
|
192
|
+
*.swo
|
|
193
|
+
|
|
194
|
+
# OS
|
|
195
|
+
.DS_Store
|
|
196
|
+
Thumbs.db
|
|
197
|
+
|
|
198
|
+
# Testing
|
|
199
|
+
.pytest_cache/
|
|
200
|
+
.coverage
|
|
201
|
+
htmlcov/
|
|
202
|
+
"""
|
|
203
|
+
(project_dir / ".gitignore").write_text(gitignore)
|
|
204
|
+
|
|
205
|
+
# Initialize git
|
|
206
|
+
run_cmd(["git", "init"], cwd=project_dir)
|
|
207
|
+
run_cmd(["git", "add", "-A"], cwd=project_dir)
|
|
208
|
+
run_cmd(["git", "commit", "-m", "Initial commit"], cwd=project_dir)
|
|
209
|
+
|
|
210
|
+
log_success("Project created")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def add_tasks(
|
|
214
|
+
project_id: str,
|
|
215
|
+
tasks: list[dict],
|
|
216
|
+
api_key: str,
|
|
217
|
+
api_endpoint: str,
|
|
218
|
+
) -> list[str]:
|
|
219
|
+
"""Add tasks via the SteerDev API. Returns list of created task IDs."""
|
|
220
|
+
log_info(f"Adding {len(tasks)} tasks to project {project_id}...")
|
|
221
|
+
|
|
222
|
+
created_ids = []
|
|
223
|
+
|
|
224
|
+
for task in tasks:
|
|
225
|
+
title = task["title"]
|
|
226
|
+
prompt = task["prompt"]
|
|
227
|
+
priority = task.get("priority", 1)
|
|
228
|
+
|
|
229
|
+
log_info(f"Creating: {title}")
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
response = httpx.post(
|
|
233
|
+
f"{api_endpoint}/tasks",
|
|
234
|
+
headers={
|
|
235
|
+
"Authorization": f"Bearer {api_key}",
|
|
236
|
+
"Content-Type": "application/json",
|
|
237
|
+
},
|
|
238
|
+
json={
|
|
239
|
+
"project_id": project_id,
|
|
240
|
+
"title": title,
|
|
241
|
+
"prompt": prompt,
|
|
242
|
+
"priority": priority,
|
|
243
|
+
},
|
|
244
|
+
timeout=30,
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if response.status_code == 201:
|
|
248
|
+
data = response.json()
|
|
249
|
+
task_id = data.get("id", "unknown")
|
|
250
|
+
created_ids.append(task_id)
|
|
251
|
+
log_success(f"Created: {task_id[:8]}...")
|
|
252
|
+
else:
|
|
253
|
+
log_error(f"Failed: {response.text}")
|
|
254
|
+
|
|
255
|
+
except httpx.RequestError as e:
|
|
256
|
+
log_error(f"Request failed: {e}")
|
|
257
|
+
|
|
258
|
+
return created_ids
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def start_agent(project_id: str, project_dir: Path) -> None:
|
|
262
|
+
"""Start the steerdev."""
|
|
263
|
+
log_info("Starting steerdev...")
|
|
264
|
+
|
|
265
|
+
os.chdir(project_dir)
|
|
266
|
+
os.environ["STEERDEV_PROJECT_ID"] = project_id
|
|
267
|
+
|
|
268
|
+
subprocess.run(
|
|
269
|
+
["steerdev", "run", "--project-id", project_id, "--workdir", str(project_dir)],
|
|
270
|
+
check=False,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def run_integration_test(
|
|
275
|
+
project_id: str,
|
|
276
|
+
project_dir: Path,
|
|
277
|
+
tasks_file: Path | None = None,
|
|
278
|
+
skip_tasks: bool = False,
|
|
279
|
+
auto_start: bool = False,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Run the full integration test."""
|
|
282
|
+
# Get environment
|
|
283
|
+
api_key = os.environ.get("STEERDEV_API_KEY")
|
|
284
|
+
if not api_key:
|
|
285
|
+
log_error("STEERDEV_API_KEY environment variable is required")
|
|
286
|
+
raise typer.Exit(1)
|
|
287
|
+
|
|
288
|
+
api_endpoint = os.environ.get("STEERDEV_API_ENDPOINT", "https://steerdev.com/api/v1")
|
|
289
|
+
|
|
290
|
+
# Show config
|
|
291
|
+
rprint("")
|
|
292
|
+
rprint(Panel.fit("SteerDev Integration Test", style="bold blue"))
|
|
293
|
+
rprint("")
|
|
294
|
+
|
|
295
|
+
table = Table(show_header=False, box=None)
|
|
296
|
+
table.add_column("Key", style="dim")
|
|
297
|
+
table.add_column("Value")
|
|
298
|
+
table.add_row("Project ID", project_id)
|
|
299
|
+
table.add_row("Project Dir", str(project_dir))
|
|
300
|
+
table.add_row("API Endpoint", api_endpoint)
|
|
301
|
+
console.print(table)
|
|
302
|
+
rprint("")
|
|
303
|
+
|
|
304
|
+
# Check uv is installed
|
|
305
|
+
if shutil.which("uv") is None:
|
|
306
|
+
log_error("uv is required but not installed")
|
|
307
|
+
raise typer.Exit(1)
|
|
308
|
+
|
|
309
|
+
# Create project
|
|
310
|
+
create_project(project_dir)
|
|
311
|
+
|
|
312
|
+
# Add tasks
|
|
313
|
+
if not skip_tasks:
|
|
314
|
+
if tasks_file and tasks_file.exists():
|
|
315
|
+
tasks = json.loads(tasks_file.read_text())
|
|
316
|
+
else:
|
|
317
|
+
tasks = DEFAULT_TASKS
|
|
318
|
+
|
|
319
|
+
add_tasks(project_id, tasks, api_key, api_endpoint)
|
|
320
|
+
else:
|
|
321
|
+
log_warn("Skipping task creation (--skip-tasks)")
|
|
322
|
+
|
|
323
|
+
log_success("Integration test environment ready!")
|
|
324
|
+
rprint("")
|
|
325
|
+
rprint(f"Project created at: [bold]{project_dir}[/bold]")
|
|
326
|
+
rprint("")
|
|
327
|
+
|
|
328
|
+
# Start agent
|
|
329
|
+
if auto_start or typer.confirm("Start the agent now?", default=True):
|
|
330
|
+
start_agent(project_id, project_dir)
|
|
331
|
+
else:
|
|
332
|
+
log_info("Run manually with:")
|
|
333
|
+
rprint(f" cd {project_dir}")
|
|
334
|
+
rprint(f" steerdev run --project-id {project_id}")
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Dynamic prompt builder for agent execution."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
|
|
7
|
+
from steerdev_agent.prompt.templates import PromptTemplates
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProjectContext(BaseModel):
|
|
11
|
+
"""Project context for prompt building."""
|
|
12
|
+
|
|
13
|
+
id: str = Field(description="Project ID")
|
|
14
|
+
name: str = Field(description="Project name")
|
|
15
|
+
description: str = Field(default="", description="Project description")
|
|
16
|
+
working_directory: str = Field(default=".", description="Default working directory")
|
|
17
|
+
custom_instructions: str | None = Field(
|
|
18
|
+
default=None, description="Custom instructions for this project"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class WaveContext(BaseModel):
|
|
23
|
+
"""Wave context for prompt building."""
|
|
24
|
+
|
|
25
|
+
wave_number: int = Field(description="Current wave number")
|
|
26
|
+
total_waves: int = Field(description="Total number of waves")
|
|
27
|
+
wave_description: str = Field(default="", description="Description of this wave")
|
|
28
|
+
wave_tasks_summary: str = Field(default="", description="Summary of tasks in this wave")
|
|
29
|
+
completed_waves_summary: str = Field(default="", description="Summary of completed waves")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TaskContext(BaseModel):
|
|
33
|
+
"""Task context for prompt building."""
|
|
34
|
+
|
|
35
|
+
id: str = Field(description="Task ID")
|
|
36
|
+
title: str = Field(description="Task title")
|
|
37
|
+
prompt: str = Field(description="Task prompt/description")
|
|
38
|
+
status: str = Field(default="unstarted", description="Task status")
|
|
39
|
+
priority: int = Field(default=3, description="Task priority (1=urgent, 4=low)")
|
|
40
|
+
working_directory: str | None = Field(default=None, description="Task working directory")
|
|
41
|
+
spec: str | None = Field(default=None, description="Technical specification")
|
|
42
|
+
metadata: dict[str, Any] | None = Field(default=None, description="Additional metadata")
|
|
43
|
+
wave: WaveContext | None = Field(
|
|
44
|
+
default=None, description="Wave context if task is part of a wave"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class WorkflowContext(BaseModel):
|
|
49
|
+
"""Workflow phase context for prompt building."""
|
|
50
|
+
|
|
51
|
+
phase_name: str = Field(description="Current workflow phase name")
|
|
52
|
+
phase_description: str = Field(default="", description="Phase description")
|
|
53
|
+
phase_instructions: str = Field(default="", description="Phase-specific instructions")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class PromptContext(BaseModel):
|
|
57
|
+
"""Complete context for building a prompt."""
|
|
58
|
+
|
|
59
|
+
project: ProjectContext | None = Field(default=None, description="Project context")
|
|
60
|
+
task: TaskContext | None = Field(default=None, description="Task context")
|
|
61
|
+
workflow: WorkflowContext | None = Field(default=None, description="Workflow context")
|
|
62
|
+
custom_instructions: str | None = Field(
|
|
63
|
+
default=None, description="Additional custom instructions"
|
|
64
|
+
)
|
|
65
|
+
resume_message: str | None = Field(
|
|
66
|
+
default=None, description="Message for resume (if resuming session)"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class PromptBuilder:
|
|
71
|
+
"""Builds prompts by combining context from various sources.
|
|
72
|
+
|
|
73
|
+
The builder combines:
|
|
74
|
+
- Project metadata and instructions
|
|
75
|
+
- Task details and requirements
|
|
76
|
+
- Workflow phase information
|
|
77
|
+
- Custom instructions
|
|
78
|
+
|
|
79
|
+
Into a single coherent prompt for the agent.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(self, templates: PromptTemplates | None = None) -> None:
|
|
83
|
+
"""Initialize the prompt builder.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
templates: Custom templates to use. Defaults to PromptTemplates.
|
|
87
|
+
"""
|
|
88
|
+
self.templates = templates or PromptTemplates()
|
|
89
|
+
|
|
90
|
+
def build(self, context: PromptContext) -> str:
|
|
91
|
+
"""Build a complete prompt from the context.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
context: The prompt context containing all information.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
A formatted prompt string.
|
|
98
|
+
"""
|
|
99
|
+
# Handle resume case
|
|
100
|
+
if context.resume_message:
|
|
101
|
+
return self._build_resume_prompt(context)
|
|
102
|
+
|
|
103
|
+
# Build components
|
|
104
|
+
project_info = self._build_project_info(context.project)
|
|
105
|
+
task_info = self._build_task_info(context.task, context.project)
|
|
106
|
+
instructions = self._build_instructions(context)
|
|
107
|
+
|
|
108
|
+
# Add workflow phase if present
|
|
109
|
+
if context.workflow:
|
|
110
|
+
workflow_info = self.templates.format_workflow_phase(
|
|
111
|
+
phase_name=context.workflow.phase_name,
|
|
112
|
+
phase_description=context.workflow.phase_description,
|
|
113
|
+
phase_instructions=context.workflow.phase_instructions,
|
|
114
|
+
)
|
|
115
|
+
task_info = f"{task_info}\n\n{workflow_info}"
|
|
116
|
+
|
|
117
|
+
return self.templates.format_system_prompt(
|
|
118
|
+
project_info=project_info,
|
|
119
|
+
task_info=task_info,
|
|
120
|
+
instructions=instructions,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _build_resume_prompt(self, context: PromptContext) -> str:
|
|
124
|
+
"""Build a prompt for resuming a session.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
context: The prompt context.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
A resume prompt string.
|
|
131
|
+
"""
|
|
132
|
+
message = context.resume_message or "Continue working on the task."
|
|
133
|
+
return self.templates.RESUME_PROMPT.format(message=message)
|
|
134
|
+
|
|
135
|
+
def _build_project_info(self, project: ProjectContext | None) -> str:
|
|
136
|
+
"""Build project information section.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
project: Project context or None.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Formatted project information.
|
|
143
|
+
"""
|
|
144
|
+
if project is None:
|
|
145
|
+
return ""
|
|
146
|
+
|
|
147
|
+
return self.templates.format_project(
|
|
148
|
+
name=project.name,
|
|
149
|
+
project_id=project.id,
|
|
150
|
+
description=project.description,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _build_task_info(
|
|
154
|
+
self,
|
|
155
|
+
task: TaskContext | None,
|
|
156
|
+
project: ProjectContext | None,
|
|
157
|
+
) -> str:
|
|
158
|
+
"""Build task information section.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
task: Task context or None.
|
|
162
|
+
project: Project context for working directory fallback.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Formatted task information.
|
|
166
|
+
"""
|
|
167
|
+
if task is None:
|
|
168
|
+
return ""
|
|
169
|
+
|
|
170
|
+
# Determine working directory
|
|
171
|
+
working_dir = task.working_directory
|
|
172
|
+
if working_dir is None and project is not None:
|
|
173
|
+
working_dir = project.working_directory
|
|
174
|
+
if working_dir is None:
|
|
175
|
+
working_dir = "."
|
|
176
|
+
|
|
177
|
+
task_info = self.templates.format_task(
|
|
178
|
+
title=task.title,
|
|
179
|
+
task_id=task.id,
|
|
180
|
+
prompt=task.prompt,
|
|
181
|
+
priority=task.priority,
|
|
182
|
+
status=task.status,
|
|
183
|
+
working_directory=working_dir,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# Add spec if present
|
|
187
|
+
if task.spec:
|
|
188
|
+
task_info = f"{task_info}\n\n### Specification\n{task.spec}"
|
|
189
|
+
|
|
190
|
+
# Add wave context if present
|
|
191
|
+
if task.wave:
|
|
192
|
+
wave_section = (
|
|
193
|
+
f"\n\n### Wave Context\n"
|
|
194
|
+
f"**Wave {task.wave.wave_number} of {task.wave.total_waves}**"
|
|
195
|
+
)
|
|
196
|
+
if task.wave.wave_description:
|
|
197
|
+
wave_section += f" — {task.wave.wave_description}"
|
|
198
|
+
if task.wave.completed_waves_summary:
|
|
199
|
+
wave_section += f"\n\n**Completed Waves:**\n{task.wave.completed_waves_summary}"
|
|
200
|
+
if task.wave.wave_tasks_summary:
|
|
201
|
+
wave_section += f"\n\n**Tasks in This Wave:**\n{task.wave.wave_tasks_summary}"
|
|
202
|
+
task_info = f"{task_info}{wave_section}"
|
|
203
|
+
|
|
204
|
+
return task_info
|
|
205
|
+
|
|
206
|
+
def _build_instructions(self, context: PromptContext) -> str:
|
|
207
|
+
"""Build the instructions section.
|
|
208
|
+
|
|
209
|
+
Combines default instructions with custom instructions from
|
|
210
|
+
project and context.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
context: The prompt context.
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
Combined instructions string.
|
|
217
|
+
"""
|
|
218
|
+
instructions_parts = []
|
|
219
|
+
|
|
220
|
+
# Add project custom instructions
|
|
221
|
+
if context.project and context.project.custom_instructions:
|
|
222
|
+
instructions_parts.append(context.project.custom_instructions)
|
|
223
|
+
|
|
224
|
+
# Add context custom instructions
|
|
225
|
+
if context.custom_instructions:
|
|
226
|
+
instructions_parts.append(context.custom_instructions)
|
|
227
|
+
|
|
228
|
+
# If no custom instructions, use defaults
|
|
229
|
+
if not instructions_parts:
|
|
230
|
+
return self.templates.DEFAULT_INSTRUCTIONS
|
|
231
|
+
|
|
232
|
+
return "\n\n".join(instructions_parts)
|
|
233
|
+
|
|
234
|
+
def build_from_dict(
|
|
235
|
+
self,
|
|
236
|
+
project: dict[str, Any] | None = None,
|
|
237
|
+
task: dict[str, Any] | None = None,
|
|
238
|
+
workflow: dict[str, Any] | None = None,
|
|
239
|
+
custom_instructions: str | None = None,
|
|
240
|
+
resume_message: str | None = None,
|
|
241
|
+
) -> str:
|
|
242
|
+
"""Build a prompt from dictionary inputs.
|
|
243
|
+
|
|
244
|
+
Convenience method for building prompts from API responses.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
project: Project data dict.
|
|
248
|
+
task: Task data dict.
|
|
249
|
+
workflow: Workflow data dict.
|
|
250
|
+
custom_instructions: Additional instructions.
|
|
251
|
+
resume_message: Message for resume.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
A formatted prompt string.
|
|
255
|
+
"""
|
|
256
|
+
context = PromptContext(
|
|
257
|
+
project=ProjectContext(**project) if project else None,
|
|
258
|
+
task=TaskContext(**task) if task else None,
|
|
259
|
+
workflow=WorkflowContext(**workflow) if workflow else None,
|
|
260
|
+
custom_instructions=custom_instructions,
|
|
261
|
+
resume_message=resume_message,
|
|
262
|
+
)
|
|
263
|
+
return self.build(context)
|