principles-disciple 1.45.0 → 1.46.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.45.0",
5
+ "version": "1.46.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
@@ -76,8 +76,8 @@
76
76
  }
77
77
  },
78
78
  "buildFingerprint": {
79
- "gitSha": "cab0dbd8e6e7",
80
- "bundleMd5": "1505a7119addd2ee24059f2473cdb1ca",
81
- "builtAt": "2026-04-14T10:58:07.896Z"
79
+ "gitSha": "9ae5cc1407da",
80
+ "bundleMd5": "68bc85f0121a780b83931b7ed5491b97",
81
+ "builtAt": "2026-04-16T02:19:50.219Z"
82
82
  }
83
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.45.0",
3
+ "version": "1.46.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -21,7 +21,7 @@ import type {
21
21
  PrincipleSuggestedRule,
22
22
  } from './evolution-types.js';
23
23
  import { isCompleteDetectorMetadata } from './evolution-types.js';
24
- import { updateTrainingStore } from './principle-tree-ledger.js';
24
+ import { updateTrainingStore, addPrincipleToLedger, type LedgerPrinciple } from './principle-tree-ledger.js';
25
25
 
26
26
 
27
27
  export interface EvolutionReducer {
@@ -381,6 +381,50 @@ export class EvolutionReducerImpl implements EvolutionReducer {
381
381
  SystemLogger.log(this.workspaceDir, 'PRINCIPLE_SYNC_WARN', `Principle ${principleId} created in memory but failed to sync to PRINCIPLES.md — manual file check required`);
382
382
  }
383
383
 
384
+ // Add to ledger tree so the compiler can find this principle.
385
+ // Without this, newly diagnosed principles are invisible to the compiler
386
+ // (which reads tree.principles, not trainingStore or PRINCIPLES.md).
387
+ if (this.stateDir) {
388
+ try {
389
+ // Build a LedgerPrinciple (tree schema) from the evolution-types Principle.
390
+ // Tree schema Principle does NOT have: source, guardrails, contextTags, validation,
391
+ // feedbackScore, usageCount, activatedAt, abstractedPrinciple, valueMetrics.
392
+ // Pain source info is stored via derivedFromPainIds (which the compiler uses).
393
+ const ledgerPrinciple: LedgerPrinciple = {
394
+ id: principle.id,
395
+ version: principle.version,
396
+ text: principle.text,
397
+ triggerPattern: principle.trigger,
398
+ action: principle.action,
399
+ status: principle.status,
400
+ evaluability: principle.evaluability,
401
+ coreAxiomId: principle.coreAxiomId,
402
+ priority: principle.priority ?? 'P1',
403
+ scope: principle.scope ?? 'general',
404
+ domain: principle.domain,
405
+ suggestedRules: principle.suggestedRules?.map((r) => r.name),
406
+ detectorMetadata: principle.detectorMetadata,
407
+ deprecatedAt: principle.deprecatedAt,
408
+ deprecatedReason: undefined,
409
+ createdAt: principle.createdAt,
410
+ updatedAt: now,
411
+ // Ledger-only fields (derived from evolution-types Principle where applicable):
412
+ valueScore: 0,
413
+ adherenceRate: 0,
414
+ painPreventedCount: 0,
415
+ lastPainPreventedAt: undefined,
416
+ derivedFromPainIds: [params.painId],
417
+ ruleIds: [],
418
+ conflictsWithPrincipleIds: [],
419
+ supersedesPrincipleId: undefined,
420
+ };
421
+ addPrincipleToLedger(this.stateDir, ledgerPrinciple);
422
+ SystemLogger.log(this.workspaceDir, 'LEDGER_PRINCIPLE_ADDED', `Principle ${principleId} added to ledger tree`);
423
+ } catch (err) {
424
+ SystemLogger.log(this.workspaceDir, 'LEDGER_PRINCIPLE_ADD_FAILED', `Failed to add ${principleId} to ledger tree: ${String(err)}`);
425
+ }
426
+ }
427
+
384
428
  // #204: Write to training store so listEvaluablePrinciples() can find this principle
385
429
  if (this.stateDir) {
386
430
  try {
package/src/core/pain.ts CHANGED
@@ -32,9 +32,11 @@ export interface PainFlagData {
32
32
  /** Whether this involves risky operation ('true' / 'false') */
33
33
  is_risky: string;
34
34
  /** Correlation trace ID (for linking events across the pipeline) */
35
- trace_id?: string;
36
- /** Short preview of text that triggered this pain signal */
37
- trigger_text_preview?: string;
35
+ trace_id: string;
36
+ /** Preview of the text that triggered this pain */
37
+ trigger_text_preview: string;
38
+ /** Trajectory pain_events row ID (set by recordPainEvent) */
39
+ pain_event_id?: string;
38
40
  }
39
41
 
40
42
  export interface PainFlagContractResult {
@@ -65,6 +67,7 @@ export function buildPainFlag(input: {
65
67
  is_risky?: boolean;
66
68
  trace_id?: string;
67
69
  trigger_text_preview?: string;
70
+ pain_event_id?: string;
68
71
  }): PainFlagData {
69
72
  // Omit optional fields when not provided — prevents writing empty lines to disk
70
73
  // which causes agent confusion (SKILL.md vs reality drift)
@@ -78,6 +81,7 @@ export function buildPainFlag(input: {
78
81
  is_risky: input.is_risky ? 'true' : 'false',
79
82
  trace_id: input.trace_id ?? '',
80
83
  trigger_text_preview: input.trigger_text_preview ?? '',
84
+ pain_event_id: input.pain_event_id,
81
85
  };
82
86
  }
83
87
 
@@ -368,7 +368,7 @@ export async function saveLedgerAsync(stateDir: string, store: HybridLedgerStore
368
368
 
369
369
  export function updateTrainingStore(
370
370
  stateDir: string,
371
-
371
+
372
372
  mutate: (store: LegacyPrincipleTrainingStore) => void,
373
373
  ): void {
374
374
  mutateLedger(stateDir, (store) => {
@@ -376,6 +376,24 @@ export function updateTrainingStore(
376
376
  });
377
377
  }
378
378
 
379
+ /**
380
+ * Add a new principle directly to the ledger tree.
381
+ * This is the companion to updatePrinciple() — use this when creating a NEW
382
+ * principle so the compiler can find it in tree.principles.
383
+ *
384
+ * Idempotent: if the principle already exists, overwrites (update semantics).
385
+ */
386
+ export function addPrincipleToLedger(
387
+ stateDir: string,
388
+ principle: LedgerPrinciple,
389
+ ): LedgerPrinciple {
390
+ return mutateLedger(stateDir, (store) => {
391
+ store.tree.principles[principle.id] = principle;
392
+ store.tree.lastUpdated = new Date().toISOString();
393
+ return principle;
394
+ });
395
+ }
396
+
379
397
  export function createRule(stateDir: string, rule: LedgerRule): LedgerRule {
380
398
  return mutateLedger(stateDir, (store) => {
381
399
  const principle = store.tree.principles[rule.principleId];
@@ -237,10 +237,11 @@ export class TrajectoryDatabase {
237
237
  return rowId;
238
238
  }
239
239
 
240
- recordPainEvent(input: TrajectoryPainEventInput): void {
240
+ recordPainEvent(input: TrajectoryPainEventInput): number {
241
241
  this.recordSession({ sessionId: input.sessionId, startedAt: input.createdAt });
242
+ let insertedId = -1;
242
243
  this.withWrite(() => {
243
- this.db.prepare(`
244
+ const runResult = this.db.prepare(`
244
245
  INSERT INTO pain_events (
245
246
  session_id, source, score, reason, severity, origin, confidence, text, created_at
246
247
  ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
@@ -255,15 +256,22 @@ export class TrajectoryDatabase {
255
256
  input.text ?? null,
256
257
  input.createdAt ?? nowIso(),
257
258
  );
258
-
259
- // Maintain FTS5 index: insert text into pain_events_fts if text is provided (MEM-03, MEM-04)
260
- if (input.text) {
261
- const lastId = this.db.prepare('SELECT last_insert_rowid() as id').get() as { id: number };
259
+ insertedId = runResult.lastInsertRowid as number;
260
+ });
261
+ // FTS indexing is best-effort — run outside the transaction so it cannot
262
+ // roll back the committed pain event row (MEM-03, MEM-04).
263
+ if (input.text && insertedId > 0) {
264
+ try {
262
265
  this.db.prepare(`
263
266
  INSERT INTO pain_events_fts (text, pain_event_id) VALUES (?, ?)
264
- `).run(input.text, lastId.id);
267
+ `).run(input.text, insertedId);
268
+ } catch (err) {
269
+ // Non-fatal: FTS index is for search convenience, not correctness.
270
+ // Log but do not re-throw — the pain event itself is already committed.
271
+ console.warn(`[trajectory] FTS index insert failed for pain_event ${insertedId}: ${String(err)}`);
265
272
  }
266
- });
273
+ }
274
+ return insertedId;
267
275
  }
268
276
 
269
277
  /**
package/src/hooks/pain.ts CHANGED
@@ -295,6 +295,19 @@ export function handleAfterToolCall(
295
295
  const painScore = computePainScore(1, false, false, isRisk ? 20 : 0, effectiveWorkspaceDir);
296
296
  const traceId = createTraceId();
297
297
 
298
+ // Record to trajectory FIRST so we get the real auto-increment ID.
299
+ // This ID propagates through the pain flag → evolution task → principle,
300
+ // so the compiler can later resolve derivedFromPainIds correctly.
301
+ const trajectoryPainId = wctx.trajectory?.recordPainEvent({
302
+ sessionId,
303
+ source: 'tool_failure',
304
+ score: painScore,
305
+ reason: `Tool ${event.toolName} failed on ${relPath}`,
306
+ severity: painScore >= 70 ? 'severe' : painScore >= 40 ? 'moderate' : 'mild',
307
+ origin: 'system_infer',
308
+ text: params.text ?? params.content ?? undefined,
309
+ });
310
+
298
311
  const painData = buildPainFlag({
299
312
  source: 'tool_failure',
300
313
  score: String(painScore),
@@ -303,6 +316,7 @@ export function handleAfterToolCall(
303
316
  trace_id: traceId,
304
317
  session_id: sessionId,
305
318
  agent_id: ctx.agentId || '',
319
+ pain_event_id: trajectoryPainId !== undefined && trajectoryPainId >= 0 ? String(trajectoryPainId) : undefined,
306
320
  });
307
321
 
308
322
  try {
@@ -355,15 +369,6 @@ export function handleAfterToolCall(
355
369
  reason: `Tool ${event.toolName} failed on ${relPath}`,
356
370
  isRisky: isRisk,
357
371
  });
358
- wctx.trajectory?.recordPainEvent?.({
359
- sessionId,
360
- source: 'tool_failure',
361
- score: painScore,
362
- reason: `Tool ${event.toolName} failed on ${relPath}`,
363
- severity: painScore >= 70 ? 'severe' : painScore >= 40 ? 'moderate' : 'mild',
364
- origin: 'system_infer',
365
- text: params.text ?? params.content ?? undefined, // Store original text/content that failed
366
- });
367
372
 
368
373
  // Log to EvolutionLogger
369
374
  const evoLogger = getEvolutionLogger(effectiveWorkspaceDir, wctx.trajectory);
@@ -141,6 +141,9 @@ export interface EvolutionQueueItem {
141
141
  // Attaches explicit recent pain signal without merging task kinds.
142
142
  // Used by target selector for ranking bias and context enrichment.
143
143
  recentPainContext?: RecentPainContext;
144
+
145
+ /** Trajectory pain_events row ID — set when pain flag includes pain_event_id */
146
+ painEventId?: number;
144
147
  }
145
148
 
146
149
  // ── Queue Migration (extracted to queue-migration.ts) ────────────────────────
@@ -305,6 +308,7 @@ export function hasEquivalentPromotedRule(dictionary: { getAllRules(): Record<st
305
308
  interface ParsedPainValues {
306
309
  score: number; source: string; reason: string; preview: string;
307
310
  traceId: string; sessionId: string; agentId: string;
311
+ painEventId?: number;
308
312
  }
309
313
 
310
314
 
@@ -352,6 +356,7 @@ async function doEnqueuePainTask(
352
356
  status: 'pending', session_id: v.sessionId || undefined,
353
357
  agent_id: v.agentId || undefined, traceId: effectiveTraceId,
354
358
  retryCount: 0, maxRetries: 3,
359
+ painEventId: v.painEventId,
355
360
  });
356
361
 
357
362
  saveEvolutionQueue(queuePath, queue);
@@ -400,6 +405,8 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
400
405
  const traceId = contract.data.trace_id ?? '';
401
406
  const sessionId = contract.data.session_id ?? '';
402
407
  const agentId = contract.data.agent_id ?? '';
408
+ const painEventIdRaw = contract.data.pain_event_id;
409
+ const painEventId = painEventIdRaw ? parseInt(painEventIdRaw, 10) : undefined;
403
410
 
404
411
  result.exists = true;
405
412
  result.score = score;
@@ -414,7 +421,7 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
414
421
 
415
422
  if (logger) logger.info(`[PD:EvolutionWorker] Detected pain flag (score: ${score}, source: ${source}). Enqueueing evolution task.`);
416
423
  return doEnqueuePainTask(wctx, logger, painFlagPath, result, {
417
- score, source, reason, preview, traceId, sessionId, agentId,
424
+ score, source, reason, preview, traceId, sessionId, agentId, painEventId,
418
425
  });
419
426
  }
420
427
 
@@ -470,6 +477,7 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
470
477
  preview: jsonPreview, traceId: '',
471
478
  sessionId: jsonPain.session_id || '',
472
479
  agentId: jsonPain.agent_id || '',
480
+ painEventId: jsonPain.pain_event_id ? parseInt(jsonPain.pain_event_id, 10) : undefined,
473
481
  });
474
482
  }
475
483
  } catch { /* Not JSON — fall through to KV/Markdown parsing */ }
@@ -492,6 +500,7 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
492
500
  let traceId = '';
493
501
  let sessionId = '';
494
502
  let agentId = '';
503
+ let painEventId: number | undefined;
495
504
 
496
505
  for (const line of lines) {
497
506
  // KV format: "key: value"
@@ -503,6 +512,10 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
503
512
  if (line.startsWith('trace_id:')) traceId = line.split(':', 2)[1].trim();
504
513
  if (line.startsWith('session_id:')) sessionId = line.slice('session_id:'.length).trim();
505
514
  if (line.startsWith('agent_id:')) agentId = line.slice('agent_id:'.length).trim();
515
+ if (line.startsWith('pain_event_id:')) {
516
+ const raw = line.slice('pain_event_id:'.length).trim();
517
+ painEventId = parseInt(raw, 10) || undefined;
518
+ }
506
519
 
507
520
  // Key=Value fallback format: "key=value" (pain skill manual output)
508
521
  // Handles both uppercase (Source=X) and lowercase (source=x) variants
@@ -544,7 +557,7 @@ async function checkPainFlag(wctx: WorkspaceContext, logger: PluginLogger): Prom
544
557
 
545
558
  return doEnqueuePainTask(wctx, logger, painFlagPath, result, {
546
559
  score, source, reason, preview,
547
- traceId, sessionId, agentId,
560
+ traceId, sessionId, agentId, painEventId,
548
561
  });
549
562
 
550
563
  } catch (err) {
@@ -68,6 +68,8 @@ export interface RecentPainContext {
68
68
  reason: string;
69
69
  timestamp: string;
70
70
  sessionId: string;
71
+ /** Trajectory pain_events row ID — set when pain flag includes pain_event_id */
72
+ painEventId?: number;
71
73
  } | null;
72
74
  recentPainCount: number;
73
75
  recentMaxPainScore: number;
@@ -150,10 +152,12 @@ export function readRecentPainContext(wctx: WorkspaceContext): RecentPainContext
150
152
  const reason = contract.data.reason ?? '';
151
153
  const timestamp = contract.data.time ?? '';
152
154
  const sessionId = contract.data.session_id ?? '';
155
+ const painEventIdRaw = contract.data.pain_event_id;
156
+ const painEventId = painEventIdRaw ? parseInt(painEventIdRaw, 10) : undefined;
153
157
 
154
158
  if (score > 0) {
155
159
  return {
156
- mostRecent: { score, source, reason, timestamp, sessionId },
160
+ mostRecent: { score, source, reason, timestamp, sessionId, painEventId },
157
161
  recentPainCount: 1,
158
162
  recentMaxPainScore: score,
159
163
  };