principles-disciple 1.27.0 → 1.28.1

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 (33) hide show
  1. package/openclaw.plugin.json +4 -4
  2. package/package.json +4 -4
  3. package/scripts/diagnose-nocturnal.mjs +139 -2
  4. package/scripts/seed-nocturnal-scenarios.mjs +377 -0
  5. package/scripts/validate-live-path.ts +18 -18
  6. package/src/commands/nocturnal-train.ts +4 -6
  7. package/src/commands/pain.ts +8 -11
  8. package/src/commands/pd-reflect.ts +1 -1
  9. package/src/core/bootstrap-rules.ts +3 -3
  10. package/src/core/merge-gate-audit.ts +1 -1
  11. package/src/core/nocturnal-candidate-scoring.ts +131 -0
  12. package/src/core/nocturnal-reasoning-deriver.ts +337 -0
  13. package/src/core/nocturnal-trinity.ts +462 -25
  14. package/src/core/pain-context-extractor.ts +1 -3
  15. package/src/core/principle-tree-migration.ts +2 -4
  16. package/src/core/thinking-os-parser.ts +3 -3
  17. package/src/hooks/bash-risk.ts +1 -1
  18. package/src/hooks/gfi-gate.ts +1 -1
  19. package/src/hooks/pain.ts +1 -1
  20. package/src/hooks/prompt.ts +36 -2
  21. package/src/hooks/subagent.ts +1 -1
  22. package/src/index.ts +3 -1
  23. package/src/service/evolution-worker.ts +138 -44
  24. package/src/service/health-query-service.ts +15 -6
  25. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +0 -1
  26. package/src/tools/write-pain-flag.ts +191 -0
  27. package/templates/langs/en/skills/pd-pain-signal/SKILL.md +34 -20
  28. package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +34 -20
  29. package/tests/core/nocturnal-candidate-scoring.test.ts +132 -0
  30. package/tests/core/nocturnal-e2e.test.ts +224 -0
  31. package/tests/core/nocturnal-reasoning-deriver.test.ts +372 -0
  32. package/tests/core/nocturnal-trinity.test.ts +791 -0
  33. package/tests/tools/write-pain-flag.test.ts +240 -0
@@ -2,7 +2,7 @@
2
2
  "id": "principles-disciple",
3
3
  "name": "Principles Disciple",
4
4
  "description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
5
- "version": "1.27.0",
5
+ "version": "1.28.1",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
@@ -76,8 +76,8 @@
76
76
  }
77
77
  },
78
78
  "buildFingerprint": {
79
- "gitSha": "9037c29faf9a",
80
- "bundleMd5": "e7e9ebf7f67083f72f8f4328314d5670",
81
- "builtAt": "2026-04-13T06:10:29.622Z"
79
+ "gitSha": "27d380ef1db5",
80
+ "bundleMd5": "e6f2b6946eddc217169568fc45c5569e",
81
+ "builtAt": "2026-04-13T08:32:02.125Z"
82
82
  }
83
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.27.0",
3
+ "version": "1.28.1",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -40,14 +40,14 @@
40
40
  "@testing-library/react": "^16.3.0",
41
41
  "@types/better-sqlite3": "^7.6.13",
42
42
  "@types/micromatch": "^4.0.10",
43
- "@types/node": "^25.5.0",
43
+ "@types/node": "^25.6.0",
44
44
  "@types/react": "^19.2.2",
45
45
  "@types/react-dom": "^19.2.2",
46
46
  "@types/ws": "^8.5.13",
47
47
  "@typescript-eslint/eslint-plugin": "^8.58.0",
48
48
  "@typescript-eslint/parser": "^8.58.0",
49
49
  "@vitest/coverage-v8": "^4.1.0",
50
- "esbuild": "^0.27.4",
50
+ "esbuild": "^0.28.0",
51
51
  "eslint": "^10.1.0",
52
52
  "jsdom": "^29.0.1",
53
53
  "typescript": "^6.0.2",
@@ -64,7 +64,7 @@
64
64
  },
65
65
  "dependencies": {
66
66
  "@sinclair/typebox": "^0.34.48",
67
- "better-sqlite3": "^12.8.0",
67
+ "better-sqlite3": "^12.9.0",
68
68
  "lucide-react": "^1.7.0",
69
69
  "micromatch": "^4.0.8",
70
70
  "react": "^19.2.0",
@@ -12,10 +12,10 @@
12
12
  * Output: Structured report with pass/fail for each checkpoint.
13
13
  */
14
14
 
15
- import { existsSync, readFileSync, readdirSync, statSync } from 'fs';
15
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, rmSync } from 'fs';
16
16
  import { join, dirname } from 'path';
17
17
  import { fileURLToPath } from 'url';
18
- import { execSync } from 'child_process';
18
+ import { execSync, execFileSync } from 'child_process';
19
19
 
20
20
  const __filename = fileURLToPath(import.meta.url);
21
21
  const __dirname = dirname(__filename);
@@ -313,6 +313,45 @@ function main() {
313
313
  return 'No active pain flag';
314
314
  }
315
315
  const content = readFileSync(painFlagPath, 'utf-8');
316
+
317
+ // Self-healing: fix [object Object] corruption caused by bash heredoc/toString
318
+ if (content.includes('[object Object]')) {
319
+ // Try to extract and parse JSON object from anywhere in the content
320
+ // The corruption might be at any position, not just the beginning
321
+ try {
322
+ // Attempt 1: Extract JSON object using regex (handles {...} anywhere)
323
+ const jsonMatch = content.match(/\{[\s\S]*\}/);
324
+ if (jsonMatch) {
325
+ const json = JSON.parse(jsonMatch[0]);
326
+ // If we got a valid object, rewrite it properly as KV format
327
+ const kv = Object.entries(json)
328
+ .map(([k, v]) => `${k}: ${v === undefined ? '' : v}`)
329
+ .join('\n');
330
+ writeFileSync(painFlagPath, kv, 'utf-8');
331
+ return `Pain flag was corrupted ([object Object]), auto-repaired from JSON backup`;
332
+ }
333
+ // Attempt 2: Try parsing the whole content after removing common prefixes
334
+ const cleaned = content.replace(/^(active:\s*|source:\s*)/, '').trim();
335
+ if (cleaned.startsWith('{')) {
336
+ const json = JSON.parse(cleaned);
337
+ const kv = Object.entries(json)
338
+ .map(([k, v]) => `${k}: ${v === undefined ? '' : v}`)
339
+ .join('\n');
340
+ writeFileSync(painFlagPath, kv, 'utf-8');
341
+ return `Pain flag was corrupted ([object Object]), auto-repaired from JSON backup`;
342
+ }
343
+ throw new Error('No valid JSON found');
344
+ } catch {
345
+ // If not JSON, delete the corrupted file to unblock the system
346
+ try {
347
+ rmSync(painFlagPath);
348
+ return `Pain flag was corrupted ([object Object]), deleted invalid file to unblock system`;
349
+ } catch {
350
+ return { status: 'warn', detail: 'Pain flag corrupted ([object Object]), could not auto-repair' };
351
+ }
352
+ }
353
+ }
354
+
316
355
  const lines = content.split('\n');
317
356
  const fields = {};
318
357
  for (const line of lines) {
@@ -394,6 +433,104 @@ function main() {
394
433
  }
395
434
  });
396
435
 
436
+ // ─────────────────────────────────────────────────────────
437
+ // CHECKPOINT 13: Correction samples (Phase 2b/3b)
438
+ // ─────────────────────────────────────────────────────────
439
+ check('13. Correction samples availability', () => {
440
+ // Read from trajectory.db using sqlite3 CLI
441
+ try {
442
+ const dbPath = join(stateDir, 'trajectory.db');
443
+ if (!existsSync(dbPath)) {
444
+ return { status: 'warn', detail: 'trajectory.db not found' };
445
+ }
446
+ let result;
447
+ try {
448
+ result = execFileSync('sqlite3', [dbPath, 'SELECT review_status, COUNT(*), AVG(quality_score) FROM correction_samples GROUP BY review_status;'], { encoding: 'utf-8', timeout: 5000 }).trim();
449
+ } catch {
450
+ return { status: 'warn', detail: 'Could not query correction samples' };
451
+ }
452
+ if (!result) {
453
+ return { status: 'warn', detail: 'Could not query correction samples' };
454
+ }
455
+ const pendingMatch = result.match(/pending\|(\d+)/);
456
+ const approvedMatch = result.match(/approved\|(\d+)/);
457
+ const rejectedMatch = result.match(/rejected\|(\d+)/);
458
+ const pending = pendingMatch ? parseInt(pendingMatch[1]) : 0;
459
+ const approved = approvedMatch ? parseInt(approvedMatch[1]) : 0;
460
+ const rejected = rejectedMatch ? parseInt(rejectedMatch[1]) : 0;
461
+ if (pending > 0) {
462
+ return { status: 'warn', detail: `${pending} pending review, ${approved} approved, ${rejected} rejected` };
463
+ }
464
+ if (approved === 0 && rejected === 0) return { status: 'warn', detail: 'No correction samples exist — no user corrections detected yet' };
465
+ return `${approved} approved, ${rejected} rejected, ${pending} pending`;
466
+ } catch {
467
+ return { status: 'warn', detail: 'Could not query trajectory.db' };
468
+ }
469
+ });
470
+
471
+ // ─────────────────────────────────────────────────────────
472
+ // CHECKPOINT 14: Signal diversity (Phase 3b)
473
+ // ─────────────────────────────────────────────────────────
474
+ check('14. Pain signal diversity', () => {
475
+ try {
476
+ const dbPath = join(stateDir, 'trajectory.db');
477
+ if (!existsSync(dbPath)) {
478
+ return { status: 'warn', detail: 'trajectory.db not found' };
479
+ }
480
+ let result;
481
+ try {
482
+ result = execFileSync('sqlite3', [dbPath, 'SELECT source, COUNT(*), ROUND(AVG(score),1) FROM pain_events GROUP BY source ORDER BY COUNT(*) DESC;'], { encoding: 'utf-8', timeout: 5000 }).trim();
483
+ } catch {
484
+ return { status: 'warn', detail: 'Could not query pain events' };
485
+ }
486
+ if (!result) {
487
+ return { status: 'warn', detail: 'Could not query pain events' };
488
+ }
489
+ const sources = result.split('\n').filter(Boolean);
490
+ if (sources.length < 2) {
491
+ return { status: 'warn', detail: `Only ${sources.length} pain signal source(s) — low diversity. Expected: tool_failure, user_empathy, correction_rejected, gate_blocked` };
492
+ }
493
+ const summary = sources.map(s => {
494
+ const parts = s.split('|');
495
+ return `${parts[0]} (${parts[1]}, avg ${parts[2]})`;
496
+ }).join(', ');
497
+ return `${sources.length} sources: ${summary}`;
498
+ } catch {
499
+ return { status: 'warn', detail: 'Could not query trajectory.db' };
500
+ }
501
+ });
502
+
503
+ // ─────────────────────────────────────────────────────────
504
+ // CHECKPOINT 15: Artifact quality (Phase 3)
505
+ // ─────────────────────────────────────────────────────────
506
+ check('15. Nocturnal artifact quality', () => {
507
+ const samplesDir = join(stateDir, 'nocturnal', 'samples');
508
+ if (!existsSync(samplesDir)) {
509
+ return { status: 'warn', detail: 'No samples directory' };
510
+ }
511
+ const files = readdirSync(samplesDir).filter(f => f.endsWith('.json'));
512
+ if (files.length === 0) return { status: 'warn', detail: 'No artifacts produced yet' };
513
+
514
+ // Check uniqueness of badDecision across recent artifacts
515
+ const recentFiles = files
516
+ .map(f => ({ name: f, mtime: statSync(join(samplesDir, f)).mtimeMs }))
517
+ .sort((a, b) => b.mtime - a.mtime)
518
+ .slice(0, 5);
519
+
520
+ const decisions = new Set();
521
+ for (const f of recentFiles) {
522
+ try {
523
+ const artifact = JSON.parse(readFileSync(join(samplesDir, f.name), 'utf-8'));
524
+ if (artifact.badDecision) decisions.add(artifact.badDecision);
525
+ } catch { /* skip */ }
526
+ }
527
+
528
+ if (decisions.size < recentFiles.length) {
529
+ return { status: 'warn', detail: `${recentFiles.length - decisions.size} duplicate decision(s) in last ${recentFiles.length} artifacts — stub reflector may not be content-aware` };
530
+ }
531
+ return `${files.length} total, last ${recentFiles.length} all unique decisions`;
532
+ });
533
+
397
534
  printReport();
398
535
  }
399
536
 
@@ -0,0 +1,377 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Nocturnal Pipeline — Seed Scenarios (Phase 3b)
5
+ *
6
+ * Injects synthetic pain/correction scenarios into the trajectory database
7
+ * to improve signal diversity for the nocturnal reflection pipeline.
8
+ *
9
+ * Uses sqlite3 CLI to avoid better-sqlite3 native module dependency.
10
+ *
11
+ * Usage:
12
+ * node scripts/seed-nocturnal-scenarios.mjs [--workspace /path/to/workspace]
13
+ */
14
+
15
+ import { existsSync } from 'fs';
16
+ import { join, dirname } from 'path';
17
+ import { fileURLToPath } from 'url';
18
+ import { execFileSync, execSync } from 'child_process';
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+
23
+ // ─── Pre-flight: check sqlite3 CLI is available ───
24
+ function ensureSqlite3() {
25
+ try {
26
+ execFileSync('sqlite3', ['--version'], { encoding: 'utf-8', timeout: 5000 });
27
+ } catch {
28
+ console.error('❌ sqlite3 CLI is required but not found in PATH');
29
+ console.error(' Install it: apt-get install sqlite3 | brew install sqlite3 | choco install sqlite');
30
+ process.exit(1);
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Run a SQL query safely via execFileSync (no shell interpolation).
36
+ * Returns trimmed stdout string.
37
+ */
38
+ function runSql(dbPath, sql) {
39
+ return execFileSync('sqlite3', [dbPath, sql], {
40
+ encoding: 'utf-8',
41
+ timeout: 5000,
42
+ }).trim();
43
+ }
44
+
45
+ // ─── Argument parsing ───
46
+ function parseArgs() {
47
+ let workspaceDir = null;
48
+ const argv = process.argv.slice(2);
49
+ for (let i = 0; i < argv.length; i++) {
50
+ if (argv[i] === '--workspace' && argv[i + 1]) {
51
+ workspaceDir = argv[++i];
52
+ }
53
+ }
54
+ if (!workspaceDir) {
55
+ try {
56
+ workspaceDir = execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
57
+ } catch {
58
+ workspaceDir = process.cwd();
59
+ }
60
+ }
61
+ return { workspaceDir };
62
+ }
63
+
64
+ function esc(s) {
65
+ // Comprehensive SQL escaping for SQLite string literals
66
+ // Handles: single quotes, newlines, carriage returns, backslashes, null bytes
67
+ return String(s)
68
+ .replace(/\\/g, '\\\\')
69
+ .replace(/'/g, "''")
70
+ .replace(/\n/g, '\\n')
71
+ .replace(/\r/g, '\\r')
72
+ .replace(/\x00/g, '\\0');
73
+ }
74
+
75
+ function nowIso() {
76
+ return new Date().toISOString();
77
+ }
78
+
79
+ function daysAgo(days) {
80
+ const d = new Date();
81
+ d.setDate(d.getDate() - days);
82
+ return d.toISOString();
83
+ }
84
+
85
+ function main() {
86
+ const { workspaceDir } = parseArgs();
87
+ const dbPath = join(workspaceDir, '.state', 'trajectory.db');
88
+
89
+ ensureSqlite3();
90
+
91
+ if (!existsSync(dbPath)) {
92
+ console.error(`❌ trajectory.db not found at ${dbPath}`);
93
+ process.exit(1);
94
+ }
95
+
96
+ const scenarios = [
97
+ // ─────────────────────────────────────────────────────────
98
+ // Scenario 1: Security violation — writing sensitive file
99
+ // ─────────────────────────────────────────────────────────
100
+ {
101
+ sessionId: 'seed-security-violation-001',
102
+ days: 5,
103
+ description: 'Agent wrote API key to public config (security violation)',
104
+ assistantText: 'I\'ve updated the config.json with the new API key for the external service.',
105
+ userText: '错了!你不应该把 API 密钥写到配置文件里,这是严重的安全问题!',
106
+ correctionCue: '错了',
107
+ toolCalls: [
108
+ { toolName: 'write', outcome: 'success', durationMs: 50, paramsJson: JSON.stringify({ path: 'public/config.json', content: '{"apiKey": "sk-1234567890"}' }) },
109
+ { toolName: 'read', outcome: 'success', durationMs: 30, paramsJson: JSON.stringify({ path: 'public/config.json' }) },
110
+ ],
111
+ painEvents: [
112
+ { source: 'tool_failure', score: 85, reason: 'Wrote API key to publicly accessible config file', severity: 'severe', origin: 'assistant_self_report', text: 'Security: apiKey exposed in public/config.json' },
113
+ ],
114
+ },
115
+
116
+ // ─────────────────────────────────────────────────────────
117
+ // Scenario 2: Over-engineering — 10 lines became 100
118
+ // ─────────────────────────────────────────────────────────
119
+ {
120
+ sessionId: 'seed-overengineering-002',
121
+ days: 4,
122
+ description: 'Agent turned a simple function into 100+ lines of abstraction',
123
+ assistantText: 'I\'ve refactored the utility function into a full class hierarchy with factory pattern, dependency injection, and abstract base classes for future extensibility.',
124
+ userText: '太复杂了!原来 10 行就能搞定的事情你写了 100 多行,完全没必要。',
125
+ correctionCue: '太复杂了',
126
+ toolCalls: [
127
+ { toolName: 'edit', outcome: 'success', durationMs: 200, paramsJson: JSON.stringify({ path: 'src/utils/format.ts' }) },
128
+ { toolName: 'read', outcome: 'success', durationMs: 20, paramsJson: JSON.stringify({ path: 'src/utils/format.ts' }) },
129
+ ],
130
+ painEvents: [
131
+ { source: 'user_empathy', score: 60, reason: 'Over-engineering: simple function replaced with unnecessary class hierarchy', severity: 'moderate', origin: 'assistant_self_report', text: 'User indicated the solution is overly complex' },
132
+ ],
133
+ },
134
+
135
+ // ─────────────────────────────────────────────────────────
136
+ // Scenario 3: Boundary condition omitted — null deref
137
+ // ─────────────────────────────────────────────────────────
138
+ {
139
+ sessionId: 'seed-boundary-omission-003',
140
+ days: 3,
141
+ description: 'Agent forgot null check, causing TypeError',
142
+ assistantText: 'I\'ve implemented the data processor that reads from the API response and extracts the nested fields.',
143
+ userText: '不对,如果 API 返回 null 或空数组会直接崩溃。你需要加边界检查。',
144
+ correctionCue: '不对',
145
+ toolCalls: [
146
+ { toolName: 'write', outcome: 'success', durationMs: 80, paramsJson: JSON.stringify({ path: 'src/processor.ts' }) },
147
+ { toolName: 'exec', outcome: 'failure', durationMs: 500, errorType: 'TypeError', errorMessage: 'Cannot read properties of undefined (reading \'items\')', exitCode: 1 },
148
+ ],
149
+ painEvents: [
150
+ { source: 'tool_failure', score: 70, reason: 'TypeError: no null check on API response before accessing nested property', severity: 'severe', origin: 'assistant_self_report', text: 'response.items accessed without null guard' },
151
+ ],
152
+ },
153
+
154
+ // ─────────────────────────────────────────────────────────
155
+ // Scenario 4: Error handling missing — silent failure
156
+ // ─────────────────────────────────────────────────────────
157
+ {
158
+ sessionId: 'seed-error-handling-004',
159
+ days: 3,
160
+ description: 'Agent ignored error from file operation, continued blindly',
161
+ assistantText: 'I\'ve updated the migration script to process all files in the directory.',
162
+ userText: '错了,文件读取失败了你直接跳过了,没有任何错误处理。如果文件不存在怎么办?',
163
+ correctionCue: '错了',
164
+ toolCalls: [
165
+ { toolName: 'read', outcome: 'failure', durationMs: 100, errorType: 'ENOENT', errorMessage: 'ENOENT: no such file or directory, open \'migrations/v2.sql\'' },
166
+ { toolName: 'write', outcome: 'success', durationMs: 50, paramsJson: JSON.stringify({ path: 'migrations/runner.ts' }) },
167
+ ],
168
+ painEvents: [
169
+ { source: 'tool_failure', score: 75, reason: 'File read failure (ENOENT) was silently ignored, no error handling or fallback', severity: 'severe', origin: 'assistant_self_report', text: 'Missing error handling for file read operation' },
170
+ ],
171
+ },
172
+
173
+ // ─────────────────────────────────────────────────────────
174
+ // Scenario 5: Edit without reading — map before territory
175
+ // ─────────────────────────────────────────────────────────
176
+ {
177
+ sessionId: 'seed-edit-without-reading-005',
178
+ days: 2,
179
+ description: 'Agent edited a file without reading it first, breaking existing logic',
180
+ assistantText: 'I\'ve modified the auth middleware to add the new token validation logic.',
181
+ userText: '你根本没有读原文件就改了,现有的 token 刷新逻辑被你删掉了!',
182
+ correctionCue: '没有读原文件',
183
+ toolCalls: [
184
+ { toolName: 'edit', outcome: 'success', durationMs: 150, paramsJson: JSON.stringify({ path: 'src/middleware/auth.ts' }) },
185
+ { toolName: 'exec', outcome: 'failure', durationMs: 300, errorType: 'ReferenceError', errorMessage: 'refreshToken is not defined', exitCode: 1 },
186
+ ],
187
+ painEvents: [
188
+ { source: 'tool_failure', score: 80, reason: 'Edit without reading: existing refreshToken logic was overwritten', severity: 'severe', origin: 'assistant_self_report', text: 'auth.ts edited without prior read, broke existing functionality' },
189
+ ],
190
+ },
191
+
192
+ // ─────────────────────────────────────────────────────────
193
+ // Scenario 6: Batch operation without planning — blast radius
194
+ // ─────────────────────────────────────────────────────────
195
+ {
196
+ sessionId: 'seed-batch-without-planning-006',
197
+ days: 2,
198
+ description: 'Agent modified multiple files simultaneously without a plan',
199
+ assistantText: 'I\'ve updated the import paths across all files in the src/ directory to use the new module structure.',
200
+ userText: '你一次性改了太多文件,没有计划也没有测试。其中几个文件的 import 路径是错的。',
201
+ correctionCue: '没有计划',
202
+ toolCalls: [
203
+ { toolName: 'edit', outcome: 'success', durationMs: 100, paramsJson: JSON.stringify({ path: 'src/module-a/index.ts' }) },
204
+ { toolName: 'edit', outcome: 'success', durationMs: 100, paramsJson: JSON.stringify({ path: 'src/module-b/index.ts' }) },
205
+ { toolName: 'edit', outcome: 'failure', durationMs: 50, errorType: 'SyntaxError', errorMessage: 'Invalid import path: @/utils/nonexistent' },
206
+ { toolName: 'exec', outcome: 'failure', durationMs: 500, errorType: 'BuildError', errorMessage: 'Import errors across modified files', exitCode: 1 },
207
+ ],
208
+ painEvents: [
209
+ { source: 'tool_failure', score: 65, reason: 'Batch edit of multiple files without planning or testing, some files broken', severity: 'moderate', origin: 'assistant_self_report', text: 'Large batch operation without verification' },
210
+ ],
211
+ },
212
+
213
+ // ─────────────────────────────────────────────────────────
214
+ // Scenario 7: Continued after failure — pain as signal ignored
215
+ // ─────────────────────────────────────────────────────────
216
+ {
217
+ sessionId: 'seed-continued-after-failure-007',
218
+ days: 1,
219
+ description: 'Agent got consecutive failures but kept going without diagnosing',
220
+ assistantText: 'I\'ve completed the data migration pipeline. All steps are in place.',
221
+ userText: '前面几步都失败了你还继续执行?应该先诊断失败原因再继续。',
222
+ correctionCue: '应该先诊断',
223
+ toolCalls: [
224
+ { toolName: 'exec', outcome: 'failure', durationMs: 200, errorType: 'ExitCode', errorMessage: 'Step 1: schema validation failed', exitCode: 1 },
225
+ { toolName: 'exec', outcome: 'failure', durationMs: 150, errorType: 'ExitCode', errorMessage: 'Step 2: data transformation failed', exitCode: 1 },
226
+ { toolName: 'exec', outcome: 'failure', durationMs: 100, errorType: 'ExitCode', errorMessage: 'Step 3: import failed', exitCode: 1 },
227
+ { toolName: 'exec', outcome: 'success', durationMs: 300, paramsJson: JSON.stringify({ cmd: 'echo done' }) },
228
+ ],
229
+ painEvents: [
230
+ { source: 'tool_failure', score: 90, reason: 'Consecutive failures ignored, agent continued without diagnosing root cause', severity: 'severe', origin: 'assistant_self_report', text: 'Pain cascade: multiple failures in sequence without pause' },
231
+ ],
232
+ },
233
+
234
+ // ─────────────────────────────────────────────────────────
235
+ // Scenario 8: Complex task not decomposed — no planning
236
+ // ─────────────────────────────────────────────────────────
237
+ {
238
+ sessionId: 'seed-complex-not-decomposed-008',
239
+ days: 1,
240
+ description: 'Agent attempted a full refactor in one turn without breaking it down',
241
+ assistantText: 'I\'ve refactored the entire authentication system from session-based to JWT-based, updated all routes, middleware, and tests.',
242
+ userText: '这个改动太大了,你应该分步骤来。先写计划,再一步步改,每步验证。',
243
+ correctionCue: '分步骤来',
244
+ toolCalls: [
245
+ { toolName: 'edit', outcome: 'success', durationMs: 300, paramsJson: JSON.stringify({ path: 'src/auth/session.ts' }) },
246
+ { toolName: 'edit', outcome: 'success', durationMs: 200, paramsJson: JSON.stringify({ path: 'src/auth/middleware.ts' }) },
247
+ { toolName: 'exec', outcome: 'failure', durationMs: 800, errorType: 'TestFailure', errorMessage: 'Tests failing after auth refactor', exitCode: 1 },
248
+ ],
249
+ painEvents: [
250
+ { source: 'tool_failure', score: 70, reason: 'Complex refactoring done in one turn without decomposition or incremental verification', severity: 'severe', origin: 'assistant_self_report', text: 'Large auth refactor without step-by-step approach' },
251
+ ],
252
+ },
253
+
254
+ // ─────────────────────────────────────────────────────────
255
+ // Scenario 9: Architecture violation — circular dependency
256
+ // ─────────────────────────────────────────────────────────
257
+ {
258
+ sessionId: 'seed-circular-dep-009',
259
+ days: 1,
260
+ description: 'Agent introduced circular dependency between modules',
261
+ assistantText: 'I\'ve connected the UserService and OrderService so they can call each other directly.',
262
+ userText: '你引入了循环依赖!UserService 引入 OrderService,OrderService 又引入 UserService。',
263
+ correctionCue: '循环依赖',
264
+ toolCalls: [
265
+ { toolName: 'edit', outcome: 'success', durationMs: 100, paramsJson: JSON.stringify({ path: 'src/services/user.ts' }) },
266
+ { toolName: 'exec', outcome: 'failure', durationMs: 400, errorType: 'CircularDependency', errorMessage: 'Circular dependency detected: user.ts → order.ts → user.ts' },
267
+ ],
268
+ painEvents: [
269
+ { source: 'tool_failure', score: 75, reason: 'Circular dependency introduced between UserService and OrderService', severity: 'severe', origin: 'assistant_self_report', text: 'Architecture: circular import chain detected' },
270
+ ],
271
+ },
272
+
273
+ // ─────────────────────────────────────────────────────────
274
+ // Scenario 10: Pure conversation correction — no tool failures
275
+ // ─────────────────────────────────────────────────────────
276
+ {
277
+ sessionId: 'seed-pure-conversation-010',
278
+ days: 0,
279
+ description: 'User corrected agent\'s analysis during pure conversation',
280
+ assistantText: 'Based on the error logs, the issue appears to be a network timeout. I recommend increasing the timeout value to 30 seconds.',
281
+ userText: '不对,这不是超时问题。错误日志明明写的是 "connection refused",是服务根本没启动。',
282
+ correctionCue: '不对',
283
+ toolCalls: [
284
+ { toolName: 'read', outcome: 'success', durationMs: 50, paramsJson: JSON.stringify({ path: 'logs/error.log' }) },
285
+ ],
286
+ painEvents: [
287
+ { source: 'user_empathy', score: 50, reason: 'Agent misdiagnosed "connection refused" as timeout, missing root cause', severity: 'moderate', origin: 'assistant_self_report', text: 'Misdiagnosis of error: connection refused vs timeout' },
288
+ ],
289
+ },
290
+ ];
291
+
292
+ console.log('\n🌱 Seeding nocturnal scenarios into trajectory.db\n');
293
+ console.log(`Workspace: ${workspaceDir}`);
294
+ console.log(`Database: ${dbPath}\n`);
295
+
296
+ let inserted = 0;
297
+ let skipped = 0;
298
+
299
+ for (const s of scenarios) {
300
+ const createdAt = daysAgo(s.days);
301
+
302
+ // Check if scenario already exists with complete data (pain_events, not just sessions)
303
+ // This prevents partial data if the script was interrupted mid-insert
304
+ try {
305
+ const existing = runSql(dbPath, `SELECT COUNT(*) FROM pain_events WHERE session_id = '${esc(s.sessionId)}';`);
306
+ if (parseInt(existing) > 0) {
307
+ console.log(` ⏭️ Skipping ${s.sessionId} (already exists with ${existing} pain events)`);
308
+ skipped++;
309
+ continue;
310
+ }
311
+ } catch (e) {
312
+ console.error(` ❌ Failed to check ${s.sessionId}: ${e.message}`);
313
+ continue;
314
+ }
315
+
316
+ const sql = [];
317
+
318
+ // 1. Session
319
+ sql.push(`INSERT INTO sessions (session_id, started_at, updated_at) VALUES ('${esc(s.sessionId)}', '${createdAt}', '${createdAt}');`);
320
+
321
+ // 2. Assistant turn
322
+ sql.push(`INSERT INTO assistant_turns (session_id, run_id, provider, model, raw_text, sanitized_text, usage_json, empathy_signal_json, blob_ref, raw_excerpt, created_at)
323
+ VALUES ('${esc(s.sessionId)}', 'run-${esc(s.sessionId)}', 'local', 'main', '${esc(s.assistantText)}', '${esc(s.assistantText.slice(0, 200))}', '{"total_tokens":500}', '{}', NULL, '${esc(s.assistantText.slice(0, 100))}', '${createdAt}');`);
324
+
325
+ // We need the assistant turn ID for the user turn reference
326
+ // Use a subquery to get it
327
+ sql.push(`INSERT INTO user_turns (session_id, turn_index, raw_text, blob_ref, raw_excerpt, correction_detected, correction_cue, references_assistant_turn_id, created_at)
328
+ SELECT '${esc(s.sessionId)}', 1, '${esc(s.userText)}', NULL, '${esc(s.userText.slice(0, 100))}', ${s.correctionCue ? 1 : 0}, ${s.correctionCue ? `'${esc(s.correctionCue)}'` : 'NULL'}, id, '${createdAt}'
329
+ FROM assistant_turns WHERE session_id = '${esc(s.sessionId)}' LIMIT 1;`);
330
+
331
+ // 3. Tool calls
332
+ for (const tc of s.toolCalls) {
333
+ sql.push(`INSERT INTO tool_calls (session_id, tool_name, outcome, duration_ms, exit_code, error_type, error_message, gfi_before, gfi_after, params_json, created_at)
334
+ VALUES ('${esc(s.sessionId)}', '${esc(tc.toolName)}', '${esc(tc.outcome)}', ${tc.durationMs ?? 100}, ${tc.exitCode !== undefined ? tc.exitCode : 'NULL'}, ${tc.errorType ? `'${esc(tc.errorType)}'` : 'NULL'}, ${tc.errorMessage ? `'${esc(tc.errorMessage)}'` : 'NULL'}, 0, 0, ${tc.paramsJson ? `'${esc(tc.paramsJson)}'` : "'{}'"}, '${createdAt}');`);
335
+ }
336
+
337
+ // 4. Pain events
338
+ for (const pe of s.painEvents) {
339
+ sql.push(`INSERT INTO pain_events (session_id, source, score, reason, severity, origin, confidence, text, created_at)
340
+ VALUES ('${esc(s.sessionId)}', '${esc(pe.source)}', ${pe.score}, '${esc(pe.reason)}', '${esc(pe.severity)}', '${esc(pe.origin)}', ${pe.confidence !== undefined ? pe.confidence : 'NULL'}, ${pe.text ? `'${esc(pe.text)}'` : 'NULL'}, '${createdAt}');`);
341
+ }
342
+
343
+ // Execute all SQL in one transaction via stdin piping
344
+ const fullSql = `BEGIN TRANSACTION;\n${sql.join('\n')}\nCOMMIT;`;
345
+ try {
346
+ execFileSync('sqlite3', [dbPath], {
347
+ input: fullSql,
348
+ encoding: 'utf-8',
349
+ timeout: 10000,
350
+ });
351
+ inserted++;
352
+ console.log(` ✅ ${s.sessionId} — ${s.description}`);
353
+ } catch (e) {
354
+ console.error(` ❌ Failed to insert ${s.sessionId}: ${e.message}`);
355
+ }
356
+ }
357
+
358
+ // ─────────────────────────────────────────────────────────
359
+ // Summary
360
+ // ─────────────────────────────────────────────────────────
361
+ console.log(`\n📊 Seed complete: ${inserted} scenarios inserted, ${skipped} skipped (already existed)`);
362
+
363
+ // Print signal diversity report
364
+ try {
365
+ const painSummary = runSql(dbPath, `.mode column\n.headers on\nSELECT source, COUNT(*) as count, ROUND(AVG(score), 1) as avg_score FROM pain_events WHERE session_id LIKE 'seed-%' GROUP BY source;`);
366
+ console.log('\n📈 Signal diversity report (seed scenarios only):');
367
+ console.log(painSummary);
368
+
369
+ const correctionSummary = runSql(dbPath, `.mode column\n.headers on\nSELECT COUNT(*) as total, SUM(CASE WHEN correction_detected = 1 THEN 1 ELSE 0 END) as with_correction FROM user_turns WHERE session_id LIKE 'seed-%';`);
370
+ console.log('📝 Correction scenarios:');
371
+ console.log(correctionSummary);
372
+ } catch (e) {
373
+ console.error(` Failed to generate summary: ${e.message}`);
374
+ }
375
+ }
376
+
377
+ main();