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