devcouncil 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +201 -6
- package/package.json +9 -2
- package/pyproject.toml +34 -2
- package/src/devcouncil/app/config.py +348 -12
- package/src/devcouncil/app/orchestrator.py +10 -6
- package/src/devcouncil/app/state_machine.py +4 -0
- package/src/devcouncil/artifacts/graph.py +32 -5
- package/src/devcouncil/assets/__init__.py +1 -0
- package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/commands/agents.py +292 -0
- package/src/devcouncil/cli/commands/artifacts.py +6 -3
- package/src/devcouncil/cli/commands/check.py +220 -0
- package/src/devcouncil/cli/commands/config.py +43 -4
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +6 -1
- package/src/devcouncil/cli/commands/design.py +186 -0
- package/src/devcouncil/cli/commands/doctor.py +379 -22
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +532 -33
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +296 -15
- package/src/devcouncil/cli/commands/init.py +161 -20
- package/src/devcouncil/cli/commands/integrate.py +1371 -124
- package/src/devcouncil/cli/commands/logs.py +106 -0
- package/src/devcouncil/cli/commands/map.py +80 -10
- package/src/devcouncil/cli/commands/okf.py +245 -0
- package/src/devcouncil/cli/commands/plan.py +256 -55
- package/src/devcouncil/cli/commands/prompt.py +18 -7
- package/src/devcouncil/cli/commands/repair.py +50 -24
- package/src/devcouncil/cli/commands/report.py +8 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
- package/src/devcouncil/cli/commands/rollback.py +27 -28
- package/src/devcouncil/cli/commands/run.py +195 -54
- package/src/devcouncil/cli/commands/runs.py +223 -0
- package/src/devcouncil/cli/commands/scaffold.py +32 -0
- package/src/devcouncil/cli/commands/semantic.py +47 -0
- package/src/devcouncil/cli/commands/setup.py +145 -6
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/skills.py +267 -0
- package/src/devcouncil/cli/commands/status.py +30 -15
- package/src/devcouncil/cli/commands/trace.py +47 -3
- package/src/devcouncil/cli/commands/verify.py +144 -3
- package/src/devcouncil/cli/commands/watch.py +32 -12
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +91 -7
- package/src/devcouncil/domain/evidence.py +29 -2
- package/src/devcouncil/domain/gap.py +27 -1
- package/src/devcouncil/domain/task.py +31 -2
- package/src/devcouncil/execution/checkpoints.py +256 -0
- package/src/devcouncil/execution/context_builder.py +1 -1
- package/src/devcouncil/execution/fs_watcher.py +205 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +162 -74
- package/src/devcouncil/execution/patch.py +65 -10
- package/src/devcouncil/execution/permissions.py +24 -24
- package/src/devcouncil/execution/policy_engine.py +350 -0
- package/src/devcouncil/execution/prompt_builder.py +751 -23
- package/src/devcouncil/execution/shell_session.py +231 -0
- package/src/devcouncil/execution/task_runner.py +24 -9
- package/src/devcouncil/executors/agent_registry.py +596 -0
- package/src/devcouncil/executors/coding_cli.py +791 -39
- package/src/devcouncil/executors/mini_swe.py +6 -0
- package/src/devcouncil/executors/native/agent.py +135 -19
- package/src/devcouncil/executors/openhands.py +6 -0
- package/src/devcouncil/gating/checks/clean_git.py +3 -1
- package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
- package/src/devcouncil/gating/policy.py +190 -11
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/ast_matcher.py +17 -7
- package/src/devcouncil/indexing/lsp.py +45 -4
- package/src/devcouncil/indexing/repo_mapper.py +1284 -15
- package/src/devcouncil/indexing/semantic_index.py +221 -0
- package/src/devcouncil/integrations/actions.py +166 -0
- package/src/devcouncil/integrations/check.py +426 -0
- package/src/devcouncil/integrations/claude_assets.py +444 -0
- package/src/devcouncil/integrations/code_review_graph.py +13 -2
- package/src/devcouncil/integrations/github_intent.py +149 -0
- package/src/devcouncil/integrations/gitnexus.py +45 -2
- package/src/devcouncil/integrations/mcp/server.py +1944 -32
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/integrations/pr_comments.py +9 -0
- package/src/devcouncil/knowledge/__init__.py +23 -0
- package/src/devcouncil/knowledge/design.py +374 -0
- package/src/devcouncil/knowledge/design_conformance.py +317 -0
- package/src/devcouncil/knowledge/fetch.py +223 -0
- package/src/devcouncil/knowledge/frontmatter.py +51 -0
- package/src/devcouncil/knowledge/okf.py +202 -0
- package/src/devcouncil/knowledge/skill_bridge.py +96 -0
- package/src/devcouncil/knowledge/sources.py +239 -0
- package/src/devcouncil/live/cards.py +181 -25
- package/src/devcouncil/live/repair_prompt.py +29 -6
- package/src/devcouncil/live/reviewer.py +72 -13
- package/src/devcouncil/live/signals.py +2 -2
- package/src/devcouncil/live/summary.py +18 -8
- package/src/devcouncil/live/transcripts.py +47 -11
- package/src/devcouncil/llm/cache.py +20 -8
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +617 -49
- package/src/devcouncil/llm/router.py +337 -53
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/optimization/skillopt.py +673 -0
- package/src/devcouncil/planning/arbiter_service.py +10 -2
- package/src/devcouncil/planning/correction_manifest.py +346 -0
- package/src/devcouncil/planning/critique_service.py +16 -4
- package/src/devcouncil/planning/plan_service.py +86 -6
- package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
- package/src/devcouncil/planning/repair_service.py +8 -2
- package/src/devcouncil/planning/spec_service.py +37 -3
- package/src/devcouncil/repo/ci_scaffold.py +165 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +384 -0
- package/src/devcouncil/reporting/json_report.py +22 -1
- package/src/devcouncil/reporting/markdown_report.py +29 -1
- package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
- package/src/devcouncil/reporting/okf_html.py +323 -0
- package/src/devcouncil/reporting/report_builder.py +18 -1
- package/src/devcouncil/skills/__init__.py +19 -0
- package/src/devcouncil/skills/library/README.md +46 -0
- package/src/devcouncil/skills/library/ai-training.md +50 -0
- package/src/devcouncil/skills/library/android.md +50 -0
- package/src/devcouncil/skills/library/backend.md +52 -0
- package/src/devcouncil/skills/library/core-engineering.md +95 -0
- package/src/devcouncil/skills/library/data-engineering.md +47 -0
- package/src/devcouncil/skills/library/desktop.md +46 -0
- package/src/devcouncil/skills/library/devops.md +48 -0
- package/src/devcouncil/skills/library/game-dev.md +46 -0
- package/src/devcouncil/skills/library/ios.md +48 -0
- package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
- package/src/devcouncil/skills/library/security.md +48 -0
- package/src/devcouncil/skills/library/systems.md +48 -0
- package/src/devcouncil/skills/library/web.md +47 -0
- package/src/devcouncil/skills/library/windows.md +47 -0
- package/src/devcouncil/skills/registry.py +408 -0
- package/src/devcouncil/storage/db.py +140 -3
- package/src/devcouncil/storage/models.py +125 -0
- package/src/devcouncil/storage/native.py +559 -0
- package/src/devcouncil/storage/repositories.py +157 -78
- package/src/devcouncil/telemetry/cost.py +123 -17
- package/src/devcouncil/telemetry/logging_setup.py +244 -0
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/stages.py +141 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +24 -10
- package/src/devcouncil/ui/dashboard.py +393 -28
- package/src/devcouncil/utils/redaction.py +9 -3
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/acceptance_compiler.py +253 -0
- package/src/devcouncil/verification/ad_hoc_check.py +135 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/implementation_reviewer.py +11 -2
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +181 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1549 -143
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -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
|
|
@@ -23,13 +23,17 @@ def _project_root(project_root: Path | None = None) -> Path:
|
|
|
23
23
|
|
|
24
24
|
|
|
25
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
|
|
26
32
|
db = get_db(root)
|
|
27
33
|
if not db:
|
|
28
34
|
return None
|
|
29
35
|
with db.get_session() as session:
|
|
30
|
-
|
|
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
|
|
36
|
+
return TaskRepository(session).get_by_id(active_id)
|
|
33
37
|
|
|
34
38
|
|
|
35
39
|
def _emit_decision(client: str, action: str, reason: str) -> None:
|
|
@@ -48,31 +52,53 @@ def _emit_decision(client: str, action: str, reason: str) -> None:
|
|
|
48
52
|
console.print(f"[yellow]DevCouncil Warning:[/yellow] {reason}")
|
|
49
53
|
|
|
50
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
|
+
|
|
51
64
|
@app.command()
|
|
52
65
|
def pre_tool_use(
|
|
53
66
|
tool_call_json: str | None = typer.Argument(None, help="The JSON string of the tool call from the coding CLI."),
|
|
54
67
|
client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
|
|
55
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
|
+
),
|
|
56
75
|
):
|
|
57
76
|
"""
|
|
58
77
|
Coding CLI hook: Inspects a tool call before execution.
|
|
59
78
|
Exits with code 2 to block unauthorized file writes.
|
|
60
79
|
"""
|
|
80
|
+
normalized_client = client.lower()
|
|
61
81
|
try:
|
|
62
82
|
if tool_call_json is None:
|
|
63
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.
|
|
64
86
|
if not tool_call_json.strip():
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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)
|
|
68
93
|
root = _project_root(project_root)
|
|
69
94
|
active_task = _active_task(root)
|
|
70
95
|
|
|
71
96
|
decision = HookPolicy(project_root=root).evaluate(call_data, active_task)
|
|
72
97
|
_emit_decision(normalized_client, decision.action, decision.reason)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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)
|
|
76
102
|
|
|
77
103
|
@app.command()
|
|
78
104
|
def post_tool_use(
|
|
@@ -116,13 +142,268 @@ def agent_response(
|
|
|
116
142
|
if client.lower() in {"codex", "gemini"}:
|
|
117
143
|
print(json.dumps({"decision": "allow", "suppressOutput": True}, separators=(",", ":")))
|
|
118
144
|
|
|
145
|
+
def _status_line(root: Path) -> str | None:
|
|
146
|
+
"""A one-line DevCouncil status snapshot, or None when uninitialized/unavailable.
|
|
147
|
+
|
|
148
|
+
Used by the SessionStart and UserPromptSubmit hooks to inject lightweight project
|
|
149
|
+
context into Claude Code. Best-effort: any failure returns None so a hook never
|
|
150
|
+
breaks the session."""
|
|
151
|
+
try:
|
|
152
|
+
db = get_db(root)
|
|
153
|
+
if not db:
|
|
154
|
+
return None
|
|
155
|
+
from devcouncil.storage.repositories import ArtifactGraphRepository, StateRepository
|
|
156
|
+
from devcouncil.app.project_status import compute_phase
|
|
157
|
+
|
|
158
|
+
with db.get_session() as session:
|
|
159
|
+
graph = ArtifactGraphRepository(session).load_graph()
|
|
160
|
+
summary = graph.coverage_summary()
|
|
161
|
+
state = StateRepository(session).get_state()
|
|
162
|
+
phase = compute_phase(graph, state.current_phase if state else None)
|
|
163
|
+
return (
|
|
164
|
+
f"DevCouncil — phase: {phase}; tasks: {summary['total_tasks']}; "
|
|
165
|
+
f"gaps: {summary['total_gaps']} ({summary['blocking_gaps']} blocking). "
|
|
166
|
+
"Use the devcouncil_* MCP tools and `dev` CLI to stay inside the verify loop."
|
|
167
|
+
)
|
|
168
|
+
except Exception:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _emit_additional_context(event_name: str, context: str | None) -> None:
|
|
173
|
+
"""Emit a Claude-Code hook result that injects additionalContext (exit 0)."""
|
|
174
|
+
if not context:
|
|
175
|
+
return
|
|
176
|
+
print(json.dumps({
|
|
177
|
+
"hookSpecificOutput": {
|
|
178
|
+
"hookEventName": event_name,
|
|
179
|
+
"additionalContext": context,
|
|
180
|
+
}
|
|
181
|
+
}, separators=(",", ":")))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _read_stdin_payload(event_json: str | None) -> dict:
|
|
185
|
+
text = event_json if event_json is not None else sys.stdin.read()
|
|
186
|
+
if not text or not text.strip():
|
|
187
|
+
return {}
|
|
188
|
+
try:
|
|
189
|
+
parsed = json.loads(text)
|
|
190
|
+
return parsed if isinstance(parsed, dict) else {"raw": text}
|
|
191
|
+
except json.JSONDecodeError:
|
|
192
|
+
return {"raw": text}
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@app.command()
|
|
196
|
+
def session_start(
|
|
197
|
+
event_json: str | None = typer.Argument(None, help="The JSON hook payload from Claude Code."),
|
|
198
|
+
client: str = typer.Option("claude", "--client", help="Hook client (claude)."),
|
|
199
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
200
|
+
):
|
|
201
|
+
"""Claude Code SessionStart hook: inject a DevCouncil status snapshot as context."""
|
|
202
|
+
_read_stdin_payload(event_json)
|
|
203
|
+
root = _project_root(project_root)
|
|
204
|
+
try:
|
|
205
|
+
TraceLogger(root).log_event("session_start", {"client": client.lower()}, summary="Claude session started.")
|
|
206
|
+
except Exception:
|
|
207
|
+
pass
|
|
208
|
+
_emit_additional_context("SessionStart", _status_line(root))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@app.command()
|
|
212
|
+
def user_prompt_submit(
|
|
213
|
+
event_json: str | None = typer.Argument(None, help="The JSON hook payload from Claude Code."),
|
|
214
|
+
client: str = typer.Option("claude", "--client", help="Hook client (claude)."),
|
|
215
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
216
|
+
):
|
|
217
|
+
"""Claude Code UserPromptSubmit hook: surface the current DevCouncil status as context."""
|
|
218
|
+
_read_stdin_payload(event_json)
|
|
219
|
+
root = _project_root(project_root)
|
|
220
|
+
_emit_additional_context("UserPromptSubmit", _status_line(root))
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
@app.command()
|
|
224
|
+
def session_end(
|
|
225
|
+
event_json: str | None = typer.Argument(None, help="The JSON hook payload from Claude Code."),
|
|
226
|
+
client: str = typer.Option("claude", "--client", help="Hook client (claude)."),
|
|
227
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
228
|
+
):
|
|
229
|
+
"""Claude Code SessionEnd hook: record session teardown in the DevCouncil trace."""
|
|
230
|
+
_read_stdin_payload(event_json)
|
|
231
|
+
root = _project_root(project_root)
|
|
232
|
+
try:
|
|
233
|
+
TraceLogger(root).log_event("session_end", {"client": client.lower()}, summary="Claude session ended.")
|
|
234
|
+
except Exception:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@app.command()
|
|
239
|
+
def pre_compact(
|
|
240
|
+
event_json: str | None = typer.Argument(None, help="The JSON hook payload from Claude Code."),
|
|
241
|
+
client: str = typer.Option("claude", "--client", help="Hook client (claude)."),
|
|
242
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
243
|
+
):
|
|
244
|
+
"""Claude Code PreCompact hook: record that context compaction is about to run."""
|
|
245
|
+
_read_stdin_payload(event_json)
|
|
246
|
+
root = _project_root(project_root)
|
|
247
|
+
try:
|
|
248
|
+
TraceLogger(root).log_event("pre_compact", {"client": client.lower()}, summary="Claude context compaction starting.")
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@app.command()
|
|
254
|
+
def subagent_stop(
|
|
255
|
+
event_json: str | None = typer.Argument(None, help="The JSON hook payload from Claude Code."),
|
|
256
|
+
client: str = typer.Option("claude", "--client", help="Hook client (claude)."),
|
|
257
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
258
|
+
):
|
|
259
|
+
"""Claude Code SubagentStop hook: write a review signal when a subagent finishes."""
|
|
260
|
+
payload = _read_stdin_payload(event_json)
|
|
261
|
+
root = _project_root(project_root)
|
|
262
|
+
if not any(key in payload for key in ("task_id", "taskId", "task")):
|
|
263
|
+
active_id = active_task_id(root)
|
|
264
|
+
if active_id:
|
|
265
|
+
payload["task_id"] = active_id
|
|
266
|
+
try:
|
|
267
|
+
signal_path = write_signal(root, client.lower(), payload)
|
|
268
|
+
TraceLogger(root).log_event(
|
|
269
|
+
"subagent_stop",
|
|
270
|
+
{"client": client.lower(), "signal": str(signal_path)},
|
|
271
|
+
summary=f"{client} subagent finished; signal recorded.",
|
|
272
|
+
)
|
|
273
|
+
except Exception:
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@app.command()
|
|
278
|
+
def notification(
|
|
279
|
+
event_json: str | None = typer.Argument(None, help="The JSON hook payload from Claude Code."),
|
|
280
|
+
client: str = typer.Option("claude", "--client", help="Hook client (claude)."),
|
|
281
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
282
|
+
):
|
|
283
|
+
"""Claude Code Notification hook: record a Claude notification in the DevCouncil trace."""
|
|
284
|
+
payload = _read_stdin_payload(event_json)
|
|
285
|
+
root = _project_root(project_root)
|
|
286
|
+
try:
|
|
287
|
+
TraceLogger(root).log_event(
|
|
288
|
+
"claude_notification",
|
|
289
|
+
{"client": client.lower(), "message": str(payload.get("message", ""))[:200]},
|
|
290
|
+
summary="Claude notification.",
|
|
291
|
+
)
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
@app.command()
|
|
297
|
+
def claude_statusline(
|
|
298
|
+
event_json: str | None = typer.Argument(None, help="The JSON status payload from Claude Code."),
|
|
299
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
300
|
+
):
|
|
301
|
+
"""Claude Code statusLine command: print a compact DevCouncil status line.
|
|
302
|
+
|
|
303
|
+
Reads Claude's status JSON on stdin (cwd, model, ...) and prints one line. Falls back
|
|
304
|
+
to a minimal marker when the project isn't initialized so the status bar never breaks."""
|
|
305
|
+
payload = _read_stdin_payload(event_json)
|
|
306
|
+
# Prefer the cwd Claude reports so the line reflects the active workspace.
|
|
307
|
+
cwd = payload.get("cwd") if isinstance(payload, dict) else None
|
|
308
|
+
root = _project_root(Path(cwd) if isinstance(cwd, str) and cwd else project_root)
|
|
309
|
+
line = _status_line(root)
|
|
310
|
+
if not line:
|
|
311
|
+
print("DevCouncil: not initialized")
|
|
312
|
+
return
|
|
313
|
+
# statusLine wants a short line; drop the trailing guidance sentence.
|
|
314
|
+
print(line.split(". Use the")[0])
|
|
315
|
+
|
|
316
|
+
|
|
119
317
|
@app.command()
|
|
120
|
-
def post_task(
|
|
318
|
+
def post_task(
|
|
319
|
+
client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
|
|
320
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
321
|
+
):
|
|
121
322
|
"""
|
|
122
323
|
Coding CLI hook: Runs after a task is completed.
|
|
123
|
-
|
|
324
|
+
|
|
325
|
+
When ``execution.verify_on_post_task`` is enabled, this runs deterministic
|
|
326
|
+
verification of the active task and records gaps; otherwise it just reminds the
|
|
327
|
+
user to run ``dev verify`` (the default, to keep hooks fast/cheap).
|
|
124
328
|
"""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
329
|
+
root = _project_root(project_root)
|
|
330
|
+
try:
|
|
331
|
+
from devcouncil.app.config import load_config
|
|
332
|
+
verify_enabled = load_config(root).execution.verify_on_post_task
|
|
333
|
+
except Exception:
|
|
334
|
+
verify_enabled = False
|
|
335
|
+
|
|
336
|
+
if not verify_enabled:
|
|
337
|
+
console.print("[cyan]DevCouncil: coding agent finished task.[/cyan]")
|
|
338
|
+
console.print("Run [bold]dev verify[/bold] to finalize implementation evidence.")
|
|
339
|
+
_emit_post_task_allow(client)
|
|
340
|
+
return
|
|
341
|
+
|
|
342
|
+
summary = _verify_active_task(root)
|
|
343
|
+
console.print(summary)
|
|
344
|
+
_emit_post_task_allow(client)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _emit_post_task_allow(client: str) -> None:
|
|
348
|
+
if client.lower() in {"codex", "gemini"}:
|
|
349
|
+
print(json.dumps({"decision": "allow", "suppressOutput": True}, separators=(",", ":")))
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _verify_active_task(root: Path) -> str:
|
|
353
|
+
"""Run deterministic verification of the active task and persist gaps/evidence.
|
|
354
|
+
Returns a human summary line. Best-effort: never raises out of a hook."""
|
|
355
|
+
try:
|
|
356
|
+
import asyncio
|
|
357
|
+
|
|
358
|
+
from devcouncil.domain.evidence import CommandResult, DiffCoverageEvidence, DiffEvidence, TestEvidence
|
|
359
|
+
from devcouncil.storage.repositories import (
|
|
360
|
+
EvidenceRepository,
|
|
361
|
+
GapRepository,
|
|
362
|
+
RequirementRepository,
|
|
363
|
+
)
|
|
364
|
+
from devcouncil.verification.next_actions import split_next_actions
|
|
365
|
+
from devcouncil.verification.verifier import Verifier
|
|
366
|
+
|
|
367
|
+
active_id = active_task_id(root)
|
|
368
|
+
db = get_db(root)
|
|
369
|
+
if not active_id or not db:
|
|
370
|
+
return "Run [bold]dev verify[/bold] to finalize implementation evidence."
|
|
371
|
+
with db.get_session() as session:
|
|
372
|
+
task = TaskRepository(session).get_by_id(active_id)
|
|
373
|
+
if not task:
|
|
374
|
+
return "Run [bold]dev verify[/bold] to finalize implementation evidence."
|
|
375
|
+
reqs = RequirementRepository(session).get_all()
|
|
376
|
+
gaps, evidence = asyncio.run(Verifier(root).verify_task(task, reqs))
|
|
377
|
+
gap_repo = GapRepository(session)
|
|
378
|
+
ev_repo = EvidenceRepository(session)
|
|
379
|
+
gap_repo.delete_for_task(task.id)
|
|
380
|
+
ev_repo.delete_for_task(task.id)
|
|
381
|
+
for gap in gaps:
|
|
382
|
+
gap_repo.save(gap)
|
|
383
|
+
for ev in evidence:
|
|
384
|
+
if isinstance(ev, CommandResult):
|
|
385
|
+
ev_repo.save_command_result(task.id, ev)
|
|
386
|
+
elif isinstance(ev, DiffCoverageEvidence):
|
|
387
|
+
ev_repo.save_diff_coverage_evidence(ev)
|
|
388
|
+
elif isinstance(ev, DiffEvidence):
|
|
389
|
+
ev_repo.save_diff_evidence(ev)
|
|
390
|
+
elif isinstance(ev, TestEvidence):
|
|
391
|
+
ev_repo.save_test_evidence(ev, task.id)
|
|
392
|
+
blocking = [g for g in gaps if g.blocking]
|
|
393
|
+
task.status = "blocked" if blocking else "verified"
|
|
394
|
+
TaskRepository(session).save(task)
|
|
395
|
+
blocking_actions, _ = split_next_actions(gaps)
|
|
396
|
+
TraceLogger(root).log_event(
|
|
397
|
+
"post_task_verified",
|
|
398
|
+
{"task_id": active_id, "blocking": len(blocking)},
|
|
399
|
+
task_id=active_id,
|
|
400
|
+
summary=f"post_task verification: {task.status}",
|
|
401
|
+
)
|
|
402
|
+
if blocking:
|
|
403
|
+
return (
|
|
404
|
+
f"[yellow]{active_id} is blocked by {len(blocking)} gap(s); "
|
|
405
|
+
f"{len(blocking_actions)} next action(s). Run [bold]dev repair[/bold].[/yellow]"
|
|
406
|
+
)
|
|
407
|
+
return f"[green]{active_id} verified.[/green]"
|
|
408
|
+
except Exception as exc: # never let a hook crash the agent
|
|
409
|
+
return f"[dim]post-task verification skipped: {exc}[/dim]"
|