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,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
|
+
)
|