runtime-memory 3.0.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 (54) hide show
  1. runtime_memory/__init__.py +28 -0
  2. runtime_memory/claude_code/__init__.py +48 -0
  3. runtime_memory/claude_code/commands.py +698 -0
  4. runtime_memory/claude_code/daemon.py +852 -0
  5. runtime_memory/claude_code/hooks.py +722 -0
  6. runtime_memory/cli/__init__.py +8 -0
  7. runtime_memory/cli/main.py +1936 -0
  8. runtime_memory/core/__init__.py +216 -0
  9. runtime_memory/core/config.py +473 -0
  10. runtime_memory/core/embeddings.py +908 -0
  11. runtime_memory/core/engine.py +1007 -0
  12. runtime_memory/core/exceptions.py +547 -0
  13. runtime_memory/core/legacy_env.py +39 -0
  14. runtime_memory/core/logging.py +160 -0
  15. runtime_memory/core/models.py +1051 -0
  16. runtime_memory/core/observability.py +725 -0
  17. runtime_memory/core/paths.py +30 -0
  18. runtime_memory/core/resilience.py +511 -0
  19. runtime_memory/core/retrieval.py +819 -0
  20. runtime_memory/core/storage.py +1105 -0
  21. runtime_memory/extraction/__init__.py +36 -0
  22. runtime_memory/extraction/extractor.py +1143 -0
  23. runtime_memory/hermes/__init__.py +39 -0
  24. runtime_memory/hermes/_base.py +154 -0
  25. runtime_memory/hermes/bridge.py +119 -0
  26. runtime_memory/hermes/plugin.yaml +13 -0
  27. runtime_memory/hermes/provider.py +536 -0
  28. runtime_memory/hermes/tools.py +230 -0
  29. runtime_memory/hermes/trace.py +177 -0
  30. runtime_memory/plugin/__init__.py +646 -0
  31. runtime_memory/sdk/__init__.py +97 -0
  32. runtime_memory/sdk/client.py +1577 -0
  33. runtime_memory/server/__init__.py +75 -0
  34. runtime_memory/server/api.py +1665 -0
  35. runtime_memory/server/mcp.py +1574 -0
  36. runtime_memory/server/static/css/styles.css +1110 -0
  37. runtime_memory/server/static/index.html +264 -0
  38. runtime_memory/server/static/js/api.js +294 -0
  39. runtime_memory/server/static/js/app.js +771 -0
  40. runtime_memory/tasks/__init__.py +114 -0
  41. runtime_memory/tasks/adapter.py +501 -0
  42. runtime_memory/tasks/claude_code_adapter.py +495 -0
  43. runtime_memory/tasks/claude_code_parser.py +339 -0
  44. runtime_memory/tasks/cli_bridge.py +415 -0
  45. runtime_memory/tasks/linking.py +397 -0
  46. runtime_memory/tasks/models.py +520 -0
  47. runtime_memory/tasks/outcomes.py +320 -0
  48. runtime_memory/tasks/parser.py +305 -0
  49. runtime_memory/tasks/unified_adapter.py +661 -0
  50. runtime_memory-3.0.0.dist-info/METADATA +497 -0
  51. runtime_memory-3.0.0.dist-info/RECORD +54 -0
  52. runtime_memory-3.0.0.dist-info/WHEEL +4 -0
  53. runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
  54. runtime_memory-3.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,320 @@
1
+ """Automatic outcome capture for Beads task integration.
2
+
3
+ This module is THE key feature of Beads integration. It automatically
4
+ records outcomes for memories when tasks complete, closing the feedback
5
+ loop without requiring manual /outcome calls.
6
+
7
+ Flow:
8
+ Task "bd-a3f8" marked "done"
9
+ → Find all memories linked to this task
10
+ → Record "worked" outcome for each memory
11
+ → Score boost (+0.2) applied automatically
12
+ → Bad advice naturally sinks, good advice rises
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from typing import TYPE_CHECKING
19
+
20
+ from runtime_memory.core.models import Outcome
21
+ from runtime_memory.tasks.linking import TaskMemoryLinker
22
+ from runtime_memory.tasks.models import (
23
+ CANCELLED_TASK_PENALTY,
24
+ TASK_STATUS_TO_OUTCOME,
25
+ BeadsSyncResult,
26
+ BeadsTask,
27
+ BeadsTaskStatus,
28
+ )
29
+ from runtime_memory.tasks.parser import BeadsParser
30
+
31
+ if TYPE_CHECKING:
32
+ from runtime_memory.core.engine import MemoryEngine
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ class OutcomeCapture:
38
+ """Automatically captures outcomes when Beads tasks complete.
39
+
40
+ This class monitors task status and records outcomes for linked
41
+ memories when tasks reach terminal states (done, cancelled).
42
+
43
+ Example:
44
+ >>> capture = OutcomeCapture(engine, linker, parser)
45
+ >>> result = await capture.on_task_completed("bd-a3f8")
46
+ >>> print(f"Updated {result} memories")
47
+ """
48
+
49
+ def __init__(
50
+ self,
51
+ engine: MemoryEngine,
52
+ linker: TaskMemoryLinker,
53
+ parser: BeadsParser | None = None,
54
+ auto_outcome_enabled: bool = True,
55
+ outcome_on_cancel: bool = False,
56
+ min_confidence_for_outcome: float = 0.0,
57
+ ) -> None:
58
+ """Initialize the outcome capture system.
59
+
60
+ Args:
61
+ engine: The Runtime Memory engine for recording outcomes.
62
+ linker: The task-memory linker for finding linked memories.
63
+ parser: Optional Beads parser for task status lookups.
64
+ auto_outcome_enabled: Whether to automatically record outcomes.
65
+ outcome_on_cancel: Whether to record "failed" on cancelled tasks.
66
+ min_confidence_for_outcome: Minimum memory confidence to auto-record.
67
+ """
68
+ self._engine = engine
69
+ self._linker = linker
70
+ self._parser = parser
71
+ self.auto_outcome_enabled = auto_outcome_enabled
72
+ self.outcome_on_cancel = outcome_on_cancel
73
+ self.min_confidence_for_outcome = min_confidence_for_outcome
74
+
75
+ async def on_task_completed(self, task_id: str) -> int:
76
+ """Handle a task being marked as completed (done).
77
+
78
+ Records "worked" outcome for all unresolved linked memories.
79
+
80
+ Args:
81
+ task_id: The Beads task ID that was completed.
82
+
83
+ Returns:
84
+ Number of memories that had outcomes recorded.
85
+ """
86
+ if not self.auto_outcome_enabled:
87
+ logger.debug(f"Auto-outcome disabled, skipping task {task_id}")
88
+ return 0
89
+
90
+ return await self._record_outcome_for_task(task_id, Outcome.WORKED)
91
+
92
+ async def on_task_failed(self, task_id: str) -> int:
93
+ """Handle a task being marked as failed/cancelled.
94
+
95
+ Records "failed" outcome for all unresolved linked memories
96
+ (if outcome_on_cancel is enabled).
97
+
98
+ Args:
99
+ task_id: The Beads task ID that failed.
100
+
101
+ Returns:
102
+ Number of memories that had outcomes recorded.
103
+ """
104
+ if not self.auto_outcome_enabled:
105
+ logger.debug(f"Auto-outcome disabled, skipping task {task_id}")
106
+ return 0
107
+
108
+ if not self.outcome_on_cancel:
109
+ logger.debug(f"Outcome on cancel disabled, skipping task {task_id}")
110
+ return 0
111
+
112
+ return await self._record_outcome_for_task(task_id, Outcome.FAILED)
113
+
114
+ async def on_task_blocked(self, task_id: str) -> int:
115
+ """Handle a task being marked as blocked.
116
+
117
+ Records "partial" outcome for all unresolved linked memories.
118
+ This indicates the advice was on the right track but couldn't
119
+ fully solve the problem.
120
+
121
+ Args:
122
+ task_id: The Beads task ID that was blocked.
123
+
124
+ Returns:
125
+ Number of memories that had outcomes recorded.
126
+ """
127
+ if not self.auto_outcome_enabled:
128
+ logger.debug(f"Auto-outcome disabled, skipping task {task_id}")
129
+ return 0
130
+
131
+ return await self._record_outcome_for_task(task_id, Outcome.PARTIAL)
132
+
133
+ async def _record_outcome_for_task(
134
+ self,
135
+ task_id: str,
136
+ outcome: Outcome,
137
+ ) -> int:
138
+ """Record outcome for all unresolved memories linked to a task.
139
+
140
+ Args:
141
+ task_id: The Beads task ID.
142
+ outcome: The outcome to record.
143
+
144
+ Returns:
145
+ Number of memories updated.
146
+ """
147
+ # Get unresolved links (memories without outcomes yet)
148
+ links = await self._linker.get_unresolved_links(task_id)
149
+
150
+ if not links:
151
+ logger.debug(f"No unresolved links for task {task_id}")
152
+ return 0
153
+
154
+ # Filter by minimum confidence if specified
155
+ memory_ids = []
156
+ for link in links:
157
+ if self.min_confidence_for_outcome > 0:
158
+ try:
159
+ memory = await self._engine.get(link.memory_id)
160
+ if memory.confidence < self.min_confidence_for_outcome:
161
+ logger.debug(
162
+ f"Skipping memory {link.memory_id} "
163
+ f"(confidence {memory.confidence} < {self.min_confidence_for_outcome})"
164
+ )
165
+ continue
166
+ except Exception:
167
+ # Memory might not exist anymore, skip it
168
+ continue
169
+ memory_ids.append(link.memory_id)
170
+
171
+ if not memory_ids:
172
+ logger.debug(f"No memories passed confidence filter for task {task_id}")
173
+ return 0
174
+
175
+ # Record outcomes in the engine
176
+ try:
177
+ await self._engine.record_outcome(memory_ids, outcome)
178
+ logger.info(
179
+ f"Recorded {outcome.value} for {len(memory_ids)} memories "
180
+ f"linked to task {task_id}"
181
+ )
182
+ except Exception as e:
183
+ logger.error(f"Failed to record outcomes for task {task_id}: {e}")
184
+ return 0
185
+
186
+ # Mark links as having outcomes recorded
187
+ await self._linker.record_task_outcome(task_id, outcome.value)
188
+
189
+ return len(memory_ids)
190
+
191
+ async def process_task_status_change(
192
+ self,
193
+ task_id: str,
194
+ old_status: BeadsTaskStatus,
195
+ new_status: BeadsTaskStatus,
196
+ ) -> int:
197
+ """Process a task status change and record outcomes if appropriate.
198
+
199
+ Args:
200
+ task_id: The Beads task ID.
201
+ old_status: Previous task status.
202
+ new_status: New task status.
203
+
204
+ Returns:
205
+ Number of memories that had outcomes recorded.
206
+ """
207
+ # Only process transitions to terminal states
208
+ if new_status == BeadsTaskStatus.DONE:
209
+ return await self.on_task_completed(task_id)
210
+ elif new_status == BeadsTaskStatus.CANCELLED:
211
+ return await self.on_task_failed(task_id)
212
+ elif new_status == BeadsTaskStatus.BLOCKED:
213
+ return await self.on_task_blocked(task_id)
214
+
215
+ return 0
216
+
217
+ async def sync_completed_tasks(self) -> BeadsSyncResult:
218
+ """Sync outcomes for all completed tasks that have unresolved links.
219
+
220
+ Scans all completed tasks in Beads and records outcomes for any
221
+ that have memories without recorded outcomes.
222
+
223
+ Returns:
224
+ BeadsSyncResult with sync statistics.
225
+ """
226
+ result = BeadsSyncResult()
227
+
228
+ if not self._parser:
229
+ result.errors.append("No parser available for task sync")
230
+ return result
231
+
232
+ if not self._parser.is_available():
233
+ result.warnings.append("Beads directory not found")
234
+ return result
235
+
236
+ # Get all completed tasks
237
+ completed_tasks = self._parser.get_completed_tasks()
238
+ result.tasks_found = len(completed_tasks)
239
+
240
+ for task in completed_tasks:
241
+ try:
242
+ # Check if task has unresolved links
243
+ unresolved = await self._linker.get_unresolved_links(task.id)
244
+ if not unresolved:
245
+ continue
246
+
247
+ # Record outcome based on task status
248
+ if task.status == BeadsTaskStatus.DONE:
249
+ count = await self.on_task_completed(task.id)
250
+ elif task.status == BeadsTaskStatus.CANCELLED:
251
+ count = await self.on_task_failed(task.id)
252
+ else:
253
+ count = 0
254
+
255
+ result.outcomes_recorded += count
256
+ result.tasks_synced += 1
257
+
258
+ except Exception as e:
259
+ result.errors.append(f"Error processing task {task.id}: {e}")
260
+
261
+ return result
262
+
263
+ async def check_and_record(self, task_id: str) -> int:
264
+ """Check task status and record outcome if completed.
265
+
266
+ Convenience method that looks up the task status and calls
267
+ the appropriate outcome handler.
268
+
269
+ Args:
270
+ task_id: The Beads task ID to check.
271
+
272
+ Returns:
273
+ Number of memories that had outcomes recorded.
274
+ """
275
+ if not self._parser:
276
+ logger.warning("No parser available, cannot check task status")
277
+ return 0
278
+
279
+ task = self._parser.get_task(task_id)
280
+ if not task:
281
+ logger.warning(f"Task {task_id} not found")
282
+ return 0
283
+
284
+ if task.status == BeadsTaskStatus.DONE:
285
+ return await self.on_task_completed(task_id)
286
+ elif task.status == BeadsTaskStatus.CANCELLED:
287
+ return await self.on_task_failed(task_id)
288
+ elif task.status == BeadsTaskStatus.BLOCKED:
289
+ return await self.on_task_blocked(task_id)
290
+
291
+ return 0
292
+
293
+
294
+ async def auto_capture_outcome(
295
+ engine: MemoryEngine,
296
+ linker: TaskMemoryLinker,
297
+ task_id: str,
298
+ status: BeadsTaskStatus,
299
+ ) -> int:
300
+ """Convenience function to capture outcome for a task.
301
+
302
+ Args:
303
+ engine: The Runtime Memory engine.
304
+ linker: The task-memory linker.
305
+ task_id: The Beads task ID.
306
+ status: The task's new status.
307
+
308
+ Returns:
309
+ Number of memories that had outcomes recorded.
310
+ """
311
+ capture = OutcomeCapture(engine, linker)
312
+
313
+ if status == BeadsTaskStatus.DONE:
314
+ return await capture.on_task_completed(task_id)
315
+ elif status == BeadsTaskStatus.CANCELLED:
316
+ return await capture.on_task_failed(task_id)
317
+ elif status == BeadsTaskStatus.BLOCKED:
318
+ return await capture.on_task_blocked(task_id)
319
+
320
+ return 0
@@ -0,0 +1,305 @@
1
+ """Beads file parser for reading task data from .beads/ directory.
2
+
3
+ This module handles:
4
+ - Discovery of .beads/ directory
5
+ - Parsing JSONL task files
6
+ - Building task index with parent-child relationships
7
+ - Graceful handling of malformed data
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import logging
14
+ import os
15
+ from pathlib import Path
16
+ from typing import TYPE_CHECKING
17
+
18
+ from runtime_memory.tasks.models import BeadsTask, BeadsTaskStatus
19
+
20
+ if TYPE_CHECKING:
21
+ from collections.abc import Iterator
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ class BeadsDirectoryNotFoundError(Exception):
27
+ """Raised when .beads/ directory cannot be found."""
28
+
29
+ pass
30
+
31
+
32
+ class BeadsParser:
33
+ """Parses Beads task data from .beads/ directory.
34
+
35
+ Supports:
36
+ - JSONL files (one JSON object per line)
37
+ - Directory discovery (walks up from current dir)
38
+ - Environment variable override ($BEADS_DIR)
39
+ - Caching of parsed tasks
40
+
41
+ Example:
42
+ >>> parser = BeadsParser()
43
+ >>> tasks = parser.list_tasks()
44
+ >>> ready = parser.get_ready_tasks()
45
+ """
46
+
47
+ def __init__(self, beads_dir: Path | str | None = None):
48
+ """Initialize the parser.
49
+
50
+ Args:
51
+ beads_dir: Explicit path to .beads/ directory.
52
+ If None, will auto-discover.
53
+ """
54
+ self._beads_dir: Path | None = None
55
+ self._explicit_dir = Path(beads_dir) if beads_dir else None
56
+ self._task_cache: dict[str, BeadsTask] = {}
57
+ self._cache_valid = False
58
+
59
+ @property
60
+ def beads_dir(self) -> Path | None:
61
+ """Get the .beads/ directory path, discovering if needed."""
62
+ if self._beads_dir is None:
63
+ self._beads_dir = self._discover_beads_dir()
64
+ return self._beads_dir
65
+
66
+ def _discover_beads_dir(self) -> Path | None:
67
+ """Discover the .beads/ directory.
68
+
69
+ Search order:
70
+ 1. Explicit path provided to constructor
71
+ 2. $BEADS_DIR environment variable
72
+ 3. Walk up from current directory
73
+
74
+ Returns:
75
+ Path to .beads/ directory or None if not found.
76
+ """
77
+ # 1. Explicit path
78
+ if self._explicit_dir:
79
+ if self._explicit_dir.is_dir():
80
+ logger.debug(f"Using explicit beads dir: {self._explicit_dir}")
81
+ return self._explicit_dir
82
+ logger.warning(f"Explicit beads dir not found: {self._explicit_dir}")
83
+ return None
84
+
85
+ # 2. Environment variable
86
+ env_dir = os.environ.get("BEADS_DIR")
87
+ if env_dir:
88
+ env_path = Path(env_dir)
89
+ if env_path.is_dir():
90
+ logger.debug(f"Using $BEADS_DIR: {env_path}")
91
+ return env_path
92
+ logger.warning(f"$BEADS_DIR not found: {env_path}")
93
+
94
+ # 3. Walk up from current directory
95
+ current = Path.cwd()
96
+ for parent in [current, *current.parents]:
97
+ beads_path = parent / ".beads"
98
+ if beads_path.is_dir():
99
+ logger.debug(f"Found .beads/ at: {beads_path}")
100
+ return beads_path
101
+
102
+ logger.debug("No .beads/ directory found")
103
+ return None
104
+
105
+ def is_available(self) -> bool:
106
+ """Check if Beads is available (directory exists)."""
107
+ return self.beads_dir is not None
108
+
109
+ def invalidate_cache(self) -> None:
110
+ """Invalidate the task cache, forcing a re-parse on next access."""
111
+ self._task_cache.clear()
112
+ self._cache_valid = False
113
+
114
+ def _ensure_cache(self) -> None:
115
+ """Ensure the task cache is populated."""
116
+ if self._cache_valid:
117
+ return
118
+
119
+ self._task_cache.clear()
120
+
121
+ if not self.beads_dir:
122
+ return
123
+
124
+ # Parse all JSONL files in .beads/
125
+ for task in self._parse_all_files():
126
+ self._task_cache[task.id] = task
127
+
128
+ self._cache_valid = True
129
+ logger.debug(f"Cached {len(self._task_cache)} tasks")
130
+
131
+ def _parse_all_files(self) -> Iterator[BeadsTask]:
132
+ """Parse all JSONL files in the .beads/ directory.
133
+
134
+ Yields:
135
+ BeadsTask objects for each valid task entry.
136
+ """
137
+ if not self.beads_dir:
138
+ return
139
+
140
+ # Find all .jsonl files
141
+ jsonl_files = list(self.beads_dir.glob("*.jsonl"))
142
+ logger.debug(f"Found {len(jsonl_files)} JSONL files")
143
+
144
+ for jsonl_file in jsonl_files:
145
+ yield from self._parse_jsonl_file(jsonl_file)
146
+
147
+ def _parse_jsonl_file(self, file_path: Path) -> Iterator[BeadsTask]:
148
+ """Parse a single JSONL file.
149
+
150
+ Args:
151
+ file_path: Path to the JSONL file.
152
+
153
+ Yields:
154
+ BeadsTask objects for each valid line.
155
+ """
156
+ try:
157
+ with open(file_path, encoding="utf-8") as f:
158
+ for line_num, line in enumerate(f, 1):
159
+ line = line.strip()
160
+ if not line:
161
+ continue
162
+
163
+ try:
164
+ data = json.loads(line)
165
+ task = BeadsTask.from_dict(data)
166
+ yield task
167
+ except json.JSONDecodeError as e:
168
+ logger.warning(
169
+ f"Malformed JSON at {file_path}:{line_num}: {e}"
170
+ )
171
+ except (KeyError, ValueError) as e:
172
+ logger.warning(
173
+ f"Invalid task data at {file_path}:{line_num}: {e}"
174
+ )
175
+ except OSError as e:
176
+ logger.error(f"Failed to read {file_path}: {e}")
177
+
178
+ def get_task(self, task_id: str) -> BeadsTask | None:
179
+ """Get a task by ID.
180
+
181
+ Args:
182
+ task_id: The task ID (e.g., "bd-a3f8").
183
+
184
+ Returns:
185
+ The task or None if not found.
186
+ """
187
+ self._ensure_cache()
188
+ return self._task_cache.get(task_id)
189
+
190
+ def list_tasks(
191
+ self,
192
+ status: BeadsTaskStatus | None = None,
193
+ parent_id: str | None = None,
194
+ ) -> list[BeadsTask]:
195
+ """List all tasks, optionally filtered.
196
+
197
+ Args:
198
+ status: Filter by status (e.g., IN_PROGRESS).
199
+ parent_id: Filter by parent task (for subtasks).
200
+
201
+ Returns:
202
+ List of matching tasks.
203
+ """
204
+ self._ensure_cache()
205
+
206
+ tasks = list(self._task_cache.values())
207
+
208
+ if status is not None:
209
+ tasks = [t for t in tasks if t.status == status]
210
+
211
+ if parent_id is not None:
212
+ tasks = [t for t in tasks if t.parent_id == parent_id]
213
+
214
+ return tasks
215
+
216
+ def get_ready_tasks(self) -> list[BeadsTask]:
217
+ """Get tasks that are ready to work on.
218
+
219
+ A task is ready if:
220
+ - Status is PENDING
221
+ - No unresolved dependencies
222
+
223
+ Returns:
224
+ List of ready tasks.
225
+ """
226
+ self._ensure_cache()
227
+
228
+ ready = []
229
+ for task in self._task_cache.values():
230
+ if task.status != BeadsTaskStatus.PENDING:
231
+ continue
232
+
233
+ # Check if all dependencies are done
234
+ deps_resolved = all(
235
+ self._task_cache.get(dep_id, BeadsTask(id=dep_id, title="")).status
236
+ == BeadsTaskStatus.DONE
237
+ for dep_id in task.dependencies
238
+ )
239
+
240
+ if deps_resolved:
241
+ ready.append(task)
242
+
243
+ return ready
244
+
245
+ def get_in_progress_tasks(self) -> list[BeadsTask]:
246
+ """Get tasks currently being worked on.
247
+
248
+ Returns:
249
+ List of in-progress tasks.
250
+ """
251
+ return self.list_tasks(status=BeadsTaskStatus.IN_PROGRESS)
252
+
253
+ def get_subtasks(self, parent_id: str) -> list[BeadsTask]:
254
+ """Get subtasks for a parent task.
255
+
256
+ Args:
257
+ parent_id: The parent task ID.
258
+
259
+ Returns:
260
+ List of subtasks.
261
+ """
262
+ return self.list_tasks(parent_id=parent_id)
263
+
264
+ def get_task_hierarchy(self, task_id: str) -> dict[str, BeadsTask | list]:
265
+ """Get a task with its subtasks in a hierarchical structure.
266
+
267
+ Args:
268
+ task_id: The task ID.
269
+
270
+ Returns:
271
+ Dict with 'task' and 'subtasks' keys.
272
+ """
273
+ task = self.get_task(task_id)
274
+ if not task:
275
+ return {"task": None, "subtasks": []}
276
+
277
+ subtasks = self.get_subtasks(task_id)
278
+ return {"task": task, "subtasks": subtasks}
279
+
280
+ def get_completed_tasks(self) -> list[BeadsTask]:
281
+ """Get tasks that have been completed (done or cancelled).
282
+
283
+ Returns:
284
+ List of completed tasks.
285
+ """
286
+ self._ensure_cache()
287
+ return [t for t in self._task_cache.values() if t.is_completed]
288
+
289
+ def get_blocked_tasks(self) -> list[BeadsTask]:
290
+ """Get tasks that are blocked.
291
+
292
+ Returns:
293
+ List of blocked tasks.
294
+ """
295
+ return self.list_tasks(status=BeadsTaskStatus.BLOCKED)
296
+
297
+ def refresh(self) -> int:
298
+ """Refresh the task cache from disk.
299
+
300
+ Returns:
301
+ Number of tasks loaded.
302
+ """
303
+ self.invalidate_cache()
304
+ self._ensure_cache()
305
+ return len(self._task_cache)