principles-disciple 1.12.0 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/openclaw.plugin.json +4 -4
- package/package.json +1 -1
- package/scripts/sync-plugin.mjs +156 -1
- package/src/commands/nocturnal-train.ts +11 -12
- package/src/core/evolution-reducer.ts +31 -4
- package/src/core/nocturnal-trinity.ts +19 -4
- package/src/core/principle-tree-ledger.ts +27 -7
- package/src/core/principle-tree-migration.ts +195 -0
- package/src/core/thinking-os-parser.ts +36 -44
- package/src/index.ts +7 -3
- package/src/service/nocturnal-service.ts +11 -7
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +18 -3
- package/templates/langs/en/principles/THINKING_OS.md +13 -0
- package/templates/langs/zh/principles/THINKING_OS.md +13 -0
- package/ui/src/i18n/ui.ts +34 -9
- package/ui/src/pages/EvolutionPage.tsx +1 -1
- package/ui/src/pages/ThinkingModelsPage.tsx +287 -69
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.13.0",
|
|
6
6
|
"skills": [
|
|
7
7
|
"./skills"
|
|
8
8
|
],
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
}
|
|
77
77
|
},
|
|
78
78
|
"buildFingerprint": {
|
|
79
|
-
"gitSha": "
|
|
80
|
-
"bundleMd5": "
|
|
81
|
-
"builtAt": "2026-04-
|
|
79
|
+
"gitSha": "ad52c5fcc9c5",
|
|
80
|
+
"bundleMd5": "3dc29cb961c7132e1ad419550686df6c",
|
|
81
|
+
"builtAt": "2026-04-10T01:19:54.357Z"
|
|
82
82
|
}
|
|
83
83
|
}
|
package/package.json
CHANGED
package/scripts/sync-plugin.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* --help Show help message
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { copyFileSync, cpSync, existsSync, rmSync, readFileSync, readFileSync as readFileSyncRaw, mkdirSync, writeFileSync, readdirSync } from 'fs';
|
|
20
|
+
import { copyFileSync, cpSync, existsSync, rmSync, readFileSync, readFileSync as readFileSyncRaw, mkdirSync, writeFileSync, readdirSync, statSync } from 'fs';
|
|
21
21
|
import { createHash } from 'crypto';
|
|
22
22
|
import { join, dirname } from 'path';
|
|
23
23
|
import { fileURLToPath } from 'url';
|
|
@@ -667,6 +667,158 @@ function syncItem(item) {
|
|
|
667
667
|
}
|
|
668
668
|
}
|
|
669
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Sync workspace templates to all workspace directories.
|
|
672
|
+
* This ensures workspaces get the latest template files when the plugin is updated.
|
|
673
|
+
*
|
|
674
|
+
* IMPORTANT SAFETY RULES:
|
|
675
|
+
* - Core files (AGENTS.md, SOUL.md, IDENTITY.md, USER.md, etc.) are NEVER overwritten.
|
|
676
|
+
* They are only copied on first-time workspace creation (file doesn't exist).
|
|
677
|
+
* Users heavily customize these files — overwriting would destroy their work.
|
|
678
|
+
* - Non-core templates (pain_samples, THINKING_OS.md, workspace boilerplate) are synced
|
|
679
|
+
* via MD5 comparison (only update if template content changed and workspace hasn't diverged).
|
|
680
|
+
*
|
|
681
|
+
* Syncs:
|
|
682
|
+
* - templates/workspace/** → workspace root (recursive, skip core files)
|
|
683
|
+
* - templates/langs/{lang}/core/** → workspace root (ONLY if missing)
|
|
684
|
+
* - templates/langs/{lang}/pain/** → .state/pain_samples/
|
|
685
|
+
* - templates/langs/{lang}/principles/THINKING_OS.md → .principles/THINKING_OS.md
|
|
686
|
+
*/
|
|
687
|
+
function syncWorkspaceTemplates(lang) {
|
|
688
|
+
const workspacesRoot = OPENCLAW_DIR;
|
|
689
|
+
if (!existsSync(workspacesRoot)) return;
|
|
690
|
+
|
|
691
|
+
const entries = readdirSync(workspacesRoot);
|
|
692
|
+
const workspaceDirs = entries.filter(e =>
|
|
693
|
+
e.startsWith('workspace-') || e === 'workspace'
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
if (workspaceDirs.length === 0) return;
|
|
697
|
+
|
|
698
|
+
// Core files that should NEVER be overwritten after creation
|
|
699
|
+
const CORE_FILES = new Set([
|
|
700
|
+
'AGENTS.md', 'SOUL.md', 'BOOT.md', 'BOOTSTRAP.md',
|
|
701
|
+
'IDENTITY.md', 'USER.md', 'TOOLS.md', 'HEARTBEAT.md',
|
|
702
|
+
'PRINCIPLES.md', 'PROFILE.json',
|
|
703
|
+
]);
|
|
704
|
+
|
|
705
|
+
let updated = 0;
|
|
706
|
+
|
|
707
|
+
// Helper: compute MD5 of a file
|
|
708
|
+
function md5(filePath) {
|
|
709
|
+
try {
|
|
710
|
+
const content = readFileSyncRaw(filePath);
|
|
711
|
+
return createHash('md5').update(content).digest('hex');
|
|
712
|
+
} catch {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
for (const ws of workspaceDirs) {
|
|
718
|
+
const wsDir = join(workspacesRoot, ws);
|
|
719
|
+
|
|
720
|
+
// 1. templates/workspace/** → workspace root (recursive)
|
|
721
|
+
const workspaceTemplateDir = join(SOURCE_DIR, 'templates', 'workspace');
|
|
722
|
+
if (existsSync(workspaceTemplateDir)) {
|
|
723
|
+
updated += syncDirRecursive(workspaceTemplateDir, wsDir, md5, CORE_FILES);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// 2. templates/langs/{lang}/core/** → workspace root (ONLY if file missing)
|
|
727
|
+
const coreDir = join(SOURCE_DIR, 'templates', 'langs', lang, 'core');
|
|
728
|
+
if (existsSync(coreDir)) {
|
|
729
|
+
updated += syncCoreFiles(coreDir, wsDir);
|
|
730
|
+
} else {
|
|
731
|
+
const zhCoreDir = join(SOURCE_DIR, 'templates', 'langs', 'zh', 'core');
|
|
732
|
+
if (existsSync(zhCoreDir)) {
|
|
733
|
+
updated += syncCoreFiles(zhCoreDir, wsDir);
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// 3. templates/langs/{lang}/pain/** → .state/pain_samples/
|
|
738
|
+
const painSrc = join(SOURCE_DIR, 'templates', 'langs', lang, 'pain');
|
|
739
|
+
const painDest = join(wsDir, '.state', 'pain_samples');
|
|
740
|
+
if (existsSync(painSrc)) {
|
|
741
|
+
updated += syncDirRecursive(painSrc, painDest, md5, CORE_FILES);
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
// 4. templates/langs/{lang}/principles/THINKING_OS.md → .principles/THINKING_OS.md
|
|
745
|
+
const thinkingOsSrc = join(SOURCE_DIR, 'templates', 'langs', lang, 'principles', 'THINKING_OS.md');
|
|
746
|
+
const thinkingOsDest = join(wsDir, '.principles', 'THINKING_OS.md');
|
|
747
|
+
if (existsSync(thinkingOsSrc)) {
|
|
748
|
+
const srcMd5 = md5(thinkingOsSrc);
|
|
749
|
+
const destMd5 = existsSync(thinkingOsDest) ? md5(thinkingOsDest) : null;
|
|
750
|
+
if (srcMd5 !== destMd5) {
|
|
751
|
+
if (!existsSync(join(wsDir, '.principles'))) {
|
|
752
|
+
mkdirSync(join(wsDir, '.principles'), { recursive: true });
|
|
753
|
+
}
|
|
754
|
+
cpSync(thinkingOsSrc, thinkingOsDest, { force: true });
|
|
755
|
+
updated++;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (updated > 0) {
|
|
761
|
+
console.log(` 📄 Workspace templates → ${updated} file(s) synced across ${workspaceDirs.length} workspace(s)`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Sync core files ONLY if they don't exist yet.
|
|
767
|
+
* NEVER overwrites existing core files (user customizations).
|
|
768
|
+
*/
|
|
769
|
+
function syncCoreFiles(srcDir, destDir) {
|
|
770
|
+
let count = 0;
|
|
771
|
+
if (!existsSync(srcDir)) return count;
|
|
772
|
+
|
|
773
|
+
const items = readdirSync(srcDir);
|
|
774
|
+
for (const item of items) {
|
|
775
|
+
const srcPath = join(srcDir, item);
|
|
776
|
+
const destPath = join(destDir, item);
|
|
777
|
+
const stat = statSync(srcPath);
|
|
778
|
+
|
|
779
|
+
if (stat.isDirectory()) continue;
|
|
780
|
+
// Core files: only copy if missing
|
|
781
|
+
if (!existsSync(destPath)) {
|
|
782
|
+
cpSync(srcPath, destPath, { force: false });
|
|
783
|
+
count++;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return count;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Recursively sync source dir to dest dir, skipping core files that already exist.
|
|
791
|
+
* Non-core files: only copy if missing or different (MD5).
|
|
792
|
+
*/
|
|
793
|
+
function syncDirRecursive(srcDir, destDir, md5Fn, coreFiles) {
|
|
794
|
+
let count = 0;
|
|
795
|
+
if (!existsSync(destDir)) {
|
|
796
|
+
mkdirSync(destDir, { recursive: true });
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const items = readdirSync(srcDir);
|
|
800
|
+
for (const item of items) {
|
|
801
|
+
const srcPath = join(srcDir, item);
|
|
802
|
+
const destPath = join(destDir, item);
|
|
803
|
+
const stat = statSync(srcPath);
|
|
804
|
+
|
|
805
|
+
if (stat.isDirectory()) {
|
|
806
|
+
count += syncDirRecursive(srcPath, destPath, md5Fn, coreFiles);
|
|
807
|
+
} else {
|
|
808
|
+
// Skip core files that already exist
|
|
809
|
+
if (coreFiles.has(item) && existsSync(destPath)) continue;
|
|
810
|
+
|
|
811
|
+
const srcMd5 = md5Fn(srcPath);
|
|
812
|
+
const destMd5 = existsSync(destPath) ? md5Fn(destPath) : null;
|
|
813
|
+
if (srcMd5 !== destMd5) {
|
|
814
|
+
cpSync(srcPath, destPath, { force: true });
|
|
815
|
+
count++;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return count;
|
|
820
|
+
}
|
|
821
|
+
|
|
670
822
|
/**
|
|
671
823
|
* Install production dependencies in target directory
|
|
672
824
|
*/
|
|
@@ -825,6 +977,9 @@ function main() {
|
|
|
825
977
|
// Step 7: Sync skills
|
|
826
978
|
syncSkills(args.lang);
|
|
827
979
|
|
|
980
|
+
// Step 7.5: Sync THINKING_OS.md to all workspace .principles/ directories
|
|
981
|
+
syncWorkspaceTemplates(args.lang);
|
|
982
|
+
|
|
828
983
|
// Step 8: Install production dependencies in target (ALWAYS — cleanTargetDir wiped node_modules)
|
|
829
984
|
// --skip-deps only applies to SOURCE directory deps, not the installed plugin.
|
|
830
985
|
installTargetDependencies();
|
|
@@ -270,12 +270,12 @@ Hardware tiers:
|
|
|
270
270
|
// This closes the gap in the create-experiment -> trainer -> import-result chain.
|
|
271
271
|
// NOTE: This blocks until training completes (could be minutes).
|
|
272
272
|
if (runNow) {
|
|
273
|
-
|
|
273
|
+
|
|
274
274
|
const {spec} = createResult;
|
|
275
275
|
const baseDir = TRAINER_SCRIPTS_DIR;
|
|
276
276
|
const scriptPath = path.join(baseDir, 'main.py');
|
|
277
277
|
const specPath = path.join(baseDir, `experiment-${spec.experimentId}.json`);
|
|
278
|
-
|
|
278
|
+
|
|
279
279
|
const {outputDir} = spec;
|
|
280
280
|
const resultFilePath = path.join(outputDir, `result-${spec.experimentId}.json`);
|
|
281
281
|
|
|
@@ -286,7 +286,7 @@ Hardware tiers:
|
|
|
286
286
|
}
|
|
287
287
|
fs.writeFileSync(specPath, JSON.stringify(spec, null, 2), 'utf-8');
|
|
288
288
|
|
|
289
|
-
|
|
289
|
+
|
|
290
290
|
let trainerResult!: import('../core/external-training-contract.js').TrainingExperimentResult;
|
|
291
291
|
|
|
292
292
|
try {
|
|
@@ -394,7 +394,7 @@ Hardware tiers:
|
|
|
394
394
|
|
|
395
395
|
// Process trainer result (register checkpoint)
|
|
396
396
|
// dry_run returns null (no checkpoint); other statuses throw on error
|
|
397
|
-
|
|
397
|
+
|
|
398
398
|
let processed: { checkpointId: string; checkpointRef: string } | null;
|
|
399
399
|
try {
|
|
400
400
|
processed = program.processResult({
|
|
@@ -536,7 +536,7 @@ Next steps:
|
|
|
536
536
|
}
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
539
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Reason: JSON.parse returns dynamic JSON - type unknown at parse time, narrowed via type narrowing below
|
|
540
540
|
let result: any;
|
|
541
541
|
try {
|
|
542
542
|
result = JSON.parse(resultJson);
|
|
@@ -567,7 +567,7 @@ Next steps:
|
|
|
567
567
|
|
|
568
568
|
// Process the result
|
|
569
569
|
const program = new TrainingProgram(workspaceDir);
|
|
570
|
-
|
|
570
|
+
|
|
571
571
|
let processed: { checkpointId: string; checkpointRef: string } | null;
|
|
572
572
|
try {
|
|
573
573
|
processed = program.processResult({
|
|
@@ -754,12 +754,11 @@ Next steps:
|
|
|
754
754
|
};
|
|
755
755
|
}
|
|
756
756
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
verdict = benchmarkResult.verdict;
|
|
757
|
+
({
|
|
758
|
+
delta: { delta, baselineScore, candidateScore },
|
|
759
|
+
benchmarkId,
|
|
760
|
+
verdict,
|
|
761
|
+
} = benchmarkResult);
|
|
763
762
|
} else {
|
|
764
763
|
// Manual mode: require explicit delta and verdict
|
|
765
764
|
if (!deltaArg || !verdictArg) {
|
|
@@ -20,9 +20,10 @@ import type {
|
|
|
20
20
|
PrincipleSuggestedRule,
|
|
21
21
|
} from './evolution-types.js';
|
|
22
22
|
import { isCompleteDetectorMetadata } from './evolution-types.js';
|
|
23
|
-
import { updateTrainingStore } from './principle-tree-ledger.js';
|
|
23
|
+
import { updateTrainingStore, createPrinciple } from './principle-tree-ledger.js';
|
|
24
|
+
import type { LedgerPrinciple } from './principle-tree-ledger.js';
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
export interface EvolutionReducer {
|
|
27
28
|
|
|
28
29
|
emit(_event: EvolutionLoopEvent): void;
|
|
@@ -72,7 +73,7 @@ export interface EvolutionReducer {
|
|
|
72
73
|
lastPromotedAt: string | null;
|
|
73
74
|
};
|
|
74
75
|
}
|
|
75
|
-
|
|
76
|
+
|
|
76
77
|
|
|
77
78
|
const PROBATION_SUCCESS_THRESHOLD = 3;
|
|
78
79
|
const CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
@@ -381,6 +382,7 @@ export class EvolutionReducerImpl implements EvolutionReducer {
|
|
|
381
382
|
}
|
|
382
383
|
|
|
383
384
|
// #204: Write to training store so listEvaluablePrinciples() can find this principle
|
|
385
|
+
// Phase 11: Also write to tree.principles for Rule/Implementation layer support
|
|
384
386
|
if (this.stateDir) {
|
|
385
387
|
try {
|
|
386
388
|
// Determine initial internalization status based on evaluability:
|
|
@@ -404,6 +406,31 @@ export class EvolutionReducerImpl implements EvolutionReducer {
|
|
|
404
406
|
};
|
|
405
407
|
});
|
|
406
408
|
SystemLogger.log(this.workspaceDir, 'TRAINING_STORE_UPDATED', `Principle ${principleId} added to training store with evaluability=${evaluability}, internalizationStatus=${initialStatus}`);
|
|
409
|
+
|
|
410
|
+
// Phase 11: Also create principle in tree.principles for Rule/Implementation layer
|
|
411
|
+
const treePrinciple: LedgerPrinciple = {
|
|
412
|
+
id: principleId,
|
|
413
|
+
version: 1,
|
|
414
|
+
text: principle.text,
|
|
415
|
+
coreAxiomId: principle.coreAxiomId,
|
|
416
|
+
triggerPattern: params.triggerPattern,
|
|
417
|
+
action: params.action,
|
|
418
|
+
status: principle.status,
|
|
419
|
+
priority: principle.priority ?? 'P1',
|
|
420
|
+
scope: principle.scope ?? 'general',
|
|
421
|
+
domain: principle.domain,
|
|
422
|
+
evaluability,
|
|
423
|
+
valueScore: 0,
|
|
424
|
+
adherenceRate: 0,
|
|
425
|
+
painPreventedCount: 0,
|
|
426
|
+
derivedFromPainIds: [params.painId],
|
|
427
|
+
ruleIds: [],
|
|
428
|
+
conflictsWithPrincipleIds: [],
|
|
429
|
+
createdAt: now,
|
|
430
|
+
updatedAt: now,
|
|
431
|
+
};
|
|
432
|
+
createPrinciple(this.stateDir, treePrinciple);
|
|
433
|
+
SystemLogger.log(this.workspaceDir, 'TREE_PRINCIPLE_CREATED', `Principle ${principleId} added to tree.principles`);
|
|
407
434
|
} catch (err) {
|
|
408
435
|
SystemLogger.log(this.workspaceDir, 'TRAINING_STORE_UPDATE_FAILED', `Failed to update training store for ${principleId}: ${String(err)}`);
|
|
409
436
|
}
|
|
@@ -663,7 +690,7 @@ export class EvolutionReducerImpl implements EvolutionReducer {
|
|
|
663
690
|
});
|
|
664
691
|
}
|
|
665
692
|
|
|
666
|
-
|
|
693
|
+
|
|
667
694
|
private onPainDetected(data: PainDetectedData, _eventTs: string): void {
|
|
668
695
|
const trigger = String(data.reason ?? data.source ?? 'unknown trigger');
|
|
669
696
|
|
|
@@ -1360,14 +1360,19 @@ export async function runTrinityAsync(options: RunTrinityOptions): Promise<Trini
|
|
|
1360
1360
|
|
|
1361
1361
|
try {
|
|
1362
1362
|
// Step 1: Dreamer — generate candidates via real subagent
|
|
1363
|
+
const dreamerStart = Date.now();
|
|
1364
|
+
console.log(`[Trinity] Step 1/3: Invoking Dreamer (principleId=${principleId}, maxCandidates=${config.maxCandidates})`);
|
|
1363
1365
|
const dreamerOutput = await adapter.invokeDreamer(snapshot, principleId, config.maxCandidates);
|
|
1366
|
+
console.log(`[Trinity] Dreamer completed in ${Date.now() - dreamerStart}ms, valid=${dreamerOutput.valid}, candidates=${dreamerOutput.candidates?.length ?? 0}`);
|
|
1364
1367
|
|
|
1365
1368
|
if (!dreamerOutput.valid || dreamerOutput.candidates.length === 0) {
|
|
1369
|
+
const reason = dreamerOutput.reason ?? 'No valid candidates generated';
|
|
1370
|
+
console.warn(`[Trinity] Dreamer failed: ${reason}`);
|
|
1366
1371
|
failures.push({
|
|
1367
1372
|
stage: 'dreamer',
|
|
1368
|
-
reason
|
|
1373
|
+
reason,
|
|
1369
1374
|
});
|
|
1370
|
-
telemetry.stageFailures.push(`Dreamer: ${
|
|
1375
|
+
telemetry.stageFailures.push(`Dreamer: ${reason}`);
|
|
1371
1376
|
return { success: false, telemetry, failures, fallbackOccurred: false };
|
|
1372
1377
|
}
|
|
1373
1378
|
|
|
@@ -1375,20 +1380,27 @@ export async function runTrinityAsync(options: RunTrinityOptions): Promise<Trini
|
|
|
1375
1380
|
telemetry.candidateCount = dreamerOutput.candidates.length;
|
|
1376
1381
|
|
|
1377
1382
|
// Step 2: Philosopher — rank candidates via real subagent
|
|
1383
|
+
const philosopherStart = Date.now();
|
|
1384
|
+
console.log(`[Trinity] Step 2/3: Invoking Philosopher (${dreamerOutput.candidates.length} candidates)`);
|
|
1378
1385
|
const philosopherOutput = await adapter.invokePhilosopher(dreamerOutput, principleId);
|
|
1386
|
+
console.log(`[Trinity] Philosopher completed in ${Date.now() - philosopherStart}ms, valid=${philosopherOutput.valid}, judgments=${philosopherOutput.judgments?.length ?? 0}`);
|
|
1379
1387
|
|
|
1380
1388
|
if (!philosopherOutput.valid || philosopherOutput.judgments.length === 0) {
|
|
1389
|
+
const reason = philosopherOutput.reason ?? 'No judgments produced';
|
|
1390
|
+
console.warn(`[Trinity] Philosopher failed: ${reason}`);
|
|
1381
1391
|
failures.push({
|
|
1382
1392
|
stage: 'philosopher',
|
|
1383
|
-
reason
|
|
1393
|
+
reason,
|
|
1384
1394
|
});
|
|
1385
|
-
telemetry.stageFailures.push(`Philosopher: ${
|
|
1395
|
+
telemetry.stageFailures.push(`Philosopher: ${reason}`);
|
|
1386
1396
|
return { success: false, telemetry, failures, fallbackOccurred: false };
|
|
1387
1397
|
}
|
|
1388
1398
|
|
|
1389
1399
|
telemetry.philosopherPassed = true;
|
|
1390
1400
|
|
|
1391
1401
|
// Step 3: Scribe — synthesize final artifact via real subagent
|
|
1402
|
+
const scribeStart = Date.now();
|
|
1403
|
+
console.log(`[Trinity] Step 3/3: Invoking Scribe (synthesizing artifact)`);
|
|
1392
1404
|
const draftArtifact = await adapter.invokeScribe(
|
|
1393
1405
|
dreamerOutput,
|
|
1394
1406
|
philosopherOutput,
|
|
@@ -1397,8 +1409,10 @@ export async function runTrinityAsync(options: RunTrinityOptions): Promise<Trini
|
|
|
1397
1409
|
telemetry,
|
|
1398
1410
|
config
|
|
1399
1411
|
);
|
|
1412
|
+
console.log(`[Trinity] Scribe completed in ${Date.now() - scribeStart}ms, artifact=${!!draftArtifact}`);
|
|
1400
1413
|
|
|
1401
1414
|
if (!draftArtifact) {
|
|
1415
|
+
console.warn(`[Trinity] Scribe failed: Failed to synthesize artifact from candidates`);
|
|
1402
1416
|
failures.push({ stage: 'scribe', reason: 'Failed to synthesize artifact from candidates' });
|
|
1403
1417
|
telemetry.stageFailures.push('Scribe: synthesis failed');
|
|
1404
1418
|
return { success: false, telemetry, failures, fallbackOccurred: false };
|
|
@@ -1406,6 +1420,7 @@ export async function runTrinityAsync(options: RunTrinityOptions): Promise<Trini
|
|
|
1406
1420
|
|
|
1407
1421
|
telemetry.scribePassed = true;
|
|
1408
1422
|
telemetry.selectedCandidateIndex = draftArtifact.selectedCandidateIndex;
|
|
1423
|
+
console.log(`[Trinity] Trinity chain completed successfully: selectedCandidateIndex=${draftArtifact.selectedCandidateIndex}`);
|
|
1409
1424
|
|
|
1410
1425
|
if (draftArtifact.telemetry) {
|
|
1411
1426
|
telemetry.tournamentTrace = draftArtifact.telemetry.tournamentTrace;
|
|
@@ -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) => {
|
|
@@ -371,6 +371,26 @@ export function updateTrainingStore(
|
|
|
371
371
|
});
|
|
372
372
|
}
|
|
373
373
|
|
|
374
|
+
export function createPrinciple(stateDir: string, principle: LedgerPrinciple): LedgerPrinciple {
|
|
375
|
+
return mutateLedger(stateDir, (store) => {
|
|
376
|
+
const existing = store.tree.principles[principle.id];
|
|
377
|
+
if (existing) {
|
|
378
|
+
// Principle already exists, return existing (idempotent)
|
|
379
|
+
return existing;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const nextPrinciple: LedgerPrinciple = {
|
|
383
|
+
...principle,
|
|
384
|
+
ruleIds: uniqueStrings(principle.ruleIds ?? []),
|
|
385
|
+
conflictsWithPrincipleIds: uniqueStrings(principle.conflictsWithPrincipleIds ?? []),
|
|
386
|
+
derivedFromPainIds: uniqueStrings(principle.derivedFromPainIds ?? []),
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
store.tree.principles[principle.id] = nextPrinciple;
|
|
390
|
+
return nextPrinciple;
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
374
394
|
export function createRule(stateDir: string, rule: LedgerRule): LedgerRule {
|
|
375
395
|
return mutateLedger(stateDir, (store) => {
|
|
376
396
|
const principle = store.tree.principles[rule.principleId];
|