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
@@ -12,14 +12,16 @@ console = Console()
12
12
 
13
13
  def reset_demo_state(
14
14
  yes: bool = typer.Option(False, "--yes", help="Confirm clearing planning/demo artifacts."),
15
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
15
16
  ):
16
17
  """Clear demo planning artifacts from the local DevCouncil state database."""
17
18
  if not yes:
18
19
  console.print("[red]Refusing to clear state without --yes.[/red]")
19
20
  raise typer.Exit(code=1)
20
21
 
21
- initialize_project(Path("."), quiet=True)
22
- db = get_db()
22
+ root = project_root.expanduser().resolve()
23
+ initialize_project(root, quiet=True)
24
+ db = get_db(root)
23
25
  if not db:
24
26
  console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
25
27
  raise typer.Exit(code=1)
@@ -1,8 +1,9 @@
1
1
  import typer
2
- import subprocess
3
2
  from pathlib import Path
4
3
  from rich.console import Console
5
4
 
5
+ from devcouncil.execution.checkpoints import CheckpointService
6
+
6
7
  app = typer.Typer()
7
8
  console = Console()
8
9
 
@@ -19,42 +20,40 @@ def rollback(
19
20
  return
20
21
 
21
22
  root = project_root.expanduser().resolve()
22
- checkpoint_file = root / ".devcouncil" / "checkpoints" / f"{task_id}-before.patch"
23
- after_patch = root / ".devcouncil" / "checkpoints" / f"{task_id}-after.patch"
23
+ checkpoint_dir = root / ".devcouncil" / "checkpoints"
24
+ checkpoint_file = checkpoint_dir / f"{task_id}-before.patch"
25
+ after_patch = checkpoint_dir / f"{task_id}-after.patch"
26
+ service = CheckpointService(root)
24
27
 
25
28
  if not checkpoint_file.exists() and not after_patch.exists():
26
- console.print(
27
- f"[red]No checkpoint found for task {task_id}. Expected {after_patch} "
28
- f"or {checkpoint_file}.[/red]"
29
- )
30
- raise typer.Exit(code=1)
29
+ before_ref = CheckpointService.REF_BEFORE.format(task_id=task_id)
30
+ after_ref = CheckpointService.REF_AFTER.format(task_id=task_id)
31
+ if not service._ref_exists(before_ref) and not service._ref_exists(after_ref):
32
+ console.print(
33
+ f"[red]No checkpoint found for task {task_id}. Expected {after_patch} "
34
+ f"or {checkpoint_file}.[/red]"
35
+ )
36
+ raise typer.Exit(code=1)
31
37
 
32
38
  console.print(f"Rolling back task [bold]{task_id}[/bold]...")
33
-
34
- try:
35
- if after_patch.exists():
36
- # Reverse-apply the task's changes only
37
- console.print(f"Applying reverse patch from [bold]{after_patch}[/bold]...")
38
- subprocess.check_call(
39
- ["git", "apply", "-R", str(after_patch)],
40
- cwd=root,
41
- )
42
- console.print(f"[green]Successfully rolled back task {task_id} changes.[/green]")
43
- else:
44
- # No after-patch, but we have the before-patch — warn and offer manual reset
39
+ result = service.rollback(task_id)
40
+ if "failed" in result.message.lower() or result.message.startswith("No checkpoint"):
41
+ console.print(f"[yellow]{result.message}[/yellow]")
42
+ if checkpoint_file.exists():
45
43
  console.print(
46
- f"[yellow]No after-patch found at {after_patch}.[/yellow]\n"
47
44
  f"The before-patch at {checkpoint_file} captured the state before the task ran.\n"
48
45
  f"To manually reset:\n"
49
46
  f" 1. [bold]git stash[/bold] (if you want to keep current changes)\n"
50
47
  f" 2. [bold]git checkout -- .[/bold] (discard working tree changes)\n"
51
48
  f" 3. [bold]git apply {checkpoint_file}[/bold] (restore pre-task state)"
52
49
  )
53
- except subprocess.CalledProcessError as e:
54
- console.print(f"[red]Failed to apply reverse patch: {e}[/red]")
55
- console.print("[yellow]The patch may conflict with current changes. Try resolving manually:[/yellow]")
56
- console.print(f" git apply -R --3way {after_patch}")
57
- raise typer.Exit(code=1)
58
- except Exception as e:
59
- console.print(f"[red]Failed to rollback: {e}[/red]")
50
+ elif after_patch.exists():
51
+ console.print(
52
+ f"Only the after-patch at {after_patch} exists (it captured the task's changes).\n"
53
+ f"To manually revert those changes from the working tree:\n"
54
+ f" 1. [bold]git apply --stat {after_patch}[/bold] (inspect what the task changed)\n"
55
+ f" 2. [bold]git apply -R {after_patch}[/bold] (reverse-apply the task's changes)"
56
+ )
60
57
  raise typer.Exit(code=1)
58
+
59
+ console.print(f"[green]Successfully rolled back task {task_id}.[/green] {result.message}")
@@ -1,5 +1,5 @@
1
+ import logging
1
2
  import typer
2
- import json
3
3
  from rich.console import Console
4
4
  from pathlib import Path
5
5
  from devcouncil.storage.db import get_db
@@ -7,6 +7,11 @@ from devcouncil.storage.repositories import TaskRepository, RequirementRepositor
7
7
  from devcouncil.executors.mini_swe import MiniSWEExecutor
8
8
  from devcouncil.executors.openhands import OpenHandsExecutor
9
9
  from devcouncil.executors.coding_cli import CodingCliExecutor
10
+ from devcouncil.executors.agent_registry import (
11
+ AGENT_ALIASES,
12
+ BUILTIN_CODING_EXECUTOR_NAMES,
13
+ load_cli_agent_specs,
14
+ )
10
15
  from devcouncil.executors.native.agent import NativeAgent
11
16
  from devcouncil.llm.provider import create_provider, validate_model_provider
12
17
  from devcouncil.llm.router import ModelRouter
@@ -16,20 +21,19 @@ from devcouncil.storage.repositories import GapRepository, EvidenceRepository, S
16
21
  from devcouncil.verification.verifier import Verifier
17
22
  from devcouncil.app.state_machine import ProjectPhase
18
23
  from devcouncil.cli.commands.init import initialize_project
24
+ from devcouncil.telemetry.traces import TraceLogger
19
25
 
20
26
  console = Console()
21
- CODING_EXECUTOR_ALIASES = {
22
- "codex": "codex",
23
- "codex-cli": "codex",
24
- "gemini": "gemini",
25
- "gemini-cli": "gemini",
26
- "claude": "claude",
27
- "claude-code": "claude",
28
- "claude-cli": "claude",
29
- }
27
+ logger = logging.getLogger(__name__)
28
+ CODING_EXECUTOR_ALIASES = {name: name for name in BUILTIN_CODING_EXECUTOR_NAMES} | AGENT_ALIASES
30
29
 
31
30
  CODING_EXECUTORS = set(CODING_EXECUTOR_ALIASES.keys())
32
31
 
32
+
33
+ def _custom_cli_agents(project_root: Path) -> set[str]:
34
+ specs = load_cli_agent_specs(project_root)
35
+ return {name for name, spec in specs.items() if not spec.built_in}
36
+
33
37
  def _current_changed_files(project_root: Path = Path(".")) -> list[str]:
34
38
  from devcouncil.verification.verifier import Verifier
35
39
 
@@ -38,28 +42,16 @@ def _current_changed_files(project_root: Path = Path(".")) -> list[str]:
38
42
  def _capture_after_patch(task_id: str, project_root: Path = Path(".")):
39
43
  """Capture the diff after task execution for use by rollback."""
40
44
  try:
41
- from devcouncil.verification.verifier import Verifier
42
-
43
- checkpoint_dir = project_root / ".devcouncil" / "checkpoints"
44
- checkpoint_dir.mkdir(exist_ok=True)
45
- diff = Verifier(project_root).get_diff()
46
- if diff:
47
- with open(checkpoint_dir / f"{task_id}-after.patch", "w", encoding="utf-8") as f:
48
- f.write(diff)
45
+ from devcouncil.execution.checkpoints import CheckpointService
46
+
47
+ CheckpointService(project_root).create_after(task_id)
49
48
  except Exception:
50
49
  pass # Non-critical — don't block execution
51
50
 
52
51
  def _capture_before_snapshot(task_id: str, project_root: Path = Path(".")):
53
- checkpoint_dir = project_root / ".devcouncil" / "checkpoints"
54
- checkpoint_dir.mkdir(exist_ok=True)
55
- snapshot = {
56
- "task_id": task_id,
57
- "changed_files": _current_changed_files(project_root),
58
- }
59
- (checkpoint_dir / f"{task_id}-before.json").write_text(
60
- json.dumps(snapshot, indent=2),
61
- encoding="utf-8",
62
- )
52
+ from devcouncil.execution.checkpoints import CheckpointService
53
+
54
+ CheckpointService(project_root).create_before(task_id)
63
55
 
64
56
  def _record_project_phase(session, phase: ProjectPhase):
65
57
  StateRepository(session).record_phase(phase.value)
@@ -68,8 +60,14 @@ def _verify_after_execution(session, task, reqs, router=None, project_root: Path
68
60
  """Run deterministic verification after an automated executor finishes."""
69
61
  import asyncio
70
62
 
63
+ logger.info("Verifying task %s (router=%s)", task.id, "yes" if router else "no")
71
64
  verifier = Verifier(project_root, router=router)
72
65
  gaps, evidence = asyncio.run(verifier.verify_task(task, reqs))
66
+ blocking = [g for g in gaps if g.blocking]
67
+ logger.info(
68
+ "Verification of %s: %d gap(s) (%d blocking), %d evidence item(s)",
69
+ task.id, len(gaps), len(blocking), len(evidence),
70
+ )
73
71
 
74
72
  gap_repo = GapRepository(session)
75
73
  evidence_repo = EvidenceRepository(session)
@@ -90,13 +88,116 @@ def _verify_after_execution(session, task, reqs, router=None, project_root: Path
90
88
  task.status = "blocked" if any(g.blocking for g in gaps) else "verified"
91
89
  return task.status == "verified"
92
90
 
91
+
92
+ def _build_verification_router(project_root: Path):
93
+ """Best-effort ``ModelRouter`` for LLM-backed verification after a coding-agent run.
94
+
95
+ Without a router the ``Verifier`` runs deterministic checks only (no
96
+ ``implementation_reviewer`` review, no acceptance-criterion compilation). The native
97
+ executor already builds a router to *run* the agent and reuses it for verification;
98
+ CLI coding agents (claude, codex, …) don't need one to execute, so they previously
99
+ verified without the LLM review at all. Build one here so the review gate guides and
100
+ monitors execution for those agents too. Per-role provider config means these review
101
+ roles can run on a different provider than planning (e.g. local Ollama).
102
+
103
+ Returns ``None`` when no provider/API key is configured so verification degrades to
104
+ deterministic-only instead of erroring — the LLM review is an enhancement, not a
105
+ hard requirement of running a task.
106
+ """
107
+ try:
108
+ config = load_config(project_root)
109
+ validate_model_provider(config.models.provider)
110
+ api_key = get_api_key(config.models.provider, project_root)
111
+ provider = create_provider(
112
+ config.models.provider, api_key, project_root=project_root, provider_prefs=config.provider
113
+ )
114
+ role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
115
+ return ModelRouter(provider, role_config, project_root=project_root)
116
+ except Exception:
117
+ return None
118
+
119
+
120
+ def _run_live_review_after_execution(project_root: Path, client: str, task_id: str | None) -> None:
121
+ """Critique the coding agent's latest turn with the ``live_reviewer`` role.
122
+
123
+ This is what makes live review actually fire during ``dev e2e``/``dev run`` (previously
124
+ it only ran via ``dev watch``). It produces an advisory critique card — it does NOT
125
+ gate the task; the deterministic/LLM verifier already does that. The card feeds the
126
+ final report's live-review summary and is routed by per-role config (e.g. a local
127
+ Ollama ``live_reviewer`` while planning runs on OpenRouter).
128
+
129
+ The transcript is resolved the same way ``dev watch`` does — the client's NATIVE
130
+ session log (e.g. claude's projects JSONL), discovered for this project root — not the
131
+ executor's streamed ``transcript.txt`` (which only exists with --stream and isn't the
132
+ structured turn format ``latest_assistant_turn`` parses).
133
+
134
+ Best-effort and opt-outable: skipped when ``integrations.live_review.enabled`` is
135
+ false, when no transcript is found, or on any error — a live-review hiccup must never
136
+ fail the run.
137
+ """
138
+ import asyncio
139
+
140
+ try:
141
+ if not load_config(project_root).integrations.live_review.enabled:
142
+ return
143
+ from devcouncil.cli.commands.watch import (
144
+ _resolve_transcript,
145
+ _review_turn,
146
+ _save_card_once,
147
+ _log_card_reviewed,
148
+ )
149
+ from devcouncil.live.transcripts import latest_assistant_turn
150
+
151
+ transcript = _resolve_transcript(project_root, client, latest=True)
152
+ if transcript is None or not transcript.exists():
153
+ return
154
+ turn = latest_assistant_turn(transcript, client=client)
155
+ if turn is None:
156
+ return
157
+ card = asyncio.run(_review_turn(turn, project_root, client, use_llm=True, task_id=task_id))
158
+ saved_path, duplicate = _save_card_once(project_root, card, persist=True, force=False)
159
+ if saved_path:
160
+ _log_card_reviewed(project_root, card, saved_path, duplicate=duplicate, source="e2e")
161
+ console.print(f"[dim]Live review ({card.verdict}): {saved_path}[/dim]")
162
+ except Exception as exc: # noqa: BLE001 - advisory; never fail the run on a review hiccup
163
+ console.print(f"[yellow]Live review skipped: {exc}[/yellow]")
164
+
165
+
166
+ def _log_exec_outcome(executor: str, task_id: str, *, verified: bool) -> None:
167
+ """Log a non-coding-CLI executor's post-verification outcome (verified vs blocked),
168
+ so standalone ``dev run`` has the same flow-decision trail as ``dev go``."""
169
+ if verified:
170
+ logger.info("%s finished and %s verified", executor, task_id)
171
+ else:
172
+ logger.warning("%s finished but %s blocked by verification gaps", executor, task_id)
173
+
174
+
175
+ def _record_agent_verification(project_root: Path, task_id: str, executor: str, run_id: str | None, verified: bool) -> None:
176
+ TraceLogger(project_root).log_event(
177
+ "agent_run_verified",
178
+ {"agent": executor, "verified": verified},
179
+ run_id=run_id,
180
+ task_id=task_id,
181
+ summary=f"{executor} verification {'passed' if verified else 'blocked'} for {task_id}",
182
+ )
183
+
93
184
  def run(
94
185
  task_id: str = typer.Argument(..., help="ID of the task to run"),
95
186
  executor: str = typer.Option(
96
187
  "manual",
97
188
  "--executor",
98
189
  "-e",
99
- help="Executor to use (manual, mini, openhands, native, codex, codex-cli, gemini, gemini-cli, claude, claude-code, claude-cli)",
190
+ help=(
191
+ "Executor to use (manual, mini, openhands, native-preview, "
192
+ "codex, gemini, claude, opencode, antigravity, warp, cursor, aider, "
193
+ "copilot, goose, amp, qwen, crush, or a configured agent)"
194
+ ),
195
+ ),
196
+ profile: str | None = typer.Option(None, "--profile", help="CLI-agent execution profile: default, yolo, prod, or a configured profile."),
197
+ stream: bool = typer.Option(
198
+ False,
199
+ "--stream",
200
+ help="Stream coding CLI stdout/stderr live (also enabled by execution.stream_cli_output).",
100
201
  ),
101
202
  project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
102
203
  ):
@@ -104,6 +205,9 @@ def run(
104
205
  Execute a specific task.
105
206
  """
106
207
  root = project_root.expanduser().resolve()
208
+ from devcouncil.telemetry.logging_setup import set_log_dir
209
+ set_log_dir(root)
210
+ logger.info("dev run: task=%s executor=%s profile=%s stream=%s", task_id, executor, profile, stream)
107
211
  initialize_project(root, quiet=True)
108
212
  db = get_db(root)
109
213
  if not db:
@@ -118,9 +222,13 @@ def run(
118
222
  return
119
223
 
120
224
  from devcouncil.gating.policy import GatePolicy
121
- policy = GatePolicy()
122
- gate_result = policy.check_task_ready(task, root)
225
+ gate_policy = GatePolicy()
226
+ gate_result = gate_policy.check_task_ready(task, root)
123
227
  if not gate_result.passed:
228
+ logger.warning(
229
+ "Task %s failed readiness gate: %s",
230
+ task_id, "; ".join(g.description for g in gate_result.gaps if g.blocking),
231
+ )
124
232
  console.print(f"[red]Task {task_id} is not ready for execution.[/red]")
125
233
  for gap in gate_result.gaps:
126
234
  if gap.blocking:
@@ -131,48 +239,70 @@ def run(
131
239
 
132
240
  # 1. Create Git checkpoint
133
241
  try:
134
- from devcouncil.verification.verifier import Verifier
135
-
136
- checkpoint_dir = root / ".devcouncil" / "checkpoints"
137
- checkpoint_dir.mkdir(exist_ok=True)
138
- _capture_before_snapshot(task_id, root)
139
- diff = Verifier(root).get_diff()
140
- if diff:
141
- with open(checkpoint_dir / f"{task_id}-before.patch", "w", encoding="utf-8") as f:
142
- f.write(diff)
143
- console.print(f"Created git checkpoint at {checkpoint_dir}/{task_id}-before.patch")
242
+ from devcouncil.execution.checkpoints import CheckpointService
243
+
244
+ result = CheckpointService(root).create_before(task_id)
245
+ if result.patch_path:
246
+ console.print(f"Created git checkpoint at {result.patch_path}")
247
+ elif result.git_ref_created:
248
+ console.print(f"Created git checkpoint ref {result.ref}")
144
249
  except Exception as e:
145
250
  console.print(f"[yellow]Warning: Failed to create git checkpoint: {e}[/yellow]")
146
251
 
147
252
  executor = executor.strip().lower().replace("_", "-")
253
+ custom_agents = _custom_cli_agents(root)
254
+ if executor not in CODING_EXECUTORS and executor not in custom_agents:
255
+ ignored = [flag for flag, value in (("--profile", profile), ("--stream", stream)) if value]
256
+ if ignored:
257
+ console.print(
258
+ f"[yellow]{' and '.join(ignored)} only apply to coding CLI executors and are ignored for '{executor}'.[/yellow]"
259
+ )
148
260
  if executor == "manual":
149
261
  _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
150
262
  task.status = "running"
151
263
  task_repo.save(task)
264
+ logger.info("%s marked RUNNING for manual sidecar execution", task_id)
152
265
  console.print(f"\n[green]Task {task_id} is now marked as RUNNING.[/green]")
153
266
  console.print("Use 'dev prompt TASK-ID' to get the prompt for this task.")
154
267
  console.print("When finished, use 'dev verify TASK-ID' to check the results.")
155
- elif executor in CODING_EXECUTORS:
268
+ elif executor in CODING_EXECUTORS or executor in custom_agents:
156
269
  _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
157
270
  req_repo = RequirementRepository(session)
158
271
  reqs = req_repo.get_all()
159
- cli_client = CODING_EXECUTOR_ALIASES[executor]
160
- cli_executor = CodingCliExecutor(root, cli_client)
272
+ cli_client = CODING_EXECUTOR_ALIASES.get(executor, executor)
273
+ cli_executor = CodingCliExecutor(root, cli_client, profile=profile, stream_output=stream or None)
161
274
  exec_result = cli_executor.run_task(task, reqs)
162
275
  _capture_after_patch(task_id, root)
163
276
  if exec_result.success:
164
277
  _record_project_phase(session, ProjectPhase.TASK_VERIFYING)
165
- verified = _verify_after_execution(session, task, reqs, project_root=root)
278
+ verified = _verify_after_execution(
279
+ session, task, reqs, router=_build_verification_router(root), project_root=root
280
+ )
281
+ _record_agent_verification(root, task.id, cli_client, getattr(cli_executor, "last_run_id", None), verified)
166
282
  _record_project_phase(
167
283
  session,
168
284
  ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
169
285
  )
170
286
  task_repo.save(task)
287
+ run_id = getattr(cli_executor, "last_run_id", None)
288
+ transcript_path = getattr(cli_executor, "last_transcript_path", None)
289
+ if run_id:
290
+ run_dir = root / ".devcouncil" / "runs" / run_id
291
+ console.print(f"Run artifacts: [dim]{run_dir}[/dim]")
292
+ if (run_dir / "run.log").exists():
293
+ console.print(f"Run log: [dim]dev logs tail --run {run_id}[/dim]")
294
+ if transcript_path:
295
+ console.print(f"Transcript: [dim]{transcript_path}[/dim]")
296
+ # Live review (advisory): critique the agent's turn with the live_reviewer
297
+ # role so monitoring actually happens during execution, not only via watch.
298
+ _run_live_review_after_execution(root, cli_client, task.id)
299
+ _log_exec_outcome(executor, task_id, verified=verified)
171
300
  if verified:
172
301
  console.print(f"\n[green]{executor.upper()} finished and task {task_id} verified.[/green]")
173
302
  else:
174
303
  console.print(f"\n[yellow]{executor.upper()} finished, but task {task_id} is blocked by verification gaps.[/yellow]")
175
304
  else:
305
+ logger.error("%s failed to start or execute for %s: %s", executor, task_id, exec_result.message)
176
306
  console.print(f"\n[red]{executor.upper()} failed to start or execute: {exec_result.message}[/red]")
177
307
  elif executor == "mini":
178
308
  _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
@@ -183,17 +313,21 @@ def run(
183
313
  _capture_after_patch(task_id, root)
184
314
  if exec_result.success:
185
315
  _record_project_phase(session, ProjectPhase.TASK_VERIFYING)
186
- verified = _verify_after_execution(session, task, reqs, project_root=root)
316
+ verified = _verify_after_execution(
317
+ session, task, reqs, router=_build_verification_router(root), project_root=root
318
+ )
187
319
  _record_project_phase(
188
320
  session,
189
321
  ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
190
322
  )
191
323
  task_repo.save(task)
324
+ _log_exec_outcome("mini-SWE-agent", task_id, verified=verified)
192
325
  if verified:
193
326
  console.print(f"\n[green]mini-SWE-agent finished and task {task_id} verified.[/green]")
194
327
  else:
195
328
  console.print(f"\n[yellow]mini-SWE-agent finished, but task {task_id} is blocked by verification gaps.[/yellow]")
196
329
  else:
330
+ logger.error("mini-SWE-agent failed to start or execute for %s", task_id)
197
331
  console.print("\n[red]mini-SWE-agent failed to start or execute.[/red]")
198
332
  elif executor == "openhands":
199
333
  _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
@@ -204,31 +338,36 @@ def run(
204
338
  _capture_after_patch(task_id, root)
205
339
  if exec_result.success:
206
340
  _record_project_phase(session, ProjectPhase.TASK_VERIFYING)
207
- verified = _verify_after_execution(session, task, reqs, project_root=root)
341
+ verified = _verify_after_execution(
342
+ session, task, reqs, router=_build_verification_router(root), project_root=root
343
+ )
208
344
  _record_project_phase(
209
345
  session,
210
346
  ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
211
347
  )
212
348
  task_repo.save(task)
349
+ _log_exec_outcome("OpenHands", task_id, verified=verified)
213
350
  if verified:
214
351
  console.print(f"\n[green]OpenHands finished and task {task_id} verified.[/green]")
215
352
  else:
216
353
  console.print(f"\n[yellow]OpenHands finished, but task {task_id} is blocked by verification gaps.[/yellow]")
217
354
  else:
355
+ logger.error("OpenHands failed to start or execute for %s", task_id)
218
356
  console.print("\n[red]OpenHands failed to start or execute.[/red]")
219
- elif executor == "native":
357
+ elif executor in {"native", "native-preview"}:
220
358
  # Load config for model routing and permissions
221
359
  try:
222
360
  config = load_config(root)
223
361
  validate_model_provider(config.models.provider)
224
362
  api_key = get_api_key(config.models.provider, root)
225
363
  except (FileNotFoundError, ValueError) as e:
364
+ logger.error("Native executor cannot start for %s: %s", task_id, e)
226
365
  console.print(f"[red]{e}[/red]")
227
366
  return
228
367
 
229
- provider = create_provider(config.models.provider, api_key)
368
+ provider = create_provider(config.models.provider, api_key, project_root=root, provider_prefs=config.provider)
230
369
  role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
231
- router = ModelRouter(provider, role_config)
370
+ router = ModelRouter(provider, role_config, project_root=root)
232
371
 
233
372
  # Setup Permission System
234
373
  from devcouncil.execution.permissions import PermissionPolicy, PermissionManager
@@ -236,19 +375,18 @@ def run(
236
375
 
237
376
  # Populate policy from config commands
238
377
  allowed_cmds = config.commands.test + config.commands.lint + config.commands.typecheck
239
- policy = PermissionPolicy(
378
+ permission_policy = PermissionPolicy(
240
379
  allowed_shell_commands=allowed_cmds,
241
380
  )
242
- perm_manager = PermissionManager(policy, root)
381
+ perm_manager = PermissionManager(permission_policy, root)
243
382
  task_runner = TaskRunner(root, perm_manager)
244
383
 
245
384
  req_repo = RequirementRepository(session)
246
385
  reqs = req_repo.get_all()
247
386
 
248
- import asyncio
249
387
  _record_project_phase(session, ProjectPhase.TASK_EXECUTING)
250
388
  agent = NativeAgent(router, task_runner)
251
- exec_result = asyncio.run(agent.run_task(task, reqs))
389
+ exec_result = agent.run_task(task, reqs)
252
390
  _capture_after_patch(task_id, root)
253
391
 
254
392
  if exec_result.success:
@@ -259,11 +397,14 @@ def run(
259
397
  ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
260
398
  )
261
399
  task_repo.save(task)
400
+ _log_exec_outcome("Native agent", task_id, verified=verified)
262
401
  if verified:
263
402
  console.print(f"\n[green]Native agent finished and task {task_id} verified.[/green]")
264
403
  else:
265
404
  console.print(f"\n[yellow]Native agent finished, but task {task_id} is blocked by verification gaps.[/yellow]")
266
405
  else:
406
+ logger.error("Native agent failed during execution for %s", task_id)
267
407
  console.print("\n[red]Native agent failed during execution.[/red]")
268
408
  else:
409
+ logger.error("Executor %r not implemented", executor)
269
410
  console.print(f"[red]Executor {executor} not yet implemented.[/red]")