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,520 @@
|
|
|
1
|
+
"""Data models for task tracker integration.
|
|
2
|
+
|
|
3
|
+
This module defines:
|
|
4
|
+
- Enums for task statuses (Beads and Claude Code)
|
|
5
|
+
- Dataclasses for tasks, links, and sync results
|
|
6
|
+
- Task source enum for unified tracking
|
|
7
|
+
- Pydantic models for validation
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TaskSource(str, Enum):
|
|
19
|
+
"""Source of task data.
|
|
20
|
+
|
|
21
|
+
Used to identify which system a task originated from.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
BEADS = "beads"
|
|
25
|
+
"""Task from Beads task tracker (.beads/ directory)."""
|
|
26
|
+
|
|
27
|
+
CLAUDE_CODE = "claude_code"
|
|
28
|
+
"""Task from Claude Code todos (~/.claude/todos/)."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BeadsTaskStatus(str, Enum):
|
|
32
|
+
"""Task statuses matching Beads conventions.
|
|
33
|
+
|
|
34
|
+
These map to the standard Beads task lifecycle.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
PENDING = "pending"
|
|
38
|
+
"""Task not yet started."""
|
|
39
|
+
|
|
40
|
+
IN_PROGRESS = "in_progress"
|
|
41
|
+
"""Task is actively being worked on."""
|
|
42
|
+
|
|
43
|
+
DONE = "done"
|
|
44
|
+
"""Task completed successfully."""
|
|
45
|
+
|
|
46
|
+
BLOCKED = "blocked"
|
|
47
|
+
"""Task waiting on a dependency or blocker."""
|
|
48
|
+
|
|
49
|
+
CANCELLED = "cancelled"
|
|
50
|
+
"""Task was abandoned or no longer needed."""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# Mapping from Beads status to Runtime Memory outcome
|
|
54
|
+
# Used when auto-recording outcomes on task completion
|
|
55
|
+
TASK_STATUS_TO_OUTCOME: dict[BeadsTaskStatus, str] = {
|
|
56
|
+
BeadsTaskStatus.DONE: "worked",
|
|
57
|
+
BeadsTaskStatus.CANCELLED: "failed",
|
|
58
|
+
BeadsTaskStatus.BLOCKED: "partial",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Score adjustment for cancelled tasks (less severe than "failed")
|
|
62
|
+
CANCELLED_TASK_PENALTY = -0.1
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class BeadsTask:
|
|
67
|
+
"""Represents a task from the Beads task tracker.
|
|
68
|
+
|
|
69
|
+
This mirrors the Beads JSONL schema for task storage.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
id: Hash-based task ID (e.g., "bd-a3f8")
|
|
73
|
+
title: Short task title
|
|
74
|
+
description: Full task description
|
|
75
|
+
status: Current task status
|
|
76
|
+
parent_id: Parent task ID for subtasks (e.g., "bd-a3f8" for "bd-a3f8.1")
|
|
77
|
+
dependencies: List of task IDs this task is blocked by
|
|
78
|
+
tags: List of tags for categorization
|
|
79
|
+
created_at: When the task was created
|
|
80
|
+
updated_at: When the task was last modified
|
|
81
|
+
metadata: Additional custom fields from Beads
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
id: str
|
|
85
|
+
title: str
|
|
86
|
+
status: BeadsTaskStatus = BeadsTaskStatus.PENDING
|
|
87
|
+
description: str = ""
|
|
88
|
+
parent_id: str | None = None
|
|
89
|
+
dependencies: list[str] = field(default_factory=list)
|
|
90
|
+
tags: list[str] = field(default_factory=list)
|
|
91
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
92
|
+
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
93
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def is_subtask(self) -> bool:
|
|
97
|
+
"""Check if this is a subtask (has a parent)."""
|
|
98
|
+
return self.parent_id is not None
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def is_ready(self) -> bool:
|
|
102
|
+
"""Check if task is ready to work on (no blockers, pending status)."""
|
|
103
|
+
return self.status == BeadsTaskStatus.PENDING and len(self.dependencies) == 0
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def is_completed(self) -> bool:
|
|
107
|
+
"""Check if task has reached a terminal state."""
|
|
108
|
+
return self.status in (BeadsTaskStatus.DONE, BeadsTaskStatus.CANCELLED)
|
|
109
|
+
|
|
110
|
+
def to_dict(self) -> dict[str, Any]:
|
|
111
|
+
"""Convert to dictionary for serialization."""
|
|
112
|
+
return {
|
|
113
|
+
"id": self.id,
|
|
114
|
+
"title": self.title,
|
|
115
|
+
"description": self.description,
|
|
116
|
+
"status": self.status.value,
|
|
117
|
+
"parent_id": self.parent_id,
|
|
118
|
+
"dependencies": self.dependencies,
|
|
119
|
+
"tags": self.tags,
|
|
120
|
+
"created_at": self.created_at.isoformat(),
|
|
121
|
+
"updated_at": self.updated_at.isoformat(),
|
|
122
|
+
"metadata": self.metadata,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def from_dict(cls, data: dict[str, Any]) -> BeadsTask:
|
|
127
|
+
"""Create a BeadsTask from a dictionary (e.g., parsed JSONL)."""
|
|
128
|
+
# Handle status as string or enum
|
|
129
|
+
status = data.get("status", "pending")
|
|
130
|
+
if isinstance(status, str):
|
|
131
|
+
status = BeadsTaskStatus(status)
|
|
132
|
+
|
|
133
|
+
# Parse timestamps
|
|
134
|
+
created_at = data.get("created_at")
|
|
135
|
+
if isinstance(created_at, str):
|
|
136
|
+
created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
|
137
|
+
elif created_at is None:
|
|
138
|
+
created_at = datetime.now(UTC)
|
|
139
|
+
|
|
140
|
+
updated_at = data.get("updated_at")
|
|
141
|
+
if isinstance(updated_at, str):
|
|
142
|
+
updated_at = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
|
|
143
|
+
elif updated_at is None:
|
|
144
|
+
updated_at = datetime.now(UTC)
|
|
145
|
+
|
|
146
|
+
return cls(
|
|
147
|
+
id=data["id"],
|
|
148
|
+
title=data.get("title", ""),
|
|
149
|
+
description=data.get("description", ""),
|
|
150
|
+
status=status,
|
|
151
|
+
parent_id=data.get("parent_id"),
|
|
152
|
+
dependencies=data.get("dependencies", []),
|
|
153
|
+
tags=data.get("tags", []),
|
|
154
|
+
created_at=created_at,
|
|
155
|
+
updated_at=updated_at,
|
|
156
|
+
metadata=data.get("metadata", {}),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# =============================================================================
|
|
161
|
+
# Claude Code Task Models
|
|
162
|
+
# =============================================================================
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class ClaudeCodeTaskStatus(str, Enum):
|
|
166
|
+
"""Task statuses for Claude Code todos.
|
|
167
|
+
|
|
168
|
+
Claude Code uses a simpler status system than Beads.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
PENDING = "pending"
|
|
172
|
+
"""Task not yet started."""
|
|
173
|
+
|
|
174
|
+
IN_PROGRESS = "in_progress"
|
|
175
|
+
"""Task is actively being worked on."""
|
|
176
|
+
|
|
177
|
+
COMPLETED = "completed"
|
|
178
|
+
"""Task completed."""
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# Mapping from Claude Code status to Runtime Memory outcome
|
|
182
|
+
CLAUDE_CODE_STATUS_TO_OUTCOME: dict[ClaudeCodeTaskStatus, str] = {
|
|
183
|
+
ClaudeCodeTaskStatus.COMPLETED: "worked",
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class ClaudeCodeTask:
|
|
189
|
+
"""Represents a task from Claude Code todos.
|
|
190
|
+
|
|
191
|
+
Claude Code stores todos in ~/.claude/todos/ as JSON files.
|
|
192
|
+
Each file is a JSON array of todo objects.
|
|
193
|
+
|
|
194
|
+
Attributes:
|
|
195
|
+
id: Generated task ID (session_id:index or content hash)
|
|
196
|
+
content: Task content/description
|
|
197
|
+
status: Current task status
|
|
198
|
+
active_form: Present continuous form shown in spinner
|
|
199
|
+
session_id: Claude session ID the task belongs to
|
|
200
|
+
agent_id: Agent ID (if task is from a sub-agent)
|
|
201
|
+
index: Index within the session's task list
|
|
202
|
+
file_path: Path to the source file
|
|
203
|
+
created_at: When the task was first seen
|
|
204
|
+
updated_at: When the task was last modified
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
id: str
|
|
208
|
+
content: str
|
|
209
|
+
status: ClaudeCodeTaskStatus = ClaudeCodeTaskStatus.PENDING
|
|
210
|
+
active_form: str = ""
|
|
211
|
+
session_id: str = ""
|
|
212
|
+
agent_id: str = ""
|
|
213
|
+
index: int = 0
|
|
214
|
+
file_path: str = ""
|
|
215
|
+
created_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
216
|
+
updated_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def title(self) -> str:
|
|
220
|
+
"""Get task title (first 80 chars of content for compatibility)."""
|
|
221
|
+
return self.content[:80] + ("..." if len(self.content) > 80 else "")
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def description(self) -> str:
|
|
225
|
+
"""Get full task description (alias for content)."""
|
|
226
|
+
return self.content
|
|
227
|
+
|
|
228
|
+
@property
|
|
229
|
+
def is_completed(self) -> bool:
|
|
230
|
+
"""Check if task has been completed."""
|
|
231
|
+
return self.status == ClaudeCodeTaskStatus.COMPLETED
|
|
232
|
+
|
|
233
|
+
@property
|
|
234
|
+
def is_ready(self) -> bool:
|
|
235
|
+
"""Check if task is ready to work on."""
|
|
236
|
+
return self.status == ClaudeCodeTaskStatus.PENDING
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def source(self) -> TaskSource:
|
|
240
|
+
"""Get the task source."""
|
|
241
|
+
return TaskSource.CLAUDE_CODE
|
|
242
|
+
|
|
243
|
+
def to_dict(self) -> dict[str, Any]:
|
|
244
|
+
"""Convert to dictionary for serialization."""
|
|
245
|
+
return {
|
|
246
|
+
"id": self.id,
|
|
247
|
+
"content": self.content,
|
|
248
|
+
"status": self.status.value,
|
|
249
|
+
"activeForm": self.active_form,
|
|
250
|
+
"session_id": self.session_id,
|
|
251
|
+
"agent_id": self.agent_id,
|
|
252
|
+
"index": self.index,
|
|
253
|
+
"file_path": self.file_path,
|
|
254
|
+
"created_at": self.created_at.isoformat(),
|
|
255
|
+
"updated_at": self.updated_at.isoformat(),
|
|
256
|
+
"source": self.source.value,
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@classmethod
|
|
260
|
+
def from_dict(
|
|
261
|
+
cls,
|
|
262
|
+
data: dict[str, Any],
|
|
263
|
+
session_id: str = "",
|
|
264
|
+
agent_id: str = "",
|
|
265
|
+
index: int = 0,
|
|
266
|
+
file_path: str = "",
|
|
267
|
+
) -> ClaudeCodeTask:
|
|
268
|
+
"""Create a ClaudeCodeTask from a dictionary.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
data: Dictionary with 'content', 'status', 'activeForm' keys.
|
|
272
|
+
session_id: The Claude session ID.
|
|
273
|
+
agent_id: The agent ID.
|
|
274
|
+
index: Index in the task list.
|
|
275
|
+
file_path: Path to the source file.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
ClaudeCodeTask instance.
|
|
279
|
+
"""
|
|
280
|
+
# Handle status mapping
|
|
281
|
+
raw_status = data.get("status", "pending")
|
|
282
|
+
if isinstance(raw_status, str):
|
|
283
|
+
# Map Claude Code statuses to our enum
|
|
284
|
+
status_map = {
|
|
285
|
+
"pending": ClaudeCodeTaskStatus.PENDING,
|
|
286
|
+
"in_progress": ClaudeCodeTaskStatus.IN_PROGRESS,
|
|
287
|
+
"completed": ClaudeCodeTaskStatus.COMPLETED,
|
|
288
|
+
}
|
|
289
|
+
status = status_map.get(raw_status, ClaudeCodeTaskStatus.PENDING)
|
|
290
|
+
else:
|
|
291
|
+
status = raw_status
|
|
292
|
+
|
|
293
|
+
content = data.get("content", "")
|
|
294
|
+
|
|
295
|
+
# Generate task ID from session_id and index
|
|
296
|
+
task_id = f"cc-{session_id[:8]}-{index}" if session_id else f"cc-{index}"
|
|
297
|
+
|
|
298
|
+
# Parse timestamps if present
|
|
299
|
+
created_at = data.get("created_at")
|
|
300
|
+
if isinstance(created_at, str):
|
|
301
|
+
created_at = datetime.fromisoformat(created_at.replace("Z", "+00:00"))
|
|
302
|
+
elif created_at is None:
|
|
303
|
+
created_at = datetime.now(UTC)
|
|
304
|
+
|
|
305
|
+
updated_at = data.get("updated_at")
|
|
306
|
+
if isinstance(updated_at, str):
|
|
307
|
+
updated_at = datetime.fromisoformat(updated_at.replace("Z", "+00:00"))
|
|
308
|
+
elif updated_at is None:
|
|
309
|
+
updated_at = datetime.now(UTC)
|
|
310
|
+
|
|
311
|
+
return cls(
|
|
312
|
+
id=task_id,
|
|
313
|
+
content=content,
|
|
314
|
+
status=status,
|
|
315
|
+
active_form=data.get("activeForm", ""),
|
|
316
|
+
session_id=session_id,
|
|
317
|
+
agent_id=agent_id,
|
|
318
|
+
index=index,
|
|
319
|
+
file_path=file_path,
|
|
320
|
+
created_at=created_at,
|
|
321
|
+
updated_at=updated_at,
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
# =============================================================================
|
|
326
|
+
# Unified Task Type
|
|
327
|
+
# =============================================================================
|
|
328
|
+
|
|
329
|
+
# Type alias for any task type
|
|
330
|
+
Task = BeadsTask | ClaudeCodeTask
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@dataclass
|
|
334
|
+
class TaskMemoryLink:
|
|
335
|
+
"""Links a memory to a task it was used for.
|
|
336
|
+
|
|
337
|
+
This enables automatic outcome capture: when a task completes,
|
|
338
|
+
all linked memories can have their outcomes recorded.
|
|
339
|
+
|
|
340
|
+
Attributes:
|
|
341
|
+
task_id: Beads task ID
|
|
342
|
+
memory_id: Runtime Memory memory ID
|
|
343
|
+
used_at: When the memory was surfaced for this task
|
|
344
|
+
outcome: Outcome recorded when task completed (None until then)
|
|
345
|
+
context: Optional context about how memory was used
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
task_id: str
|
|
349
|
+
memory_id: str
|
|
350
|
+
used_at: datetime = field(default_factory=lambda: datetime.now(UTC))
|
|
351
|
+
outcome: str | None = None
|
|
352
|
+
context: str | None = None
|
|
353
|
+
|
|
354
|
+
def to_dict(self) -> dict[str, Any]:
|
|
355
|
+
"""Convert to dictionary for storage."""
|
|
356
|
+
return {
|
|
357
|
+
"task_id": self.task_id,
|
|
358
|
+
"memory_id": self.memory_id,
|
|
359
|
+
"used_at": self.used_at.isoformat(),
|
|
360
|
+
"outcome": self.outcome,
|
|
361
|
+
"context": self.context,
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def from_dict(cls, data: dict[str, Any]) -> TaskMemoryLink:
|
|
366
|
+
"""Create from dictionary."""
|
|
367
|
+
used_at = data.get("used_at")
|
|
368
|
+
if isinstance(used_at, str):
|
|
369
|
+
used_at = datetime.fromisoformat(used_at.replace("Z", "+00:00"))
|
|
370
|
+
elif used_at is None:
|
|
371
|
+
used_at = datetime.now(UTC)
|
|
372
|
+
|
|
373
|
+
return cls(
|
|
374
|
+
task_id=data["task_id"],
|
|
375
|
+
memory_id=data["memory_id"],
|
|
376
|
+
used_at=used_at,
|
|
377
|
+
outcome=data.get("outcome"),
|
|
378
|
+
context=data.get("context"),
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@dataclass
|
|
383
|
+
class BeadsSyncResult:
|
|
384
|
+
"""Result of syncing with Beads task tracker.
|
|
385
|
+
|
|
386
|
+
Attributes:
|
|
387
|
+
tasks_found: Total tasks discovered in .beads/
|
|
388
|
+
tasks_synced: Tasks that were processed
|
|
389
|
+
outcomes_recorded: Number of automatic outcome recordings
|
|
390
|
+
memories_linked: Number of new task-memory links created
|
|
391
|
+
errors: List of error messages encountered
|
|
392
|
+
warnings: List of warning messages
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
tasks_found: int = 0
|
|
396
|
+
tasks_synced: int = 0
|
|
397
|
+
outcomes_recorded: int = 0
|
|
398
|
+
memories_linked: int = 0
|
|
399
|
+
errors: list[str] = field(default_factory=list)
|
|
400
|
+
warnings: list[str] = field(default_factory=list)
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def success(self) -> bool:
|
|
404
|
+
"""Check if sync completed without errors."""
|
|
405
|
+
return len(self.errors) == 0
|
|
406
|
+
|
|
407
|
+
def to_dict(self) -> dict[str, Any]:
|
|
408
|
+
"""Convert to dictionary for JSON serialization."""
|
|
409
|
+
return {
|
|
410
|
+
"tasks_found": self.tasks_found,
|
|
411
|
+
"tasks_synced": self.tasks_synced,
|
|
412
|
+
"outcomes_recorded": self.outcomes_recorded,
|
|
413
|
+
"memories_linked": self.memories_linked,
|
|
414
|
+
"errors": self.errors,
|
|
415
|
+
"warnings": self.warnings,
|
|
416
|
+
"success": self.success,
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
@dataclass
|
|
421
|
+
class TaskSyncResult:
|
|
422
|
+
"""Result of syncing with any task tracker.
|
|
423
|
+
|
|
424
|
+
Generic version that works with both Beads and Claude Code tasks.
|
|
425
|
+
|
|
426
|
+
Attributes:
|
|
427
|
+
source: Which task system was synced
|
|
428
|
+
tasks_found: Total tasks discovered
|
|
429
|
+
tasks_synced: Tasks that were processed
|
|
430
|
+
outcomes_recorded: Number of automatic outcome recordings
|
|
431
|
+
memories_linked: Number of new task-memory links created
|
|
432
|
+
errors: List of error messages encountered
|
|
433
|
+
warnings: List of warning messages
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
source: TaskSource = TaskSource.BEADS
|
|
437
|
+
tasks_found: int = 0
|
|
438
|
+
tasks_synced: int = 0
|
|
439
|
+
outcomes_recorded: int = 0
|
|
440
|
+
memories_linked: int = 0
|
|
441
|
+
errors: list[str] = field(default_factory=list)
|
|
442
|
+
warnings: list[str] = field(default_factory=list)
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def success(self) -> bool:
|
|
446
|
+
"""Check if sync completed without errors."""
|
|
447
|
+
return len(self.errors) == 0
|
|
448
|
+
|
|
449
|
+
def to_dict(self) -> dict[str, Any]:
|
|
450
|
+
"""Convert to dictionary for JSON serialization."""
|
|
451
|
+
return {
|
|
452
|
+
"source": self.source.value,
|
|
453
|
+
"tasks_found": self.tasks_found,
|
|
454
|
+
"tasks_synced": self.tasks_synced,
|
|
455
|
+
"outcomes_recorded": self.outcomes_recorded,
|
|
456
|
+
"memories_linked": self.memories_linked,
|
|
457
|
+
"errors": self.errors,
|
|
458
|
+
"warnings": self.warnings,
|
|
459
|
+
"success": self.success,
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@dataclass
|
|
464
|
+
class TaskContext:
|
|
465
|
+
"""Unified context combining task info and relevant memories.
|
|
466
|
+
|
|
467
|
+
Used for context injection at the start of a task-focused session.
|
|
468
|
+
Works with both Beads and Claude Code tasks.
|
|
469
|
+
|
|
470
|
+
Attributes:
|
|
471
|
+
task: The current task (Beads or Claude Code)
|
|
472
|
+
memories: Relevant memories for this task
|
|
473
|
+
formatted: Pre-formatted string for injection
|
|
474
|
+
source: Which task system the task is from
|
|
475
|
+
"""
|
|
476
|
+
|
|
477
|
+
task: Task # BeadsTask | ClaudeCodeTask
|
|
478
|
+
memories: list[Any] # list[Memory] - Any to avoid circular import
|
|
479
|
+
formatted: str = ""
|
|
480
|
+
source: TaskSource = TaskSource.BEADS
|
|
481
|
+
|
|
482
|
+
def to_markdown(self) -> str:
|
|
483
|
+
"""Format as markdown for context injection."""
|
|
484
|
+
# Get title - works for both task types
|
|
485
|
+
title = self.task.title if hasattr(self.task, "title") else str(self.task)
|
|
486
|
+
|
|
487
|
+
lines = [
|
|
488
|
+
f"## Current Task: {title}",
|
|
489
|
+
"",
|
|
490
|
+
f"**Status:** {self.task.status.value}",
|
|
491
|
+
f"**ID:** {self.task.id}",
|
|
492
|
+
f"**Source:** {self.source.value}",
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
# Get description - works differently for each type
|
|
496
|
+
description = ""
|
|
497
|
+
if isinstance(self.task, BeadsTask):
|
|
498
|
+
description = self.task.description
|
|
499
|
+
elif isinstance(self.task, ClaudeCodeTask):
|
|
500
|
+
description = self.task.content
|
|
501
|
+
|
|
502
|
+
if description:
|
|
503
|
+
lines.extend(["", "### Description", description])
|
|
504
|
+
|
|
505
|
+
# Dependencies - only for Beads tasks
|
|
506
|
+
if isinstance(self.task, BeadsTask) and self.task.dependencies:
|
|
507
|
+
lines.extend(
|
|
508
|
+
["", "### Blocked By", *[f"- {dep}" for dep in self.task.dependencies]]
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
if self.memories:
|
|
512
|
+
lines.extend(["", "### Relevant Memories", ""])
|
|
513
|
+
for mem in self.memories:
|
|
514
|
+
content = getattr(mem, "content", str(mem))
|
|
515
|
+
category = getattr(mem, "category", "unknown")
|
|
516
|
+
if hasattr(category, "value"):
|
|
517
|
+
category = category.value
|
|
518
|
+
lines.append(f"- **[{category}]** {content[:200]}...")
|
|
519
|
+
|
|
520
|
+
return "\n".join(lines)
|