spec-kitty-cli 0.12.1__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.
- spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
- spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
- spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
- spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
- spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
- specify_cli/__init__.py +171 -0
- specify_cli/acceptance.py +627 -0
- specify_cli/agent_utils/README.md +157 -0
- specify_cli/agent_utils/__init__.py +9 -0
- specify_cli/agent_utils/status.py +356 -0
- specify_cli/cli/__init__.py +6 -0
- specify_cli/cli/commands/__init__.py +46 -0
- specify_cli/cli/commands/accept.py +189 -0
- specify_cli/cli/commands/agent/__init__.py +22 -0
- specify_cli/cli/commands/agent/config.py +382 -0
- specify_cli/cli/commands/agent/context.py +191 -0
- specify_cli/cli/commands/agent/feature.py +1057 -0
- specify_cli/cli/commands/agent/release.py +11 -0
- specify_cli/cli/commands/agent/tasks.py +1253 -0
- specify_cli/cli/commands/agent/workflow.py +801 -0
- specify_cli/cli/commands/context.py +246 -0
- specify_cli/cli/commands/dashboard.py +85 -0
- specify_cli/cli/commands/implement.py +973 -0
- specify_cli/cli/commands/init.py +827 -0
- specify_cli/cli/commands/init_help.py +62 -0
- specify_cli/cli/commands/merge.py +755 -0
- specify_cli/cli/commands/mission.py +240 -0
- specify_cli/cli/commands/ops.py +265 -0
- specify_cli/cli/commands/orchestrate.py +640 -0
- specify_cli/cli/commands/repair.py +175 -0
- specify_cli/cli/commands/research.py +165 -0
- specify_cli/cli/commands/sync.py +364 -0
- specify_cli/cli/commands/upgrade.py +249 -0
- specify_cli/cli/commands/validate_encoding.py +186 -0
- specify_cli/cli/commands/validate_tasks.py +186 -0
- specify_cli/cli/commands/verify.py +310 -0
- specify_cli/cli/helpers.py +123 -0
- specify_cli/cli/step_tracker.py +91 -0
- specify_cli/cli/ui.py +192 -0
- specify_cli/core/__init__.py +53 -0
- specify_cli/core/agent_context.py +311 -0
- specify_cli/core/config.py +96 -0
- specify_cli/core/context_validation.py +362 -0
- specify_cli/core/dependency_graph.py +351 -0
- specify_cli/core/git_ops.py +129 -0
- specify_cli/core/multi_parent_merge.py +323 -0
- specify_cli/core/paths.py +260 -0
- specify_cli/core/project_resolver.py +110 -0
- specify_cli/core/stale_detection.py +263 -0
- specify_cli/core/tool_checker.py +79 -0
- specify_cli/core/utils.py +43 -0
- specify_cli/core/vcs/__init__.py +114 -0
- specify_cli/core/vcs/detection.py +341 -0
- specify_cli/core/vcs/exceptions.py +85 -0
- specify_cli/core/vcs/git.py +1304 -0
- specify_cli/core/vcs/jujutsu.py +1208 -0
- specify_cli/core/vcs/protocol.py +285 -0
- specify_cli/core/vcs/types.py +249 -0
- specify_cli/core/version_checker.py +261 -0
- specify_cli/core/worktree.py +506 -0
- specify_cli/dashboard/__init__.py +28 -0
- specify_cli/dashboard/diagnostics.py +204 -0
- specify_cli/dashboard/handlers/__init__.py +17 -0
- specify_cli/dashboard/handlers/api.py +143 -0
- specify_cli/dashboard/handlers/base.py +65 -0
- specify_cli/dashboard/handlers/features.py +390 -0
- specify_cli/dashboard/handlers/router.py +81 -0
- specify_cli/dashboard/handlers/static.py +50 -0
- specify_cli/dashboard/lifecycle.py +541 -0
- specify_cli/dashboard/scanner.py +437 -0
- specify_cli/dashboard/server.py +123 -0
- specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
- specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
- specify_cli/dashboard/static/spec-kitty.png +0 -0
- specify_cli/dashboard/templates/__init__.py +36 -0
- specify_cli/dashboard/templates/index.html +258 -0
- specify_cli/doc_generators.py +621 -0
- specify_cli/doc_state.py +408 -0
- specify_cli/frontmatter.py +384 -0
- specify_cli/gap_analysis.py +915 -0
- specify_cli/gitignore_manager.py +300 -0
- specify_cli/guards.py +145 -0
- specify_cli/legacy_detector.py +83 -0
- specify_cli/manifest.py +286 -0
- specify_cli/merge/__init__.py +63 -0
- specify_cli/merge/executor.py +653 -0
- specify_cli/merge/forecast.py +215 -0
- specify_cli/merge/ordering.py +126 -0
- specify_cli/merge/preflight.py +230 -0
- specify_cli/merge/state.py +185 -0
- specify_cli/merge/status_resolver.py +354 -0
- specify_cli/mission.py +654 -0
- specify_cli/missions/documentation/command-templates/implement.md +309 -0
- specify_cli/missions/documentation/command-templates/plan.md +275 -0
- specify_cli/missions/documentation/command-templates/review.md +344 -0
- specify_cli/missions/documentation/command-templates/specify.md +206 -0
- specify_cli/missions/documentation/command-templates/tasks.md +189 -0
- specify_cli/missions/documentation/mission.yaml +113 -0
- specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
- specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
- specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
- specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
- specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
- specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
- specify_cli/missions/documentation/templates/plan-template.md +269 -0
- specify_cli/missions/documentation/templates/release-template.md +222 -0
- specify_cli/missions/documentation/templates/spec-template.md +172 -0
- specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
- specify_cli/missions/documentation/templates/tasks-template.md +159 -0
- specify_cli/missions/research/command-templates/merge.md +388 -0
- specify_cli/missions/research/command-templates/plan.md +125 -0
- specify_cli/missions/research/command-templates/review.md +144 -0
- specify_cli/missions/research/command-templates/tasks.md +225 -0
- specify_cli/missions/research/mission.yaml +115 -0
- specify_cli/missions/research/templates/data-model-template.md +33 -0
- specify_cli/missions/research/templates/plan-template.md +161 -0
- specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
- specify_cli/missions/research/templates/research/source-register.csv +18 -0
- specify_cli/missions/research/templates/research-template.md +35 -0
- specify_cli/missions/research/templates/spec-template.md +64 -0
- specify_cli/missions/research/templates/task-prompt-template.md +148 -0
- specify_cli/missions/research/templates/tasks-template.md +114 -0
- specify_cli/missions/software-dev/command-templates/accept.md +75 -0
- specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
- specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
- specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
- specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
- specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
- specify_cli/missions/software-dev/command-templates/implement.md +41 -0
- specify_cli/missions/software-dev/command-templates/merge.md +383 -0
- specify_cli/missions/software-dev/command-templates/plan.md +171 -0
- specify_cli/missions/software-dev/command-templates/review.md +32 -0
- specify_cli/missions/software-dev/command-templates/specify.md +321 -0
- specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
- specify_cli/missions/software-dev/mission.yaml +100 -0
- specify_cli/missions/software-dev/templates/plan-template.md +132 -0
- specify_cli/missions/software-dev/templates/spec-template.md +116 -0
- specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
- specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
- specify_cli/orchestrator/__init__.py +75 -0
- specify_cli/orchestrator/agent_config.py +224 -0
- specify_cli/orchestrator/agents/__init__.py +170 -0
- specify_cli/orchestrator/agents/augment.py +112 -0
- specify_cli/orchestrator/agents/base.py +243 -0
- specify_cli/orchestrator/agents/claude.py +112 -0
- specify_cli/orchestrator/agents/codex.py +106 -0
- specify_cli/orchestrator/agents/copilot.py +137 -0
- specify_cli/orchestrator/agents/cursor.py +139 -0
- specify_cli/orchestrator/agents/gemini.py +115 -0
- specify_cli/orchestrator/agents/kilocode.py +94 -0
- specify_cli/orchestrator/agents/opencode.py +132 -0
- specify_cli/orchestrator/agents/qwen.py +96 -0
- specify_cli/orchestrator/config.py +455 -0
- specify_cli/orchestrator/executor.py +642 -0
- specify_cli/orchestrator/integration.py +1230 -0
- specify_cli/orchestrator/monitor.py +898 -0
- specify_cli/orchestrator/scheduler.py +832 -0
- specify_cli/orchestrator/state.py +508 -0
- specify_cli/orchestrator/testing/__init__.py +122 -0
- specify_cli/orchestrator/testing/availability.py +346 -0
- specify_cli/orchestrator/testing/fixtures.py +684 -0
- specify_cli/orchestrator/testing/paths.py +218 -0
- specify_cli/plan_validation.py +107 -0
- specify_cli/scripts/debug-dashboard-scan.py +61 -0
- specify_cli/scripts/tasks/acceptance_support.py +695 -0
- specify_cli/scripts/tasks/task_helpers.py +506 -0
- specify_cli/scripts/tasks/tasks_cli.py +848 -0
- specify_cli/scripts/validate_encoding.py +180 -0
- specify_cli/task_metadata_validation.py +274 -0
- specify_cli/tasks_support.py +447 -0
- specify_cli/template/__init__.py +47 -0
- specify_cli/template/asset_generator.py +206 -0
- specify_cli/template/github_client.py +334 -0
- specify_cli/template/manager.py +193 -0
- specify_cli/template/renderer.py +99 -0
- specify_cli/templates/AGENTS.md +190 -0
- specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
- specify_cli/templates/agent-file-template.md +35 -0
- specify_cli/templates/checklist-template.md +42 -0
- specify_cli/templates/claudeignore-template +58 -0
- specify_cli/templates/command-templates/accept.md +141 -0
- specify_cli/templates/command-templates/analyze.md +253 -0
- specify_cli/templates/command-templates/checklist.md +352 -0
- specify_cli/templates/command-templates/clarify.md +224 -0
- specify_cli/templates/command-templates/constitution.md +432 -0
- specify_cli/templates/command-templates/dashboard.md +175 -0
- specify_cli/templates/command-templates/implement.md +190 -0
- specify_cli/templates/command-templates/merge.md +374 -0
- specify_cli/templates/command-templates/plan.md +171 -0
- specify_cli/templates/command-templates/research.md +88 -0
- specify_cli/templates/command-templates/review.md +510 -0
- specify_cli/templates/command-templates/specify.md +321 -0
- specify_cli/templates/command-templates/status.md +92 -0
- specify_cli/templates/command-templates/tasks.md +199 -0
- specify_cli/templates/git-hooks/pre-commit +22 -0
- specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
- specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
- specify_cli/templates/plan-template.md +108 -0
- specify_cli/templates/spec-template.md +118 -0
- specify_cli/templates/task-prompt-template.md +165 -0
- specify_cli/templates/tasks-template.md +161 -0
- specify_cli/templates/vscode-settings.json +13 -0
- specify_cli/text_sanitization.py +225 -0
- specify_cli/upgrade/__init__.py +18 -0
- specify_cli/upgrade/detector.py +239 -0
- specify_cli/upgrade/metadata.py +182 -0
- specify_cli/upgrade/migrations/__init__.py +65 -0
- specify_cli/upgrade/migrations/base.py +80 -0
- specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
- specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
- specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
- specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
- specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
- specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
- specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
- specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
- specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
- specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
- specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
- specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
- specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
- specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
- specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
- specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
- specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
- specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
- specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
- specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
- specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
- specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
- specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
- specify_cli/upgrade/registry.py +121 -0
- specify_cli/upgrade/runner.py +284 -0
- specify_cli/validators/__init__.py +14 -0
- specify_cli/validators/paths.py +154 -0
- specify_cli/validators/research.py +428 -0
- specify_cli/verify_enhanced.py +270 -0
- specify_cli/workspace_context.py +224 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Base protocol and classes for agent invokers.
|
|
2
|
+
|
|
3
|
+
This module defines:
|
|
4
|
+
- AgentInvoker Protocol for type checking
|
|
5
|
+
- InvocationResult dataclass for execution results
|
|
6
|
+
- BaseAgentInvoker abstract base class with common functionality
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import shutil
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Protocol, runtime_checkable
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class InvocationResult:
|
|
23
|
+
"""Result of an agent invocation.
|
|
24
|
+
|
|
25
|
+
Captures all relevant information from an agent execution including
|
|
26
|
+
success status, output, and any extracted structured data.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
success: bool
|
|
30
|
+
exit_code: int
|
|
31
|
+
stdout: str
|
|
32
|
+
stderr: str
|
|
33
|
+
duration_seconds: float
|
|
34
|
+
files_modified: list[str] = field(default_factory=list)
|
|
35
|
+
commits_made: list[str] = field(default_factory=list)
|
|
36
|
+
errors: list[str] = field(default_factory=list)
|
|
37
|
+
warnings: list[str] = field(default_factory=list)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@runtime_checkable
|
|
41
|
+
class AgentInvoker(Protocol):
|
|
42
|
+
"""Protocol defining the interface for agent invokers.
|
|
43
|
+
|
|
44
|
+
All agent invokers must implement this protocol to be usable
|
|
45
|
+
by the orchestrator.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
agent_id: str
|
|
49
|
+
command: str
|
|
50
|
+
uses_stdin: bool
|
|
51
|
+
|
|
52
|
+
def is_installed(self) -> bool:
|
|
53
|
+
"""Check if agent CLI is available on the system."""
|
|
54
|
+
...
|
|
55
|
+
|
|
56
|
+
def build_command(
|
|
57
|
+
self,
|
|
58
|
+
prompt: str,
|
|
59
|
+
working_dir: Path,
|
|
60
|
+
role: str,
|
|
61
|
+
) -> list[str]:
|
|
62
|
+
"""Build the full command with agent-specific flags.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
prompt: The task prompt to send to the agent.
|
|
66
|
+
working_dir: Directory where agent should execute.
|
|
67
|
+
role: Either "implementation" or "review".
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
List of command arguments for subprocess execution.
|
|
71
|
+
"""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
def parse_output(
|
|
75
|
+
self,
|
|
76
|
+
stdout: str,
|
|
77
|
+
stderr: str,
|
|
78
|
+
exit_code: int,
|
|
79
|
+
duration_seconds: float,
|
|
80
|
+
) -> InvocationResult:
|
|
81
|
+
"""Parse agent output into structured result.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
stdout: Standard output from the agent process.
|
|
85
|
+
stderr: Standard error from the agent process.
|
|
86
|
+
exit_code: Process exit code.
|
|
87
|
+
duration_seconds: How long the process ran.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Structured InvocationResult with extracted data.
|
|
91
|
+
"""
|
|
92
|
+
...
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class BaseInvoker:
|
|
96
|
+
"""Base class with common invoker functionality.
|
|
97
|
+
|
|
98
|
+
Provides default implementations for common operations that
|
|
99
|
+
most invokers can inherit.
|
|
100
|
+
"""
|
|
101
|
+
|
|
102
|
+
agent_id: str = ""
|
|
103
|
+
command: str = ""
|
|
104
|
+
uses_stdin: bool = True
|
|
105
|
+
|
|
106
|
+
def is_installed(self) -> bool:
|
|
107
|
+
"""Check if agent CLI is available via shutil.which()."""
|
|
108
|
+
return shutil.which(self.command) is not None
|
|
109
|
+
|
|
110
|
+
def _parse_json_output(self, stdout: str) -> dict | None:
|
|
111
|
+
"""Attempt to parse JSON from agent output.
|
|
112
|
+
|
|
113
|
+
Handles both single JSON object and JSONL (one JSON per line) formats.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
stdout: Raw stdout from the agent.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Parsed JSON dict or None if parsing fails.
|
|
120
|
+
"""
|
|
121
|
+
if not stdout.strip():
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
# Try parsing as single JSON object
|
|
125
|
+
try:
|
|
126
|
+
return json.loads(stdout)
|
|
127
|
+
except json.JSONDecodeError:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
# Try parsing last line as JSON (JSONL format)
|
|
131
|
+
lines = stdout.strip().split("\n")
|
|
132
|
+
for line in reversed(lines):
|
|
133
|
+
line = line.strip()
|
|
134
|
+
if line.startswith("{") or line.startswith("["):
|
|
135
|
+
try:
|
|
136
|
+
return json.loads(line)
|
|
137
|
+
except json.JSONDecodeError:
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
def _extract_files_from_output(self, data: dict | None) -> list[str]:
|
|
143
|
+
"""Extract list of modified files from parsed JSON output."""
|
|
144
|
+
if not data:
|
|
145
|
+
return []
|
|
146
|
+
|
|
147
|
+
# Common field names for file lists
|
|
148
|
+
for key in ["files", "files_modified", "modified_files", "changedFiles"]:
|
|
149
|
+
if key in data and isinstance(data[key], list):
|
|
150
|
+
return [str(f) for f in data[key]]
|
|
151
|
+
|
|
152
|
+
return []
|
|
153
|
+
|
|
154
|
+
def _extract_commits_from_output(self, data: dict | None) -> list[str]:
|
|
155
|
+
"""Extract list of commits from parsed JSON output."""
|
|
156
|
+
if not data:
|
|
157
|
+
return []
|
|
158
|
+
|
|
159
|
+
# Common field names for commit lists
|
|
160
|
+
for key in ["commits", "commits_made", "commitShas"]:
|
|
161
|
+
if key in data and isinstance(data[key], list):
|
|
162
|
+
return [str(c) for c in data[key]]
|
|
163
|
+
|
|
164
|
+
return []
|
|
165
|
+
|
|
166
|
+
def _extract_errors_from_output(
|
|
167
|
+
self, data: dict | None, stderr: str
|
|
168
|
+
) -> list[str]:
|
|
169
|
+
"""Extract errors from parsed JSON output and stderr."""
|
|
170
|
+
errors = []
|
|
171
|
+
|
|
172
|
+
if data:
|
|
173
|
+
for key in ["errors", "error"]:
|
|
174
|
+
if key in data:
|
|
175
|
+
val = data[key]
|
|
176
|
+
if isinstance(val, list):
|
|
177
|
+
errors.extend(str(e) for e in val)
|
|
178
|
+
elif val:
|
|
179
|
+
errors.append(str(val))
|
|
180
|
+
|
|
181
|
+
# Add non-empty stderr lines as potential errors
|
|
182
|
+
if stderr.strip():
|
|
183
|
+
stderr_lines = [
|
|
184
|
+
line.strip()
|
|
185
|
+
for line in stderr.split("\n")
|
|
186
|
+
if line.strip() and not line.startswith("warning")
|
|
187
|
+
]
|
|
188
|
+
# Only add stderr if it looks like errors (not just logging)
|
|
189
|
+
if any("error" in line.lower() for line in stderr_lines):
|
|
190
|
+
errors.extend(stderr_lines[:5]) # Limit to first 5 lines
|
|
191
|
+
|
|
192
|
+
return errors
|
|
193
|
+
|
|
194
|
+
def _extract_warnings_from_output(
|
|
195
|
+
self, data: dict | None, stderr: str
|
|
196
|
+
) -> list[str]:
|
|
197
|
+
"""Extract warnings from parsed JSON output and stderr."""
|
|
198
|
+
warnings = []
|
|
199
|
+
|
|
200
|
+
if data:
|
|
201
|
+
for key in ["warnings", "warning"]:
|
|
202
|
+
if key in data:
|
|
203
|
+
val = data[key]
|
|
204
|
+
if isinstance(val, list):
|
|
205
|
+
warnings.extend(str(w) for w in val)
|
|
206
|
+
elif val:
|
|
207
|
+
warnings.append(str(val))
|
|
208
|
+
|
|
209
|
+
# Add warning lines from stderr
|
|
210
|
+
if stderr.strip():
|
|
211
|
+
warnings.extend(
|
|
212
|
+
line.strip()
|
|
213
|
+
for line in stderr.split("\n")
|
|
214
|
+
if line.strip().lower().startswith("warning")
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return warnings
|
|
218
|
+
|
|
219
|
+
def parse_output(
|
|
220
|
+
self,
|
|
221
|
+
stdout: str,
|
|
222
|
+
stderr: str,
|
|
223
|
+
exit_code: int,
|
|
224
|
+
duration_seconds: float,
|
|
225
|
+
) -> InvocationResult:
|
|
226
|
+
"""Default output parsing implementation.
|
|
227
|
+
|
|
228
|
+
Subclasses can override for agent-specific parsing.
|
|
229
|
+
"""
|
|
230
|
+
success = exit_code == 0
|
|
231
|
+
data = self._parse_json_output(stdout)
|
|
232
|
+
|
|
233
|
+
return InvocationResult(
|
|
234
|
+
success=success,
|
|
235
|
+
exit_code=exit_code,
|
|
236
|
+
stdout=stdout,
|
|
237
|
+
stderr=stderr,
|
|
238
|
+
duration_seconds=duration_seconds,
|
|
239
|
+
files_modified=self._extract_files_from_output(data),
|
|
240
|
+
commits_made=self._extract_commits_from_output(data),
|
|
241
|
+
errors=self._extract_errors_from_output(data, stderr),
|
|
242
|
+
warnings=self._extract_warnings_from_output(data, stderr),
|
|
243
|
+
)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Claude Code invoker.
|
|
2
|
+
|
|
3
|
+
Implements the AgentInvoker protocol for Claude Code CLI.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from specify_cli.orchestrator.agents.base import BaseInvoker, InvocationResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ClaudeInvoker(BaseInvoker):
|
|
14
|
+
"""Invoker for Claude Code CLI (claude).
|
|
15
|
+
|
|
16
|
+
Claude Code accepts prompts via stdin and supports JSON output format.
|
|
17
|
+
It runs in headless mode with -p flag and can be restricted to specific tools.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
agent_id = "claude-code"
|
|
21
|
+
command = "claude"
|
|
22
|
+
uses_stdin = True
|
|
23
|
+
|
|
24
|
+
def build_command(
|
|
25
|
+
self,
|
|
26
|
+
prompt: str,
|
|
27
|
+
working_dir: Path,
|
|
28
|
+
role: str,
|
|
29
|
+
) -> list[str]:
|
|
30
|
+
"""Build Claude Code command.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
prompt: Task prompt (passed via stdin, not in command).
|
|
34
|
+
working_dir: Directory for execution.
|
|
35
|
+
role: "implementation" or "review".
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Command arguments list.
|
|
39
|
+
"""
|
|
40
|
+
cmd = [
|
|
41
|
+
"claude",
|
|
42
|
+
"-p", # Headless/print mode (non-interactive)
|
|
43
|
+
"--output-format", "json", # Structured JSON output
|
|
44
|
+
"--dangerously-skip-permissions", # Allow all tools without prompts
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Restrict tools based on role
|
|
48
|
+
if role == "implementation":
|
|
49
|
+
cmd.extend([
|
|
50
|
+
"--allowedTools",
|
|
51
|
+
"Read,Write,Edit,Bash,Glob,Grep,TodoWrite",
|
|
52
|
+
])
|
|
53
|
+
elif role == "review":
|
|
54
|
+
# Review should be more read-focused
|
|
55
|
+
cmd.extend([
|
|
56
|
+
"--allowedTools",
|
|
57
|
+
"Read,Glob,Grep,Bash",
|
|
58
|
+
])
|
|
59
|
+
|
|
60
|
+
return cmd
|
|
61
|
+
|
|
62
|
+
def parse_output(
|
|
63
|
+
self,
|
|
64
|
+
stdout: str,
|
|
65
|
+
stderr: str,
|
|
66
|
+
exit_code: int,
|
|
67
|
+
duration_seconds: float,
|
|
68
|
+
) -> InvocationResult:
|
|
69
|
+
"""Parse Claude Code JSON output.
|
|
70
|
+
|
|
71
|
+
Claude outputs conversation turns in JSON format. We extract
|
|
72
|
+
the relevant information from the final state.
|
|
73
|
+
"""
|
|
74
|
+
success = exit_code == 0
|
|
75
|
+
data = self._parse_json_output(stdout)
|
|
76
|
+
|
|
77
|
+
# Claude-specific JSON structure handling
|
|
78
|
+
files_modified = []
|
|
79
|
+
commits_made = []
|
|
80
|
+
errors = []
|
|
81
|
+
warnings = []
|
|
82
|
+
|
|
83
|
+
if data:
|
|
84
|
+
# Check for Claude-specific fields
|
|
85
|
+
if isinstance(data, dict):
|
|
86
|
+
# Extract from result field if present
|
|
87
|
+
result = data.get("result", data)
|
|
88
|
+
if isinstance(result, dict):
|
|
89
|
+
files_modified = self._extract_files_from_output(result)
|
|
90
|
+
commits_made = self._extract_commits_from_output(result)
|
|
91
|
+
|
|
92
|
+
# Check for error in response
|
|
93
|
+
if "error" in data:
|
|
94
|
+
errors.append(str(data["error"]))
|
|
95
|
+
|
|
96
|
+
# Fall back to stderr for errors
|
|
97
|
+
if not errors and stderr.strip():
|
|
98
|
+
errors = self._extract_errors_from_output(None, stderr)
|
|
99
|
+
|
|
100
|
+
warnings = self._extract_warnings_from_output(data, stderr)
|
|
101
|
+
|
|
102
|
+
return InvocationResult(
|
|
103
|
+
success=success,
|
|
104
|
+
exit_code=exit_code,
|
|
105
|
+
stdout=stdout,
|
|
106
|
+
stderr=stderr,
|
|
107
|
+
duration_seconds=duration_seconds,
|
|
108
|
+
files_modified=files_modified,
|
|
109
|
+
commits_made=commits_made,
|
|
110
|
+
errors=errors,
|
|
111
|
+
warnings=warnings,
|
|
112
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""GitHub Codex invoker.
|
|
2
|
+
|
|
3
|
+
Implements the AgentInvoker protocol for GitHub Codex CLI.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from specify_cli.orchestrator.agents.base import BaseInvoker, InvocationResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CodexInvoker(BaseInvoker):
|
|
14
|
+
"""Invoker for GitHub Codex CLI (codex).
|
|
15
|
+
|
|
16
|
+
Codex uses `codex exec -` to read prompts from stdin.
|
|
17
|
+
It supports JSON output and fully autonomous execution.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
agent_id = "codex"
|
|
21
|
+
command = "codex"
|
|
22
|
+
uses_stdin = True
|
|
23
|
+
|
|
24
|
+
def build_command(
|
|
25
|
+
self,
|
|
26
|
+
prompt: str,
|
|
27
|
+
working_dir: Path,
|
|
28
|
+
role: str,
|
|
29
|
+
) -> list[str]:
|
|
30
|
+
"""Build Codex command.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
prompt: Task prompt (passed via stdin with `-`).
|
|
34
|
+
working_dir: Directory for execution.
|
|
35
|
+
role: "implementation" or "review".
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Command arguments list.
|
|
39
|
+
"""
|
|
40
|
+
cmd = [
|
|
41
|
+
"codex", "exec",
|
|
42
|
+
"-", # Read prompt from stdin
|
|
43
|
+
"--json", # JSON output format
|
|
44
|
+
"--full-auto", # Fully autonomous mode
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
# Add role-specific flags if needed
|
|
48
|
+
if role == "review":
|
|
49
|
+
# Codex doesn't have a built-in review mode,
|
|
50
|
+
# but we can hint via the prompt structure
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
return cmd
|
|
54
|
+
|
|
55
|
+
def parse_output(
|
|
56
|
+
self,
|
|
57
|
+
stdout: str,
|
|
58
|
+
stderr: str,
|
|
59
|
+
exit_code: int,
|
|
60
|
+
duration_seconds: float,
|
|
61
|
+
) -> InvocationResult:
|
|
62
|
+
"""Parse Codex JSON output.
|
|
63
|
+
|
|
64
|
+
Codex outputs structured JSON with execution results.
|
|
65
|
+
"""
|
|
66
|
+
success = exit_code == 0
|
|
67
|
+
data = self._parse_json_output(stdout)
|
|
68
|
+
|
|
69
|
+
files_modified = []
|
|
70
|
+
commits_made = []
|
|
71
|
+
errors = []
|
|
72
|
+
warnings = []
|
|
73
|
+
|
|
74
|
+
if data:
|
|
75
|
+
# Codex-specific JSON structure
|
|
76
|
+
if isinstance(data, dict):
|
|
77
|
+
# Extract modified files
|
|
78
|
+
files_modified = self._extract_files_from_output(data)
|
|
79
|
+
|
|
80
|
+
# Extract commits if any
|
|
81
|
+
commits_made = self._extract_commits_from_output(data)
|
|
82
|
+
|
|
83
|
+
# Check for execution errors
|
|
84
|
+
if "status" in data and data["status"] == "error":
|
|
85
|
+
errors.append(data.get("message", "Unknown error"))
|
|
86
|
+
elif "error" in data:
|
|
87
|
+
errors.append(str(data["error"]))
|
|
88
|
+
|
|
89
|
+
# Extract any warnings
|
|
90
|
+
warnings = self._extract_warnings_from_output(data, stderr)
|
|
91
|
+
|
|
92
|
+
# Fall back to stderr for errors
|
|
93
|
+
if not errors and stderr.strip() and not success:
|
|
94
|
+
errors = self._extract_errors_from_output(None, stderr)
|
|
95
|
+
|
|
96
|
+
return InvocationResult(
|
|
97
|
+
success=success,
|
|
98
|
+
exit_code=exit_code,
|
|
99
|
+
stdout=stdout,
|
|
100
|
+
stderr=stderr,
|
|
101
|
+
duration_seconds=duration_seconds,
|
|
102
|
+
files_modified=files_modified,
|
|
103
|
+
commits_made=commits_made,
|
|
104
|
+
errors=errors,
|
|
105
|
+
warnings=warnings,
|
|
106
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""GitHub Copilot invoker.
|
|
2
|
+
|
|
3
|
+
Implements the AgentInvoker protocol for GitHub Copilot CLI.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from specify_cli.orchestrator.agents.base import BaseInvoker, InvocationResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CopilotInvoker(BaseInvoker):
|
|
14
|
+
"""Invoker for GitHub Copilot CLI (gh copilot).
|
|
15
|
+
|
|
16
|
+
Copilot takes prompts as command-line arguments (not stdin).
|
|
17
|
+
It uses the gh CLI extension and runs in autonomous mode with --yolo.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
agent_id = "copilot"
|
|
21
|
+
command = "gh" # Uses gh CLI with copilot extension
|
|
22
|
+
uses_stdin = False # Prompt passed as argument
|
|
23
|
+
|
|
24
|
+
def is_installed(self) -> bool:
|
|
25
|
+
"""Check if gh CLI with copilot extension is available."""
|
|
26
|
+
import shutil
|
|
27
|
+
import subprocess
|
|
28
|
+
|
|
29
|
+
# First check if gh is installed
|
|
30
|
+
if not shutil.which("gh"):
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
# Then check if copilot extension is installed
|
|
34
|
+
try:
|
|
35
|
+
result = subprocess.run(
|
|
36
|
+
["gh", "extension", "list"],
|
|
37
|
+
capture_output=True,
|
|
38
|
+
text=True,
|
|
39
|
+
timeout=10,
|
|
40
|
+
)
|
|
41
|
+
return "copilot" in result.stdout.lower()
|
|
42
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
def build_command(
|
|
46
|
+
self,
|
|
47
|
+
prompt: str,
|
|
48
|
+
working_dir: Path,
|
|
49
|
+
role: str,
|
|
50
|
+
) -> list[str]:
|
|
51
|
+
"""Build Copilot command.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
prompt: Task prompt (passed as -p argument).
|
|
55
|
+
working_dir: Directory for execution.
|
|
56
|
+
role: "implementation" or "review".
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Command arguments list.
|
|
60
|
+
"""
|
|
61
|
+
cmd = [
|
|
62
|
+
"gh", "copilot",
|
|
63
|
+
"-p", prompt, # Prompt as argument
|
|
64
|
+
"--yolo", # Autonomous mode (no confirmations)
|
|
65
|
+
"--silent", # Minimal output noise
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
return cmd
|
|
69
|
+
|
|
70
|
+
def parse_output(
|
|
71
|
+
self,
|
|
72
|
+
stdout: str,
|
|
73
|
+
stderr: str,
|
|
74
|
+
exit_code: int,
|
|
75
|
+
duration_seconds: float,
|
|
76
|
+
) -> InvocationResult:
|
|
77
|
+
"""Parse Copilot output.
|
|
78
|
+
|
|
79
|
+
Copilot doesn't output structured JSON, so we rely primarily
|
|
80
|
+
on exit code and parse stdout/stderr for useful information.
|
|
81
|
+
"""
|
|
82
|
+
success = exit_code == 0
|
|
83
|
+
|
|
84
|
+
# Copilot doesn't have structured JSON output
|
|
85
|
+
# We can try to extract file info from stdout text
|
|
86
|
+
files_modified = self._extract_files_from_text(stdout)
|
|
87
|
+
commits_made = []
|
|
88
|
+
errors = []
|
|
89
|
+
warnings = []
|
|
90
|
+
|
|
91
|
+
# Check stderr for errors
|
|
92
|
+
if stderr.strip():
|
|
93
|
+
if not success:
|
|
94
|
+
errors = self._extract_errors_from_output(None, stderr)
|
|
95
|
+
warnings = self._extract_warnings_from_output(None, stderr)
|
|
96
|
+
|
|
97
|
+
# Check stdout for error indicators
|
|
98
|
+
if not success and not errors:
|
|
99
|
+
stdout_lower = stdout.lower()
|
|
100
|
+
if "error" in stdout_lower or "failed" in stdout_lower:
|
|
101
|
+
# Extract error lines from stdout
|
|
102
|
+
error_lines = [
|
|
103
|
+
line.strip()
|
|
104
|
+
for line in stdout.split("\n")
|
|
105
|
+
if "error" in line.lower() or "failed" in line.lower()
|
|
106
|
+
]
|
|
107
|
+
errors.extend(error_lines[:3])
|
|
108
|
+
|
|
109
|
+
return InvocationResult(
|
|
110
|
+
success=success,
|
|
111
|
+
exit_code=exit_code,
|
|
112
|
+
stdout=stdout,
|
|
113
|
+
stderr=stderr,
|
|
114
|
+
duration_seconds=duration_seconds,
|
|
115
|
+
files_modified=files_modified,
|
|
116
|
+
commits_made=commits_made,
|
|
117
|
+
errors=errors,
|
|
118
|
+
warnings=warnings,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def _extract_files_from_text(self, text: str) -> list[str]:
|
|
122
|
+
"""Extract file paths mentioned in unstructured text output."""
|
|
123
|
+
files = []
|
|
124
|
+
# Look for common patterns indicating file modifications
|
|
125
|
+
import re
|
|
126
|
+
|
|
127
|
+
# Patterns like "Created file.py", "Modified src/foo.py", etc.
|
|
128
|
+
patterns = [
|
|
129
|
+
r"(?:created|modified|updated|wrote|edited)\s+['\"]?([^\s'\"]+\.\w+)['\"]?",
|
|
130
|
+
r"(?:writing to|saving)\s+['\"]?([^\s'\"]+\.\w+)['\"]?",
|
|
131
|
+
]
|
|
132
|
+
|
|
133
|
+
for pattern in patterns:
|
|
134
|
+
matches = re.findall(pattern, text, re.IGNORECASE)
|
|
135
|
+
files.extend(matches)
|
|
136
|
+
|
|
137
|
+
return list(set(files)) # Remove duplicates
|