principles-disciple 1.55.0 → 1.56.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.
@@ -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.55.0",
5
+ "version": "1.56.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.55.0",
3
+ "version": "1.56.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",
@@ -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
 
@@ -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 (err) {
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
@@ -926,6 +926,21 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
926
926
  if (fs.existsSync(reportPath)) {
927
927
  try {
928
928
  const reportData = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
929
+
930
+ // ── Step 3: Noise Classification Filter ──
931
+ // Skip principle creation for low-value noise categories that don't represent
932
+ // systemic failures or behavioral issues worth encoding as principles.
933
+ const classification = reportData?.classification;
934
+ const noiseCategories: Record<string, boolean> = {
935
+ 'development_transient': true, // CRLF drift, duplicate match, self-resolved dev issues
936
+ 'user_error': true, // User mistakes, wrong file, bad input
937
+ };
938
+ if (classification?.category && noiseCategories[classification.category]) {
939
+ if (logger) logger.info(`[PD:EvolutionWorker] Skipping principle for noise category "${classification.category}" — pain was ${classification.severity || 'low'} severity, not a systemic failure`);
940
+ task.status = 'completed';
941
+ task.completed_at = new Date().toISOString();
942
+ task.resolution = 'noise_classified';
943
+ } else {
929
944
  // Check ALL known nesting paths — matches subagent.ts parseDiagnosticianReport
930
945
  const principle = reportData?.principle
931
946
  || reportData?.phases?.principle_extraction?.principle
@@ -1017,6 +1032,7 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1017
1032
  } else {
1018
1033
  logger.warn(`[PD:EvolutionWorker] Diagnostician report for task ${task.id} missing principle fields — diagnostician did not produce a principle`);
1019
1034
  }
1035
+ }
1020
1036
  } catch (err) {
1021
1037
  logger.warn(`[PD:EvolutionWorker] Failed to parse diagnostician report for task ${task.id}: ${String(err)}`);
1022
1038
  }
@@ -1051,6 +1067,13 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1051
1067
  durationMs,
1052
1068
  });
1053
1069
 
1070
+ // Record task completion in event stats
1071
+ eventLog.recordEvolutionTaskCompleted({
1072
+ taskId: task.id,
1073
+ taskType: task.source || 'unknown',
1074
+ reason: task.reason || '',
1075
+ });
1076
+
1054
1077
  // Update evolution_tasks table
1055
1078
  wctx.trajectory?.updateEvolutionTask?.(task.id, {
1056
1079
  status: 'completed',
@@ -1139,6 +1162,13 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
1139
1162
  durationMs: age,
1140
1163
  });
1141
1164
 
1165
+ // Record task completion in event stats (for timeout path too)
1166
+ eventLog.recordEvolutionTaskCompleted({
1167
+ taskId: task.id,
1168
+ taskType: task.source || 'unknown',
1169
+ reason: task.reason || '',
1170
+ });
1171
+
1142
1172
  // Update evolution_tasks table - use task.resolution, not hardcoded value
1143
1173
  wctx.trajectory?.updateEvolutionTask?.(task.id, {
1144
1174
  status: 'completed',
@@ -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('/tmp', 'pd-test-agents-' + Date.now());
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(fs.realpathSync('/tmp'), 'pain-lifecycle-test-'));
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(fs.realpathSync('/tmp'), 'pain-lifecycle-e2e-'));
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
- });