devcouncil 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/devcouncil/app/config.py +181 -7
- package/src/devcouncil/app/orchestrator.py +10 -6
- package/src/devcouncil/app/state_machine.py +4 -0
- package/src/devcouncil/artifacts/graph.py +9 -2
- package/src/devcouncil/cli/commands/check.py +12 -1
- package/src/devcouncil/cli/commands/design.py +186 -0
- package/src/devcouncil/cli/commands/doctor.py +160 -3
- package/src/devcouncil/cli/commands/go.py +96 -16
- package/src/devcouncil/cli/commands/hook.py +172 -0
- package/src/devcouncil/cli/commands/init.py +7 -2
- package/src/devcouncil/cli/commands/integrate.py +492 -34
- package/src/devcouncil/cli/commands/logs.py +106 -0
- package/src/devcouncil/cli/commands/okf.py +245 -0
- package/src/devcouncil/cli/commands/plan.py +54 -14
- package/src/devcouncil/cli/commands/repair.py +12 -3
- package/src/devcouncil/cli/commands/run.py +128 -7
- package/src/devcouncil/cli/commands/skills.py +180 -1
- package/src/devcouncil/cli/commands/status.py +7 -16
- package/src/devcouncil/cli/commands/verify.py +16 -10
- package/src/devcouncil/cli/commands/watch.py +24 -4
- package/src/devcouncil/cli/main.py +36 -1
- package/src/devcouncil/domain/evidence.py +7 -0
- package/src/devcouncil/execution/checkpoints.py +12 -2
- package/src/devcouncil/execution/fs_watcher.py +27 -2
- package/src/devcouncil/execution/handoff.py +1 -1
- package/src/devcouncil/execution/patch.py +6 -0
- package/src/devcouncil/execution/permissions.py +7 -0
- package/src/devcouncil/execution/policy_engine.py +12 -5
- package/src/devcouncil/execution/prompt_builder.py +126 -10
- package/src/devcouncil/execution/shell_session.py +6 -0
- package/src/devcouncil/execution/task_runner.py +18 -7
- package/src/devcouncil/executors/agent_registry.py +22 -1
- package/src/devcouncil/executors/coding_cli.py +133 -5
- package/src/devcouncil/executors/mini_swe.py +6 -0
- package/src/devcouncil/executors/native/agent.py +15 -0
- package/src/devcouncil/executors/openhands.py +6 -0
- package/src/devcouncil/gating/checks/secret_scan_check.py +7 -0
- package/src/devcouncil/gating/policy.py +38 -7
- package/src/devcouncil/indexing/ast_matcher.py +16 -6
- package/src/devcouncil/indexing/repo_mapper.py +30 -8
- package/src/devcouncil/indexing/semantic_index.py +42 -26
- package/src/devcouncil/integrations/actions.py +24 -4
- package/src/devcouncil/integrations/check.py +7 -4
- package/src/devcouncil/integrations/claude_assets.py +444 -0
- package/src/devcouncil/integrations/code_review_graph.py +13 -2
- package/src/devcouncil/integrations/github_intent.py +8 -1
- package/src/devcouncil/integrations/gitnexus.py +10 -2
- package/src/devcouncil/integrations/mcp/server.py +404 -15
- package/src/devcouncil/integrations/pr_comments.py +9 -0
- package/src/devcouncil/knowledge/__init__.py +23 -0
- package/src/devcouncil/knowledge/design.py +374 -0
- package/src/devcouncil/knowledge/design_conformance.py +317 -0
- package/src/devcouncil/knowledge/fetch.py +223 -0
- package/src/devcouncil/knowledge/frontmatter.py +51 -0
- package/src/devcouncil/knowledge/okf.py +202 -0
- package/src/devcouncil/knowledge/skill_bridge.py +96 -0
- package/src/devcouncil/knowledge/sources.py +239 -0
- package/src/devcouncil/live/cards.py +20 -6
- package/src/devcouncil/live/repair_prompt.py +29 -6
- package/src/devcouncil/live/reviewer.py +72 -13
- package/src/devcouncil/live/summary.py +18 -8
- package/src/devcouncil/live/transcripts.py +38 -5
- package/src/devcouncil/llm/cache.py +14 -6
- package/src/devcouncil/llm/provider.py +179 -92
- package/src/devcouncil/llm/router.py +122 -23
- package/src/devcouncil/optimization/skillopt.py +673 -0
- package/src/devcouncil/planning/arbiter_service.py +10 -2
- package/src/devcouncil/planning/correction_manifest.py +47 -4
- package/src/devcouncil/planning/critique_service.py +9 -2
- package/src/devcouncil/planning/plan_service.py +69 -3
- package/src/devcouncil/planning/prompt_enhancer_service.py +124 -0
- package/src/devcouncil/planning/repair_service.py +8 -2
- package/src/devcouncil/planning/spec_service.py +10 -2
- package/src/devcouncil/repo/ci_scaffold.py +13 -5
- package/src/devcouncil/repo/sca.py +11 -1
- package/src/devcouncil/reporting/json_report.py +11 -0
- package/src/devcouncil/reporting/markdown_report.py +14 -1
- package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
- package/src/devcouncil/reporting/okf_html.py +323 -0
- package/src/devcouncil/reporting/report_builder.py +18 -1
- package/src/devcouncil/skills/registry.py +111 -33
- package/src/devcouncil/storage/db.py +58 -2
- package/src/devcouncil/storage/models.py +4 -0
- package/src/devcouncil/storage/native.py +20 -18
- package/src/devcouncil/storage/repositories.py +35 -18
- package/src/devcouncil/telemetry/logging_setup.py +244 -0
- package/src/devcouncil/telemetry/stages.py +141 -0
- package/src/devcouncil/telemetry/tracker.py +12 -1
- package/src/devcouncil/ui/dashboard.py +69 -5
- package/src/devcouncil/verification/acceptance_compiler.py +147 -19
- package/src/devcouncil/verification/ad_hoc_check.py +6 -0
- package/src/devcouncil/verification/implementation_reviewer.py +11 -2
- package/src/devcouncil/verification/sandbox.py +7 -4
- package/src/devcouncil/verification/verifier.py +905 -517
- package/uv.lock +1 -1
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
"""Generate the full Claude Code asset surface for a DevCouncil project.
|
|
2
|
+
|
|
3
|
+
DevCouncil already installs a Claude Code MCP server and three hooks. This module adds
|
|
4
|
+
the *static* extensibility surfaces Claude Code reads from a repository so a project gets
|
|
5
|
+
complete, installable Claude Code support:
|
|
6
|
+
|
|
7
|
+
* **Slash commands** — ``.claude/commands/devcouncil/*.md`` (``/devcouncil:status`` ...).
|
|
8
|
+
* **Subagents** — ``.claude/agents/devcouncil-*.md``.
|
|
9
|
+
* **Output style** — ``.claude/output-styles/devcouncil.md``.
|
|
10
|
+
* **Statusline** — a ``settings.json`` ``statusLine`` entry pointing at ``devcouncil
|
|
11
|
+
claude-statusline`` plus the matching CLI command.
|
|
12
|
+
* **Permissions** — a ``settings.json`` allow-list for the read-only ``dev``/``devcouncil``
|
|
13
|
+
commands so the slash commands and hooks don't prompt on every run.
|
|
14
|
+
* **Plugin bundle** — a self-contained Claude Code plugin + single-repo marketplace under
|
|
15
|
+
``.devcouncil/claude-plugin/`` bundling the commands, agents, skills, hooks, and MCP
|
|
16
|
+
server so the whole integration installs with one ``/plugin install``.
|
|
17
|
+
|
|
18
|
+
Every builder is pure (returns text); writers return the list of paths actually changed so
|
|
19
|
+
re-running is an idempotent no-op. Keeping the generation here (not in the Typer command)
|
|
20
|
+
keeps it unit-testable without a CLI round-trip.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from devcouncil.knowledge.frontmatter import build_frontmatter_markdown
|
|
30
|
+
|
|
31
|
+
# Tools a DevCouncil subagent should be allowed to use: the standard read/edit/run set
|
|
32
|
+
# plus the DevCouncil MCP tools it drives the task loop with. Listing the MCP tools keeps
|
|
33
|
+
# the agent inside the policy-gated workflow rather than free-handing edits.
|
|
34
|
+
_MCP = "mcp__devcouncil"
|
|
35
|
+
_SUBAGENT_CORE_TOOLS = ["Read", "Grep", "Glob", "Bash", "Edit", "Write", "TodoWrite"]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class GeneratedAsset:
|
|
40
|
+
"""One generated file: where it goes and what it should contain."""
|
|
41
|
+
|
|
42
|
+
path: Path
|
|
43
|
+
content: str
|
|
44
|
+
|
|
45
|
+
def write_if_changed(self) -> bool:
|
|
46
|
+
"""Write the file only when its content differs; return True if it changed."""
|
|
47
|
+
if self.path.exists() and self.path.read_text(encoding="utf-8") == self.content:
|
|
48
|
+
return False
|
|
49
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
self.path.write_text(self.content, encoding="utf-8")
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# --- Slash commands -------------------------------------------------------------
|
|
55
|
+
# Each command file lives under .claude/commands/devcouncil/ so it surfaces as
|
|
56
|
+
# /devcouncil:<name>. Lines beginning with ! run as bash (their output is injected),
|
|
57
|
+
# so allowed-tools scopes Bash to exactly the dev subcommand the file shells out to.
|
|
58
|
+
|
|
59
|
+
@dataclass(frozen=True)
|
|
60
|
+
class _SlashCommand:
|
|
61
|
+
name: str
|
|
62
|
+
description: str
|
|
63
|
+
argument_hint: str
|
|
64
|
+
bash: str # the dev command run via ! ; "" means no shell-out
|
|
65
|
+
body: str
|
|
66
|
+
allowed_tools: str
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _slash_commands() -> list[_SlashCommand]:
|
|
70
|
+
return [
|
|
71
|
+
_SlashCommand(
|
|
72
|
+
name="status",
|
|
73
|
+
description="Show the DevCouncil project status (phase, tasks, blocking gaps).",
|
|
74
|
+
argument_hint="",
|
|
75
|
+
bash="dev status",
|
|
76
|
+
body=(
|
|
77
|
+
"Summarize the DevCouncil status above for the user: the current phase, how many "
|
|
78
|
+
"tasks are planned/running/verified, and any blocking gaps. Recommend the single "
|
|
79
|
+
"best next action (e.g. `/devcouncil:next` or `/devcouncil:repair`)."
|
|
80
|
+
),
|
|
81
|
+
allowed_tools="Bash(dev status:*)",
|
|
82
|
+
),
|
|
83
|
+
_SlashCommand(
|
|
84
|
+
name="next",
|
|
85
|
+
description="Find and start the next unblocked DevCouncil task.",
|
|
86
|
+
argument_hint="",
|
|
87
|
+
bash="dev tasks list",
|
|
88
|
+
body=(
|
|
89
|
+
"Identify the highest-priority unblocked task from the list above. Use the DevCouncil "
|
|
90
|
+
"MCP tools to implement it: call `mcp__devcouncil__devcouncil_next_task`, then "
|
|
91
|
+
"`mcp__devcouncil__devcouncil_checkout_task` to acquire a lease, make changes only "
|
|
92
|
+
"through the policy-gated write tools, run tests, then "
|
|
93
|
+
"`mcp__devcouncil__devcouncil_verify_task` and release the lease when verified."
|
|
94
|
+
),
|
|
95
|
+
allowed_tools="Bash(dev tasks:*)",
|
|
96
|
+
),
|
|
97
|
+
_SlashCommand(
|
|
98
|
+
name="verify",
|
|
99
|
+
description="Run DevCouncil verification for a task (or the active task).",
|
|
100
|
+
argument_hint="[TASK-ID]",
|
|
101
|
+
bash="dev verify $ARGUMENTS",
|
|
102
|
+
body=(
|
|
103
|
+
"Report the verification result above. If there are blocking gaps, list them and "
|
|
104
|
+
"propose minimal fixes; do not consider the task complete while blocking gaps remain."
|
|
105
|
+
),
|
|
106
|
+
allowed_tools="Bash(dev verify:*)",
|
|
107
|
+
),
|
|
108
|
+
_SlashCommand(
|
|
109
|
+
name="repair",
|
|
110
|
+
description="Repair the blocking verification gaps for a task.",
|
|
111
|
+
argument_hint="[TASK-ID]",
|
|
112
|
+
bash="dev repair $ARGUMENTS",
|
|
113
|
+
body=(
|
|
114
|
+
"Work through the repair guidance above. Apply the smallest changes that close the "
|
|
115
|
+
"blocking gaps via the policy-gated DevCouncil write tools, then re-run "
|
|
116
|
+
"`/devcouncil:verify` until the task is clean."
|
|
117
|
+
),
|
|
118
|
+
allowed_tools="Bash(dev repair:*)",
|
|
119
|
+
),
|
|
120
|
+
_SlashCommand(
|
|
121
|
+
name="plan",
|
|
122
|
+
description="Plan a goal into DevCouncil requirements and tasks.",
|
|
123
|
+
argument_hint="<goal>",
|
|
124
|
+
bash="",
|
|
125
|
+
body=(
|
|
126
|
+
"Plan the following goal with DevCouncil: $ARGUMENTS\n\n"
|
|
127
|
+
"Run `dev plan \"$ARGUMENTS\"` to generate the requirement/task breakdown, then "
|
|
128
|
+
"summarize the resulting tasks and their scope for the user before any execution."
|
|
129
|
+
),
|
|
130
|
+
allowed_tools="Bash(dev plan:*)",
|
|
131
|
+
),
|
|
132
|
+
_SlashCommand(
|
|
133
|
+
name="review",
|
|
134
|
+
description="Review pending live-review critique cards and resolve blockers.",
|
|
135
|
+
argument_hint="[TASK-ID]",
|
|
136
|
+
bash="dev watch cards $ARGUMENTS",
|
|
137
|
+
body=(
|
|
138
|
+
"Review the live critique cards above. For each blocking card, use "
|
|
139
|
+
"`mcp__devcouncil__devcouncil_live_repair_prompt` to get a ready-to-apply repair, "
|
|
140
|
+
"apply it through the policy-gated write tools, and re-verify."
|
|
141
|
+
),
|
|
142
|
+
allowed_tools="Bash(dev watch:*)",
|
|
143
|
+
),
|
|
144
|
+
_SlashCommand(
|
|
145
|
+
name="report",
|
|
146
|
+
description="Show the full DevCouncil coverage report.",
|
|
147
|
+
argument_hint="",
|
|
148
|
+
bash="dev report",
|
|
149
|
+
body=(
|
|
150
|
+
"Present the coverage report above concisely: requirement coverage, unmapped "
|
|
151
|
+
"requirements, orphaned tasks, and the blocking gaps that need attention next."
|
|
152
|
+
),
|
|
153
|
+
allowed_tools="Bash(dev report:*)",
|
|
154
|
+
),
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _slash_command_markdown(cmd: _SlashCommand) -> str:
|
|
159
|
+
meta: dict[str, object] = {"description": cmd.description}
|
|
160
|
+
if cmd.argument_hint:
|
|
161
|
+
meta["argument-hint"] = cmd.argument_hint
|
|
162
|
+
if cmd.allowed_tools:
|
|
163
|
+
meta["allowed-tools"] = cmd.allowed_tools
|
|
164
|
+
lines: list[str] = []
|
|
165
|
+
if cmd.bash:
|
|
166
|
+
lines.append(f"!`{cmd.bash}`")
|
|
167
|
+
lines.append("")
|
|
168
|
+
lines.append(cmd.body)
|
|
169
|
+
return build_frontmatter_markdown(meta, "\n".join(lines))
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def build_slash_commands(root: Path) -> list[GeneratedAsset]:
|
|
173
|
+
base = root / ".claude" / "commands" / "devcouncil"
|
|
174
|
+
return [
|
|
175
|
+
GeneratedAsset(base / f"{cmd.name}.md", _slash_command_markdown(cmd))
|
|
176
|
+
for cmd in _slash_commands()
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# --- Subagents ------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
@dataclass(frozen=True)
|
|
183
|
+
class _Subagent:
|
|
184
|
+
name: str
|
|
185
|
+
description: str
|
|
186
|
+
tools: list[str]
|
|
187
|
+
body: str
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _subagents() -> list[_Subagent]:
|
|
191
|
+
impl_tools = _SUBAGENT_CORE_TOOLS + [
|
|
192
|
+
f"{_MCP}__devcouncil_next_task",
|
|
193
|
+
f"{_MCP}__devcouncil_get_task",
|
|
194
|
+
f"{_MCP}__devcouncil_get_prompt",
|
|
195
|
+
f"{_MCP}__devcouncil_checkout_task",
|
|
196
|
+
f"{_MCP}__devcouncil_read_file",
|
|
197
|
+
f"{_MCP}__devcouncil_get_diff",
|
|
198
|
+
f"{_MCP}__devcouncil_write_file",
|
|
199
|
+
f"{_MCP}__devcouncil_apply_patch",
|
|
200
|
+
f"{_MCP}__devcouncil_run_command",
|
|
201
|
+
f"{_MCP}__devcouncil_verify_task",
|
|
202
|
+
f"{_MCP}__devcouncil_release_task",
|
|
203
|
+
f"{_MCP}__devcouncil_update_task_scope",
|
|
204
|
+
]
|
|
205
|
+
verify_tools = ["Read", "Grep", "Glob", "Bash"] + [
|
|
206
|
+
f"{_MCP}__devcouncil_get_task",
|
|
207
|
+
f"{_MCP}__devcouncil_get_gaps",
|
|
208
|
+
f"{_MCP}__devcouncil_get_next_actions",
|
|
209
|
+
f"{_MCP}__devcouncil_get_evidence",
|
|
210
|
+
f"{_MCP}__devcouncil_get_task_provenance",
|
|
211
|
+
f"{_MCP}__devcouncil_verify_task",
|
|
212
|
+
]
|
|
213
|
+
review_tools = ["Read", "Grep", "Glob", "Bash"] + [
|
|
214
|
+
f"{_MCP}__devcouncil_get_diff",
|
|
215
|
+
f"{_MCP}__devcouncil_live_review",
|
|
216
|
+
f"{_MCP}__devcouncil_live_cards",
|
|
217
|
+
f"{_MCP}__devcouncil_live_repair_prompt",
|
|
218
|
+
f"{_MCP}__devcouncil_graph_context",
|
|
219
|
+
f"{_MCP}__devcouncil_policy_check_write",
|
|
220
|
+
]
|
|
221
|
+
return [
|
|
222
|
+
_Subagent(
|
|
223
|
+
name="devcouncil-implementer",
|
|
224
|
+
description=(
|
|
225
|
+
"Implements a DevCouncil task end-to-end under policy enforcement. Use when the "
|
|
226
|
+
"user wants to pick up the next task or implement a specific TASK-ID through the "
|
|
227
|
+
"DevCouncil lease/verify loop."
|
|
228
|
+
),
|
|
229
|
+
tools=impl_tools,
|
|
230
|
+
body=(
|
|
231
|
+
"You are the DevCouncil implementer subagent. You make code changes ONLY through "
|
|
232
|
+
"DevCouncil's policy-gated workflow.\n\n"
|
|
233
|
+
"Workflow:\n"
|
|
234
|
+
"1. `devcouncil_next_task` (or use the TASK-ID you were given) and "
|
|
235
|
+
"`devcouncil_checkout_task` to acquire a lease.\n"
|
|
236
|
+
"2. `devcouncil_get_task` + `devcouncil_get_prompt` for scope; `devcouncil_read_file` "
|
|
237
|
+
"and `devcouncil_get_diff` to inspect.\n"
|
|
238
|
+
"3. Change files only via `devcouncil_write_file` / `devcouncil_apply_patch` (the gate "
|
|
239
|
+
"rejects out-of-scope or protected paths) and run tests via `devcouncil_run_command`.\n"
|
|
240
|
+
"4. `devcouncil_verify_task`; fix any blocking gaps and re-verify.\n"
|
|
241
|
+
"5. `devcouncil_release_task` when verified.\n\n"
|
|
242
|
+
"Never touch files outside the task scope. Report the final status and remaining gaps."
|
|
243
|
+
),
|
|
244
|
+
),
|
|
245
|
+
_Subagent(
|
|
246
|
+
name="devcouncil-verifier",
|
|
247
|
+
description=(
|
|
248
|
+
"Runs DevCouncil verification and reports blocking gaps and next actions without "
|
|
249
|
+
"modifying code. Use to confirm whether a task actually meets its requirements."
|
|
250
|
+
),
|
|
251
|
+
tools=verify_tools,
|
|
252
|
+
body=(
|
|
253
|
+
"You are the DevCouncil verifier subagent. You are read-only with respect to source "
|
|
254
|
+
"code: never edit files.\n\n"
|
|
255
|
+
"For the task under review, call `devcouncil_verify_task` (requires a lease) or read "
|
|
256
|
+
"the persisted state with `devcouncil_get_gaps`, `devcouncil_get_next_actions`, "
|
|
257
|
+
"`devcouncil_get_evidence`, and `devcouncil_get_task_provenance`. Report the blocking "
|
|
258
|
+
"gaps, whether the changed code was actually exercised (diff coverage), and the "
|
|
259
|
+
"concrete next actions. Do not declare success while blocking gaps remain."
|
|
260
|
+
),
|
|
261
|
+
),
|
|
262
|
+
_Subagent(
|
|
263
|
+
name="devcouncil-reviewer",
|
|
264
|
+
description=(
|
|
265
|
+
"Reviews the working-tree diff against DevCouncil policy and live-review critique "
|
|
266
|
+
"cards. Use for a structural, policy-aware code review before merge."
|
|
267
|
+
),
|
|
268
|
+
tools=review_tools,
|
|
269
|
+
body=(
|
|
270
|
+
"You are the DevCouncil reviewer subagent. Review the current changes for "
|
|
271
|
+
"correctness, scope, and policy compliance.\n\n"
|
|
272
|
+
"Use `devcouncil_get_diff` for the change set, `devcouncil_graph_context` for "
|
|
273
|
+
"structural impact, `devcouncil_policy_check_write` to confirm changed paths are "
|
|
274
|
+
"in-scope, and `devcouncil_live_review` / `devcouncil_live_cards` for outstanding "
|
|
275
|
+
"critique cards. Summarize findings as blocking vs advisory and reference "
|
|
276
|
+
"file:line. Do not edit code — hand fixes back to the implementer."
|
|
277
|
+
),
|
|
278
|
+
),
|
|
279
|
+
]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _subagent_markdown(agent: _Subagent) -> str:
|
|
283
|
+
meta = {
|
|
284
|
+
"name": agent.name,
|
|
285
|
+
"description": agent.description,
|
|
286
|
+
"tools": ", ".join(agent.tools),
|
|
287
|
+
}
|
|
288
|
+
return build_frontmatter_markdown(meta, agent.body)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def build_subagents(root: Path) -> list[GeneratedAsset]:
|
|
292
|
+
base = root / ".claude" / "agents"
|
|
293
|
+
return [
|
|
294
|
+
GeneratedAsset(base / f"{agent.name}.md", _subagent_markdown(agent))
|
|
295
|
+
for agent in _subagents()
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# --- Output style ---------------------------------------------------------------
|
|
300
|
+
|
|
301
|
+
def build_output_style(root: Path) -> list[GeneratedAsset]:
|
|
302
|
+
meta = {
|
|
303
|
+
"name": "DevCouncil",
|
|
304
|
+
"description": "Evidence-first engineering discipline aligned with DevCouncil's verify loop.",
|
|
305
|
+
}
|
|
306
|
+
body = (
|
|
307
|
+
"You are operating inside a DevCouncil-managed repository. Hold to evidence-first "
|
|
308
|
+
"engineering discipline:\n\n"
|
|
309
|
+
"- Prefer the DevCouncil MCP tools and `dev` CLI for status, scope, and verification "
|
|
310
|
+
"rather than guessing project state.\n"
|
|
311
|
+
"- Make the smallest change that satisfies the task's requirements; stay inside the "
|
|
312
|
+
"task scope and never edit protected/secret paths.\n"
|
|
313
|
+
"- Back claims with evidence: run the tests, show the verification result, and cite "
|
|
314
|
+
"`file:line`. Do not call work done while blocking gaps remain.\n"
|
|
315
|
+
"- When unsure of project conventions, consult `.devcouncil/repo_map.json` and the "
|
|
316
|
+
"applicable skills before writing code.\n"
|
|
317
|
+
"- Be concise: report what changed, what was verified, and what is still blocking."
|
|
318
|
+
)
|
|
319
|
+
return [GeneratedAsset(root / ".claude" / "output-styles" / "devcouncil.md", build_frontmatter_markdown(meta, body))]
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# --- Plugin bundle + marketplace ------------------------------------------------
|
|
323
|
+
# A self-contained Claude Code plugin (and a single-repo marketplace pointing at it) so the
|
|
324
|
+
# whole DevCouncil integration installs with one /plugin install. The plugin bundles its own
|
|
325
|
+
# copies of the commands/agents/skills and a hooks.json + .mcp.json that resolve paths via
|
|
326
|
+
# ${CLAUDE_PROJECT_DIR}, so it works from whatever repo the plugin is enabled in.
|
|
327
|
+
|
|
328
|
+
PLUGIN_ROOT_REL = Path(".devcouncil") / "claude-plugin"
|
|
329
|
+
_PLUGIN_NAME = "devcouncil"
|
|
330
|
+
_MARKETPLACE_NAME = "devcouncil-local"
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def _plugin_dir(root: Path) -> Path:
|
|
334
|
+
return root / PLUGIN_ROOT_REL / _PLUGIN_NAME
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _plugin_json(version: str) -> str:
|
|
338
|
+
manifest = {
|
|
339
|
+
"name": _PLUGIN_NAME,
|
|
340
|
+
"description": "DevCouncil: evidence-gated planning, execution, and verification for coding agents.",
|
|
341
|
+
"version": version,
|
|
342
|
+
"author": {"name": "DevCouncil"},
|
|
343
|
+
"keywords": ["devcouncil", "verification", "planning", "mcp", "code-review"],
|
|
344
|
+
}
|
|
345
|
+
return json.dumps(manifest, indent=2) + "\n"
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _marketplace_json(version: str) -> str:
|
|
349
|
+
manifest = {
|
|
350
|
+
"name": _MARKETPLACE_NAME,
|
|
351
|
+
"owner": {"name": "DevCouncil"},
|
|
352
|
+
"plugins": [
|
|
353
|
+
{
|
|
354
|
+
"name": _PLUGIN_NAME,
|
|
355
|
+
"source": f"./{_PLUGIN_NAME}",
|
|
356
|
+
"description": "DevCouncil Claude Code integration: commands, subagents, skills, hooks, and MCP.",
|
|
357
|
+
"version": version,
|
|
358
|
+
}
|
|
359
|
+
],
|
|
360
|
+
}
|
|
361
|
+
return json.dumps(manifest, indent=2) + "\n"
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def _plugin_hooks_json(*, write_gate: bool = False) -> str:
|
|
365
|
+
"""hooks.json for the plugin, resolving the project root via ${CLAUDE_PROJECT_DIR}.
|
|
366
|
+
|
|
367
|
+
Assist-mode by default (no blocking write-gate) so installing the plugin into an
|
|
368
|
+
interactive session never fail-closes it. The blocking PreToolUse/PostToolUse gate is
|
|
369
|
+
included only when ``write_gate`` is True."""
|
|
370
|
+
def cmd(event: str) -> str:
|
|
371
|
+
return f'devcouncil hook {event} --client claude --project-root "${{CLAUDE_PROJECT_DIR}}"'
|
|
372
|
+
|
|
373
|
+
hooks: dict[str, list] = {
|
|
374
|
+
"Stop": [{"hooks": [{"type": "command", "command": cmd("agent-response"), "timeout": 10000}]}],
|
|
375
|
+
"SessionStart": [{"matcher": "startup|resume", "hooks": [{"type": "command", "command": cmd("session-start"), "timeout": 10000}]}],
|
|
376
|
+
"UserPromptSubmit": [{"hooks": [{"type": "command", "command": cmd("user-prompt-submit"), "timeout": 10000}]}],
|
|
377
|
+
"SubagentStop": [{"hooks": [{"type": "command", "command": cmd("subagent-stop"), "timeout": 10000}]}],
|
|
378
|
+
"Notification": [{"hooks": [{"type": "command", "command": cmd("notification"), "timeout": 10000}]}],
|
|
379
|
+
}
|
|
380
|
+
if write_gate:
|
|
381
|
+
tool_matcher = "Bash|Write|Edit|MultiEdit"
|
|
382
|
+
hooks["PreToolUse"] = [{"matcher": tool_matcher, "hooks": [{"type": "command", "command": cmd("pre-tool-use"), "timeout": 10000}]}]
|
|
383
|
+
hooks["PostToolUse"] = [{"matcher": tool_matcher, "hooks": [{"type": "command", "command": cmd("post-tool-use"), "timeout": 10000}]}]
|
|
384
|
+
return json.dumps({"hooks": hooks}, indent=2) + "\n"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _plugin_mcp_json(root: Path) -> str:
|
|
388
|
+
config = {
|
|
389
|
+
"mcpServers": {
|
|
390
|
+
"devcouncil": {
|
|
391
|
+
"type": "stdio",
|
|
392
|
+
"command": "devcouncil",
|
|
393
|
+
"args": ["mcp-server"],
|
|
394
|
+
"env": {"DEVCOUNCIL_PROJECT_ROOT": "${CLAUDE_PROJECT_DIR}"},
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return json.dumps(config, indent=2) + "\n"
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _plugin_readme() -> str:
|
|
402
|
+
return (
|
|
403
|
+
"# DevCouncil Claude Code plugin\n\n"
|
|
404
|
+
"This plugin bundles DevCouncil's full Claude Code integration: slash commands, "
|
|
405
|
+
"subagents, engineering skills, lifecycle hooks, and the DevCouncil MCP server.\n\n"
|
|
406
|
+
"## Install\n\n"
|
|
407
|
+
"```\n"
|
|
408
|
+
"/plugin marketplace add <path-to-repo>/.devcouncil/claude-plugin\n"
|
|
409
|
+
f"/plugin install {_PLUGIN_NAME}@{_MARKETPLACE_NAME}\n"
|
|
410
|
+
"```\n\n"
|
|
411
|
+
"Requires the `devcouncil` CLI on PATH (`pipx install devcouncil`) and a "
|
|
412
|
+
"DevCouncil-initialized repo (`dev init`).\n"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def build_plugin_bundle(
|
|
417
|
+
root: Path, *, version: str, skill_assets: list[GeneratedAsset] | None = None, write_gate: bool = False
|
|
418
|
+
) -> list[GeneratedAsset]:
|
|
419
|
+
"""Build the plugin tree: manifest, marketplace, bundled commands/agents, hooks, MCP."""
|
|
420
|
+
plugin = _plugin_dir(root)
|
|
421
|
+
market_root = root / PLUGIN_ROOT_REL
|
|
422
|
+
assets: list[GeneratedAsset] = [
|
|
423
|
+
GeneratedAsset(market_root / ".claude-plugin" / "marketplace.json", _marketplace_json(version)),
|
|
424
|
+
GeneratedAsset(plugin / ".claude-plugin" / "plugin.json", _plugin_json(version)),
|
|
425
|
+
GeneratedAsset(plugin / "hooks" / "hooks.json", _plugin_hooks_json(write_gate=write_gate)),
|
|
426
|
+
GeneratedAsset(plugin / ".mcp.json", _plugin_mcp_json(root)),
|
|
427
|
+
GeneratedAsset(plugin / "README.md", _plugin_readme()),
|
|
428
|
+
]
|
|
429
|
+
# Bundle command + agent copies into the plugin tree (plugin layout puts them at the
|
|
430
|
+
# plugin root, not under .claude/).
|
|
431
|
+
for cmd in _slash_commands():
|
|
432
|
+
assets.append(GeneratedAsset(plugin / "commands" / "devcouncil" / f"{cmd.name}.md", _slash_command_markdown(cmd)))
|
|
433
|
+
for agent in _subagents():
|
|
434
|
+
assets.append(GeneratedAsset(plugin / "agents" / f"{agent.name}.md", _subagent_markdown(agent)))
|
|
435
|
+
# Bundle the selected skills (passed in so selection logic stays in the skills layer).
|
|
436
|
+
for skill_asset in skill_assets or []:
|
|
437
|
+
rel = skill_asset.path
|
|
438
|
+
# skill_asset.path is .claude/skills/<name>/SKILL.md — re-root under the plugin.
|
|
439
|
+
try:
|
|
440
|
+
tail = rel.relative_to(root / ".claude" / "skills")
|
|
441
|
+
except ValueError:
|
|
442
|
+
tail = Path(rel.name)
|
|
443
|
+
assets.append(GeneratedAsset(plugin / "skills" / tail, skill_asset.content))
|
|
444
|
+
return assets
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import logging
|
|
2
3
|
import shutil
|
|
3
4
|
import subprocess
|
|
4
5
|
from pathlib import Path
|
|
@@ -8,6 +9,8 @@ from pydantic import BaseModel, Field
|
|
|
8
9
|
|
|
9
10
|
from devcouncil.app.config import load_config
|
|
10
11
|
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
11
14
|
|
|
12
15
|
class CodeReviewGraphContext(BaseModel):
|
|
13
16
|
available: bool
|
|
@@ -24,11 +27,17 @@ class CodeReviewGraphAdapter:
|
|
|
24
27
|
|
|
25
28
|
def __init__(self, project_root: Path, command: str | None = None):
|
|
26
29
|
self.project_root = project_root
|
|
30
|
+
self._config: Any = None
|
|
27
31
|
self.command = command or self._configured_command()
|
|
28
32
|
|
|
33
|
+
def _get_config(self) -> Any:
|
|
34
|
+
if self._config is None:
|
|
35
|
+
self._config = load_config(self.project_root)
|
|
36
|
+
return self._config
|
|
37
|
+
|
|
29
38
|
def is_enabled(self) -> bool:
|
|
30
39
|
try:
|
|
31
|
-
return
|
|
40
|
+
return self._get_config().integrations.code_review_graph.enabled
|
|
32
41
|
except Exception:
|
|
33
42
|
return False
|
|
34
43
|
|
|
@@ -57,9 +66,11 @@ class CodeReviewGraphAdapter:
|
|
|
57
66
|
for command in commands:
|
|
58
67
|
result = self._run(command)
|
|
59
68
|
if result.returncode == 0 and result.output.strip():
|
|
69
|
+
logger.debug("code-review-graph context obtained via: %s", " ".join(command))
|
|
60
70
|
return self._parse_context(result.output, changed_files)
|
|
61
71
|
errors.append(result.output.strip() or f"exit {result.returncode}")
|
|
62
72
|
|
|
73
|
+
logger.warning("code-review-graph ran but returned no context (%d command(s) tried)", len(commands))
|
|
63
74
|
return CodeReviewGraphContext(
|
|
64
75
|
available=True,
|
|
65
76
|
summary="code-review-graph ran but did not return context.",
|
|
@@ -84,7 +95,7 @@ class CodeReviewGraphAdapter:
|
|
|
84
95
|
|
|
85
96
|
def _configured_command(self) -> str:
|
|
86
97
|
try:
|
|
87
|
-
return
|
|
98
|
+
return self._get_config().integrations.code_review_graph.command
|
|
88
99
|
except Exception:
|
|
89
100
|
return "code-review-graph"
|
|
90
101
|
|
|
@@ -11,6 +11,7 @@ caller keeps the original goal text unchanged — expansion is strictly additive
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import json
|
|
14
|
+
import logging
|
|
14
15
|
import re
|
|
15
16
|
import shutil
|
|
16
17
|
import subprocess
|
|
@@ -19,6 +20,8 @@ from pathlib import Path
|
|
|
19
20
|
|
|
20
21
|
from devcouncil.utils.subprocess_env import clean_subprocess_env
|
|
21
22
|
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
22
25
|
# Cap each pulled discussion comment so a long thread can't dominate the prompt.
|
|
23
26
|
_MAX_COMMENT_CHARS = 600
|
|
24
27
|
|
|
@@ -58,6 +61,7 @@ def _gh_view(ref: IntentRef, sub: str, root: Path) -> dict | None:
|
|
|
58
61
|
"""Run ``gh <issue|pr> view`` and return the parsed JSON, or None on failure."""
|
|
59
62
|
gh = shutil.which("gh")
|
|
60
63
|
if not gh:
|
|
64
|
+
logger.debug("gh CLI not on PATH; cannot expand %s #%s", sub, ref.number)
|
|
61
65
|
return None
|
|
62
66
|
cmd = [gh, sub, "view", str(ref.number), "--json", "title,body,comments,url,state"]
|
|
63
67
|
if ref.repo:
|
|
@@ -67,13 +71,16 @@ def _gh_view(ref: IntentRef, sub: str, root: Path) -> dict | None:
|
|
|
67
71
|
cmd, cwd=root, capture_output=True, text=True,
|
|
68
72
|
encoding="utf-8", errors="replace", timeout=20, env=clean_subprocess_env(),
|
|
69
73
|
)
|
|
70
|
-
except Exception:
|
|
74
|
+
except Exception as exc:
|
|
75
|
+
logger.warning("gh %s view %s failed: %s", sub, ref.number, exc)
|
|
71
76
|
return None
|
|
72
77
|
if result.returncode != 0 or not result.stdout.strip():
|
|
78
|
+
logger.warning("gh %s view %s returned %s: %s", sub, ref.number, result.returncode, (result.stderr or "").strip()[:200])
|
|
73
79
|
return None
|
|
74
80
|
try:
|
|
75
81
|
data = json.loads(result.stdout)
|
|
76
82
|
except json.JSONDecodeError:
|
|
83
|
+
logger.warning("gh %s view %s returned unparseable JSON", sub, ref.number)
|
|
77
84
|
return None
|
|
78
85
|
return data if isinstance(data, dict) else None
|
|
79
86
|
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
2
6
|
from rich.console import Console
|
|
3
|
-
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from devcouncil.indexing.graph_index import GraphIndex
|
|
4
10
|
|
|
5
11
|
console = Console()
|
|
6
12
|
AGENT_GUIDE_MARKER = "<!-- Managed by dev map: keep this file in sync with .devcouncil/repo_map.json. -->"
|
|
@@ -58,5 +64,7 @@ class GitNexusIntegration:
|
|
|
58
64
|
"""
|
|
59
65
|
Export DevCouncil artifact graph to GitNexus.
|
|
60
66
|
"""
|
|
67
|
+
# TODO: Consume graph_index to export the artifact graph. This is still a stub;
|
|
68
|
+
# the GraphIndex is intentionally not loaded/instantiated yet to avoid dead work.
|
|
69
|
+
# The import is kept under TYPE_CHECKING so module import does no eager work.
|
|
61
70
|
console.print(" - Syncing DevCouncil artifacts to GitNexus...")
|
|
62
|
-
pass
|