devcouncil 0.1.0 → 0.1.1

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 (128) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +62 -543
  3. package/package.json +1 -1
  4. package/pyproject.toml +29 -26
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +135 -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 +143 -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/cli/commands/artifacts.py +51 -48
  22. package/src/devcouncil/cli/commands/ast.py +22 -0
  23. package/src/devcouncil/cli/commands/baseline.py +35 -32
  24. package/src/devcouncil/cli/commands/config.py +76 -54
  25. package/src/devcouncil/cli/commands/dashboard.py +26 -0
  26. package/src/devcouncil/cli/commands/doctor.py +86 -42
  27. package/src/devcouncil/cli/commands/go.py +237 -0
  28. package/src/devcouncil/cli/commands/hook.py +96 -29
  29. package/src/devcouncil/cli/commands/init.py +67 -56
  30. package/src/devcouncil/cli/commands/integrate.py +320 -14
  31. package/src/devcouncil/cli/commands/lsp.py +20 -0
  32. package/src/devcouncil/cli/commands/map.py +25 -21
  33. package/src/devcouncil/cli/commands/plan.py +257 -206
  34. package/src/devcouncil/cli/commands/prompt.py +36 -33
  35. package/src/devcouncil/cli/commands/repair.py +72 -69
  36. package/src/devcouncil/cli/commands/report.py +112 -54
  37. package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
  38. package/src/devcouncil/cli/commands/rollback.py +49 -47
  39. package/src/devcouncil/cli/commands/run.py +252 -207
  40. package/src/devcouncil/cli/commands/setup.py +159 -18
  41. package/src/devcouncil/cli/commands/show.py +76 -57
  42. package/src/devcouncil/cli/commands/status.py +117 -105
  43. package/src/devcouncil/cli/commands/tasks.py +55 -41
  44. package/src/devcouncil/cli/commands/trace.py +2 -1
  45. package/src/devcouncil/cli/commands/verify.py +158 -128
  46. package/src/devcouncil/cli/commands/version.py +20 -20
  47. package/src/devcouncil/cli/commands/watch.py +574 -0
  48. package/src/devcouncil/cli/main.py +42 -24
  49. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  50. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  51. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  52. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  53. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  54. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  55. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  56. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  57. package/src/devcouncil/domain/assumption.py +17 -17
  58. package/src/devcouncil/domain/critique.py +32 -32
  59. package/src/devcouncil/domain/evidence.py +27 -27
  60. package/src/devcouncil/domain/gap.py +26 -26
  61. package/src/devcouncil/domain/requirement.py +22 -22
  62. package/src/devcouncil/domain/task.py +26 -26
  63. package/src/devcouncil/execution/__init__.py +1 -1
  64. package/src/devcouncil/execution/context_builder.py +54 -54
  65. package/src/devcouncil/execution/executor.py +15 -15
  66. package/src/devcouncil/execution/hook_policy.py +24 -3
  67. package/src/devcouncil/execution/patch.py +28 -28
  68. package/src/devcouncil/execution/permissions.py +44 -44
  69. package/src/devcouncil/execution/prompt_builder.py +23 -23
  70. package/src/devcouncil/execution/task_runner.py +63 -63
  71. package/src/devcouncil/executors/__init__.py +1 -1
  72. package/src/devcouncil/executors/coding_cli.py +112 -0
  73. package/src/devcouncil/executors/mini_swe.py +63 -63
  74. package/src/devcouncil/executors/native/agent.py +81 -81
  75. package/src/devcouncil/executors/openhands.py +56 -56
  76. package/src/devcouncil/gating/__init__.py +1 -1
  77. package/src/devcouncil/gating/checks/clean_git.py +50 -45
  78. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  79. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  80. package/src/devcouncil/gating/checks/secret_scan_check.py +34 -34
  81. package/src/devcouncil/gating/policy.py +157 -157
  82. package/src/devcouncil/indexing/__init__.py +1 -1
  83. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  84. package/src/devcouncil/indexing/graph_index.py +48 -48
  85. package/src/devcouncil/indexing/lsp.py +120 -0
  86. package/src/devcouncil/indexing/repo_mapper.py +208 -204
  87. package/src/devcouncil/integrations/github.py +35 -35
  88. package/src/devcouncil/integrations/gitnexus.py +27 -27
  89. package/src/devcouncil/integrations/graphify.py +34 -34
  90. package/src/devcouncil/integrations/mcp/server.py +549 -96
  91. package/src/devcouncil/integrations/pr_comments.py +62 -0
  92. package/src/devcouncil/live/__init__.py +2 -0
  93. package/src/devcouncil/live/cards.py +207 -0
  94. package/src/devcouncil/live/models.py +63 -0
  95. package/src/devcouncil/live/repair_prompt.py +83 -0
  96. package/src/devcouncil/live/reviewer.py +70 -0
  97. package/src/devcouncil/live/signals.py +135 -0
  98. package/src/devcouncil/live/summary.py +34 -0
  99. package/src/devcouncil/live/tasks.py +18 -0
  100. package/src/devcouncil/live/transcripts.py +138 -0
  101. package/src/devcouncil/llm/__init__.py +1 -1
  102. package/src/devcouncil/llm/cache.py +38 -38
  103. package/src/devcouncil/llm/provider.py +146 -125
  104. package/src/devcouncil/llm/router.py +111 -111
  105. package/src/devcouncil/planning/__init__.py +1 -1
  106. package/src/devcouncil/planning/arbiter_service.py +57 -57
  107. package/src/devcouncil/planning/critique_service.py +66 -66
  108. package/src/devcouncil/planning/plan_service.py +46 -46
  109. package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
  110. package/src/devcouncil/planning/repair_service.py +39 -39
  111. package/src/devcouncil/planning/spec_service.py +44 -44
  112. package/src/devcouncil/reporting/github_check.py +32 -32
  113. package/src/devcouncil/reporting/json_report.py +20 -17
  114. package/src/devcouncil/reporting/markdown_report.py +68 -46
  115. package/src/devcouncil/reporting/report_builder.py +14 -14
  116. package/src/devcouncil/storage/db.py +66 -66
  117. package/src/devcouncil/storage/models.py +83 -83
  118. package/src/devcouncil/storage/repositories.py +299 -222
  119. package/src/devcouncil/telemetry/cost.py +34 -34
  120. package/src/devcouncil/telemetry/tracker.py +49 -49
  121. package/src/devcouncil/ui/__init__.py +1 -0
  122. package/src/devcouncil/ui/dashboard.py +122 -0
  123. package/src/devcouncil/utils/__init__.py +1 -1
  124. package/src/devcouncil/utils/redaction.py +141 -141
  125. package/src/devcouncil/verification/__init__.py +1 -1
  126. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  127. package/src/devcouncil/verification/verifier.py +319 -302
  128. package/uv.lock +1 -1
@@ -1,18 +1,149 @@
1
1
  from pathlib import Path
2
+ import os
2
3
  import shutil
4
+ import sys
3
5
 
4
6
  import typer
7
+ import yaml
5
8
  from rich.console import Console
6
9
  from rich.panel import Panel
7
10
 
11
+ from devcouncil.app.config import load_config, load_local_secrets, provider_api_key_env_var
8
12
  from devcouncil.cli.commands.doctor import render_doctor_check
9
13
  from devcouncil.cli.commands.init import initialize_project
10
- from devcouncil.cli.commands.integrate import _codex_command, _configure, _gemini_command
14
+ from devcouncil.cli.commands.integrate import (
15
+ _claude_command,
16
+ _codex_command,
17
+ _configure_native_hooks,
18
+ _configure,
19
+ _cursor_command,
20
+ _gemini_command,
21
+ )
22
+ from devcouncil.llm.provider import validate_model_provider
11
23
 
12
24
  app = typer.Typer()
13
25
  console = Console()
14
26
 
15
27
 
28
+ def _set_model_provider(project_root: Path, provider: str) -> None:
29
+ normalized = validate_model_provider(provider)
30
+ config_path = project_root / ".devcouncil" / "config.yaml"
31
+ raw_config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
32
+ raw_config.setdefault("models", {})
33
+ previous = raw_config["models"].get("provider", "openrouter")
34
+ raw_config["models"]["provider"] = normalized
35
+ config_path.write_text(yaml.dump(raw_config, default_flow_style=False), encoding="utf-8")
36
+ if previous != normalized:
37
+ console.print(f"[green]Updated model provider from {previous} to {normalized}.[/green]")
38
+
39
+
40
+ def _write_local_secret(project_root: Path, env_var: str, value: str) -> Path:
41
+ if "\n" in value or "\r" in value:
42
+ raise ValueError("API keys cannot contain newlines.")
43
+ secrets = load_local_secrets(project_root)
44
+ secrets[env_var] = value
45
+ path = project_root / ".devcouncil" / "secrets.env"
46
+ path.parent.mkdir(parents=True, exist_ok=True)
47
+ lines = [
48
+ "# Local DevCouncil secrets. This file is ignored by git.",
49
+ "# Process environment variables with the same name take precedence.",
50
+ *[f"{key}={val}" for key, val in sorted(secrets.items())],
51
+ "",
52
+ ]
53
+ path.write_text("\n".join(lines), encoding="utf-8")
54
+ return path
55
+
56
+
57
+ def _configure_api_key(project_root: Path, api_key: str | None, skip_api_key: bool) -> None:
58
+ config = load_config(project_root)
59
+ provider = config.models.provider
60
+ env_var = provider_api_key_env_var(provider)
61
+ local_secrets = load_local_secrets(project_root)
62
+
63
+ if os.environ.get(env_var):
64
+ console.print(f"[green]{env_var} is already set in the environment.[/green]")
65
+ return
66
+ if local_secrets.get(env_var):
67
+ console.print(f"[green]{env_var} is already set in .devcouncil/secrets.env.[/green]")
68
+ return
69
+ if api_key:
70
+ _write_local_secret(project_root, env_var, api_key)
71
+ console.print(f"[green]Saved {env_var} to .devcouncil/secrets.env.[/green]")
72
+ return
73
+ if skip_api_key:
74
+ console.print(f"[yellow]Skipped {env_var} setup. Model-backed commands will ask again if it is missing.[/yellow]")
75
+ return
76
+ if not sys.stdin.isatty():
77
+ console.print(
78
+ f"[yellow]{env_var} is not set.[/yellow] "
79
+ f"Run [bold]dev setup --api-key YOUR_KEY[/bold] or set it in your shell before model-backed commands."
80
+ )
81
+ return
82
+
83
+ console.print()
84
+ console.print(Panel.fit(
85
+ "\n".join([
86
+ f"Provider: {provider}",
87
+ f"Required key: {env_var}",
88
+ "Press Enter without a value to skip for now.",
89
+ ]),
90
+ title="Model API Key",
91
+ border_style="cyan",
92
+ ))
93
+ entered = typer.prompt(f"{env_var}", default="", hide_input=True, show_default=False)
94
+ if not entered:
95
+ console.print(f"[yellow]Skipped {env_var} setup.[/yellow]")
96
+ return
97
+ _write_local_secret(project_root, env_var, entered)
98
+ console.print(f"[green]Saved {env_var} to .devcouncil/secrets.env.[/green]")
99
+
100
+
101
+ def _is_interactive_terminal() -> bool:
102
+ return sys.stdin.isatty()
103
+
104
+
105
+ def _configure_coding_cli_integrations(project_root: Path, apply: bool, gemini_scope: str) -> None:
106
+ console.print()
107
+ console.print("[bold]Coding CLI integration[/bold]")
108
+ commands = [
109
+ ("Codex CLI", _codex_command(project_root)),
110
+ ("Gemini CLI", _gemini_command(project_root, gemini_scope)),
111
+ ("Claude Code", _claude_command(project_root, "local")),
112
+ ("Cursor", _cursor_command(project_root)),
113
+ ]
114
+ results = []
115
+ for tool, command in commands:
116
+ if apply and not shutil.which(command[0]):
117
+ console.print(f"[yellow]{tool} CLI not found on PATH. Skipping optional integration.[/yellow]")
118
+ continue
119
+ results.append(_configure(tool, command, apply))
120
+ _configure_native_hooks(project_root, "all", apply)
121
+ if apply and any(not ok for ok in results):
122
+ raise typer.Exit(code=1)
123
+
124
+
125
+ def _prompt_for_first_run_integrations(project_root: Path, apply: bool, gemini_scope: str) -> bool:
126
+ if not _is_interactive_terminal():
127
+ return False
128
+
129
+ console.print()
130
+ console.print(Panel.fit(
131
+ "\n".join([
132
+ "DevCouncil can configure supported coding CLIs now.",
133
+ "This adds MCP setup and native hook config for detected clients.",
134
+ "Missing optional clients are skipped.",
135
+ ]),
136
+ title="Coding CLI Setup",
137
+ border_style="cyan",
138
+ ))
139
+ if not typer.confirm("Set up coding CLI integrations now?", default=True):
140
+ console.print("[yellow]Skipped coding CLI integration setup.[/yellow]")
141
+ return False
142
+
143
+ _configure_coding_cli_integrations(project_root, apply=apply, gemini_scope=gemini_scope)
144
+ return True
145
+
146
+
16
147
  @app.callback(invoke_without_command=True)
17
148
  def setup(
18
149
  ctx: typer.Context,
@@ -22,9 +153,13 @@ def setup(
22
153
  help="Target project repository root. Defaults to the terminal's current directory.",
23
154
  ),
24
155
  name: str | None = typer.Option(None, "--name", "-n", help="Project name for .devcouncil/config.yaml."),
25
- integrate: bool = typer.Option(False, "--integrate", help="Configure supported coding CLI MCP integrations."),
156
+ integrate: bool = typer.Option(False, "--integrate", help="Configure supported coding CLI MCP integrations and native hooks."),
26
157
  apply: bool = typer.Option(False, "--apply", help="Apply integration config instead of previewing commands."),
27
158
  gemini_scope: str = typer.Option("project", "--gemini-scope", help="Gemini MCP config scope: project or user."),
159
+ provider: str | None = typer.Option(None, "--provider", help="Set models.provider before configuring the API key."),
160
+ api_key: str | None = typer.Option(None, "--api-key", help="Store the configured provider API key in local .devcouncil/secrets.env."),
161
+ skip_api_key: bool = typer.Option(False, "--skip-api-key", help="Skip the first-run model API key prompt."),
162
+ skip_integrations: bool = typer.Option(False, "--skip-integrations", help="Skip the first-run coding CLI integration prompt."),
28
163
  ):
29
164
  """
30
165
  Initialize DevCouncil from a normal terminal in the target repository root.
@@ -43,38 +178,44 @@ def setup(
43
178
  if not created:
44
179
  console.print(f"[yellow]DevCouncil is already initialized at {root / '.devcouncil'}.[/yellow]")
45
180
 
181
+ if provider:
182
+ try:
183
+ _set_model_provider(root, provider)
184
+ except ValueError as e:
185
+ console.print(f"[red]{e}[/red]")
186
+ raise typer.Exit(code=2) from e
187
+
188
+ _configure_api_key(root, api_key, skip_api_key)
189
+
46
190
  console.print()
47
- render_doctor_check()
191
+ render_doctor_check(root)
48
192
 
49
193
  if integrate:
50
- console.print()
51
- console.print("[bold]Coding CLI integration[/bold]")
52
- commands = [
53
- ("Codex CLI", _codex_command(root)),
54
- ("Gemini CLI", _gemini_command(root, gemini_scope)),
55
- ]
56
- results = []
57
- for tool, command in commands:
58
- if apply and not shutil.which(command[0]):
59
- console.print(f"[yellow]{tool} CLI not found on PATH. Skipping optional integration.[/yellow]")
60
- continue
61
- results.append(_configure(tool, command, apply))
62
- if apply and any(not ok for ok in results):
63
- raise typer.Exit(code=1)
194
+ _configure_coding_cli_integrations(root, apply=apply, gemini_scope=gemini_scope)
195
+ elif created and not skip_integrations:
196
+ _prompt_for_first_run_integrations(root, apply=True, gemini_scope=gemini_scope)
64
197
 
65
198
  console.print()
66
199
  console.print(Panel.fit(
67
200
  "\n".join([
68
201
  "[bold]Next commands[/bold]",
69
202
  f"Keep running DevCouncil commands in this terminal at: {root}",
203
+ "One-command agent path:",
204
+ "dev e2e \"Describe the implementation goal\"",
205
+ "",
206
+ "Manual sidecar path:",
70
207
  "dev plan \"Describe the implementation goal\"",
71
208
  "dev tasks",
72
209
  "dev run TASK-001 --executor manual",
73
210
  "dev prompt TASK-001",
74
211
  "Paste only the dev prompt output into your coding CLI.",
212
+ "Paste only the dev prompt output into your coding CLI, or run directly:",
213
+ "dev run TASK-001 --executor codex",
214
+ "dev run TASK-001 --executor gemini",
215
+ "dev run TASK-001 --executor claude",
75
216
  "dev verify TASK-001",
76
217
  "",
77
- "Use [bold]dev setup --integrate[/bold] to preview coding CLI MCP setup.",
218
+ "Use [bold]dev setup --integrate[/bold] to preview coding CLI MCP and native hook setup.",
78
219
  "Use [bold]dev setup --integrate --apply[/bold] to configure detected clients.",
79
220
  ]),
80
221
  title="DevCouncil is ready",
@@ -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))
@@ -1,105 +1,117 @@
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
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
+ "task_status_counts": status_counts,
55
+ "blocking_gaps": [gap.model_dump() for gap in blocking_gaps],
56
+ "live_review": live_review_summary(project_root),
57
+ }
58
+
59
+
60
+ def status(
61
+ json_format: bool = typer.Option(False, "--json", help="Output machine-readable JSON."),
62
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
63
+ ):
64
+ """
65
+ Show the current status of the DevCouncil project.
66
+ """
67
+ root = project_root.expanduser().resolve()
68
+ payload = _status_payload(root)
69
+ if json_format:
70
+ typer.echo(json.dumps(payload, indent=2))
71
+ return
72
+
73
+ if not payload["initialized"]:
74
+ console.print("[yellow]DevCouncil state is not available in this directory.[/yellow]")
75
+ return
76
+
77
+ summary = payload["coverage_summary"]
78
+ phase = payload["phase"]
79
+ phase_colors = {
80
+ "NEW": "yellow",
81
+ "REQUIREMENTS_DRAFTED": "cyan",
82
+ "PLAN_APPROVED": "green",
83
+ "TASK_EXECUTING": "blue",
84
+ "TASK_BLOCKED": "red",
85
+ "PROJECT_DONE": "green bold",
86
+ }
87
+ phase_color = phase_colors.get(phase, "white")
88
+
89
+ console.print(Panel(
90
+ f"[bold]Phase:[/bold] [{phase_color}]{phase}[/{phase_color}]\n"
91
+ f"[bold]Requirements:[/bold] {summary['total_requirements']} ({summary['requirements_without_tasks']} unmapped)\n"
92
+ f"[bold]Tasks:[/bold] {summary['total_tasks']} ({summary['tasks_without_requirements']} orphaned)\n"
93
+ f"[bold]Acceptance Criteria:[/bold] {summary['total_ac']} ({summary['ac_without_evidence']} unverified)\n"
94
+ f"[bold]Gaps:[/bold] {summary['total_gaps']} ({summary['blocking_gaps']} blocking)\n"
95
+ f"[bold]Live Review:[/bold] {payload['live_review']['cards']['critical_open']} open critical, "
96
+ f"{len(payload['live_review']['blocking_cards'])} blocking in scope, "
97
+ f"{payload['live_review']['pending_signals']} pending signal(s)\n"
98
+ f"[bold]Total Cost:[/bold] ${payload['total_cost']:.4f}",
99
+ title="DevCouncil Status",
100
+ expand=False,
101
+ ))
102
+
103
+ if payload["task_status_counts"]:
104
+ table = Table(title="Task Summary")
105
+ table.add_column("Status", style="magenta")
106
+ table.add_column("Count", justify="right")
107
+ for state, count in sorted(payload["task_status_counts"].items()):
108
+ table.add_row(state, str(count))
109
+ console.print(table)
110
+
111
+ blocking_gaps = payload["blocking_gaps"]
112
+ if blocking_gaps:
113
+ console.print(f"\n[red bold]WARNING: {len(blocking_gaps)} blocking gap(s) must be resolved:[/red bold]")
114
+ for gap in blocking_gaps[:5]:
115
+ console.print(f" - [red]{gap['id']}[/red]: {gap['description'][:80]}")
116
+ if len(blocking_gaps) > 5:
117
+ console.print(f" ... and {len(blocking_gaps) - 5} more. Run [bold]dev report[/bold] for details.")