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.
- package/README.md +190 -6
- package/package.json +9 -2
- package/pyproject.toml +34 -2
- package/src/devcouncil/app/config.py +167 -5
- package/src/devcouncil/artifacts/graph.py +23 -3
- 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 +6 -3
- package/src/devcouncil/cli/commands/check.py +209 -0
- package/src/devcouncil/cli/commands/config.py +43 -4
- package/src/devcouncil/cli/commands/cost.py +57 -0
- package/src/devcouncil/cli/commands/dashboard.py +6 -1
- package/src/devcouncil/cli/commands/doctor.py +221 -21
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +452 -33
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +124 -15
- package/src/devcouncil/cli/commands/init.py +154 -18
- package/src/devcouncil/cli/commands/integrate.py +894 -105
- package/src/devcouncil/cli/commands/map.py +80 -10
- package/src/devcouncil/cli/commands/plan.py +212 -51
- package/src/devcouncil/cli/commands/prompt.py +18 -7
- package/src/devcouncil/cli/commands/repair.py +40 -23
- package/src/devcouncil/cli/commands/report.py +8 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +4 -2
- package/src/devcouncil/cli/commands/rollback.py +27 -28
- package/src/devcouncil/cli/commands/run.py +69 -49
- 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 +145 -6
- package/src/devcouncil/cli/commands/shell.py +73 -0
- package/src/devcouncil/cli/commands/skills.py +88 -0
- package/src/devcouncil/cli/commands/status.py +25 -1
- package/src/devcouncil/cli/commands/trace.py +47 -3
- package/src/devcouncil/cli/commands/verify.py +138 -3
- package/src/devcouncil/cli/commands/watch.py +9 -9
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +56 -7
- package/src/devcouncil/domain/evidence.py +22 -2
- package/src/devcouncil/domain/gap.py +27 -1
- package/src/devcouncil/domain/task.py +31 -2
- package/src/devcouncil/execution/checkpoints.py +246 -0
- package/src/devcouncil/execution/context_builder.py +1 -1
- package/src/devcouncil/execution/fs_watcher.py +180 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +162 -74
- package/src/devcouncil/execution/patch.py +59 -10
- package/src/devcouncil/execution/permissions.py +17 -24
- package/src/devcouncil/execution/policy_engine.py +343 -0
- package/src/devcouncil/execution/prompt_builder.py +633 -21
- package/src/devcouncil/execution/shell_session.py +225 -0
- package/src/devcouncil/execution/task_runner.py +6 -2
- package/src/devcouncil/executors/agent_registry.py +575 -0
- package/src/devcouncil/executors/coding_cli.py +663 -39
- package/src/devcouncil/executors/native/agent.py +121 -20
- package/src/devcouncil/gating/checks/clean_git.py +3 -1
- package/src/devcouncil/gating/checks/secret_scan_check.py +40 -21
- package/src/devcouncil/gating/policy.py +158 -10
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/ast_matcher.py +1 -1
- package/src/devcouncil/indexing/lsp.py +45 -4
- package/src/devcouncil/indexing/repo_mapper.py +1256 -9
- 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_intent.py +142 -0
- package/src/devcouncil/integrations/gitnexus.py +35 -0
- package/src/devcouncil/integrations/mcp/server.py +1552 -29
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/live/cards.py +161 -19
- package/src/devcouncil/live/signals.py +2 -2
- package/src/devcouncil/live/transcripts.py +9 -6
- package/src/devcouncil/llm/cache.py +10 -6
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +515 -34
- package/src/devcouncil/llm/router.py +231 -46
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/planning/correction_manifest.py +303 -0
- package/src/devcouncil/planning/critique_service.py +7 -2
- package/src/devcouncil/planning/plan_service.py +17 -3
- package/src/devcouncil/planning/prompt_enhancer_service.py +82 -1
- package/src/devcouncil/planning/spec_service.py +27 -1
- 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/json_report.py +11 -1
- package/src/devcouncil/reporting/markdown_report.py +15 -0
- 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 +83 -2
- package/src/devcouncil/storage/models.py +121 -0
- package/src/devcouncil/storage/native.py +557 -0
- package/src/devcouncil/storage/repositories.py +137 -75
- package/src/devcouncil/telemetry/cost.py +123 -17
- 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 +12 -9
- package/src/devcouncil/ui/dashboard.py +324 -23
- package/src/devcouncil/utils/redaction.py +9 -3
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- 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/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 +1065 -47
- package/uv.lock +205 -64
- 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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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:
|
|
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.
|
|
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]) ->
|
|
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
|
|
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
|
|
112
|
+
return nested if nested in KNOWN_ROLES else "unknown"
|
|
110
113
|
record_type = raw.get("type")
|
|
111
|
-
if record_type in
|
|
112
|
-
return
|
|
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
|