devcouncil 0.1.1 → 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.
Files changed (159) hide show
  1. package/README.md +201 -6
  2. package/package.json +9 -2
  3. package/pyproject.toml +34 -2
  4. package/src/devcouncil/app/config.py +348 -12
  5. package/src/devcouncil/app/orchestrator.py +10 -6
  6. package/src/devcouncil/app/state_machine.py +4 -0
  7. package/src/devcouncil/artifacts/graph.py +32 -5
  8. package/src/devcouncil/assets/__init__.py +1 -0
  9. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  10. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  11. package/src/devcouncil/cli/commands/agents.py +292 -0
  12. package/src/devcouncil/cli/commands/artifacts.py +6 -3
  13. package/src/devcouncil/cli/commands/check.py +220 -0
  14. package/src/devcouncil/cli/commands/config.py +43 -4
  15. package/src/devcouncil/cli/commands/cost.py +57 -0
  16. package/src/devcouncil/cli/commands/dashboard.py +6 -1
  17. package/src/devcouncil/cli/commands/design.py +186 -0
  18. package/src/devcouncil/cli/commands/doctor.py +379 -22
  19. package/src/devcouncil/cli/commands/evidence.py +48 -0
  20. package/src/devcouncil/cli/commands/go.py +532 -33
  21. package/src/devcouncil/cli/commands/handoff.py +69 -0
  22. package/src/devcouncil/cli/commands/hook.py +296 -15
  23. package/src/devcouncil/cli/commands/init.py +161 -20
  24. package/src/devcouncil/cli/commands/integrate.py +1371 -124
  25. package/src/devcouncil/cli/commands/logs.py +106 -0
  26. package/src/devcouncil/cli/commands/map.py +80 -10
  27. package/src/devcouncil/cli/commands/okf.py +245 -0
  28. package/src/devcouncil/cli/commands/plan.py +256 -55
  29. package/src/devcouncil/cli/commands/prompt.py +18 -7
  30. package/src/devcouncil/cli/commands/repair.py +50 -24
  31. package/src/devcouncil/cli/commands/report.py +8 -0
  32. package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
  33. package/src/devcouncil/cli/commands/rollback.py +27 -28
  34. package/src/devcouncil/cli/commands/run.py +195 -54
  35. package/src/devcouncil/cli/commands/runs.py +223 -0
  36. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  37. package/src/devcouncil/cli/commands/semantic.py +47 -0
  38. package/src/devcouncil/cli/commands/setup.py +145 -6
  39. package/src/devcouncil/cli/commands/shell.py +73 -0
  40. package/src/devcouncil/cli/commands/skills.py +267 -0
  41. package/src/devcouncil/cli/commands/status.py +30 -15
  42. package/src/devcouncil/cli/commands/trace.py +47 -3
  43. package/src/devcouncil/cli/commands/verify.py +144 -3
  44. package/src/devcouncil/cli/commands/watch.py +32 -12
  45. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  46. package/src/devcouncil/cli/main.py +91 -7
  47. package/src/devcouncil/domain/evidence.py +29 -2
  48. package/src/devcouncil/domain/gap.py +27 -1
  49. package/src/devcouncil/domain/task.py +31 -2
  50. package/src/devcouncil/execution/checkpoints.py +256 -0
  51. package/src/devcouncil/execution/context_builder.py +1 -1
  52. package/src/devcouncil/execution/fs_watcher.py +205 -0
  53. package/src/devcouncil/execution/handoff.py +102 -0
  54. package/src/devcouncil/execution/hook_policy.py +162 -74
  55. package/src/devcouncil/execution/patch.py +65 -10
  56. package/src/devcouncil/execution/permissions.py +24 -24
  57. package/src/devcouncil/execution/policy_engine.py +350 -0
  58. package/src/devcouncil/execution/prompt_builder.py +751 -23
  59. package/src/devcouncil/execution/shell_session.py +231 -0
  60. package/src/devcouncil/execution/task_runner.py +24 -9
  61. package/src/devcouncil/executors/agent_registry.py +596 -0
  62. package/src/devcouncil/executors/coding_cli.py +791 -39
  63. package/src/devcouncil/executors/mini_swe.py +6 -0
  64. package/src/devcouncil/executors/native/agent.py +135 -19
  65. package/src/devcouncil/executors/openhands.py +6 -0
  66. package/src/devcouncil/gating/checks/clean_git.py +3 -1
  67. package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
  68. package/src/devcouncil/gating/policy.py +190 -11
  69. package/src/devcouncil/hardware.py +184 -0
  70. package/src/devcouncil/indexing/ast_matcher.py +17 -7
  71. package/src/devcouncil/indexing/lsp.py +45 -4
  72. package/src/devcouncil/indexing/repo_mapper.py +1284 -15
  73. package/src/devcouncil/indexing/semantic_index.py +221 -0
  74. package/src/devcouncil/integrations/actions.py +166 -0
  75. package/src/devcouncil/integrations/check.py +426 -0
  76. package/src/devcouncil/integrations/claude_assets.py +444 -0
  77. package/src/devcouncil/integrations/code_review_graph.py +13 -2
  78. package/src/devcouncil/integrations/github_intent.py +149 -0
  79. package/src/devcouncil/integrations/gitnexus.py +45 -2
  80. package/src/devcouncil/integrations/mcp/server.py +1944 -32
  81. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  82. package/src/devcouncil/integrations/pr_comments.py +9 -0
  83. package/src/devcouncil/knowledge/__init__.py +23 -0
  84. package/src/devcouncil/knowledge/design.py +374 -0
  85. package/src/devcouncil/knowledge/design_conformance.py +317 -0
  86. package/src/devcouncil/knowledge/fetch.py +223 -0
  87. package/src/devcouncil/knowledge/frontmatter.py +51 -0
  88. package/src/devcouncil/knowledge/okf.py +202 -0
  89. package/src/devcouncil/knowledge/skill_bridge.py +96 -0
  90. package/src/devcouncil/knowledge/sources.py +239 -0
  91. package/src/devcouncil/live/cards.py +181 -25
  92. package/src/devcouncil/live/repair_prompt.py +29 -6
  93. package/src/devcouncil/live/reviewer.py +72 -13
  94. package/src/devcouncil/live/signals.py +2 -2
  95. package/src/devcouncil/live/summary.py +18 -8
  96. package/src/devcouncil/live/transcripts.py +47 -11
  97. package/src/devcouncil/llm/cache.py +20 -8
  98. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  99. package/src/devcouncil/llm/provider.py +617 -49
  100. package/src/devcouncil/llm/router.py +337 -53
  101. package/src/devcouncil/optimization/__init__.py +1 -0
  102. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  103. package/src/devcouncil/optimization/skillopt.py +673 -0
  104. package/src/devcouncil/planning/arbiter_service.py +10 -2
  105. package/src/devcouncil/planning/correction_manifest.py +346 -0
  106. package/src/devcouncil/planning/critique_service.py +16 -4
  107. package/src/devcouncil/planning/plan_service.py +86 -6
  108. package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
  109. package/src/devcouncil/planning/repair_service.py +8 -2
  110. package/src/devcouncil/planning/spec_service.py +37 -3
  111. package/src/devcouncil/repo/ci_scaffold.py +165 -0
  112. package/src/devcouncil/repo/gitignore.py +123 -0
  113. package/src/devcouncil/repo/sca.py +384 -0
  114. package/src/devcouncil/reporting/json_report.py +22 -1
  115. package/src/devcouncil/reporting/markdown_report.py +29 -1
  116. package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
  117. package/src/devcouncil/reporting/okf_html.py +323 -0
  118. package/src/devcouncil/reporting/report_builder.py +18 -1
  119. package/src/devcouncil/skills/__init__.py +19 -0
  120. package/src/devcouncil/skills/library/README.md +46 -0
  121. package/src/devcouncil/skills/library/ai-training.md +50 -0
  122. package/src/devcouncil/skills/library/android.md +50 -0
  123. package/src/devcouncil/skills/library/backend.md +52 -0
  124. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  125. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  126. package/src/devcouncil/skills/library/desktop.md +46 -0
  127. package/src/devcouncil/skills/library/devops.md +48 -0
  128. package/src/devcouncil/skills/library/game-dev.md +46 -0
  129. package/src/devcouncil/skills/library/ios.md +48 -0
  130. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  131. package/src/devcouncil/skills/library/security.md +48 -0
  132. package/src/devcouncil/skills/library/systems.md +48 -0
  133. package/src/devcouncil/skills/library/web.md +47 -0
  134. package/src/devcouncil/skills/library/windows.md +47 -0
  135. package/src/devcouncil/skills/registry.py +408 -0
  136. package/src/devcouncil/storage/db.py +140 -3
  137. package/src/devcouncil/storage/models.py +125 -0
  138. package/src/devcouncil/storage/native.py +559 -0
  139. package/src/devcouncil/storage/repositories.py +157 -78
  140. package/src/devcouncil/telemetry/cost.py +123 -17
  141. package/src/devcouncil/telemetry/logging_setup.py +244 -0
  142. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  143. package/src/devcouncil/telemetry/pricing.py +28 -0
  144. package/src/devcouncil/telemetry/stages.py +141 -0
  145. package/src/devcouncil/telemetry/traces.py +62 -7
  146. package/src/devcouncil/telemetry/tracker.py +24 -10
  147. package/src/devcouncil/ui/dashboard.py +393 -28
  148. package/src/devcouncil/utils/redaction.py +9 -3
  149. package/src/devcouncil/utils/subprocess_env.py +69 -0
  150. package/src/devcouncil/verification/acceptance_compiler.py +253 -0
  151. package/src/devcouncil/verification/ad_hoc_check.py +135 -0
  152. package/src/devcouncil/verification/diff_coverage.py +353 -0
  153. package/src/devcouncil/verification/implementation_reviewer.py +11 -2
  154. package/src/devcouncil/verification/next_actions.py +189 -0
  155. package/src/devcouncil/verification/sandbox.py +181 -0
  156. package/src/devcouncil/verification/test_resolver.py +91 -0
  157. package/src/devcouncil/verification/verifier.py +1549 -143
  158. package/uv.lock +205 -64
  159. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -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 load_config(self.project_root).integrations.code_review_graph.enabled
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 load_config(self.project_root).integrations.code_review_graph.command
98
+ return self._get_config().integrations.code_review_graph.command
88
99
  except Exception:
89
100
  return "code-review-graph"
90
101
 
@@ -0,0 +1,149 @@
1
+ """Resolve a DevCouncil goal from a GitHub issue or pull-request reference.
2
+
3
+ A terse goal like ``"#142"`` or a full issue URL carries far more intent than a
4
+ one-line argument — the issue body usually *is* the spec. This module detects
5
+ such references and expands them into a rich goal string (title + body + a few
6
+ comments) by shelling out to the authenticated ``gh`` CLI, so private repos work
7
+ without any token plumbing. When ``gh`` is unavailable or the lookup fails, the
8
+ caller keeps the original goal text unchanged — expansion is strictly additive.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import logging
15
+ import re
16
+ import shutil
17
+ import subprocess
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+
21
+ from devcouncil.utils.subprocess_env import clean_subprocess_env
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ # Cap each pulled discussion comment so a long thread can't dominate the prompt.
26
+ _MAX_COMMENT_CHARS = 600
27
+
28
+ # "#142", "GH-142", "owner/repo#142", or a full issues/pull URL.
29
+ _SHORT_REF = re.compile(r"^\s*(?:GH-|#)(\d+)\s*$", re.IGNORECASE)
30
+ _OWNER_REPO_REF = re.compile(r"^\s*([\w.-]+/[\w.-]+)#(\d+)\s*$")
31
+ _URL_REF = re.compile(
32
+ r"^\s*https?://github\.com/([\w.-]+/[\w.-]+)/(issues|pull)/(\d+)",
33
+ re.IGNORECASE,
34
+ )
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class IntentRef:
39
+ number: int
40
+ kind: str # "issue" | "pull" | "auto"
41
+ repo: str | None # "owner/repo" when explicit, else None (current repo)
42
+
43
+
44
+ def parse_intent_ref(goal: str) -> IntentRef | None:
45
+ """Return the GitHub reference a goal points at, or None if it's plain text."""
46
+ text = goal.strip()
47
+ m = _URL_REF.match(text)
48
+ if m:
49
+ kind = "pull" if m.group(2).lower() == "pull" else "issue"
50
+ return IntentRef(number=int(m.group(3)), kind=kind, repo=m.group(1))
51
+ m = _OWNER_REPO_REF.match(text)
52
+ if m:
53
+ return IntentRef(number=int(m.group(2)), kind="auto", repo=m.group(1))
54
+ m = _SHORT_REF.match(text)
55
+ if m:
56
+ return IntentRef(number=int(m.group(1)), kind="auto", repo=None)
57
+ return None
58
+
59
+
60
+ def _gh_view(ref: IntentRef, sub: str, root: Path) -> dict | None:
61
+ """Run ``gh <issue|pr> view`` and return the parsed JSON, or None on failure."""
62
+ gh = shutil.which("gh")
63
+ if not gh:
64
+ logger.debug("gh CLI not on PATH; cannot expand %s #%s", sub, ref.number)
65
+ return None
66
+ cmd = [gh, sub, "view", str(ref.number), "--json", "title,body,comments,url,state"]
67
+ if ref.repo:
68
+ cmd += ["--repo", ref.repo]
69
+ try:
70
+ result = subprocess.run(
71
+ cmd, cwd=root, capture_output=True, text=True,
72
+ encoding="utf-8", errors="replace", timeout=20, env=clean_subprocess_env(),
73
+ )
74
+ except Exception as exc:
75
+ logger.warning("gh %s view %s failed: %s", sub, ref.number, exc)
76
+ return None
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])
79
+ return None
80
+ try:
81
+ data = json.loads(result.stdout)
82
+ except json.JSONDecodeError:
83
+ logger.warning("gh %s view %s returned unparseable JSON", sub, ref.number)
84
+ return None
85
+ return data if isinstance(data, dict) else None
86
+
87
+
88
+ def _compose_goal(ref: IntentRef, data: dict, source: str) -> str:
89
+ title = str(data.get("title") or "").strip()
90
+ body = str(data.get("body") or "").strip()
91
+ url = str(data.get("url") or "").strip()
92
+ lines = [f"Implement {source} #{ref.number}: {title}".rstrip(": ").rstrip()]
93
+ if url:
94
+ lines.append(f"Source: {url}")
95
+ if body:
96
+ lines += ["", body]
97
+ # Pull in up to three discussion comments — clarifications often live there.
98
+ # Cap each so a long thread can't bloat the planning prompt; the issue body
99
+ # above is the primary spec, comments are secondary context.
100
+ comments = data.get("comments")
101
+ if isinstance(comments, list) and comments:
102
+ snippets = []
103
+ for comment in comments[:3]:
104
+ text = str((comment or {}).get("body") or "").strip()
105
+ if text:
106
+ if len(text) > _MAX_COMMENT_CHARS:
107
+ text = text[:_MAX_COMMENT_CHARS].rstrip() + " […]"
108
+ snippets.append(text)
109
+ if snippets:
110
+ lines += ["", "Discussion notes:"]
111
+ lines += [f"- {s}" for s in snippets]
112
+ return "\n".join(lines).strip()
113
+
114
+
115
+ def resolve_goal_intent(goal: str, root: Path) -> tuple[str, str | None]:
116
+ """Expand a GitHub issue/PR reference into a full goal.
117
+
118
+ Returns ``(goal, note)``. When ``goal`` is a reference and the lookup
119
+ succeeds, the first element is the composed goal and ``note`` describes the
120
+ expansion (for display). Otherwise the original goal is returned with a
121
+ ``note`` explaining why it could not be expanded (or ``None`` when the goal
122
+ was plain text and no expansion was attempted).
123
+ """
124
+ ref = parse_intent_ref(goal)
125
+ if ref is None:
126
+ return goal, None
127
+
128
+ if not shutil.which("gh"):
129
+ return goal, (
130
+ f"Goal looks like GitHub reference #{ref.number}, but the `gh` CLI is not on "
131
+ "PATH — using the literal text. Install/auth gh to pull the issue/PR body."
132
+ )
133
+
134
+ order = (
135
+ ["pull", "issue"] if ref.kind == "pull"
136
+ else ["issue", "pull"] if ref.kind == "issue"
137
+ else ["issue", "pull"]
138
+ )
139
+ for sub in order:
140
+ data = _gh_view(ref, "pr" if sub == "pull" else "issue", root)
141
+ if data is not None:
142
+ source = "pull request" if sub == "pull" else "issue"
143
+ composed = _compose_goal(ref, data, source)
144
+ return composed, f"Pulled intent from {source} #{ref.number} via gh."
145
+
146
+ return goal, (
147
+ f"Could not fetch GitHub #{ref.number} via gh (not found, no access, or not a "
148
+ "git/GitHub repo) — using the literal text."
149
+ )