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.
- package/README.md +190 -6
- package/package.json +9 -2
- package/pyproject.toml +34 -2
- package/src/devcouncil/app/config.py +167 -5
- package/src/devcouncil/artifacts/graph.py +23 -3
- 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 +209 -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/doctor.py +221 -21
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +452 -33
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +124 -15
- package/src/devcouncil/cli/commands/init.py +154 -18
- package/src/devcouncil/cli/commands/integrate.py +894 -105
- package/src/devcouncil/cli/commands/map.py +80 -10
- package/src/devcouncil/cli/commands/plan.py +212 -51
- package/src/devcouncil/cli/commands/prompt.py +18 -7
- package/src/devcouncil/cli/commands/repair.py +40 -23
- 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 +69 -49
- 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 +88 -0
- package/src/devcouncil/cli/commands/status.py +25 -1
- package/src/devcouncil/cli/commands/trace.py +47 -3
- package/src/devcouncil/cli/commands/verify.py +138 -3
- package/src/devcouncil/cli/commands/watch.py +9 -9
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +56 -7
- package/src/devcouncil/domain/evidence.py +22 -2
- package/src/devcouncil/domain/gap.py +27 -1
- package/src/devcouncil/domain/task.py +31 -2
- package/src/devcouncil/execution/checkpoints.py +246 -0
- package/src/devcouncil/execution/context_builder.py +1 -1
- package/src/devcouncil/execution/fs_watcher.py +180 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +162 -74
- package/src/devcouncil/execution/patch.py +59 -10
- package/src/devcouncil/execution/permissions.py +17 -24
- package/src/devcouncil/execution/policy_engine.py +343 -0
- package/src/devcouncil/execution/prompt_builder.py +633 -21
- package/src/devcouncil/execution/shell_session.py +225 -0
- package/src/devcouncil/execution/task_runner.py +6 -2
- package/src/devcouncil/executors/agent_registry.py +575 -0
- package/src/devcouncil/executors/coding_cli.py +663 -39
- package/src/devcouncil/executors/native/agent.py +121 -20
- package/src/devcouncil/gating/checks/clean_git.py +3 -1
- package/src/devcouncil/gating/checks/secret_scan_check.py +40 -21
- package/src/devcouncil/gating/policy.py +158 -10
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/ast_matcher.py +1 -1
- package/src/devcouncil/indexing/lsp.py +45 -4
- package/src/devcouncil/indexing/repo_mapper.py +1256 -9
- package/src/devcouncil/indexing/semantic_index.py +205 -0
- package/src/devcouncil/integrations/actions.py +146 -0
- package/src/devcouncil/integrations/check.py +423 -0
- package/src/devcouncil/integrations/github_intent.py +142 -0
- package/src/devcouncil/integrations/gitnexus.py +35 -0
- package/src/devcouncil/integrations/mcp/server.py +1552 -29
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/live/cards.py +161 -19
- package/src/devcouncil/live/signals.py +2 -2
- package/src/devcouncil/live/transcripts.py +9 -6
- package/src/devcouncil/llm/cache.py +10 -6
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +515 -34
- package/src/devcouncil/llm/router.py +231 -46
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/planning/correction_manifest.py +303 -0
- package/src/devcouncil/planning/critique_service.py +7 -2
- package/src/devcouncil/planning/plan_service.py +17 -3
- package/src/devcouncil/planning/prompt_enhancer_service.py +82 -1
- package/src/devcouncil/planning/spec_service.py +27 -1
- package/src/devcouncil/repo/ci_scaffold.py +157 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +374 -0
- package/src/devcouncil/reporting/json_report.py +11 -1
- package/src/devcouncil/reporting/markdown_report.py +15 -0
- 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 +330 -0
- package/src/devcouncil/storage/db.py +83 -2
- package/src/devcouncil/storage/models.py +121 -0
- package/src/devcouncil/storage/native.py +557 -0
- package/src/devcouncil/storage/repositories.py +137 -75
- package/src/devcouncil/telemetry/cost.py +123 -17
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +12 -9
- package/src/devcouncil/ui/dashboard.py +324 -23
- package/src/devcouncil/utils/redaction.py +9 -3
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/acceptance_compiler.py +125 -0
- package/src/devcouncil/verification/ad_hoc_check.py +129 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +178 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1065 -47
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -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,4 @@
|
|
|
1
1
|
import typer
|
|
2
|
-
import json
|
|
3
2
|
from rich.console import Console
|
|
4
3
|
from pathlib import Path
|
|
5
4
|
from devcouncil.storage.db import get_db
|
|
@@ -7,6 +6,11 @@ from devcouncil.storage.repositories import TaskRepository, RequirementRepositor
|
|
|
7
6
|
from devcouncil.executors.mini_swe import MiniSWEExecutor
|
|
8
7
|
from devcouncil.executors.openhands import OpenHandsExecutor
|
|
9
8
|
from devcouncil.executors.coding_cli import CodingCliExecutor
|
|
9
|
+
from devcouncil.executors.agent_registry import (
|
|
10
|
+
AGENT_ALIASES,
|
|
11
|
+
BUILTIN_CODING_EXECUTOR_NAMES,
|
|
12
|
+
load_cli_agent_specs,
|
|
13
|
+
)
|
|
10
14
|
from devcouncil.executors.native.agent import NativeAgent
|
|
11
15
|
from devcouncil.llm.provider import create_provider, validate_model_provider
|
|
12
16
|
from devcouncil.llm.router import ModelRouter
|
|
@@ -16,20 +20,18 @@ from devcouncil.storage.repositories import GapRepository, EvidenceRepository, S
|
|
|
16
20
|
from devcouncil.verification.verifier import Verifier
|
|
17
21
|
from devcouncil.app.state_machine import ProjectPhase
|
|
18
22
|
from devcouncil.cli.commands.init import initialize_project
|
|
23
|
+
from devcouncil.telemetry.traces import TraceLogger
|
|
19
24
|
|
|
20
25
|
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
|
-
}
|
|
26
|
+
CODING_EXECUTOR_ALIASES = {name: name for name in BUILTIN_CODING_EXECUTOR_NAMES} | AGENT_ALIASES
|
|
30
27
|
|
|
31
28
|
CODING_EXECUTORS = set(CODING_EXECUTOR_ALIASES.keys())
|
|
32
29
|
|
|
30
|
+
|
|
31
|
+
def _custom_cli_agents(project_root: Path) -> set[str]:
|
|
32
|
+
specs = load_cli_agent_specs(project_root)
|
|
33
|
+
return {name for name, spec in specs.items() if not spec.built_in}
|
|
34
|
+
|
|
33
35
|
def _current_changed_files(project_root: Path = Path(".")) -> list[str]:
|
|
34
36
|
from devcouncil.verification.verifier import Verifier
|
|
35
37
|
|
|
@@ -38,28 +40,16 @@ def _current_changed_files(project_root: Path = Path(".")) -> list[str]:
|
|
|
38
40
|
def _capture_after_patch(task_id: str, project_root: Path = Path(".")):
|
|
39
41
|
"""Capture the diff after task execution for use by rollback."""
|
|
40
42
|
try:
|
|
41
|
-
from devcouncil.
|
|
43
|
+
from devcouncil.execution.checkpoints import CheckpointService
|
|
42
44
|
|
|
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
|
+
CheckpointService(project_root).create_after(task_id)
|
|
49
46
|
except Exception:
|
|
50
47
|
pass # Non-critical — don't block execution
|
|
51
48
|
|
|
52
49
|
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
|
-
)
|
|
50
|
+
from devcouncil.execution.checkpoints import CheckpointService
|
|
51
|
+
|
|
52
|
+
CheckpointService(project_root).create_before(task_id)
|
|
63
53
|
|
|
64
54
|
def _record_project_phase(session, phase: ProjectPhase):
|
|
65
55
|
StateRepository(session).record_phase(phase.value)
|
|
@@ -90,13 +80,33 @@ def _verify_after_execution(session, task, reqs, router=None, project_root: Path
|
|
|
90
80
|
task.status = "blocked" if any(g.blocking for g in gaps) else "verified"
|
|
91
81
|
return task.status == "verified"
|
|
92
82
|
|
|
83
|
+
|
|
84
|
+
def _record_agent_verification(project_root: Path, task_id: str, executor: str, run_id: str | None, verified: bool) -> None:
|
|
85
|
+
TraceLogger(project_root).log_event(
|
|
86
|
+
"agent_run_verified",
|
|
87
|
+
{"agent": executor, "verified": verified},
|
|
88
|
+
run_id=run_id,
|
|
89
|
+
task_id=task_id,
|
|
90
|
+
summary=f"{executor} verification {'passed' if verified else 'blocked'} for {task_id}",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
93
|
def run(
|
|
94
94
|
task_id: str = typer.Argument(..., help="ID of the task to run"),
|
|
95
95
|
executor: str = typer.Option(
|
|
96
96
|
"manual",
|
|
97
97
|
"--executor",
|
|
98
98
|
"-e",
|
|
99
|
-
help=
|
|
99
|
+
help=(
|
|
100
|
+
"Executor to use (manual, mini, openhands, native-preview, "
|
|
101
|
+
"codex, gemini, claude, opencode, antigravity, warp, cursor, aider, "
|
|
102
|
+
"copilot, goose, amp, qwen, crush, or a configured agent)"
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
profile: str | None = typer.Option(None, "--profile", help="CLI-agent execution profile: default, yolo, prod, or a configured profile."),
|
|
106
|
+
stream: bool = typer.Option(
|
|
107
|
+
False,
|
|
108
|
+
"--stream",
|
|
109
|
+
help="Stream coding CLI stdout/stderr live (also enabled by execution.stream_cli_output).",
|
|
100
110
|
),
|
|
101
111
|
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
102
112
|
):
|
|
@@ -118,8 +128,8 @@ def run(
|
|
|
118
128
|
return
|
|
119
129
|
|
|
120
130
|
from devcouncil.gating.policy import GatePolicy
|
|
121
|
-
|
|
122
|
-
gate_result =
|
|
131
|
+
gate_policy = GatePolicy()
|
|
132
|
+
gate_result = gate_policy.check_task_ready(task, root)
|
|
123
133
|
if not gate_result.passed:
|
|
124
134
|
console.print(f"[red]Task {task_id} is not ready for execution.[/red]")
|
|
125
135
|
for gap in gate_result.gaps:
|
|
@@ -131,20 +141,23 @@ def run(
|
|
|
131
141
|
|
|
132
142
|
# 1. Create Git checkpoint
|
|
133
143
|
try:
|
|
134
|
-
from devcouncil.
|
|
144
|
+
from devcouncil.execution.checkpoints import CheckpointService
|
|
135
145
|
|
|
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")
|
|
146
|
+
result = CheckpointService(root).create_before(task_id)
|
|
147
|
+
if result.patch_path:
|
|
148
|
+
console.print(f"Created git checkpoint at {result.patch_path}")
|
|
149
|
+
elif result.git_ref_created:
|
|
150
|
+
console.print(f"Created git checkpoint ref {result.ref}")
|
|
144
151
|
except Exception as e:
|
|
145
152
|
console.print(f"[yellow]Warning: Failed to create git checkpoint: {e}[/yellow]")
|
|
146
153
|
|
|
147
154
|
executor = executor.strip().lower().replace("_", "-")
|
|
155
|
+
if executor not in CODING_EXECUTORS and executor not in _custom_cli_agents(root):
|
|
156
|
+
ignored = [flag for flag, value in (("--profile", profile), ("--stream", stream)) if value]
|
|
157
|
+
if ignored:
|
|
158
|
+
console.print(
|
|
159
|
+
f"[yellow]{' and '.join(ignored)} only apply to coding CLI executors and are ignored for '{executor}'.[/yellow]"
|
|
160
|
+
)
|
|
148
161
|
if executor == "manual":
|
|
149
162
|
_record_project_phase(session, ProjectPhase.TASK_EXECUTING)
|
|
150
163
|
task.status = "running"
|
|
@@ -152,22 +165,30 @@ def run(
|
|
|
152
165
|
console.print(f"\n[green]Task {task_id} is now marked as RUNNING.[/green]")
|
|
153
166
|
console.print("Use 'dev prompt TASK-ID' to get the prompt for this task.")
|
|
154
167
|
console.print("When finished, use 'dev verify TASK-ID' to check the results.")
|
|
155
|
-
elif executor in CODING_EXECUTORS:
|
|
168
|
+
elif executor in CODING_EXECUTORS or executor in _custom_cli_agents(root):
|
|
156
169
|
_record_project_phase(session, ProjectPhase.TASK_EXECUTING)
|
|
157
170
|
req_repo = RequirementRepository(session)
|
|
158
171
|
reqs = req_repo.get_all()
|
|
159
|
-
cli_client = CODING_EXECUTOR_ALIASES
|
|
160
|
-
cli_executor = CodingCliExecutor(root, cli_client)
|
|
172
|
+
cli_client = CODING_EXECUTOR_ALIASES.get(executor, executor)
|
|
173
|
+
cli_executor = CodingCliExecutor(root, cli_client, profile=profile, stream_output=stream or None)
|
|
161
174
|
exec_result = cli_executor.run_task(task, reqs)
|
|
162
175
|
_capture_after_patch(task_id, root)
|
|
163
176
|
if exec_result.success:
|
|
164
177
|
_record_project_phase(session, ProjectPhase.TASK_VERIFYING)
|
|
165
178
|
verified = _verify_after_execution(session, task, reqs, project_root=root)
|
|
179
|
+
_record_agent_verification(root, task.id, cli_client, getattr(cli_executor, "last_run_id", None), verified)
|
|
166
180
|
_record_project_phase(
|
|
167
181
|
session,
|
|
168
182
|
ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
|
|
169
183
|
)
|
|
170
184
|
task_repo.save(task)
|
|
185
|
+
run_id = getattr(cli_executor, "last_run_id", None)
|
|
186
|
+
if run_id:
|
|
187
|
+
run_dir = root / ".devcouncil" / "runs" / run_id
|
|
188
|
+
console.print(f"Run artifacts: [dim]{run_dir}[/dim]")
|
|
189
|
+
transcript_path = getattr(cli_executor, "last_transcript_path", None)
|
|
190
|
+
if transcript_path:
|
|
191
|
+
console.print(f"Transcript: [dim]{transcript_path}[/dim]")
|
|
171
192
|
if verified:
|
|
172
193
|
console.print(f"\n[green]{executor.upper()} finished and task {task_id} verified.[/green]")
|
|
173
194
|
else:
|
|
@@ -216,7 +237,7 @@ def run(
|
|
|
216
237
|
console.print(f"\n[yellow]OpenHands finished, but task {task_id} is blocked by verification gaps.[/yellow]")
|
|
217
238
|
else:
|
|
218
239
|
console.print("\n[red]OpenHands failed to start or execute.[/red]")
|
|
219
|
-
elif executor
|
|
240
|
+
elif executor in {"native", "native-preview"}:
|
|
220
241
|
# Load config for model routing and permissions
|
|
221
242
|
try:
|
|
222
243
|
config = load_config(root)
|
|
@@ -226,9 +247,9 @@ def run(
|
|
|
226
247
|
console.print(f"[red]{e}[/red]")
|
|
227
248
|
return
|
|
228
249
|
|
|
229
|
-
provider = create_provider(config.models.provider, api_key)
|
|
250
|
+
provider = create_provider(config.models.provider, api_key, project_root=root)
|
|
230
251
|
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
231
|
-
router = ModelRouter(provider, role_config)
|
|
252
|
+
router = ModelRouter(provider, role_config, project_root=root)
|
|
232
253
|
|
|
233
254
|
# Setup Permission System
|
|
234
255
|
from devcouncil.execution.permissions import PermissionPolicy, PermissionManager
|
|
@@ -236,19 +257,18 @@ def run(
|
|
|
236
257
|
|
|
237
258
|
# Populate policy from config commands
|
|
238
259
|
allowed_cmds = config.commands.test + config.commands.lint + config.commands.typecheck
|
|
239
|
-
|
|
260
|
+
permission_policy = PermissionPolicy(
|
|
240
261
|
allowed_shell_commands=allowed_cmds,
|
|
241
262
|
)
|
|
242
|
-
perm_manager = PermissionManager(
|
|
263
|
+
perm_manager = PermissionManager(permission_policy, root)
|
|
243
264
|
task_runner = TaskRunner(root, perm_manager)
|
|
244
265
|
|
|
245
266
|
req_repo = RequirementRepository(session)
|
|
246
267
|
reqs = req_repo.get_all()
|
|
247
268
|
|
|
248
|
-
import asyncio
|
|
249
269
|
_record_project_phase(session, ProjectPhase.TASK_EXECUTING)
|
|
250
270
|
agent = NativeAgent(router, task_runner)
|
|
251
|
-
exec_result =
|
|
271
|
+
exec_result = agent.run_task(task, reqs)
|
|
252
272
|
_capture_after_patch(task_id, root)
|
|
253
273
|
|
|
254
274
|
if exec_result.success:
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""`dev runs` — list and inspect per-run agent manifests.
|
|
2
|
+
|
|
3
|
+
Coding-CLI executors write a manifest at
|
|
4
|
+
``.devcouncil/runs/<run-id>/agent-run.json`` (prompt file, executor, profile,
|
|
5
|
+
resolved command, exit status, run metadata). These commands let a developer or a
|
|
6
|
+
supervisor list and inspect those runs without reading raw JSON, and flag a run
|
|
7
|
+
whose status is still ``running`` but whose manifest has gone stale (the executor
|
|
8
|
+
process likely crashed) as ``orphaned``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import typer
|
|
18
|
+
from rich.console import Console
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
|
|
21
|
+
from devcouncil.utils.redaction import redact_text
|
|
22
|
+
|
|
23
|
+
app = typer.Typer(help="List and inspect coding-agent run manifests.")
|
|
24
|
+
console = Console()
|
|
25
|
+
|
|
26
|
+
# A run still marked ``running`` whose manifest has not been touched for longer
|
|
27
|
+
# than this is treated as orphaned (the executor process likely died). Used as a
|
|
28
|
+
# sane default; can be overridden per-call with --orphan-after.
|
|
29
|
+
_DEFAULT_ORPHAN_AFTER_SECONDS = 600
|
|
30
|
+
|
|
31
|
+
# Transcript/log files (in priority order) whose tail `dev runs show` surfaces.
|
|
32
|
+
_TRANSCRIPT_CANDIDATES = ("transcript.txt", "transcript.log", "output.log", "run.log")
|
|
33
|
+
_TRANSCRIPT_TAIL_LINES = 40
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _runs_dir(project_root: Path) -> Path:
|
|
37
|
+
return project_root / ".devcouncil" / "runs"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _orphan_after_seconds(project_root: Path) -> int:
|
|
41
|
+
"""Threshold (seconds) after which a stale ``running`` manifest is orphaned.
|
|
42
|
+
|
|
43
|
+
Config-driven when available (execution.lease_ttl_seconds is a reasonable
|
|
44
|
+
proxy for "how long a live run can plausibly stay quiet"); falls back to a
|
|
45
|
+
sane default so the command works before `dev init`."""
|
|
46
|
+
try:
|
|
47
|
+
from devcouncil.app.config import load_config
|
|
48
|
+
|
|
49
|
+
ttl = int(load_config(project_root).execution.lease_ttl_seconds)
|
|
50
|
+
if ttl > 0:
|
|
51
|
+
return ttl
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
return _DEFAULT_ORPHAN_AFTER_SECONDS
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _load_manifest(manifest_path: Path) -> dict | None:
|
|
58
|
+
try:
|
|
59
|
+
data = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
60
|
+
except (OSError, json.JSONDecodeError):
|
|
61
|
+
return None
|
|
62
|
+
return data if isinstance(data, dict) else None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _is_orphaned(manifest: dict, manifest_path: Path, *, orphan_after: int, now: float) -> bool:
|
|
66
|
+
"""A run is orphaned when it is still ``running`` but its manifest file has
|
|
67
|
+
not been updated within the threshold — i.e. no heartbeat, executor gone."""
|
|
68
|
+
if manifest.get("status") != "running":
|
|
69
|
+
return False
|
|
70
|
+
try:
|
|
71
|
+
mtime = manifest_path.stat().st_mtime
|
|
72
|
+
except OSError:
|
|
73
|
+
return False
|
|
74
|
+
return (now - mtime) > max(0, orphan_after)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _run_summary(manifest: dict, manifest_path: Path, *, orphan_after: int, now: float) -> dict:
|
|
78
|
+
return {
|
|
79
|
+
"run_id": manifest.get("run_id") or manifest_path.parent.name,
|
|
80
|
+
"task_id": manifest.get("task_id"),
|
|
81
|
+
"agent": manifest.get("agent"),
|
|
82
|
+
"profile": manifest.get("profile"),
|
|
83
|
+
"status": manifest.get("status"),
|
|
84
|
+
"started_at": manifest.get("started_at") or manifest.get("timestamp"),
|
|
85
|
+
"finished_at": manifest.get("finished_at"),
|
|
86
|
+
"returncode": manifest.get("returncode"),
|
|
87
|
+
"orphaned": _is_orphaned(manifest, manifest_path, orphan_after=orphan_after, now=now),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _collect_runs(project_root: Path, *, orphan_after: int) -> list[dict]:
|
|
92
|
+
runs_dir = _runs_dir(project_root)
|
|
93
|
+
if not runs_dir.is_dir():
|
|
94
|
+
return []
|
|
95
|
+
now = time.time()
|
|
96
|
+
summaries: list[tuple[float, dict]] = []
|
|
97
|
+
for manifest_path in runs_dir.glob("*/agent-run.json"):
|
|
98
|
+
manifest = _load_manifest(manifest_path)
|
|
99
|
+
if manifest is None:
|
|
100
|
+
continue
|
|
101
|
+
try:
|
|
102
|
+
sort_key = manifest_path.stat().st_mtime
|
|
103
|
+
except OSError:
|
|
104
|
+
sort_key = 0.0
|
|
105
|
+
summaries.append((sort_key, _run_summary(manifest, manifest_path, orphan_after=orphan_after, now=now)))
|
|
106
|
+
# Newest first.
|
|
107
|
+
summaries.sort(key=lambda item: item[0], reverse=True)
|
|
108
|
+
return [summary for _, summary in summaries]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _find_transcript(run_dir: Path, manifest: dict) -> Path | None:
|
|
112
|
+
recorded = manifest.get("transcript")
|
|
113
|
+
if isinstance(recorded, str) and recorded:
|
|
114
|
+
candidate = Path(recorded)
|
|
115
|
+
if not candidate.is_absolute():
|
|
116
|
+
candidate = run_dir / recorded
|
|
117
|
+
if candidate.is_file():
|
|
118
|
+
return candidate
|
|
119
|
+
for name in _TRANSCRIPT_CANDIDATES:
|
|
120
|
+
candidate = run_dir / name
|
|
121
|
+
if candidate.is_file():
|
|
122
|
+
return candidate
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _transcript_tail(path: Path, *, lines: int = _TRANSCRIPT_TAIL_LINES) -> str:
|
|
127
|
+
"""Return the redacted tail of a transcript file (best-effort)."""
|
|
128
|
+
try:
|
|
129
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
130
|
+
except OSError:
|
|
131
|
+
return ""
|
|
132
|
+
tail = content.splitlines()[-lines:]
|
|
133
|
+
return redact_text("\n".join(tail))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.command("list")
|
|
137
|
+
def list_runs(
|
|
138
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
|
|
139
|
+
limit: int = typer.Option(20, "--limit", help="Maximum number of runs to show."),
|
|
140
|
+
status: str | None = typer.Option(None, "--status", help="Filter by run status (e.g. running, finished, failed, timeout)."),
|
|
141
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
142
|
+
) -> None:
|
|
143
|
+
"""List recorded coding-agent runs, newest first."""
|
|
144
|
+
root = project_root.expanduser().resolve()
|
|
145
|
+
orphan_after = _orphan_after_seconds(root)
|
|
146
|
+
runs = _collect_runs(root, orphan_after=orphan_after)
|
|
147
|
+
if status:
|
|
148
|
+
runs = [run for run in runs if run.get("status") == status]
|
|
149
|
+
if limit > 0:
|
|
150
|
+
runs = runs[:limit]
|
|
151
|
+
|
|
152
|
+
if json_output:
|
|
153
|
+
console.print_json(data={"runs": runs, "count": len(runs)})
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
if not runs:
|
|
157
|
+
console.print("[dim]No agent runs found under .devcouncil/runs/.[/dim]")
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
table = Table(title="Agent runs")
|
|
161
|
+
table.add_column("Run ID", overflow="fold")
|
|
162
|
+
table.add_column("Task")
|
|
163
|
+
table.add_column("Agent")
|
|
164
|
+
table.add_column("Profile")
|
|
165
|
+
table.add_column("Status")
|
|
166
|
+
table.add_column("Started")
|
|
167
|
+
for run in runs:
|
|
168
|
+
status_text = str(run.get("status") or "?")
|
|
169
|
+
if run.get("orphaned"):
|
|
170
|
+
status_text = f"[red]{status_text} (orphaned)[/red]"
|
|
171
|
+
table.add_row(
|
|
172
|
+
str(run.get("run_id") or ""),
|
|
173
|
+
str(run.get("task_id") or ""),
|
|
174
|
+
str(run.get("agent") or ""),
|
|
175
|
+
str(run.get("profile") or ""),
|
|
176
|
+
status_text,
|
|
177
|
+
str(run.get("started_at") or ""),
|
|
178
|
+
)
|
|
179
|
+
console.print(table)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@app.command("show")
|
|
183
|
+
def show_run(
|
|
184
|
+
run_id: str = typer.Argument(..., help="The run id to inspect."),
|
|
185
|
+
json_output: bool = typer.Option(False, "--json", help="Emit machine-readable JSON."),
|
|
186
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
187
|
+
) -> None:
|
|
188
|
+
"""Show the full manifest for a run plus a redacted transcript tail."""
|
|
189
|
+
root = project_root.expanduser().resolve()
|
|
190
|
+
run_dir = _runs_dir(root) / run_id
|
|
191
|
+
manifest_path = run_dir / "agent-run.json"
|
|
192
|
+
manifest = _load_manifest(manifest_path)
|
|
193
|
+
if manifest is None:
|
|
194
|
+
if json_output:
|
|
195
|
+
console.print_json(data={"ok": False, "error": f"Run {run_id} not found.", "run_id": run_id})
|
|
196
|
+
else:
|
|
197
|
+
console.print(f"[red]Run {run_id} not found under .devcouncil/runs/.[/red]")
|
|
198
|
+
raise typer.Exit(code=1)
|
|
199
|
+
|
|
200
|
+
orphan_after = _orphan_after_seconds(root)
|
|
201
|
+
orphaned = _is_orphaned(manifest, manifest_path, orphan_after=orphan_after, now=time.time())
|
|
202
|
+
transcript_path = _find_transcript(run_dir, manifest)
|
|
203
|
+
transcript_tail = _transcript_tail(transcript_path) if transcript_path else ""
|
|
204
|
+
|
|
205
|
+
if json_output:
|
|
206
|
+
console.print_json(data={
|
|
207
|
+
"ok": True,
|
|
208
|
+
"run_id": run_id,
|
|
209
|
+
"manifest": manifest,
|
|
210
|
+
"orphaned": orphaned,
|
|
211
|
+
"transcript_path": str(transcript_path) if transcript_path else None,
|
|
212
|
+
"transcript_tail": transcript_tail,
|
|
213
|
+
})
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
console.print_json(data=manifest)
|
|
217
|
+
if orphaned:
|
|
218
|
+
console.print("[red]This run is orphaned: still marked running but its manifest is stale.[/red]")
|
|
219
|
+
if transcript_path:
|
|
220
|
+
console.print(f"\n[bold]Transcript tail[/bold] [dim]({transcript_path})[/dim]:")
|
|
221
|
+
console.print(transcript_tail or "[dim](empty)[/dim]")
|
|
222
|
+
else:
|
|
223
|
+
console.print("\n[dim]No transcript file found for this run.[/dim]")
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from devcouncil.repo.ci_scaffold import WORKFLOW_RELPATH, detect_stacks, scaffold_ci
|
|
7
|
+
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def scaffold_ci_command(
|
|
12
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root to scaffold CI into."),
|
|
13
|
+
force: bool = typer.Option(False, "--force", help="Overwrite an existing devcouncil.yml workflow."),
|
|
14
|
+
):
|
|
15
|
+
"""Write a starter GitHub Actions workflow derived from the configured commands."""
|
|
16
|
+
root = project_root.expanduser().resolve()
|
|
17
|
+
if not (root / ".devcouncil").exists():
|
|
18
|
+
console.print("[red]DevCouncil is not initialized here. Run 'dev setup' first.[/red]")
|
|
19
|
+
raise typer.Exit(code=1)
|
|
20
|
+
|
|
21
|
+
target = scaffold_ci(root, force=force)
|
|
22
|
+
if target is None:
|
|
23
|
+
console.print(
|
|
24
|
+
f"[yellow]{WORKFLOW_RELPATH.as_posix()} already exists. "
|
|
25
|
+
f"Re-run with --force to overwrite.[/yellow]"
|
|
26
|
+
)
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
stacks = detect_stacks(root)
|
|
30
|
+
detected = ", ".join(sorted(stacks)) if stacks else "none auto-detected"
|
|
31
|
+
console.print(f"[green]Wrote {target.relative_to(root).as_posix()} (stacks: {detected}).[/green]")
|
|
32
|
+
console.print("[dim]Review the setup/install steps and commands before relying on it.[/dim]")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import typer
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
|
|
6
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
7
|
+
from devcouncil.indexing.semantic_index import SemanticIndex
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="Semantic snapshots and diffs.")
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@app.command("snapshot")
|
|
14
|
+
def snapshot(
|
|
15
|
+
task_id: str = typer.Argument(...),
|
|
16
|
+
stage: str = typer.Option("before", "--stage", help="before or after"),
|
|
17
|
+
project_root: Path = typer.Option(Path("."), "--project-root"),
|
|
18
|
+
json_format: bool = typer.Option(False, "--json"),
|
|
19
|
+
):
|
|
20
|
+
root = project_root.expanduser().resolve()
|
|
21
|
+
initialize_project(root, quiet=True)
|
|
22
|
+
if stage not in {"before", "after"}:
|
|
23
|
+
console.print("[red]--stage must be before or after[/red]")
|
|
24
|
+
raise typer.Exit(code=2)
|
|
25
|
+
path = SemanticIndex(root).create_snapshot(task_id, stage)
|
|
26
|
+
payload = {"task_id": task_id, "stage": stage, "path": str(path)}
|
|
27
|
+
if json_format:
|
|
28
|
+
typer.echo(json.dumps(payload, indent=2))
|
|
29
|
+
else:
|
|
30
|
+
console.print(f"[green]Wrote semantic snapshot:[/green] {path}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@app.command("diff")
|
|
34
|
+
def semantic_diff(
|
|
35
|
+
task_id: str = typer.Argument(...),
|
|
36
|
+
project_root: Path = typer.Option(Path("."), "--project-root"),
|
|
37
|
+
json_format: bool = typer.Option(False, "--json"),
|
|
38
|
+
):
|
|
39
|
+
root = project_root.expanduser().resolve()
|
|
40
|
+
initialize_project(root, quiet=True)
|
|
41
|
+
result = SemanticIndex(root).diff(task_id)
|
|
42
|
+
if json_format:
|
|
43
|
+
typer.echo(json.dumps(result, indent=2))
|
|
44
|
+
else:
|
|
45
|
+
console.print(f"[cyan]Summary:[/cyan] {result['summary']}")
|
|
46
|
+
for item in result["classifications"]:
|
|
47
|
+
console.print(f" - {item['type']}: {item.get('path', '')}")
|