devcouncil 0.1.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/LICENSE +201 -0
- package/README.md +643 -0
- package/bin/devcouncil.js +62 -0
- package/package.json +47 -0
- package/pyproject.toml +31 -0
- package/src/devcouncil/__init__.py +0 -0
- package/src/devcouncil/__main__.py +4 -0
- package/src/devcouncil/app/__init__.py +28 -0
- package/src/devcouncil/app/config.py +131 -0
- package/src/devcouncil/app/errors.py +23 -0
- package/src/devcouncil/app/events.py +44 -0
- package/src/devcouncil/app/orchestrator.py +92 -0
- package/src/devcouncil/app/run_context.py +39 -0
- package/src/devcouncil/app/state_machine.py +108 -0
- package/src/devcouncil/artifacts/__init__.py +1 -0
- package/src/devcouncil/artifacts/coverage.py +96 -0
- package/src/devcouncil/artifacts/graph.py +143 -0
- package/src/devcouncil/artifacts/migrations.py +20 -0
- package/src/devcouncil/artifacts/schemas.py +23 -0
- package/src/devcouncil/artifacts/serializer.py +21 -0
- package/src/devcouncil/artifacts/validators.py +27 -0
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/artifacts.py +48 -0
- package/src/devcouncil/cli/commands/baseline.py +32 -0
- package/src/devcouncil/cli/commands/config.py +54 -0
- package/src/devcouncil/cli/commands/doctor.py +96 -0
- package/src/devcouncil/cli/commands/hook.py +61 -0
- package/src/devcouncil/cli/commands/init.py +142 -0
- package/src/devcouncil/cli/commands/integrate.py +420 -0
- package/src/devcouncil/cli/commands/map.py +38 -0
- package/src/devcouncil/cli/commands/mcp_server.py +18 -0
- package/src/devcouncil/cli/commands/plan.py +276 -0
- package/src/devcouncil/cli/commands/prompt.py +47 -0
- package/src/devcouncil/cli/commands/repair.py +69 -0
- package/src/devcouncil/cli/commands/report.py +71 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +28 -0
- package/src/devcouncil/cli/commands/rollback.py +58 -0
- package/src/devcouncil/cli/commands/run.py +224 -0
- package/src/devcouncil/cli/commands/setup.py +82 -0
- package/src/devcouncil/cli/commands/show.py +57 -0
- package/src/devcouncil/cli/commands/status.py +105 -0
- package/src/devcouncil/cli/commands/tasks.py +41 -0
- package/src/devcouncil/cli/commands/trace.py +43 -0
- package/src/devcouncil/cli/commands/verify.py +163 -0
- package/src/devcouncil/cli/commands/version.py +20 -0
- package/src/devcouncil/cli/main.py +70 -0
- package/src/devcouncil/council/__init__.py +0 -0
- package/src/devcouncil/council/prompts/__init__.py +0 -0
- package/src/devcouncil/council/prompts/arbiter.md +19 -0
- package/src/devcouncil/council/prompts/critic_a.md +10 -0
- package/src/devcouncil/council/prompts/critic_b.md +10 -0
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -0
- package/src/devcouncil/council/prompts/planner_a.md +16 -0
- package/src/devcouncil/council/prompts/planner_b.md +16 -0
- package/src/devcouncil/council/prompts/rebuttal.md +10 -0
- package/src/devcouncil/council/prompts/spec_writer.md +12 -0
- package/src/devcouncil/domain/__init__.py +0 -0
- package/src/devcouncil/domain/assumption.py +17 -0
- package/src/devcouncil/domain/critique.py +32 -0
- package/src/devcouncil/domain/evidence.py +27 -0
- package/src/devcouncil/domain/gap.py +26 -0
- package/src/devcouncil/domain/requirement.py +22 -0
- package/src/devcouncil/domain/task.py +26 -0
- package/src/devcouncil/execution/__init__.py +1 -0
- package/src/devcouncil/execution/context_builder.py +60 -0
- package/src/devcouncil/execution/executor.py +15 -0
- package/src/devcouncil/execution/hook_policy.py +144 -0
- package/src/devcouncil/execution/patch.py +28 -0
- package/src/devcouncil/execution/paths.py +14 -0
- package/src/devcouncil/execution/permissions.py +92 -0
- package/src/devcouncil/execution/prompt_builder.py +59 -0
- package/src/devcouncil/execution/task_runner.py +166 -0
- package/src/devcouncil/executors/__init__.py +1 -0
- package/src/devcouncil/executors/mini_swe.py +73 -0
- package/src/devcouncil/executors/native/__init__.py +0 -0
- package/src/devcouncil/executors/native/agent.py +107 -0
- package/src/devcouncil/executors/openhands.py +71 -0
- package/src/devcouncil/gating/__init__.py +1 -0
- package/src/devcouncil/gating/checks/__init__.py +0 -0
- package/src/devcouncil/gating/checks/clean_git.py +45 -0
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -0
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -0
- package/src/devcouncil/gating/checks/secret_scan_check.py +34 -0
- package/src/devcouncil/gating/policy.py +190 -0
- package/src/devcouncil/indexing/__init__.py +1 -0
- package/src/devcouncil/indexing/graph_index.py +48 -0
- package/src/devcouncil/indexing/repo_mapper.py +204 -0
- package/src/devcouncil/indexing/symbol_index.py +0 -0
- package/src/devcouncil/integrations/code_review_graph.py +163 -0
- package/src/devcouncil/integrations/github.py +39 -0
- package/src/devcouncil/integrations/gitnexus.py +27 -0
- package/src/devcouncil/integrations/graphify.py +34 -0
- package/src/devcouncil/integrations/mcp/__init__.py +0 -0
- package/src/devcouncil/integrations/mcp/server.py +146 -0
- package/src/devcouncil/llm/__init__.py +1 -0
- package/src/devcouncil/llm/cache.py +38 -0
- package/src/devcouncil/llm/provider.py +125 -0
- package/src/devcouncil/llm/router.py +125 -0
- package/src/devcouncil/planning/__init__.py +1 -0
- package/src/devcouncil/planning/arbiter_service.py +57 -0
- package/src/devcouncil/planning/critique_service.py +66 -0
- package/src/devcouncil/planning/plan_service.py +46 -0
- package/src/devcouncil/planning/repair_service.py +39 -0
- package/src/devcouncil/planning/spec_service.py +44 -0
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/reporting/__init__.py +0 -0
- package/src/devcouncil/reporting/github_check.py +32 -0
- package/src/devcouncil/reporting/json_report.py +17 -0
- package/src/devcouncil/reporting/markdown_report.py +46 -0
- package/src/devcouncil/reporting/report_builder.py +14 -0
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +66 -0
- package/src/devcouncil/storage/models.py +83 -0
- package/src/devcouncil/storage/repositories.py +346 -0
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +34 -0
- package/src/devcouncil/telemetry/traces.py +91 -0
- package/src/devcouncil/telemetry/tracker.py +49 -0
- package/src/devcouncil/utils/__init__.py +1 -0
- package/src/devcouncil/utils/redaction.py +141 -0
- package/src/devcouncil/verification/__init__.py +1 -0
- package/src/devcouncil/verification/implementation_reviewer.py +55 -0
- package/src/devcouncil/verification/verifier.py +513 -0
- package/uv.lock +1085 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import json
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from devcouncil.storage.db import get_db
|
|
6
|
+
from devcouncil.storage.repositories import TaskRepository, RequirementRepository
|
|
7
|
+
from devcouncil.executors.mini_swe import MiniSWEExecutor
|
|
8
|
+
from devcouncil.executors.openhands import OpenHandsExecutor
|
|
9
|
+
from devcouncil.executors.native.agent import NativeAgent
|
|
10
|
+
from devcouncil.llm.provider import OpenRouterProvider
|
|
11
|
+
from devcouncil.llm.router import ModelRouter
|
|
12
|
+
from devcouncil.app.config import load_config, get_api_key
|
|
13
|
+
from devcouncil.domain.evidence import CommandResult, DiffEvidence, TestEvidence
|
|
14
|
+
from devcouncil.storage.repositories import GapRepository, EvidenceRepository, StateRepository
|
|
15
|
+
from devcouncil.verification.verifier import Verifier
|
|
16
|
+
from devcouncil.app.state_machine import ProjectPhase
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
def _current_changed_files() -> list[str]:
|
|
21
|
+
from devcouncil.verification.verifier import Verifier
|
|
22
|
+
|
|
23
|
+
return Verifier(Path(".")).get_changed_files()
|
|
24
|
+
|
|
25
|
+
def _capture_after_patch(task_id: str):
|
|
26
|
+
"""Capture the diff after task execution for use by rollback."""
|
|
27
|
+
try:
|
|
28
|
+
from devcouncil.verification.verifier import Verifier
|
|
29
|
+
|
|
30
|
+
checkpoint_dir = Path(".devcouncil/checkpoints")
|
|
31
|
+
checkpoint_dir.mkdir(exist_ok=True)
|
|
32
|
+
diff = Verifier(Path(".")).get_diff()
|
|
33
|
+
if diff:
|
|
34
|
+
with open(checkpoint_dir / f"{task_id}-after.patch", "w", encoding="utf-8") as f:
|
|
35
|
+
f.write(diff)
|
|
36
|
+
except Exception:
|
|
37
|
+
pass # Non-critical — don't block execution
|
|
38
|
+
|
|
39
|
+
def _capture_before_snapshot(task_id: str):
|
|
40
|
+
checkpoint_dir = Path(".devcouncil/checkpoints")
|
|
41
|
+
checkpoint_dir.mkdir(exist_ok=True)
|
|
42
|
+
snapshot = {
|
|
43
|
+
"task_id": task_id,
|
|
44
|
+
"changed_files": _current_changed_files(),
|
|
45
|
+
}
|
|
46
|
+
(checkpoint_dir / f"{task_id}-before.json").write_text(
|
|
47
|
+
json.dumps(snapshot, indent=2),
|
|
48
|
+
encoding="utf-8",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
def _record_project_phase(session, phase: ProjectPhase):
|
|
52
|
+
StateRepository(session).record_phase(phase.value)
|
|
53
|
+
|
|
54
|
+
def _verify_after_execution(session, task, reqs, router=None) -> bool:
|
|
55
|
+
"""Run deterministic verification after an automated executor finishes."""
|
|
56
|
+
import asyncio
|
|
57
|
+
|
|
58
|
+
verifier = Verifier(Path("."), router=router)
|
|
59
|
+
gaps, evidence = asyncio.run(verifier.verify_task(task, reqs))
|
|
60
|
+
|
|
61
|
+
gap_repo = GapRepository(session)
|
|
62
|
+
evidence_repo = EvidenceRepository(session)
|
|
63
|
+
gap_repo.delete_for_task(task.id)
|
|
64
|
+
evidence_repo.delete_for_task(task.id)
|
|
65
|
+
|
|
66
|
+
for gap in gaps:
|
|
67
|
+
gap_repo.save(gap)
|
|
68
|
+
|
|
69
|
+
for ev in evidence:
|
|
70
|
+
if isinstance(ev, CommandResult):
|
|
71
|
+
evidence_repo.save_command_result(task.id, ev)
|
|
72
|
+
elif isinstance(ev, DiffEvidence):
|
|
73
|
+
evidence_repo.save_diff_evidence(ev)
|
|
74
|
+
elif isinstance(ev, TestEvidence):
|
|
75
|
+
evidence_repo.save_test_evidence(ev, task.id)
|
|
76
|
+
|
|
77
|
+
task.status = "blocked" if any(g.blocking for g in gaps) else "verified"
|
|
78
|
+
return task.status == "verified"
|
|
79
|
+
|
|
80
|
+
def run(
|
|
81
|
+
task_id: str = typer.Argument(..., help="ID of the task to run"),
|
|
82
|
+
executor: str = typer.Option("manual", "--executor", "-e", help="Executor to use (manual, shell)"),
|
|
83
|
+
):
|
|
84
|
+
"""
|
|
85
|
+
Execute a specific task.
|
|
86
|
+
"""
|
|
87
|
+
db = get_db()
|
|
88
|
+
if not db:
|
|
89
|
+
console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
with db.get_session() as session:
|
|
93
|
+
task_repo = TaskRepository(session)
|
|
94
|
+
task = task_repo.get_by_id(task_id)
|
|
95
|
+
if not task:
|
|
96
|
+
console.print(f"[red]Task {task_id} not found.[/red]")
|
|
97
|
+
return
|
|
98
|
+
|
|
99
|
+
from devcouncil.gating.policy import GatePolicy
|
|
100
|
+
policy = GatePolicy()
|
|
101
|
+
gate_result = policy.check_task_ready(task, Path("."))
|
|
102
|
+
if not gate_result.passed:
|
|
103
|
+
console.print(f"[red]Task {task_id} is not ready for execution.[/red]")
|
|
104
|
+
for gap in gate_result.gaps:
|
|
105
|
+
if gap.blocking:
|
|
106
|
+
console.print(f" - [red][BLOCKING][/red] {gap.description} (Fix: {gap.recommended_fix})")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
console.print(f"Running task [bold]{task_id}[/bold] using [bold]{executor}[/bold] executor...")
|
|
110
|
+
|
|
111
|
+
# 1. Create Git checkpoint
|
|
112
|
+
try:
|
|
113
|
+
from devcouncil.verification.verifier import Verifier
|
|
114
|
+
|
|
115
|
+
checkpoint_dir = Path(".devcouncil/checkpoints")
|
|
116
|
+
checkpoint_dir.mkdir(exist_ok=True)
|
|
117
|
+
_capture_before_snapshot(task_id)
|
|
118
|
+
diff = Verifier(Path(".")).get_diff()
|
|
119
|
+
if diff:
|
|
120
|
+
with open(checkpoint_dir / f"{task_id}-before.patch", "w", encoding="utf-8") as f:
|
|
121
|
+
f.write(diff)
|
|
122
|
+
console.print(f"Created git checkpoint at {checkpoint_dir}/{task_id}-before.patch")
|
|
123
|
+
except Exception as e:
|
|
124
|
+
console.print(f"[yellow]Warning: Failed to create git checkpoint: {e}[/yellow]")
|
|
125
|
+
|
|
126
|
+
if executor == "manual":
|
|
127
|
+
_record_project_phase(session, ProjectPhase.TASK_EXECUTING)
|
|
128
|
+
task.status = "running"
|
|
129
|
+
task_repo.save(task)
|
|
130
|
+
console.print(f"\n[green]Task {task_id} is now marked as RUNNING.[/green]")
|
|
131
|
+
console.print("Use 'dev prompt TASK-ID' to get the prompt for this task.")
|
|
132
|
+
console.print("When finished, use 'dev verify TASK-ID' to check the results.")
|
|
133
|
+
elif executor == "mini":
|
|
134
|
+
_record_project_phase(session, ProjectPhase.TASK_EXECUTING)
|
|
135
|
+
req_repo = RequirementRepository(session)
|
|
136
|
+
reqs = req_repo.get_all()
|
|
137
|
+
mini_executor = MiniSWEExecutor(Path("."))
|
|
138
|
+
exec_result = mini_executor.run_task(task, reqs)
|
|
139
|
+
_capture_after_patch(task_id)
|
|
140
|
+
if exec_result.success:
|
|
141
|
+
_record_project_phase(session, ProjectPhase.TASK_VERIFYING)
|
|
142
|
+
verified = _verify_after_execution(session, task, reqs)
|
|
143
|
+
_record_project_phase(
|
|
144
|
+
session,
|
|
145
|
+
ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
|
|
146
|
+
)
|
|
147
|
+
task_repo.save(task)
|
|
148
|
+
if verified:
|
|
149
|
+
console.print(f"\n[green]mini-SWE-agent finished and task {task_id} verified.[/green]")
|
|
150
|
+
else:
|
|
151
|
+
console.print(f"\n[yellow]mini-SWE-agent finished, but task {task_id} is blocked by verification gaps.[/yellow]")
|
|
152
|
+
else:
|
|
153
|
+
console.print("\n[red]mini-SWE-agent failed to start or execute.[/red]")
|
|
154
|
+
elif executor == "openhands":
|
|
155
|
+
_record_project_phase(session, ProjectPhase.TASK_EXECUTING)
|
|
156
|
+
req_repo = RequirementRepository(session)
|
|
157
|
+
reqs = req_repo.get_all()
|
|
158
|
+
oh_executor = OpenHandsExecutor(Path("."))
|
|
159
|
+
exec_result = oh_executor.run_task(task, reqs)
|
|
160
|
+
_capture_after_patch(task_id)
|
|
161
|
+
if exec_result.success:
|
|
162
|
+
_record_project_phase(session, ProjectPhase.TASK_VERIFYING)
|
|
163
|
+
verified = _verify_after_execution(session, task, reqs)
|
|
164
|
+
_record_project_phase(
|
|
165
|
+
session,
|
|
166
|
+
ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
|
|
167
|
+
)
|
|
168
|
+
task_repo.save(task)
|
|
169
|
+
if verified:
|
|
170
|
+
console.print(f"\n[green]OpenHands finished and task {task_id} verified.[/green]")
|
|
171
|
+
else:
|
|
172
|
+
console.print(f"\n[yellow]OpenHands finished, but task {task_id} is blocked by verification gaps.[/yellow]")
|
|
173
|
+
else:
|
|
174
|
+
console.print("\n[red]OpenHands failed to start or execute.[/red]")
|
|
175
|
+
elif executor == "native":
|
|
176
|
+
# Load config for model routing and permissions
|
|
177
|
+
try:
|
|
178
|
+
config = load_config(Path("."))
|
|
179
|
+
api_key = get_api_key(config.models.provider)
|
|
180
|
+
except (FileNotFoundError, ValueError) as e:
|
|
181
|
+
console.print(f"[red]{e}[/red]")
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
provider = OpenRouterProvider(api_key)
|
|
185
|
+
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
186
|
+
router = ModelRouter(provider, role_config)
|
|
187
|
+
|
|
188
|
+
# Setup Permission System
|
|
189
|
+
from devcouncil.execution.permissions import PermissionPolicy, PermissionManager
|
|
190
|
+
from devcouncil.execution.task_runner import TaskRunner
|
|
191
|
+
|
|
192
|
+
# Populate policy from config commands
|
|
193
|
+
allowed_cmds = config.commands.test + config.commands.lint + config.commands.typecheck
|
|
194
|
+
policy = PermissionPolicy(
|
|
195
|
+
allowed_shell_commands=allowed_cmds,
|
|
196
|
+
)
|
|
197
|
+
perm_manager = PermissionManager(policy, Path("."))
|
|
198
|
+
task_runner = TaskRunner(Path("."), perm_manager)
|
|
199
|
+
|
|
200
|
+
req_repo = RequirementRepository(session)
|
|
201
|
+
reqs = req_repo.get_all()
|
|
202
|
+
|
|
203
|
+
import asyncio
|
|
204
|
+
_record_project_phase(session, ProjectPhase.TASK_EXECUTING)
|
|
205
|
+
agent = NativeAgent(router, task_runner)
|
|
206
|
+
exec_result = asyncio.run(agent.run_task(task, reqs))
|
|
207
|
+
_capture_after_patch(task_id)
|
|
208
|
+
|
|
209
|
+
if exec_result.success:
|
|
210
|
+
_record_project_phase(session, ProjectPhase.TASK_VERIFYING)
|
|
211
|
+
verified = _verify_after_execution(session, task, reqs, router=router)
|
|
212
|
+
_record_project_phase(
|
|
213
|
+
session,
|
|
214
|
+
ProjectPhase.TASK_VERIFIED if verified else ProjectPhase.TASK_BLOCKED,
|
|
215
|
+
)
|
|
216
|
+
task_repo.save(task)
|
|
217
|
+
if verified:
|
|
218
|
+
console.print(f"\n[green]Native agent finished and task {task_id} verified.[/green]")
|
|
219
|
+
else:
|
|
220
|
+
console.print(f"\n[yellow]Native agent finished, but task {task_id} is blocked by verification gaps.[/yellow]")
|
|
221
|
+
else:
|
|
222
|
+
console.print("\n[red]Native agent failed during execution.[/red]")
|
|
223
|
+
else:
|
|
224
|
+
console.print(f"[red]Executor {executor} not yet implemented.[/red]")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import shutil
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
|
|
8
|
+
from devcouncil.cli.commands.doctor import render_doctor_check
|
|
9
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
10
|
+
from devcouncil.cli.commands.integrate import _codex_command, _configure, _gemini_command
|
|
11
|
+
|
|
12
|
+
app = typer.Typer()
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@app.callback(invoke_without_command=True)
|
|
17
|
+
def setup(
|
|
18
|
+
ctx: typer.Context,
|
|
19
|
+
project_root: Path = typer.Option(
|
|
20
|
+
Path("."),
|
|
21
|
+
"--project-root",
|
|
22
|
+
help="Target project repository root. Defaults to the terminal's current directory.",
|
|
23
|
+
),
|
|
24
|
+
name: str | None = typer.Option(None, "--name", "-n", help="Project name for .devcouncil/config.yaml."),
|
|
25
|
+
integrate: bool = typer.Option(False, "--integrate", help="Configure supported coding CLI MCP integrations."),
|
|
26
|
+
apply: bool = typer.Option(False, "--apply", help="Apply integration config instead of previewing commands."),
|
|
27
|
+
gemini_scope: str = typer.Option("project", "--gemini-scope", help="Gemini MCP config scope: project or user."),
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Initialize DevCouncil from a normal terminal in the target repository root.
|
|
31
|
+
|
|
32
|
+
Use the coding CLI later only for the generated dev prompt output.
|
|
33
|
+
"""
|
|
34
|
+
if ctx.invoked_subcommand is not None:
|
|
35
|
+
return
|
|
36
|
+
|
|
37
|
+
if gemini_scope not in {"project", "user"}:
|
|
38
|
+
console.print("[red]--gemini-scope must be 'project' or 'user'.[/red]")
|
|
39
|
+
raise typer.Exit(code=2)
|
|
40
|
+
|
|
41
|
+
root = project_root.expanduser().resolve()
|
|
42
|
+
created = initialize_project(root, project_name=name)
|
|
43
|
+
if not created:
|
|
44
|
+
console.print(f"[yellow]DevCouncil is already initialized at {root / '.devcouncil'}.[/yellow]")
|
|
45
|
+
|
|
46
|
+
console.print()
|
|
47
|
+
render_doctor_check()
|
|
48
|
+
|
|
49
|
+
if integrate:
|
|
50
|
+
console.print()
|
|
51
|
+
console.print("[bold]Coding CLI integration[/bold]")
|
|
52
|
+
commands = [
|
|
53
|
+
("Codex CLI", _codex_command(root)),
|
|
54
|
+
("Gemini CLI", _gemini_command(root, gemini_scope)),
|
|
55
|
+
]
|
|
56
|
+
results = []
|
|
57
|
+
for tool, command in commands:
|
|
58
|
+
if apply and not shutil.which(command[0]):
|
|
59
|
+
console.print(f"[yellow]{tool} CLI not found on PATH. Skipping optional integration.[/yellow]")
|
|
60
|
+
continue
|
|
61
|
+
results.append(_configure(tool, command, apply))
|
|
62
|
+
if apply and any(not ok for ok in results):
|
|
63
|
+
raise typer.Exit(code=1)
|
|
64
|
+
|
|
65
|
+
console.print()
|
|
66
|
+
console.print(Panel.fit(
|
|
67
|
+
"\n".join([
|
|
68
|
+
"[bold]Next commands[/bold]",
|
|
69
|
+
f"Keep running DevCouncil commands in this terminal at: {root}",
|
|
70
|
+
"dev plan \"Describe the implementation goal\"",
|
|
71
|
+
"dev tasks",
|
|
72
|
+
"dev run TASK-001 --executor manual",
|
|
73
|
+
"dev prompt TASK-001",
|
|
74
|
+
"Paste only the dev prompt output into your coding CLI.",
|
|
75
|
+
"dev verify TASK-001",
|
|
76
|
+
"",
|
|
77
|
+
"Use [bold]dev setup --integrate[/bold] to preview coding CLI MCP setup.",
|
|
78
|
+
"Use [bold]dev setup --integrate --apply[/bold] to configure detected clients.",
|
|
79
|
+
]),
|
|
80
|
+
title="DevCouncil is ready",
|
|
81
|
+
border_style="green",
|
|
82
|
+
))
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from devcouncil.storage.db import get_db
|
|
5
|
+
from devcouncil.storage.repositories import TaskRepository, RequirementRepository
|
|
6
|
+
|
|
7
|
+
app = typer.Typer()
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
@app.callback(invoke_without_command=True)
|
|
11
|
+
def show(
|
|
12
|
+
ctx: typer.Context,
|
|
13
|
+
task_id: str = typer.Argument(..., help="ID of the task to show"),
|
|
14
|
+
):
|
|
15
|
+
"""
|
|
16
|
+
Show details of a specific task.
|
|
17
|
+
"""
|
|
18
|
+
if ctx.invoked_subcommand is not None:
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
db = get_db()
|
|
22
|
+
if not db:
|
|
23
|
+
console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
|
|
24
|
+
raise typer.Exit(code=1)
|
|
25
|
+
|
|
26
|
+
with db.get_session() as session:
|
|
27
|
+
task_repo = TaskRepository(session)
|
|
28
|
+
req_repo = RequirementRepository(session)
|
|
29
|
+
|
|
30
|
+
task = task_repo.get_by_id(task_id)
|
|
31
|
+
if not task:
|
|
32
|
+
console.print(f"[red]Task {task_id} not found.[/red]")
|
|
33
|
+
raise typer.Exit(code=1)
|
|
34
|
+
|
|
35
|
+
reqs = req_repo.get_all()
|
|
36
|
+
req_map = {r.id: r for r in reqs}
|
|
37
|
+
|
|
38
|
+
output = f"[bold]Status:[/bold] {task.status}\n\n"
|
|
39
|
+
output += f"[bold]Description:[/bold]\n{task.description}\n\n"
|
|
40
|
+
|
|
41
|
+
output += "[bold]Linked Requirements:[/bold]\n"
|
|
42
|
+
for req_id in task.requirement_ids:
|
|
43
|
+
req = req_map.get(req_id)
|
|
44
|
+
if req:
|
|
45
|
+
output += f" - [cyan]{req.id}[/cyan]: {req.title}\n"
|
|
46
|
+
else:
|
|
47
|
+
output += f" - [cyan]{req_id}[/cyan]: (Requirement not found)\n"
|
|
48
|
+
|
|
49
|
+
output += "\n[bold]Planned Files:[/bold]\n"
|
|
50
|
+
for pf in task.planned_files:
|
|
51
|
+
output += f" - {pf.path} ({pf.allowed_change}): {pf.reason}\n"
|
|
52
|
+
|
|
53
|
+
output += "\n[bold]Expected Tests:[/bold]\n"
|
|
54
|
+
for et in task.expected_tests:
|
|
55
|
+
output += f" - {et}\n"
|
|
56
|
+
|
|
57
|
+
console.print(Panel(output, title=f"Task {task.id}: {task.title}", expand=False))
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.table import Table
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import json
|
|
6
|
+
from devcouncil.storage.db import get_db
|
|
7
|
+
from devcouncil.storage.repositories import ArtifactGraphRepository
|
|
8
|
+
from devcouncil.telemetry.cost import CostEstimator
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
def status():
|
|
13
|
+
"""
|
|
14
|
+
Show the current status of the DevCouncil project.
|
|
15
|
+
"""
|
|
16
|
+
db = get_db()
|
|
17
|
+
if not db:
|
|
18
|
+
console.print("[yellow]DevCouncil not initialized in this directory.[/yellow]")
|
|
19
|
+
console.print("Run [bold]dev init[/bold] to get started.")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
with db.get_session() as session:
|
|
23
|
+
graph_repo = ArtifactGraphRepository(session)
|
|
24
|
+
graph = graph_repo.load_graph()
|
|
25
|
+
summary = graph.coverage_summary()
|
|
26
|
+
|
|
27
|
+
reqs = list(graph.requirements.values())
|
|
28
|
+
tasks = list(graph.tasks.values())
|
|
29
|
+
blocking_gaps = graph.blocking_gaps()
|
|
30
|
+
|
|
31
|
+
# Determine phase
|
|
32
|
+
if not reqs and not tasks:
|
|
33
|
+
phase = "NEW"
|
|
34
|
+
elif reqs and not tasks:
|
|
35
|
+
phase = "REQUIREMENTS_DRAFTED"
|
|
36
|
+
elif blocking_gaps:
|
|
37
|
+
phase = "TASK_BLOCKED"
|
|
38
|
+
elif tasks:
|
|
39
|
+
statuses = {t.status for t in tasks}
|
|
40
|
+
if "running" in statuses:
|
|
41
|
+
phase = "TASK_EXECUTING"
|
|
42
|
+
elif "blocked" in statuses:
|
|
43
|
+
phase = "TASK_BLOCKED"
|
|
44
|
+
elif all(s in ("verified", "done") for s in statuses):
|
|
45
|
+
phase = "PROJECT_DONE"
|
|
46
|
+
else:
|
|
47
|
+
phase = "PLAN_APPROVED"
|
|
48
|
+
else:
|
|
49
|
+
phase = "NEW"
|
|
50
|
+
|
|
51
|
+
# Phase color
|
|
52
|
+
phase_colors = {
|
|
53
|
+
"NEW": "yellow",
|
|
54
|
+
"REQUIREMENTS_DRAFTED": "cyan",
|
|
55
|
+
"PLAN_APPROVED": "green",
|
|
56
|
+
"TASK_EXECUTING": "blue",
|
|
57
|
+
"TASK_BLOCKED": "red",
|
|
58
|
+
"PROJECT_DONE": "green bold",
|
|
59
|
+
}
|
|
60
|
+
phase_color = phase_colors.get(phase, "white")
|
|
61
|
+
|
|
62
|
+
# Calculate cost
|
|
63
|
+
total_cost = 0.0
|
|
64
|
+
log_file = Path(".devcouncil/logs/model_calls.jsonl")
|
|
65
|
+
if log_file.exists():
|
|
66
|
+
with open(log_file, "r", encoding="utf-8") as f:
|
|
67
|
+
for line in f:
|
|
68
|
+
try:
|
|
69
|
+
entry = json.loads(line)
|
|
70
|
+
total_cost += CostEstimator.estimate_cost(
|
|
71
|
+
entry.get("response", {}).get("model", ""),
|
|
72
|
+
entry.get("usage", {})
|
|
73
|
+
)
|
|
74
|
+
except Exception:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
console.print(Panel(
|
|
78
|
+
f"[bold]Phase:[/bold] [{phase_color}]{phase}[/{phase_color}]\n"
|
|
79
|
+
f"[bold]Requirements:[/bold] {summary['total_requirements']} ({summary['requirements_without_tasks']} unmapped)\n"
|
|
80
|
+
f"[bold]Tasks:[/bold] {summary['total_tasks']} ({summary['tasks_without_requirements']} orphaned)\n"
|
|
81
|
+
f"[bold]Acceptance Criteria:[/bold] {summary['total_ac']} ({summary['ac_without_evidence']} unverified)\n"
|
|
82
|
+
f"[bold]Gaps:[/bold] {summary['total_gaps']} ({summary['blocking_gaps']} blocking)\n"
|
|
83
|
+
f"[bold]Total Cost:[/bold] ${total_cost:.4f}",
|
|
84
|
+
title="DevCouncil Status",
|
|
85
|
+
expand=False,
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
if tasks:
|
|
89
|
+
table = Table(title="Task Summary")
|
|
90
|
+
table.add_column("Status", style="magenta")
|
|
91
|
+
table.add_column("Count", justify="right")
|
|
92
|
+
|
|
93
|
+
status_counts: dict[str, int] = {}
|
|
94
|
+
for t in tasks:
|
|
95
|
+
status_counts[t.status] = status_counts.get(t.status, 0) + 1
|
|
96
|
+
for s, count in sorted(status_counts.items()):
|
|
97
|
+
table.add_row(s, str(count))
|
|
98
|
+
console.print(table)
|
|
99
|
+
|
|
100
|
+
if blocking_gaps:
|
|
101
|
+
console.print(f"\n[red bold]WARNING: {len(blocking_gaps)} blocking gap(s) must be resolved:[/red bold]")
|
|
102
|
+
for g in blocking_gaps[:5]:
|
|
103
|
+
console.print(f" - [red]{g.id}[/red]: {g.description[:80]}")
|
|
104
|
+
if len(blocking_gaps) > 5:
|
|
105
|
+
console.print(f" ... and {len(blocking_gaps) - 5} more. Run [bold]dev report[/bold] for details.")
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.table import Table
|
|
4
|
+
from devcouncil.storage.db import get_db
|
|
5
|
+
from devcouncil.storage.repositories import TaskRepository
|
|
6
|
+
|
|
7
|
+
app = typer.Typer()
|
|
8
|
+
console = Console()
|
|
9
|
+
|
|
10
|
+
@app.callback(invoke_without_command=True)
|
|
11
|
+
def tasks(ctx: typer.Context):
|
|
12
|
+
"""
|
|
13
|
+
List task graph and task gate status.
|
|
14
|
+
"""
|
|
15
|
+
if ctx.invoked_subcommand is not None:
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
db = get_db()
|
|
19
|
+
if not db:
|
|
20
|
+
console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
|
|
21
|
+
raise typer.Exit(code=1)
|
|
22
|
+
|
|
23
|
+
with db.get_session() as session:
|
|
24
|
+
task_repo = TaskRepository(session)
|
|
25
|
+
tasks_list = task_repo.get_all()
|
|
26
|
+
|
|
27
|
+
if not tasks_list:
|
|
28
|
+
console.print("No tasks found. Run 'dev plan' to generate tasks.")
|
|
29
|
+
return
|
|
30
|
+
|
|
31
|
+
table = Table(title="DevCouncil Tasks")
|
|
32
|
+
table.add_column("Task ID", style="cyan", no_wrap=True)
|
|
33
|
+
table.add_column("Title", style="white")
|
|
34
|
+
table.add_column("Status", style="magenta")
|
|
35
|
+
table.add_column("Linked Reqs", style="green")
|
|
36
|
+
|
|
37
|
+
for t in tasks_list:
|
|
38
|
+
reqs = ", ".join(t.requirement_ids)
|
|
39
|
+
table.add_row(t.id, t.title, t.status, reqs)
|
|
40
|
+
|
|
41
|
+
console.print(table)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from devcouncil.telemetry.traces import read_trace_events
|
|
9
|
+
|
|
10
|
+
app = typer.Typer(help="Inspect DevCouncil trace events.")
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command("tail")
|
|
15
|
+
def tail(
|
|
16
|
+
follow: bool = typer.Option(False, "--follow", "-f", help="Continue polling for new events."),
|
|
17
|
+
limit: int = typer.Option(50, "--limit", "-n", help="Maximum events to print before following."),
|
|
18
|
+
jsonl: bool = typer.Option(True, "--jsonl/--pretty", help="Print JSONL or compact text rows."),
|
|
19
|
+
):
|
|
20
|
+
"""Print the DevCouncil trace JSONL stream for replay or debugging."""
|
|
21
|
+
project_root = Path(".")
|
|
22
|
+
printed = 0
|
|
23
|
+
|
|
24
|
+
def emit_new(start_index: int) -> int:
|
|
25
|
+
events = list(read_trace_events(project_root))
|
|
26
|
+
selected = events[start_index:]
|
|
27
|
+
for event in selected:
|
|
28
|
+
if jsonl:
|
|
29
|
+
typer.echo(event.model_dump_json())
|
|
30
|
+
else:
|
|
31
|
+
console.print(
|
|
32
|
+
f"{event.timestamp} {event.type} "
|
|
33
|
+
f"{event.task_id or '-'} {event.summary or json.dumps(event.details)}"
|
|
34
|
+
)
|
|
35
|
+
return len(events)
|
|
36
|
+
|
|
37
|
+
events = list(read_trace_events(project_root))
|
|
38
|
+
start = max(0, len(events) - limit)
|
|
39
|
+
printed = emit_new(start)
|
|
40
|
+
|
|
41
|
+
while follow:
|
|
42
|
+
time.sleep(1)
|
|
43
|
+
printed = emit_new(printed)
|