codeframe-ai 0.9.0__py3-none-any.whl
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.
- codeframe/__init__.py +11 -0
- codeframe/__main__.py +20 -0
- codeframe/adapters/__init__.py +5 -0
- codeframe/adapters/e2b/__init__.py +13 -0
- codeframe/adapters/e2b/adapter.py +342 -0
- codeframe/adapters/e2b/budget.py +71 -0
- codeframe/adapters/e2b/credential_scanner.py +134 -0
- codeframe/adapters/llm/__init__.py +92 -0
- codeframe/adapters/llm/anthropic.py +414 -0
- codeframe/adapters/llm/base.py +444 -0
- codeframe/adapters/llm/mock.py +281 -0
- codeframe/adapters/llm/openai.py +483 -0
- codeframe/agents/__init__.py +8 -0
- codeframe/agents/dependency_resolver.py +714 -0
- codeframe/auth/__init__.py +16 -0
- codeframe/auth/api_key_router.py +238 -0
- codeframe/auth/api_keys.py +156 -0
- codeframe/auth/dependencies.py +358 -0
- codeframe/auth/manager.py +178 -0
- codeframe/auth/models.py +30 -0
- codeframe/auth/router.py +93 -0
- codeframe/auth/schemas.py +15 -0
- codeframe/auth/scopes.py +53 -0
- codeframe/cli/__init__.py +12 -0
- codeframe/cli/__main__.py +20 -0
- codeframe/cli/api_client.py +275 -0
- codeframe/cli/app.py +5688 -0
- codeframe/cli/auth.py +122 -0
- codeframe/cli/auth_commands.py +958 -0
- codeframe/cli/commands/__init__.py +5 -0
- codeframe/cli/config_commands.py +79 -0
- codeframe/cli/dashboard_commands.py +67 -0
- codeframe/cli/engines_commands.py +205 -0
- codeframe/cli/env_commands.py +409 -0
- codeframe/cli/helpers.py +56 -0
- codeframe/cli/hooks_commands.py +208 -0
- codeframe/cli/import_commands.py +129 -0
- codeframe/cli/pr_commands.py +549 -0
- codeframe/cli/proof_commands.py +415 -0
- codeframe/cli/stats_commands.py +311 -0
- codeframe/cli/telemetry_runtime.py +153 -0
- codeframe/cli/validators.py +123 -0
- codeframe/config/rate_limits.py +165 -0
- codeframe/core/__init__.py +15 -0
- codeframe/core/adapters/__init__.py +43 -0
- codeframe/core/adapters/agent_adapter.py +114 -0
- codeframe/core/adapters/builtin.py +326 -0
- codeframe/core/adapters/claude_code.py +62 -0
- codeframe/core/adapters/codex.py +393 -0
- codeframe/core/adapters/git_utils.py +40 -0
- codeframe/core/adapters/kilocode.py +126 -0
- codeframe/core/adapters/opencode.py +48 -0
- codeframe/core/adapters/streaming_chat.py +483 -0
- codeframe/core/adapters/subprocess_adapter.py +213 -0
- codeframe/core/adapters/verification_wrapper.py +269 -0
- codeframe/core/agent.py +2183 -0
- codeframe/core/agents_config.py +569 -0
- codeframe/core/api_key_service.py +211 -0
- codeframe/core/artifacts.py +428 -0
- codeframe/core/blocker_detection.py +218 -0
- codeframe/core/blockers.py +433 -0
- codeframe/core/checkpoints.py +481 -0
- codeframe/core/conductor.py +2255 -0
- codeframe/core/config.py +827 -0
- codeframe/core/config_watcher.py +268 -0
- codeframe/core/context.py +542 -0
- codeframe/core/context_packager.py +234 -0
- codeframe/core/credentials.py +735 -0
- codeframe/core/dependency_analyzer.py +229 -0
- codeframe/core/dependency_graph.py +290 -0
- codeframe/core/diagnostic_agent.py +712 -0
- codeframe/core/diagnostics.py +616 -0
- codeframe/core/editor.py +556 -0
- codeframe/core/engine_registry.py +256 -0
- codeframe/core/engine_stats.py +231 -0
- codeframe/core/environment.py +697 -0
- codeframe/core/events.py +375 -0
- codeframe/core/executor.py +1005 -0
- codeframe/core/fix_tracker.py +480 -0
- codeframe/core/gates.py +1322 -0
- codeframe/core/git.py +477 -0
- codeframe/core/github_connect_service.py +178 -0
- codeframe/core/github_integration_config.py +118 -0
- codeframe/core/github_issues_service.py +449 -0
- codeframe/core/hooks.py +184 -0
- codeframe/core/importers/__init__.py +1 -0
- codeframe/core/importers/ralph.py +540 -0
- codeframe/core/installer.py +650 -0
- codeframe/core/models.py +1026 -0
- codeframe/core/notifications_config.py +183 -0
- codeframe/core/planner.py +437 -0
- codeframe/core/prd.py +670 -0
- codeframe/core/prd_discovery.py +1118 -0
- codeframe/core/prd_stress_test.py +499 -0
- codeframe/core/progress.py +126 -0
- codeframe/core/proof/__init__.py +34 -0
- codeframe/core/proof/capture.py +79 -0
- codeframe/core/proof/evidence.py +56 -0
- codeframe/core/proof/ledger.py +574 -0
- codeframe/core/proof/models.py +162 -0
- codeframe/core/proof/obligations.py +103 -0
- codeframe/core/proof/runner.py +233 -0
- codeframe/core/proof/scope.py +81 -0
- codeframe/core/proof/stubs.py +156 -0
- codeframe/core/quick_fixes.py +558 -0
- codeframe/core/react_agent.py +1650 -0
- codeframe/core/reconciliation.py +183 -0
- codeframe/core/replay.py +788 -0
- codeframe/core/review.py +285 -0
- codeframe/core/runtime.py +1134 -0
- codeframe/core/sandbox/__init__.py +27 -0
- codeframe/core/sandbox/context.py +98 -0
- codeframe/core/sandbox/worktree.py +20 -0
- codeframe/core/schedule.py +396 -0
- codeframe/core/stall_detector.py +71 -0
- codeframe/core/stall_monitor.py +134 -0
- codeframe/core/state_machine.py +121 -0
- codeframe/core/streaming.py +502 -0
- codeframe/core/task_tree.py +400 -0
- codeframe/core/tasks.py +1022 -0
- codeframe/core/telemetry.py +232 -0
- codeframe/core/templates.py +221 -0
- codeframe/core/tools.py +942 -0
- codeframe/core/workspace.py +887 -0
- codeframe/core/worktrees.py +276 -0
- codeframe/git/__init__.py +5 -0
- codeframe/git/github_integration.py +505 -0
- codeframe/lib/__init__.py +0 -0
- codeframe/lib/audit_logger.py +248 -0
- codeframe/lib/metrics_tracker.py +800 -0
- codeframe/lib/quality/__init__.py +7 -0
- codeframe/lib/quality/complexity_analyzer.py +316 -0
- codeframe/lib/quality/owasp_patterns.py +284 -0
- codeframe/lib/quality/security_scanner.py +250 -0
- codeframe/lib/rate_limiter.py +312 -0
- codeframe/notifications/__init__.py +0 -0
- codeframe/notifications/webhook.py +380 -0
- codeframe/planning/__init__.py +30 -0
- codeframe/planning/issue_generator.py +219 -0
- codeframe/planning/prd_template_functions.py +137 -0
- codeframe/planning/prd_templates.py +975 -0
- codeframe/planning/task_scheduler.py +511 -0
- codeframe/planning/task_templates.py +533 -0
- codeframe/platform_store/__init__.py +5 -0
- codeframe/platform_store/database.py +277 -0
- codeframe/platform_store/repositories/__init__.py +24 -0
- codeframe/platform_store/repositories/api_key_repository.py +245 -0
- codeframe/platform_store/repositories/audit_repository.py +67 -0
- codeframe/platform_store/repositories/base.py +295 -0
- codeframe/platform_store/repositories/interactive_sessions.py +165 -0
- codeframe/platform_store/repositories/token_repository.py +598 -0
- codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
- codeframe/platform_store/schema_manager.py +321 -0
- codeframe/templates/AGENTS.md.default +94 -0
- codeframe/tui/__init__.py +5 -0
- codeframe/tui/app.py +256 -0
- codeframe/tui/data_service.py +103 -0
- codeframe/ui/__init__.py +0 -0
- codeframe/ui/dependencies.py +103 -0
- codeframe/ui/models.py +999 -0
- codeframe/ui/response_models.py +201 -0
- codeframe/ui/routers/__init__.py +5 -0
- codeframe/ui/routers/_helpers.py +29 -0
- codeframe/ui/routers/batches_v2.py +315 -0
- codeframe/ui/routers/blockers_v2.py +320 -0
- codeframe/ui/routers/checkpoints_v2.py +310 -0
- codeframe/ui/routers/costs_v2.py +322 -0
- codeframe/ui/routers/diagnose_v2.py +225 -0
- codeframe/ui/routers/discovery_v2.py +417 -0
- codeframe/ui/routers/environment_v2.py +284 -0
- codeframe/ui/routers/events_v2.py +75 -0
- codeframe/ui/routers/gates_v2.py +166 -0
- codeframe/ui/routers/git_v2.py +284 -0
- codeframe/ui/routers/github_integrations_v2.py +532 -0
- codeframe/ui/routers/interactive_sessions_v2.py +238 -0
- codeframe/ui/routers/pr_v2.py +709 -0
- codeframe/ui/routers/prd_v2.py +695 -0
- codeframe/ui/routers/proof_v2.py +755 -0
- codeframe/ui/routers/review_v2.py +360 -0
- codeframe/ui/routers/schedule_v2.py +214 -0
- codeframe/ui/routers/session_chat_ws.py +354 -0
- codeframe/ui/routers/settings_v2.py +562 -0
- codeframe/ui/routers/streaming_v2.py +155 -0
- codeframe/ui/routers/tasks_v2.py +1098 -0
- codeframe/ui/routers/templates_v2.py +232 -0
- codeframe/ui/routers/terminal_ws.py +267 -0
- codeframe/ui/routers/workspace_v2.py +527 -0
- codeframe/ui/server.py +568 -0
- codeframe/ui/shared.py +241 -0
- codeframe/workspace/__init__.py +5 -0
- codeframe/workspace/manager.py +249 -0
- codeframe_ai-0.9.0.dist-info/METADATA +517 -0
- codeframe_ai-0.9.0.dist-info/RECORD +197 -0
- codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
- codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
- codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
- codeframe_ai-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""Checkpoint management for CodeFRAME v2.
|
|
2
|
+
|
|
3
|
+
Checkpoints are snapshots of workspace state that can be restored later.
|
|
4
|
+
They capture tasks, blockers, and optionally git refs.
|
|
5
|
+
|
|
6
|
+
This module is headless - no FastAPI or HTTP dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import subprocess
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
17
|
+
from codeframe.core.workspace import Workspace, get_db_connection
|
|
18
|
+
from codeframe.core import events, tasks, blockers, prd
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _utc_now() -> datetime:
|
|
22
|
+
"""Get current UTC time as timezone-aware datetime."""
|
|
23
|
+
return datetime.now(timezone.utc)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class Checkpoint:
|
|
28
|
+
"""Represents a state checkpoint.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
id: Unique checkpoint identifier (UUID)
|
|
32
|
+
workspace_id: Workspace this checkpoint belongs to
|
|
33
|
+
name: Human-readable checkpoint name
|
|
34
|
+
snapshot: JSON snapshot of state
|
|
35
|
+
created_at: When the checkpoint was created
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
id: str
|
|
39
|
+
workspace_id: str
|
|
40
|
+
name: str
|
|
41
|
+
snapshot: dict
|
|
42
|
+
created_at: datetime
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def create(
|
|
46
|
+
workspace: Workspace,
|
|
47
|
+
name: str,
|
|
48
|
+
include_git_ref: bool = True,
|
|
49
|
+
) -> Checkpoint:
|
|
50
|
+
"""Create a new checkpoint.
|
|
51
|
+
|
|
52
|
+
Captures current state of tasks, blockers, PRD, and optionally git ref.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
workspace: Target workspace
|
|
56
|
+
name: Checkpoint name
|
|
57
|
+
include_git_ref: Whether to capture current git HEAD
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Created Checkpoint
|
|
61
|
+
"""
|
|
62
|
+
checkpoint_id = str(uuid.uuid4())
|
|
63
|
+
now = _utc_now().isoformat()
|
|
64
|
+
|
|
65
|
+
# Build snapshot
|
|
66
|
+
snapshot = _build_snapshot(workspace, include_git_ref)
|
|
67
|
+
snapshot_json = json.dumps(snapshot)
|
|
68
|
+
|
|
69
|
+
conn = get_db_connection(workspace)
|
|
70
|
+
try:
|
|
71
|
+
cursor = conn.cursor()
|
|
72
|
+
cursor.execute(
|
|
73
|
+
"""
|
|
74
|
+
INSERT INTO checkpoints (id, workspace_id, name, snapshot, created_at)
|
|
75
|
+
VALUES (?, ?, ?, ?, ?)
|
|
76
|
+
""",
|
|
77
|
+
(checkpoint_id, workspace.id, name, snapshot_json, now),
|
|
78
|
+
)
|
|
79
|
+
conn.commit()
|
|
80
|
+
finally:
|
|
81
|
+
conn.close()
|
|
82
|
+
|
|
83
|
+
checkpoint = Checkpoint(
|
|
84
|
+
id=checkpoint_id,
|
|
85
|
+
workspace_id=workspace.id,
|
|
86
|
+
name=name,
|
|
87
|
+
snapshot=snapshot,
|
|
88
|
+
created_at=datetime.fromisoformat(now),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Emit event
|
|
92
|
+
events.emit_for_workspace(
|
|
93
|
+
workspace,
|
|
94
|
+
events.EventType.CHECKPOINT_CREATED,
|
|
95
|
+
{
|
|
96
|
+
"checkpoint_id": checkpoint_id,
|
|
97
|
+
"name": name,
|
|
98
|
+
"tasks_count": len(snapshot.get("tasks", [])),
|
|
99
|
+
},
|
|
100
|
+
print_event=True,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return checkpoint
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get(workspace: Workspace, checkpoint_id: str) -> Optional[Checkpoint]:
|
|
107
|
+
"""Get a checkpoint by ID or name.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
workspace: Workspace to query
|
|
111
|
+
checkpoint_id: Checkpoint ID or name
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Checkpoint if found, None otherwise
|
|
115
|
+
"""
|
|
116
|
+
conn = get_db_connection(workspace)
|
|
117
|
+
try:
|
|
118
|
+
cursor = conn.cursor()
|
|
119
|
+
|
|
120
|
+
# Try exact ID match
|
|
121
|
+
cursor.execute(
|
|
122
|
+
"""
|
|
123
|
+
SELECT id, workspace_id, name, snapshot, created_at
|
|
124
|
+
FROM checkpoints
|
|
125
|
+
WHERE workspace_id = ? AND (id = ? OR name = ?)
|
|
126
|
+
""",
|
|
127
|
+
(workspace.id, checkpoint_id, checkpoint_id),
|
|
128
|
+
)
|
|
129
|
+
row = cursor.fetchone()
|
|
130
|
+
|
|
131
|
+
# Try prefix match if no exact match
|
|
132
|
+
if not row:
|
|
133
|
+
cursor.execute(
|
|
134
|
+
"""
|
|
135
|
+
SELECT id, workspace_id, name, snapshot, created_at
|
|
136
|
+
FROM checkpoints
|
|
137
|
+
WHERE workspace_id = ? AND id LIKE ?
|
|
138
|
+
""",
|
|
139
|
+
(workspace.id, f"{checkpoint_id}%"),
|
|
140
|
+
)
|
|
141
|
+
rows = cursor.fetchall()
|
|
142
|
+
if len(rows) == 1:
|
|
143
|
+
row = rows[0]
|
|
144
|
+
finally:
|
|
145
|
+
conn.close()
|
|
146
|
+
|
|
147
|
+
if not row:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
return _row_to_checkpoint(row)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def list_all(workspace: Workspace, limit: int = 50) -> list[Checkpoint]:
|
|
154
|
+
"""List all checkpoints.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
workspace: Workspace to query
|
|
158
|
+
limit: Maximum checkpoints to return
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
List of Checkpoints, newest first
|
|
162
|
+
"""
|
|
163
|
+
conn = get_db_connection(workspace)
|
|
164
|
+
try:
|
|
165
|
+
cursor = conn.cursor()
|
|
166
|
+
cursor.execute(
|
|
167
|
+
"""
|
|
168
|
+
SELECT id, workspace_id, name, snapshot, created_at
|
|
169
|
+
FROM checkpoints
|
|
170
|
+
WHERE workspace_id = ?
|
|
171
|
+
ORDER BY created_at DESC
|
|
172
|
+
LIMIT ?
|
|
173
|
+
""",
|
|
174
|
+
(workspace.id, limit),
|
|
175
|
+
)
|
|
176
|
+
rows = cursor.fetchall()
|
|
177
|
+
finally:
|
|
178
|
+
conn.close()
|
|
179
|
+
|
|
180
|
+
return [_row_to_checkpoint(row) for row in rows]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def restore(workspace: Workspace, checkpoint_id: str) -> Checkpoint:
|
|
184
|
+
"""Restore state from a checkpoint.
|
|
185
|
+
|
|
186
|
+
Restores task statuses from the checkpoint. Does not modify files.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
workspace: Target workspace
|
|
190
|
+
checkpoint_id: Checkpoint ID or name
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
Restored Checkpoint
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
ValueError: If checkpoint not found
|
|
197
|
+
"""
|
|
198
|
+
checkpoint = get(workspace, checkpoint_id)
|
|
199
|
+
if not checkpoint:
|
|
200
|
+
raise ValueError(f"Checkpoint not found: {checkpoint_id}")
|
|
201
|
+
|
|
202
|
+
snapshot = checkpoint.snapshot
|
|
203
|
+
|
|
204
|
+
# Restore task statuses
|
|
205
|
+
conn = get_db_connection(workspace)
|
|
206
|
+
try:
|
|
207
|
+
cursor = conn.cursor()
|
|
208
|
+
for task_data in snapshot.get("tasks", []):
|
|
209
|
+
cursor.execute(
|
|
210
|
+
"""
|
|
211
|
+
UPDATE tasks
|
|
212
|
+
SET status = ?, updated_at = ?
|
|
213
|
+
WHERE id = ? AND workspace_id = ?
|
|
214
|
+
""",
|
|
215
|
+
(task_data["status"], _utc_now().isoformat(), task_data["id"], workspace.id),
|
|
216
|
+
)
|
|
217
|
+
conn.commit()
|
|
218
|
+
finally:
|
|
219
|
+
conn.close()
|
|
220
|
+
|
|
221
|
+
# Emit event
|
|
222
|
+
events.emit_for_workspace(
|
|
223
|
+
workspace,
|
|
224
|
+
events.EventType.CHECKPOINT_RESTORED,
|
|
225
|
+
{
|
|
226
|
+
"checkpoint_id": checkpoint.id,
|
|
227
|
+
"name": checkpoint.name,
|
|
228
|
+
},
|
|
229
|
+
print_event=True,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return checkpoint
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def delete(workspace: Workspace, checkpoint_id: str) -> bool:
|
|
236
|
+
"""Delete a checkpoint.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
workspace: Target workspace
|
|
240
|
+
checkpoint_id: Checkpoint ID or name
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
True if deleted, False if not found
|
|
244
|
+
"""
|
|
245
|
+
checkpoint = get(workspace, checkpoint_id)
|
|
246
|
+
if not checkpoint:
|
|
247
|
+
return False
|
|
248
|
+
|
|
249
|
+
conn = get_db_connection(workspace)
|
|
250
|
+
try:
|
|
251
|
+
cursor = conn.cursor()
|
|
252
|
+
cursor.execute(
|
|
253
|
+
"DELETE FROM checkpoints WHERE id = ?",
|
|
254
|
+
(checkpoint.id,),
|
|
255
|
+
)
|
|
256
|
+
conn.commit()
|
|
257
|
+
finally:
|
|
258
|
+
conn.close()
|
|
259
|
+
|
|
260
|
+
return True
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _build_snapshot(workspace: Workspace, include_git_ref: bool = True) -> dict:
|
|
264
|
+
"""Build a snapshot of current state."""
|
|
265
|
+
snapshot = {
|
|
266
|
+
"version": 1,
|
|
267
|
+
"created_at": _utc_now().isoformat(),
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# Capture tasks
|
|
271
|
+
all_tasks = tasks.list_tasks(workspace, limit=1000)
|
|
272
|
+
snapshot["tasks"] = [
|
|
273
|
+
{
|
|
274
|
+
"id": t.id,
|
|
275
|
+
"title": t.title,
|
|
276
|
+
"status": t.status.value,
|
|
277
|
+
"priority": t.priority,
|
|
278
|
+
"prd_id": t.prd_id,
|
|
279
|
+
}
|
|
280
|
+
for t in all_tasks
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
# Capture blockers
|
|
284
|
+
all_blockers = blockers.list_all(workspace, limit=1000)
|
|
285
|
+
snapshot["blockers"] = [
|
|
286
|
+
{
|
|
287
|
+
"id": b.id,
|
|
288
|
+
"question": b.question,
|
|
289
|
+
"answer": b.answer,
|
|
290
|
+
"status": b.status.value,
|
|
291
|
+
"task_id": b.task_id,
|
|
292
|
+
}
|
|
293
|
+
for b in all_blockers
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
# Capture latest PRD reference
|
|
297
|
+
latest_prd = prd.get_latest(workspace)
|
|
298
|
+
if latest_prd:
|
|
299
|
+
snapshot["prd"] = {
|
|
300
|
+
"id": latest_prd.id,
|
|
301
|
+
"title": latest_prd.title,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
# Capture git ref
|
|
305
|
+
if include_git_ref:
|
|
306
|
+
git_ref = _get_git_head(workspace.repo_path)
|
|
307
|
+
if git_ref:
|
|
308
|
+
snapshot["git_ref"] = git_ref
|
|
309
|
+
|
|
310
|
+
# Task counts summary
|
|
311
|
+
counts = tasks.count_by_status(workspace)
|
|
312
|
+
snapshot["summary"] = {
|
|
313
|
+
"total_tasks": len(all_tasks),
|
|
314
|
+
"tasks_by_status": counts,
|
|
315
|
+
"open_blockers": sum(1 for b in all_blockers if b.status.value == "OPEN"),
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return snapshot
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _get_git_head(repo_path: Path) -> Optional[str]:
|
|
322
|
+
"""Get current git HEAD reference."""
|
|
323
|
+
try:
|
|
324
|
+
result = subprocess.run(
|
|
325
|
+
["git", "rev-parse", "HEAD"],
|
|
326
|
+
cwd=repo_path,
|
|
327
|
+
capture_output=True,
|
|
328
|
+
text=True,
|
|
329
|
+
timeout=5, # Prevent hanging on slow/unresponsive git
|
|
330
|
+
)
|
|
331
|
+
if result.returncode == 0:
|
|
332
|
+
return result.stdout.strip()
|
|
333
|
+
except subprocess.TimeoutExpired:
|
|
334
|
+
# Git command took too long, return None gracefully
|
|
335
|
+
return None
|
|
336
|
+
except Exception:
|
|
337
|
+
pass
|
|
338
|
+
return None
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _row_to_checkpoint(row: tuple) -> Checkpoint:
|
|
342
|
+
"""Convert a database row to a Checkpoint object."""
|
|
343
|
+
return Checkpoint(
|
|
344
|
+
id=row[0],
|
|
345
|
+
workspace_id=row[1],
|
|
346
|
+
name=row[2],
|
|
347
|
+
snapshot=json.loads(row[3]) if row[3] else {},
|
|
348
|
+
created_at=datetime.fromisoformat(row[4]),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# ============================================================================
|
|
353
|
+
# Checkpoint Diff (Route Delegation Helper)
|
|
354
|
+
# ============================================================================
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@dataclass
|
|
358
|
+
class TaskDiff:
|
|
359
|
+
"""Difference in a single task between checkpoints."""
|
|
360
|
+
|
|
361
|
+
task_id: str
|
|
362
|
+
title: str
|
|
363
|
+
old_status: Optional[str]
|
|
364
|
+
new_status: Optional[str]
|
|
365
|
+
change_type: str # "added", "removed", "status_changed", "unchanged"
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@dataclass
|
|
369
|
+
class CheckpointDiff:
|
|
370
|
+
"""Difference between two checkpoints.
|
|
371
|
+
|
|
372
|
+
Attributes:
|
|
373
|
+
checkpoint_a: First checkpoint (older)
|
|
374
|
+
checkpoint_b: Second checkpoint (newer)
|
|
375
|
+
task_diffs: List of task differences
|
|
376
|
+
summary: Summary counts of changes
|
|
377
|
+
"""
|
|
378
|
+
|
|
379
|
+
checkpoint_a_id: str
|
|
380
|
+
checkpoint_a_name: str
|
|
381
|
+
checkpoint_b_id: str
|
|
382
|
+
checkpoint_b_name: str
|
|
383
|
+
task_diffs: list[TaskDiff]
|
|
384
|
+
summary: dict[str, int]
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def diff(
|
|
388
|
+
workspace: Workspace,
|
|
389
|
+
checkpoint_id_a: str,
|
|
390
|
+
checkpoint_id_b: str,
|
|
391
|
+
) -> CheckpointDiff:
|
|
392
|
+
"""Compare two checkpoints and return the differences.
|
|
393
|
+
|
|
394
|
+
Compares task statuses between two checkpoints to show what changed.
|
|
395
|
+
|
|
396
|
+
Args:
|
|
397
|
+
workspace: Target workspace
|
|
398
|
+
checkpoint_id_a: First checkpoint ID (typically older)
|
|
399
|
+
checkpoint_id_b: Second checkpoint ID (typically newer)
|
|
400
|
+
|
|
401
|
+
Returns:
|
|
402
|
+
CheckpointDiff with task differences and summary
|
|
403
|
+
|
|
404
|
+
Raises:
|
|
405
|
+
ValueError: If either checkpoint not found
|
|
406
|
+
|
|
407
|
+
Example:
|
|
408
|
+
diff_result = diff(workspace, "abc123", "def456")
|
|
409
|
+
for task_diff in diff_result.task_diffs:
|
|
410
|
+
if task_diff.change_type == "status_changed":
|
|
411
|
+
print(f"{task_diff.title}: {task_diff.old_status} -> {task_diff.new_status}")
|
|
412
|
+
"""
|
|
413
|
+
# Load both checkpoints
|
|
414
|
+
checkpoint_a = get(workspace, checkpoint_id_a)
|
|
415
|
+
if not checkpoint_a:
|
|
416
|
+
raise ValueError(f"Checkpoint not found: {checkpoint_id_a}")
|
|
417
|
+
|
|
418
|
+
checkpoint_b = get(workspace, checkpoint_id_b)
|
|
419
|
+
if not checkpoint_b:
|
|
420
|
+
raise ValueError(f"Checkpoint not found: {checkpoint_id_b}")
|
|
421
|
+
|
|
422
|
+
# Extract task data from snapshots
|
|
423
|
+
tasks_a = {t["id"]: t for t in checkpoint_a.snapshot.get("tasks", [])}
|
|
424
|
+
tasks_b = {t["id"]: t for t in checkpoint_b.snapshot.get("tasks", [])}
|
|
425
|
+
|
|
426
|
+
# Find all task IDs
|
|
427
|
+
all_task_ids = set(tasks_a.keys()) | set(tasks_b.keys())
|
|
428
|
+
|
|
429
|
+
# Compare tasks
|
|
430
|
+
task_diffs = []
|
|
431
|
+
summary = {"added": 0, "removed": 0, "status_changed": 0, "unchanged": 0}
|
|
432
|
+
|
|
433
|
+
for task_id in sorted(all_task_ids):
|
|
434
|
+
task_a = tasks_a.get(task_id)
|
|
435
|
+
task_b = tasks_b.get(task_id)
|
|
436
|
+
|
|
437
|
+
if task_a is None:
|
|
438
|
+
# Task added in checkpoint B
|
|
439
|
+
task_diffs.append(TaskDiff(
|
|
440
|
+
task_id=task_id,
|
|
441
|
+
title=task_b.get("title", "Unknown"),
|
|
442
|
+
old_status=None,
|
|
443
|
+
new_status=task_b.get("status"),
|
|
444
|
+
change_type="added",
|
|
445
|
+
))
|
|
446
|
+
summary["added"] += 1
|
|
447
|
+
|
|
448
|
+
elif task_b is None:
|
|
449
|
+
# Task removed in checkpoint B
|
|
450
|
+
task_diffs.append(TaskDiff(
|
|
451
|
+
task_id=task_id,
|
|
452
|
+
title=task_a.get("title", "Unknown"),
|
|
453
|
+
old_status=task_a.get("status"),
|
|
454
|
+
new_status=None,
|
|
455
|
+
change_type="removed",
|
|
456
|
+
))
|
|
457
|
+
summary["removed"] += 1
|
|
458
|
+
|
|
459
|
+
elif task_a.get("status") != task_b.get("status"):
|
|
460
|
+
# Status changed
|
|
461
|
+
task_diffs.append(TaskDiff(
|
|
462
|
+
task_id=task_id,
|
|
463
|
+
title=task_b.get("title", task_a.get("title", "Unknown")),
|
|
464
|
+
old_status=task_a.get("status"),
|
|
465
|
+
new_status=task_b.get("status"),
|
|
466
|
+
change_type="status_changed",
|
|
467
|
+
))
|
|
468
|
+
summary["status_changed"] += 1
|
|
469
|
+
|
|
470
|
+
else:
|
|
471
|
+
# Unchanged (only include if requested, skip for brevity)
|
|
472
|
+
summary["unchanged"] += 1
|
|
473
|
+
|
|
474
|
+
return CheckpointDiff(
|
|
475
|
+
checkpoint_a_id=checkpoint_a.id,
|
|
476
|
+
checkpoint_a_name=checkpoint_a.name,
|
|
477
|
+
checkpoint_b_id=checkpoint_b.id,
|
|
478
|
+
checkpoint_b_name=checkpoint_b.name,
|
|
479
|
+
task_diffs=task_diffs,
|
|
480
|
+
summary=summary,
|
|
481
|
+
)
|