principles-disciple 1.55.0 → 1.57.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.
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
- package/scripts/sync-plugin.mjs +47 -4
- package/src/core/event-log.ts +33 -0
- package/src/core/evolution-logger.ts +3 -1
- package/src/core/evolution-types.ts +1 -1
- package/src/core/observability.ts +1 -1
- package/src/service/evolution-queue-migration.ts +2 -1
- package/src/service/evolution-worker.ts +46 -9
- package/src/tools/write-pain-flag.ts +0 -1
- package/tests/core/event-log.test.ts +74 -0
- package/tests/core/pain-context-extractor.test.ts +2 -1
- package/tests/core/pain-lifecycle.test.ts +2 -1
- package/tests/integration/pain-lifecycle-e2e.test.ts +2 -1
- package/tests/commands/evolver.test.ts +0 -22
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "principles-disciple",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.57.0",
|
|
4
4
|
"description": "Native OpenClaw plugin for Principles Disciple",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/bundle.js",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
}
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
+
"@principles/core": "^0.1.0",
|
|
70
71
|
"@sinclair/typebox": "^0.34.48",
|
|
71
72
|
"better-sqlite3": "^12.9.0",
|
|
72
73
|
"lucide-react": "^1.7.0",
|
package/scripts/sync-plugin.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* --help Show help message
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { copyFileSync, cpSync, existsSync, rmSync, readFileSync, readFileSync as readFileSyncRaw, mkdirSync, writeFileSync, readdirSync } from 'fs';
|
|
20
|
+
import { copyFileSync, cpSync, existsSync, lstatSync, rmSync, readFileSync, readFileSync as readFileSyncRaw, mkdirSync, writeFileSync, readdirSync } from 'fs';
|
|
21
21
|
import { createHash } from 'crypto';
|
|
22
22
|
import { join, dirname } from 'path';
|
|
23
23
|
import { fileURLToPath } from 'url';
|
|
@@ -80,7 +80,7 @@ function parseArgs() {
|
|
|
80
80
|
skipBuild: false,
|
|
81
81
|
skipDeps: false,
|
|
82
82
|
force: false,
|
|
83
|
-
restart:
|
|
83
|
+
restart: true,
|
|
84
84
|
dev: false,
|
|
85
85
|
bump: false,
|
|
86
86
|
help: false,
|
|
@@ -100,7 +100,8 @@ function parseArgs() {
|
|
|
100
100
|
args.skipDeps = true;
|
|
101
101
|
break;
|
|
102
102
|
case '--restart':
|
|
103
|
-
|
|
103
|
+
case '--no-restart':
|
|
104
|
+
args.restart = !arg.startsWith('--no-');
|
|
104
105
|
break;
|
|
105
106
|
case '--dev':
|
|
106
107
|
case '-d':
|
|
@@ -145,7 +146,7 @@ Options:
|
|
|
145
146
|
--lang <zh|en> Language for skills (default: zh)
|
|
146
147
|
--skip-build Skip build step (use existing dist/)
|
|
147
148
|
--skip-deps Skip dependency installation
|
|
148
|
-
--restart
|
|
149
|
+
--restart Automatically restart OpenClaw gateway after installation (default: true, use --no-restart to skip)
|
|
149
150
|
--dev, -d Developer mode: --force + --restart + --bump + clean stale backups
|
|
150
151
|
--bump, -b Auto-bump patch version if there are uncommitted source changes
|
|
151
152
|
--force, -f Force overwrite without prompts
|
|
@@ -610,6 +611,47 @@ function syncItem(item) {
|
|
|
610
611
|
}
|
|
611
612
|
}
|
|
612
613
|
|
|
614
|
+
/**
|
|
615
|
+
* Recursive directory copy (Windows-safe, no symlinks).
|
|
616
|
+
*/
|
|
617
|
+
function copyDir(src, dest) {
|
|
618
|
+
mkdirSync(dest, { recursive: true });
|
|
619
|
+
for (const entry of readdirSync(src)) {
|
|
620
|
+
const srcPath = join(src, entry);
|
|
621
|
+
const destPath = join(dest, entry);
|
|
622
|
+
if (lstatSync(srcPath).isDirectory()) {
|
|
623
|
+
copyDir(srcPath, destPath);
|
|
624
|
+
} else {
|
|
625
|
+
copyFileSync(srcPath, destPath);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* Inject local workspace packages (monorepo) into node_modules before npm install.
|
|
632
|
+
* @principles/core is a workspace package, not published to npm — we must copy it
|
|
633
|
+
* from the monorepo's node_modules so npm install --production doesn't 404 it.
|
|
634
|
+
*/
|
|
635
|
+
function injectLocalWorkspacePackages() {
|
|
636
|
+
const monorepoModules = join(SOURCE_DIR, '..', '..', 'node_modules', '@principles', 'core');
|
|
637
|
+
const targetModules = join(INSTALL_DIR, 'node_modules', '@principles', 'core');
|
|
638
|
+
|
|
639
|
+
if (!existsSync(monorepoModules)) {
|
|
640
|
+
// Not in monorepo context (e.g., npm pack / CI tarball) — skip
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
console.log(' 📦 Injecting local workspace packages (@principles/core)...');
|
|
645
|
+
mkdirSync(dirname(targetModules), { recursive: true });
|
|
646
|
+
// cpSync creates symlinks on Windows for symlinked dirs — use cp -rL (dereference) via exec
|
|
647
|
+
try {
|
|
648
|
+
execSync(`cp -rL "${monorepoModules}" "${targetModules}"`, { stdio: 'ignore' });
|
|
649
|
+
} catch {
|
|
650
|
+
// Fallback: manual copy via node (Windows-compatible)
|
|
651
|
+
copyDir(monorepoModules, targetModules);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
613
655
|
/**
|
|
614
656
|
* Install production dependencies in target.
|
|
615
657
|
*/
|
|
@@ -844,6 +886,7 @@ function main() {
|
|
|
844
886
|
for (const item of SYNC_ITEMS) syncItem(item);
|
|
845
887
|
syncSkills(args.lang);
|
|
846
888
|
|
|
889
|
+
injectLocalWorkspacePackages();
|
|
847
890
|
installTargetDependencies();
|
|
848
891
|
|
|
849
892
|
console.log('\n🔍 Verifying installed plugin can load native dependencies...');
|
package/src/core/event-log.ts
CHANGED
|
@@ -51,6 +51,9 @@ export class EventLog {
|
|
|
51
51
|
private currentEventsFile: string | undefined;
|
|
52
52
|
private currentDate: string | undefined;
|
|
53
53
|
|
|
54
|
+
// Pain score sum per date (for avgScore calculation)
|
|
55
|
+
private readonly painScoreSums: Map<string, number> = new Map();
|
|
56
|
+
|
|
54
57
|
constructor(stateDir: string, logger?: PluginLogger) {
|
|
55
58
|
this.logsDir = path.join(stateDir, 'logs');
|
|
56
59
|
if (!fs.existsSync(this.logsDir)) {
|
|
@@ -156,6 +159,10 @@ export class EventLog {
|
|
|
156
159
|
recordEvolutionTask(data: EvolutionTaskEventData): void {
|
|
157
160
|
this.record('evolution_task', 'enqueued', undefined, data);
|
|
158
161
|
}
|
|
162
|
+
|
|
163
|
+
recordEvolutionTaskCompleted(data: EvolutionTaskEventData): void {
|
|
164
|
+
this.record('evolution_task', 'completed', undefined, data);
|
|
165
|
+
}
|
|
159
166
|
|
|
160
167
|
recordDeepReflection(sessionId: string | undefined, data: DeepReflectionEventData): void {
|
|
161
168
|
const category = data.passed ? 'passed' : data.timeout ? 'failure' : 'completed';
|
|
@@ -238,6 +245,18 @@ export class EventLog {
|
|
|
238
245
|
stats.pain.signalsDetected++;
|
|
239
246
|
stats.pain.maxScore = Math.max(stats.pain.maxScore, data.score);
|
|
240
247
|
|
|
248
|
+
// Track signals by source
|
|
249
|
+
if (data.source) {
|
|
250
|
+
stats.pain.signalsBySource[data.source] = (stats.pain.signalsBySource[data.source] || 0) + 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Accumulate score for avg calculation
|
|
254
|
+
const currentSum = this.painScoreSums.get(entry.date) ?? 0;
|
|
255
|
+
this.painScoreSums.set(entry.date, currentSum + (data.score || 0));
|
|
256
|
+
stats.pain.avgScore = stats.pain.signalsDetected > 0
|
|
257
|
+
? Math.round((currentSum + (data.score || 0)) / stats.pain.signalsDetected)
|
|
258
|
+
: 0;
|
|
259
|
+
|
|
241
260
|
// Update empathy stats for user_empathy source
|
|
242
261
|
if (data.source === 'user_empathy') {
|
|
243
262
|
if (data.deduped) {
|
|
@@ -291,6 +310,20 @@ export class EventLog {
|
|
|
291
310
|
const data = entry.data as unknown as EmpathyRollbackEventData;
|
|
292
311
|
stats.empathy.rollbackCount++;
|
|
293
312
|
stats.empathy.rolledBackScore += data.originalScore || 0;
|
|
313
|
+
} else if (entry.type === 'rule_match') {
|
|
314
|
+
const data = entry.data as unknown as RuleMatchEventData;
|
|
315
|
+
if (data.ruleId) {
|
|
316
|
+
stats.pain.rulesMatched[data.ruleId] = (stats.pain.rulesMatched[data.ruleId] || 0) + 1;
|
|
317
|
+
}
|
|
318
|
+
} else if (entry.type === 'rule_promotion') {
|
|
319
|
+
stats.pain.candidatesPromoted++;
|
|
320
|
+
stats.evolution.rulesPromoted++;
|
|
321
|
+
} else if (entry.type === 'evolution_task') {
|
|
322
|
+
if (entry.category === 'completed') {
|
|
323
|
+
stats.evolution.tasksCompleted++;
|
|
324
|
+
} else if (entry.category === 'enqueued') {
|
|
325
|
+
stats.evolution.tasksEnqueued++;
|
|
326
|
+
}
|
|
294
327
|
}
|
|
295
328
|
}
|
|
296
329
|
|
|
@@ -260,7 +260,7 @@ export class EvolutionLogger {
|
|
|
260
260
|
logCompleted(params: {
|
|
261
261
|
traceId: string;
|
|
262
262
|
taskId: string;
|
|
263
|
-
resolution: 'marker_detected' | 'auto_completed_timeout' | 'manual' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'diagnostician_timeout';
|
|
263
|
+
resolution: 'marker_detected' | 'auto_completed_timeout' | 'manual' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'diagnostician_timeout' | 'noise_classified' | 'duplicate' | 'serverDuplicate';
|
|
264
264
|
durationMs?: number;
|
|
265
265
|
principlesGenerated?: number;
|
|
266
266
|
}): void {
|
|
@@ -271,6 +271,8 @@ export class EvolutionLogger {
|
|
|
271
271
|
summary = `任务 ${params.taskId} 完成,已生成 ${params.principlesGenerated || 0} 条原则`;
|
|
272
272
|
} else if (params.resolution === 'auto_completed_timeout' || params.resolution === 'diagnostician_timeout' || params.resolution === 'late_marker_no_principle') {
|
|
273
273
|
summary = `任务 ${params.taskId} 超时自动完成`;
|
|
274
|
+
} else if (params.resolution === 'noise_classified' || params.resolution === 'duplicate' || params.resolution === 'serverDuplicate') {
|
|
275
|
+
summary = `任务 ${params.taskId} 分类为噪音/重复,已过滤`;
|
|
274
276
|
} else {
|
|
275
277
|
summary = `任务 ${params.taskId} 已完成`;
|
|
276
278
|
}
|
|
@@ -470,7 +470,7 @@ export type EvolutionLoopEvent =
|
|
|
470
470
|
|
|
471
471
|
// V2 Queue Types (moved from evolution-worker.ts for shared use)
|
|
472
472
|
export type QueueStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'canceled';
|
|
473
|
-
export type TaskResolution = 'marker_detected' | 'auto_completed_timeout' | 'failed_max_retries' | 'runtime_unavailable' | 'canceled' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'stub_fallback' | 'skipped_thin_violation' | 'success' | 'failure' | 'skipped';
|
|
473
|
+
export type TaskResolution = 'marker_detected' | 'auto_completed_timeout' | 'failed_max_retries' | 'runtime_unavailable' | 'canceled' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'stub_fallback' | 'skipped_thin_violation' | 'success' | 'failure' | 'skipped' | 'noise_classified';
|
|
474
474
|
|
|
475
475
|
export interface EvolutionQueueItem {
|
|
476
476
|
id: string;
|
|
@@ -235,7 +235,7 @@ function persistBaselines(stateDir: string, baselines: ObservabilityBaselines):
|
|
|
235
235
|
fs.mkdirSync(dir, { recursive: true });
|
|
236
236
|
}
|
|
237
237
|
atomicWriteFileSync(filePath, JSON.stringify(baselines, null, 2));
|
|
238
|
-
} catch
|
|
238
|
+
} catch {
|
|
239
239
|
// Baselines persistence is best-effort — don't crash the caller
|
|
240
240
|
// (the SystemLogger call above already logged the values)
|
|
241
241
|
}
|
|
@@ -26,7 +26,8 @@ export type TaskResolution =
|
|
|
26
26
|
| 'late_marker_principle_created'
|
|
27
27
|
| 'late_marker_no_principle'
|
|
28
28
|
| 'stub_fallback'
|
|
29
|
-
| 'skipped_thin_violation'
|
|
29
|
+
| 'skipped_thin_violation'
|
|
30
|
+
| 'noise_classified';
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Recent pain context for sleep_reflection tasks.
|
|
@@ -109,7 +109,7 @@ let timeoutId: NodeJS.Timeout | null = null;
|
|
|
109
109
|
* Old queue items (without taskKind) are migrated to pain_diagnosis for compatibility.
|
|
110
110
|
*/
|
|
111
111
|
export type QueueStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'canceled';
|
|
112
|
-
export type TaskResolution = 'marker_detected' | 'auto_completed_timeout' | 'failed_max_retries' | 'runtime_unavailable' | 'canceled' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'stub_fallback' | 'skipped_thin_violation';
|
|
112
|
+
export type TaskResolution = 'marker_detected' | 'auto_completed_timeout' | 'failed_max_retries' | 'runtime_unavailable' | 'canceled' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'stub_fallback' | 'skipped_thin_violation' | 'noise_classified';
|
|
113
113
|
|
|
114
114
|
export interface EvolutionQueueItem {
|
|
115
115
|
// Core identity
|
|
@@ -921,11 +921,27 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
921
921
|
if (fs.existsSync(completeMarker)) {
|
|
922
922
|
if (logger) logger.info(`[PD:EvolutionWorker] Task ${task.id} completed - marker file detected`);
|
|
923
923
|
|
|
924
|
+
let principlesGenerated = 0;
|
|
924
925
|
// Create principle from the diagnostician's JSON report.
|
|
925
926
|
const reportPath = path.join(wctx.stateDir, `.diagnostician_report_${task.id}.json`);
|
|
926
927
|
if (fs.existsSync(reportPath)) {
|
|
927
928
|
try {
|
|
928
929
|
const reportData = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
|
930
|
+
|
|
931
|
+
// ── Step 3: Noise Classification Filter ──
|
|
932
|
+
// Skip principle creation for low-value noise categories that don't represent
|
|
933
|
+
// systemic failures or behavioral issues worth encoding as principles.
|
|
934
|
+
const classification = reportData?.classification;
|
|
935
|
+
const noiseCategories: Record<string, boolean> = {
|
|
936
|
+
'development_transient': true, // CRLF drift, duplicate match, self-resolved dev issues
|
|
937
|
+
'user_error': true, // User mistakes, wrong file, bad input
|
|
938
|
+
};
|
|
939
|
+
if (classification?.category && noiseCategories[classification.category]) {
|
|
940
|
+
if (logger) logger.info(`[PD:EvolutionWorker] Skipping principle for noise category "${classification.category}" — pain was ${classification.severity || 'low'} severity, not a systemic failure`);
|
|
941
|
+
task.status = 'completed';
|
|
942
|
+
task.completed_at = new Date().toISOString();
|
|
943
|
+
task.resolution = 'noise_classified';
|
|
944
|
+
} else {
|
|
929
945
|
// Check ALL known nesting paths — matches subagent.ts parseDiagnosticianReport
|
|
930
946
|
const principle = reportData?.principle
|
|
931
947
|
|| reportData?.phases?.principle_extraction?.principle
|
|
@@ -1006,6 +1022,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1006
1022
|
});
|
|
1007
1023
|
if (principleId) {
|
|
1008
1024
|
logger.info(`[PD:EvolutionWorker] Created principle ${principleId} from marker fallback for task ${task.id}`);
|
|
1025
|
+
principlesGenerated = 1;
|
|
1009
1026
|
} else {
|
|
1010
1027
|
logger.warn(`[PD:EvolutionWorker] createPrincipleFromDiagnosis returned null for task ${task.id} (may be duplicate or blacklisted)`);
|
|
1011
1028
|
}
|
|
@@ -1017,6 +1034,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1017
1034
|
} else {
|
|
1018
1035
|
logger.warn(`[PD:EvolutionWorker] Diagnostician report for task ${task.id} missing principle fields — diagnostician did not produce a principle`);
|
|
1019
1036
|
}
|
|
1037
|
+
}
|
|
1020
1038
|
} catch (err) {
|
|
1021
1039
|
logger.warn(`[PD:EvolutionWorker] Failed to parse diagnostician report for task ${task.id}: ${String(err)}`);
|
|
1022
1040
|
}
|
|
@@ -1026,7 +1044,8 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1026
1044
|
|
|
1027
1045
|
task.status = 'completed';
|
|
1028
1046
|
task.completed_at = new Date().toISOString();
|
|
1029
|
-
|
|
1047
|
+
// resolution already set by each branch (noise_classified | marker_detected)
|
|
1048
|
+
if (!task.resolution) task.resolution = 'marker_detected';
|
|
1030
1049
|
try {
|
|
1031
1050
|
fs.unlinkSync(completeMarker);
|
|
1032
1051
|
} catch { /* marker may have been deleted already, not critical */ }
|
|
@@ -1047,22 +1066,30 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1047
1066
|
evoLogger.logCompleted({
|
|
1048
1067
|
traceId: task.traceId || task.id,
|
|
1049
1068
|
taskId: task.id,
|
|
1050
|
-
resolution: 'marker_detected',
|
|
1069
|
+
resolution: task.resolution as 'marker_detected' | 'auto_completed_timeout' | 'manual' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'diagnostician_timeout',
|
|
1051
1070
|
durationMs,
|
|
1071
|
+
principlesGenerated,
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1074
|
+
// Record task completion in event stats
|
|
1075
|
+
eventLog.recordEvolutionTaskCompleted({
|
|
1076
|
+
taskId: task.id,
|
|
1077
|
+
taskType: task.source || 'unknown',
|
|
1078
|
+
reason: task.reason || '',
|
|
1052
1079
|
});
|
|
1053
1080
|
|
|
1054
1081
|
// Update evolution_tasks table
|
|
1055
1082
|
wctx.trajectory?.updateEvolutionTask?.(task.id, {
|
|
1056
1083
|
status: 'completed',
|
|
1057
1084
|
completedAt: task.completed_at,
|
|
1058
|
-
resolution: 'marker_detected',
|
|
1085
|
+
resolution: task.resolution as 'marker_detected' | 'auto_completed_timeout' | 'manual' | 'late_marker_principle_created' | 'late_marker_no_principle' | 'diagnostician_timeout',
|
|
1059
1086
|
});
|
|
1060
1087
|
|
|
1061
1088
|
wctx.trajectory?.recordTaskOutcome({
|
|
1062
1089
|
sessionId: task.assigned_session_key || 'heartbeat:diagnostician',
|
|
1063
1090
|
taskId: task.id,
|
|
1064
1091
|
outcome: 'ok',
|
|
1065
|
-
summary: `Task ${task.id} completed
|
|
1092
|
+
summary: `Task ${task.id} completed — ${principlesGenerated} principle(s) generated (${task.resolution}).`
|
|
1066
1093
|
});
|
|
1067
1094
|
queueChanged = true;
|
|
1068
1095
|
continue;
|
|
@@ -1075,9 +1102,11 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1075
1102
|
const timeoutCompleteMarker = path.join(wctx.stateDir, `.evolution_complete_${task.id}`);
|
|
1076
1103
|
const timeoutReportPath = path.join(wctx.stateDir, `.diagnostician_report_${task.id}.json`);
|
|
1077
1104
|
|
|
1105
|
+
let principlesGenerated = 0;
|
|
1106
|
+
|
|
1107
|
+
|
|
1078
1108
|
if (fs.existsSync(timeoutCompleteMarker) && fs.existsSync(timeoutReportPath)) {
|
|
1079
1109
|
if (logger) logger.info(`[PD:EvolutionWorker] Task ${task.id} timed out but marker found — creating principle anyway`);
|
|
1080
|
-
let principleCreated = false;
|
|
1081
1110
|
try {
|
|
1082
1111
|
const reportData = JSON.parse(fs.readFileSync(timeoutReportPath, 'utf8'));
|
|
1083
1112
|
const principle = reportData?.principle
|
|
@@ -1103,7 +1132,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1103
1132
|
});
|
|
1104
1133
|
if (principleId) {
|
|
1105
1134
|
logger.info(`[PD:EvolutionWorker] Created principle ${principleId} from late marker for task ${task.id}`);
|
|
1106
|
-
|
|
1135
|
+
principlesGenerated = 1;
|
|
1107
1136
|
}
|
|
1108
1137
|
}
|
|
1109
1138
|
}
|
|
@@ -1116,7 +1145,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1116
1145
|
const lateReportPath = path.join(wctx.stateDir, `.diagnostician_report_${task.id}.json`);
|
|
1117
1146
|
if (fs.existsSync(lateReportPath)) fs.unlinkSync(lateReportPath);
|
|
1118
1147
|
} catch { /* report may not exist, not critical */ }
|
|
1119
|
-
task.resolution =
|
|
1148
|
+
task.resolution = principlesGenerated > 0 ? 'late_marker_principle_created' : 'late_marker_no_principle';
|
|
1120
1149
|
} else {
|
|
1121
1150
|
if (logger) logger.info(`[PD:EvolutionWorker] Task ${task.id} auto-completed after ${timeoutMinutes} minute timeout`);
|
|
1122
1151
|
// #190: Clean up diagnostician report file even on timeout (may have been written late)
|
|
@@ -1137,6 +1166,14 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1137
1166
|
taskId: task.id,
|
|
1138
1167
|
resolution: task.resolution,
|
|
1139
1168
|
durationMs: age,
|
|
1169
|
+
principlesGenerated,
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
// Record task completion in event stats (for timeout path too)
|
|
1173
|
+
eventLog.recordEvolutionTaskCompleted({
|
|
1174
|
+
taskId: task.id,
|
|
1175
|
+
taskType: task.source || 'unknown',
|
|
1176
|
+
reason: task.reason || '',
|
|
1140
1177
|
});
|
|
1141
1178
|
|
|
1142
1179
|
// Update evolution_tasks table - use task.resolution, not hardcoded value
|
|
@@ -1150,7 +1187,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
1150
1187
|
sessionId: task.assigned_session_key || 'heartbeat:diagnostician',
|
|
1151
1188
|
taskId: task.id,
|
|
1152
1189
|
outcome: 'timeout',
|
|
1153
|
-
summary: `Task ${task.id}
|
|
1190
|
+
summary: `Task ${task.id} completed — ${principlesGenerated} principle(s) generated (${task.resolution}).`
|
|
1154
1191
|
});
|
|
1155
1192
|
queueChanged = true;
|
|
1156
1193
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from '../openclaw-sdk.js';
|
|
2
2
|
import { Type } from '@sinclair/typebox';
|
|
3
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4
3
|
import { buildPainFlag } from '../core/pain.js';
|
|
5
4
|
import { resolveWorkspaceDirFromApi } from '../core/path-resolver.js';
|
|
6
5
|
import { TrajectoryRegistry } from '../core/trajectory.js';
|
|
@@ -179,4 +179,78 @@ describe('EventLog', () => {
|
|
|
179
179
|
expect(stats.totalPenaltyScore).toBe(12);
|
|
180
180
|
});
|
|
181
181
|
});
|
|
182
|
+
|
|
183
|
+
describe('Evolution and rule stats', () => {
|
|
184
|
+
it('should count evolution_task enqueued events', () => {
|
|
185
|
+
eventLog.recordEvolutionTask({ taskId: 't1', taskType: 'pain_diagnosis', reason: 'test' });
|
|
186
|
+
eventLog.recordEvolutionTask({ taskId: 't2', taskType: 'pain_diagnosis', reason: 'test' });
|
|
187
|
+
|
|
188
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
189
|
+
const stats = eventLog.getDailyStats(today);
|
|
190
|
+
|
|
191
|
+
expect(stats.evolution.tasksEnqueued).toBe(2);
|
|
192
|
+
expect(stats.evolution.tasksCompleted).toBe(0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should count evolution_task completed events', () => {
|
|
196
|
+
// First enqueue
|
|
197
|
+
eventLog.recordEvolutionTask({ taskId: 't1', taskType: 'pain_diagnosis', reason: 'test' });
|
|
198
|
+
// Then complete
|
|
199
|
+
eventLog.recordEvolutionTaskCompleted({ taskId: 't1', taskType: 'pain_diagnosis', reason: 'test' });
|
|
200
|
+
|
|
201
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
202
|
+
const stats = eventLog.getDailyStats(today);
|
|
203
|
+
|
|
204
|
+
expect(stats.evolution.tasksEnqueued).toBe(1);
|
|
205
|
+
expect(stats.evolution.tasksCompleted).toBe(1);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should track rule_match events in rulesMatched', () => {
|
|
209
|
+
eventLog.recordRuleMatch('s1', { ruleId: 'edit-exact-match', layer: 'L2', severity: 0.8, textPreview: 'test' });
|
|
210
|
+
eventLog.recordRuleMatch('s1', { ruleId: 'edit-exact-match', layer: 'L2', severity: 0.8, textPreview: 'test' });
|
|
211
|
+
eventLog.recordRuleMatch('s1', { ruleId: 'path-traversal', layer: 'L1', severity: 0.9, textPreview: 'test2' });
|
|
212
|
+
|
|
213
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
214
|
+
const stats = eventLog.getDailyStats(today);
|
|
215
|
+
|
|
216
|
+
expect(stats.pain.rulesMatched['edit-exact-match']).toBe(2);
|
|
217
|
+
expect(stats.pain.rulesMatched['path-traversal']).toBe(1);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should track rule_promotion events', () => {
|
|
221
|
+
eventLog.recordRulePromotion({ fingerprint: 'fp1', ruleId: 'r1', phrase: 'test', sampleCount: 5, avgSimilarity: 0.9 });
|
|
222
|
+
eventLog.recordRulePromotion({ fingerprint: 'fp2', ruleId: 'r2', phrase: 'test2', sampleCount: 3, avgSimilarity: 0.8 });
|
|
223
|
+
|
|
224
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
225
|
+
const stats = eventLog.getDailyStats(today);
|
|
226
|
+
|
|
227
|
+
expect(stats.pain.candidatesPromoted).toBe(2);
|
|
228
|
+
expect(stats.evolution.rulesPromoted).toBe(2);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should track pain signals by source', () => {
|
|
232
|
+
eventLog.recordPainSignal('s1', { source: 'tool_failure', score: 50, reason: 'edit failed' });
|
|
233
|
+
eventLog.recordPainSignal('s2', { source: 'tool_failure', score: 60, reason: 'read failed' });
|
|
234
|
+
eventLog.recordPainSignal('s3', { source: 'user_empathy', score: 10, reason: 'user frustrated' });
|
|
235
|
+
|
|
236
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
237
|
+
const stats = eventLog.getDailyStats(today);
|
|
238
|
+
|
|
239
|
+
expect(stats.pain.signalsBySource['tool_failure']).toBe(2);
|
|
240
|
+
expect(stats.pain.signalsBySource['user_empathy']).toBe(1);
|
|
241
|
+
expect(stats.pain.signalsDetected).toBe(3);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should calculate avgScore for pain signals', () => {
|
|
245
|
+
eventLog.recordPainSignal('s1', { source: 'tool_failure', score: 50, reason: 'test' });
|
|
246
|
+
eventLog.recordPainSignal('s2', { source: 'tool_failure', score: 70, reason: 'test' });
|
|
247
|
+
eventLog.recordPainSignal('s3', { source: 'tool_failure', score: 60, reason: 'test' });
|
|
248
|
+
|
|
249
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
250
|
+
const stats = eventLog.getDailyStats(today);
|
|
251
|
+
|
|
252
|
+
expect(stats.pain.avgScore).toBe(60); // (50+70+60)/3 = 60
|
|
253
|
+
expect(stats.pain.maxScore).toBe(70);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
182
256
|
});
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
4
5
|
|
|
5
|
-
const TEST_AGENTS_DIR = path.join(
|
|
6
|
+
const TEST_AGENTS_DIR = path.join(os.tmpdir(), 'pd-test-agents-' + Date.now());
|
|
6
7
|
|
|
7
8
|
// Set env before module load
|
|
8
9
|
process.env.PD_TEST_AGENTS_DIR = TEST_AGENTS_DIR;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
4
5
|
import { clearPainFlag, PAIN_FLAG_FILENAME } from '../../src/core/pain-lifecycle.js';
|
|
5
6
|
import { resolvePdPath } from '../../src/core/paths.js';
|
|
6
7
|
|
|
7
8
|
describe('PainLifecycle', () => {
|
|
8
|
-
const workspaceDir = fs.mkdtempSync(path.join(
|
|
9
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pain-lifecycle-test-'));
|
|
9
10
|
const painFlagPath = resolvePdPath(workspaceDir, 'PAIN_FLAG');
|
|
10
11
|
|
|
11
12
|
beforeEach(() => {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
+
import * as os from 'os';
|
|
4
5
|
import { clearPainFlag } from '../../src/core/pain-lifecycle.js';
|
|
5
6
|
import { resolvePdPath } from '../../src/core/paths.js';
|
|
6
7
|
|
|
7
8
|
describe('Pain Lifecycle E2E', () => {
|
|
8
|
-
const workspaceDir = fs.mkdtempSync(path.join(
|
|
9
|
+
const workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pain-lifecycle-e2e-'));
|
|
9
10
|
const painFlagPath = resolvePdPath(workspaceDir, 'PAIN_FLAG');
|
|
10
11
|
const stateDir = path.dirname(painFlagPath);
|
|
11
12
|
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { handleEvolveTask } from '../../src/commands/evolver';
|
|
3
|
-
|
|
4
|
-
describe('Evolver Synergy Module', () => {
|
|
5
|
-
it('should handle /evolve-task command', () => {
|
|
6
|
-
const mockCtx = {
|
|
7
|
-
workspaceDir: '/mock/workspace',
|
|
8
|
-
commandBody: '/evolve-task',
|
|
9
|
-
channel: 'cli',
|
|
10
|
-
isAuthorizedSender: true,
|
|
11
|
-
config: {} as any,
|
|
12
|
-
args: 'Fix tests'
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
const result = handleEvolveTask(mockCtx as any);
|
|
16
|
-
|
|
17
|
-
expect(result).toBeDefined();
|
|
18
|
-
expect(result.text).toContain('Evolver Handoff Requested');
|
|
19
|
-
expect(result.text).toContain('sessions_spawn');
|
|
20
|
-
expect(result.text).toContain('Fix tests');
|
|
21
|
-
});
|
|
22
|
-
});
|