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,346 @@
|
|
|
1
|
+
"""Agent availability detection for orchestrator e2e tests.
|
|
2
|
+
|
|
3
|
+
This module provides functions to detect which AI coding agents are installed
|
|
4
|
+
and authenticated on the system. Results are categorized into tiers:
|
|
5
|
+
|
|
6
|
+
- Core tier (5 agents): Tests FAIL if unavailable
|
|
7
|
+
claude, codex, copilot, gemini, opencode
|
|
8
|
+
|
|
9
|
+
- Extended tier (7 agents): Tests SKIP if unavailable
|
|
10
|
+
cursor, qwen, augment, kilocode, roo, windsurf, amazonq
|
|
11
|
+
|
|
12
|
+
Example usage:
|
|
13
|
+
from specify_cli.orchestrator.testing.availability import (
|
|
14
|
+
detect_all_agents,
|
|
15
|
+
get_available_agents,
|
|
16
|
+
CORE_AGENTS,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Detect all agents (cached for session)
|
|
20
|
+
agents = await detect_all_agents()
|
|
21
|
+
|
|
22
|
+
# Get list of available agent IDs
|
|
23
|
+
available = get_available_agents()
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import asyncio
|
|
29
|
+
import os
|
|
30
|
+
import shutil
|
|
31
|
+
import time
|
|
32
|
+
from dataclasses import dataclass
|
|
33
|
+
from typing import Literal
|
|
34
|
+
|
|
35
|
+
# Agent tier constants
|
|
36
|
+
# Core tier: Tests fail if these are unavailable
|
|
37
|
+
CORE_AGENTS = frozenset({"claude", "codex", "copilot", "gemini", "opencode"})
|
|
38
|
+
|
|
39
|
+
# Extended tier: Tests skip if these are unavailable
|
|
40
|
+
EXTENDED_AGENTS = frozenset({
|
|
41
|
+
"cursor", "qwen", "augment", "kilocode", "roo", "windsurf", "amazonq"
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
# All supported agents
|
|
45
|
+
ALL_AGENTS = CORE_AGENTS | EXTENDED_AGENTS
|
|
46
|
+
|
|
47
|
+
# Mapping from canonical agent IDs to orchestrator registry IDs
|
|
48
|
+
# The orchestrator uses "claude-code" but testing uses "claude"
|
|
49
|
+
AGENT_ID_TO_REGISTRY: dict[str, str] = {
|
|
50
|
+
"claude": "claude-code",
|
|
51
|
+
"codex": "codex",
|
|
52
|
+
"copilot": "copilot",
|
|
53
|
+
"gemini": "gemini",
|
|
54
|
+
"opencode": "opencode",
|
|
55
|
+
"cursor": "cursor",
|
|
56
|
+
"qwen": "qwen",
|
|
57
|
+
"augment": "augment",
|
|
58
|
+
"kilocode": "kilocode",
|
|
59
|
+
# These 3 don't have invokers yet (TODO: add when implemented)
|
|
60
|
+
"roo": None,
|
|
61
|
+
"windsurf": None,
|
|
62
|
+
"amazonq": None,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
# Probe timeout in seconds (configurable via environment)
|
|
66
|
+
PROBE_TIMEOUT = int(os.environ.get("ORCHESTRATOR_PROBE_TIMEOUT", "10"))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class AgentAvailability:
|
|
71
|
+
"""Result of detecting an agent's availability for testing.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
agent_id: Canonical agent identifier (e.g., 'claude', 'codex')
|
|
75
|
+
is_installed: True if the agent CLI binary exists and is executable
|
|
76
|
+
is_authenticated: True if the agent responded to a probe API call
|
|
77
|
+
tier: Agent tier ('core' or 'extended')
|
|
78
|
+
failure_reason: Human-readable reason if unavailable
|
|
79
|
+
probe_duration_ms: Time taken for auth probe in milliseconds
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
agent_id: str
|
|
83
|
+
is_installed: bool
|
|
84
|
+
is_authenticated: bool
|
|
85
|
+
tier: Literal["core", "extended"]
|
|
86
|
+
failure_reason: str | None = None
|
|
87
|
+
probe_duration_ms: int | None = None
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def is_available(self) -> bool:
|
|
91
|
+
"""Agent is available if installed and authenticated."""
|
|
92
|
+
return self.is_installed and self.is_authenticated
|
|
93
|
+
|
|
94
|
+
@classmethod
|
|
95
|
+
def get_tier(cls, agent_id: str) -> Literal["core", "extended"]:
|
|
96
|
+
"""Determine tier for an agent ID.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
agent_id: Canonical agent identifier
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
'core' if agent is in core tier, 'extended' otherwise
|
|
103
|
+
"""
|
|
104
|
+
if agent_id in CORE_AGENTS:
|
|
105
|
+
return "core"
|
|
106
|
+
return "extended"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _get_invoker_class(agent_id: str):
|
|
110
|
+
"""Get the invoker class for an agent ID.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
agent_id: Canonical agent identifier
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Invoker class or None if not available
|
|
117
|
+
"""
|
|
118
|
+
# Map canonical ID to registry ID
|
|
119
|
+
registry_id = AGENT_ID_TO_REGISTRY.get(agent_id)
|
|
120
|
+
if registry_id is None:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
# Import registry to avoid circular imports
|
|
124
|
+
from specify_cli.orchestrator.agents import AGENT_REGISTRY
|
|
125
|
+
|
|
126
|
+
return AGENT_REGISTRY.get(registry_id)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def check_installed(agent_id: str) -> tuple[bool, str | None]:
|
|
130
|
+
"""Check if an agent CLI is installed.
|
|
131
|
+
|
|
132
|
+
Uses shutil.which() to check if the agent's command is in PATH.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
agent_id: Canonical agent identifier (e.g., 'claude', 'codex')
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Tuple of (is_installed, failure_reason)
|
|
139
|
+
- is_installed: True if CLI is found in PATH
|
|
140
|
+
- failure_reason: Human-readable reason if not installed, None otherwise
|
|
141
|
+
"""
|
|
142
|
+
# Check if we have a mapping for this agent
|
|
143
|
+
registry_id = AGENT_ID_TO_REGISTRY.get(agent_id)
|
|
144
|
+
if registry_id is None:
|
|
145
|
+
# Agent doesn't have an invoker yet
|
|
146
|
+
return False, f"No invoker implemented for agent: {agent_id}"
|
|
147
|
+
|
|
148
|
+
# Get invoker class
|
|
149
|
+
invoker_class = _get_invoker_class(agent_id)
|
|
150
|
+
if invoker_class is None:
|
|
151
|
+
return False, f"Unknown agent: {agent_id}"
|
|
152
|
+
|
|
153
|
+
# Get the command from the invoker
|
|
154
|
+
command = getattr(invoker_class, "command", None)
|
|
155
|
+
if command is None:
|
|
156
|
+
return False, f"Agent {agent_id} has no command attribute"
|
|
157
|
+
|
|
158
|
+
# Check if command exists in PATH
|
|
159
|
+
if shutil.which(command) is not None:
|
|
160
|
+
return True, None
|
|
161
|
+
|
|
162
|
+
return False, f"CLI not found: {command}"
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def probe_agent_auth(agent_id: str) -> tuple[bool, str | None, int]:
|
|
166
|
+
"""Probe an agent to verify authentication.
|
|
167
|
+
|
|
168
|
+
Makes a minimal API call to verify the agent can communicate.
|
|
169
|
+
For agents without a probe() method, assumes authenticated if installed.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
agent_id: Canonical agent identifier
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Tuple of (is_authenticated, failure_reason, duration_ms)
|
|
176
|
+
- is_authenticated: True if probe succeeded
|
|
177
|
+
- failure_reason: Human-readable reason if probe failed
|
|
178
|
+
- duration_ms: Time taken for probe in milliseconds
|
|
179
|
+
"""
|
|
180
|
+
invoker_class = _get_invoker_class(agent_id)
|
|
181
|
+
if invoker_class is None:
|
|
182
|
+
return False, f"Unknown agent: {agent_id}", 0
|
|
183
|
+
|
|
184
|
+
start_time = time.monotonic()
|
|
185
|
+
|
|
186
|
+
try:
|
|
187
|
+
# Create invoker instance
|
|
188
|
+
invoker = invoker_class()
|
|
189
|
+
|
|
190
|
+
# Check if invoker has a probe() method
|
|
191
|
+
if hasattr(invoker, "probe") and callable(getattr(invoker, "probe")):
|
|
192
|
+
# Call probe method with timeout
|
|
193
|
+
result = await asyncio.wait_for(
|
|
194
|
+
invoker.probe(),
|
|
195
|
+
timeout=PROBE_TIMEOUT
|
|
196
|
+
)
|
|
197
|
+
duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
198
|
+
|
|
199
|
+
if result:
|
|
200
|
+
return True, None, duration_ms
|
|
201
|
+
else:
|
|
202
|
+
return False, "Probe returned failure", duration_ms
|
|
203
|
+
else:
|
|
204
|
+
# No probe method - assume authenticated if installed
|
|
205
|
+
duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
206
|
+
return True, None, duration_ms
|
|
207
|
+
|
|
208
|
+
except asyncio.TimeoutError:
|
|
209
|
+
duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
210
|
+
return False, f"Probe timed out after {PROBE_TIMEOUT}s", duration_ms
|
|
211
|
+
except Exception as e:
|
|
212
|
+
duration_ms = int((time.monotonic() - start_time) * 1000)
|
|
213
|
+
return False, f"Probe error: {str(e)}", duration_ms
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# Module-level cache for agent detection results
|
|
217
|
+
_agent_cache: dict[str, AgentAvailability] | None = None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def clear_agent_cache() -> None:
|
|
221
|
+
"""Clear the cached agent detection results.
|
|
222
|
+
|
|
223
|
+
Call this to force re-detection on the next detect_all_agents() call.
|
|
224
|
+
Useful for testing or when agent availability may have changed.
|
|
225
|
+
"""
|
|
226
|
+
global _agent_cache
|
|
227
|
+
_agent_cache = None
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
async def detect_agent(agent_id: str) -> AgentAvailability:
|
|
231
|
+
"""Detect availability of a single agent.
|
|
232
|
+
|
|
233
|
+
Checks both installation (CLI in PATH) and authentication (probe API call).
|
|
234
|
+
|
|
235
|
+
Args:
|
|
236
|
+
agent_id: Canonical agent identifier (e.g., 'claude', 'codex')
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
AgentAvailability with detection results
|
|
240
|
+
"""
|
|
241
|
+
tier = AgentAvailability.get_tier(agent_id)
|
|
242
|
+
|
|
243
|
+
# Check installation
|
|
244
|
+
is_installed, install_reason = check_installed(agent_id)
|
|
245
|
+
|
|
246
|
+
if not is_installed:
|
|
247
|
+
return AgentAvailability(
|
|
248
|
+
agent_id=agent_id,
|
|
249
|
+
is_installed=False,
|
|
250
|
+
is_authenticated=False,
|
|
251
|
+
tier=tier,
|
|
252
|
+
failure_reason=install_reason,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Probe authentication
|
|
256
|
+
is_authenticated, auth_reason, duration_ms = await probe_agent_auth(agent_id)
|
|
257
|
+
|
|
258
|
+
return AgentAvailability(
|
|
259
|
+
agent_id=agent_id,
|
|
260
|
+
is_installed=True,
|
|
261
|
+
is_authenticated=is_authenticated,
|
|
262
|
+
tier=tier,
|
|
263
|
+
failure_reason=auth_reason,
|
|
264
|
+
probe_duration_ms=duration_ms,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
async def detect_all_agents() -> dict[str, AgentAvailability]:
|
|
269
|
+
"""Detect availability of all supported agents.
|
|
270
|
+
|
|
271
|
+
Results are cached for the session duration. Call clear_agent_cache()
|
|
272
|
+
to force re-detection.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Dict mapping agent_id to AgentAvailability for all 12 agents,
|
|
276
|
+
sorted alphabetically by agent_id
|
|
277
|
+
"""
|
|
278
|
+
global _agent_cache
|
|
279
|
+
|
|
280
|
+
if _agent_cache is not None:
|
|
281
|
+
return _agent_cache
|
|
282
|
+
|
|
283
|
+
results = {}
|
|
284
|
+
for agent_id in sorted(ALL_AGENTS):
|
|
285
|
+
results[agent_id] = await detect_agent(agent_id)
|
|
286
|
+
|
|
287
|
+
_agent_cache = results
|
|
288
|
+
return results
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def get_available_agents() -> list[str]:
|
|
292
|
+
"""Get list of available (installed + authenticated) agent IDs.
|
|
293
|
+
|
|
294
|
+
Returns agent IDs sorted alphabetically.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
List of available agent IDs
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
RuntimeError: If detect_all_agents() has not been called yet
|
|
301
|
+
"""
|
|
302
|
+
if _agent_cache is None:
|
|
303
|
+
raise RuntimeError(
|
|
304
|
+
"Call detect_all_agents() first before using get_available_agents()"
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
return sorted([
|
|
308
|
+
agent_id for agent_id, avail in _agent_cache.items()
|
|
309
|
+
if avail.is_available
|
|
310
|
+
])
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def get_core_agents_available() -> list[str]:
|
|
314
|
+
"""Get list of available core tier agents.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
List of available core agent IDs, sorted alphabetically
|
|
318
|
+
|
|
319
|
+
Raises:
|
|
320
|
+
RuntimeError: If detect_all_agents() has not been called yet
|
|
321
|
+
"""
|
|
322
|
+
if _agent_cache is None:
|
|
323
|
+
raise RuntimeError("Call detect_all_agents() first")
|
|
324
|
+
|
|
325
|
+
return sorted([
|
|
326
|
+
agent_id for agent_id, avail in _agent_cache.items()
|
|
327
|
+
if avail.is_available and agent_id in CORE_AGENTS
|
|
328
|
+
])
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def get_extended_agents_available() -> list[str]:
|
|
332
|
+
"""Get list of available extended tier agents.
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
List of available extended agent IDs, sorted alphabetically
|
|
336
|
+
|
|
337
|
+
Raises:
|
|
338
|
+
RuntimeError: If detect_all_agents() has not been called yet
|
|
339
|
+
"""
|
|
340
|
+
if _agent_cache is None:
|
|
341
|
+
raise RuntimeError("Call detect_all_agents() first")
|
|
342
|
+
|
|
343
|
+
return sorted([
|
|
344
|
+
agent_id for agent_id, avail in _agent_cache.items()
|
|
345
|
+
if avail.is_available and agent_id in EXTENDED_AGENTS
|
|
346
|
+
])
|