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
|
@@ -8,18 +8,27 @@ import yaml
|
|
|
8
8
|
from rich.console import Console
|
|
9
9
|
from rich.panel import Panel
|
|
10
10
|
|
|
11
|
-
from devcouncil.app.config import
|
|
11
|
+
from devcouncil.app.config import (
|
|
12
|
+
_normalized_provider_name,
|
|
13
|
+
get_gcloud_access_token,
|
|
14
|
+
load_config,
|
|
15
|
+
load_local_secrets,
|
|
16
|
+
provider_api_key_env_var,
|
|
17
|
+
)
|
|
12
18
|
from devcouncil.cli.commands.doctor import render_doctor_check
|
|
13
|
-
from devcouncil.cli.commands.init import initialize_project
|
|
19
|
+
from devcouncil.cli.commands.init import initialize_project, parse_role_model_overrides
|
|
14
20
|
from devcouncil.cli.commands.integrate import (
|
|
15
21
|
_claude_command,
|
|
16
22
|
_codex_command,
|
|
23
|
+
_configure_antigravity,
|
|
24
|
+
_configure_cursor,
|
|
17
25
|
_configure_native_hooks,
|
|
26
|
+
_configure_opencode,
|
|
27
|
+
_configure_warp,
|
|
18
28
|
_configure,
|
|
19
|
-
_cursor_command,
|
|
20
29
|
_gemini_command,
|
|
21
30
|
)
|
|
22
|
-
from devcouncil.llm.provider import validate_model_provider
|
|
31
|
+
from devcouncil.llm.provider import apply_provider_default_role_models, build_role_model_config, validate_model_provider
|
|
23
32
|
|
|
24
33
|
app = typer.Typer()
|
|
25
34
|
console = Console()
|
|
@@ -32,9 +41,36 @@ def _set_model_provider(project_root: Path, provider: str) -> None:
|
|
|
32
41
|
raw_config.setdefault("models", {})
|
|
33
42
|
previous = raw_config["models"].get("provider", "openrouter")
|
|
34
43
|
raw_config["models"]["provider"] = normalized
|
|
44
|
+
updated_role_defaults = apply_provider_default_role_models(raw_config, previous, normalized)
|
|
35
45
|
config_path.write_text(yaml.dump(raw_config, default_flow_style=False), encoding="utf-8")
|
|
36
46
|
if previous != normalized:
|
|
37
47
|
console.print(f"[green]Updated model provider from {previous} to {normalized}.[/green]")
|
|
48
|
+
if updated_role_defaults:
|
|
49
|
+
console.print(f"[green]Updated default role models for {normalized}.[/green]")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _set_model_roles(
|
|
53
|
+
project_root: Path,
|
|
54
|
+
model: str | None = None,
|
|
55
|
+
role_models: dict[str, str] | None = None,
|
|
56
|
+
) -> None:
|
|
57
|
+
if not model and not role_models:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
config_path = project_root / ".devcouncil" / "config.yaml"
|
|
61
|
+
raw_config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
|
|
62
|
+
raw_config.setdefault("models", {})
|
|
63
|
+
provider = validate_model_provider(raw_config["models"].get("provider", "openrouter"))
|
|
64
|
+
raw_config["models"]["roles"] = build_role_model_config(
|
|
65
|
+
provider,
|
|
66
|
+
model=model,
|
|
67
|
+
role_models=role_models,
|
|
68
|
+
)
|
|
69
|
+
config_path.write_text(yaml.dump(raw_config, default_flow_style=False), encoding="utf-8")
|
|
70
|
+
if model:
|
|
71
|
+
console.print(f"[green]Updated all model roles to use {model}.[/green]")
|
|
72
|
+
for role, selected_model in (role_models or {}).items():
|
|
73
|
+
console.print(f"[green]Updated {role} to use {selected_model}.[/green]")
|
|
38
74
|
|
|
39
75
|
|
|
40
76
|
def _write_local_secret(project_root: Path, env_var: str, value: str) -> Path:
|
|
@@ -54,9 +90,43 @@ def _write_local_secret(project_root: Path, env_var: str, value: str) -> Path:
|
|
|
54
90
|
return path
|
|
55
91
|
|
|
56
92
|
|
|
93
|
+
def _configure_vertexai_settings(
|
|
94
|
+
project_root: Path,
|
|
95
|
+
provider: str,
|
|
96
|
+
vertex_project: str | None,
|
|
97
|
+
vertex_location: str | None,
|
|
98
|
+
) -> None:
|
|
99
|
+
if provider != "vertexai":
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
local_secrets = load_local_secrets(project_root)
|
|
103
|
+
if vertex_project:
|
|
104
|
+
_write_local_secret(project_root, "VERTEXAI_PROJECT", vertex_project)
|
|
105
|
+
console.print("[green]Saved VERTEXAI_PROJECT to .devcouncil/secrets.env.[/green]")
|
|
106
|
+
elif not (
|
|
107
|
+
os.environ.get("VERTEXAI_PROJECT")
|
|
108
|
+
or os.environ.get("GOOGLE_CLOUD_PROJECT")
|
|
109
|
+
or local_secrets.get("VERTEXAI_PROJECT")
|
|
110
|
+
or local_secrets.get("GOOGLE_CLOUD_PROJECT")
|
|
111
|
+
):
|
|
112
|
+
console.print(
|
|
113
|
+
"[yellow]VERTEXAI_PROJECT is not set. "
|
|
114
|
+
"Set it in your shell or rerun setup with --vertex-project PROJECT_ID.[/yellow]"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if vertex_location:
|
|
118
|
+
_write_local_secret(project_root, "VERTEXAI_LOCATION", vertex_location)
|
|
119
|
+
console.print("[green]Saved VERTEXAI_LOCATION to .devcouncil/secrets.env.[/green]")
|
|
120
|
+
|
|
121
|
+
|
|
57
122
|
def _configure_api_key(project_root: Path, api_key: str | None, skip_api_key: bool) -> None:
|
|
58
123
|
config = load_config(project_root)
|
|
59
124
|
provider = config.models.provider
|
|
125
|
+
if _normalized_provider_name(provider) == "ollama":
|
|
126
|
+
console.print(
|
|
127
|
+
"[green]Ollama uses a local server (default http://localhost:11434); no API key required.[/green]"
|
|
128
|
+
)
|
|
129
|
+
return
|
|
60
130
|
env_var = provider_api_key_env_var(provider)
|
|
61
131
|
local_secrets = load_local_secrets(project_root)
|
|
62
132
|
|
|
@@ -70,6 +140,9 @@ def _configure_api_key(project_root: Path, api_key: str | None, skip_api_key: bo
|
|
|
70
140
|
_write_local_secret(project_root, env_var, api_key)
|
|
71
141
|
console.print(f"[green]Saved {env_var} to .devcouncil/secrets.env.[/green]")
|
|
72
142
|
return
|
|
143
|
+
if provider == "vertexai" and get_gcloud_access_token():
|
|
144
|
+
console.print("[green]Vertex AI access token is available from gcloud auth print-access-token.[/green]")
|
|
145
|
+
return
|
|
73
146
|
if skip_api_key:
|
|
74
147
|
console.print(f"[yellow]Skipped {env_var} setup. Model-backed commands will ask again if it is missing.[/yellow]")
|
|
75
148
|
return
|
|
@@ -109,7 +182,6 @@ def _configure_coding_cli_integrations(project_root: Path, apply: bool, gemini_s
|
|
|
109
182
|
("Codex CLI", _codex_command(project_root)),
|
|
110
183
|
("Gemini CLI", _gemini_command(project_root, gemini_scope)),
|
|
111
184
|
("Claude Code", _claude_command(project_root, "local")),
|
|
112
|
-
("Cursor", _cursor_command(project_root)),
|
|
113
185
|
]
|
|
114
186
|
results = []
|
|
115
187
|
for tool, command in commands:
|
|
@@ -117,6 +189,10 @@ def _configure_coding_cli_integrations(project_root: Path, apply: bool, gemini_s
|
|
|
117
189
|
console.print(f"[yellow]{tool} CLI not found on PATH. Skipping optional integration.[/yellow]")
|
|
118
190
|
continue
|
|
119
191
|
results.append(_configure(tool, command, apply))
|
|
192
|
+
results.append(_configure_cursor(project_root, apply))
|
|
193
|
+
results.append(_configure_opencode(project_root, apply))
|
|
194
|
+
results.append(_configure_antigravity(project_root, apply))
|
|
195
|
+
results.append(_configure_warp(project_root, apply))
|
|
120
196
|
_configure_native_hooks(project_root, "all", apply)
|
|
121
197
|
if apply and any(not ok for ok in results):
|
|
122
198
|
raise typer.Exit(code=1)
|
|
@@ -157,9 +233,20 @@ def setup(
|
|
|
157
233
|
apply: bool = typer.Option(False, "--apply", help="Apply integration config instead of previewing commands."),
|
|
158
234
|
gemini_scope: str = typer.Option("project", "--gemini-scope", help="Gemini MCP config scope: project or user."),
|
|
159
235
|
provider: str | None = typer.Option(None, "--provider", help="Set models.provider before configuring the API key."),
|
|
236
|
+
model: str | None = typer.Option(None, "--model", "-m", help="Model id to use for every default role."),
|
|
237
|
+
role_model: list[str] | None = typer.Option(
|
|
238
|
+
None,
|
|
239
|
+
"--role-model",
|
|
240
|
+
help="Per-role model override in ROLE=MODEL form. Can be repeated.",
|
|
241
|
+
),
|
|
160
242
|
api_key: str | None = typer.Option(None, "--api-key", help="Store the configured provider API key in local .devcouncil/secrets.env."),
|
|
243
|
+
vertex_project: str | None = typer.Option(None, "--vertex-project", help="Store VERTEXAI_PROJECT for the vertexai provider."),
|
|
244
|
+
vertex_location: str | None = typer.Option(None, "--vertex-location", help="Store VERTEXAI_LOCATION for the vertexai provider. Defaults to global."),
|
|
161
245
|
skip_api_key: bool = typer.Option(False, "--skip-api-key", help="Skip the first-run model API key prompt."),
|
|
162
246
|
skip_integrations: bool = typer.Option(False, "--skip-integrations", help="Skip the first-run coding CLI integration prompt."),
|
|
247
|
+
skip_map: bool = typer.Option(False, "--skip-map", help="Skip generating repo_map.json and agent guides on init."),
|
|
248
|
+
skip_skills: bool = typer.Option(False, "--skip-skills", help="Skip scaffolding engineering skills into .claude/skills/ on init."),
|
|
249
|
+
scaffold_ci: bool = typer.Option(False, "--scaffold-ci", help="Write a starter .github/workflows/devcouncil.yml from the configured commands."),
|
|
163
250
|
):
|
|
164
251
|
"""
|
|
165
252
|
Initialize DevCouncil from a normal terminal in the target repository root.
|
|
@@ -174,7 +261,44 @@ def setup(
|
|
|
174
261
|
raise typer.Exit(code=2)
|
|
175
262
|
|
|
176
263
|
root = project_root.expanduser().resolve()
|
|
177
|
-
|
|
264
|
+
try:
|
|
265
|
+
role_models = parse_role_model_overrides(role_model)
|
|
266
|
+
initial_provider = validate_model_provider(provider) if provider else "openrouter"
|
|
267
|
+
except ValueError as e:
|
|
268
|
+
console.print(f"[red]{e}[/red]")
|
|
269
|
+
raise typer.Exit(code=2) from e
|
|
270
|
+
|
|
271
|
+
# Size the default local model to the host's memory ceiling (unified RAM on Apple
|
|
272
|
+
# Silicon, VRAM on a discrete GPU, system RAM otherwise) instead of the static 7b,
|
|
273
|
+
# so users on any OS get a capable council out of the box. Only when the user chose
|
|
274
|
+
# Ollama and did not pin a model.
|
|
275
|
+
if initial_provider == "ollama" and model is None:
|
|
276
|
+
from devcouncil import hardware
|
|
277
|
+
|
|
278
|
+
host = hardware.describe_host()
|
|
279
|
+
model = host.recommended_ollama_model
|
|
280
|
+
console.print(
|
|
281
|
+
f"[green]Detected {host.chip_label} ({host.memory_label}); "
|
|
282
|
+
f"defaulting Ollama model to {model}.[/green] "
|
|
283
|
+
"Override with --model, and pull it first: "
|
|
284
|
+
f"[bold]ollama pull {model}[/bold]."
|
|
285
|
+
)
|
|
286
|
+
elif initial_provider == "ollama" and model is not None:
|
|
287
|
+
# Model pinned explicitly — still remind the user it must be pulled locally.
|
|
288
|
+
console.print(
|
|
289
|
+
f"[green]Using Ollama model {model}.[/green] "
|
|
290
|
+
f"Pull it first if needed: [bold]ollama pull {model}[/bold]."
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
created = initialize_project(
|
|
294
|
+
root,
|
|
295
|
+
project_name=name,
|
|
296
|
+
model_provider=initial_provider,
|
|
297
|
+
model=model,
|
|
298
|
+
role_models=role_models,
|
|
299
|
+
with_map=not skip_map,
|
|
300
|
+
with_skills=not skip_skills,
|
|
301
|
+
)
|
|
178
302
|
if not created:
|
|
179
303
|
console.print(f"[yellow]DevCouncil is already initialized at {root / '.devcouncil'}.[/yellow]")
|
|
180
304
|
|
|
@@ -185,11 +309,24 @@ def setup(
|
|
|
185
309
|
console.print(f"[red]{e}[/red]")
|
|
186
310
|
raise typer.Exit(code=2) from e
|
|
187
311
|
|
|
312
|
+
_set_model_roles(root, model=model, role_models=role_models)
|
|
313
|
+
|
|
314
|
+
configured_provider = load_config(root).models.provider
|
|
315
|
+
_configure_vertexai_settings(root, configured_provider, vertex_project, vertex_location)
|
|
188
316
|
_configure_api_key(root, api_key, skip_api_key)
|
|
189
317
|
|
|
190
318
|
console.print()
|
|
191
319
|
render_doctor_check(root)
|
|
192
320
|
|
|
321
|
+
if scaffold_ci:
|
|
322
|
+
from devcouncil.repo.ci_scaffold import WORKFLOW_RELPATH, scaffold_ci as scaffold_ci_workflow
|
|
323
|
+
|
|
324
|
+
written = scaffold_ci_workflow(root)
|
|
325
|
+
if written is None:
|
|
326
|
+
console.print(f"[yellow]{WORKFLOW_RELPATH.as_posix()} already exists; left unchanged.[/yellow]")
|
|
327
|
+
else:
|
|
328
|
+
console.print(f"[green]Wrote starter CI workflow {written.relative_to(root).as_posix()}.[/green]")
|
|
329
|
+
|
|
193
330
|
if integrate:
|
|
194
331
|
_configure_coding_cli_integrations(root, apply=apply, gemini_scope=gemini_scope)
|
|
195
332
|
elif created and not skip_integrations:
|
|
@@ -213,6 +350,8 @@ def setup(
|
|
|
213
350
|
"dev run TASK-001 --executor codex",
|
|
214
351
|
"dev run TASK-001 --executor gemini",
|
|
215
352
|
"dev run TASK-001 --executor claude",
|
|
353
|
+
"dev run TASK-001 --executor opencode",
|
|
354
|
+
"dev run TASK-001 --executor antigravity",
|
|
216
355
|
"dev verify TASK-001",
|
|
217
356
|
"",
|
|
218
357
|
"Use [bold]dev setup --integrate[/bold] to preview coding CLI MCP and native hook setup.",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
|
|
5
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
6
|
+
from devcouncil.execution.shell_session import GuardedShellSession
|
|
7
|
+
from devcouncil.storage.db import get_db
|
|
8
|
+
from devcouncil.storage.repositories import TaskRepository
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def shell(
|
|
14
|
+
task_id: str = typer.Argument(..., help="Task ID"),
|
|
15
|
+
command: str | None = typer.Option(None, "--command", help="Run one guarded command and exit."),
|
|
16
|
+
shell_name: str = typer.Option("auto", "--shell", help="Shell backend: auto, pwsh, bash, zsh."),
|
|
17
|
+
force: bool = typer.Option(False, "--force", help="Reclaim a stale lease from a previous (possibly crashed) session."),
|
|
18
|
+
project_root: Path = typer.Option(Path("."), "--project-root"),
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
Run guarded shell commands for a task.
|
|
22
|
+
"""
|
|
23
|
+
root = project_root.expanduser().resolve()
|
|
24
|
+
initialize_project(root, quiet=True)
|
|
25
|
+
db = get_db(root)
|
|
26
|
+
if not db:
|
|
27
|
+
console.print("[red]DevCouncil not initialized.[/red]")
|
|
28
|
+
raise typer.Exit(code=1)
|
|
29
|
+
|
|
30
|
+
with db.get_session() as session:
|
|
31
|
+
task = TaskRepository(session).get_by_id(task_id)
|
|
32
|
+
if not task:
|
|
33
|
+
console.print(f"[red]Task {task_id} not found.[/red]")
|
|
34
|
+
raise typer.Exit(code=1)
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
session_runner = GuardedShellSession(root, task, shell=shell_name)
|
|
38
|
+
except ValueError as exc:
|
|
39
|
+
console.print(f"[red]{exc}[/red]")
|
|
40
|
+
raise typer.Exit(code=2)
|
|
41
|
+
try:
|
|
42
|
+
session_runner.start(force=force)
|
|
43
|
+
except ValueError as exc:
|
|
44
|
+
# A live or stale lease from a prior session blocks a new one. Don't dump
|
|
45
|
+
# a traceback — tell the user how to recover.
|
|
46
|
+
console.print(f"[red]{exc}[/red]")
|
|
47
|
+
console.print(
|
|
48
|
+
"[yellow]Another shell session may be active for this task. If it is "
|
|
49
|
+
"stale (a previous session crashed), re-run with [bold]--force[/bold] to "
|
|
50
|
+
"reclaim it.[/yellow]"
|
|
51
|
+
)
|
|
52
|
+
raise typer.Exit(code=2)
|
|
53
|
+
try:
|
|
54
|
+
if command:
|
|
55
|
+
code = session_runner.run_one(command)
|
|
56
|
+
raise typer.Exit(code=code)
|
|
57
|
+
|
|
58
|
+
console.print(f"[cyan]Guarded shell for {task_id}. Type exit or quit to end.[/cyan]")
|
|
59
|
+
while True:
|
|
60
|
+
try:
|
|
61
|
+
line = input(f"devcouncil:{task_id}> ")
|
|
62
|
+
except (EOFError, KeyboardInterrupt):
|
|
63
|
+
break
|
|
64
|
+
normalized = line.strip()
|
|
65
|
+
if normalized.lower() in {"exit", "quit"}:
|
|
66
|
+
break
|
|
67
|
+
if not normalized:
|
|
68
|
+
continue
|
|
69
|
+
code = session_runner.run_one(normalized)
|
|
70
|
+
if code != 0:
|
|
71
|
+
console.print(f"[yellow]Command exited with {code}[/yellow]")
|
|
72
|
+
finally:
|
|
73
|
+
session_runner.finish()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from devcouncil.skills.registry import get_skill, load_skills, scaffold_skills, select_skills
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(help="Inspect and scaffold DevCouncil engineering skills for coding agents.")
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _is_repo_skill(skill, project_root: Path) -> bool:
|
|
14
|
+
if skill.source_path is None:
|
|
15
|
+
return False
|
|
16
|
+
try:
|
|
17
|
+
skill.source_path.resolve().relative_to(project_root.resolve())
|
|
18
|
+
return True
|
|
19
|
+
except ValueError:
|
|
20
|
+
return False
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.callback(invoke_without_command=True)
|
|
24
|
+
def skills(
|
|
25
|
+
ctx: typer.Context,
|
|
26
|
+
goal: str = typer.Option("", "--goal", help="Optional goal text; highlights the skills that would apply."),
|
|
27
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root used for file-based skill triggers."),
|
|
28
|
+
):
|
|
29
|
+
"""List available skills and show which apply to this repository/goal."""
|
|
30
|
+
if ctx.invoked_subcommand is not None:
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
root = project_root.expanduser().resolve()
|
|
34
|
+
all_skills = load_skills(project_root=root)
|
|
35
|
+
if not all_skills:
|
|
36
|
+
console.print("[yellow]No skills found in the DevCouncil skills library.[/yellow]")
|
|
37
|
+
raise typer.Exit()
|
|
38
|
+
|
|
39
|
+
selected = {skill.name for skill in select_skills(goal, root)}
|
|
40
|
+
table = Table(title="DevCouncil Skills")
|
|
41
|
+
table.add_column("Skill", style="cyan")
|
|
42
|
+
table.add_column("Source", justify="center")
|
|
43
|
+
table.add_column("Applies", justify="center")
|
|
44
|
+
table.add_column("Description")
|
|
45
|
+
for skill in all_skills:
|
|
46
|
+
applies = "always" if skill.always else ("yes" if skill.name in selected else "-")
|
|
47
|
+
style = "green" if skill.name in selected else "dim"
|
|
48
|
+
source = "repo" if _is_repo_skill(skill, root) else "library"
|
|
49
|
+
table.add_row(skill.name, source, f"[{style}]{applies}[/{style}]", skill.description)
|
|
50
|
+
console.print(table)
|
|
51
|
+
console.print(
|
|
52
|
+
"\nScaffold the applicable skills into this repo with: "
|
|
53
|
+
"[bold]dev skills scaffold[/bold] (add a goal to widen selection, or --all)."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@app.command("show")
|
|
58
|
+
def show(
|
|
59
|
+
name: str = typer.Argument(..., help="Skill name, e.g. core-engineering or android."),
|
|
60
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root, so repo-local skills are found too."),
|
|
61
|
+
):
|
|
62
|
+
"""Print the full body of a single skill."""
|
|
63
|
+
skill = get_skill(name, project_root=project_root.expanduser().resolve())
|
|
64
|
+
if skill is None:
|
|
65
|
+
console.print(f"[red]No skill named '{name}'. Run 'dev skills' to list available skills.[/red]")
|
|
66
|
+
raise typer.Exit(code=1)
|
|
67
|
+
console.print(skill.to_skill_md())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.command("scaffold")
|
|
71
|
+
def scaffold(
|
|
72
|
+
goal: str = typer.Argument("", help="Optional goal text used to widen domain-skill selection."),
|
|
73
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root to scaffold skills into."),
|
|
74
|
+
all_skills: bool = typer.Option(False, "--all", help="Scaffold every skill, not just the ones that apply."),
|
|
75
|
+
):
|
|
76
|
+
"""Write the applicable skills into <repo>/.claude/skills/<name>/SKILL.md."""
|
|
77
|
+
root = project_root.expanduser().resolve()
|
|
78
|
+
chosen = load_skills(project_root=root) if all_skills else select_skills(goal, root)
|
|
79
|
+
written = scaffold_skills(root, chosen)
|
|
80
|
+
if not written:
|
|
81
|
+
console.print(
|
|
82
|
+
f"[green]Skills already up to date in {root / '.claude' / 'skills'} "
|
|
83
|
+
f"({len(chosen)} applicable).[/green]"
|
|
84
|
+
)
|
|
85
|
+
return
|
|
86
|
+
console.print(f"[green]Wrote {len(written)} skill file(s):[/green]")
|
|
87
|
+
for path in written:
|
|
88
|
+
console.print(f" {path.relative_to(root).as_posix()}")
|
|
@@ -8,7 +8,7 @@ from devcouncil.cli.commands.init import initialize_project
|
|
|
8
8
|
from devcouncil.app.project_status import compute_phase
|
|
9
9
|
from devcouncil.storage.db import get_db
|
|
10
10
|
from devcouncil.storage.repositories import ArtifactGraphRepository, StateRepository
|
|
11
|
-
from devcouncil.telemetry.cost import CostEstimator
|
|
11
|
+
from devcouncil.telemetry.cost import CostEstimator, cost_by_task
|
|
12
12
|
from devcouncil.live.summary import live_review_summary
|
|
13
13
|
|
|
14
14
|
console = Console()
|
|
@@ -51,6 +51,7 @@ def _status_payload(project_root: Path) -> dict:
|
|
|
51
51
|
"phase": phase,
|
|
52
52
|
"coverage_summary": summary,
|
|
53
53
|
"total_cost": total_cost,
|
|
54
|
+
"cost_by_task": cost_by_task(project_root),
|
|
54
55
|
"task_status_counts": status_counts,
|
|
55
56
|
"blocking_gaps": [gap.model_dump() for gap in blocking_gaps],
|
|
56
57
|
"live_review": live_review_summary(project_root),
|
|
@@ -59,6 +60,11 @@ def _status_payload(project_root: Path) -> dict:
|
|
|
59
60
|
|
|
60
61
|
def status(
|
|
61
62
|
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
63
|
+
fail_on_blocking: bool = typer.Option(
|
|
64
|
+
False,
|
|
65
|
+
"--fail-on-blocking",
|
|
66
|
+
help="Exit non-zero when blocking gaps remain, so shell-driven agents can gate on $?.",
|
|
67
|
+
),
|
|
62
68
|
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
63
69
|
):
|
|
64
70
|
"""
|
|
@@ -66,8 +72,14 @@ def status(
|
|
|
66
72
|
"""
|
|
67
73
|
root = project_root.expanduser().resolve()
|
|
68
74
|
payload = _status_payload(root)
|
|
75
|
+
|
|
76
|
+
def _maybe_fail() -> None:
|
|
77
|
+
if fail_on_blocking and payload.get("blocking_gaps"):
|
|
78
|
+
raise typer.Exit(code=1)
|
|
79
|
+
|
|
69
80
|
if json_format:
|
|
70
81
|
typer.echo(json.dumps(payload, indent=2))
|
|
82
|
+
_maybe_fail()
|
|
71
83
|
return
|
|
72
84
|
|
|
73
85
|
if not payload["initialized"]:
|
|
@@ -108,6 +120,16 @@ def status(
|
|
|
108
120
|
table.add_row(state, str(count))
|
|
109
121
|
console.print(table)
|
|
110
122
|
|
|
123
|
+
cost_groups = payload.get("cost_by_task") or {}
|
|
124
|
+
if cost_groups:
|
|
125
|
+
cost_table = Table(title="Cost by Task")
|
|
126
|
+
cost_table.add_column("Task", style="cyan")
|
|
127
|
+
cost_table.add_column("Cost ($)", justify="right")
|
|
128
|
+
cost_table.add_column("Calls", justify="right")
|
|
129
|
+
for name, stats in sorted(cost_groups.items(), key=lambda kv: kv[1]["cost"], reverse=True):
|
|
130
|
+
cost_table.add_row(name, f"{stats['cost']:.4f}", str(stats["calls"]))
|
|
131
|
+
console.print(cost_table)
|
|
132
|
+
|
|
111
133
|
blocking_gaps = payload["blocking_gaps"]
|
|
112
134
|
if blocking_gaps:
|
|
113
135
|
console.print(f"\n[red bold]WARNING: {len(blocking_gaps)} blocking gap(s) must be resolved:[/red bold]")
|
|
@@ -115,3 +137,5 @@ def status(
|
|
|
115
137
|
console.print(f" - [red]{gap['id']}[/red]: {gap['description'][:80]}")
|
|
116
138
|
if len(blocking_gaps) > 5:
|
|
117
139
|
console.print(f" ... and {len(blocking_gaps) - 5} more. Run [bold]dev report[/bold] for details.")
|
|
140
|
+
|
|
141
|
+
_maybe_fail()
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import time
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
4
5
|
|
|
5
6
|
import typer
|
|
6
7
|
from rich.console import Console
|
|
7
8
|
|
|
8
|
-
from devcouncil.telemetry.traces import read_trace_events
|
|
9
|
+
from devcouncil.telemetry.traces import read_trace_events, read_trace_events_since
|
|
9
10
|
|
|
10
11
|
app = typer.Typer(help="Inspect DevCouncil trace events.")
|
|
11
12
|
console = Console()
|
|
@@ -13,13 +14,56 @@ console = Console()
|
|
|
13
14
|
|
|
14
15
|
@app.command("tail")
|
|
15
16
|
def tail(
|
|
16
|
-
follow: bool = typer.Option(
|
|
17
|
+
follow: bool = typer.Option(
|
|
18
|
+
False, "--follow/--no-follow", "-f", help="Continue polling for new events (default is a single shot)."
|
|
19
|
+
),
|
|
17
20
|
limit: int = typer.Option(50, "--limit", "-n", help="Maximum events to print before following."),
|
|
18
21
|
jsonl: bool = typer.Option(True, "--jsonl/--pretty", help="Print JSONL or compact text rows."),
|
|
22
|
+
since: Optional[int] = typer.Option(
|
|
23
|
+
None,
|
|
24
|
+
"--since",
|
|
25
|
+
help="Byte-offset cursor from a previous run; emit only events after it (stateless incremental polling).",
|
|
26
|
+
),
|
|
27
|
+
json_summary: bool = typer.Option(
|
|
28
|
+
False,
|
|
29
|
+
"--json",
|
|
30
|
+
help="Emit a single {events, next_cursor} JSON object (incremental mode). Implies --no-follow.",
|
|
31
|
+
),
|
|
19
32
|
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
20
33
|
):
|
|
21
|
-
"""Print the DevCouncil trace JSONL stream for replay or debugging.
|
|
34
|
+
"""Print the DevCouncil trace JSONL stream for replay or debugging.
|
|
35
|
+
|
|
36
|
+
With ``--since <cursor> --no-follow`` (or ``--json``) a single-shot supervising
|
|
37
|
+
agent gets only the events appended after the cursor plus a ``next_cursor`` to
|
|
38
|
+
pass back on the next poll, so each poll is O(new) rather than O(all).
|
|
39
|
+
"""
|
|
22
40
|
project_root = project_root.expanduser().resolve()
|
|
41
|
+
|
|
42
|
+
# Incremental cursor mode: any of --since / --json / explicit --no-follow.
|
|
43
|
+
incremental = since is not None or json_summary
|
|
44
|
+
if incremental:
|
|
45
|
+
events, next_cursor = read_trace_events_since(project_root, since)
|
|
46
|
+
if json_summary:
|
|
47
|
+
typer.echo(
|
|
48
|
+
json.dumps(
|
|
49
|
+
{
|
|
50
|
+
"events": [event.model_dump(by_alias=True) for event in events],
|
|
51
|
+
"next_cursor": next_cursor,
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
return
|
|
56
|
+
for event in events:
|
|
57
|
+
if jsonl:
|
|
58
|
+
typer.echo(event.model_dump_json())
|
|
59
|
+
else:
|
|
60
|
+
console.print(
|
|
61
|
+
f"{event.timestamp} {event.type} "
|
|
62
|
+
f"{event.task_id or '-'} {event.summary or json.dumps(event.details)}"
|
|
63
|
+
)
|
|
64
|
+
console.print(f"[dim]next_cursor: {next_cursor}[/dim]")
|
|
65
|
+
return
|
|
66
|
+
|
|
23
67
|
printed = 0
|
|
24
68
|
|
|
25
69
|
def emit_new(start_index: int) -> int:
|