devcouncil 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +62 -543
  3. package/package.json +1 -1
  4. package/pyproject.toml +29 -26
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +135 -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 +143 -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/cli/commands/artifacts.py +51 -48
  22. package/src/devcouncil/cli/commands/ast.py +22 -0
  23. package/src/devcouncil/cli/commands/baseline.py +35 -32
  24. package/src/devcouncil/cli/commands/config.py +76 -54
  25. package/src/devcouncil/cli/commands/dashboard.py +26 -0
  26. package/src/devcouncil/cli/commands/doctor.py +86 -42
  27. package/src/devcouncil/cli/commands/go.py +237 -0
  28. package/src/devcouncil/cli/commands/hook.py +96 -29
  29. package/src/devcouncil/cli/commands/init.py +67 -56
  30. package/src/devcouncil/cli/commands/integrate.py +320 -14
  31. package/src/devcouncil/cli/commands/lsp.py +20 -0
  32. package/src/devcouncil/cli/commands/map.py +25 -21
  33. package/src/devcouncil/cli/commands/plan.py +257 -206
  34. package/src/devcouncil/cli/commands/prompt.py +36 -33
  35. package/src/devcouncil/cli/commands/repair.py +72 -69
  36. package/src/devcouncil/cli/commands/report.py +112 -54
  37. package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
  38. package/src/devcouncil/cli/commands/rollback.py +49 -47
  39. package/src/devcouncil/cli/commands/run.py +252 -207
  40. package/src/devcouncil/cli/commands/setup.py +159 -18
  41. package/src/devcouncil/cli/commands/show.py +76 -57
  42. package/src/devcouncil/cli/commands/status.py +117 -105
  43. package/src/devcouncil/cli/commands/tasks.py +55 -41
  44. package/src/devcouncil/cli/commands/trace.py +2 -1
  45. package/src/devcouncil/cli/commands/verify.py +158 -128
  46. package/src/devcouncil/cli/commands/version.py +20 -20
  47. package/src/devcouncil/cli/commands/watch.py +574 -0
  48. package/src/devcouncil/cli/main.py +42 -24
  49. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  50. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  51. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  52. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  53. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  54. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  55. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  56. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  57. package/src/devcouncil/domain/assumption.py +17 -17
  58. package/src/devcouncil/domain/critique.py +32 -32
  59. package/src/devcouncil/domain/evidence.py +27 -27
  60. package/src/devcouncil/domain/gap.py +26 -26
  61. package/src/devcouncil/domain/requirement.py +22 -22
  62. package/src/devcouncil/domain/task.py +26 -26
  63. package/src/devcouncil/execution/__init__.py +1 -1
  64. package/src/devcouncil/execution/context_builder.py +54 -54
  65. package/src/devcouncil/execution/executor.py +15 -15
  66. package/src/devcouncil/execution/hook_policy.py +24 -3
  67. package/src/devcouncil/execution/patch.py +28 -28
  68. package/src/devcouncil/execution/permissions.py +44 -44
  69. package/src/devcouncil/execution/prompt_builder.py +23 -23
  70. package/src/devcouncil/execution/task_runner.py +63 -63
  71. package/src/devcouncil/executors/__init__.py +1 -1
  72. package/src/devcouncil/executors/coding_cli.py +112 -0
  73. package/src/devcouncil/executors/mini_swe.py +63 -63
  74. package/src/devcouncil/executors/native/agent.py +81 -81
  75. package/src/devcouncil/executors/openhands.py +56 -56
  76. package/src/devcouncil/gating/__init__.py +1 -1
  77. package/src/devcouncil/gating/checks/clean_git.py +50 -45
  78. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  79. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  80. package/src/devcouncil/gating/checks/secret_scan_check.py +34 -34
  81. package/src/devcouncil/gating/policy.py +157 -157
  82. package/src/devcouncil/indexing/__init__.py +1 -1
  83. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  84. package/src/devcouncil/indexing/graph_index.py +48 -48
  85. package/src/devcouncil/indexing/lsp.py +120 -0
  86. package/src/devcouncil/indexing/repo_mapper.py +208 -204
  87. package/src/devcouncil/integrations/github.py +35 -35
  88. package/src/devcouncil/integrations/gitnexus.py +27 -27
  89. package/src/devcouncil/integrations/graphify.py +34 -34
  90. package/src/devcouncil/integrations/mcp/server.py +549 -96
  91. package/src/devcouncil/integrations/pr_comments.py +62 -0
  92. package/src/devcouncil/live/__init__.py +2 -0
  93. package/src/devcouncil/live/cards.py +207 -0
  94. package/src/devcouncil/live/models.py +63 -0
  95. package/src/devcouncil/live/repair_prompt.py +83 -0
  96. package/src/devcouncil/live/reviewer.py +70 -0
  97. package/src/devcouncil/live/signals.py +135 -0
  98. package/src/devcouncil/live/summary.py +34 -0
  99. package/src/devcouncil/live/tasks.py +18 -0
  100. package/src/devcouncil/live/transcripts.py +138 -0
  101. package/src/devcouncil/llm/__init__.py +1 -1
  102. package/src/devcouncil/llm/cache.py +38 -38
  103. package/src/devcouncil/llm/provider.py +146 -125
  104. package/src/devcouncil/llm/router.py +111 -111
  105. package/src/devcouncil/planning/__init__.py +1 -1
  106. package/src/devcouncil/planning/arbiter_service.py +57 -57
  107. package/src/devcouncil/planning/critique_service.py +66 -66
  108. package/src/devcouncil/planning/plan_service.py +46 -46
  109. package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
  110. package/src/devcouncil/planning/repair_service.py +39 -39
  111. package/src/devcouncil/planning/spec_service.py +44 -44
  112. package/src/devcouncil/reporting/github_check.py +32 -32
  113. package/src/devcouncil/reporting/json_report.py +20 -17
  114. package/src/devcouncil/reporting/markdown_report.py +68 -46
  115. package/src/devcouncil/reporting/report_builder.py +14 -14
  116. package/src/devcouncil/storage/db.py +66 -66
  117. package/src/devcouncil/storage/models.py +83 -83
  118. package/src/devcouncil/storage/repositories.py +299 -222
  119. package/src/devcouncil/telemetry/cost.py +34 -34
  120. package/src/devcouncil/telemetry/tracker.py +49 -49
  121. package/src/devcouncil/ui/__init__.py +1 -0
  122. package/src/devcouncil/ui/dashboard.py +122 -0
  123. package/src/devcouncil/utils/__init__.py +1 -1
  124. package/src/devcouncil/utils/redaction.py +141 -141
  125. package/src/devcouncil/verification/__init__.py +1 -1
  126. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  127. package/src/devcouncil/verification/verifier.py +319 -302
  128. package/uv.lock +1 -1
@@ -1,15 +1,135 @@
1
1
  import asyncio
2
+ import json
2
3
  import os
4
+ import subprocess
5
+ import sys
3
6
  from pathlib import Path
4
- from mcp.server import Server
5
- from mcp.server.stdio import stdio_server
6
- from mcp.types import Tool, TextContent
7
+ from mcp.server import Server
8
+ from mcp.server.stdio import stdio_server
9
+ from mcp.types import Tool, TextContent
7
10
  from devcouncil.storage.db import get_db
8
- from devcouncil.storage.repositories import TaskRepository, ArtifactGraphRepository, StateRepository
11
+ from devcouncil.storage.repositories import TaskRepository, ArtifactGraphRepository, StateRepository, RequirementRepository
9
12
  from devcouncil.reporting.report_builder import ReportBuilder
10
13
  from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
11
-
14
+ from devcouncil.execution.hook_policy import HookPolicy
15
+ from devcouncil.execution.prompt_builder import PromptBuilder
16
+ from devcouncil.telemetry.traces import read_trace_events
17
+ from devcouncil.indexing.ast_matcher import AstMatcher
18
+ from devcouncil.indexing.lsp import LspInspector
19
+ from devcouncil.app.project_status import compute_phase
20
+ from devcouncil.live.cards import filter_cards, get_card, load_cards
21
+ from devcouncil.live.repair_prompt import build_bulk_live_repair_prompt, build_live_repair_prompt
22
+ from devcouncil.live.summary import live_review_summary
23
+
12
24
  app = Server("devcouncil")
25
+ _DB_REQUIRED_TOOLS = {
26
+ "devcouncil_status",
27
+ "devcouncil_report",
28
+ "devcouncil_get_task",
29
+ "devcouncil_list_tasks",
30
+ "devcouncil_get_prompt",
31
+ "devcouncil_tail_trace",
32
+ "devcouncil_policy_check_write",
33
+ "devcouncil_graph_context",
34
+ "devcouncil_prepare_execution",
35
+ }
36
+ _CLI_ALLOWED_ROOTS = {"status", "tasks", "report", "map", "prompt", "show", "trace", "lsp", "ast", "verify"}
37
+ _CLI_FORBIDDEN_FLAGS = {"--project-root", "--github", "--github-pr-comment", "--gitlab-pr-comment"}
38
+ _CLI_TIMEOUT_SECONDS = 120
39
+ _CLI_OUTPUT_LIMIT = 20_000
40
+
41
+
42
+ def _forbidden_cli_flags(args: list[str]) -> list[str]:
43
+ forbidden: set[str] = set()
44
+ for arg in args:
45
+ for flag in _CLI_FORBIDDEN_FLAGS:
46
+ if arg == flag or arg.startswith(f"{flag}="):
47
+ forbidden.add(flag)
48
+ return sorted(forbidden)
49
+
50
+
51
+ def _truncate_text(value: str | bytes | None, limit: int = _CLI_OUTPUT_LIMIT) -> tuple[str, bool]:
52
+ if value is None:
53
+ return "", False
54
+ if isinstance(value, bytes):
55
+ value = value.decode("utf-8", errors="replace")
56
+ if len(value) <= limit:
57
+ return value, False
58
+ marker = f"\n...[truncated to {limit} characters]"
59
+ return value[:limit] + marker, True
60
+
61
+
62
+ def _json_text(payload: dict[str, object]) -> list[TextContent]:
63
+ return [TextContent(type="text", text=json.dumps(payload, indent=2))]
64
+
65
+
66
+ def _error_text(message: str, *, code: str = "error", **details: object) -> list[TextContent]:
67
+ return _json_text({"ok": False, "error": message, "code": code, **details})
68
+
69
+
70
+ def _normalize_arguments(arguments: object) -> dict:
71
+ return arguments if isinstance(arguments, dict) else {}
72
+
73
+
74
+ def _int_argument(arguments: dict, name: str, default: int, *, minimum: int, maximum: int) -> int:
75
+ value = arguments.get(name, default)
76
+ if not isinstance(value, int) or isinstance(value, bool):
77
+ value = default
78
+ return max(minimum, min(value, maximum))
79
+
80
+
81
+ def _optional_string_argument(arguments: dict, name: str) -> str | None:
82
+ value = arguments.get(name)
83
+ if value is None:
84
+ return None
85
+ return value if isinstance(value, str) else ""
86
+
87
+
88
+ def _required_string_argument(arguments: dict, name: str) -> tuple[str | None, list[TextContent] | None]:
89
+ value = arguments.get(name)
90
+ if value is None or value == "":
91
+ return None, _error_text(f"Missing {name}", code="missing_argument", argument=name)
92
+ if not isinstance(value, str):
93
+ return None, _error_text(f"{name} must be a string", code="invalid_arguments", argument=name)
94
+ return value, None
95
+
96
+
97
+ def _run_cli_command(args: list[str], root: Path) -> dict[str, object]:
98
+ command = [sys.executable, "-m", "devcouncil", *args, "--project-root", str(root)]
99
+ try:
100
+ result = subprocess.run(
101
+ command,
102
+ cwd=root,
103
+ capture_output=True,
104
+ text=True,
105
+ encoding="utf-8",
106
+ errors="replace",
107
+ timeout=_CLI_TIMEOUT_SECONDS,
108
+ )
109
+ stdout, stdout_truncated = _truncate_text(result.stdout)
110
+ stderr, stderr_truncated = _truncate_text(result.stderr)
111
+ return {
112
+ "ok": result.returncode == 0,
113
+ "returncode": result.returncode,
114
+ "stdout": stdout,
115
+ "stderr": stderr,
116
+ "stdout_truncated": stdout_truncated,
117
+ "stderr_truncated": stderr_truncated,
118
+ "timed_out": False,
119
+ }
120
+ except subprocess.TimeoutExpired as exc:
121
+ stdout, stdout_truncated = _truncate_text(exc.output)
122
+ stderr, stderr_truncated = _truncate_text(exc.stderr)
123
+ return {
124
+ "ok": False,
125
+ "returncode": None,
126
+ "stdout": stdout,
127
+ "stderr": stderr,
128
+ "stdout_truncated": stdout_truncated,
129
+ "stderr_truncated": stderr_truncated,
130
+ "timed_out": True,
131
+ "timeout_seconds": _CLI_TIMEOUT_SECONDS,
132
+ }
13
133
 
14
134
 
15
135
  def _project_root() -> Path:
@@ -17,60 +137,150 @@ def _project_root() -> Path:
17
137
  return Path(configured).expanduser().resolve() if configured else Path(".")
18
138
 
19
139
 
20
- def _computed_phase(graph) -> str:
21
- reqs = list(graph.requirements.values())
22
- tasks = list(graph.tasks.values())
23
- blocking_gaps = graph.blocking_gaps()
24
- if not reqs and not tasks:
25
- return "NEW"
26
- if reqs and not tasks:
27
- return "REQUIREMENTS_DRAFTED"
28
- if blocking_gaps:
29
- return "TASK_BLOCKED"
30
- if tasks:
31
- statuses = {task.status for task in tasks}
32
- if "running" in statuses:
33
- return "TASK_EXECUTING"
34
- if "blocked" in statuses:
35
- return "TASK_BLOCKED"
36
- if all(status in {"verified", "done"} for status in statuses):
37
- return "PROJECT_DONE"
38
- return "PLAN_APPROVED"
39
- return "NEW"
40
-
41
- @app.list_tools()
42
- async def list_tools() -> list[Tool]:
43
- return [
44
- Tool(
45
- name="devcouncil_status",
46
- description="Get the current status of the DevCouncil project, including phase, tasks, and gaps.",
47
- inputSchema={
48
- "type": "object",
49
- "properties": {}
50
- }
51
- ),
52
- Tool(
53
- name="devcouncil_report",
54
- description="Get the full coverage report and a list of all requirements and blocking gaps.",
55
- inputSchema={
56
- "type": "object",
57
- "properties": {}
58
- }
59
- ),
140
+ @app.list_tools()
141
+ async def list_tools() -> list[Tool]:
142
+ return [
143
+ Tool(
144
+ name="devcouncil_status",
145
+ description="Get the current status of the DevCouncil project, including phase, tasks, and gaps.",
146
+ inputSchema={
147
+ "type": "object",
148
+ "properties": {}
149
+ }
150
+ ),
151
+ Tool(
152
+ name="devcouncil_report",
153
+ description="Get the full coverage report and a list of all requirements and blocking gaps.",
154
+ inputSchema={
155
+ "type": "object",
156
+ "properties": {}
157
+ }
158
+ ),
60
159
  Tool(
61
160
  name="devcouncil_get_task",
62
- description="Get details, constraints, and requirements for a specific implementation task.",
63
- inputSchema={
64
- "type": "object",
65
- "properties": {
66
- "task_id": {
67
- "type": "string",
68
- "description": "The ID of the task, e.g. TASK-001"
69
- }
70
- },
71
- "required": ["task_id"]
161
+ description="Get details, constraints, and requirements for a specific implementation task.",
162
+ inputSchema={
163
+ "type": "object",
164
+ "properties": {
165
+ "task_id": {
166
+ "type": "string",
167
+ "description": "The ID of the task, e.g. TASK-001"
168
+ }
169
+ },
170
+ "required": ["task_id"]
72
171
  }
73
172
  ),
173
+ Tool(
174
+ name="devcouncil_live_review",
175
+ description="Get live coding-agent review status, pending signals, critique-card counts, and blockers.",
176
+ inputSchema={
177
+ "type": "object",
178
+ "properties": {
179
+ "task_id": {
180
+ "type": "string",
181
+ "description": "Optional task scope for live-review blocker calculation.",
182
+ }
183
+ },
184
+ },
185
+ ),
186
+ Tool(
187
+ name="devcouncil_live_cards",
188
+ description="List live-review critique cards with optional task, status, verdict, and client filters.",
189
+ inputSchema={
190
+ "type": "object",
191
+ "properties": {
192
+ "task_id": {
193
+ "type": "string",
194
+ "description": "Optional task scope for critique cards.",
195
+ },
196
+ "status": {
197
+ "type": "string",
198
+ "enum": ["open", "resolved", "ignored"],
199
+ "description": "Optional card status filter.",
200
+ },
201
+ "verdict": {
202
+ "type": "string",
203
+ "enum": ["approved", "concerns", "critical"],
204
+ "description": "Optional card verdict filter.",
205
+ },
206
+ "client": {
207
+ "type": "string",
208
+ "description": "Optional coding-agent client filter.",
209
+ },
210
+ "limit": {
211
+ "type": "integer",
212
+ "minimum": 1,
213
+ "maximum": 200,
214
+ "default": 20,
215
+ },
216
+ },
217
+ },
218
+ ),
219
+ Tool(
220
+ name="devcouncil_live_repair_prompt",
221
+ description="Generate a ready-to-paste repair prompt for a live-review critique card.",
222
+ inputSchema={
223
+ "type": "object",
224
+ "properties": {
225
+ "card_id": {
226
+ "type": "string",
227
+ "description": "The critique card ID, e.g. CARD-abc123.",
228
+ }
229
+ },
230
+ "required": ["card_id"],
231
+ },
232
+ ),
233
+ Tool(
234
+ name="devcouncil_live_repair_all",
235
+ description="Generate one repair prompt for all blocking live-review critique cards in scope.",
236
+ inputSchema={
237
+ "type": "object",
238
+ "properties": {
239
+ "task_id": {
240
+ "type": "string",
241
+ "description": "Optional task scope for blocking live-review cards.",
242
+ }
243
+ },
244
+ },
245
+ ),
246
+ Tool(
247
+ name="devcouncil_list_tasks",
248
+ description="List DevCouncil tasks with status and requirement mappings.",
249
+ inputSchema={"type": "object", "properties": {}},
250
+ ),
251
+ Tool(
252
+ name="devcouncil_get_prompt",
253
+ description="Get the raw implementation prompt for a DevCouncil task.",
254
+ inputSchema={
255
+ "type": "object",
256
+ "properties": {
257
+ "task_id": {"type": "string", "description": "The ID of the task, e.g. TASK-001"},
258
+ },
259
+ "required": ["task_id"],
260
+ },
261
+ ),
262
+ Tool(
263
+ name="devcouncil_tail_trace",
264
+ description="Return recent DevCouncil trace events as JSON.",
265
+ inputSchema={
266
+ "type": "object",
267
+ "properties": {
268
+ "limit": {"type": "integer", "minimum": 1, "maximum": 200, "default": 20},
269
+ },
270
+ },
271
+ ),
272
+ Tool(
273
+ name="devcouncil_policy_check_write",
274
+ description="Check whether a file write is allowed for a task or the active running task.",
275
+ inputSchema={
276
+ "type": "object",
277
+ "properties": {
278
+ "path": {"type": "string", "description": "Repository-relative or absolute path to check."},
279
+ "task_id": {"type": "string", "description": "Optional task ID. Defaults to the running task."},
280
+ },
281
+ "required": ["path"],
282
+ },
283
+ ),
74
284
  Tool(
75
285
  name="devcouncil_graph_context",
76
286
  description="Get optional code-review-graph structural context for changed or planned files.",
@@ -85,62 +295,305 @@ async def list_tools() -> list[Tool]:
85
295
  },
86
296
  },
87
297
  ),
298
+ Tool(
299
+ name="devcouncil_lsp_status",
300
+ description="Return detected language servers and starter LSP initialize payloads.",
301
+ inputSchema={"type": "object", "properties": {}},
302
+ ),
303
+ Tool(
304
+ name="devcouncil_ast_match",
305
+ description="Search code symbols structurally using optional tree-sitter support and deterministic fallbacks.",
306
+ inputSchema={
307
+ "type": "object",
308
+ "properties": {
309
+ "query": {"type": "string"},
310
+ "language": {"type": "string"},
311
+ "kind": {"type": "string"},
312
+ "limit": {"type": "integer", "minimum": 1, "maximum": 500, "default": 100},
313
+ },
314
+ },
315
+ ),
316
+ Tool(
317
+ name="devcouncil_cli",
318
+ description="Run a safe DevCouncil CLI command for status, tasks, report, map, prompt, show, trace, lsp, or ast.",
319
+ inputSchema={
320
+ "type": "object",
321
+ "properties": {
322
+ "args": {
323
+ "type": "array",
324
+ "items": {"type": "string"},
325
+ "description": "Arguments after the dev command, for example ['status','--json'].",
326
+ }
327
+ },
328
+ "required": ["args"],
329
+ },
330
+ ),
331
+ Tool(
332
+ name="devcouncil_prepare_execution",
333
+ description="Return a task prompt plus planned files and allowed commands for external execution tooling.",
334
+ inputSchema={
335
+ "type": "object",
336
+ "properties": {
337
+ "task_id": {"type": "string", "description": "The ID of the task, e.g. TASK-001"},
338
+ },
339
+ "required": ["task_id"],
340
+ },
341
+ ),
88
342
  ]
89
-
90
- @app.call_tool()
343
+
344
+ @app.call_tool()
91
345
  async def call_tool(name: str, arguments: dict) -> list[TextContent]:
92
- db = get_db(_project_root())
93
- if not db:
94
- return [TextContent(type="text", text="Error: DevCouncil not initialized in this directory.")]
95
-
96
- if name == "devcouncil_status":
97
- with db.get_session() as session:
346
+ arguments = _normalize_arguments(arguments)
347
+ root = _project_root()
348
+ db = get_db(root)
349
+ if name in _DB_REQUIRED_TOOLS and not db:
350
+ return _error_text("DevCouncil not initialized in this directory.", code="not_initialized")
351
+
352
+ if name == "devcouncil_status":
353
+ assert db is not None
354
+ with db.get_session() as session:
98
355
  graph_repo = ArtifactGraphRepository(session)
99
356
  graph = graph_repo.load_graph()
100
357
  summary = graph.coverage_summary()
101
358
  state = StateRepository(session).get_state()
102
- phase = state.current_phase if state else _computed_phase(graph)
359
+ phase = compute_phase(graph, state.current_phase if state else None)
103
360
 
104
361
  status_str = f"Phase: {phase}\n"
105
362
  status_str += f"Requirements: {summary['total_requirements']} ({summary['requirements_without_tasks']} unmapped)\n"
106
- status_str += f"Tasks: {summary['total_tasks']} ({summary['tasks_without_requirements']} orphaned)\n"
107
- status_str += f"Gaps: {summary['total_gaps']} ({summary['blocking_gaps']} blocking)\n"
108
-
109
- return [TextContent(type="text", text=status_str)]
110
-
111
- elif name == "devcouncil_report":
112
- with db.get_session() as session:
113
- graph_repo = ArtifactGraphRepository(session)
114
- graph = graph_repo.load_graph()
115
- markdown_report = ReportBuilder.build_markdown(graph)
116
- return [TextContent(type="text", text=markdown_report)]
117
-
363
+ status_str += f"Tasks: {summary['total_tasks']} ({summary['tasks_without_requirements']} orphaned)\n"
364
+ status_str += f"Gaps: {summary['total_gaps']} ({summary['blocking_gaps']} blocking)\n"
365
+
366
+ return [TextContent(type="text", text=status_str)]
367
+
368
+ elif name == "devcouncil_report":
369
+ assert db is not None
370
+ with db.get_session() as session:
371
+ graph_repo = ArtifactGraphRepository(session)
372
+ graph = graph_repo.load_graph()
373
+ markdown_report = ReportBuilder.build_markdown(graph, live_review=live_review_summary(root))
374
+ return [TextContent(type="text", text=markdown_report)]
375
+
376
+ elif name == "devcouncil_live_review":
377
+ task_id = _optional_string_argument(arguments, "task_id")
378
+ if task_id == "":
379
+ return _error_text("task_id must be a string", code="invalid_arguments", argument="task_id")
380
+ return [TextContent(
381
+ type="text",
382
+ text=json.dumps(live_review_summary(root, task_id=task_id), indent=2),
383
+ )]
384
+
385
+ elif name == "devcouncil_live_cards":
386
+ task_id = _optional_string_argument(arguments, "task_id")
387
+ status = _optional_string_argument(arguments, "status")
388
+ verdict = _optional_string_argument(arguments, "verdict")
389
+ client = _optional_string_argument(arguments, "client")
390
+ for arg_name, value in [
391
+ ("task_id", task_id),
392
+ ("status", status),
393
+ ("verdict", verdict),
394
+ ("client", client),
395
+ ]:
396
+ if value == "":
397
+ return _error_text(f"{arg_name} must be a string", code="invalid_arguments", argument=arg_name)
398
+
399
+ limit = _int_argument(arguments, "limit", 20, minimum=1, maximum=200)
400
+ filtered, error, argument = filter_cards(
401
+ load_cards(root),
402
+ task_id=task_id,
403
+ status=status,
404
+ verdict=verdict,
405
+ client=client,
406
+ )
407
+ if error:
408
+ return _error_text(error, code="invalid_arguments", argument=argument)
409
+
410
+ total = len(filtered)
411
+ return [TextContent(
412
+ type="text",
413
+ text=json.dumps({
414
+ "cards": [card.model_dump() for card in filtered[:limit]],
415
+ "filters": {
416
+ "task_id": task_id,
417
+ "status": status,
418
+ "verdict": verdict,
419
+ "client": client,
420
+ },
421
+ "limit": limit,
422
+ "total": total,
423
+ }, indent=2),
424
+ )]
425
+
426
+ elif name == "devcouncil_live_repair_prompt":
427
+ card_id, error = _required_string_argument(arguments, "card_id")
428
+ if error:
429
+ return error
430
+ card = get_card(root, card_id)
431
+ if not card:
432
+ return _error_text(f"Critique card {card_id} not found.", code="not_found", card_id=card_id)
433
+ return [TextContent(
434
+ type="text",
435
+ text=json.dumps({
436
+ "card": card.model_dump(),
437
+ "prompt": build_live_repair_prompt(root, card),
438
+ }, indent=2),
439
+ )]
440
+
441
+ elif name == "devcouncil_live_repair_all":
442
+ task_id = _optional_string_argument(arguments, "task_id")
443
+ if task_id == "":
444
+ return _error_text("task_id must be a string", code="invalid_arguments", argument="task_id")
445
+ summary = live_review_summary(root, task_id=task_id)
446
+ cards = [
447
+ get_card(root, item["id"])
448
+ for item in summary["blocking_cards"]
449
+ if isinstance(item.get("id"), str)
450
+ ]
451
+ cards = [card for card in cards if card is not None]
452
+ return [TextContent(
453
+ type="text",
454
+ text=json.dumps({
455
+ "scope_task_id": summary["scope_task_id"],
456
+ "cards": [card.model_dump() for card in cards],
457
+ "prompt": build_bulk_live_repair_prompt(root, cards),
458
+ }, indent=2),
459
+ )]
460
+
118
461
  elif name == "devcouncil_get_task":
119
- task_id = arguments.get("task_id")
120
- if not task_id:
121
- return [TextContent(type="text", text="Error: Missing task_id")]
122
-
123
- with db.get_session() as session:
124
- task_repo = TaskRepository(session)
125
- task = task_repo.get_by_id(task_id)
126
- if not task:
127
- return [TextContent(type="text", text=f"Error: Task {task_id} not found.")]
128
-
462
+ assert db is not None
463
+ task_id, error = _required_string_argument(arguments, "task_id")
464
+ if error:
465
+ return error
466
+
467
+ with db.get_session() as session:
468
+ task_repo = TaskRepository(session)
469
+ task = task_repo.get_by_id(task_id)
470
+ if not task:
471
+ return _error_text(f"Task {task_id} not found.", code="not_found", task_id=str(task_id))
472
+
129
473
  return [TextContent(type="text", text=task.model_dump_json(indent=2))]
130
474
 
475
+ elif name == "devcouncil_list_tasks":
476
+ assert db is not None
477
+ with db.get_session() as session:
478
+ task_repo = TaskRepository(session)
479
+ tasks = [task.model_dump() for task in task_repo.get_all()]
480
+
481
+ return [TextContent(type="text", text=json.dumps({"tasks": tasks}, indent=2))]
482
+
483
+ elif name == "devcouncil_get_prompt":
484
+ assert db is not None
485
+ task_id, error = _required_string_argument(arguments, "task_id")
486
+ if error:
487
+ return error
488
+
489
+ with db.get_session() as session:
490
+ task_repo = TaskRepository(session)
491
+ req_repo = RequirementRepository(session)
492
+ task = task_repo.get_by_id(task_id)
493
+ if not task:
494
+ return _error_text(f"Task {task_id} not found.", code="not_found", task_id=str(task_id))
495
+ prompt = PromptBuilder(root).build_task_prompt(task, req_repo.get_all())
496
+ return [TextContent(type="text", text=prompt)]
497
+
498
+ elif name == "devcouncil_tail_trace":
499
+ limit = _int_argument(arguments, "limit", 20, minimum=1, maximum=200)
500
+ events = list(read_trace_events(root))[-limit:]
501
+
502
+ return [TextContent(
503
+ type="text",
504
+ text=json.dumps({"events": [event.model_dump(by_alias=True) for event in events]}, indent=2),
505
+ )]
506
+
507
+ elif name == "devcouncil_policy_check_write":
508
+ assert db is not None
509
+ path, error = _required_string_argument(arguments, "path")
510
+ if error:
511
+ return error
512
+ task_id = _optional_string_argument(arguments, "task_id")
513
+ if task_id == "":
514
+ return _error_text("task_id must be a string", code="invalid_arguments", argument="task_id")
515
+ with db.get_session() as session:
516
+ task_repo = TaskRepository(session)
517
+ if task_id:
518
+ task = task_repo.get_by_id(task_id)
519
+ else:
520
+ running = [task for task in task_repo.get_all() if task.status == "running"]
521
+ task = running[0] if running else None
522
+ decision = HookPolicy(project_root=root).evaluate_file_write(path, task)
523
+ return [TextContent(type="text", text=json.dumps({
524
+ "action": decision.action,
525
+ "allowed": decision.allowed,
526
+ "reason": decision.reason,
527
+ "target": decision.target,
528
+ "task_id": task.id if task else None,
529
+ }, indent=2))]
530
+
131
531
  elif name == "devcouncil_graph_context":
132
532
  files = arguments.get("files", [])
133
533
  if not isinstance(files, list):
134
534
  files = []
135
- context = CodeReviewGraphAdapter(_project_root()).get_context([str(file) for file in files])
535
+ context = CodeReviewGraphAdapter(root).get_context([file for file in files if isinstance(file, str)])
136
536
  return [TextContent(type="text", text=context.model_dump_json(indent=2))]
137
537
 
138
- return [TextContent(type="text", text=f"Unknown tool: {name}")]
139
-
140
- async def run():
141
- # Use stdio to communicate
142
- async with stdio_server() as (read_stream, write_stream):
143
- await app.run(read_stream, write_stream, app.create_initialization_options())
144
-
145
- if __name__ == "__main__":
146
- asyncio.run(run())
538
+ elif name == "devcouncil_lsp_status":
539
+ return [TextContent(type="text", text=LspInspector(root).summary_json())]
540
+
541
+ elif name == "devcouncil_ast_match":
542
+ query = _optional_string_argument(arguments, "query")
543
+ language = _optional_string_argument(arguments, "language")
544
+ kind = _optional_string_argument(arguments, "kind")
545
+ for arg_name, value in [("query", query), ("language", language), ("kind", kind)]:
546
+ if value == "":
547
+ return _error_text(f"{arg_name} must be a string", code="invalid_arguments", argument=arg_name)
548
+ limit = _int_argument(arguments, "limit", 100, minimum=1, maximum=500)
549
+ matches = AstMatcher(root).match(
550
+ query=query or "",
551
+ language=language,
552
+ kind=kind,
553
+ limit=limit,
554
+ )
555
+ return [TextContent(type="text", text=json.dumps({"matches": [item.model_dump() for item in matches]}, indent=2))]
556
+
557
+ elif name == "devcouncil_cli":
558
+ args = arguments.get("args")
559
+ if not isinstance(args, list) or not all(isinstance(arg, str) for arg in args) or not args:
560
+ return _error_text("args must be a non-empty string array", code="invalid_arguments")
561
+ if args[0] not in _CLI_ALLOWED_ROOTS:
562
+ return _error_text(f"command {args[0]} is not allowed through MCP", code="command_not_allowed", command=args[0])
563
+ forbidden = _forbidden_cli_flags(args)
564
+ if forbidden:
565
+ return _error_text("forbidden flag(s) through MCP: " + ", ".join(forbidden), code="forbidden_flags", flags=forbidden)
566
+ try:
567
+ return _json_text(_run_cli_command(args, root))
568
+ except Exception as exc:
569
+ return _error_text(str(exc), code="cli_execution_error")
570
+
571
+ elif name == "devcouncil_prepare_execution":
572
+ assert db is not None
573
+ task_id, error = _required_string_argument(arguments, "task_id")
574
+ if error:
575
+ return error
576
+ with db.get_session() as session:
577
+ task_repo = TaskRepository(session)
578
+ req_repo = RequirementRepository(session)
579
+ task = task_repo.get_by_id(task_id)
580
+ if not task:
581
+ return _error_text(f"Task {task_id} not found.", code="not_found", task_id=str(task_id))
582
+ prompt = PromptBuilder(root).build_task_prompt(task, req_repo.get_all())
583
+ return [TextContent(type="text", text=json.dumps({
584
+ "task_id": task.id,
585
+ "prompt": prompt,
586
+ "planned_files": [file.model_dump() for file in task.planned_files],
587
+ "allowed_commands": task.allowed_commands,
588
+ "expected_tests": task.expected_tests,
589
+ }, indent=2))]
590
+
591
+ return _error_text(f"Unknown tool: {name}", code="unknown_tool", tool=name)
592
+
593
+ async def run():
594
+ # Use stdio to communicate
595
+ async with stdio_server() as (read_stream, write_stream):
596
+ await app.run(read_stream, write_stream, app.create_initialization_options())
597
+
598
+ if __name__ == "__main__":
599
+ asyncio.run(run())