devcouncil 0.1.1 → 0.2.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 (146) hide show
  1. package/README.md +218 -96
  2. package/bin/devcouncil.js +130 -32
  3. package/package.json +7 -11
  4. package/pyproject.toml +0 -34
  5. package/src/devcouncil/__init__.py +0 -0
  6. package/src/devcouncil/__main__.py +0 -4
  7. package/src/devcouncil/app/__init__.py +0 -28
  8. package/src/devcouncil/app/config.py +0 -158
  9. package/src/devcouncil/app/errors.py +0 -23
  10. package/src/devcouncil/app/events.py +0 -44
  11. package/src/devcouncil/app/orchestrator.py +0 -92
  12. package/src/devcouncil/app/project_status.py +0 -29
  13. package/src/devcouncil/app/run_context.py +0 -39
  14. package/src/devcouncil/app/state_machine.py +0 -108
  15. package/src/devcouncil/artifacts/__init__.py +0 -1
  16. package/src/devcouncil/artifacts/coverage.py +0 -96
  17. package/src/devcouncil/artifacts/graph.py +0 -143
  18. package/src/devcouncil/artifacts/migrations.py +0 -20
  19. package/src/devcouncil/artifacts/schemas.py +0 -23
  20. package/src/devcouncil/artifacts/serializer.py +0 -21
  21. package/src/devcouncil/artifacts/validators.py +0 -27
  22. package/src/devcouncil/cli/__init__.py +0 -0
  23. package/src/devcouncil/cli/commands/__init__.py +0 -0
  24. package/src/devcouncil/cli/commands/artifacts.py +0 -51
  25. package/src/devcouncil/cli/commands/ast.py +0 -22
  26. package/src/devcouncil/cli/commands/baseline.py +0 -35
  27. package/src/devcouncil/cli/commands/config.py +0 -76
  28. package/src/devcouncil/cli/commands/dashboard.py +0 -26
  29. package/src/devcouncil/cli/commands/doctor.py +0 -140
  30. package/src/devcouncil/cli/commands/go.py +0 -237
  31. package/src/devcouncil/cli/commands/hook.py +0 -128
  32. package/src/devcouncil/cli/commands/init.py +0 -153
  33. package/src/devcouncil/cli/commands/integrate.py +0 -726
  34. package/src/devcouncil/cli/commands/lsp.py +0 -20
  35. package/src/devcouncil/cli/commands/map.py +0 -42
  36. package/src/devcouncil/cli/commands/mcp_server.py +0 -18
  37. package/src/devcouncil/cli/commands/plan.py +0 -327
  38. package/src/devcouncil/cli/commands/prompt.py +0 -50
  39. package/src/devcouncil/cli/commands/repair.py +0 -72
  40. package/src/devcouncil/cli/commands/report.py +0 -129
  41. package/src/devcouncil/cli/commands/reset_demo_state.py +0 -31
  42. package/src/devcouncil/cli/commands/rollback.py +0 -60
  43. package/src/devcouncil/cli/commands/run.py +0 -269
  44. package/src/devcouncil/cli/commands/setup.py +0 -223
  45. package/src/devcouncil/cli/commands/show.py +0 -76
  46. package/src/devcouncil/cli/commands/status.py +0 -117
  47. package/src/devcouncil/cli/commands/tasks.py +0 -55
  48. package/src/devcouncil/cli/commands/trace.py +0 -44
  49. package/src/devcouncil/cli/commands/verify.py +0 -193
  50. package/src/devcouncil/cli/commands/version.py +0 -20
  51. package/src/devcouncil/cli/commands/watch.py +0 -574
  52. package/src/devcouncil/cli/main.py +0 -88
  53. package/src/devcouncil/council/__init__.py +0 -0
  54. package/src/devcouncil/council/prompts/__init__.py +0 -0
  55. package/src/devcouncil/council/prompts/arbiter.md +0 -19
  56. package/src/devcouncil/council/prompts/critic_a.md +0 -10
  57. package/src/devcouncil/council/prompts/critic_b.md +0 -10
  58. package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
  59. package/src/devcouncil/council/prompts/planner_a.md +0 -16
  60. package/src/devcouncil/council/prompts/planner_b.md +0 -16
  61. package/src/devcouncil/council/prompts/rebuttal.md +0 -10
  62. package/src/devcouncil/council/prompts/spec_writer.md +0 -12
  63. package/src/devcouncil/domain/__init__.py +0 -0
  64. package/src/devcouncil/domain/assumption.py +0 -17
  65. package/src/devcouncil/domain/critique.py +0 -32
  66. package/src/devcouncil/domain/evidence.py +0 -27
  67. package/src/devcouncil/domain/gap.py +0 -26
  68. package/src/devcouncil/domain/requirement.py +0 -22
  69. package/src/devcouncil/domain/task.py +0 -26
  70. package/src/devcouncil/execution/__init__.py +0 -1
  71. package/src/devcouncil/execution/context_builder.py +0 -60
  72. package/src/devcouncil/execution/executor.py +0 -15
  73. package/src/devcouncil/execution/hook_policy.py +0 -165
  74. package/src/devcouncil/execution/patch.py +0 -28
  75. package/src/devcouncil/execution/paths.py +0 -14
  76. package/src/devcouncil/execution/permissions.py +0 -92
  77. package/src/devcouncil/execution/prompt_builder.py +0 -59
  78. package/src/devcouncil/execution/task_runner.py +0 -166
  79. package/src/devcouncil/executors/__init__.py +0 -1
  80. package/src/devcouncil/executors/coding_cli.py +0 -112
  81. package/src/devcouncil/executors/mini_swe.py +0 -73
  82. package/src/devcouncil/executors/native/__init__.py +0 -0
  83. package/src/devcouncil/executors/native/agent.py +0 -107
  84. package/src/devcouncil/executors/openhands.py +0 -71
  85. package/src/devcouncil/gating/__init__.py +0 -1
  86. package/src/devcouncil/gating/checks/__init__.py +0 -0
  87. package/src/devcouncil/gating/checks/clean_git.py +0 -50
  88. package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
  89. package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
  90. package/src/devcouncil/gating/checks/secret_scan_check.py +0 -34
  91. package/src/devcouncil/gating/policy.py +0 -190
  92. package/src/devcouncil/indexing/__init__.py +0 -1
  93. package/src/devcouncil/indexing/ast_matcher.py +0 -168
  94. package/src/devcouncil/indexing/graph_index.py +0 -48
  95. package/src/devcouncil/indexing/lsp.py +0 -120
  96. package/src/devcouncil/indexing/repo_mapper.py +0 -208
  97. package/src/devcouncil/indexing/symbol_index.py +0 -0
  98. package/src/devcouncil/integrations/code_review_graph.py +0 -163
  99. package/src/devcouncil/integrations/github.py +0 -39
  100. package/src/devcouncil/integrations/gitnexus.py +0 -27
  101. package/src/devcouncil/integrations/graphify.py +0 -34
  102. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  103. package/src/devcouncil/integrations/mcp/server.py +0 -599
  104. package/src/devcouncil/integrations/pr_comments.py +0 -62
  105. package/src/devcouncil/live/__init__.py +0 -2
  106. package/src/devcouncil/live/cards.py +0 -207
  107. package/src/devcouncil/live/models.py +0 -63
  108. package/src/devcouncil/live/repair_prompt.py +0 -83
  109. package/src/devcouncil/live/reviewer.py +0 -70
  110. package/src/devcouncil/live/signals.py +0 -135
  111. package/src/devcouncil/live/summary.py +0 -34
  112. package/src/devcouncil/live/tasks.py +0 -18
  113. package/src/devcouncil/live/transcripts.py +0 -138
  114. package/src/devcouncil/llm/__init__.py +0 -1
  115. package/src/devcouncil/llm/cache.py +0 -38
  116. package/src/devcouncil/llm/provider.py +0 -146
  117. package/src/devcouncil/llm/router.py +0 -125
  118. package/src/devcouncil/planning/__init__.py +0 -1
  119. package/src/devcouncil/planning/arbiter_service.py +0 -57
  120. package/src/devcouncil/planning/critique_service.py +0 -66
  121. package/src/devcouncil/planning/plan_service.py +0 -46
  122. package/src/devcouncil/planning/prompt_enhancer_service.py +0 -86
  123. package/src/devcouncil/planning/repair_service.py +0 -39
  124. package/src/devcouncil/planning/spec_service.py +0 -44
  125. package/src/devcouncil/repo/__init__.py +0 -0
  126. package/src/devcouncil/reporting/__init__.py +0 -0
  127. package/src/devcouncil/reporting/github_check.py +0 -32
  128. package/src/devcouncil/reporting/json_report.py +0 -20
  129. package/src/devcouncil/reporting/markdown_report.py +0 -68
  130. package/src/devcouncil/reporting/report_builder.py +0 -14
  131. package/src/devcouncil/storage/__init__.py +0 -0
  132. package/src/devcouncil/storage/db.py +0 -66
  133. package/src/devcouncil/storage/models.py +0 -83
  134. package/src/devcouncil/storage/repositories.py +0 -423
  135. package/src/devcouncil/telemetry/__init__.py +0 -0
  136. package/src/devcouncil/telemetry/cost.py +0 -34
  137. package/src/devcouncil/telemetry/traces.py +0 -91
  138. package/src/devcouncil/telemetry/tracker.py +0 -49
  139. package/src/devcouncil/ui/__init__.py +0 -1
  140. package/src/devcouncil/ui/dashboard.py +0 -122
  141. package/src/devcouncil/utils/__init__.py +0 -1
  142. package/src/devcouncil/utils/redaction.py +0 -141
  143. package/src/devcouncil/verification/__init__.py +0 -1
  144. package/src/devcouncil/verification/implementation_reviewer.py +0 -55
  145. package/src/devcouncil/verification/verifier.py +0 -530
  146. package/uv.lock +0 -1085
@@ -1,143 +0,0 @@
1
- """Artifact graph: the persistent, directed graph that links requirements to evidence.
2
-
3
- This is DevCouncil's core data structure:
4
- Requirement -> AcceptanceCriterion -> Task -> PlannedFile -> ChangedFile
5
- -> CommandResult -> TestEvidence -> Gap
6
-
7
- The graph enables coverage queries like:
8
- - Which requirements have no tasks?
9
- - Which tasks produced no changed files?
10
- - Which acceptance criteria have no evidence?
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- from dataclasses import dataclass, field
16
- from typing import Any, Dict, List, Set, Tuple
17
-
18
- from devcouncil.domain.requirement import Requirement, AcceptanceCriterion
19
- from devcouncil.domain.task import Task
20
- from devcouncil.domain.assumption import Assumption
21
- from devcouncil.domain.evidence import CommandResult, DiffEvidence, TestEvidence
22
- from devcouncil.domain.gap import Gap
23
- from devcouncil.domain.critique import CritiqueFinding
24
-
25
-
26
- @dataclass
27
- class ArtifactGraph:
28
- """Directed graph of all planning and execution artifacts."""
29
-
30
- requirements: Dict[str, Requirement] = field(default_factory=dict)
31
- tasks: Dict[str, Task] = field(default_factory=dict)
32
- assumptions: Dict[str, Assumption] = field(default_factory=dict)
33
- findings: Dict[str, CritiqueFinding] = field(default_factory=dict)
34
- gaps: Dict[str, Gap] = field(default_factory=dict)
35
- test_evidence: List[TestEvidence] = field(default_factory=list)
36
- diff_evidence: List[DiffEvidence] = field(default_factory=list)
37
- command_results: List[CommandResult] = field(default_factory=list)
38
-
39
- # --- Mutation ---
40
-
41
- def add_requirement(self, req: Requirement) -> None:
42
- self.requirements[req.id] = req
43
-
44
- def add_task(self, task: Task) -> None:
45
- self.tasks[task.id] = task
46
-
47
- def add_assumption(self, asm: Assumption) -> None:
48
- self.assumptions[asm.id] = asm
49
-
50
- def add_finding(self, finding: CritiqueFinding) -> None:
51
- self.findings[finding.id] = finding
52
-
53
- def add_gap(self, gap: Gap) -> None:
54
- self.gaps[gap.id] = gap
55
-
56
- def add_test_evidence(self, ev: TestEvidence) -> None:
57
- self.test_evidence.append(ev)
58
-
59
- def add_diff_evidence(self, ev: DiffEvidence) -> None:
60
- self.diff_evidence.append(ev)
61
-
62
- def add_command_result(self, cr: CommandResult) -> None:
63
- self.command_results.append(cr)
64
-
65
- # --- Coverage Queries ---
66
-
67
- def requirements_without_tasks(self) -> List[Requirement]:
68
- """Requirements that are not mapped to any task."""
69
- task_req_ids: Set[str] = set()
70
- for task in self.tasks.values():
71
- task_req_ids.update(task.requirement_ids)
72
- return [r for r in self.requirements.values() if r.id not in task_req_ids]
73
-
74
- def tasks_without_requirements(self) -> List[Task]:
75
- """Tasks that don't map back to any requirement."""
76
- return [t for t in self.tasks.values() if not t.requirement_ids]
77
-
78
- def requirements_without_acceptance_criteria(self) -> List[Requirement]:
79
- """Requirements with no acceptance criteria defined."""
80
- return [r for r in self.requirements.values() if not r.acceptance_criteria]
81
-
82
- def acceptance_criteria_without_evidence(self) -> List[Tuple[str, AcceptanceCriterion]]:
83
- """AC IDs that have no test evidence mapped to them."""
84
- evidenced_ac_ids: Set[str] = set()
85
- for ev in self.test_evidence:
86
- evidenced_ac_ids.add(ev.acceptance_criterion_id)
87
-
88
- results: List[Tuple[str, AcceptanceCriterion]] = []
89
- for req in self.requirements.values():
90
- for ac in req.acceptance_criteria:
91
- if ac.id not in evidenced_ac_ids:
92
- results.append((req.id, ac))
93
- return results
94
-
95
- def tasks_without_changed_files(self) -> List[Task]:
96
- """Tasks that have not produced any diff evidence."""
97
- tasks_with_diffs: Set[str] = set()
98
- for de in self.diff_evidence:
99
- tasks_with_diffs.add(de.task_id)
100
- return [
101
- t for t in self.tasks.values()
102
- if t.id not in tasks_with_diffs and t.status not in ("planned", "ready")
103
- ]
104
-
105
- def open_findings(self, min_severity: str = "low") -> List[CritiqueFinding]:
106
- """Critique findings that are still open."""
107
- severity_order = {"low": 0, "medium": 1, "high": 2, "critical": 3}
108
- min_rank = severity_order.get(min_severity, 0)
109
- return [
110
- f for f in self.findings.values()
111
- if f.status == "open" and severity_order.get(f.severity, 0) >= min_rank
112
- ]
113
-
114
- def blocking_gaps(self) -> List[Gap]:
115
- """Gaps that are blocking progress."""
116
- return [g for g in self.gaps.values() if g.blocking]
117
-
118
- def unconfirmed_high_impact_assumptions(self) -> List[Assumption]:
119
- """Assumptions with high impact that are still open."""
120
- return [
121
- a for a in self.assumptions.values()
122
- if a.status == "open" and a.impact == "high"
123
- ]
124
-
125
- # --- Aggregate ---
126
-
127
- def coverage_summary(self) -> Dict[str, Any]:
128
- """Produce a coverage summary for reporting."""
129
- return {
130
- "total_requirements": len(self.requirements),
131
- "requirements_without_tasks": len(self.requirements_without_tasks()),
132
- "requirements_without_ac": len(self.requirements_without_acceptance_criteria()),
133
- "total_tasks": len(self.tasks),
134
- "tasks_without_requirements": len(self.tasks_without_requirements()),
135
- "tasks_without_diffs": len(self.tasks_without_changed_files()),
136
- "total_ac": sum(len(r.acceptance_criteria) for r in self.requirements.values()),
137
- "ac_without_evidence": len(self.acceptance_criteria_without_evidence()),
138
- "total_gaps": len(self.gaps),
139
- "blocking_gaps": len(self.blocking_gaps()),
140
- "open_findings": len(self.open_findings()),
141
- "high_critical_open_findings": len(self.open_findings("high")),
142
- "unconfirmed_high_assumptions": len(self.unconfirmed_high_impact_assumptions()),
143
- }
@@ -1,20 +0,0 @@
1
- from typing import Any, Dict
2
-
3
- class ArtifactMigrator:
4
- """Migrates artifact schemas across DevCouncil versions."""
5
-
6
- @staticmethod
7
- def migrate_requirement(data: Dict[str, Any]) -> Dict[str, Any]:
8
- """Upgrade requirement payload to current schema."""
9
- if "priority" not in data:
10
- data["priority"] = "medium"
11
- return data
12
-
13
- @staticmethod
14
- def migrate_task(data: Dict[str, Any]) -> Dict[str, Any]:
15
- """Upgrade task payload to current schema."""
16
- if "forbidden_changes" not in data:
17
- data["forbidden_changes"] = []
18
- if "expected_tests" not in data:
19
- data["expected_tests"] = []
20
- return data
@@ -1,23 +0,0 @@
1
- from typing import List
2
- from pydantic import BaseModel, Field
3
-
4
- # Common schemas that might be shared across artifacts
5
- class FileModification(BaseModel):
6
- path: str
7
- diff: str
8
-
9
- class CoverageMatrix(BaseModel):
10
- """Represents a requirement to task/test mapping coverage."""
11
- requirement_id: str
12
- task_ids: List[str] = Field(default_factory=list)
13
- test_evidence_ids: List[str] = Field(default_factory=list)
14
- is_covered: bool = False
15
- missing_tasks: bool = False
16
- missing_tests: bool = False
17
-
18
- class ReportSchema(BaseModel):
19
- project_id: str
20
- tasks_completed: int
21
- tasks_blocked: int
22
- open_gaps: int
23
- coverage_matrix: List[CoverageMatrix]
@@ -1,21 +0,0 @@
1
- import json
2
- from typing import Any, Dict, TypeVar, Type
3
- from pydantic import BaseModel
4
-
5
- T = TypeVar("T", bound=BaseModel)
6
-
7
- class ArtifactSerializer:
8
- """Handles serialization and deserialization of DevCouncil artifacts."""
9
-
10
- @staticmethod
11
- def to_json(artifact: BaseModel) -> str:
12
- return artifact.model_dump_json(indent=2)
13
-
14
- @staticmethod
15
- def from_json(json_str: str, model_class: Type[T]) -> T:
16
- data = json.loads(json_str)
17
- return model_class.model_validate(data)
18
-
19
- @staticmethod
20
- def to_dict(artifact: BaseModel) -> Dict[str, Any]:
21
- return artifact.model_dump()
@@ -1,27 +0,0 @@
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.")
File without changes
File without changes
@@ -1,51 +0,0 @@
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)
@@ -1,22 +0,0 @@
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,35 +0,0 @@
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,76 +0,0 @@
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]")
@@ -1,26 +0,0 @@
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,140 +0,0 @@
1
- import typer
2
- import subprocess
3
- import os
4
- import shutil
5
- from pathlib import Path
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
-
12
- app = typer.Typer()
13
- console = Console()
14
-
15
- def render_doctor_check(project_root: Path = Path(".")):
16
- def _command_version(command: list[str]) -> str | None:
17
- executable = shutil.which(command[0])
18
- if not executable:
19
- return None
20
-
21
- resolved_command = [executable, *command[1:]]
22
- use_shell = os.name == "nt" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
23
- invocation = subprocess.list2cmdline(resolved_command) if use_shell else resolved_command
24
- try:
25
- return subprocess.check_output(
26
- invocation,
27
- stderr=subprocess.STDOUT,
28
- text=True,
29
- encoding="utf-8",
30
- errors="replace",
31
- shell=use_shell,
32
- timeout=10,
33
- ).splitlines()[0].strip()
34
- except Exception:
35
- return None
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
-
62
- # Check ripgrep
63
- rg_ver = _command_version(["rg", "--version"])
64
- if rg_ver:
65
- table.add_row("ripgrep (rg)", "[green]OK[/green]", rg_ver)
66
- else:
67
- table.add_row("ripgrep (rg)", "[yellow]Missing[/yellow]", "ripgrep is highly recommended for fast repo mapping.")
68
-
69
- # Check supported coding CLIs
70
- codex_ver = _command_version(["codex", "--version"])
71
- if codex_ver:
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
- )
77
- else:
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
- )
83
-
84
- gemini_ver = _command_version(["gemini", "--version"])
85
- if gemini_ver:
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
- )
91
- else:
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
- )
97
-
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'.")
121
-
122
- console.print(table)
123
-
124
-
125
- @app.callback(invoke_without_command=True)
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
- ):
134
- """
135
- Check the environment for DevCouncil prerequisites.
136
- """
137
- if ctx.invoked_subcommand is not None:
138
- return
139
-
140
- render_doctor_check(project_root.expanduser().resolve())