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.
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/core/evolution-reducer.ts +45 -1
- package/src/core/pain.ts +7 -3
- package/src/core/principle-tree-ledger.ts +19 -1
- package/src/core/trajectory.ts +16 -8
- package/src/hooks/pain.ts +14 -9
- package/src/service/evolution-worker.ts +15 -2
- package/src/service/queue-io.ts +5 -1
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
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": "
|
|
80
|
-
"bundleMd5": "
|
|
81
|
-
"builtAt": "2026-04-
|
|
79
|
+
"gitSha": "9ae5cc1407da",
|
|
80
|
+
"bundleMd5": "68bc85f0121a780b83931b7ed5491b97",
|
|
81
|
+
"builtAt": "2026-04-16T02:19:50.219Z"
|
|
82
82
|
}
|
|
83
83
|
}
|
package/package.json
CHANGED
|
@@ -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
|
|
36
|
-
/**
|
|
37
|
-
trigger_text_preview
|
|
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];
|
package/src/core/trajectory.ts
CHANGED
|
@@ -237,10 +237,11 @@ export class TrajectoryDatabase {
|
|
|
237
237
|
return rowId;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
-
recordPainEvent(input: TrajectoryPainEventInput):
|
|
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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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,
|
|
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) {
|
package/src/service/queue-io.ts
CHANGED
|
@@ -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
|
};
|