principles-disciple 1.16.0 → 1.18.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 +4 -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 +480 -158
- 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 +221 -109
- 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 +11 -4
- 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/ai-sprint-orchestration/references/specs/nocturnal-trinity-quality-enhancement.json +111 -0
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/lib/task-specs.mjs +1 -1
- package/templates/langs/zh/skills/ai-sprint-orchestration/scripts/run.mjs +1 -1
- 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 +118 -109
- package/tests/service/nocturnal-runtime-hardening.test.ts +33 -0
- package/tests/utils/subagent-probe.test.ts +32 -0
|
@@ -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) => {
|
|
@@ -70,7 +70,7 @@ export const DEFAULT_ALLOWED_MARGIN = 0.05;
|
|
|
70
70
|
* Allowed worker profiles for Phase 7 shadow rollout.
|
|
71
71
|
* Only bounded local workers eligible. local-reader first, local-editor deferred.
|
|
72
72
|
*/
|
|
73
|
-
// eslint-disable-next-line
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Reason: reserved for Phase 7 shadow rollout profile validation
|
|
74
74
|
const ALLOWED_ROLLOUT_PROFILES: readonly TrainableWorkerProfile[] = ['local-reader'];
|
|
75
75
|
|
|
76
76
|
/**
|
|
@@ -233,7 +233,7 @@ function writeRegistry(stateDir: string, registry: PromotionRegistry): void {
|
|
|
233
233
|
*/
|
|
234
234
|
function withPromotionRegistryLock<T>(
|
|
235
235
|
stateDir: string,
|
|
236
|
-
|
|
236
|
+
|
|
237
237
|
fn: (_registry: PromotionRegistry) => T
|
|
238
238
|
): T {
|
|
239
239
|
const registryPath = getRegistryPath(stateDir);
|
|
@@ -318,7 +318,7 @@ export function evaluatePromotionGate(
|
|
|
318
318
|
): PromotionGateResult {
|
|
319
319
|
const {
|
|
320
320
|
checkpointId,
|
|
321
|
-
// eslint-disable-next-line
|
|
321
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Reason: reserved for Phase 7 profile-based targeting
|
|
322
322
|
targetProfile: _targetProfile,
|
|
323
323
|
baselineMetrics,
|
|
324
324
|
minDelta = DEFAULT_MIN_DELTA,
|
|
@@ -388,9 +388,9 @@ export function evaluatePromotionGate(
|
|
|
388
388
|
// PREFER real shadow evidence over eval verdict proxy
|
|
389
389
|
// Shadow evidence comes from actual runtime routing decisions
|
|
390
390
|
const shadowStats = computeShadowStats(stateDir, { checkpointId });
|
|
391
|
-
|
|
391
|
+
|
|
392
392
|
let arbiterRejectRate: number;
|
|
393
|
-
|
|
393
|
+
|
|
394
394
|
let arbiterRejectSource: 'shadow' | 'eval-proxy';
|
|
395
395
|
|
|
396
396
|
if (shadowStats && shadowStats.isStatisticallySignificant) {
|
|
@@ -424,9 +424,9 @@ export function evaluatePromotionGate(
|
|
|
424
424
|
|
|
425
425
|
// --- Check 6: Executability reject rate constraint ---
|
|
426
426
|
// PREFER real shadow evidence: escalation rate + profile rejection rate
|
|
427
|
-
|
|
427
|
+
|
|
428
428
|
let executabilityRejectRate: number;
|
|
429
|
-
|
|
429
|
+
|
|
430
430
|
let executabilityRejectSource: 'shadow' | 'eval-proxy';
|
|
431
431
|
|
|
432
432
|
if (shadowStats && shadowStats.isStatisticallySignificant) {
|
|
@@ -484,7 +484,7 @@ export function evaluatePromotionGate(
|
|
|
484
484
|
qualityCheck.passed;
|
|
485
485
|
|
|
486
486
|
// --- Suggest state based on checks ---
|
|
487
|
-
|
|
487
|
+
|
|
488
488
|
let suggestedState: PromotionState | undefined;
|
|
489
489
|
if (allPassed) {
|
|
490
490
|
suggestedState = 'candidate_only';
|
|
@@ -585,7 +585,7 @@ export function advancePromotion(
|
|
|
585
585
|
// - rejected → candidate_only/shadow_ready: allowed via re-evaluation
|
|
586
586
|
// (new eval data may reverse a previous rejection)
|
|
587
587
|
//
|
|
588
|
-
|
|
588
|
+
|
|
589
589
|
let targetState: PromotionState;
|
|
590
590
|
if (!gateResult.passes) {
|
|
591
591
|
targetState = 'rejected';
|
|
@@ -69,9 +69,9 @@ export interface ReplayReport {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
export interface CandidateEvaluator {
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
evaluate(sample: unknown): { passed: boolean; reason?: string; decision: string };
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
export class ReplayEngine {
|
|
@@ -112,7 +112,7 @@ export class ReplayEngine {
|
|
|
112
112
|
return samples;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
runSingleSample(sample: ReplaySample, evaluator: CandidateEvaluator): ReplayResult {
|
|
117
117
|
const evaluation = evaluator.evaluate(sample);
|
|
118
118
|
return {
|
|
@@ -202,12 +202,12 @@ export class ReplayEngine {
|
|
|
202
202
|
throw new Error(`Implementation ${implementation.id} does not export evaluate().`);
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
|
|
205
|
+
|
|
206
206
|
const evaluate = moduleExports.evaluate as (
|
|
207
207
|
_input: RuleHostInput,
|
|
208
208
|
_helpers: RuleHostHelpers,
|
|
209
209
|
) => RuleHostResult;
|
|
210
|
-
|
|
210
|
+
|
|
211
211
|
|
|
212
212
|
return {
|
|
213
213
|
evaluate: (sample: unknown) => {
|
|
@@ -284,7 +284,7 @@ export class ReplayEngine {
|
|
|
284
284
|
};
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
-
|
|
287
|
+
|
|
288
288
|
private _selectToolCall(
|
|
289
289
|
snapshot: NocturnalSessionSnapshot,
|
|
290
290
|
classification: SampleClassification,
|
|
@@ -314,7 +314,7 @@ export class ReplayEngine {
|
|
|
314
314
|
return byNewest[0] ?? null;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
|
|
317
|
+
|
|
318
318
|
private _matchGateBlock(
|
|
319
319
|
gateBlocks: NocturnalGateBlock[],
|
|
320
320
|
toolCall: NocturnalToolCall,
|
|
@@ -351,7 +351,7 @@ export class ReplayEngine {
|
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
353
|
|
|
354
|
-
|
|
354
|
+
|
|
355
355
|
private _estimateLineChanges(toolCall: NocturnalToolCall): number {
|
|
356
356
|
if (toolCall.toolName === 'edit' || toolCall.toolName === 'write') {
|
|
357
357
|
return 20;
|
|
@@ -359,7 +359,7 @@ export class ReplayEngine {
|
|
|
359
359
|
return 0;
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
|
|
362
|
+
|
|
363
363
|
private _inferBashRisk(toolCall: NocturnalToolCall): 'safe' | 'normal' | 'dangerous' | 'unknown' {
|
|
364
364
|
if (toolCall.toolName !== 'bash' && toolCall.toolName !== 'run_shell_command') {
|
|
365
365
|
return 'unknown';
|
|
@@ -371,7 +371,7 @@ export class ReplayEngine {
|
|
|
371
371
|
return toolCall.outcome === 'success' ? 'safe' : 'normal';
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
-
|
|
374
|
+
|
|
375
375
|
private _scoreEvaluation(
|
|
376
376
|
sample: ReplaySample,
|
|
377
377
|
result: RuleHostResult,
|
|
@@ -465,7 +465,7 @@ export class ReplayEngine {
|
|
|
465
465
|
};
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
|
|
468
|
+
|
|
469
469
|
private _determineDecision(
|
|
470
470
|
pain: ClassificationSummary,
|
|
471
471
|
success: ClassificationSummary,
|
|
@@ -495,7 +495,7 @@ export class ReplayEngine {
|
|
|
495
495
|
});
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
-
|
|
498
|
+
|
|
499
499
|
private _deriveExpectedOutcome(
|
|
500
500
|
record: NocturnalDatasetRecord,
|
|
501
501
|
): ReplaySample['expectedOutcome'] {
|
|
@@ -93,7 +93,7 @@ export function getTargetFileLineCount(absoluteFilePath: string): number | null
|
|
|
93
93
|
* @param maxLines - Optional upper bound to prevent misconfiguration
|
|
94
94
|
* @returns Maximum allowed lines (at least minLines, at most maxLines if provided)
|
|
95
95
|
*/
|
|
96
|
-
|
|
96
|
+
|
|
97
97
|
export function calculatePercentageThreshold(
|
|
98
98
|
targetLineCount: number,
|
|
99
99
|
percentage: number,
|
|
@@ -76,7 +76,7 @@ export interface LoadedImplementation {
|
|
|
76
76
|
implId: string;
|
|
77
77
|
ruleId: string;
|
|
78
78
|
meta: RuleHostMeta;
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
evaluate: (_input: RuleHostInput) => RuleHostResult;
|
|
81
|
-
|
|
81
|
+
|
|
82
82
|
}
|
package/src/core/rule-host.ts
CHANGED
|
@@ -36,9 +36,9 @@ import type {
|
|
|
36
36
|
import type { Implementation } from '../types/principle-tree-schema.js';
|
|
37
37
|
|
|
38
38
|
export interface RuleHostLogger {
|
|
39
|
-
|
|
39
|
+
|
|
40
40
|
warn?: (_message: string) => void;
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export class RuleHost {
|
|
@@ -69,7 +69,7 @@ export class RuleHost {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
// Merge decisions from all active implementations
|
|
72
|
-
|
|
72
|
+
|
|
73
73
|
let blocked: RuleHostResult | undefined;
|
|
74
74
|
const approvals: RuleHostResult[] = [];
|
|
75
75
|
|
|
@@ -218,12 +218,12 @@ export class RuleHost {
|
|
|
218
218
|
|
|
219
219
|
// Return a loaded implementation that wraps the compiled evaluate
|
|
220
220
|
// with the actual helpers from the input at evaluation time
|
|
221
|
-
|
|
221
|
+
|
|
222
222
|
const rawEvaluate = moduleExports.evaluate as (
|
|
223
223
|
_input: RuleHostInput,
|
|
224
224
|
_helpers: ReturnType<typeof createRuleHostHelpers>
|
|
225
225
|
) => RuleHostResult;
|
|
226
|
-
|
|
226
|
+
|
|
227
227
|
|
|
228
228
|
return {
|
|
229
229
|
implId: impl.id,
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Kept separate to avoid circular dependencies between schema-definitions and migration-runner.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
|
|
8
8
|
/** Minimal interface for better-sqlite3 Database instances. */
|
|
9
9
|
export interface Db {
|
|
@@ -35,7 +35,7 @@ export interface Migration {
|
|
|
35
35
|
name: string;
|
|
36
36
|
/** Which database file this migration applies to */
|
|
37
37
|
db: DbType;
|
|
38
|
-
|
|
38
|
+
|
|
39
39
|
/** Apply this migration */
|
|
40
40
|
up: (_db: Db) => void;
|
|
41
41
|
/** Revert this migration */
|