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
|
@@ -12,15 +12,15 @@
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
export enum EvolutionTier {
|
|
15
|
-
|
|
15
|
+
|
|
16
16
|
Seed = 1, // 起步:150行 + 3文件 + 子智能体(现代 AI 能力已足够强)
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
Sprout = 2, // 成长:300行 + 5文件
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
Sapling = 3, // 独当:500行 + 10文件 + 风险路径
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
Tree = 4, // 专家:1000行 + 20文件
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
Forest = 5 // 大师:完全自主
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -403,7 +403,7 @@ export function computeConfigFingerprint(config: Partial<TrainingHyperparameters
|
|
|
403
403
|
* If the file cannot be read, falls back to path+count hash (legacy behavior).
|
|
404
404
|
*/
|
|
405
405
|
export function computeDatasetFingerprint(exportPath: string, sampleCount: number): string {
|
|
406
|
-
|
|
406
|
+
|
|
407
407
|
let contentHash: string;
|
|
408
408
|
try {
|
|
409
409
|
const content = fs.readFileSync(exportPath, 'utf-8');
|
|
@@ -428,7 +428,7 @@ export function extractWorkingMemory(
|
|
|
428
428
|
toolUse.name === 'write_file' || toolUse.name === 'create_file' ? 'created' : 'modified';
|
|
429
429
|
|
|
430
430
|
// 尝试从文本中提取描述
|
|
431
|
-
|
|
431
|
+
|
|
432
432
|
const description = extractDescription(text, filePath);
|
|
433
433
|
|
|
434
434
|
snapshot.artifacts.push({
|
|
@@ -443,20 +443,20 @@ export function extractWorkingMemory(
|
|
|
443
443
|
if (!text) continue;
|
|
444
444
|
|
|
445
445
|
// 从文本中提取文件操作(备用方式)
|
|
446
|
-
|
|
446
|
+
|
|
447
447
|
extractFileArtifacts(text, snapshot.artifacts, workspaceDir);
|
|
448
448
|
|
|
449
449
|
// 提取问题
|
|
450
|
-
|
|
450
|
+
|
|
451
451
|
extractProblems(text, snapshot.activeProblems);
|
|
452
452
|
|
|
453
453
|
// 提取下一步
|
|
454
|
-
|
|
454
|
+
|
|
455
455
|
extractNextActions(text, snapshot.nextActions);
|
|
456
456
|
}
|
|
457
457
|
|
|
458
458
|
// 去重和限制数量
|
|
459
|
-
|
|
459
|
+
|
|
460
460
|
snapshot.artifacts = deduplicateArtifacts(snapshot.artifacts).slice(-MAX_ARTIFACTS);
|
|
461
461
|
snapshot.activeProblems = snapshot.activeProblems.slice(-MAX_PROBLEMS);
|
|
462
462
|
snapshot.nextActions = snapshot.nextActions.slice(-MAX_NEXT_ACTIONS);
|
|
@@ -476,7 +476,7 @@ function extractFileArtifacts(
|
|
|
476
476
|
// 格式: file_path: "/path/to/file" 或 absolute_path: "/path/to/file"
|
|
477
477
|
const filePathRegex = /(?:file_path|absolute_path)["']?\s*[:=]\s*["']([^"']+\.(ts|js|json|md|yaml|yml|py|sh|mjs|cjs))["']/gi;
|
|
478
478
|
|
|
479
|
-
|
|
479
|
+
|
|
480
480
|
let match;
|
|
481
481
|
while ((match = filePathRegex.exec(text)) !== null) {
|
|
482
482
|
const [, filePath] = match;
|
|
@@ -506,7 +506,7 @@ function extractFileArtifacts(
|
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
// 尝试提取描述(从附近的文本)
|
|
509
|
-
|
|
509
|
+
|
|
510
510
|
const description = extractDescription(text, filePath);
|
|
511
511
|
|
|
512
512
|
artifacts.push({
|
|
@@ -539,7 +539,7 @@ function extractFileArtifacts(
|
|
|
539
539
|
continue;
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
-
|
|
542
|
+
|
|
543
543
|
const description = extractDescription(text, filePath);
|
|
544
544
|
|
|
545
545
|
artifacts.push({
|
|
@@ -587,7 +587,7 @@ function extractProblems(
|
|
|
587
587
|
): void {
|
|
588
588
|
// 问题模式(匹配问题描述)
|
|
589
589
|
const problemPattern = /(?:问题|problem|error|错误|失败|failed)[::]\s*([^\n]{5,100})/gi;
|
|
590
|
-
|
|
590
|
+
|
|
591
591
|
let match;
|
|
592
592
|
while ((match = problemPattern.exec(text)) !== null) {
|
|
593
593
|
const content = match[1].trim();
|
|
@@ -630,7 +630,7 @@ function extractNextActions(text: string, actions: string[]): void {
|
|
|
630
630
|
];
|
|
631
631
|
|
|
632
632
|
for (const pattern of patterns) {
|
|
633
|
-
|
|
633
|
+
|
|
634
634
|
let match;
|
|
635
635
|
while ((match = pattern.exec(text)) !== null) {
|
|
636
636
|
const action = match[1].trim();
|
|
@@ -689,7 +689,7 @@ export function parseWorkingMemorySection(content: string): WorkingMemorySnapsho
|
|
|
689
689
|
// 解析文件记录表格
|
|
690
690
|
// | 文件路径 | 操作 | 描述 |
|
|
691
691
|
const tableRegex = /\|\s*`?([^`|\n]+)`?\s*\|\s*(created|modified|deleted)\s*\|\s*([^|\n]*)\s*\|/gi;
|
|
692
|
-
|
|
692
|
+
|
|
693
693
|
let match;
|
|
694
694
|
while ((match = tableRegex.exec(wmContent)) !== null) {
|
|
695
695
|
snapshot.artifacts.push({
|
|
@@ -728,7 +728,7 @@ export function mergeWorkingMemory(content: string, snapshot: WorkingMemorySnaps
|
|
|
728
728
|
const wmIndex = content.indexOf(WORKING_MEMORY_SECTION);
|
|
729
729
|
|
|
730
730
|
// 生成 Working Memory 章节
|
|
731
|
-
|
|
731
|
+
|
|
732
732
|
const wmSection = generateWorkingMemorySection(snapshot);
|
|
733
733
|
|
|
734
734
|
if (wmIndex === -1) {
|
|
@@ -1405,9 +1405,9 @@ export function recoverFromTemplate(
|
|
|
1405
1405
|
export function safeReadCurrentFocus(
|
|
1406
1406
|
focusPath: string,
|
|
1407
1407
|
extensionRoot: string,
|
|
1408
|
-
|
|
1408
|
+
|
|
1409
1409
|
logger?: { warn?: (msg: string) => void; info?: (msg: string) => void }
|
|
1410
|
-
|
|
1410
|
+
|
|
1411
1411
|
): {
|
|
1412
1412
|
content: string;
|
|
1413
1413
|
recovered: boolean;
|
|
@@ -46,7 +46,7 @@ export class HygieneTracker {
|
|
|
46
46
|
const backupPath = `${this.statsFile}.bak`;
|
|
47
47
|
fs.renameSync(this.statsFile, backupPath);
|
|
48
48
|
this.logger?.warn(`[PD] Corrupted hygiene stats backed up to ${backupPath}`);
|
|
49
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Reason: catch parameter intentionally unused - error handling only
|
|
50
50
|
} catch (_renameErr) {
|
|
51
51
|
// Empty - corrupted stats backup is non-fatal
|
|
52
52
|
}
|
package/src/core/init.ts
CHANGED
|
@@ -43,7 +43,7 @@ export function ensureWorkspaceTemplates(api: OpenClawPluginApi, workspaceDir: s
|
|
|
43
43
|
const commonTemplatesDir = path.resolve(__dirname, '..', '..', 'templates', 'workspace');
|
|
44
44
|
if (fs.existsSync(commonTemplatesDir)) {
|
|
45
45
|
api.logger.info(`[PD] Syncing workspace templates: ${workspaceDir}...`);
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
copyRecursiveSync(commonTemplatesDir, workspaceDir, api);
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -85,7 +85,7 @@ export function ensureWorkspaceTemplates(api: OpenClawPluginApi, workspaceDir: s
|
|
|
85
85
|
if (!fs.existsSync(painDestDir)) {
|
|
86
86
|
fs.mkdirSync(painDestDir, { recursive: true });
|
|
87
87
|
}
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
copyRecursiveSync(painTemplatesDir, painDestDir, api);
|
|
90
90
|
}
|
|
91
91
|
|
|
@@ -249,7 +249,7 @@ function writeRegistry(stateDir: string, registry: ModelDeploymentRegistry): voi
|
|
|
249
249
|
/**
|
|
250
250
|
* Execute a read-modify-write under an exclusive file lock.
|
|
251
251
|
*/
|
|
252
|
-
|
|
252
|
+
|
|
253
253
|
function withDeploymentRegistryLock<T>(
|
|
254
254
|
stateDir: string,
|
|
255
255
|
fn: (_registry: ModelDeploymentRegistry) => T
|
|
@@ -349,7 +349,7 @@ export function assertPromotionGatePassed(stateDir: string, checkpointId: string
|
|
|
349
349
|
* @throws Error if checkpoint is not found or not deployable
|
|
350
350
|
* @throws Error if checkpoint's targetModelFamily violates profile constraints
|
|
351
351
|
*/
|
|
352
|
-
|
|
352
|
+
|
|
353
353
|
export function bindCheckpointToWorkerProfile(
|
|
354
354
|
stateDir: string,
|
|
355
355
|
workerProfile: WorkerProfile,
|
|
@@ -249,7 +249,7 @@ function writeRegistry(stateDir: string, registry: ModelTrainingRegistry): void
|
|
|
249
249
|
*/
|
|
250
250
|
function withRegistryLock<T>(
|
|
251
251
|
stateDir: string,
|
|
252
|
-
|
|
252
|
+
|
|
253
253
|
fn: (_: ModelTrainingRegistry) => T
|
|
254
254
|
): T {
|
|
255
255
|
const registryPath = getRegistryPath(stateDir);
|
|
@@ -321,7 +321,7 @@ export function registerTrainingRun(
|
|
|
321
321
|
*
|
|
322
322
|
* @throws Error if run not found or transition is invalid
|
|
323
323
|
*/
|
|
324
|
-
|
|
324
|
+
|
|
325
325
|
export function updateTrainingRunStatus(
|
|
326
326
|
stateDir: string,
|
|
327
327
|
trainRunId: string,
|
|
@@ -689,7 +689,7 @@ export function parseAndValidateArtifact(
|
|
|
689
689
|
options: ArbiterOptions = {}
|
|
690
690
|
): ArbiterResult {
|
|
691
691
|
// Step 1: Parse JSON
|
|
692
|
-
|
|
692
|
+
|
|
693
693
|
let parsed: unknown;
|
|
694
694
|
try {
|
|
695
695
|
parsed = JSON.parse(jsonString);
|
|
@@ -220,8 +220,8 @@ export function shouldRunArtificer(
|
|
|
220
220
|
|
|
221
221
|
const signalCount =
|
|
222
222
|
snapshot.stats.totalPainEvents +
|
|
223
|
-
snapshot.stats.totalGateBlocks +
|
|
224
|
-
snapshot.stats.failureCount;
|
|
223
|
+
(snapshot.stats.totalGateBlocks ?? 0) +
|
|
224
|
+
(snapshot.stats.failureCount ?? 0);
|
|
225
225
|
|
|
226
226
|
return signalCount >= minimumSignalCount;
|
|
227
227
|
}
|
|
@@ -241,7 +241,7 @@ export function checkThresholds(
|
|
|
241
241
|
* @param weights - Scoring weights
|
|
242
242
|
* @returns All scored and ranked candidates
|
|
243
243
|
*/
|
|
244
|
-
|
|
244
|
+
|
|
245
245
|
export function rankCandidates(
|
|
246
246
|
candidates: DreamerCandidate[],
|
|
247
247
|
judgments: PhilosopherJudgment[],
|
|
@@ -328,7 +328,7 @@ export function rankCandidates(
|
|
|
328
328
|
* @param weights - Scoring weights
|
|
329
329
|
* @returns Tournament result with winner
|
|
330
330
|
*/
|
|
331
|
-
|
|
331
|
+
|
|
332
332
|
export function runTournament(
|
|
333
333
|
candidates: DreamerCandidate[],
|
|
334
334
|
judgments: PhilosopherJudgment[],
|
|
@@ -239,7 +239,7 @@ export function detectOpportunity(principleId: string, session: SessionEvents):
|
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
// T-xx principles — specific deterministic detection
|
|
242
|
-
|
|
242
|
+
|
|
243
243
|
switch (principleId) {
|
|
244
244
|
case 'T-01':
|
|
245
245
|
return detectT01Opportunity(session);
|
|
@@ -537,7 +537,7 @@ export function detectViolation(principleId: string, session: SessionEvents): Vi
|
|
|
537
537
|
}
|
|
538
538
|
|
|
539
539
|
// T-xx principles — specific deterministic detection
|
|
540
|
-
|
|
540
|
+
|
|
541
541
|
switch (principleId) {
|
|
542
542
|
case 'T-01':
|
|
543
543
|
return detectT01Violation(session);
|
|
@@ -558,6 +558,7 @@ export function detectViolation(principleId: string, session: SessionEvents): Vi
|
|
|
558
558
|
case 'T-09':
|
|
559
559
|
return detectT09Violation(session);
|
|
560
560
|
default:
|
|
561
|
+
console.warn(`[PD:Compliance] Unknown principle ID: ${principleId} — treating as no violation. Check for typos (P-001 vs P_001).`);
|
|
561
562
|
return { violated: false, reason: `Unknown principle: ${principleId}` };
|
|
562
563
|
}
|
|
563
564
|
}
|
|
@@ -986,7 +987,7 @@ function computeViolationTrend(
|
|
|
986
987
|
/**
|
|
987
988
|
* Builds a human-readable explanation for the compliance result.
|
|
988
989
|
*/
|
|
989
|
-
|
|
990
|
+
|
|
990
991
|
function buildExplanation(
|
|
991
992
|
principleId: string,
|
|
992
993
|
applicableOpportunityCount: number,
|
|
@@ -259,7 +259,7 @@ function writeRegistry(workspaceDir: string, records: NocturnalDatasetRecord[]):
|
|
|
259
259
|
* Execute a read-modify-write on the registry under an exclusive lock.
|
|
260
260
|
* This prevents concurrent writers from racing on the same file.
|
|
261
261
|
*/
|
|
262
|
-
|
|
262
|
+
|
|
263
263
|
function withRegistryLock<T>(workspaceDir: string, fn: (_records: NocturnalDatasetRecord[]) => T): T {
|
|
264
264
|
const registryPath = getRegistryPath(workspaceDir);
|
|
265
265
|
return withLock(registryPath, () => {
|
|
@@ -283,7 +283,7 @@ function withRegistryLock<T>(workspaceDir: string, fn: (_records: NocturnalDatas
|
|
|
283
283
|
* @param classification - Optional replay classification
|
|
284
284
|
* @returns RegisterSampleResult
|
|
285
285
|
*/
|
|
286
|
-
|
|
286
|
+
|
|
287
287
|
export function registerSample(
|
|
288
288
|
workspaceDir: string,
|
|
289
289
|
artifact: NocturnalArtifact,
|
|
@@ -425,7 +425,7 @@ const VALID_TRANSITIONS: Record<NocturnalReviewStatus, NocturnalReviewStatus[]>
|
|
|
425
425
|
* @returns Updated record, or null if not found
|
|
426
426
|
* @throws Error if transition is invalid
|
|
427
427
|
*/
|
|
428
|
-
|
|
428
|
+
|
|
429
429
|
export function updateReviewStatus(
|
|
430
430
|
workspaceDir: string,
|
|
431
431
|
sampleFingerprint: string,
|
|
@@ -136,7 +136,7 @@ function computeDatasetFingerprint(sampleFingerprints: string[]): string {
|
|
|
136
136
|
* Serialize a single dataset record + artifact to ORPO JSONL line.
|
|
137
137
|
* Caller guarantees record.targetModelFamily is non-null.
|
|
138
138
|
*/
|
|
139
|
-
|
|
139
|
+
|
|
140
140
|
function serializeORPOSample(
|
|
141
141
|
record: NocturnalDatasetRecord,
|
|
142
142
|
artifact: ReturnType<typeof readDatasetArtifact>,
|
|
@@ -183,7 +183,7 @@ function serializeORPOSample(
|
|
|
183
183
|
export function exportORPOSamples(
|
|
184
184
|
workspaceDir: string,
|
|
185
185
|
targetModelFamily?: string | null,
|
|
186
|
-
_options: Record<string, never> = {}
|
|
186
|
+
_options: Record<string, never> = {}
|
|
187
187
|
): ExportResult {
|
|
188
188
|
const exportId = crypto.randomUUID();
|
|
189
189
|
const now = new Date().toISOString();
|
|
@@ -195,7 +195,7 @@ export function exportORPOSamples(
|
|
|
195
195
|
reviewStatus: 'approved_for_training',
|
|
196
196
|
});
|
|
197
197
|
|
|
198
|
-
|
|
198
|
+
|
|
199
199
|
let eligibleRecords: typeof allApprovedRecords;
|
|
200
200
|
|
|
201
201
|
if (targetModelFamily !== undefined && targetModelFamily !== null) {
|
|
@@ -244,7 +244,7 @@ export function exportORPOSamples(
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
// Read artifact (throws on error — distinguishes read failure from missing artifact)
|
|
247
|
-
|
|
247
|
+
|
|
248
248
|
let artifact;
|
|
249
249
|
try {
|
|
250
250
|
artifact = readDatasetArtifact(workspaceDir, record.sampleFingerprint);
|
|
@@ -201,7 +201,7 @@ export function validateRuleImplementationCandidate(
|
|
|
201
201
|
try {
|
|
202
202
|
const moduleExports = loadRuleImplementationModule(normalizedSource, 'nocturnal-candidate.js') as {
|
|
203
203
|
meta?: unknown;
|
|
204
|
-
|
|
204
|
+
|
|
205
205
|
evaluate?: (_input: RuleHostInput, _helpers: RuleHostHelpers) => unknown;
|
|
206
206
|
};
|
|
207
207
|
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { NocturnalSessionSnapshot } from './nocturnal-trajectory-extractor.js';
|
|
2
|
+
|
|
3
|
+
export interface NocturnalSnapshotContractResult {
|
|
4
|
+
status: 'valid' | 'invalid';
|
|
5
|
+
reasons: string[];
|
|
6
|
+
snapshot?: NocturnalSessionSnapshot;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
10
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
14
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isNumberOrNull(value: unknown): value is number | null {
|
|
18
|
+
return value === null || (typeof value === 'number' && Number.isFinite(value));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateNocturnalSnapshotIngress(
|
|
22
|
+
value: unknown
|
|
23
|
+
): NocturnalSnapshotContractResult {
|
|
24
|
+
const reasons: string[] = [];
|
|
25
|
+
|
|
26
|
+
if (!isObjectRecord(value)) {
|
|
27
|
+
return { status: 'invalid', reasons: ['snapshot must be an object'] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!isNonEmptyString(value.sessionId)) {
|
|
31
|
+
reasons.push('snapshot.sessionId must be a non-empty string');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!isNonEmptyString(value.startedAt)) {
|
|
35
|
+
reasons.push('snapshot.startedAt must be a non-empty string');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!isNonEmptyString(value.updatedAt)) {
|
|
39
|
+
reasons.push('snapshot.updatedAt must be a non-empty string');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const arrayFields = [
|
|
43
|
+
'assistantTurns',
|
|
44
|
+
'userTurns',
|
|
45
|
+
'toolCalls',
|
|
46
|
+
'painEvents',
|
|
47
|
+
'gateBlocks',
|
|
48
|
+
] as const;
|
|
49
|
+
for (const field of arrayFields) {
|
|
50
|
+
if (!Array.isArray(value[field])) {
|
|
51
|
+
reasons.push(`snapshot.${field} must be an array`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const stats = value.stats;
|
|
56
|
+
if (!isObjectRecord(stats)) {
|
|
57
|
+
reasons.push('snapshot.stats must be an object');
|
|
58
|
+
} else {
|
|
59
|
+
if (!isNumberOrNull(stats.totalAssistantTurns)) {
|
|
60
|
+
reasons.push('snapshot.stats.totalAssistantTurns must be a number or null');
|
|
61
|
+
}
|
|
62
|
+
if (!isNumberOrNull(stats.totalToolCalls)) {
|
|
63
|
+
reasons.push('snapshot.stats.totalToolCalls must be a number or null');
|
|
64
|
+
}
|
|
65
|
+
if (typeof stats.totalPainEvents !== 'number' || !Number.isFinite(stats.totalPainEvents)) {
|
|
66
|
+
reasons.push('snapshot.stats.totalPainEvents must be a finite number');
|
|
67
|
+
}
|
|
68
|
+
if (!isNumberOrNull(stats.totalGateBlocks)) {
|
|
69
|
+
reasons.push('snapshot.stats.totalGateBlocks must be a number or null');
|
|
70
|
+
}
|
|
71
|
+
if (!isNumberOrNull(stats.failureCount)) {
|
|
72
|
+
reasons.push('snapshot.stats.failureCount must be a number or null');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const isFallback = value._dataSource === 'pain_context_fallback';
|
|
77
|
+
if (value._dataSource !== undefined && !isFallback) {
|
|
78
|
+
reasons.push('snapshot._dataSource must be omitted or pain_context_fallback');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isFallback && isObjectRecord(stats) && Array.isArray(value.painEvents)) {
|
|
82
|
+
const hasPainSignal = value.painEvents.length > 0 || ((stats.totalPainEvents as number) > 0);
|
|
83
|
+
if (!hasPainSignal) {
|
|
84
|
+
reasons.push('fallback snapshot must contain at least one pain signal');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!isFallback && isObjectRecord(stats)) {
|
|
89
|
+
if (stats.totalAssistantTurns === null) {
|
|
90
|
+
reasons.push('non-fallback snapshot.stats.totalAssistantTurns must be a number');
|
|
91
|
+
}
|
|
92
|
+
if (stats.totalToolCalls === null) {
|
|
93
|
+
reasons.push('non-fallback snapshot.stats.totalToolCalls must be a number');
|
|
94
|
+
}
|
|
95
|
+
if (stats.totalGateBlocks === null) {
|
|
96
|
+
reasons.push('non-fallback snapshot.stats.totalGateBlocks must be a number');
|
|
97
|
+
}
|
|
98
|
+
if (stats.failureCount === null) {
|
|
99
|
+
reasons.push('non-fallback snapshot.stats.failureCount must be a number');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (reasons.length > 0) {
|
|
104
|
+
return { status: 'invalid', reasons };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
status: 'valid',
|
|
109
|
+
reasons: [],
|
|
110
|
+
snapshot: value as unknown as NocturnalSessionSnapshot,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -116,13 +116,15 @@ export interface NocturnalSessionSnapshot {
|
|
|
116
116
|
gateBlocks: NocturnalGateBlock[];
|
|
117
117
|
/**
|
|
118
118
|
* Summary statistics for quick triage.
|
|
119
|
+
* When _dataSource is 'pain_context_fallback', these fields are null
|
|
120
|
+
* to distinguish "no data" from "data is zero".
|
|
119
121
|
*/
|
|
120
122
|
stats: {
|
|
121
|
-
totalAssistantTurns: number;
|
|
122
|
-
totalToolCalls: number;
|
|
123
|
+
totalAssistantTurns: number | null;
|
|
124
|
+
totalToolCalls: number | null;
|
|
123
125
|
totalPainEvents: number;
|
|
124
|
-
totalGateBlocks: number;
|
|
125
|
-
failureCount: number;
|
|
126
|
+
totalGateBlocks: number | null;
|
|
127
|
+
failureCount: number | null;
|
|
126
128
|
};
|
|
127
129
|
/**
|
|
128
130
|
* #219: Marker for data source to identify fallback/partial stats.
|
|
@@ -364,7 +366,7 @@ export class NocturnalTrajectoryExtractor {
|
|
|
364
366
|
* const sessions = extractor.listRecentNocturnalCandidateSessions({ limit: 10 });
|
|
365
367
|
* const snapshot = extractor.getNocturnalSessionSnapshot(sessionId);
|
|
366
368
|
*/
|
|
367
|
-
|
|
369
|
+
|
|
368
370
|
export function createNocturnalTrajectoryExtractor(
|
|
369
371
|
workspaceDir: string,
|
|
370
372
|
_stateDir?: string
|