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,224 @@
1
+ """Agent configuration for the orchestrator.
2
+
3
+ This module manages agent configuration that is set during `spec-kitty init`
4
+ and used by the orchestrator to select agents for implementation and review.
5
+
6
+ The configuration is stored in .kittify/config.yaml under the `agents` key.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import random
12
+ from dataclasses import dataclass, field
13
+ from enum import Enum
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ from ruamel.yaml import YAML
18
+
19
+ import logging
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ class SelectionStrategy(str, Enum):
25
+ """Strategy for selecting agents."""
26
+
27
+ PREFERRED = "preferred" # Use user-specified preferred agents
28
+ RANDOM = "random" # Randomly select from available agents
29
+
30
+
31
+ @dataclass
32
+ class AgentSelectionConfig:
33
+ """Configuration for agent selection.
34
+
35
+ Attributes:
36
+ strategy: How to select agents (preferred or random)
37
+ preferred_implementer: Agent ID for implementation (if strategy=preferred)
38
+ preferred_reviewer: Agent ID for review (if strategy=preferred)
39
+ """
40
+
41
+ strategy: SelectionStrategy = SelectionStrategy.PREFERRED
42
+ preferred_implementer: str | None = None
43
+ preferred_reviewer: str | None = None
44
+
45
+
46
+ @dataclass
47
+ class AgentConfig:
48
+ """Full agent configuration.
49
+
50
+ Attributes:
51
+ available: List of agent IDs that are available for use
52
+ selection: Configuration for how to select agents
53
+ """
54
+
55
+ available: list[str] = field(default_factory=list)
56
+ selection: AgentSelectionConfig = field(default_factory=AgentSelectionConfig)
57
+
58
+ def select_implementer(self, exclude: str | None = None) -> str | None:
59
+ """Select an agent for implementation.
60
+
61
+ Args:
62
+ exclude: Optional agent ID to exclude from selection
63
+
64
+ Returns:
65
+ Selected agent ID or None if no agents available
66
+ """
67
+ candidates = [a for a in self.available if a != exclude]
68
+ if not candidates:
69
+ return None
70
+
71
+ if self.selection.strategy == SelectionStrategy.PREFERRED:
72
+ if self.selection.preferred_implementer in candidates:
73
+ return self.selection.preferred_implementer
74
+ # Fall back to first available
75
+ return candidates[0]
76
+ else: # RANDOM
77
+ return random.choice(candidates)
78
+
79
+ def select_reviewer(self, implementer: str | None = None) -> str | None:
80
+ """Select an agent for review.
81
+
82
+ Prefers a different agent than the implementer for cross-review.
83
+
84
+ Args:
85
+ implementer: Agent that did implementation (prefer different agent)
86
+
87
+ Returns:
88
+ Selected agent ID or None if no agents available
89
+ """
90
+ # Prefer different agent for cross-review
91
+ candidates = [a for a in self.available if a != implementer]
92
+
93
+ # Fall back to same agent if no other available
94
+ if not candidates:
95
+ candidates = self.available.copy()
96
+
97
+ if not candidates:
98
+ return None
99
+
100
+ if self.selection.strategy == SelectionStrategy.PREFERRED:
101
+ if self.selection.preferred_reviewer in candidates:
102
+ return self.selection.preferred_reviewer
103
+ # Fall back to first available that's not the implementer
104
+ return candidates[0]
105
+ else: # RANDOM
106
+ return random.choice(candidates)
107
+
108
+
109
+ def load_agent_config(repo_root: Path) -> AgentConfig:
110
+ """Load agent configuration from .kittify/config.yaml.
111
+
112
+ Args:
113
+ repo_root: Repository root directory
114
+
115
+ Returns:
116
+ AgentConfig instance (defaults if not configured)
117
+ """
118
+ config_file = repo_root / ".kittify" / "config.yaml"
119
+
120
+ if not config_file.exists():
121
+ logger.warning(f"Config file not found: {config_file}")
122
+ return AgentConfig()
123
+
124
+ yaml = YAML()
125
+ yaml.preserve_quotes = True
126
+
127
+ try:
128
+ with open(config_file, "r") as f:
129
+ data = yaml.load(f) or {}
130
+ except Exception as e:
131
+ logger.error(f"Failed to load config: {e}")
132
+ return AgentConfig()
133
+
134
+ agents_data = data.get("agents", {})
135
+ if not agents_data:
136
+ logger.info("No agents section in config.yaml")
137
+ return AgentConfig()
138
+
139
+ # Parse available agents
140
+ available = agents_data.get("available", [])
141
+ if isinstance(available, str):
142
+ available = [available]
143
+
144
+ # Parse selection config
145
+ selection_data = agents_data.get("selection", {})
146
+ strategy_str = selection_data.get("strategy", "preferred")
147
+ try:
148
+ strategy = SelectionStrategy(strategy_str)
149
+ except ValueError:
150
+ logger.warning(f"Invalid strategy '{strategy_str}', defaulting to 'preferred'")
151
+ strategy = SelectionStrategy.PREFERRED
152
+
153
+ selection = AgentSelectionConfig(
154
+ strategy=strategy,
155
+ preferred_implementer=selection_data.get("preferred_implementer"),
156
+ preferred_reviewer=selection_data.get("preferred_reviewer"),
157
+ )
158
+
159
+ return AgentConfig(available=available, selection=selection)
160
+
161
+
162
+ def save_agent_config(repo_root: Path, config: AgentConfig) -> None:
163
+ """Save agent configuration to .kittify/config.yaml.
164
+
165
+ Merges with existing config (preserves other sections like vcs).
166
+
167
+ Args:
168
+ repo_root: Repository root directory
169
+ config: AgentConfig to save
170
+ """
171
+ config_dir = repo_root / ".kittify"
172
+ config_file = config_dir / "config.yaml"
173
+
174
+ yaml = YAML()
175
+ yaml.preserve_quotes = True
176
+
177
+ # Load existing config or create new
178
+ if config_file.exists():
179
+ with open(config_file, "r") as f:
180
+ data = yaml.load(f) or {}
181
+ else:
182
+ data = {}
183
+ config_dir.mkdir(parents=True, exist_ok=True)
184
+
185
+ # Update agents section
186
+ data["agents"] = {
187
+ "available": config.available,
188
+ "selection": {
189
+ "strategy": config.selection.strategy.value,
190
+ "preferred_implementer": config.selection.preferred_implementer,
191
+ "preferred_reviewer": config.selection.preferred_reviewer,
192
+ },
193
+ }
194
+
195
+ # Write back
196
+ with open(config_file, "w") as f:
197
+ yaml.dump(data, f)
198
+
199
+ logger.info(f"Saved agent config to {config_file}")
200
+
201
+
202
+ def get_configured_agents(repo_root: Path) -> list[str]:
203
+ """Get list of configured agents.
204
+
205
+ This is the DEFINITIVE list of available agents, set during init.
206
+
207
+ Args:
208
+ repo_root: Repository root directory
209
+
210
+ Returns:
211
+ List of agent IDs, empty if not configured
212
+ """
213
+ config = load_agent_config(repo_root)
214
+ return config.available
215
+
216
+
217
+ __all__ = [
218
+ "SelectionStrategy",
219
+ "AgentSelectionConfig",
220
+ "AgentConfig",
221
+ "load_agent_config",
222
+ "save_agent_config",
223
+ "get_configured_agents",
224
+ ]
@@ -0,0 +1,170 @@
1
+ """Agent invokers for the orchestrator.
2
+
3
+ This subpackage contains implementations of AgentInvoker for each supported
4
+ AI coding agent. Each invoker knows how to:
5
+ - Build the command line for that agent
6
+ - Pipe prompts via stdin or file
7
+ - Parse output and detect success/failure
8
+
9
+ Supported Agents (9 total):
10
+ Core (WP02):
11
+ - claude-code: Claude Code (Anthropic)
12
+ - codex: GitHub Codex
13
+ - copilot: GitHub Copilot
14
+ - gemini: Google Gemini
15
+
16
+ Additional (WP03):
17
+ - qwen: Qwen Code
18
+ - opencode: OpenCode
19
+ - kilocode: Kilocode
20
+ - augment: Augment Code (auggie)
21
+ - cursor: Cursor (with timeout wrapper)
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import TYPE_CHECKING
27
+
28
+ from specify_cli.orchestrator.agents.augment import AugmentInvoker
29
+ from specify_cli.orchestrator.agents.base import (
30
+ AgentInvoker,
31
+ BaseInvoker,
32
+ InvocationResult,
33
+ )
34
+ from specify_cli.orchestrator.agents.claude import ClaudeInvoker
35
+ from specify_cli.orchestrator.agents.codex import CodexInvoker
36
+ from specify_cli.orchestrator.agents.copilot import CopilotInvoker
37
+ from specify_cli.orchestrator.agents.cursor import CursorInvoker
38
+ from specify_cli.orchestrator.agents.gemini import GeminiInvoker
39
+ from specify_cli.orchestrator.agents.kilocode import KilocodeInvoker
40
+ from specify_cli.orchestrator.agents.opencode import OpenCodeInvoker
41
+ from specify_cli.orchestrator.agents.qwen import QwenInvoker
42
+
43
+ if TYPE_CHECKING:
44
+ pass
45
+
46
+ # Registry mapping agent IDs to invoker classes
47
+ AGENT_REGISTRY: dict[str, type[BaseInvoker]] = {
48
+ "claude-code": ClaudeInvoker,
49
+ "codex": CodexInvoker,
50
+ "copilot": CopilotInvoker,
51
+ "gemini": GeminiInvoker,
52
+ "qwen": QwenInvoker,
53
+ "opencode": OpenCodeInvoker,
54
+ "kilocode": KilocodeInvoker,
55
+ "augment": AugmentInvoker,
56
+ "cursor": CursorInvoker,
57
+ }
58
+
59
+ # Aliases for user-friendly names to canonical registry names
60
+ # These match the keys used in AI_CHOICES (core/config.py) and .kittify/config.yaml
61
+ AGENT_ALIASES: dict[str, str] = {
62
+ "claude": "claude-code", # User-facing name -> registry name
63
+ "auggie": "augment", # Alternative name used in gitignore_manager
64
+ }
65
+
66
+ # Priority order for agent selection (lower index = higher priority)
67
+ # Based on feature 019 research recommendations
68
+ AGENT_PRIORITY_ORDER = [
69
+ "claude-code",
70
+ "codex",
71
+ "copilot",
72
+ "gemini",
73
+ "qwen",
74
+ "opencode",
75
+ "kilocode",
76
+ "augment",
77
+ "cursor",
78
+ ]
79
+
80
+
81
+ def normalize_agent_id(agent_id: str) -> str:
82
+ """Normalize agent ID to canonical registry name.
83
+
84
+ Resolves user-friendly aliases (e.g., "claude" -> "claude-code").
85
+
86
+ Args:
87
+ agent_id: User-provided agent ID (may be an alias).
88
+
89
+ Returns:
90
+ Canonical agent ID used in AGENT_REGISTRY.
91
+ """
92
+ return AGENT_ALIASES.get(agent_id, agent_id)
93
+
94
+
95
+ def get_invoker(agent_id: str) -> BaseInvoker:
96
+ """Get invoker instance for agent ID.
97
+
98
+ Args:
99
+ agent_id: The agent identifier (e.g., "claude-code", "codex").
100
+ Aliases like "claude" are automatically resolved.
101
+
102
+ Returns:
103
+ Instantiated invoker for the specified agent.
104
+
105
+ Raises:
106
+ ValueError: If agent_id is not recognized.
107
+ """
108
+ # Resolve aliases to canonical names
109
+ canonical_id = normalize_agent_id(agent_id)
110
+
111
+ invoker_class = AGENT_REGISTRY.get(canonical_id)
112
+ if not invoker_class:
113
+ valid_agents = ", ".join(sorted(AGENT_REGISTRY.keys()))
114
+ # Include aliases in the error message for clarity
115
+ alias_info = ", ".join(f"{k}->{v}" for k, v in AGENT_ALIASES.items())
116
+ raise ValueError(
117
+ f"Unknown agent: {agent_id}. Valid agents: {valid_agents}. "
118
+ f"Aliases: {alias_info}"
119
+ )
120
+ return invoker_class()
121
+
122
+
123
+ def detect_installed_agents() -> list[str]:
124
+ """Detect which agents are installed on the system.
125
+
126
+ Returns:
127
+ List of installed agent IDs, sorted by default priority
128
+ (claude-code first, cursor last).
129
+ """
130
+ installed = []
131
+ for agent_id, invoker_class in AGENT_REGISTRY.items():
132
+ invoker = invoker_class()
133
+ if invoker.is_installed():
134
+ installed.append(agent_id)
135
+
136
+ # Sort by priority order
137
+ return sorted(
138
+ installed,
139
+ key=lambda x: (
140
+ AGENT_PRIORITY_ORDER.index(x)
141
+ if x in AGENT_PRIORITY_ORDER
142
+ else 999
143
+ ),
144
+ )
145
+
146
+
147
+ __all__ = [
148
+ # Protocol and base classes
149
+ "AgentInvoker",
150
+ "BaseInvoker",
151
+ "InvocationResult",
152
+ # Core invokers (WP02)
153
+ "ClaudeInvoker",
154
+ "CodexInvoker",
155
+ "CopilotInvoker",
156
+ "GeminiInvoker",
157
+ # Additional invokers (WP03)
158
+ "QwenInvoker",
159
+ "OpenCodeInvoker",
160
+ "KilocodeInvoker",
161
+ "AugmentInvoker",
162
+ "CursorInvoker",
163
+ # Registry and utilities
164
+ "AGENT_REGISTRY",
165
+ "AGENT_ALIASES",
166
+ "AGENT_PRIORITY_ORDER",
167
+ "get_invoker",
168
+ "normalize_agent_id",
169
+ "detect_installed_agents",
170
+ ]
@@ -0,0 +1,112 @@
1
+ """Augment Code invoker.
2
+
3
+ Implements the AgentInvoker protocol for Augment Code CLI (auggie).
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 AugmentInvoker(BaseInvoker):
14
+ """Invoker for Augment Code CLI (auggie).
15
+
16
+ Auggie uses --acp for autonomous coding prompt mode.
17
+ Does not support JSON output - relies on exit code only.
18
+ """
19
+
20
+ agent_id = "augment"
21
+ command = "auggie"
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 Auggie 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
+ "auggie",
42
+ "--acp", # Autonomous coding prompt mode
43
+ prompt, # Prompt as positional argument
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 Auggie output.
56
+
57
+ Auggie doesn't support JSON output, so we rely primarily
58
+ on exit code and parse stdout/stderr for useful information.
59
+ """
60
+ success = exit_code == 0
61
+
62
+ # No JSON output - extract what we can from text
63
+ files_modified = self._extract_files_from_text(stdout)
64
+ commits_made = []
65
+ errors = []
66
+ warnings = []
67
+
68
+ # Check stderr for errors
69
+ if stderr.strip():
70
+ if not success:
71
+ errors = self._extract_errors_from_output(None, stderr)
72
+ warnings = self._extract_warnings_from_output(None, stderr)
73
+
74
+ # Check stdout for error indicators
75
+ if not success and not errors:
76
+ stdout_lower = stdout.lower()
77
+ if "error" in stdout_lower or "failed" in stdout_lower:
78
+ error_lines = [
79
+ line.strip()
80
+ for line in stdout.split("\n")
81
+ if "error" in line.lower() or "failed" in line.lower()
82
+ ]
83
+ errors.extend(error_lines[:3])
84
+
85
+ return InvocationResult(
86
+ success=success,
87
+ exit_code=exit_code,
88
+ stdout=stdout,
89
+ stderr=stderr,
90
+ duration_seconds=duration_seconds,
91
+ files_modified=files_modified,
92
+ commits_made=commits_made,
93
+ errors=errors,
94
+ warnings=warnings,
95
+ )
96
+
97
+ def _extract_files_from_text(self, text: str) -> list[str]:
98
+ """Extract file paths mentioned in unstructured text output."""
99
+ files = []
100
+ import re
101
+
102
+ # Patterns like "Created file.py", "Modified src/foo.py", etc.
103
+ patterns = [
104
+ r"(?:created|modified|updated|wrote|edited)\s+['\"]?([^\s'\"]+\.\w+)['\"]?",
105
+ r"(?:writing to|saving)\s+['\"]?([^\s'\"]+\.\w+)['\"]?",
106
+ ]
107
+
108
+ for pattern in patterns:
109
+ matches = re.findall(pattern, text, re.IGNORECASE)
110
+ files.extend(matches)
111
+
112
+ return list(set(files))