devcouncil 0.1.1 → 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 (146) hide show
  1. package/README.md +218 -96
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +7 -11
  4. package/pyproject.toml +0 -34
  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 -158
  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 -143
  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/cli/__init__.py +0 -0
  23. package/src/devcouncil/cli/commands/__init__.py +0 -0
  24. package/src/devcouncil/cli/commands/artifacts.py +0 -51
  25. package/src/devcouncil/cli/commands/ast.py +0 -22
  26. package/src/devcouncil/cli/commands/baseline.py +0 -35
  27. package/src/devcouncil/cli/commands/config.py +0 -76
  28. package/src/devcouncil/cli/commands/dashboard.py +0 -26
  29. package/src/devcouncil/cli/commands/doctor.py +0 -140
  30. package/src/devcouncil/cli/commands/go.py +0 -237
  31. package/src/devcouncil/cli/commands/hook.py +0 -128
  32. package/src/devcouncil/cli/commands/init.py +0 -153
  33. package/src/devcouncil/cli/commands/integrate.py +0 -726
  34. package/src/devcouncil/cli/commands/lsp.py +0 -20
  35. package/src/devcouncil/cli/commands/map.py +0 -42
  36. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  37. package/src/devcouncil/cli/commands/plan.py +0 -327
  38. package/src/devcouncil/cli/commands/prompt.py +0 -50
  39. package/src/devcouncil/cli/commands/repair.py +0 -72
  40. package/src/devcouncil/cli/commands/report.py +0 -129
  41. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -31
  42. package/src/devcouncil/cli/commands/rollback.py +0 -60
  43. package/src/devcouncil/cli/commands/run.py +0 -269
  44. package/src/devcouncil/cli/commands/setup.py +0 -223
  45. package/src/devcouncil/cli/commands/show.py +0 -76
  46. package/src/devcouncil/cli/commands/status.py +0 -117
  47. package/src/devcouncil/cli/commands/tasks.py +0 -55
  48. package/src/devcouncil/cli/commands/trace.py +0 -44
  49. package/src/devcouncil/cli/commands/verify.py +0 -193
  50. package/src/devcouncil/cli/commands/version.py +0 -20
  51. package/src/devcouncil/cli/commands/watch.py +0 -574
  52. package/src/devcouncil/cli/main.py +0 -88
  53. package/src/devcouncil/council/__init__.py +0 -0
  54. package/src/devcouncil/council/prompts/__init__.py +0 -0
  55. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  56. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  57. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  58. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  59. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  60. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  61. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  62. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  63. package/src/devcouncil/domain/__init__.py +0 -0
  64. package/src/devcouncil/domain/assumption.py +0 -17
  65. package/src/devcouncil/domain/critique.py +0 -32
  66. package/src/devcouncil/domain/evidence.py +0 -27
  67. package/src/devcouncil/domain/gap.py +0 -26
  68. package/src/devcouncil/domain/requirement.py +0 -22
  69. package/src/devcouncil/domain/task.py +0 -26
  70. package/src/devcouncil/execution/__init__.py +0 -1
  71. package/src/devcouncil/execution/context_builder.py +0 -60
  72. package/src/devcouncil/execution/executor.py +0 -15
  73. package/src/devcouncil/execution/hook_policy.py +0 -165
  74. package/src/devcouncil/execution/patch.py +0 -28
  75. package/src/devcouncil/execution/paths.py +0 -14
  76. package/src/devcouncil/execution/permissions.py +0 -92
  77. package/src/devcouncil/execution/prompt_builder.py +0 -59
  78. package/src/devcouncil/execution/task_runner.py +0 -166
  79. package/src/devcouncil/executors/__init__.py +0 -1
  80. package/src/devcouncil/executors/coding_cli.py +0 -112
  81. package/src/devcouncil/executors/mini_swe.py +0 -73
  82. package/src/devcouncil/executors/native/__init__.py +0 -0
  83. package/src/devcouncil/executors/native/agent.py +0 -107
  84. package/src/devcouncil/executors/openhands.py +0 -71
  85. package/src/devcouncil/gating/__init__.py +0 -1
  86. package/src/devcouncil/gating/checks/__init__.py +0 -0
  87. package/src/devcouncil/gating/checks/clean_git.py +0 -50
  88. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  89. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  90. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -34
  91. package/src/devcouncil/gating/policy.py +0 -190
  92. package/src/devcouncil/indexing/__init__.py +0 -1
  93. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  94. package/src/devcouncil/indexing/graph_index.py +0 -48
  95. package/src/devcouncil/indexing/lsp.py +0 -120
  96. package/src/devcouncil/indexing/repo_mapper.py +0 -208
  97. package/src/devcouncil/indexing/symbol_index.py +0 -0
  98. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  99. package/src/devcouncil/integrations/github.py +0 -39
  100. package/src/devcouncil/integrations/gitnexus.py +0 -27
  101. package/src/devcouncil/integrations/graphify.py +0 -34
  102. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  103. package/src/devcouncil/integrations/mcp/server.py +0 -599
  104. package/src/devcouncil/integrations/pr_comments.py +0 -62
  105. package/src/devcouncil/live/__init__.py +0 -2
  106. package/src/devcouncil/live/cards.py +0 -207
  107. package/src/devcouncil/live/models.py +0 -63
  108. package/src/devcouncil/live/repair_prompt.py +0 -83
  109. package/src/devcouncil/live/reviewer.py +0 -70
  110. package/src/devcouncil/live/signals.py +0 -135
  111. package/src/devcouncil/live/summary.py +0 -34
  112. package/src/devcouncil/live/tasks.py +0 -18
  113. package/src/devcouncil/live/transcripts.py +0 -138
  114. package/src/devcouncil/llm/__init__.py +0 -1
  115. package/src/devcouncil/llm/cache.py +0 -38
  116. package/src/devcouncil/llm/provider.py +0 -146
  117. package/src/devcouncil/llm/router.py +0 -125
  118. package/src/devcouncil/planning/__init__.py +0 -1
  119. package/src/devcouncil/planning/arbiter_service.py +0 -57
  120. package/src/devcouncil/planning/critique_service.py +0 -66
  121. package/src/devcouncil/planning/plan_service.py +0 -46
  122. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -86
  123. package/src/devcouncil/planning/repair_service.py +0 -39
  124. package/src/devcouncil/planning/spec_service.py +0 -44
  125. package/src/devcouncil/repo/__init__.py +0 -0
  126. package/src/devcouncil/reporting/__init__.py +0 -0
  127. package/src/devcouncil/reporting/github_check.py +0 -32
  128. package/src/devcouncil/reporting/json_report.py +0 -20
  129. package/src/devcouncil/reporting/markdown_report.py +0 -68
  130. package/src/devcouncil/reporting/report_builder.py +0 -14
  131. package/src/devcouncil/storage/__init__.py +0 -0
  132. package/src/devcouncil/storage/db.py +0 -66
  133. package/src/devcouncil/storage/models.py +0 -83
  134. package/src/devcouncil/storage/repositories.py +0 -423
  135. package/src/devcouncil/telemetry/__init__.py +0 -0
  136. package/src/devcouncil/telemetry/cost.py +0 -34
  137. package/src/devcouncil/telemetry/traces.py +0 -91
  138. package/src/devcouncil/telemetry/tracker.py +0 -49
  139. package/src/devcouncil/ui/__init__.py +0 -1
  140. package/src/devcouncil/ui/dashboard.py +0 -122
  141. package/src/devcouncil/utils/__init__.py +0 -1
  142. package/src/devcouncil/utils/redaction.py +0 -141
  143. package/src/devcouncil/verification/__init__.py +0 -1
  144. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  145. package/src/devcouncil/verification/verifier.py +0 -530
  146. package/uv.lock +0 -1085
@@ -1,599 +0,0 @@
1
- import asyncio
2
- import json
3
- import os
4
- import subprocess
5
- import sys
6
- from pathlib import Path
7
- from mcp.server import Server
8
- from mcp.server.stdio import stdio_server
9
- from mcp.types import Tool, TextContent
10
- from devcouncil.storage.db import get_db
11
- from devcouncil.storage.repositories import TaskRepository, ArtifactGraphRepository, StateRepository, RequirementRepository
12
- from devcouncil.reporting.report_builder import ReportBuilder
13
- from devcouncil.integrations.code_review_graph import CodeReviewGraphAdapter
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
-
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
- }
133
-
134
-
135
- def _project_root() -> Path:
136
- configured = os.environ.get("DEVCOUNCIL_PROJECT_ROOT")
137
- return Path(configured).expanduser().resolve() if configured else Path(".")
138
-
139
-
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
- ),
159
- Tool(
160
- name="devcouncil_get_task",
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"]
171
- }
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
- ),
284
- Tool(
285
- name="devcouncil_graph_context",
286
- description="Get optional code-review-graph structural context for changed or planned files.",
287
- inputSchema={
288
- "type": "object",
289
- "properties": {
290
- "files": {
291
- "type": "array",
292
- "items": {"type": "string"},
293
- "description": "Repository-relative files to contextualize.",
294
- }
295
- },
296
- },
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
- ),
342
- ]
343
-
344
- @app.call_tool()
345
- async def call_tool(name: str, arguments: dict) -> list[TextContent]:
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:
355
- graph_repo = ArtifactGraphRepository(session)
356
- graph = graph_repo.load_graph()
357
- summary = graph.coverage_summary()
358
- state = StateRepository(session).get_state()
359
- phase = compute_phase(graph, state.current_phase if state else None)
360
-
361
- status_str = f"Phase: {phase}\n"
362
- status_str += f"Requirements: {summary['total_requirements']} ({summary['requirements_without_tasks']} unmapped)\n"
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
-
461
- elif name == "devcouncil_get_task":
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
-
473
- return [TextContent(type="text", text=task.model_dump_json(indent=2))]
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
-
531
- elif name == "devcouncil_graph_context":
532
- files = arguments.get("files", [])
533
- if not isinstance(files, list):
534
- files = []
535
- context = CodeReviewGraphAdapter(root).get_context([file for file in files if isinstance(file, str)])
536
- return [TextContent(type="text", text=context.model_dump_json(indent=2))]
537
-
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())
@@ -1,62 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import httpx
4
- from urllib.parse import quote
5
-
6
- from devcouncil.artifacts.graph import ArtifactGraph
7
- from devcouncil.reporting.report_builder import ReportBuilder
8
-
9
-
10
- class PullRequestCommentError(RuntimeError):
11
- pass
12
-
13
-
14
- def build_pr_comment_body(graph: ArtifactGraph, live_review: dict | None = None) -> str:
15
- report = ReportBuilder.build_markdown(graph, live_review=live_review)
16
- return "\n".join([
17
- "## DevCouncil Verification",
18
- "",
19
- report,
20
- ])
21
-
22
-
23
- class GitHubPRCommenter:
24
- def __init__(self, token: str, repository: str, pull_number: int, *, base_url: str = "https://api.github.com"):
25
- self.token = token
26
- self.repository = repository
27
- self.pull_number = pull_number
28
- self.base_url = base_url.rstrip("/")
29
-
30
- async def post_comment(self, body: str) -> dict:
31
- url = f"{self.base_url}/repos/{self.repository}/issues/{self.pull_number}/comments"
32
- headers = {
33
- "Authorization": f"Bearer {self.token}",
34
- "Accept": "application/vnd.github+json",
35
- "Content-Type": "application/json",
36
- }
37
- async with httpx.AsyncClient() as client:
38
- response = await client.post(url, headers=headers, json={"body": body})
39
- if response.status_code >= 400:
40
- raise PullRequestCommentError(f"GitHub comment failed with HTTP {response.status_code}: {response.text}")
41
- return response.json() if response.content else {}
42
-
43
-
44
- class GitLabMRCommenter:
45
- def __init__(self, token: str, project_id: str, merge_request_iid: int, *, base_url: str = "https://gitlab.com/api/v4"):
46
- self.token = token
47
- self.project_id = project_id
48
- self.merge_request_iid = merge_request_iid
49
- self.base_url = base_url.rstrip("/")
50
-
51
- async def post_comment(self, body: str) -> dict:
52
- project = quote(self.project_id, safe="")
53
- url = f"{self.base_url}/projects/{project}/merge_requests/{self.merge_request_iid}/notes"
54
- headers = {
55
- "PRIVATE-TOKEN": self.token,
56
- "Content-Type": "application/json",
57
- }
58
- async with httpx.AsyncClient() as client:
59
- response = await client.post(url, headers=headers, json={"body": body})
60
- if response.status_code >= 400:
61
- raise PullRequestCommentError(f"GitLab comment failed with HTTP {response.status_code}: {response.text}")
62
- return response.json() if response.content else {}
@@ -1,2 +0,0 @@
1
- """Live coding-agent review primitives."""
2
-