devcouncil 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +643 -0
- package/bin/devcouncil.js +62 -0
- package/package.json +47 -0
- package/pyproject.toml +31 -0
- package/src/devcouncil/__init__.py +0 -0
- package/src/devcouncil/__main__.py +4 -0
- package/src/devcouncil/app/__init__.py +28 -0
- package/src/devcouncil/app/config.py +131 -0
- package/src/devcouncil/app/errors.py +23 -0
- package/src/devcouncil/app/events.py +44 -0
- package/src/devcouncil/app/orchestrator.py +92 -0
- package/src/devcouncil/app/run_context.py +39 -0
- package/src/devcouncil/app/state_machine.py +108 -0
- package/src/devcouncil/artifacts/__init__.py +1 -0
- package/src/devcouncil/artifacts/coverage.py +96 -0
- package/src/devcouncil/artifacts/graph.py +143 -0
- package/src/devcouncil/artifacts/migrations.py +20 -0
- package/src/devcouncil/artifacts/schemas.py +23 -0
- package/src/devcouncil/artifacts/serializer.py +21 -0
- package/src/devcouncil/artifacts/validators.py +27 -0
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/artifacts.py +48 -0
- package/src/devcouncil/cli/commands/baseline.py +32 -0
- package/src/devcouncil/cli/commands/config.py +54 -0
- package/src/devcouncil/cli/commands/doctor.py +96 -0
- package/src/devcouncil/cli/commands/hook.py +61 -0
- package/src/devcouncil/cli/commands/init.py +142 -0
- package/src/devcouncil/cli/commands/integrate.py +420 -0
- package/src/devcouncil/cli/commands/map.py +38 -0
- package/src/devcouncil/cli/commands/mcp_server.py +18 -0
- package/src/devcouncil/cli/commands/plan.py +276 -0
- package/src/devcouncil/cli/commands/prompt.py +47 -0
- package/src/devcouncil/cli/commands/repair.py +69 -0
- package/src/devcouncil/cli/commands/report.py +71 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +28 -0
- package/src/devcouncil/cli/commands/rollback.py +58 -0
- package/src/devcouncil/cli/commands/run.py +224 -0
- package/src/devcouncil/cli/commands/setup.py +82 -0
- package/src/devcouncil/cli/commands/show.py +57 -0
- package/src/devcouncil/cli/commands/status.py +105 -0
- package/src/devcouncil/cli/commands/tasks.py +41 -0
- package/src/devcouncil/cli/commands/trace.py +43 -0
- package/src/devcouncil/cli/commands/verify.py +163 -0
- package/src/devcouncil/cli/commands/version.py +20 -0
- package/src/devcouncil/cli/main.py +70 -0
- package/src/devcouncil/council/__init__.py +0 -0
- package/src/devcouncil/council/prompts/__init__.py +0 -0
- package/src/devcouncil/council/prompts/arbiter.md +19 -0
- package/src/devcouncil/council/prompts/critic_a.md +10 -0
- package/src/devcouncil/council/prompts/critic_b.md +10 -0
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -0
- package/src/devcouncil/council/prompts/planner_a.md +16 -0
- package/src/devcouncil/council/prompts/planner_b.md +16 -0
- package/src/devcouncil/council/prompts/rebuttal.md +10 -0
- package/src/devcouncil/council/prompts/spec_writer.md +12 -0
- package/src/devcouncil/domain/__init__.py +0 -0
- package/src/devcouncil/domain/assumption.py +17 -0
- package/src/devcouncil/domain/critique.py +32 -0
- package/src/devcouncil/domain/evidence.py +27 -0
- package/src/devcouncil/domain/gap.py +26 -0
- package/src/devcouncil/domain/requirement.py +22 -0
- package/src/devcouncil/domain/task.py +26 -0
- package/src/devcouncil/execution/__init__.py +1 -0
- package/src/devcouncil/execution/context_builder.py +60 -0
- package/src/devcouncil/execution/executor.py +15 -0
- package/src/devcouncil/execution/hook_policy.py +144 -0
- package/src/devcouncil/execution/patch.py +28 -0
- package/src/devcouncil/execution/paths.py +14 -0
- package/src/devcouncil/execution/permissions.py +92 -0
- package/src/devcouncil/execution/prompt_builder.py +59 -0
- package/src/devcouncil/execution/task_runner.py +166 -0
- package/src/devcouncil/executors/__init__.py +1 -0
- package/src/devcouncil/executors/mini_swe.py +73 -0
- package/src/devcouncil/executors/native/__init__.py +0 -0
- package/src/devcouncil/executors/native/agent.py +107 -0
- package/src/devcouncil/executors/openhands.py +71 -0
- package/src/devcouncil/gating/__init__.py +1 -0
- package/src/devcouncil/gating/checks/__init__.py +0 -0
- package/src/devcouncil/gating/checks/clean_git.py +45 -0
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -0
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -0
- package/src/devcouncil/gating/checks/secret_scan_check.py +34 -0
- package/src/devcouncil/gating/policy.py +190 -0
- package/src/devcouncil/indexing/__init__.py +1 -0
- package/src/devcouncil/indexing/graph_index.py +48 -0
- package/src/devcouncil/indexing/repo_mapper.py +204 -0
- package/src/devcouncil/indexing/symbol_index.py +0 -0
- package/src/devcouncil/integrations/code_review_graph.py +163 -0
- package/src/devcouncil/integrations/github.py +39 -0
- package/src/devcouncil/integrations/gitnexus.py +27 -0
- package/src/devcouncil/integrations/graphify.py +34 -0
- package/src/devcouncil/integrations/mcp/__init__.py +0 -0
- package/src/devcouncil/integrations/mcp/server.py +146 -0
- package/src/devcouncil/llm/__init__.py +1 -0
- package/src/devcouncil/llm/cache.py +38 -0
- package/src/devcouncil/llm/provider.py +125 -0
- package/src/devcouncil/llm/router.py +125 -0
- package/src/devcouncil/planning/__init__.py +1 -0
- package/src/devcouncil/planning/arbiter_service.py +57 -0
- package/src/devcouncil/planning/critique_service.py +66 -0
- package/src/devcouncil/planning/plan_service.py +46 -0
- package/src/devcouncil/planning/repair_service.py +39 -0
- package/src/devcouncil/planning/spec_service.py +44 -0
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/reporting/__init__.py +0 -0
- package/src/devcouncil/reporting/github_check.py +32 -0
- package/src/devcouncil/reporting/json_report.py +17 -0
- package/src/devcouncil/reporting/markdown_report.py +46 -0
- package/src/devcouncil/reporting/report_builder.py +14 -0
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +66 -0
- package/src/devcouncil/storage/models.py +83 -0
- package/src/devcouncil/storage/repositories.py +346 -0
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +34 -0
- package/src/devcouncil/telemetry/traces.py +91 -0
- package/src/devcouncil/telemetry/tracker.py +49 -0
- package/src/devcouncil/utils/__init__.py +1 -0
- package/src/devcouncil/utils/redaction.py +141 -0
- package/src/devcouncil/verification/__init__.py +1 -0
- package/src/devcouncil/verification/implementation_reviewer.py +55 -0
- package/src/devcouncil/verification/verifier.py +513 -0
- package/uv.lock +1085 -0
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
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
|
|
@@ -0,0 +1,23 @@
|
|
|
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]
|
|
@@ -0,0 +1,21 @@
|
|
|
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()
|
|
@@ -0,0 +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.")
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,48 @@
|
|
|
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)
|
|
@@ -0,0 +1,32 @@
|
|
|
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]")
|
|
@@ -0,0 +1,54 @@
|
|
|
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]")
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
app = typer.Typer()
|
|
10
|
+
console = Console()
|
|
11
|
+
|
|
12
|
+
def render_doctor_check():
|
|
13
|
+
def _command_version(command: list[str]) -> str | None:
|
|
14
|
+
executable = shutil.which(command[0])
|
|
15
|
+
if not executable:
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
resolved_command = [executable, *command[1:]]
|
|
19
|
+
use_shell = os.name == "nt" and Path(executable).suffix.lower() in {".bat", ".cmd", ".ps1"}
|
|
20
|
+
invocation = subprocess.list2cmdline(resolved_command) if use_shell else resolved_command
|
|
21
|
+
try:
|
|
22
|
+
return subprocess.check_output(
|
|
23
|
+
invocation,
|
|
24
|
+
stderr=subprocess.STDOUT,
|
|
25
|
+
text=True,
|
|
26
|
+
encoding="utf-8",
|
|
27
|
+
errors="replace",
|
|
28
|
+
shell=use_shell,
|
|
29
|
+
timeout=10,
|
|
30
|
+
).splitlines()[0].strip()
|
|
31
|
+
except Exception:
|
|
32
|
+
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
|
+
|
|
59
|
+
# Check ripgrep
|
|
60
|
+
rg_ver = _command_version(["rg", "--version"])
|
|
61
|
+
if rg_ver:
|
|
62
|
+
table.add_row("ripgrep (rg)", "[green]OK[/green]", rg_ver)
|
|
63
|
+
else:
|
|
64
|
+
table.add_row("ripgrep (rg)", "[yellow]Missing[/yellow]", "ripgrep is highly recommended for fast repo mapping.")
|
|
65
|
+
|
|
66
|
+
# Check supported coding CLIs
|
|
67
|
+
codex_ver = _command_version(["codex", "--version"])
|
|
68
|
+
if codex_ver:
|
|
69
|
+
table.add_row("Codex CLI", "[green]OK[/green]", f"{codex_ver}. Setup: dev integrate codex --apply")
|
|
70
|
+
else:
|
|
71
|
+
table.add_row("Codex CLI", "[yellow]Missing[/yellow]", "Optional. Install Codex, then run 'dev integrate codex --apply'.")
|
|
72
|
+
|
|
73
|
+
gemini_ver = _command_version(["gemini", "--version"])
|
|
74
|
+
if gemini_ver:
|
|
75
|
+
table.add_row("Gemini CLI", "[green]OK[/green]", f"{gemini_ver}. Setup: dev integrate gemini --apply")
|
|
76
|
+
else:
|
|
77
|
+
table.add_row("Gemini CLI", "[yellow]Missing[/yellow]", "Optional. Install Gemini CLI, then run 'dev integrate gemini --apply'.")
|
|
78
|
+
|
|
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.")
|
|
84
|
+
|
|
85
|
+
console.print(table)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@app.callback(invoke_without_command=True)
|
|
89
|
+
def doctor(ctx: typer.Context):
|
|
90
|
+
"""
|
|
91
|
+
Check the environment for DevCouncil prerequisites.
|
|
92
|
+
"""
|
|
93
|
+
if ctx.invoked_subcommand is not None:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
render_doctor_check()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from devcouncil.storage.db import get_db
|
|
8
|
+
from devcouncil.storage.repositories import TaskRepository
|
|
9
|
+
from devcouncil.execution.hook_policy import HookPolicy
|
|
10
|
+
|
|
11
|
+
app = typer.Typer()
|
|
12
|
+
console = Console()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _project_root() -> Path:
|
|
16
|
+
configured = os.environ.get("DEVCOUNCIL_PROJECT_ROOT")
|
|
17
|
+
return Path(configured).expanduser().resolve() if configured else Path(".").resolve()
|
|
18
|
+
|
|
19
|
+
@app.command()
|
|
20
|
+
def pre_tool_use(
|
|
21
|
+
tool_call_json: str | None = typer.Argument(None, help="The JSON string of the tool call from Claude Code")
|
|
22
|
+
):
|
|
23
|
+
"""
|
|
24
|
+
Claude Code hook: Inspects a tool call before execution.
|
|
25
|
+
Exits with code 2 to block unauthorized file writes.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
if tool_call_json is None:
|
|
29
|
+
tool_call_json = sys.stdin.read()
|
|
30
|
+
if not tool_call_json.strip():
|
|
31
|
+
raise typer.Exit(code=0)
|
|
32
|
+
call_data = json.loads(tool_call_json)
|
|
33
|
+
active_task = None
|
|
34
|
+
root = _project_root()
|
|
35
|
+
db = get_db(root)
|
|
36
|
+
if db:
|
|
37
|
+
with db.get_session() as session:
|
|
38
|
+
task_repo = TaskRepository(session)
|
|
39
|
+
running_tasks = [t for t in task_repo.get_all() if t.status == "running"]
|
|
40
|
+
active_task = running_tasks[0] if running_tasks else None
|
|
41
|
+
|
|
42
|
+
decision = HookPolicy(project_root=root).evaluate(call_data, active_task)
|
|
43
|
+
if decision.action == "deny":
|
|
44
|
+
console.print(f"[red]DevCouncil Blocked Action:[/red] {decision.reason}")
|
|
45
|
+
sys.exit(2)
|
|
46
|
+
if decision.action == "warn":
|
|
47
|
+
console.print(f"[yellow]DevCouncil Warning:[/yellow] {decision.reason}")
|
|
48
|
+
|
|
49
|
+
except json.JSONDecodeError:
|
|
50
|
+
raise typer.Exit(code=0)
|
|
51
|
+
|
|
52
|
+
@app.command()
|
|
53
|
+
def post_task():
|
|
54
|
+
"""
|
|
55
|
+
Claude Code hook: Runs after a task is completed.
|
|
56
|
+
Triggers deterministic verification.
|
|
57
|
+
"""
|
|
58
|
+
console.print("[cyan]DevCouncil: Claude finished task. Triggering automatic verification...[/cyan]")
|
|
59
|
+
# In a real environment, this would invoke 'dev verify <active-task>'
|
|
60
|
+
# For the hook script, we just notify the user.
|
|
61
|
+
console.print("Run [bold]dev verify[/bold] to finalize implementation evidence.")
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import typer
|
|
3
|
+
import yaml
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from devcouncil.storage.db import Database
|
|
7
|
+
from devcouncil.integrations.gitnexus import GitNexusIntegration
|
|
8
|
+
from devcouncil.integrations.graphify import GraphifyIntegration
|
|
9
|
+
|
|
10
|
+
app = typer.Typer()
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
DEFAULT_CONFIG = {
|
|
14
|
+
"project": {
|
|
15
|
+
"name": "devcouncil-project",
|
|
16
|
+
"root": ".",
|
|
17
|
+
"default_branch": "main",
|
|
18
|
+
},
|
|
19
|
+
"models": {
|
|
20
|
+
"provider": "openrouter",
|
|
21
|
+
"roles": {
|
|
22
|
+
"spec_writer": {"model": "anthropic/claude-3.5-sonnet"},
|
|
23
|
+
"planner_a": {"model": "anthropic/claude-3.5-sonnet"},
|
|
24
|
+
"planner_b": {"model": "google/gemini-pro-1.5"},
|
|
25
|
+
"critic_a": {"model": "openai/gpt-4o"},
|
|
26
|
+
"critic_b": {"model": "anthropic/claude-3-opus"},
|
|
27
|
+
"arbiter": {"model": "openai/gpt-4o"},
|
|
28
|
+
"native_agent": {"model": "anthropic/claude-3.5-sonnet"},
|
|
29
|
+
"implementation_reviewer": {"model": "openai/gpt-4o"},
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"commands": {
|
|
33
|
+
"test": ["pytest", "npm test"],
|
|
34
|
+
"lint": ["flake8", "eslint"],
|
|
35
|
+
"typecheck": ["mypy", "tsc"]
|
|
36
|
+
},
|
|
37
|
+
"gates": {
|
|
38
|
+
"require_clean_git_before_task": True,
|
|
39
|
+
"block_orphan_diffs": True,
|
|
40
|
+
"block_missing_tests_for_high_requirements": True,
|
|
41
|
+
"block_dependency_changes_without_approval": True,
|
|
42
|
+
"block_schema_change_without_migration": True,
|
|
43
|
+
"block_failed_commands": True
|
|
44
|
+
},
|
|
45
|
+
"execution": {
|
|
46
|
+
"default_executor": "native",
|
|
47
|
+
"max_repair_attempts": 3,
|
|
48
|
+
"checkpoint_before_each_task": True
|
|
49
|
+
},
|
|
50
|
+
"privacy": {
|
|
51
|
+
"redact_env_vars": True,
|
|
52
|
+
"redact_secrets_in_logs": True,
|
|
53
|
+
"store_prompts_locally": True
|
|
54
|
+
},
|
|
55
|
+
"integrations": {
|
|
56
|
+
"agent_flow": {
|
|
57
|
+
"enabled": False,
|
|
58
|
+
"trace_path": ".devcouncil/logs/traces.jsonl",
|
|
59
|
+
"mode": "jsonl",
|
|
60
|
+
},
|
|
61
|
+
"code_review_graph": {
|
|
62
|
+
"enabled": False,
|
|
63
|
+
"command": "code-review-graph",
|
|
64
|
+
"optional": True,
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def initialize_project(
|
|
71
|
+
project_root: Path = Path("."),
|
|
72
|
+
project_name: str | None = None,
|
|
73
|
+
with_gitnexus: bool = False,
|
|
74
|
+
with_graphify: bool = False,
|
|
75
|
+
) -> bool:
|
|
76
|
+
"""Initialize DevCouncil project state.
|
|
77
|
+
|
|
78
|
+
Returns True when a fresh .devcouncil directory was created.
|
|
79
|
+
"""
|
|
80
|
+
project_root = project_root.resolve()
|
|
81
|
+
dev_dir = project_root / ".devcouncil"
|
|
82
|
+
created = False
|
|
83
|
+
|
|
84
|
+
if not dev_dir.exists():
|
|
85
|
+
console.print("Initializing DevCouncil...")
|
|
86
|
+
dev_dir.mkdir(exist_ok=True)
|
|
87
|
+
(dev_dir / "runs").mkdir(exist_ok=True)
|
|
88
|
+
(dev_dir / "cache").mkdir(exist_ok=True)
|
|
89
|
+
(dev_dir / "checkpoints").mkdir(exist_ok=True)
|
|
90
|
+
(dev_dir / "logs").mkdir(exist_ok=True)
|
|
91
|
+
|
|
92
|
+
config_path = dev_dir / "config.yaml"
|
|
93
|
+
config = copy.deepcopy(DEFAULT_CONFIG)
|
|
94
|
+
if project_name:
|
|
95
|
+
config["project"]["name"] = project_name
|
|
96
|
+
else:
|
|
97
|
+
config["project"]["name"] = project_root.name
|
|
98
|
+
|
|
99
|
+
with open(config_path, "w") as f:
|
|
100
|
+
yaml.dump(config, f, default_flow_style=False)
|
|
101
|
+
|
|
102
|
+
db = Database(dev_dir / "state.sqlite")
|
|
103
|
+
db.create_db_and_tables()
|
|
104
|
+
console.print(f"[green]Successfully initialized DevCouncil in {dev_dir}[/green]")
|
|
105
|
+
created = True
|
|
106
|
+
|
|
107
|
+
if with_gitnexus:
|
|
108
|
+
nexus = GitNexusIntegration(project_root)
|
|
109
|
+
nexus.initialize()
|
|
110
|
+
|
|
111
|
+
if with_graphify:
|
|
112
|
+
graphify = GraphifyIntegration(project_root)
|
|
113
|
+
graphify.initialize()
|
|
114
|
+
|
|
115
|
+
return created
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.callback(invoke_without_command=True)
|
|
119
|
+
def init(
|
|
120
|
+
ctx: typer.Context,
|
|
121
|
+
project_name: str = typer.Option(None, "--name", "-n", help="Project name"),
|
|
122
|
+
with_gitnexus: bool = typer.Option(False, "--gitnexus", help="Initialize GitNexus structural awareness"),
|
|
123
|
+
with_graphify: bool = typer.Option(False, "--graphify", help="Initialize Graphify knowledge graph engine"),
|
|
124
|
+
):
|
|
125
|
+
"""
|
|
126
|
+
Initialize DevCouncil in the current directory.
|
|
127
|
+
"""
|
|
128
|
+
if ctx.invoked_subcommand is not None:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
dev_dir = Path(".devcouncil")
|
|
132
|
+
if dev_dir.exists() and not (with_gitnexus or with_graphify):
|
|
133
|
+
console.print("[yellow]DevCouncil is already initialized in this directory.[/yellow]")
|
|
134
|
+
console.print("Use --gitnexus or --graphify to add upgrade paths.")
|
|
135
|
+
raise typer.Exit()
|
|
136
|
+
|
|
137
|
+
initialize_project(
|
|
138
|
+
Path("."),
|
|
139
|
+
project_name=project_name,
|
|
140
|
+
with_gitnexus=with_gitnexus,
|
|
141
|
+
with_graphify=with_graphify,
|
|
142
|
+
)
|