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.
Files changed (242) hide show
  1. spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
  2. spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
  3. spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
  4. spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
  5. spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
  6. specify_cli/__init__.py +171 -0
  7. specify_cli/acceptance.py +627 -0
  8. specify_cli/agent_utils/README.md +157 -0
  9. specify_cli/agent_utils/__init__.py +9 -0
  10. specify_cli/agent_utils/status.py +356 -0
  11. specify_cli/cli/__init__.py +6 -0
  12. specify_cli/cli/commands/__init__.py +46 -0
  13. specify_cli/cli/commands/accept.py +189 -0
  14. specify_cli/cli/commands/agent/__init__.py +22 -0
  15. specify_cli/cli/commands/agent/config.py +382 -0
  16. specify_cli/cli/commands/agent/context.py +191 -0
  17. specify_cli/cli/commands/agent/feature.py +1057 -0
  18. specify_cli/cli/commands/agent/release.py +11 -0
  19. specify_cli/cli/commands/agent/tasks.py +1253 -0
  20. specify_cli/cli/commands/agent/workflow.py +801 -0
  21. specify_cli/cli/commands/context.py +246 -0
  22. specify_cli/cli/commands/dashboard.py +85 -0
  23. specify_cli/cli/commands/implement.py +973 -0
  24. specify_cli/cli/commands/init.py +827 -0
  25. specify_cli/cli/commands/init_help.py +62 -0
  26. specify_cli/cli/commands/merge.py +755 -0
  27. specify_cli/cli/commands/mission.py +240 -0
  28. specify_cli/cli/commands/ops.py +265 -0
  29. specify_cli/cli/commands/orchestrate.py +640 -0
  30. specify_cli/cli/commands/repair.py +175 -0
  31. specify_cli/cli/commands/research.py +165 -0
  32. specify_cli/cli/commands/sync.py +364 -0
  33. specify_cli/cli/commands/upgrade.py +249 -0
  34. specify_cli/cli/commands/validate_encoding.py +186 -0
  35. specify_cli/cli/commands/validate_tasks.py +186 -0
  36. specify_cli/cli/commands/verify.py +310 -0
  37. specify_cli/cli/helpers.py +123 -0
  38. specify_cli/cli/step_tracker.py +91 -0
  39. specify_cli/cli/ui.py +192 -0
  40. specify_cli/core/__init__.py +53 -0
  41. specify_cli/core/agent_context.py +311 -0
  42. specify_cli/core/config.py +96 -0
  43. specify_cli/core/context_validation.py +362 -0
  44. specify_cli/core/dependency_graph.py +351 -0
  45. specify_cli/core/git_ops.py +129 -0
  46. specify_cli/core/multi_parent_merge.py +323 -0
  47. specify_cli/core/paths.py +260 -0
  48. specify_cli/core/project_resolver.py +110 -0
  49. specify_cli/core/stale_detection.py +263 -0
  50. specify_cli/core/tool_checker.py +79 -0
  51. specify_cli/core/utils.py +43 -0
  52. specify_cli/core/vcs/__init__.py +114 -0
  53. specify_cli/core/vcs/detection.py +341 -0
  54. specify_cli/core/vcs/exceptions.py +85 -0
  55. specify_cli/core/vcs/git.py +1304 -0
  56. specify_cli/core/vcs/jujutsu.py +1208 -0
  57. specify_cli/core/vcs/protocol.py +285 -0
  58. specify_cli/core/vcs/types.py +249 -0
  59. specify_cli/core/version_checker.py +261 -0
  60. specify_cli/core/worktree.py +506 -0
  61. specify_cli/dashboard/__init__.py +28 -0
  62. specify_cli/dashboard/diagnostics.py +204 -0
  63. specify_cli/dashboard/handlers/__init__.py +17 -0
  64. specify_cli/dashboard/handlers/api.py +143 -0
  65. specify_cli/dashboard/handlers/base.py +65 -0
  66. specify_cli/dashboard/handlers/features.py +390 -0
  67. specify_cli/dashboard/handlers/router.py +81 -0
  68. specify_cli/dashboard/handlers/static.py +50 -0
  69. specify_cli/dashboard/lifecycle.py +541 -0
  70. specify_cli/dashboard/scanner.py +437 -0
  71. specify_cli/dashboard/server.py +123 -0
  72. specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
  73. specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
  74. specify_cli/dashboard/static/spec-kitty.png +0 -0
  75. specify_cli/dashboard/templates/__init__.py +36 -0
  76. specify_cli/dashboard/templates/index.html +258 -0
  77. specify_cli/doc_generators.py +621 -0
  78. specify_cli/doc_state.py +408 -0
  79. specify_cli/frontmatter.py +384 -0
  80. specify_cli/gap_analysis.py +915 -0
  81. specify_cli/gitignore_manager.py +300 -0
  82. specify_cli/guards.py +145 -0
  83. specify_cli/legacy_detector.py +83 -0
  84. specify_cli/manifest.py +286 -0
  85. specify_cli/merge/__init__.py +63 -0
  86. specify_cli/merge/executor.py +653 -0
  87. specify_cli/merge/forecast.py +215 -0
  88. specify_cli/merge/ordering.py +126 -0
  89. specify_cli/merge/preflight.py +230 -0
  90. specify_cli/merge/state.py +185 -0
  91. specify_cli/merge/status_resolver.py +354 -0
  92. specify_cli/mission.py +654 -0
  93. specify_cli/missions/documentation/command-templates/implement.md +309 -0
  94. specify_cli/missions/documentation/command-templates/plan.md +275 -0
  95. specify_cli/missions/documentation/command-templates/review.md +344 -0
  96. specify_cli/missions/documentation/command-templates/specify.md +206 -0
  97. specify_cli/missions/documentation/command-templates/tasks.md +189 -0
  98. specify_cli/missions/documentation/mission.yaml +113 -0
  99. specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
  100. specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
  101. specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
  102. specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
  103. specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
  104. specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
  105. specify_cli/missions/documentation/templates/plan-template.md +269 -0
  106. specify_cli/missions/documentation/templates/release-template.md +222 -0
  107. specify_cli/missions/documentation/templates/spec-template.md +172 -0
  108. specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
  109. specify_cli/missions/documentation/templates/tasks-template.md +159 -0
  110. specify_cli/missions/research/command-templates/merge.md +388 -0
  111. specify_cli/missions/research/command-templates/plan.md +125 -0
  112. specify_cli/missions/research/command-templates/review.md +144 -0
  113. specify_cli/missions/research/command-templates/tasks.md +225 -0
  114. specify_cli/missions/research/mission.yaml +115 -0
  115. specify_cli/missions/research/templates/data-model-template.md +33 -0
  116. specify_cli/missions/research/templates/plan-template.md +161 -0
  117. specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
  118. specify_cli/missions/research/templates/research/source-register.csv +18 -0
  119. specify_cli/missions/research/templates/research-template.md +35 -0
  120. specify_cli/missions/research/templates/spec-template.md +64 -0
  121. specify_cli/missions/research/templates/task-prompt-template.md +148 -0
  122. specify_cli/missions/research/templates/tasks-template.md +114 -0
  123. specify_cli/missions/software-dev/command-templates/accept.md +75 -0
  124. specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
  125. specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
  126. specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
  127. specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
  128. specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
  129. specify_cli/missions/software-dev/command-templates/implement.md +41 -0
  130. specify_cli/missions/software-dev/command-templates/merge.md +383 -0
  131. specify_cli/missions/software-dev/command-templates/plan.md +171 -0
  132. specify_cli/missions/software-dev/command-templates/review.md +32 -0
  133. specify_cli/missions/software-dev/command-templates/specify.md +321 -0
  134. specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
  135. specify_cli/missions/software-dev/mission.yaml +100 -0
  136. specify_cli/missions/software-dev/templates/plan-template.md +132 -0
  137. specify_cli/missions/software-dev/templates/spec-template.md +116 -0
  138. specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
  139. specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
  140. specify_cli/orchestrator/__init__.py +75 -0
  141. specify_cli/orchestrator/agent_config.py +224 -0
  142. specify_cli/orchestrator/agents/__init__.py +170 -0
  143. specify_cli/orchestrator/agents/augment.py +112 -0
  144. specify_cli/orchestrator/agents/base.py +243 -0
  145. specify_cli/orchestrator/agents/claude.py +112 -0
  146. specify_cli/orchestrator/agents/codex.py +106 -0
  147. specify_cli/orchestrator/agents/copilot.py +137 -0
  148. specify_cli/orchestrator/agents/cursor.py +139 -0
  149. specify_cli/orchestrator/agents/gemini.py +115 -0
  150. specify_cli/orchestrator/agents/kilocode.py +94 -0
  151. specify_cli/orchestrator/agents/opencode.py +132 -0
  152. specify_cli/orchestrator/agents/qwen.py +96 -0
  153. specify_cli/orchestrator/config.py +455 -0
  154. specify_cli/orchestrator/executor.py +642 -0
  155. specify_cli/orchestrator/integration.py +1230 -0
  156. specify_cli/orchestrator/monitor.py +898 -0
  157. specify_cli/orchestrator/scheduler.py +832 -0
  158. specify_cli/orchestrator/state.py +508 -0
  159. specify_cli/orchestrator/testing/__init__.py +122 -0
  160. specify_cli/orchestrator/testing/availability.py +346 -0
  161. specify_cli/orchestrator/testing/fixtures.py +684 -0
  162. specify_cli/orchestrator/testing/paths.py +218 -0
  163. specify_cli/plan_validation.py +107 -0
  164. specify_cli/scripts/debug-dashboard-scan.py +61 -0
  165. specify_cli/scripts/tasks/acceptance_support.py +695 -0
  166. specify_cli/scripts/tasks/task_helpers.py +506 -0
  167. specify_cli/scripts/tasks/tasks_cli.py +848 -0
  168. specify_cli/scripts/validate_encoding.py +180 -0
  169. specify_cli/task_metadata_validation.py +274 -0
  170. specify_cli/tasks_support.py +447 -0
  171. specify_cli/template/__init__.py +47 -0
  172. specify_cli/template/asset_generator.py +206 -0
  173. specify_cli/template/github_client.py +334 -0
  174. specify_cli/template/manager.py +193 -0
  175. specify_cli/template/renderer.py +99 -0
  176. specify_cli/templates/AGENTS.md +190 -0
  177. specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
  178. specify_cli/templates/agent-file-template.md +35 -0
  179. specify_cli/templates/checklist-template.md +42 -0
  180. specify_cli/templates/claudeignore-template +58 -0
  181. specify_cli/templates/command-templates/accept.md +141 -0
  182. specify_cli/templates/command-templates/analyze.md +253 -0
  183. specify_cli/templates/command-templates/checklist.md +352 -0
  184. specify_cli/templates/command-templates/clarify.md +224 -0
  185. specify_cli/templates/command-templates/constitution.md +432 -0
  186. specify_cli/templates/command-templates/dashboard.md +175 -0
  187. specify_cli/templates/command-templates/implement.md +190 -0
  188. specify_cli/templates/command-templates/merge.md +374 -0
  189. specify_cli/templates/command-templates/plan.md +171 -0
  190. specify_cli/templates/command-templates/research.md +88 -0
  191. specify_cli/templates/command-templates/review.md +510 -0
  192. specify_cli/templates/command-templates/specify.md +321 -0
  193. specify_cli/templates/command-templates/status.md +92 -0
  194. specify_cli/templates/command-templates/tasks.md +199 -0
  195. specify_cli/templates/git-hooks/pre-commit +22 -0
  196. specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
  197. specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
  198. specify_cli/templates/plan-template.md +108 -0
  199. specify_cli/templates/spec-template.md +118 -0
  200. specify_cli/templates/task-prompt-template.md +165 -0
  201. specify_cli/templates/tasks-template.md +161 -0
  202. specify_cli/templates/vscode-settings.json +13 -0
  203. specify_cli/text_sanitization.py +225 -0
  204. specify_cli/upgrade/__init__.py +18 -0
  205. specify_cli/upgrade/detector.py +239 -0
  206. specify_cli/upgrade/metadata.py +182 -0
  207. specify_cli/upgrade/migrations/__init__.py +65 -0
  208. specify_cli/upgrade/migrations/base.py +80 -0
  209. specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
  210. specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
  211. specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
  212. specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
  213. specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
  214. specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
  215. specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
  216. specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
  217. specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
  218. specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
  219. specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
  220. specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
  221. specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
  222. specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
  223. specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
  224. specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
  225. specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
  226. specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
  227. specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
  228. specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
  229. specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
  230. specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
  231. specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
  232. specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
  233. specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
  234. specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
  235. specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
  236. specify_cli/upgrade/registry.py +121 -0
  237. specify_cli/upgrade/runner.py +284 -0
  238. specify_cli/validators/__init__.py +14 -0
  239. specify_cli/validators/paths.py +154 -0
  240. specify_cli/validators/research.py +428 -0
  241. specify_cli/verify_enhanced.py +270 -0
  242. 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