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.
- package/README.md +218 -96
- package/bin/devcouncil.js +130 -32
- package/package.json +7 -11
- package/pyproject.toml +0 -34
- package/src/devcouncil/__init__.py +0 -0
- package/src/devcouncil/__main__.py +0 -4
- package/src/devcouncil/app/__init__.py +0 -28
- package/src/devcouncil/app/config.py +0 -158
- package/src/devcouncil/app/errors.py +0 -23
- package/src/devcouncil/app/events.py +0 -44
- package/src/devcouncil/app/orchestrator.py +0 -92
- package/src/devcouncil/app/project_status.py +0 -29
- package/src/devcouncil/app/run_context.py +0 -39
- package/src/devcouncil/app/state_machine.py +0 -108
- package/src/devcouncil/artifacts/__init__.py +0 -1
- package/src/devcouncil/artifacts/coverage.py +0 -96
- package/src/devcouncil/artifacts/graph.py +0 -143
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/artifacts/validators.py +0 -27
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/artifacts.py +0 -51
- package/src/devcouncil/cli/commands/ast.py +0 -22
- package/src/devcouncil/cli/commands/baseline.py +0 -35
- package/src/devcouncil/cli/commands/config.py +0 -76
- package/src/devcouncil/cli/commands/dashboard.py +0 -26
- package/src/devcouncil/cli/commands/doctor.py +0 -140
- package/src/devcouncil/cli/commands/go.py +0 -237
- package/src/devcouncil/cli/commands/hook.py +0 -128
- package/src/devcouncil/cli/commands/init.py +0 -153
- package/src/devcouncil/cli/commands/integrate.py +0 -726
- package/src/devcouncil/cli/commands/lsp.py +0 -20
- package/src/devcouncil/cli/commands/map.py +0 -42
- package/src/devcouncil/cli/commands/mcp_server.py +0 -18
- package/src/devcouncil/cli/commands/plan.py +0 -327
- package/src/devcouncil/cli/commands/prompt.py +0 -50
- package/src/devcouncil/cli/commands/repair.py +0 -72
- package/src/devcouncil/cli/commands/report.py +0 -129
- package/src/devcouncil/cli/commands/reset_demo_state.py +0 -31
- package/src/devcouncil/cli/commands/rollback.py +0 -60
- package/src/devcouncil/cli/commands/run.py +0 -269
- package/src/devcouncil/cli/commands/setup.py +0 -223
- package/src/devcouncil/cli/commands/show.py +0 -76
- package/src/devcouncil/cli/commands/status.py +0 -117
- package/src/devcouncil/cli/commands/tasks.py +0 -55
- package/src/devcouncil/cli/commands/trace.py +0 -44
- package/src/devcouncil/cli/commands/verify.py +0 -193
- package/src/devcouncil/cli/commands/version.py +0 -20
- package/src/devcouncil/cli/commands/watch.py +0 -574
- package/src/devcouncil/cli/main.py +0 -88
- package/src/devcouncil/council/__init__.py +0 -0
- package/src/devcouncil/council/prompts/__init__.py +0 -0
- package/src/devcouncil/council/prompts/arbiter.md +0 -19
- package/src/devcouncil/council/prompts/critic_a.md +0 -10
- package/src/devcouncil/council/prompts/critic_b.md +0 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
- package/src/devcouncil/council/prompts/planner_a.md +0 -16
- package/src/devcouncil/council/prompts/planner_b.md +0 -16
- package/src/devcouncil/council/prompts/rebuttal.md +0 -10
- package/src/devcouncil/council/prompts/spec_writer.md +0 -12
- package/src/devcouncil/domain/__init__.py +0 -0
- package/src/devcouncil/domain/assumption.py +0 -17
- package/src/devcouncil/domain/critique.py +0 -32
- package/src/devcouncil/domain/evidence.py +0 -27
- package/src/devcouncil/domain/gap.py +0 -26
- package/src/devcouncil/domain/requirement.py +0 -22
- package/src/devcouncil/domain/task.py +0 -26
- package/src/devcouncil/execution/__init__.py +0 -1
- package/src/devcouncil/execution/context_builder.py +0 -60
- package/src/devcouncil/execution/executor.py +0 -15
- package/src/devcouncil/execution/hook_policy.py +0 -165
- package/src/devcouncil/execution/patch.py +0 -28
- package/src/devcouncil/execution/paths.py +0 -14
- package/src/devcouncil/execution/permissions.py +0 -92
- package/src/devcouncil/execution/prompt_builder.py +0 -59
- package/src/devcouncil/execution/task_runner.py +0 -166
- package/src/devcouncil/executors/__init__.py +0 -1
- package/src/devcouncil/executors/coding_cli.py +0 -112
- package/src/devcouncil/executors/mini_swe.py +0 -73
- package/src/devcouncil/executors/native/__init__.py +0 -0
- package/src/devcouncil/executors/native/agent.py +0 -107
- package/src/devcouncil/executors/openhands.py +0 -71
- package/src/devcouncil/gating/__init__.py +0 -1
- package/src/devcouncil/gating/checks/__init__.py +0 -0
- package/src/devcouncil/gating/checks/clean_git.py +0 -50
- package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +0 -34
- package/src/devcouncil/gating/policy.py +0 -190
- package/src/devcouncil/indexing/__init__.py +0 -1
- package/src/devcouncil/indexing/ast_matcher.py +0 -168
- package/src/devcouncil/indexing/graph_index.py +0 -48
- package/src/devcouncil/indexing/lsp.py +0 -120
- package/src/devcouncil/indexing/repo_mapper.py +0 -208
- package/src/devcouncil/indexing/symbol_index.py +0 -0
- package/src/devcouncil/integrations/code_review_graph.py +0 -163
- package/src/devcouncil/integrations/github.py +0 -39
- package/src/devcouncil/integrations/gitnexus.py +0 -27
- package/src/devcouncil/integrations/graphify.py +0 -34
- package/src/devcouncil/integrations/mcp/__init__.py +0 -0
- package/src/devcouncil/integrations/mcp/server.py +0 -599
- package/src/devcouncil/integrations/pr_comments.py +0 -62
- package/src/devcouncil/live/__init__.py +0 -2
- package/src/devcouncil/live/cards.py +0 -207
- package/src/devcouncil/live/models.py +0 -63
- package/src/devcouncil/live/repair_prompt.py +0 -83
- package/src/devcouncil/live/reviewer.py +0 -70
- package/src/devcouncil/live/signals.py +0 -135
- package/src/devcouncil/live/summary.py +0 -34
- package/src/devcouncil/live/tasks.py +0 -18
- package/src/devcouncil/live/transcripts.py +0 -138
- package/src/devcouncil/llm/__init__.py +0 -1
- package/src/devcouncil/llm/cache.py +0 -38
- package/src/devcouncil/llm/provider.py +0 -146
- package/src/devcouncil/llm/router.py +0 -125
- package/src/devcouncil/planning/__init__.py +0 -1
- package/src/devcouncil/planning/arbiter_service.py +0 -57
- package/src/devcouncil/planning/critique_service.py +0 -66
- package/src/devcouncil/planning/plan_service.py +0 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +0 -86
- package/src/devcouncil/planning/repair_service.py +0 -39
- package/src/devcouncil/planning/spec_service.py +0 -44
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/reporting/__init__.py +0 -0
- package/src/devcouncil/reporting/github_check.py +0 -32
- package/src/devcouncil/reporting/json_report.py +0 -20
- package/src/devcouncil/reporting/markdown_report.py +0 -68
- package/src/devcouncil/reporting/report_builder.py +0 -14
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +0 -66
- package/src/devcouncil/storage/models.py +0 -83
- package/src/devcouncil/storage/repositories.py +0 -423
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +0 -34
- package/src/devcouncil/telemetry/traces.py +0 -91
- package/src/devcouncil/telemetry/tracker.py +0 -49
- package/src/devcouncil/ui/__init__.py +0 -1
- package/src/devcouncil/ui/dashboard.py +0 -122
- package/src/devcouncil/utils/__init__.py +0 -1
- package/src/devcouncil/utils/redaction.py +0 -141
- package/src/devcouncil/verification/__init__.py +0 -1
- package/src/devcouncil/verification/implementation_reviewer.py +0 -55
- package/src/devcouncil/verification/verifier.py +0 -530
- 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 {}
|