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.
- package/README.md +201 -6
- package/package.json +9 -2
- package/pyproject.toml +34 -2
- package/src/devcouncil/app/config.py +348 -12
- package/src/devcouncil/app/orchestrator.py +10 -6
- package/src/devcouncil/app/state_machine.py +4 -0
- package/src/devcouncil/artifacts/graph.py +32 -5
- package/src/devcouncil/assets/__init__.py +1 -0
- package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/commands/agents.py +292 -0
- package/src/devcouncil/cli/commands/artifacts.py +6 -3
- package/src/devcouncil/cli/commands/check.py +220 -0
- package/src/devcouncil/cli/commands/config.py +43 -4
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +6 -1
- package/src/devcouncil/cli/commands/design.py +186 -0
- package/src/devcouncil/cli/commands/doctor.py +379 -22
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +532 -33
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +296 -15
- package/src/devcouncil/cli/commands/init.py +161 -20
- package/src/devcouncil/cli/commands/integrate.py +1371 -124
- package/src/devcouncil/cli/commands/logs.py +106 -0
- package/src/devcouncil/cli/commands/map.py +80 -10
- package/src/devcouncil/cli/commands/okf.py +245 -0
- package/src/devcouncil/cli/commands/plan.py +256 -55
- package/src/devcouncil/cli/commands/prompt.py +18 -7
- package/src/devcouncil/cli/commands/repair.py +50 -24
- package/src/devcouncil/cli/commands/report.py +8 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
- package/src/devcouncil/cli/commands/rollback.py +27 -28
- package/src/devcouncil/cli/commands/run.py +195 -54
- package/src/devcouncil/cli/commands/runs.py +223 -0
- package/src/devcouncil/cli/commands/scaffold.py +32 -0
- package/src/devcouncil/cli/commands/semantic.py +47 -0
- package/src/devcouncil/cli/commands/setup.py +145 -6
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/skills.py +267 -0
- package/src/devcouncil/cli/commands/status.py +30 -15
- package/src/devcouncil/cli/commands/trace.py +47 -3
- package/src/devcouncil/cli/commands/verify.py +144 -3
- package/src/devcouncil/cli/commands/watch.py +32 -12
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +91 -7
- package/src/devcouncil/domain/evidence.py +29 -2
- package/src/devcouncil/domain/gap.py +27 -1
- package/src/devcouncil/domain/task.py +31 -2
- package/src/devcouncil/execution/checkpoints.py +256 -0
- package/src/devcouncil/execution/context_builder.py +1 -1
- package/src/devcouncil/execution/fs_watcher.py +205 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +162 -74
- package/src/devcouncil/execution/patch.py +65 -10
- package/src/devcouncil/execution/permissions.py +24 -24
- package/src/devcouncil/execution/policy_engine.py +350 -0
- package/src/devcouncil/execution/prompt_builder.py +751 -23
- package/src/devcouncil/execution/shell_session.py +231 -0
- package/src/devcouncil/execution/task_runner.py +24 -9
- package/src/devcouncil/executors/agent_registry.py +596 -0
- package/src/devcouncil/executors/coding_cli.py +791 -39
- package/src/devcouncil/executors/mini_swe.py +6 -0
- package/src/devcouncil/executors/native/agent.py +135 -19
- package/src/devcouncil/executors/openhands.py +6 -0
- package/src/devcouncil/gating/checks/clean_git.py +3 -1
- package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
- package/src/devcouncil/gating/policy.py +190 -11
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/ast_matcher.py +17 -7
- package/src/devcouncil/indexing/lsp.py +45 -4
- package/src/devcouncil/indexing/repo_mapper.py +1284 -15
- package/src/devcouncil/indexing/semantic_index.py +221 -0
- package/src/devcouncil/integrations/actions.py +166 -0
- package/src/devcouncil/integrations/check.py +426 -0
- 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 +149 -0
- package/src/devcouncil/integrations/gitnexus.py +45 -2
- package/src/devcouncil/integrations/mcp/server.py +1944 -32
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- 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 +181 -25
- package/src/devcouncil/live/repair_prompt.py +29 -6
- package/src/devcouncil/live/reviewer.py +72 -13
- package/src/devcouncil/live/signals.py +2 -2
- package/src/devcouncil/live/summary.py +18 -8
- package/src/devcouncil/live/transcripts.py +47 -11
- package/src/devcouncil/llm/cache.py +20 -8
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +617 -49
- package/src/devcouncil/llm/router.py +337 -53
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/optimization/skillopt.py +673 -0
- package/src/devcouncil/planning/arbiter_service.py +10 -2
- package/src/devcouncil/planning/correction_manifest.py +346 -0
- package/src/devcouncil/planning/critique_service.py +16 -4
- package/src/devcouncil/planning/plan_service.py +86 -6
- package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
- package/src/devcouncil/planning/repair_service.py +8 -2
- package/src/devcouncil/planning/spec_service.py +37 -3
- package/src/devcouncil/repo/ci_scaffold.py +165 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +384 -0
- package/src/devcouncil/reporting/json_report.py +22 -1
- package/src/devcouncil/reporting/markdown_report.py +29 -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/__init__.py +19 -0
- package/src/devcouncil/skills/library/README.md +46 -0
- package/src/devcouncil/skills/library/ai-training.md +50 -0
- package/src/devcouncil/skills/library/android.md +50 -0
- package/src/devcouncil/skills/library/backend.md +52 -0
- package/src/devcouncil/skills/library/core-engineering.md +95 -0
- package/src/devcouncil/skills/library/data-engineering.md +47 -0
- package/src/devcouncil/skills/library/desktop.md +46 -0
- package/src/devcouncil/skills/library/devops.md +48 -0
- package/src/devcouncil/skills/library/game-dev.md +46 -0
- package/src/devcouncil/skills/library/ios.md +48 -0
- package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
- package/src/devcouncil/skills/library/security.md +48 -0
- package/src/devcouncil/skills/library/systems.md +48 -0
- package/src/devcouncil/skills/library/web.md +47 -0
- package/src/devcouncil/skills/library/windows.md +47 -0
- package/src/devcouncil/skills/registry.py +408 -0
- package/src/devcouncil/storage/db.py +140 -3
- package/src/devcouncil/storage/models.py +125 -0
- package/src/devcouncil/storage/native.py +559 -0
- package/src/devcouncil/storage/repositories.py +157 -78
- package/src/devcouncil/telemetry/cost.py +123 -17
- package/src/devcouncil/telemetry/logging_setup.py +244 -0
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/stages.py +141 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +24 -10
- package/src/devcouncil/ui/dashboard.py +393 -28
- package/src/devcouncil/utils/redaction.py +9 -3
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/acceptance_compiler.py +253 -0
- package/src/devcouncil/verification/ad_hoc_check.py +135 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/implementation_reviewer.py +11 -2
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +181 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1549 -143
- package/uv.lock +205 -64
- 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
|
|
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
|
|
|
@@ -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
|
+
)
|