devcouncil 0.1.0 → 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/LICENSE +201 -201
- package/README.md +197 -494
- package/package.json +9 -2
- package/pyproject.toml +62 -27
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +297 -108
- package/src/devcouncil/app/errors.py +23 -23
- package/src/devcouncil/app/events.py +44 -44
- package/src/devcouncil/app/orchestrator.py +67 -67
- package/src/devcouncil/app/project_status.py +29 -0
- package/src/devcouncil/app/run_context.py +39 -39
- package/src/devcouncil/app/state_machine.py +108 -108
- package/src/devcouncil/artifacts/__init__.py +1 -1
- package/src/devcouncil/artifacts/coverage.py +96 -96
- package/src/devcouncil/artifacts/graph.py +163 -143
- package/src/devcouncil/artifacts/migrations.py +20 -20
- package/src/devcouncil/artifacts/schemas.py +23 -23
- package/src/devcouncil/artifacts/serializer.py +21 -21
- package/src/devcouncil/artifacts/validators.py +27 -27
- 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 +54 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/check.py +209 -0
- package/src/devcouncil/cli/commands/config.py +115 -54
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +31 -0
- package/src/devcouncil/cli/commands/doctor.py +291 -47
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +656 -0
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +209 -33
- package/src/devcouncil/cli/commands/init.py +204 -57
- package/src/devcouncil/cli/commands/integrate.py +1171 -76
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +96 -22
- package/src/devcouncil/cli/commands/plan.py +422 -210
- package/src/devcouncil/cli/commands/prompt.py +48 -34
- package/src/devcouncil/cli/commands/repair.py +89 -69
- package/src/devcouncil/cli/commands/report.py +120 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
- package/src/devcouncil/cli/commands/rollback.py +55 -54
- package/src/devcouncil/cli/commands/run.py +285 -220
- 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 +300 -20
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/skills.py +88 -0
- package/src/devcouncil/cli/commands/status.py +141 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +49 -4
- package/src/devcouncil/cli/commands/verify.py +293 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +92 -25
- package/src/devcouncil/council/prompts/arbiter.md +19 -19
- package/src/devcouncil/council/prompts/critic_a.md +10 -10
- package/src/devcouncil/council/prompts/critic_b.md +10 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
- package/src/devcouncil/council/prompts/planner_a.md +16 -16
- package/src/devcouncil/council/prompts/planner_b.md +16 -16
- package/src/devcouncil/council/prompts/rebuttal.md +10 -10
- package/src/devcouncil/council/prompts/spec_writer.md +12 -12
- package/src/devcouncil/domain/assumption.py +17 -17
- package/src/devcouncil/domain/critique.py +32 -32
- package/src/devcouncil/domain/evidence.py +47 -27
- package/src/devcouncil/domain/gap.py +52 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +55 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/checkpoints.py +246 -0
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/fs_watcher.py +180 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +186 -77
- package/src/devcouncil/execution/patch.py +77 -28
- package/src/devcouncil/execution/permissions.py +52 -59
- package/src/devcouncil/execution/policy_engine.py +343 -0
- package/src/devcouncil/execution/prompt_builder.py +650 -38
- package/src/devcouncil/execution/shell_session.py +225 -0
- package/src/devcouncil/execution/task_runner.py +68 -64
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/agent_registry.py +575 -0
- package/src/devcouncil/executors/coding_cli.py +736 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +186 -85
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +52 -45
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
- package/src/devcouncil/gating/policy.py +315 -167
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/__init__.py +1 -1
- package/src/devcouncil/indexing/ast_matcher.py +168 -0
- package/src/devcouncil/indexing/graph_index.py +48 -48
- package/src/devcouncil/indexing/lsp.py +161 -0
- package/src/devcouncil/indexing/repo_mapper.py +1455 -204
- 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.py +35 -35
- package/src/devcouncil/integrations/github_intent.py +142 -0
- package/src/devcouncil/integrations/gitnexus.py +62 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +2072 -96
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +349 -0
- package/src/devcouncil/live/models.py +63 -0
- package/src/devcouncil/live/repair_prompt.py +83 -0
- package/src/devcouncil/live/reviewer.py +70 -0
- package/src/devcouncil/live/signals.py +135 -0
- package/src/devcouncil/live/summary.py +34 -0
- package/src/devcouncil/live/tasks.py +18 -0
- package/src/devcouncil/live/transcripts.py +141 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +42 -38
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +627 -125
- package/src/devcouncil/llm/router.py +303 -118
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/correction_manifest.py +303 -0
- package/src/devcouncil/planning/critique_service.py +71 -66
- package/src/devcouncil/planning/plan_service.py +60 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +70 -44
- 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/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +30 -17
- package/src/devcouncil/reporting/markdown_report.py +83 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- 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 +147 -66
- package/src/devcouncil/storage/models.py +204 -83
- package/src/devcouncil/storage/native.py +557 -0
- package/src/devcouncil/storage/repositories.py +388 -249
- package/src/devcouncil/telemetry/cost.py +140 -34
- 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 +52 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +423 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +147 -141
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/__init__.py +1 -1
- 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/implementation_reviewer.py +55 -55
- 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 +1342 -307
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from typing import NoReturn
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
9
|
+
from devcouncil.execution.handoff import HandoffService
|
|
10
|
+
from devcouncil.executors.agent_registry import load_cli_agent_specs, normalize_agent_name
|
|
11
|
+
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handoff(
|
|
16
|
+
task_id: str = typer.Argument(...),
|
|
17
|
+
from_agent: str = typer.Option(..., "--from"),
|
|
18
|
+
to_agent: str = typer.Option(..., "--to"),
|
|
19
|
+
instruction: str = typer.Option("", "--instruction"),
|
|
20
|
+
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON the agent can chain on."),
|
|
21
|
+
project_root: Path = typer.Option(Path("."), "--project-root"),
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Hand off a task between coding CLI agents.
|
|
25
|
+
"""
|
|
26
|
+
def _fail(message: str) -> NoReturn:
|
|
27
|
+
if json_format:
|
|
28
|
+
typer.echo(json.dumps({"ok": False, "task_id": task_id, "error": message}, indent=2))
|
|
29
|
+
else:
|
|
30
|
+
console.print(f"[red]{message}[/red]")
|
|
31
|
+
raise typer.Exit(code=1)
|
|
32
|
+
|
|
33
|
+
root = project_root.expanduser().resolve()
|
|
34
|
+
initialize_project(root, quiet=True)
|
|
35
|
+
specs = load_cli_agent_specs(root)
|
|
36
|
+
from_name = normalize_agent_name(from_agent)
|
|
37
|
+
to_name = normalize_agent_name(to_agent)
|
|
38
|
+
if from_name not in specs or to_name not in specs:
|
|
39
|
+
_fail("Unknown agent name. Use dev agents list.")
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
manifest, path, run_id = HandoffService(root).create(
|
|
43
|
+
task_id,
|
|
44
|
+
from_name,
|
|
45
|
+
to_name,
|
|
46
|
+
instruction=instruction,
|
|
47
|
+
)
|
|
48
|
+
except ValueError as exc:
|
|
49
|
+
_fail(str(exc))
|
|
50
|
+
|
|
51
|
+
next_command = f"dev run {task_id} --executor {to_name}"
|
|
52
|
+
if json_format:
|
|
53
|
+
typer.echo(json.dumps({
|
|
54
|
+
"ok": True,
|
|
55
|
+
"task_id": task_id,
|
|
56
|
+
"from": from_name,
|
|
57
|
+
"to": to_name,
|
|
58
|
+
"manifest_path": str(path),
|
|
59
|
+
"run_id": run_id,
|
|
60
|
+
"next_command": next_command,
|
|
61
|
+
}, indent=2))
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
console.print(f"[green]Handoff manifest:[/green] {path}")
|
|
65
|
+
console.print(f"[cyan]Next:[/cyan] {next_command}")
|
|
66
|
+
console.print(f"[dim]Run artifacts: .devcouncil/runs/{run_id}[/dim]")
|
|
67
|
+
if instruction:
|
|
68
|
+
console.print(f"[dim]Instruction: {instruction}[/dim]")
|
|
69
|
+
_ = manifest
|
|
@@ -7,55 +7,231 @@ from rich.console import Console
|
|
|
7
7
|
from devcouncil.storage.db import get_db
|
|
8
8
|
from devcouncil.storage.repositories import TaskRepository
|
|
9
9
|
from devcouncil.execution.hook_policy import HookPolicy
|
|
10
|
+
from devcouncil.telemetry.traces import TraceLogger
|
|
11
|
+
from devcouncil.live.signals import write_signal
|
|
12
|
+
from devcouncil.live.tasks import active_task_id
|
|
10
13
|
|
|
11
14
|
app = typer.Typer()
|
|
12
15
|
console = Console()
|
|
13
16
|
|
|
14
17
|
|
|
15
|
-
def _project_root() -> Path:
|
|
18
|
+
def _project_root(project_root: Path | None = None) -> Path:
|
|
19
|
+
if project_root:
|
|
20
|
+
return project_root.expanduser().resolve()
|
|
16
21
|
configured = os.environ.get("DEVCOUNCIL_PROJECT_ROOT")
|
|
17
22
|
return Path(configured).expanduser().resolve() if configured else Path(".").resolve()
|
|
18
|
-
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _active_task(root: Path):
|
|
26
|
+
# Resolve the *single* unambiguous running task. active_task_id returns None when
|
|
27
|
+
# zero or multiple tasks are running, so we never authorize a write against the
|
|
28
|
+
# wrong task; the policy engine then denies for task=None (fail-closed).
|
|
29
|
+
active_id = active_task_id(root)
|
|
30
|
+
if not active_id:
|
|
31
|
+
return None
|
|
32
|
+
db = get_db(root)
|
|
33
|
+
if not db:
|
|
34
|
+
return None
|
|
35
|
+
with db.get_session() as session:
|
|
36
|
+
return TaskRepository(session).get_by_id(active_id)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _emit_decision(client: str, action: str, reason: str) -> None:
|
|
40
|
+
if action == "deny":
|
|
41
|
+
print(reason, file=sys.stderr)
|
|
42
|
+
raise typer.Exit(code=2)
|
|
43
|
+
|
|
44
|
+
if client in {"codex", "gemini"}:
|
|
45
|
+
payload = {"decision": "allow", "reason": reason, "suppressOutput": True}
|
|
46
|
+
if action == "warn":
|
|
47
|
+
payload["systemMessage"] = f"DevCouncil Warning: {reason}"
|
|
48
|
+
print(json.dumps(payload, separators=(",", ":")))
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
if action == "warn":
|
|
52
|
+
console.print(f"[yellow]DevCouncil Warning:[/yellow] {reason}")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _emit_unevaluable(client: str, reason: str, strict: bool, *, action: str = "warn") -> None:
|
|
56
|
+
"""Decide what to do when a tool call cannot be evaluated (empty/malformed/error).
|
|
57
|
+
|
|
58
|
+
Fail-closed in strict mode (block), otherwise surface a warning but allow — and
|
|
59
|
+
never leak an undefined exit code, which would silently disable the only pre-action
|
|
60
|
+
gate."""
|
|
61
|
+
_emit_decision(client, "deny" if strict else action, f"{reason}{' (strict mode: blocking)' if strict else ''}")
|
|
62
|
+
|
|
63
|
+
|
|
19
64
|
@app.command()
|
|
20
65
|
def pre_tool_use(
|
|
21
|
-
tool_call_json: str | None = typer.Argument(None, help="The JSON string of the tool call from
|
|
66
|
+
tool_call_json: str | None = typer.Argument(None, help="The JSON string of the tool call from the coding CLI."),
|
|
67
|
+
client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
|
|
68
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
69
|
+
strict: bool = typer.Option(
|
|
70
|
+
False,
|
|
71
|
+
"--strict",
|
|
72
|
+
envvar="DEVCOUNCIL_HOOK_STRICT",
|
|
73
|
+
help="Fail closed (block) when a tool call cannot be parsed or evaluated.",
|
|
74
|
+
),
|
|
22
75
|
):
|
|
23
|
-
"""
|
|
24
|
-
|
|
76
|
+
"""
|
|
77
|
+
Coding CLI hook: Inspects a tool call before execution.
|
|
25
78
|
Exits with code 2 to block unauthorized file writes.
|
|
26
79
|
"""
|
|
80
|
+
normalized_client = client.lower()
|
|
27
81
|
try:
|
|
28
82
|
if tool_call_json is None:
|
|
29
83
|
tool_call_json = sys.stdin.read()
|
|
84
|
+
# Empty payload: nothing to evaluate. Benign in normal use, so allow — but make
|
|
85
|
+
# it observable, and block under --strict.
|
|
30
86
|
if not tool_call_json.strip():
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
running_tasks = [t for t in task_repo.get_all() if t.status == "running"]
|
|
40
|
-
active_task = running_tasks[0] if running_tasks else None
|
|
87
|
+
return _emit_unevaluable(normalized_client, "Empty tool-call payload; nothing to evaluate.", strict, action="allow")
|
|
88
|
+
try:
|
|
89
|
+
call_data = json.loads(tool_call_json)
|
|
90
|
+
except json.JSONDecodeError:
|
|
91
|
+
# A real tool call we cannot parse must not silently pass the gate.
|
|
92
|
+
return _emit_unevaluable(normalized_client, "Tool-call payload was not valid JSON; could not enforce policy.", strict)
|
|
93
|
+
root = _project_root(project_root)
|
|
94
|
+
active_task = _active_task(root)
|
|
41
95
|
|
|
42
96
|
decision = HookPolicy(project_root=root).evaluate(call_data, active_task)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
97
|
+
_emit_decision(normalized_client, decision.action, decision.reason)
|
|
98
|
+
except typer.Exit:
|
|
99
|
+
raise
|
|
100
|
+
except Exception as exc: # never emit an undefined exit code from a crashing hook
|
|
101
|
+
return _emit_unevaluable(normalized_client, f"Hook error: {exc}", strict)
|
|
102
|
+
|
|
103
|
+
@app.command()
|
|
104
|
+
def post_tool_use(
|
|
105
|
+
tool_call_json: str | None = typer.Argument(None, help="The JSON string of the completed tool call."),
|
|
106
|
+
client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
|
|
107
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
108
|
+
):
|
|
109
|
+
"""
|
|
110
|
+
Coding CLI hook: Records a post-tool-use checkpoint for native hook clients.
|
|
111
|
+
"""
|
|
112
|
+
_ = tool_call_json if tool_call_json is not None else sys.stdin.read()
|
|
113
|
+
_ = _project_root(project_root)
|
|
114
|
+
if client.lower() in {"codex", "gemini"}:
|
|
115
|
+
print(json.dumps({"decision": "allow", "suppressOutput": True}, separators=(",", ":")))
|
|
116
|
+
|
|
117
|
+
@app.command()
|
|
118
|
+
def agent_response(
|
|
119
|
+
event_json: str | None = typer.Argument(None, help="The JSON hook payload from the coding CLI."),
|
|
120
|
+
client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
|
|
121
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
122
|
+
):
|
|
123
|
+
"""
|
|
124
|
+
Coding CLI hook: records that an agent response is ready for DevCouncil watch review.
|
|
125
|
+
"""
|
|
126
|
+
payload_text = event_json if event_json is not None else sys.stdin.read()
|
|
127
|
+
root = _project_root(project_root)
|
|
128
|
+
try:
|
|
129
|
+
payload = json.loads(payload_text) if payload_text.strip() else {}
|
|
49
130
|
except json.JSONDecodeError:
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
131
|
+
payload = {"raw": payload_text}
|
|
132
|
+
if isinstance(payload, dict) and not any(key in payload for key in ("task_id", "taskId", "task")):
|
|
133
|
+
active_id = active_task_id(root)
|
|
134
|
+
if active_id:
|
|
135
|
+
payload["task_id"] = active_id
|
|
136
|
+
signal_path = write_signal(root, client.lower(), payload)
|
|
137
|
+
TraceLogger(root).log_event(
|
|
138
|
+
"agent_response_ready",
|
|
139
|
+
{"client": client.lower(), "signal": str(signal_path)},
|
|
140
|
+
summary=f"{client} response ready for critique-card review.",
|
|
141
|
+
)
|
|
142
|
+
if client.lower() in {"codex", "gemini"}:
|
|
143
|
+
print(json.dumps({"decision": "allow", "suppressOutput": True}, separators=(",", ":")))
|
|
144
|
+
|
|
145
|
+
@app.command()
|
|
146
|
+
def post_task(
|
|
147
|
+
client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
|
|
148
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
149
|
+
):
|
|
150
|
+
"""
|
|
151
|
+
Coding CLI hook: Runs after a task is completed.
|
|
152
|
+
|
|
153
|
+
When ``execution.verify_on_post_task`` is enabled, this runs deterministic
|
|
154
|
+
verification of the active task and records gaps; otherwise it just reminds the
|
|
155
|
+
user to run ``dev verify`` (the default, to keep hooks fast/cheap).
|
|
156
|
+
"""
|
|
157
|
+
root = _project_root(project_root)
|
|
158
|
+
try:
|
|
159
|
+
from devcouncil.app.config import load_config
|
|
160
|
+
verify_enabled = load_config(root).execution.verify_on_post_task
|
|
161
|
+
except Exception:
|
|
162
|
+
verify_enabled = False
|
|
163
|
+
|
|
164
|
+
if not verify_enabled:
|
|
165
|
+
console.print("[cyan]DevCouncil: coding agent finished task.[/cyan]")
|
|
166
|
+
console.print("Run [bold]dev verify[/bold] to finalize implementation evidence.")
|
|
167
|
+
_emit_post_task_allow(client)
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
summary = _verify_active_task(root)
|
|
171
|
+
console.print(summary)
|
|
172
|
+
_emit_post_task_allow(client)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _emit_post_task_allow(client: str) -> None:
|
|
176
|
+
if client.lower() in {"codex", "gemini"}:
|
|
177
|
+
print(json.dumps({"decision": "allow", "suppressOutput": True}, separators=(",", ":")))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _verify_active_task(root: Path) -> str:
|
|
181
|
+
"""Run deterministic verification of the active task and persist gaps/evidence.
|
|
182
|
+
Returns a human summary line. Best-effort: never raises out of a hook."""
|
|
183
|
+
try:
|
|
184
|
+
import asyncio
|
|
185
|
+
|
|
186
|
+
from devcouncil.domain.evidence import CommandResult, DiffCoverageEvidence, DiffEvidence, TestEvidence
|
|
187
|
+
from devcouncil.storage.repositories import (
|
|
188
|
+
EvidenceRepository,
|
|
189
|
+
GapRepository,
|
|
190
|
+
RequirementRepository,
|
|
191
|
+
)
|
|
192
|
+
from devcouncil.verification.next_actions import split_next_actions
|
|
193
|
+
from devcouncil.verification.verifier import Verifier
|
|
194
|
+
|
|
195
|
+
active_id = active_task_id(root)
|
|
196
|
+
db = get_db(root)
|
|
197
|
+
if not active_id or not db:
|
|
198
|
+
return "Run [bold]dev verify[/bold] to finalize implementation evidence."
|
|
199
|
+
with db.get_session() as session:
|
|
200
|
+
task = TaskRepository(session).get_by_id(active_id)
|
|
201
|
+
if not task:
|
|
202
|
+
return "Run [bold]dev verify[/bold] to finalize implementation evidence."
|
|
203
|
+
reqs = RequirementRepository(session).get_all()
|
|
204
|
+
gaps, evidence = asyncio.run(Verifier(root).verify_task(task, reqs))
|
|
205
|
+
gap_repo = GapRepository(session)
|
|
206
|
+
ev_repo = EvidenceRepository(session)
|
|
207
|
+
gap_repo.delete_for_task(task.id)
|
|
208
|
+
ev_repo.delete_for_task(task.id)
|
|
209
|
+
for gap in gaps:
|
|
210
|
+
gap_repo.save(gap)
|
|
211
|
+
for ev in evidence:
|
|
212
|
+
if isinstance(ev, CommandResult):
|
|
213
|
+
ev_repo.save_command_result(task.id, ev)
|
|
214
|
+
elif isinstance(ev, DiffCoverageEvidence):
|
|
215
|
+
ev_repo.save_diff_coverage_evidence(ev)
|
|
216
|
+
elif isinstance(ev, DiffEvidence):
|
|
217
|
+
ev_repo.save_diff_evidence(ev)
|
|
218
|
+
elif isinstance(ev, TestEvidence):
|
|
219
|
+
ev_repo.save_test_evidence(ev, task.id)
|
|
220
|
+
blocking = [g for g in gaps if g.blocking]
|
|
221
|
+
task.status = "blocked" if blocking else "verified"
|
|
222
|
+
TaskRepository(session).save(task)
|
|
223
|
+
blocking_actions, _ = split_next_actions(gaps)
|
|
224
|
+
TraceLogger(root).log_event(
|
|
225
|
+
"post_task_verified",
|
|
226
|
+
{"task_id": active_id, "blocking": len(blocking)},
|
|
227
|
+
task_id=active_id,
|
|
228
|
+
summary=f"post_task verification: {task.status}",
|
|
229
|
+
)
|
|
230
|
+
if blocking:
|
|
231
|
+
return (
|
|
232
|
+
f"[yellow]{active_id} is blocked by {len(blocking)} gap(s); "
|
|
233
|
+
f"{len(blocking_actions)} next action(s). Run [bold]dev repair[/bold].[/yellow]"
|
|
234
|
+
)
|
|
235
|
+
return f"[green]{active_id} verified.[/green]"
|
|
236
|
+
except Exception as exc: # never let a hook crash the agent
|
|
237
|
+
return f"[dim]post-task verification skipped: {exc}[/dim]"
|
|
@@ -1,52 +1,79 @@
|
|
|
1
|
-
import copy
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
from
|
|
7
|
-
from devcouncil.
|
|
8
|
-
from devcouncil.integrations.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
1
|
+
import copy
|
|
2
|
+
from typing import Any
|
|
3
|
+
import typer
|
|
4
|
+
import yaml
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from devcouncil.storage.db import Database
|
|
8
|
+
from devcouncil.integrations.gitnexus import GitNexusIntegration
|
|
9
|
+
from devcouncil.integrations.graphify import GraphifyIntegration
|
|
10
|
+
from devcouncil.llm.provider import build_role_model_config, validate_model_provider
|
|
11
|
+
from devcouncil.repo.gitignore import ensure_gitignore
|
|
12
|
+
|
|
13
|
+
app = typer.Typer()
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
# Per-stack default verification commands. A fresh project gets ONLY the commands
|
|
17
|
+
# for the stack(s) actually detected in the repo, so the verifier never inherits a
|
|
18
|
+
# cross-stack gate (e.g. `npm test`/`eslint`/`tsc` on a Python repo) that it would run
|
|
19
|
+
# as a blocking fallback and fail for tooling/stack reasons instead of a real defect.
|
|
20
|
+
_STACK_COMMAND_DEFAULTS: dict[str, dict[str, list[str]]] = {
|
|
21
|
+
"python": {"test": ["pytest"], "lint": ["ruff check ."], "typecheck": ["mypy ."]},
|
|
22
|
+
"node": {"test": ["npm test"], "lint": ["eslint ."], "typecheck": ["tsc --noEmit"]},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _stack_aware_commands(project_root: Path) -> dict[str, list[str]]:
|
|
27
|
+
"""Default test/lint/typecheck commands scoped to the repo's detected stack(s).
|
|
28
|
+
|
|
29
|
+
Returns empty lists when no stack is detected — empty is safe (no speculative
|
|
30
|
+
fallback gates) and far better than guessing wrong-stack tools."""
|
|
31
|
+
from devcouncil.repo.ci_scaffold import detect_stacks
|
|
32
|
+
|
|
33
|
+
commands: dict[str, list[str]] = {"test": [], "lint": [], "typecheck": []}
|
|
34
|
+
try:
|
|
35
|
+
stacks = detect_stacks(project_root)
|
|
36
|
+
except Exception:
|
|
37
|
+
return commands
|
|
38
|
+
for stack in sorted(stacks):
|
|
39
|
+
for key, cmds in _STACK_COMMAND_DEFAULTS.get(stack, {}).items():
|
|
40
|
+
for command in cmds:
|
|
41
|
+
if command not in commands[key]:
|
|
42
|
+
commands[key].append(command)
|
|
43
|
+
return commands
|
|
44
|
+
|
|
45
|
+
|
|
13
46
|
DEFAULT_CONFIG = {
|
|
14
|
-
"project": {
|
|
15
|
-
"name": "devcouncil-project",
|
|
16
|
-
"root": ".",
|
|
17
|
-
"default_branch": "main",
|
|
18
|
-
},
|
|
19
|
-
"models": {
|
|
20
|
-
"provider": "openrouter",
|
|
21
|
-
"roles":
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
},
|
|
45
|
-
"execution": {
|
|
46
|
-
"default_executor": "native",
|
|
47
|
-
"max_repair_attempts": 3,
|
|
48
|
-
"checkpoint_before_each_task": True
|
|
49
|
-
},
|
|
47
|
+
"project": {
|
|
48
|
+
"name": "devcouncil-project",
|
|
49
|
+
"root": ".",
|
|
50
|
+
"default_branch": "main",
|
|
51
|
+
},
|
|
52
|
+
"models": {
|
|
53
|
+
"provider": "openrouter",
|
|
54
|
+
"roles": build_role_model_config("openrouter"),
|
|
55
|
+
},
|
|
56
|
+
"commands": {
|
|
57
|
+
"test": [],
|
|
58
|
+
"lint": [],
|
|
59
|
+
"typecheck": [],
|
|
60
|
+
},
|
|
61
|
+
"gates": {
|
|
62
|
+
"require_clean_git_before_task": True,
|
|
63
|
+
"block_orphan_diffs": True,
|
|
64
|
+
"block_missing_tests_for_high_requirements": True,
|
|
65
|
+
"block_dependency_changes_without_approval": True,
|
|
66
|
+
"block_schema_change_without_migration": True,
|
|
67
|
+
"block_failed_commands": True
|
|
68
|
+
},
|
|
69
|
+
"execution": {
|
|
70
|
+
"default_executor": "manual",
|
|
71
|
+
"max_repair_attempts": 3,
|
|
72
|
+
"checkpoint_before_each_task": True,
|
|
73
|
+
"stream_cli_output": False,
|
|
74
|
+
"cursor_resume_mode": "off",
|
|
75
|
+
"coding_cli_probe_order": [],
|
|
76
|
+
},
|
|
50
77
|
"privacy": {
|
|
51
78
|
"redact_env_vars": True,
|
|
52
79
|
"redact_secrets_in_logs": True,
|
|
@@ -63,15 +90,97 @@ DEFAULT_CONFIG = {
|
|
|
63
90
|
"command": "code-review-graph",
|
|
64
91
|
"optional": True,
|
|
65
92
|
},
|
|
93
|
+
"live_review": {
|
|
94
|
+
"enabled": True,
|
|
95
|
+
"cards_path": ".devcouncil/live/cards",
|
|
96
|
+
"signals_path": ".devcouncil/live/signals",
|
|
97
|
+
"default_client": "claude",
|
|
98
|
+
},
|
|
99
|
+
"cli_agents": {
|
|
100
|
+
"enabled": True,
|
|
101
|
+
"profiles": {
|
|
102
|
+
"default": {
|
|
103
|
+
"description": "Balanced local execution with DevCouncil verification.",
|
|
104
|
+
},
|
|
105
|
+
"yolo": {
|
|
106
|
+
"description": "Faster local execution; DevCouncil still verifies the final diff.",
|
|
107
|
+
"timeout_seconds": 3600,
|
|
108
|
+
"prompt_preamble": "Profile: yolo. Move efficiently within the task scope.",
|
|
109
|
+
},
|
|
110
|
+
"prod": {
|
|
111
|
+
"description": "Restrictive execution for high-risk repositories.",
|
|
112
|
+
"timeout_seconds": 1800,
|
|
113
|
+
"prompt_preamble": "Profile: prod. Keep edits minimal and explicitly within task scope.",
|
|
114
|
+
"require_explicit_confirmation": True,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
"agents": {},
|
|
118
|
+
},
|
|
66
119
|
}
|
|
67
120
|
}
|
|
68
121
|
|
|
69
122
|
|
|
123
|
+
def parse_role_model_overrides(values: list[str] | None) -> dict[str, str]:
|
|
124
|
+
overrides: dict[str, str] = {}
|
|
125
|
+
for value in values or []:
|
|
126
|
+
if "=" not in value:
|
|
127
|
+
raise ValueError(f"Invalid --role-model value '{value}'. Use ROLE=MODEL.")
|
|
128
|
+
role, model = value.split("=", 1)
|
|
129
|
+
role = role.strip()
|
|
130
|
+
model = model.strip()
|
|
131
|
+
if not role or not model:
|
|
132
|
+
raise ValueError(f"Invalid --role-model value '{value}'. Use ROLE=MODEL.")
|
|
133
|
+
overrides[role] = model
|
|
134
|
+
return overrides
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _generate_initial_map(project_root: Path, quiet: bool) -> None:
|
|
138
|
+
"""Best-effort repo map + agent guide generation on fresh init.
|
|
139
|
+
|
|
140
|
+
Imported lazily to avoid a circular import with the map command, and wrapped
|
|
141
|
+
so a mapping failure never blocks initialization.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
from devcouncil.cli.commands.map import generate_map_artifacts
|
|
145
|
+
|
|
146
|
+
generate_map_artifacts(project_root, project_root / ".devcouncil" / "repo_map.json")
|
|
147
|
+
if not quiet:
|
|
148
|
+
console.print("[green]Generated .devcouncil/repo_map.json and agent guides (AGENTS.md, CLAUDE.md).[/green]")
|
|
149
|
+
except Exception as exc: # mapping is best-effort, never fatal
|
|
150
|
+
if not quiet:
|
|
151
|
+
console.print(f"[yellow]Skipped repo map generation: {exc}. Run 'dev map' later.[/yellow]")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _scaffold_initial_skills(project_root: Path, quiet: bool) -> None:
|
|
155
|
+
"""Best-effort scaffolding of applicable engineering skills into .claude/skills/.
|
|
156
|
+
|
|
157
|
+
Always writes the core-engineering skill; adds domain skills (android, ios, web,
|
|
158
|
+
...) whose file triggers match the repository. Never fatal.
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
from devcouncil.skills.registry import scaffold_skills, select_skills
|
|
162
|
+
|
|
163
|
+
selected = select_skills(project_root=project_root)
|
|
164
|
+
written = scaffold_skills(project_root, selected)
|
|
165
|
+
if written and not quiet:
|
|
166
|
+
names = ", ".join(sorted(skill.name for skill in selected))
|
|
167
|
+
console.print(f"[green]Scaffolded {len(written)} skill(s) into .claude/skills/ ({names}).[/green]")
|
|
168
|
+
except Exception as exc: # skill scaffolding is best-effort, never fatal
|
|
169
|
+
if not quiet:
|
|
170
|
+
console.print(f"[yellow]Skipped skill scaffolding: {exc}. Run 'dev skills scaffold' later.[/yellow]")
|
|
171
|
+
|
|
172
|
+
|
|
70
173
|
def initialize_project(
|
|
71
174
|
project_root: Path = Path("."),
|
|
72
175
|
project_name: str | None = None,
|
|
176
|
+
model_provider: str = "openrouter",
|
|
177
|
+
model: str | None = None,
|
|
178
|
+
role_models: dict[str, str] | None = None,
|
|
73
179
|
with_gitnexus: bool = False,
|
|
74
180
|
with_graphify: bool = False,
|
|
181
|
+
with_map: bool = True,
|
|
182
|
+
with_skills: bool = True,
|
|
183
|
+
quiet: bool = False,
|
|
75
184
|
) -> bool:
|
|
76
185
|
"""Initialize DevCouncil project state.
|
|
77
186
|
|
|
@@ -82,7 +191,8 @@ def initialize_project(
|
|
|
82
191
|
created = False
|
|
83
192
|
|
|
84
193
|
if not dev_dir.exists():
|
|
85
|
-
|
|
194
|
+
if not quiet:
|
|
195
|
+
console.print("Initializing DevCouncil...")
|
|
86
196
|
dev_dir.mkdir(exist_ok=True)
|
|
87
197
|
(dev_dir / "runs").mkdir(exist_ok=True)
|
|
88
198
|
(dev_dir / "cache").mkdir(exist_ok=True)
|
|
@@ -90,20 +200,35 @@ def initialize_project(
|
|
|
90
200
|
(dev_dir / "logs").mkdir(exist_ok=True)
|
|
91
201
|
|
|
92
202
|
config_path = dev_dir / "config.yaml"
|
|
93
|
-
config = copy.deepcopy(DEFAULT_CONFIG)
|
|
203
|
+
config: dict[str, Any] = copy.deepcopy(DEFAULT_CONFIG)
|
|
204
|
+
# Scope default verification commands to the repo's actual stack(s).
|
|
205
|
+
config["commands"] = _stack_aware_commands(project_root)
|
|
94
206
|
if project_name:
|
|
95
207
|
config["project"]["name"] = project_name
|
|
96
208
|
else:
|
|
97
209
|
config["project"]["name"] = project_root.name
|
|
210
|
+
provider = validate_model_provider(model_provider)
|
|
211
|
+
config["models"]["provider"] = provider
|
|
212
|
+
config["models"]["roles"] = build_role_model_config(
|
|
213
|
+
provider,
|
|
214
|
+
model=model,
|
|
215
|
+
role_models=role_models,
|
|
216
|
+
)
|
|
98
217
|
|
|
99
218
|
with open(config_path, "w") as f:
|
|
100
219
|
yaml.dump(config, f, default_flow_style=False)
|
|
101
220
|
|
|
102
221
|
db = Database(dev_dir / "state.sqlite")
|
|
103
222
|
db.create_db_and_tables()
|
|
104
|
-
|
|
223
|
+
if not quiet:
|
|
224
|
+
console.print(f"[green]Successfully initialized DevCouncil in {dev_dir}[/green]")
|
|
105
225
|
created = True
|
|
106
226
|
|
|
227
|
+
if with_map:
|
|
228
|
+
_generate_initial_map(project_root, quiet)
|
|
229
|
+
if with_skills:
|
|
230
|
+
_scaffold_initial_skills(project_root, quiet)
|
|
231
|
+
|
|
107
232
|
if with_gitnexus:
|
|
108
233
|
nexus = GitNexusIntegration(project_root)
|
|
109
234
|
nexus.initialize()
|
|
@@ -112,17 +237,27 @@ def initialize_project(
|
|
|
112
237
|
graphify = GraphifyIntegration(project_root)
|
|
113
238
|
graphify.initialize()
|
|
114
239
|
|
|
240
|
+
ensure_gitignore(project_root)
|
|
115
241
|
return created
|
|
116
242
|
|
|
117
243
|
|
|
118
244
|
@app.callback(invoke_without_command=True)
|
|
119
245
|
def init(
|
|
120
|
-
ctx: typer.Context,
|
|
121
|
-
project_name: str = typer.Option(None, "--name", "-n", help="Project name"),
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
246
|
+
ctx: typer.Context,
|
|
247
|
+
project_name: str = typer.Option(None, "--name", "-n", help="Project name"),
|
|
248
|
+
provider: str = typer.Option("openrouter", "--provider", help="Model provider for generated config."),
|
|
249
|
+
model: str | None = typer.Option(None, "--model", "-m", help="Model id to use for every default role."),
|
|
250
|
+
role_model: list[str] | None = typer.Option(
|
|
251
|
+
None,
|
|
252
|
+
"--role-model",
|
|
253
|
+
help="Per-role model override in ROLE=MODEL form. Can be repeated.",
|
|
254
|
+
),
|
|
255
|
+
with_gitnexus: bool = typer.Option(False, "--gitnexus", help="Initialize GitNexus structural awareness"),
|
|
256
|
+
with_graphify: bool = typer.Option(False, "--graphify", help="Initialize Graphify knowledge graph engine"),
|
|
257
|
+
skip_map: bool = typer.Option(False, "--skip-map", help="Skip generating repo_map.json and agent guides on init."),
|
|
258
|
+
skip_skills: bool = typer.Option(False, "--skip-skills", help="Skip scaffolding engineering skills into .claude/skills/ on init."),
|
|
259
|
+
):
|
|
260
|
+
"""
|
|
126
261
|
Initialize DevCouncil in the current directory.
|
|
127
262
|
"""
|
|
128
263
|
if ctx.invoked_subcommand is not None:
|
|
@@ -134,9 +269,21 @@ def init(
|
|
|
134
269
|
console.print("Use --gitnexus or --graphify to add upgrade paths.")
|
|
135
270
|
raise typer.Exit()
|
|
136
271
|
|
|
272
|
+
try:
|
|
273
|
+
role_models = parse_role_model_overrides(role_model)
|
|
274
|
+
model_provider = validate_model_provider(provider)
|
|
275
|
+
except ValueError as e:
|
|
276
|
+
console.print(f"[red]{e}[/red]")
|
|
277
|
+
raise typer.Exit(code=2) from e
|
|
278
|
+
|
|
137
279
|
initialize_project(
|
|
138
280
|
Path("."),
|
|
139
281
|
project_name=project_name,
|
|
282
|
+
model_provider=model_provider,
|
|
283
|
+
model=model,
|
|
284
|
+
role_models=role_models,
|
|
140
285
|
with_gitnexus=with_gitnexus,
|
|
141
286
|
with_graphify=with_graphify,
|
|
287
|
+
with_map=not skip_map,
|
|
288
|
+
with_skills=not skip_skills,
|
|
142
289
|
)
|