devcouncil 0.1.1 → 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 (129) hide show
  1. package/README.md +190 -6
  2. package/package.json +9 -2
  3. package/pyproject.toml +34 -2
  4. package/src/devcouncil/app/config.py +167 -5
  5. package/src/devcouncil/artifacts/graph.py +23 -3
  6. package/src/devcouncil/assets/__init__.py +1 -0
  7. package/src/devcouncil/assets/devcouncil-logo.svg +60 -0
  8. package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
  9. package/src/devcouncil/cli/commands/agents.py +292 -0
  10. package/src/devcouncil/cli/commands/artifacts.py +6 -3
  11. package/src/devcouncil/cli/commands/check.py +209 -0
  12. package/src/devcouncil/cli/commands/config.py +43 -4
  13. package/src/devcouncil/cli/commands/cost.py +57 -0
  14. package/src/devcouncil/cli/commands/dashboard.py +6 -1
  15. package/src/devcouncil/cli/commands/doctor.py +221 -21
  16. package/src/devcouncil/cli/commands/evidence.py +48 -0
  17. package/src/devcouncil/cli/commands/go.py +452 -33
  18. package/src/devcouncil/cli/commands/handoff.py +69 -0
  19. package/src/devcouncil/cli/commands/hook.py +124 -15
  20. package/src/devcouncil/cli/commands/init.py +154 -18
  21. package/src/devcouncil/cli/commands/integrate.py +894 -105
  22. package/src/devcouncil/cli/commands/map.py +80 -10
  23. package/src/devcouncil/cli/commands/plan.py +212 -51
  24. package/src/devcouncil/cli/commands/prompt.py +18 -7
  25. package/src/devcouncil/cli/commands/repair.py +40 -23
  26. package/src/devcouncil/cli/commands/report.py +8 -0
  27. package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
  28. package/src/devcouncil/cli/commands/rollback.py +27 -28
  29. package/src/devcouncil/cli/commands/run.py +69 -49
  30. package/src/devcouncil/cli/commands/runs.py +223 -0
  31. package/src/devcouncil/cli/commands/scaffold.py +32 -0
  32. package/src/devcouncil/cli/commands/semantic.py +47 -0
  33. package/src/devcouncil/cli/commands/setup.py +145 -6
  34. package/src/devcouncil/cli/commands/shell.py +73 -0
  35. package/src/devcouncil/cli/commands/skills.py +88 -0
  36. package/src/devcouncil/cli/commands/status.py +25 -1
  37. package/src/devcouncil/cli/commands/trace.py +47 -3
  38. package/src/devcouncil/cli/commands/verify.py +138 -3
  39. package/src/devcouncil/cli/commands/watch.py +9 -9
  40. package/src/devcouncil/cli/commands/watch_fs.py +40 -0
  41. package/src/devcouncil/cli/main.py +56 -7
  42. package/src/devcouncil/domain/evidence.py +22 -2
  43. package/src/devcouncil/domain/gap.py +27 -1
  44. package/src/devcouncil/domain/task.py +31 -2
  45. package/src/devcouncil/execution/checkpoints.py +246 -0
  46. package/src/devcouncil/execution/context_builder.py +1 -1
  47. package/src/devcouncil/execution/fs_watcher.py +180 -0
  48. package/src/devcouncil/execution/handoff.py +102 -0
  49. package/src/devcouncil/execution/hook_policy.py +162 -74
  50. package/src/devcouncil/execution/patch.py +59 -10
  51. package/src/devcouncil/execution/permissions.py +17 -24
  52. package/src/devcouncil/execution/policy_engine.py +343 -0
  53. package/src/devcouncil/execution/prompt_builder.py +633 -21
  54. package/src/devcouncil/execution/shell_session.py +225 -0
  55. package/src/devcouncil/execution/task_runner.py +6 -2
  56. package/src/devcouncil/executors/agent_registry.py +575 -0
  57. package/src/devcouncil/executors/coding_cli.py +663 -39
  58. package/src/devcouncil/executors/native/agent.py +121 -20
  59. package/src/devcouncil/gating/checks/clean_git.py +3 -1
  60. package/src/devcouncil/gating/checks/secret_scan_check.py +40 -21
  61. package/src/devcouncil/gating/policy.py +158 -10
  62. package/src/devcouncil/hardware.py +184 -0
  63. package/src/devcouncil/indexing/ast_matcher.py +1 -1
  64. package/src/devcouncil/indexing/lsp.py +45 -4
  65. package/src/devcouncil/indexing/repo_mapper.py +1256 -9
  66. package/src/devcouncil/indexing/semantic_index.py +205 -0
  67. package/src/devcouncil/integrations/actions.py +146 -0
  68. package/src/devcouncil/integrations/check.py +423 -0
  69. package/src/devcouncil/integrations/github_intent.py +142 -0
  70. package/src/devcouncil/integrations/gitnexus.py +35 -0
  71. package/src/devcouncil/integrations/mcp/server.py +1552 -29
  72. package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
  73. package/src/devcouncil/live/cards.py +161 -19
  74. package/src/devcouncil/live/signals.py +2 -2
  75. package/src/devcouncil/live/transcripts.py +9 -6
  76. package/src/devcouncil/llm/cache.py +10 -6
  77. package/src/devcouncil/llm/model_defaults.yaml +44 -0
  78. package/src/devcouncil/llm/provider.py +515 -34
  79. package/src/devcouncil/llm/router.py +231 -46
  80. package/src/devcouncil/optimization/__init__.py +1 -0
  81. package/src/devcouncil/optimization/gepa_agent.py +318 -0
  82. package/src/devcouncil/planning/correction_manifest.py +303 -0
  83. package/src/devcouncil/planning/critique_service.py +7 -2
  84. package/src/devcouncil/planning/plan_service.py +17 -3
  85. package/src/devcouncil/planning/prompt_enhancer_service.py +82 -1
  86. package/src/devcouncil/planning/spec_service.py +27 -1
  87. package/src/devcouncil/repo/ci_scaffold.py +157 -0
  88. package/src/devcouncil/repo/gitignore.py +123 -0
  89. package/src/devcouncil/repo/sca.py +374 -0
  90. package/src/devcouncil/reporting/json_report.py +11 -1
  91. package/src/devcouncil/reporting/markdown_report.py +15 -0
  92. package/src/devcouncil/skills/__init__.py +19 -0
  93. package/src/devcouncil/skills/library/README.md +46 -0
  94. package/src/devcouncil/skills/library/ai-training.md +50 -0
  95. package/src/devcouncil/skills/library/android.md +50 -0
  96. package/src/devcouncil/skills/library/backend.md +52 -0
  97. package/src/devcouncil/skills/library/core-engineering.md +95 -0
  98. package/src/devcouncil/skills/library/data-engineering.md +47 -0
  99. package/src/devcouncil/skills/library/desktop.md +46 -0
  100. package/src/devcouncil/skills/library/devops.md +48 -0
  101. package/src/devcouncil/skills/library/game-dev.md +46 -0
  102. package/src/devcouncil/skills/library/ios.md +48 -0
  103. package/src/devcouncil/skills/library/mobile-cross-platform.md +46 -0
  104. package/src/devcouncil/skills/library/security.md +48 -0
  105. package/src/devcouncil/skills/library/systems.md +48 -0
  106. package/src/devcouncil/skills/library/web.md +47 -0
  107. package/src/devcouncil/skills/library/windows.md +47 -0
  108. package/src/devcouncil/skills/registry.py +330 -0
  109. package/src/devcouncil/storage/db.py +83 -2
  110. package/src/devcouncil/storage/models.py +121 -0
  111. package/src/devcouncil/storage/native.py +557 -0
  112. package/src/devcouncil/storage/repositories.py +137 -75
  113. package/src/devcouncil/telemetry/cost.py +123 -17
  114. package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
  115. package/src/devcouncil/telemetry/pricing.py +28 -0
  116. package/src/devcouncil/telemetry/traces.py +62 -7
  117. package/src/devcouncil/telemetry/tracker.py +12 -9
  118. package/src/devcouncil/ui/dashboard.py +324 -23
  119. package/src/devcouncil/utils/redaction.py +9 -3
  120. package/src/devcouncil/utils/subprocess_env.py +69 -0
  121. package/src/devcouncil/verification/acceptance_compiler.py +125 -0
  122. package/src/devcouncil/verification/ad_hoc_check.py +129 -0
  123. package/src/devcouncil/verification/diff_coverage.py +353 -0
  124. package/src/devcouncil/verification/next_actions.py +189 -0
  125. package/src/devcouncil/verification/sandbox.py +178 -0
  126. package/src/devcouncil/verification/test_resolver.py +91 -0
  127. package/src/devcouncil/verification/verifier.py +1065 -47
  128. package/uv.lock +205 -64
  129. package/src/devcouncil/indexing/symbol_index.py +0 -0
@@ -0,0 +1,24 @@
1
+ import { spawnSync } from "node:child_process";
2
+
3
+ const projectRoot = process.env.DEVCOUNCIL_PROJECT_ROOT || process.cwd();
4
+
5
+ function runHook(event, payload) {
6
+ const args = ["hook", event, "--client", "opencode", "--project-root", projectRoot];
7
+ const result = spawnSync("devcouncil", args, {
8
+ input: JSON.stringify(payload ?? {}),
9
+ encoding: "utf-8",
10
+ env: { ...process.env, DEVCOUNCIL_PROJECT_ROOT: projectRoot },
11
+ });
12
+ if (result.status === 2) {
13
+ throw new Error(result.stderr || result.stdout || "DevCouncil blocked the tool call.");
14
+ }
15
+ }
16
+
17
+ export const DevCouncilOpenCodeHook = async () => ({
18
+ "tool.execute.before": async (input, output) => {
19
+ runHook("pre-tool-use", { tool: input.tool, arguments: output.args });
20
+ },
21
+ "tool.execute.after": async (input, output) => {
22
+ runHook("post-tool-use", { tool: input.tool, arguments: output.args });
23
+ },
24
+ });
@@ -2,9 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  import hashlib
4
4
  import json
5
+ import re
6
+ from dataclasses import dataclass
5
7
  from pathlib import Path
6
8
 
7
- from devcouncil.live.models import AgentTurn, CardStatus, CritiqueCard
9
+ from devcouncil.live.models import AgentTurn, CardStatus, CritiqueCard, Verdict
8
10
 
9
11
  RISK_TERMS = (
10
12
  "skip tests",
@@ -31,21 +33,171 @@ EVIDENCE_TERMS = (
31
33
  "verified",
32
34
  )
33
35
 
36
+ # Word-boundary matchers so "done" matches "I'm done" but not "abandoned"/"undone".
37
+ _COMPLETION_RE = re.compile(
38
+ r"\b(done|complete|completed|finished|implemented|fixed|ready|all set|"
39
+ r"ship it|good to go|works now|it works)\b"
40
+ )
41
+ # An agent asserting its verification actually passed (the claim we cross-check).
42
+ _PASS_CLAIM_RE = re.compile(
43
+ r"(tests?\s+(?:are\s+|now\s+)?pass(?:ing|ed|es)?"
44
+ r"|all\s+(?:tests?|checks?|cases?)\s+pass"
45
+ r"|passing\s+tests?"
46
+ r"|\bverified\b|verification\s+(?:pass|succeed)"
47
+ r"|tests?\s+green|green\s+tests?"
48
+ r"|(?:ran|run)\s+[^.\n]{0,40}?\bpass)"
49
+ )
50
+ # Negations that flip a nearby claim ("not done", "tests do not pass", "still failing").
51
+ _NEGATION_RE = re.compile(
52
+ r"\b(not|isn'?t|aren'?t|won'?t|can'?t|cannot|haven'?t|hasn'?t|don'?t|"
53
+ r"doesn'?t|didn'?t|no longer|never|yet to|still need|still failing|"
54
+ r"not yet|unable|fail(?:s|ing|ed)?)\b"
55
+ )
56
+ _NEGATION_WINDOW = 30
34
57
 
35
- def review_turn(turn: AgentTurn, project_root: Path, client: str | None = None) -> CritiqueCard:
36
- """Generate a deterministic critique card for an agent response."""
58
+
59
+ def _claim_present(pattern: re.Pattern[str], lower: str) -> bool:
60
+ """True if `pattern` matches and is not negated by a word shortly before it."""
61
+ for match in pattern.finditer(lower):
62
+ prefix = lower[max(0, match.start() - _NEGATION_WINDOW):match.start()]
63
+ if _NEGATION_RE.search(prefix):
64
+ continue
65
+ return True
66
+ return False
67
+
68
+
69
+ @dataclass
70
+ class _TaskGrounding:
71
+ """A snapshot of a task's real verification state from the artifact graph."""
72
+
73
+ task_id: str
74
+ status: str
75
+ blocking_gaps: int
76
+ failing_commands: int
77
+ acs_total: int
78
+ acs_passing: int
79
+
80
+ @property
81
+ def acs_unproven(self) -> int:
82
+ return max(0, self.acs_total - self.acs_passing)
83
+
84
+ @property
85
+ def is_satisfied(self) -> bool:
86
+ return (
87
+ self.status in ("verified", "done")
88
+ and self.blocking_gaps == 0
89
+ and (self.acs_total == 0 or self.acs_passing >= self.acs_total)
90
+ )
91
+
92
+
93
+ def _load_task_grounding(project_root: Path, task_id: str | None) -> _TaskGrounding | None:
94
+ """Load the scoped task's real verification state so claims can be checked
95
+ against evidence instead of trusted on the agent's word. Best-effort: any
96
+ failure (no DB, unknown task) returns None and the caller falls back to the
97
+ pure-heuristic review."""
98
+ if not task_id:
99
+ return None
100
+ try:
101
+ from devcouncil.storage.db import get_db
102
+ from devcouncil.storage.repositories import ArtifactGraphRepository
103
+
104
+ db = get_db(project_root)
105
+ if not db:
106
+ return None
107
+ with db.get_session() as session:
108
+ graph = ArtifactGraphRepository(session).load_graph()
109
+ except Exception:
110
+ return None
111
+
112
+ task = graph.tasks.get(task_id)
113
+ if task is None:
114
+ return None
115
+
116
+ blocking = [g for g in graph.gaps.values() if g.task_id == task_id and g.blocking]
117
+ failing = [g for g in blocking if g.gap_type == "test_failed"]
118
+ ac_ids = set(task.acceptance_criterion_ids)
119
+ passing_ac = {
120
+ ev.acceptance_criterion_id
121
+ for ev in graph.test_evidence
122
+ if ev.acceptance_criterion_id in ac_ids and getattr(ev, "status", "") == "passed"
123
+ }
124
+ return _TaskGrounding(
125
+ task_id=task_id,
126
+ status=task.status,
127
+ blocking_gaps=len(blocking),
128
+ failing_commands=len(failing),
129
+ acs_total=len(ac_ids),
130
+ acs_passing=len(passing_ac),
131
+ )
132
+
133
+
134
+ def review_turn(
135
+ turn: AgentTurn,
136
+ project_root: Path,
137
+ client: str | None = None,
138
+ task_id: str | None = None,
139
+ ) -> CritiqueCard:
140
+ """Generate a deterministic critique card for an agent response.
141
+
142
+ When ``task_id`` resolves to a known task, completion/verification claims are
143
+ checked against the task's real artifact state (status, blocking gaps, passing
144
+ acceptance-criterion evidence) instead of being trusted by keyword alone. With
145
+ no task state available it falls back to the lightweight keyword heuristic.
146
+ """
37
147
  content = turn.content.strip()
38
148
  lower = content.lower()
39
149
  concerns: list[str] = []
40
150
  alternatives: list[str] = []
41
151
  evidence_requests: list[str] = []
152
+ verdict: Verdict = "Approved"
153
+
154
+ grounding = _load_task_grounding(project_root, task_id)
42
155
 
43
156
  risky_terms = [term for term in RISK_TERMS if term in lower]
44
157
  if risky_terms:
45
158
  concerns.append(f"Response contains risky implementation language: {', '.join(risky_terms[:4])}.")
46
159
  alternatives.append("Replace risky shortcuts with a scoped implementation and explicit rollback or verification path.")
47
160
 
48
- if _looks_like_completion_claim(lower) and not any(term in lower for term in EVIDENCE_TERMS):
161
+ claims_completion = _claim_present(_COMPLETION_RE, lower)
162
+ claims_passing = _claim_present(_PASS_CLAIM_RE, lower)
163
+
164
+ if grounding is not None:
165
+ # Evidence-grounded review: cross-check the agent's claims against reality.
166
+ if claims_passing and grounding.failing_commands > 0:
167
+ concerns.append(
168
+ f"Agent claims verification passes, but DevCouncil recorded "
169
+ f"{grounding.failing_commands} failing verification command(s) for "
170
+ f"task {grounding.task_id}."
171
+ )
172
+ evidence_requests.append(
173
+ f"Re-run 'dev verify {grounding.task_id}' and fix the failing command(s) "
174
+ "before claiming success."
175
+ )
176
+ verdict = "Critical Issues"
177
+ elif (claims_completion or claims_passing) and not grounding.is_satisfied:
178
+ details = [f"task {grounding.task_id} is '{grounding.status}'"]
179
+ if grounding.blocking_gaps:
180
+ details.append(f"{grounding.blocking_gaps} blocking gap(s)")
181
+ if grounding.acs_unproven:
182
+ details.append(
183
+ f"{grounding.acs_unproven}/{grounding.acs_total} acceptance "
184
+ "criteria still lack passing evidence"
185
+ )
186
+ concerns.append(
187
+ "Completion claim is not yet backed by DevCouncil evidence: "
188
+ + ", ".join(details) + "."
189
+ )
190
+ evidence_requests.append(
191
+ f"Run 'dev verify {grounding.task_id}' and resolve the gaps so the "
192
+ "claim is supported by passing evidence."
193
+ )
194
+ elif claims_completion and grounding.is_satisfied:
195
+ alternatives.append(
196
+ f"Completion is corroborated by passing evidence for task {grounding.task_id}; "
197
+ "proceed."
198
+ )
199
+ elif claims_completion and not any(term in lower for term in EVIDENCE_TERMS):
200
+ # No task state to ground against: best-effort keyword heuristic.
49
201
  concerns.append("The response appears to claim completion without naming verification evidence.")
50
202
  evidence_requests.append("State the exact commands, checks, or reviewed artifacts that prove the change.")
51
203
 
@@ -56,8 +208,7 @@ def review_turn(turn: AgentTurn, project_root: Path, client: str | None = None)
56
208
  if "todo" in lower or "follow-up" in lower or "later" in lower:
57
209
  evidence_requests.append("List any remaining TODOs as DevCouncil gaps or repair tasks instead of burying them in chat.")
58
210
 
59
- verdict = "Approved"
60
- if concerns:
211
+ if concerns and verdict == "Approved":
61
212
  verdict = "Concerns"
62
213
  if any(term in lower for term in ("--no-verify", "reset --hard", "force push", "ignore failing")):
63
214
  verdict = "Critical Issues"
@@ -69,9 +220,11 @@ def review_turn(turn: AgentTurn, project_root: Path, client: str | None = None)
69
220
  message_for_agent = _message_for_agent(verdict, concerns, evidence_requests)
70
221
  card_id = _card_id(turn)
71
222
  return CritiqueCard(
223
+ schema="devcouncil.critique_card.v1",
72
224
  id=card_id,
73
225
  session_id=turn.session_id,
74
226
  turn_id=turn.turn_id,
227
+ task_id=task_id,
75
228
  client=client or turn.source,
76
229
  verdict=verdict,
77
230
  summary=summary,
@@ -173,21 +326,10 @@ def unresolved_blocking_cards(project_root: Path, task_id: str | None = None) ->
173
326
 
174
327
 
175
328
  def _card_id(turn: AgentTurn) -> str:
176
- digest = hashlib.sha1(f"{turn.session_id}:{turn.turn_id}:{turn.content}".encode("utf-8")).hexdigest()
329
+ digest = hashlib.sha256(f"{turn.session_id}:{turn.turn_id}:{turn.content}".encode("utf-8")).hexdigest()
177
330
  return f"CARD-{digest[:12]}"
178
331
 
179
332
 
180
- def _looks_like_completion_claim(lower: str) -> bool:
181
- return any(phrase in lower for phrase in (
182
- "done",
183
- "completed",
184
- "implemented",
185
- "fixed",
186
- "ready",
187
- "all set",
188
- ))
189
-
190
-
191
333
  def _mentions_broad_change(lower: str) -> bool:
192
334
  return any(phrase in lower for phrase in (
193
335
  "refactor the entire",
@@ -198,7 +340,7 @@ def _mentions_broad_change(lower: str) -> bool:
198
340
  ))
199
341
 
200
342
 
201
- def _message_for_agent(verdict: str, concerns: list[str], evidence_requests: list[str]) -> str:
343
+ def _message_for_agent(verdict: Verdict, concerns: list[str], evidence_requests: list[str]) -> str:
202
344
  if verdict == "Approved":
203
345
  return "Continue, but keep the next response grounded in changed files and verification evidence."
204
346
  pieces = ["Pause and address this review before proceeding."]
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hashlib
3
4
  import json
4
5
  from datetime import datetime, timezone
5
6
  from pathlib import Path
@@ -42,9 +43,8 @@ def write_signal(project_root: Path, client: str, payload: dict[str, Any]) -> Pa
42
43
  review_command=_review_command(client.lower(), transcript_path, task_id),
43
44
  )
44
45
  key = transcript_path or session_id or json.dumps(payload, sort_keys=True, default=str)
45
- import hashlib
46
46
 
47
- digest = hashlib.sha1(key.encode("utf-8", errors="replace")).hexdigest()[:12]
47
+ digest = hashlib.sha256(key.encode("utf-8", errors="replace")).hexdigest()[:12]
48
48
  path = directory / f"{client.lower()}-{digest}.json"
49
49
  signal.path = str(path)
50
50
  path.write_text(signal.model_dump_json(indent=2) + "\n", encoding="utf-8")
@@ -2,10 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
  from pathlib import Path
5
- from typing import Any, Iterable
5
+ from typing import Any, Iterable, Literal
6
6
 
7
7
  from devcouncil.live.models import AgentSession, AgentTurn, session_id_from_path
8
8
 
9
+ RoleName = Literal["user", "assistant", "system", "tool", "unknown"]
10
+ KNOWN_ROLES: set[RoleName] = {"user", "assistant", "system", "tool"}
11
+
9
12
 
10
13
  CLAUDE_TRANSCRIPT_ROOT = Path.home() / ".claude" / "projects"
11
14
 
@@ -98,18 +101,18 @@ def _turn_from_record(raw: dict[str, Any], session_id: str, turn_index: int, cli
98
101
  )
99
102
 
100
103
 
101
- def _role(raw: dict[str, Any]) -> str:
104
+ def _role(raw: dict[str, Any]) -> RoleName:
102
105
  role = raw.get("role")
103
106
  if isinstance(role, str):
104
- return role if role in {"user", "assistant", "system", "tool"} else "unknown"
107
+ return role if role in KNOWN_ROLES else "unknown"
105
108
  message = raw.get("message")
106
109
  if isinstance(message, dict):
107
110
  nested = message.get("role")
108
111
  if isinstance(nested, str):
109
- return nested if nested in {"user", "assistant", "system", "tool"} else "unknown"
112
+ return nested if nested in KNOWN_ROLES else "unknown"
110
113
  record_type = raw.get("type")
111
- if record_type in {"user", "assistant", "system"}:
112
- return str(record_type)
114
+ if record_type in KNOWN_ROLES:
115
+ return record_type
113
116
  return "unknown"
114
117
 
115
118
 
@@ -9,18 +9,22 @@ class LLMCache:
9
9
  self.cache_dir = project_root / ".devcouncil" / "cache" / "llm"
10
10
  self.cache_dir.mkdir(parents=True, exist_ok=True)
11
11
 
12
- def _get_key(self, model: str, messages: list, temp: float, json_mode: bool) -> str:
12
+ def _get_key(self, model: str, messages: list, temp: float, json_mode: bool, provider_fingerprint: str = "") -> str:
13
13
  data = {
14
14
  "model": model,
15
15
  "messages": messages,
16
16
  "temp": temp,
17
- "json_mode": json_mode
17
+ "json_mode": json_mode,
18
+ # Provider-specific knobs that change the output for an identical prompt
19
+ # (e.g. Ollama's num_ctx / base_url). Empty for providers without such knobs,
20
+ # so their cache keys are unchanged.
21
+ "provider": provider_fingerprint,
18
22
  }
19
23
  s = json.dumps(data, sort_keys=True)
20
24
  return hashlib.sha256(s.encode("utf-8")).hexdigest()
21
25
 
22
- def get(self, model: str, messages: list, temp: float, json_mode: bool) -> Optional[LLMResponse]:
23
- key = self._get_key(model, messages, temp, json_mode)
26
+ def get(self, model: str, messages: list, temp: float, json_mode: bool, provider_fingerprint: str = "") -> Optional[LLMResponse]:
27
+ key = self._get_key(model, messages, temp, json_mode, provider_fingerprint)
24
28
  cache_file = self.cache_dir / f"{key}.json"
25
29
  if cache_file.exists():
26
30
  try:
@@ -31,8 +35,8 @@ class LLMCache:
31
35
  pass
32
36
  return None
33
37
 
34
- def set(self, model: str, messages: list, temp: float, json_mode: bool, response: LLMResponse):
35
- key = self._get_key(model, messages, temp, json_mode)
38
+ def set(self, model: str, messages: list, temp: float, json_mode: bool, response: LLMResponse, provider_fingerprint: str = ""):
39
+ key = self._get_key(model, messages, temp, json_mode, provider_fingerprint)
36
40
  cache_file = self.cache_dir / f"{key}.json"
37
41
  with open(cache_file, "w") as f:
38
42
  json.dump(response.model_dump(), f)
@@ -0,0 +1,44 @@
1
+ openrouter:
2
+ spec_writer: anthropic/claude-sonnet-4.6
3
+ prompt_enhancer: anthropic/claude-sonnet-4.6
4
+ planner_a: anthropic/claude-sonnet-4.6
5
+ planner_b: google/gemini-2.5-pro
6
+ critic_a: openai/gpt-5.5
7
+ critic_b: anthropic/claude-opus-4.8
8
+ arbiter: openai/gpt-5.5
9
+ native_agent: anthropic/claude-sonnet-4.6
10
+ implementation_reviewer: openai/gpt-5.5
11
+ live_reviewer: openai/gpt-5.5
12
+ vertexai:
13
+ spec_writer: google/gemini-2.5-flash
14
+ prompt_enhancer: google/gemini-2.5-flash
15
+ planner_a: google/gemini-2.5-flash
16
+ planner_b: google/gemini-2.5-flash
17
+ critic_a: google/gemini-2.5-flash
18
+ critic_b: google/gemini-2.5-flash
19
+ arbiter: google/gemini-2.5-flash
20
+ native_agent: google/gemini-2.5-flash
21
+ implementation_reviewer: google/gemini-2.5-flash
22
+ live_reviewer: google/gemini-2.5-flash
23
+ doubleword:
24
+ spec_writer: deepseek/deepseek-v4
25
+ prompt_enhancer: deepseek/deepseek-v4
26
+ planner_a: deepseek/deepseek-v4
27
+ planner_b: deepseek/deepseek-v4
28
+ critic_a: deepseek/deepseek-v4
29
+ critic_b: deepseek/deepseek-v4
30
+ arbiter: deepseek/deepseek-v4
31
+ native_agent: deepseek/deepseek-v4
32
+ implementation_reviewer: deepseek/deepseek-v4
33
+ live_reviewer: deepseek/deepseek-v4
34
+ ollama:
35
+ spec_writer: qwen2.5-coder:7b
36
+ prompt_enhancer: qwen2.5-coder:7b
37
+ planner_a: qwen2.5-coder:7b
38
+ planner_b: qwen2.5-coder:7b
39
+ critic_a: qwen2.5-coder:7b
40
+ critic_b: qwen2.5-coder:7b
41
+ arbiter: qwen2.5-coder:7b
42
+ native_agent: qwen2.5-coder:7b
43
+ implementation_reviewer: qwen2.5-coder:7b
44
+ live_reviewer: qwen2.5-coder:7b