devcouncil 0.1.0 → 0.2.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 -201
- package/README.md +197 -494
- package/package.json +9 -2
- package/pyproject.toml +62 -27
- package/src/devcouncil/__main__.py +4 -4
- package/src/devcouncil/app/__init__.py +28 -28
- package/src/devcouncil/app/config.py +297 -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 +163 -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/assets/__init__.py +1 -0
- package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/commands/agents.py +292 -0
- package/src/devcouncil/cli/commands/artifacts.py +54 -48
- package/src/devcouncil/cli/commands/ast.py +22 -0
- package/src/devcouncil/cli/commands/baseline.py +35 -32
- package/src/devcouncil/cli/commands/check.py +209 -0
- package/src/devcouncil/cli/commands/config.py +115 -54
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +31 -0
- package/src/devcouncil/cli/commands/doctor.py +291 -47
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +656 -0
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +209 -33
- package/src/devcouncil/cli/commands/init.py +204 -57
- package/src/devcouncil/cli/commands/integrate.py +1171 -76
- package/src/devcouncil/cli/commands/lsp.py +20 -0
- package/src/devcouncil/cli/commands/map.py +96 -22
- package/src/devcouncil/cli/commands/plan.py +422 -210
- package/src/devcouncil/cli/commands/prompt.py +48 -34
- package/src/devcouncil/cli/commands/repair.py +89 -69
- package/src/devcouncil/cli/commands/report.py +120 -54
- package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
- package/src/devcouncil/cli/commands/rollback.py +55 -54
- package/src/devcouncil/cli/commands/run.py +285 -220
- package/src/devcouncil/cli/commands/runs.py +223 -0
- package/src/devcouncil/cli/commands/scaffold.py +32 -0
- package/src/devcouncil/cli/commands/semantic.py +47 -0
- package/src/devcouncil/cli/commands/setup.py +300 -20
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/show.py +76 -57
- package/src/devcouncil/cli/commands/skills.py +88 -0
- package/src/devcouncil/cli/commands/status.py +141 -105
- package/src/devcouncil/cli/commands/tasks.py +55 -41
- package/src/devcouncil/cli/commands/trace.py +49 -4
- package/src/devcouncil/cli/commands/verify.py +293 -128
- package/src/devcouncil/cli/commands/version.py +20 -20
- package/src/devcouncil/cli/commands/watch.py +574 -0
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +92 -25
- 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 +47 -27
- package/src/devcouncil/domain/gap.py +52 -26
- package/src/devcouncil/domain/requirement.py +22 -22
- package/src/devcouncil/domain/task.py +55 -26
- package/src/devcouncil/execution/__init__.py +1 -1
- package/src/devcouncil/execution/checkpoints.py +246 -0
- package/src/devcouncil/execution/context_builder.py +54 -54
- package/src/devcouncil/execution/executor.py +15 -15
- package/src/devcouncil/execution/fs_watcher.py +180 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +186 -77
- package/src/devcouncil/execution/patch.py +77 -28
- package/src/devcouncil/execution/permissions.py +52 -59
- package/src/devcouncil/execution/policy_engine.py +343 -0
- package/src/devcouncil/execution/prompt_builder.py +650 -38
- package/src/devcouncil/execution/shell_session.py +225 -0
- package/src/devcouncil/execution/task_runner.py +68 -64
- package/src/devcouncil/executors/__init__.py +1 -1
- package/src/devcouncil/executors/agent_registry.py +575 -0
- package/src/devcouncil/executors/coding_cli.py +736 -0
- package/src/devcouncil/executors/mini_swe.py +63 -63
- package/src/devcouncil/executors/native/agent.py +186 -85
- package/src/devcouncil/executors/openhands.py +56 -56
- package/src/devcouncil/gating/__init__.py +1 -1
- package/src/devcouncil/gating/checks/clean_git.py +52 -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 +53 -34
- package/src/devcouncil/gating/policy.py +315 -167
- package/src/devcouncil/hardware.py +184 -0
- 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 +161 -0
- package/src/devcouncil/indexing/repo_mapper.py +1455 -204
- package/src/devcouncil/indexing/semantic_index.py +205 -0
- package/src/devcouncil/integrations/actions.py +146 -0
- package/src/devcouncil/integrations/check.py +423 -0
- package/src/devcouncil/integrations/github.py +35 -35
- package/src/devcouncil/integrations/github_intent.py +142 -0
- package/src/devcouncil/integrations/gitnexus.py +62 -27
- package/src/devcouncil/integrations/graphify.py +34 -34
- package/src/devcouncil/integrations/mcp/server.py +2072 -96
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/integrations/pr_comments.py +62 -0
- package/src/devcouncil/live/__init__.py +2 -0
- package/src/devcouncil/live/cards.py +349 -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 +141 -0
- package/src/devcouncil/llm/__init__.py +1 -1
- package/src/devcouncil/llm/cache.py +42 -38
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +627 -125
- package/src/devcouncil/llm/router.py +303 -118
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/planning/__init__.py +1 -1
- package/src/devcouncil/planning/arbiter_service.py +57 -57
- package/src/devcouncil/planning/correction_manifest.py +303 -0
- package/src/devcouncil/planning/critique_service.py +71 -66
- package/src/devcouncil/planning/plan_service.py +60 -46
- package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
- package/src/devcouncil/planning/repair_service.py +39 -39
- package/src/devcouncil/planning/spec_service.py +70 -44
- package/src/devcouncil/repo/ci_scaffold.py +157 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +374 -0
- package/src/devcouncil/reporting/github_check.py +32 -32
- package/src/devcouncil/reporting/json_report.py +30 -17
- package/src/devcouncil/reporting/markdown_report.py +83 -46
- package/src/devcouncil/reporting/report_builder.py +14 -14
- package/src/devcouncil/skills/__init__.py +19 -0
- package/src/devcouncil/skills/library/README.md +46 -0
- package/src/devcouncil/skills/library/ai-training.md +50 -0
- package/src/devcouncil/skills/library/android.md +50 -0
- package/src/devcouncil/skills/library/backend.md +52 -0
- package/src/devcouncil/skills/library/core-engineering.md +95 -0
- package/src/devcouncil/skills/library/data-engineering.md +47 -0
- package/src/devcouncil/skills/library/desktop.md +46 -0
- package/src/devcouncil/skills/library/devops.md +48 -0
- package/src/devcouncil/skills/library/game-dev.md +46 -0
- package/src/devcouncil/skills/library/ios.md +48 -0
- package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
- package/src/devcouncil/skills/library/security.md +48 -0
- package/src/devcouncil/skills/library/systems.md +48 -0
- package/src/devcouncil/skills/library/web.md +47 -0
- package/src/devcouncil/skills/library/windows.md +47 -0
- package/src/devcouncil/skills/registry.py +330 -0
- package/src/devcouncil/storage/db.py +147 -66
- package/src/devcouncil/storage/models.py +204 -83
- package/src/devcouncil/storage/native.py +557 -0
- package/src/devcouncil/storage/repositories.py +388 -249
- package/src/devcouncil/telemetry/cost.py +140 -34
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +52 -49
- package/src/devcouncil/ui/__init__.py +1 -0
- package/src/devcouncil/ui/dashboard.py +423 -0
- package/src/devcouncil/utils/__init__.py +1 -1
- package/src/devcouncil/utils/redaction.py +147 -141
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/__init__.py +1 -1
- package/src/devcouncil/verification/acceptance_compiler.py +125 -0
- package/src/devcouncil/verification/ad_hoc_check.py +129 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/implementation_reviewer.py +55 -55
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +178 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1342 -307
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
import yaml # type: ignore[import-untyped]
|
|
13
|
+
|
|
14
|
+
from devcouncil.executors.agent_registry import (
|
|
15
|
+
load_agent_profiles,
|
|
16
|
+
load_cli_agent_specs,
|
|
17
|
+
normalize_agent_name,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
DEFAULT_OBJECTIVE = (
|
|
22
|
+
"Optimize the DevCouncil CLI-agent profile prompt preamble so coding agents stay inside "
|
|
23
|
+
"planned scope, run the expected verification commands, preserve evidence, avoid destructive "
|
|
24
|
+
"or out-of-policy changes, and address the observed failures in the evaluation examples."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class GepaUnavailableError(RuntimeError):
|
|
29
|
+
"""Raised when the optional GEPA package is not installed."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class AgentPromptOptimizationResult:
|
|
34
|
+
agent: str
|
|
35
|
+
profile_name: str
|
|
36
|
+
seed_preamble: str
|
|
37
|
+
best_preamble: str
|
|
38
|
+
best_score: float | None
|
|
39
|
+
artifact_path: Path
|
|
40
|
+
applied: bool
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def optimize_agent_profile(
|
|
44
|
+
*,
|
|
45
|
+
project_root: Path,
|
|
46
|
+
agent: str,
|
|
47
|
+
profile_name: str,
|
|
48
|
+
evals_path: Path,
|
|
49
|
+
max_metric_calls: int = 40,
|
|
50
|
+
objective: str | None = None,
|
|
51
|
+
apply: bool = False,
|
|
52
|
+
output_path: Path | None = None,
|
|
53
|
+
) -> AgentPromptOptimizationResult:
|
|
54
|
+
"""Run GEPA over a CLI-agent profile prompt preamble.
|
|
55
|
+
|
|
56
|
+
GEPA needs an evaluator. DevCouncil's evaluator consumes an offline JSON/JSONL
|
|
57
|
+
dataset of observed agent failures and desired prompt behavior, then feeds that
|
|
58
|
+
context back to GEPA as actionable side information.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
root = project_root.expanduser().resolve()
|
|
62
|
+
normalized_agent = normalize_agent_name(agent)
|
|
63
|
+
if normalized_agent not in load_cli_agent_specs(root):
|
|
64
|
+
raise ValueError(f"Agent '{agent}' is not registered or supported.")
|
|
65
|
+
|
|
66
|
+
profiles = load_agent_profiles(root)
|
|
67
|
+
profile = profiles.get(profile_name)
|
|
68
|
+
if profile is None:
|
|
69
|
+
raise ValueError(f"Profile '{profile_name}' is not configured.")
|
|
70
|
+
|
|
71
|
+
resolved_evals_path = evals_path.expanduser()
|
|
72
|
+
if not resolved_evals_path.is_absolute():
|
|
73
|
+
resolved_evals_path = root / resolved_evals_path
|
|
74
|
+
dataset = load_agent_eval_dataset(resolved_evals_path)
|
|
75
|
+
gepa_module = _load_gepa_optimize_anything()
|
|
76
|
+
seed_preamble = profile.prompt_preamble or ""
|
|
77
|
+
effective_objective = objective or DEFAULT_OBJECTIVE
|
|
78
|
+
|
|
79
|
+
def evaluator(candidate: Any, example: dict[str, Any] | None = None) -> float:
|
|
80
|
+
if example is None:
|
|
81
|
+
scores = [
|
|
82
|
+
_score_candidate(
|
|
83
|
+
candidate,
|
|
84
|
+
item,
|
|
85
|
+
log=getattr(gepa_module, "log", lambda message: None),
|
|
86
|
+
)
|
|
87
|
+
for item in dataset
|
|
88
|
+
]
|
|
89
|
+
return sum(scores) / len(scores)
|
|
90
|
+
return _score_candidate(
|
|
91
|
+
candidate,
|
|
92
|
+
example,
|
|
93
|
+
log=getattr(gepa_module, "log", lambda message: None),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
engine = gepa_module.EngineConfig(max_metric_calls=max_metric_calls)
|
|
97
|
+
config = gepa_module.GEPAConfig(engine=engine)
|
|
98
|
+
gepa_result = gepa_module.optimize_anything(
|
|
99
|
+
seed_candidate=seed_preamble,
|
|
100
|
+
evaluator=evaluator,
|
|
101
|
+
dataset=dataset,
|
|
102
|
+
objective=effective_objective,
|
|
103
|
+
config=config,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
best_preamble = _candidate_to_text(_best_candidate(gepa_result)).strip()
|
|
107
|
+
if not best_preamble:
|
|
108
|
+
best_preamble = seed_preamble
|
|
109
|
+
best_score = _best_score(gepa_result)
|
|
110
|
+
|
|
111
|
+
artifact_path = output_path or _default_artifact_path(root, normalized_agent, profile_name)
|
|
112
|
+
artifact_path = artifact_path.expanduser()
|
|
113
|
+
if not artifact_path.is_absolute():
|
|
114
|
+
artifact_path = root / artifact_path
|
|
115
|
+
_write_result_artifact(
|
|
116
|
+
artifact_path,
|
|
117
|
+
{
|
|
118
|
+
"optimizer": "gepa.optimize_anything",
|
|
119
|
+
"agent": normalized_agent,
|
|
120
|
+
"profile": profile_name,
|
|
121
|
+
"objective": effective_objective,
|
|
122
|
+
"evals_path": str(resolved_evals_path),
|
|
123
|
+
"max_metric_calls": max_metric_calls,
|
|
124
|
+
"example_count": len(dataset),
|
|
125
|
+
"seed_preamble": seed_preamble,
|
|
126
|
+
"best_preamble": best_preamble,
|
|
127
|
+
"best_score": best_score,
|
|
128
|
+
"applied": apply,
|
|
129
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if apply:
|
|
134
|
+
_apply_profile_preamble(root, profile_name, best_preamble)
|
|
135
|
+
|
|
136
|
+
return AgentPromptOptimizationResult(
|
|
137
|
+
agent=normalized_agent,
|
|
138
|
+
profile_name=profile_name,
|
|
139
|
+
seed_preamble=seed_preamble,
|
|
140
|
+
best_preamble=best_preamble,
|
|
141
|
+
best_score=best_score,
|
|
142
|
+
artifact_path=artifact_path,
|
|
143
|
+
applied=apply,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def load_agent_eval_dataset(path: Path) -> list[dict[str, Any]]:
|
|
148
|
+
evals_path = path.expanduser()
|
|
149
|
+
if not evals_path.exists():
|
|
150
|
+
raise ValueError(f"GEPA eval dataset not found: {evals_path}")
|
|
151
|
+
|
|
152
|
+
if evals_path.suffix.lower() == ".jsonl":
|
|
153
|
+
examples = [
|
|
154
|
+
json.loads(line)
|
|
155
|
+
for line in evals_path.read_text(encoding="utf-8").splitlines()
|
|
156
|
+
if line.strip()
|
|
157
|
+
]
|
|
158
|
+
else:
|
|
159
|
+
raw = json.loads(evals_path.read_text(encoding="utf-8"))
|
|
160
|
+
if isinstance(raw, dict):
|
|
161
|
+
if isinstance(raw.get("examples"), list):
|
|
162
|
+
examples = raw["examples"]
|
|
163
|
+
elif isinstance(raw.get("dataset"), list):
|
|
164
|
+
examples = raw["dataset"]
|
|
165
|
+
else:
|
|
166
|
+
examples = [raw]
|
|
167
|
+
elif isinstance(raw, list):
|
|
168
|
+
examples = raw
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError("GEPA eval dataset must be a JSON object, JSON array, or JSONL file.")
|
|
171
|
+
|
|
172
|
+
normalized: list[dict[str, Any]] = []
|
|
173
|
+
for index, item in enumerate(examples, start=1):
|
|
174
|
+
if not isinstance(item, dict):
|
|
175
|
+
raise ValueError("Every GEPA eval example must be a JSON object.")
|
|
176
|
+
example = dict(item)
|
|
177
|
+
example.setdefault("id", f"example-{index}")
|
|
178
|
+
normalized.append(example)
|
|
179
|
+
|
|
180
|
+
if not normalized:
|
|
181
|
+
raise ValueError("GEPA eval dataset must contain at least one example.")
|
|
182
|
+
return normalized
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _load_gepa_optimize_anything() -> Any:
|
|
186
|
+
injected_module = sys.modules.get("gepa.optimize_anything")
|
|
187
|
+
if injected_module is not None:
|
|
188
|
+
return injected_module
|
|
189
|
+
try:
|
|
190
|
+
return importlib.import_module("gepa.optimize_anything")
|
|
191
|
+
except ImportError as exc:
|
|
192
|
+
raise GepaUnavailableError(
|
|
193
|
+
"GEPA is not installed in this environment. Reinstall or sync DevCouncil dependencies, "
|
|
194
|
+
"then rerun `dev agents optimize`."
|
|
195
|
+
) from exc
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _score_candidate(candidate: Any, example: dict[str, Any], *, log: Callable[[str], None]) -> float:
|
|
199
|
+
candidate_text = _candidate_to_text(candidate)
|
|
200
|
+
lower_candidate = candidate_text.lower()
|
|
201
|
+
required_terms = _string_list(example, "required_terms", "expected_prompt_fragments", "must_include")
|
|
202
|
+
forbidden_terms = _string_list(example, "forbidden_terms", "must_avoid")
|
|
203
|
+
|
|
204
|
+
log(f"Example {example.get('id')}: {example.get('task', '')}".strip())
|
|
205
|
+
for key in ("observed_failure", "desired_behavior", "feedback", "rubric"):
|
|
206
|
+
value = example.get(key)
|
|
207
|
+
if value:
|
|
208
|
+
log(f"{key.replace('_', ' ').title()}: {value}")
|
|
209
|
+
if required_terms:
|
|
210
|
+
log(f"Required prompt terms: {', '.join(required_terms)}")
|
|
211
|
+
if forbidden_terms:
|
|
212
|
+
log(f"Forbidden prompt terms: {', '.join(forbidden_terms)}")
|
|
213
|
+
|
|
214
|
+
if not candidate_text.strip():
|
|
215
|
+
log("Candidate preamble is empty.")
|
|
216
|
+
return 0.0
|
|
217
|
+
|
|
218
|
+
required_hits = [term for term in required_terms if term.lower() in lower_candidate]
|
|
219
|
+
forbidden_hits = [term for term in forbidden_terms if term.lower() in lower_candidate]
|
|
220
|
+
missing_terms = [term for term in required_terms if term not in required_hits]
|
|
221
|
+
if missing_terms:
|
|
222
|
+
log(f"Missing required terms: {', '.join(missing_terms)}")
|
|
223
|
+
if forbidden_hits:
|
|
224
|
+
log(f"Forbidden terms present: {', '.join(forbidden_hits)}")
|
|
225
|
+
|
|
226
|
+
required_score = len(required_hits) / len(required_terms) if required_terms else 0.5
|
|
227
|
+
forbidden_score = 1.0 - (len(forbidden_hits) / len(forbidden_terms)) if forbidden_terms else 1.0
|
|
228
|
+
length_score = _length_score(candidate_text)
|
|
229
|
+
score = (required_score * 0.65) + (forbidden_score * 0.25) + (length_score * 0.10)
|
|
230
|
+
return max(0.0, min(1.0, score))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _candidate_to_text(candidate: Any) -> str:
|
|
234
|
+
if candidate is None:
|
|
235
|
+
return ""
|
|
236
|
+
if isinstance(candidate, str):
|
|
237
|
+
return candidate
|
|
238
|
+
if isinstance(candidate, dict):
|
|
239
|
+
for key in ("prompt_preamble", "preamble", "prompt", "system_prompt", "text"):
|
|
240
|
+
value = candidate.get(key)
|
|
241
|
+
if isinstance(value, str):
|
|
242
|
+
return value
|
|
243
|
+
return json.dumps(candidate, sort_keys=True)
|
|
244
|
+
for attr in ("prompt_preamble", "preamble", "prompt", "system_prompt", "text"):
|
|
245
|
+
value = getattr(candidate, attr, None)
|
|
246
|
+
if isinstance(value, str):
|
|
247
|
+
return value
|
|
248
|
+
return str(candidate)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _best_candidate(result: Any) -> Any:
|
|
252
|
+
if isinstance(result, dict):
|
|
253
|
+
for key in ("best_candidate", "candidate", "best"):
|
|
254
|
+
if key in result:
|
|
255
|
+
return result[key]
|
|
256
|
+
return result
|
|
257
|
+
for attr in ("best_candidate", "candidate", "best"):
|
|
258
|
+
if hasattr(result, attr):
|
|
259
|
+
return getattr(result, attr)
|
|
260
|
+
return result
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _best_score(result: Any) -> float | None:
|
|
264
|
+
raw_score: Any
|
|
265
|
+
if isinstance(result, dict):
|
|
266
|
+
raw_score = result.get("best_score", result.get("score"))
|
|
267
|
+
else:
|
|
268
|
+
raw_score = getattr(result, "best_score", getattr(result, "score", None))
|
|
269
|
+
if raw_score is None:
|
|
270
|
+
return None
|
|
271
|
+
try:
|
|
272
|
+
return float(raw_score)
|
|
273
|
+
except (TypeError, ValueError):
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _string_list(example: dict[str, Any], *keys: str) -> list[str]:
|
|
278
|
+
values: list[str] = []
|
|
279
|
+
for key in keys:
|
|
280
|
+
raw = example.get(key)
|
|
281
|
+
if raw is None:
|
|
282
|
+
continue
|
|
283
|
+
if isinstance(raw, str):
|
|
284
|
+
values.append(raw)
|
|
285
|
+
elif isinstance(raw, Iterable):
|
|
286
|
+
values.extend(str(item) for item in raw if str(item).strip())
|
|
287
|
+
return [value.strip() for value in values if value.strip()]
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _length_score(candidate_text: str) -> float:
|
|
291
|
+
words = candidate_text.split()
|
|
292
|
+
if 10 <= len(words) <= 180:
|
|
293
|
+
return 1.0
|
|
294
|
+
if len(words) < 10:
|
|
295
|
+
return len(words) / 10
|
|
296
|
+
return max(0.0, 1.0 - ((len(words) - 180) / 180))
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _default_artifact_path(project_root: Path, agent: str, profile_name: str) -> Path:
|
|
300
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
301
|
+
safe_profile = profile_name.replace("/", "-").replace("\\", "-")
|
|
302
|
+
return project_root / ".devcouncil" / "optimizations" / f"{timestamp}-{agent}-{safe_profile}-gepa.json"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _write_result_artifact(path: Path, payload: dict[str, Any]) -> None:
|
|
306
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
307
|
+
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _apply_profile_preamble(project_root: Path, profile_name: str, best_preamble: str) -> None:
|
|
311
|
+
config_path = project_root / ".devcouncil" / "config.yaml"
|
|
312
|
+
raw_config = yaml.safe_load(config_path.read_text(encoding="utf-8")) if config_path.exists() else {}
|
|
313
|
+
raw_config = raw_config or {}
|
|
314
|
+
profiles = raw_config.setdefault("integrations", {}).setdefault("cli_agents", {}).setdefault("profiles", {})
|
|
315
|
+
profile = profiles.setdefault(profile_name, {})
|
|
316
|
+
profile["prompt_preamble"] = best_preamble
|
|
317
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
318
|
+
config_path.write_text(yaml.safe_dump(raw_config, sort_keys=False), encoding="utf-8")
|
|
@@ -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
|
+
)
|