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.
- runtime_memory/__init__.py +28 -0
- runtime_memory/claude_code/__init__.py +48 -0
- runtime_memory/claude_code/commands.py +698 -0
- runtime_memory/claude_code/daemon.py +852 -0
- runtime_memory/claude_code/hooks.py +722 -0
- runtime_memory/cli/__init__.py +8 -0
- runtime_memory/cli/main.py +1936 -0
- runtime_memory/core/__init__.py +216 -0
- runtime_memory/core/config.py +473 -0
- runtime_memory/core/embeddings.py +908 -0
- runtime_memory/core/engine.py +1007 -0
- runtime_memory/core/exceptions.py +547 -0
- runtime_memory/core/legacy_env.py +39 -0
- runtime_memory/core/logging.py +160 -0
- runtime_memory/core/models.py +1051 -0
- runtime_memory/core/observability.py +725 -0
- runtime_memory/core/paths.py +30 -0
- runtime_memory/core/resilience.py +511 -0
- runtime_memory/core/retrieval.py +819 -0
- runtime_memory/core/storage.py +1105 -0
- runtime_memory/extraction/__init__.py +36 -0
- runtime_memory/extraction/extractor.py +1143 -0
- runtime_memory/hermes/__init__.py +39 -0
- runtime_memory/hermes/_base.py +154 -0
- runtime_memory/hermes/bridge.py +119 -0
- runtime_memory/hermes/plugin.yaml +13 -0
- runtime_memory/hermes/provider.py +536 -0
- runtime_memory/hermes/tools.py +230 -0
- runtime_memory/hermes/trace.py +177 -0
- runtime_memory/plugin/__init__.py +646 -0
- runtime_memory/sdk/__init__.py +97 -0
- runtime_memory/sdk/client.py +1577 -0
- runtime_memory/server/__init__.py +75 -0
- runtime_memory/server/api.py +1665 -0
- runtime_memory/server/mcp.py +1574 -0
- runtime_memory/server/static/css/styles.css +1110 -0
- runtime_memory/server/static/index.html +264 -0
- runtime_memory/server/static/js/api.js +294 -0
- runtime_memory/server/static/js/app.js +771 -0
- runtime_memory/tasks/__init__.py +114 -0
- runtime_memory/tasks/adapter.py +501 -0
- runtime_memory/tasks/claude_code_adapter.py +495 -0
- runtime_memory/tasks/claude_code_parser.py +339 -0
- runtime_memory/tasks/cli_bridge.py +415 -0
- runtime_memory/tasks/linking.py +397 -0
- runtime_memory/tasks/models.py +520 -0
- runtime_memory/tasks/outcomes.py +320 -0
- runtime_memory/tasks/parser.py +305 -0
- runtime_memory/tasks/unified_adapter.py +661 -0
- runtime_memory-3.0.0.dist-info/METADATA +497 -0
- runtime_memory-3.0.0.dist-info/RECORD +54 -0
- runtime_memory-3.0.0.dist-info/WHEEL +4 -0
- runtime_memory-3.0.0.dist-info/entry_points.txt +6 -0
- 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
|
+
)
|