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.
- package/LICENSE +201 -201
- package/README.md +62 -543
- package/package.json +1 -1
- package/pyproject.toml +29 -26
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +135 -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 +143 -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/cli/commands/artifacts.py +51 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/config.py +76 -54
- package/src/devcouncil/cli/commands/dashboard.py +26 -0
- package/src/devcouncil/cli/commands/doctor.py +86 -42
- package/src/devcouncil/cli/commands/go.py +237 -0
- package/src/devcouncil/cli/commands/hook.py +96 -29
- package/src/devcouncil/cli/commands/init.py +67 -56
- package/src/devcouncil/cli/commands/integrate.py +320 -14
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +25 -21
- package/src/devcouncil/cli/commands/plan.py +257 -206
- package/src/devcouncil/cli/commands/prompt.py +36 -33
- package/src/devcouncil/cli/commands/repair.py +72 -69
- package/src/devcouncil/cli/commands/report.py +112 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
- package/src/devcouncil/cli/commands/rollback.py +49 -47
- package/src/devcouncil/cli/commands/run.py +252 -207
- package/src/devcouncil/cli/commands/setup.py +159 -18
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/status.py +117 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +2 -1
- package/src/devcouncil/cli/commands/verify.py +158 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/main.py +42 -24
- 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 +27 -27
- package/src/devcouncil/domain/gap.py +26 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +26 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/hook_policy.py +24 -3
- package/src/devcouncil/execution/patch.py +28 -28
- package/src/devcouncil/execution/permissions.py +44 -44
- package/src/devcouncil/execution/prompt_builder.py +23 -23
- package/src/devcouncil/execution/task_runner.py +63 -63
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/coding_cli.py +112 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +81 -81
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +50 -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 +34 -34
- package/src/devcouncil/gating/policy.py +157 -157
- 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 +120 -0
- package/src/devcouncil/indexing/repo_mapper.py +208 -204
- package/src/devcouncil/integrations/github.py +35 -35
- package/src/devcouncil/integrations/gitnexus.py +27 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +549 -96
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +207 -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 +138 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +38 -38
- package/src/devcouncil/llm/provider.py +146 -125
- package/src/devcouncil/llm/router.py +111 -111
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/critique_service.py +66 -66
- package/src/devcouncil/planning/plan_service.py +46 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +44 -44
- package/src/devcouncil/reporting/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +20 -17
- package/src/devcouncil/reporting/markdown_report.py +68 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- package/src/devcouncil/storage/db.py +66 -66
- package/src/devcouncil/storage/models.py +83 -83
- package/src/devcouncil/storage/repositories.py +299 -222
- package/src/devcouncil/telemetry/cost.py +34 -34
- package/src/devcouncil/telemetry/tracker.py +49 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +122 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +141 -141
- package/src/devcouncil/verification/__init__.py +1 -1
- package/src/devcouncil/verification/implementation_reviewer.py +55 -55
- package/src/devcouncil/verification/verifier.py +319 -302
- 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
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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))
|
|
@@ -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
|
-
|
|
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
|
|
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.")
|