devcouncil 0.2.0 → 0.2.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.
Files changed (205) hide show
  1. package/README.md +191 -253
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +6 -17
  4. package/pyproject.toml +0 -66
  5. package/src/devcouncil/__init__.py +0 -0
  6. package/src/devcouncil/__main__.py +0 -4
  7. package/src/devcouncil/app/__init__.py +0 -28
  8. package/src/devcouncil/app/config.py +0 -320
  9. package/src/devcouncil/app/errors.py +0 -23
  10. package/src/devcouncil/app/events.py +0 -44
  11. package/src/devcouncil/app/orchestrator.py +0 -92
  12. package/src/devcouncil/app/project_status.py +0 -29
  13. package/src/devcouncil/app/run_context.py +0 -39
  14. package/src/devcouncil/app/state_machine.py +0 -108
  15. package/src/devcouncil/artifacts/__init__.py +0 -1
  16. package/src/devcouncil/artifacts/coverage.py +0 -96
  17. package/src/devcouncil/artifacts/graph.py +0 -163
  18. package/src/devcouncil/artifacts/migrations.py +0 -20
  19. package/src/devcouncil/artifacts/schemas.py +0 -23
  20. package/src/devcouncil/artifacts/serializer.py +0 -21
  21. package/src/devcouncil/artifacts/validators.py +0 -27
  22. package/src/devcouncil/assets/__init__.py +0 -1
  23. package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
  24. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  25. package/src/devcouncil/cli/__init__.py +0 -0
  26. package/src/devcouncil/cli/commands/__init__.py +0 -0
  27. package/src/devcouncil/cli/commands/agents.py +0 -292
  28. package/src/devcouncil/cli/commands/artifacts.py +0 -54
  29. package/src/devcouncil/cli/commands/ast.py +0 -22
  30. package/src/devcouncil/cli/commands/baseline.py +0 -35
  31. package/src/devcouncil/cli/commands/check.py +0 -209
  32. package/src/devcouncil/cli/commands/config.py +0 -115
  33. package/src/devcouncil/cli/commands/cost.py +0 -57
  34. package/src/devcouncil/cli/commands/dashboard.py +0 -31
  35. package/src/devcouncil/cli/commands/doctor.py +0 -340
  36. package/src/devcouncil/cli/commands/evidence.py +0 -48
  37. package/src/devcouncil/cli/commands/go.py +0 -656
  38. package/src/devcouncil/cli/commands/handoff.py +0 -69
  39. package/src/devcouncil/cli/commands/hook.py +0 -237
  40. package/src/devcouncil/cli/commands/init.py +0 -289
  41. package/src/devcouncil/cli/commands/integrate.py +0 -1515
  42. package/src/devcouncil/cli/commands/lsp.py +0 -20
  43. package/src/devcouncil/cli/commands/map.py +0 -112
  44. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  45. package/src/devcouncil/cli/commands/plan.py +0 -488
  46. package/src/devcouncil/cli/commands/prompt.py +0 -61
  47. package/src/devcouncil/cli/commands/repair.py +0 -89
  48. package/src/devcouncil/cli/commands/report.py +0 -137
  49. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
  50. package/src/devcouncil/cli/commands/rollback.py +0 -59
  51. package/src/devcouncil/cli/commands/run.py +0 -289
  52. package/src/devcouncil/cli/commands/runs.py +0 -223
  53. package/src/devcouncil/cli/commands/scaffold.py +0 -32
  54. package/src/devcouncil/cli/commands/semantic.py +0 -47
  55. package/src/devcouncil/cli/commands/setup.py +0 -362
  56. package/src/devcouncil/cli/commands/shell.py +0 -73
  57. package/src/devcouncil/cli/commands/show.py +0 -76
  58. package/src/devcouncil/cli/commands/skills.py +0 -88
  59. package/src/devcouncil/cli/commands/status.py +0 -141
  60. package/src/devcouncil/cli/commands/tasks.py +0 -55
  61. package/src/devcouncil/cli/commands/trace.py +0 -88
  62. package/src/devcouncil/cli/commands/verify.py +0 -328
  63. package/src/devcouncil/cli/commands/version.py +0 -20
  64. package/src/devcouncil/cli/commands/watch.py +0 -574
  65. package/src/devcouncil/cli/commands/watch_fs.py +0 -40
  66. package/src/devcouncil/cli/main.py +0 -137
  67. package/src/devcouncil/council/__init__.py +0 -0
  68. package/src/devcouncil/council/prompts/__init__.py +0 -0
  69. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  70. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  71. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  72. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  73. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  74. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  75. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  76. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  77. package/src/devcouncil/domain/__init__.py +0 -0
  78. package/src/devcouncil/domain/assumption.py +0 -17
  79. package/src/devcouncil/domain/critique.py +0 -32
  80. package/src/devcouncil/domain/evidence.py +0 -47
  81. package/src/devcouncil/domain/gap.py +0 -52
  82. package/src/devcouncil/domain/requirement.py +0 -22
  83. package/src/devcouncil/domain/task.py +0 -55
  84. package/src/devcouncil/execution/__init__.py +0 -1
  85. package/src/devcouncil/execution/checkpoints.py +0 -246
  86. package/src/devcouncil/execution/context_builder.py +0 -60
  87. package/src/devcouncil/execution/executor.py +0 -15
  88. package/src/devcouncil/execution/fs_watcher.py +0 -180
  89. package/src/devcouncil/execution/handoff.py +0 -102
  90. package/src/devcouncil/execution/hook_policy.py +0 -253
  91. package/src/devcouncil/execution/patch.py +0 -77
  92. package/src/devcouncil/execution/paths.py +0 -14
  93. package/src/devcouncil/execution/permissions.py +0 -85
  94. package/src/devcouncil/execution/policy_engine.py +0 -343
  95. package/src/devcouncil/execution/prompt_builder.py +0 -671
  96. package/src/devcouncil/execution/shell_session.py +0 -225
  97. package/src/devcouncil/execution/task_runner.py +0 -170
  98. package/src/devcouncil/executors/__init__.py +0 -1
  99. package/src/devcouncil/executors/agent_registry.py +0 -575
  100. package/src/devcouncil/executors/coding_cli.py +0 -736
  101. package/src/devcouncil/executors/mini_swe.py +0 -73
  102. package/src/devcouncil/executors/native/__init__.py +0 -0
  103. package/src/devcouncil/executors/native/agent.py +0 -208
  104. package/src/devcouncil/executors/openhands.py +0 -71
  105. package/src/devcouncil/gating/__init__.py +0 -1
  106. package/src/devcouncil/gating/checks/__init__.py +0 -0
  107. package/src/devcouncil/gating/checks/clean_git.py +0 -52
  108. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  109. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  110. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
  111. package/src/devcouncil/gating/policy.py +0 -338
  112. package/src/devcouncil/hardware.py +0 -184
  113. package/src/devcouncil/indexing/__init__.py +0 -1
  114. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  115. package/src/devcouncil/indexing/graph_index.py +0 -48
  116. package/src/devcouncil/indexing/lsp.py +0 -161
  117. package/src/devcouncil/indexing/repo_mapper.py +0 -1455
  118. package/src/devcouncil/indexing/semantic_index.py +0 -205
  119. package/src/devcouncil/integrations/actions.py +0 -146
  120. package/src/devcouncil/integrations/check.py +0 -423
  121. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  122. package/src/devcouncil/integrations/github.py +0 -39
  123. package/src/devcouncil/integrations/github_intent.py +0 -142
  124. package/src/devcouncil/integrations/gitnexus.py +0 -62
  125. package/src/devcouncil/integrations/graphify.py +0 -34
  126. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  127. package/src/devcouncil/integrations/mcp/server.py +0 -2122
  128. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
  129. package/src/devcouncil/integrations/pr_comments.py +0 -62
  130. package/src/devcouncil/live/__init__.py +0 -2
  131. package/src/devcouncil/live/cards.py +0 -349
  132. package/src/devcouncil/live/models.py +0 -63
  133. package/src/devcouncil/live/repair_prompt.py +0 -83
  134. package/src/devcouncil/live/reviewer.py +0 -70
  135. package/src/devcouncil/live/signals.py +0 -135
  136. package/src/devcouncil/live/summary.py +0 -34
  137. package/src/devcouncil/live/tasks.py +0 -18
  138. package/src/devcouncil/live/transcripts.py +0 -141
  139. package/src/devcouncil/llm/__init__.py +0 -1
  140. package/src/devcouncil/llm/cache.py +0 -42
  141. package/src/devcouncil/llm/model_defaults.yaml +0 -44
  142. package/src/devcouncil/llm/provider.py +0 -627
  143. package/src/devcouncil/llm/router.py +0 -310
  144. package/src/devcouncil/optimization/__init__.py +0 -1
  145. package/src/devcouncil/optimization/gepa_agent.py +0 -318
  146. package/src/devcouncil/planning/__init__.py +0 -1
  147. package/src/devcouncil/planning/arbiter_service.py +0 -57
  148. package/src/devcouncil/planning/correction_manifest.py +0 -303
  149. package/src/devcouncil/planning/critique_service.py +0 -71
  150. package/src/devcouncil/planning/plan_service.py +0 -60
  151. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
  152. package/src/devcouncil/planning/repair_service.py +0 -39
  153. package/src/devcouncil/planning/spec_service.py +0 -70
  154. package/src/devcouncil/repo/__init__.py +0 -0
  155. package/src/devcouncil/repo/ci_scaffold.py +0 -157
  156. package/src/devcouncil/repo/gitignore.py +0 -123
  157. package/src/devcouncil/repo/sca.py +0 -374
  158. package/src/devcouncil/reporting/__init__.py +0 -0
  159. package/src/devcouncil/reporting/github_check.py +0 -32
  160. package/src/devcouncil/reporting/json_report.py +0 -30
  161. package/src/devcouncil/reporting/markdown_report.py +0 -83
  162. package/src/devcouncil/reporting/report_builder.py +0 -14
  163. package/src/devcouncil/skills/__init__.py +0 -19
  164. package/src/devcouncil/skills/library/README.md +0 -46
  165. package/src/devcouncil/skills/library/ai-training.md +0 -50
  166. package/src/devcouncil/skills/library/android.md +0 -50
  167. package/src/devcouncil/skills/library/backend.md +0 -52
  168. package/src/devcouncil/skills/library/core-engineering.md +0 -95
  169. package/src/devcouncil/skills/library/data-engineering.md +0 -47
  170. package/src/devcouncil/skills/library/desktop.md +0 -46
  171. package/src/devcouncil/skills/library/devops.md +0 -48
  172. package/src/devcouncil/skills/library/game-dev.md +0 -46
  173. package/src/devcouncil/skills/library/ios.md +0 -48
  174. package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
  175. package/src/devcouncil/skills/library/security.md +0 -48
  176. package/src/devcouncil/skills/library/systems.md +0 -48
  177. package/src/devcouncil/skills/library/web.md +0 -47
  178. package/src/devcouncil/skills/library/windows.md +0 -47
  179. package/src/devcouncil/skills/registry.py +0 -330
  180. package/src/devcouncil/storage/__init__.py +0 -0
  181. package/src/devcouncil/storage/db.py +0 -147
  182. package/src/devcouncil/storage/models.py +0 -204
  183. package/src/devcouncil/storage/native.py +0 -557
  184. package/src/devcouncil/storage/repositories.py +0 -485
  185. package/src/devcouncil/telemetry/__init__.py +0 -0
  186. package/src/devcouncil/telemetry/cost.py +0 -140
  187. package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
  188. package/src/devcouncil/telemetry/pricing.py +0 -28
  189. package/src/devcouncil/telemetry/traces.py +0 -146
  190. package/src/devcouncil/telemetry/tracker.py +0 -52
  191. package/src/devcouncil/ui/__init__.py +0 -1
  192. package/src/devcouncil/ui/dashboard.py +0 -423
  193. package/src/devcouncil/utils/__init__.py +0 -1
  194. package/src/devcouncil/utils/redaction.py +0 -147
  195. package/src/devcouncil/utils/subprocess_env.py +0 -69
  196. package/src/devcouncil/verification/__init__.py +0 -1
  197. package/src/devcouncil/verification/acceptance_compiler.py +0 -125
  198. package/src/devcouncil/verification/ad_hoc_check.py +0 -129
  199. package/src/devcouncil/verification/diff_coverage.py +0 -353
  200. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  201. package/src/devcouncil/verification/next_actions.py +0 -189
  202. package/src/devcouncil/verification/sandbox.py +0 -178
  203. package/src/devcouncil/verification/test_resolver.py +0 -91
  204. package/src/devcouncil/verification/verifier.py +0 -1548
  205. package/uv.lock +0 -1226
@@ -1,69 +0,0 @@
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
@@ -1,237 +0,0 @@
1
- import typer
2
- import json
3
- import os
4
- import sys
5
- from pathlib import Path
6
- from rich.console import Console
7
- from devcouncil.storage.db import get_db
8
- from devcouncil.storage.repositories import TaskRepository
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
13
-
14
- app = typer.Typer()
15
- console = Console()
16
-
17
-
18
- def _project_root(project_root: Path | None = None) -> Path:
19
- if project_root:
20
- return project_root.expanduser().resolve()
21
- configured = os.environ.get("DEVCOUNCIL_PROJECT_ROOT")
22
- return Path(configured).expanduser().resolve() if configured else Path(".").resolve()
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
-
64
- @app.command()
65
- def pre_tool_use(
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
- ),
75
- ):
76
- """
77
- Coding CLI hook: Inspects a tool call before execution.
78
- Exits with code 2 to block unauthorized file writes.
79
- """
80
- normalized_client = client.lower()
81
- try:
82
- if tool_call_json is None:
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.
86
- if not tool_call_json.strip():
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)
95
-
96
- decision = HookPolicy(project_root=root).evaluate(call_data, active_task)
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 {}
130
- except json.JSONDecodeError:
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,289 +0,0 @@
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
-
46
- DEFAULT_CONFIG = {
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
- },
77
- "privacy": {
78
- "redact_env_vars": True,
79
- "redact_secrets_in_logs": True,
80
- "store_prompts_locally": True
81
- },
82
- "integrations": {
83
- "agent_flow": {
84
- "enabled": False,
85
- "trace_path": ".devcouncil/logs/traces.jsonl",
86
- "mode": "jsonl",
87
- },
88
- "code_review_graph": {
89
- "enabled": False,
90
- "command": "code-review-graph",
91
- "optional": True,
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
- },
119
- }
120
- }
121
-
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
-
173
- def initialize_project(
174
- project_root: Path = Path("."),
175
- project_name: str | None = None,
176
- model_provider: str = "openrouter",
177
- model: str | None = None,
178
- role_models: dict[str, str] | None = None,
179
- with_gitnexus: bool = False,
180
- with_graphify: bool = False,
181
- with_map: bool = True,
182
- with_skills: bool = True,
183
- quiet: bool = False,
184
- ) -> bool:
185
- """Initialize DevCouncil project state.
186
-
187
- Returns True when a fresh .devcouncil directory was created.
188
- """
189
- project_root = project_root.resolve()
190
- dev_dir = project_root / ".devcouncil"
191
- created = False
192
-
193
- if not dev_dir.exists():
194
- if not quiet:
195
- console.print("Initializing DevCouncil...")
196
- dev_dir.mkdir(exist_ok=True)
197
- (dev_dir / "runs").mkdir(exist_ok=True)
198
- (dev_dir / "cache").mkdir(exist_ok=True)
199
- (dev_dir / "checkpoints").mkdir(exist_ok=True)
200
- (dev_dir / "logs").mkdir(exist_ok=True)
201
-
202
- config_path = dev_dir / "config.yaml"
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)
206
- if project_name:
207
- config["project"]["name"] = project_name
208
- else:
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
- )
217
-
218
- with open(config_path, "w") as f:
219
- yaml.dump(config, f, default_flow_style=False)
220
-
221
- db = Database(dev_dir / "state.sqlite")
222
- db.create_db_and_tables()
223
- if not quiet:
224
- console.print(f"[green]Successfully initialized DevCouncil in {dev_dir}[/green]")
225
- created = True
226
-
227
- if with_map:
228
- _generate_initial_map(project_root, quiet)
229
- if with_skills:
230
- _scaffold_initial_skills(project_root, quiet)
231
-
232
- if with_gitnexus:
233
- nexus = GitNexusIntegration(project_root)
234
- nexus.initialize()
235
-
236
- if with_graphify:
237
- graphify = GraphifyIntegration(project_root)
238
- graphify.initialize()
239
-
240
- ensure_gitignore(project_root)
241
- return created
242
-
243
-
244
- @app.callback(invoke_without_command=True)
245
- def init(
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
- """
261
- Initialize DevCouncil in the current directory.
262
- """
263
- if ctx.invoked_subcommand is not None:
264
- return
265
-
266
- dev_dir = Path(".devcouncil")
267
- if dev_dir.exists() and not (with_gitnexus or with_graphify):
268
- console.print("[yellow]DevCouncil is already initialized in this directory.[/yellow]")
269
- console.print("Use --gitnexus or --graphify to add upgrade paths.")
270
- raise typer.Exit()
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
-
279
- initialize_project(
280
- Path("."),
281
- project_name=project_name,
282
- model_provider=model_provider,
283
- model=model,
284
- role_models=role_models,
285
- with_gitnexus=with_gitnexus,
286
- with_graphify=with_graphify,
287
- with_map=not skip_map,
288
- with_skills=not skip_skills,
289
- )