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.
Files changed (190) hide show
  1. package/LICENSE +201 -201
  2. package/README.md +197 -494
  3. package/package.json +9 -2
  4. package/pyproject.toml +62 -27
  5. package/src/devcouncil/__main__.py +4 -4
  6. package/src/devcouncil/app/__init__.py +28 -28
  7. package/src/devcouncil/app/config.py +297 -108
  8. package/src/devcouncil/app/errors.py +23 -23
  9. package/src/devcouncil/app/events.py +44 -44
  10. package/src/devcouncil/app/orchestrator.py +67 -67
  11. package/src/devcouncil/app/project_status.py +29 -0
  12. package/src/devcouncil/app/run_context.py +39 -39
  13. package/src/devcouncil/app/state_machine.py +108 -108
  14. package/src/devcouncil/artifacts/__init__.py +1 -1
  15. package/src/devcouncil/artifacts/coverage.py +96 -96
  16. package/src/devcouncil/artifacts/graph.py +163 -143
  17. package/src/devcouncil/artifacts/migrations.py +20 -20
  18. package/src/devcouncil/artifacts/schemas.py +23 -23
  19. package/src/devcouncil/artifacts/serializer.py +21 -21
  20. package/src/devcouncil/artifacts/validators.py +27 -27
  21. package/src/devcouncil/assets/__init__.py +1 -0
  22. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  23. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  24. package/src/devcouncil/cli/commands/agents.py +292 -0
  25. package/src/devcouncil/cli/commands/artifacts.py +54 -48
  26. package/src/devcouncil/cli/commands/ast.py +22 -0
  27. package/src/devcouncil/cli/commands/baseline.py +35 -32
  28. package/src/devcouncil/cli/commands/check.py +209 -0
  29. package/src/devcouncil/cli/commands/config.py +115 -54
  30. package/src/devcouncil/cli/commands/cost.py +57 -0
  31. package/src/devcouncil/cli/commands/dashboard.py +31 -0
  32. package/src/devcouncil/cli/commands/doctor.py +291 -47
  33. package/src/devcouncil/cli/commands/evidence.py +48 -0
  34. package/src/devcouncil/cli/commands/go.py +656 -0
  35. package/src/devcouncil/cli/commands/handoff.py +69 -0
  36. package/src/devcouncil/cli/commands/hook.py +209 -33
  37. package/src/devcouncil/cli/commands/init.py +204 -57
  38. package/src/devcouncil/cli/commands/integrate.py +1171 -76
  39. package/src/devcouncil/cli/commands/lsp.py +20 -0
  40. package/src/devcouncil/cli/commands/map.py +96 -22
  41. package/src/devcouncil/cli/commands/plan.py +422 -210
  42. package/src/devcouncil/cli/commands/prompt.py +48 -34
  43. package/src/devcouncil/cli/commands/repair.py +89 -69
  44. package/src/devcouncil/cli/commands/report.py +120 -54
  45. package/src/devcouncil/cli/commands/reset_demo_state.py +33 -28
  46. package/src/devcouncil/cli/commands/rollback.py +55 -54
  47. package/src/devcouncil/cli/commands/run.py +285 -220
  48. package/src/devcouncil/cli/commands/runs.py +223 -0
  49. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  50. package/src/devcouncil/cli/commands/semantic.py +47 -0
  51. package/src/devcouncil/cli/commands/setup.py +300 -20
  52. package/src/devcouncil/cli/commands/shell.py +73 -0
  53. package/src/devcouncil/cli/commands/show.py +76 -57
  54. package/src/devcouncil/cli/commands/skills.py +88 -0
  55. package/src/devcouncil/cli/commands/status.py +141 -105
  56. package/src/devcouncil/cli/commands/tasks.py +55 -41
  57. package/src/devcouncil/cli/commands/trace.py +49 -4
  58. package/src/devcouncil/cli/commands/verify.py +293 -128
  59. package/src/devcouncil/cli/commands/version.py +20 -20
  60. package/src/devcouncil/cli/commands/watch.py +574 -0
  61. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  62. package/src/devcouncil/cli/main.py +92 -25
  63. package/src/devcouncil/council/prompts/arbiter.md +19 -19
  64. package/src/devcouncil/council/prompts/critic_a.md +10 -10
  65. package/src/devcouncil/council/prompts/critic_b.md +10 -10
  66. package/src/devcouncil/council/prompts/implementation_reviewer.md +16 -16
  67. package/src/devcouncil/council/prompts/planner_a.md +16 -16
  68. package/src/devcouncil/council/prompts/planner_b.md +16 -16
  69. package/src/devcouncil/council/prompts/rebuttal.md +10 -10
  70. package/src/devcouncil/council/prompts/spec_writer.md +12 -12
  71. package/src/devcouncil/domain/assumption.py +17 -17
  72. package/src/devcouncil/domain/critique.py +32 -32
  73. package/src/devcouncil/domain/evidence.py +47 -27
  74. package/src/devcouncil/domain/gap.py +52 -26
  75. package/src/devcouncil/domain/requirement.py +22 -22
  76. package/src/devcouncil/domain/task.py +55 -26
  77. package/src/devcouncil/execution/__init__.py +1 -1
  78. package/src/devcouncil/execution/checkpoints.py +246 -0
  79. package/src/devcouncil/execution/context_builder.py +54 -54
  80. package/src/devcouncil/execution/executor.py +15 -15
  81. package/src/devcouncil/execution/fs_watcher.py +180 -0
  82. package/src/devcouncil/execution/handoff.py +102 -0
  83. package/src/devcouncil/execution/hook_policy.py +186 -77
  84. package/src/devcouncil/execution/patch.py +77 -28
  85. package/src/devcouncil/execution/permissions.py +52 -59
  86. package/src/devcouncil/execution/policy_engine.py +343 -0
  87. package/src/devcouncil/execution/prompt_builder.py +650 -38
  88. package/src/devcouncil/execution/shell_session.py +225 -0
  89. package/src/devcouncil/execution/task_runner.py +68 -64
  90. package/src/devcouncil/executors/__init__.py +1 -1
  91. package/src/devcouncil/executors/agent_registry.py +575 -0
  92. package/src/devcouncil/executors/coding_cli.py +736 -0
  93. package/src/devcouncil/executors/mini_swe.py +63 -63
  94. package/src/devcouncil/executors/native/agent.py +186 -85
  95. package/src/devcouncil/executors/openhands.py +56 -56
  96. package/src/devcouncil/gating/__init__.py +1 -1
  97. package/src/devcouncil/gating/checks/clean_git.py +52 -45
  98. package/src/devcouncil/gating/checks/planned_files_check.py +32 -32
  99. package/src/devcouncil/gating/checks/requirement_coverage.py +26 -26
  100. package/src/devcouncil/gating/checks/secret_scan_check.py +53 -34
  101. package/src/devcouncil/gating/policy.py +315 -167
  102. package/src/devcouncil/hardware.py +184 -0
  103. package/src/devcouncil/indexing/__init__.py +1 -1
  104. package/src/devcouncil/indexing/ast_matcher.py +168 -0
  105. package/src/devcouncil/indexing/graph_index.py +48 -48
  106. package/src/devcouncil/indexing/lsp.py +161 -0
  107. package/src/devcouncil/indexing/repo_mapper.py +1455 -204
  108. package/src/devcouncil/indexing/semantic_index.py +205 -0
  109. package/src/devcouncil/integrations/actions.py +146 -0
  110. package/src/devcouncil/integrations/check.py +423 -0
  111. package/src/devcouncil/integrations/github.py +35 -35
  112. package/src/devcouncil/integrations/github_intent.py +142 -0
  113. package/src/devcouncil/integrations/gitnexus.py +62 -27
  114. package/src/devcouncil/integrations/graphify.py +34 -34
  115. package/src/devcouncil/integrations/mcp/server.py +2072 -96
  116. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  117. package/src/devcouncil/integrations/pr_comments.py +62 -0
  118. package/src/devcouncil/live/__init__.py +2 -0
  119. package/src/devcouncil/live/cards.py +349 -0
  120. package/src/devcouncil/live/models.py +63 -0
  121. package/src/devcouncil/live/repair_prompt.py +83 -0
  122. package/src/devcouncil/live/reviewer.py +70 -0
  123. package/src/devcouncil/live/signals.py +135 -0
  124. package/src/devcouncil/live/summary.py +34 -0
  125. package/src/devcouncil/live/tasks.py +18 -0
  126. package/src/devcouncil/live/transcripts.py +141 -0
  127. package/src/devcouncil/llm/__init__.py +1 -1
  128. package/src/devcouncil/llm/cache.py +42 -38
  129. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  130. package/src/devcouncil/llm/provider.py +627 -125
  131. package/src/devcouncil/llm/router.py +303 -118
  132. package/src/devcouncil/optimization/__init__.py +1 -0
  133. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  134. package/src/devcouncil/planning/__init__.py +1 -1
  135. package/src/devcouncil/planning/arbiter_service.py +57 -57
  136. package/src/devcouncil/planning/correction_manifest.py +303 -0
  137. package/src/devcouncil/planning/critique_service.py +71 -66
  138. package/src/devcouncil/planning/plan_service.py +60 -46
  139. package/src/devcouncil/planning/prompt_enhancer_service.py +167 -0
  140. package/src/devcouncil/planning/repair_service.py +39 -39
  141. package/src/devcouncil/planning/spec_service.py +70 -44
  142. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  143. package/src/devcouncil/repo/gitignore.py +123 -0
  144. package/src/devcouncil/repo/sca.py +374 -0
  145. package/src/devcouncil/reporting/github_check.py +32 -32
  146. package/src/devcouncil/reporting/json_report.py +30 -17
  147. package/src/devcouncil/reporting/markdown_report.py +83 -46
  148. package/src/devcouncil/reporting/report_builder.py +14 -14
  149. package/src/devcouncil/skills/__init__.py +19 -0
  150. package/src/devcouncil/skills/library/README.md +46 -0
  151. package/src/devcouncil/skills/library/ai-training.md +50 -0
  152. package/src/devcouncil/skills/library/android.md +50 -0
  153. package/src/devcouncil/skills/library/backend.md +52 -0
  154. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  155. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  156. package/src/devcouncil/skills/library/desktop.md +46 -0
  157. package/src/devcouncil/skills/library/devops.md +48 -0
  158. package/src/devcouncil/skills/library/game-dev.md +46 -0
  159. package/src/devcouncil/skills/library/ios.md +48 -0
  160. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  161. package/src/devcouncil/skills/library/security.md +48 -0
  162. package/src/devcouncil/skills/library/systems.md +48 -0
  163. package/src/devcouncil/skills/library/web.md +47 -0
  164. package/src/devcouncil/skills/library/windows.md +47 -0
  165. package/src/devcouncil/skills/registry.py +330 -0
  166. package/src/devcouncil/storage/db.py +147 -66
  167. package/src/devcouncil/storage/models.py +204 -83
  168. package/src/devcouncil/storage/native.py +557 -0
  169. package/src/devcouncil/storage/repositories.py +388 -249
  170. package/src/devcouncil/telemetry/cost.py +140 -34
  171. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  172. package/src/devcouncil/telemetry/pricing.py +28 -0
  173. package/src/devcouncil/telemetry/traces.py +62 -7
  174. package/src/devcouncil/telemetry/tracker.py +52 -49
  175. package/src/devcouncil/ui/__init__.py +1 -0
  176. package/src/devcouncil/ui/dashboard.py +423 -0
  177. package/src/devcouncil/utils/__init__.py +1 -1
  178. package/src/devcouncil/utils/redaction.py +147 -141
  179. package/src/devcouncil/utils/subprocess_env.py +69 -0
  180. package/src/devcouncil/verification/__init__.py +1 -1
  181. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  182. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  183. package/src/devcouncil/verification/diff_coverage.py +353 -0
  184. package/src/devcouncil/verification/implementation_reviewer.py +55 -55
  185. package/src/devcouncil/verification/next_actions.py +189 -0
  186. package/src/devcouncil/verification/sandbox.py +178 -0
  187. package/src/devcouncil/verification/test_resolver.py +91 -0
  188. package/src/devcouncil/verification/verifier.py +1342 -307
  189. package/uv.lock +205 -64
  190. 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
+ )