devcouncil 0.1.0 → 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/LICENSE +201 -201
- package/README.md +197 -494
- package/package.json +9 -2
- package/pyproject.toml +62 -27
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +297 -108
- package/src/devcouncil/app/errors.py +23 -23
- package/src/devcouncil/app/events.py +44 -44
- package/src/devcouncil/app/orchestrator.py +67 -67
- package/src/devcouncil/app/project_status.py +29 -0
- package/src/devcouncil/app/run_context.py +39 -39
- package/src/devcouncil/app/state_machine.py +108 -108
- package/src/devcouncil/artifacts/__init__.py +1 -1
- package/src/devcouncil/artifacts/coverage.py +96 -96
- package/src/devcouncil/artifacts/graph.py +163 -143
- package/src/devcouncil/artifacts/migrations.py +20 -20
- package/src/devcouncil/artifacts/schemas.py +23 -23
- package/src/devcouncil/artifacts/serializer.py +21 -21
- package/src/devcouncil/artifacts/validators.py +27 -27
- 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 +54 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/check.py +209 -0
- package/src/devcouncil/cli/commands/config.py +115 -54
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +31 -0
- package/src/devcouncil/cli/commands/doctor.py +291 -47
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +656 -0
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +209 -33
- package/src/devcouncil/cli/commands/init.py +204 -57
- package/src/devcouncil/cli/commands/integrate.py +1171 -76
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +96 -22
- package/src/devcouncil/cli/commands/plan.py +422 -210
- package/src/devcouncil/cli/commands/prompt.py +48 -34
- package/src/devcouncil/cli/commands/repair.py +89 -69
- package/src/devcouncil/cli/commands/report.py +120 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
- package/src/devcouncil/cli/commands/rollback.py +55 -54
- package/src/devcouncil/cli/commands/run.py +285 -220
- 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 +300 -20
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/skills.py +88 -0
- package/src/devcouncil/cli/commands/status.py +141 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +49 -4
- package/src/devcouncil/cli/commands/verify.py +293 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +92 -25
- package/src/devcouncil/council/prompts/arbiter.md +19 -19
- package/src/devcouncil/council/prompts/critic_a.md +10 -10
- package/src/devcouncil/council/prompts/critic_b.md +10 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
- package/src/devcouncil/council/prompts/planner_a.md +16 -16
- package/src/devcouncil/council/prompts/planner_b.md +16 -16
- package/src/devcouncil/council/prompts/rebuttal.md +10 -10
- package/src/devcouncil/council/prompts/spec_writer.md +12 -12
- package/src/devcouncil/domain/assumption.py +17 -17
- package/src/devcouncil/domain/critique.py +32 -32
- package/src/devcouncil/domain/evidence.py +47 -27
- package/src/devcouncil/domain/gap.py +52 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +55 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/checkpoints.py +246 -0
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/fs_watcher.py +180 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +186 -77
- package/src/devcouncil/execution/patch.py +77 -28
- package/src/devcouncil/execution/permissions.py +52 -59
- package/src/devcouncil/execution/policy_engine.py +343 -0
- package/src/devcouncil/execution/prompt_builder.py +650 -38
- package/src/devcouncil/execution/shell_session.py +225 -0
- package/src/devcouncil/execution/task_runner.py +68 -64
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/agent_registry.py +575 -0
- package/src/devcouncil/executors/coding_cli.py +736 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +186 -85
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +52 -45
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
- package/src/devcouncil/gating/policy.py +315 -167
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/__init__.py +1 -1
- package/src/devcouncil/indexing/ast_matcher.py +168 -0
- package/src/devcouncil/indexing/graph_index.py +48 -48
- package/src/devcouncil/indexing/lsp.py +161 -0
- package/src/devcouncil/indexing/repo_mapper.py +1455 -204
- 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.py +35 -35
- package/src/devcouncil/integrations/github_intent.py +142 -0
- package/src/devcouncil/integrations/gitnexus.py +62 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +2072 -96
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +349 -0
- package/src/devcouncil/live/models.py +63 -0
- package/src/devcouncil/live/repair_prompt.py +83 -0
- package/src/devcouncil/live/reviewer.py +70 -0
- package/src/devcouncil/live/signals.py +135 -0
- package/src/devcouncil/live/summary.py +34 -0
- package/src/devcouncil/live/tasks.py +18 -0
- package/src/devcouncil/live/transcripts.py +141 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +42 -38
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +627 -125
- package/src/devcouncil/llm/router.py +303 -118
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/correction_manifest.py +303 -0
- package/src/devcouncil/planning/critique_service.py +71 -66
- package/src/devcouncil/planning/plan_service.py +60 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +70 -44
- 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/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +30 -17
- package/src/devcouncil/reporting/markdown_report.py +83 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- 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 +147 -66
- package/src/devcouncil/storage/models.py +204 -83
- package/src/devcouncil/storage/native.py +557 -0
- package/src/devcouncil/storage/repositories.py +388 -249
- package/src/devcouncil/telemetry/cost.py +140 -34
- 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 +52 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +423 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +147 -141
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/__init__.py +1 -1
- 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/implementation_reviewer.py +55 -55
- 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 +1342 -307
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -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()
|
|
@@ -1,57 +1,76 @@
|
|
|
1
|
-
import
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
""
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
8
|
+
from devcouncil.storage.db import get_db
|
|
9
|
+
from devcouncil.storage.repositories import TaskRepository, RequirementRepository
|
|
10
|
+
|
|
11
|
+
app = typer.Typer()
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
@app.callback(invoke_without_command=True)
|
|
15
|
+
def show(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
task_id: str = typer.Argument(..., help="ID of the task to show"),
|
|
18
|
+
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
19
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
20
|
+
):
|
|
21
|
+
"""
|
|
22
|
+
Show details of a specific task.
|
|
23
|
+
"""
|
|
24
|
+
if ctx.invoked_subcommand is not None:
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
root = project_root.expanduser().resolve()
|
|
28
|
+
initialize_project(root, quiet=True)
|
|
29
|
+
db = get_db(root)
|
|
30
|
+
if not db:
|
|
31
|
+
raise typer.Exit(code=1)
|
|
32
|
+
|
|
33
|
+
with db.get_session() as session:
|
|
34
|
+
task_repo = TaskRepository(session)
|
|
35
|
+
req_repo = RequirementRepository(session)
|
|
36
|
+
|
|
37
|
+
task = task_repo.get_by_id(task_id)
|
|
38
|
+
if not task:
|
|
39
|
+
console.print(f"[red]Task {task_id} not found.[/red]")
|
|
40
|
+
raise typer.Exit(code=1)
|
|
41
|
+
|
|
42
|
+
reqs = req_repo.get_all()
|
|
43
|
+
req_map = {r.id: r for r in reqs}
|
|
44
|
+
|
|
45
|
+
if json_format:
|
|
46
|
+
linked_requirements = [
|
|
47
|
+
req_map[req_id].model_dump()
|
|
48
|
+
for req_id in task.requirement_ids
|
|
49
|
+
if req_id in req_map
|
|
50
|
+
]
|
|
51
|
+
typer.echo(json.dumps({
|
|
52
|
+
"task": task.model_dump(),
|
|
53
|
+
"linked_requirements": linked_requirements,
|
|
54
|
+
}, indent=2))
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
output = f"[bold]Status:[/bold] {task.status}\n\n"
|
|
58
|
+
output += f"[bold]Description:[/bold]\n{task.description}\n\n"
|
|
59
|
+
|
|
60
|
+
output += "[bold]Linked Requirements:[/bold]\n"
|
|
61
|
+
for req_id in task.requirement_ids:
|
|
62
|
+
req = req_map.get(req_id)
|
|
63
|
+
if req:
|
|
64
|
+
output += f" - [cyan]{req.id}[/cyan]: {req.title}\n"
|
|
65
|
+
else:
|
|
66
|
+
output += f" - [cyan]{req_id}[/cyan]: (Requirement not found)\n"
|
|
67
|
+
|
|
68
|
+
output += "\n[bold]Planned Files:[/bold]\n"
|
|
69
|
+
for pf in task.planned_files:
|
|
70
|
+
output += f" - {pf.path} ({pf.allowed_change}): {pf.reason}\n"
|
|
71
|
+
|
|
72
|
+
output += "\n[bold]Expected Tests:[/bold]\n"
|
|
73
|
+
for et in task.expected_tests:
|
|
74
|
+
output += f" - {et}\n"
|
|
75
|
+
|
|
76
|
+
console.print(Panel(output, title=f"Task {task.id}: {task.title}", expand=False))
|
|
@@ -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()}")
|
|
@@ -1,105 +1,141 @@
|
|
|
1
|
-
from rich.console import Console
|
|
2
|
-
from rich.table import Table
|
|
3
|
-
from rich.panel import Panel
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
import json
|
|
6
|
-
|
|
7
|
-
from devcouncil.
|
|
8
|
-
from devcouncil.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
with db.get_session() as session:
|
|
23
|
-
graph_repo = ArtifactGraphRepository(session)
|
|
24
|
-
graph = graph_repo.load_graph()
|
|
25
|
-
summary = graph.coverage_summary()
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
1
|
+
from rich.console import Console
|
|
2
|
+
from rich.table import Table
|
|
3
|
+
from rich.panel import Panel
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import json
|
|
6
|
+
import typer
|
|
7
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
8
|
+
from devcouncil.app.project_status import compute_phase
|
|
9
|
+
from devcouncil.storage.db import get_db
|
|
10
|
+
from devcouncil.storage.repositories import ArtifactGraphRepository, StateRepository
|
|
11
|
+
from devcouncil.telemetry.cost import CostEstimator, cost_by_task
|
|
12
|
+
from devcouncil.live.summary import live_review_summary
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
def _status_payload(project_root: Path) -> dict:
|
|
17
|
+
initialize_project(project_root, quiet=True)
|
|
18
|
+
db = get_db(project_root)
|
|
19
|
+
if not db:
|
|
20
|
+
return {"initialized": False, "phase": "UNINITIALIZED"}
|
|
21
|
+
|
|
22
|
+
with db.get_session() as session:
|
|
23
|
+
graph_repo = ArtifactGraphRepository(session)
|
|
24
|
+
graph = graph_repo.load_graph()
|
|
25
|
+
summary = graph.coverage_summary()
|
|
26
|
+
|
|
27
|
+
blocking_gaps = graph.blocking_gaps()
|
|
28
|
+
state = StateRepository(session).get_state()
|
|
29
|
+
phase = compute_phase(graph, state.current_phase if state else None)
|
|
30
|
+
|
|
31
|
+
total_cost = 0.0
|
|
32
|
+
log_file = project_root / ".devcouncil" / "logs" / "model_calls.jsonl"
|
|
33
|
+
if log_file.exists():
|
|
34
|
+
with open(log_file, "r", encoding="utf-8") as f:
|
|
35
|
+
for line in f:
|
|
36
|
+
try:
|
|
37
|
+
entry = json.loads(line)
|
|
38
|
+
total_cost += CostEstimator.estimate_cost(
|
|
39
|
+
entry.get("response", {}).get("model", ""),
|
|
40
|
+
entry.get("usage", {}),
|
|
41
|
+
)
|
|
42
|
+
except Exception:
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
status_counts: dict[str, int] = {}
|
|
46
|
+
for task in graph.tasks.values():
|
|
47
|
+
status_counts[task.status] = status_counts.get(task.status, 0) + 1
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
"initialized": True,
|
|
51
|
+
"phase": phase,
|
|
52
|
+
"coverage_summary": summary,
|
|
53
|
+
"total_cost": total_cost,
|
|
54
|
+
"cost_by_task": cost_by_task(project_root),
|
|
55
|
+
"task_status_counts": status_counts,
|
|
56
|
+
"blocking_gaps": [gap.model_dump() for gap in blocking_gaps],
|
|
57
|
+
"live_review": live_review_summary(project_root),
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def status(
|
|
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
|
+
),
|
|
68
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
69
|
+
):
|
|
70
|
+
"""
|
|
71
|
+
Show the current status of the DevCouncil project.
|
|
72
|
+
"""
|
|
73
|
+
root = project_root.expanduser().resolve()
|
|
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
|
+
|
|
80
|
+
if json_format:
|
|
81
|
+
typer.echo(json.dumps(payload, indent=2))
|
|
82
|
+
_maybe_fail()
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
if not payload["initialized"]:
|
|
86
|
+
console.print("[yellow]DevCouncil state is not available in this directory.[/yellow]")
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
summary = payload["coverage_summary"]
|
|
90
|
+
phase = payload["phase"]
|
|
91
|
+
phase_colors = {
|
|
92
|
+
"NEW": "yellow",
|
|
93
|
+
"REQUIREMENTS_DRAFTED": "cyan",
|
|
94
|
+
"PLAN_APPROVED": "green",
|
|
95
|
+
"TASK_EXECUTING": "blue",
|
|
96
|
+
"TASK_BLOCKED": "red",
|
|
97
|
+
"PROJECT_DONE": "green bold",
|
|
98
|
+
}
|
|
99
|
+
phase_color = phase_colors.get(phase, "white")
|
|
100
|
+
|
|
101
|
+
console.print(Panel(
|
|
102
|
+
f"[bold]Phase:[/bold] [{phase_color}]{phase}[/{phase_color}]\n"
|
|
103
|
+
f"[bold]Requirements:[/bold] {summary['total_requirements']} ({summary['requirements_without_tasks']} unmapped)\n"
|
|
104
|
+
f"[bold]Tasks:[/bold] {summary['total_tasks']} ({summary['tasks_without_requirements']} orphaned)\n"
|
|
105
|
+
f"[bold]Acceptance Criteria:[/bold] {summary['total_ac']} ({summary['ac_without_evidence']} unverified)\n"
|
|
106
|
+
f"[bold]Gaps:[/bold] {summary['total_gaps']} ({summary['blocking_gaps']} blocking)\n"
|
|
107
|
+
f"[bold]Live Review:[/bold] {payload['live_review']['cards']['critical_open']} open critical, "
|
|
108
|
+
f"{len(payload['live_review']['blocking_cards'])} blocking in scope, "
|
|
109
|
+
f"{payload['live_review']['pending_signals']} pending signal(s)\n"
|
|
110
|
+
f"[bold]Total Cost:[/bold] ${payload['total_cost']:.4f}",
|
|
111
|
+
title="DevCouncil Status",
|
|
112
|
+
expand=False,
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
if payload["task_status_counts"]:
|
|
116
|
+
table = Table(title="Task Summary")
|
|
117
|
+
table.add_column("Status", style="magenta")
|
|
118
|
+
table.add_column("Count", justify="right")
|
|
119
|
+
for state, count in sorted(payload["task_status_counts"].items()):
|
|
120
|
+
table.add_row(state, str(count))
|
|
121
|
+
console.print(table)
|
|
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
|
+
|
|
133
|
+
blocking_gaps = payload["blocking_gaps"]
|
|
134
|
+
if blocking_gaps:
|
|
135
|
+
console.print(f"\n[red bold]WARNING: {len(blocking_gaps)} blocking gap(s) must be resolved:[/red bold]")
|
|
136
|
+
for gap in blocking_gaps[:5]:
|
|
137
|
+
console.print(f" - [red]{gap['id']}[/red]: {gap['description'][:80]}")
|
|
138
|
+
if len(blocking_gaps) > 5:
|
|
139
|
+
console.print(f" ... and {len(blocking_gaps) - 5} more. Run [bold]dev report[/bold] for details.")
|
|
140
|
+
|
|
141
|
+
_maybe_fail()
|
|
@@ -1,41 +1,55 @@
|
|
|
1
|
-
import
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
from devcouncil.cli.commands.init import initialize_project
|
|
8
|
+
from devcouncil.storage.db import get_db
|
|
9
|
+
from devcouncil.storage.repositories import TaskRepository
|
|
10
|
+
|
|
11
|
+
app = typer.Typer()
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
@app.callback(invoke_without_command=True)
|
|
15
|
+
def tasks(
|
|
16
|
+
ctx: typer.Context,
|
|
17
|
+
json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
|
|
18
|
+
project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
|
|
19
|
+
):
|
|
20
|
+
"""
|
|
21
|
+
List task graph and task gate status.
|
|
22
|
+
"""
|
|
23
|
+
if ctx.invoked_subcommand is not None:
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
root = project_root.expanduser().resolve()
|
|
27
|
+
initialize_project(root, quiet=True)
|
|
28
|
+
db = get_db(root)
|
|
29
|
+
if not db:
|
|
30
|
+
console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
|
|
31
|
+
raise typer.Exit(code=1)
|
|
32
|
+
|
|
33
|
+
with db.get_session() as session:
|
|
34
|
+
task_repo = TaskRepository(session)
|
|
35
|
+
tasks_list = task_repo.get_all()
|
|
36
|
+
|
|
37
|
+
if json_format:
|
|
38
|
+
typer.echo(json.dumps({"tasks": [task.model_dump() for task in tasks_list]}, indent=2))
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
if not tasks_list:
|
|
42
|
+
console.print("No tasks found. Run 'dev plan' to generate tasks.")
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
table = Table(title="DevCouncil Tasks")
|
|
46
|
+
table.add_column("Task ID", style="cyan", no_wrap=True)
|
|
47
|
+
table.add_column("Title", style="white")
|
|
48
|
+
table.add_column("Status", style="magenta")
|
|
49
|
+
table.add_column("Linked Reqs", style="green")
|
|
50
|
+
|
|
51
|
+
for t in tasks_list:
|
|
52
|
+
reqs = ", ".join(t.requirement_ids)
|
|
53
|
+
table.add_row(t.id, t.title, t.status, reqs)
|
|
54
|
+
|
|
55
|
+
console.print(table)
|