@yolk_vat-y/dsh-project-memory 0.4.4 → 0.5.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.
@@ -0,0 +1,390 @@
1
+ // PR 3 服务器侧:/insight 命令(记忆视图:task 草稿 / project / global)
2
+ // 动作:list / confirm / promote / demote / archive / restore / delete
3
+ // 纯函数 actInsight / listInsights 操作传入的 store/gs 内存对象,持久化由调用方
4
+ // (命令 handler:store.commit + gs.commit)统一负责,便于单测。
5
+ import { ProjectMemoryStore } from '../store.js'
6
+ import { memoryRootFor } from '../util/fs.js'
7
+ import { cfgInsight, GlobalStore, normalizeInsight, defaultGlobalFile, saveInsight, INSIGHT_KINDS } from '../insight-store.js'
8
+ import { projectRootFor } from '../setup/taskbridge.js'
9
+
10
+ function insSummary(ins, extra = {}) {
11
+ const s = ins || {}
12
+ return {
13
+ id: s.id,
14
+ kind: s.kind || 'lesson',
15
+ scope: s.scope || 'project',
16
+ title: s.title || '',
17
+ // 摘要按 kind 暴露“非标题正文”,编辑表单据此精确预填(不再用单一 body 猜)
18
+ fix: s.fix && s.fix !== s.title ? s.fix : undefined,
19
+ solution: s.solution && s.solution !== s.title ? s.solution : undefined,
20
+ reason: s.reason && s.reason !== s.title ? s.reason : undefined,
21
+ pattern: s.pattern && s.pattern !== s.title ? s.pattern : undefined,
22
+ problem: s.problem && s.problem !== s.title ? s.problem : undefined,
23
+ choice: s.choice && s.choice !== s.title ? s.choice : undefined,
24
+ body: (() => {
25
+ if (s.kind === 'procedure' && Array.isArray(s.steps) && s.steps.length) {
26
+ return `步骤 ${s.steps.length} 条:${String(s.steps[0]).slice(0, 80)}`
27
+ }
28
+ const byKind = s.kind === 'lesson' ? s.fix : s.kind === 'decision' ? s.reason : s.kind === 'experience' ? s.solution : s.body
29
+ const cands = [byKind, s.fix, s.solution, s.reason, s.body, s.choice, s.pattern, s.problem]
30
+ for (const v of cands) if (v && v !== s.title) return v
31
+ return ''
32
+ })(),
33
+ draft: s.draft === true,
34
+ archived: s.archived === true,
35
+ confidence: typeof s.confidence === 'number' ? s.confidence : null,
36
+ hitCount: s.hitCount || 0,
37
+ members: (s.sourceTaskIds || []).length,
38
+ files: (s.files || []).length,
39
+ symbols: (s.symbols || []).length,
40
+ trigger: s.trigger ? { keywords: s.trigger.keywords || [], scope: s.trigger.scope || [] } : null,
41
+ steps: Array.isArray(s.steps) ? s.steps.slice(0, 50) : null,
42
+ updatedAt: s.updatedAt || '',
43
+ ...extra,
44
+ }
45
+ }
46
+
47
+ function findTaskInsight(store, id) {
48
+ for (const t of store.getTasks() || []) {
49
+ const hit = (t.insights || []).find((i) => i.id === id)
50
+ if (hit) return { task: t, insight: hit }
51
+ }
52
+ return null
53
+ }
54
+
55
+ /** 列出某 scope 的记忆(task 聚合所有任务草稿;project/global 各自池)。 */
56
+ export function listInsights(store, gs, scope) {
57
+ const s = scope || 'project'
58
+ const items = []
59
+ if (s === 'task') {
60
+ for (const t of store.getTasks() || []) {
61
+ for (const i of t.insights || []) items.push(insSummary(i, { taskId: t.id, taskTitle: t.title }))
62
+ }
63
+ } else if (s === 'global') {
64
+ for (const i of (gs ? gs.items() : [])) items.push(insSummary(i))
65
+ } else {
66
+ for (const i of store.insightItems()) items.push(insSummary(i))
67
+ }
68
+ return { scope: s, count: items.length, items }
69
+ }
70
+
71
+ /**
72
+ * 记忆动作(内存变更)。返回 { ok, text };失败不动任何数据。
73
+ * action: confirm(draft→正式) | promote(向上移一级) | demote(向下移一级) |
74
+ * archive | restore | delete;scope: task|project|global(列表里条目的当前 scope)。
75
+ * demote 到 task 需要 taskId;promote/global 之上无层级时返回错误。
76
+ */
77
+ export function actInsight({ store, gs, action, scope, id, taskId, cfg }) {
78
+ const act = action || ''
79
+ const s = scope || 'project'
80
+ if (!id) return { ok: false, text: '需要 id' }
81
+ const invalid = () => ({ ok: false, text: `insight ${id} 不在 ${s} 层(可能已移动/删除)` })
82
+
83
+ if (s === 'task') {
84
+ const found = findTaskInsight(store, id)
85
+ if (!found) return invalid()
86
+ const { task, insight } = found
87
+ if (act === 'confirm') {
88
+ insight.draft = false
89
+ insight.updatedAt = new Date().toISOString()
90
+ store.markTasksDirty()
91
+ return { ok: true, text: `已审核:${insight.title}` }
92
+ }
93
+ if (act === 'delete') {
94
+ task.insights = (task.insights || []).filter((i) => i.id !== id)
95
+ store.markTasksDirty()
96
+ return { ok: true, text: `已删除任务级条目 ${id}` }
97
+ }
98
+ if (act === 'archive' || act === 'restore') {
99
+ insight.archived = act === 'archive'
100
+ insight.updatedAt = new Date().toISOString()
101
+ store.markTasksDirty()
102
+ return { ok: true, text: `已${act === 'archive' ? '归档' : '恢复'}` }
103
+ }
104
+ if (act === 'promote') {
105
+ // task → project(手动提升,无视自动阈值)
106
+ const items = store.insightItems()
107
+ const copy = normalizeInsight(insight, { scope: 'project', source: insight.source, nowIso: new Date().toISOString() })
108
+ copy.id = id
109
+ copy.draft = false
110
+ copy.confidence = Math.max(insight.confidence || 0, (cfg && cfg.promoteConfidence) || 0.7)
111
+ copy.movedFrom = { scope: 'task', id, at: new Date().toISOString() }
112
+ copy.createdAt = insight.createdAt || copy.createdAt
113
+ copy.hitCount = insight.hitCount || 0
114
+ items.push(copy)
115
+ store.replaceInsightItems(items)
116
+ task.insights = (task.insights || []).filter((i) => i.id !== id)
117
+ store.markTasksDirty()
118
+ return { ok: true, text: `已提升到 project:${copy.title}` }
119
+ }
120
+ return { ok: false, text: `task 级不支持动作: ${act}` }
121
+ }
122
+
123
+ if (s === 'project') {
124
+ const items = store.insightItems()
125
+ const idx = items.findIndex((i) => i.id === id)
126
+ if (idx === -1) return invalid()
127
+ const ins = items[idx]
128
+ if (act === 'archive' || act === 'restore') {
129
+ ins.archived = act === 'archive'
130
+ ins.updatedAt = new Date().toISOString()
131
+ store.replaceInsightItems(items)
132
+ return { ok: true, text: `已${act === 'archive' ? '归档' : '恢复'}` }
133
+ }
134
+ if (act === 'delete') {
135
+ items.splice(idx, 1)
136
+ store.replaceInsightItems(items)
137
+ return { ok: true, text: `已删除 project 条目 ${id}` }
138
+ }
139
+ if (act === 'promote') {
140
+ // project → global
141
+ if (!gs) return { ok: false, text: 'global 存储不可用' }
142
+ const now = new Date().toISOString()
143
+ const copy = normalizeInsight(ins, { scope: 'global', source: ins.source, nowIso: now })
144
+ copy.id = id
145
+ copy.draft = false
146
+ copy.movedFrom = { scope: 'project', id, at: now }
147
+ copy.createdAt = ins.createdAt || copy.createdAt
148
+ copy.hitCount = ins.hitCount || 0
149
+ gs.items().push(copy)
150
+ gs.markDirty()
151
+ items.splice(idx, 1)
152
+ store.replaceInsightItems(items)
153
+ return { ok: true, text: `已提升到 global:${copy.title}` }
154
+ }
155
+ if (act === 'demote') {
156
+ // project → task(需目标任务)
157
+ const task = store.getTask(taskId)
158
+ if (!task) return { ok: false, text: '降级到任务需要 taskId' }
159
+ const now = new Date().toISOString()
160
+ const copy = normalizeInsight(ins, { scope: 'task', source: ins.source, nowIso: now })
161
+ copy.id = `${id}_d${Date.now().toString(36)}`
162
+ copy.movedFrom = { scope: 'project', id, at: now }
163
+ copy.createdAt = ins.createdAt || copy.createdAt
164
+ if (!Array.isArray(task.insights)) task.insights = []
165
+ task.insights.push(copy)
166
+ store.markTasksDirty()
167
+ items.splice(idx, 1)
168
+ store.replaceInsightItems(items)
169
+ return { ok: true, text: `已降级到任务 ${task.title}` }
170
+ }
171
+ return { ok: false, text: `project 级不支持动作: ${act}` }
172
+ }
173
+
174
+ if (s === 'global') {
175
+ if (!gs) return { ok: false, text: 'global 存储不可用' }
176
+ const items = gs.items()
177
+ const idx = items.findIndex((i) => i.id === id)
178
+ if (idx === -1) return invalid()
179
+ const ins = items[idx]
180
+ if (act === 'archive' || act === 'restore') {
181
+ ins.archived = act === 'archive'
182
+ ins.updatedAt = new Date().toISOString()
183
+ gs.markDirty()
184
+ return { ok: true, text: `已${act === 'archive' ? '归档' : '恢复'}` }
185
+ }
186
+ if (act === 'delete') {
187
+ items.splice(idx, 1)
188
+ gs.markDirty()
189
+ return { ok: true, text: `已删除 global 条目 ${id}` }
190
+ }
191
+ if (act === 'demote') {
192
+ // global → project
193
+ const now = new Date().toISOString()
194
+ const pjItems = store.insightItems()
195
+ const copy = normalizeInsight(ins, { scope: 'project', source: ins.source, nowIso: now })
196
+ copy.id = `${id}_d${Date.now().toString(36)}`
197
+ copy.movedFrom = { scope: 'global', id, at: now }
198
+ copy.createdAt = ins.createdAt || copy.createdAt
199
+ pjItems.push(copy)
200
+ store.replaceInsightItems(pjItems)
201
+ items.splice(idx, 1)
202
+ gs.markDirty()
203
+ return { ok: true, text: `已降级到 project:${copy.title}` }
204
+ }
205
+ return { ok: false, text: `global 级不支持动作: ${act}` }
206
+ }
207
+ return { ok: false, text: `未知 scope: ${s}` }
208
+ }
209
+
210
+ /** 新建/保存记忆(project/global)。内部走与 save_lesson 同一 saveInsight 语义(去重合并)。 */
211
+ export function saveMemoryItem({ store, gs, cfg, scope, fields }) {
212
+ const s = scope === 'global' ? 'global' : 'project'
213
+ if (s === 'global' && !gs) return { ok: false, text: 'global 存储不可用' }
214
+ const res = saveInsight({ store, globalStore: gs, raw: fields, scope: s, cfg, source: 'user' })
215
+ if (!res.ok) return res
216
+ return { ok: true, text: `${s} 记忆已保存(${res.action})` }
217
+ }
218
+
219
+ const EDIT_FIELDS = new Set(['title', 'kind', 'body', 'pattern', 'fix', 'problem', 'solution', 'choice', 'reason', 'confidence', 'steps', 'trigger', 'tags', 'files', 'symbols'])
220
+
221
+ // kind 各自的“内容字段”;切 kind 时清掉不再属于它的旧字段(否则旧经验文本会残留显示)
222
+ const KIND_CONTENT = {
223
+ lesson: ['pattern', 'fix'],
224
+ decision: ['choice', 'reason'],
225
+ procedure: ['steps', 'trigger'],
226
+ experience: ['problem', 'solution'],
227
+ }
228
+ const ALL_CONTENT = ['body', 'pattern', 'fix', 'problem', 'solution', 'choice', 'reason', 'steps', 'trigger']
229
+
230
+ /** 编辑既有条目(title/kind/正文/trigger 等白名单字段)。任务级按 id 在任务池定位。 */
231
+ export function editMemoryItem({ store, gs, scope, id, fields }) {
232
+ if (!fields || typeof fields !== 'object') return { ok: false, text: 'edit 需要字段对象' }
233
+ const applyTo = (item) => {
234
+ let changed = false
235
+ const oldKind = item.kind
236
+ for (const key of Object.keys(fields)) {
237
+ const v = fields[key]
238
+ if (!EDIT_FIELDS.has(key)) continue
239
+ if (key === 'kind') {
240
+ if (!INSIGHT_KINDS.includes(v) || v === item.kind) continue
241
+ item.kind = v
242
+ changed = true
243
+ continue
244
+ }
245
+ if (key === 'trigger') {
246
+ if (!v || typeof v !== 'object') continue
247
+ item.trigger = {
248
+ keywords: Array.isArray(v.keywords) ? v.keywords.map(String) : [],
249
+ ...(Array.isArray(v.scope) ? { scope: v.scope.map(String) } : {}),
250
+ }
251
+ changed = true
252
+ continue
253
+ }
254
+ if (key === 'steps' || key === 'tags' || key === 'files' || key === 'symbols') {
255
+ if (!Array.isArray(v)) continue
256
+ item[key] = v.map(String)
257
+ changed = true
258
+ continue
259
+ }
260
+ if (key === 'confidence') {
261
+ const n = Number(v)
262
+ if (!Number.isFinite(n)) continue
263
+ item.confidence = Math.max(0, Math.min(1, n))
264
+ changed = true
265
+ continue
266
+ }
267
+ const s = v === null || v === undefined ? '' : String(v)
268
+ if (key === 'title' && !s.trim()) continue
269
+ item[key] = s
270
+ changed = true
271
+ }
272
+ // 切 kind → 清理不再属于新 kind 的旧内容字段
273
+ if (changed && item.kind !== oldKind && KIND_CONTENT[item.kind]) {
274
+ const keep = new Set(KIND_CONTENT[item.kind])
275
+ const isProcedure = item.kind === 'procedure'
276
+ for (const f of ALL_CONTENT) {
277
+ if (keep.has(f)) continue
278
+ if (isProcedure && f === 'body') continue // procedure 保留通用 body
279
+ delete item[f]
280
+ }
281
+ }
282
+ if (changed) item.updatedAt = new Date().toISOString()
283
+ return changed
284
+ }
285
+ const s = scope || 'project'
286
+ if (s === 'task') {
287
+ const found = findTaskInsight(store, id)
288
+ if (!found) return { ok: false, text: `insight ${id} 不在任务层` }
289
+ if (!applyTo(found.insight)) return { ok: false, text: '没有可更新的字段' }
290
+ store.markTasksDirty()
291
+ return { ok: true, text: '已更新任务级记忆' }
292
+ }
293
+ const items = s === 'global' ? (gs ? gs.items() : null) : store.insightItems()
294
+ if (!items) return { ok: false, text: '存储不可用' }
295
+ const item = items.find((i) => i.id === id)
296
+ if (!item) return { ok: false, text: `insight ${id} 不在 ${s} 层` }
297
+ if (!applyTo(item)) return { ok: false, text: '没有可更新的字段' }
298
+ if (s === 'global') gs.markDirty()
299
+ else store.replaceInsightItems(items)
300
+ return { ok: true, text: '已更新记忆条目' }
301
+ }
302
+
303
+ function jsonText(payload, note) {
304
+ return `${note}\n\n\`\`\`json\n${JSON.stringify(payload)}\n\`\`\``
305
+ }
306
+
307
+ /** /insight <verb> <args…>:list [task|project|global] | confirm/promote/demote/archive/restore/delete <id> [taskId] */
308
+ export function insightCommandDefinition(config, ctx) {
309
+ return {
310
+ name: 'insight',
311
+ description: '记忆视图动作(面板按钮调用):/insight list project|global|task;/insight <confirm|promote|demote|archive|restore|delete> <scope> <id> [taskId]',
312
+ input: { hint: 'list [scope] | <动作> <scope> <id> [taskId]' },
313
+ handler: (invocation) => {
314
+ try {
315
+ const agent = invocation?.agent
316
+ const sid = agent?.id || agent?.session?.id
317
+ const session = sid && agent?.ctx ? agent.ctx.sessions?.get(sid) : (agent?.session || null)
318
+ const cwd = session?.header?.cwd || agent?.session?.header?.cwd
319
+ const lineRaw = (invocation?.rawInput || '').trim()
320
+ const raw = lineRaw.split(/\s+/).filter(Boolean)
321
+ const root = projectRootFor(cwd)
322
+ if (!raw[0]) return { kind: 'error', text: '[insight] 用法见面板' }
323
+ const verb = raw[0]
324
+ if (verb === 'list') {
325
+ const scope = raw[1] === 'task' || raw[1] === 'global' || raw[1] === 'project' ? raw[1] : 'project'
326
+ const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
327
+ const gs = new GlobalStore(cfgInsight(config).globalFile || defaultGlobalFile()).load()
328
+ const payload = listInsights(store, gs, scope)
329
+ return { kind: 'success', text: jsonText(payload, `记忆 ${scope}:${payload.count} 条`) }
330
+ }
331
+ if (verb === 'save') {
332
+ // /insight save <scope> <json>(新建/合并;project|global)
333
+ const scope = raw[1]
334
+ if (scope !== 'project' && scope !== 'global') return { kind: 'error', text: '[insight] save 仅支持 project|global 作用域' }
335
+ const scopeIdx = lineRaw.indexOf(scope, verb.length)
336
+ const json = scopeIdx === -1 ? '' : lineRaw.slice(scopeIdx + scope.length).trim()
337
+ let fields
338
+ try {
339
+ fields = JSON.parse(json)
340
+ } catch {
341
+ return { kind: 'error', text: '[insight] save 需要 JSON 参数(由面板生成)' }
342
+ }
343
+ if (!fields || typeof fields !== 'object' || (!fields.title && !fields.pattern && !fields.topic)) {
344
+ return { kind: 'error', text: '[insight] save JSON 需要 title/pattern/topic' }
345
+ }
346
+ const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
347
+ const gs = new GlobalStore(cfgInsight(config).globalFile || defaultGlobalFile()).load()
348
+ const res = saveMemoryItem({ store, gs, cfg: cfgInsight(config), scope, fields })
349
+ store.commit(() => 0)
350
+ gs.commit()
351
+ return res.ok ? { kind: 'success', text: res.text } : { kind: 'error', text: res.text }
352
+ }
353
+ if (verb === 'edit') {
354
+ // /insight edit <scope> <id> <json>
355
+ const scope = raw[1]
356
+ const id = raw[2]
357
+ if (!id) return { kind: 'error', text: '[insight] edit 需要 id' }
358
+ const at = lineRaw.indexOf(id, 0)
359
+ const json = at === -1 ? '' : lineRaw.slice(at + id.length).trim()
360
+ let fields
361
+ try {
362
+ fields = JSON.parse(json)
363
+ } catch {
364
+ return { kind: 'error', text: '[insight] edit 需要 JSON 参数(由面板生成)' }
365
+ }
366
+ const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
367
+ const gs = new GlobalStore(cfgInsight(config).globalFile || defaultGlobalFile()).load()
368
+ const res = editMemoryItem({ store, gs, scope, id, fields })
369
+ store.commit(() => 0)
370
+ gs.commit()
371
+ return res.ok ? { kind: 'success', text: res.text } : { kind: 'error', text: res.text }
372
+ }
373
+ const scope = raw[1]
374
+ const id = raw[2]
375
+ const taskId = raw[3]
376
+ if (!['confirm', 'promote', 'demote', 'archive', 'restore', 'delete'].includes(verb) || !id) {
377
+ return { kind: 'error', text: '[insight] 用法: /insight <动作|save|edit> …' }
378
+ }
379
+ const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
380
+ const gs = new GlobalStore(cfgInsight(config).globalFile || defaultGlobalFile()).load()
381
+ const res = actInsight({ store, gs, action: verb, scope, id, taskId, cfg: cfgInsight(config) })
382
+ store.commit(() => 0) // 项目侧(tasks/insights)落盘
383
+ gs.commit() // global 侧若有变更落盘
384
+ return res.ok ? { kind: 'success', text: res.text } : { kind: 'error', text: res.text }
385
+ } catch (err) {
386
+ return { kind: 'error', text: `[insight] ${err?.message || err}` }
387
+ }
388
+ },
389
+ }
390
+ }
@@ -6,6 +6,7 @@ import { memoryRootFor } from '../util/fs.js'
6
6
  import { ProjectMemoryStore } from '../store.js'
7
7
  import { projectRootFor, adoptStepsToSession, shouldAdoptToHost } from '../setup/taskbridge.js'
8
8
  import { renderTaskSnapshot, buildTaskPayload } from './tasks.js'
9
+ import { fireReflect } from '../reflection-pipeline.js'
9
10
 
10
11
  const VERBS = { switch: 'switch', archive: 'archive' }
11
12
 
@@ -127,6 +128,7 @@ export function taskCommandDefinition(config, ctx) {
127
128
  if (verb === VERBS.switch) {
128
129
  const task = store.getTask(taskId)
129
130
  if (!task) return { kind: 'error', text: `[task] 找不到任务: ${taskId}(/tasks 查看)` }
131
+ const prevBound = sid ? store.getBoundTaskId(sid) : null
130
132
  if (task.archived) task.archived = false
131
133
  if (sid) {
132
134
  store.setBinding(sid, task.id)
@@ -134,6 +136,10 @@ export function taskCommandDefinition(config, ctx) {
134
136
  task.updatedAt = new Date().toISOString()
135
137
  task.lastActiveAt = task.updatedAt
136
138
  store.save()
139
+ // 反思钩子:切走旧任务异步收割(默认关)
140
+ if (prevBound && prevBound !== task.id) {
141
+ fireReflect(config, { llm: ctx?.llm }, root, prevBound, 'switch-away')
142
+ }
137
143
  // 反向接管:切换成功后把任务步骤推成宿主 todo/write(dsh 清单跟随)
138
144
  if (shouldAdoptToHost(config)) {
139
145
  adoptStepsToSession(session, task)
@@ -149,6 +155,8 @@ export function taskCommandDefinition(config, ctx) {
149
155
  task.archived = true
150
156
  task.updatedAt = new Date().toISOString()
151
157
  store.save()
158
+ // 反思钩子:归档即收割(默认关)
159
+ fireReflect(config, { llm: ctx?.llm }, root, taskId, 'archive')
152
160
  const note = `已归档: ${describeTask(task)}(select_task 可恢复)`
153
161
  return withTaskSnapshot(config, cwd, sid, store, note)
154
162
  }
@@ -25,6 +25,7 @@ export function buildTaskPayload(tasks, boundId, archived) {
25
25
  lastActiveAt: t.lastActiveAt,
26
26
  updatedAt: t.updatedAt,
27
27
  archived: t.archived || false,
28
+ insights: (t.insights || []).filter((i) => !i.archived).map((i) => ({ id: i.id, kind: i.kind || 'lesson', title: i.title || '', draft: i.draft === true })),
28
29
  })),
29
30
  boundId: boundId || null,
30
31
  archived,
@@ -0,0 +1,41 @@
1
+ // global.json 幂等初始化与种子(v0.5)
2
+ // - ensureGlobalInit:确保文档结构与文件存在(幂等,仅补缺失 id)
3
+ // - 不做任何内置"个人经验"种子(WSL 发包等属用户数据,由用户/测试提供 seeds)
4
+ import { GlobalStore, defaultGlobalFile } from './insight-store.js'
5
+
6
+ /**
7
+ * 确保 global 存储文件/结构就绪。
8
+ * @param {object} [config] plugin config(insight.globalFile 可覆盖默认路径)
9
+ * @param {Array<object>} [seeds] 种子条目(需含稳定 id);只补缺失 id,绝不覆盖
10
+ * @returns {{ store: GlobalStore, file: string, created: boolean, seeded: number }}
11
+ */
12
+ export function ensureGlobalInit(config = {}, seeds = []) {
13
+ const c = (config.insight || {})
14
+ const file = typeof c.globalFile === 'string' && c.globalFile ? c.globalFile : defaultGlobalFile()
15
+ const store = new GlobalStore(file).load()
16
+ let created = false
17
+ if (!store.doc.createdAt) {
18
+ store.doc.createdAt = new Date().toISOString()
19
+ created = true
20
+ }
21
+ let seeded = 0
22
+ for (const seed of seeds || []) {
23
+ if (!seed || !seed.id) continue
24
+ if (store.items().some((it) => it.id === seed.id)) continue
25
+ const now = new Date().toISOString()
26
+ store.items().push({
27
+ kind: 'procedure',
28
+ ...seed,
29
+ scope: seed.scope || 'global',
30
+ source: seed.source || 'seed',
31
+ createdAt: seed.createdAt || now,
32
+ updatedAt: seed.updatedAt || now,
33
+ })
34
+ seeded++
35
+ }
36
+ if (created || seeded) store.markDirty()
37
+ store.commit(() => undefined)
38
+ return { store, file, created, seeded }
39
+ }
40
+
41
+ export { defaultGlobalFile }
package/src/index.js CHANGED
@@ -11,8 +11,11 @@ import { setupLazyIndexing } from './lazy.js'
11
11
  import { initTypeScript } from './enhancer.js'
12
12
  import { setupTaskbridge } from './setup/taskbridge.js'
13
13
  import { listTasksTool, selectTaskTool, archiveTaskTool, showTaskPanelTool } from './tools/task-tools.js'
14
+ import { lessonTool } from './tools/lesson-tools.js'
15
+ import { installAutoInject } from './auto-inject.js'
14
16
  import { tasksCommandDefinition } from './commands/tasks.js'
15
17
  import { taskCommandDefinition } from './commands/task-actions.js'
18
+ import { insightCommandDefinition } from './commands/insight-actions.js'
16
19
 
17
20
  export const name = 'dsh-project-memory'
18
21
  export const inject = ['llm', 'tools']
@@ -37,6 +40,31 @@ export const Config = Schema.object({
37
40
  // 任务成为会话绑定(select_task / /task switch)时,把任务步骤推成宿主 todo/write 快照
38
41
  syncHostOnAdopt: Schema.boolean().default(true),
39
42
  }).default({}),
43
+ // v0.5 单一 insight 实体:分层去重/强化/提升/容量/归档(详见 PLAN-v0.5.0.md)
44
+ insight: Schema.object({
45
+ dedupOverlap: Schema.number().default(0.7),
46
+ reinforceBand: Schema.number().default(0.65),
47
+ maxProject: Schema.number().default(100),
48
+ maxGlobalProcedures: Schema.number().default(200),
49
+ promoteConfidence: Schema.number().default(0.7),
50
+ globalPromoteTasks: Schema.number().default(3),
51
+ decayDays: Schema.number().default(90),
52
+ globalFile: Schema.string(),
53
+ }).default({}),
54
+ // PR 1b:反思管线(LLM 消费点,默认关)。产出只写 task 级草稿,见 PLAN §3
55
+ reflection: Schema.object({
56
+ enabled: Schema.boolean().default(false),
57
+ cooldownMs: Schema.number().default(1800000),
58
+ maxLessonsPerReflect: Schema.number().default(3),
59
+ maxDecisionsPerReflect: Schema.number().default(2),
60
+ }).default({}),
61
+ // PR 2:静默注入(entry 常驻块 + relevance 门控)。无项目 root 可解析时零副作用
62
+ autoContext: Schema.object({
63
+ enabled: Schema.boolean().default(true),
64
+ entryOn: Schema.boolean().default(true),
65
+ maxTokens: Schema.number().default(400),
66
+ relevanceMin: Schema.number().default(0.25),
67
+ }).default({}),
40
68
  })
41
69
 
42
70
  export function apply(ctx, config) {
@@ -61,18 +89,19 @@ export function apply(ctx, config) {
61
89
  // TaskBridge:任务实体 + 宿主 todo 同步
62
90
  setupTaskbridge(ctx, config)
63
91
  ctx.tools.register(listTasksTool(config))
64
- ctx.tools.register(selectTaskTool(config))
65
- ctx.tools.register(archiveTaskTool(config))
92
+ ctx.tools.register(selectTaskTool(config, { llm: ctx.llm, ctx }))
93
+ ctx.tools.register(archiveTaskTool(config, { llm: ctx.llm, ctx }))
66
94
  ctx.tools.register(showTaskPanelTool(config))
67
95
 
68
- // /tasks、/task 用户命令(宿主 commands 服务存在时注册,feature-detect 降级)
96
+ // /tasks、/task、/insight 用户命令(宿主 commands 服务存在时注册,feature-detect 降级)
69
97
  try {
70
98
  ctx.inject(['commands'], (commandsCtx) => {
71
99
  commandsCtx.commands.register(tasksCommandDefinition(config, ctx))
72
100
  commandsCtx.commands.register(taskCommandDefinition(config, ctx))
101
+ commandsCtx.commands.register(insightCommandDefinition(config, ctx))
73
102
  })
74
103
  } catch (err) {
75
- console.error(`[dsh-project-memory] /tasks,/task registration skipped: ${err.message}`)
104
+ console.error(`[dsh-project-memory] /tasks,/task,/insight registration skipped: ${err.message}`)
76
105
  }
77
106
 
78
107
  ctx.tools.register(indexDocTool(ctx, config))
@@ -80,9 +109,14 @@ export function apply(ctx, config) {
80
109
  ctx.tools.register(queryMemoryTool(ctx, config))
81
110
  ctx.tools.register(rememberTool(config))
82
111
  ctx.tools.register(forgetTool(config))
112
+ ctx.tools.register(lessonTool(config))
83
113
  ctx.tools.register(watchRepoTool(watchManager, config))
84
114
  ctx.tools.register(statsTool(config))
85
115
 
116
+ // PR 2:注册 agent/pre-step 监听,向每步请求的 enter 决策追加记忆消息(默认开;
117
+ // 任何异常/无会话 cwd → 交回默认决策,零副作用)
118
+ installAutoInject(ctx, config)
119
+
86
120
  if (config.autoIndexOnFirstUse) {
87
121
  ctx.effect(async () => {
88
122
  const root = process.cwd()