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.
Files changed (190) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +197 -494
  3. package/package.json +9 -2
  4. package/pyproject.toml +62 -27
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +297 -108
  8. package/src/devcouncil/app/errors.py +23 -23
  9. package/src/devcouncil/app/events.py +44 -44
  10. package/src/devcouncil/app/orchestrator.py +67 -67
  11. package/src/devcouncil/app/project_status.py +29 -0
  12. package/src/devcouncil/app/run_context.py +39 -39
  13. package/src/devcouncil/app/state_machine.py +108 -108
  14. package/src/devcouncil/artifacts/__init__.py +1 -1
  15. package/src/devcouncil/artifacts/coverage.py +96 -96
  16. package/src/devcouncil/artifacts/graph.py +163 -143
  17. package/src/devcouncil/artifacts/migrations.py +20 -20
  18. package/src/devcouncil/artifacts/schemas.py +23 -23
  19. package/src/devcouncil/artifacts/serializer.py +21 -21
  20. package/src/devcouncil/artifacts/validators.py +27 -27
  21. package/src/devcouncil/assets/__init__.py +1 -0
  22. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  23. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  24. package/src/devcouncil/cli/commands/agents.py +292 -0
  25. package/src/devcouncil/cli/commands/artifacts.py +54 -48
  26. package/src/devcouncil/cli/commands/ast.py +22 -0
  27. package/src/devcouncil/cli/commands/baseline.py +35 -32
  28. package/src/devcouncil/cli/commands/check.py +209 -0
  29. package/src/devcouncil/cli/commands/config.py +115 -54
  30. package/src/devcouncil/cli/commands/cost.py +57 -0
  31. package/src/devcouncil/cli/commands/dashboard.py +31 -0
  32. package/src/devcouncil/cli/commands/doctor.py +291 -47
  33. package/src/devcouncil/cli/commands/evidence.py +48 -0
  34. package/src/devcouncil/cli/commands/go.py +656 -0
  35. package/src/devcouncil/cli/commands/handoff.py +69 -0
  36. package/src/devcouncil/cli/commands/hook.py +209 -33
  37. package/src/devcouncil/cli/commands/init.py +204 -57
  38. package/src/devcouncil/cli/commands/integrate.py +1171 -76
  39. package/src/devcouncil/cli/commands/lsp.py +20 -0
  40. package/src/devcouncil/cli/commands/map.py +96 -22
  41. package/src/devcouncil/cli/commands/plan.py +422 -210
  42. package/src/devcouncil/cli/commands/prompt.py +48 -34
  43. package/src/devcouncil/cli/commands/repair.py +89 -69
  44. package/src/devcouncil/cli/commands/report.py +120 -54
  45. package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
  46. package/src/devcouncil/cli/commands/rollback.py +55 -54
  47. package/src/devcouncil/cli/commands/run.py +285 -220
  48. package/src/devcouncil/cli/commands/runs.py +223 -0
  49. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  50. package/src/devcouncil/cli/commands/semantic.py +47 -0
  51. package/src/devcouncil/cli/commands/setup.py +300 -20
  52. package/src/devcouncil/cli/commands/shell.py +73 -0
  53. package/src/devcouncil/cli/commands/show.py +76 -57
  54. package/src/devcouncil/cli/commands/skills.py +88 -0
  55. package/src/devcouncil/cli/commands/status.py +141 -105
  56. package/src/devcouncil/cli/commands/tasks.py +55 -41
  57. package/src/devcouncil/cli/commands/trace.py +49 -4
  58. package/src/devcouncil/cli/commands/verify.py +293 -128
  59. package/src/devcouncil/cli/commands/version.py +20 -20
  60. package/src/devcouncil/cli/commands/watch.py +574 -0
  61. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  62. package/src/devcouncil/cli/main.py +92 -25
  63. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  64. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  65. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  66. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  67. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  68. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  69. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  70. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  71. package/src/devcouncil/domain/assumption.py +17 -17
  72. package/src/devcouncil/domain/critique.py +32 -32
  73. package/src/devcouncil/domain/evidence.py +47 -27
  74. package/src/devcouncil/domain/gap.py +52 -26
  75. package/src/devcouncil/domain/requirement.py +22 -22
  76. package/src/devcouncil/domain/task.py +55 -26
  77. package/src/devcouncil/execution/__init__.py +1 -1
  78. package/src/devcouncil/execution/checkpoints.py +246 -0
  79. package/src/devcouncil/execution/context_builder.py +54 -54
  80. package/src/devcouncil/execution/executor.py +15 -15
  81. package/src/devcouncil/execution/fs_watcher.py +180 -0
  82. package/src/devcouncil/execution/handoff.py +102 -0
  83. package/src/devcouncil/execution/hook_policy.py +186 -77
  84. package/src/devcouncil/execution/patch.py +77 -28
  85. package/src/devcouncil/execution/permissions.py +52 -59
  86. package/src/devcouncil/execution/policy_engine.py +343 -0
  87. package/src/devcouncil/execution/prompt_builder.py +650 -38
  88. package/src/devcouncil/execution/shell_session.py +225 -0
  89. package/src/devcouncil/execution/task_runner.py +68 -64
  90. package/src/devcouncil/executors/__init__.py +1 -1
  91. package/src/devcouncil/executors/agent_registry.py +575 -0
  92. package/src/devcouncil/executors/coding_cli.py +736 -0
  93. package/src/devcouncil/executors/mini_swe.py +63 -63
  94. package/src/devcouncil/executors/native/agent.py +186 -85
  95. package/src/devcouncil/executors/openhands.py +56 -56
  96. package/src/devcouncil/gating/__init__.py +1 -1
  97. package/src/devcouncil/gating/checks/clean_git.py +52 -45
  98. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  99. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  100. package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
  101. package/src/devcouncil/gating/policy.py +315 -167
  102. package/src/devcouncil/hardware.py +184 -0
  103. package/src/devcouncil/indexing/__init__.py +1 -1
  104. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  105. package/src/devcouncil/indexing/graph_index.py +48 -48
  106. package/src/devcouncil/indexing/lsp.py +161 -0
  107. package/src/devcouncil/indexing/repo_mapper.py +1455 -204
  108. package/src/devcouncil/indexing/semantic_index.py +205 -0
  109. package/src/devcouncil/integrations/actions.py +146 -0
  110. package/src/devcouncil/integrations/check.py +423 -0
  111. package/src/devcouncil/integrations/github.py +35 -35
  112. package/src/devcouncil/integrations/github_intent.py +142 -0
  113. package/src/devcouncil/integrations/gitnexus.py +62 -27
  114. package/src/devcouncil/integrations/graphify.py +34 -34
  115. package/src/devcouncil/integrations/mcp/server.py +2072 -96
  116. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  117. package/src/devcouncil/integrations/pr_comments.py +62 -0
  118. package/src/devcouncil/live/__init__.py +2 -0
  119. package/src/devcouncil/live/cards.py +349 -0
  120. package/src/devcouncil/live/models.py +63 -0
  121. package/src/devcouncil/live/repair_prompt.py +83 -0
  122. package/src/devcouncil/live/reviewer.py +70 -0
  123. package/src/devcouncil/live/signals.py +135 -0
  124. package/src/devcouncil/live/summary.py +34 -0
  125. package/src/devcouncil/live/tasks.py +18 -0
  126. package/src/devcouncil/live/transcripts.py +141 -0
  127. package/src/devcouncil/llm/__init__.py +1 -1
  128. package/src/devcouncil/llm/cache.py +42 -38
  129. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  130. package/src/devcouncil/llm/provider.py +627 -125
  131. package/src/devcouncil/llm/router.py +303 -118
  132. package/src/devcouncil/optimization/__init__.py +1 -0
  133. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  134. package/src/devcouncil/planning/__init__.py +1 -1
  135. package/src/devcouncil/planning/arbiter_service.py +57 -57
  136. package/src/devcouncil/planning/correction_manifest.py +303 -0
  137. package/src/devcouncil/planning/critique_service.py +71 -66
  138. package/src/devcouncil/planning/plan_service.py +60 -46
  139. package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
  140. package/src/devcouncil/planning/repair_service.py +39 -39
  141. package/src/devcouncil/planning/spec_service.py +70 -44
  142. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  143. package/src/devcouncil/repo/gitignore.py +123 -0
  144. package/src/devcouncil/repo/sca.py +374 -0
  145. package/src/devcouncil/reporting/github_check.py +32 -32
  146. package/src/devcouncil/reporting/json_report.py +30 -17
  147. package/src/devcouncil/reporting/markdown_report.py +83 -46
  148. package/src/devcouncil/reporting/report_builder.py +14 -14
  149. package/src/devcouncil/skills/__init__.py +19 -0
  150. package/src/devcouncil/skills/library/README.md +46 -0
  151. package/src/devcouncil/skills/library/ai-training.md +50 -0
  152. package/src/devcouncil/skills/library/android.md +50 -0
  153. package/src/devcouncil/skills/library/backend.md +52 -0
  154. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  155. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  156. package/src/devcouncil/skills/library/desktop.md +46 -0
  157. package/src/devcouncil/skills/library/devops.md +48 -0
  158. package/src/devcouncil/skills/library/game-dev.md +46 -0
  159. package/src/devcouncil/skills/library/ios.md +48 -0
  160. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  161. package/src/devcouncil/skills/library/security.md +48 -0
  162. package/src/devcouncil/skills/library/systems.md +48 -0
  163. package/src/devcouncil/skills/library/web.md +47 -0
  164. package/src/devcouncil/skills/library/windows.md +47 -0
  165. package/src/devcouncil/skills/registry.py +330 -0
  166. package/src/devcouncil/storage/db.py +147 -66
  167. package/src/devcouncil/storage/models.py +204 -83
  168. package/src/devcouncil/storage/native.py +557 -0
  169. package/src/devcouncil/storage/repositories.py +388 -249
  170. package/src/devcouncil/telemetry/cost.py +140 -34
  171. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  172. package/src/devcouncil/telemetry/pricing.py +28 -0
  173. package/src/devcouncil/telemetry/traces.py +62 -7
  174. package/src/devcouncil/telemetry/tracker.py +52 -49
  175. package/src/devcouncil/ui/__init__.py +1 -0
  176. package/src/devcouncil/ui/dashboard.py +423 -0
  177. package/src/devcouncil/utils/__init__.py +1 -1
  178. package/src/devcouncil/utils/redaction.py +147 -141
  179. package/src/devcouncil/utils/subprocess_env.py +69 -0
  180. package/src/devcouncil/verification/__init__.py +1 -1
  181. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  182. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  183. package/src/devcouncil/verification/diff_coverage.py +353 -0
  184. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  185. package/src/devcouncil/verification/next_actions.py +189 -0
  186. package/src/devcouncil/verification/sandbox.py +178 -0
  187. package/src/devcouncil/verification/test_resolver.py +91 -0
  188. package/src/devcouncil/verification/verifier.py +1342 -307
  189. package/uv.lock +205 -64
  190. 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 Claude Code")
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
- Claude Code hook: Inspects a tool call before execution.
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
- raise typer.Exit(code=0)
32
- call_data = json.loads(tool_call_json)
33
- active_task = None
34
- root = _project_root()
35
- db = get_db(root)
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
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
- if decision.action == "deny":
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}")
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
- raise typer.Exit(code=0)
51
-
52
- @app.command()
53
- def post_task():
54
- """
55
- Claude Code hook: Runs after a task is completed.
56
- Triggers deterministic verification.
57
- """
58
- console.print("[cyan]DevCouncil: Claude finished task. Triggering automatic verification...[/cyan]")
59
- # In a real environment, this would invoke 'dev verify <active-task>'
60
- # For the hook script, we just notify the user.
61
- console.print("Run [bold]dev verify[/bold] to finalize implementation evidence.")
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 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
+ 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
- "spec_writer": {"model": "anthropic/claude-3.5-sonnet"},
23
- "planner_a": {"model": "anthropic/claude-3.5-sonnet"},
24
- "planner_b": {"model": "google/gemini-pro-1.5"},
25
- "critic_a": {"model": "openai/gpt-4o"},
26
- "critic_b": {"model": "anthropic/claude-3-opus"},
27
- "arbiter": {"model": "openai/gpt-4o"},
28
- "native_agent": {"model": "anthropic/claude-3.5-sonnet"},
29
- "implementation_reviewer": {"model": "openai/gpt-4o"},
30
- }
31
- },
32
- "commands": {
33
- "test": ["pytest", "npm test"],
34
- "lint": ["flake8", "eslint"],
35
- "typecheck": ["mypy", "tsc"]
36
- },
37
- "gates": {
38
- "require_clean_git_before_task": True,
39
- "block_orphan_diffs": True,
40
- "block_missing_tests_for_high_requirements": True,
41
- "block_dependency_changes_without_approval": True,
42
- "block_schema_change_without_migration": True,
43
- "block_failed_commands": True
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
- console.print("Initializing DevCouncil...")
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
- console.print(f"[green]Successfully initialized DevCouncil in {dev_dir}[/green]")
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
- 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
- """
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
  )