principles-disciple 1.16.0 → 1.17.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/README.md +13 -5
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/src/commands/archive-impl.ts +3 -3
- package/src/commands/capabilities.ts +1 -1
- package/src/commands/context.ts +3 -3
- package/src/commands/disable-impl.ts +1 -1
- package/src/commands/evolution-status.ts +2 -2
- package/src/commands/focus.ts +2 -2
- package/src/commands/nocturnal-train.ts +6 -6
- package/src/commands/pain.ts +4 -4
- package/src/commands/pd-reflect.ts +87 -0
- package/src/commands/rollback-impl.ts +4 -4
- package/src/commands/rollback.ts +2 -2
- package/src/commands/samples.ts +2 -2
- package/src/commands/workflow-debug.ts +1 -1
- package/src/config/errors.ts +1 -1
- package/src/core/adaptive-thresholds.ts +1 -1
- package/src/core/code-implementation-storage.ts +2 -2
- package/src/core/config.ts +1 -1
- package/src/core/diagnostician-task-store.ts +2 -2
- package/src/core/empathy-keyword-matcher.ts +3 -3
- package/src/core/event-log.ts +5 -5
- package/src/core/evolution-engine.ts +4 -4
- package/src/core/evolution-logger.ts +1 -1
- package/src/core/evolution-reducer.ts +3 -3
- package/src/core/evolution-types.ts +5 -5
- package/src/core/external-training-contract.ts +1 -1
- package/src/core/focus-history.ts +14 -14
- package/src/core/hygiene/tracker.ts +1 -1
- package/src/core/init.ts +2 -2
- package/src/core/model-deployment-registry.ts +2 -2
- package/src/core/model-training-registry.ts +2 -2
- package/src/core/nocturnal-arbiter.ts +1 -1
- package/src/core/nocturnal-artificer.ts +2 -2
- package/src/core/nocturnal-candidate-scoring.ts +2 -2
- package/src/core/nocturnal-compliance.ts +3 -3
- package/src/core/nocturnal-dataset.ts +3 -3
- package/src/core/nocturnal-export.ts +4 -4
- package/src/core/nocturnal-rule-implementation-validator.ts +1 -1
- package/src/core/nocturnal-snapshot-contract.ts +112 -0
- package/src/core/nocturnal-trajectory-extractor.ts +7 -5
- package/src/core/nocturnal-trinity.ts +27 -28
- package/src/core/pain-context-extractor.ts +3 -3
- package/src/core/pain.ts +124 -11
- package/src/core/path-resolver.ts +4 -4
- package/src/core/pd-task-reconciler.ts +10 -10
- package/src/core/pd-task-service.ts +1 -1
- package/src/core/pd-task-store.ts +1 -1
- package/src/core/principle-internalization/deprecated-readiness.ts +1 -1
- package/src/core/principle-training-state.ts +2 -2
- package/src/core/principle-tree-ledger.ts +7 -7
- package/src/core/promotion-gate.ts +9 -9
- package/src/core/replay-engine.ts +12 -12
- package/src/core/risk-calculator.ts +1 -1
- package/src/core/rule-host-types.ts +2 -2
- package/src/core/rule-host.ts +5 -5
- package/src/core/schema/db-types.ts +1 -1
- package/src/core/schema/schema-definitions.ts +1 -1
- package/src/core/session-tracker.ts +96 -4
- package/src/core/shadow-observation-registry.ts +3 -3
- package/src/core/system-logger.ts +2 -2
- package/src/core/thinking-os-parser.ts +1 -1
- package/src/core/training-program.ts +2 -2
- package/src/core/trajectory.ts +8 -8
- package/src/core/workspace-context.ts +2 -2
- package/src/core/workspace-dir-service.ts +85 -0
- package/src/core/workspace-dir-validation.ts +30 -107
- package/src/hooks/bash-risk.ts +3 -3
- package/src/hooks/edit-verification.ts +4 -4
- package/src/hooks/gate-block-helper.ts +4 -4
- package/src/hooks/gate.ts +10 -10
- package/src/hooks/gfi-gate.ts +7 -7
- package/src/hooks/lifecycle.ts +2 -2
- package/src/hooks/llm.ts +1 -1
- package/src/hooks/pain.ts +25 -5
- package/src/hooks/progressive-trust-gate.ts +7 -7
- package/src/hooks/prompt.ts +24 -5
- package/src/hooks/subagent.ts +2 -2
- package/src/hooks/thinking-checkpoint.ts +2 -2
- package/src/hooks/trajectory-collector.ts +1 -1
- package/src/http/principles-console-route.ts +14 -6
- package/src/i18n/commands.ts +4 -0
- package/src/index.ts +181 -185
- package/src/service/central-health-service.ts +1 -1
- package/src/service/central-overview-service.ts +3 -3
- package/src/service/evolution-query-service.ts +1 -1
- package/src/service/evolution-worker.ts +209 -104
- package/src/service/health-query-service.ts +27 -17
- package/src/service/monitoring-query-service.ts +3 -3
- package/src/service/nocturnal-runtime.ts +4 -4
- package/src/service/nocturnal-service.ts +40 -23
- package/src/service/nocturnal-target-selector.ts +2 -2
- package/src/service/runtime-summary-service.ts +1 -1
- package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -1
- package/src/service/subagent-workflow/empathy-observer-workflow-manager.ts +3 -3
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -13
- package/src/service/subagent-workflow/runtime-direct-driver.ts +10 -6
- package/src/service/subagent-workflow/types.ts +4 -4
- package/src/service/subagent-workflow/workflow-manager-base.ts +5 -5
- package/src/service/subagent-workflow/workflow-store.ts +2 -2
- package/src/tools/critique-prompt.ts +2 -3
- package/src/tools/deep-reflect.ts +17 -16
- package/src/tools/model-index.ts +1 -1
- package/src/utils/file-lock.ts +1 -1
- package/src/utils/io.ts +7 -2
- package/src/utils/nlp.ts +1 -1
- package/src/utils/plugin-logger.ts +2 -2
- package/src/utils/retry.ts +3 -2
- package/src/utils/subagent-probe.ts +20 -33
- package/templates/langs/en/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/langs/zh/skills/pd-pain-signal/SKILL.md +8 -7
- package/templates/pain_settings.json +1 -1
- package/tests/build-artifacts.test.ts +4 -58
- package/tests/commands/pd-reflect.test.ts +49 -0
- package/tests/core/nocturnal-snapshot-contract.test.ts +70 -0
- package/tests/core/pain-auto-repair.test.ts +96 -0
- package/tests/core/pain-integration.test.ts +483 -0
- package/tests/core/pain.test.ts +5 -4
- package/tests/core/workspace-dir-service.test.ts +68 -0
- package/tests/core/workspace-dir-validation.test.ts +56 -192
- package/tests/hooks/pain.test.ts +20 -0
- package/tests/http/principles-console-route.test.ts +42 -20
- package/tests/integration/empathy-workflow-integration.test.ts +1 -2
- package/tests/integration/tool-hooks-workspace-dir.e2e.test.ts +9 -17
- package/tests/service/empathy-observer-workflow-manager.test.ts +1 -2
- package/tests/service/evolution-worker.nocturnal.test.ts +562 -6
- package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
- package/tests/utils/subagent-probe.test.ts +32 -0
|
@@ -45,9 +45,8 @@ import {
|
|
|
45
45
|
// ---------------------------------------------------------------------------
|
|
46
46
|
// Embedded Role Prompts
|
|
47
47
|
// ---------------------------------------------------------------------------
|
|
48
|
-
// These prompts are embedded at build time
|
|
49
|
-
//
|
|
50
|
-
// did not copy the agents/ directory into the bundle.
|
|
48
|
+
// These prompts are embedded at build time. The agents/ directory was removed
|
|
49
|
+
// to eliminate fragile runtime file dependencies on the file system.
|
|
51
50
|
|
|
52
51
|
const NOCTURNAL_DREAMER_PROMPT = `# Nocturnal Dreamer — Candidate Generation
|
|
53
52
|
|
|
@@ -276,7 +275,7 @@ If you cannot synthesize an artifact:
|
|
|
276
275
|
* Interface for Trinity stage invocation.
|
|
277
276
|
* Implementations can use real subagent runtimes or stubs.
|
|
278
277
|
*/
|
|
279
|
-
|
|
278
|
+
|
|
280
279
|
export interface TrinityRuntimeAdapter {
|
|
281
280
|
/**
|
|
282
281
|
* Invoke the Dreamer stage.
|
|
@@ -327,7 +326,7 @@ export interface TrinityRuntimeAdapter {
|
|
|
327
326
|
*/
|
|
328
327
|
close?(): Promise<void>;
|
|
329
328
|
}
|
|
330
|
-
|
|
329
|
+
|
|
331
330
|
|
|
332
331
|
// ---------------------------------------------------------------------------
|
|
333
332
|
// OpenClaw Runtime Adapter
|
|
@@ -339,7 +338,7 @@ export interface TrinityRuntimeAdapter {
|
|
|
339
338
|
* Does NOT depend on OpenClaw internals.
|
|
340
339
|
*/
|
|
341
340
|
export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
|
|
342
|
-
|
|
341
|
+
|
|
343
342
|
private readonly api: {
|
|
344
343
|
runtime: {
|
|
345
344
|
subagent: {
|
|
@@ -366,7 +365,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
|
|
|
366
365
|
};
|
|
367
366
|
};
|
|
368
367
|
};
|
|
369
|
-
|
|
368
|
+
|
|
370
369
|
|
|
371
370
|
private readonly stageTimeoutMs: number;
|
|
372
371
|
|
|
@@ -472,14 +471,14 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
|
|
|
472
471
|
}
|
|
473
472
|
}
|
|
474
473
|
|
|
475
|
-
|
|
474
|
+
|
|
476
475
|
async invokeScribe(
|
|
477
476
|
dreamerOutput: DreamerOutput,
|
|
478
477
|
philosopherOutput: PhilosopherOutput,
|
|
479
478
|
snapshot: NocturnalSessionSnapshot,
|
|
480
479
|
principleId: string,
|
|
481
480
|
telemetry: TrinityTelemetry,
|
|
482
|
-
|
|
481
|
+
|
|
483
482
|
_config: TrinityConfig
|
|
484
483
|
): Promise<TrinityDraftArtifact | null> {
|
|
485
484
|
const sessionKey = `agent:main:subagent:ne-scribe-${randomUUID()}`;
|
|
@@ -519,7 +518,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
|
|
|
519
518
|
}
|
|
520
519
|
}
|
|
521
520
|
|
|
522
|
-
|
|
521
|
+
|
|
523
522
|
async close(): Promise<void> {
|
|
524
523
|
// Nothing to clean up in this implementation
|
|
525
524
|
}
|
|
@@ -528,7 +527,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
|
|
|
528
527
|
// Private Helper Methods
|
|
529
528
|
// ---------------------------------------------------------------------------
|
|
530
529
|
|
|
531
|
-
|
|
530
|
+
|
|
532
531
|
private extractAssistantText(
|
|
533
532
|
messages: { role: string; text?: string; content?: string }[]
|
|
534
533
|
): string {
|
|
@@ -541,7 +540,7 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
|
|
|
541
540
|
return '';
|
|
542
541
|
}
|
|
543
542
|
|
|
544
|
-
|
|
543
|
+
|
|
545
544
|
private buildDreamerPrompt(
|
|
546
545
|
snapshot: NocturnalSessionSnapshot,
|
|
547
546
|
principleId: string,
|
|
@@ -551,18 +550,18 @@ export class OpenClawTrinityRuntimeAdapter implements TrinityRuntimeAdapter {
|
|
|
551
550
|
|
|
552
551
|
Session Snapshot:
|
|
553
552
|
- Session ID: ${snapshot.sessionId}
|
|
554
|
-
- Assistant Turns: ${snapshot.stats.totalAssistantTurns}
|
|
555
|
-
- Tool Calls: ${snapshot.stats.totalToolCalls}
|
|
556
|
-
- Failures: ${snapshot.stats.failureCount}
|
|
553
|
+
- Assistant Turns: ${snapshot.stats.totalAssistantTurns ?? 'N/A (data unavailable)'}
|
|
554
|
+
- Tool Calls: ${snapshot.stats.totalToolCalls ?? 'N/A (data unavailable)'}
|
|
555
|
+
- Failures: ${snapshot.stats.failureCount ?? 'N/A (data unavailable)'}
|
|
557
556
|
- Pain Events: ${snapshot.stats.totalPainEvents}
|
|
558
|
-
- Gate Blocks: ${snapshot.stats.totalGateBlocks}
|
|
557
|
+
- Gate Blocks: ${snapshot.stats.totalGateBlocks ?? 'N/A (data unavailable)'}
|
|
559
558
|
|
|
560
559
|
Please analyze this session and generate ${maxCandidates} candidate corrections. Each candidate should identify a bad decision and propose a better alternative grounded in the target principle.
|
|
561
560
|
|
|
562
561
|
Respond with ONLY a valid JSON object matching the DreamerOutput contract.`;
|
|
563
562
|
}
|
|
564
563
|
|
|
565
|
-
|
|
564
|
+
|
|
566
565
|
private buildPhilosopherPrompt(
|
|
567
566
|
dreamerOutput: DreamerOutput,
|
|
568
567
|
principleId: string
|
|
@@ -576,7 +575,7 @@ ${candidatesJson}
|
|
|
576
575
|
Please evaluate each candidate and rank them by principle alignment, specificity, and actionability. Respond with ONLY a valid JSON object matching the PhilosopherOutput contract.`;
|
|
577
576
|
}
|
|
578
577
|
|
|
579
|
-
|
|
578
|
+
|
|
580
579
|
private buildScribePrompt(
|
|
581
580
|
dreamerOutput: DreamerOutput,
|
|
582
581
|
philosopherOutput: PhilosopherOutput,
|
|
@@ -596,7 +595,7 @@ ${judgmentsJson}
|
|
|
596
595
|
|
|
597
596
|
Select the best candidate (Philosopher's rank 1) and synthesize it into a final TrinityDraftArtifact. Respond with ONLY a valid JSON object.`;
|
|
598
597
|
}
|
|
599
|
-
|
|
598
|
+
|
|
600
599
|
|
|
601
600
|
private parseDreamerOutput(text: string): DreamerOutput {
|
|
602
601
|
const json = this.extractJson(text);
|
|
@@ -694,12 +693,12 @@ Select the best candidate (Philosopher's rank 1) and synthesize it into a final
|
|
|
694
693
|
}
|
|
695
694
|
}
|
|
696
695
|
|
|
697
|
-
|
|
696
|
+
|
|
698
697
|
private parseScribeOutput(
|
|
699
698
|
text: string,
|
|
700
699
|
snapshot: NocturnalSessionSnapshot,
|
|
701
700
|
principleId: string,
|
|
702
|
-
|
|
701
|
+
|
|
703
702
|
_telemetry: TrinityTelemetry
|
|
704
703
|
): TrinityDraftArtifact | null {
|
|
705
704
|
const json = this.extractJson(text);
|
|
@@ -740,7 +739,7 @@ Select the best candidate (Philosopher's rank 1) and synthesize it into a final
|
|
|
740
739
|
/**
|
|
741
740
|
* Extract JSON object from text that may contain markdown code blocks.
|
|
742
741
|
*/
|
|
743
|
-
|
|
742
|
+
|
|
744
743
|
private extractJson(text: string): string | null {
|
|
745
744
|
// Try direct parse first
|
|
746
745
|
try {
|
|
@@ -996,9 +995,9 @@ export function invokeStubDreamer(
|
|
|
996
995
|
principleId: string,
|
|
997
996
|
maxCandidates: number
|
|
998
997
|
): DreamerOutput {
|
|
999
|
-
const hasFailures = snapshot.stats.failureCount > 0;
|
|
998
|
+
const hasFailures = (snapshot.stats.failureCount ?? 0) > 0;
|
|
1000
999
|
const hasPain = snapshot.stats.totalPainEvents > 0;
|
|
1001
|
-
const hasGateBlocks = snapshot.stats.totalGateBlocks > 0;
|
|
1000
|
+
const hasGateBlocks = (snapshot.stats.totalGateBlocks ?? 0) > 0;
|
|
1002
1001
|
|
|
1003
1002
|
// #219: Detect fallback data source - stats may be incomplete
|
|
1004
1003
|
const isFallback = snapshot._dataSource === 'pain_context_fallback';
|
|
@@ -1126,7 +1125,7 @@ export function invokeStubDreamer(
|
|
|
1126
1125
|
*/
|
|
1127
1126
|
export function invokeStubPhilosopher(
|
|
1128
1127
|
dreamerOutput: DreamerOutput,
|
|
1129
|
-
|
|
1128
|
+
|
|
1130
1129
|
_principleId: string
|
|
1131
1130
|
): PhilosopherOutput {
|
|
1132
1131
|
if (!dreamerOutput.valid || dreamerOutput.candidates.length === 0) {
|
|
@@ -1201,7 +1200,7 @@ export function invokeStubPhilosopher(
|
|
|
1201
1200
|
* In production, this would call the actual Scribe subagent.
|
|
1202
1201
|
* The stub uses tournament selection (scoring + thresholds) to pick the winner.
|
|
1203
1202
|
*/
|
|
1204
|
-
|
|
1203
|
+
|
|
1205
1204
|
export function invokeStubScribe(
|
|
1206
1205
|
dreamerOutput: DreamerOutput,
|
|
1207
1206
|
philosopherOutput: PhilosopherOutput,
|
|
@@ -1279,7 +1278,7 @@ export function runTrinity(options: RunTrinityOptions): TrinityResult {
|
|
|
1279
1278
|
|
|
1280
1279
|
// Stub path: use synchronous stub implementations
|
|
1281
1280
|
if (config.useStubs) {
|
|
1282
|
-
|
|
1281
|
+
|
|
1283
1282
|
return runTrinityWithStubs(snapshot, principleId, config);
|
|
1284
1283
|
}
|
|
1285
1284
|
|
|
@@ -1318,7 +1317,7 @@ export async function runTrinityAsync(options: RunTrinityOptions): Promise<Trini
|
|
|
1318
1317
|
|
|
1319
1318
|
if (config.useStubs) {
|
|
1320
1319
|
// Stub path: use synchronous stubs
|
|
1321
|
-
|
|
1320
|
+
|
|
1322
1321
|
return runTrinityWithStubs(snapshot, principleId, config);
|
|
1323
1322
|
}
|
|
1324
1323
|
|
|
@@ -53,7 +53,7 @@ function getAgentsDir(): string {
|
|
|
53
53
|
async function safeTail(filePath: string): Promise<string[]> {
|
|
54
54
|
try {
|
|
55
55
|
// Check existence and stats asynchronously
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
let stat: fs.Stats;
|
|
58
58
|
try {
|
|
59
59
|
stat = await fsPromises.stat(filePath);
|
|
@@ -235,8 +235,8 @@ export async function extractRecentConversation(
|
|
|
235
235
|
/**
|
|
236
236
|
* Extracts failed tool call context with argument correlation.
|
|
237
237
|
*/
|
|
238
|
-
|
|
239
|
-
|
|
238
|
+
// Reason: breaking API change - default param must precede required params for type inference compatibility
|
|
239
|
+
|
|
240
240
|
export async function extractFailedToolContext(
|
|
241
241
|
sessionId: string,
|
|
242
242
|
agentId = 'main',
|
package/src/core/pain.ts
CHANGED
|
@@ -3,6 +3,7 @@ import * as path from 'path';
|
|
|
3
3
|
import { serializeKvLines, parseKvLines } from '../utils/io.js';
|
|
4
4
|
import { resolvePdPath } from './paths.js';
|
|
5
5
|
import { ConfigService } from './config-service.js';
|
|
6
|
+
import { SystemLogger } from './system-logger.js';
|
|
6
7
|
|
|
7
8
|
// =========================================================================
|
|
8
9
|
// Pain Flag Contract (Single Source of Truth)
|
|
@@ -36,6 +37,13 @@ export interface PainFlagData {
|
|
|
36
37
|
trigger_text_preview?: string;
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
export interface PainFlagContractResult {
|
|
41
|
+
status: 'missing' | 'valid' | 'invalid';
|
|
42
|
+
format: 'missing' | 'empty' | 'kv' | 'json' | 'invalid_json';
|
|
43
|
+
data: Record<string, string>;
|
|
44
|
+
missingFields: string[];
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
/**
|
|
40
48
|
* Factory function — the ONLY way to construct pain flag data.
|
|
41
49
|
*
|
|
@@ -58,16 +66,18 @@ export function buildPainFlag(input: {
|
|
|
58
66
|
trace_id?: string;
|
|
59
67
|
trigger_text_preview?: string;
|
|
60
68
|
}): PainFlagData {
|
|
69
|
+
// Omit optional fields when not provided — prevents writing empty lines to disk
|
|
70
|
+
// which causes agent confusion (SKILL.md vs reality drift)
|
|
61
71
|
return {
|
|
62
72
|
source: input.source,
|
|
63
73
|
score: input.score,
|
|
64
74
|
time: input.time || new Date().toISOString(),
|
|
65
75
|
reason: input.reason,
|
|
66
|
-
session_id: input.session_id
|
|
67
|
-
agent_id: input.agent_id
|
|
76
|
+
session_id: input.session_id ?? '',
|
|
77
|
+
agent_id: input.agent_id ?? '',
|
|
68
78
|
is_risky: input.is_risky ? 'true' : 'false',
|
|
69
|
-
trace_id: input.trace_id
|
|
70
|
-
trigger_text_preview: input.trigger_text_preview
|
|
79
|
+
trace_id: input.trace_id ?? '',
|
|
80
|
+
trigger_text_preview: input.trigger_text_preview ?? '',
|
|
71
81
|
};
|
|
72
82
|
}
|
|
73
83
|
|
|
@@ -77,7 +87,9 @@ export function buildPainFlag(input: {
|
|
|
77
87
|
*/
|
|
78
88
|
export function validatePainFlag(data: Record<string, string>): string[] {
|
|
79
89
|
const missing: string[] = [];
|
|
80
|
-
|
|
90
|
+
// Only source/score/time/reason are truly required — session_id/agent_id
|
|
91
|
+
// may be empty in automated contexts (heartbeat, background workers)
|
|
92
|
+
const required = ['source', 'score', 'time', 'reason'] as const;
|
|
81
93
|
for (const field of required) {
|
|
82
94
|
if (!data[field] || data[field].trim() === '') {
|
|
83
95
|
missing.push(field);
|
|
@@ -86,7 +98,6 @@ export function validatePainFlag(data: Record<string, string>): string[] {
|
|
|
86
98
|
return missing;
|
|
87
99
|
}
|
|
88
100
|
|
|
89
|
-
// eslint-disable-next-line @typescript-eslint/max-params -- Reason: Score computation requires all 5 parameters - refactoring to options object would be breaking API change
|
|
90
101
|
export function computePainScore(rc: number, isSpiral: boolean, missingTestCommand: boolean, softScore: number, projectDir?: string): number {
|
|
91
102
|
let score = Math.max(0, softScore || 0);
|
|
92
103
|
|
|
@@ -146,26 +157,128 @@ export function writePainFlag(projectDir: string, painData: PainFlagData): void
|
|
|
146
157
|
fs.writeFileSync(painFlagPath, serializeKvLines(painData), "utf-8");
|
|
147
158
|
}
|
|
148
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Converts a JSON pain flag object to KV format.
|
|
162
|
+
*/
|
|
163
|
+
function convertJsonToKv(json: Record<string, unknown>): Record<string, string> {
|
|
164
|
+
const kvData: Record<string, string> = {};
|
|
165
|
+
const fieldMap: Record<string, string> = {
|
|
166
|
+
source: 'source',
|
|
167
|
+
score: 'score',
|
|
168
|
+
time: 'time',
|
|
169
|
+
timestamp: 'time',
|
|
170
|
+
reason: 'reason',
|
|
171
|
+
session_id: 'session_id',
|
|
172
|
+
sessionId: 'session_id',
|
|
173
|
+
agent_id: 'agent_id',
|
|
174
|
+
agentId: 'agent_id',
|
|
175
|
+
is_risky: 'is_risky',
|
|
176
|
+
isRisky: 'is_risky',
|
|
177
|
+
severity: 'severity',
|
|
178
|
+
painId: 'pain_id',
|
|
179
|
+
};
|
|
180
|
+
for (const [jsonKey, kvKey] of Object.entries(fieldMap)) {
|
|
181
|
+
if (json[jsonKey] !== undefined) {
|
|
182
|
+
kvData[kvKey] = String(json[jsonKey]);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
for (const [key, value] of Object.entries(json)) {
|
|
186
|
+
if (fieldMap[key] === undefined && value !== undefined && value !== null) {
|
|
187
|
+
kvData[key] = String(value);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return kvData;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Reads and validates the pain flag file with auto-repair.
|
|
195
|
+
*
|
|
196
|
+
* - If file doesn't exist → returns {}
|
|
197
|
+
* - If file is JSON format (wrong) → converts to KV, logs warning, rewrites file
|
|
198
|
+
* - If file is KV format → validates required fields, logs warning if missing
|
|
199
|
+
* - If file has unknown fields → silently ignores them (forward-compatible)
|
|
200
|
+
*/
|
|
149
201
|
export function readPainFlagData(projectDir: string): Record<string, string> {
|
|
150
202
|
const painFlagPath = resolvePdPath(projectDir, 'PAIN_FLAG');
|
|
151
203
|
try {
|
|
152
204
|
if (!fs.existsSync(painFlagPath)) {
|
|
153
205
|
return {};
|
|
154
206
|
}
|
|
155
|
-
const content = fs.readFileSync(painFlagPath, "utf-8");
|
|
156
|
-
|
|
157
|
-
|
|
207
|
+
const content = fs.readFileSync(painFlagPath, "utf-8").trim();
|
|
208
|
+
if (!content) {
|
|
209
|
+
return {};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Detect JSON format (wrong — should be KV)
|
|
213
|
+
if (content.startsWith('{')) {
|
|
214
|
+
let json: Record<string, unknown>;
|
|
215
|
+
try {
|
|
216
|
+
json = JSON.parse(content);
|
|
217
|
+
} catch {
|
|
218
|
+
SystemLogger.log(projectDir, 'PAIN_FLAG_CORRUPT', 'Pain flag file contains invalid JSON');
|
|
219
|
+
return {};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Auto-repair: convert JSON to KV format
|
|
223
|
+
const kvData = convertJsonToKv(json);
|
|
224
|
+
|
|
225
|
+
const repaired = serializeKvLines(kvData);
|
|
226
|
+
fs.writeFileSync(painFlagPath, repaired, 'utf-8');
|
|
227
|
+
SystemLogger.log(projectDir, 'PAIN_FLAG_AUTO_REPAIRED', `Auto-repaired pain flag from JSON to KV format (${Object.keys(json).length} fields)`);
|
|
228
|
+
return kvData;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// KV format — parse and validate
|
|
232
|
+
const data = parseKvLines(content);
|
|
233
|
+
const missing = validatePainFlag(data);
|
|
234
|
+
if (missing.length > 0) {
|
|
235
|
+
SystemLogger.log(projectDir, 'PAIN_FLAG_INCOMPLETE', `Pain flag missing required fields: ${missing.join(', ')}`);
|
|
236
|
+
}
|
|
237
|
+
return data;
|
|
238
|
+
} catch (e) {
|
|
239
|
+
SystemLogger.log(projectDir, 'PAIN_FLAG_READ_ERROR', `Failed to read pain flag: ${String(e)}`);
|
|
158
240
|
return {};
|
|
159
241
|
}
|
|
160
242
|
}
|
|
161
243
|
|
|
244
|
+
export function readPainFlagContract(projectDir: string): PainFlagContractResult {
|
|
245
|
+
const data = readPainFlagData(projectDir);
|
|
246
|
+
|
|
247
|
+
if (Object.keys(data).length === 0) {
|
|
248
|
+
const painFlagPath = resolvePdPath(projectDir, 'PAIN_FLAG');
|
|
249
|
+
if (!fs.existsSync(painFlagPath)) {
|
|
250
|
+
return { status: 'missing', format: 'missing', data: {}, missingFields: [] };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const raw = fs.readFileSync(painFlagPath, 'utf-8').trim();
|
|
254
|
+
if (!raw) {
|
|
255
|
+
return { status: 'missing', format: 'empty', data: {}, missingFields: [] };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
status: 'invalid',
|
|
260
|
+
format: raw.startsWith('{') ? 'invalid_json' : 'kv',
|
|
261
|
+
data: {},
|
|
262
|
+
missingFields: ['unparseable'],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const missing = validatePainFlag(data);
|
|
267
|
+
return {
|
|
268
|
+
status: missing.length > 0 ? 'invalid' : 'valid',
|
|
269
|
+
format: 'kv',
|
|
270
|
+
data,
|
|
271
|
+
missingFields: missing,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
162
275
|
/**
|
|
163
276
|
* Track principle value metrics when a pain signal is written.
|
|
164
277
|
* This is observation-only — it does NOT affect the pain flag write flow.
|
|
165
278
|
* If any principle matches the pain signal, its painPreventedCount is incremented.
|
|
166
279
|
* Errors are silently ignored to avoid disrupting the pain pipeline.
|
|
167
280
|
*/
|
|
168
|
-
|
|
281
|
+
|
|
169
282
|
export function trackPrincipleValue(
|
|
170
283
|
workspaceDir: string,
|
|
171
284
|
painData: { reason?: string; source?: string; score?: string },
|
|
@@ -174,7 +287,7 @@ export function trackPrincipleValue(
|
|
|
174
287
|
trigger: string;
|
|
175
288
|
valueMetrics?: { painPreventedCount: number; lastPainPreventedAt?: string; calculatedAt: string };
|
|
176
289
|
}[],
|
|
177
|
-
updatePrincipleMetrics: (_id: string, _metrics: { painPreventedCount: number; lastPainPreventedAt: string; calculatedAt: string }) => void,
|
|
290
|
+
updatePrincipleMetrics: (_id: string, _metrics: { painPreventedCount: number; lastPainPreventedAt: string; calculatedAt: string }) => void,
|
|
178
291
|
): void {
|
|
179
292
|
try {
|
|
180
293
|
const activePrinciples = getActivePrinciples();
|
|
@@ -8,12 +8,12 @@ export interface PathResolverOptions {
|
|
|
8
8
|
workspaceDir?: string;
|
|
9
9
|
normalizeWorkspace?: boolean;
|
|
10
10
|
logger?: {
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
debug?: (_msg: string) => void;
|
|
13
13
|
info?: (_msg: string) => void;
|
|
14
14
|
warn?: (_msg: string) => void;
|
|
15
15
|
error?: (_msg: string) => void;
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -420,9 +420,9 @@ export function resolveWorkspaceDirFromApi(
|
|
|
420
420
|
if (!api) return undefined;
|
|
421
421
|
|
|
422
422
|
// 1. Official API: api.runtime.agent.resolveAgentWorkspaceDir
|
|
423
|
-
|
|
423
|
+
|
|
424
424
|
const officialAgent = (api.runtime as { agent?: { resolveAgentWorkspaceDir?: (cfg: unknown, id: string) => string } }).agent;
|
|
425
|
-
|
|
425
|
+
|
|
426
426
|
if (officialAgent?.resolveAgentWorkspaceDir) {
|
|
427
427
|
try {
|
|
428
428
|
return officialAgent.resolveAgentWorkspaceDir(api.config, agentId ?? 'main');
|
|
@@ -72,14 +72,14 @@ export interface ReconcileResult {
|
|
|
72
72
|
export interface ReconcileOptions {
|
|
73
73
|
dryRun?: boolean;
|
|
74
74
|
workspaceDir: string;
|
|
75
|
-
|
|
75
|
+
|
|
76
76
|
logger?: { info?: (_: string) => void; warn?: (_: string) => void };
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
async function readCronStore(logger?: { info?: (_: string) => void; warn?: (_: string) => void }): Promise<CronStoreFile> {
|
|
82
|
-
|
|
82
|
+
|
|
83
83
|
if (!fs.existsSync(CRON_STORE_PATH)) {
|
|
84
84
|
logger?.info?.(`[PD:Reconciler] cron/jobs.json not found, starting with empty store`);
|
|
85
85
|
return { version: 1, jobs: [] };
|
|
@@ -143,7 +143,7 @@ function diff(declared: PDTaskSpec[], actual: CronJob[]): DiffAction[] {
|
|
|
143
143
|
function buildCronJob(
|
|
144
144
|
task: PDTaskSpec,
|
|
145
145
|
nowMs: number,
|
|
146
|
-
|
|
146
|
+
|
|
147
147
|
logger?: { info?: (_: string) => void },
|
|
148
148
|
): CronJob {
|
|
149
149
|
logger?.info?.(`[PD:Reconciler] Building cron job: ${task.name} (id=${task.id}, interval=${task.schedule.everyMs}ms)`);
|
|
@@ -158,7 +158,7 @@ function buildCronJob(
|
|
|
158
158
|
wakeMode: 'now',
|
|
159
159
|
payload: {
|
|
160
160
|
kind: 'agentTurn',
|
|
161
|
-
|
|
161
|
+
|
|
162
162
|
message: buildTaskPrompt(task, logger),
|
|
163
163
|
lightContext: task.execution.lightContext ?? true,
|
|
164
164
|
timeoutSeconds: task.execution.timeoutSeconds ?? 120,
|
|
@@ -177,7 +177,7 @@ function buildCronJob(
|
|
|
177
177
|
};
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
|
|
180
|
+
|
|
181
181
|
function buildTaskPrompt(task: PDTaskSpec, logger?: { info?: (_: string) => void }): string {
|
|
182
182
|
if (task.id === 'empathy-optimizer') {
|
|
183
183
|
logger?.info?.(`[PD:Reconciler] Building empathy optimizer prompt`);
|
|
@@ -292,7 +292,7 @@ export async function reconcilePDTasks(
|
|
|
292
292
|
});
|
|
293
293
|
|
|
294
294
|
const cronStore = await readCronStore(logger);
|
|
295
|
-
|
|
295
|
+
|
|
296
296
|
const healthUpdated = healthCheck(declared, cronStore, logger);
|
|
297
297
|
const actions = diff(healthUpdated, cronStore.jobs);
|
|
298
298
|
|
|
@@ -369,9 +369,9 @@ export async function reconcilePDTasks(
|
|
|
369
369
|
function healthCheck(
|
|
370
370
|
tasks: PDTaskSpec[],
|
|
371
371
|
cronStore: CronStoreFile,
|
|
372
|
-
|
|
372
|
+
|
|
373
373
|
logger: { info?: (_msg: string) => void; warn?: (_msg: string) => void },
|
|
374
|
-
|
|
374
|
+
|
|
375
375
|
): PDTaskSpec[] {
|
|
376
376
|
const jobByName = new Map(cronStore.jobs.map((j) => [j.name, j]));
|
|
377
377
|
|
|
@@ -35,7 +35,7 @@ export const PDTaskService: OpenClawPluginService = {
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
stop(_ctx: OpenClawPluginServiceContext): void {
|
|
40
40
|
/* intentionally empty - no cleanup required for this service */
|
|
41
41
|
},
|
|
@@ -55,7 +55,7 @@ export function initTaskMeta(task: PDTaskSpec): PDTaskSpec {
|
|
|
55
55
|
return task;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
export function updateSyncMeta(
|
|
60
60
|
task: PDTaskSpec,
|
|
61
61
|
status: 'ok' | 'error',
|
|
@@ -63,7 +63,7 @@ export function assessDeprecatedReadiness(
|
|
|
63
63
|
adherence.repeatedErrorReductionScore * 0.15,
|
|
64
64
|
);
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
let status: DeprecatedReadinessStatus;
|
|
68
68
|
if (blockingReasons.length === 0 && stableCoverageRatio === 1) {
|
|
69
69
|
status = 'ready';
|
|
@@ -62,7 +62,7 @@ export function createDefaultPrincipleState(principleId: string): PrincipleTrain
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export function loadStore(stateDir: string): PrincipleTrainingStore {
|
|
65
|
-
|
|
65
|
+
|
|
66
66
|
return ledgerTrainingStore(stateDir);
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -75,7 +75,7 @@ export function saveStore(stateDir: string, store: PrincipleTrainingStore): void
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export async function loadStoreAsync(stateDir: string): Promise<PrincipleTrainingStore> {
|
|
78
|
-
|
|
78
|
+
|
|
79
79
|
return ledgerTrainingStore(stateDir);
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -77,7 +77,7 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
77
77
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
|
|
81
81
|
function clampFloat(value: unknown, min: number, max: number, fallback: number): number {
|
|
82
82
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
83
83
|
return fallback;
|
|
@@ -85,7 +85,7 @@ function clampFloat(value: unknown, min: number, max: number, fallback: number):
|
|
|
85
85
|
return Math.max(min, Math.min(max, value));
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
function clampInt(value: unknown, min: number, max: number, fallback: number): number {
|
|
90
90
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
91
91
|
return fallback;
|
|
@@ -315,9 +315,9 @@ function writeLedgerUnlocked(filePath: string, store: HybridLedgerStore): void {
|
|
|
315
315
|
fs.writeFileSync(filePath, serializeLedger(store), 'utf-8');
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
|
|
319
319
|
function mutateLedger<T>(stateDir: string, mutate: (store: HybridLedgerStore) => T): T {
|
|
320
|
-
|
|
320
|
+
|
|
321
321
|
const filePath = getLedgerFilePath(stateDir);
|
|
322
322
|
return withLock(filePath, () => {
|
|
323
323
|
const store = readLedgerFromFile(filePath);
|
|
@@ -327,9 +327,9 @@ function mutateLedger<T>(stateDir: string, mutate: (store: HybridLedgerStore) =>
|
|
|
327
327
|
});
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
|
|
330
|
+
|
|
331
331
|
async function mutateLedgerAsync<T>(stateDir: string, mutate: (store: HybridLedgerStore) => Promise<T>): Promise<T> {
|
|
332
|
-
|
|
332
|
+
|
|
333
333
|
const filePath = getLedgerFilePath(stateDir);
|
|
334
334
|
return withLockAsync(filePath, async () => {
|
|
335
335
|
const store = readLedgerFromFile(filePath);
|
|
@@ -363,7 +363,7 @@ export async function saveLedgerAsync(stateDir: string, store: HybridLedgerStore
|
|
|
363
363
|
|
|
364
364
|
export function updateTrainingStore(
|
|
365
365
|
stateDir: string,
|
|
366
|
-
|
|
366
|
+
|
|
367
367
|
mutate: (store: LegacyPrincipleTrainingStore) => void,
|
|
368
368
|
): void {
|
|
369
369
|
mutateLedger(stateDir, (store) => {
|