devpilot-agentic-cli 1.0.0__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.
@@ -0,0 +1,132 @@
1
+ """
2
+ agent/providers/system_prompt.py
3
+ ──────────────────────────────────
4
+ Single source of truth for DevPilot's core system prompt.
5
+
6
+ Both AnthropicProvider and OpenAIProvider import `build_system_prompt()`
7
+ from here so the prompt is never duplicated.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import platform
13
+
14
+
15
+ def build_system_prompt(repo_context_block: str = "") -> str:
16
+ """
17
+ Build the full DevPilot system prompt.
18
+
19
+ Args:
20
+ repo_context_block: Output of RepoContext.build_context_block(),
21
+ injected at every call so the model always
22
+ knows what it has already read this session.
23
+ """
24
+ if platform.system() == "Windows":
25
+ shell_rules = """\
26
+ ### RULES FOR SHELL COMMANDS
27
+ - You are running on **Windows with PowerShell**.
28
+ - For finding files: `Get-ChildItem -Recurse -Filter *.py`
29
+ - For searching text: `Select-String -Path . -Recurse -Pattern "keyword"`
30
+ - For running scripts: `python script.py` (never `./script.py`)
31
+ - Chain commands with `;` not `&&`."""
32
+ else:
33
+ shell_rules = """\
34
+ ### RULES FOR SHELL COMMANDS
35
+ - You are running on **Linux / macOS with bash/zsh**.
36
+ - For finding files: `find . -name "*.py"` or `fd -e py`
37
+ - For searching text: `grep -r "keyword" .` or `rg "keyword"`
38
+ - For running scripts: `python script.py` or `./script.py`
39
+ - Chain commands with `&&`."""
40
+
41
+ context_section = ""
42
+ if repo_context_block.strip():
43
+ context_section = f"""\
44
+
45
+ ### CURRENT SESSION CONTEXT
46
+ The following is automatically maintained by DevPilot. It shows every file
47
+ you have already read this session and a snapshot of the project structure.
48
+ Use it to avoid redundant reads. Files marked ⚠ have changed on disk since
49
+ you last read them — re-read before editing.
50
+
51
+ {repo_context_block}
52
+ """
53
+
54
+ return f"""\
55
+ You are DevPilot, an elite autonomous AI software engineer running directly in
56
+ the user's terminal. Your goal is to solve complex engineering tasks autonomously
57
+ while maintaining absolute code integrity.
58
+
59
+ You have a powerful suite of tools: read_file, write_file, edit_file, list_files,
60
+ run_bash, search_code, git_status, git_commit, and more.
61
+
62
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
63
+ CORE METHODOLOGY — PLAN → ACT → VERIFY
64
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
65
+
66
+ Follow this methodology for every non-trivial task:
67
+
68
+ 0. RECALL
69
+ At the start of every session, check if `.devpilot/memory.md` exists using
70
+ read_file. If it does, absorb its architectural notes before proceeding.
71
+
72
+ 1. EXPLORE
73
+ Never guess file paths, variable names, or architecture.
74
+ - Check the SESSION CONTEXT block below first — you may already have the file.
75
+ - Use list_files or search_code to locate what you need.
76
+ - Use read_file to understand exact context before editing.
77
+
78
+ 2. PLAN
79
+ For tasks spanning multiple files, write your plan to `.devpilot/memory.md`
80
+ using write_file before acting. This persists your thinking across sessions.
81
+ Use a <thinking> block for shorter in-line reasoning.
82
+
83
+ 3. ACT
84
+ Execute your plan step by step:
85
+ - Use edit_file for targeted replacements in existing files (preferred).
86
+ - Use write_file only for new files or complete rewrites.
87
+ - Never use placeholders like "# ... existing code ..." in write_file output.
88
+ You must always write the ENTIRE file, every line, without omission.
89
+
90
+ 4. VERIFY
91
+ After every code change, run tests, a linter, or a compile command via
92
+ run_bash. Do not assume your code works. Report the result to the user.
93
+
94
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
95
+ RULES FOR EDITING CODE
96
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
97
+
98
+ - ALWAYS call read_file before edit_file. The old_content parameter must be an
99
+ exact, character-for-character match including all whitespace and indentation.
100
+ If old_content doesn't match, the edit is rejected — read first, always.
101
+ - edit_file is preferred over write_file for existing files. It is token-efficient
102
+ and surgically precise; write_file rewrites the entire file and wastes context.
103
+ - When write_file is unavoidable, output the COMPLETE file. Not a summary.
104
+ Not a stub. Every. Single. Line.
105
+
106
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
107
+ TOOL SELECTION GUIDE
108
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
109
+
110
+ Explore structure → list_files (recursive=true)
111
+ Find exact text in code → search_code (faster than grep for source)
112
+
113
+ Find files by name → run_bash with find / Get-ChildItem
114
+ Targeted code replacement → edit_file ← PREFERRED for existing files
115
+ Create a new file → write_file
116
+ Run tests / linter / build → run_bash (pytest, tsc, eslint, cargo test…)
117
+ Check uncommitted changes → git_status
118
+ Commit completed work → git_commit (surgical staging, not git add .)
119
+ Remember across sessions → write_file → .devpilot/memory.md
120
+
121
+ {shell_rules}
122
+
123
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
124
+ COMMUNICATION
125
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
126
+
127
+ - Be concise. Developers do not want paragraphs of fluff.
128
+ - Before calling a tool, state in one sentence what you are about to do and why.
129
+ - After completing a task (including verification), give a brief summary of
130
+ what changed and what was verified.
131
+ - If a task is ambiguous, ask one clarifying question before acting.
132
+ {context_section}"""
agent/setup_wizard.py ADDED
@@ -0,0 +1,309 @@
1
+ """
2
+ agent/setup_wizard.py
3
+ ──────────────────────
4
+ First-run setup wizard for DevPilot.
5
+
6
+ Runs when no API key is detected. Guides the user through:
7
+ 1. Choosing a provider (Anthropic, OpenAI, or custom compatible)
8
+ 2. Entering their API key
9
+ 3. Optionally choosing a model
10
+ 4. Saving everything to a .env file in the current directory
11
+
12
+ Completely non-interactive when running in CI (no TTY).
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import os
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ from rich.console import Console
22
+ from rich.panel import Panel
23
+ from rich.prompt import Confirm, Prompt
24
+ from rich.table import Table
25
+
26
+ console = Console()
27
+
28
+ # ── Model lists ───────────────────────────────────────────────────────────────
29
+
30
+ _ANTHROPIC_MODELS = [
31
+ ("claude-opus-4-5-20251101", "Most capable — best for complex tasks"),
32
+ ("claude-sonnet-4-5-20251101", "Balanced — fast and capable"),
33
+ ("claude-haiku-4-5-20251101", "Fastest — best for simple tasks"),
34
+ ("claude-3-7-sonnet-20250219", "Extended thinking support"),
35
+ ]
36
+
37
+ _OPENAI_MODELS = [
38
+ ("gpt-4o", "Latest GPT-4o — best for coding"),
39
+ ("gpt-4o-mini", "Fast and cheap — good for simple tasks"),
40
+ ("o3", "Most powerful reasoning model"),
41
+ ("o4-mini", "Fast reasoning — great for code"),
42
+ ]
43
+
44
+ # OpenAI-compatible third-party providers
45
+ # (base_url, display_name, key_env_var, key_url, models)
46
+ _COMPATIBLE_PROVIDERS = {
47
+ "1": (
48
+ "Groq",
49
+ "https://console.groq.com/keys",
50
+ "GROQ_API_KEY",
51
+ "https://api.groq.com/openai/v1",
52
+ [
53
+ ("llama-3.3-70b-versatile", "Best Groq model — fast & capable"),
54
+ ("llama-3.1-8b-instant", "Ultra-fast, lightweight"),
55
+ ("mixtral-8x7b-32768", "Large context window"),
56
+ ("gemma2-9b-it", "Google Gemma 2 — fast"),
57
+ ],
58
+ ),
59
+ "2": (
60
+ "Together AI",
61
+ "https://api.together.xyz/settings/api-keys",
62
+ "TOGETHER_API_KEY",
63
+ "https://api.together.xyz/v1",
64
+ [
65
+ ("meta-llama/Llama-3-70b-chat-hf", "Llama 3 70B — best quality"),
66
+ ("meta-llama/Llama-3-8b-chat-hf", "Llama 3 8B — faster"),
67
+ ("mistralai/Mixtral-8x7B-v0.1", "Mixtral — large context"),
68
+ ],
69
+ ),
70
+ "3": (
71
+ "Mistral AI",
72
+ "https://console.mistral.ai/api-keys/",
73
+ "MISTRAL_API_KEY",
74
+ "https://api.mistral.ai/v1",
75
+ [
76
+ ("mistral-large-latest", "Most capable Mistral model"),
77
+ ("mistral-small-latest", "Fast and affordable"),
78
+ ("codestral-latest", "Optimised for code"),
79
+ ],
80
+ ),
81
+ "4": (
82
+ "Ollama (local)",
83
+ "https://ollama.com/library",
84
+ "OLLAMA_API_KEY", # Ollama doesn't need a real key
85
+ "http://localhost:11434/v1",
86
+ [
87
+ ("qwen2.5-coder:7b", "Best local coding model (recommended)"),
88
+ ("codellama:13b", "Meta CodeLlama 13B"),
89
+ ("deepseek-coder:6b", "DeepSeek Coder 6B"),
90
+ ("llama3.2:3b", "Llama 3.2 3B — very fast"),
91
+ ],
92
+ ),
93
+ "5": (
94
+ "Other (custom)",
95
+ "",
96
+ "",
97
+ "",
98
+ [],
99
+ ),
100
+ }
101
+
102
+
103
+ # ── Helpers ───────────────────────────────────────────────────────────────────
104
+
105
+ def _is_interactive() -> bool:
106
+ return sys.stdin.isatty() and sys.stdout.isatty()
107
+
108
+
109
+ def _write_env_file(
110
+ path: Path,
111
+ updates: dict[str, str],
112
+ ) -> None:
113
+ """Write or update .env file preserving existing entries."""
114
+ existing: dict[str, str] = {}
115
+ if path.exists():
116
+ for line in path.read_text(encoding="utf-8").splitlines():
117
+ line = line.strip()
118
+ if line and not line.startswith("#") and "=" in line:
119
+ k, _, v = line.partition("=")
120
+ existing[k.strip()] = v.strip()
121
+
122
+ existing.update(updates)
123
+
124
+ lines = [
125
+ "# DevPilot configuration — generated by setup wizard",
126
+ "# Add this file to .gitignore — never commit API keys!\n",
127
+ ]
128
+ for k, v in existing.items():
129
+ lines.append(f"{k}={v}")
130
+
131
+ path.write_text("\n".join(lines) + "\n", encoding="utf-8")
132
+
133
+
134
+ def _pick_model(models: list[tuple[str, str]], allow_custom: bool = False) -> str:
135
+ """Show a model selection table and return the chosen model string."""
136
+ table = Table(show_header=False, box=None, padding=(0, 2))
137
+ for i, (name, desc) in enumerate(models, 1):
138
+ table.add_row(
139
+ f"[cyan]{i}[/cyan]",
140
+ f"[bold]{name}[/bold]",
141
+ f"[dim]{desc}[/dim]",
142
+ )
143
+ if allow_custom:
144
+ table.add_row(
145
+ f"[cyan]{len(models)+1}[/cyan]",
146
+ "[bold]Other[/bold]",
147
+ "[dim]Enter a custom model name[/dim]",
148
+ )
149
+ console.print(table)
150
+
151
+ choices = [str(i) for i in range(1, len(models) + (2 if allow_custom else 1))]
152
+ choice = Prompt.ask("\nModel", choices=choices, default="1")
153
+
154
+ if allow_custom and choice == str(len(models) + 1):
155
+ return Prompt.ask("Enter model name").strip()
156
+
157
+ return models[int(choice) - 1][0]
158
+
159
+
160
+ # ── Main wizard ───────────────────────────────────────────────────────────────
161
+
162
+ def run_setup_wizard(env_path: Path | None = None) -> bool:
163
+ """
164
+ Run the interactive first-run setup wizard.
165
+ Returns True if setup completed, False if skipped/failed.
166
+ """
167
+ if not _is_interactive():
168
+ return False
169
+
170
+ env_path = env_path or Path(".env")
171
+
172
+ console.print(Panel(
173
+ "[bold cyan]Welcome to DevPilot! 🚀[/bold cyan]\n\n"
174
+ "No API key was found. Let's set up your configuration.\n"
175
+ "This will create a [bold].env[/bold] file in your current directory.",
176
+ border_style="cyan",
177
+ expand=False,
178
+ ))
179
+
180
+ # ── Step 1: Choose provider ───────────────────────────────────────────────
181
+ console.print("\n[bold]Step 1 of 3 — Choose your AI provider[/bold]\n")
182
+ console.print(" [cyan]1[/cyan] Anthropic (Claude — recommended)")
183
+ console.print(" [cyan]2[/cyan] OpenAI (GPT-4o, o3, o4-mini)")
184
+ console.print(" [cyan]3[/cyan] Groq (Llama 3, Mixtral — very fast, free tier)")
185
+ console.print(" [cyan]4[/cyan] Together AI (Llama 3, Mixtral)")
186
+ console.print(" [cyan]5[/cyan] Mistral AI (Mistral, Codestral)")
187
+ console.print(" [cyan]6[/cyan] Ollama (local models, no API key needed)")
188
+ console.print(" [cyan]7[/cyan] Other (any OpenAI-compatible endpoint)")
189
+
190
+ choice = Prompt.ask("\nProvider", choices=["1","2","3","4","5","6","7"], default="1")
191
+
192
+ env_updates: dict[str, str] = {}
193
+
194
+ # ── Anthropic ─────────────────────────────────────────────────────────────
195
+ if choice == "1":
196
+ console.print("\n[bold]Step 2 of 3 — Enter your Anthropic API key[/bold]")
197
+ console.print(" Get one at: [link=https://console.anthropic.com/]https://console.anthropic.com/[/link]")
198
+ api_key = Prompt.ask("\nANTHROPIC_API_KEY", password=True).strip()
199
+ if not api_key:
200
+ console.print("[red]API key cannot be empty.[/red]")
201
+ return False
202
+ if not api_key.startswith("sk-ant-"):
203
+ console.print("[yellow]⚠ Key doesn't look like a typical Anthropic key (sk-ant-...). Saving anyway.[/yellow]")
204
+
205
+ console.print("\n[bold]Step 3 of 3 — Choose a model[/bold]\n")
206
+ model = _pick_model(_ANTHROPIC_MODELS)
207
+
208
+ env_updates = {
209
+ "ANTHROPIC_API_KEY": api_key,
210
+ "DEVPILOT_PROVIDER": "anthropic",
211
+ "DEVPILOT_MODEL": model,
212
+ }
213
+
214
+ # ── OpenAI ────────────────────────────────────────────────────────────────
215
+ elif choice == "2":
216
+ console.print("\n[bold]Step 2 of 3 — Enter your OpenAI API key[/bold]")
217
+ console.print(" Get one at: [link=https://platform.openai.com/api-keys]https://platform.openai.com/api-keys[/link]")
218
+ api_key = Prompt.ask("\nOPENAI_API_KEY", password=True).strip()
219
+ if not api_key:
220
+ console.print("[red]API key cannot be empty.[/red]")
221
+ return False
222
+ if not api_key.startswith("sk-"):
223
+ console.print("[yellow]⚠ Key doesn't look like a typical OpenAI key (sk-...). Saving anyway.[/yellow]")
224
+
225
+ console.print("\n[bold]Step 3 of 3 — Choose a model[/bold]\n")
226
+ model = _pick_model(_OPENAI_MODELS, allow_custom=True)
227
+
228
+ env_updates = {
229
+ "OPENAI_API_KEY": api_key,
230
+ "DEVPILOT_PROVIDER": "openai",
231
+ "DEVPILOT_MODEL": model,
232
+ }
233
+
234
+ # ── Compatible providers (Groq, Together, Mistral, Ollama) ───────────────
235
+ elif choice in ("3", "4", "5", "6"):
236
+ compat_key = str(int(choice) - 2) # maps 3→1, 4→2, 5→3, 6→4
237
+ name, key_url, key_env, base_url, models = _COMPATIBLE_PROVIDERS[compat_key]
238
+
239
+ console.print(f"\n[bold]Step 2 of 3 — Enter your {name} API key[/bold]")
240
+
241
+ if choice == "6": # Ollama — no real key needed
242
+ console.print(" [dim]Ollama runs locally — no API key required.[/dim]")
243
+ console.print(" Make sure Ollama is running: [bold]ollama serve[/bold]")
244
+ api_key = "ollama"
245
+ else:
246
+ console.print(f" Get one at: [link={key_url}]{key_url}[/link]")
247
+ api_key = Prompt.ask(f"\n{key_env}", password=True).strip()
248
+ if not api_key:
249
+ console.print("[red]API key cannot be empty.[/red]")
250
+ return False
251
+
252
+ console.print(f"\n[bold]Step 3 of 3 — Choose a model[/bold]\n")
253
+ model = _pick_model(models, allow_custom=True)
254
+
255
+ env_updates = {
256
+ key_env: api_key,
257
+ "OPENAI_API_KEY": api_key, # DevPilot reads OPENAI_API_KEY for openai provider
258
+ "DEVPILOT_PROVIDER": "openai",
259
+ "DEVPILOT_MODEL": model,
260
+ "DEVPILOT_BASE_URL": base_url,
261
+ }
262
+
263
+ # ── Custom endpoint ───────────────────────────────────────────────────────
264
+ elif choice == "7":
265
+ console.print("\n[bold]Step 2 of 3 — Custom OpenAI-compatible endpoint[/bold]")
266
+ base_url = Prompt.ask("Base URL (e.g. https://api.example.com/v1)").strip()
267
+ api_key = Prompt.ask("API key", password=True).strip()
268
+ model = Prompt.ask("Model name (e.g. llama-3-70b)").strip()
269
+
270
+ if not base_url or not model:
271
+ console.print("[red]Base URL and model name are required.[/red]")
272
+ return False
273
+
274
+ env_updates = {
275
+ "OPENAI_API_KEY": api_key or "none",
276
+ "DEVPILOT_PROVIDER": "openai",
277
+ "DEVPILOT_MODEL": model,
278
+ "DEVPILOT_BASE_URL": base_url,
279
+ }
280
+
281
+ # ── Save .env ─────────────────────────────────────────────────────────────
282
+ try:
283
+ _write_env_file(env_path, env_updates)
284
+ except OSError as e:
285
+ console.print(f"[red]Failed to write .env file: {e}[/red]")
286
+ return False
287
+
288
+ # Inject into current process immediately
289
+ for k, v in env_updates.items():
290
+ os.environ[k] = v
291
+
292
+ provider_display = env_updates.get("DEVPILOT_PROVIDER", "openai")
293
+ model_display = env_updates.get("DEVPILOT_MODEL", "")
294
+ base_url_display = env_updates.get("DEVPILOT_BASE_URL", "")
295
+
296
+ summary = (
297
+ f"[bold green]✓ Setup complete![/bold green]\n\n"
298
+ f" Provider : [cyan]{provider_display}[/cyan]\n"
299
+ f" Model : [cyan]{model_display}[/cyan]\n"
300
+ )
301
+ if base_url_display:
302
+ summary += f" Base URL : [cyan]{base_url_display}[/cyan]\n"
303
+ summary += (
304
+ f" Saved to : [cyan]{env_path.resolve()}[/cyan]\n\n"
305
+ "[dim]Add .env to your .gitignore — never commit API keys![/dim]"
306
+ )
307
+
308
+ console.print(Panel(summary, border_style="green", expand=False))
309
+ return True
@@ -0,0 +1,15 @@
1
+ """
2
+ agent/tools package.
3
+ Contains all built-in tools and the ToolRegistry.
4
+ """
5
+
6
+ from agent.tools.base import ToolResult, ToolSchema, BaseTool
7
+ from agent.tools.registry import ToolRegistry, PermissionGuard
8
+
9
+ __all__ = [
10
+ "ToolResult",
11
+ "ToolSchema",
12
+ "BaseTool",
13
+ "ToolRegistry",
14
+ "PermissionGuard",
15
+ ]
agent/tools/a2a.py ADDED
@@ -0,0 +1,56 @@
1
+ """
2
+ agent/tools/a2a.py
3
+ ──────────────────
4
+ A2A delegation tool.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ from agent.a2a_client import delegate_task_to_peer
12
+ from agent.tools.base import BaseTool, ToolResult, ToolSchema
13
+
14
+ if TYPE_CHECKING:
15
+ from agent.config import Config
16
+
17
+
18
+ class A2ATool(BaseTool):
19
+ """Delegate tasks to peer agents."""
20
+
21
+ def __init__(self, config: "Config") -> None:
22
+ self._config = config
23
+
24
+ @property
25
+ def schema(self) -> ToolSchema:
26
+ return ToolSchema(
27
+ name="a2a_delegate_task",
28
+ description=(
29
+ "Delegate a subtask to an external A2A (Agent-to-Agent) peer. "
30
+ "Use this when you need help from a specialist agent or another node. "
31
+ "Provide the base URL of the peer agent and the task prompt."
32
+ ),
33
+ parameters={
34
+ "type": "object",
35
+ "properties": {
36
+ "peer_url": {
37
+ "type": "string",
38
+ "description": "Base URL of the peer agent (e.g., http://localhost:8001)"
39
+ },
40
+ "prompt": {
41
+ "type": "string",
42
+ "description": "The coding task or instruction to delegate."
43
+ },
44
+ "token": {
45
+ "type": "string",
46
+ "description": "Optional Bearer token if the peer requires authentication."
47
+ }
48
+ },
49
+ "required": ["peer_url", "prompt"]
50
+ },
51
+ required=["peer_url", "prompt"],
52
+ sprint="Sprint 4",
53
+ )
54
+
55
+ async def execute(self, peer_url: str, prompt: str, token: str | None = None) -> ToolResult: # type: ignore[override]
56
+ return await delegate_task_to_peer(peer_url, prompt, token)
agent/tools/base.py ADDED
@@ -0,0 +1,52 @@
1
+ """
2
+ agent/tools/base.py
3
+ ───────────────────
4
+ Base interfaces for DevPilot tools.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from abc import ABC, abstractmethod
10
+ from dataclasses import dataclass, field
11
+ from typing import Any
12
+
13
+ @dataclass
14
+ class ToolResult:
15
+ """Returned by every tool executor."""
16
+ output: str # String content to send back to the model
17
+ is_error: bool # True if the tool raised an error or was cancelled
18
+
19
+
20
+ @dataclass
21
+ class ToolSchema:
22
+ """Portable tool schema — provider-agnostic."""
23
+ name: str
24
+ description: str
25
+ parameters: dict[str, Any] # JSON Schema object for input
26
+ required: list[str] = field(default_factory=list)
27
+ is_destructive: bool = False # If True, permission guard prompts
28
+ sprint: str = "Sprint 1" # Implemented in which sprint
29
+
30
+
31
+ class BaseTool(ABC):
32
+ """Abstract base class for all DevPilot tools."""
33
+
34
+ @property
35
+ @abstractmethod
36
+ def schema(self) -> ToolSchema:
37
+ """Return the tool's JSON schema for the model."""
38
+
39
+ @abstractmethod
40
+ async def execute(self, **kwargs: Any) -> ToolResult:
41
+ """
42
+ Execute the tool with the arguments the model provided.
43
+ Returns a ToolResult containing output and error status.
44
+ """
45
+
46
+ @property
47
+ def name(self) -> str:
48
+ return self.schema.name
49
+
50
+ @property
51
+ def is_destructive(self) -> bool:
52
+ return self.schema.is_destructive