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.
Files changed (197) hide show
  1. codeframe/__init__.py +11 -0
  2. codeframe/__main__.py +20 -0
  3. codeframe/adapters/__init__.py +5 -0
  4. codeframe/adapters/e2b/__init__.py +13 -0
  5. codeframe/adapters/e2b/adapter.py +342 -0
  6. codeframe/adapters/e2b/budget.py +71 -0
  7. codeframe/adapters/e2b/credential_scanner.py +134 -0
  8. codeframe/adapters/llm/__init__.py +92 -0
  9. codeframe/adapters/llm/anthropic.py +414 -0
  10. codeframe/adapters/llm/base.py +444 -0
  11. codeframe/adapters/llm/mock.py +281 -0
  12. codeframe/adapters/llm/openai.py +483 -0
  13. codeframe/agents/__init__.py +8 -0
  14. codeframe/agents/dependency_resolver.py +714 -0
  15. codeframe/auth/__init__.py +16 -0
  16. codeframe/auth/api_key_router.py +238 -0
  17. codeframe/auth/api_keys.py +156 -0
  18. codeframe/auth/dependencies.py +358 -0
  19. codeframe/auth/manager.py +178 -0
  20. codeframe/auth/models.py +30 -0
  21. codeframe/auth/router.py +93 -0
  22. codeframe/auth/schemas.py +15 -0
  23. codeframe/auth/scopes.py +53 -0
  24. codeframe/cli/__init__.py +12 -0
  25. codeframe/cli/__main__.py +20 -0
  26. codeframe/cli/api_client.py +275 -0
  27. codeframe/cli/app.py +5688 -0
  28. codeframe/cli/auth.py +122 -0
  29. codeframe/cli/auth_commands.py +958 -0
  30. codeframe/cli/commands/__init__.py +5 -0
  31. codeframe/cli/config_commands.py +79 -0
  32. codeframe/cli/dashboard_commands.py +67 -0
  33. codeframe/cli/engines_commands.py +205 -0
  34. codeframe/cli/env_commands.py +409 -0
  35. codeframe/cli/helpers.py +56 -0
  36. codeframe/cli/hooks_commands.py +208 -0
  37. codeframe/cli/import_commands.py +129 -0
  38. codeframe/cli/pr_commands.py +549 -0
  39. codeframe/cli/proof_commands.py +415 -0
  40. codeframe/cli/stats_commands.py +311 -0
  41. codeframe/cli/telemetry_runtime.py +153 -0
  42. codeframe/cli/validators.py +123 -0
  43. codeframe/config/rate_limits.py +165 -0
  44. codeframe/core/__init__.py +15 -0
  45. codeframe/core/adapters/__init__.py +43 -0
  46. codeframe/core/adapters/agent_adapter.py +114 -0
  47. codeframe/core/adapters/builtin.py +326 -0
  48. codeframe/core/adapters/claude_code.py +62 -0
  49. codeframe/core/adapters/codex.py +393 -0
  50. codeframe/core/adapters/git_utils.py +40 -0
  51. codeframe/core/adapters/kilocode.py +126 -0
  52. codeframe/core/adapters/opencode.py +48 -0
  53. codeframe/core/adapters/streaming_chat.py +483 -0
  54. codeframe/core/adapters/subprocess_adapter.py +213 -0
  55. codeframe/core/adapters/verification_wrapper.py +269 -0
  56. codeframe/core/agent.py +2183 -0
  57. codeframe/core/agents_config.py +569 -0
  58. codeframe/core/api_key_service.py +211 -0
  59. codeframe/core/artifacts.py +428 -0
  60. codeframe/core/blocker_detection.py +218 -0
  61. codeframe/core/blockers.py +433 -0
  62. codeframe/core/checkpoints.py +481 -0
  63. codeframe/core/conductor.py +2255 -0
  64. codeframe/core/config.py +827 -0
  65. codeframe/core/config_watcher.py +268 -0
  66. codeframe/core/context.py +542 -0
  67. codeframe/core/context_packager.py +234 -0
  68. codeframe/core/credentials.py +735 -0
  69. codeframe/core/dependency_analyzer.py +229 -0
  70. codeframe/core/dependency_graph.py +290 -0
  71. codeframe/core/diagnostic_agent.py +712 -0
  72. codeframe/core/diagnostics.py +616 -0
  73. codeframe/core/editor.py +556 -0
  74. codeframe/core/engine_registry.py +256 -0
  75. codeframe/core/engine_stats.py +231 -0
  76. codeframe/core/environment.py +697 -0
  77. codeframe/core/events.py +375 -0
  78. codeframe/core/executor.py +1005 -0
  79. codeframe/core/fix_tracker.py +480 -0
  80. codeframe/core/gates.py +1322 -0
  81. codeframe/core/git.py +477 -0
  82. codeframe/core/github_connect_service.py +178 -0
  83. codeframe/core/github_integration_config.py +118 -0
  84. codeframe/core/github_issues_service.py +449 -0
  85. codeframe/core/hooks.py +184 -0
  86. codeframe/core/importers/__init__.py +1 -0
  87. codeframe/core/importers/ralph.py +540 -0
  88. codeframe/core/installer.py +650 -0
  89. codeframe/core/models.py +1026 -0
  90. codeframe/core/notifications_config.py +183 -0
  91. codeframe/core/planner.py +437 -0
  92. codeframe/core/prd.py +670 -0
  93. codeframe/core/prd_discovery.py +1118 -0
  94. codeframe/core/prd_stress_test.py +499 -0
  95. codeframe/core/progress.py +126 -0
  96. codeframe/core/proof/__init__.py +34 -0
  97. codeframe/core/proof/capture.py +79 -0
  98. codeframe/core/proof/evidence.py +56 -0
  99. codeframe/core/proof/ledger.py +574 -0
  100. codeframe/core/proof/models.py +162 -0
  101. codeframe/core/proof/obligations.py +103 -0
  102. codeframe/core/proof/runner.py +233 -0
  103. codeframe/core/proof/scope.py +81 -0
  104. codeframe/core/proof/stubs.py +156 -0
  105. codeframe/core/quick_fixes.py +558 -0
  106. codeframe/core/react_agent.py +1650 -0
  107. codeframe/core/reconciliation.py +183 -0
  108. codeframe/core/replay.py +788 -0
  109. codeframe/core/review.py +285 -0
  110. codeframe/core/runtime.py +1134 -0
  111. codeframe/core/sandbox/__init__.py +27 -0
  112. codeframe/core/sandbox/context.py +98 -0
  113. codeframe/core/sandbox/worktree.py +20 -0
  114. codeframe/core/schedule.py +396 -0
  115. codeframe/core/stall_detector.py +71 -0
  116. codeframe/core/stall_monitor.py +134 -0
  117. codeframe/core/state_machine.py +121 -0
  118. codeframe/core/streaming.py +502 -0
  119. codeframe/core/task_tree.py +400 -0
  120. codeframe/core/tasks.py +1022 -0
  121. codeframe/core/telemetry.py +232 -0
  122. codeframe/core/templates.py +221 -0
  123. codeframe/core/tools.py +942 -0
  124. codeframe/core/workspace.py +887 -0
  125. codeframe/core/worktrees.py +276 -0
  126. codeframe/git/__init__.py +5 -0
  127. codeframe/git/github_integration.py +505 -0
  128. codeframe/lib/__init__.py +0 -0
  129. codeframe/lib/audit_logger.py +248 -0
  130. codeframe/lib/metrics_tracker.py +800 -0
  131. codeframe/lib/quality/__init__.py +7 -0
  132. codeframe/lib/quality/complexity_analyzer.py +316 -0
  133. codeframe/lib/quality/owasp_patterns.py +284 -0
  134. codeframe/lib/quality/security_scanner.py +250 -0
  135. codeframe/lib/rate_limiter.py +312 -0
  136. codeframe/notifications/__init__.py +0 -0
  137. codeframe/notifications/webhook.py +380 -0
  138. codeframe/planning/__init__.py +30 -0
  139. codeframe/planning/issue_generator.py +219 -0
  140. codeframe/planning/prd_template_functions.py +137 -0
  141. codeframe/planning/prd_templates.py +975 -0
  142. codeframe/planning/task_scheduler.py +511 -0
  143. codeframe/planning/task_templates.py +533 -0
  144. codeframe/platform_store/__init__.py +5 -0
  145. codeframe/platform_store/database.py +277 -0
  146. codeframe/platform_store/repositories/__init__.py +24 -0
  147. codeframe/platform_store/repositories/api_key_repository.py +245 -0
  148. codeframe/platform_store/repositories/audit_repository.py +67 -0
  149. codeframe/platform_store/repositories/base.py +295 -0
  150. codeframe/platform_store/repositories/interactive_sessions.py +165 -0
  151. codeframe/platform_store/repositories/token_repository.py +598 -0
  152. codeframe/platform_store/repositories/workspace_registry_repository.py +175 -0
  153. codeframe/platform_store/schema_manager.py +321 -0
  154. codeframe/templates/AGENTS.md.default +94 -0
  155. codeframe/tui/__init__.py +5 -0
  156. codeframe/tui/app.py +256 -0
  157. codeframe/tui/data_service.py +103 -0
  158. codeframe/ui/__init__.py +0 -0
  159. codeframe/ui/dependencies.py +103 -0
  160. codeframe/ui/models.py +999 -0
  161. codeframe/ui/response_models.py +201 -0
  162. codeframe/ui/routers/__init__.py +5 -0
  163. codeframe/ui/routers/_helpers.py +29 -0
  164. codeframe/ui/routers/batches_v2.py +315 -0
  165. codeframe/ui/routers/blockers_v2.py +320 -0
  166. codeframe/ui/routers/checkpoints_v2.py +310 -0
  167. codeframe/ui/routers/costs_v2.py +322 -0
  168. codeframe/ui/routers/diagnose_v2.py +225 -0
  169. codeframe/ui/routers/discovery_v2.py +417 -0
  170. codeframe/ui/routers/environment_v2.py +284 -0
  171. codeframe/ui/routers/events_v2.py +75 -0
  172. codeframe/ui/routers/gates_v2.py +166 -0
  173. codeframe/ui/routers/git_v2.py +284 -0
  174. codeframe/ui/routers/github_integrations_v2.py +532 -0
  175. codeframe/ui/routers/interactive_sessions_v2.py +238 -0
  176. codeframe/ui/routers/pr_v2.py +709 -0
  177. codeframe/ui/routers/prd_v2.py +695 -0
  178. codeframe/ui/routers/proof_v2.py +755 -0
  179. codeframe/ui/routers/review_v2.py +360 -0
  180. codeframe/ui/routers/schedule_v2.py +214 -0
  181. codeframe/ui/routers/session_chat_ws.py +354 -0
  182. codeframe/ui/routers/settings_v2.py +562 -0
  183. codeframe/ui/routers/streaming_v2.py +155 -0
  184. codeframe/ui/routers/tasks_v2.py +1098 -0
  185. codeframe/ui/routers/templates_v2.py +232 -0
  186. codeframe/ui/routers/terminal_ws.py +267 -0
  187. codeframe/ui/routers/workspace_v2.py +527 -0
  188. codeframe/ui/server.py +568 -0
  189. codeframe/ui/shared.py +241 -0
  190. codeframe/workspace/__init__.py +5 -0
  191. codeframe/workspace/manager.py +249 -0
  192. codeframe_ai-0.9.0.dist-info/METADATA +517 -0
  193. codeframe_ai-0.9.0.dist-info/RECORD +197 -0
  194. codeframe_ai-0.9.0.dist-info/WHEEL +5 -0
  195. codeframe_ai-0.9.0.dist-info/entry_points.txt +3 -0
  196. codeframe_ai-0.9.0.dist-info/licenses/LICENSE +661 -0
  197. 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
+ )