devcouncil 0.1.0 → 0.1.1
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 +62 -543
- package/package.json +1 -1
- package/pyproject.toml +29 -26
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +135 -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 +143 -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/cli/commands/artifacts.py +51 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/config.py +76 -54
- package/src/devcouncil/cli/commands/dashboard.py +26 -0
- package/src/devcouncil/cli/commands/doctor.py +86 -42
- package/src/devcouncil/cli/commands/go.py +237 -0
- package/src/devcouncil/cli/commands/hook.py +96 -29
- package/src/devcouncil/cli/commands/init.py +67 -56
- package/src/devcouncil/cli/commands/integrate.py +320 -14
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +25 -21
- package/src/devcouncil/cli/commands/plan.py +257 -206
- package/src/devcouncil/cli/commands/prompt.py +36 -33
- package/src/devcouncil/cli/commands/repair.py +72 -69
- package/src/devcouncil/cli/commands/report.py +112 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
- package/src/devcouncil/cli/commands/rollback.py +49 -47
- package/src/devcouncil/cli/commands/run.py +252 -207
- package/src/devcouncil/cli/commands/setup.py +159 -18
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/status.py +117 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +2 -1
- package/src/devcouncil/cli/commands/verify.py +158 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/main.py +42 -24
- 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 +27 -27
- package/src/devcouncil/domain/gap.py +26 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +26 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/hook_policy.py +24 -3
- package/src/devcouncil/execution/patch.py +28 -28
- package/src/devcouncil/execution/permissions.py +44 -44
- package/src/devcouncil/execution/prompt_builder.py +23 -23
- package/src/devcouncil/execution/task_runner.py +63 -63
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/coding_cli.py +112 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +81 -81
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +50 -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 +34 -34
- package/src/devcouncil/gating/policy.py +157 -157
- 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 +120 -0
- package/src/devcouncil/indexing/repo_mapper.py +208 -204
- package/src/devcouncil/integrations/github.py +35 -35
- package/src/devcouncil/integrations/gitnexus.py +27 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +549 -96
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +207 -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 +138 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +38 -38
- package/src/devcouncil/llm/provider.py +146 -125
- package/src/devcouncil/llm/router.py +111 -111
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/critique_service.py +66 -66
- package/src/devcouncil/planning/plan_service.py +46 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +44 -44
- package/src/devcouncil/reporting/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +20 -17
- package/src/devcouncil/reporting/markdown_report.py +68 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- package/src/devcouncil/storage/db.py +66 -66
- package/src/devcouncil/storage/models.py +83 -83
- package/src/devcouncil/storage/repositories.py +299 -222
- package/src/devcouncil/telemetry/cost.py +34 -34
- package/src/devcouncil/telemetry/tracker.py +49 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +122 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +141 -141
- package/src/devcouncil/verification/__init__.py +1 -1
- package/src/devcouncil/verification/implementation_reviewer.py +55 -55
- package/src/devcouncil/verification/verifier.py +319 -302
- package/uv.lock +1 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
|
|
5
|
+
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from devcouncil.cli.commands import plan as plan_command
|
|
9
|
+
from devcouncil.cli.commands import report as report_command
|
|
10
|
+
from devcouncil.cli.commands import run as run_command
|
|
11
|
+
from devcouncil.app.config import load_config
|
|
12
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
13
|
+
from devcouncil.storage.db import get_db
|
|
14
|
+
from devcouncil.storage.repositories import ArtifactGraphRepository, StateRepository, TaskRepository
|
|
15
|
+
from devcouncil.app.state_machine import ProjectPhase
|
|
16
|
+
from devcouncil.live.summary import live_review_summary
|
|
17
|
+
from devcouncil.reporting.report_builder import ReportBuilder
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
console = Console()
|
|
21
|
+
|
|
22
|
+
SUPPORTED_EXECUTORS = {
|
|
23
|
+
"codex",
|
|
24
|
+
"codex-cli",
|
|
25
|
+
"gemini",
|
|
26
|
+
"gemini-cli",
|
|
27
|
+
"claude",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"claude-cli",
|
|
30
|
+
"native",
|
|
31
|
+
"mini",
|
|
32
|
+
"openhands",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
AGENT_REPORT_FILE = Path(".devcouncil/reports/latest.json")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _normalize_executor(executor: str) -> str:
|
|
39
|
+
return executor.strip().lower().replace("_", "-")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _configured_executor(root: Path) -> str:
|
|
43
|
+
try:
|
|
44
|
+
configured = load_config(root).execution.default_executor
|
|
45
|
+
except FileNotFoundError:
|
|
46
|
+
configured = "codex"
|
|
47
|
+
return _normalize_executor(configured or "codex")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _load_tasks(root: Path):
|
|
51
|
+
db = get_db(root)
|
|
52
|
+
if not db:
|
|
53
|
+
return []
|
|
54
|
+
with db.get_session() as session:
|
|
55
|
+
return TaskRepository(session).get_all()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _load_tasks_by_id(root: Path, task_ids: list[str]):
|
|
59
|
+
db = get_db(root)
|
|
60
|
+
if not db:
|
|
61
|
+
return [], task_ids
|
|
62
|
+
with db.get_session() as session:
|
|
63
|
+
repo = TaskRepository(session)
|
|
64
|
+
tasks = []
|
|
65
|
+
missing = []
|
|
66
|
+
for task_id in task_ids:
|
|
67
|
+
task = repo.get_by_id(task_id)
|
|
68
|
+
if task is None:
|
|
69
|
+
missing.append(task_id)
|
|
70
|
+
else:
|
|
71
|
+
tasks.append(task)
|
|
72
|
+
return tasks, missing
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _unique_task_ids(task_ids: list[str]) -> list[str]:
|
|
76
|
+
seen = set()
|
|
77
|
+
unique = []
|
|
78
|
+
for task_id in task_ids:
|
|
79
|
+
if task_id in seen:
|
|
80
|
+
continue
|
|
81
|
+
seen.add(task_id)
|
|
82
|
+
unique.append(task_id)
|
|
83
|
+
return unique
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _record_project_done(root: Path) -> None:
|
|
87
|
+
db = get_db(root)
|
|
88
|
+
if not db:
|
|
89
|
+
return
|
|
90
|
+
with db.get_session() as session:
|
|
91
|
+
StateRepository(session).record_phase(ProjectPhase.PROJECT_DONE.value)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _record_project_blocked(root: Path) -> None:
|
|
95
|
+
db = get_db(root)
|
|
96
|
+
if not db:
|
|
97
|
+
return
|
|
98
|
+
with db.get_session() as session:
|
|
99
|
+
StateRepository(session).record_phase(ProjectPhase.TASK_BLOCKED.value)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _render_final_report(root: Path, json_report: bool) -> str:
|
|
103
|
+
db = get_db(root)
|
|
104
|
+
if not db:
|
|
105
|
+
raise RuntimeError("DevCouncil state is unavailable in this directory.")
|
|
106
|
+
with db.get_session() as session:
|
|
107
|
+
graph = ArtifactGraphRepository(session).load_graph()
|
|
108
|
+
live_review = live_review_summary(root)
|
|
109
|
+
if json_report:
|
|
110
|
+
return ReportBuilder.build_json(graph, live_review=live_review)
|
|
111
|
+
return ReportBuilder.build_markdown(graph, live_review=live_review)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _write_report_file(root: Path, report_file: Path, content: str) -> Path:
|
|
115
|
+
path = report_file.expanduser()
|
|
116
|
+
if not path.is_absolute():
|
|
117
|
+
path = root / path
|
|
118
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
path.write_text(content, encoding="utf-8")
|
|
120
|
+
return path
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _command_label(ctx: typer.Context) -> str:
|
|
124
|
+
command = ctx.info_name or "e2e"
|
|
125
|
+
return f"dev {command}"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def go(
|
|
129
|
+
ctx: typer.Context,
|
|
130
|
+
goal: str = typer.Argument(..., help="Implementation goal to plan, execute, verify, and report."),
|
|
131
|
+
executor: str | None = typer.Option(
|
|
132
|
+
None,
|
|
133
|
+
"--executor",
|
|
134
|
+
"-e",
|
|
135
|
+
help="Automated executor to use. Defaults to execution.default_executor in .devcouncil/config.yaml.",
|
|
136
|
+
),
|
|
137
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Use mock planning responses for local smoke testing."),
|
|
138
|
+
continue_on_blocked: bool = typer.Option(
|
|
139
|
+
False,
|
|
140
|
+
"--continue-on-blocked",
|
|
141
|
+
help="Continue later tasks even if an earlier task is blocked by verification.",
|
|
142
|
+
),
|
|
143
|
+
json_report: bool = typer.Option(False, "--json-report", "--json", help="Print the final report as JSON."),
|
|
144
|
+
report_file: Path | None = typer.Option(
|
|
145
|
+
None,
|
|
146
|
+
"--report-file",
|
|
147
|
+
help="Write the final report to a file. Relative paths resolve from --project-root.",
|
|
148
|
+
),
|
|
149
|
+
agent: bool = typer.Option(
|
|
150
|
+
False,
|
|
151
|
+
"--agent",
|
|
152
|
+
help="Use coding-agent defaults: JSON report plus .devcouncil/reports/latest.json.",
|
|
153
|
+
),
|
|
154
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
155
|
+
):
|
|
156
|
+
"""
|
|
157
|
+
Run the full DevCouncil loop in one command.
|
|
158
|
+
"""
|
|
159
|
+
root = project_root.expanduser().resolve()
|
|
160
|
+
initialize_project(root, quiet=True)
|
|
161
|
+
if agent:
|
|
162
|
+
json_report = True
|
|
163
|
+
if report_file is None:
|
|
164
|
+
report_file = AGENT_REPORT_FILE
|
|
165
|
+
|
|
166
|
+
normalized_executor = _normalize_executor(executor) if executor else _configured_executor(root)
|
|
167
|
+
command_label = _command_label(ctx)
|
|
168
|
+
if normalized_executor == "manual":
|
|
169
|
+
console.print(
|
|
170
|
+
f"[red]`{command_label}` requires an automated executor. "
|
|
171
|
+
"Use `dev run TASK-ID --executor manual` for handoff mode.[/red]"
|
|
172
|
+
)
|
|
173
|
+
raise typer.Exit(code=2)
|
|
174
|
+
if normalized_executor not in SUPPORTED_EXECUTORS:
|
|
175
|
+
console.print(
|
|
176
|
+
f"[red]Unsupported executor for `{command_label}`: "
|
|
177
|
+
f"{normalized_executor}. Supported: {', '.join(sorted(SUPPORTED_EXECUTORS))}.[/red]"
|
|
178
|
+
)
|
|
179
|
+
raise typer.Exit(code=2)
|
|
180
|
+
|
|
181
|
+
console.print(f"[bold]Planning goal:[/bold] {goal}")
|
|
182
|
+
planned_task_ids = asyncio.run(plan_command.run_plan_flow(goal, dry_run=dry_run, persist=True, project_root=root))
|
|
183
|
+
|
|
184
|
+
task_ids = _unique_task_ids(planned_task_ids or [])
|
|
185
|
+
tasks, missing_task_ids = _load_tasks_by_id(root, task_ids)
|
|
186
|
+
if missing_task_ids:
|
|
187
|
+
console.print(f"[red]Planning returned task IDs that were not persisted: {', '.join(missing_task_ids)}[/red]")
|
|
188
|
+
raise typer.Exit(code=1)
|
|
189
|
+
if not tasks:
|
|
190
|
+
console.print("[red]Planning did not produce any approved tasks.[/red]")
|
|
191
|
+
raise typer.Exit(code=1)
|
|
192
|
+
|
|
193
|
+
failed: list[str] = []
|
|
194
|
+
executed_task_ids: list[str] = []
|
|
195
|
+
for task in tasks:
|
|
196
|
+
if task.status in {"verified", "done"}:
|
|
197
|
+
console.print(f"[green]Skipping {task.id}; already {task.status}.[/green]")
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
console.print(f"\n[bold]Executing {task.id}[/bold] with [bold]{normalized_executor}[/bold]...")
|
|
201
|
+
executed_task_ids.append(task.id)
|
|
202
|
+
run_command.run(task.id, executor=normalized_executor, project_root=root)
|
|
203
|
+
|
|
204
|
+
latest = {item.id: item for item in _load_tasks(root)}.get(task.id)
|
|
205
|
+
latest_status = latest.status if latest else "missing"
|
|
206
|
+
if latest_status not in {"verified", "done"}:
|
|
207
|
+
failed.append(f"{task.id} ({latest_status})")
|
|
208
|
+
if latest_status != "blocked" or not continue_on_blocked:
|
|
209
|
+
console.print(f"[red]Stopping because {task.id} ended as {latest_status}.[/red]")
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
if not executed_task_ids:
|
|
213
|
+
failed.append("all planned tasks were already completed before execution")
|
|
214
|
+
|
|
215
|
+
if not failed:
|
|
216
|
+
_record_project_done(root)
|
|
217
|
+
else:
|
|
218
|
+
_record_project_blocked(root)
|
|
219
|
+
|
|
220
|
+
console.print("\n[bold]Final DevCouncil report[/bold]")
|
|
221
|
+
report_command.report(
|
|
222
|
+
SimpleNamespace(invoked_subcommand=None),
|
|
223
|
+
planning_only=False,
|
|
224
|
+
json_format=json_report,
|
|
225
|
+
github=False,
|
|
226
|
+
github_pr_comment=False,
|
|
227
|
+
gitlab_pr_comment=False,
|
|
228
|
+
project_root=root,
|
|
229
|
+
)
|
|
230
|
+
if report_file is not None:
|
|
231
|
+
output = _render_final_report(root, json_report=json_report)
|
|
232
|
+
written = _write_report_file(root, report_file, output)
|
|
233
|
+
console.print(f"[green]Final report written to {written}[/green]")
|
|
234
|
+
|
|
235
|
+
if failed:
|
|
236
|
+
console.print(f"\n[red]Unfinished task(s): {', '.join(failed)}[/red]")
|
|
237
|
+
raise typer.Exit(code=1)
|
|
@@ -7,21 +7,55 @@ 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
|
+
db = get_db(root)
|
|
27
|
+
if not db:
|
|
28
|
+
return None
|
|
29
|
+
with db.get_session() as session:
|
|
30
|
+
task_repo = TaskRepository(session)
|
|
31
|
+
running_tasks = [t for t in task_repo.get_all() if t.status == "running"]
|
|
32
|
+
return running_tasks[0] if running_tasks else None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _emit_decision(client: str, action: str, reason: str) -> None:
|
|
36
|
+
if action == "deny":
|
|
37
|
+
print(reason, file=sys.stderr)
|
|
38
|
+
raise typer.Exit(code=2)
|
|
39
|
+
|
|
40
|
+
if client in {"codex", "gemini"}:
|
|
41
|
+
payload = {"decision": "allow", "reason": reason, "suppressOutput": True}
|
|
42
|
+
if action == "warn":
|
|
43
|
+
payload["systemMessage"] = f"DevCouncil Warning: {reason}"
|
|
44
|
+
print(json.dumps(payload, separators=(",", ":")))
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
if action == "warn":
|
|
48
|
+
console.print(f"[yellow]DevCouncil Warning:[/yellow] {reason}")
|
|
49
|
+
|
|
50
|
+
|
|
19
51
|
@app.command()
|
|
20
52
|
def pre_tool_use(
|
|
21
|
-
tool_call_json: str | None = typer.Argument(None, help="The JSON string of the tool call from
|
|
53
|
+
tool_call_json: str | None = typer.Argument(None, help="The JSON string of the tool call from the coding CLI."),
|
|
54
|
+
client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
|
|
55
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
22
56
|
):
|
|
23
|
-
"""
|
|
24
|
-
|
|
57
|
+
"""
|
|
58
|
+
Coding CLI hook: Inspects a tool call before execution.
|
|
25
59
|
Exits with code 2 to block unauthorized file writes.
|
|
26
60
|
"""
|
|
27
61
|
try:
|
|
@@ -30,32 +64,65 @@ def pre_tool_use(
|
|
|
30
64
|
if not tool_call_json.strip():
|
|
31
65
|
raise typer.Exit(code=0)
|
|
32
66
|
call_data = json.loads(tool_call_json)
|
|
33
|
-
|
|
34
|
-
root = _project_root()
|
|
35
|
-
|
|
36
|
-
if db:
|
|
37
|
-
with db.get_session() as session:
|
|
38
|
-
task_repo = TaskRepository(session)
|
|
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
|
|
67
|
+
normalized_client = client.lower()
|
|
68
|
+
root = _project_root(project_root)
|
|
69
|
+
active_task = _active_task(root)
|
|
41
70
|
|
|
42
71
|
decision = HookPolicy(project_root=root).evaluate(call_data, active_task)
|
|
43
|
-
|
|
44
|
-
console.print(f"[red]DevCouncil Blocked Action:[/red] {decision.reason}")
|
|
45
|
-
sys.exit(2)
|
|
46
|
-
if decision.action == "warn":
|
|
47
|
-
console.print(f"[yellow]DevCouncil Warning:[/yellow] {decision.reason}")
|
|
72
|
+
_emit_decision(normalized_client, decision.action, decision.reason)
|
|
48
73
|
|
|
49
74
|
except json.JSONDecodeError:
|
|
50
75
|
raise typer.Exit(code=0)
|
|
51
|
-
|
|
52
|
-
@app.command()
|
|
53
|
-
def
|
|
54
|
-
""
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
76
|
+
|
|
77
|
+
@app.command()
|
|
78
|
+
def post_tool_use(
|
|
79
|
+
tool_call_json: str | None = typer.Argument(None, help="The JSON string of the completed tool call."),
|
|
80
|
+
client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
|
|
81
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
82
|
+
):
|
|
83
|
+
"""
|
|
84
|
+
Coding CLI hook: Records a post-tool-use checkpoint for native hook clients.
|
|
85
|
+
"""
|
|
86
|
+
_ = tool_call_json if tool_call_json is not None else sys.stdin.read()
|
|
87
|
+
_ = _project_root(project_root)
|
|
88
|
+
if client.lower() in {"codex", "gemini"}:
|
|
89
|
+
print(json.dumps({"decision": "allow", "suppressOutput": True}, separators=(",", ":")))
|
|
90
|
+
|
|
91
|
+
@app.command()
|
|
92
|
+
def agent_response(
|
|
93
|
+
event_json: str | None = typer.Argument(None, help="The JSON hook payload from the coding CLI."),
|
|
94
|
+
client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
|
|
95
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
96
|
+
):
|
|
97
|
+
"""
|
|
98
|
+
Coding CLI hook: records that an agent response is ready for DevCouncil watch review.
|
|
99
|
+
"""
|
|
100
|
+
payload_text = event_json if event_json is not None else sys.stdin.read()
|
|
101
|
+
root = _project_root(project_root)
|
|
102
|
+
try:
|
|
103
|
+
payload = json.loads(payload_text) if payload_text.strip() else {}
|
|
104
|
+
except json.JSONDecodeError:
|
|
105
|
+
payload = {"raw": payload_text}
|
|
106
|
+
if isinstance(payload, dict) and not any(key in payload for key in ("task_id", "taskId", "task")):
|
|
107
|
+
active_id = active_task_id(root)
|
|
108
|
+
if active_id:
|
|
109
|
+
payload["task_id"] = active_id
|
|
110
|
+
signal_path = write_signal(root, client.lower(), payload)
|
|
111
|
+
TraceLogger(root).log_event(
|
|
112
|
+
"agent_response_ready",
|
|
113
|
+
{"client": client.lower(), "signal": str(signal_path)},
|
|
114
|
+
summary=f"{client} response ready for critique-card review.",
|
|
115
|
+
)
|
|
116
|
+
if client.lower() in {"codex", "gemini"}:
|
|
117
|
+
print(json.dumps({"decision": "allow", "suppressOutput": True}, separators=(",", ":")))
|
|
118
|
+
|
|
119
|
+
@app.command()
|
|
120
|
+
def post_task():
|
|
121
|
+
"""
|
|
122
|
+
Coding CLI hook: Runs after a task is completed.
|
|
123
|
+
Triggers deterministic verification.
|
|
124
|
+
"""
|
|
125
|
+
console.print("[cyan]DevCouncil: coding agent finished task. Triggering automatic verification...[/cyan]")
|
|
126
|
+
# In a real environment, this would invoke 'dev verify <active-task>'
|
|
127
|
+
# For the hook script, we just notify the user.
|
|
128
|
+
console.print("Run [bold]dev verify[/bold] to finalize implementation evidence.")
|
|
@@ -1,52 +1,54 @@
|
|
|
1
|
-
import copy
|
|
2
|
-
import typer
|
|
3
|
-
import yaml
|
|
4
|
-
from rich.console import Console
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from devcouncil.storage.db import Database
|
|
7
|
-
from devcouncil.integrations.gitnexus import GitNexusIntegration
|
|
8
|
-
from devcouncil.integrations.graphify import GraphifyIntegration
|
|
9
|
-
|
|
10
|
-
app = typer.Typer()
|
|
11
|
-
console = Console()
|
|
12
|
-
|
|
1
|
+
import copy
|
|
2
|
+
import typer
|
|
3
|
+
import yaml
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from devcouncil.storage.db import Database
|
|
7
|
+
from devcouncil.integrations.gitnexus import GitNexusIntegration
|
|
8
|
+
from devcouncil.integrations.graphify import GraphifyIntegration
|
|
9
|
+
|
|
10
|
+
app = typer.Typer()
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
13
|
DEFAULT_CONFIG = {
|
|
14
|
-
"project": {
|
|
15
|
-
"name": "devcouncil-project",
|
|
16
|
-
"root": ".",
|
|
17
|
-
"default_branch": "main",
|
|
18
|
-
},
|
|
19
|
-
"models": {
|
|
20
|
-
"provider": "openrouter",
|
|
21
|
-
"roles": {
|
|
22
|
-
"spec_writer": {"model": "anthropic/claude-3.5-sonnet"},
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
14
|
+
"project": {
|
|
15
|
+
"name": "devcouncil-project",
|
|
16
|
+
"root": ".",
|
|
17
|
+
"default_branch": "main",
|
|
18
|
+
},
|
|
19
|
+
"models": {
|
|
20
|
+
"provider": "openrouter",
|
|
21
|
+
"roles": {
|
|
22
|
+
"spec_writer": {"model": "anthropic/claude-3.5-sonnet"},
|
|
23
|
+
"prompt_enhancer": {"model": "anthropic/claude-3.5-sonnet"},
|
|
24
|
+
"planner_a": {"model": "anthropic/claude-3.5-sonnet"},
|
|
25
|
+
"planner_b": {"model": "google/gemini-pro-1.5"},
|
|
26
|
+
"critic_a": {"model": "openai/gpt-4o"},
|
|
27
|
+
"critic_b": {"model": "anthropic/claude-3-opus"},
|
|
28
|
+
"arbiter": {"model": "openai/gpt-4o"},
|
|
29
|
+
"native_agent": {"model": "anthropic/claude-3.5-sonnet"},
|
|
30
|
+
"implementation_reviewer": {"model": "openai/gpt-4o"},
|
|
31
|
+
"live_reviewer": {"model": "openai/gpt-4o"},
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"commands": {
|
|
35
|
+
"test": ["pytest", "npm test"],
|
|
36
|
+
"lint": ["flake8", "eslint"],
|
|
37
|
+
"typecheck": ["mypy", "tsc"]
|
|
38
|
+
},
|
|
39
|
+
"gates": {
|
|
40
|
+
"require_clean_git_before_task": True,
|
|
41
|
+
"block_orphan_diffs": True,
|
|
42
|
+
"block_missing_tests_for_high_requirements": True,
|
|
43
|
+
"block_dependency_changes_without_approval": True,
|
|
44
|
+
"block_schema_change_without_migration": True,
|
|
45
|
+
"block_failed_commands": True
|
|
46
|
+
},
|
|
47
|
+
"execution": {
|
|
48
|
+
"default_executor": "native",
|
|
49
|
+
"max_repair_attempts": 3,
|
|
50
|
+
"checkpoint_before_each_task": True
|
|
51
|
+
},
|
|
50
52
|
"privacy": {
|
|
51
53
|
"redact_env_vars": True,
|
|
52
54
|
"redact_secrets_in_logs": True,
|
|
@@ -63,6 +65,12 @@ DEFAULT_CONFIG = {
|
|
|
63
65
|
"command": "code-review-graph",
|
|
64
66
|
"optional": True,
|
|
65
67
|
},
|
|
68
|
+
"live_review": {
|
|
69
|
+
"enabled": True,
|
|
70
|
+
"cards_path": ".devcouncil/live/cards",
|
|
71
|
+
"signals_path": ".devcouncil/live/signals",
|
|
72
|
+
"default_client": "claude",
|
|
73
|
+
},
|
|
66
74
|
}
|
|
67
75
|
}
|
|
68
76
|
|
|
@@ -72,6 +80,7 @@ def initialize_project(
|
|
|
72
80
|
project_name: str | None = None,
|
|
73
81
|
with_gitnexus: bool = False,
|
|
74
82
|
with_graphify: bool = False,
|
|
83
|
+
quiet: bool = False,
|
|
75
84
|
) -> bool:
|
|
76
85
|
"""Initialize DevCouncil project state.
|
|
77
86
|
|
|
@@ -82,7 +91,8 @@ def initialize_project(
|
|
|
82
91
|
created = False
|
|
83
92
|
|
|
84
93
|
if not dev_dir.exists():
|
|
85
|
-
|
|
94
|
+
if not quiet:
|
|
95
|
+
console.print("Initializing DevCouncil...")
|
|
86
96
|
dev_dir.mkdir(exist_ok=True)
|
|
87
97
|
(dev_dir / "runs").mkdir(exist_ok=True)
|
|
88
98
|
(dev_dir / "cache").mkdir(exist_ok=True)
|
|
@@ -101,7 +111,8 @@ def initialize_project(
|
|
|
101
111
|
|
|
102
112
|
db = Database(dev_dir / "state.sqlite")
|
|
103
113
|
db.create_db_and_tables()
|
|
104
|
-
|
|
114
|
+
if not quiet:
|
|
115
|
+
console.print(f"[green]Successfully initialized DevCouncil in {dev_dir}[/green]")
|
|
105
116
|
created = True
|
|
106
117
|
|
|
107
118
|
if with_gitnexus:
|
|
@@ -117,12 +128,12 @@ def initialize_project(
|
|
|
117
128
|
|
|
118
129
|
@app.callback(invoke_without_command=True)
|
|
119
130
|
def init(
|
|
120
|
-
ctx: typer.Context,
|
|
121
|
-
project_name: str = typer.Option(None, "--name", "-n", help="Project name"),
|
|
122
|
-
with_gitnexus: bool = typer.Option(False, "--gitnexus", help="Initialize GitNexus structural awareness"),
|
|
123
|
-
with_graphify: bool = typer.Option(False, "--graphify", help="Initialize Graphify knowledge graph engine"),
|
|
124
|
-
):
|
|
125
|
-
"""
|
|
131
|
+
ctx: typer.Context,
|
|
132
|
+
project_name: str = typer.Option(None, "--name", "-n", help="Project name"),
|
|
133
|
+
with_gitnexus: bool = typer.Option(False, "--gitnexus", help="Initialize GitNexus structural awareness"),
|
|
134
|
+
with_graphify: bool = typer.Option(False, "--graphify", help="Initialize Graphify knowledge graph engine"),
|
|
135
|
+
):
|
|
136
|
+
"""
|
|
126
137
|
Initialize DevCouncil in the current directory.
|
|
127
138
|
"""
|
|
128
139
|
if ctx.invoked_subcommand is not None:
|