@ddtcorex/dsh-maestro-memory 1.0.1

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 (109) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +21 -0
  3. package/README.md +448 -0
  4. package/cordis.patch.yml +13 -0
  5. package/lib/client.js +756 -0
  6. package/lib/index.d.ts +10 -0
  7. package/lib/index.d.ts.map +1 -0
  8. package/lib/index.js +627 -0
  9. package/lib/index.js.map +1 -0
  10. package/lib/memory/batch.d.ts +44 -0
  11. package/lib/memory/batch.d.ts.map +1 -0
  12. package/lib/memory/batch.js +75 -0
  13. package/lib/memory/batch.js.map +1 -0
  14. package/lib/memory/feedback.d.ts +18 -0
  15. package/lib/memory/feedback.d.ts.map +1 -0
  16. package/lib/memory/feedback.js +29 -0
  17. package/lib/memory/feedback.js.map +1 -0
  18. package/lib/memory/store.d.ts +154 -0
  19. package/lib/memory/store.d.ts.map +1 -0
  20. package/lib/memory/store.js +606 -0
  21. package/lib/memory/store.js.map +1 -0
  22. package/lib/migration/cli.d.ts +18 -0
  23. package/lib/migration/cli.d.ts.map +1 -0
  24. package/lib/migration/cli.js +143 -0
  25. package/lib/migration/cli.js.map +1 -0
  26. package/lib/migration/fixture.d.ts +45 -0
  27. package/lib/migration/fixture.d.ts.map +1 -0
  28. package/lib/migration/fixture.js +177 -0
  29. package/lib/migration/fixture.js.map +1 -0
  30. package/lib/migration/service.d.ts +75 -0
  31. package/lib/migration/service.d.ts.map +1 -0
  32. package/lib/migration/service.js +713 -0
  33. package/lib/migration/service.js.map +1 -0
  34. package/lib/prompt/snapshot.d.ts +26 -0
  35. package/lib/prompt/snapshot.d.ts.map +1 -0
  36. package/lib/prompt/snapshot.js +74 -0
  37. package/lib/prompt/snapshot.js.map +1 -0
  38. package/lib/review/queue.d.ts +72 -0
  39. package/lib/review/queue.d.ts.map +1 -0
  40. package/lib/review/queue.js +306 -0
  41. package/lib/review/queue.js.map +1 -0
  42. package/lib/skills-browser.d.ts +67 -0
  43. package/lib/skills-browser.d.ts.map +1 -0
  44. package/lib/skills-browser.js +185 -0
  45. package/lib/skills-browser.js.map +1 -0
  46. package/lib/storage/atomic-store.d.ts +211 -0
  47. package/lib/storage/atomic-store.d.ts.map +1 -0
  48. package/lib/storage/atomic-store.js +582 -0
  49. package/lib/storage/atomic-store.js.map +1 -0
  50. package/lib/storage/layout.d.ts +81 -0
  51. package/lib/storage/layout.d.ts.map +1 -0
  52. package/lib/storage/layout.js +162 -0
  53. package/lib/storage/layout.js.map +1 -0
  54. package/lib/storage/legacy-format.d.ts +104 -0
  55. package/lib/storage/legacy-format.d.ts.map +1 -0
  56. package/lib/storage/legacy-format.js +262 -0
  57. package/lib/storage/legacy-format.js.map +1 -0
  58. package/lib/sync/config.d.ts +22 -0
  59. package/lib/sync/config.d.ts.map +1 -0
  60. package/lib/sync/config.js +57 -0
  61. package/lib/sync/config.js.map +1 -0
  62. package/lib/sync/git.d.ts +97 -0
  63. package/lib/sync/git.d.ts.map +1 -0
  64. package/lib/sync/git.js +282 -0
  65. package/lib/sync/git.js.map +1 -0
  66. package/lib/sync/index.d.ts +6 -0
  67. package/lib/sync/index.d.ts.map +1 -0
  68. package/lib/sync/index.js +6 -0
  69. package/lib/sync/index.js.map +1 -0
  70. package/lib/sync/layout.d.ts +15 -0
  71. package/lib/sync/layout.d.ts.map +1 -0
  72. package/lib/sync/layout.js +33 -0
  73. package/lib/sync/layout.js.map +1 -0
  74. package/lib/sync/merge.d.ts +42 -0
  75. package/lib/sync/merge.d.ts.map +1 -0
  76. package/lib/sync/merge.js +159 -0
  77. package/lib/sync/merge.js.map +1 -0
  78. package/lib/sync/service.d.ts +70 -0
  79. package/lib/sync/service.d.ts.map +1 -0
  80. package/lib/sync/service.js +508 -0
  81. package/lib/sync/service.js.map +1 -0
  82. package/lib/todo/store.d.ts +142 -0
  83. package/lib/todo/store.d.ts.map +1 -0
  84. package/lib/todo/store.js +452 -0
  85. package/lib/todo/store.js.map +1 -0
  86. package/lib/types/client/index.d.ts +7 -0
  87. package/lib/types/client/index.d.ts.map +1 -0
  88. package/package.json +63 -0
  89. package/src/client/index.tsx +994 -0
  90. package/src/host/index.ts +591 -0
  91. package/src/host/memory/batch.ts +107 -0
  92. package/src/host/memory/feedback.ts +36 -0
  93. package/src/host/memory/store.ts +650 -0
  94. package/src/host/migration/cli.ts +139 -0
  95. package/src/host/migration/fixture.ts +218 -0
  96. package/src/host/migration/service.ts +786 -0
  97. package/src/host/prompt/snapshot.ts +95 -0
  98. package/src/host/review/queue.ts +325 -0
  99. package/src/host/skills-browser.ts +191 -0
  100. package/src/host/storage/atomic-store.ts +605 -0
  101. package/src/host/storage/layout.ts +191 -0
  102. package/src/host/storage/legacy-format.ts +308 -0
  103. package/src/host/sync/config.ts +67 -0
  104. package/src/host/sync/git.ts +280 -0
  105. package/src/host/sync/index.ts +5 -0
  106. package/src/host/sync/layout.ts +39 -0
  107. package/src/host/sync/merge.ts +188 -0
  108. package/src/host/sync/service.ts +494 -0
  109. package/src/host/todo/store.ts +480 -0
@@ -0,0 +1,591 @@
1
+ /**
2
+ * dsh-maestro-memory — host entry (M2-PR-B: confirmation queue, gated memory_suggest, RPC decide, Review UI)
3
+ */
4
+ import { defineTool } from '@deepseek-ai/dsh-tools'
5
+ import { MaestroMemoryStore } from './memory/store.ts'
6
+ import { applyBatch } from './memory/batch.ts'
7
+ import { buildFeedbackLine } from './memory/feedback.ts'
8
+ import { TodoStore, resolveQuadrant, DEFAULT_VIEW_LIMIT } from './todo/store.ts'
9
+ import { TODO_TARGETS, TODO_STATUSES } from './storage/legacy-format.ts'
10
+ import { SuggestionQueue, enqueueSuggestion, approveSuggestions, rejectSuggestions } from './review/queue.ts'
11
+ import { resolveMemoryRoot, suggestionsPath, globalArchivePath, userArchivePath, projectKeyArchivePath, todoArchivePath } from './storage/layout.ts'
12
+ import { appendEntryAtomicSync } from './storage/atomic-store.ts'
13
+ import * as migration from './migration/service.ts'
14
+ import { SyncService } from './sync/service.ts'
15
+ import { RealGitAdapter } from './sync/git.ts'
16
+ import { listSkillsSync, resolveDefaultMaestroSkillsDir } from './skills-browser.ts'
17
+ import { renderSnapshot } from './prompt/snapshot.ts'
18
+
19
+ export const inject = ['tools', 'systemPrompt', 'connection'] as const
20
+
21
+ export interface MaestroMemoryConfig {
22
+ memoryDir?: string | null
23
+ snapshotOrder?: number
24
+ }
25
+
26
+ export const DEFAULTS: Required<MaestroMemoryConfig> = {
27
+ memoryDir: null,
28
+ snapshotOrder: 500,
29
+ }
30
+
31
+ // Extended unions for memory tool (M2-PR-A + M2-PR-B queue)
32
+ export type MemoryTarget = 'memory' | 'user' | 'project' | 'key' | 'daily'
33
+ export type MemoryAction = 'add' | 'list' | 'replace' | 'remove' | 'archive' | 'expand'
34
+
35
+ const CONTENT_OUTPUT = {
36
+ schema: {
37
+ type: 'object',
38
+ additionalProperties: false,
39
+ properties: { content: { type: 'array', required: true } },
40
+ } as const,
41
+ render: (_args: unknown, value: { content: any[] }) => value.content,
42
+ }
43
+
44
+ export function apply(ctx: any, config: MaestroMemoryConfig = {}): void {
45
+ const order = config.snapshotOrder ?? DEFAULTS.snapshotOrder
46
+ const store = new MaestroMemoryStore(config.memoryDir ?? null)
47
+ const todoStore = new TodoStore(config.memoryDir ?? null)
48
+ const root = resolveMemoryRoot(config.memoryDir ?? null)
49
+ const queue = new SuggestionQueue(suggestionsPath(root))
50
+ const syncService = new SyncService(config.memoryDir ?? null, new RealGitAdapter())
51
+
52
+ ctx.effect(() => {
53
+ const dispose = ctx.systemPrompt.context({
54
+ name: 'memory:snapshot',
55
+ order,
56
+ text: (promptCtx: any) => {
57
+ const cwd: string | null = promptCtx?.agent?.session?.header?.cwd ?? null
58
+ const branch: string | undefined = promptCtx?.agent?.session?.header?.branch ?? undefined
59
+ const sessionId: string | undefined = promptCtx?.agent?.session?.header?.sessionId
60
+ ?? promptCtx?.agent?.session?.id
61
+ ?? undefined
62
+ const sessionName: string | undefined = promptCtx?.agent?.session?.header?.sessionName
63
+ ?? promptCtx?.agent?.session?.name
64
+ ?? undefined
65
+ return renderSnapshot(store, { cwd, branch, sessionId, sessionName })
66
+ },
67
+ })
68
+ return () => {
69
+ if (typeof dispose === 'function') dispose()
70
+ }
71
+ }, 'maestro-memory: snapshot')
72
+
73
+ ctx.effect(() => {
74
+ const tool = defineTool({
75
+ name: 'memory',
76
+ description: 'Maestro memory (M2: five tracks, query, replace/remove, archive, branch, summary/expand)',
77
+ parameters: {
78
+ action: { type: 'string', required: true, enum: ['add', 'list', 'replace', 'remove', 'archive', 'expand'], description: 'Memory action' },
79
+ target: { type: 'string', description: 'Memory track (daily=YYYY-MM-DD file, project=per-cwd log, key=per-cwd long-term); optional only when entries[] is used' },
80
+ entries: { type: 'array', description: 'Batch add: array of {target,content,cwd?,date?,branches?,summary?} — sequential through store.add with rollback on first failure' },
81
+ content: { type: 'string', description: 'Entry content (add) or new content (replace)' },
82
+ match: { type: 'string', description: 'Unique substring identifying entry (replace/remove/archive)' },
83
+ filter: { type: 'string', description: 'Content substring filter (list)' },
84
+ since: { type: 'string', description: 'Start date YYYY-MM-DD (list)' },
85
+ until: { type: 'string', description: 'End date YYYY-MM-DD (list)' },
86
+ limit: { type: 'integer', description: 'Max entries (list)' },
87
+ recent: { type: 'boolean', description: 'Newest first (list)' },
88
+ branch: { type: 'string', description: 'Branch filter for key (list)' },
89
+ branches: { type: 'string', description: 'Branch scope csv for key add, e.g. main,dev (empty=all)' },
90
+ summary: { type: 'string', description: 'One-line summary for key add (progressive disclosure)' },
91
+ sentiment: { type: 'string', enum: ['positive', 'negative', 'neutral'], description: 'Attach [Feedback] line on add when set (single add or entries[])' },
92
+ category: { type: 'string', description: 'Feedback category (requires sentiment)' },
93
+ quote: { type: 'string', description: 'Feedback quote (requires sentiment)' },
94
+ note: { type: 'string', description: 'Feedback note (requires sentiment)' },
95
+ id: { type: 'string', description: 'Entry id for expand (key)' },
96
+ archived: { type: 'boolean', description: 'Query archive files (list)' },
97
+ cwd: { type: 'string', description: 'Working directory for project/key tracks' },
98
+ date: { type: 'string', description: 'Date YYYY-MM-DD for daily track (add/list/replace/remove)' },
99
+ },
100
+ output: CONTENT_OUTPUT,
101
+ execute: async (args: any, exec: any) => {
102
+ const target = args.target as MemoryTarget
103
+ const action = args.action as MemoryAction
104
+ const cwd: string | undefined = args.cwd ?? exec?.agent?.session?.header?.cwd
105
+ try {
106
+ switch (action) {
107
+ case 'add': {
108
+ // Batch path: entries[] takes precedence over the single target/content form.
109
+ if (Array.isArray(args.entries)) {
110
+ if (!args.target && !args.content) {
111
+ // Inject the session cwd as per-entry fallback, mirroring the
112
+ // single-add path — otherwise project/key entries without an
113
+ // explicit cwd fail deep inside the store mid-batch.
114
+ const batchEntries = args.entries.map((entry: any) => ({ ...entry, cwd: entry.cwd ?? cwd }))
115
+ const batchRes = applyBatch(store, batchEntries)
116
+ if (!batchRes.ok) {
117
+ return { content: [{ type: 'text', text: `batch failed at [${batchRes.index}]: ${batchRes.error}` }] }
118
+ }
119
+ return { content: [{ type: 'text', text: `added ${batchRes.ids.length} ${batchRes.ids.length === 1 ? 'entry' : 'entries'} (batch)` }] }
120
+ }
121
+ } else if (!target) {
122
+ return { content: [{ type: 'text', text: 'add failed: target is required for single add (or pass entries[])' }] }
123
+ }
124
+ let entryText = args.content ?? ''
125
+ if (args.sentiment !== undefined) {
126
+ entryText = `${entryText.trimEnd()} ${buildFeedbackLine({
127
+ sentiment: args.sentiment,
128
+ category: args.category,
129
+ quote: args.quote,
130
+ note: args.note,
131
+ })}`
132
+ }
133
+ const res = store.add(target, entryText, cwd, {
134
+ branches: args.branches,
135
+ summary: args.summary,
136
+ // daily add targets a specific day when the caller passes date
137
+ date: args.date,
138
+ })
139
+ if (!res.ok) return { content: [{ type: 'text', text: `add failed: ${res.error}` }] }
140
+ return { content: [{ type: 'text', text: res.duplicate ? 'duplicate' : `added to ${target}` }] }
141
+ }
142
+ case 'list': {
143
+ const entries = store.list(target, cwd, {
144
+ filter: args.filter,
145
+ since: args.since,
146
+ until: args.until,
147
+ limit: args.limit,
148
+ recent: args.recent,
149
+ branch: args.branch,
150
+ archived: args.archived,
151
+ date: args.date,
152
+ })
153
+ const text = entries.length ? entries.join('\n---\n') : '(no entries)'
154
+ return { content: [{ type: 'text', text }] }
155
+ }
156
+ case 'replace': {
157
+ const res = store.replace(target, args.match ?? '', args.content ?? '', cwd, { date: args.date })
158
+ if (!res.ok) return { content: [{ type: 'text', text: `replace failed: ${res.error}` }] }
159
+ return { content: [{ type: 'text', text: 'replaced' }] }
160
+ }
161
+ case 'remove': {
162
+ const res = store.remove(target, args.match ?? '', cwd, { date: args.date })
163
+ if (!res.ok) return { content: [{ type: 'text', text: `remove failed: ${res.error}` }] }
164
+ return { content: [{ type: 'text', text: 'removed' }] }
165
+ }
166
+ case 'archive': {
167
+ const res = store.archive(target, args.match ?? '', cwd)
168
+ if (!res.ok) return { content: [{ type: 'text', text: `archive failed: ${res.error}` }] }
169
+ return { content: [{ type: 'text', text: 'archived' }] }
170
+ }
171
+ case 'expand': {
172
+ const res = store.expand(target, args.id ?? '', cwd)
173
+ if (!res.ok) return { content: [{ type: 'text', text: `expand failed: ${res.error}` }] }
174
+ return { content: [{ type: 'text', text: res.entry }] }
175
+ }
176
+ default:
177
+ return { content: [{ type: 'text', text: `unknown action ${action}` }] }
178
+ }
179
+ } catch (e: any) {
180
+ return { content: [{ type: 'text', text: `error: ${e?.message ?? String(e)}` }] }
181
+ }
182
+ },
183
+ })
184
+ const dispose = ctx.tools.register(tool)
185
+ return () => {
186
+ if (typeof dispose === 'function') dispose()
187
+ }
188
+ }, 'maestro-memory: tool')
189
+
190
+ // Gated memory_suggest tool — model proposals go to queue, never directly to memory
191
+ ctx.effect(() => {
192
+ const tool = defineTool({
193
+ name: 'memory_suggest',
194
+ description: 'Propose memory/todo for confirmation queue (gated, requires user approve). Targets: memory/user/key/todo-*',
195
+ parameters: {
196
+ target: { type: 'string', required: true, enum: ['memory', 'user', 'key', 'todo-life', 'todo-work', 'todo-project', 'todo-daily'] },
197
+ content: { type: 'string', required: true },
198
+ reason: { type: 'string', required: true },
199
+ },
200
+ output: CONTENT_OUTPUT,
201
+ execute: async (args: any, exec: any) => {
202
+ const target = String(args.target ?? '').trim()
203
+ const content = String(args.content ?? '').trim()
204
+ const reason = String(args.reason ?? '').trim()
205
+ const valid = ['memory', 'user', 'key', 'todo-life', 'todo-work', 'todo-project', 'todo-daily']
206
+ if (!valid.includes(target)) return { content: [{ type: 'text', text: `invalid target ${target}` }] }
207
+ if (!content) return { content: [{ type: 'text', text: 'empty content' }] }
208
+ if (!reason) return { content: [{ type: 'text', text: 'empty reason' }] }
209
+ const agent = exec?.agent
210
+ const res = enqueueSuggestion(queue, target, content, reason, agent)
211
+ if (!res.ok) return { content: [{ type: 'text', text: res.message ?? 'failed' }] }
212
+ const msg = (res as any).hits ? `queued (deduped hits=${(res as any).hits})` : `queued (${res.queued})`
213
+ return { content: [{ type: 'text', text: msg }] }
214
+ },
215
+ })
216
+ const dispose = ctx.tools.register(tool)
217
+ return () => {
218
+ if (typeof dispose === 'function') dispose()
219
+ }
220
+ }, 'maestro-memory: suggest-tool')
221
+
222
+ // dtodo compatibility tool (four tracks, IDs, status/due/quadrant, smart view, historical lookup)
223
+ ctx.effect(() => {
224
+ const tool = defineTool({
225
+ name: 'dtodo',
226
+ description: 'Todos: life/work/project/daily with IDs, status/due/quadrant, smart view (overdue/today/project/Q1-Q2, limit 8), historical daily lookup',
227
+ parameters: {
228
+ action: { type: 'string', required: true, enum: ['add', 'list', 'done', 'update', 'remove'] },
229
+ target: { type: 'string', enum: [...TODO_TARGETS] },
230
+ content: { type: 'string' },
231
+ id: { type: 'string' },
232
+ due: { type: 'string' },
233
+ quadrant: { type: 'string', enum: ['q1', 'q2', 'q3', 'q4'] },
234
+ important: { type: 'boolean' },
235
+ urgent: { type: 'boolean' },
236
+ cat: { type: 'string' },
237
+ status: { type: 'string', enum: [...TODO_STATUSES] },
238
+ all: { type: 'boolean' },
239
+ past: { type: 'boolean' },
240
+ expired: { type: 'boolean' },
241
+ cwd: { type: 'string' },
242
+ date: { type: 'string' },
243
+ },
244
+ output: CONTENT_OUTPUT,
245
+ execute: async (args: any, exec: any) => {
246
+ const action = args.action as string
247
+ const cwd: string | undefined = args.cwd ?? exec?.agent?.session?.header?.cwd
248
+ const dateArg = (v: any) => (typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v) ? v : undefined)
249
+ try {
250
+ if (action === 'add') {
251
+ const target = (args.target ?? (cwd ? 'project' : 'work')) as any
252
+ if (!TODO_TARGETS.includes(target)) return { content: [{ type: 'text', text: `invalid target ${target}` }] }
253
+ const due = typeof args.due === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(args.due) ? args.due : undefined
254
+ const cat = typeof args.cat === 'string' && args.cat.trim() !== '' ? args.cat.trim() : undefined
255
+ const quadrant = resolveQuadrant({ quadrant: args.quadrant, important: args.important, urgent: args.urgent })
256
+ const res = todoStore.addTodo(target, String(args.content ?? ''), { quadrant, due, cat }, cwd)
257
+ if (!res.ok) return { content: [{ type: 'text', text: res.message }] }
258
+ return { content: [{ type: 'text', text: `${res.message} (id: ${res.id})` }] }
259
+ }
260
+ if (action === 'list') {
261
+ const targets: any[] = args.target !== undefined ? [args.target] : [...TODO_TARGETS]
262
+ const projectCwd = typeof args.cwd === 'string' && args.cwd.trim() !== '' ? args.cwd.trim() : cwd
263
+ const result = todoStore.listTodos(targets, {
264
+ status: args.status,
265
+ quadrant: args.quadrant,
266
+ due: args.due,
267
+ cat: args.cat,
268
+ all: args.all === true,
269
+ past: args.past === true,
270
+ expired: args.expired === true,
271
+ date: dateArg(args.date),
272
+ }, projectCwd, dateArg(args.date))
273
+ const text = todoStore.formatList(result, dateArg(args.date))
274
+ return { content: [{ type: 'text', text }] }
275
+ }
276
+ if (action === 'done') {
277
+ const id = String(args.id ?? '').trim()
278
+ if (!id) return { content: [{ type: 'text', text: 'id required' }] }
279
+ const res = todoStore.doneTodo(args.target, id, cwd, dateArg(args.date))
280
+ return { content: [{ type: 'text', text: res.message }] }
281
+ }
282
+ if (action === 'remove') {
283
+ const id = String(args.id ?? '').trim()
284
+ if (!id) return { content: [{ type: 'text', text: 'id required' }] }
285
+ const res = todoStore.removeTodo(args.target, id, cwd, dateArg(args.date))
286
+ return { content: [{ type: 'text', text: res.message }] }
287
+ }
288
+ if (action === 'update') {
289
+ const id = String(args.id ?? '').trim()
290
+ if (!id) return { content: [{ type: 'text', text: 'id required' }] }
291
+ const patch: any = {}
292
+ if (args.status !== undefined) patch.status = args.status
293
+ if (args.quadrant !== undefined) patch.quadrant = /^q[1-4]$/.test(args.quadrant) ? args.quadrant : undefined
294
+ else if (args.important !== undefined || args.urgent !== undefined) {
295
+ const q = resolveQuadrant({ important: args.important, urgent: args.urgent })
296
+ if (q) patch.quadrant = q
297
+ }
298
+ if (args.due !== undefined) patch.due = /^\d{4}-\d{2}-\d{2}$/.test(args.due) ? args.due : null
299
+ if (args.cat !== undefined) patch.cat = args.cat === '' ? null : args.cat
300
+ if (args.content !== undefined) patch.content = String(args.content)
301
+ const res = todoStore.updateTodo(args.target, id, patch, cwd, dateArg(args.date))
302
+ return { content: [{ type: 'text', text: res.message }] }
303
+ }
304
+ return { content: [{ type: 'text', text: `unknown action ${action}` }] }
305
+ } catch (e: any) {
306
+ return { content: [{ type: 'text', text: `error: ${e?.message ?? String(e)}` }] }
307
+ }
308
+ },
309
+ })
310
+ const dispose = ctx.tools.register(tool)
311
+ return () => {
312
+ if (typeof dispose === 'function') dispose()
313
+ }
314
+ }, 'maestro-memory: dtodo-tool')
315
+
316
+ // RPC channel for Review queue — explicit user-click decisions
317
+ ctx.effect(() => {
318
+ const channel = '/dsh-maestro-memory'
319
+ const rpcDate = (v: any) => (typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v) ? v : undefined)
320
+ const legacyHandler = async (endpoint: string, payload: any) => {
321
+ switch (endpoint) {
322
+ case 'queue.list': {
323
+ const entries = queue.read()
324
+ return { ok: true, entries }
325
+ }
326
+ case 'queue.decide': {
327
+ const action = payload?.action as 'approve' | 'reject' | 'archive'
328
+ const indices: number[] = Array.isArray(payload?.indices) ? payload.indices.map((n: any) => Number(n)).filter((n: number) => Number.isInteger(n) && n >= 1) : []
329
+ if (indices.length === 0) return { ok: false, error: 'indices required' }
330
+ if (action === 'approve') {
331
+ const editsRaw = payload?.edits as Record<string, string> | undefined
332
+ const targetsRaw = payload?.targets as Record<string, string> | undefined
333
+ const edits = editsRaw ? new Map(Object.entries(editsRaw).map(([k, v]) => [Number(k), String(v)])) : undefined
334
+ const targets = targetsRaw ? new Map(Object.entries(targetsRaw).map(([k, v]) => [Number(k), String(v)])) : undefined
335
+ const agent = payload?.cwd ? { session: { header: { cwd: payload.cwd } } } : undefined
336
+ const res = approveSuggestions(store, todoStore, queue, indices, agent, edits, targets)
337
+ return { ok: true, ...res }
338
+ }
339
+ if (action === 'reject') {
340
+ const res = rejectSuggestions(queue, indices)
341
+ return { ok: true, ...res }
342
+ }
343
+ if (action === 'archive') {
344
+ const res = queue.mutate((entries) => {
345
+ const kept: typeof entries = []
346
+ const lines: string[] = []
347
+ entries.forEach((entry, idx) => {
348
+ const number = idx + 1
349
+ if (!indices.includes(number)) {
350
+ kept.push(entry)
351
+ return
352
+ }
353
+ try {
354
+ const stamp = new Date().toISOString().slice(0, 10)
355
+ const originTag = entry.target.startsWith('todo-') ? `\n(original track: ${entry.target})` : ''
356
+ const stamped = `[${stamp}] ${entry.content}${originTag}${entry.reason ? `\n(archive reason: ${entry.reason})` : ''}`
357
+ let archiveFile: string | null = null
358
+ if (entry.target === 'memory') archiveFile = globalArchivePath(root)
359
+ else if (entry.target === 'user') archiveFile = userArchivePath(root)
360
+ else if (entry.target === 'key' && entry.cwd) archiveFile = projectKeyArchivePath(root, entry.cwd)
361
+ else if (entry.target.startsWith('todo-')) archiveFile = todoArchivePath(root)
362
+ // No archivable target (e.g. key without cwd): DO NOT silently drop the suggestion.
363
+ if (!archiveFile) {
364
+ lines.push(`✗ #${number} [${entry.target}] cannot archive (missing cwd or unsupported target)`)
365
+ kept.push(entry)
366
+ return
367
+ }
368
+ const appended = appendEntryAtomicSync(archiveFile, stamped)
369
+ if (!appended.ok) {
370
+ lines.push(`✗ #${number} [${entry.target}] archive failed: ${appended.error}`)
371
+ kept.push(entry)
372
+ return
373
+ }
374
+ lines.push(`#${number} [${entry.target}] archived`)
375
+ } catch (e: any) {
376
+ lines.push(`✗ #${number} [${entry.target}] ${e?.message ?? String(e)}`)
377
+ kept.push(entry)
378
+ }
379
+ })
380
+ entries.length = 0
381
+ entries.push(...kept)
382
+ return { lines, remaining: kept.length }
383
+ })
384
+ return { ok: true, ...res }
385
+ }
386
+ return { ok: false, error: `unknown action ${action}` }
387
+ }
388
+ case 'memory.list': {
389
+ const target = payload?.target as string
390
+ const cwd = payload?.cwd as string | undefined
391
+ const entries = store.list(target as any, cwd, { ...(payload?.opts ?? {}), date: payload?.date })
392
+ return { ok: true, entries }
393
+ }
394
+ case 'memory.mutate': {
395
+ const action = payload?.action as string
396
+ const target = payload?.target as string
397
+ const cwd = payload?.cwd as string | undefined
398
+ const content = payload?.content as string | undefined
399
+ const match = payload?.match as string | undefined
400
+ const id = payload?.id as string | undefined
401
+ const opts = payload?.opts ?? {}
402
+ try {
403
+ if (action === 'list') {
404
+ return { ok: true, entries: store.list(target as any, cwd, { ...opts, date: payload?.date }) }
405
+ }
406
+ if (action === 'add') {
407
+ return store.add(target as any, content ?? '', cwd, { branches: payload?.branches, summary: payload?.summary, date: payload?.date })
408
+ }
409
+ if (action === 'replace') {
410
+ return store.replace(target as any, match ?? '', content ?? '', cwd, { date: payload?.date })
411
+ }
412
+ if (action === 'remove') {
413
+ return store.remove(target as any, match ?? '', cwd, { date: payload?.date })
414
+ }
415
+ if (action === 'archive') {
416
+ return store.archive(target as any, match ?? '', cwd)
417
+ }
418
+ if (action === 'expand') {
419
+ return store.expand(target as any, id ?? '', cwd)
420
+ }
421
+ return { ok: false, error: `unknown memory action ${action}` }
422
+ } catch (e: any) {
423
+ return { ok: false, error: e?.message ?? String(e) }
424
+ }
425
+ }
426
+ case 'todo.list': {
427
+ const targets: any[] = payload?.targets ?? (payload?.target ? [payload.target] : [...TODO_TARGETS])
428
+ const cwd = payload?.cwd as string | undefined
429
+ const opts = payload?.opts ?? payload ?? {}
430
+ const date = rpcDate(opts.date)
431
+ const result = todoStore.listTodos(targets, {
432
+ status: opts.status,
433
+ quadrant: opts.quadrant,
434
+ due: opts.due,
435
+ cat: opts.cat,
436
+ all: opts.all === true,
437
+ past: opts.past === true,
438
+ expired: opts.expired === true,
439
+ date,
440
+ }, cwd, date)
441
+ return { ok: true, ...result, text: todoStore.formatList(result, date) }
442
+ }
443
+ case 'todo.mutate': {
444
+ const action = payload?.action as string
445
+ const target = payload?.target as any
446
+ const cwd = payload?.cwd as string | undefined
447
+ const date = rpcDate(payload?.date as string | undefined)
448
+ if (action === 'add') {
449
+ const due = typeof payload?.due === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(payload.due) ? payload.due : undefined
450
+ const quadrant = resolveQuadrant({ quadrant: payload?.quadrant, important: payload?.important, urgent: payload?.urgent })
451
+ const res = todoStore.addTodo(target ?? (cwd ? 'project' : 'work'), String(payload?.content ?? ''), { quadrant, due, cat: payload?.cat }, cwd)
452
+ return { ...res }
453
+ }
454
+ if (action === 'done') {
455
+ const res = todoStore.doneTodo(target, String(payload?.id ?? ''), cwd, date)
456
+ return { ...res }
457
+ }
458
+ if (action === 'remove') {
459
+ const res = todoStore.removeTodo(target, String(payload?.id ?? ''), cwd, date)
460
+ return { ...res }
461
+ }
462
+ if (action === 'update') {
463
+ const patch: any = {}
464
+ if (payload?.status !== undefined) patch.status = payload.status
465
+ if (payload?.quadrant !== undefined) patch.quadrant = payload.quadrant
466
+ if (payload?.due !== undefined) patch.due = payload.due
467
+ if (payload?.cat !== undefined) patch.cat = payload.cat
468
+ if (payload?.content !== undefined) patch.content = payload.content
469
+ const res = todoStore.updateTodo(target, String(payload?.id ?? ''), patch, cwd, date)
470
+ return { ...res }
471
+ }
472
+ return { ok: false, error: `unknown todo action ${action}` }
473
+ }
474
+ case 'status': {
475
+ return { ok: true, queue: queue.read().length, blocked: migration.isWriteBlocked(root) }
476
+ }
477
+ case 'migration.inspect': {
478
+ const insp = await migration.inspect(root)
479
+ return { ...insp }
480
+ }
481
+ case 'migration.dryRun': {
482
+ const res = await migration.dryRun(root)
483
+ return { ...res }
484
+ }
485
+ case 'migration.run': {
486
+ // RPC run requires explicit apply flag in payload to enforce CLI's --apply semantics
487
+ if (payload?.apply !== true) {
488
+ return { ok: false, error: 'migration requires explicit apply=true (read-only by default)' }
489
+ }
490
+ const res = await migration.run(root)
491
+ return { ...res }
492
+ }
493
+ case 'migration.verify': {
494
+ const res = await migration.verify(root, payload?.runId)
495
+ return { ...res }
496
+ }
497
+ case 'sync.enable': {
498
+ const cwd = String(payload?.cwd ?? '').trim()
499
+ const remoteUrl = String(payload?.remoteUrl ?? payload?.remote ?? '').trim()
500
+ const branch = payload?.branch ? String(payload.branch).trim() : undefined
501
+ if (!cwd) return { ok: false, error: 'cwd required' }
502
+ if (!remoteUrl) return { ok: false, error: 'remoteUrl required' }
503
+ const res = syncService.enable(cwd, remoteUrl, branch)
504
+ return { ...res }
505
+ }
506
+ case 'sync.disable': {
507
+ const cwd = String(payload?.cwd ?? '').trim()
508
+ if (!cwd) return { ok: false, error: 'cwd required' }
509
+ const res = syncService.disable(cwd)
510
+ return { ...res }
511
+ }
512
+ case 'sync.status': {
513
+ const cwd = String(payload?.cwd ?? '').trim()
514
+ if (!cwd) return { ok: false, error: 'cwd required' }
515
+ const reveal = payload?.reveal === true
516
+ // explicit fetch on status when requested
517
+ if (payload?.fetch === true) {
518
+ const fetchRes = await syncService.fetch(cwd)
519
+ if (!fetchRes.ok) return { ok: false, error: fetchRes.error }
520
+ const st = syncService.status(cwd, reveal)
521
+ return { ok: true, ...st, fetched: true, conflicts: fetchRes.conflicts }
522
+ }
523
+ const st = syncService.status(cwd, reveal)
524
+ return { ok: true, ...st }
525
+ }
526
+ case 'sync.fetch': {
527
+ const cwd = String(payload?.cwd ?? '').trim()
528
+ if (!cwd) return { ok: false, error: 'cwd required' }
529
+ const res = await syncService.fetch(cwd)
530
+ return { ...res }
531
+ }
532
+ case 'sync.push': {
533
+ const cwd = String(payload?.cwd ?? '').trim()
534
+ if (!cwd) return { ok: false, error: 'cwd required' }
535
+ const res = await syncService.push(cwd, payload?.message ? String(payload.message) : undefined)
536
+ return { ...res }
537
+ }
538
+ case 'sync.pull': {
539
+ const cwd = String(payload?.cwd ?? '').trim()
540
+ if (!cwd) return { ok: false, error: 'cwd required' }
541
+ const res = await syncService.pull(cwd)
542
+ return { ...res }
543
+ }
544
+ case 'sync.resolve': {
545
+ const cwd = String(payload?.cwd ?? '').trim()
546
+ const id = String(payload?.id ?? '').trim()
547
+ const choice = String(payload?.choice ?? '').trim() as any
548
+ if (!cwd) return { ok: false, error: 'cwd required' }
549
+ if (!id) return { ok: false, error: 'id required' }
550
+ const res = syncService.resolve(cwd, id, choice)
551
+ return { ...res }
552
+ }
553
+ case 'sync.listConflicts': {
554
+ const cwd = String(payload?.cwd ?? '').trim()
555
+ if (!cwd) return { ok: false, error: 'cwd required' }
556
+ const conflicts = syncService.listConflicts(cwd)
557
+ return { ok: true, conflicts }
558
+ }
559
+ case 'skills.list': {
560
+ // M6 read-first: list metadata/origin only, no mutation, no body content.
561
+ // Constrained to the resolved default maestro-skills checkout — the RPC
562
+ // must NOT honor an arbitrary client-supplied dir/roots (that would let a
563
+ // payload read any directory). The client sends {} and relies on the default.
564
+ try {
565
+ const def = resolveDefaultMaestroSkillsDir()
566
+ if (def) {
567
+ const entries = listSkillsSync(def, 'maestro-skills')
568
+ return { ok: true, entries }
569
+ }
570
+ return { ok: true, entries: [] }
571
+ } catch (e: any) {
572
+ return { ok: false, error: e?.message ?? String(e) }
573
+ }
574
+ }
575
+ default:
576
+ return { ok: false, error: `unknown endpoint ${endpoint}` }
577
+ }
578
+ }
579
+ const handler = async (endpoint: string, payload: unknown, _signal: AbortSignal) => ({
580
+ ok: true as const,
581
+ value: await legacyHandler(endpoint, payload),
582
+ })
583
+ // ctx.connection may be undefined in tests; guard
584
+ const conn = (ctx as any).connection ?? (ctx.get && ctx.get('connection'))
585
+ if (!conn?.rpc?.handle) return () => {}
586
+ const dispose = conn.rpc.handle(channel, handler, { authority: 'loopback' })
587
+ return () => {
588
+ if (typeof dispose === 'function') dispose()
589
+ }
590
+ }, 'maestro-memory: rpc')
591
+ }