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