principles-disciple 1.36.0 → 1.38.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 (77) hide show
  1. package/openclaw.plugin.json +1 -1
  2. package/package.json +1 -1
  3. package/src/commands/capabilities.ts +2 -1
  4. package/src/commands/context.ts +5 -4
  5. package/src/commands/focus.ts +5 -4
  6. package/src/commands/nocturnal-train.ts +14 -12
  7. package/src/commands/pd-reflect.ts +4 -3
  8. package/src/commands/promote-impl.ts +2 -1
  9. package/src/commands/rollback-impl.ts +6 -5
  10. package/src/core/adaptive-thresholds.ts +3 -4
  11. package/src/core/code-implementation-storage.ts +6 -5
  12. package/src/core/config.ts +3 -2
  13. package/src/core/correction-cue-learner.ts +42 -14
  14. package/src/core/correction-types.ts +1 -1
  15. package/src/core/diagnostician-task-store.ts +5 -8
  16. package/src/core/dictionary.ts +3 -2
  17. package/src/core/empathy-keyword-matcher.ts +5 -4
  18. package/src/core/event-log.ts +5 -1
  19. package/src/core/evolution-engine.ts +1 -0
  20. package/src/core/evolution-reducer.ts +4 -3
  21. package/src/core/focus-history.ts +20 -17
  22. package/src/core/hygiene/tracker.ts +2 -1
  23. package/src/core/init.ts +2 -1
  24. package/src/core/model-deployment-registry.ts +3 -4
  25. package/src/core/model-training-registry.ts +3 -4
  26. package/src/core/nocturnal-artifact-lineage.ts +2 -3
  27. package/src/core/nocturnal-dataset.ts +4 -5
  28. package/src/core/nocturnal-export.ts +8 -7
  29. package/src/core/nocturnal-trinity-types.ts +124 -0
  30. package/src/core/pain.ts +6 -6
  31. package/src/core/path-resolver.ts +2 -1
  32. package/src/core/pd-task-reconciler.ts +4 -5
  33. package/src/core/pd-task-store.ts +3 -4
  34. package/src/core/principle-tree-ledger.ts +6 -5
  35. package/src/core/promotion-gate.ts +8 -9
  36. package/src/core/replay-engine.ts +10 -10
  37. package/src/core/session-tracker.ts +3 -1
  38. package/src/core/shadow-observation-registry.ts +3 -4
  39. package/src/core/training-program.ts +3 -1
  40. package/src/core/trajectory.ts +8 -7
  41. package/src/hooks/gate-block-helper.ts +1 -1
  42. package/src/hooks/lifecycle.ts +3 -2
  43. package/src/hooks/llm.ts +3 -2
  44. package/src/hooks/prompt.ts +77 -18
  45. package/src/index.ts +2 -1
  46. package/src/service/central-sync-service.ts +2 -0
  47. package/src/service/cooldown-strategy.ts +97 -0
  48. package/src/service/evolution-dedup.ts +74 -0
  49. package/src/service/evolution-pain-context.ts +79 -0
  50. package/src/service/evolution-queue-lock.ts +47 -0
  51. package/src/service/evolution-queue-migration.ts +173 -0
  52. package/src/service/evolution-worker.ts +131 -31
  53. package/src/service/failure-classifier.ts +79 -0
  54. package/src/service/keyword-optimization-service.ts +22 -15
  55. package/src/service/nocturnal-config.ts +149 -7
  56. package/src/service/nocturnal-runtime.ts +24 -4
  57. package/src/service/nocturnal-service.ts +1 -8
  58. package/src/service/startup-reconciler.ts +112 -0
  59. package/src/service/subagent-workflow/correction-observer-types.ts +2 -2
  60. package/src/service/subagent-workflow/correction-observer-workflow-manager.ts +1 -1
  61. package/src/service/subagent-workflow/index.ts +0 -14
  62. package/src/service/subagent-workflow/workflow-manager-base.ts +1 -0
  63. package/src/tools/write-pain-flag.ts +2 -3
  64. package/src/utils/io.ts +43 -1
  65. package/tests/core/dictionary.test.ts +4 -1
  66. package/tests/core/pain-score.property.test.ts +205 -0
  67. package/tests/integration/chaos-resilience.test.ts +348 -0
  68. package/tests/integration/gate-real-io.e2e.test.ts +251 -0
  69. package/tests/integration/pain-diagnostician-loop.e2e.test.ts +380 -0
  70. package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +8 -2
  71. package/tests/integration/trajectory-lifecycle.e2e.test.ts +523 -0
  72. package/tests/service/cooldown-strategy.test.ts +163 -0
  73. package/tests/service/failure-classifier.test.ts +171 -0
  74. package/tests/service/startup-reconciler.test.ts +148 -0
  75. package/vitest.config.ts +23 -4
  76. package/src/service/correction-observer-types.ts +0 -69
  77. package/src/service/correction-observer-workflow-manager.ts +0 -247
@@ -19,6 +19,7 @@ import type {
19
19
  EmpathyRollbackEventData,
20
20
  } from '../types/event-types.js';
21
21
  import { createEmptyDailyStats } from '../types/event-types.js';
22
+ import { atomicWriteFileSync } from '../utils/io.js';
22
23
  import type { PluginLogger } from '../openclaw-sdk.js';
23
24
 
24
25
  /**
@@ -295,6 +296,9 @@ export class EventLog {
295
296
 
296
297
  private startFlushTimer(): void {
297
298
  this.flushTimer = setInterval(() => this.flush(), this.flushIntervalMs);
299
+ // Don't keep the process alive just for this timer
300
+ // This allows tests and CLI to exit without waiting for flush
301
+ this.flushTimer.unref();
298
302
  }
299
303
 
300
304
  flush(): void {
@@ -386,7 +390,7 @@ export class EventLog {
386
390
  });
387
391
 
388
392
  try {
389
- fs.writeFileSync(this.statsFile, JSON.stringify(data, null, 2), 'utf-8');
393
+ atomicWriteFileSync(this.statsFile, JSON.stringify(data, null, 2));
390
394
  } catch (e) {
391
395
  if (this.logger) this.logger.error(`[PD] Failed to flush daily-stats.json: ${String(e)}`);
392
396
  }
@@ -469,6 +469,7 @@ export class EvolutionEngine {
469
469
  this.retryTimer = setTimeout(() => {
470
470
  this.processRetryQueue();
471
471
  }, 1000);
472
+ this.retryTimer.unref(); // Don't keep process alive for retry
472
473
  }
473
474
  }
474
475
 
@@ -2,6 +2,7 @@ import * as crypto from 'crypto';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
4
  import { withLock } from '../utils/file-lock.js';
5
+ import { atomicWriteFileSync } from '../utils/io.js';
5
6
  import { PathResolver } from './path-resolver.js';
6
7
  import { SystemLogger } from './system-logger.js';
7
8
  import { shouldIgnorePainProtocolText } from './dictionary.js';
@@ -441,7 +442,7 @@ export class EvolutionReducerImpl implements EvolutionReducer {
441
442
  if (!fs.existsSync(parentDir)) {
442
443
  fs.mkdirSync(parentDir, { recursive: true });
443
444
  }
444
- fs.writeFileSync(this.principlesPath, content, 'utf8');
445
+ atomicWriteFileSync(this.principlesPath, content);
445
446
  SystemLogger.log(this.workspaceDir, 'PRINCIPLES_CREATED', `Created PRINCIPLES.md`);
446
447
  }
447
448
 
@@ -460,7 +461,7 @@ export class EvolutionReducerImpl implements EvolutionReducer {
460
461
  content = content + separator + formatted;
461
462
  }
462
463
 
463
- fs.writeFileSync(this.principlesPath, content, 'utf8');
464
+ atomicWriteFileSync(this.principlesPath, content);
464
465
  SystemLogger.log(this.workspaceDir, 'PRINCIPLE_SYNCED', `Principle ${principle.id} synced to PRINCIPLES.md`);
465
466
  }, { lockStaleMs: 10000 });
466
467
  return true;
@@ -752,7 +753,7 @@ export class EvolutionReducerImpl implements EvolutionReducer {
752
753
  private persistBlacklist(entry: { painId?: string; pattern?: string; reason: string; rolledBackAt: string }): void {
753
754
  const list = this.loadBlacklist();
754
755
  list.push(entry);
755
- fs.writeFileSync(this.blacklistPath, JSON.stringify(list, null, 2), 'utf8');
756
+ atomicWriteFileSync(this.blacklistPath, JSON.stringify(list, null, 2));
756
757
  }
757
758
 
758
759
  private loadBlacklist(): { painId?: string; pattern?: string; reason: string; rolledBackAt: string }[] {
@@ -10,6 +10,7 @@
10
10
 
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
13
+ import { atomicWriteFileSync } from '../utils/io.js';
13
14
 
14
15
  // ============================================================================
15
16
  // 工作记忆数据结构
@@ -124,7 +125,7 @@ export function backupToHistory(focusPath: string, content: string): string | nu
124
125
  }
125
126
 
126
127
  try {
127
- fs.writeFileSync(backupPath, content, 'utf-8');
128
+ atomicWriteFileSync(backupPath, content);
128
129
  return backupPath;
129
130
  } catch (error) {
130
131
  logError(`Failed to write backup file: ${backupPath}`, error);
@@ -236,7 +237,7 @@ export function compressFocus(focusPath: string, newContent: string): {
236
237
  .replace(/\*\*更新\*\*:\s*\d{4}-\d{2}-\d{2}/, `**更新**: ${today}`);
237
238
 
238
239
  // 写入新内容
239
- fs.writeFileSync(focusPath, updatedContent, 'utf-8');
240
+ atomicWriteFileSync(focusPath, updatedContent);
240
241
 
241
242
  // 清理过期历史
242
243
  const historyDir = getHistoryDir(focusPath);
@@ -430,7 +431,7 @@ export function extractWorkingMemory(
430
431
 
431
432
  // 尝试从文本中提取描述
432
433
 
433
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
434
+
434
435
  const description = extractDescription(text, filePath);
435
436
 
436
437
  snapshot.artifacts.push({
@@ -446,23 +447,23 @@ export function extractWorkingMemory(
446
447
 
447
448
  // 从文本中提取文件操作(备用方式)
448
449
 
449
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
450
+
450
451
  extractFileArtifacts(text, snapshot.artifacts, workspaceDir);
451
452
 
452
453
  // 提取问题
453
454
 
454
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
455
+
455
456
  extractProblems(text, snapshot.activeProblems);
456
457
 
457
458
  // 提取下一步
458
459
 
459
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
460
+
460
461
  extractNextActions(text, snapshot.nextActions);
461
462
  }
462
463
 
463
464
  // 去重和限制数量
464
465
 
465
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
466
+
466
467
  snapshot.artifacts = deduplicateArtifacts(snapshot.artifacts).slice(-MAX_ARTIFACTS);
467
468
  snapshot.activeProblems = snapshot.activeProblems.slice(-MAX_PROBLEMS);
468
469
  snapshot.nextActions = snapshot.nextActions.slice(-MAX_NEXT_ACTIONS);
@@ -483,7 +484,7 @@ function extractFileArtifacts(
483
484
  const filePathRegex = /(?:file_path|absolute_path)["']?\s*[:=]\s*["']([^"']+\.(ts|js|json|md|yaml|yml|py|sh|mjs|cjs))["']/gi;
484
485
 
485
486
 
486
- // eslint-disable-next-line @typescript-eslint/init-declarations
487
+
487
488
  let match;
488
489
  while ((match = filePathRegex.exec(text)) !== null) {
489
490
  const [, filePath] = match;
@@ -514,7 +515,7 @@ function extractFileArtifacts(
514
515
 
515
516
  // 尝试提取描述(从附近的文本)
516
517
 
517
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
518
+
518
519
  const description = extractDescription(text, filePath);
519
520
 
520
521
  artifacts.push({
@@ -548,7 +549,7 @@ function extractFileArtifacts(
548
549
  }
549
550
 
550
551
 
551
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
552
+
552
553
  const description = extractDescription(text, filePath);
553
554
 
554
555
  artifacts.push({
@@ -597,7 +598,7 @@ function extractProblems(
597
598
  // 问题模式(匹配问题描述)
598
599
  const problemPattern = /(?:问题|problem|error|错误|失败|failed)[::]\s*([^\n]{5,100})/gi;
599
600
 
600
- // eslint-disable-next-line @typescript-eslint/init-declarations
601
+
601
602
  let match;
602
603
  while ((match = problemPattern.exec(text)) !== null) {
603
604
  const content = match[1].trim();
@@ -641,7 +642,7 @@ function extractNextActions(text: string, actions: string[]): void {
641
642
 
642
643
  for (const pattern of patterns) {
643
644
 
644
- // eslint-disable-next-line @typescript-eslint/init-declarations
645
+
645
646
  let match;
646
647
  while ((match = pattern.exec(text)) !== null) {
647
648
  const action = match[1].trim();
@@ -701,7 +702,7 @@ export function parseWorkingMemorySection(content: string): WorkingMemorySnapsho
701
702
  // | 文件路径 | 操作 | 描述 |
702
703
  const tableRegex = /\|\s*`?([^`|\n]+)`?\s*\|\s*(created|modified|deleted)\s*\|\s*([^|\n]*)\s*\|/gi;
703
704
 
704
- // eslint-disable-next-line @typescript-eslint/init-declarations
705
+
705
706
  let match;
706
707
  while ((match = tableRegex.exec(wmContent)) !== null) {
707
708
  snapshot.artifacts.push({
@@ -741,7 +742,7 @@ export function mergeWorkingMemory(content: string, snapshot: WorkingMemorySnaps
741
742
 
742
743
  // 生成 Working Memory 章节
743
744
 
744
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
745
+
745
746
  const wmSection = generateWorkingMemorySection(snapshot);
746
747
 
747
748
  if (wmIndex === -1) {
@@ -958,7 +959,7 @@ function recordCompressTime(stateDir: string): void {
958
959
  if (!fs.existsSync(stateDir)) {
959
960
  fs.mkdirSync(stateDir, { recursive: true });
960
961
  }
961
- fs.writeFileSync(path.join(stateDir, LAST_COMPRESS_FILE), Date.now().toString(), 'utf-8');
962
+ atomicWriteFileSync(path.join(stateDir, LAST_COMPRESS_FILE), Date.now().toString());
962
963
  } catch (error) {
963
964
  logError('Failed to record compress time', error);
964
965
  }
@@ -1171,6 +1172,7 @@ export function autoCompressFocus(
1171
1172
  milestonesArchived: boolean;
1172
1173
  backupPath: string | null;
1173
1174
  reason: string;
1175
+ newContent?: string;
1174
1176
  } {
1175
1177
  // 检查文件是否存在
1176
1178
  if (!fs.existsSync(focusPath)) {
@@ -1250,7 +1252,7 @@ export function autoCompressFocus(
1250
1252
  cleanupHistory(focusPath);
1251
1253
 
1252
1254
  // 8. 写入新内容
1253
- fs.writeFileSync(focusPath, newContent, 'utf-8');
1255
+ atomicWriteFileSync(focusPath, newContent);
1254
1256
 
1255
1257
  // 9. 记录压缩时间
1256
1258
  if (stateDir) {
@@ -1265,6 +1267,7 @@ export function autoCompressFocus(
1265
1267
  newLines,
1266
1268
  milestonesArchived,
1267
1269
  backupPath,
1270
+ newContent,
1268
1271
  reason: `Auto-compressed: ${oldLines} → ${newLines} lines`
1269
1272
  };
1270
1273
  }
@@ -1393,7 +1396,7 @@ export function recoverFromTemplate(
1393
1396
  }
1394
1397
 
1395
1398
  // 写入恢复的内容
1396
- fs.writeFileSync(focusPath, template, 'utf-8');
1399
+ atomicWriteFileSync(focusPath, template);
1397
1400
 
1398
1401
  return {
1399
1402
  success: true,
@@ -2,6 +2,7 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import type { HygieneStats, PersistenceAction } from '../../types/hygiene-types.js';
4
4
  import { createEmptyHygieneStats } from '../../types/hygiene-types.js';
5
+ import { atomicWriteFileSync } from '../../utils/io.js';
5
6
  import type { PluginLogger } from '../../openclaw-sdk.js';
6
7
 
7
8
  /**
@@ -79,7 +80,7 @@ export class HygieneTracker {
79
80
 
80
81
  try {
81
82
  // Use a temporary file for atomic write if possible, or simple write
82
- fs.writeFileSync(this.statsFile, JSON.stringify(allStats, null, 2), 'utf-8');
83
+ atomicWriteFileSync(this.statsFile, JSON.stringify(allStats, null, 2));
83
84
  } catch (e) {
84
85
  this.logger?.error(`[PD] Failed to write hygiene-stats.json: ${String(e)}`);
85
86
  }
package/src/core/init.ts CHANGED
@@ -5,6 +5,7 @@ import type { OpenClawPluginApi, PluginLogger } from '../openclaw-sdk.js';
5
5
  import { PD_DIRS } from './paths.js';
6
6
  import { defaultContextConfig } from '../types.js';
7
7
  import { loadStore, setPrincipleState, type PrincipleTrainingState } from './principle-training-state.js';
8
+ import { atomicWriteFileSync } from '../utils/io.js';
8
9
 
9
10
  /**
10
11
  * Default PROFILE.json content
@@ -102,7 +103,7 @@ export function ensureWorkspaceTemplates(api: OpenClawPluginApi, workspaceDir: s
102
103
  if (!fs.existsSync(principlesDir)) {
103
104
  fs.mkdirSync(principlesDir, { recursive: true });
104
105
  }
105
- fs.writeFileSync(profilePath, JSON.stringify(DEFAULT_PROFILE, null, 2), 'utf-8');
106
+ atomicWriteFileSync(profilePath, JSON.stringify(DEFAULT_PROFILE, null, 2));
106
107
  api.logger.info(`[PD] Initialized PROFILE.json with default contextInjection config`);
107
108
  }
108
109
  } catch (err) {
@@ -33,6 +33,7 @@ import * as fs from 'fs';
33
33
  import * as path from 'path';
34
34
  import * as crypto from 'crypto';
35
35
  import { withLock } from '../utils/file-lock.js';
36
+ import { atomicWriteFileSync } from '../utils/io.js';
36
37
  import type { Checkpoint } from './model-training-registry.js';
37
38
  import {
38
39
  getCheckpoint,
@@ -241,9 +242,7 @@ function readRegistry(stateDir: string): ModelDeploymentRegistry {
241
242
  function writeRegistry(stateDir: string, registry: ModelDeploymentRegistry): void {
242
243
  ensureRegistryDir(stateDir);
243
244
  const registryPath = getRegistryPath(stateDir);
244
- const tmpPath = `${registryPath}.tmp`;
245
- fs.writeFileSync(tmpPath, JSON.stringify(registry, null, 2), 'utf-8');
246
- fs.renameSync(tmpPath, registryPath);
245
+ atomicWriteFileSync(registryPath, JSON.stringify(registry, null, 2));
247
246
  }
248
247
 
249
248
  /**
@@ -350,7 +349,7 @@ export function assertPromotionGatePassed(stateDir: string, checkpointId: string
350
349
  * @throws Error if checkpoint's targetModelFamily violates profile constraints
351
350
  */
352
351
 
353
- // eslint-disable-next-line @typescript-eslint/max-params
352
+
354
353
  export function bindCheckpointToWorkerProfile(
355
354
  stateDir: string,
356
355
  workerProfile: WorkerProfile,
@@ -34,6 +34,7 @@ import * as fs from 'fs';
34
34
  import * as path from 'path';
35
35
  import * as crypto from 'crypto';
36
36
  import { withLock } from '../utils/file-lock.js';
37
+ import { atomicWriteFileSync } from '../utils/io.js';
37
38
 
38
39
  // ---------------------------------------------------------------------------
39
40
  // Constants
@@ -239,9 +240,7 @@ function readRegistry(stateDir: string): ModelTrainingRegistry {
239
240
  function writeRegistry(stateDir: string, registry: ModelTrainingRegistry): void {
240
241
  ensureRegistryDir(stateDir);
241
242
  const registryPath = getRegistryPath(stateDir);
242
- const tmpPath = `${registryPath}.tmp`;
243
- fs.writeFileSync(tmpPath, JSON.stringify(registry, null, 2), 'utf-8');
244
- fs.renameSync(tmpPath, registryPath);
243
+ atomicWriteFileSync(registryPath, JSON.stringify(registry, null, 2));
245
244
  }
246
245
 
247
246
  /**
@@ -322,7 +321,7 @@ export function registerTrainingRun(
322
321
  * @throws Error if run not found or transition is invalid
323
322
  */
324
323
 
325
- // eslint-disable-next-line @typescript-eslint/max-params
324
+
326
325
  export function updateTrainingRunStatus(
327
326
  stateDir: string,
328
327
  trainRunId: string,
@@ -2,6 +2,7 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { resolveNocturnalDir } from './nocturnal-paths.js';
4
4
  import { withLock } from '../utils/file-lock.js';
5
+ import { atomicWriteFileSync } from '../utils/io.js';
5
6
 
6
7
  export type ArtifactKind = 'behavioral-sample' | 'rule-implementation-candidate';
7
8
 
@@ -56,9 +57,7 @@ function writeArtifactLineageRegistry(
56
57
  ): void {
57
58
  const registryPath = getLineageRegistryPath(workspaceDir);
58
59
  fs.mkdirSync(path.dirname(registryPath), { recursive: true });
59
- const tmpPath = `${registryPath}.tmp`;
60
- fs.writeFileSync(tmpPath, JSON.stringify(records, null, 2), 'utf-8');
61
- fs.renameSync(tmpPath, registryPath);
60
+ atomicWriteFileSync(registryPath, JSON.stringify(records, null, 2));
62
61
  }
63
62
 
64
63
  export function appendArtifactLineageRecord(
@@ -31,6 +31,7 @@ import * as crypto from 'crypto';
31
31
  import { NocturnalPathResolver, resolveNocturnalDir } from './nocturnal-paths.js';
32
32
  import type { NocturnalArtifact } from './nocturnal-arbiter.js';
33
33
  import { withLock } from '../utils/file-lock.js';
34
+ import { atomicWriteFileSync } from '../utils/io.js';
34
35
 
35
36
  // ---------------------------------------------------------------------------
36
37
  // Types
@@ -246,9 +247,7 @@ function readRegistry(workspaceDir: string): NocturnalDatasetRecord[] {
246
247
  function writeRegistry(workspaceDir: string, records: NocturnalDatasetRecord[]): void {
247
248
  ensureRegistryDir(workspaceDir);
248
249
  const registryPath = getRegistryPath(workspaceDir);
249
- const tmpPath = `${registryPath}.tmp`;
250
- fs.writeFileSync(tmpPath, JSON.stringify(records, null, 2), 'utf-8');
251
- fs.renameSync(tmpPath, registryPath);
250
+ atomicWriteFileSync(registryPath, JSON.stringify(records, null, 2));
252
251
  }
253
252
 
254
253
  // ---------------------------------------------------------------------------
@@ -284,7 +283,7 @@ function withRegistryLock<T>(workspaceDir: string, fn: (_records: NocturnalDatas
284
283
  * @returns RegisterSampleResult
285
284
  */
286
285
 
287
- // eslint-disable-next-line @typescript-eslint/max-params
286
+
288
287
  export function registerSample(
289
288
  workspaceDir: string,
290
289
  artifact: NocturnalArtifact,
@@ -427,7 +426,7 @@ const VALID_TRANSITIONS: Record<NocturnalReviewStatus, NocturnalReviewStatus[]>
427
426
  * @throws Error if transition is invalid
428
427
  */
429
428
 
430
- // eslint-disable-next-line @typescript-eslint/max-params
429
+
431
430
  export function updateReviewStatus(
432
431
  workspaceDir: string,
433
432
  sampleFingerprint: string,
@@ -47,6 +47,7 @@
47
47
  import * as fs from 'fs';
48
48
  import * as path from 'path';
49
49
  import * as crypto from 'crypto';
50
+ import { atomicWriteFileSync } from '../utils/io.js';
50
51
  import {
51
52
  listDatasetRecords,
52
53
  readDatasetArtifact,
@@ -158,7 +159,7 @@ function computeDatasetFingerprint(sampleFingerprints: string[]): string {
158
159
  * Caller guarantees record.targetModelFamily is non-null.
159
160
  */
160
161
 
161
- // eslint-disable-next-line @typescript-eslint/max-params
162
+
162
163
  function serializeORPOSample(
163
164
  record: NocturnalDatasetRecord,
164
165
  artifact: ReturnType<typeof readDatasetArtifact>,
@@ -167,7 +168,7 @@ function serializeORPOSample(
167
168
  datasetFingerprint: string
168
169
  ): ORPOSample {
169
170
  const now = new Date().toISOString();
170
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
171
+
171
172
  const rejected = buildEvidenceBoundedRejected(artifact, evidenceSummary);
172
173
 
173
174
  return {
@@ -180,7 +181,7 @@ function serializeORPOSample(
180
181
  prompt: rejected,
181
182
  chosen: artifact.betterDecision,
182
183
  rejected,
183
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
184
+
184
185
  rationale: buildEvidenceBoundedRationale(evidenceSummary),
185
186
  datasetMetadata: {
186
187
  sampleFingerprint: record.sampleFingerprint,
@@ -295,7 +296,7 @@ export function exportORPOSamples(
295
296
  });
296
297
 
297
298
 
298
- // eslint-disable-next-line @typescript-eslint/init-declarations
299
+
299
300
  let eligibleRecords: typeof allApprovedRecords;
300
301
 
301
302
  if (targetModelFamily !== undefined && targetModelFamily !== null) {
@@ -345,7 +346,7 @@ export function exportORPOSamples(
345
346
 
346
347
  // Read artifact (throws on error — distinguishes read failure from missing artifact)
347
348
 
348
- // eslint-disable-next-line @typescript-eslint/init-declarations
349
+
349
350
  let artifact;
350
351
  try {
351
352
  artifact = readDatasetArtifact(workspaceDir, record.sampleFingerprint);
@@ -385,7 +386,7 @@ export function exportORPOSamples(
385
386
  const exportsDir = NocturnalPathResolver.exportsDir(workspaceDir);
386
387
  const jsonlPath = path.join(exportsDir, `${exportId}.jsonl`);
387
388
  const lines = orpoSamples.map((s) => JSON.stringify(s)).join('\n') + '\n';
388
- fs.writeFileSync(jsonlPath, lines, 'utf-8');
389
+ atomicWriteFileSync(jsonlPath, lines);
389
390
 
390
391
  // Step 8: Write manifest
391
392
  const manifest: ORPOExportManifest = {
@@ -404,7 +405,7 @@ export function exportORPOSamples(
404
405
  })),
405
406
  };
406
407
 
407
- fs.writeFileSync(manifest.manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
408
+ atomicWriteFileSync(manifest.manifestPath, JSON.stringify(manifest, null, 2));
408
409
 
409
410
  return {
410
411
  success: true,
@@ -5,6 +5,8 @@
5
5
  * Extracted to break circular dependency.
6
6
  */
7
7
 
8
+ import type { TrinityArtificerContext } from './nocturnal-artificer.js';
9
+
8
10
  // ---------------------------------------------------------------------------
9
11
  // Dreamer Types
10
12
  // ---------------------------------------------------------------------------
@@ -92,3 +94,125 @@ export interface PhilosopherOutput {
92
94
  /** Timestamp of generation */
93
95
  generatedAt: string;
94
96
  }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Trinity Result Types
100
+ // ---------------------------------------------------------------------------
101
+
102
+ /**
103
+ * Tournament trace entry for explainability.
104
+ */
105
+ export interface TournamentTraceEntry {
106
+ candidateIndex: number;
107
+ reason: string;
108
+ }
109
+
110
+ /**
111
+ * Analysis of a rejected candidate — why it lost the tournament.
112
+ * Informs training signal for "what to avoid".
113
+ */
114
+ export interface RejectedAnalysis {
115
+ /** Mental model that led to the rejected candidate */
116
+ whyRejected: string;
117
+ /** Observable caution triggers that were missed or ignored */
118
+ warningSignals: string[];
119
+ /** Correct reasoning path that should have been seen */
120
+ correctiveThinking: string;
121
+ }
122
+
123
+ /**
124
+ * Justification for the chosen candidate — why it won the tournament.
125
+ * Informs training signal for "what to do".
126
+ */
127
+ export interface ChosenJustification {
128
+ /** Why this candidate was selected over others */
129
+ whyChosen: string;
130
+ /** 1-3 transferable insights from this decision */
131
+ keyInsights: string[];
132
+ /** When this approach does NOT apply */
133
+ limitations: string[];
134
+ }
135
+
136
+ /**
137
+ * Contrastive analysis: key differences between chosen and rejected paths.
138
+ * Synthesizes the core lesson from the tournament.
139
+ */
140
+ export interface ContrastiveAnalysis {
141
+ /** ONE key insight distinguishing chosen from rejected */
142
+ criticalDifference: string;
143
+ /** Pattern: "When X, do Y" */
144
+ decisionTrigger: string;
145
+ /** How to systematically avoid the rejected path */
146
+ preventionStrategy: string;
147
+ }
148
+
149
+ /**
150
+ * Telemetry about Trinity chain execution.
151
+ */
152
+ export interface TrinityTelemetry {
153
+ chainMode: 'trinity' | 'single-reflector';
154
+ usedStubs: boolean;
155
+ dreamerPassed: boolean;
156
+ philosopherPassed: boolean;
157
+ scribePassed: boolean;
158
+ candidateCount: number;
159
+ selectedCandidateIndex: number;
160
+ stageFailures: string[];
161
+ tournamentTrace?: TournamentTraceEntry[];
162
+ winnerAggregateScore?: number;
163
+ winnerThresholdPassed?: boolean;
164
+ eligibleCandidateCount?: number;
165
+ diversityCheckPassed?: boolean;
166
+ candidateRiskLevels?: string[];
167
+ philosopher6D?: {
168
+ avgScores: {
169
+ principleAlignment: number;
170
+ specificity: number;
171
+ actionability: number;
172
+ executability: number;
173
+ safetyImpact: number;
174
+ uxImpact: number;
175
+ };
176
+ highRiskCount: number;
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Validation failure for a Trinity stage.
182
+ */
183
+ export interface TrinityStageFailure {
184
+ stage: 'dreamer' | 'philosopher' | 'scribe';
185
+ reason: string;
186
+ }
187
+
188
+ /**
189
+ * Result of Trinity chain execution.
190
+ */
191
+ export interface TrinityResult {
192
+ success: boolean;
193
+ artifact?: TrinityDraftArtifact;
194
+ telemetry: TrinityTelemetry;
195
+ failures: TrinityStageFailure[];
196
+ fallbackOccurred: boolean;
197
+ artificerContext?: TrinityArtificerContext;
198
+ }
199
+
200
+ /**
201
+ * Scribe output — final structured artifact draft.
202
+ */
203
+ export interface TrinityDraftArtifact {
204
+ selectedCandidateIndex: number;
205
+ badDecision: string;
206
+ betterDecision: string;
207
+ rationale: string;
208
+ sessionId: string;
209
+ principleId: string;
210
+ sourceSnapshotRef: string;
211
+ telemetry: TrinityTelemetry;
212
+ thinkingModelDelta?: number;
213
+ planningRatioGain?: number;
214
+ artificerContext?: TrinityArtificerContext;
215
+ contrastiveAnalysis?: ContrastiveAnalysis;
216
+ rejectedAnalysis?: RejectedAnalysis;
217
+ chosenJustification?: ChosenJustification;
218
+ }
package/src/core/pain.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
- import { serializeKvLines, parseKvLines } from '../utils/io.js';
3
+ import { serializeKvLines, parseKvLines, atomicWriteFileSync } from '../utils/io.js';
4
4
  import { resolvePdPath } from './paths.js';
5
5
  import { ConfigService } from './config-service.js';
6
6
  import { SystemLogger } from './system-logger.js';
@@ -98,7 +98,7 @@ export function validatePainFlag(data: Record<string, string>): string[] {
98
98
  return missing;
99
99
  }
100
100
 
101
- // eslint-disable-next-line @typescript-eslint/max-params
101
+
102
102
  export function computePainScore(rc: number, isSpiral: boolean, missingTestCommand: boolean, softScore: number, projectDir?: string): number {
103
103
  let score = Math.max(0, softScore || 0);
104
104
 
@@ -155,7 +155,7 @@ export function writePainFlag(projectDir: string, painData: PainFlagData): void
155
155
  if (!fs.existsSync(dir)) {
156
156
  fs.mkdirSync(dir, { recursive: true });
157
157
  }
158
- fs.writeFileSync(painFlagPath, serializeKvLines(painData), "utf-8");
158
+ atomicWriteFileSync(painFlagPath, serializeKvLines(painData));
159
159
  }
160
160
 
161
161
  /**
@@ -212,7 +212,7 @@ export function readPainFlagData(projectDir: string): Record<string, string> {
212
212
 
213
213
  // Detect JSON format (wrong — should be KV)
214
214
  if (content.startsWith('{')) {
215
- // eslint-disable-next-line @typescript-eslint/init-declarations
215
+
216
216
  let json: Record<string, unknown>;
217
217
  try {
218
218
  json = JSON.parse(content);
@@ -225,7 +225,7 @@ export function readPainFlagData(projectDir: string): Record<string, string> {
225
225
  const kvData = convertJsonToKv(json);
226
226
 
227
227
  const repaired = serializeKvLines(kvData);
228
- fs.writeFileSync(painFlagPath, repaired, 'utf-8');
228
+ atomicWriteFileSync(painFlagPath, repaired);
229
229
  SystemLogger.log(projectDir, 'PAIN_FLAG_AUTO_REPAIRED', `Auto-repaired pain flag from JSON to KV format (${Object.keys(json).length} fields)`);
230
230
  return kvData;
231
231
  }
@@ -281,7 +281,7 @@ export function readPainFlagContract(projectDir: string): PainFlagContractResult
281
281
  * Errors are silently ignored to avoid disrupting the pain pipeline.
282
282
  */
283
283
 
284
- // eslint-disable-next-line @typescript-eslint/max-params -- complexity 12, refactor candidate
284
+
285
285
  export function trackPrincipleValue(
286
286
  workspaceDir: string,
287
287
  painData: { reason?: string; source?: string; score?: string },
@@ -3,6 +3,7 @@ import * as os from 'os';
3
3
  import * as fs from 'fs';
4
4
  import type { OpenClawPluginApi } from '../openclaw-sdk.js';
5
5
  import { PathResolutionError } from '../config/index.js';
6
+ import { atomicWriteFileSync } from '../utils/io.js';
6
7
 
7
8
  export interface PathResolverOptions {
8
9
  workspaceDir?: string;
@@ -391,7 +392,7 @@ export function createDefaultConfig(targetPath?: string): string {
391
392
  fs.mkdirSync(dir, { recursive: true });
392
393
  }
393
394
 
394
- fs.writeFileSync(target, JSON.stringify(defaultConfig, null, 2), 'utf8');
395
+ atomicWriteFileSync(target, JSON.stringify(defaultConfig, null, 2));
395
396
 
396
397
  console.log(`✅ Created default config at: ${target}`);
397
398
  console.log(` You can edit this file to customize paths.`);