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,139 @@
1
+ """Cursor invoker.
2
+
3
+ Implements the AgentInvoker protocol for Cursor CLI with timeout wrapper.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import shutil
9
+ from pathlib import Path
10
+
11
+ from specify_cli.orchestrator.agents.base import BaseInvoker, InvocationResult
12
+
13
+
14
+ # Exit code from timeout command when process is killed
15
+ TIMEOUT_EXIT_CODE = 124
16
+
17
+
18
+ class CursorInvoker(BaseInvoker):
19
+ """Invoker for Cursor CLI (cursor) with timeout wrapper.
20
+
21
+ IMPORTANT: Cursor CLI may hang indefinitely, so we ALWAYS wrap it
22
+ with a timeout command to ensure the process eventually terminates.
23
+
24
+ Uses `cursor agent` subcommand with -p flag for prompts.
25
+ """
26
+
27
+ agent_id = "cursor"
28
+ command = "cursor"
29
+ uses_stdin = False # Prompt passed as -p argument
30
+ default_timeout = 300 # 5 minutes
31
+
32
+ def __init__(self, timeout_seconds: int | None = None):
33
+ """Initialize Cursor invoker.
34
+
35
+ Args:
36
+ timeout_seconds: Override default timeout (300s). Must be positive.
37
+ """
38
+ if timeout_seconds is not None and timeout_seconds > 0:
39
+ self.timeout = timeout_seconds
40
+ else:
41
+ self.timeout = self.default_timeout
42
+
43
+ def is_installed(self) -> bool:
44
+ """Check if Cursor CLI and timeout command are available."""
45
+ # Need both cursor and timeout to be installed
46
+ return (
47
+ shutil.which(self.command) is not None
48
+ and shutil.which("timeout") is not None
49
+ )
50
+
51
+ def build_command(
52
+ self,
53
+ prompt: str,
54
+ working_dir: Path,
55
+ role: str,
56
+ ) -> list[str]:
57
+ """Build Cursor command with timeout wrapper.
58
+
59
+ Args:
60
+ prompt: Task prompt (passed as -p argument).
61
+ working_dir: Directory for execution.
62
+ role: "implementation" or "review".
63
+
64
+ Returns:
65
+ Command arguments list wrapped with timeout.
66
+ """
67
+ # CRITICAL: Always wrap with timeout to prevent hanging
68
+ cmd = [
69
+ "timeout", str(self.timeout), # Timeout wrapper
70
+ "cursor", "agent",
71
+ "-p", prompt, # Prompt as argument
72
+ "--force", # Autonomous mode (no confirmations)
73
+ "--output-format", "json", # JSON output
74
+ ]
75
+
76
+ return cmd
77
+
78
+ def parse_output(
79
+ self,
80
+ stdout: str,
81
+ stderr: str,
82
+ exit_code: int,
83
+ duration_seconds: float,
84
+ ) -> InvocationResult:
85
+ """Parse Cursor output.
86
+
87
+ Handles the special case of timeout (exit code 124) as a failure.
88
+ """
89
+ # Check for timeout
90
+ if exit_code == TIMEOUT_EXIT_CODE:
91
+ return InvocationResult(
92
+ success=False,
93
+ exit_code=exit_code,
94
+ stdout=stdout,
95
+ stderr=stderr,
96
+ duration_seconds=duration_seconds,
97
+ files_modified=[],
98
+ commits_made=[],
99
+ errors=[
100
+ f"Cursor execution timed out after {self.timeout} seconds. "
101
+ "This is a known issue with Cursor CLI."
102
+ ],
103
+ warnings=[],
104
+ )
105
+
106
+ success = exit_code == 0
107
+ data = self._parse_json_output(stdout)
108
+
109
+ files_modified = []
110
+ commits_made = []
111
+ errors = []
112
+ warnings = []
113
+
114
+ if data:
115
+ if isinstance(data, dict):
116
+ files_modified = self._extract_files_from_output(data)
117
+ commits_made = self._extract_commits_from_output(data)
118
+
119
+ # Check for error in JSON response
120
+ if "error" in data:
121
+ errors.append(str(data["error"]))
122
+
123
+ # Fall back to stderr for errors
124
+ if not errors and stderr.strip() and not success:
125
+ errors = self._extract_errors_from_output(None, stderr)
126
+
127
+ warnings = self._extract_warnings_from_output(data, stderr)
128
+
129
+ return InvocationResult(
130
+ success=success,
131
+ exit_code=exit_code,
132
+ stdout=stdout,
133
+ stderr=stderr,
134
+ duration_seconds=duration_seconds,
135
+ files_modified=files_modified,
136
+ commits_made=commits_made,
137
+ errors=errors,
138
+ warnings=warnings,
139
+ )
@@ -0,0 +1,115 @@
1
+ """Google Gemini invoker.
2
+
3
+ Implements the AgentInvoker protocol for Google Gemini 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
+ # Gemini-specific exit codes
14
+ GEMINI_EXIT_SUCCESS = 0
15
+ GEMINI_EXIT_AUTH_ERROR = 41
16
+ GEMINI_EXIT_RATE_LIMIT = 42
17
+ GEMINI_EXIT_GENERAL_ERROR = 52
18
+ GEMINI_EXIT_INTERRUPTED = 130
19
+
20
+
21
+ class GeminiInvoker(BaseInvoker):
22
+ """Invoker for Google Gemini CLI (gemini).
23
+
24
+ Gemini accepts prompts via stdin with -p flag for headless mode.
25
+ It supports JSON output and has specific exit codes for different errors.
26
+ """
27
+
28
+ agent_id = "gemini"
29
+ command = "gemini"
30
+ uses_stdin = True
31
+
32
+ def build_command(
33
+ self,
34
+ prompt: str,
35
+ working_dir: Path,
36
+ role: str,
37
+ ) -> list[str]:
38
+ """Build Gemini command.
39
+
40
+ Args:
41
+ prompt: Task prompt (passed via stdin).
42
+ working_dir: Directory for execution.
43
+ role: "implementation" or "review".
44
+
45
+ Returns:
46
+ Command arguments list.
47
+ """
48
+ cmd = [
49
+ "gemini",
50
+ "-p", # Headless/non-interactive mode
51
+ "--yolo", # Autonomous mode (no confirmations)
52
+ "--output-format", "json", # Structured output
53
+ ]
54
+
55
+ return cmd
56
+
57
+ def parse_output(
58
+ self,
59
+ stdout: str,
60
+ stderr: str,
61
+ exit_code: int,
62
+ duration_seconds: float,
63
+ ) -> InvocationResult:
64
+ """Parse Gemini JSON output.
65
+
66
+ Handles Gemini-specific exit codes and JSON structure.
67
+ """
68
+ success = exit_code == GEMINI_EXIT_SUCCESS
69
+ data = self._parse_json_output(stdout)
70
+
71
+ files_modified = []
72
+ commits_made = []
73
+ errors = []
74
+ warnings = []
75
+
76
+ # Handle specific exit codes
77
+ if exit_code == GEMINI_EXIT_AUTH_ERROR:
78
+ errors.append("Gemini authentication error - check GOOGLE_API_KEY")
79
+ elif exit_code == GEMINI_EXIT_RATE_LIMIT:
80
+ errors.append("Gemini rate limit exceeded - wait before retrying")
81
+ elif exit_code == GEMINI_EXIT_GENERAL_ERROR:
82
+ errors.append("Gemini general error")
83
+ elif exit_code == GEMINI_EXIT_INTERRUPTED:
84
+ errors.append("Gemini execution was interrupted")
85
+
86
+ if data:
87
+ # Extract file and commit information
88
+ if isinstance(data, dict):
89
+ files_modified = self._extract_files_from_output(data)
90
+ commits_made = self._extract_commits_from_output(data)
91
+
92
+ # Check for error in JSON response
93
+ if "error" in data:
94
+ errors.append(str(data["error"]))
95
+ if "status" in data and data.get("status") == "error":
96
+ msg = data.get("message", "Unknown Gemini error")
97
+ errors.append(msg)
98
+
99
+ # Fall back to stderr for errors
100
+ if not errors and stderr.strip() and not success:
101
+ errors = self._extract_errors_from_output(None, stderr)
102
+
103
+ warnings = self._extract_warnings_from_output(data, stderr)
104
+
105
+ return InvocationResult(
106
+ success=success,
107
+ exit_code=exit_code,
108
+ stdout=stdout,
109
+ stderr=stderr,
110
+ duration_seconds=duration_seconds,
111
+ files_modified=files_modified,
112
+ commits_made=commits_made,
113
+ errors=errors,
114
+ warnings=warnings,
115
+ )
@@ -0,0 +1,94 @@
1
+ """Kilocode invoker.
2
+
3
+ Implements the AgentInvoker protocol for Kilocode 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 KilocodeInvoker(BaseInvoker):
14
+ """Invoker for Kilocode CLI (kilocode).
15
+
16
+ Kilocode takes prompts as positional arguments (not stdin).
17
+ Uses -a for autonomous mode and -j for JSON output.
18
+ """
19
+
20
+ agent_id = "kilocode"
21
+ command = "kilocode"
22
+ uses_stdin = False # Prompt passed as argument
23
+
24
+ def build_command(
25
+ self,
26
+ prompt: str,
27
+ working_dir: Path,
28
+ role: str,
29
+ ) -> list[str]:
30
+ """Build Kilocode command.
31
+
32
+ Args:
33
+ prompt: Task prompt (passed as argument).
34
+ working_dir: Directory for execution.
35
+ role: "implementation" or "review".
36
+
37
+ Returns:
38
+ Command arguments list.
39
+ """
40
+ cmd = [
41
+ "kilocode",
42
+ "-a", # Autonomous agent mode
43
+ "--yolo", # No confirmations
44
+ "-j", # JSON output
45
+ prompt, # Prompt as positional argument
46
+ ]
47
+
48
+ return cmd
49
+
50
+ def parse_output(
51
+ self,
52
+ stdout: str,
53
+ stderr: str,
54
+ exit_code: int,
55
+ duration_seconds: float,
56
+ ) -> InvocationResult:
57
+ """Parse Kilocode JSON output."""
58
+ success = exit_code == 0
59
+ data = self._parse_json_output(stdout)
60
+
61
+ files_modified = []
62
+ commits_made = []
63
+ errors = []
64
+ warnings = []
65
+
66
+ if data:
67
+ if isinstance(data, dict):
68
+ files_modified = self._extract_files_from_output(data)
69
+ commits_made = self._extract_commits_from_output(data)
70
+
71
+ # Check for error in JSON response
72
+ if "error" in data:
73
+ errors.append(str(data["error"]))
74
+ if "status" in data and data.get("status") == "error":
75
+ msg = data.get("message", "Unknown Kilocode error")
76
+ errors.append(msg)
77
+
78
+ # Fall back to stderr for errors
79
+ if not errors and stderr.strip() and not success:
80
+ errors = self._extract_errors_from_output(None, stderr)
81
+
82
+ warnings = self._extract_warnings_from_output(data, stderr)
83
+
84
+ return InvocationResult(
85
+ success=success,
86
+ exit_code=exit_code,
87
+ stdout=stdout,
88
+ stderr=stderr,
89
+ duration_seconds=duration_seconds,
90
+ files_modified=files_modified,
91
+ commits_made=commits_made,
92
+ errors=errors,
93
+ warnings=warnings,
94
+ )
@@ -0,0 +1,132 @@
1
+ """OpenCode invoker.
2
+
3
+ Implements the AgentInvoker protocol for OpenCode 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 OpenCodeInvoker(BaseInvoker):
14
+ """Invoker for OpenCode CLI (opencode).
15
+
16
+ OpenCode is a multi-provider agent that supports various LLM backends.
17
+ Uses `opencode run` subcommand with stdin for prompts.
18
+ """
19
+
20
+ agent_id = "opencode"
21
+ command = "opencode"
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 OpenCode command.
31
+
32
+ Args:
33
+ prompt: Task prompt (passed via stdin).
34
+ working_dir: Directory for execution.
35
+ role: "implementation" or "review".
36
+
37
+ Returns:
38
+ Command arguments list.
39
+ """
40
+ cmd = [
41
+ "opencode", "run",
42
+ "--agent", "build", # Use build agent with broad permissions
43
+ "--format", "json", # JSON output format
44
+ ]
45
+
46
+ return cmd
47
+
48
+ def parse_output(
49
+ self,
50
+ stdout: str,
51
+ stderr: str,
52
+ exit_code: int,
53
+ duration_seconds: float,
54
+ ) -> InvocationResult:
55
+ """Parse OpenCode JSON streaming output.
56
+
57
+ OpenCode outputs one JSON event per line with types:
58
+ - step_start, step_finish: workflow markers
59
+ - text: assistant messages
60
+ - tool_use: tool invocations
61
+ - error: error events
62
+ """
63
+ import json
64
+
65
+ success = exit_code == 0
66
+ files_modified = []
67
+ commits_made = []
68
+ errors = []
69
+ warnings = []
70
+
71
+ # Parse all JSON lines to find errors and extract data
72
+ if stdout.strip():
73
+ for line in stdout.strip().split("\n"):
74
+ line = line.strip()
75
+ if not line or not line.startswith("{"):
76
+ continue
77
+ try:
78
+ event = json.loads(line)
79
+ event_type = event.get("type", "")
80
+
81
+ # Check for error events
82
+ if event_type == "error":
83
+ error_info = event.get("error", {})
84
+ error_name = error_info.get("name", "UnknownError")
85
+ error_data = error_info.get("data", {})
86
+ error_msg = error_data.get("message", str(error_info))
87
+ errors.append(f"{error_name}: {error_msg}")
88
+ success = False
89
+
90
+ # Extract tool results for file modifications
91
+ elif event_type == "tool_use":
92
+ part = event.get("part", {})
93
+ state = part.get("state", {})
94
+ tool = part.get("tool", "")
95
+
96
+ # Track file edits
97
+ if tool in ("edit", "write"):
98
+ input_data = state.get("input", {})
99
+ file_path = input_data.get("filePath") or input_data.get("file_path")
100
+ if file_path and file_path not in files_modified:
101
+ files_modified.append(file_path)
102
+
103
+ # Track commits
104
+ elif tool == "bash":
105
+ output = state.get("output", "")
106
+ if "commit" in state.get("input", {}).get("command", ""):
107
+ # Extract commit hash if present
108
+ for word in output.split():
109
+ if len(word) == 40 and all(c in "0123456789abcdef" for c in word):
110
+ commits_made.append(word[:8])
111
+ break
112
+
113
+ except json.JSONDecodeError:
114
+ continue
115
+
116
+ # Fall back to stderr for errors
117
+ if not errors and stderr.strip() and not success:
118
+ errors = self._extract_errors_from_output(None, stderr)
119
+
120
+ warnings = self._extract_warnings_from_output(None, stderr)
121
+
122
+ return InvocationResult(
123
+ success=success,
124
+ exit_code=exit_code,
125
+ stdout=stdout,
126
+ stderr=stderr,
127
+ duration_seconds=duration_seconds,
128
+ files_modified=files_modified,
129
+ commits_made=commits_made,
130
+ errors=errors,
131
+ warnings=warnings,
132
+ )
@@ -0,0 +1,96 @@
1
+ """Qwen Code invoker.
2
+
3
+ Implements the AgentInvoker protocol for Qwen 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 QwenInvoker(BaseInvoker):
14
+ """Invoker for Qwen Code CLI (qwen).
15
+
16
+ Qwen is a fork of Gemini CLI with similar flags.
17
+ Accepts prompts via stdin with -p flag for headless mode.
18
+ """
19
+
20
+ agent_id = "qwen"
21
+ command = "qwen"
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 Qwen command.
31
+
32
+ Args:
33
+ prompt: Task prompt (passed via stdin).
34
+ working_dir: Directory for execution.
35
+ role: "implementation" or "review".
36
+
37
+ Returns:
38
+ Command arguments list.
39
+ """
40
+ cmd = [
41
+ "qwen",
42
+ "-p", # Headless/non-interactive mode
43
+ "--yolo", # Autonomous mode (no confirmations)
44
+ "--output-format", "json", # Structured output
45
+ ]
46
+
47
+ return cmd
48
+
49
+ def parse_output(
50
+ self,
51
+ stdout: str,
52
+ stderr: str,
53
+ exit_code: int,
54
+ duration_seconds: float,
55
+ ) -> InvocationResult:
56
+ """Parse Qwen JSON output.
57
+
58
+ Similar parsing to Gemini since Qwen is a fork.
59
+ """
60
+ success = exit_code == 0
61
+ data = self._parse_json_output(stdout)
62
+
63
+ files_modified = []
64
+ commits_made = []
65
+ errors = []
66
+ warnings = []
67
+
68
+ if data:
69
+ if isinstance(data, dict):
70
+ files_modified = self._extract_files_from_output(data)
71
+ commits_made = self._extract_commits_from_output(data)
72
+
73
+ # Check for error in JSON response
74
+ if "error" in data:
75
+ errors.append(str(data["error"]))
76
+ if "status" in data and data.get("status") == "error":
77
+ msg = data.get("message", "Unknown Qwen error")
78
+ errors.append(msg)
79
+
80
+ # Fall back to stderr for errors
81
+ if not errors and stderr.strip() and not success:
82
+ errors = self._extract_errors_from_output(None, stderr)
83
+
84
+ warnings = self._extract_warnings_from_output(data, stderr)
85
+
86
+ return InvocationResult(
87
+ success=success,
88
+ exit_code=exit_code,
89
+ stdout=stdout,
90
+ stderr=stderr,
91
+ duration_seconds=duration_seconds,
92
+ files_modified=files_modified,
93
+ commits_made=commits_made,
94
+ errors=errors,
95
+ warnings=warnings,
96
+ )