claude-memory-layer 1.0.0

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 (127) hide show
  1. package/.claude-plugin/commands/memory-forget.md +42 -0
  2. package/.claude-plugin/commands/memory-history.md +34 -0
  3. package/.claude-plugin/commands/memory-import.md +56 -0
  4. package/.claude-plugin/commands/memory-list.md +37 -0
  5. package/.claude-plugin/commands/memory-search.md +36 -0
  6. package/.claude-plugin/commands/memory-stats.md +34 -0
  7. package/.claude-plugin/hooks.json +59 -0
  8. package/.claude-plugin/plugin.json +24 -0
  9. package/.history/package_20260201112328.json +45 -0
  10. package/.history/package_20260201113602.json +45 -0
  11. package/.history/package_20260201113713.json +45 -0
  12. package/.history/package_20260201114110.json +45 -0
  13. package/Memo.txt +558 -0
  14. package/README.md +520 -0
  15. package/context.md +636 -0
  16. package/dist/.claude-plugin/commands/memory-forget.md +42 -0
  17. package/dist/.claude-plugin/commands/memory-history.md +34 -0
  18. package/dist/.claude-plugin/commands/memory-import.md +56 -0
  19. package/dist/.claude-plugin/commands/memory-list.md +37 -0
  20. package/dist/.claude-plugin/commands/memory-search.md +36 -0
  21. package/dist/.claude-plugin/commands/memory-stats.md +34 -0
  22. package/dist/.claude-plugin/hooks.json +59 -0
  23. package/dist/.claude-plugin/plugin.json +24 -0
  24. package/dist/cli/index.js +3539 -0
  25. package/dist/cli/index.js.map +7 -0
  26. package/dist/core/index.js +4408 -0
  27. package/dist/core/index.js.map +7 -0
  28. package/dist/hooks/session-end.js +2971 -0
  29. package/dist/hooks/session-end.js.map +7 -0
  30. package/dist/hooks/session-start.js +2969 -0
  31. package/dist/hooks/session-start.js.map +7 -0
  32. package/dist/hooks/stop.js +3123 -0
  33. package/dist/hooks/stop.js.map +7 -0
  34. package/dist/hooks/user-prompt-submit.js +2960 -0
  35. package/dist/hooks/user-prompt-submit.js.map +7 -0
  36. package/dist/services/memory-service.js +2931 -0
  37. package/dist/services/memory-service.js.map +7 -0
  38. package/package.json +45 -0
  39. package/plan.md +1642 -0
  40. package/scripts/build.ts +102 -0
  41. package/spec.md +624 -0
  42. package/specs/citations-system/context.md +243 -0
  43. package/specs/citations-system/plan.md +495 -0
  44. package/specs/citations-system/spec.md +371 -0
  45. package/specs/endless-mode/context.md +305 -0
  46. package/specs/endless-mode/plan.md +620 -0
  47. package/specs/endless-mode/spec.md +455 -0
  48. package/specs/entity-edge-model/context.md +401 -0
  49. package/specs/entity-edge-model/plan.md +459 -0
  50. package/specs/entity-edge-model/spec.md +391 -0
  51. package/specs/evidence-aligner-v2/context.md +401 -0
  52. package/specs/evidence-aligner-v2/plan.md +303 -0
  53. package/specs/evidence-aligner-v2/spec.md +312 -0
  54. package/specs/mcp-desktop-integration/context.md +278 -0
  55. package/specs/mcp-desktop-integration/plan.md +550 -0
  56. package/specs/mcp-desktop-integration/spec.md +494 -0
  57. package/specs/post-tool-use-hook/context.md +319 -0
  58. package/specs/post-tool-use-hook/plan.md +469 -0
  59. package/specs/post-tool-use-hook/spec.md +364 -0
  60. package/specs/private-tags/context.md +288 -0
  61. package/specs/private-tags/plan.md +412 -0
  62. package/specs/private-tags/spec.md +345 -0
  63. package/specs/progressive-disclosure/context.md +346 -0
  64. package/specs/progressive-disclosure/plan.md +663 -0
  65. package/specs/progressive-disclosure/spec.md +415 -0
  66. package/specs/task-entity-system/context.md +297 -0
  67. package/specs/task-entity-system/plan.md +301 -0
  68. package/specs/task-entity-system/spec.md +314 -0
  69. package/specs/vector-outbox-v2/context.md +470 -0
  70. package/specs/vector-outbox-v2/plan.md +562 -0
  71. package/specs/vector-outbox-v2/spec.md +466 -0
  72. package/specs/web-viewer-ui/context.md +384 -0
  73. package/specs/web-viewer-ui/plan.md +797 -0
  74. package/specs/web-viewer-ui/spec.md +516 -0
  75. package/src/cli/index.ts +570 -0
  76. package/src/core/canonical-key.ts +186 -0
  77. package/src/core/citation-generator.ts +63 -0
  78. package/src/core/consolidated-store.ts +279 -0
  79. package/src/core/consolidation-worker.ts +384 -0
  80. package/src/core/context-formatter.ts +276 -0
  81. package/src/core/continuity-manager.ts +336 -0
  82. package/src/core/edge-repo.ts +324 -0
  83. package/src/core/embedder.ts +124 -0
  84. package/src/core/entity-repo.ts +342 -0
  85. package/src/core/event-store.ts +672 -0
  86. package/src/core/evidence-aligner.ts +635 -0
  87. package/src/core/graduation.ts +365 -0
  88. package/src/core/index.ts +32 -0
  89. package/src/core/matcher.ts +210 -0
  90. package/src/core/metadata-extractor.ts +203 -0
  91. package/src/core/privacy/filter.ts +179 -0
  92. package/src/core/privacy/index.ts +20 -0
  93. package/src/core/privacy/tag-parser.ts +145 -0
  94. package/src/core/progressive-retriever.ts +415 -0
  95. package/src/core/retriever.ts +235 -0
  96. package/src/core/task/blocker-resolver.ts +325 -0
  97. package/src/core/task/index.ts +9 -0
  98. package/src/core/task/task-matcher.ts +238 -0
  99. package/src/core/task/task-projector.ts +345 -0
  100. package/src/core/task/task-resolver.ts +414 -0
  101. package/src/core/types.ts +841 -0
  102. package/src/core/vector-outbox.ts +295 -0
  103. package/src/core/vector-store.ts +182 -0
  104. package/src/core/vector-worker.ts +488 -0
  105. package/src/core/working-set-store.ts +244 -0
  106. package/src/hooks/post-tool-use.ts +127 -0
  107. package/src/hooks/session-end.ts +78 -0
  108. package/src/hooks/session-start.ts +57 -0
  109. package/src/hooks/stop.ts +78 -0
  110. package/src/hooks/user-prompt-submit.ts +54 -0
  111. package/src/mcp/handlers.ts +212 -0
  112. package/src/mcp/index.ts +47 -0
  113. package/src/mcp/tools.ts +78 -0
  114. package/src/server/api/citations.ts +101 -0
  115. package/src/server/api/events.ts +101 -0
  116. package/src/server/api/index.ts +18 -0
  117. package/src/server/api/search.ts +98 -0
  118. package/src/server/api/sessions.ts +111 -0
  119. package/src/server/api/stats.ts +97 -0
  120. package/src/server/index.ts +91 -0
  121. package/src/services/memory-service.ts +626 -0
  122. package/src/services/session-history-importer.ts +367 -0
  123. package/tests/canonical-key.test.ts +101 -0
  124. package/tests/evidence-aligner.test.ts +152 -0
  125. package/tests/matcher.test.ts +112 -0
  126. package/tsconfig.json +24 -0
  127. package/vitest.config.ts +15 -0
@@ -0,0 +1,414 @@
1
+ /**
2
+ * Task Resolver - Process extracted task entries and emit task events
3
+ * AXIOMMIND: Task state via event fold, no direct updates
4
+ */
5
+
6
+ import { Database } from 'duckdb';
7
+ import { randomUUID } from 'crypto';
8
+ import type {
9
+ Entity,
10
+ TaskStatus,
11
+ TaskPriority,
12
+ BlockerRef,
13
+ BlockerMode
14
+ } from '../types.js';
15
+ import { makeEntityCanonicalKey, makeTaskEventDedupeKey } from '../canonical-key.js';
16
+ import { TaskMatcher } from './task-matcher.js';
17
+ import { BlockerResolver } from './blocker-resolver.js';
18
+
19
+ export interface ExtractedTask {
20
+ title: string;
21
+ status?: TaskStatus;
22
+ priority?: TaskPriority;
23
+ blockedBy?: string[];
24
+ description?: string;
25
+ project?: string;
26
+ }
27
+
28
+ export interface TaskResolverConfig {
29
+ sessionId: string;
30
+ project?: string;
31
+ evidenceAligned?: boolean;
32
+ }
33
+
34
+ // Valid status transitions
35
+ const VALID_TRANSITIONS: Record<TaskStatus, TaskStatus[]> = {
36
+ pending: ['in_progress', 'cancelled'],
37
+ in_progress: ['blocked', 'done', 'cancelled'],
38
+ blocked: ['in_progress', 'done', 'cancelled'],
39
+ done: [], // Terminal state
40
+ cancelled: [] // Terminal state
41
+ };
42
+
43
+ export class TaskResolver {
44
+ private taskMatcher: TaskMatcher;
45
+ private blockerResolver: BlockerResolver;
46
+
47
+ constructor(
48
+ private db: Database,
49
+ private config: TaskResolverConfig
50
+ ) {
51
+ this.taskMatcher = new TaskMatcher(db);
52
+ this.blockerResolver = new BlockerResolver(db, { project: config.project });
53
+ }
54
+
55
+ /**
56
+ * Process extracted task entry
57
+ * 1. Find or create task entity
58
+ * 2. Emit status/priority change events if needed
59
+ * 3. Process blockers
60
+ */
61
+ async processTask(extracted: ExtractedTask, sourceEntryId?: string): Promise<{
62
+ taskId: string;
63
+ isNew: boolean;
64
+ events: string[];
65
+ }> {
66
+ const events: string[] = [];
67
+
68
+ // Step 1: Find existing task or create new
69
+ const { task, isNew, eventId: createEventId } = await this.findOrCreateTask(extracted);
70
+
71
+ if (isNew && createEventId) {
72
+ events.push(createEventId);
73
+ }
74
+
75
+ // Step 2: Handle status changes
76
+ if (extracted.status) {
77
+ const statusEvent = await this.handleStatusChange(task, extracted.status);
78
+ if (statusEvent) {
79
+ events.push(statusEvent);
80
+ }
81
+ }
82
+
83
+ // Step 3: Handle priority changes
84
+ if (extracted.priority) {
85
+ const priorityEvent = await this.handlePriorityChange(task, extracted.priority);
86
+ if (priorityEvent) {
87
+ events.push(priorityEvent);
88
+ }
89
+ }
90
+
91
+ // Step 4: Handle blockers
92
+ if (extracted.blockedBy && extracted.blockedBy.length > 0) {
93
+ const blockerEvent = await this.handleBlockers(
94
+ task,
95
+ extracted.blockedBy,
96
+ sourceEntryId
97
+ );
98
+ if (blockerEvent) {
99
+ events.push(blockerEvent);
100
+ }
101
+ } else if (extracted.status === 'blocked') {
102
+ // Status is blocked but no blockers provided
103
+ // Create unknown placeholder
104
+ const blockerEvent = await this.handleUnknownBlocker(task);
105
+ if (blockerEvent) {
106
+ events.push(blockerEvent);
107
+ }
108
+ }
109
+
110
+ return {
111
+ taskId: task.entityId,
112
+ isNew,
113
+ events
114
+ };
115
+ }
116
+
117
+ /**
118
+ * Find existing task or create new one
119
+ */
120
+ private async findOrCreateTask(extracted: ExtractedTask): Promise<{
121
+ task: Entity;
122
+ isNew: boolean;
123
+ eventId?: string;
124
+ }> {
125
+ // Try to find existing task
126
+ const matchResult = await this.taskMatcher.match(extracted.title, extracted.project);
127
+
128
+ if (matchResult.confidence === 'high' && matchResult.match) {
129
+ return {
130
+ task: matchResult.match,
131
+ isNew: false
132
+ };
133
+ }
134
+
135
+ // Create new task
136
+ const taskId = randomUUID();
137
+ const canonicalKey = makeEntityCanonicalKey('task', extracted.title, {
138
+ project: extracted.project
139
+ });
140
+
141
+ // Correct initial status: never start as 'done'
142
+ let initialStatus = extracted.status ?? 'pending';
143
+ if (initialStatus === 'done') {
144
+ initialStatus = 'in_progress'; // Correct: can't start as done
145
+ }
146
+
147
+ const now = new Date();
148
+
149
+ const currentJson = {
150
+ status: initialStatus,
151
+ priority: extracted.priority ?? 'medium',
152
+ description: extracted.description,
153
+ project: extracted.project ?? this.config.project
154
+ };
155
+
156
+ // Insert entity
157
+ await this.db.run(
158
+ `INSERT INTO entities (
159
+ entity_id, entity_type, canonical_key, title, stage, status,
160
+ current_json, title_norm, search_text, created_at, updated_at
161
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
162
+ [
163
+ taskId,
164
+ 'task',
165
+ canonicalKey,
166
+ extracted.title,
167
+ 'raw',
168
+ 'active',
169
+ JSON.stringify(currentJson),
170
+ extracted.title.toLowerCase().trim(),
171
+ `${extracted.title} ${extracted.description ?? ''}`,
172
+ now.toISOString(),
173
+ now.toISOString()
174
+ ]
175
+ );
176
+
177
+ // Create alias
178
+ await this.db.run(
179
+ `INSERT INTO entity_aliases (entity_type, canonical_key, entity_id, is_primary)
180
+ VALUES (?, ?, ?, TRUE)
181
+ ON CONFLICT (entity_type, canonical_key) DO NOTHING`,
182
+ ['task', canonicalKey, taskId]
183
+ );
184
+
185
+ // Emit task_created event
186
+ const eventId = await this.emitTaskEvent('task_created', {
187
+ taskId,
188
+ title: extracted.title,
189
+ canonicalKey,
190
+ initialStatus,
191
+ priority: extracted.priority ?? 'medium',
192
+ description: extracted.description,
193
+ project: extracted.project ?? this.config.project
194
+ });
195
+
196
+ // Return created entity
197
+ const task: Entity = {
198
+ entityId: taskId,
199
+ entityType: 'task',
200
+ canonicalKey,
201
+ title: extracted.title,
202
+ stage: 'raw',
203
+ status: 'active',
204
+ currentJson,
205
+ titleNorm: extracted.title.toLowerCase().trim(),
206
+ searchText: `${extracted.title} ${extracted.description ?? ''}`,
207
+ createdAt: now,
208
+ updatedAt: now
209
+ };
210
+
211
+ return { task, isNew: true, eventId };
212
+ }
213
+
214
+ /**
215
+ * Handle task status change
216
+ */
217
+ private async handleStatusChange(
218
+ task: Entity,
219
+ newStatus: TaskStatus
220
+ ): Promise<string | null> {
221
+ const currentJson = task.currentJson as { status: TaskStatus };
222
+ const currentStatus = currentJson.status;
223
+
224
+ if (currentStatus === newStatus) {
225
+ return null; // No change
226
+ }
227
+
228
+ // Validate transition
229
+ const validNextStates = VALID_TRANSITIONS[currentStatus] ?? [];
230
+ if (!validNextStates.includes(newStatus)) {
231
+ // Invalid transition - emit rejection event
232
+ return this.emitTaskEvent('task_transition_rejected', {
233
+ taskId: task.entityId,
234
+ fromStatus: currentStatus,
235
+ toStatus: newStatus,
236
+ reason: `Invalid transition from ${currentStatus} to ${newStatus}`
237
+ });
238
+ }
239
+
240
+ // Emit status change event
241
+ const eventId = await this.emitTaskEvent('task_status_changed', {
242
+ taskId: task.entityId,
243
+ fromStatus: currentStatus,
244
+ toStatus: newStatus
245
+ });
246
+
247
+ // Update entity (projector will do this, but we update for immediate consistency)
248
+ await this.db.run(
249
+ `UPDATE entities
250
+ SET current_json = json_set(current_json, '$.status', ?),
251
+ updated_at = ?
252
+ WHERE entity_id = ?`,
253
+ [newStatus, new Date().toISOString(), task.entityId]
254
+ );
255
+
256
+ return eventId;
257
+ }
258
+
259
+ /**
260
+ * Handle task priority change
261
+ */
262
+ private async handlePriorityChange(
263
+ task: Entity,
264
+ newPriority: TaskPriority
265
+ ): Promise<string | null> {
266
+ const currentJson = task.currentJson as { priority?: TaskPriority };
267
+ const currentPriority = currentJson.priority ?? 'medium';
268
+
269
+ if (currentPriority === newPriority) {
270
+ return null; // No change
271
+ }
272
+
273
+ // Emit priority change event
274
+ const eventId = await this.emitTaskEvent('task_priority_changed', {
275
+ taskId: task.entityId,
276
+ fromPriority: currentPriority,
277
+ toPriority: newPriority
278
+ });
279
+
280
+ // Update entity
281
+ await this.db.run(
282
+ `UPDATE entities
283
+ SET current_json = json_set(current_json, '$.priority', ?),
284
+ updated_at = ?
285
+ WHERE entity_id = ?`,
286
+ [newPriority, new Date().toISOString(), task.entityId]
287
+ );
288
+
289
+ return eventId;
290
+ }
291
+
292
+ /**
293
+ * Handle blockers
294
+ */
295
+ private async handleBlockers(
296
+ task: Entity,
297
+ blockedByTexts: string[],
298
+ sourceEntryId?: string
299
+ ): Promise<string | null> {
300
+ // Resolve blocker texts to entity refs
301
+ const blockerRefs = await this.blockerResolver.resolveBlockers(blockedByTexts);
302
+
303
+ // Determine mode based on evidence alignment
304
+ const mode: BlockerMode = this.config.evidenceAligned ? 'replace' : 'suggest';
305
+
306
+ // Emit task_blockers_set event
307
+ const eventId = await this.emitTaskEvent('task_blockers_set', {
308
+ taskId: task.entityId,
309
+ mode,
310
+ blockers: blockerRefs,
311
+ sourceEntryId
312
+ });
313
+
314
+ return eventId;
315
+ }
316
+
317
+ /**
318
+ * Handle unknown blocker (status=blocked but no blockedBy)
319
+ */
320
+ private async handleUnknownBlocker(task: Entity): Promise<string | null> {
321
+ const placeholderRef = await this.blockerResolver.createUnknownPlaceholder(task.title);
322
+
323
+ const eventId = await this.emitTaskEvent('task_blockers_set', {
324
+ taskId: task.entityId,
325
+ mode: 'suggest' as BlockerMode,
326
+ blockers: [placeholderRef]
327
+ });
328
+
329
+ return eventId;
330
+ }
331
+
332
+ /**
333
+ * Emit task event to events table
334
+ */
335
+ private async emitTaskEvent(
336
+ eventType: string,
337
+ payload: Record<string, unknown>
338
+ ): Promise<string> {
339
+ const eventId = randomUUID();
340
+ const now = new Date();
341
+
342
+ // Generate dedupe key
343
+ const dedupeKey = makeTaskEventDedupeKey(
344
+ eventType,
345
+ payload.taskId as string,
346
+ this.config.sessionId,
347
+ JSON.stringify(payload)
348
+ );
349
+
350
+ // Check for duplicate
351
+ const existing = await this.db.all<Array<{ event_id: string }>>(
352
+ `SELECT event_id FROM event_dedup WHERE dedupe_key = ?`,
353
+ [dedupeKey]
354
+ );
355
+
356
+ if (existing.length > 0) {
357
+ return existing[0].event_id; // Return existing event ID
358
+ }
359
+
360
+ // Insert event
361
+ await this.db.run(
362
+ `INSERT INTO events (
363
+ id, event_type, session_id, timestamp, content,
364
+ canonical_key, dedupe_key, metadata
365
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
366
+ [
367
+ eventId,
368
+ eventType,
369
+ this.config.sessionId,
370
+ now.toISOString(),
371
+ JSON.stringify(payload),
372
+ `task_event:${eventType}:${payload.taskId}`,
373
+ dedupeKey,
374
+ JSON.stringify({ source: 'task_resolver' })
375
+ ]
376
+ );
377
+
378
+ // Insert dedup record
379
+ await this.db.run(
380
+ `INSERT INTO event_dedup (dedupe_key, event_id)
381
+ VALUES (?, ?)
382
+ ON CONFLICT DO NOTHING`,
383
+ [dedupeKey, eventId]
384
+ );
385
+
386
+ return eventId;
387
+ }
388
+
389
+ /**
390
+ * Resolve condition to task (when condition is identified as existing task)
391
+ */
392
+ async resolveConditionToTask(
393
+ conditionId: string,
394
+ taskId: string
395
+ ): Promise<string> {
396
+ const eventId = await this.emitTaskEvent('condition_resolved_to', {
397
+ conditionId,
398
+ resolvedTo: {
399
+ kind: 'task',
400
+ entityId: taskId
401
+ }
402
+ });
403
+
404
+ // Create resolves_to edge
405
+ await this.db.run(
406
+ `INSERT INTO edges (edge_id, src_type, src_id, rel_type, dst_type, dst_id, meta_json)
407
+ VALUES (?, 'entity', ?, 'resolves_to', 'entity', ?, ?)
408
+ ON CONFLICT DO NOTHING`,
409
+ [randomUUID(), conditionId, taskId, JSON.stringify({ resolved_at: new Date().toISOString() })]
410
+ );
411
+
412
+ return eventId;
413
+ }
414
+ }