principles-disciple 1.34.1 → 1.35.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.
@@ -0,0 +1,19 @@
1
+ {
2
+ "forbidden": [
3
+ {
4
+ "name": "no-circular",
5
+ "severity": "error",
6
+ "comment": "Cycles hurt maintainability and testability.",
7
+ "from": {},
8
+ "to": {
9
+ "circular": true
10
+ }
11
+ }
12
+ ],
13
+ "options": {
14
+ "tsPreCompilationDeps": true,
15
+ "enhancedResolveOptions": {
16
+ "extensions": [".ts", ".tsx", ".js", ".jsx", ".json"]
17
+ }
18
+ }
19
+ }
@@ -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.34.1",
5
+ "version": "1.35.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.34.1",
3
+ "version": "1.35.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -30,8 +30,11 @@
30
30
  "build:web": "node scripts/build-web.mjs",
31
31
  "build:bundle": "node esbuild.config.js && node scripts/build-web.mjs",
32
32
  "build:production": "node esbuild.config.js --production && node scripts/build-web.mjs --production && node scripts/verify-build.mjs",
33
- "test": "vitest run",
34
- "test:coverage": "vitest run --coverage",
33
+ "test": "vitest run --project=unit",
34
+ "test:unit": "vitest run --project=unit",
35
+ "test:integration": "vitest run --project=integration",
36
+ "test:coverage": "vitest run --project=unit --coverage",
37
+ "test:all": "vitest run",
35
38
  "lint": "eslint src/",
36
39
  "bootstrap-rules": "node scripts/bootstrap-rules.mjs",
37
40
  "validate-live-path": "tsx scripts/validate-live-path.ts"
@@ -1,10 +1,52 @@
1
1
  /**
2
2
  * Centralized Runtime Defaults
3
- *
3
+ *
4
4
  * All runtime-related constants that were previously scattered across modules.
5
5
  * Centralizing these makes it easier to tune behavior and understand limits.
6
6
  */
7
7
 
8
+ // ── Time Constants ──────────────────────────────────────────────────────────────
9
+
10
+ /** Milliseconds per second */
11
+ export const MS_PER_SECOND = 1000;
12
+
13
+ /** Seconds per minute */
14
+ export const SECONDS_PER_MINUTE = 60;
15
+
16
+ /** Minutes per hour */
17
+ export const MINUTES_PER_HOUR = 60;
18
+
19
+ /** Hours per day */
20
+ export const HOURS_PER_DAY = 24;
21
+
22
+ /** Days per week */
23
+ export const DAYS_PER_WEEK = 7;
24
+
25
+ /** One minute in milliseconds */
26
+ export const ONE_MINUTE_MS = SECONDS_PER_MINUTE * MS_PER_SECOND;
27
+
28
+ /** One hour in milliseconds */
29
+ export const ONE_HOUR_MS = MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND;
30
+
31
+ /** One day in milliseconds */
32
+ export const ONE_DAY_MS = HOURS_PER_DAY * ONE_HOUR_MS;
33
+
34
+ /** One week in milliseconds */
35
+ export const ONE_WEEK_MS = DAYS_PER_WEEK * ONE_DAY_MS;
36
+
37
+ // ── Workflow TTL & Timeouts ────────────────────────────────────────────────────
38
+
39
+ /** Default TTL for helper workflows (5 minutes) */
40
+ export const WORKFLOW_TTL_MS = 5 * ONE_MINUTE_MS;
41
+
42
+ /** Default workflow timeout (15 minutes) */
43
+ export const WORKFLOW_TIMEOUT_MS = 15 * ONE_MINUTE_MS;
44
+
45
+ /** Default workflow sweep interval (30 minutes) */
46
+ export const WORKFLOW_SWEEP_MS = 30 * ONE_MINUTE_MS;
47
+
48
+ // ── Trajectory Gate Block Retry Settings ──────────────────────────────────────
49
+
8
50
  /**
9
51
  * Trajectory gate block retry settings
10
52
  * Used when trajectory recording fails and needs to retry
@@ -12,40 +54,74 @@
12
54
  export const TRAJECTORY_GATE_BLOCK_RETRY_DELAY_MS = 250;
13
55
  export const TRAJECTORY_GATE_BLOCK_MAX_RETRIES = 3;
14
56
 
15
- /**
16
- * Thinking checkpoint defaults (P-10)
17
- */
18
- export const THINKING_CHECKPOINT_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
57
+ // ── Thinking Checkpoint Defaults (P-10) ───────────────────────────────────────
58
+
59
+ export const THINKING_CHECKPOINT_WINDOW_MS = 5 * ONE_MINUTE_MS;
19
60
  export const THINKING_CHECKPOINT_DEFAULT_HIGH_RISK_TOOLS = [
20
61
  'run_shell_command',
21
- 'delete_file',
62
+ 'delete_file',
22
63
  'move_file',
23
64
  ] as const;
24
65
 
25
- /**
26
- * Large change threshold for GFI gate adjustments
27
- */
66
+ // ── GFI Gate Thresholds ───────────────────────────────────────────────────────
67
+
68
+ /** Large change threshold for GFI gate adjustments */
28
69
  export const GFI_LARGE_CHANGE_LINES = 50;
29
70
 
30
- /**
31
- * Agent spawn GFI threshold (critically high = no spawn)
32
- */
71
+ /** Agent spawn GFI threshold (critically high = no spawn) */
33
72
  export const AGENT_SPAWN_GFI_THRESHOLD = 90;
34
73
 
35
- /**
36
- * Evolution worker polling intervals
37
- */
38
- export const EVOLUTION_WORKER_POLL_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
74
+ // ── Evolution Worker Settings ───────────────────────────────────────────────────
75
+
76
+ /** Evolution worker polling interval (15 minutes) */
77
+ export const EVOLUTION_WORKER_POLL_INTERVAL_MS = 15 * ONE_MINUTE_MS;
78
+
79
+ /** Evolution queue batch size */
39
80
  export const EVOLUTION_QUEUE_BATCH_SIZE = 10;
40
81
 
41
- /**
42
- * Session tracker settings
43
- */
82
+ /** Pain queue dedup window (30 minutes) */
83
+ export const PAIN_QUEUE_DEDUP_WINDOW_MS = 30 * ONE_MINUTE_MS;
84
+
85
+ // ── Session Tracker Settings ───────────────────────────────────────────────────
86
+
44
87
  export const SESSION_TOKEN_WARNING_THRESHOLD = 8000;
45
- export const SESSION_MAX_IDLE_MS = 30 * 60 * 1000; // 30 minutes
88
+ export const SESSION_MAX_IDLE_MS = 30 * ONE_MINUTE_MS;
89
+
90
+ // ── Event Log Buffer Settings ───────────────────────────────────────────────────
46
91
 
47
- /**
48
- * Event log buffer settings
49
- */
50
92
  export const EVENT_LOG_BUFFER_SIZE = 20;
51
- export const EVENT_LOG_FLUSH_INTERVAL_MS = 30 * 1000; // 30 seconds
93
+ export const EVENT_LOG_FLUSH_INTERVAL_MS = 30 * ONE_MINUTE_MS;
94
+
95
+ // ── Default Busy Timeout ───────────────────────────────────────────────────────
96
+
97
+ /** Default busy timeout for SQLite operations (5 seconds) */
98
+ export const DEFAULT_BUSY_TIMEOUT_MS = 5 * MS_PER_SECOND;
99
+
100
+ // ── Nocturnal Runtime Settings ─────────────────────────────────────────────────
101
+
102
+ /** Idle threshold (30 minutes) */
103
+ export const DEFAULT_IDLE_THRESHOLD_MS = 30 * ONE_MINUTE_MS;
104
+
105
+ /** Quota window (24 hours) */
106
+ export const DEFAULT_QUOTA_WINDOW_MS = ONE_DAY_MS;
107
+
108
+ /** Cool down period (30 minutes) */
109
+ export const DEFAULT_COOLDOWN_MS = 30 * ONE_MINUTE_MS;
110
+
111
+ // ── String & Size Limits ────────────────────────────────────────────────────────
112
+
113
+ /** Max string length for trajectory/event logs */
114
+ export const MAX_STRING_LENGTH = 1000;
115
+
116
+ /** Default max file size (10MB) */
117
+ export const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024;
118
+
119
+ // ── Workflow TTL Settings ───────────────────────────────────────────────────────
120
+
121
+ /** Deep-reflect workflow TTL (10 minutes) */
122
+ export const DEEP_REFLECT_TTL_MS = 10 * ONE_MINUTE_MS;
123
+
124
+ // ── Time Window Constants ───────────────────────────────────────────────────────
125
+
126
+ /** Two hours in milliseconds */
127
+ export const TWO_HOURS_MS = 2 * ONE_HOUR_MS;
@@ -23,31 +23,96 @@ import type { PluginLogger } from '../openclaw-sdk.js';
23
23
 
24
24
  /**
25
25
  * EventLog - Structured event logging with daily statistics aggregation.
26
+ *
27
+ * Log files are date-stamped: events_YYYY-MM-DD.jsonl
28
+ * Old event files are automatically cleaned up based on retention policy.
26
29
  */
30
+
31
+ /**
32
+ * Event log retention in days.
33
+ * Files older than this are deleted on cleanup.
34
+ */
35
+ const EVENT_LOG_RETENTION_DAYS = 7;
36
+
27
37
  export class EventLog {
28
- private readonly eventsFile: string;
38
+ private readonly logsDir: string;
29
39
  private readonly statsFile: string;
30
40
  private readonly logger?: PluginLogger;
31
-
41
+
32
42
  private readonly statsCache: Map<string, DailyStats> = new Map();
33
43
  private eventBuffer: EventLogEntry[] = [];
34
44
  private readonly maxBufferSize = 20;
35
45
  private readonly flushIntervalMs = 30000;
36
46
  private flushTimer?: ReturnType<typeof setInterval>;
37
-
47
+
48
+ // Cached event file path for current date
49
+ private currentEventsFile: string | undefined;
50
+ private currentDate: string | undefined;
51
+
38
52
  constructor(stateDir: string, logger?: PluginLogger) {
39
- const logsDir = path.join(stateDir, 'logs');
40
- if (!fs.existsSync(logsDir)) {
41
- fs.mkdirSync(logsDir, { recursive: true });
53
+ this.logsDir = path.join(stateDir, 'logs');
54
+ if (!fs.existsSync(this.logsDir)) {
55
+ fs.mkdirSync(this.logsDir, { recursive: true });
42
56
  }
43
-
44
- this.eventsFile = path.join(logsDir, 'events.jsonl');
45
- this.statsFile = path.join(logsDir, 'daily-stats.json');
57
+
58
+ this.statsFile = path.join(this.logsDir, 'daily-stats.json');
46
59
  this.logger = logger;
47
-
60
+
48
61
  this.loadStats();
49
62
  this.startFlushTimer();
50
63
  }
64
+
65
+ /**
66
+ * Get the event file path for a given date.
67
+ */
68
+ private getEventsFile(date: string): string {
69
+ return path.join(this.logsDir, `events_${date}.jsonl`);
70
+ }
71
+
72
+ /**
73
+ * Get today's date string (YYYY-MM-DD).
74
+ */
75
+ private getTodayStr(): string {
76
+ return new Date().toISOString().split('T')[0];
77
+ }
78
+
79
+ /**
80
+ * Ensure we have the correct events file for today's date.
81
+ */
82
+ private ensureEventsFile(): string {
83
+ const today = this.getTodayStr();
84
+ if (this.currentDate !== today || !this.currentEventsFile) {
85
+ this.currentDate = today;
86
+ this.currentEventsFile = this.getEventsFile(today);
87
+ // Run cleanup if date changed
88
+ this.cleanupOldEventFiles(today);
89
+ }
90
+ return this.currentEventsFile;
91
+ }
92
+
93
+ /**
94
+ * Clean up event files older than EVENT_LOG_RETENTION_DAYS.
95
+ */
96
+ private cleanupOldEventFiles(today: string): void {
97
+ if (EVENT_LOG_RETENTION_DAYS <= 0) return;
98
+
99
+ try {
100
+ const cutoffMs = Date.now() - EVENT_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
101
+ const files = fs.readdirSync(this.logsDir);
102
+
103
+ for (const file of files) {
104
+ if (!file.startsWith('events_') || !file.endsWith('.jsonl')) continue;
105
+
106
+ const filePath = path.join(this.logsDir, file);
107
+ const stat = fs.statSync(filePath);
108
+ if (stat.mtimeMs < cutoffMs) {
109
+ fs.unlinkSync(filePath);
110
+ }
111
+ }
112
+ } catch {
113
+ // Silently fail cleanup
114
+ }
115
+ }
51
116
 
52
117
  recordToolCall(sessionId: string | undefined, data: ToolCallEventData): void {
53
118
  const category = data.error ? 'failure' : 'success';
@@ -108,7 +173,7 @@ export class EventLog {
108
173
  }
109
174
 
110
175
 
111
- // eslint-disable-next-line @typescript-eslint/max-params
176
+
112
177
  private record(
113
178
  type: EventType,
114
179
  category: EventCategory,
@@ -136,7 +201,7 @@ export class EventLog {
136
201
  }
137
202
 
138
203
 
139
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this
204
+
140
205
  private formatDate(date: Date): string {
141
206
  return date.toISOString().split('T')[0];
142
207
  }
@@ -246,7 +311,7 @@ export class EventLog {
246
311
  }
247
312
 
248
313
 
249
- // eslint-disable-next-line @typescript-eslint/class-methods-use-this -- complexity 13, refactor candidate
314
+
250
315
  private getEventDedupKey(entry: EventLogEntry): string {
251
316
  const eventId = typeof (entry.data as { eventId?: unknown } | undefined)?.eventId === 'string'
252
317
  ? String((entry.data as { eventId?: string }).eventId)
@@ -268,10 +333,11 @@ export class EventLog {
268
333
  }
269
334
 
270
335
  private readPersistedEvents(): EventLogEntry[] {
271
- if (!fs.existsSync(this.eventsFile)) return [];
336
+ const eventsFile = this.ensureEventsFile();
337
+ if (!fs.existsSync(eventsFile)) return [];
272
338
 
273
339
  try {
274
- const content = fs.readFileSync(this.eventsFile, 'utf-8');
340
+ const content = fs.readFileSync(eventsFile, 'utf-8');
275
341
  return content
276
342
  .trim()
277
343
  .split('\n')
@@ -285,7 +351,7 @@ export class EventLog {
285
351
  })
286
352
  .filter((entry): entry is EventLogEntry => entry !== null);
287
353
  } catch (e) {
288
- if (this.logger) this.logger.error(`[PD] Failed to read events.jsonl: ${String(e)}`);
354
+ if (this.logger) this.logger.error(`[PD] Failed to read events file: ${String(e)}`);
289
355
  return [];
290
356
  }
291
357
  }
@@ -300,13 +366,14 @@ export class EventLog {
300
366
 
301
367
  private flushEvents(): void {
302
368
  if (this.eventBuffer.length === 0) return;
303
-
369
+
370
+ const eventsFile = this.ensureEventsFile();
304
371
  const lines = this.eventBuffer.map(e => JSON.stringify(e)).join('\n') + '\n';
305
372
  try {
306
- fs.appendFileSync(this.eventsFile, lines, 'utf-8');
373
+ fs.appendFileSync(eventsFile, lines, 'utf-8');
307
374
  this.eventBuffer = [];
308
375
  } catch (e) {
309
- if (this.logger) this.logger.error(`[PD] Failed to flush events.jsonl: ${String(e)}`);
376
+ if (this.logger) this.logger.error(`[PD] Failed to flush events: ${String(e)}`);
310
377
  }
311
378
  }
312
379
 
@@ -464,7 +531,7 @@ export class EventLog {
464
531
  * Returns the rolled back score, or 0 if event not found.
465
532
  */
466
533
 
467
- // eslint-disable-next-line @typescript-eslint/max-params
534
+
468
535
  rollbackEmpathyEvent(eventId: string, sessionId: string | undefined, reason: string, triggeredBy: 'user_command' | 'natural_language' | 'system'): number {
469
536
  const allEvents = this.getMergedEvents();
470
537
  let foundEvent: { entry: EventLogEntry; data: PainSignalEventData } | null = null;
@@ -23,7 +23,7 @@
23
23
  * PHASE 6 ONLY — No real training, no automatic deployment
24
24
  */
25
25
 
26
- import type { DreamerCandidate, PhilosopherJudgment } from './nocturnal-trinity.js';
26
+ import type { DreamerCandidate, PhilosopherJudgment } from './nocturnal-trinity-types.js';
27
27
  import type { ThresholdValues } from './adaptive-thresholds.js';
28
28
 
29
29
  // ---------------------------------------------------------------------------
@@ -293,7 +293,7 @@ export function validateCandidateDiversity(
293
293
 
294
294
  for (let i = 0; i < candidates.length; i++) {
295
295
  for (let j = i + 1; j < candidates.length; j++) {
296
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
296
+
297
297
  const overlap = computeKeywordOverlap(
298
298
  candidates[i].betterDecision ?? '',
299
299
  candidates[j].betterDecision ?? '',
@@ -335,9 +335,9 @@ export function validateCandidateDiversity(
335
335
  * Returns value between 0 and 1.
336
336
  */
337
337
  function computeKeywordOverlap(textA: string, textB: string): number {
338
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
338
+
339
339
  const wordsA = extractKeywords(textA);
340
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
340
+
341
341
  const wordsB = extractKeywords(textB);
342
342
 
343
343
  if (wordsA.length === 0 && wordsB.length === 0) return 0;
@@ -376,7 +376,7 @@ function extractKeywords(text: string): string[] {
376
376
  * @returns All scored and ranked candidates
377
377
  */
378
378
 
379
- // eslint-disable-next-line @typescript-eslint/max-params
379
+
380
380
  export function rankCandidates(
381
381
  candidates: DreamerCandidate[],
382
382
  judgments: PhilosopherJudgment[],
@@ -464,7 +464,7 @@ export function rankCandidates(
464
464
  * @returns Tournament result with winner
465
465
  */
466
466
 
467
- // eslint-disable-next-line @typescript-eslint/max-params
467
+
468
468
  export function runTournament(
469
469
  candidates: DreamerCandidate[],
470
470
  judgments: PhilosopherJudgment[],
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Nocturnal Trinity Shared Types
3
+ *
4
+ * Types shared between nocturnal-trinity.ts and nocturnal-candidate-scoring.ts.
5
+ * Extracted to break circular dependency.
6
+ */
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Dreamer Types
10
+ // ---------------------------------------------------------------------------
11
+
12
+ /**
13
+ * Individual candidate from Dreamer (alternative decision).
14
+ * Each candidate represents an alternative "better decision" approach.
15
+ */
16
+ export interface DreamerCandidate {
17
+ /** Unique index for this candidate within the Dreamer output */
18
+ candidateIndex: number;
19
+ /** The bad decision this candidate addresses */
20
+ badDecision: string;
21
+ /** The alternative/better decision */
22
+ betterDecision: string;
23
+ /** Why this alternative is better (brief) */
24
+ rationale: string;
25
+ /** Confidence that this candidate is valid (0-1) */
26
+ confidence: number;
27
+ /** Risk level of this candidate's approach -- LLM-judged per D-02 */
28
+ riskLevel?: "low" | "medium" | "high";
29
+ /** Which strategic perspective this candidate embodies per D-01 */
30
+ strategicPerspective?: "conservative_fix" | "structural_improvement" | "paradigm_shift";
31
+ }
32
+
33
+ export interface DreamerOutput {
34
+ /** Whether Dreamer succeeded */
35
+ valid: boolean;
36
+ /** List of candidate corrections */
37
+ candidates: DreamerCandidate[];
38
+ /** Why Dreamer could not generate (if valid === false) */
39
+ reason?: string;
40
+ /** Timestamp of generation */
41
+ generatedAt: string;
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Philosopher Types
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export interface PhilosopherRiskAssessment {
49
+ /** Estimated probability that this candidate is a false positive (0-1) */
50
+ falsePositiveEstimate: number;
51
+ /** How complex is this candidate to implement */
52
+ implementationComplexity: 'low' | 'medium' | 'high';
53
+ /** Whether implementing this candidate risks breaking existing functionality */
54
+ breakingChangeRisk: boolean;
55
+ }
56
+
57
+ export interface Philosopher6DScores {
58
+ principleAlignment: number;
59
+ specificity: number;
60
+ actionability: number;
61
+ executability: number;
62
+ safetyImpact: number;
63
+ uxImpact: number;
64
+ }
65
+
66
+ export interface PhilosopherJudgment {
67
+ /** Index of the judged candidate (references DreamerCandidate.candidateIndex) */
68
+ candidateIndex: number;
69
+ /** Principle-grounded critique of this candidate */
70
+ critique: string;
71
+ /** Whether this candidate aligns with the target principle */
72
+ principleAligned: boolean;
73
+ /** Ranking score (higher = better, 0-1) */
74
+ score: number;
75
+ /** Rank among all candidates (1 = best) */
76
+ rank: number;
77
+ /** Per-dimension scores (6D evaluation) — informational, not used for tournament ranking */
78
+ scores?: Philosopher6DScores;
79
+ /** Risk assessment for this candidate — informational, consumed by Scribe (Phase 37) */
80
+ risks?: PhilosopherRiskAssessment;
81
+ }
82
+
83
+ export interface PhilosopherOutput {
84
+ /** Whether Philosopher succeeded */
85
+ valid: boolean;
86
+ /** Judgments for each candidate */
87
+ judgments: PhilosopherJudgment[];
88
+ /** Overall assessment of the candidate set */
89
+ overallAssessment: string;
90
+ /** Why Philosopher could not judge (if valid === false) */
91
+ reason?: string;
92
+ /** Timestamp of generation */
93
+ generatedAt: string;
94
+ }