devcouncil 0.1.1 → 0.3.0

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 (159) hide show
  1. package/README.md +201 -6
  2. package/package.json +9 -2
  3. package/pyproject.toml +34 -2
  4. package/src/devcouncil/app/config.py +348 -12
  5. package/src/devcouncil/app/orchestrator.py +10 -6
  6. package/src/devcouncil/app/state_machine.py +4 -0
  7. package/src/devcouncil/artifacts/graph.py +32 -5
  8. package/src/devcouncil/assets/__init__.py +1 -0
  9. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  10. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  11. package/src/devcouncil/cli/commands/agents.py +292 -0
  12. package/src/devcouncil/cli/commands/artifacts.py +6 -3
  13. package/src/devcouncil/cli/commands/check.py +220 -0
  14. package/src/devcouncil/cli/commands/config.py +43 -4
  15. package/src/devcouncil/cli/commands/cost.py +57 -0
  16. package/src/devcouncil/cli/commands/dashboard.py +6 -1
  17. package/src/devcouncil/cli/commands/design.py +186 -0
  18. package/src/devcouncil/cli/commands/doctor.py +379 -22
  19. package/src/devcouncil/cli/commands/evidence.py +48 -0
  20. package/src/devcouncil/cli/commands/go.py +532 -33
  21. package/src/devcouncil/cli/commands/handoff.py +69 -0
  22. package/src/devcouncil/cli/commands/hook.py +296 -15
  23. package/src/devcouncil/cli/commands/init.py +161 -20
  24. package/src/devcouncil/cli/commands/integrate.py +1371 -124
  25. package/src/devcouncil/cli/commands/logs.py +106 -0
  26. package/src/devcouncil/cli/commands/map.py +80 -10
  27. package/src/devcouncil/cli/commands/okf.py +245 -0
  28. package/src/devcouncil/cli/commands/plan.py +256 -55
  29. package/src/devcouncil/cli/commands/prompt.py +18 -7
  30. package/src/devcouncil/cli/commands/repair.py +50 -24
  31. package/src/devcouncil/cli/commands/report.py +8 -0
  32. package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
  33. package/src/devcouncil/cli/commands/rollback.py +27 -28
  34. package/src/devcouncil/cli/commands/run.py +195 -54
  35. package/src/devcouncil/cli/commands/runs.py +223 -0
  36. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  37. package/src/devcouncil/cli/commands/semantic.py +47 -0
  38. package/src/devcouncil/cli/commands/setup.py +145 -6
  39. package/src/devcouncil/cli/commands/shell.py +73 -0
  40. package/src/devcouncil/cli/commands/skills.py +267 -0
  41. package/src/devcouncil/cli/commands/status.py +30 -15
  42. package/src/devcouncil/cli/commands/trace.py +47 -3
  43. package/src/devcouncil/cli/commands/verify.py +144 -3
  44. package/src/devcouncil/cli/commands/watch.py +32 -12
  45. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  46. package/src/devcouncil/cli/main.py +91 -7
  47. package/src/devcouncil/domain/evidence.py +29 -2
  48. package/src/devcouncil/domain/gap.py +27 -1
  49. package/src/devcouncil/domain/task.py +31 -2
  50. package/src/devcouncil/execution/checkpoints.py +256 -0
  51. package/src/devcouncil/execution/context_builder.py +1 -1
  52. package/src/devcouncil/execution/fs_watcher.py +205 -0
  53. package/src/devcouncil/execution/handoff.py +102 -0
  54. package/src/devcouncil/execution/hook_policy.py +162 -74
  55. package/src/devcouncil/execution/patch.py +65 -10
  56. package/src/devcouncil/execution/permissions.py +24 -24
  57. package/src/devcouncil/execution/policy_engine.py +350 -0
  58. package/src/devcouncil/execution/prompt_builder.py +751 -23
  59. package/src/devcouncil/execution/shell_session.py +231 -0
  60. package/src/devcouncil/execution/task_runner.py +24 -9
  61. package/src/devcouncil/executors/agent_registry.py +596 -0
  62. package/src/devcouncil/executors/coding_cli.py +791 -39
  63. package/src/devcouncil/executors/mini_swe.py +6 -0
  64. package/src/devcouncil/executors/native/agent.py +135 -19
  65. package/src/devcouncil/executors/openhands.py +6 -0
  66. package/src/devcouncil/gating/checks/clean_git.py +3 -1
  67. package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
  68. package/src/devcouncil/gating/policy.py +190 -11
  69. package/src/devcouncil/hardware.py +184 -0
  70. package/src/devcouncil/indexing/ast_matcher.py +17 -7
  71. package/src/devcouncil/indexing/lsp.py +45 -4
  72. package/src/devcouncil/indexing/repo_mapper.py +1284 -15
  73. package/src/devcouncil/indexing/semantic_index.py +221 -0
  74. package/src/devcouncil/integrations/actions.py +166 -0
  75. package/src/devcouncil/integrations/check.py +426 -0
  76. package/src/devcouncil/integrations/claude_assets.py +444 -0
  77. package/src/devcouncil/integrations/code_review_graph.py +13 -2
  78. package/src/devcouncil/integrations/github_intent.py +149 -0
  79. package/src/devcouncil/integrations/gitnexus.py +45 -2
  80. package/src/devcouncil/integrations/mcp/server.py +1944 -32
  81. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  82. package/src/devcouncil/integrations/pr_comments.py +9 -0
  83. package/src/devcouncil/knowledge/__init__.py +23 -0
  84. package/src/devcouncil/knowledge/design.py +374 -0
  85. package/src/devcouncil/knowledge/design_conformance.py +317 -0
  86. package/src/devcouncil/knowledge/fetch.py +223 -0
  87. package/src/devcouncil/knowledge/frontmatter.py +51 -0
  88. package/src/devcouncil/knowledge/okf.py +202 -0
  89. package/src/devcouncil/knowledge/skill_bridge.py +96 -0
  90. package/src/devcouncil/knowledge/sources.py +239 -0
  91. package/src/devcouncil/live/cards.py +181 -25
  92. package/src/devcouncil/live/repair_prompt.py +29 -6
  93. package/src/devcouncil/live/reviewer.py +72 -13
  94. package/src/devcouncil/live/signals.py +2 -2
  95. package/src/devcouncil/live/summary.py +18 -8
  96. package/src/devcouncil/live/transcripts.py +47 -11
  97. package/src/devcouncil/llm/cache.py +20 -8
  98. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  99. package/src/devcouncil/llm/provider.py +617 -49
  100. package/src/devcouncil/llm/router.py +337 -53
  101. package/src/devcouncil/optimization/__init__.py +1 -0
  102. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  103. package/src/devcouncil/optimization/skillopt.py +673 -0
  104. package/src/devcouncil/planning/arbiter_service.py +10 -2
  105. package/src/devcouncil/planning/correction_manifest.py +346 -0
  106. package/src/devcouncil/planning/critique_service.py +16 -4
  107. package/src/devcouncil/planning/plan_service.py +86 -6
  108. package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
  109. package/src/devcouncil/planning/repair_service.py +8 -2
  110. package/src/devcouncil/planning/spec_service.py +37 -3
  111. package/src/devcouncil/repo/ci_scaffold.py +165 -0
  112. package/src/devcouncil/repo/gitignore.py +123 -0
  113. package/src/devcouncil/repo/sca.py +384 -0
  114. package/src/devcouncil/reporting/json_report.py +22 -1
  115. package/src/devcouncil/reporting/markdown_report.py +29 -1
  116. package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
  117. package/src/devcouncil/reporting/okf_html.py +323 -0
  118. package/src/devcouncil/reporting/report_builder.py +18 -1
  119. package/src/devcouncil/skills/__init__.py +19 -0
  120. package/src/devcouncil/skills/library/README.md +46 -0
  121. package/src/devcouncil/skills/library/ai-training.md +50 -0
  122. package/src/devcouncil/skills/library/android.md +50 -0
  123. package/src/devcouncil/skills/library/backend.md +52 -0
  124. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  125. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  126. package/src/devcouncil/skills/library/desktop.md +46 -0
  127. package/src/devcouncil/skills/library/devops.md +48 -0
  128. package/src/devcouncil/skills/library/game-dev.md +46 -0
  129. package/src/devcouncil/skills/library/ios.md +48 -0
  130. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  131. package/src/devcouncil/skills/library/security.md +48 -0
  132. package/src/devcouncil/skills/library/systems.md +48 -0
  133. package/src/devcouncil/skills/library/web.md +47 -0
  134. package/src/devcouncil/skills/library/windows.md +47 -0
  135. package/src/devcouncil/skills/registry.py +408 -0
  136. package/src/devcouncil/storage/db.py +140 -3
  137. package/src/devcouncil/storage/models.py +125 -0
  138. package/src/devcouncil/storage/native.py +559 -0
  139. package/src/devcouncil/storage/repositories.py +157 -78
  140. package/src/devcouncil/telemetry/cost.py +123 -17
  141. package/src/devcouncil/telemetry/logging_setup.py +244 -0
  142. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  143. package/src/devcouncil/telemetry/pricing.py +28 -0
  144. package/src/devcouncil/telemetry/stages.py +141 -0
  145. package/src/devcouncil/telemetry/traces.py +62 -7
  146. package/src/devcouncil/telemetry/tracker.py +24 -10
  147. package/src/devcouncil/ui/dashboard.py +393 -28
  148. package/src/devcouncil/utils/redaction.py +9 -3
  149. package/src/devcouncil/utils/subprocess_env.py +69 -0
  150. package/src/devcouncil/verification/acceptance_compiler.py +253 -0
  151. package/src/devcouncil/verification/ad_hoc_check.py +135 -0
  152. package/src/devcouncil/verification/diff_coverage.py +353 -0
  153. package/src/devcouncil/verification/implementation_reviewer.py +11 -2
  154. package/src/devcouncil/verification/next_actions.py +189 -0
  155. package/src/devcouncil/verification/sandbox.py +181 -0
  156. package/src/devcouncil/verification/test_resolver.py +91 -0
  157. package/src/devcouncil/verification/verifier.py +1549 -143
  158. package/uv.lock +205 -64
  159. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -0,0 +1,596 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from devcouncil.app.config import CliAgentProfileConfig, load_config
9
+
10
+
11
+ AGENT_ALIASES = {
12
+ "agy": "antigravity",
13
+ "agy-cli": "antigravity",
14
+ "antigravity-cli": "antigravity",
15
+ "google-antigravity": "antigravity",
16
+ "codex-cli": "codex",
17
+ "gemini-cli": "gemini",
18
+ "claude-cli": "claude",
19
+ "claude-code": "claude",
20
+ "opencode-cli": "opencode",
21
+ "open-code": "opencode",
22
+ "warp-cli": "warp",
23
+ "oz": "warp",
24
+ "oz-cli": "warp",
25
+ "cursor-agent": "cursor",
26
+ "cursor-cli": "cursor",
27
+ "copilot-cli": "copilot",
28
+ "github-copilot": "copilot",
29
+ "gh-copilot": "copilot",
30
+ "goose-cli": "goose",
31
+ "block-goose": "goose",
32
+ "amp-cli": "amp",
33
+ "sourcegraph-amp": "amp",
34
+ "qwen-code": "qwen",
35
+ "qwen-cli": "qwen",
36
+ "crush-cli": "crush",
37
+ "charm-crush": "crush",
38
+ }
39
+
40
+ BUILTIN_AGENT_NAMES = {
41
+ "aider",
42
+ "amp",
43
+ "antigravity",
44
+ "claude",
45
+ "codex",
46
+ "copilot",
47
+ "crush",
48
+ "cursor",
49
+ "gemini",
50
+ "goose",
51
+ "opencode",
52
+ "qwen",
53
+ "warp",
54
+ }
55
+
56
+ BUILTIN_CODING_EXECUTOR_NAMES = tuple(sorted(BUILTIN_AGENT_NAMES))
57
+ VALID_INPUT_MODES = {"stdin", "argument", "prompt-file"}
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class CliAgentSpec:
62
+ name: str
63
+ command: str
64
+ args: list[str] = field(default_factory=list)
65
+ input_mode: str = "stdin"
66
+ prompt_arg: str | None = None
67
+ timeout_seconds: int | None = None
68
+ env: dict[str, str] = field(default_factory=dict)
69
+ display_name: str | None = None
70
+ kind: str = "custom"
71
+ supports_mcp: bool = False
72
+ supports_diff_review: bool = False
73
+ default_profile: str = "default"
74
+ help_command: list[str] = field(default_factory=list)
75
+ built_in: bool = False
76
+ capabilities: list[str] = field(default_factory=list)
77
+ supports_handoff: bool = False
78
+ preferred_artifact_format: str = "json"
79
+
80
+ @property
81
+ def label(self) -> str:
82
+ return self.display_name or self.name
83
+
84
+ @property
85
+ def executable(self) -> str:
86
+ return self.command
87
+
88
+ def base_command(self) -> list[str]:
89
+ return [self.command, *self.args]
90
+
91
+
92
+ def normalize_agent_name(name: str) -> str:
93
+ normalized = (name or "").strip().lower().replace("_", "-")
94
+ return AGENT_ALIASES.get(normalized, normalized)
95
+
96
+
97
+ def resolve_cursor_agent_executable() -> str | None:
98
+ # NOTE: intentionally NOT lru_cached. It takes no project_root key, so a
99
+ # process-wide cache would leak one test's PATH probe (which the suite
100
+ # monkeypatches shutil.which to control) into the next, breaking isolation.
101
+ # The PATH search is two cheap shutil.which() calls; the dominant repeated
102
+ # cost (load_cli_agent_specs / config parsing) is cached instead.
103
+ for candidate in ("cursor-agent", "agent"):
104
+ if shutil.which(candidate):
105
+ return candidate
106
+ return None
107
+
108
+
109
+ CODING_CLI_PROBE_ORDER: tuple[str, ...] = (
110
+ "codex",
111
+ "gemini",
112
+ "claude",
113
+ "cursor",
114
+ "opencode",
115
+ "antigravity",
116
+ "warp",
117
+ "aider",
118
+ "copilot",
119
+ "goose",
120
+ "amp",
121
+ "qwen",
122
+ "crush",
123
+ )
124
+
125
+ @dataclass(frozen=True)
126
+ class CodingCliIntegrationInfo:
127
+ name: str
128
+ label: str
129
+ tier: int
130
+ headless: bool
131
+ mcp: bool
132
+ hooks: bool
133
+ launcher_shim: bool
134
+ notes: str
135
+
136
+ @property
137
+ def enforcement(self) -> str:
138
+ """Honest containment posture: ``pre-action`` when a native hook blocks
139
+ unauthorized writes/commands before they happen, else ``verify-only`` — for
140
+ which "forbidden changes" are caught only post-hoc by verification, not blocked
141
+ up front. Surfaced so users aren't misled into assuming hard containment."""
142
+ return "pre-action" if self.hooks else "verify-only"
143
+
144
+
145
+ # Tier 1 = headless executor + verify; tier 2 = MCP companion; hooks = native pre-tool policy.
146
+ CODING_CLI_INTEGRATION_INFO: dict[str, CodingCliIntegrationInfo] = {
147
+ "codex": CodingCliIntegrationInfo(
148
+ "codex", "Codex CLI", 1, True, True, True, True, "dev integrate codex --apply"
149
+ ),
150
+ "gemini": CodingCliIntegrationInfo(
151
+ "gemini", "Gemini CLI", 1, True, True, True, True, "dev integrate gemini --apply"
152
+ ),
153
+ "claude": CodingCliIntegrationInfo(
154
+ "claude", "Claude Code", 1, True, True, True, True, "dev integrate claude --apply"
155
+ ),
156
+ "cursor": CodingCliIntegrationInfo(
157
+ "cursor",
158
+ "Cursor",
159
+ 1,
160
+ True,
161
+ True,
162
+ True,
163
+ True,
164
+ "dev integrate cursor --apply; cursor-agent for headless",
165
+ ),
166
+ "opencode": CodingCliIntegrationInfo(
167
+ "opencode", "OpenCode", 1, True, True, True, True, "dev integrate opencode --apply"
168
+ ),
169
+ "antigravity": CodingCliIntegrationInfo(
170
+ "antigravity",
171
+ "Google Antigravity CLI",
172
+ 1,
173
+ True,
174
+ True,
175
+ False,
176
+ True,
177
+ "dev integrate antigravity --apply",
178
+ ),
179
+ "warp": CodingCliIntegrationInfo(
180
+ "warp", "Warp / Oz", 1, True, True, False, True, "dev integrate warp --apply"
181
+ ),
182
+ "aider": CodingCliIntegrationInfo(
183
+ "aider", "Aider", 1, True, False, False, True, "dev integrate aider --apply"
184
+ ),
185
+ "copilot": CodingCliIntegrationInfo(
186
+ "copilot", "GitHub Copilot CLI", 1, True, True, False, True, "dev run TASK-ID --executor copilot"
187
+ ),
188
+ "goose": CodingCliIntegrationInfo(
189
+ "goose", "Goose", 1, True, True, False, True, "dev run TASK-ID --executor goose"
190
+ ),
191
+ "amp": CodingCliIntegrationInfo(
192
+ "amp", "Amp (Sourcegraph)", 1, True, True, False, True, "dev run TASK-ID --executor amp"
193
+ ),
194
+ "qwen": CodingCliIntegrationInfo(
195
+ "qwen", "Qwen Code", 1, True, True, False, True, "dev run TASK-ID --executor qwen"
196
+ ),
197
+ "crush": CodingCliIntegrationInfo(
198
+ "crush", "Crush (Charm)", 1, True, True, False, True, "dev run TASK-ID --executor crush"
199
+ ),
200
+ }
201
+
202
+ # First successful probe wins (cursor tries cursor-agent then cursor).
203
+ CODING_CLI_VERSION_COMMANDS: dict[str, tuple[tuple[str, ...], ...]] = {
204
+ "codex": (("codex", "--version"),),
205
+ "gemini": (("gemini", "--version"),),
206
+ "claude": (("claude", "--version"),),
207
+ "cursor": (("cursor-agent", "--version"), ("cursor", "--version")),
208
+ "opencode": (("opencode", "--version"),),
209
+ "antigravity": (("agy", "--version"),),
210
+ "warp": (("oz", "--version"),),
211
+ "aider": (("aider", "--version"),),
212
+ "copilot": (("copilot", "--version"),),
213
+ "goose": (("goose", "--version"),),
214
+ "amp": (("amp", "--version"),),
215
+ "qwen": (("qwen", "--version"),),
216
+ "crush": (("crush", "--version"),),
217
+ }
218
+
219
+
220
+ def integration_tier_label(client: str) -> str:
221
+ info = CODING_CLI_INTEGRATION_INFO.get(normalize_agent_name(client))
222
+ tier = info.tier if info is not None else 3
223
+ if tier == 1:
224
+ return "Tier 1 (headless executor)"
225
+ if tier == 2:
226
+ return "Tier 2 (MCP companion)"
227
+ return "Tier 3 (sidecar)"
228
+
229
+
230
+ def resolve_coding_cli_executable(project_root: Path, client: str) -> str | None:
231
+ normalized = normalize_agent_name(client)
232
+ if normalized == "cursor":
233
+ return resolve_cursor_agent_executable()
234
+ spec = get_cli_agent_spec(project_root, normalized)
235
+ if not spec:
236
+ return None
237
+ return spec.executable if shutil.which(spec.executable) else None
238
+
239
+
240
+ def resolve_coding_cli_probe_order(project_root: Path) -> tuple[str, ...]:
241
+ try:
242
+ configured = load_config(project_root).execution.coding_cli_probe_order
243
+ if configured:
244
+ return tuple(normalize_agent_name(name) for name in configured)
245
+ except Exception:
246
+ pass
247
+ return CODING_CLI_PROBE_ORDER
248
+
249
+
250
+ def detect_available_coding_cli(
251
+ project_root: Path,
252
+ probe_order: tuple[str, ...] | None = None,
253
+ ) -> str | None:
254
+ # Load specs once for the whole probe rather than per client (each
255
+ # resolve_coding_cli_executable() call would otherwise rebuild the spec
256
+ # table and re-parse config.yaml). Resolution below mirrors
257
+ # resolve_coding_cli_executable() exactly, just against the prebuilt table.
258
+ specs = load_cli_agent_specs(project_root)
259
+ for client in probe_order or resolve_coding_cli_probe_order(project_root):
260
+ normalized = normalize_agent_name(client)
261
+ if normalized == "cursor":
262
+ if resolve_cursor_agent_executable():
263
+ return client
264
+ continue
265
+ spec = specs.get(normalized)
266
+ if spec and shutil.which(spec.executable):
267
+ return client
268
+ return None
269
+
270
+
271
+ def resolve_automated_executor(
272
+ project_root: Path,
273
+ explicit: str | None = None,
274
+ *,
275
+ probe_order: tuple[str, ...] | None = None,
276
+ ) -> str:
277
+ if explicit:
278
+ return normalize_agent_name(explicit)
279
+ try:
280
+ config = load_config(project_root).execution
281
+ configured = normalize_agent_name(config.default_executor)
282
+ if probe_order is None:
283
+ probe_order = resolve_coding_cli_probe_order(project_root)
284
+ except Exception:
285
+ configured = "manual"
286
+ if probe_order is None:
287
+ probe_order = CODING_CLI_PROBE_ORDER
288
+ if configured != "manual":
289
+ return configured
290
+ detected = detect_available_coding_cli(project_root, probe_order=probe_order)
291
+ return detected or configured
292
+
293
+
294
+ def is_reserved_agent_name(name: str) -> bool:
295
+ return normalize_agent_name(name) in BUILTIN_AGENT_NAMES
296
+
297
+
298
+ def builtin_agent_specs(project_root: Path) -> dict[str, CliAgentSpec]:
299
+ warp_mcp_path = project_root / ".devcouncil" / "integrations" / "warp-mcp.json"
300
+ return {
301
+ "codex": CliAgentSpec(
302
+ name="codex",
303
+ command="codex",
304
+ args=["exec", "-"],
305
+ display_name="Codex CLI",
306
+ kind="coding-cli",
307
+ supports_mcp=True,
308
+ supports_diff_review=True,
309
+ supports_handoff=True,
310
+ built_in=True,
311
+ ),
312
+ "gemini": CliAgentSpec(
313
+ name="gemini",
314
+ command="gemini",
315
+ display_name="Gemini CLI",
316
+ kind="coding-cli",
317
+ supports_mcp=True,
318
+ built_in=True,
319
+ ),
320
+ "claude": CliAgentSpec(
321
+ name="claude",
322
+ # `-p` runs headless; `--permission-mode acceptEdits` lets Claude Code
323
+ # actually apply file edits without an interactive approval prompt
324
+ # (otherwise it only describes the change and the diff stays empty).
325
+ command="claude",
326
+ args=["-p", "--permission-mode", "acceptEdits"],
327
+ display_name="Claude Code",
328
+ kind="coding-cli",
329
+ supports_mcp=True,
330
+ supports_diff_review=True,
331
+ supports_handoff=True,
332
+ built_in=True,
333
+ ),
334
+ "opencode": CliAgentSpec(
335
+ name="opencode",
336
+ command="opencode",
337
+ args=[
338
+ "run",
339
+ "--file",
340
+ "{prompt_file}",
341
+ "Execute the DevCouncil task described in the attached prompt file.",
342
+ ],
343
+ input_mode="prompt-file",
344
+ display_name="OpenCode",
345
+ kind="coding-cli",
346
+ supports_mcp=True,
347
+ supports_diff_review=True,
348
+ supports_handoff=True,
349
+ built_in=True,
350
+ ),
351
+ "antigravity": CliAgentSpec(
352
+ name="antigravity",
353
+ command="agy",
354
+ args=[
355
+ "--print",
356
+ "--print-timeout",
357
+ "30m",
358
+ "Read and execute the DevCouncil task prompt at {prompt_file}.",
359
+ ],
360
+ input_mode="prompt-file",
361
+ display_name="Google Antigravity CLI",
362
+ kind="coding-cli",
363
+ supports_mcp=True,
364
+ supports_diff_review=True,
365
+ supports_handoff=True,
366
+ built_in=True,
367
+ ),
368
+ "warp": CliAgentSpec(
369
+ name="warp",
370
+ command="oz",
371
+ args=["agent", "run", "--cwd", str(project_root), "--mcp", str(warp_mcp_path), "--prompt"],
372
+ input_mode="argument",
373
+ display_name="Warp / Oz",
374
+ kind="agent-platform",
375
+ supports_mcp=True,
376
+ supports_diff_review=True,
377
+ supports_handoff=True,
378
+ built_in=True,
379
+ ),
380
+ "cursor": CliAgentSpec(
381
+ name="cursor",
382
+ command=resolve_cursor_agent_executable() or "cursor-agent",
383
+ args=[
384
+ "--print",
385
+ "--trust",
386
+ "--workspace",
387
+ str(project_root),
388
+ "Read and execute the DevCouncil task prompt at {prompt_file}.",
389
+ ],
390
+ input_mode="prompt-file",
391
+ display_name="Cursor Agent",
392
+ kind="coding-cli",
393
+ supports_mcp=True,
394
+ supports_diff_review=True,
395
+ supports_handoff=True,
396
+ built_in=True,
397
+ ),
398
+ "aider": CliAgentSpec(
399
+ name="aider",
400
+ command="aider",
401
+ args=["--yes", "--no-show-model-warnings", "--message"],
402
+ input_mode="argument",
403
+ display_name="Aider",
404
+ kind="coding-cli",
405
+ supports_mcp=False,
406
+ supports_diff_review=True,
407
+ supports_handoff=True,
408
+ built_in=True,
409
+ ),
410
+ "copilot": CliAgentSpec(
411
+ name="copilot",
412
+ command="copilot",
413
+ args=["--allow-all-tools", "-p"],
414
+ input_mode="argument",
415
+ display_name="GitHub Copilot CLI",
416
+ kind="coding-cli",
417
+ supports_mcp=True,
418
+ supports_diff_review=True,
419
+ supports_handoff=True,
420
+ built_in=True,
421
+ ),
422
+ "goose": CliAgentSpec(
423
+ name="goose",
424
+ command="goose",
425
+ args=["run", "-i", "{prompt_file}"],
426
+ input_mode="prompt-file",
427
+ display_name="Goose",
428
+ kind="coding-cli",
429
+ supports_mcp=True,
430
+ supports_diff_review=True,
431
+ supports_handoff=True,
432
+ built_in=True,
433
+ ),
434
+ "amp": CliAgentSpec(
435
+ name="amp",
436
+ command="amp",
437
+ args=["-x"],
438
+ input_mode="argument",
439
+ display_name="Amp (Sourcegraph)",
440
+ kind="coding-cli",
441
+ supports_mcp=True,
442
+ supports_diff_review=True,
443
+ supports_handoff=True,
444
+ built_in=True,
445
+ ),
446
+ "qwen": CliAgentSpec(
447
+ name="qwen",
448
+ command="qwen",
449
+ display_name="Qwen Code",
450
+ kind="coding-cli",
451
+ supports_mcp=True,
452
+ supports_diff_review=True,
453
+ supports_handoff=True,
454
+ built_in=True,
455
+ ),
456
+ "crush": CliAgentSpec(
457
+ name="crush",
458
+ command="crush",
459
+ args=["run"],
460
+ input_mode="argument",
461
+ display_name="Crush (Charm)",
462
+ kind="coding-cli",
463
+ supports_mcp=True,
464
+ supports_diff_review=True,
465
+ supports_handoff=True,
466
+ built_in=True,
467
+ ),
468
+ }
469
+
470
+
471
+ def load_cli_agent_specs(project_root: Path) -> dict[str, CliAgentSpec]:
472
+ # NOTE: intentionally NOT cached. Callers (e.g. `setup` then `integrate
473
+ # doctor`) rewrite config.yaml for the same project_root within a single
474
+ # process and expect the next load to reflect it, so this must stay a live
475
+ # read. The repeated-reload hot path (the CLI probe loop) instead hoists a
476
+ # single load — see detect_available_coding_cli.
477
+ specs = builtin_agent_specs(project_root)
478
+ try:
479
+ configured = load_config(project_root).integrations.cli_agents.agents
480
+ except Exception:
481
+ configured = {}
482
+
483
+ for raw_name, agent in configured.items():
484
+ name = normalize_agent_name(raw_name)
485
+ if name in BUILTIN_AGENT_NAMES:
486
+ continue
487
+ data = agent.model_dump()
488
+ specs[name] = CliAgentSpec(
489
+ name=name,
490
+ command=data["command"],
491
+ args=list(data.get("args") or []),
492
+ input_mode=data.get("input_mode") or "stdin",
493
+ prompt_arg=data.get("prompt_arg"),
494
+ timeout_seconds=data.get("timeout_seconds"),
495
+ env=dict(data.get("env") or {}),
496
+ display_name=data.get("display_name"),
497
+ kind=data.get("kind") or "custom",
498
+ supports_mcp=bool(data.get("supports_mcp")),
499
+ supports_diff_review=bool(data.get("supports_diff_review")),
500
+ default_profile=data.get("default_profile") or "default",
501
+ help_command=list(data.get("help_command") or []),
502
+ built_in=False,
503
+ )
504
+ return specs
505
+
506
+
507
+ def get_cli_agent_spec(project_root: Path, name: str) -> CliAgentSpec | None:
508
+ return load_cli_agent_specs(project_root).get(normalize_agent_name(name))
509
+
510
+
511
+ def default_agent_profiles() -> dict[str, dict[str, Any]]:
512
+ return {
513
+ "default": {
514
+ "description": "Balanced local execution with DevCouncil verification.",
515
+ "timeout_seconds": None,
516
+ "prompt_preamble": "",
517
+ "require_explicit_confirmation": False,
518
+ },
519
+ "yolo": {
520
+ "description": "Faster local execution; DevCouncil still verifies the final diff.",
521
+ "timeout_seconds": 3600,
522
+ "prompt_preamble": (
523
+ "Profile: yolo. Move efficiently within the task scope, but keep all changes "
524
+ "inside the planned files and run the expected verification commands."
525
+ ),
526
+ "require_explicit_confirmation": False,
527
+ # yolo leans on the CLI's most permissive auto-apply mode so the agent
528
+ # never stalls on an approval prompt.
529
+ "permission_mode": "auto",
530
+ },
531
+ "prod": {
532
+ "description": "Restrictive execution for high-risk repositories.",
533
+ "timeout_seconds": 1800,
534
+ "prompt_preamble": (
535
+ "Profile: prod. Treat this as high-risk work. Do not broaden scope, do not "
536
+ "change dependencies or schemas unless the task explicitly allows it, and "
537
+ "prefer minimal, easily reviewed edits."
538
+ ),
539
+ "require_explicit_confirmation": True,
540
+ # prod drops blanket auto-approval: edits are gated rather than blindly
541
+ # applied, so the subprocess invocation is materially more contained than
542
+ # yolo's instead of being byte-for-byte identical.
543
+ "permission_mode": "gated",
544
+ },
545
+ }
546
+
547
+
548
+ def load_agent_profiles(project_root: Path) -> dict[str, CliAgentProfileConfig]:
549
+ raw_profiles = default_agent_profiles()
550
+ try:
551
+ configured = load_config(project_root).integrations.cli_agents.profiles
552
+ except Exception:
553
+ configured = {}
554
+ profiles = {
555
+ name: CliAgentProfileConfig.model_validate(data)
556
+ for name, data in raw_profiles.items()
557
+ }
558
+ profiles.update(configured)
559
+ return profiles
560
+
561
+
562
+ def agent_config_entry(
563
+ *,
564
+ command: str,
565
+ args: list[str] | None = None,
566
+ input_mode: str = "stdin",
567
+ prompt_arg: str | None = None,
568
+ timeout_seconds: int | None = None,
569
+ env: dict[str, str] | None = None,
570
+ display_name: str | None = None,
571
+ kind: str = "custom",
572
+ supports_mcp: bool = False,
573
+ supports_diff_review: bool = False,
574
+ default_profile: str = "default",
575
+ help_command: list[str] | None = None,
576
+ ) -> dict[str, Any]:
577
+ entry: dict[str, Any] = {
578
+ "command": command,
579
+ "args": args or [],
580
+ "input_mode": input_mode,
581
+ "kind": kind,
582
+ "supports_mcp": supports_mcp,
583
+ "supports_diff_review": supports_diff_review,
584
+ "default_profile": default_profile,
585
+ }
586
+ if prompt_arg:
587
+ entry["prompt_arg"] = prompt_arg
588
+ if timeout_seconds is not None:
589
+ entry["timeout_seconds"] = timeout_seconds
590
+ if env:
591
+ entry["env"] = env
592
+ if display_name:
593
+ entry["display_name"] = display_name
594
+ if help_command:
595
+ entry["help_command"] = help_command
596
+ return entry