devcouncil 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -253
- package/bin/devcouncil.js +130 -32
- package/package.json +6 -17
- package/pyproject.toml +0 -66
- package/src/devcouncil/__init__.py +0 -0
- package/src/devcouncil/__main__.py +0 -4
- package/src/devcouncil/app/__init__.py +0 -28
- package/src/devcouncil/app/config.py +0 -320
- package/src/devcouncil/app/errors.py +0 -23
- package/src/devcouncil/app/events.py +0 -44
- package/src/devcouncil/app/orchestrator.py +0 -92
- package/src/devcouncil/app/project_status.py +0 -29
- package/src/devcouncil/app/run_context.py +0 -39
- package/src/devcouncil/app/state_machine.py +0 -108
- package/src/devcouncil/artifacts/__init__.py +0 -1
- package/src/devcouncil/artifacts/coverage.py +0 -96
- package/src/devcouncil/artifacts/graph.py +0 -163
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/artifacts/validators.py +0 -27
- package/src/devcouncil/assets/__init__.py +0 -1
- package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/agents.py +0 -292
- package/src/devcouncil/cli/commands/artifacts.py +0 -54
- package/src/devcouncil/cli/commands/ast.py +0 -22
- package/src/devcouncil/cli/commands/baseline.py +0 -35
- package/src/devcouncil/cli/commands/check.py +0 -209
- package/src/devcouncil/cli/commands/config.py +0 -115
- package/src/devcouncil/cli/commands/cost.py +0 -57
- package/src/devcouncil/cli/commands/dashboard.py +0 -31
- package/src/devcouncil/cli/commands/doctor.py +0 -340
- package/src/devcouncil/cli/commands/evidence.py +0 -48
- package/src/devcouncil/cli/commands/go.py +0 -656
- package/src/devcouncil/cli/commands/handoff.py +0 -69
- package/src/devcouncil/cli/commands/hook.py +0 -237
- package/src/devcouncil/cli/commands/init.py +0 -289
- package/src/devcouncil/cli/commands/integrate.py +0 -1515
- package/src/devcouncil/cli/commands/lsp.py +0 -20
- package/src/devcouncil/cli/commands/map.py +0 -112
- package/src/devcouncil/cli/commands/mcp_server.py +0 -18
- package/src/devcouncil/cli/commands/plan.py +0 -488
- package/src/devcouncil/cli/commands/prompt.py +0 -61
- package/src/devcouncil/cli/commands/repair.py +0 -89
- package/src/devcouncil/cli/commands/report.py +0 -137
- package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
- package/src/devcouncil/cli/commands/rollback.py +0 -59
- package/src/devcouncil/cli/commands/run.py +0 -289
- package/src/devcouncil/cli/commands/runs.py +0 -223
- package/src/devcouncil/cli/commands/scaffold.py +0 -32
- package/src/devcouncil/cli/commands/semantic.py +0 -47
- package/src/devcouncil/cli/commands/setup.py +0 -362
- package/src/devcouncil/cli/commands/shell.py +0 -73
- package/src/devcouncil/cli/commands/show.py +0 -76
- package/src/devcouncil/cli/commands/skills.py +0 -88
- package/src/devcouncil/cli/commands/status.py +0 -141
- package/src/devcouncil/cli/commands/tasks.py +0 -55
- package/src/devcouncil/cli/commands/trace.py +0 -88
- package/src/devcouncil/cli/commands/verify.py +0 -328
- package/src/devcouncil/cli/commands/version.py +0 -20
- package/src/devcouncil/cli/commands/watch.py +0 -574
- package/src/devcouncil/cli/commands/watch_fs.py +0 -40
- package/src/devcouncil/cli/main.py +0 -137
- package/src/devcouncil/council/__init__.py +0 -0
- package/src/devcouncil/council/prompts/__init__.py +0 -0
- package/src/devcouncil/council/prompts/arbiter.md +0 -19
- package/src/devcouncil/council/prompts/critic_a.md +0 -10
- package/src/devcouncil/council/prompts/critic_b.md +0 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
- package/src/devcouncil/council/prompts/planner_a.md +0 -16
- package/src/devcouncil/council/prompts/planner_b.md +0 -16
- package/src/devcouncil/council/prompts/rebuttal.md +0 -10
- package/src/devcouncil/council/prompts/spec_writer.md +0 -12
- package/src/devcouncil/domain/__init__.py +0 -0
- package/src/devcouncil/domain/assumption.py +0 -17
- package/src/devcouncil/domain/critique.py +0 -32
- package/src/devcouncil/domain/evidence.py +0 -47
- package/src/devcouncil/domain/gap.py +0 -52
- package/src/devcouncil/domain/requirement.py +0 -22
- package/src/devcouncil/domain/task.py +0 -55
- package/src/devcouncil/execution/__init__.py +0 -1
- package/src/devcouncil/execution/checkpoints.py +0 -246
- package/src/devcouncil/execution/context_builder.py +0 -60
- package/src/devcouncil/execution/executor.py +0 -15
- package/src/devcouncil/execution/fs_watcher.py +0 -180
- package/src/devcouncil/execution/handoff.py +0 -102
- package/src/devcouncil/execution/hook_policy.py +0 -253
- package/src/devcouncil/execution/patch.py +0 -77
- package/src/devcouncil/execution/paths.py +0 -14
- package/src/devcouncil/execution/permissions.py +0 -85
- package/src/devcouncil/execution/policy_engine.py +0 -343
- package/src/devcouncil/execution/prompt_builder.py +0 -671
- package/src/devcouncil/execution/shell_session.py +0 -225
- package/src/devcouncil/execution/task_runner.py +0 -170
- package/src/devcouncil/executors/__init__.py +0 -1
- package/src/devcouncil/executors/agent_registry.py +0 -575
- package/src/devcouncil/executors/coding_cli.py +0 -736
- package/src/devcouncil/executors/mini_swe.py +0 -73
- package/src/devcouncil/executors/native/__init__.py +0 -0
- package/src/devcouncil/executors/native/agent.py +0 -208
- package/src/devcouncil/executors/openhands.py +0 -71
- package/src/devcouncil/gating/__init__.py +0 -1
- package/src/devcouncil/gating/checks/__init__.py +0 -0
- package/src/devcouncil/gating/checks/clean_git.py +0 -52
- package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
- package/src/devcouncil/gating/policy.py +0 -338
- package/src/devcouncil/hardware.py +0 -184
- package/src/devcouncil/indexing/__init__.py +0 -1
- package/src/devcouncil/indexing/ast_matcher.py +0 -168
- package/src/devcouncil/indexing/graph_index.py +0 -48
- package/src/devcouncil/indexing/lsp.py +0 -161
- package/src/devcouncil/indexing/repo_mapper.py +0 -1455
- package/src/devcouncil/indexing/semantic_index.py +0 -205
- package/src/devcouncil/integrations/actions.py +0 -146
- package/src/devcouncil/integrations/check.py +0 -423
- package/src/devcouncil/integrations/code_review_graph.py +0 -163
- package/src/devcouncil/integrations/github.py +0 -39
- package/src/devcouncil/integrations/github_intent.py +0 -142
- package/src/devcouncil/integrations/gitnexus.py +0 -62
- package/src/devcouncil/integrations/graphify.py +0 -34
- package/src/devcouncil/integrations/mcp/__init__.py +0 -0
- package/src/devcouncil/integrations/mcp/server.py +0 -2122
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
- package/src/devcouncil/integrations/pr_comments.py +0 -62
- package/src/devcouncil/live/__init__.py +0 -2
- package/src/devcouncil/live/cards.py +0 -349
- package/src/devcouncil/live/models.py +0 -63
- package/src/devcouncil/live/repair_prompt.py +0 -83
- package/src/devcouncil/live/reviewer.py +0 -70
- package/src/devcouncil/live/signals.py +0 -135
- package/src/devcouncil/live/summary.py +0 -34
- package/src/devcouncil/live/tasks.py +0 -18
- package/src/devcouncil/live/transcripts.py +0 -141
- package/src/devcouncil/llm/__init__.py +0 -1
- package/src/devcouncil/llm/cache.py +0 -42
- package/src/devcouncil/llm/model_defaults.yaml +0 -44
- package/src/devcouncil/llm/provider.py +0 -627
- package/src/devcouncil/llm/router.py +0 -310
- package/src/devcouncil/optimization/__init__.py +0 -1
- package/src/devcouncil/optimization/gepa_agent.py +0 -318
- package/src/devcouncil/planning/__init__.py +0 -1
- package/src/devcouncil/planning/arbiter_service.py +0 -57
- package/src/devcouncil/planning/correction_manifest.py +0 -303
- package/src/devcouncil/planning/critique_service.py +0 -71
- package/src/devcouncil/planning/plan_service.py +0 -60
- package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
- package/src/devcouncil/planning/repair_service.py +0 -39
- package/src/devcouncil/planning/spec_service.py +0 -70
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/repo/ci_scaffold.py +0 -157
- package/src/devcouncil/repo/gitignore.py +0 -123
- package/src/devcouncil/repo/sca.py +0 -374
- package/src/devcouncil/reporting/__init__.py +0 -0
- package/src/devcouncil/reporting/github_check.py +0 -32
- package/src/devcouncil/reporting/json_report.py +0 -30
- package/src/devcouncil/reporting/markdown_report.py +0 -83
- package/src/devcouncil/reporting/report_builder.py +0 -14
- package/src/devcouncil/skills/__init__.py +0 -19
- package/src/devcouncil/skills/library/README.md +0 -46
- package/src/devcouncil/skills/library/ai-training.md +0 -50
- package/src/devcouncil/skills/library/android.md +0 -50
- package/src/devcouncil/skills/library/backend.md +0 -52
- package/src/devcouncil/skills/library/core-engineering.md +0 -95
- package/src/devcouncil/skills/library/data-engineering.md +0 -47
- package/src/devcouncil/skills/library/desktop.md +0 -46
- package/src/devcouncil/skills/library/devops.md +0 -48
- package/src/devcouncil/skills/library/game-dev.md +0 -46
- package/src/devcouncil/skills/library/ios.md +0 -48
- package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
- package/src/devcouncil/skills/library/security.md +0 -48
- package/src/devcouncil/skills/library/systems.md +0 -48
- package/src/devcouncil/skills/library/web.md +0 -47
- package/src/devcouncil/skills/library/windows.md +0 -47
- package/src/devcouncil/skills/registry.py +0 -330
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +0 -147
- package/src/devcouncil/storage/models.py +0 -204
- package/src/devcouncil/storage/native.py +0 -557
- package/src/devcouncil/storage/repositories.py +0 -485
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +0 -140
- package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
- package/src/devcouncil/telemetry/pricing.py +0 -28
- package/src/devcouncil/telemetry/traces.py +0 -146
- package/src/devcouncil/telemetry/tracker.py +0 -52
- package/src/devcouncil/ui/__init__.py +0 -1
- package/src/devcouncil/ui/dashboard.py +0 -423
- package/src/devcouncil/utils/__init__.py +0 -1
- package/src/devcouncil/utils/redaction.py +0 -147
- package/src/devcouncil/utils/subprocess_env.py +0 -69
- package/src/devcouncil/verification/__init__.py +0 -1
- package/src/devcouncil/verification/acceptance_compiler.py +0 -125
- package/src/devcouncil/verification/ad_hoc_check.py +0 -129
- package/src/devcouncil/verification/diff_coverage.py +0 -353
- package/src/devcouncil/verification/implementation_reviewer.py +0 -55
- package/src/devcouncil/verification/next_actions.py +0 -189
- package/src/devcouncil/verification/sandbox.py +0 -178
- package/src/devcouncil/verification/test_resolver.py +0 -91
- package/src/devcouncil/verification/verifier.py +0 -1548
- package/uv.lock +0 -1226
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import hashlib
|
|
4
|
-
import json
|
|
5
|
-
from datetime import datetime, timezone
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
from typing import Any
|
|
8
|
-
|
|
9
|
-
from pydantic import BaseModel, Field
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class ReviewSignal(BaseModel):
|
|
13
|
-
client: str = "generic"
|
|
14
|
-
payload: dict[str, Any] = Field(default_factory=dict)
|
|
15
|
-
transcript_path: str | None = None
|
|
16
|
-
session_id: str | None = None
|
|
17
|
-
task_id: str | None = None
|
|
18
|
-
created_at: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
19
|
-
review_command: str | None = None
|
|
20
|
-
path: str | None = None
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def signal_dir(project_root: Path) -> Path:
|
|
24
|
-
return project_root / ".devcouncil" / "live" / "signals"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def processed_signal_dir(project_root: Path) -> Path:
|
|
28
|
-
return signal_dir(project_root) / "processed"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def write_signal(project_root: Path, client: str, payload: dict[str, Any]) -> Path:
|
|
32
|
-
directory = signal_dir(project_root)
|
|
33
|
-
directory.mkdir(parents=True, exist_ok=True)
|
|
34
|
-
transcript_path = extract_transcript_path(payload)
|
|
35
|
-
session_id = _string_value(payload, "session_id", "sessionId", "session", "cwd")
|
|
36
|
-
task_id = extract_task_id(payload)
|
|
37
|
-
signal = ReviewSignal(
|
|
38
|
-
client=client.lower(),
|
|
39
|
-
payload=payload,
|
|
40
|
-
transcript_path=transcript_path,
|
|
41
|
-
session_id=session_id,
|
|
42
|
-
task_id=task_id,
|
|
43
|
-
review_command=_review_command(client.lower(), transcript_path, task_id),
|
|
44
|
-
)
|
|
45
|
-
key = transcript_path or session_id or json.dumps(payload, sort_keys=True, default=str)
|
|
46
|
-
|
|
47
|
-
digest = hashlib.sha256(key.encode("utf-8", errors="replace")).hexdigest()[:12]
|
|
48
|
-
path = directory / f"{client.lower()}-{digest}.json"
|
|
49
|
-
signal.path = str(path)
|
|
50
|
-
path.write_text(signal.model_dump_json(indent=2) + "\n", encoding="utf-8")
|
|
51
|
-
return path
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def load_signals(project_root: Path) -> list[ReviewSignal]:
|
|
55
|
-
directory = signal_dir(project_root)
|
|
56
|
-
if not directory.exists():
|
|
57
|
-
return []
|
|
58
|
-
signals: list[ReviewSignal] = []
|
|
59
|
-
for path in sorted(directory.glob("*.json"), key=lambda item: item.stat().st_mtime, reverse=True):
|
|
60
|
-
try:
|
|
61
|
-
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
62
|
-
signal = ReviewSignal.model_validate(raw)
|
|
63
|
-
signal.path = str(path)
|
|
64
|
-
signals.append(signal)
|
|
65
|
-
except Exception:
|
|
66
|
-
continue
|
|
67
|
-
return signals
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def mark_processed(signal: ReviewSignal, project_root: Path) -> Path | None:
|
|
71
|
-
if not signal.path:
|
|
72
|
-
return None
|
|
73
|
-
source = Path(signal.path)
|
|
74
|
-
if not source.exists():
|
|
75
|
-
return None
|
|
76
|
-
target_dir = processed_signal_dir(project_root)
|
|
77
|
-
target_dir.mkdir(parents=True, exist_ok=True)
|
|
78
|
-
target = target_dir / source.name
|
|
79
|
-
if target.exists():
|
|
80
|
-
target.unlink()
|
|
81
|
-
source.replace(target)
|
|
82
|
-
return target
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def extract_transcript_path(payload: dict[str, Any]) -> str | None:
|
|
86
|
-
direct = _string_value(
|
|
87
|
-
payload,
|
|
88
|
-
"transcript_path",
|
|
89
|
-
"transcriptPath",
|
|
90
|
-
"transcript",
|
|
91
|
-
"conversation_path",
|
|
92
|
-
"conversationPath",
|
|
93
|
-
"file",
|
|
94
|
-
"path",
|
|
95
|
-
)
|
|
96
|
-
if direct:
|
|
97
|
-
return direct
|
|
98
|
-
for key in ("session", "message", "hook_event", "event"):
|
|
99
|
-
nested = payload.get(key)
|
|
100
|
-
if isinstance(nested, dict):
|
|
101
|
-
value = extract_transcript_path(nested)
|
|
102
|
-
if value:
|
|
103
|
-
return value
|
|
104
|
-
return None
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def extract_task_id(payload: dict[str, Any]) -> str | None:
|
|
108
|
-
direct = _string_value(payload, "task_id", "taskId", "task", "active_task", "activeTask")
|
|
109
|
-
if direct:
|
|
110
|
-
return direct
|
|
111
|
-
for key in ("session", "message", "hook_event", "event", "metadata"):
|
|
112
|
-
nested = payload.get(key)
|
|
113
|
-
if isinstance(nested, dict):
|
|
114
|
-
value = extract_task_id(nested)
|
|
115
|
-
if value:
|
|
116
|
-
return value
|
|
117
|
-
return None
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
def _string_value(payload: dict[str, Any], *keys: str) -> str | None:
|
|
121
|
-
for key in keys:
|
|
122
|
-
value = payload.get(key)
|
|
123
|
-
if isinstance(value, str) and value.strip():
|
|
124
|
-
return value
|
|
125
|
-
return None
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
def _review_command(client: str, transcript_path: str | None, task_id: str | None = None) -> str:
|
|
129
|
-
if transcript_path:
|
|
130
|
-
command = f"dev watch review --client {client} --transcript {transcript_path}"
|
|
131
|
-
else:
|
|
132
|
-
command = f"dev watch pending --client {client}"
|
|
133
|
-
if task_id:
|
|
134
|
-
command += f" --task-id {task_id}"
|
|
135
|
-
return command
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from devcouncil.live.cards import load_cards, unresolved_blocking_cards
|
|
6
|
-
from devcouncil.live.signals import load_signals
|
|
7
|
-
from devcouncil.live.tasks import active_task_id
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def live_review_summary(project_root: Path, task_id: str | None = None) -> dict:
|
|
11
|
-
cards = load_cards(project_root)
|
|
12
|
-
signals = load_signals(project_root)
|
|
13
|
-
active_id = active_task_id(project_root)
|
|
14
|
-
scoped_task_id = task_id or active_id
|
|
15
|
-
blockers = unresolved_blocking_cards(project_root, task_id=scoped_task_id)
|
|
16
|
-
pending_signal_items = [signal.model_dump() for signal in signals]
|
|
17
|
-
return {
|
|
18
|
-
"active_task_id": active_id,
|
|
19
|
-
"scope_task_id": scoped_task_id,
|
|
20
|
-
"pending_signals": len(signals),
|
|
21
|
-
"pending_signal_items": pending_signal_items[:10],
|
|
22
|
-
"cards": {
|
|
23
|
-
"total": len(cards),
|
|
24
|
-
"open": len([card for card in cards if card.status == "open"]),
|
|
25
|
-
"resolved": len([card for card in cards if card.status == "resolved"]),
|
|
26
|
-
"ignored": len([card for card in cards if card.status == "ignored"]),
|
|
27
|
-
"critical_open": len([
|
|
28
|
-
card for card in cards
|
|
29
|
-
if card.status == "open" and card.verdict == "Critical Issues"
|
|
30
|
-
]),
|
|
31
|
-
},
|
|
32
|
-
"blocking_cards": [card.model_dump() for card in blockers],
|
|
33
|
-
"recent_cards": [card.model_dump() for card in cards[:5]],
|
|
34
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
|
|
5
|
-
from devcouncil.storage.db import get_db
|
|
6
|
-
from devcouncil.storage.repositories import TaskRepository
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def active_task_id(project_root: Path) -> str | None:
|
|
10
|
-
"""Return the single running DevCouncil task ID, if one is unambiguous."""
|
|
11
|
-
db = get_db(project_root)
|
|
12
|
-
if not db:
|
|
13
|
-
return None
|
|
14
|
-
with db.get_session() as session:
|
|
15
|
-
running = [task for task in TaskRepository(session).get_all() if task.status == "running"]
|
|
16
|
-
if len(running) != 1:
|
|
17
|
-
return None
|
|
18
|
-
return running[0].id
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from typing import Any, Iterable, Literal
|
|
6
|
-
|
|
7
|
-
from devcouncil.live.models import AgentSession, AgentTurn, session_id_from_path
|
|
8
|
-
|
|
9
|
-
RoleName = Literal["user", "assistant", "system", "tool", "unknown"]
|
|
10
|
-
KNOWN_ROLES: set[RoleName] = {"user", "assistant", "system", "tool"}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
CLAUDE_TRANSCRIPT_ROOT = Path.home() / ".claude" / "projects"
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def discover_sessions(project_root: Path, client: str = "claude") -> list[AgentSession]:
|
|
17
|
-
"""Find local coding-agent transcripts DevCouncil can review."""
|
|
18
|
-
client = client.lower()
|
|
19
|
-
if client == "claude":
|
|
20
|
-
candidates = _claude_transcript_candidates(project_root)
|
|
21
|
-
else:
|
|
22
|
-
candidates = sorted((project_root / ".devcouncil" / "live" / client).glob("*.jsonl"))
|
|
23
|
-
|
|
24
|
-
sessions: list[AgentSession] = []
|
|
25
|
-
for path in candidates:
|
|
26
|
-
if not path.exists() or not path.is_file():
|
|
27
|
-
continue
|
|
28
|
-
stat = path.stat()
|
|
29
|
-
sessions.append(AgentSession(
|
|
30
|
-
id=session_id_from_path(path),
|
|
31
|
-
client=client,
|
|
32
|
-
transcript_path=str(path),
|
|
33
|
-
updated_at=str(stat.st_mtime),
|
|
34
|
-
turns=sum(1 for _ in _safe_lines(path)),
|
|
35
|
-
))
|
|
36
|
-
return sorted(sessions, key=lambda item: item.updated_at or "", reverse=True)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def load_turns(path: Path, client: str = "generic") -> list[AgentTurn]:
|
|
40
|
-
"""Parse a transcript into normalized turns.
|
|
41
|
-
|
|
42
|
-
Supports Claude Code JSONL plus generic JSONL records with role/content fields.
|
|
43
|
-
"""
|
|
44
|
-
turns: list[AgentTurn] = []
|
|
45
|
-
session_id = session_id_from_path(path)
|
|
46
|
-
for index, raw in enumerate(_read_jsonl(path)):
|
|
47
|
-
turn = _turn_from_record(raw, session_id=session_id, turn_index=index, client=client)
|
|
48
|
-
if turn and turn.content.strip():
|
|
49
|
-
turns.append(turn)
|
|
50
|
-
return turns
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def latest_assistant_turn(path: Path, client: str = "generic") -> AgentTurn | None:
|
|
54
|
-
for turn in reversed(load_turns(path, client=client)):
|
|
55
|
-
if turn.role == "assistant":
|
|
56
|
-
return turn
|
|
57
|
-
return None
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _claude_transcript_candidates(project_root: Path) -> list[Path]:
|
|
61
|
-
local_runtime = project_root / ".devcouncil" / "live" / "claude"
|
|
62
|
-
candidates = list(local_runtime.glob("*.jsonl"))
|
|
63
|
-
if CLAUDE_TRANSCRIPT_ROOT.exists():
|
|
64
|
-
candidates.extend(CLAUDE_TRANSCRIPT_ROOT.rglob("*.jsonl"))
|
|
65
|
-
return sorted(set(candidates), key=lambda path: path.stat().st_mtime if path.exists() else 0, reverse=True)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _safe_lines(path: Path) -> Iterable[str]:
|
|
69
|
-
try:
|
|
70
|
-
return path.read_text(encoding="utf-8", errors="replace").splitlines()
|
|
71
|
-
except OSError:
|
|
72
|
-
return []
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def _read_jsonl(path: Path) -> Iterable[dict[str, Any]]:
|
|
76
|
-
for line in _safe_lines(path):
|
|
77
|
-
if not line.strip():
|
|
78
|
-
continue
|
|
79
|
-
try:
|
|
80
|
-
value = json.loads(line)
|
|
81
|
-
except json.JSONDecodeError:
|
|
82
|
-
continue
|
|
83
|
-
if isinstance(value, dict):
|
|
84
|
-
yield value
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _turn_from_record(raw: dict[str, Any], session_id: str, turn_index: int, client: str) -> AgentTurn | None:
|
|
88
|
-
role = _role(raw)
|
|
89
|
-
content = _content(raw)
|
|
90
|
-
if not content:
|
|
91
|
-
return None
|
|
92
|
-
turn_id = str(raw.get("uuid") or raw.get("id") or raw.get("message_id") or f"turn-{turn_index}")
|
|
93
|
-
return AgentTurn(
|
|
94
|
-
session_id=str(raw.get("sessionId") or raw.get("session_id") or session_id),
|
|
95
|
-
turn_id=turn_id,
|
|
96
|
-
source=client,
|
|
97
|
-
role=role,
|
|
98
|
-
content=content,
|
|
99
|
-
timestamp=raw.get("timestamp") or raw.get("created_at"),
|
|
100
|
-
raw=raw,
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def _role(raw: dict[str, Any]) -> RoleName:
|
|
105
|
-
role = raw.get("role")
|
|
106
|
-
if isinstance(role, str):
|
|
107
|
-
return role if role in KNOWN_ROLES else "unknown"
|
|
108
|
-
message = raw.get("message")
|
|
109
|
-
if isinstance(message, dict):
|
|
110
|
-
nested = message.get("role")
|
|
111
|
-
if isinstance(nested, str):
|
|
112
|
-
return nested if nested in KNOWN_ROLES else "unknown"
|
|
113
|
-
record_type = raw.get("type")
|
|
114
|
-
if record_type in KNOWN_ROLES:
|
|
115
|
-
return record_type
|
|
116
|
-
return "unknown"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def _content(raw: dict[str, Any]) -> str:
|
|
120
|
-
direct = raw.get("content") or raw.get("text")
|
|
121
|
-
if isinstance(direct, str):
|
|
122
|
-
return direct
|
|
123
|
-
message = raw.get("message")
|
|
124
|
-
if isinstance(message, dict):
|
|
125
|
-
nested = message.get("content")
|
|
126
|
-
if isinstance(nested, str):
|
|
127
|
-
return nested
|
|
128
|
-
if isinstance(nested, list):
|
|
129
|
-
return "\n".join(_content_block_text(block) for block in nested).strip()
|
|
130
|
-
if isinstance(direct, list):
|
|
131
|
-
return "\n".join(_content_block_text(block) for block in direct).strip()
|
|
132
|
-
return ""
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def _content_block_text(block: Any) -> str:
|
|
136
|
-
if isinstance(block, str):
|
|
137
|
-
return block
|
|
138
|
-
if isinstance(block, dict):
|
|
139
|
-
value = block.get("text") or block.get("content")
|
|
140
|
-
return value if isinstance(value, str) else ""
|
|
141
|
-
return ""
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import hashlib
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from typing import Optional
|
|
5
|
-
from devcouncil.llm.provider import LLMResponse
|
|
6
|
-
|
|
7
|
-
class LLMCache:
|
|
8
|
-
def __init__(self, project_root: Path):
|
|
9
|
-
self.cache_dir = project_root / ".devcouncil" / "cache" / "llm"
|
|
10
|
-
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
11
|
-
|
|
12
|
-
def _get_key(self, model: str, messages: list, temp: float, json_mode: bool, provider_fingerprint: str = "") -> str:
|
|
13
|
-
data = {
|
|
14
|
-
"model": model,
|
|
15
|
-
"messages": messages,
|
|
16
|
-
"temp": temp,
|
|
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,
|
|
22
|
-
}
|
|
23
|
-
s = json.dumps(data, sort_keys=True)
|
|
24
|
-
return hashlib.sha256(s.encode("utf-8")).hexdigest()
|
|
25
|
-
|
|
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)
|
|
28
|
-
cache_file = self.cache_dir / f"{key}.json"
|
|
29
|
-
if cache_file.exists():
|
|
30
|
-
try:
|
|
31
|
-
with open(cache_file, "r") as f:
|
|
32
|
-
data = json.load(f)
|
|
33
|
-
return LLMResponse(**data)
|
|
34
|
-
except Exception:
|
|
35
|
-
pass
|
|
36
|
-
return None
|
|
37
|
-
|
|
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)
|
|
40
|
-
cache_file = self.cache_dir / f"{key}.json"
|
|
41
|
-
with open(cache_file, "w") as f:
|
|
42
|
-
json.dump(response.model_dump(), f)
|
|
@@ -1,44 +0,0 @@
|
|
|
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
|