devcouncil 0.1.1 → 0.2.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 +190 -6
- package/package.json +9 -2
- package/pyproject.toml +34 -2
- package/src/devcouncil/app/config.py +167 -5
- package/src/devcouncil/artifacts/graph.py +23 -3
- 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 +209 -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/doctor.py +221 -21
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +452 -33
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +124 -15
- package/src/devcouncil/cli/commands/init.py +154 -18
- package/src/devcouncil/cli/commands/integrate.py +894 -105
- package/src/devcouncil/cli/commands/map.py +80 -10
- package/src/devcouncil/cli/commands/plan.py +212 -51
- package/src/devcouncil/cli/commands/prompt.py +18 -7
- package/src/devcouncil/cli/commands/repair.py +40 -23
- 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 +69 -49
- 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 +88 -0
- package/src/devcouncil/cli/commands/status.py +25 -1
- package/src/devcouncil/cli/commands/trace.py +47 -3
- package/src/devcouncil/cli/commands/verify.py +138 -3
- package/src/devcouncil/cli/commands/watch.py +9 -9
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +56 -7
- package/src/devcouncil/domain/evidence.py +22 -2
- package/src/devcouncil/domain/gap.py +27 -1
- package/src/devcouncil/domain/task.py +31 -2
- package/src/devcouncil/execution/checkpoints.py +246 -0
- package/src/devcouncil/execution/context_builder.py +1 -1
- package/src/devcouncil/execution/fs_watcher.py +180 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +162 -74
- package/src/devcouncil/execution/patch.py +59 -10
- package/src/devcouncil/execution/permissions.py +17 -24
- package/src/devcouncil/execution/policy_engine.py +343 -0
- package/src/devcouncil/execution/prompt_builder.py +633 -21
- package/src/devcouncil/execution/shell_session.py +225 -0
- package/src/devcouncil/execution/task_runner.py +6 -2
- package/src/devcouncil/executors/agent_registry.py +575 -0
- package/src/devcouncil/executors/coding_cli.py +663 -39
- package/src/devcouncil/executors/native/agent.py +121 -20
- package/src/devcouncil/gating/checks/clean_git.py +3 -1
- package/src/devcouncil/gating/checks/secret_scan_check.py +40 -21
- package/src/devcouncil/gating/policy.py +158 -10
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/ast_matcher.py +1 -1
- package/src/devcouncil/indexing/lsp.py +45 -4
- package/src/devcouncil/indexing/repo_mapper.py +1256 -9
- package/src/devcouncil/indexing/semantic_index.py +205 -0
- package/src/devcouncil/integrations/actions.py +146 -0
- package/src/devcouncil/integrations/check.py +423 -0
- package/src/devcouncil/integrations/github_intent.py +142 -0
- package/src/devcouncil/integrations/gitnexus.py +35 -0
- package/src/devcouncil/integrations/mcp/server.py +1552 -29
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/live/cards.py +161 -19
- package/src/devcouncil/live/signals.py +2 -2
- package/src/devcouncil/live/transcripts.py +9 -6
- package/src/devcouncil/llm/cache.py +10 -6
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +515 -34
- package/src/devcouncil/llm/router.py +231 -46
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/planning/correction_manifest.py +303 -0
- package/src/devcouncil/planning/critique_service.py +7 -2
- package/src/devcouncil/planning/plan_service.py +17 -3
- package/src/devcouncil/planning/prompt_enhancer_service.py +82 -1
- package/src/devcouncil/planning/spec_service.py +27 -1
- package/src/devcouncil/repo/ci_scaffold.py +157 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +374 -0
- package/src/devcouncil/reporting/json_report.py +11 -1
- package/src/devcouncil/reporting/markdown_report.py +15 -0
- 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 +330 -0
- package/src/devcouncil/storage/db.py +83 -2
- package/src/devcouncil/storage/models.py +121 -0
- package/src/devcouncil/storage/native.py +557 -0
- package/src/devcouncil/storage/repositories.py +137 -75
- package/src/devcouncil/telemetry/cost.py +123 -17
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +12 -9
- package/src/devcouncil/ui/dashboard.py +324 -23
- package/src/devcouncil/utils/redaction.py +9 -3
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/acceptance_compiler.py +125 -0
- package/src/devcouncil/verification/ad_hoc_check.py +129 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +178 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1065 -47
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -23,13 +23,17 @@ def _project_root(project_root: Path | None = None) -> Path:
|
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
def _active_task(root: Path):
|
|
26
|
+
# Resolve the *single* unambiguous running task. active_task_id returns None when
|
|
27
|
+
# zero or multiple tasks are running, so we never authorize a write against the
|
|
28
|
+
# wrong task; the policy engine then denies for task=None (fail-closed).
|
|
29
|
+
active_id = active_task_id(root)
|
|
30
|
+
if not active_id:
|
|
31
|
+
return None
|
|
26
32
|
db = get_db(root)
|
|
27
33
|
if not db:
|
|
28
34
|
return None
|
|
29
35
|
with db.get_session() as session:
|
|
30
|
-
|
|
31
|
-
running_tasks = [t for t in task_repo.get_all() if t.status == "running"]
|
|
32
|
-
return running_tasks[0] if running_tasks else None
|
|
36
|
+
return TaskRepository(session).get_by_id(active_id)
|
|
33
37
|
|
|
34
38
|
|
|
35
39
|
def _emit_decision(client: str, action: str, reason: str) -> None:
|
|
@@ -48,31 +52,53 @@ def _emit_decision(client: str, action: str, reason: str) -> None:
|
|
|
48
52
|
console.print(f"[yellow]DevCouncil Warning:[/yellow] {reason}")
|
|
49
53
|
|
|
50
54
|
|
|
55
|
+
def _emit_unevaluable(client: str, reason: str, strict: bool, *, action: str = "warn") -> None:
|
|
56
|
+
"""Decide what to do when a tool call cannot be evaluated (empty/malformed/error).
|
|
57
|
+
|
|
58
|
+
Fail-closed in strict mode (block), otherwise surface a warning but allow — and
|
|
59
|
+
never leak an undefined exit code, which would silently disable the only pre-action
|
|
60
|
+
gate."""
|
|
61
|
+
_emit_decision(client, "deny" if strict else action, f"{reason}{' (strict mode: blocking)' if strict else ''}")
|
|
62
|
+
|
|
63
|
+
|
|
51
64
|
@app.command()
|
|
52
65
|
def pre_tool_use(
|
|
53
66
|
tool_call_json: str | None = typer.Argument(None, help="The JSON string of the tool call from the coding CLI."),
|
|
54
67
|
client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
|
|
55
68
|
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
69
|
+
strict: bool = typer.Option(
|
|
70
|
+
False,
|
|
71
|
+
"--strict",
|
|
72
|
+
envvar="DEVCOUNCIL_HOOK_STRICT",
|
|
73
|
+
help="Fail closed (block) when a tool call cannot be parsed or evaluated.",
|
|
74
|
+
),
|
|
56
75
|
):
|
|
57
76
|
"""
|
|
58
77
|
Coding CLI hook: Inspects a tool call before execution.
|
|
59
78
|
Exits with code 2 to block unauthorized file writes.
|
|
60
79
|
"""
|
|
80
|
+
normalized_client = client.lower()
|
|
61
81
|
try:
|
|
62
82
|
if tool_call_json is None:
|
|
63
83
|
tool_call_json = sys.stdin.read()
|
|
84
|
+
# Empty payload: nothing to evaluate. Benign in normal use, so allow — but make
|
|
85
|
+
# it observable, and block under --strict.
|
|
64
86
|
if not tool_call_json.strip():
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
87
|
+
return _emit_unevaluable(normalized_client, "Empty tool-call payload; nothing to evaluate.", strict, action="allow")
|
|
88
|
+
try:
|
|
89
|
+
call_data = json.loads(tool_call_json)
|
|
90
|
+
except json.JSONDecodeError:
|
|
91
|
+
# A real tool call we cannot parse must not silently pass the gate.
|
|
92
|
+
return _emit_unevaluable(normalized_client, "Tool-call payload was not valid JSON; could not enforce policy.", strict)
|
|
68
93
|
root = _project_root(project_root)
|
|
69
94
|
active_task = _active_task(root)
|
|
70
95
|
|
|
71
96
|
decision = HookPolicy(project_root=root).evaluate(call_data, active_task)
|
|
72
97
|
_emit_decision(normalized_client, decision.action, decision.reason)
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
except typer.Exit:
|
|
99
|
+
raise
|
|
100
|
+
except Exception as exc: # never emit an undefined exit code from a crashing hook
|
|
101
|
+
return _emit_unevaluable(normalized_client, f"Hook error: {exc}", strict)
|
|
76
102
|
|
|
77
103
|
@app.command()
|
|
78
104
|
def post_tool_use(
|
|
@@ -117,12 +143,95 @@ def agent_response(
|
|
|
117
143
|
print(json.dumps({"decision": "allow", "suppressOutput": True}, separators=(",", ":")))
|
|
118
144
|
|
|
119
145
|
@app.command()
|
|
120
|
-
def post_task(
|
|
146
|
+
def post_task(
|
|
147
|
+
client: str = typer.Option("claude", "--client", help="Hook client: claude, codex, gemini, cursor, or generic."),
|
|
148
|
+
project_root: Path | None = typer.Option(None, "--project-root", help="Repository root containing .devcouncil/."),
|
|
149
|
+
):
|
|
121
150
|
"""
|
|
122
151
|
Coding CLI hook: Runs after a task is completed.
|
|
123
|
-
|
|
152
|
+
|
|
153
|
+
When ``execution.verify_on_post_task`` is enabled, this runs deterministic
|
|
154
|
+
verification of the active task and records gaps; otherwise it just reminds the
|
|
155
|
+
user to run ``dev verify`` (the default, to keep hooks fast/cheap).
|
|
124
156
|
"""
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
157
|
+
root = _project_root(project_root)
|
|
158
|
+
try:
|
|
159
|
+
from devcouncil.app.config import load_config
|
|
160
|
+
verify_enabled = load_config(root).execution.verify_on_post_task
|
|
161
|
+
except Exception:
|
|
162
|
+
verify_enabled = False
|
|
163
|
+
|
|
164
|
+
if not verify_enabled:
|
|
165
|
+
console.print("[cyan]DevCouncil: coding agent finished task.[/cyan]")
|
|
166
|
+
console.print("Run [bold]dev verify[/bold] to finalize implementation evidence.")
|
|
167
|
+
_emit_post_task_allow(client)
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
summary = _verify_active_task(root)
|
|
171
|
+
console.print(summary)
|
|
172
|
+
_emit_post_task_allow(client)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _emit_post_task_allow(client: str) -> None:
|
|
176
|
+
if client.lower() in {"codex", "gemini"}:
|
|
177
|
+
print(json.dumps({"decision": "allow", "suppressOutput": True}, separators=(",", ":")))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _verify_active_task(root: Path) -> str:
|
|
181
|
+
"""Run deterministic verification of the active task and persist gaps/evidence.
|
|
182
|
+
Returns a human summary line. Best-effort: never raises out of a hook."""
|
|
183
|
+
try:
|
|
184
|
+
import asyncio
|
|
185
|
+
|
|
186
|
+
from devcouncil.domain.evidence import CommandResult, DiffCoverageEvidence, DiffEvidence, TestEvidence
|
|
187
|
+
from devcouncil.storage.repositories import (
|
|
188
|
+
EvidenceRepository,
|
|
189
|
+
GapRepository,
|
|
190
|
+
RequirementRepository,
|
|
191
|
+
)
|
|
192
|
+
from devcouncil.verification.next_actions import split_next_actions
|
|
193
|
+
from devcouncil.verification.verifier import Verifier
|
|
194
|
+
|
|
195
|
+
active_id = active_task_id(root)
|
|
196
|
+
db = get_db(root)
|
|
197
|
+
if not active_id or not db:
|
|
198
|
+
return "Run [bold]dev verify[/bold] to finalize implementation evidence."
|
|
199
|
+
with db.get_session() as session:
|
|
200
|
+
task = TaskRepository(session).get_by_id(active_id)
|
|
201
|
+
if not task:
|
|
202
|
+
return "Run [bold]dev verify[/bold] to finalize implementation evidence."
|
|
203
|
+
reqs = RequirementRepository(session).get_all()
|
|
204
|
+
gaps, evidence = asyncio.run(Verifier(root).verify_task(task, reqs))
|
|
205
|
+
gap_repo = GapRepository(session)
|
|
206
|
+
ev_repo = EvidenceRepository(session)
|
|
207
|
+
gap_repo.delete_for_task(task.id)
|
|
208
|
+
ev_repo.delete_for_task(task.id)
|
|
209
|
+
for gap in gaps:
|
|
210
|
+
gap_repo.save(gap)
|
|
211
|
+
for ev in evidence:
|
|
212
|
+
if isinstance(ev, CommandResult):
|
|
213
|
+
ev_repo.save_command_result(task.id, ev)
|
|
214
|
+
elif isinstance(ev, DiffCoverageEvidence):
|
|
215
|
+
ev_repo.save_diff_coverage_evidence(ev)
|
|
216
|
+
elif isinstance(ev, DiffEvidence):
|
|
217
|
+
ev_repo.save_diff_evidence(ev)
|
|
218
|
+
elif isinstance(ev, TestEvidence):
|
|
219
|
+
ev_repo.save_test_evidence(ev, task.id)
|
|
220
|
+
blocking = [g for g in gaps if g.blocking]
|
|
221
|
+
task.status = "blocked" if blocking else "verified"
|
|
222
|
+
TaskRepository(session).save(task)
|
|
223
|
+
blocking_actions, _ = split_next_actions(gaps)
|
|
224
|
+
TraceLogger(root).log_event(
|
|
225
|
+
"post_task_verified",
|
|
226
|
+
{"task_id": active_id, "blocking": len(blocking)},
|
|
227
|
+
task_id=active_id,
|
|
228
|
+
summary=f"post_task verification: {task.status}",
|
|
229
|
+
)
|
|
230
|
+
if blocking:
|
|
231
|
+
return (
|
|
232
|
+
f"[yellow]{active_id} is blocked by {len(blocking)} gap(s); "
|
|
233
|
+
f"{len(blocking_actions)} next action(s). Run [bold]dev repair[/bold].[/yellow]"
|
|
234
|
+
)
|
|
235
|
+
return f"[green]{active_id} verified.[/green]"
|
|
236
|
+
except Exception as exc: # never let a hook crash the agent
|
|
237
|
+
return f"[dim]post-task verification skipped: {exc}[/dim]"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import copy
|
|
2
|
+
from typing import Any
|
|
2
3
|
import typer
|
|
3
4
|
import yaml
|
|
4
5
|
from rich.console import Console
|
|
@@ -6,10 +7,42 @@ from pathlib import Path
|
|
|
6
7
|
from devcouncil.storage.db import Database
|
|
7
8
|
from devcouncil.integrations.gitnexus import GitNexusIntegration
|
|
8
9
|
from devcouncil.integrations.graphify import GraphifyIntegration
|
|
10
|
+
from devcouncil.llm.provider import build_role_model_config, validate_model_provider
|
|
11
|
+
from devcouncil.repo.gitignore import ensure_gitignore
|
|
9
12
|
|
|
10
13
|
app = typer.Typer()
|
|
11
14
|
console = Console()
|
|
12
15
|
|
|
16
|
+
# Per-stack default verification commands. A fresh project gets ONLY the commands
|
|
17
|
+
# for the stack(s) actually detected in the repo, so the verifier never inherits a
|
|
18
|
+
# cross-stack gate (e.g. `npm test`/`eslint`/`tsc` on a Python repo) that it would run
|
|
19
|
+
# as a blocking fallback and fail for tooling/stack reasons instead of a real defect.
|
|
20
|
+
_STACK_COMMAND_DEFAULTS: dict[str, dict[str, list[str]]] = {
|
|
21
|
+
"python": {"test": ["pytest"], "lint": ["ruff check ."], "typecheck": ["mypy ."]},
|
|
22
|
+
"node": {"test": ["npm test"], "lint": ["eslint ."], "typecheck": ["tsc --noEmit"]},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _stack_aware_commands(project_root: Path) -> dict[str, list[str]]:
|
|
27
|
+
"""Default test/lint/typecheck commands scoped to the repo's detected stack(s).
|
|
28
|
+
|
|
29
|
+
Returns empty lists when no stack is detected — empty is safe (no speculative
|
|
30
|
+
fallback gates) and far better than guessing wrong-stack tools."""
|
|
31
|
+
from devcouncil.repo.ci_scaffold import detect_stacks
|
|
32
|
+
|
|
33
|
+
commands: dict[str, list[str]] = {"test": [], "lint": [], "typecheck": []}
|
|
34
|
+
try:
|
|
35
|
+
stacks = detect_stacks(project_root)
|
|
36
|
+
except Exception:
|
|
37
|
+
return commands
|
|
38
|
+
for stack in sorted(stacks):
|
|
39
|
+
for key, cmds in _STACK_COMMAND_DEFAULTS.get(stack, {}).items():
|
|
40
|
+
for command in cmds:
|
|
41
|
+
if command not in commands[key]:
|
|
42
|
+
commands[key].append(command)
|
|
43
|
+
return commands
|
|
44
|
+
|
|
45
|
+
|
|
13
46
|
DEFAULT_CONFIG = {
|
|
14
47
|
"project": {
|
|
15
48
|
"name": "devcouncil-project",
|
|
@@ -18,23 +51,12 @@ DEFAULT_CONFIG = {
|
|
|
18
51
|
},
|
|
19
52
|
"models": {
|
|
20
53
|
"provider": "openrouter",
|
|
21
|
-
"roles":
|
|
22
|
-
"spec_writer": {"model": "anthropic/claude-3.5-sonnet"},
|
|
23
|
-
"prompt_enhancer": {"model": "anthropic/claude-3.5-sonnet"},
|
|
24
|
-
"planner_a": {"model": "anthropic/claude-3.5-sonnet"},
|
|
25
|
-
"planner_b": {"model": "google/gemini-pro-1.5"},
|
|
26
|
-
"critic_a": {"model": "openai/gpt-4o"},
|
|
27
|
-
"critic_b": {"model": "anthropic/claude-3-opus"},
|
|
28
|
-
"arbiter": {"model": "openai/gpt-4o"},
|
|
29
|
-
"native_agent": {"model": "anthropic/claude-3.5-sonnet"},
|
|
30
|
-
"implementation_reviewer": {"model": "openai/gpt-4o"},
|
|
31
|
-
"live_reviewer": {"model": "openai/gpt-4o"},
|
|
32
|
-
}
|
|
54
|
+
"roles": build_role_model_config("openrouter"),
|
|
33
55
|
},
|
|
34
56
|
"commands": {
|
|
35
|
-
"test": [
|
|
36
|
-
"lint": [
|
|
37
|
-
"typecheck": [
|
|
57
|
+
"test": [],
|
|
58
|
+
"lint": [],
|
|
59
|
+
"typecheck": [],
|
|
38
60
|
},
|
|
39
61
|
"gates": {
|
|
40
62
|
"require_clean_git_before_task": True,
|
|
@@ -45,9 +67,12 @@ DEFAULT_CONFIG = {
|
|
|
45
67
|
"block_failed_commands": True
|
|
46
68
|
},
|
|
47
69
|
"execution": {
|
|
48
|
-
"default_executor": "
|
|
70
|
+
"default_executor": "manual",
|
|
49
71
|
"max_repair_attempts": 3,
|
|
50
|
-
"checkpoint_before_each_task": True
|
|
72
|
+
"checkpoint_before_each_task": True,
|
|
73
|
+
"stream_cli_output": False,
|
|
74
|
+
"cursor_resume_mode": "off",
|
|
75
|
+
"coding_cli_probe_order": [],
|
|
51
76
|
},
|
|
52
77
|
"privacy": {
|
|
53
78
|
"redact_env_vars": True,
|
|
@@ -71,15 +96,90 @@ DEFAULT_CONFIG = {
|
|
|
71
96
|
"signals_path": ".devcouncil/live/signals",
|
|
72
97
|
"default_client": "claude",
|
|
73
98
|
},
|
|
99
|
+
"cli_agents": {
|
|
100
|
+
"enabled": True,
|
|
101
|
+
"profiles": {
|
|
102
|
+
"default": {
|
|
103
|
+
"description": "Balanced local execution with DevCouncil verification.",
|
|
104
|
+
},
|
|
105
|
+
"yolo": {
|
|
106
|
+
"description": "Faster local execution; DevCouncil still verifies the final diff.",
|
|
107
|
+
"timeout_seconds": 3600,
|
|
108
|
+
"prompt_preamble": "Profile: yolo. Move efficiently within the task scope.",
|
|
109
|
+
},
|
|
110
|
+
"prod": {
|
|
111
|
+
"description": "Restrictive execution for high-risk repositories.",
|
|
112
|
+
"timeout_seconds": 1800,
|
|
113
|
+
"prompt_preamble": "Profile: prod. Keep edits minimal and explicitly within task scope.",
|
|
114
|
+
"require_explicit_confirmation": True,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
"agents": {},
|
|
118
|
+
},
|
|
74
119
|
}
|
|
75
120
|
}
|
|
76
121
|
|
|
77
122
|
|
|
123
|
+
def parse_role_model_overrides(values: list[str] | None) -> dict[str, str]:
|
|
124
|
+
overrides: dict[str, str] = {}
|
|
125
|
+
for value in values or []:
|
|
126
|
+
if "=" not in value:
|
|
127
|
+
raise ValueError(f"Invalid --role-model value '{value}'. Use ROLE=MODEL.")
|
|
128
|
+
role, model = value.split("=", 1)
|
|
129
|
+
role = role.strip()
|
|
130
|
+
model = model.strip()
|
|
131
|
+
if not role or not model:
|
|
132
|
+
raise ValueError(f"Invalid --role-model value '{value}'. Use ROLE=MODEL.")
|
|
133
|
+
overrides[role] = model
|
|
134
|
+
return overrides
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _generate_initial_map(project_root: Path, quiet: bool) -> None:
|
|
138
|
+
"""Best-effort repo map + agent guide generation on fresh init.
|
|
139
|
+
|
|
140
|
+
Imported lazily to avoid a circular import with the map command, and wrapped
|
|
141
|
+
so a mapping failure never blocks initialization.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
from devcouncil.cli.commands.map import generate_map_artifacts
|
|
145
|
+
|
|
146
|
+
generate_map_artifacts(project_root, project_root / ".devcouncil" / "repo_map.json")
|
|
147
|
+
if not quiet:
|
|
148
|
+
console.print("[green]Generated .devcouncil/repo_map.json and agent guides (AGENTS.md, CLAUDE.md).[/green]")
|
|
149
|
+
except Exception as exc: # mapping is best-effort, never fatal
|
|
150
|
+
if not quiet:
|
|
151
|
+
console.print(f"[yellow]Skipped repo map generation: {exc}. Run 'dev map' later.[/yellow]")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _scaffold_initial_skills(project_root: Path, quiet: bool) -> None:
|
|
155
|
+
"""Best-effort scaffolding of applicable engineering skills into .claude/skills/.
|
|
156
|
+
|
|
157
|
+
Always writes the core-engineering skill; adds domain skills (android, ios, web,
|
|
158
|
+
...) whose file triggers match the repository. Never fatal.
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
from devcouncil.skills.registry import scaffold_skills, select_skills
|
|
162
|
+
|
|
163
|
+
selected = select_skills(project_root=project_root)
|
|
164
|
+
written = scaffold_skills(project_root, selected)
|
|
165
|
+
if written and not quiet:
|
|
166
|
+
names = ", ".join(sorted(skill.name for skill in selected))
|
|
167
|
+
console.print(f"[green]Scaffolded {len(written)} skill(s) into .claude/skills/ ({names}).[/green]")
|
|
168
|
+
except Exception as exc: # skill scaffolding is best-effort, never fatal
|
|
169
|
+
if not quiet:
|
|
170
|
+
console.print(f"[yellow]Skipped skill scaffolding: {exc}. Run 'dev skills scaffold' later.[/yellow]")
|
|
171
|
+
|
|
172
|
+
|
|
78
173
|
def initialize_project(
|
|
79
174
|
project_root: Path = Path("."),
|
|
80
175
|
project_name: str | None = None,
|
|
176
|
+
model_provider: str = "openrouter",
|
|
177
|
+
model: str | None = None,
|
|
178
|
+
role_models: dict[str, str] | None = None,
|
|
81
179
|
with_gitnexus: bool = False,
|
|
82
180
|
with_graphify: bool = False,
|
|
181
|
+
with_map: bool = True,
|
|
182
|
+
with_skills: bool = True,
|
|
83
183
|
quiet: bool = False,
|
|
84
184
|
) -> bool:
|
|
85
185
|
"""Initialize DevCouncil project state.
|
|
@@ -100,11 +200,20 @@ def initialize_project(
|
|
|
100
200
|
(dev_dir / "logs").mkdir(exist_ok=True)
|
|
101
201
|
|
|
102
202
|
config_path = dev_dir / "config.yaml"
|
|
103
|
-
config = copy.deepcopy(DEFAULT_CONFIG)
|
|
203
|
+
config: dict[str, Any] = copy.deepcopy(DEFAULT_CONFIG)
|
|
204
|
+
# Scope default verification commands to the repo's actual stack(s).
|
|
205
|
+
config["commands"] = _stack_aware_commands(project_root)
|
|
104
206
|
if project_name:
|
|
105
207
|
config["project"]["name"] = project_name
|
|
106
208
|
else:
|
|
107
209
|
config["project"]["name"] = project_root.name
|
|
210
|
+
provider = validate_model_provider(model_provider)
|
|
211
|
+
config["models"]["provider"] = provider
|
|
212
|
+
config["models"]["roles"] = build_role_model_config(
|
|
213
|
+
provider,
|
|
214
|
+
model=model,
|
|
215
|
+
role_models=role_models,
|
|
216
|
+
)
|
|
108
217
|
|
|
109
218
|
with open(config_path, "w") as f:
|
|
110
219
|
yaml.dump(config, f, default_flow_style=False)
|
|
@@ -115,6 +224,11 @@ def initialize_project(
|
|
|
115
224
|
console.print(f"[green]Successfully initialized DevCouncil in {dev_dir}[/green]")
|
|
116
225
|
created = True
|
|
117
226
|
|
|
227
|
+
if with_map:
|
|
228
|
+
_generate_initial_map(project_root, quiet)
|
|
229
|
+
if with_skills:
|
|
230
|
+
_scaffold_initial_skills(project_root, quiet)
|
|
231
|
+
|
|
118
232
|
if with_gitnexus:
|
|
119
233
|
nexus = GitNexusIntegration(project_root)
|
|
120
234
|
nexus.initialize()
|
|
@@ -123,6 +237,7 @@ def initialize_project(
|
|
|
123
237
|
graphify = GraphifyIntegration(project_root)
|
|
124
238
|
graphify.initialize()
|
|
125
239
|
|
|
240
|
+
ensure_gitignore(project_root)
|
|
126
241
|
return created
|
|
127
242
|
|
|
128
243
|
|
|
@@ -130,8 +245,17 @@ def initialize_project(
|
|
|
130
245
|
def init(
|
|
131
246
|
ctx: typer.Context,
|
|
132
247
|
project_name: str = typer.Option(None, "--name", "-n", help="Project name"),
|
|
248
|
+
provider: str = typer.Option("openrouter", "--provider", help="Model provider for generated config."),
|
|
249
|
+
model: str | None = typer.Option(None, "--model", "-m", help="Model id to use for every default role."),
|
|
250
|
+
role_model: list[str] | None = typer.Option(
|
|
251
|
+
None,
|
|
252
|
+
"--role-model",
|
|
253
|
+
help="Per-role model override in ROLE=MODEL form. Can be repeated.",
|
|
254
|
+
),
|
|
133
255
|
with_gitnexus: bool = typer.Option(False, "--gitnexus", help="Initialize GitNexus structural awareness"),
|
|
134
256
|
with_graphify: bool = typer.Option(False, "--graphify", help="Initialize Graphify knowledge graph engine"),
|
|
257
|
+
skip_map: bool = typer.Option(False, "--skip-map", help="Skip generating repo_map.json and agent guides on init."),
|
|
258
|
+
skip_skills: bool = typer.Option(False, "--skip-skills", help="Skip scaffolding engineering skills into .claude/skills/ on init."),
|
|
135
259
|
):
|
|
136
260
|
"""
|
|
137
261
|
Initialize DevCouncil in the current directory.
|
|
@@ -145,9 +269,21 @@ def init(
|
|
|
145
269
|
console.print("Use --gitnexus or --graphify to add upgrade paths.")
|
|
146
270
|
raise typer.Exit()
|
|
147
271
|
|
|
272
|
+
try:
|
|
273
|
+
role_models = parse_role_model_overrides(role_model)
|
|
274
|
+
model_provider = validate_model_provider(provider)
|
|
275
|
+
except ValueError as e:
|
|
276
|
+
console.print(f"[red]{e}[/red]")
|
|
277
|
+
raise typer.Exit(code=2) from e
|
|
278
|
+
|
|
148
279
|
initialize_project(
|
|
149
280
|
Path("."),
|
|
150
281
|
project_name=project_name,
|
|
282
|
+
model_provider=model_provider,
|
|
283
|
+
model=model,
|
|
284
|
+
role_models=role_models,
|
|
151
285
|
with_gitnexus=with_gitnexus,
|
|
152
286
|
with_graphify=with_graphify,
|
|
287
|
+
with_map=not skip_map,
|
|
288
|
+
with_skills=not skip_skills,
|
|
153
289
|
)
|