devcouncil 0.1.1 → 0.3.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 +201 -6
- package/package.json +9 -2
- package/pyproject.toml +34 -2
- package/src/devcouncil/app/config.py +348 -12
- package/src/devcouncil/app/orchestrator.py +10 -6
- package/src/devcouncil/app/state_machine.py +4 -0
- package/src/devcouncil/artifacts/graph.py +32 -5
- 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 +220 -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/design.py +186 -0
- package/src/devcouncil/cli/commands/doctor.py +379 -22
- package/src/devcouncil/cli/commands/evidence.py +48 -0
- package/src/devcouncil/cli/commands/go.py +532 -33
- package/src/devcouncil/cli/commands/handoff.py +69 -0
- package/src/devcouncil/cli/commands/hook.py +296 -15
- package/src/devcouncil/cli/commands/init.py +161 -20
- package/src/devcouncil/cli/commands/integrate.py +1371 -124
- package/src/devcouncil/cli/commands/logs.py +106 -0
- package/src/devcouncil/cli/commands/map.py +80 -10
- package/src/devcouncil/cli/commands/okf.py +245 -0
- package/src/devcouncil/cli/commands/plan.py +256 -55
- package/src/devcouncil/cli/commands/prompt.py +18 -7
- package/src/devcouncil/cli/commands/repair.py +50 -24
- 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 +195 -54
- 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 +267 -0
- package/src/devcouncil/cli/commands/status.py +30 -15
- package/src/devcouncil/cli/commands/trace.py +47 -3
- package/src/devcouncil/cli/commands/verify.py +144 -3
- package/src/devcouncil/cli/commands/watch.py +32 -12
- package/src/devcouncil/cli/commands/watch_fs.py +40 -0
- package/src/devcouncil/cli/main.py +91 -7
- package/src/devcouncil/domain/evidence.py +29 -2
- package/src/devcouncil/domain/gap.py +27 -1
- package/src/devcouncil/domain/task.py +31 -2
- package/src/devcouncil/execution/checkpoints.py +256 -0
- package/src/devcouncil/execution/context_builder.py +1 -1
- package/src/devcouncil/execution/fs_watcher.py +205 -0
- package/src/devcouncil/execution/handoff.py +102 -0
- package/src/devcouncil/execution/hook_policy.py +162 -74
- package/src/devcouncil/execution/patch.py +65 -10
- package/src/devcouncil/execution/permissions.py +24 -24
- package/src/devcouncil/execution/policy_engine.py +350 -0
- package/src/devcouncil/execution/prompt_builder.py +751 -23
- package/src/devcouncil/execution/shell_session.py +231 -0
- package/src/devcouncil/execution/task_runner.py +24 -9
- package/src/devcouncil/executors/agent_registry.py +596 -0
- package/src/devcouncil/executors/coding_cli.py +791 -39
- package/src/devcouncil/executors/mini_swe.py +6 -0
- package/src/devcouncil/executors/native/agent.py +135 -19
- package/src/devcouncil/executors/openhands.py +6 -0
- package/src/devcouncil/gating/checks/clean_git.py +3 -1
- package/src/devcouncil/gating/checks/secret_scan_check.py +47 -21
- package/src/devcouncil/gating/policy.py +190 -11
- package/src/devcouncil/hardware.py +184 -0
- package/src/devcouncil/indexing/ast_matcher.py +17 -7
- package/src/devcouncil/indexing/lsp.py +45 -4
- package/src/devcouncil/indexing/repo_mapper.py +1284 -15
- package/src/devcouncil/indexing/semantic_index.py +221 -0
- package/src/devcouncil/integrations/actions.py +166 -0
- package/src/devcouncil/integrations/check.py +426 -0
- package/src/devcouncil/integrations/claude_assets.py +444 -0
- package/src/devcouncil/integrations/code_review_graph.py +13 -2
- package/src/devcouncil/integrations/github_intent.py +149 -0
- package/src/devcouncil/integrations/gitnexus.py +45 -2
- package/src/devcouncil/integrations/mcp/server.py +1944 -32
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +24 -0
- package/src/devcouncil/integrations/pr_comments.py +9 -0
- package/src/devcouncil/knowledge/__init__.py +23 -0
- package/src/devcouncil/knowledge/design.py +374 -0
- package/src/devcouncil/knowledge/design_conformance.py +317 -0
- package/src/devcouncil/knowledge/fetch.py +223 -0
- package/src/devcouncil/knowledge/frontmatter.py +51 -0
- package/src/devcouncil/knowledge/okf.py +202 -0
- package/src/devcouncil/knowledge/skill_bridge.py +96 -0
- package/src/devcouncil/knowledge/sources.py +239 -0
- package/src/devcouncil/live/cards.py +181 -25
- package/src/devcouncil/live/repair_prompt.py +29 -6
- package/src/devcouncil/live/reviewer.py +72 -13
- package/src/devcouncil/live/signals.py +2 -2
- package/src/devcouncil/live/summary.py +18 -8
- package/src/devcouncil/live/transcripts.py +47 -11
- package/src/devcouncil/llm/cache.py +20 -8
- package/src/devcouncil/llm/model_defaults.yaml +44 -0
- package/src/devcouncil/llm/provider.py +617 -49
- package/src/devcouncil/llm/router.py +337 -53
- package/src/devcouncil/optimization/__init__.py +1 -0
- package/src/devcouncil/optimization/gepa_agent.py +318 -0
- package/src/devcouncil/optimization/skillopt.py +673 -0
- package/src/devcouncil/planning/arbiter_service.py +10 -2
- package/src/devcouncil/planning/correction_manifest.py +346 -0
- package/src/devcouncil/planning/critique_service.py +16 -4
- package/src/devcouncil/planning/plan_service.py +86 -6
- package/src/devcouncil/planning/prompt_enhancer_service.py +206 -1
- package/src/devcouncil/planning/repair_service.py +8 -2
- package/src/devcouncil/planning/spec_service.py +37 -3
- package/src/devcouncil/repo/ci_scaffold.py +165 -0
- package/src/devcouncil/repo/gitignore.py +123 -0
- package/src/devcouncil/repo/sca.py +384 -0
- package/src/devcouncil/reporting/json_report.py +22 -1
- package/src/devcouncil/reporting/markdown_report.py +29 -1
- package/src/devcouncil/reporting/okf_bundle_writer.py +364 -0
- package/src/devcouncil/reporting/okf_html.py +323 -0
- package/src/devcouncil/reporting/report_builder.py +18 -1
- 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 +408 -0
- package/src/devcouncil/storage/db.py +140 -3
- package/src/devcouncil/storage/models.py +125 -0
- package/src/devcouncil/storage/native.py +559 -0
- package/src/devcouncil/storage/repositories.py +157 -78
- package/src/devcouncil/telemetry/cost.py +123 -17
- package/src/devcouncil/telemetry/logging_setup.py +244 -0
- package/src/devcouncil/telemetry/model_pricing.yaml +48 -0
- package/src/devcouncil/telemetry/pricing.py +28 -0
- package/src/devcouncil/telemetry/stages.py +141 -0
- package/src/devcouncil/telemetry/traces.py +62 -7
- package/src/devcouncil/telemetry/tracker.py +24 -10
- package/src/devcouncil/ui/dashboard.py +393 -28
- package/src/devcouncil/utils/redaction.py +9 -3
- package/src/devcouncil/utils/subprocess_env.py +69 -0
- package/src/devcouncil/verification/acceptance_compiler.py +253 -0
- package/src/devcouncil/verification/ad_hoc_check.py +135 -0
- package/src/devcouncil/verification/diff_coverage.py +353 -0
- package/src/devcouncil/verification/implementation_reviewer.py +11 -2
- package/src/devcouncil/verification/next_actions.py +189 -0
- package/src/devcouncil/verification/sandbox.py +181 -0
- package/src/devcouncil/verification/test_resolver.py +91 -0
- package/src/devcouncil/verification/verifier.py +1549 -143
- package/uv.lock +205 -64
- package/src/devcouncil/indexing/symbol_index.py +0 -0
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
"""Native control-plane storage repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import secrets
|
|
7
|
+
import uuid
|
|
8
|
+
from datetime import datetime, timedelta, timezone
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
from sqlalchemy.exc import IntegrityError
|
|
13
|
+
from sqlmodel import Session, col, select
|
|
14
|
+
|
|
15
|
+
from devcouncil.storage.models import (
|
|
16
|
+
AgentHandoffModel,
|
|
17
|
+
CorrectionManifestModel,
|
|
18
|
+
FileChangeEventModel,
|
|
19
|
+
SemanticDiffModel,
|
|
20
|
+
ShellCommandEventModel,
|
|
21
|
+
ShellSessionModel,
|
|
22
|
+
TaskLeaseModel,
|
|
23
|
+
VerificationRunModel,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _utc_now() -> str:
|
|
28
|
+
return datetime.now(timezone.utc).isoformat()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TaskLeaseRecord(BaseModel):
|
|
32
|
+
id: str
|
|
33
|
+
task_id: str
|
|
34
|
+
owner: str
|
|
35
|
+
agent: Optional[str] = None
|
|
36
|
+
client_id: Optional[str] = None
|
|
37
|
+
run_id: Optional[str] = None
|
|
38
|
+
branch: Optional[str] = None
|
|
39
|
+
lease_token: str
|
|
40
|
+
status: str
|
|
41
|
+
created_at: str
|
|
42
|
+
expires_at: Optional[str] = None
|
|
43
|
+
released_at: Optional[str] = None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ShellSessionRecord(BaseModel):
|
|
47
|
+
id: str
|
|
48
|
+
task_id: str
|
|
49
|
+
lease_id: Optional[str] = None
|
|
50
|
+
shell: str
|
|
51
|
+
cwd: str
|
|
52
|
+
status: str
|
|
53
|
+
started_at: str
|
|
54
|
+
ended_at: Optional[str] = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ShellCommandRecord(BaseModel):
|
|
58
|
+
id: int
|
|
59
|
+
task_id: str
|
|
60
|
+
session_id: Optional[str] = None
|
|
61
|
+
command: str
|
|
62
|
+
status: str
|
|
63
|
+
exit_code: Optional[int] = None
|
|
64
|
+
reason: str = ""
|
|
65
|
+
stdout_path: str = ""
|
|
66
|
+
stderr_path: str = ""
|
|
67
|
+
created_at: str
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class FileChangeRecord(BaseModel):
|
|
71
|
+
id: int
|
|
72
|
+
task_id: Optional[str] = None
|
|
73
|
+
lease_id: Optional[str] = None
|
|
74
|
+
session_id: Optional[str] = None
|
|
75
|
+
path: str
|
|
76
|
+
operation: str
|
|
77
|
+
allowed: bool
|
|
78
|
+
reason: str = ""
|
|
79
|
+
created_at: str
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class SemanticDiffRecord(BaseModel):
|
|
83
|
+
id: int
|
|
84
|
+
task_id: str
|
|
85
|
+
before_snapshot_path: str
|
|
86
|
+
after_snapshot_path: str
|
|
87
|
+
classifications: list[dict]
|
|
88
|
+
summary: str
|
|
89
|
+
created_at: str
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class AgentHandoffRecord(BaseModel):
|
|
93
|
+
id: str
|
|
94
|
+
task_id: str
|
|
95
|
+
from_agent: str
|
|
96
|
+
to_agent: str
|
|
97
|
+
run_id: str
|
|
98
|
+
manifest_path: str
|
|
99
|
+
status: str
|
|
100
|
+
created_at: str
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class CorrectionManifestRecord(BaseModel):
|
|
104
|
+
id: str
|
|
105
|
+
task_id: str
|
|
106
|
+
run_id: Optional[str] = None
|
|
107
|
+
manifest_path: str
|
|
108
|
+
retry_budget: int
|
|
109
|
+
attempt: int
|
|
110
|
+
status: str
|
|
111
|
+
created_at: str
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class VerificationRunRecord(BaseModel):
|
|
115
|
+
id: str
|
|
116
|
+
task_id: str
|
|
117
|
+
sandbox: str
|
|
118
|
+
environment: dict
|
|
119
|
+
commands: list[dict]
|
|
120
|
+
status: str
|
|
121
|
+
started_at: str
|
|
122
|
+
finished_at: Optional[str] = None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _lease_from_model(model: TaskLeaseModel) -> TaskLeaseRecord:
|
|
126
|
+
return TaskLeaseRecord.model_validate(model.model_dump())
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TaskLeaseRepository:
|
|
130
|
+
def __init__(self, session: Session):
|
|
131
|
+
self.session = session
|
|
132
|
+
|
|
133
|
+
def acquire(
|
|
134
|
+
self,
|
|
135
|
+
task_id: str,
|
|
136
|
+
owner: str,
|
|
137
|
+
*,
|
|
138
|
+
agent: Optional[str] = None,
|
|
139
|
+
client_id: Optional[str] = None,
|
|
140
|
+
run_id: Optional[str] = None,
|
|
141
|
+
branch: Optional[str] = None,
|
|
142
|
+
ttl_seconds: Optional[int] = None,
|
|
143
|
+
force: bool = False,
|
|
144
|
+
) -> TaskLeaseRecord:
|
|
145
|
+
active = self._active_model_for_task(task_id)
|
|
146
|
+
if active is not None and not force:
|
|
147
|
+
raise ValueError(f"Active lease already exists for task {task_id}")
|
|
148
|
+
if active is not None and force:
|
|
149
|
+
active.status = "stale"
|
|
150
|
+
active.released_at = _utc_now()
|
|
151
|
+
self.session.add(active)
|
|
152
|
+
self.session.commit()
|
|
153
|
+
|
|
154
|
+
lease_id = str(uuid.uuid4())
|
|
155
|
+
token = secrets.token_urlsafe(32)
|
|
156
|
+
expires_at = None
|
|
157
|
+
if ttl_seconds is not None:
|
|
158
|
+
expires_at = (datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)).isoformat()
|
|
159
|
+
|
|
160
|
+
model = TaskLeaseModel(
|
|
161
|
+
id=lease_id,
|
|
162
|
+
task_id=task_id,
|
|
163
|
+
owner=owner,
|
|
164
|
+
agent=agent,
|
|
165
|
+
client_id=client_id,
|
|
166
|
+
run_id=run_id,
|
|
167
|
+
branch=branch,
|
|
168
|
+
lease_token=token,
|
|
169
|
+
status="active",
|
|
170
|
+
created_at=_utc_now(),
|
|
171
|
+
expires_at=expires_at,
|
|
172
|
+
)
|
|
173
|
+
self.session.add(model)
|
|
174
|
+
try:
|
|
175
|
+
self.session.commit()
|
|
176
|
+
except IntegrityError as exc:
|
|
177
|
+
# Lost a concurrent race: another writer inserted the active lease between
|
|
178
|
+
# our active_for_task() check and this commit. The partial unique index
|
|
179
|
+
# rejected the duplicate — surface it as the same conflict callers expect.
|
|
180
|
+
self.session.rollback()
|
|
181
|
+
raise ValueError(f"Active lease already exists for task {task_id}") from exc
|
|
182
|
+
self.session.refresh(model)
|
|
183
|
+
return _lease_from_model(model)
|
|
184
|
+
|
|
185
|
+
def release(self, task_id: str, lease_token: str, status: str = "released") -> bool:
|
|
186
|
+
model = self._active_model_for_task(task_id)
|
|
187
|
+
if model is None or model.lease_token != lease_token:
|
|
188
|
+
return False
|
|
189
|
+
model.status = status
|
|
190
|
+
model.released_at = _utc_now()
|
|
191
|
+
self.session.add(model)
|
|
192
|
+
self.session.commit()
|
|
193
|
+
return True
|
|
194
|
+
|
|
195
|
+
def _active_model_for_task(self, task_id: str) -> Optional[TaskLeaseModel]:
|
|
196
|
+
"""Return the live ACTIVE lease *model* for a task (or None), lazily expiring a
|
|
197
|
+
lease that is past its TTL. Callers that need a Record wrap the result via
|
|
198
|
+
_lease_from_model — this avoids re-fetching the same row by primary key after a
|
|
199
|
+
Record-returning lookup."""
|
|
200
|
+
statement = (
|
|
201
|
+
select(TaskLeaseModel)
|
|
202
|
+
.where(TaskLeaseModel.task_id == task_id)
|
|
203
|
+
.where(TaskLeaseModel.status == "active")
|
|
204
|
+
)
|
|
205
|
+
model = self.session.exec(statement).first()
|
|
206
|
+
if model is None:
|
|
207
|
+
return None
|
|
208
|
+
if model.expires_at:
|
|
209
|
+
expires = datetime.fromisoformat(model.expires_at)
|
|
210
|
+
if expires.tzinfo is None:
|
|
211
|
+
expires = expires.replace(tzinfo=timezone.utc)
|
|
212
|
+
if datetime.now(timezone.utc) > expires:
|
|
213
|
+
model.status = "stale"
|
|
214
|
+
model.released_at = _utc_now()
|
|
215
|
+
self.session.add(model)
|
|
216
|
+
self.session.commit()
|
|
217
|
+
return None
|
|
218
|
+
return model
|
|
219
|
+
|
|
220
|
+
def active_for_task(self, task_id: str) -> TaskLeaseRecord | None:
|
|
221
|
+
model = self._active_model_for_task(task_id)
|
|
222
|
+
if model is None:
|
|
223
|
+
return None
|
|
224
|
+
return _lease_from_model(model)
|
|
225
|
+
|
|
226
|
+
def validate(self, task_id: str, lease_token: str) -> bool:
|
|
227
|
+
active = self.active_for_task(task_id)
|
|
228
|
+
return active is not None and active.lease_token == lease_token
|
|
229
|
+
|
|
230
|
+
def renew(self, task_id: str, lease_token: str, ttl_seconds: int) -> TaskLeaseRecord | None:
|
|
231
|
+
"""Push the lease's expiry out by ``ttl_seconds`` from now. Returns the updated
|
|
232
|
+
record, or None when the token is invalid / the lease already expired."""
|
|
233
|
+
model = self._active_model_for_task(task_id)
|
|
234
|
+
if model is None or model.lease_token != lease_token:
|
|
235
|
+
return None
|
|
236
|
+
model.expires_at = (datetime.now(timezone.utc) + timedelta(seconds=ttl_seconds)).isoformat()
|
|
237
|
+
self.session.add(model)
|
|
238
|
+
self.session.commit()
|
|
239
|
+
self.session.refresh(model)
|
|
240
|
+
return _lease_from_model(model)
|
|
241
|
+
|
|
242
|
+
def list_leases(self, *, active_only: bool = True) -> list[tuple[TaskLeaseRecord, bool]]:
|
|
243
|
+
"""Return ``(lease, expired)`` pairs for fleet supervision. ``expired`` means the
|
|
244
|
+
lease is still marked active but past its TTL (a crashed/disconnected agent)."""
|
|
245
|
+
statement = select(TaskLeaseModel)
|
|
246
|
+
if active_only:
|
|
247
|
+
statement = statement.where(TaskLeaseModel.status == "active")
|
|
248
|
+
statement = statement.order_by(col(TaskLeaseModel.created_at).desc())
|
|
249
|
+
now = datetime.now(timezone.utc)
|
|
250
|
+
results: list[tuple[TaskLeaseRecord, bool]] = []
|
|
251
|
+
for model in self.session.exec(statement).all():
|
|
252
|
+
expired = False
|
|
253
|
+
if model.status == "active" and model.expires_at:
|
|
254
|
+
expires = datetime.fromisoformat(model.expires_at)
|
|
255
|
+
if expires.tzinfo is None:
|
|
256
|
+
expires = expires.replace(tzinfo=timezone.utc)
|
|
257
|
+
expired = now > expires
|
|
258
|
+
results.append((_lease_from_model(model), expired))
|
|
259
|
+
return results
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
class ShellSessionRepository:
|
|
263
|
+
def __init__(self, session: Session):
|
|
264
|
+
self.session = session
|
|
265
|
+
|
|
266
|
+
def start(
|
|
267
|
+
self,
|
|
268
|
+
task_id: str,
|
|
269
|
+
shell: str,
|
|
270
|
+
cwd: str,
|
|
271
|
+
*,
|
|
272
|
+
lease_id: Optional[str] = None,
|
|
273
|
+
status: str = "active",
|
|
274
|
+
) -> ShellSessionRecord:
|
|
275
|
+
session_id = str(uuid.uuid4())
|
|
276
|
+
model = ShellSessionModel(
|
|
277
|
+
id=session_id,
|
|
278
|
+
task_id=task_id,
|
|
279
|
+
lease_id=lease_id,
|
|
280
|
+
shell=shell,
|
|
281
|
+
cwd=cwd,
|
|
282
|
+
status=status,
|
|
283
|
+
started_at=_utc_now(),
|
|
284
|
+
)
|
|
285
|
+
self.session.add(model)
|
|
286
|
+
self.session.commit()
|
|
287
|
+
self.session.refresh(model)
|
|
288
|
+
return ShellSessionRecord.model_validate(model.model_dump())
|
|
289
|
+
|
|
290
|
+
def finish(self, session_id: str, status: str) -> bool:
|
|
291
|
+
model = self.session.get(ShellSessionModel, session_id)
|
|
292
|
+
if model is None:
|
|
293
|
+
return False
|
|
294
|
+
model.status = status
|
|
295
|
+
model.ended_at = _utc_now()
|
|
296
|
+
self.session.add(model)
|
|
297
|
+
self.session.commit()
|
|
298
|
+
return True
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class ShellCommandRepository:
|
|
302
|
+
def __init__(self, session: Session):
|
|
303
|
+
self.session = session
|
|
304
|
+
|
|
305
|
+
def record(
|
|
306
|
+
self,
|
|
307
|
+
task_id: str,
|
|
308
|
+
command: str,
|
|
309
|
+
status: str,
|
|
310
|
+
*,
|
|
311
|
+
session_id: Optional[str] = None,
|
|
312
|
+
exit_code: Optional[int] = None,
|
|
313
|
+
reason: str = "",
|
|
314
|
+
stdout_path: str = "",
|
|
315
|
+
stderr_path: str = "",
|
|
316
|
+
) -> ShellCommandRecord:
|
|
317
|
+
model = ShellCommandEventModel(
|
|
318
|
+
task_id=task_id,
|
|
319
|
+
session_id=session_id,
|
|
320
|
+
command=command,
|
|
321
|
+
status=status,
|
|
322
|
+
exit_code=exit_code,
|
|
323
|
+
reason=reason,
|
|
324
|
+
stdout_path=stdout_path,
|
|
325
|
+
stderr_path=stderr_path,
|
|
326
|
+
created_at=_utc_now(),
|
|
327
|
+
)
|
|
328
|
+
self.session.add(model)
|
|
329
|
+
self.session.commit()
|
|
330
|
+
self.session.refresh(model)
|
|
331
|
+
return ShellCommandRecord.model_validate(model.model_dump())
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
class FileChangeRepository:
|
|
335
|
+
def __init__(self, session: Session):
|
|
336
|
+
self.session = session
|
|
337
|
+
|
|
338
|
+
def record(
|
|
339
|
+
self,
|
|
340
|
+
path: str,
|
|
341
|
+
operation: str,
|
|
342
|
+
allowed: bool,
|
|
343
|
+
*,
|
|
344
|
+
task_id: Optional[str] = None,
|
|
345
|
+
lease_id: Optional[str] = None,
|
|
346
|
+
session_id: Optional[str] = None,
|
|
347
|
+
reason: str = "",
|
|
348
|
+
) -> FileChangeRecord:
|
|
349
|
+
model = FileChangeEventModel(
|
|
350
|
+
task_id=task_id,
|
|
351
|
+
lease_id=lease_id,
|
|
352
|
+
session_id=session_id,
|
|
353
|
+
path=path,
|
|
354
|
+
operation=operation,
|
|
355
|
+
allowed=allowed,
|
|
356
|
+
reason=reason,
|
|
357
|
+
created_at=_utc_now(),
|
|
358
|
+
)
|
|
359
|
+
self.session.add(model)
|
|
360
|
+
self.session.commit()
|
|
361
|
+
self.session.refresh(model)
|
|
362
|
+
return FileChangeRecord.model_validate(model.model_dump())
|
|
363
|
+
|
|
364
|
+
def list_for_task(self, task_id: str) -> list[FileChangeRecord]:
|
|
365
|
+
statement = (
|
|
366
|
+
select(FileChangeEventModel)
|
|
367
|
+
.where(FileChangeEventModel.task_id == task_id)
|
|
368
|
+
.order_by(col(FileChangeEventModel.created_at))
|
|
369
|
+
)
|
|
370
|
+
return [FileChangeRecord.model_validate(m.model_dump()) for m in self.session.exec(statement).all()]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class SemanticDiffRepository:
|
|
374
|
+
def __init__(self, session: Session):
|
|
375
|
+
self.session = session
|
|
376
|
+
|
|
377
|
+
def save(
|
|
378
|
+
self,
|
|
379
|
+
task_id: str,
|
|
380
|
+
before_snapshot_path: str,
|
|
381
|
+
after_snapshot_path: str,
|
|
382
|
+
classifications: list[dict],
|
|
383
|
+
summary: str,
|
|
384
|
+
) -> SemanticDiffRecord:
|
|
385
|
+
model = SemanticDiffModel(
|
|
386
|
+
task_id=task_id,
|
|
387
|
+
before_snapshot_path=before_snapshot_path,
|
|
388
|
+
after_snapshot_path=after_snapshot_path,
|
|
389
|
+
classifications_json=json.dumps(classifications),
|
|
390
|
+
summary=summary,
|
|
391
|
+
created_at=_utc_now(),
|
|
392
|
+
)
|
|
393
|
+
self.session.add(model)
|
|
394
|
+
self.session.commit()
|
|
395
|
+
self.session.refresh(model)
|
|
396
|
+
return SemanticDiffRecord(
|
|
397
|
+
id=model.id or 0,
|
|
398
|
+
task_id=model.task_id,
|
|
399
|
+
before_snapshot_path=model.before_snapshot_path,
|
|
400
|
+
after_snapshot_path=model.after_snapshot_path,
|
|
401
|
+
classifications=classifications,
|
|
402
|
+
summary=model.summary,
|
|
403
|
+
created_at=model.created_at,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
def latest_for_task(self, task_id: str) -> SemanticDiffRecord | None:
|
|
407
|
+
statement = (
|
|
408
|
+
select(SemanticDiffModel)
|
|
409
|
+
.where(SemanticDiffModel.task_id == task_id)
|
|
410
|
+
.order_by(col(SemanticDiffModel.created_at).desc())
|
|
411
|
+
)
|
|
412
|
+
model = self.session.exec(statement).first()
|
|
413
|
+
if model is None:
|
|
414
|
+
return None
|
|
415
|
+
return SemanticDiffRecord(
|
|
416
|
+
id=model.id or 0,
|
|
417
|
+
task_id=model.task_id,
|
|
418
|
+
before_snapshot_path=model.before_snapshot_path,
|
|
419
|
+
after_snapshot_path=model.after_snapshot_path,
|
|
420
|
+
classifications=json.loads(model.classifications_json),
|
|
421
|
+
summary=model.summary,
|
|
422
|
+
created_at=model.created_at,
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class AgentHandoffRepository:
|
|
427
|
+
def __init__(self, session: Session):
|
|
428
|
+
self.session = session
|
|
429
|
+
|
|
430
|
+
def save(
|
|
431
|
+
self,
|
|
432
|
+
task_id: str,
|
|
433
|
+
from_agent: str,
|
|
434
|
+
to_agent: str,
|
|
435
|
+
run_id: str,
|
|
436
|
+
manifest_path: str,
|
|
437
|
+
status: str,
|
|
438
|
+
*,
|
|
439
|
+
handoff_id: Optional[str] = None,
|
|
440
|
+
) -> AgentHandoffRecord:
|
|
441
|
+
hid = handoff_id or str(uuid.uuid4())
|
|
442
|
+
model = AgentHandoffModel(
|
|
443
|
+
id=hid,
|
|
444
|
+
task_id=task_id,
|
|
445
|
+
from_agent=from_agent,
|
|
446
|
+
to_agent=to_agent,
|
|
447
|
+
run_id=run_id,
|
|
448
|
+
manifest_path=manifest_path,
|
|
449
|
+
status=status,
|
|
450
|
+
created_at=_utc_now(),
|
|
451
|
+
)
|
|
452
|
+
self.session.add(model)
|
|
453
|
+
self.session.commit()
|
|
454
|
+
self.session.refresh(model)
|
|
455
|
+
return AgentHandoffRecord.model_validate(model.model_dump())
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
class CorrectionManifestRepository:
|
|
459
|
+
def __init__(self, session: Session):
|
|
460
|
+
self.session = session
|
|
461
|
+
|
|
462
|
+
def save(
|
|
463
|
+
self,
|
|
464
|
+
task_id: str,
|
|
465
|
+
manifest_path: str,
|
|
466
|
+
status: str,
|
|
467
|
+
*,
|
|
468
|
+
run_id: Optional[str] = None,
|
|
469
|
+
retry_budget: int = 3,
|
|
470
|
+
attempt: int = 0,
|
|
471
|
+
manifest_id: Optional[str] = None,
|
|
472
|
+
) -> CorrectionManifestRecord:
|
|
473
|
+
mid = manifest_id or str(uuid.uuid4())
|
|
474
|
+
model = CorrectionManifestModel(
|
|
475
|
+
id=mid,
|
|
476
|
+
task_id=task_id,
|
|
477
|
+
run_id=run_id,
|
|
478
|
+
manifest_path=manifest_path,
|
|
479
|
+
retry_budget=retry_budget,
|
|
480
|
+
attempt=attempt,
|
|
481
|
+
status=status,
|
|
482
|
+
created_at=_utc_now(),
|
|
483
|
+
)
|
|
484
|
+
self.session.add(model)
|
|
485
|
+
self.session.commit()
|
|
486
|
+
self.session.refresh(model)
|
|
487
|
+
return CorrectionManifestRecord.model_validate(model.model_dump())
|
|
488
|
+
|
|
489
|
+
def latest_for_task(self, task_id: str) -> CorrectionManifestRecord | None:
|
|
490
|
+
statement = (
|
|
491
|
+
select(CorrectionManifestModel)
|
|
492
|
+
.where(CorrectionManifestModel.task_id == task_id)
|
|
493
|
+
.order_by(col(CorrectionManifestModel.created_at).desc())
|
|
494
|
+
)
|
|
495
|
+
model = self.session.exec(statement).first()
|
|
496
|
+
if model is None:
|
|
497
|
+
return None
|
|
498
|
+
return CorrectionManifestRecord.model_validate(model.model_dump())
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
class VerificationRunRepository:
|
|
502
|
+
def __init__(self, session: Session):
|
|
503
|
+
self.session = session
|
|
504
|
+
|
|
505
|
+
def save(
|
|
506
|
+
self,
|
|
507
|
+
task_id: str,
|
|
508
|
+
sandbox: str,
|
|
509
|
+
environment: dict,
|
|
510
|
+
commands: list[dict],
|
|
511
|
+
status: str,
|
|
512
|
+
*,
|
|
513
|
+
run_id: Optional[str] = None,
|
|
514
|
+
finished_at: Optional[str] = None,
|
|
515
|
+
) -> VerificationRunRecord:
|
|
516
|
+
rid = run_id or str(uuid.uuid4())
|
|
517
|
+
model = VerificationRunModel(
|
|
518
|
+
id=rid,
|
|
519
|
+
task_id=task_id,
|
|
520
|
+
sandbox=sandbox,
|
|
521
|
+
environment_json=json.dumps(environment),
|
|
522
|
+
commands_json=json.dumps(commands),
|
|
523
|
+
status=status,
|
|
524
|
+
started_at=_utc_now(),
|
|
525
|
+
finished_at=finished_at,
|
|
526
|
+
)
|
|
527
|
+
self.session.add(model)
|
|
528
|
+
self.session.commit()
|
|
529
|
+
self.session.refresh(model)
|
|
530
|
+
return VerificationRunRecord(
|
|
531
|
+
id=model.id,
|
|
532
|
+
task_id=model.task_id,
|
|
533
|
+
sandbox=model.sandbox,
|
|
534
|
+
environment=environment,
|
|
535
|
+
commands=commands,
|
|
536
|
+
status=model.status,
|
|
537
|
+
started_at=model.started_at,
|
|
538
|
+
finished_at=model.finished_at,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
def list_for_task(self, task_id: str) -> list[VerificationRunRecord]:
|
|
542
|
+
statement = (
|
|
543
|
+
select(VerificationRunModel)
|
|
544
|
+
.where(VerificationRunModel.task_id == task_id)
|
|
545
|
+
.order_by(col(VerificationRunModel.started_at))
|
|
546
|
+
)
|
|
547
|
+
records: list[VerificationRunRecord] = []
|
|
548
|
+
for model in self.session.exec(statement).all():
|
|
549
|
+
records.append(VerificationRunRecord(
|
|
550
|
+
id=model.id,
|
|
551
|
+
task_id=model.task_id,
|
|
552
|
+
sandbox=model.sandbox,
|
|
553
|
+
environment=json.loads(model.environment_json),
|
|
554
|
+
commands=json.loads(model.commands_json),
|
|
555
|
+
status=model.status,
|
|
556
|
+
started_at=model.started_at,
|
|
557
|
+
finished_at=model.finished_at,
|
|
558
|
+
))
|
|
559
|
+
return records
|