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.
- package/README.md +201 -6
- package/package.json +9 -2
- package/pyproject.toml +34 -2
- package/src/devcouncil/app/config.py +348 -12
- package/src/devcouncil/app/orchestrator.py +10 -6
- package/src/devcouncil/app/state_machine.py +4 -0
- package/src/devcouncil/artifacts/graph.py +32 -5
- package/src/devcouncil/assets/__init__.py +1 -0
- package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/commands/agents.py +292 -0
- package/src/devcouncil/cli/commands/artifacts.py +6 -3
- package/src/devcouncil/cli/commands/check.py +220 -0
- package/src/devcouncil/cli/commands/config.py +43 -4
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +6 -1
- package/src/devcouncil/cli/commands/design.py +186 -0
- package/src/devcouncil/cli/commands/doctor.py +379 -22
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +532 -33
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +296 -15
- package/src/devcouncil/cli/commands/init.py +161 -20
- package/src/devcouncil/cli/commands/integrate.py +1371 -124
- package/src/devcouncil/cli/commands/logs.py +106 -0
- package/src/devcouncil/cli/commands/map.py +80 -10
- package/src/devcouncil/cli/commands/okf.py +245 -0
- package/src/devcouncil/cli/commands/plan.py +256 -55
- package/src/devcouncil/cli/commands/prompt.py +18 -7
- package/src/devcouncil/cli/commands/repair.py +50 -24
- package/src/devcouncil/cli/commands/report.py +8 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
- package/src/devcouncil/cli/commands/rollback.py +27 -28
- package/src/devcouncil/cli/commands/run.py +195 -54
- package/src/devcouncil/cli/commands/runs.py +223 -0
- package/src/devcouncil/cli/commands/scaffold.py +32 -0
- package/src/devcouncil/cli/commands/semantic.py +47 -0
- package/src/devcouncil/cli/commands/setup.py +145 -6
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/skills.py +267 -0
- package/src/devcouncil/cli/commands/status.py +30 -15
- package/src/devcouncil/cli/commands/trace.py +47 -3
- package/src/devcouncil/cli/commands/verify.py +144 -3
- package/src/devcouncil/cli/commands/watch.py +32 -12
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +91 -7
- package/src/devcouncil/domain/evidence.py +29 -2
- package/src/devcouncil/domain/gap.py +27 -1
- package/src/devcouncil/domain/task.py +31 -2
- package/src/devcouncil/execution/checkpoints.py +256 -0
- package/src/devcouncil/execution/context_builder.py +1 -1
- package/src/devcouncil/execution/fs_watcher.py +205 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +162 -74
- package/src/devcouncil/execution/patch.py +65 -10
- package/src/devcouncil/execution/permissions.py +24 -24
- package/src/devcouncil/execution/policy_engine.py +350 -0
- package/src/devcouncil/execution/prompt_builder.py +751 -23
- package/src/devcouncil/execution/shell_session.py +231 -0
- package/src/devcouncil/execution/task_runner.py +24 -9
- package/src/devcouncil/executors/agent_registry.py +596 -0
- package/src/devcouncil/executors/coding_cli.py +791 -39
- package/src/devcouncil/executors/mini_swe.py +6 -0
- package/src/devcouncil/executors/native/agent.py +135 -19
- package/src/devcouncil/executors/openhands.py +6 -0
- package/src/devcouncil/gating/checks/clean_git.py +3 -1
- package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
- package/src/devcouncil/gating/policy.py +190 -11
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/ast_matcher.py +17 -7
- package/src/devcouncil/indexing/lsp.py +45 -4
- package/src/devcouncil/indexing/repo_mapper.py +1284 -15
- package/src/devcouncil/indexing/semantic_index.py +221 -0
- package/src/devcouncil/integrations/actions.py +166 -0
- package/src/devcouncil/integrations/check.py +426 -0
- package/src/devcouncil/integrations/claude_assets.py +444 -0
- package/src/devcouncil/integrations/code_review_graph.py +13 -2
- package/src/devcouncil/integrations/github_intent.py +149 -0
- package/src/devcouncil/integrations/gitnexus.py +45 -2
- package/src/devcouncil/integrations/mcp/server.py +1944 -32
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/integrations/pr_comments.py +9 -0
- package/src/devcouncil/knowledge/__init__.py +23 -0
- package/src/devcouncil/knowledge/design.py +374 -0
- package/src/devcouncil/knowledge/design_conformance.py +317 -0
- package/src/devcouncil/knowledge/fetch.py +223 -0
- package/src/devcouncil/knowledge/frontmatter.py +51 -0
- package/src/devcouncil/knowledge/okf.py +202 -0
- package/src/devcouncil/knowledge/skill_bridge.py +96 -0
- package/src/devcouncil/knowledge/sources.py +239 -0
- package/src/devcouncil/live/cards.py +181 -25
- package/src/devcouncil/live/repair_prompt.py +29 -6
- package/src/devcouncil/live/reviewer.py +72 -13
- package/src/devcouncil/live/signals.py +2 -2
- package/src/devcouncil/live/summary.py +18 -8
- package/src/devcouncil/live/transcripts.py +47 -11
- package/src/devcouncil/llm/cache.py +20 -8
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +617 -49
- package/src/devcouncil/llm/router.py +337 -53
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/optimization/skillopt.py +673 -0
- package/src/devcouncil/planning/arbiter_service.py +10 -2
- package/src/devcouncil/planning/correction_manifest.py +346 -0
- package/src/devcouncil/planning/critique_service.py +16 -4
- package/src/devcouncil/planning/plan_service.py +86 -6
- package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
- package/src/devcouncil/planning/repair_service.py +8 -2
- package/src/devcouncil/planning/spec_service.py +37 -3
- package/src/devcouncil/repo/ci_scaffold.py +165 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +384 -0
- package/src/devcouncil/reporting/json_report.py +22 -1
- package/src/devcouncil/reporting/markdown_report.py +29 -1
- package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
- package/src/devcouncil/reporting/okf_html.py +323 -0
- package/src/devcouncil/reporting/report_builder.py +18 -1
- package/src/devcouncil/skills/__init__.py +19 -0
- package/src/devcouncil/skills/library/README.md +46 -0
- package/src/devcouncil/skills/library/ai-training.md +50 -0
- package/src/devcouncil/skills/library/android.md +50 -0
- package/src/devcouncil/skills/library/backend.md +52 -0
- package/src/devcouncil/skills/library/core-engineering.md +95 -0
- package/src/devcouncil/skills/library/data-engineering.md +47 -0
- package/src/devcouncil/skills/library/desktop.md +46 -0
- package/src/devcouncil/skills/library/devops.md +48 -0
- package/src/devcouncil/skills/library/game-dev.md +46 -0
- package/src/devcouncil/skills/library/ios.md +48 -0
- package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
- package/src/devcouncil/skills/library/security.md +48 -0
- package/src/devcouncil/skills/library/systems.md +48 -0
- package/src/devcouncil/skills/library/web.md +47 -0
- package/src/devcouncil/skills/library/windows.md +47 -0
- package/src/devcouncil/skills/registry.py +408 -0
- package/src/devcouncil/storage/db.py +140 -3
- package/src/devcouncil/storage/models.py +125 -0
- package/src/devcouncil/storage/native.py +559 -0
- package/src/devcouncil/storage/repositories.py +157 -78
- package/src/devcouncil/telemetry/cost.py +123 -17
- package/src/devcouncil/telemetry/logging_setup.py +244 -0
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/stages.py +141 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +24 -10
- package/src/devcouncil/ui/dashboard.py +393 -28
- package/src/devcouncil/utils/redaction.py +9 -3
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/acceptance_compiler.py +253 -0
- package/src/devcouncil/verification/ad_hoc_check.py +135 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/implementation_reviewer.py +11 -2
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +181 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1549 -143
- package/uv.lock +205 -64
- 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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
23
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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.
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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=
|
|
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
|
-
|
|
122
|
-
gate_result =
|
|
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.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
378
|
+
permission_policy = PermissionPolicy(
|
|
240
379
|
allowed_shell_commands=allowed_cmds,
|
|
241
380
|
)
|
|
242
|
-
perm_manager = PermissionManager(
|
|
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 =
|
|
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]")
|