devcouncil 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -201
- package/README.md +62 -543
- package/package.json +1 -1
- package/pyproject.toml +29 -26
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +135 -108
- package/src/devcouncil/app/errors.py +23 -23
- package/src/devcouncil/app/events.py +44 -44
- package/src/devcouncil/app/orchestrator.py +67 -67
- package/src/devcouncil/app/project_status.py +29 -0
- package/src/devcouncil/app/run_context.py +39 -39
- package/src/devcouncil/app/state_machine.py +108 -108
- package/src/devcouncil/artifacts/__init__.py +1 -1
- package/src/devcouncil/artifacts/coverage.py +96 -96
- package/src/devcouncil/artifacts/graph.py +143 -143
- package/src/devcouncil/artifacts/migrations.py +20 -20
- package/src/devcouncil/artifacts/schemas.py +23 -23
- package/src/devcouncil/artifacts/serializer.py +21 -21
- package/src/devcouncil/artifacts/validators.py +27 -27
- package/src/devcouncil/cli/commands/artifacts.py +51 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/config.py +76 -54
- package/src/devcouncil/cli/commands/dashboard.py +26 -0
- package/src/devcouncil/cli/commands/doctor.py +86 -42
- package/src/devcouncil/cli/commands/go.py +237 -0
- package/src/devcouncil/cli/commands/hook.py +96 -29
- package/src/devcouncil/cli/commands/init.py +67 -56
- package/src/devcouncil/cli/commands/integrate.py +320 -14
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +25 -21
- package/src/devcouncil/cli/commands/plan.py +257 -206
- package/src/devcouncil/cli/commands/prompt.py +36 -33
- package/src/devcouncil/cli/commands/repair.py +72 -69
- package/src/devcouncil/cli/commands/report.py +112 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +31 -28
- package/src/devcouncil/cli/commands/rollback.py +49 -47
- package/src/devcouncil/cli/commands/run.py +252 -207
- package/src/devcouncil/cli/commands/setup.py +159 -18
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/status.py +117 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +2 -1
- package/src/devcouncil/cli/commands/verify.py +158 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/main.py +42 -24
- package/src/devcouncil/council/prompts/arbiter.md +19 -19
- package/src/devcouncil/council/prompts/critic_a.md +10 -10
- package/src/devcouncil/council/prompts/critic_b.md +10 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
- package/src/devcouncil/council/prompts/planner_a.md +16 -16
- package/src/devcouncil/council/prompts/planner_b.md +16 -16
- package/src/devcouncil/council/prompts/rebuttal.md +10 -10
- package/src/devcouncil/council/prompts/spec_writer.md +12 -12
- package/src/devcouncil/domain/assumption.py +17 -17
- package/src/devcouncil/domain/critique.py +32 -32
- package/src/devcouncil/domain/evidence.py +27 -27
- package/src/devcouncil/domain/gap.py +26 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +26 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/hook_policy.py +24 -3
- package/src/devcouncil/execution/patch.py +28 -28
- package/src/devcouncil/execution/permissions.py +44 -44
- package/src/devcouncil/execution/prompt_builder.py +23 -23
- package/src/devcouncil/execution/task_runner.py +63 -63
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/coding_cli.py +112 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +81 -81
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +50 -45
- package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +34 -34
- package/src/devcouncil/gating/policy.py +157 -157
- package/src/devcouncil/indexing/__init__.py +1 -1
- package/src/devcouncil/indexing/ast_matcher.py +168 -0
- package/src/devcouncil/indexing/graph_index.py +48 -48
- package/src/devcouncil/indexing/lsp.py +120 -0
- package/src/devcouncil/indexing/repo_mapper.py +208 -204
- package/src/devcouncil/integrations/github.py +35 -35
- package/src/devcouncil/integrations/gitnexus.py +27 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +549 -96
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +207 -0
- package/src/devcouncil/live/models.py +63 -0
- package/src/devcouncil/live/repair_prompt.py +83 -0
- package/src/devcouncil/live/reviewer.py +70 -0
- package/src/devcouncil/live/signals.py +135 -0
- package/src/devcouncil/live/summary.py +34 -0
- package/src/devcouncil/live/tasks.py +18 -0
- package/src/devcouncil/live/transcripts.py +138 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +38 -38
- package/src/devcouncil/llm/provider.py +146 -125
- package/src/devcouncil/llm/router.py +111 -111
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/critique_service.py +66 -66
- package/src/devcouncil/planning/plan_service.py +46 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +86 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +44 -44
- package/src/devcouncil/reporting/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +20 -17
- package/src/devcouncil/reporting/markdown_report.py +68 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- package/src/devcouncil/storage/db.py +66 -66
- package/src/devcouncil/storage/models.py +83 -83
- package/src/devcouncil/storage/repositories.py +299 -222
- package/src/devcouncil/telemetry/cost.py +34 -34
- package/src/devcouncil/telemetry/tracker.py +49 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +122 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +141 -141
- package/src/devcouncil/verification/__init__.py +1 -1
- package/src/devcouncil/verification/implementation_reviewer.py +55 -55
- package/src/devcouncil/verification/verifier.py +319 -302
- package/uv.lock +1 -1
|
@@ -1,58 +1,58 @@
|
|
|
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)
|
|
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
56
|
tracker = TelemetryTracker(project_root)
|
|
57
57
|
|
|
58
58
|
# Check cache first
|
|
@@ -62,64 +62,64 @@ class ModelRouter:
|
|
|
62
62
|
if not response:
|
|
63
63
|
for attempt in range(3):
|
|
64
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
|
|
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
76
|
logger.warning(f"LLM request failed (attempt {attempt+1}): {e}. Retrying...")
|
|
77
77
|
await asyncio.sleep(2 ** attempt)
|
|
78
78
|
|
|
79
79
|
if not cache_hit:
|
|
80
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]}...")
|
|
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]}...")
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
@@ -1,57 +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
|
-
)
|
|
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
|
+
)
|
|
@@ -1,66 +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
|
-
)
|
|
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
|
+
)
|
|
@@ -1,46 +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
|
-
)
|
|
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,86 @@
|
|
|
1
|
+
from pydantic import BaseModel, Field
|
|
2
|
+
|
|
3
|
+
from devcouncil.llm.router import ModelRouter
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class PromptEnhancement(BaseModel):
|
|
7
|
+
original_goal: str
|
|
8
|
+
enhanced_goal: str
|
|
9
|
+
codebase_context: list[str] = Field(default_factory=list)
|
|
10
|
+
debate_focus: list[str] = Field(default_factory=list)
|
|
11
|
+
constraints: list[str] = Field(default_factory=list)
|
|
12
|
+
|
|
13
|
+
def normalized(self, original_goal: str) -> "PromptEnhancement":
|
|
14
|
+
enhanced_goal = self.enhanced_goal.strip() or original_goal
|
|
15
|
+
return self.model_copy(
|
|
16
|
+
update={
|
|
17
|
+
"original_goal": original_goal,
|
|
18
|
+
"enhanced_goal": enhanced_goal,
|
|
19
|
+
"codebase_context": _clean_items(self.codebase_context),
|
|
20
|
+
"debate_focus": _clean_items(self.debate_focus),
|
|
21
|
+
"constraints": _clean_items(self.constraints),
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def debate_prompt(self) -> str:
|
|
26
|
+
sections = [
|
|
27
|
+
"# Enhanced Planning Prompt",
|
|
28
|
+
"",
|
|
29
|
+
"## Original user goal",
|
|
30
|
+
self.original_goal,
|
|
31
|
+
"",
|
|
32
|
+
"## Codebase-specific goal",
|
|
33
|
+
self.enhanced_goal,
|
|
34
|
+
]
|
|
35
|
+
if self.codebase_context:
|
|
36
|
+
sections.extend(["", "## Relevant codebase context"])
|
|
37
|
+
sections.extend(f"- {item}" for item in self.codebase_context)
|
|
38
|
+
if self.constraints:
|
|
39
|
+
sections.extend(["", "## Constraints to preserve"])
|
|
40
|
+
sections.extend(f"- {item}" for item in self.constraints)
|
|
41
|
+
if self.debate_focus:
|
|
42
|
+
sections.extend(["", "## Debate focus"])
|
|
43
|
+
sections.extend(f"- {item}" for item in self.debate_focus)
|
|
44
|
+
return "\n".join(sections)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PromptEnhancerService:
|
|
48
|
+
def __init__(self, router: ModelRouter):
|
|
49
|
+
self.router = router
|
|
50
|
+
|
|
51
|
+
async def enhance_prompt(
|
|
52
|
+
self,
|
|
53
|
+
goal: str,
|
|
54
|
+
repo_map_json: str,
|
|
55
|
+
graph_context_json: str | None = None,
|
|
56
|
+
) -> PromptEnhancement:
|
|
57
|
+
prompt = f"""
|
|
58
|
+
Original user goal:
|
|
59
|
+
{goal}
|
|
60
|
+
|
|
61
|
+
Repository map:
|
|
62
|
+
{repo_map_json}
|
|
63
|
+
|
|
64
|
+
Code review graph context:
|
|
65
|
+
{graph_context_json or "{}"}
|
|
66
|
+
|
|
67
|
+
You are DevCouncil's codebase-specific prompt enhancer.
|
|
68
|
+
Rewrite the user goal into a better planning prompt before it is sent to the council debate.
|
|
69
|
+
|
|
70
|
+
Requirements:
|
|
71
|
+
- Preserve the user's intent exactly; do not add unrelated features.
|
|
72
|
+
- Make the goal specific to the mapped repository architecture, languages, tests, and likely ownership boundaries.
|
|
73
|
+
- Identify constraints the planners and critics must preserve.
|
|
74
|
+
- Identify debate focus areas that should force useful disagreement between pragmatic and production-readiness plans.
|
|
75
|
+
- Keep the enhanced_goal concise enough to be used as the goal for spec, planning, critique, and arbitration.
|
|
76
|
+
"""
|
|
77
|
+
enhancement = await self.router.complete_structured(
|
|
78
|
+
role="prompt_enhancer",
|
|
79
|
+
messages=[{"role": "user", "content": prompt}],
|
|
80
|
+
schema=PromptEnhancement,
|
|
81
|
+
)
|
|
82
|
+
return enhancement.normalized(goal)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _clean_items(items: list[str]) -> list[str]:
|
|
86
|
+
return [item.strip() for item in items if item.strip()]
|