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,661 @@
1
+ """Unified task adapter combining Beads and Claude Code task systems.
2
+
3
+ This module provides a single interface that works with both task systems,
4
+ auto-detecting which systems are available and providing unified access.
5
+
6
+ Example:
7
+ >>> from runtime_memory.tasks import UnifiedTaskAdapter
8
+ >>> adapter = UnifiedTaskAdapter(engine)
9
+ >>> await adapter.initialize()
10
+ >>>
11
+ >>> # List all tasks from all sources
12
+ >>> tasks = adapter.list_tasks()
13
+ >>>
14
+ >>> # Sync outcomes for all completed tasks
15
+ >>> results = await adapter.sync_all()
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+ from typing import TYPE_CHECKING
24
+
25
+ from runtime_memory.tasks.adapter import BeadsAdapter, NullBeadsAdapter, create_adapter
26
+ from runtime_memory.tasks.claude_code_adapter import (
27
+ ClaudeCodeAdapter,
28
+ NullClaudeCodeAdapter,
29
+ create_claude_code_adapter,
30
+ )
31
+ from runtime_memory.tasks.models import (
32
+ BeadsTask,
33
+ BeadsTaskStatus,
34
+ ClaudeCodeTask,
35
+ ClaudeCodeTaskStatus,
36
+ Task,
37
+ TaskContext,
38
+ TaskSource,
39
+ TaskSyncResult,
40
+ )
41
+
42
+ if TYPE_CHECKING:
43
+ from runtime_memory.core.engine import MemoryEngine
44
+ from runtime_memory.core.models import Memory
45
+
46
+ logger = logging.getLogger(__name__)
47
+
48
+
49
+ @dataclass
50
+ class UnifiedTask:
51
+ """Wrapper providing unified interface for any task type.
52
+
53
+ Normalizes differences between Beads and Claude Code tasks.
54
+ """
55
+
56
+ task: Task # BeadsTask | ClaudeCodeTask
57
+ source: TaskSource
58
+
59
+ @property
60
+ def id(self) -> str:
61
+ return self.task.id
62
+
63
+ @property
64
+ def title(self) -> str:
65
+ return self.task.title
66
+
67
+ @property
68
+ def description(self) -> str:
69
+ if isinstance(self.task, BeadsTask):
70
+ return self.task.description
71
+ return self.task.content
72
+
73
+ @property
74
+ def status(self) -> str:
75
+ """Get normalized status string."""
76
+ return self.task.status.value
77
+
78
+ @property
79
+ def is_completed(self) -> bool:
80
+ return self.task.is_completed
81
+
82
+ @property
83
+ def is_ready(self) -> bool:
84
+ return self.task.is_ready
85
+
86
+ def to_dict(self) -> dict:
87
+ """Convert to dictionary with source info."""
88
+ data = self.task.to_dict()
89
+ data["source"] = self.source.value
90
+ return data
91
+
92
+
93
+ @dataclass
94
+ class UnifiedSyncResult:
95
+ """Combined sync results from all task sources.
96
+
97
+ Attributes:
98
+ results: Individual results by source
99
+ total_tasks_found: Total across all sources
100
+ total_outcomes_recorded: Total outcomes recorded
101
+ errors: All errors from all sources
102
+ """
103
+
104
+ results: dict[TaskSource, TaskSyncResult] = field(default_factory=dict)
105
+
106
+ @property
107
+ def total_tasks_found(self) -> int:
108
+ return sum(r.tasks_found for r in self.results.values())
109
+
110
+ @property
111
+ def total_tasks_synced(self) -> int:
112
+ return sum(r.tasks_synced for r in self.results.values())
113
+
114
+ @property
115
+ def total_outcomes_recorded(self) -> int:
116
+ return sum(r.outcomes_recorded for r in self.results.values())
117
+
118
+ @property
119
+ def errors(self) -> list[str]:
120
+ errors = []
121
+ for source, result in self.results.items():
122
+ for error in result.errors:
123
+ errors.append(f"[{source.value}] {error}")
124
+ return errors
125
+
126
+ @property
127
+ def success(self) -> bool:
128
+ return len(self.errors) == 0
129
+
130
+ def to_dict(self) -> dict:
131
+ return {
132
+ "results": {k.value: v.to_dict() for k, v in self.results.items()},
133
+ "total_tasks_found": self.total_tasks_found,
134
+ "total_tasks_synced": self.total_tasks_synced,
135
+ "total_outcomes_recorded": self.total_outcomes_recorded,
136
+ "errors": self.errors,
137
+ "success": self.success,
138
+ }
139
+
140
+
141
+ class UnifiedTaskAdapter:
142
+ """Unified adapter that works with both Beads and Claude Code tasks.
143
+
144
+ Provides a single interface for:
145
+ - Listing tasks from all available sources
146
+ - Linking memories to tasks
147
+ - Recording outcomes
148
+ - Generating unified context
149
+
150
+ Auto-detects which task systems are available.
151
+ """
152
+
153
+ def __init__(
154
+ self,
155
+ engine: MemoryEngine,
156
+ beads_dir: Path | str | None = None,
157
+ todos_dir: Path | str | None = None,
158
+ auto_outcome_enabled: bool = True,
159
+ ) -> None:
160
+ """Initialize the unified adapter.
161
+
162
+ Args:
163
+ engine: The Runtime Memory engine.
164
+ beads_dir: Explicit path to .beads/ directory.
165
+ todos_dir: Explicit path to ~/.claude/todos/ directory.
166
+ auto_outcome_enabled: Whether to auto-record outcomes.
167
+ """
168
+ self._engine = engine
169
+ self._beads_dir = beads_dir
170
+ self._todos_dir = todos_dir
171
+ self._auto_outcome_enabled = auto_outcome_enabled
172
+
173
+ # Adapters will be created on initialize()
174
+ self._beads_adapter: BeadsAdapter | NullBeadsAdapter | None = None
175
+ self._claude_adapter: ClaudeCodeAdapter | NullClaudeCodeAdapter | None = None
176
+
177
+ self._initialized = False
178
+
179
+ async def initialize(self) -> None:
180
+ """Initialize all adapters.
181
+
182
+ Creates adapters for available task systems.
183
+ Must be called before using the unified adapter.
184
+ """
185
+ if self._initialized:
186
+ return
187
+
188
+ # Create Beads adapter
189
+ self._beads_adapter = create_adapter(
190
+ self._engine,
191
+ self._beads_dir,
192
+ auto_outcome_enabled=self._auto_outcome_enabled,
193
+ )
194
+ if isinstance(self._beads_adapter, BeadsAdapter):
195
+ await self._beads_adapter.initialize()
196
+
197
+ # Create Claude Code adapter
198
+ self._claude_adapter = create_claude_code_adapter(
199
+ self._engine,
200
+ self._todos_dir,
201
+ auto_outcome_enabled=self._auto_outcome_enabled,
202
+ )
203
+ if isinstance(self._claude_adapter, ClaudeCodeAdapter):
204
+ await self._claude_adapter.initialize()
205
+
206
+ self._initialized = True
207
+ logger.debug(
208
+ f"UnifiedTaskAdapter initialized: beads={self._beads_adapter.is_available}, "
209
+ f"claude_code={self._claude_adapter.is_available}"
210
+ )
211
+
212
+ def _ensure_initialized(self) -> None:
213
+ """Ensure the adapter is initialized."""
214
+ if not self._initialized:
215
+ raise RuntimeError(
216
+ "UnifiedTaskAdapter not initialized. Call initialize() first."
217
+ )
218
+
219
+ @property
220
+ def beads_available(self) -> bool:
221
+ """Check if Beads is available."""
222
+ return self._beads_adapter is not None and self._beads_adapter.is_available
223
+
224
+ @property
225
+ def claude_code_available(self) -> bool:
226
+ """Check if Claude Code todos are available."""
227
+ return self._claude_adapter is not None and self._claude_adapter.is_available
228
+
229
+ @property
230
+ def available_sources(self) -> list[TaskSource]:
231
+ """Get list of available task sources."""
232
+ sources = []
233
+ if self.beads_available:
234
+ sources.append(TaskSource.BEADS)
235
+ if self.claude_code_available:
236
+ sources.append(TaskSource.CLAUDE_CODE)
237
+ return sources
238
+
239
+ # =========================================================================
240
+ # Task Operations
241
+ # =========================================================================
242
+
243
+ def get_task(
244
+ self,
245
+ task_id: str,
246
+ source: TaskSource | None = None,
247
+ ) -> UnifiedTask | None:
248
+ """Get a task by ID.
249
+
250
+ Args:
251
+ task_id: The task ID.
252
+ source: Which source to check. If None, checks all sources.
253
+
254
+ Returns:
255
+ UnifiedTask wrapper or None if not found.
256
+ """
257
+ self._ensure_initialized()
258
+
259
+ # Check Beads
260
+ if source in (None, TaskSource.BEADS) and self.beads_available:
261
+ task = self._beads_adapter.get_task(task_id)
262
+ if task:
263
+ return UnifiedTask(task=task, source=TaskSource.BEADS)
264
+
265
+ # Check Claude Code
266
+ if source in (None, TaskSource.CLAUDE_CODE) and self.claude_code_available:
267
+ task = self._claude_adapter.get_task(task_id)
268
+ if task:
269
+ return UnifiedTask(task=task, source=TaskSource.CLAUDE_CODE)
270
+
271
+ return None
272
+
273
+ def list_tasks(
274
+ self,
275
+ source: TaskSource | None = None,
276
+ status: str | None = None,
277
+ ) -> list[UnifiedTask]:
278
+ """List all tasks, optionally filtered.
279
+
280
+ Args:
281
+ source: Filter by source. If None, includes all sources.
282
+ status: Filter by status (normalized: "pending", "in_progress", "done"/"completed").
283
+
284
+ Returns:
285
+ List of UnifiedTask wrappers.
286
+ """
287
+ self._ensure_initialized()
288
+
289
+ tasks = []
290
+
291
+ # Get Beads tasks
292
+ if source in (None, TaskSource.BEADS) and self.beads_available:
293
+ beads_status = None
294
+ if status:
295
+ # Map normalized status to Beads status
296
+ status_map = {
297
+ "pending": BeadsTaskStatus.PENDING,
298
+ "in_progress": BeadsTaskStatus.IN_PROGRESS,
299
+ "done": BeadsTaskStatus.DONE,
300
+ "completed": BeadsTaskStatus.DONE,
301
+ "blocked": BeadsTaskStatus.BLOCKED,
302
+ "cancelled": BeadsTaskStatus.CANCELLED,
303
+ }
304
+ beads_status = status_map.get(status.lower())
305
+
306
+ for task in self._beads_adapter.list_tasks(status=beads_status):
307
+ tasks.append(UnifiedTask(task=task, source=TaskSource.BEADS))
308
+
309
+ # Get Claude Code tasks
310
+ if source in (None, TaskSource.CLAUDE_CODE) and self.claude_code_available:
311
+ cc_status = None
312
+ if status:
313
+ # Map normalized status to Claude Code status
314
+ status_map = {
315
+ "pending": ClaudeCodeTaskStatus.PENDING,
316
+ "in_progress": ClaudeCodeTaskStatus.IN_PROGRESS,
317
+ "done": ClaudeCodeTaskStatus.COMPLETED,
318
+ "completed": ClaudeCodeTaskStatus.COMPLETED,
319
+ }
320
+ cc_status = status_map.get(status.lower())
321
+
322
+ for task in self._claude_adapter.list_tasks(status=cc_status):
323
+ tasks.append(UnifiedTask(task=task, source=TaskSource.CLAUDE_CODE))
324
+
325
+ return tasks
326
+
327
+ def get_ready_tasks(
328
+ self,
329
+ source: TaskSource | None = None,
330
+ ) -> list[UnifiedTask]:
331
+ """Get tasks that are ready to work on.
332
+
333
+ Args:
334
+ source: Filter by source. If None, includes all sources.
335
+
336
+ Returns:
337
+ List of ready tasks.
338
+ """
339
+ self._ensure_initialized()
340
+
341
+ tasks = []
342
+
343
+ if source in (None, TaskSource.BEADS) and self.beads_available:
344
+ for task in self._beads_adapter.get_ready_tasks():
345
+ tasks.append(UnifiedTask(task=task, source=TaskSource.BEADS))
346
+
347
+ if source in (None, TaskSource.CLAUDE_CODE) and self.claude_code_available:
348
+ for task in self._claude_adapter.get_ready_tasks():
349
+ tasks.append(UnifiedTask(task=task, source=TaskSource.CLAUDE_CODE))
350
+
351
+ return tasks
352
+
353
+ def get_current_task(
354
+ self,
355
+ source: TaskSource | None = None,
356
+ ) -> UnifiedTask | None:
357
+ """Get the currently active task (in_progress).
358
+
359
+ Args:
360
+ source: Which source to check first. If None, prefers Claude Code.
361
+
362
+ Returns:
363
+ The in-progress task, or None if no task is active.
364
+ """
365
+ self._ensure_initialized()
366
+
367
+ # Check Claude Code first (more likely to be active during Claude sessions)
368
+ if source in (None, TaskSource.CLAUDE_CODE) and self.claude_code_available:
369
+ task = self._claude_adapter.get_current_task()
370
+ if task:
371
+ return UnifiedTask(task=task, source=TaskSource.CLAUDE_CODE)
372
+
373
+ # Check Beads
374
+ if source in (None, TaskSource.BEADS) and self.beads_available:
375
+ task = self._beads_adapter.get_current_task()
376
+ if task:
377
+ return UnifiedTask(task=task, source=TaskSource.BEADS)
378
+
379
+ return None
380
+
381
+ # =========================================================================
382
+ # Memory Linking
383
+ # =========================================================================
384
+
385
+ async def link_memory_to_task(
386
+ self,
387
+ task_id: str,
388
+ memory_id: str,
389
+ source: TaskSource | None = None,
390
+ context: str | None = None,
391
+ ) -> None:
392
+ """Link a memory to a task.
393
+
394
+ Args:
395
+ task_id: The task ID.
396
+ memory_id: The Runtime Memory memory ID.
397
+ source: Which adapter to use. Auto-detects if None.
398
+ context: Optional context about how memory was used.
399
+ """
400
+ self._ensure_initialized()
401
+
402
+ # Auto-detect source from task ID prefix
403
+ if source is None:
404
+ if task_id.startswith("cc-"):
405
+ source = TaskSource.CLAUDE_CODE
406
+ elif task_id.startswith("bd-"):
407
+ source = TaskSource.BEADS
408
+ else:
409
+ # Try both
410
+ source = TaskSource.BEADS if self.beads_available else TaskSource.CLAUDE_CODE
411
+
412
+ if source == TaskSource.BEADS and self.beads_available:
413
+ await self._beads_adapter.link_memory_to_task(task_id, memory_id, context)
414
+ elif source == TaskSource.CLAUDE_CODE and self.claude_code_available:
415
+ await self._claude_adapter.link_memory_to_task(task_id, memory_id, context)
416
+
417
+ async def get_task_memories(
418
+ self,
419
+ task_id: str,
420
+ source: TaskSource | None = None,
421
+ ) -> list[Memory]:
422
+ """Get all memories linked to a task.
423
+
424
+ Args:
425
+ task_id: The task ID.
426
+ source: Which adapter to use. Auto-detects if None.
427
+
428
+ Returns:
429
+ List of Memory objects linked to the task.
430
+ """
431
+ self._ensure_initialized()
432
+
433
+ # Auto-detect source from task ID prefix
434
+ if source is None:
435
+ if task_id.startswith("cc-"):
436
+ source = TaskSource.CLAUDE_CODE
437
+ elif task_id.startswith("bd-"):
438
+ source = TaskSource.BEADS
439
+
440
+ if source == TaskSource.BEADS and self.beads_available:
441
+ return await self._beads_adapter.get_task_memories(task_id)
442
+ elif source == TaskSource.CLAUDE_CODE and self.claude_code_available:
443
+ return await self._claude_adapter.get_task_memories(task_id)
444
+
445
+ return []
446
+
447
+ # =========================================================================
448
+ # Outcome Capture
449
+ # =========================================================================
450
+
451
+ async def on_task_completed(
452
+ self,
453
+ task_id: str,
454
+ source: TaskSource | None = None,
455
+ ) -> int:
456
+ """Handle a task being marked as completed.
457
+
458
+ Args:
459
+ task_id: The task ID.
460
+ source: Which adapter to use. Auto-detects if None.
461
+
462
+ Returns:
463
+ Number of memories that had outcomes recorded.
464
+ """
465
+ self._ensure_initialized()
466
+
467
+ # Auto-detect source from task ID prefix
468
+ if source is None:
469
+ if task_id.startswith("cc-"):
470
+ source = TaskSource.CLAUDE_CODE
471
+ elif task_id.startswith("bd-"):
472
+ source = TaskSource.BEADS
473
+
474
+ if source == TaskSource.BEADS and self.beads_available:
475
+ return await self._beads_adapter.on_task_done(task_id)
476
+ elif source == TaskSource.CLAUDE_CODE and self.claude_code_available:
477
+ return await self._claude_adapter.on_task_completed(task_id)
478
+
479
+ return 0
480
+
481
+ async def sync(
482
+ self,
483
+ source: TaskSource | None = None,
484
+ ) -> TaskSyncResult | UnifiedSyncResult:
485
+ """Sync outcomes for completed tasks.
486
+
487
+ Args:
488
+ source: Which source to sync. If None, syncs all sources.
489
+
490
+ Returns:
491
+ TaskSyncResult if single source, UnifiedSyncResult if all sources.
492
+ """
493
+ self._ensure_initialized()
494
+
495
+ if source == TaskSource.BEADS:
496
+ return await self._beads_adapter.sync()
497
+ elif source == TaskSource.CLAUDE_CODE:
498
+ return await self._claude_adapter.sync()
499
+
500
+ # Sync all sources
501
+ result = UnifiedSyncResult()
502
+
503
+ if self.beads_available:
504
+ result.results[TaskSource.BEADS] = await self._beads_adapter.sync()
505
+
506
+ if self.claude_code_available:
507
+ result.results[TaskSource.CLAUDE_CODE] = await self._claude_adapter.sync()
508
+
509
+ return result
510
+
511
+ async def sync_all(self) -> UnifiedSyncResult:
512
+ """Sync outcomes for all completed tasks from all sources.
513
+
514
+ Returns:
515
+ UnifiedSyncResult with combined statistics.
516
+ """
517
+ result = await self.sync(source=None)
518
+ if isinstance(result, UnifiedSyncResult):
519
+ return result
520
+ # Convert single result to unified
521
+ unified = UnifiedSyncResult()
522
+ unified.results[result.source] = result
523
+ return unified
524
+
525
+ # =========================================================================
526
+ # Context Generation
527
+ # =========================================================================
528
+
529
+ async def get_unified_context(
530
+ self,
531
+ task_id: str | None = None,
532
+ source: TaskSource | None = None,
533
+ max_memories: int = 10,
534
+ ) -> TaskContext | None:
535
+ """Get unified context combining task info and relevant memories.
536
+
537
+ Args:
538
+ task_id: The task ID. If None, uses current task.
539
+ source: Which source. Auto-detects if None.
540
+ max_memories: Maximum number of memories to include.
541
+
542
+ Returns:
543
+ TaskContext object or None if no task found.
544
+ """
545
+ self._ensure_initialized()
546
+
547
+ # Auto-detect source from task ID or find current task
548
+ if task_id:
549
+ if task_id.startswith("cc-"):
550
+ source = TaskSource.CLAUDE_CODE
551
+ elif task_id.startswith("bd-"):
552
+ source = TaskSource.BEADS
553
+
554
+ if source == TaskSource.BEADS and self.beads_available:
555
+ return await self._beads_adapter.get_unified_context(task_id, max_memories)
556
+ elif source == TaskSource.CLAUDE_CODE and self.claude_code_available:
557
+ return await self._claude_adapter.get_unified_context(task_id, max_memories)
558
+
559
+ # No source specified, try current task from any source
560
+ current = self.get_current_task()
561
+ if current:
562
+ if current.source == TaskSource.BEADS:
563
+ return await self._beads_adapter.get_unified_context(
564
+ current.id, max_memories
565
+ )
566
+ else:
567
+ return await self._claude_adapter.get_unified_context(
568
+ current.id, max_memories
569
+ )
570
+
571
+ return None
572
+
573
+ async def get_context_for_injection(
574
+ self,
575
+ task_id: str | None = None,
576
+ source: TaskSource | None = None,
577
+ max_memories: int = 10,
578
+ ) -> str:
579
+ """Get formatted context string for injection into prompts.
580
+
581
+ Args:
582
+ task_id: The task ID. If None, uses current task.
583
+ source: Which source. Auto-detects if None.
584
+ max_memories: Maximum number of memories to include.
585
+
586
+ Returns:
587
+ Formatted markdown string for context injection.
588
+ """
589
+ context = await self.get_unified_context(task_id, source, max_memories)
590
+ if not context:
591
+ return ""
592
+ return context.formatted
593
+
594
+ # =========================================================================
595
+ # Statistics
596
+ # =========================================================================
597
+
598
+ async def get_stats(self) -> dict:
599
+ """Get statistics about all task integrations.
600
+
601
+ Returns:
602
+ Dict with statistics from all sources.
603
+ """
604
+ self._ensure_initialized()
605
+
606
+ stats = {
607
+ "available_sources": [s.value for s in self.available_sources],
608
+ "beads": {},
609
+ "claude_code": {},
610
+ }
611
+
612
+ if self.beads_available:
613
+ stats["beads"] = await self._beads_adapter.get_stats()
614
+
615
+ if self.claude_code_available:
616
+ stats["claude_code"] = await self._claude_adapter.get_stats()
617
+
618
+ return stats
619
+
620
+ def refresh(self) -> dict[TaskSource, int]:
621
+ """Refresh task caches from disk.
622
+
623
+ Returns:
624
+ Dict mapping source to number of tasks loaded.
625
+ """
626
+ self._ensure_initialized()
627
+
628
+ counts = {}
629
+
630
+ if self.beads_available:
631
+ counts[TaskSource.BEADS] = self._beads_adapter.refresh()
632
+
633
+ if self.claude_code_available:
634
+ counts[TaskSource.CLAUDE_CODE] = self._claude_adapter.refresh()
635
+
636
+ return counts
637
+
638
+
639
+ def create_unified_adapter(
640
+ engine: MemoryEngine,
641
+ beads_dir: Path | str | None = None,
642
+ todos_dir: Path | str | None = None,
643
+ **kwargs,
644
+ ) -> UnifiedTaskAdapter:
645
+ """Factory function to create a unified adapter.
646
+
647
+ Args:
648
+ engine: The Runtime Memory engine.
649
+ beads_dir: Explicit path to .beads/ directory.
650
+ todos_dir: Explicit path to ~/.claude/todos/ directory.
651
+ **kwargs: Additional arguments.
652
+
653
+ Returns:
654
+ UnifiedTaskAdapter instance.
655
+ """
656
+ return UnifiedTaskAdapter(
657
+ engine,
658
+ beads_dir=beads_dir,
659
+ todos_dir=todos_dir,
660
+ **kwargs,
661
+ )