cs-bots 0.1.1 → 0.7.5

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 (48) hide show
  1. package/CHANGELOG.md +539 -0
  2. package/README.md +65 -10
  3. package/docs/01-/346/236/266/346/236/204/346/226/207/346/241/243.md +364 -0
  4. package/docs/02-/351/234/200/346/261/202/350/247/204/346/240/274/344/271/246.md +284 -0
  5. package/docs/03-/350/257/246/347/273/206/350/256/276/350/256/241.md +567 -0
  6. package/docs/04-/346/223/215/344/275/234/346/211/213/345/206/214.md +569 -0
  7. package/docs/ARCHITECTURE.md +277 -0
  8. package/docs/adr/ADR-001-/351/241/265/347/255/276/347/273/223/346/236/204/345/233/236/345/210/260/344/270/203/351/241/265.md +70 -0
  9. package/docs/steps/step-01-/345/216/237/346/236/266/346/236/204/345/244/215/345/216/237/344/270/216IA/350/257/212/346/226/255.md +80 -0
  10. package/docs/steps/step-02-/351/241/265/347/255/276/346/224/266/346/225/233/344/270/216/345/215/207/347/272/247/351/200/232/351/201/223.md +51 -0
  11. package/docs/steps/step-03-/346/236/266/346/236/204/350/265/204/346/226/231/346/210/220/346/226/207.md +16 -0
  12. package/docs/steps/step-04-/344/270/212/347/272/277/344/270/216/351/252/214/350/257/201.md +46 -0
  13. package/docs//344/272/244/344/273/230/350/257/264/346/230/216-v0.2.0.md +95 -0
  14. package/docs//346/223/215/344/275/234/346/211/213/345/206/214-v1.md +509 -0
  15. package/docs//350/277/201/347/247/273/346/226/271/346/241/210-v1.md +64 -0
  16. package/docs//351/225/277/346/234/237/350/256/260/345/277/206/346/226/271/346/241/210-v1.md +85 -0
  17. package/package.json +4 -2
  18. package/src/admission.js +365 -0
  19. package/src/api.js +4097 -420
  20. package/src/board.js +883 -14
  21. package/src/botprofile.js +190 -3
  22. package/src/client.js +6596 -1082
  23. package/src/cycle.js +516 -0
  24. package/src/delivery-watch.js +255 -0
  25. package/src/dirbrowse.js +20 -0
  26. package/src/discipline.js +1188 -0
  27. package/src/fence.js +142 -0
  28. package/src/index.js +387 -4
  29. package/src/kernel.js +636 -24
  30. package/src/library.js +1224 -11
  31. package/src/memory.js +617 -37
  32. package/src/metrics.js +339 -0
  33. package/src/prompts.js +1318 -0
  34. package/src/reviewer.js +709 -0
  35. package/src/room.js +655 -14
  36. package/src/roster.js +346 -26
  37. package/src/routing/complexity.js +320 -0
  38. package/src/routing/hints.js +707 -0
  39. package/src/scratch.js +477 -0
  40. package/src/settings.js +97 -0
  41. package/src/stage.js +263 -0
  42. package/src/store.js +133 -1
  43. package/src/subscription.js +571 -0
  44. package/src/suggest.js +1203 -0
  45. package/src/teams.js +527 -0
  46. package/src/todo.js +1301 -0
  47. package/src/tools.js +734 -10
  48. package/src/verify.js +356 -0
package/src/cycle.js ADDED
@@ -0,0 +1,516 @@
1
+ /**
2
+ * cs-bots CycleService —— PDCA 复盘与标准化(Act)。
3
+ *
4
+ * 为什么需要它:PDCA 的「Act」此前没有落点 —— 周期结束后经验无处沉淀,
5
+ * 下次照样踩同一个坑。本模块给一个任务绑定一个「周期」,强制走完
6
+ * planned → doing → checking → acting → done 四步,**不许跳步**(跳步即抛
7
+ * `CYCLE_BAD_PHASE_FLOW`)—— 这正是「持续优化」的纪律。
8
+ *
9
+ * 复盘(review)会把 lessons 提议成记忆建议、standards 提议成纪律建议,
10
+ * 走既有的「建议确认制」:**模型只提议,人点采纳才落库**。suggest 缺失或抛错时
11
+ * 降级为「只记复盘内容」,绝不阻断、绝不假装建议已提。
12
+ *
13
+ * 不变量:事件流真相源 / 只追加 / 绝不静默失败 / CAS。
14
+ * 依赖面:node 内置 + `./store.js` + `./kernel.js`。零第三方依赖。
15
+ */
16
+
17
+ import { appendJsonl, readJsonl, statePath, writeJsonAtomic } from './store.js'
18
+
19
+ let kernel = null
20
+ let kernelError = null
21
+ try {
22
+ kernel = await import('./kernel.js')
23
+ } catch (error) {
24
+ kernelError = error
25
+ }
26
+
27
+ /**
28
+ * 周期类型(需求4,2026-11-08)。
29
+ *
30
+ * 用户原话:「我要求 pdca 是对用户长任务进行复盘跟进的,人类主导;
31
+ * 智能体的 pdca 流程你优化一下,看看有没有必要或优化,
32
+ * 我关心的是用户能否对长任务进行管理,智能体的 pdca 需要定期归纳提升」
33
+ *
34
+ * ⇒ 两种周期,语义完全不同,**不能混在一起**:
35
+ *
36
+ * | kind | 谁主导 | 用来干什么 | 阶段纪律 |
37
+ * |---|---|---|---|
38
+ * | `human` | **人** | 跟一个长任务:目标漂移没有、卡在哪、下一步是什么 | 强制走完五步(人不许跳步,防自欺) |
39
+ * | `agent` | 智能体 | 事后归纳:同类问题是否重复出现、该沉淀什么标准 | **可以一次性提交**(见 `review` 的快捷路径) |
40
+ *
41
+ * 为什么 agent 周期不强制走五步:智能体的复盘是「批量归纳」,
42
+ * 让它假装走 planned→doing 是对事实的虚构;人需要的是「结论 + 可采纳的标准」。
43
+ * 但**归纳本身要定期发生**——由 reviewer 的回合计数驱动(见 `reviewer.js`)。
44
+ */
45
+
46
+ /** 合法周期类型。 */
47
+ export const CYCLE_KINDS = Object.freeze(['human', 'agent'])
48
+
49
+ /** 合法阶段(顺序即流转顺序)。 */
50
+ export const CYCLE_PHASES = Object.freeze(['planned', 'doing', 'checking', 'acting', 'done'])
51
+
52
+ /** 合法状态。 */
53
+ export const CYCLE_STATUSES = Object.freeze(['open', 'closed'])
54
+
55
+ /** 合法动作。 */
56
+ export const CYLE_ACTIONS = Object.freeze(['start', 'advance', 'check', 'review', 'close'])
57
+
58
+ /** 阶段流转表(下一步只允许这些):不许跳步。 */
59
+ const NEXT_PHASE = Object.freeze({
60
+ planned: 'doing',
61
+ doing: 'checking',
62
+ checking: 'acting',
63
+ acting: 'done',
64
+ })
65
+
66
+ /** 结构化错误。 */
67
+ export class CycleError extends Error {
68
+ constructor(message, code = 'CYCLE_ERROR', details) {
69
+ super(message)
70
+ this.name = 'CycleError'
71
+ this.code = code
72
+ if (details !== undefined) this.details = details
73
+ }
74
+ }
75
+
76
+ function clone(value) {
77
+ if (value === undefined || value === null) return value
78
+ return JSON.parse(JSON.stringify(value))
79
+ }
80
+
81
+ function normalizeNow(now, where) {
82
+ if (now === undefined || now === null) return Date.now()
83
+ if (typeof now !== 'number' || !Number.isFinite(now)) {
84
+ throw new CycleError(`cycle.${where}:now 必须是有限数字,收到 ${JSON.stringify(now)}`, 'CYCLE_BAD_FIELD', { where, field: 'now' })
85
+ }
86
+ return now
87
+ }
88
+
89
+ function requireString(value, field, where) {
90
+ if (typeof value !== 'string' || value.trim() === '') {
91
+ throw new CycleError(`cycle.${where}:${field} 必须是非空字符串,收到 ${JSON.stringify(value)}`, 'CYCLE_BAD_FIELD', { where, field })
92
+ }
93
+ return value.trim()
94
+ }
95
+
96
+ function requirePlainObject(value, where) {
97
+ if (value === undefined || value === null) return {}
98
+ if (typeof value !== 'object' || Array.isArray(value)) {
99
+ throw new CycleError(`cycle.${where}:必须是普通对象,收到 ${JSON.stringify(value)}`, 'CYCLE_BAD_FIELD', { where })
100
+ }
101
+ return value
102
+ }
103
+
104
+ function resolveStateDir(config) {
105
+ if (typeof config?.stateDir === 'string' && config.stateDir.trim() !== '') return config.stateDir.trim()
106
+ if (kernel && typeof kernel.stateDir === 'function') return kernel.stateDir()
107
+ const reason = kernelError?.message ?? 'kernel.stateDir 不存在'
108
+ throw new CycleError(`cycle.createCycle:既未提供 config.stateDir,也无法从 kernel.js 取得 stateDir(${reason})。`, 'CYCLE_NO_STATE_DIR')
109
+ }
110
+
111
+ function splitArgs(ctx, config) {
112
+ const cfgGiven = config && typeof config === 'object' && Object.keys(config).length > 0
113
+ if (cfgGiven) return { context: ctx ?? {}, config }
114
+ if (ctx && typeof ctx === 'object' && typeof ctx.stateDir === 'string') return { context: {}, config: ctx }
115
+ return { context: ctx ?? {}, config: config ?? {} }
116
+ }
117
+
118
+ /** 字符串数组归一(≤20 条,每条 ≤2000)。 */
119
+ function stringList(value, field, where) {
120
+ if (value === undefined || value === null) return []
121
+ if (!Array.isArray(value)) {
122
+ throw new CycleError(`cycle.${where}:${field} 必须是数组,收到 ${JSON.stringify(value)}`, 'CYCLE_BAD_FIELD', { where, field })
123
+ }
124
+ if (value.length > 20) {
125
+ throw new CycleError(`cycle.${where}:${field} 最多 20 条(收到 ${value.length})`, 'CYCLE_BAD_FIELD', { where, field })
126
+ }
127
+ return value.map((v) => {
128
+ if (typeof v !== 'string') {
129
+ throw new CycleError(`cycle.${where}:${field} 每项必须是非空字符串`, 'CYCLE_BAD_FIELD', { where, field })
130
+ }
131
+ const t = v.trim()
132
+ if (t.length === 0) throw new CycleError(`cycle.${where}:${field} 每项必须非空`, 'CYCLE_BAD_FIELD', { where, field })
133
+ if (t.length > 2000) {
134
+ throw new CycleError(`cycle.${where}:${field} 每项 ≤2000 字符(收到 ${t.length})`, 'CYCLE_BAD_FIELD', { where, field })
135
+ }
136
+ return t
137
+ })
138
+ }
139
+
140
+ function progressOf(value, where) {
141
+ if (value === undefined || value === null) return undefined
142
+ if (typeof value !== 'number' || !Number.isFinite(value) || value < 0 || value > 100) {
143
+ throw new CycleError(`cycle.${where}:progress 必须在 0-100 之间,收到 ${JSON.stringify(value)}`, 'CYCLE_BAD_FIELD', { where, field: 'progress' })
144
+ }
145
+ return value
146
+ }
147
+
148
+ /**
149
+ * 创建复盘服务。
150
+ * @param {{ logger?: { info?: Function, warn?: Function } }} [ctx]
151
+ * @param {{ stateDir?: string, name?: string, suggest?: object }} [config]
152
+ * @returns {object} cycle service
153
+ */
154
+ export function createCycle(ctx, config = {}) {
155
+ const { context, config: cfg } = splitArgs(ctx, config)
156
+ const stateDir = resolveStateDir(cfg)
157
+
158
+ const jsonlPath = statePath(stateDir, 'cycle.jsonl')
159
+ const snapPath = statePath(stateDir, 'cycle.snap.json')
160
+
161
+ const info = (...args) => { try { context?.logger?.info?.(...args) } catch { /* */ } }
162
+ const warn = (...args) => { try { context?.logger?.warn?.(...args) } catch { /* */ } }
163
+
164
+ const suggestions = cfg.suggest ?? null
165
+ const entries = new Map()
166
+ const events = []
167
+ let idCounter = 0
168
+ let disposed = false
169
+
170
+ const diagnostics = {
171
+ stateDir, jsonlPath, snapPath,
172
+ kernel: kernel ? 'loaded' : `load-failed:${kernelError?.name ?? 'unknown'}`,
173
+ hydratedFrom: null, replayedEvents: 0, badLines: 0,
174
+ snapshotWriteFailures: 0, lastReplayAt: undefined,
175
+ suggestAvailable: suggestions !== null,
176
+ suggestFailures: 0,
177
+ }
178
+
179
+ function assertAlive(where) {
180
+ if (disposed) {
181
+ throw new CycleError(`cycle.${where}:实例已 dispose(),请重新 createCycle()(拒绝在已销毁实例上静默返回空数据)`, 'CYCLE_DISPOSED', { where })
182
+ }
183
+ }
184
+
185
+ function snapshot() { return { version: 1, savedAt: Date.now(), entries: [...entries.values()] } }
186
+
187
+ function commit(action, at, by, before, after) {
188
+ if (!appendJsonl(jsonlPath, { id: after.id, rev: after.rev, action, by, at, before, after })) {
189
+ throw new CycleError(`cycle.${action}:事件追加失败(${jsonlPath})—— 拒绝「只改内存不落盘」`, 'CYCLE_APPEND_FAILED', { action })
190
+ }
191
+ entries.set(after.id, clone(after))
192
+ events.push({ id: after.id, rev: after.rev, action, by, at, before: before ? clone(before) : null, after: clone(after) })
193
+ if (!writeJsonAtomic(snapPath, snapshot())) {
194
+ diagnostics.snapshotWriteFailures += 1
195
+ warn(`[cs-bots/cycle] 快照写入失败(${snapPath})——事件流已落盘,replay() 可恢复`)
196
+ }
197
+ }
198
+
199
+ function hydrate() {
200
+ entries.clear(); events.length = 0; idCounter = 0
201
+ const { rows, bad } = readJsonl(jsonlPath)
202
+ diagnostics.badLines = bad
203
+ diagnostics.replayedEvents = rows.length
204
+ if (bad > 0) warn(`[cs-bots/cycle] ${jsonlPath} 有 ${bad} 行无法解析——已跳过,请核对`)
205
+ if (rows.length === 0) { diagnostics.hydratedFrom = 'empty'; return }
206
+ let skipped = 0
207
+ for (const ev of rows) {
208
+ if (!ev || typeof ev.id !== 'string' || !ev.after) { skipped += 1; continue }
209
+ const current = entries.get(ev.id)
210
+ if (current && Number.isFinite(ev.rev) && ev.rev <= current.rev) { skipped += 1; continue }
211
+ entries.set(ev.id, clone(ev.after)); events.push(clone(ev))
212
+ const m = /^c-(\d+)$/.exec(ev.id)
213
+ if (m) idCounter = Math.max(idCounter, Number(m[1]))
214
+ }
215
+ if (skipped > 0) warn(`[cs-bots/cycle] 回放跳过 ${skipped} 条非递增/残缺事件`)
216
+ diagnostics.hydratedFrom = 'jsonl'
217
+ }
218
+
219
+ function requireTask(id, where) {
220
+ const t = entries.get(id)
221
+ if (!t) throw new CycleError(`cycle.${where}:周期 ${id} 不存在`, 'CYCLE_NOT_FOUND', { where, id })
222
+ return t
223
+ }
224
+
225
+ /** CAS:返回当前任务或抛错。 */
226
+ function cas(id, expectedRev, where) {
227
+ const t = requireTask(id, where)
228
+ if (expectedRev !== undefined && expectedRev !== null) {
229
+ if (!Number.isInteger(expectedRev)) {
230
+ throw new CycleError(`cycle.${where}:expectedRev 必须是整数(当前 rev=${t.rev})`, 'CYCLE_BAD_FIELD', { where, field: 'expectedRev' })
231
+ }
232
+ if (t.rev !== expectedRev) {
233
+ throw new CycleError(
234
+ `cycle.${where} CAS 冲突:周期 ${id} 期望 rev=${expectedRev},实际 rev=${t.rev} —— 拒绝静默覆盖`,
235
+ 'CYCLE_REV_CONFLICT', { where, id, expectedRev, rev: t.rev },
236
+ )
237
+ }
238
+ }
239
+ return t
240
+ }
241
+
242
+ // ── API:写 ────────────────────────────────────────────────────────────────
243
+
244
+ /**
245
+ * 为一个任务开一个复盘周期。
246
+ * @param {{ taskId: string, objective: string, cycle?: string, milestone?: string,
247
+ * estimate?: number, kind?: 'human'|'agent', by: string, now?: number }} input
248
+ */
249
+ function start(input = {}) {
250
+ assertAlive('start')
251
+ const { taskId, objective, cycle, milestone, estimate, kind, by, now } = input ?? {}
252
+ const taskIdNorm = requireString(taskId, 'taskId', 'start')
253
+ const objectiveNorm = requireString(objective, 'objective', 'start')
254
+ const author = requireString(by, 'by', 'start')
255
+ const at = normalizeNow(now, 'start')
256
+ const est = progressOf(estimate, 'start')
257
+ // 需求4:kind 决定这是「人跟长任务」还是「智能体事后归纳」
258
+ const kindNorm = kind === undefined || kind === null ? 'human' : String(kind)
259
+ if (!CYCLE_KINDS.includes(kindNorm)) {
260
+ throw new CycleError(
261
+ `cycle.start:kind=${JSON.stringify(kind)} 非法(合法:${CYCLE_KINDS.join(' | ')})`,
262
+ 'CYCLE_BAD_KIND', { kind },
263
+ )
264
+ }
265
+
266
+ idCounter += 1
267
+ const id = `c-${idCounter}`
268
+ /** @type {Record<string, unknown>} */
269
+ const rec = {
270
+ id, rev: 1, taskId: taskIdNorm, objective: objectiveNorm,
271
+ cycle: cycle ?? null, milestone: milestone ?? null, estimate: est ?? null,
272
+ kind: kindNorm,
273
+ status: 'open', phase: 'planned', progress: 0,
274
+ review: null, closedAt: null,
275
+ createdAt: at, createdBy: author, updatedAt: at, updatedBy: author,
276
+ }
277
+ commit('start', at, author, null, rec)
278
+ info(`[cs-bots/cycle] 周期 ${id} 开启(task=${taskIdNorm},phase=planned)`)
279
+ return clone(rec)
280
+ }
281
+
282
+ /**
283
+ * 推进阶段。**不许跳步**:planned→doing→checking→acting→done。
284
+ * @param {{ id: string, expectedRev?: number, phase: string, progress?: number, evidence?: string, by: string, now?: number }} input
285
+ */
286
+ function advance(input = {}) {
287
+ assertAlive('advance')
288
+ const { id, expectedRev, phase, progress, evidence, by, now } = input ?? {}
289
+ const author = requireString(by, 'by', 'advance')
290
+ const at = normalizeNow(now, 'advance')
291
+ if (!CYCLE_PHASES.includes(phase)) {
292
+ throw new CycleError(`cycle.advance:phase=${JSON.stringify(phase)} 非法(合法:${CYCLE_PHASES.join(' | ')})`, 'CYCLE_BAD_PHASE', { phase })
293
+ }
294
+ const current = cas(id, expectedRev, 'advance')
295
+ if (current.phase === 'done') {
296
+ throw new CycleError(`cycle.advance:周期 ${id} 已完成,不可再推进`, 'CYCLE_ALREADY_DONE', { id })
297
+ }
298
+ const expectedNext = NEXT_PHASE[current.phase]
299
+ if (phase !== expectedNext) {
300
+ throw new CycleError(
301
+ `cycle.advance 阶段流转非法:周期 ${id} 当前 ${current.phase},下一步只能是 ${expectedNext},收到 ${phase} —— 不许跳步`,
302
+ 'CYCLE_BAD_PHASE_FLOW',
303
+ { id, current: current.phase, expected: expectedNext, received: phase },
304
+ )
305
+ }
306
+ const next = clone(current)
307
+ next.rev = current.rev + 1
308
+ next.phase = phase
309
+ if (phase === 'done') {
310
+ next.status = 'closed'
311
+ next.closedAt = at
312
+ }
313
+ const prog = progressOf(progress, 'advance')
314
+ if (prog !== undefined) next.progress = prog
315
+ if (evidence !== undefined && evidence !== null) {
316
+ next.evidence = String(evidence).slice(0, 4000)
317
+ }
318
+ next.updatedAt = at
319
+ next.updatedBy = author
320
+ commit('advance', at, author, clone(current), next)
321
+ info(`[cs-bots/cycle] 周期 ${id} → ${phase}`)
322
+ return clone(next)
323
+ }
324
+
325
+ /**
326
+ * 记录一次检查结果。reject 打回 doing(重做),pass 前进到 acting。
327
+ * @param {{ id: string, expectedRev?: number, result: 'pass'|'reject', evidence?: string, by: string, now?: number }} input
328
+ */
329
+ function check(input = {}) {
330
+ assertAlive('check')
331
+ const { id, expectedRev, result, evidence, by, now } = input ?? {}
332
+ const author = requireString(by, 'by', 'check')
333
+ const at = normalizeNow(now, 'check')
334
+ if (!['pass', 'reject'].includes(result)) {
335
+ throw new CycleError(`cycle.check:result 必须是 pass | reject(收到 ${JSON.stringify(result)})`, 'CYCLE_BAD_FIELD', { field: 'result' })
336
+ }
337
+ const current = cas(id, expectedRev, 'check')
338
+ const next = clone(current)
339
+ next.rev = current.rev + 1
340
+ if (result === 'pass') {
341
+ next.phase = 'acting'
342
+ } else {
343
+ // 驳回 → 打回重做(doing)。progress 不变(重做不退款,也不前进)。
344
+ next.phase = 'doing'
345
+ next.rejectedAt = at
346
+ next.rejectCount = (next.rejectCount ?? 0) + 1
347
+ }
348
+ if (evidence !== undefined && evidence !== null) next.checkEvidence = String(evidence).slice(0, 4000)
349
+ next.updatedAt = at
350
+ next.updatedBy = author
351
+ commit('check', at, author, clone(current), next)
352
+ info(`[cs-bots/cycle] 周期 ${id} 检查 ${result} → ${next.phase}`)
353
+ return clone(next)
354
+ }
355
+
356
+ /**
357
+ * 复盘(Act):记录经验教训,并提议标准化建议。
358
+ * @param {{ id: string, expectedRev?: number, summary: string, lessons?: string[],
359
+ * standards?: {scope: string, scopeId?: string, kind: string, title: string, content: string}[],
360
+ * by: string, now?: number }} input
361
+ */
362
+ function review(input = {}) {
363
+ assertAlive('review')
364
+ const { id, expectedRev, summary, lessons, standards, by, now } = input ?? {}
365
+ const author = requireString(by, 'by', 'review')
366
+ const at = normalizeNow(now, 'review')
367
+ const summaryNorm = requireString(summary, 'summary', 'review')
368
+ if (summaryNorm.length > 8000) {
369
+ throw new CycleError(`cycle.review:summary ≤8000 字符(收到 ${summaryNorm.length})`, 'CYCLE_BAD_FIELD', { field: 'summary' })
370
+ }
371
+ const lessonList = stringList(lessons, 'lessons', 'review')
372
+ const standardList = Array.isArray(standards) ? standards : []
373
+ if (standardList.length > 20) {
374
+ throw new CycleError(`cycle.review:standards 最多 20 条(收到 ${standardList.length})`, 'CYCLE_BAD_FIELD', { field: 'standards' })
375
+ }
376
+
377
+ const current = cas(id, expectedRev, 'review')
378
+ // 需求4:human 周期强制 acting 阶段(人不许跳步,防自欺);
379
+ // agent 周期是批量归纳,允许一次性提交(不必假装走完 planned→doing→checking)。
380
+ const isAgent = (current.kind ?? 'human') === 'agent'
381
+ if (!isAgent && current.phase !== 'acting') {
382
+ throw new CycleError(
383
+ `cycle.review:周期 ${id} 当前阶段 ${current.phase},复盘只能在 acting 阶段做 —— 请先 check pass`
384
+ + '(只有 agent 类型的周期允许一次性提交)',
385
+ 'CYCLE_BAD_PHASE',
386
+ { id, phase: current.phase, kind: current.kind ?? 'human' },
387
+ )
388
+ }
389
+
390
+ // ── 先记复盘内容(事件流是真相源;建议提议失败绝不阻断这一步)──
391
+ const next = clone(current)
392
+ next.rev = current.rev + 1
393
+ next.review = { summary: summaryNorm, lessons: lessonList, standards: clone(standardList), at, by: author }
394
+ next.updatedAt = at
395
+ next.updatedBy = author
396
+ commit('review', at, author, clone(current), next)
397
+ info(`[cs-bots/cycle] 周期 ${id} 复盘完成(${lessonList.length} 条教训,${standardList.length} 条标准化建议)`)
398
+
399
+ // ── 提议标准化建议(走「建议确认制」;suggest 缺失/抛错 → 降级)──
400
+ /** @type {object[]} */
401
+ const proposed = []
402
+ if (!suggestions || typeof suggestions.suggest !== 'function') {
403
+ warn(`[cs-bots/cycle] suggest 服务不可用 → 周期 ${id} 的复盘已记录,但标准化建议未提议(需人工整理)`)
404
+ return { cycle: clone(next), suggestions: [], proposedCount: 0, degraded: true }
405
+ }
406
+ for (const s of standardList) {
407
+ try {
408
+ const out = suggestions.suggest({
409
+ kind: 'discipline',
410
+ payload: {
411
+ scope: s.scope,
412
+ scopeId: s.scopeId ?? null,
413
+ kind: s.kind,
414
+ title: s.title,
415
+ content: s.content,
416
+ },
417
+ reason: `周期 ${id}(${current.taskId})复盘标准化`,
418
+ source: 'cycle-review',
419
+ by: author,
420
+ confidence: 0.7,
421
+ })
422
+ proposed.push({ kind: 'discipline', id: out.id, title: s.title })
423
+ } catch (error) {
424
+ diagnostics.suggestFailures += 1
425
+ warn(`[cs-bots/cycle] 周期 ${id} 提议纪律建议失败(${error?.message ?? error})—— 复盘内容已落盘,建议未提`)
426
+ }
427
+ }
428
+ for (const lesson of lessonList) {
429
+ try {
430
+ const out = suggestions.suggest({
431
+ kind: 'memory',
432
+ layer: 'L2',
433
+ payload: { content: lesson, tags: ['cycle-review', current.taskId ?? 'unknown'] },
434
+ reason: `周期 ${id}(${current.taskId})复盘教训`,
435
+ source: 'cycle-review',
436
+ by: author,
437
+ confidence: 0.6,
438
+ })
439
+ proposed.push({ kind: 'memory', id: out.id, title: lesson.slice(0, 60) })
440
+ } catch (error) {
441
+ diagnostics.suggestFailures += 1
442
+ warn(`[cs-bots/cycle] 周期 ${id} 提议教训记忆失败(${error?.message ?? error})`)
443
+ }
444
+ }
445
+ // 需求4:agent 周期归纳完即闭环——智能体不需要「人点完成」这个仪式;
446
+ // human 周期留在 acting,由人显式 advance 到 done(人要对复盘结论负责)。
447
+ if (isAgent) {
448
+ const closed = clone(next)
449
+ closed.rev = next.rev + 1
450
+ closed.phase = 'done'
451
+ closed.status = 'closed'
452
+ closed.closedAt = at
453
+ closed.updatedAt = at
454
+ closed.updatedBy = author
455
+ commit('advance', at, author, clone(next), closed)
456
+ info(`[cs-bots/cycle] 周期 ${id}(agent)归纳完成 → done`)
457
+ return { cycle: clone(closed), suggestions: proposed, proposedCount: proposed.length, degraded: false, autoClosed: true }
458
+ }
459
+
460
+ return { cycle: clone(next), suggestions: proposed, proposedCount: proposed.length, degraded: false, autoClosed: false }
461
+ }
462
+
463
+ // ── API:读 ────────────────────────────────────────────────────────────────
464
+
465
+ function list(query = {}) {
466
+ assertAlive('list')
467
+ let rows = [...entries.values()]
468
+ // 需求4:按 kind 分离——人只关心 human 周期(长任务跟进),不着 AI 的归纳周期
469
+ if (query?.kind !== undefined && query?.kind !== null) rows = rows.filter((r) => (r.kind ?? 'human') === query.kind)
470
+ if (query?.status !== undefined && query?.status !== null) rows = rows.filter((r) => r.status === query.status)
471
+ if (query?.phase !== undefined && query?.phase !== null) rows = rows.filter((r) => r.phase === query.phase)
472
+ if (query?.taskId !== undefined && query?.taskId !== null) rows = rows.filter((r) => r.taskId === query.taskId)
473
+ return clone(rows)
474
+ }
475
+
476
+ function get(id) {
477
+ assertAlive('get')
478
+ const t = entries.get(id)
479
+ return t ? clone(t) : null
480
+ }
481
+
482
+ function stats() {
483
+ const byPhase = {}
484
+ const byStatus = {}
485
+ const byKind = {}
486
+ for (const r of entries.values()) {
487
+ byPhase[r.phase] = (byPhase[r.phase] ?? 0) + 1
488
+ byStatus[r.status] = (byStatus[r.status] ?? 0) + 1
489
+ const k = r.kind ?? 'human'
490
+ byKind[k] = (byKind[k] ?? 0) + 1
491
+ }
492
+ return { total: entries.size, open: byStatus.open ?? 0, closed: byStatus.closed ?? 0, byPhase, byStatus, byKind, events: events.length, diagnostics: clone(diagnostics) }
493
+ }
494
+
495
+ function replay() { hydrate(); diagnostics.lastReplayAt = Date.now(); return { from: diagnostics.hydratedFrom, events: events.length, bad: diagnostics.badLines } }
496
+ function dispose() { disposed = true }
497
+
498
+ hydrate()
499
+
500
+ return {
501
+ name: cfg.name ?? 'cycle',
502
+ paths: Object.freeze({ stateDir, jsonl: jsonlPath, snapshot: snapPath }),
503
+ start,
504
+ advance,
505
+ check,
506
+ review,
507
+ list,
508
+ get,
509
+ stats,
510
+ replay,
511
+ diagnostics: () => clone(diagnostics),
512
+ dispose,
513
+ }
514
+ }
515
+
516
+ export default createCycle