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.
Files changed (190) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +197 -494
  3. package/package.json +9 -2
  4. package/pyproject.toml +62 -27
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +297 -108
  8. package/src/devcouncil/app/errors.py +23 -23
  9. package/src/devcouncil/app/events.py +44 -44
  10. package/src/devcouncil/app/orchestrator.py +67 -67
  11. package/src/devcouncil/app/project_status.py +29 -0
  12. package/src/devcouncil/app/run_context.py +39 -39
  13. package/src/devcouncil/app/state_machine.py +108 -108
  14. package/src/devcouncil/artifacts/__init__.py +1 -1
  15. package/src/devcouncil/artifacts/coverage.py +96 -96
  16. package/src/devcouncil/artifacts/graph.py +163 -143
  17. package/src/devcouncil/artifacts/migrations.py +20 -20
  18. package/src/devcouncil/artifacts/schemas.py +23 -23
  19. package/src/devcouncil/artifacts/serializer.py +21 -21
  20. package/src/devcouncil/artifacts/validators.py +27 -27
  21. package/src/devcouncil/assets/__init__.py +1 -0
  22. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  23. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  24. package/src/devcouncil/cli/commands/agents.py +292 -0
  25. package/src/devcouncil/cli/commands/artifacts.py +54 -48
  26. package/src/devcouncil/cli/commands/ast.py +22 -0
  27. package/src/devcouncil/cli/commands/baseline.py +35 -32
  28. package/src/devcouncil/cli/commands/check.py +209 -0
  29. package/src/devcouncil/cli/commands/config.py +115 -54
  30. package/src/devcouncil/cli/commands/cost.py +57 -0
  31. package/src/devcouncil/cli/commands/dashboard.py +31 -0
  32. package/src/devcouncil/cli/commands/doctor.py +291 -47
  33. package/src/devcouncil/cli/commands/evidence.py +48 -0
  34. package/src/devcouncil/cli/commands/go.py +656 -0
  35. package/src/devcouncil/cli/commands/handoff.py +69 -0
  36. package/src/devcouncil/cli/commands/hook.py +209 -33
  37. package/src/devcouncil/cli/commands/init.py +204 -57
  38. package/src/devcouncil/cli/commands/integrate.py +1171 -76
  39. package/src/devcouncil/cli/commands/lsp.py +20 -0
  40. package/src/devcouncil/cli/commands/map.py +96 -22
  41. package/src/devcouncil/cli/commands/plan.py +422 -210
  42. package/src/devcouncil/cli/commands/prompt.py +48 -34
  43. package/src/devcouncil/cli/commands/repair.py +89 -69
  44. package/src/devcouncil/cli/commands/report.py +120 -54
  45. package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
  46. package/src/devcouncil/cli/commands/rollback.py +55 -54
  47. package/src/devcouncil/cli/commands/run.py +285 -220
  48. package/src/devcouncil/cli/commands/runs.py +223 -0
  49. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  50. package/src/devcouncil/cli/commands/semantic.py +47 -0
  51. package/src/devcouncil/cli/commands/setup.py +300 -20
  52. package/src/devcouncil/cli/commands/shell.py +73 -0
  53. package/src/devcouncil/cli/commands/show.py +76 -57
  54. package/src/devcouncil/cli/commands/skills.py +88 -0
  55. package/src/devcouncil/cli/commands/status.py +141 -105
  56. package/src/devcouncil/cli/commands/tasks.py +55 -41
  57. package/src/devcouncil/cli/commands/trace.py +49 -4
  58. package/src/devcouncil/cli/commands/verify.py +293 -128
  59. package/src/devcouncil/cli/commands/version.py +20 -20
  60. package/src/devcouncil/cli/commands/watch.py +574 -0
  61. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  62. package/src/devcouncil/cli/main.py +92 -25
  63. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  64. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  65. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  66. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  67. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  68. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  69. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  70. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  71. package/src/devcouncil/domain/assumption.py +17 -17
  72. package/src/devcouncil/domain/critique.py +32 -32
  73. package/src/devcouncil/domain/evidence.py +47 -27
  74. package/src/devcouncil/domain/gap.py +52 -26
  75. package/src/devcouncil/domain/requirement.py +22 -22
  76. package/src/devcouncil/domain/task.py +55 -26
  77. package/src/devcouncil/execution/__init__.py +1 -1
  78. package/src/devcouncil/execution/checkpoints.py +246 -0
  79. package/src/devcouncil/execution/context_builder.py +54 -54
  80. package/src/devcouncil/execution/executor.py +15 -15
  81. package/src/devcouncil/execution/fs_watcher.py +180 -0
  82. package/src/devcouncil/execution/handoff.py +102 -0
  83. package/src/devcouncil/execution/hook_policy.py +186 -77
  84. package/src/devcouncil/execution/patch.py +77 -28
  85. package/src/devcouncil/execution/permissions.py +52 -59
  86. package/src/devcouncil/execution/policy_engine.py +343 -0
  87. package/src/devcouncil/execution/prompt_builder.py +650 -38
  88. package/src/devcouncil/execution/shell_session.py +225 -0
  89. package/src/devcouncil/execution/task_runner.py +68 -64
  90. package/src/devcouncil/executors/__init__.py +1 -1
  91. package/src/devcouncil/executors/agent_registry.py +575 -0
  92. package/src/devcouncil/executors/coding_cli.py +736 -0
  93. package/src/devcouncil/executors/mini_swe.py +63 -63
  94. package/src/devcouncil/executors/native/agent.py +186 -85
  95. package/src/devcouncil/executors/openhands.py +56 -56
  96. package/src/devcouncil/gating/__init__.py +1 -1
  97. package/src/devcouncil/gating/checks/clean_git.py +52 -45
  98. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  99. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  100. package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
  101. package/src/devcouncil/gating/policy.py +315 -167
  102. package/src/devcouncil/hardware.py +184 -0
  103. package/src/devcouncil/indexing/__init__.py +1 -1
  104. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  105. package/src/devcouncil/indexing/graph_index.py +48 -48
  106. package/src/devcouncil/indexing/lsp.py +161 -0
  107. package/src/devcouncil/indexing/repo_mapper.py +1455 -204
  108. package/src/devcouncil/indexing/semantic_index.py +205 -0
  109. package/src/devcouncil/integrations/actions.py +146 -0
  110. package/src/devcouncil/integrations/check.py +423 -0
  111. package/src/devcouncil/integrations/github.py +35 -35
  112. package/src/devcouncil/integrations/github_intent.py +142 -0
  113. package/src/devcouncil/integrations/gitnexus.py +62 -27
  114. package/src/devcouncil/integrations/graphify.py +34 -34
  115. package/src/devcouncil/integrations/mcp/server.py +2072 -96
  116. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  117. package/src/devcouncil/integrations/pr_comments.py +62 -0
  118. package/src/devcouncil/live/__init__.py +2 -0
  119. package/src/devcouncil/live/cards.py +349 -0
  120. package/src/devcouncil/live/models.py +63 -0
  121. package/src/devcouncil/live/repair_prompt.py +83 -0
  122. package/src/devcouncil/live/reviewer.py +70 -0
  123. package/src/devcouncil/live/signals.py +135 -0
  124. package/src/devcouncil/live/summary.py +34 -0
  125. package/src/devcouncil/live/tasks.py +18 -0
  126. package/src/devcouncil/live/transcripts.py +141 -0
  127. package/src/devcouncil/llm/__init__.py +1 -1
  128. package/src/devcouncil/llm/cache.py +42 -38
  129. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  130. package/src/devcouncil/llm/provider.py +627 -125
  131. package/src/devcouncil/llm/router.py +303 -118
  132. package/src/devcouncil/optimization/__init__.py +1 -0
  133. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  134. package/src/devcouncil/planning/__init__.py +1 -1
  135. package/src/devcouncil/planning/arbiter_service.py +57 -57
  136. package/src/devcouncil/planning/correction_manifest.py +303 -0
  137. package/src/devcouncil/planning/critique_service.py +71 -66
  138. package/src/devcouncil/planning/plan_service.py +60 -46
  139. package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
  140. package/src/devcouncil/planning/repair_service.py +39 -39
  141. package/src/devcouncil/planning/spec_service.py +70 -44
  142. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  143. package/src/devcouncil/repo/gitignore.py +123 -0
  144. package/src/devcouncil/repo/sca.py +374 -0
  145. package/src/devcouncil/reporting/github_check.py +32 -32
  146. package/src/devcouncil/reporting/json_report.py +30 -17
  147. package/src/devcouncil/reporting/markdown_report.py +83 -46
  148. package/src/devcouncil/reporting/report_builder.py +14 -14
  149. package/src/devcouncil/skills/__init__.py +19 -0
  150. package/src/devcouncil/skills/library/README.md +46 -0
  151. package/src/devcouncil/skills/library/ai-training.md +50 -0
  152. package/src/devcouncil/skills/library/android.md +50 -0
  153. package/src/devcouncil/skills/library/backend.md +52 -0
  154. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  155. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  156. package/src/devcouncil/skills/library/desktop.md +46 -0
  157. package/src/devcouncil/skills/library/devops.md +48 -0
  158. package/src/devcouncil/skills/library/game-dev.md +46 -0
  159. package/src/devcouncil/skills/library/ios.md +48 -0
  160. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  161. package/src/devcouncil/skills/library/security.md +48 -0
  162. package/src/devcouncil/skills/library/systems.md +48 -0
  163. package/src/devcouncil/skills/library/web.md +47 -0
  164. package/src/devcouncil/skills/library/windows.md +47 -0
  165. package/src/devcouncil/skills/registry.py +330 -0
  166. package/src/devcouncil/storage/db.py +147 -66
  167. package/src/devcouncil/storage/models.py +204 -83
  168. package/src/devcouncil/storage/native.py +557 -0
  169. package/src/devcouncil/storage/repositories.py +388 -249
  170. package/src/devcouncil/telemetry/cost.py +140 -34
  171. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  172. package/src/devcouncil/telemetry/pricing.py +28 -0
  173. package/src/devcouncil/telemetry/traces.py +62 -7
  174. package/src/devcouncil/telemetry/tracker.py +52 -49
  175. package/src/devcouncil/ui/__init__.py +1 -0
  176. package/src/devcouncil/ui/dashboard.py +423 -0
  177. package/src/devcouncil/utils/__init__.py +1 -1
  178. package/src/devcouncil/utils/redaction.py +147 -141
  179. package/src/devcouncil/utils/subprocess_env.py +69 -0
  180. package/src/devcouncil/verification/__init__.py +1 -1
  181. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  182. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  183. package/src/devcouncil/verification/diff_coverage.py +353 -0
  184. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  185. package/src/devcouncil/verification/next_actions.py +189 -0
  186. package/src/devcouncil/verification/sandbox.py +178 -0
  187. package/src/devcouncil/verification/test_resolver.py +91 -0
  188. package/src/devcouncil/verification/verifier.py +1342 -307
  189. package/uv.lock +205 -64
  190. 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 typer
2
- from rich.console import Console
3
- from rich.panel import Panel
4
- from devcouncil.storage.db import get_db
5
- from devcouncil.storage.repositories import TaskRepository, RequirementRepository
6
-
7
- app = typer.Typer()
8
- console = Console()
9
-
10
- @app.callback(invoke_without_command=True)
11
- def show(
12
- ctx: typer.Context,
13
- task_id: str = typer.Argument(..., help="ID of the task to show"),
14
- ):
15
- """
16
- Show details of a specific task.
17
- """
18
- if ctx.invoked_subcommand is not None:
19
- return
20
-
21
- db = get_db()
22
- if not db:
23
- console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
24
- raise typer.Exit(code=1)
25
-
26
- with db.get_session() as session:
27
- task_repo = TaskRepository(session)
28
- req_repo = RequirementRepository(session)
29
-
30
- task = task_repo.get_by_id(task_id)
31
- if not task:
32
- console.print(f"[red]Task {task_id} not found.[/red]")
33
- raise typer.Exit(code=1)
34
-
35
- reqs = req_repo.get_all()
36
- req_map = {r.id: r for r in reqs}
37
-
38
- output = f"[bold]Status:[/bold] {task.status}\n\n"
39
- output += f"[bold]Description:[/bold]\n{task.description}\n\n"
40
-
41
- output += "[bold]Linked Requirements:[/bold]\n"
42
- for req_id in task.requirement_ids:
43
- req = req_map.get(req_id)
44
- if req:
45
- output += f" - [cyan]{req.id}[/cyan]: {req.title}\n"
46
- else:
47
- output += f" - [cyan]{req_id}[/cyan]: (Requirement not found)\n"
48
-
49
- output += "\n[bold]Planned Files:[/bold]\n"
50
- for pf in task.planned_files:
51
- output += f" - {pf.path} ({pf.allowed_change}): {pf.reason}\n"
52
-
53
- output += "\n[bold]Expected Tests:[/bold]\n"
54
- for et in task.expected_tests:
55
- output += f" - {et}\n"
56
-
57
- console.print(Panel(output, title=f"Task {task.id}: {task.title}", expand=False))
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
- from devcouncil.storage.db import get_db
7
- from devcouncil.storage.repositories import ArtifactGraphRepository
8
- from devcouncil.telemetry.cost import CostEstimator
9
-
10
- console = Console()
11
-
12
- def status():
13
- """
14
- Show the current status of the DevCouncil project.
15
- """
16
- db = get_db()
17
- if not db:
18
- console.print("[yellow]DevCouncil not initialized in this directory.[/yellow]")
19
- console.print("Run [bold]dev init[/bold] to get started.")
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
- reqs = list(graph.requirements.values())
28
- tasks = list(graph.tasks.values())
29
- blocking_gaps = graph.blocking_gaps()
30
-
31
- # Determine phase
32
- if not reqs and not tasks:
33
- phase = "NEW"
34
- elif reqs and not tasks:
35
- phase = "REQUIREMENTS_DRAFTED"
36
- elif blocking_gaps:
37
- phase = "TASK_BLOCKED"
38
- elif tasks:
39
- statuses = {t.status for t in tasks}
40
- if "running" in statuses:
41
- phase = "TASK_EXECUTING"
42
- elif "blocked" in statuses:
43
- phase = "TASK_BLOCKED"
44
- elif all(s in ("verified", "done") for s in statuses):
45
- phase = "PROJECT_DONE"
46
- else:
47
- phase = "PLAN_APPROVED"
48
- else:
49
- phase = "NEW"
50
-
51
- # Phase color
52
- phase_colors = {
53
- "NEW": "yellow",
54
- "REQUIREMENTS_DRAFTED": "cyan",
55
- "PLAN_APPROVED": "green",
56
- "TASK_EXECUTING": "blue",
57
- "TASK_BLOCKED": "red",
58
- "PROJECT_DONE": "green bold",
59
- }
60
- phase_color = phase_colors.get(phase, "white")
61
-
62
- # Calculate cost
63
- total_cost = 0.0
64
- log_file = Path(".devcouncil/logs/model_calls.jsonl")
65
- if log_file.exists():
66
- with open(log_file, "r", encoding="utf-8") as f:
67
- for line in f:
68
- try:
69
- entry = json.loads(line)
70
- total_cost += CostEstimator.estimate_cost(
71
- entry.get("response", {}).get("model", ""),
72
- entry.get("usage", {})
73
- )
74
- except Exception:
75
- continue
76
-
77
- console.print(Panel(
78
- f"[bold]Phase:[/bold] [{phase_color}]{phase}[/{phase_color}]\n"
79
- f"[bold]Requirements:[/bold] {summary['total_requirements']} ({summary['requirements_without_tasks']} unmapped)\n"
80
- f"[bold]Tasks:[/bold] {summary['total_tasks']} ({summary['tasks_without_requirements']} orphaned)\n"
81
- f"[bold]Acceptance Criteria:[/bold] {summary['total_ac']} ({summary['ac_without_evidence']} unverified)\n"
82
- f"[bold]Gaps:[/bold] {summary['total_gaps']} ({summary['blocking_gaps']} blocking)\n"
83
- f"[bold]Total Cost:[/bold] ${total_cost:.4f}",
84
- title="DevCouncil Status",
85
- expand=False,
86
- ))
87
-
88
- if tasks:
89
- table = Table(title="Task Summary")
90
- table.add_column("Status", style="magenta")
91
- table.add_column("Count", justify="right")
92
-
93
- status_counts: dict[str, int] = {}
94
- for t in tasks:
95
- status_counts[t.status] = status_counts.get(t.status, 0) + 1
96
- for s, count in sorted(status_counts.items()):
97
- table.add_row(s, str(count))
98
- console.print(table)
99
-
100
- if blocking_gaps:
101
- console.print(f"\n[red bold]WARNING: {len(blocking_gaps)} blocking gap(s) must be resolved:[/red bold]")
102
- for g in blocking_gaps[:5]:
103
- console.print(f" - [red]{g.id}[/red]: {g.description[:80]}")
104
- if len(blocking_gaps) > 5:
105
- console.print(f" ... and {len(blocking_gaps) - 5} more. Run [bold]dev report[/bold] for details.")
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 typer
2
- from rich.console import Console
3
- from rich.table import Table
4
- from devcouncil.storage.db import get_db
5
- from devcouncil.storage.repositories import TaskRepository
6
-
7
- app = typer.Typer()
8
- console = Console()
9
-
10
- @app.callback(invoke_without_command=True)
11
- def tasks(ctx: typer.Context):
12
- """
13
- List task graph and task gate status.
14
- """
15
- if ctx.invoked_subcommand is not None:
16
- return
17
-
18
- db = get_db()
19
- if not db:
20
- console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
21
- raise typer.Exit(code=1)
22
-
23
- with db.get_session() as session:
24
- task_repo = TaskRepository(session)
25
- tasks_list = task_repo.get_all()
26
-
27
- if not tasks_list:
28
- console.print("No tasks found. Run 'dev plan' to generate tasks.")
29
- return
30
-
31
- table = Table(title="DevCouncil Tasks")
32
- table.add_column("Task ID", style="cyan", no_wrap=True)
33
- table.add_column("Title", style="white")
34
- table.add_column("Status", style="magenta")
35
- table.add_column("Linked Reqs", style="green")
36
-
37
- for t in tasks_list:
38
- reqs = ", ".join(t.requirement_ids)
39
- table.add_row(t.id, t.title, t.status, reqs)
40
-
41
- console.print(table)
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)