principles-disciple 1.5.4 → 1.6.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 (43) hide show
  1. package/dist/commands/context.d.ts +5 -0
  2. package/dist/commands/context.js +308 -0
  3. package/dist/commands/focus.d.ts +14 -0
  4. package/dist/commands/focus.js +579 -0
  5. package/dist/commands/pain.js +135 -6
  6. package/dist/commands/rollback.d.ts +19 -0
  7. package/dist/commands/rollback.js +119 -0
  8. package/dist/core/config.d.ts +32 -0
  9. package/dist/core/config.js +47 -0
  10. package/dist/core/event-log.d.ts +21 -1
  11. package/dist/core/event-log.js +316 -0
  12. package/dist/core/focus-history.d.ts +65 -0
  13. package/dist/core/focus-history.js +266 -0
  14. package/dist/core/init.js +30 -7
  15. package/dist/core/migration.js +0 -2
  16. package/dist/core/path-resolver.d.ts +3 -0
  17. package/dist/core/path-resolver.js +20 -0
  18. package/dist/hooks/gate.js +203 -1
  19. package/dist/hooks/llm.d.ts +8 -0
  20. package/dist/hooks/llm.js +234 -1
  21. package/dist/hooks/message-sanitize.d.ts +3 -0
  22. package/dist/hooks/message-sanitize.js +37 -0
  23. package/dist/hooks/prompt.d.ts +12 -0
  24. package/dist/hooks/prompt.js +309 -135
  25. package/dist/hooks/subagent.d.ts +9 -2
  26. package/dist/hooks/subagent.js +13 -2
  27. package/dist/i18n/commands.js +32 -20
  28. package/dist/index.js +181 -4
  29. package/dist/service/empathy-observer-manager.d.ts +42 -0
  30. package/dist/service/empathy-observer-manager.js +147 -0
  31. package/dist/service/evolution-worker.d.ts +1 -0
  32. package/dist/service/evolution-worker.js +4 -2
  33. package/dist/tools/deep-reflect.js +80 -0
  34. package/dist/types/event-types.d.ts +77 -2
  35. package/dist/types/event-types.js +33 -0
  36. package/dist/types.d.ts +42 -0
  37. package/dist/types.js +19 -1
  38. package/openclaw.plugin.json +1 -1
  39. package/package.json +3 -3
  40. package/templates/langs/zh/core/HEARTBEAT.md +28 -4
  41. package/templates/pain_settings.json +54 -2
  42. package/templates/workspace/.principles/PROFILE.json +2 -0
  43. package/templates/workspace/okr/CURRENT_FOCUS.md +57 -0
@@ -57,6 +57,9 @@ export class EventLog {
57
57
  recordTrustChange(sessionId, data) {
58
58
  this.record('trust_change', 'changed', sessionId, data);
59
59
  }
60
+ recordEmpathyRollback(sessionId, data) {
61
+ this.record('empathy_rollback', 'rolled_back', sessionId, data);
62
+ }
60
63
  recordError(sessionId, message, context) {
61
64
  this.record('error', 'failure', sessionId, { message, ...context });
62
65
  }
@@ -115,6 +118,45 @@ export class EventLog {
115
118
  const data = entry.data;
116
119
  stats.pain.signalsDetected++;
117
120
  stats.pain.maxScore = Math.max(stats.pain.maxScore, data.score);
121
+ // Update empathy stats for user_empathy source
122
+ if (data.source === 'user_empathy') {
123
+ if (data.deduped) {
124
+ stats.empathy.dedupedCount++;
125
+ }
126
+ else {
127
+ stats.empathy.totalEvents++;
128
+ stats.empathy.totalPenaltyScore += data.score || 0;
129
+ // By severity
130
+ if (data.severity) {
131
+ stats.empathy.bySeverity[data.severity]++;
132
+ stats.empathy.scoreBySeverity[data.severity] += data.score || 0;
133
+ }
134
+ // By detection mode
135
+ if (data.detection_mode) {
136
+ stats.empathy.byDetectionMode[data.detection_mode]++;
137
+ }
138
+ // By origin
139
+ if (data.origin) {
140
+ stats.empathy.byOrigin[data.origin]++;
141
+ }
142
+ // Confidence distribution
143
+ const conf = data.confidence ?? 1;
144
+ if (conf >= 0.8)
145
+ stats.empathy.confidenceDistribution.high++;
146
+ else if (conf >= 0.5)
147
+ stats.empathy.confidenceDistribution.medium++;
148
+ else
149
+ stats.empathy.confidenceDistribution.low++;
150
+ }
151
+ // Update dedupe hit rate
152
+ const total = stats.empathy.totalEvents + stats.empathy.dedupedCount;
153
+ stats.empathy.dedupeHitRate = total > 0 ? stats.empathy.dedupedCount / total : 0;
154
+ }
155
+ }
156
+ else if (entry.type === 'empathy_rollback') {
157
+ const data = entry.data;
158
+ stats.empathy.rollbackCount++;
159
+ stats.empathy.rolledBackScore += data.originalScore || 0;
118
160
  }
119
161
  }
120
162
  startFlushTimer() {
@@ -164,6 +206,280 @@ export class EventLog {
164
206
  }
165
207
  return stats;
166
208
  }
209
+ /**
210
+ * Get aggregated empathy statistics for multiple time ranges.
211
+ * @param range 'today' | 'week' | 'session'
212
+ * @param sessionId Optional session ID for session-scoped stats
213
+ */
214
+ getEmpathyStats(range, sessionId) {
215
+ const now = new Date();
216
+ const today = this.formatDate(now);
217
+ // Aggregate stats based on range
218
+ const result = {
219
+ totalEvents: 0,
220
+ dedupedCount: 0,
221
+ dedupeHitRate: 0,
222
+ totalPenaltyScore: 0,
223
+ rolledBackScore: 0,
224
+ rollbackCount: 0,
225
+ bySeverity: { mild: 0, moderate: 0, severe: 0 },
226
+ scoreBySeverity: { mild: 0, moderate: 0, severe: 0 },
227
+ byDetectionMode: { structured: 0, legacy_tag: 0 },
228
+ byOrigin: { assistant_self_report: 0, user_manual: 0, system_infer: 0 },
229
+ confidenceDistribution: { high: 0, medium: 0, low: 0 },
230
+ dailyTrend: [],
231
+ };
232
+ if (range === 'session' && sessionId) {
233
+ // For session range, scan event buffer and events file
234
+ this.aggregateSessionEmpathy(sessionId, result);
235
+ }
236
+ else if (range === 'week') {
237
+ // For week range, aggregate last 7 days
238
+ for (let i = 0; i < 7; i++) {
239
+ const date = new Date(now);
240
+ date.setDate(date.getDate() - i);
241
+ const dateStr = this.formatDate(date);
242
+ const stats = this.getDailyStats(dateStr);
243
+ result.totalEvents += stats.empathy.totalEvents;
244
+ result.dedupedCount += stats.empathy.dedupedCount;
245
+ result.totalPenaltyScore += stats.empathy.totalPenaltyScore;
246
+ result.rolledBackScore += stats.empathy.rolledBackScore;
247
+ result.rollbackCount += stats.empathy.rollbackCount;
248
+ for (const sev of ['mild', 'moderate', 'severe']) {
249
+ result.bySeverity[sev] += stats.empathy.bySeverity[sev];
250
+ result.scoreBySeverity[sev] += stats.empathy.scoreBySeverity[sev];
251
+ }
252
+ result.byDetectionMode.structured += stats.empathy.byDetectionMode.structured;
253
+ result.byDetectionMode.legacy_tag += stats.empathy.byDetectionMode.legacy_tag;
254
+ for (const org of ['assistant_self_report', 'user_manual', 'system_infer']) {
255
+ result.byOrigin[org] += stats.empathy.byOrigin[org];
256
+ }
257
+ result.confidenceDistribution.high += stats.empathy.confidenceDistribution.high;
258
+ result.confidenceDistribution.medium += stats.empathy.confidenceDistribution.medium;
259
+ result.confidenceDistribution.low += stats.empathy.confidenceDistribution.low;
260
+ if (stats.empathy.totalEvents > 0 || stats.empathy.dedupedCount > 0) {
261
+ result.dailyTrend.push({
262
+ date: dateStr,
263
+ count: stats.empathy.totalEvents,
264
+ score: stats.empathy.totalPenaltyScore,
265
+ });
266
+ }
267
+ }
268
+ }
269
+ else {
270
+ // Today only
271
+ const stats = this.getDailyStats(today);
272
+ Object.assign(result, stats.empathy);
273
+ if (stats.empathy.totalEvents > 0 || stats.empathy.dedupedCount > 0) {
274
+ result.dailyTrend = [{
275
+ date: today,
276
+ count: stats.empathy.totalEvents,
277
+ score: stats.empathy.totalPenaltyScore,
278
+ }];
279
+ }
280
+ }
281
+ // Calculate dedupe hit rate
282
+ const total = result.totalEvents + result.dedupedCount;
283
+ result.dedupeHitRate = total > 0 ? result.dedupedCount / total : 0;
284
+ return result;
285
+ }
286
+ /**
287
+ * Aggregate empathy stats for a specific session.
288
+ */
289
+ aggregateSessionEmpathy(sessionId, result) {
290
+ // Check event buffer first
291
+ for (const entry of this.eventBuffer) {
292
+ if (entry.sessionId === sessionId && entry.type === 'pain_signal') {
293
+ const data = entry.data;
294
+ if (data.source === 'user_empathy') {
295
+ if (data.deduped) {
296
+ result.dedupedCount++;
297
+ }
298
+ else {
299
+ result.totalEvents++;
300
+ result.totalPenaltyScore += data.score || 0;
301
+ if (data.severity) {
302
+ result.bySeverity[data.severity]++;
303
+ result.scoreBySeverity[data.severity] += data.score || 0;
304
+ }
305
+ if (data.detection_mode)
306
+ result.byDetectionMode[data.detection_mode]++;
307
+ if (data.origin)
308
+ result.byOrigin[data.origin]++;
309
+ const conf = data.confidence ?? 1;
310
+ if (conf >= 0.8)
311
+ result.confidenceDistribution.high++;
312
+ else if (conf >= 0.5)
313
+ result.confidenceDistribution.medium++;
314
+ else
315
+ result.confidenceDistribution.low++;
316
+ }
317
+ }
318
+ }
319
+ else if (entry.sessionId === sessionId && entry.type === 'empathy_rollback') {
320
+ const data = entry.data;
321
+ result.rollbackCount++;
322
+ result.rolledBackScore += data.originalScore || 0;
323
+ }
324
+ }
325
+ // Also check events file for persisted events
326
+ if (fs.existsSync(this.eventsFile)) {
327
+ try {
328
+ const content = fs.readFileSync(this.eventsFile, 'utf-8');
329
+ const lines = content.trim().split('\n');
330
+ for (const line of lines) {
331
+ if (!line.trim())
332
+ continue;
333
+ try {
334
+ const entry = JSON.parse(line);
335
+ if (entry.sessionId === sessionId) {
336
+ if (entry.type === 'pain_signal') {
337
+ const data = entry.data;
338
+ if (data.source === 'user_empathy') {
339
+ if (data.deduped) {
340
+ result.dedupedCount++;
341
+ }
342
+ else {
343
+ result.totalEvents++;
344
+ result.totalPenaltyScore += data.score || 0;
345
+ if (data.severity) {
346
+ result.bySeverity[data.severity]++;
347
+ result.scoreBySeverity[data.severity] += data.score || 0;
348
+ }
349
+ if (data.detection_mode)
350
+ result.byDetectionMode[data.detection_mode]++;
351
+ if (data.origin)
352
+ result.byOrigin[data.origin]++;
353
+ const conf = data.confidence ?? 1;
354
+ if (conf >= 0.8)
355
+ result.confidenceDistribution.high++;
356
+ else if (conf >= 0.5)
357
+ result.confidenceDistribution.medium++;
358
+ else
359
+ result.confidenceDistribution.low++;
360
+ }
361
+ }
362
+ }
363
+ else if (entry.type === 'empathy_rollback') {
364
+ const data = entry.data;
365
+ result.rollbackCount++;
366
+ result.rolledBackScore += data.originalScore || 0;
367
+ }
368
+ }
369
+ }
370
+ catch {
371
+ // Skip malformed lines
372
+ }
373
+ }
374
+ }
375
+ catch (e) {
376
+ if (this.logger)
377
+ this.logger.error(`[PD] Failed to read events.jsonl: ${String(e)}`);
378
+ }
379
+ }
380
+ }
381
+ /**
382
+ * Rollback an empathy event by ID.
383
+ * Returns the rolled back score, or 0 if event not found.
384
+ */
385
+ rollbackEmpathyEvent(eventId, sessionId, reason, triggeredBy) {
386
+ // Search for the event in buffer and file
387
+ let foundEvent = null;
388
+ // Check buffer first
389
+ for (const entry of this.eventBuffer) {
390
+ if (entry.type === 'pain_signal') {
391
+ const data = entry.data;
392
+ if (entry.data.eventId === eventId && data.source === 'user_empathy') {
393
+ foundEvent = { entry, data };
394
+ break;
395
+ }
396
+ }
397
+ }
398
+ // If not in buffer, check file
399
+ if (!foundEvent && fs.existsSync(this.eventsFile)) {
400
+ try {
401
+ const content = fs.readFileSync(this.eventsFile, 'utf-8');
402
+ const lines = content.trim().split('\n');
403
+ for (const line of lines) {
404
+ if (!line.trim())
405
+ continue;
406
+ try {
407
+ const entry = JSON.parse(line);
408
+ if (entry.type === 'pain_signal') {
409
+ const data = entry.data;
410
+ if (entry.data.eventId === eventId && data.source === 'user_empathy') {
411
+ foundEvent = { entry, data };
412
+ break;
413
+ }
414
+ }
415
+ }
416
+ catch {
417
+ // Skip malformed lines
418
+ }
419
+ }
420
+ }
421
+ catch (e) {
422
+ if (this.logger)
423
+ this.logger.error(`[PD] Failed to read events.jsonl: ${String(e)}`);
424
+ }
425
+ }
426
+ if (!foundEvent || foundEvent.data.deduped) {
427
+ return 0;
428
+ }
429
+ const originalScore = foundEvent.data.score || 0;
430
+ // Record the rollback event
431
+ this.recordEmpathyRollback(sessionId, {
432
+ eventId,
433
+ originalScore,
434
+ originalSessionId: foundEvent.entry.sessionId,
435
+ reason,
436
+ triggeredBy,
437
+ });
438
+ return originalScore;
439
+ }
440
+ /**
441
+ * Get the last empathy event ID for a session (for rollback).
442
+ */
443
+ getLastEmpathyEventId(sessionId) {
444
+ // Check buffer in reverse
445
+ for (let i = this.eventBuffer.length - 1; i >= 0; i--) {
446
+ const entry = this.eventBuffer[i];
447
+ if (entry.sessionId === sessionId && entry.type === 'pain_signal') {
448
+ const data = entry.data;
449
+ if (data.source === 'user_empathy' && !data.deduped) {
450
+ return entry.data.eventId || null;
451
+ }
452
+ }
453
+ }
454
+ // Check file
455
+ if (fs.existsSync(this.eventsFile)) {
456
+ try {
457
+ const content = fs.readFileSync(this.eventsFile, 'utf-8');
458
+ const lines = content.trim().split('\n');
459
+ for (let i = lines.length - 1; i >= 0; i--) {
460
+ if (!lines[i].trim())
461
+ continue;
462
+ try {
463
+ const entry = JSON.parse(lines[i]);
464
+ if (entry.sessionId === sessionId && entry.type === 'pain_signal') {
465
+ const data = entry.data;
466
+ if (data.source === 'user_empathy' && !data.deduped) {
467
+ return entry.data.eventId || null;
468
+ }
469
+ }
470
+ }
471
+ catch {
472
+ // Skip malformed lines
473
+ }
474
+ }
475
+ }
476
+ catch (e) {
477
+ if (this.logger)
478
+ this.logger.error(`[PD] Failed to read events.jsonl: ${String(e)}`);
479
+ }
480
+ }
481
+ return null;
482
+ }
167
483
  /**
168
484
  * Dispose of the EventLog, flushing pending data and clearing timer.
169
485
  */
@@ -0,0 +1,65 @@
1
+ /**
2
+ * CURRENT_FOCUS 历史版本管理
3
+ *
4
+ * 功能:
5
+ * - 压缩时备份当前版本到历史目录
6
+ * - 清理过期历史版本
7
+ * - 读取历史版本(用于 full 模式)
8
+ */
9
+ /**
10
+ * 获取历史目录路径
11
+ */
12
+ export declare function getHistoryDir(focusPath: string): string;
13
+ /**
14
+ * 从 CURRENT_FOCUS.md 提取版本号
15
+ * 支持整数和小数版本(如 v1, v1.1, v1.2)
16
+ */
17
+ export declare function extractVersion(content: string): string;
18
+ /**
19
+ * 从 CURRENT_FOCUS.md 提取更新日期
20
+ */
21
+ export declare function extractDate(content: string): string;
22
+ /**
23
+ * 备份当前版本到历史目录
24
+ *
25
+ * @param focusPath CURRENT_FOCUS.md 的完整路径
26
+ * @param content 当前内容
27
+ * @returns 备份文件路径,失败返回 null
28
+ */
29
+ export declare function backupToHistory(focusPath: string, content: string): string | null;
30
+ /**
31
+ * 清理过期历史版本
32
+ *
33
+ * @param focusPath CURRENT_FOCUS.md 的完整路径
34
+ * @param maxFiles 最大保留数量
35
+ */
36
+ export declare function cleanupHistory(focusPath: string, maxFiles?: number): void;
37
+ /**
38
+ * 获取历史版本列表
39
+ *
40
+ * @param focusPath CURRENT_FOCUS.md 的完整路径
41
+ * @param count 获取数量
42
+ * @returns 历史版本内容数组(按时间倒序)
43
+ */
44
+ export declare function getHistoryVersions(focusPath: string, count?: number): string[];
45
+ /**
46
+ * 压缩 CURRENT_FOCUS.md
47
+ *
48
+ * @param focusPath CURRENT_FOCUS.md 的完整路径
49
+ * @param newContent 新内容
50
+ * @returns 压缩后的信息
51
+ */
52
+ export declare function compressFocus(focusPath: string, newContent: string): {
53
+ backupPath: string | null;
54
+ cleanedCount: number;
55
+ };
56
+ /**
57
+ * 智能摘要提取
58
+ *
59
+ * 优先提取关键章节,确保不丢失重要信息
60
+ * 对于非结构化内容,回退到简单的行截取
61
+ *
62
+ * @param content CURRENT_FOCUS.md 内容
63
+ * @param maxLines 最大行数
64
+ */
65
+ export declare function extractSummary(content: string, maxLines?: number): string;
@@ -0,0 +1,266 @@
1
+ /**
2
+ * CURRENT_FOCUS 历史版本管理
3
+ *
4
+ * 功能:
5
+ * - 压缩时备份当前版本到历史目录
6
+ * - 清理过期历史版本
7
+ * - 读取历史版本(用于 full 模式)
8
+ */
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ /**
12
+ * 简单的日志记录器
13
+ */
14
+ function logError(message, error) {
15
+ const timestamp = new Date().toISOString();
16
+ const errorStr = error instanceof Error ? error.message : String(error);
17
+ console.error(`[focus-history] ${timestamp} ERROR: ${message}${errorStr ? ' - ' + errorStr : ''}`);
18
+ }
19
+ /** 历史版本保留数量 */
20
+ const MAX_HISTORY_FILES = 10;
21
+ /** full 模式读取的历史版本数 */
22
+ const FULL_MODE_HISTORY_COUNT = 3;
23
+ /**
24
+ * 获取历史目录路径
25
+ */
26
+ export function getHistoryDir(focusPath) {
27
+ return path.join(path.dirname(focusPath), '.history');
28
+ }
29
+ /**
30
+ * 从 CURRENT_FOCUS.md 提取版本号
31
+ * 支持整数和小数版本(如 v1, v1.1, v1.2)
32
+ */
33
+ export function extractVersion(content) {
34
+ const match = content.match(/\*\*版本\*\*:\s*v([\d.]+)/i);
35
+ return match ? match[1] : '1';
36
+ }
37
+ /**
38
+ * 从 CURRENT_FOCUS.md 提取更新日期
39
+ */
40
+ export function extractDate(content) {
41
+ const match = content.match(/\*\*更新\*\*:\s*(\d{4}-\d{2}-\d{2})/);
42
+ return match ? match[1] : new Date().toISOString().split('T')[0];
43
+ }
44
+ /**
45
+ * 备份当前版本到历史目录
46
+ *
47
+ * @param focusPath CURRENT_FOCUS.md 的完整路径
48
+ * @param content 当前内容
49
+ * @returns 备份文件路径,失败返回 null
50
+ */
51
+ export function backupToHistory(focusPath, content) {
52
+ try {
53
+ const historyDir = getHistoryDir(focusPath);
54
+ // 确保历史目录存在
55
+ if (!fs.existsSync(historyDir)) {
56
+ try {
57
+ fs.mkdirSync(historyDir, { recursive: true });
58
+ }
59
+ catch (error) {
60
+ logError(`Failed to create history directory: ${historyDir}`, error);
61
+ return null;
62
+ }
63
+ }
64
+ const version = extractVersion(content);
65
+ const date = extractDate(content);
66
+ // 使用时间戳作为唯一标识,避免同名冲突
67
+ const timestamp = Date.now();
68
+ const backupName = `CURRENT_FOCUS.v${version}.${date}.${timestamp}.md`;
69
+ const backupPath = path.join(historyDir, backupName);
70
+ // 如果备份已存在,跳过
71
+ if (fs.existsSync(backupPath)) {
72
+ return null;
73
+ }
74
+ try {
75
+ fs.writeFileSync(backupPath, content, 'utf-8');
76
+ return backupPath;
77
+ }
78
+ catch (error) {
79
+ logError(`Failed to write backup file: ${backupPath}`, error);
80
+ return null;
81
+ }
82
+ }
83
+ catch (error) {
84
+ logError('Unexpected error in backupToHistory', error);
85
+ return null;
86
+ }
87
+ }
88
+ /**
89
+ * 清理过期历史版本
90
+ *
91
+ * @param focusPath CURRENT_FOCUS.md 的完整路径
92
+ * @param maxFiles 最大保留数量
93
+ */
94
+ export function cleanupHistory(focusPath, maxFiles = MAX_HISTORY_FILES) {
95
+ try {
96
+ const historyDir = getHistoryDir(focusPath);
97
+ if (!fs.existsSync(historyDir)) {
98
+ return;
99
+ }
100
+ // 获取所有历史文件并按修改时间排序(最新的在前)
101
+ const files = fs.readdirSync(historyDir)
102
+ .filter(f => f.startsWith('CURRENT_FOCUS.v') && f.endsWith('.md'))
103
+ .map(f => ({
104
+ name: f,
105
+ path: path.join(historyDir, f),
106
+ mtime: fs.statSync(path.join(historyDir, f)).mtime.getTime()
107
+ }))
108
+ .sort((a, b) => b.mtime - a.mtime);
109
+ // 删除超出数量的文件
110
+ const toDelete = files.slice(maxFiles);
111
+ for (const file of toDelete) {
112
+ try {
113
+ fs.unlinkSync(file.path);
114
+ }
115
+ catch (error) {
116
+ // 单个文件删除失败不应中断整个清理过程
117
+ logError(`Failed to delete history file: ${file.path}`, error);
118
+ }
119
+ }
120
+ }
121
+ catch (error) {
122
+ logError('Unexpected error in cleanupHistory', error);
123
+ }
124
+ }
125
+ /**
126
+ * 获取历史版本列表
127
+ *
128
+ * @param focusPath CURRENT_FOCUS.md 的完整路径
129
+ * @param count 获取数量
130
+ * @returns 历史版本内容数组(按时间倒序)
131
+ */
132
+ export function getHistoryVersions(focusPath, count = FULL_MODE_HISTORY_COUNT) {
133
+ const historyDir = getHistoryDir(focusPath);
134
+ if (!fs.existsSync(historyDir)) {
135
+ return [];
136
+ }
137
+ // 获取所有历史文件并按修改时间排序(最新的在前)
138
+ const files = fs.readdirSync(historyDir)
139
+ .filter(f => f.startsWith('CURRENT_FOCUS.v') && f.endsWith('.md'))
140
+ .map(f => ({
141
+ path: path.join(historyDir, f),
142
+ mtime: fs.statSync(path.join(historyDir, f)).mtime.getTime()
143
+ }))
144
+ .sort((a, b) => b.mtime - a.mtime)
145
+ .slice(0, count);
146
+ return files.map(f => fs.readFileSync(f.path, 'utf-8'));
147
+ }
148
+ /**
149
+ * 压缩 CURRENT_FOCUS.md
150
+ *
151
+ * @param focusPath CURRENT_FOCUS.md 的完整路径
152
+ * @param newContent 新内容
153
+ * @returns 压缩后的信息
154
+ */
155
+ export function compressFocus(focusPath, newContent) {
156
+ // 读取当前内容
157
+ let oldContent = '';
158
+ if (fs.existsSync(focusPath)) {
159
+ oldContent = fs.readFileSync(focusPath, 'utf-8');
160
+ }
161
+ // 备份当前版本
162
+ const backupPath = oldContent ? backupToHistory(focusPath, oldContent) : null;
163
+ // 递增版本号(支持小数版本)
164
+ const oldVersion = extractVersion(oldContent);
165
+ // 解析版本号并递增
166
+ const versionParts = oldVersion.split('.');
167
+ const majorVersion = parseInt(versionParts[0], 10) || 1;
168
+ const newVersion = `${majorVersion + 1}`;
169
+ const today = new Date().toISOString().split('T')[0];
170
+ // 更新版本号和日期
171
+ const updatedContent = newContent
172
+ .replace(/\*\*版本\*\*:\s*v[\d.]+/i, `**版本**: v${newVersion}`)
173
+ .replace(/\*\*更新\*\*:\s*\d{4}-\d{2}-\d{2}/, `**更新**: ${today}`);
174
+ // 写入新内容
175
+ fs.writeFileSync(focusPath, updatedContent, 'utf-8');
176
+ // 清理过期历史
177
+ const historyDir = getHistoryDir(focusPath);
178
+ const beforeCount = fs.existsSync(historyDir)
179
+ ? fs.readdirSync(historyDir).filter(f => f.startsWith('CURRENT_FOCUS.v')).length
180
+ : 0;
181
+ cleanupHistory(focusPath);
182
+ const afterCount = fs.existsSync(historyDir)
183
+ ? fs.readdirSync(historyDir).filter(f => f.startsWith('CURRENT_FOCUS.v')).length
184
+ : 0;
185
+ return {
186
+ backupPath,
187
+ cleanedCount: beforeCount - afterCount
188
+ };
189
+ }
190
+ /**
191
+ * 智能摘要提取
192
+ *
193
+ * 优先提取关键章节,确保不丢失重要信息
194
+ * 对于非结构化内容,回退到简单的行截取
195
+ *
196
+ * @param content CURRENT_FOCUS.md 内容
197
+ * @param maxLines 最大行数
198
+ */
199
+ export function extractSummary(content, maxLines = 30) {
200
+ const lines = content.split('\n');
201
+ const sections = {
202
+ header: [], // 标题和元数据
203
+ snapshot: [], // 状态快照
204
+ current: [], // 当前任务
205
+ nextSteps: [], // 下一步
206
+ reference: [] // 参考
207
+ };
208
+ let currentSection = 'header';
209
+ let hasStructuredSections = false;
210
+ for (const line of lines) {
211
+ // 识别章节(使用更宽松的匹配,支持不同格式)
212
+ const trimmedLine = line.trim();
213
+ // 使用正则匹配,支持 h1-h3 和多种格式
214
+ if (/^#{1,3}\s*.*状态快照|📍/.test(trimmedLine)) {
215
+ currentSection = 'snapshot';
216
+ hasStructuredSections = true;
217
+ }
218
+ else if (/^#{1,3}\s*.*当前任务|🔄/.test(trimmedLine)) {
219
+ currentSection = 'current';
220
+ hasStructuredSections = true;
221
+ }
222
+ else if (/^#{1,3}\s*.*下一步|➡️/.test(trimmedLine)) {
223
+ currentSection = 'nextSteps';
224
+ hasStructuredSections = true;
225
+ }
226
+ else if (/^#{1,3}\s*.*参考|📎/.test(trimmedLine)) {
227
+ currentSection = 'reference';
228
+ hasStructuredSections = true;
229
+ }
230
+ else if (trimmedLine === '---') {
231
+ continue; // 跳过分隔线
232
+ }
233
+ else if (line.startsWith('<!--')) {
234
+ continue; // 跳过注释
235
+ }
236
+ sections[currentSection].push(line);
237
+ }
238
+ // 如果没有结构化章节,回退到简单的行截取
239
+ if (!hasStructuredSections) {
240
+ const result = lines.slice(0, maxLines);
241
+ if (lines.length > maxLines) {
242
+ result.push('');
243
+ result.push('...[truncated, see CURRENT_FOCUS.md for full context]');
244
+ }
245
+ return result.join('\n');
246
+ }
247
+ // 按优先级拼接
248
+ const result = [
249
+ ...sections.header.slice(0, 5), // 标题 + 元数据
250
+ '',
251
+ '---',
252
+ '',
253
+ ...sections.snapshot.slice(0, 10), // 状态快照
254
+ '',
255
+ ...sections.nextSteps.slice(0, 10), // 下一步(优先级高)
256
+ '',
257
+ ...sections.current.slice(0, 15), // 当前任务
258
+ ];
259
+ // 限制总行数
260
+ const trimmed = result.slice(0, maxLines);
261
+ if (result.length > maxLines) {
262
+ trimmed.push('');
263
+ trimmed.push('...[truncated, see CURRENT_FOCUS.md for full context]');
264
+ }
265
+ return trimmed.join('\n');
266
+ }