principles-disciple 1.5.4 → 1.7.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 (88) hide show
  1. package/dist/commands/context.d.ts +5 -0
  2. package/dist/commands/context.js +312 -0
  3. package/dist/commands/evolution-status.d.ts +4 -0
  4. package/dist/commands/evolution-status.js +138 -0
  5. package/dist/commands/export.d.ts +2 -0
  6. package/dist/commands/export.js +45 -0
  7. package/dist/commands/focus.d.ts +14 -0
  8. package/dist/commands/focus.js +582 -0
  9. package/dist/commands/pain.js +143 -6
  10. package/dist/commands/principle-rollback.d.ts +4 -0
  11. package/dist/commands/principle-rollback.js +22 -0
  12. package/dist/commands/rollback.d.ts +19 -0
  13. package/dist/commands/rollback.js +119 -0
  14. package/dist/commands/samples.d.ts +2 -0
  15. package/dist/commands/samples.js +55 -0
  16. package/dist/core/config.d.ts +37 -0
  17. package/dist/core/config.js +47 -0
  18. package/dist/core/control-ui-db.d.ts +68 -0
  19. package/dist/core/control-ui-db.js +274 -0
  20. package/dist/core/detection-funnel.d.ts +1 -1
  21. package/dist/core/detection-funnel.js +4 -0
  22. package/dist/core/dictionary.d.ts +2 -0
  23. package/dist/core/dictionary.js +13 -0
  24. package/dist/core/event-log.d.ts +22 -1
  25. package/dist/core/event-log.js +319 -0
  26. package/dist/core/evolution-engine.d.ts +5 -5
  27. package/dist/core/evolution-engine.js +18 -18
  28. package/dist/core/evolution-migration.d.ts +5 -0
  29. package/dist/core/evolution-migration.js +65 -0
  30. package/dist/core/evolution-reducer.d.ts +69 -0
  31. package/dist/core/evolution-reducer.js +369 -0
  32. package/dist/core/evolution-types.d.ts +103 -0
  33. package/dist/core/focus-history.d.ts +65 -0
  34. package/dist/core/focus-history.js +266 -0
  35. package/dist/core/init.js +30 -7
  36. package/dist/core/migration.js +0 -2
  37. package/dist/core/path-resolver.d.ts +3 -0
  38. package/dist/core/path-resolver.js +90 -31
  39. package/dist/core/paths.d.ts +7 -8
  40. package/dist/core/paths.js +48 -40
  41. package/dist/core/profile.js +1 -1
  42. package/dist/core/session-tracker.d.ts +4 -0
  43. package/dist/core/session-tracker.js +15 -0
  44. package/dist/core/thinking-models.d.ts +38 -0
  45. package/dist/core/thinking-models.js +170 -0
  46. package/dist/core/trajectory.d.ts +184 -0
  47. package/dist/core/trajectory.js +817 -0
  48. package/dist/core/trust-engine.d.ts +2 -0
  49. package/dist/core/trust-engine.js +30 -4
  50. package/dist/core/workspace-context.d.ts +13 -0
  51. package/dist/core/workspace-context.js +50 -7
  52. package/dist/hooks/gate.js +301 -30
  53. package/dist/hooks/llm.d.ts +8 -0
  54. package/dist/hooks/llm.js +347 -69
  55. package/dist/hooks/message-sanitize.d.ts +3 -0
  56. package/dist/hooks/message-sanitize.js +37 -0
  57. package/dist/hooks/pain.js +105 -5
  58. package/dist/hooks/prompt.d.ts +20 -11
  59. package/dist/hooks/prompt.js +558 -158
  60. package/dist/hooks/subagent.d.ts +9 -2
  61. package/dist/hooks/subagent.js +40 -3
  62. package/dist/http/principles-console-route.d.ts +2 -0
  63. package/dist/http/principles-console-route.js +257 -0
  64. package/dist/i18n/commands.js +48 -20
  65. package/dist/index.js +264 -8
  66. package/dist/service/control-ui-query-service.d.ts +217 -0
  67. package/dist/service/control-ui-query-service.js +537 -0
  68. package/dist/service/empathy-observer-manager.d.ts +42 -0
  69. package/dist/service/empathy-observer-manager.js +147 -0
  70. package/dist/service/evolution-worker.d.ts +10 -0
  71. package/dist/service/evolution-worker.js +156 -24
  72. package/dist/service/trajectory-service.d.ts +2 -0
  73. package/dist/service/trajectory-service.js +15 -0
  74. package/dist/tools/agent-spawn.d.ts +27 -6
  75. package/dist/tools/agent-spawn.js +339 -87
  76. package/dist/tools/deep-reflect.d.ts +27 -7
  77. package/dist/tools/deep-reflect.js +282 -113
  78. package/dist/types/event-types.d.ts +84 -2
  79. package/dist/types/event-types.js +33 -0
  80. package/dist/types.d.ts +52 -0
  81. package/dist/types.js +24 -1
  82. package/openclaw.plugin.json +43 -11
  83. package/package.json +16 -6
  84. package/templates/langs/zh/core/HEARTBEAT.md +28 -4
  85. package/templates/langs/zh/skills/pd-daily/SKILL.md +97 -13
  86. package/templates/pain_settings.json +54 -2
  87. package/templates/workspace/.principles/PROFILE.json +2 -0
  88. package/templates/workspace/okr/CURRENT_FOCUS.md +57 -0
@@ -44,6 +44,9 @@ export class EventLog {
44
44
  recordGateBlock(sessionId, data) {
45
45
  this.record('gate_block', 'blocked', sessionId, data);
46
46
  }
47
+ recordGateBypass(sessionId, data) {
48
+ this.record('gate_bypass', 'bypassed', sessionId, data);
49
+ }
47
50
  recordPlanApproval(sessionId, data) {
48
51
  this.record('plan_approval', 'approved', sessionId, data);
49
52
  }
@@ -57,6 +60,9 @@ export class EventLog {
57
60
  recordTrustChange(sessionId, data) {
58
61
  this.record('trust_change', 'changed', sessionId, data);
59
62
  }
63
+ recordEmpathyRollback(sessionId, data) {
64
+ this.record('empathy_rollback', 'rolled_back', sessionId, data);
65
+ }
60
66
  recordError(sessionId, message, context) {
61
67
  this.record('error', 'failure', sessionId, { message, ...context });
62
68
  }
@@ -115,6 +121,45 @@ export class EventLog {
115
121
  const data = entry.data;
116
122
  stats.pain.signalsDetected++;
117
123
  stats.pain.maxScore = Math.max(stats.pain.maxScore, data.score);
124
+ // Update empathy stats for user_empathy source
125
+ if (data.source === 'user_empathy') {
126
+ if (data.deduped) {
127
+ stats.empathy.dedupedCount++;
128
+ }
129
+ else {
130
+ stats.empathy.totalEvents++;
131
+ stats.empathy.totalPenaltyScore += data.score || 0;
132
+ // By severity
133
+ if (data.severity) {
134
+ stats.empathy.bySeverity[data.severity]++;
135
+ stats.empathy.scoreBySeverity[data.severity] += data.score || 0;
136
+ }
137
+ // By detection mode
138
+ if (data.detection_mode) {
139
+ stats.empathy.byDetectionMode[data.detection_mode]++;
140
+ }
141
+ // By origin
142
+ if (data.origin) {
143
+ stats.empathy.byOrigin[data.origin]++;
144
+ }
145
+ // Confidence distribution
146
+ const conf = data.confidence ?? 1;
147
+ if (conf >= 0.8)
148
+ stats.empathy.confidenceDistribution.high++;
149
+ else if (conf >= 0.5)
150
+ stats.empathy.confidenceDistribution.medium++;
151
+ else
152
+ stats.empathy.confidenceDistribution.low++;
153
+ }
154
+ // Update dedupe hit rate
155
+ const total = stats.empathy.totalEvents + stats.empathy.dedupedCount;
156
+ stats.empathy.dedupeHitRate = total > 0 ? stats.empathy.dedupedCount / total : 0;
157
+ }
158
+ }
159
+ else if (entry.type === 'empathy_rollback') {
160
+ const data = entry.data;
161
+ stats.empathy.rollbackCount++;
162
+ stats.empathy.rolledBackScore += data.originalScore || 0;
118
163
  }
119
164
  }
120
165
  startFlushTimer() {
@@ -164,6 +209,280 @@ export class EventLog {
164
209
  }
165
210
  return stats;
166
211
  }
212
+ /**
213
+ * Get aggregated empathy statistics for multiple time ranges.
214
+ * @param range 'today' | 'week' | 'session'
215
+ * @param sessionId Optional session ID for session-scoped stats
216
+ */
217
+ getEmpathyStats(range, sessionId) {
218
+ const now = new Date();
219
+ const today = this.formatDate(now);
220
+ // Aggregate stats based on range
221
+ const result = {
222
+ totalEvents: 0,
223
+ dedupedCount: 0,
224
+ dedupeHitRate: 0,
225
+ totalPenaltyScore: 0,
226
+ rolledBackScore: 0,
227
+ rollbackCount: 0,
228
+ bySeverity: { mild: 0, moderate: 0, severe: 0 },
229
+ scoreBySeverity: { mild: 0, moderate: 0, severe: 0 },
230
+ byDetectionMode: { structured: 0, legacy_tag: 0 },
231
+ byOrigin: { assistant_self_report: 0, user_manual: 0, system_infer: 0 },
232
+ confidenceDistribution: { high: 0, medium: 0, low: 0 },
233
+ dailyTrend: [],
234
+ };
235
+ if (range === 'session' && sessionId) {
236
+ // For session range, scan event buffer and events file
237
+ this.aggregateSessionEmpathy(sessionId, result);
238
+ }
239
+ else if (range === 'week') {
240
+ // For week range, aggregate last 7 days
241
+ for (let i = 0; i < 7; i++) {
242
+ const date = new Date(now);
243
+ date.setDate(date.getDate() - i);
244
+ const dateStr = this.formatDate(date);
245
+ const stats = this.getDailyStats(dateStr);
246
+ result.totalEvents += stats.empathy.totalEvents;
247
+ result.dedupedCount += stats.empathy.dedupedCount;
248
+ result.totalPenaltyScore += stats.empathy.totalPenaltyScore;
249
+ result.rolledBackScore += stats.empathy.rolledBackScore;
250
+ result.rollbackCount += stats.empathy.rollbackCount;
251
+ for (const sev of ['mild', 'moderate', 'severe']) {
252
+ result.bySeverity[sev] += stats.empathy.bySeverity[sev];
253
+ result.scoreBySeverity[sev] += stats.empathy.scoreBySeverity[sev];
254
+ }
255
+ result.byDetectionMode.structured += stats.empathy.byDetectionMode.structured;
256
+ result.byDetectionMode.legacy_tag += stats.empathy.byDetectionMode.legacy_tag;
257
+ for (const org of ['assistant_self_report', 'user_manual', 'system_infer']) {
258
+ result.byOrigin[org] += stats.empathy.byOrigin[org];
259
+ }
260
+ result.confidenceDistribution.high += stats.empathy.confidenceDistribution.high;
261
+ result.confidenceDistribution.medium += stats.empathy.confidenceDistribution.medium;
262
+ result.confidenceDistribution.low += stats.empathy.confidenceDistribution.low;
263
+ if (stats.empathy.totalEvents > 0 || stats.empathy.dedupedCount > 0) {
264
+ result.dailyTrend.push({
265
+ date: dateStr,
266
+ count: stats.empathy.totalEvents,
267
+ score: stats.empathy.totalPenaltyScore,
268
+ });
269
+ }
270
+ }
271
+ }
272
+ else {
273
+ // Today only
274
+ const stats = this.getDailyStats(today);
275
+ Object.assign(result, stats.empathy);
276
+ if (stats.empathy.totalEvents > 0 || stats.empathy.dedupedCount > 0) {
277
+ result.dailyTrend = [{
278
+ date: today,
279
+ count: stats.empathy.totalEvents,
280
+ score: stats.empathy.totalPenaltyScore,
281
+ }];
282
+ }
283
+ }
284
+ // Calculate dedupe hit rate
285
+ const total = result.totalEvents + result.dedupedCount;
286
+ result.dedupeHitRate = total > 0 ? result.dedupedCount / total : 0;
287
+ return result;
288
+ }
289
+ /**
290
+ * Aggregate empathy stats for a specific session.
291
+ */
292
+ aggregateSessionEmpathy(sessionId, result) {
293
+ // Check event buffer first
294
+ for (const entry of this.eventBuffer) {
295
+ if (entry.sessionId === sessionId && entry.type === 'pain_signal') {
296
+ const data = entry.data;
297
+ if (data.source === 'user_empathy') {
298
+ if (data.deduped) {
299
+ result.dedupedCount++;
300
+ }
301
+ else {
302
+ result.totalEvents++;
303
+ result.totalPenaltyScore += data.score || 0;
304
+ if (data.severity) {
305
+ result.bySeverity[data.severity]++;
306
+ result.scoreBySeverity[data.severity] += data.score || 0;
307
+ }
308
+ if (data.detection_mode)
309
+ result.byDetectionMode[data.detection_mode]++;
310
+ if (data.origin)
311
+ result.byOrigin[data.origin]++;
312
+ const conf = data.confidence ?? 1;
313
+ if (conf >= 0.8)
314
+ result.confidenceDistribution.high++;
315
+ else if (conf >= 0.5)
316
+ result.confidenceDistribution.medium++;
317
+ else
318
+ result.confidenceDistribution.low++;
319
+ }
320
+ }
321
+ }
322
+ else if (entry.sessionId === sessionId && entry.type === 'empathy_rollback') {
323
+ const data = entry.data;
324
+ result.rollbackCount++;
325
+ result.rolledBackScore += data.originalScore || 0;
326
+ }
327
+ }
328
+ // Also check events file for persisted events
329
+ if (fs.existsSync(this.eventsFile)) {
330
+ try {
331
+ const content = fs.readFileSync(this.eventsFile, 'utf-8');
332
+ const lines = content.trim().split('\n');
333
+ for (const line of lines) {
334
+ if (!line.trim())
335
+ continue;
336
+ try {
337
+ const entry = JSON.parse(line);
338
+ if (entry.sessionId === sessionId) {
339
+ if (entry.type === 'pain_signal') {
340
+ const data = entry.data;
341
+ if (data.source === 'user_empathy') {
342
+ if (data.deduped) {
343
+ result.dedupedCount++;
344
+ }
345
+ else {
346
+ result.totalEvents++;
347
+ result.totalPenaltyScore += data.score || 0;
348
+ if (data.severity) {
349
+ result.bySeverity[data.severity]++;
350
+ result.scoreBySeverity[data.severity] += data.score || 0;
351
+ }
352
+ if (data.detection_mode)
353
+ result.byDetectionMode[data.detection_mode]++;
354
+ if (data.origin)
355
+ result.byOrigin[data.origin]++;
356
+ const conf = data.confidence ?? 1;
357
+ if (conf >= 0.8)
358
+ result.confidenceDistribution.high++;
359
+ else if (conf >= 0.5)
360
+ result.confidenceDistribution.medium++;
361
+ else
362
+ result.confidenceDistribution.low++;
363
+ }
364
+ }
365
+ }
366
+ else if (entry.type === 'empathy_rollback') {
367
+ const data = entry.data;
368
+ result.rollbackCount++;
369
+ result.rolledBackScore += data.originalScore || 0;
370
+ }
371
+ }
372
+ }
373
+ catch {
374
+ // Skip malformed lines
375
+ }
376
+ }
377
+ }
378
+ catch (e) {
379
+ if (this.logger)
380
+ this.logger.error(`[PD] Failed to read events.jsonl: ${String(e)}`);
381
+ }
382
+ }
383
+ }
384
+ /**
385
+ * Rollback an empathy event by ID.
386
+ * Returns the rolled back score, or 0 if event not found.
387
+ */
388
+ rollbackEmpathyEvent(eventId, sessionId, reason, triggeredBy) {
389
+ // Search for the event in buffer and file
390
+ let foundEvent = null;
391
+ // Check buffer first
392
+ for (const entry of this.eventBuffer) {
393
+ if (entry.type === 'pain_signal') {
394
+ const data = entry.data;
395
+ if (entry.data.eventId === eventId && data.source === 'user_empathy') {
396
+ foundEvent = { entry, data };
397
+ break;
398
+ }
399
+ }
400
+ }
401
+ // If not in buffer, check file
402
+ if (!foundEvent && fs.existsSync(this.eventsFile)) {
403
+ try {
404
+ const content = fs.readFileSync(this.eventsFile, 'utf-8');
405
+ const lines = content.trim().split('\n');
406
+ for (const line of lines) {
407
+ if (!line.trim())
408
+ continue;
409
+ try {
410
+ const entry = JSON.parse(line);
411
+ if (entry.type === 'pain_signal') {
412
+ const data = entry.data;
413
+ if (entry.data.eventId === eventId && data.source === 'user_empathy') {
414
+ foundEvent = { entry, data };
415
+ break;
416
+ }
417
+ }
418
+ }
419
+ catch {
420
+ // Skip malformed lines
421
+ }
422
+ }
423
+ }
424
+ catch (e) {
425
+ if (this.logger)
426
+ this.logger.error(`[PD] Failed to read events.jsonl: ${String(e)}`);
427
+ }
428
+ }
429
+ if (!foundEvent || foundEvent.data.deduped) {
430
+ return 0;
431
+ }
432
+ const originalScore = foundEvent.data.score || 0;
433
+ // Record the rollback event
434
+ this.recordEmpathyRollback(sessionId, {
435
+ eventId,
436
+ originalScore,
437
+ originalSessionId: foundEvent.entry.sessionId,
438
+ reason,
439
+ triggeredBy,
440
+ });
441
+ return originalScore;
442
+ }
443
+ /**
444
+ * Get the last empathy event ID for a session (for rollback).
445
+ */
446
+ getLastEmpathyEventId(sessionId) {
447
+ // Check buffer in reverse
448
+ for (let i = this.eventBuffer.length - 1; i >= 0; i--) {
449
+ const entry = this.eventBuffer[i];
450
+ if (entry.sessionId === sessionId && entry.type === 'pain_signal') {
451
+ const data = entry.data;
452
+ if (data.source === 'user_empathy' && !data.deduped) {
453
+ return entry.data.eventId || null;
454
+ }
455
+ }
456
+ }
457
+ // Check file
458
+ if (fs.existsSync(this.eventsFile)) {
459
+ try {
460
+ const content = fs.readFileSync(this.eventsFile, 'utf-8');
461
+ const lines = content.trim().split('\n');
462
+ for (let i = lines.length - 1; i >= 0; i--) {
463
+ if (!lines[i].trim())
464
+ continue;
465
+ try {
466
+ const entry = JSON.parse(lines[i]);
467
+ if (entry.sessionId === sessionId && entry.type === 'pain_signal') {
468
+ const data = entry.data;
469
+ if (data.source === 'user_empathy' && !data.deduped) {
470
+ return entry.data.eventId || null;
471
+ }
472
+ }
473
+ }
474
+ catch {
475
+ // Skip malformed lines
476
+ }
477
+ }
478
+ }
479
+ catch (e) {
480
+ if (this.logger)
481
+ this.logger.error(`[PD] Failed to read events.jsonl: ${String(e)}`);
482
+ }
483
+ }
484
+ return null;
485
+ }
167
486
  /**
168
487
  * Dispose of the EventLog, flushing pending data and clearing timer.
169
488
  */
@@ -81,13 +81,13 @@ export declare class EvolutionEngine {
81
81
  private isLockStale;
82
82
  /** 持久化评分卡(含锁保护) */
83
83
  private saveScorecard;
84
- /** 保存失败后的重试队列 */
85
- private static retryQueue;
86
- private static retryTimer;
84
+ /** Per-instance retry queue (P0 fix: was static, causing cross-instance race) */
85
+ private retryQueue;
86
+ private retryTimer;
87
87
  /** 调度重试保存 */
88
- private static scheduleRetrySave;
88
+ private scheduleRetrySave;
89
89
  /** 处理重试队列 */
90
- private static processRetryQueue;
90
+ private processRetryQueue;
91
91
  /** 无锁快速保存(用于重试) */
92
92
  private saveScorecardImmediate;
93
93
  private generateId;
@@ -30,10 +30,10 @@ const EXPLORATORY_TOOLS = new Set([
30
30
  const CONSTRUCTIVE_TOOLS = new Set([
31
31
  'write', 'write_file', 'edit', 'edit_file', 'replace', 'apply_patch',
32
32
  'insert', 'patch', 'delete_file', 'move_file', 'run_shell_command',
33
- 'pd_spawn_agent', 'sessions_spawn',
33
+ 'pd_run_worker', 'sessions_spawn',
34
34
  ]);
35
35
  // 高风险工具:需要 allowRiskPath 权限
36
- // 注意:pd_spawn_agent 和 sessions_spawn 已从高风险中移出,它们由 allowSubagentSpawn 单独控制
36
+ // 注意:pd_run_worker 和 sessions_spawn 已从高风险中移出,它们由 allowSubagentSpawn 单独控制
37
37
  const HIGH_RISK_TOOLS = new Set([
38
38
  'run_shell_command', 'delete_file', 'move_file',
39
39
  ]);
@@ -202,7 +202,7 @@ export class EvolutionEngine {
202
202
  };
203
203
  }
204
204
  // 子智能体检查
205
- if ((context.toolName === 'pd_spawn_agent' || context.toolName === 'sessions_spawn') && !perms.allowSubagentSpawn) {
205
+ if ((context.toolName === 'pd_run_worker' || context.toolName === 'sessions_spawn') && !perms.allowSubagentSpawn) {
206
206
  return {
207
207
  allowed: false,
208
208
  reason: `Tier ${this.scorecard.currentTier} (${tierDef.name}) 未解锁子智能体权限`,
@@ -464,29 +464,29 @@ export class EvolutionEngine {
464
464
  release();
465
465
  }
466
466
  }
467
- /** 保存失败后的重试队列 */
468
- static retryQueue = [];
469
- static retryTimer = null;
467
+ /** Per-instance retry queue (P0 fix: was static, causing cross-instance race) */
468
+ retryQueue = [];
469
+ retryTimer = null;
470
470
  /** 调度重试保存 */
471
- static scheduleRetrySave(engine) {
471
+ scheduleRetrySave() {
472
472
  // 每个引擎只保留最新数据
473
- EvolutionEngine.retryQueue = EvolutionEngine.retryQueue.filter(item => item.engine !== engine);
474
- EvolutionEngine.retryQueue.push({ engine, data: { ...engine.scorecard } });
475
- // 启动重试定时器
476
- if (!EvolutionEngine.retryTimer) {
477
- EvolutionEngine.retryTimer = setTimeout(() => {
478
- EvolutionEngine.processRetryQueue();
473
+ this.retryQueue = this.retryQueue.filter(item => item.engine !== this);
474
+ this.retryQueue.push({ engine: this, data: { ...this.scorecard } });
475
+ // 启动重试定时器(每个实例独立)
476
+ if (!this.retryTimer) {
477
+ this.retryTimer = setTimeout(() => {
478
+ this.processRetryQueue();
479
479
  }, 1000);
480
480
  }
481
481
  }
482
482
  /** 处理重试队列 */
483
- static processRetryQueue() {
484
- EvolutionEngine.retryTimer = null;
483
+ processRetryQueue() {
484
+ this.retryTimer = null;
485
485
  const latestByEngine = new Map();
486
- for (const item of EvolutionEngine.retryQueue) {
486
+ for (const item of this.retryQueue) {
487
487
  latestByEngine.set(item.engine, item.data);
488
488
  }
489
- EvolutionEngine.retryQueue = [];
489
+ this.retryQueue = [];
490
490
  for (const [engine, data] of latestByEngine) {
491
491
  try {
492
492
  engine.saveScorecardImmediate(data);
@@ -494,7 +494,7 @@ export class EvolutionEngine {
494
494
  }
495
495
  catch (e) {
496
496
  console.error(`[Evolution] Retry save failed: ${String(e)}`);
497
- EvolutionEngine.scheduleRetrySave(engine);
497
+ engine.scheduleRetrySave(); // 每个引擎独立重试
498
498
  }
499
499
  }
500
500
  }
@@ -0,0 +1,5 @@
1
+ export interface MigrationResult {
2
+ importedEvents: number;
3
+ streamPath: string;
4
+ }
5
+ export declare function migrateLegacyEvolutionData(workspaceDir: string): MigrationResult;
@@ -0,0 +1,65 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { stableContentHash } from './evolution-reducer.js';
4
+ import { SystemLogger } from './system-logger.js';
5
+ function appendEvent(streamPath, event) {
6
+ fs.appendFileSync(streamPath, `${JSON.stringify(event)}\n`, 'utf8');
7
+ }
8
+ function loadImportedHashes(streamPath, workspaceDir) {
9
+ if (!fs.existsSync(streamPath))
10
+ return new Set();
11
+ const raw = fs.readFileSync(streamPath, 'utf8').trim();
12
+ if (!raw)
13
+ return new Set();
14
+ const hashes = new Set();
15
+ for (const line of raw.split('\n')) {
16
+ try {
17
+ const event = JSON.parse(line);
18
+ if (event.type !== 'legacy_import')
19
+ continue;
20
+ const hash = event.data.contentHash;
21
+ if (typeof hash === 'string')
22
+ hashes.add(hash);
23
+ }
24
+ catch (e) {
25
+ SystemLogger.log(workspaceDir, 'MIGRATION_WARN', `skip malformed line: ${String(e)}`);
26
+ }
27
+ }
28
+ return hashes;
29
+ }
30
+ export function migrateLegacyEvolutionData(workspaceDir) {
31
+ const streamPath = path.join(workspaceDir, 'memory', 'evolution.jsonl');
32
+ fs.mkdirSync(path.dirname(streamPath), { recursive: true });
33
+ const candidates = [
34
+ path.join(workspaceDir, 'memory', 'ISSUE_LOG.md'),
35
+ path.join(workspaceDir, 'memory', 'DECISIONS.md'),
36
+ path.join(workspaceDir, '.principles', 'PRINCIPLES.md'),
37
+ ];
38
+ const existingHashes = loadImportedHashes(streamPath, workspaceDir);
39
+ let importedEvents = 0;
40
+ for (const sourceFile of candidates) {
41
+ if (!fs.existsSync(sourceFile)) {
42
+ continue;
43
+ }
44
+ const content = fs.readFileSync(sourceFile, 'utf8').trim();
45
+ if (!content) {
46
+ continue;
47
+ }
48
+ const contentHash = stableContentHash(`${sourceFile}:${content}`);
49
+ if (existingHashes.has(contentHash)) {
50
+ continue;
51
+ }
52
+ appendEvent(streamPath, {
53
+ ts: new Date().toISOString(),
54
+ type: 'legacy_import',
55
+ data: {
56
+ sourceFile: path.relative(workspaceDir, sourceFile),
57
+ content,
58
+ contentHash,
59
+ },
60
+ });
61
+ importedEvents += 1;
62
+ existingHashes.add(contentHash);
63
+ }
64
+ return { importedEvents, streamPath };
65
+ }
@@ -0,0 +1,69 @@
1
+ import type { EvolutionLoopEvent, Principle } from './evolution-types.js';
2
+ export interface EvolutionReducer {
3
+ emit(event: EvolutionLoopEvent): void;
4
+ emitSync(event: EvolutionLoopEvent): void;
5
+ getEventLog(): EvolutionLoopEvent[];
6
+ getCandidatePrinciples(): Principle[];
7
+ getProbationPrinciples(): Principle[];
8
+ getActivePrinciples(): Principle[];
9
+ getPrincipleById(id: string): Principle | null;
10
+ promote(principleId: string, reason?: string): void;
11
+ deprecate(principleId: string, reason: string): void;
12
+ rollbackPrinciple(principleId: string, reason: string): void;
13
+ recordProbationFeedback(principleId: string, success: boolean): void;
14
+ getStats(): {
15
+ candidateCount: number;
16
+ probationCount: number;
17
+ activeCount: number;
18
+ deprecatedCount: number;
19
+ lastPromotedAt: string | null;
20
+ };
21
+ }
22
+ export declare class EvolutionReducerImpl implements EvolutionReducer {
23
+ private readonly streamPath;
24
+ private readonly lockTargetPath;
25
+ private readonly blacklistPath;
26
+ private readonly workspaceDir;
27
+ private readonly memoryEvents;
28
+ private readonly principles;
29
+ private readonly failureStreak;
30
+ private lastPromotedAt;
31
+ private isReplaying;
32
+ constructor(opts: {
33
+ workspaceDir: string;
34
+ });
35
+ emit(event: EvolutionLoopEvent): void;
36
+ emitSync(event: EvolutionLoopEvent): void;
37
+ getEventLog(): EvolutionLoopEvent[];
38
+ getCandidatePrinciples(): Principle[];
39
+ getProbationPrinciples(): Principle[];
40
+ getActivePrinciples(): Principle[];
41
+ getPrincipleById(id: string): Principle | null;
42
+ promote(principleId: string, reason?: string): void;
43
+ deprecate(principleId: string, reason: string): void;
44
+ rollbackPrinciple(principleId: string, reason: string): void;
45
+ recordProbationFeedback(principleId: string, success: boolean): void;
46
+ getStats(): {
47
+ candidateCount: number;
48
+ probationCount: number;
49
+ activeCount: number;
50
+ deprecatedCount: number;
51
+ lastPromotedAt: string | null;
52
+ };
53
+ private ensureDirs;
54
+ private loadFromStream;
55
+ private applyEvent;
56
+ private onCandidateCreated;
57
+ private onPrinciplePromoted;
58
+ private onPrincipleDeprecated;
59
+ private onPrincipleRolledBack;
60
+ private onPainDetected;
61
+ private updateFailureStreakFromPain;
62
+ private nextPrincipleId;
63
+ private getByStatus;
64
+ private sweepExpiredProbation;
65
+ private persistBlacklist;
66
+ private loadBlacklist;
67
+ private isBlacklisted;
68
+ }
69
+ export declare function stableContentHash(input: string): string;