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.
Files changed (125) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +643 -0
  3. package/bin/devcouncil.js +62 -0
  4. package/package.json +47 -0
  5. package/pyproject.toml +31 -0
  6. package/src/devcouncil/__init__.py +0 -0
  7. package/src/devcouncil/__main__.py +4 -0
  8. package/src/devcouncil/app/__init__.py +28 -0
  9. package/src/devcouncil/app/config.py +131 -0
  10. package/src/devcouncil/app/errors.py +23 -0
  11. package/src/devcouncil/app/events.py +44 -0
  12. package/src/devcouncil/app/orchestrator.py +92 -0
  13. package/src/devcouncil/app/run_context.py +39 -0
  14. package/src/devcouncil/app/state_machine.py +108 -0
  15. package/src/devcouncil/artifacts/__init__.py +1 -0
  16. package/src/devcouncil/artifacts/coverage.py +96 -0
  17. package/src/devcouncil/artifacts/graph.py +143 -0
  18. package/src/devcouncil/artifacts/migrations.py +20 -0
  19. package/src/devcouncil/artifacts/schemas.py +23 -0
  20. package/src/devcouncil/artifacts/serializer.py +21 -0
  21. package/src/devcouncil/artifacts/validators.py +27 -0
  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 +48 -0
  25. package/src/devcouncil/cli/commands/baseline.py +32 -0
  26. package/src/devcouncil/cli/commands/config.py +54 -0
  27. package/src/devcouncil/cli/commands/doctor.py +96 -0
  28. package/src/devcouncil/cli/commands/hook.py +61 -0
  29. package/src/devcouncil/cli/commands/init.py +142 -0
  30. package/src/devcouncil/cli/commands/integrate.py +420 -0
  31. package/src/devcouncil/cli/commands/map.py +38 -0
  32. package/src/devcouncil/cli/commands/mcp_server.py +18 -0
  33. package/src/devcouncil/cli/commands/plan.py +276 -0
  34. package/src/devcouncil/cli/commands/prompt.py +47 -0
  35. package/src/devcouncil/cli/commands/repair.py +69 -0
  36. package/src/devcouncil/cli/commands/report.py +71 -0
  37. package/src/devcouncil/cli/commands/reset_demo_state.py +28 -0
  38. package/src/devcouncil/cli/commands/rollback.py +58 -0
  39. package/src/devcouncil/cli/commands/run.py +224 -0
  40. package/src/devcouncil/cli/commands/setup.py +82 -0
  41. package/src/devcouncil/cli/commands/show.py +57 -0
  42. package/src/devcouncil/cli/commands/status.py +105 -0
  43. package/src/devcouncil/cli/commands/tasks.py +41 -0
  44. package/src/devcouncil/cli/commands/trace.py +43 -0
  45. package/src/devcouncil/cli/commands/verify.py +163 -0
  46. package/src/devcouncil/cli/commands/version.py +20 -0
  47. package/src/devcouncil/cli/main.py +70 -0
  48. package/src/devcouncil/council/__init__.py +0 -0
  49. package/src/devcouncil/council/prompts/__init__.py +0 -0
  50. package/src/devcouncil/council/prompts/arbiter.md +19 -0
  51. package/src/devcouncil/council/prompts/critic_a.md +10 -0
  52. package/src/devcouncil/council/prompts/critic_b.md +10 -0
  53. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -0
  54. package/src/devcouncil/council/prompts/planner_a.md +16 -0
  55. package/src/devcouncil/council/prompts/planner_b.md +16 -0
  56. package/src/devcouncil/council/prompts/rebuttal.md +10 -0
  57. package/src/devcouncil/council/prompts/spec_writer.md +12 -0
  58. package/src/devcouncil/domain/__init__.py +0 -0
  59. package/src/devcouncil/domain/assumption.py +17 -0
  60. package/src/devcouncil/domain/critique.py +32 -0
  61. package/src/devcouncil/domain/evidence.py +27 -0
  62. package/src/devcouncil/domain/gap.py +26 -0
  63. package/src/devcouncil/domain/requirement.py +22 -0
  64. package/src/devcouncil/domain/task.py +26 -0
  65. package/src/devcouncil/execution/__init__.py +1 -0
  66. package/src/devcouncil/execution/context_builder.py +60 -0
  67. package/src/devcouncil/execution/executor.py +15 -0
  68. package/src/devcouncil/execution/hook_policy.py +144 -0
  69. package/src/devcouncil/execution/patch.py +28 -0
  70. package/src/devcouncil/execution/paths.py +14 -0
  71. package/src/devcouncil/execution/permissions.py +92 -0
  72. package/src/devcouncil/execution/prompt_builder.py +59 -0
  73. package/src/devcouncil/execution/task_runner.py +166 -0
  74. package/src/devcouncil/executors/__init__.py +1 -0
  75. package/src/devcouncil/executors/mini_swe.py +73 -0
  76. package/src/devcouncil/executors/native/__init__.py +0 -0
  77. package/src/devcouncil/executors/native/agent.py +107 -0
  78. package/src/devcouncil/executors/openhands.py +71 -0
  79. package/src/devcouncil/gating/__init__.py +1 -0
  80. package/src/devcouncil/gating/checks/__init__.py +0 -0
  81. package/src/devcouncil/gating/checks/clean_git.py +45 -0
  82. package/src/devcouncil/gating/checks/planned_files_check.py +32 -0
  83. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -0
  84. package/src/devcouncil/gating/checks/secret_scan_check.py +34 -0
  85. package/src/devcouncil/gating/policy.py +190 -0
  86. package/src/devcouncil/indexing/__init__.py +1 -0
  87. package/src/devcouncil/indexing/graph_index.py +48 -0
  88. package/src/devcouncil/indexing/repo_mapper.py +204 -0
  89. package/src/devcouncil/indexing/symbol_index.py +0 -0
  90. package/src/devcouncil/integrations/code_review_graph.py +163 -0
  91. package/src/devcouncil/integrations/github.py +39 -0
  92. package/src/devcouncil/integrations/gitnexus.py +27 -0
  93. package/src/devcouncil/integrations/graphify.py +34 -0
  94. package/src/devcouncil/integrations/mcp/__init__.py +0 -0
  95. package/src/devcouncil/integrations/mcp/server.py +146 -0
  96. package/src/devcouncil/llm/__init__.py +1 -0
  97. package/src/devcouncil/llm/cache.py +38 -0
  98. package/src/devcouncil/llm/provider.py +125 -0
  99. package/src/devcouncil/llm/router.py +125 -0
  100. package/src/devcouncil/planning/__init__.py +1 -0
  101. package/src/devcouncil/planning/arbiter_service.py +57 -0
  102. package/src/devcouncil/planning/critique_service.py +66 -0
  103. package/src/devcouncil/planning/plan_service.py +46 -0
  104. package/src/devcouncil/planning/repair_service.py +39 -0
  105. package/src/devcouncil/planning/spec_service.py +44 -0
  106. package/src/devcouncil/repo/__init__.py +0 -0
  107. package/src/devcouncil/reporting/__init__.py +0 -0
  108. package/src/devcouncil/reporting/github_check.py +32 -0
  109. package/src/devcouncil/reporting/json_report.py +17 -0
  110. package/src/devcouncil/reporting/markdown_report.py +46 -0
  111. package/src/devcouncil/reporting/report_builder.py +14 -0
  112. package/src/devcouncil/storage/__init__.py +0 -0
  113. package/src/devcouncil/storage/db.py +66 -0
  114. package/src/devcouncil/storage/models.py +83 -0
  115. package/src/devcouncil/storage/repositories.py +346 -0
  116. package/src/devcouncil/telemetry/__init__.py +0 -0
  117. package/src/devcouncil/telemetry/cost.py +34 -0
  118. package/src/devcouncil/telemetry/traces.py +91 -0
  119. package/src/devcouncil/telemetry/tracker.py +49 -0
  120. package/src/devcouncil/utils/__init__.py +1 -0
  121. package/src/devcouncil/utils/redaction.py +141 -0
  122. package/src/devcouncil/verification/__init__.py +1 -0
  123. package/src/devcouncil/verification/implementation_reviewer.py +55 -0
  124. package/src/devcouncil/verification/verifier.py +513 -0
  125. package/uv.lock +1085 -0
@@ -0,0 +1,125 @@
1
+ from abc import ABC, abstractmethod
2
+ import copy
3
+ from typing import List, Dict, Any, Optional
4
+ from pydantic import BaseModel
5
+ import httpx
6
+ import json
7
+ from pathlib import Path
8
+
9
+ class LLMResponse(BaseModel):
10
+ content: str
11
+ model: str
12
+ usage: Dict[str, int]
13
+ raw_response: Dict[str, Any]
14
+
15
+ class Provider(ABC):
16
+ @abstractmethod
17
+ async def complete(
18
+ self,
19
+ model: str,
20
+ messages: List[Dict[str, str]],
21
+ temperature: float = 0.0,
22
+ json_mode: bool = False
23
+ ) -> LLMResponse:
24
+ pass
25
+
26
+ class OpenRouterProvider(Provider):
27
+ def __init__(self, api_key: str):
28
+ self.api_key = api_key
29
+ self.base_url = "https://openrouter.ai/api/v1"
30
+
31
+ async def complete(
32
+ self,
33
+ model: str,
34
+ messages: List[Dict[str, str]],
35
+ temperature: float = 0.0,
36
+ json_mode: bool = False
37
+ ) -> LLMResponse:
38
+ # Deep-copy to avoid mutating the caller's messages list
39
+ msgs = copy.deepcopy(messages)
40
+
41
+ headers = {
42
+ "Authorization": f"Bearer {self.api_key}",
43
+ "Content-Type": "application/json",
44
+ "HTTP-Referer": "https://github.com/devcouncil/devcouncil", # Optional
45
+ "X-Title": "DevCouncil", # Optional
46
+ }
47
+
48
+ payload = {
49
+ "model": model,
50
+ "messages": msgs,
51
+ "temperature": temperature,
52
+ }
53
+
54
+ if json_mode:
55
+ payload["response_format"] = {"type": "json_object"}
56
+ # Ensure the user message mentions JSON
57
+ if msgs[-1]["role"] == "user":
58
+ msgs[-1]["content"] += "\n\nOutput must be a valid JSON object."
59
+
60
+ async with httpx.AsyncClient(timeout=180.0) as client:
61
+ response = await client.post(
62
+ f"{self.base_url}/chat/completions",
63
+ headers=headers,
64
+ json=payload
65
+ )
66
+ response.raise_for_status()
67
+ data = response.json()
68
+
69
+ resp = LLMResponse(
70
+ content=data["choices"][0]["message"]["content"],
71
+ model=data["model"],
72
+ usage=data.get("usage", {}),
73
+ raw_response=data
74
+ )
75
+
76
+ # Log the call
77
+ try:
78
+ from devcouncil.utils.redaction import redact_dict
79
+ log_dir = Path(".devcouncil/logs")
80
+ log_dir.mkdir(parents=True, exist_ok=True)
81
+ log_file = log_dir / "model_calls.jsonl"
82
+
83
+ # Create a redacted copy of both request and response for logging
84
+ log_payload = {
85
+ "request": redact_dict(payload),
86
+ "response": redact_dict(data),
87
+ "usage": resp.usage,
88
+ }
89
+ with open(log_file, "a", encoding="utf-8") as f:
90
+ f.write(json.dumps(log_payload) + "\n")
91
+ except Exception as e:
92
+ import logging as _log
93
+ _log.getLogger(__name__).debug("Failed to log model call: %s", e)
94
+
95
+ return resp
96
+
97
+ class MockProvider(Provider):
98
+ """Mock provider for dry runs and testing."""
99
+ def __init__(self, responses: Optional[Dict[str, Any]] = None):
100
+ # responses can be a dict of model -> str OR model -> list of str
101
+ self.responses = responses or {}
102
+ self._counts: Dict[str, int] = {}
103
+
104
+ async def complete(
105
+ self,
106
+ model: str,
107
+ messages: List[Dict[str, str]],
108
+ temperature: float = 0.0,
109
+ json_mode: bool = False
110
+ ) -> LLMResponse:
111
+ res = self.responses.get(model, '{"mock": "response"}')
112
+
113
+ if isinstance(res, list):
114
+ count = self._counts.get(model, 0)
115
+ content = res[min(count, len(res)-1)]
116
+ self._counts[model] = count + 1
117
+ else:
118
+ content = res
119
+
120
+ return LLMResponse(
121
+ content=content,
122
+ model=f"mock/{model}",
123
+ usage={"prompt_tokens": 10, "completion_tokens": 10, "total_tokens": 20},
124
+ raw_response={"choices": [{"message": {"content": content}}]}
125
+ )
@@ -0,0 +1,125 @@
1
+ from typing import List, Dict, Any, Type, Optional
2
+ import copy
3
+ import json
4
+ import logging
5
+ import asyncio
6
+ from pathlib import Path
7
+
8
+ from pydantic import BaseModel
9
+ from devcouncil.llm.provider import Provider
10
+ from devcouncil.llm.cache import LLMCache
11
+ from devcouncil.telemetry.tracker import TelemetryTracker
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ class ModelRouter:
16
+ def __init__(self, provider: Provider, role_config: Dict[str, Dict[str, Any]]):
17
+ self.provider = provider
18
+ self.role_config = role_config
19
+
20
+ async def complete_structured(
21
+ self,
22
+ role: str,
23
+ messages: List[Dict[str, str]],
24
+ schema: Type[BaseModel],
25
+ temperature: Optional[float] = None,
26
+ run_id: Optional[str] = None,
27
+ ) -> BaseModel:
28
+ config = self.role_config.get(role)
29
+ if not config:
30
+ raise ValueError(f"No config found for role: {role}")
31
+
32
+ model = config["model"]
33
+ temp = temperature if temperature is not None else config.get("temperature", 0.0)
34
+
35
+ # Deep-copy to avoid mutating the caller's messages list
36
+ msgs = copy.deepcopy(messages)
37
+
38
+ # Add schema instructions to system or user message
39
+ schema_json = json.dumps(schema.model_json_schema(), indent=2)
40
+ instruction = f"\n\nYou MUST output a JSON object matching this schema:\n{schema_json}"
41
+
42
+ found_system = False
43
+ for msg in msgs:
44
+ if msg["role"] == "system":
45
+ msg["content"] += instruction
46
+ found_system = True
47
+ break
48
+
49
+ if not found_system:
50
+ msgs.insert(0, {"role": "system", "content": f"You are a helpful assistant.{instruction}"})
51
+
52
+ logger.info("LLM call: role=%s model=%s run_id=%s", role, model, run_id)
53
+
54
+ project_root = Path(".")
55
+ cache = LLMCache(project_root)
56
+ tracker = TelemetryTracker(project_root)
57
+
58
+ # Check cache first
59
+ response = cache.get(model, msgs, temp, True)
60
+ cache_hit = response is not None
61
+
62
+ if not response:
63
+ for attempt in range(3):
64
+ try:
65
+ response = await self.provider.complete(
66
+ model=model,
67
+ messages=msgs,
68
+ temperature=temp,
69
+ json_mode=True
70
+ )
71
+ cache.set(model, msgs, temp, True, response)
72
+ break
73
+ except Exception as e:
74
+ if attempt == 2:
75
+ raise
76
+ logger.warning(f"LLM request failed (attempt {attempt+1}): {e}. Retrying...")
77
+ await asyncio.sleep(2 ** attempt)
78
+
79
+ if not cache_hit:
80
+ tracker.log_usage(model, response.usage)
81
+
82
+ logger.info(
83
+ "LLM response: role=%s model=%s tokens=%s",
84
+ role, response.model, response.usage,
85
+ )
86
+
87
+ try:
88
+ # Attempt to find JSON block if it's wrapped in markdown
89
+ content = response.content.strip()
90
+ if "```json" in content:
91
+ content = content.split("```json")[1].split("```")[0].strip()
92
+ elif "```" in content:
93
+ content = content.split("```")[1].split("```")[0].strip()
94
+
95
+ data = json.loads(content)
96
+ return schema.model_validate(data)
97
+ except Exception as e:
98
+ logger.warning(f"Initial parse failed for {role}, attempting healing: {e}")
99
+
100
+ # Healing attempt: Ask the model to fix its own JSON
101
+ healing_prompt = f"""
102
+ The following JSON was returned but failed to parse or validate against the schema.
103
+ Error: {str(e)}
104
+ Content:
105
+ {response.content}
106
+
107
+ Please return the corrected JSON object only. No prose.
108
+ """
109
+ # We use a lower temperature for healing
110
+ healed_response = await self.provider.complete(
111
+ model=model,
112
+ messages=[{"role": "user", "content": healing_prompt}],
113
+ temperature=0.0,
114
+ json_mode=True
115
+ )
116
+
117
+ try:
118
+ healed_content = healed_response.content.strip()
119
+ if "```json" in healed_content:
120
+ healed_content = healed_content.split("```json")[1].split("```")[0].strip()
121
+ data = json.loads(healed_content)
122
+ return schema.model_validate(data)
123
+ except Exception as final_e:
124
+ logger.error(f"Healing failed for {role}: {final_e}")
125
+ raise ValueError(f"Failed to parse or validate LLM response after healing: {final_e}\nContent (truncated): {response.content[:200]}...")
@@ -0,0 +1,57 @@
1
+ from typing import List, Dict
2
+ from pydantic import BaseModel
3
+ from devcouncil.domain.requirement import Requirement
4
+ from devcouncil.domain.task import Task
5
+ from devcouncil.llm.router import ModelRouter
6
+
7
+ class ArbiterDecision(BaseModel):
8
+ accepted_finding_ids: List[str]
9
+ rejected_finding_ids: List[Dict[str, str]] # id, reason
10
+ final_requirements: List[Requirement]
11
+ final_tasks: List[Task]
12
+
13
+ class ArbiterService:
14
+ def __init__(self, router: ModelRouter):
15
+ self.router = router
16
+
17
+ async def arbitrate(
18
+ self,
19
+ goal: str,
20
+ requirements_json: str,
21
+ plan_a_json: str,
22
+ plan_b_json: str,
23
+ critique_a_json: str,
24
+ critique_b_json: str,
25
+ rebuttal_a_json: str,
26
+ rebuttal_b_json: str
27
+ ) -> ArbiterDecision:
28
+ prompt = f"""
29
+ Goal: {goal}
30
+
31
+ Initial Requirements:
32
+ {requirements_json}
33
+
34
+ Plan A: {plan_a_json}
35
+ Plan B: {plan_b_json}
36
+
37
+ Critique of Plan B by Critic A: {critique_a_json}
38
+ Critique of Plan A by Critic B: {critique_b_json}
39
+
40
+ Rebuttal of Critic B by Planner A: {rebuttal_a_json}
41
+ Rebuttal of Critic A by Planner B: {rebuttal_b_json}
42
+
43
+ You are the arbiter engineering manager. Your goal is to produce the final, definitive set of requirements and tasks.
44
+ - You do not decide by vibes.
45
+ - High-severity unrefuted findings from critics must be incorporated into the final requirements or tasks.
46
+ - If a planner successfully rebutted a finding, you may skip it.
47
+ - Produce a single, coherent task graph.
48
+ """
49
+ messages = [
50
+ {"role": "user", "content": prompt}
51
+ ]
52
+
53
+ return await self.router.complete_structured(
54
+ role="arbiter",
55
+ messages=messages,
56
+ schema=ArbiterDecision
57
+ )
@@ -0,0 +1,66 @@
1
+ from typing import List
2
+ from pydantic import BaseModel
3
+ from devcouncil.domain.critique import CritiqueFinding
4
+ from devcouncil.llm.router import ModelRouter
5
+
6
+ class CritiqueOutput(BaseModel):
7
+ findings: List[CritiqueFinding]
8
+
9
+ class RebuttalItem(BaseModel):
10
+ finding_id: str
11
+ decision: str # "accepted", "rejected"
12
+ reason: str
13
+ suggested_change: str | None = None
14
+
15
+ class RebuttalOutput(BaseModel):
16
+ rebuttals: List[RebuttalItem]
17
+
18
+ class CritiqueService:
19
+ def __init__(self, router: ModelRouter):
20
+ self.router = router
21
+
22
+ async def generate_critique(self, role: str, target_plan_json: str, requirements_json: str) -> CritiqueOutput:
23
+ prompt = f"""
24
+ Requirements:
25
+ {requirements_json}
26
+
27
+ Target Plan:
28
+ {target_plan_json}
29
+
30
+ You are a hostile staff engineer reviewing another team's implementation plan.
31
+ Find missing requirements, bad assumptions, missing tests, security risks, migration risks, and unverifiable claims.
32
+ Do not praise. Do not rewrite the plan.
33
+ Every finding must include a falsifiable_check.
34
+ """
35
+ messages = [
36
+ {"role": "user", "content": prompt}
37
+ ]
38
+
39
+ return await self.router.complete_structured(
40
+ role=role,
41
+ messages=messages,
42
+ schema=CritiqueOutput
43
+ )
44
+
45
+ async def generate_rebuttal(self, role: str, original_plan_json: str, findings_json: str) -> RebuttalOutput:
46
+ prompt = f"""
47
+ Original Plan:
48
+ {original_plan_json}
49
+
50
+ Critique Findings:
51
+ {findings_json}
52
+
53
+ You are the planner who created the original plan. Review the critique findings.
54
+ - A finding can be rejected only with artifact evidence or strong justification.
55
+ - A finding can be accepted and converted into a requirement/task/test.
56
+ - No hand-wavy rebuttals.
57
+ """
58
+ messages = [
59
+ {"role": "user", "content": prompt}
60
+ ]
61
+
62
+ return await self.router.complete_structured(
63
+ role=role,
64
+ messages=messages,
65
+ schema=RebuttalOutput
66
+ )
@@ -0,0 +1,46 @@
1
+ from typing import List
2
+ from pydantic import BaseModel
3
+ from devcouncil.domain.task import Task
4
+ from devcouncil.llm.router import ModelRouter
5
+
6
+ class PlanOutput(BaseModel):
7
+ id: str
8
+ rationale: str
9
+ tasks: List[Task]
10
+
11
+ class PlanService:
12
+ def __init__(self, router: ModelRouter):
13
+ self.router = router
14
+
15
+ async def generate_plan(self, role: str, goal: str, requirements_json: str, repo_map_json: str) -> PlanOutput:
16
+ prompt = f"""
17
+ Goal: {goal}
18
+
19
+ Requirements:
20
+ {requirements_json}
21
+
22
+ Repository Map:
23
+ {repo_map_json}
24
+
25
+ Your task is to create a detailed implementation plan.
26
+ - Break down the requirements into atomic implementation tasks.
27
+ - For each task, specify which files will be created or modified.
28
+ - Specify which tests are expected to verify the task.
29
+ - Ensure each task maps back to at least one requirement.
30
+
31
+ Role-specific instructions:
32
+ """
33
+ if role == "planner_a":
34
+ prompt += "You are the pragmatic tech lead. Optimize for simplicity and minimal dependencies."
35
+ else:
36
+ prompt += "You are the production-readiness architect. Optimize for security, performance, and edge cases."
37
+
38
+ messages = [
39
+ {"role": "user", "content": prompt}
40
+ ]
41
+
42
+ return await self.router.complete_structured(
43
+ role=role,
44
+ messages=messages,
45
+ schema=PlanOutput
46
+ )
@@ -0,0 +1,39 @@
1
+ from typing import List
2
+ import json
3
+ from pydantic import BaseModel
4
+ from devcouncil.domain.gap import Gap
5
+ from devcouncil.domain.task import Task
6
+ from devcouncil.llm.router import ModelRouter
7
+
8
+ class RepairOutput(BaseModel):
9
+ suggested_tasks: List[Task]
10
+
11
+ class RepairService:
12
+ """Uses LLM to infer focused repair tasks from blocking gaps."""
13
+
14
+ def __init__(self, router: ModelRouter):
15
+ self.router = router
16
+
17
+ async def generate_repair_plan(self, gaps: List[Gap], project_context: str) -> RepairOutput:
18
+ prompt = f"""
19
+ The following blocking gaps were detected during verification.
20
+ Gaps:
21
+ {json.dumps([g.model_dump() for g in gaps], indent=2)}
22
+
23
+ Project Context:
24
+ {project_context}
25
+
26
+ Your task is to generate focused implementation tasks to fix these gaps.
27
+ - Each task must have a clear description and recommended fix.
28
+ - Specify 'planned_files' that need modification (infer from gap evidence).
29
+ - Link each task to the relevant 'requirement_id' mentioned in the gap.
30
+
31
+ Return a JSON object with 'suggested_tasks'.
32
+ """
33
+ messages = [{"role": "user", "content": prompt}]
34
+
35
+ return await self.router.complete_structured(
36
+ role="planner_a", # Pragmatic tech lead is best suited for repair task generation
37
+ messages=messages,
38
+ schema=RepairOutput
39
+ )
@@ -0,0 +1,44 @@
1
+ from typing import List
2
+ from pydantic import BaseModel
3
+ from devcouncil.domain.requirement import Requirement
4
+ from devcouncil.domain.assumption import Assumption
5
+ from devcouncil.llm.router import ModelRouter
6
+
7
+ class BlockingQuestion(BaseModel):
8
+ id: str
9
+ question: str
10
+ reason: str
11
+
12
+ class SpecOutput(BaseModel):
13
+ requirements: List[Requirement]
14
+ assumptions: List[Assumption]
15
+ blocking_questions: List[BlockingQuestion]
16
+
17
+ class SpecService:
18
+ def __init__(self, router: ModelRouter):
19
+ self.router = router
20
+
21
+ async def generate_spec(self, goal: str, repo_map_json: str) -> SpecOutput:
22
+ prompt = f"""
23
+ Goal: {goal}
24
+
25
+ Repository Map:
26
+ {repo_map_json}
27
+
28
+ Your task is to draft the initial software specification for this goal.
29
+ 1. Identify functional and non-functional requirements.
30
+ 2. Extract any assumptions you are making about the codebase or architecture.
31
+ 3. List any blocking questions that the user must answer before implementation can proceed.
32
+
33
+ Each requirement MUST have clear acceptance criteria with verification methods.
34
+ Each assumption MUST have a confidence and impact level.
35
+ """
36
+ messages = [
37
+ {"role": "user", "content": prompt}
38
+ ]
39
+
40
+ return await self.router.complete_structured(
41
+ role="spec_writer",
42
+ messages=messages,
43
+ schema=SpecOutput
44
+ )
File without changes
File without changes
@@ -0,0 +1,32 @@
1
+ from devcouncil.artifacts.graph import ArtifactGraph
2
+
3
+ class GitHubCheckGenerator:
4
+ """Generates GitHub Checks API payloads."""
5
+
6
+ @staticmethod
7
+ def generate(graph: ArtifactGraph) -> dict:
8
+ summary = graph.coverage_summary()
9
+ blocking_gaps = graph.blocking_gaps()
10
+
11
+ status = "completed"
12
+ conclusion = "failure" if summary["blocking_gaps"] > 0 else "success"
13
+
14
+ text = f"**Requirements**: {summary['total_requirements']} | "
15
+ text += f"**Tasks**: {summary['total_tasks']} | "
16
+ text += f"**Gaps**: {summary['blocking_gaps']} blocking\n\n"
17
+
18
+ if blocking_gaps:
19
+ text += "### Blocking Gaps\n"
20
+ for gap in blocking_gaps:
21
+ text += f"- **{gap.id}**: {gap.description}\n"
22
+
23
+ return {
24
+ "name": "DevCouncil Verification",
25
+ "status": status,
26
+ "conclusion": conclusion,
27
+ "output": {
28
+ "title": f"DevCouncil: {conclusion.capitalize()}",
29
+ "summary": f"Found {summary['blocking_gaps']} blocking gaps.",
30
+ "text": text
31
+ }
32
+ }
@@ -0,0 +1,17 @@
1
+ import json
2
+ from devcouncil.artifacts.graph import ArtifactGraph
3
+
4
+ class JsonReportGenerator:
5
+ """Generates a JSON evidence report."""
6
+
7
+ @staticmethod
8
+ def generate(graph: ArtifactGraph) -> str:
9
+ summary = graph.coverage_summary()
10
+
11
+ report = {
12
+ "verdict": "blocked" if summary["blocking_gaps"] > 0 else "passed",
13
+ "coverage_summary": summary,
14
+ "blocking_gaps": [g.model_dump() for g in graph.blocking_gaps()]
15
+ }
16
+
17
+ return json.dumps(report, indent=2)
@@ -0,0 +1,46 @@
1
+ from devcouncil.artifacts.graph import ArtifactGraph
2
+
3
+ class MarkdownReportGenerator:
4
+ """Generates a Markdown evidence report."""
5
+
6
+ MAX_INLINE_GAPS = 25
7
+
8
+ @staticmethod
9
+ def generate(graph: ArtifactGraph) -> str:
10
+ summary = graph.coverage_summary()
11
+
12
+ md_output = "# DevCouncil Report\n\n"
13
+ md_output += "## Verdict\n"
14
+ if summary["blocking_gaps"] > 0:
15
+ md_output += f"**Blocked**: {summary['blocking_gaps']} high-severity gaps remain.\n\n"
16
+ else:
17
+ md_output += "**Passed**: Ready for release.\n\n"
18
+
19
+ md_output += "## Coverage Summary\n"
20
+ md_output += f"- **Requirements**: {summary['total_requirements']} ({summary['requirements_without_tasks']} unmapped)\n"
21
+ md_output += f"- **Tasks**: {summary['total_tasks']} ({summary['tasks_without_requirements']} orphaned)\n"
22
+ md_output += f"- **Evidence**: {summary['total_ac'] - summary['ac_without_evidence']}/{summary['total_ac']} AC verified\n\n"
23
+
24
+ md_output += "## Requirements Coverage Table\n"
25
+ md_output += "| Requirement | Task Mapping | Status |\n"
26
+ md_output += "|---|---|---|\n"
27
+
28
+ for req in graph.requirements.values():
29
+ linked_tasks = [t for t in graph.tasks.values() if req.id in t.requirement_ids]
30
+ task_str = ", ".join([t.id for t in linked_tasks]) if linked_tasks else "*None*"
31
+ status_str = "Covered" if linked_tasks else "**Unmapped**"
32
+ md_output += f"| {req.id} {req.title} | {task_str} | {status_str} |\n"
33
+
34
+ md_output += "\n## Blocking Gaps\n"
35
+ blocking_gaps = graph.blocking_gaps()
36
+ if not blocking_gaps:
37
+ md_output += "None.\n"
38
+ else:
39
+ for gap in blocking_gaps[:MarkdownReportGenerator.MAX_INLINE_GAPS]:
40
+ md_output += f"### {gap.id}: {gap.description}\n"
41
+ md_output += f"**Recommended fix**: {gap.recommended_fix}\n\n"
42
+ if len(blocking_gaps) > MarkdownReportGenerator.MAX_INLINE_GAPS:
43
+ remaining = len(blocking_gaps) - MarkdownReportGenerator.MAX_INLINE_GAPS
44
+ md_output += f"_Omitted {remaining} additional blocking gap(s). Use JSON output for the full list._\n"
45
+
46
+ return md_output
@@ -0,0 +1,14 @@
1
+ from devcouncil.artifacts.graph import ArtifactGraph
2
+ from devcouncil.reporting.markdown_report import MarkdownReportGenerator
3
+ from devcouncil.reporting.json_report import JsonReportGenerator
4
+
5
+ class ReportBuilder:
6
+ """Builds reports in various formats from the artifact graph."""
7
+
8
+ @staticmethod
9
+ def build_markdown(graph: ArtifactGraph) -> str:
10
+ return MarkdownReportGenerator.generate(graph)
11
+
12
+ @staticmethod
13
+ def build_json(graph: ArtifactGraph) -> str:
14
+ return JsonReportGenerator.generate(graph)
File without changes