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,27 +1,27 @@
1
- from devcouncil.domain.requirement import Requirement
2
- from devcouncil.domain.task import Task
3
- from devcouncil.app.errors import GatingError
4
-
5
- class ArtifactValidator:
6
- """Validates DevCouncil artifacts (tasks, requirements, etc.)."""
7
-
8
- @staticmethod
9
- def validate_requirement(req: Requirement) -> None:
10
- if not req.title:
11
- raise GatingError(f"Requirement {req.id} missing title.")
12
- if not req.acceptance_criteria:
13
- raise GatingError(f"Requirement {req.id} must have at least one acceptance criterion.")
14
- for ac in req.acceptance_criteria:
15
- if not ac.verification_method:
16
- raise GatingError(f"Acceptance criterion {ac.id} in {req.id} missing verification method.")
17
-
18
- @staticmethod
19
- def validate_task(task: Task) -> None:
20
- if not task.requirement_ids:
21
- raise GatingError(f"Task {task.id} must map to at least one requirement.")
22
- if not task.planned_files:
23
- raise GatingError(f"Task {task.id} must have at least one planned file.")
24
- if not task.acceptance_criterion_ids:
25
- raise GatingError(f"Task {task.id} must map to at least one acceptance criterion.")
26
- if not task.allowed_commands and not task.expected_tests:
27
- raise GatingError(f"Task {task.id} must define allowed commands or expected tests.")
1
+ from devcouncil.domain.requirement import Requirement
2
+ from devcouncil.domain.task import Task
3
+ from devcouncil.app.errors import GatingError
4
+
5
+ class ArtifactValidator:
6
+ """Validates DevCouncil artifacts (tasks, requirements, etc.)."""
7
+
8
+ @staticmethod
9
+ def validate_requirement(req: Requirement) -> None:
10
+ if not req.title:
11
+ raise GatingError(f"Requirement {req.id} missing title.")
12
+ if not req.acceptance_criteria:
13
+ raise GatingError(f"Requirement {req.id} must have at least one acceptance criterion.")
14
+ for ac in req.acceptance_criteria:
15
+ if not ac.verification_method:
16
+ raise GatingError(f"Acceptance criterion {ac.id} in {req.id} missing verification method.")
17
+
18
+ @staticmethod
19
+ def validate_task(task: Task) -> None:
20
+ if not task.requirement_ids:
21
+ raise GatingError(f"Task {task.id} must map to at least one requirement.")
22
+ if not task.planned_files:
23
+ raise GatingError(f"Task {task.id} must have at least one planned file.")
24
+ if not task.acceptance_criterion_ids:
25
+ raise GatingError(f"Task {task.id} must map to at least one acceptance criterion.")
26
+ if not task.allowed_commands and not task.expected_tests:
27
+ raise GatingError(f"Task {task.id} must define allowed commands or expected tests.")
@@ -1,48 +1,51 @@
1
- import typer
2
- from rich.console import Console
3
- from rich.table import Table
4
-
5
- from devcouncil.app.errors import GatingError
6
- from devcouncil.artifacts.validators import ArtifactValidator
7
- from devcouncil.storage.db import get_db
8
- from devcouncil.storage.repositories import RequirementRepository, TaskRepository
9
-
10
- app = typer.Typer()
11
- console = Console()
12
-
13
-
14
- @app.command(name="validate")
15
- def validate():
16
- """Validate requirements and tasks stored in the artifact graph."""
17
- db = get_db()
18
- if not db:
19
- console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
20
- raise typer.Exit(code=1)
21
-
22
- errors: list[str] = []
23
- with db.get_session() as session:
24
- req_repo = RequirementRepository(session)
25
- task_repo = TaskRepository(session)
26
-
27
- for req in req_repo.get_all():
28
- try:
29
- ArtifactValidator.validate_requirement(req)
30
- except GatingError as exc:
31
- errors.append(str(exc))
32
-
33
- for task in task_repo.get_all():
34
- try:
35
- ArtifactValidator.validate_task(task)
36
- except GatingError as exc:
37
- errors.append(str(exc))
38
-
39
- if not errors:
40
- console.print("[green]Artifacts are valid.[/green]")
41
- return
42
-
43
- table = Table(title="Artifact Validation Errors")
44
- table.add_column("Error", style="red")
45
- for error in errors:
46
- table.add_row(error)
47
- console.print(table)
48
- raise typer.Exit(code=1)
1
+ import typer
2
+ from rich.console import Console
3
+ from rich.table import Table
4
+ from pathlib import Path
5
+
6
+ from devcouncil.app.errors import GatingError
7
+ from devcouncil.artifacts.validators import ArtifactValidator
8
+ from devcouncil.storage.db import get_db
9
+ from devcouncil.storage.repositories import RequirementRepository, TaskRepository
10
+ from devcouncil.cli.commands.init import initialize_project
11
+
12
+ app = typer.Typer()
13
+ console = Console()
14
+
15
+
16
+ @app.command(name="validate")
17
+ def validate():
18
+ """Validate requirements and tasks stored in the artifact graph."""
19
+ initialize_project(Path("."), quiet=True)
20
+ db = get_db()
21
+ if not db:
22
+ console.print("[red]DevCouncil state is unavailable in this directory.[/red]")
23
+ raise typer.Exit(code=1)
24
+
25
+ errors: list[str] = []
26
+ with db.get_session() as session:
27
+ req_repo = RequirementRepository(session)
28
+ task_repo = TaskRepository(session)
29
+
30
+ for req in req_repo.get_all():
31
+ try:
32
+ ArtifactValidator.validate_requirement(req)
33
+ except GatingError as exc:
34
+ errors.append(str(exc))
35
+
36
+ for task in task_repo.get_all():
37
+ try:
38
+ ArtifactValidator.validate_task(task)
39
+ except GatingError as exc:
40
+ errors.append(str(exc))
41
+
42
+ if not errors:
43
+ console.print("[green]Artifacts are valid.[/green]")
44
+ return
45
+
46
+ table = Table(title="Artifact Validation Errors")
47
+ table.add_column("Error", style="red")
48
+ for error in errors:
49
+ table.add_row(error)
50
+ console.print(table)
51
+ raise typer.Exit(code=1)
@@ -0,0 +1,22 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import typer
5
+
6
+ from devcouncil.indexing.ast_matcher import AstMatcher
7
+
8
+ app = typer.Typer(help="Run structural AST searches.")
9
+
10
+
11
+ @app.command("match")
12
+ def match(
13
+ query: str = typer.Argument("", help="Name or source text to match."),
14
+ language: str | None = typer.Option(None, "--language", "-l", help="Language filter."),
15
+ kind: str | None = typer.Option(None, "--kind", "-k", help="Symbol kind filter."),
16
+ limit: int = typer.Option(100, "--limit", help="Maximum matches."),
17
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root."),
18
+ ):
19
+ """Find function/class/type symbols using tree-sitter when available and fallbacks otherwise."""
20
+ root = project_root.expanduser().resolve()
21
+ matches = AstMatcher(root).match(query=query, language=language, kind=kind, limit=max(1, limit))
22
+ typer.echo(json.dumps({"matches": [item.model_dump() for item in matches]}, indent=2))
@@ -1,32 +1,35 @@
1
- import json
2
- from pathlib import Path
3
-
4
- import typer
5
- from rich.console import Console
6
-
7
- from devcouncil.storage.db import get_db
8
- from devcouncil.verification.verifier import Verifier
9
-
10
- console = Console()
11
-
12
-
13
- def baseline(
14
- force: bool = typer.Option(False, "--force", help="Overwrite an existing baseline snapshot."),
15
- ):
16
- """Capture the current repo state as DevCouncil's verification baseline."""
17
- if not get_db():
18
- console.print("[red]DevCouncil not initialized. Run 'dev init' first.[/red]")
19
- raise typer.Exit(code=1)
20
-
21
- baseline_path = Path(".devcouncil") / "baseline.json"
22
- if baseline_path.exists() and not force:
23
- console.print("[yellow]Baseline already exists. Use --force to replace it.[/yellow]")
24
- raise typer.Exit(code=1)
25
-
26
- changed_files = Verifier(Path(".")).get_changed_files()
27
- payload = {
28
- "changed_files": changed_files,
29
- "note": "Files present in this snapshot are excluded from future task-scoped verification diffs.",
30
- }
31
- baseline_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
32
- console.print(f"[green]Captured baseline with {len(changed_files)} changed file(s).[/green]")
1
+ import json
2
+ from pathlib import Path
3
+
4
+ import typer
5
+ from rich.console import Console
6
+
7
+ from devcouncil.cli.commands.init import initialize_project
8
+ from devcouncil.storage.db import get_db
9
+ from devcouncil.verification.verifier import Verifier
10
+
11
+ console = Console()
12
+
13
+
14
+ def baseline(
15
+ force: bool = typer.Option(False, "--force", help="Overwrite an existing baseline snapshot."),
16
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
17
+ ):
18
+ """Capture the current repo state as DevCouncil's verification baseline."""
19
+ root = project_root.expanduser().resolve()
20
+ initialize_project(root, quiet=True)
21
+ if not get_db(root):
22
+ raise typer.Exit(code=1)
23
+
24
+ baseline_path = root / ".devcouncil" / "baseline.json"
25
+ if baseline_path.exists() and not force:
26
+ console.print("[yellow]Baseline already exists. Use --force to replace it.[/yellow]")
27
+ raise typer.Exit(code=1)
28
+
29
+ changed_files = Verifier(root).get_changed_files()
30
+ payload = {
31
+ "changed_files": changed_files,
32
+ "note": "Files present in this snapshot are excluded from future task-scoped verification diffs.",
33
+ }
34
+ baseline_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
35
+ console.print(f"[green]Captured baseline with {len(changed_files)} changed file(s).[/green]")
@@ -1,54 +1,76 @@
1
- import typer
2
- import yaml
3
- from pathlib import Path
4
- from rich.console import Console
5
- from devcouncil.app.config import load_config
6
-
7
- app = typer.Typer(help="Manage DevCouncil configuration")
8
- console = Console()
9
-
10
- @app.command("models")
11
- def models(
12
- role: str = typer.Option(None, "--role", "-r", help="Specific role to show/edit"),
13
- model: str = typer.Option(None, "--model", "-m", help="New model string to set for the role")
14
- ):
15
- """View or edit model role configuration."""
16
- try:
17
- load_config(Path("."))
18
- except FileNotFoundError as e:
19
- console.print(f"[red]{e}[/red]")
20
- return
21
-
22
- config_path = Path(".devcouncil/config.yaml")
23
-
24
- with open(config_path) as f:
25
- raw_config = yaml.safe_load(f) or {}
26
-
27
- if not role:
28
- console.print("[bold]Model Configuration[/bold]")
29
- for r, m in raw_config.get("models", {}).get("roles", {}).items():
30
- console.print(f" [cyan]{r}[/cyan]: {m.get('model')}")
31
- return
32
-
33
- if not model:
34
- m = raw_config.get("models", {}).get("roles", {}).get(role)
35
- if m:
36
- console.print(f"[cyan]{role}[/cyan]: {m.get('model')}")
37
- else:
38
- console.print(f"[red]Role '{role}' not found.[/red]")
39
- return
40
-
41
- if "models" not in raw_config:
42
- raw_config["models"] = {"roles": {}}
43
- if "roles" not in raw_config["models"]:
44
- raw_config["models"]["roles"] = {}
45
-
46
- if role not in raw_config["models"]["roles"]:
47
- raw_config["models"]["roles"][role] = {}
48
-
49
- raw_config["models"]["roles"][role]["model"] = model
50
-
51
- with open(config_path, "w") as f:
52
- yaml.dump(raw_config, f, default_flow_style=False)
53
-
54
- console.print(f"[green]Updated '{role}' to use model '{model}'[/green]")
1
+ import typer
2
+ import yaml
3
+ from pathlib import Path
4
+ from rich.console import Console
5
+ from devcouncil.app.config import load_config
6
+ from devcouncil.llm.provider import SUPPORTED_MODEL_PROVIDERS, validate_model_provider
7
+
8
+ app = typer.Typer(help="Manage DevCouncil configuration")
9
+ console = Console()
10
+
11
+ @app.command("models")
12
+ def models(
13
+ role: str = typer.Option(None, "--role", "-r", help="Specific role to show/edit"),
14
+ model: str = typer.Option(None, "--model", "-m", help="New model string to set for the role"),
15
+ provider: str = typer.Option(None, "--provider", help="Set the model provider."),
16
+ ):
17
+ """View or edit model role configuration."""
18
+ try:
19
+ load_config(Path("."))
20
+ except FileNotFoundError as e:
21
+ console.print(f"[red]{e}[/red]")
22
+ return
23
+
24
+ config_path = Path(".devcouncil/config.yaml")
25
+
26
+ with open(config_path) as f:
27
+ raw_config = yaml.safe_load(f) or {}
28
+
29
+ if provider:
30
+ try:
31
+ normalized_provider = validate_model_provider(provider)
32
+ except ValueError as e:
33
+ console.print(f"[red]{e}[/red]")
34
+ raise typer.Exit(code=2) from e
35
+ raw_config.setdefault("models", {})
36
+ previous = raw_config["models"].get("provider", "openrouter")
37
+ raw_config["models"]["provider"] = normalized_provider
38
+ with open(config_path, "w") as f:
39
+ yaml.dump(raw_config, f, default_flow_style=False)
40
+ if previous == normalized_provider:
41
+ console.print(f"[green]Model provider remains '{normalized_provider}'.[/green]")
42
+ else:
43
+ console.print(f"[green]Updated model provider from '{previous}' to '{normalized_provider}'.[/green]")
44
+ return
45
+
46
+ if not role:
47
+ console.print("[bold]Model Configuration[/bold]")
48
+ configured_provider = raw_config.get("models", {}).get("provider", "openrouter")
49
+ supported = ", ".join(SUPPORTED_MODEL_PROVIDERS)
50
+ console.print(f" [cyan]provider[/cyan]: {configured_provider} (supported: {supported})")
51
+ for r, m in raw_config.get("models", {}).get("roles", {}).items():
52
+ console.print(f" [cyan]{r}[/cyan]: {m.get('model')}")
53
+ return
54
+
55
+ if not model:
56
+ m = raw_config.get("models", {}).get("roles", {}).get(role)
57
+ if m:
58
+ console.print(f"[cyan]{role}[/cyan]: {m.get('model')}")
59
+ else:
60
+ console.print(f"[red]Role '{role}' not found.[/red]")
61
+ return
62
+
63
+ if "models" not in raw_config:
64
+ raw_config["models"] = {"roles": {}}
65
+ if "roles" not in raw_config["models"]:
66
+ raw_config["models"]["roles"] = {}
67
+
68
+ if role not in raw_config["models"]["roles"]:
69
+ raw_config["models"]["roles"][role] = {}
70
+
71
+ raw_config["models"]["roles"][role]["model"] = model
72
+
73
+ with open(config_path, "w") as f:
74
+ yaml.dump(raw_config, f, default_flow_style=False)
75
+
76
+ console.print(f"[green]Updated '{role}' to use model '{model}'[/green]")
@@ -0,0 +1,26 @@
1
+ from pathlib import Path
2
+
3
+ import typer
4
+ from rich.console import Console
5
+
6
+ from devcouncil.cli.commands.init import initialize_project
7
+ from devcouncil.ui.dashboard import run_dashboard
8
+
9
+ app = typer.Typer(help="Serve the live DevCouncil dashboard.")
10
+ console = Console()
11
+
12
+
13
+ @app.callback(invoke_without_command=True)
14
+ def dashboard(
15
+ ctx: typer.Context,
16
+ host: str = typer.Option("127.0.0.1", "--host", help="Dashboard bind host."),
17
+ port: int = typer.Option(8765, "--port", help="Dashboard bind port."),
18
+ project_root: Path = typer.Option(Path("."), "--project-root", help="Repository root containing .devcouncil/."),
19
+ ):
20
+ """Serve a local live dashboard with project status, tasks, coverage, and traces."""
21
+ if ctx.invoked_subcommand is not None:
22
+ return
23
+ root = project_root.expanduser().resolve()
24
+ initialize_project(root, quiet=True)
25
+ console.print(f"Serving DevCouncil dashboard at http://{host}:{port}")
26
+ run_dashboard(root, host=host, port=port)
@@ -1,15 +1,18 @@
1
- import typer
1
+ import typer
2
2
  import subprocess
3
3
  import os
4
4
  import shutil
5
5
  from pathlib import Path
6
- from rich.console import Console
7
- from rich.table import Table
8
-
6
+ from rich.console import Console
7
+ from rich.table import Table
8
+
9
+ from devcouncil.app.config import load_config, load_local_secrets, provider_api_key_env_var
10
+ from devcouncil.llm.provider import SUPPORTED_MODEL_PROVIDERS, validate_model_provider
11
+
9
12
  app = typer.Typer()
10
13
  console = Console()
11
14
 
12
- def render_doctor_check():
15
+ def render_doctor_check(project_root: Path = Path(".")):
13
16
  def _command_version(command: list[str]) -> str | None:
14
17
  executable = shutil.which(command[0])
15
18
  if not executable:
@@ -30,32 +33,32 @@ def render_doctor_check():
30
33
  ).splitlines()[0].strip()
31
34
  except Exception:
32
35
  return None
33
-
34
- table = Table(title="DevCouncil Doctor Check")
35
- table.add_column("Component", style="cyan")
36
- table.add_column("Status", style="magenta")
37
- table.add_column("Notes", style="green")
38
-
39
- # Check Git
40
- git_ver = _command_version(["git", "--version"])
41
- if git_ver:
42
- table.add_row("Git", "[green]OK[/green]", git_ver)
43
- else:
44
- table.add_row("Git", "[red]Missing[/red]", "Git is required for repo mapping and checkpoints.")
45
-
46
- # Check uv
47
- uv_ver = _command_version(["uv", "--version"])
48
- if uv_ver:
49
- table.add_row("uv", "[green]OK[/green]", uv_ver)
50
- else:
51
- table.add_row("uv", "[red]Missing[/red]", "Install uv to run or install DevCouncil.")
52
-
53
- # Check CLI shims
54
- if shutil.which("devcouncil"):
55
- table.add_row("devcouncil CLI", "[green]OK[/green]", "Found on PATH.")
56
- else:
57
- table.add_row("devcouncil CLI", "[yellow]Missing[/yellow]", "Run via 'uv run devcouncil' or install with 'uv tool install --force .'.")
58
-
36
+
37
+ table = Table(title="DevCouncil Doctor Check")
38
+ table.add_column("Component", style="cyan")
39
+ table.add_column("Status", style="magenta")
40
+ table.add_column("Notes", style="green")
41
+
42
+ # Check Git
43
+ git_ver = _command_version(["git", "--version"])
44
+ if git_ver:
45
+ table.add_row("Git", "[green]OK[/green]", git_ver)
46
+ else:
47
+ table.add_row("Git", "[red]Missing[/red]", "Git is required for repo mapping and checkpoints.")
48
+
49
+ # Check uv
50
+ uv_ver = _command_version(["uv", "--version"])
51
+ if uv_ver:
52
+ table.add_row("uv", "[green]OK[/green]", uv_ver)
53
+ else:
54
+ table.add_row("uv", "[red]Missing[/red]", "Install uv to run or install DevCouncil.")
55
+
56
+ # Check CLI shims
57
+ if shutil.which("devcouncil"):
58
+ table.add_row("devcouncil CLI", "[green]OK[/green]", "Found on PATH.")
59
+ else:
60
+ table.add_row("devcouncil CLI", "[yellow]Missing[/yellow]", "Run via 'uv run devcouncil' or install with 'uv tool install --force .'.")
61
+
59
62
  # Check ripgrep
60
63
  rg_ver = _command_version(["rg", "--version"])
61
64
  if rg_ver:
@@ -66,31 +69,72 @@ def render_doctor_check():
66
69
  # Check supported coding CLIs
67
70
  codex_ver = _command_version(["codex", "--version"])
68
71
  if codex_ver:
69
- table.add_row("Codex CLI", "[green]OK[/green]", f"{codex_ver}. Setup: dev integrate codex --apply")
72
+ table.add_row(
73
+ "Codex CLI",
74
+ "[green]OK[/green]",
75
+ f"{codex_ver}. Setup: dev integrate codex --apply (or dev setup --integrate --apply).",
76
+ )
70
77
  else:
71
- table.add_row("Codex CLI", "[yellow]Missing[/yellow]", "Optional. Install Codex, then run 'dev integrate codex --apply'.")
78
+ table.add_row(
79
+ "Codex CLI",
80
+ "[yellow]Missing[/yellow]",
81
+ "Optional. Install Codex, then run 'dev integrate codex --apply' (or 'dev setup --integrate --apply').",
82
+ )
72
83
 
73
84
  gemini_ver = _command_version(["gemini", "--version"])
74
85
  if gemini_ver:
75
- table.add_row("Gemini CLI", "[green]OK[/green]", f"{gemini_ver}. Setup: dev integrate gemini --apply")
86
+ table.add_row(
87
+ "Gemini CLI",
88
+ "[green]OK[/green]",
89
+ f"{gemini_ver}. Setup: dev integrate gemini --apply (or dev setup --integrate --apply).",
90
+ )
76
91
  else:
77
- table.add_row("Gemini CLI", "[yellow]Missing[/yellow]", "Optional. Install Gemini CLI, then run 'dev integrate gemini --apply'.")
92
+ table.add_row(
93
+ "Gemini CLI",
94
+ "[yellow]Missing[/yellow]",
95
+ "Optional. Install Gemini CLI, then run 'dev integrate gemini --apply' (or 'dev setup --integrate --apply').",
96
+ )
78
97
 
79
- # Check OpenRouter API Key
80
- if os.environ.get("OPENROUTER_API_KEY"):
81
- table.add_row("OPENROUTER_API_KEY", "[green]OK[/green]", "Found in environment.")
82
- else:
83
- table.add_row("OPENROUTER_API_KEY", "[yellow]Missing[/yellow]", "Required if using OpenRouter provider.")
98
+ try:
99
+ provider = load_config(project_root).models.provider
100
+ except Exception:
101
+ provider = "openrouter"
102
+ try:
103
+ provider = validate_model_provider(provider)
104
+ except ValueError:
105
+ supported = ", ".join(SUPPORTED_MODEL_PROVIDERS)
106
+ table.add_row(
107
+ "models.provider",
108
+ "[red]Unsupported[/red]",
109
+ f"{provider} is configured, but this runtime supports: {supported}.",
110
+ )
111
+ console.print(table)
112
+ return
113
+ env_var = provider_api_key_env_var(provider)
114
+ local_secrets = load_local_secrets(project_root)
115
+ if os.environ.get(env_var):
116
+ table.add_row(env_var, "[green]OK[/green]", f"Found in environment for {provider}.")
117
+ elif local_secrets.get(env_var):
118
+ table.add_row(env_var, "[green]OK[/green]", f"Found in .devcouncil/secrets.env for {provider}.")
119
+ else:
120
+ table.add_row(env_var, "[yellow]Missing[/yellow]", f"Required if using {provider} provider. Run 'dev setup'.")
84
121
 
85
122
  console.print(table)
86
123
 
87
124
 
88
125
  @app.callback(invoke_without_command=True)
89
- def doctor(ctx: typer.Context):
126
+ def doctor(
127
+ ctx: typer.Context,
128
+ project_root: Path = typer.Option(
129
+ Path("."),
130
+ "--project-root",
131
+ help="Repository root containing .devcouncil/config.yaml.",
132
+ ),
133
+ ):
90
134
  """
91
135
  Check the environment for DevCouncil prerequisites.
92
136
  """
93
137
  if ctx.invoked_subcommand is not None:
94
138
  return
95
139
 
96
- render_doctor_check()
140
+ render_doctor_check(project_root.expanduser().resolve())