principles-disciple 1.13.0 → 1.14.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 +1 -156
- package/src/commands/nocturnal-train.ts +12 -11
- package/src/core/evolution-reducer.ts +4 -31
- package/src/core/nocturnal-trinity.ts +4 -19
- package/src/core/principle-tree-ledger.ts +7 -27
- package/src/core/thinking-os-parser.ts +44 -36
- package/src/index.ts +3 -7
- package/src/service/nocturnal-service.ts +7 -11
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +3 -18
- package/templates/langs/en/principles/THINKING_OS.md +0 -13
- package/templates/langs/zh/principles/THINKING_OS.md +0 -13
- package/ui/src/i18n/ui.ts +52 -0
- package/ui/src/pages/EvolutionPage.tsx +38 -57
- package/ui/src/pages/FeedbackPage.tsx +0 -2
- package/ui/src/pages/GateMonitorPage.tsx +3 -3
- package/ui/src/pages/LoginPage.tsx +2 -1
- package/ui/src/pages/OverviewPage.tsx +10 -9
- package/ui/src/pages/SamplesPage.tsx +3 -3
- package/ui/src/pages/ThinkingModelsPage.tsx +444 -95
- package/ui/src/styles.css +316 -0
- package/src/core/principle-tree-migration.ts +0 -195
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.14.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": "a320b37a2418",
|
|
80
|
+
"bundleMd5": "6a5838169d98e4e6419e736dd034d93a",
|
|
81
|
+
"builtAt": "2026-04-09T13:57:36.081Z"
|
|
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
|
|
20
|
+
import { copyFileSync, cpSync, existsSync, rmSync, readFileSync, readFileSync as readFileSyncRaw, mkdirSync, writeFileSync, readdirSync } from 'fs';
|
|
21
21
|
import { createHash } from 'crypto';
|
|
22
22
|
import { join, dirname } from 'path';
|
|
23
23
|
import { fileURLToPath } from 'url';
|
|
@@ -667,158 +667,6 @@ 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
|
-
|
|
822
670
|
/**
|
|
823
671
|
* Install production dependencies in target directory
|
|
824
672
|
*/
|
|
@@ -977,9 +825,6 @@ function main() {
|
|
|
977
825
|
// Step 7: Sync skills
|
|
978
826
|
syncSkills(args.lang);
|
|
979
827
|
|
|
980
|
-
// Step 7.5: Sync THINKING_OS.md to all workspace .principles/ directories
|
|
981
|
-
syncWorkspaceTemplates(args.lang);
|
|
982
|
-
|
|
983
828
|
// Step 8: Install production dependencies in target (ALWAYS — cleanTargetDir wiped node_modules)
|
|
984
829
|
// --skip-deps only applies to SOURCE directory deps, not the installed plugin.
|
|
985
830
|
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
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow -- Reason: shadowing is intentional - inner block scoping for trainer execution
|
|
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
|
+
// eslint-disable-next-line @typescript-eslint/no-shadow -- Reason: shadowing is intentional - inner block scoping for trainer output directory
|
|
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
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations, @typescript-eslint/consistent-type-imports -- Reason: type assertion required - trainer result type from external contract module
|
|
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
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned in try block immediately after declaration
|
|
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/no-explicit-any -- Reason: JSON.parse returns dynamic JSON - type unknown at parse time, narrowed via type narrowing below
|
|
539
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations, @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
|
+
// eslint-disable-next-line @typescript-eslint/init-declarations -- Reason: assigned in try block immediately after declaration
|
|
571
571
|
let processed: { checkpointId: string; checkpointRef: string } | null;
|
|
572
572
|
try {
|
|
573
573
|
processed = program.processResult({
|
|
@@ -754,11 +754,12 @@ Next steps:
|
|
|
754
754
|
};
|
|
755
755
|
}
|
|
756
756
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
757
|
+
// Destructure benchmark result - delta property contains the actual delta value
|
|
758
|
+
delta = benchmarkResult.delta.delta;
|
|
759
|
+
baselineScore = benchmarkResult.delta.baselineScore;
|
|
760
|
+
candidateScore = benchmarkResult.delta.candidateScore;
|
|
761
|
+
benchmarkId = benchmarkResult.benchmarkId;
|
|
762
|
+
verdict = benchmarkResult.verdict;
|
|
762
763
|
} else {
|
|
763
764
|
// Manual mode: require explicit delta and verdict
|
|
764
765
|
if (!deltaArg || !verdictArg) {
|
|
@@ -20,10 +20,9 @@ import type {
|
|
|
20
20
|
PrincipleSuggestedRule,
|
|
21
21
|
} from './evolution-types.js';
|
|
22
22
|
import { isCompleteDetectorMetadata } from './evolution-types.js';
|
|
23
|
-
import { updateTrainingStore
|
|
24
|
-
import type { LedgerPrinciple } from './principle-tree-ledger.js';
|
|
23
|
+
import { updateTrainingStore } from './principle-tree-ledger.js';
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
/* eslint-disable no-unused-vars -- Reason: interface method params are type signatures, implementations use actual values */
|
|
27
26
|
export interface EvolutionReducer {
|
|
28
27
|
|
|
29
28
|
emit(_event: EvolutionLoopEvent): void;
|
|
@@ -73,7 +72,7 @@ export interface EvolutionReducer {
|
|
|
73
72
|
lastPromotedAt: string | null;
|
|
74
73
|
};
|
|
75
74
|
}
|
|
76
|
-
|
|
75
|
+
/* eslint-enable no-unused-vars */
|
|
77
76
|
|
|
78
77
|
const PROBATION_SUCCESS_THRESHOLD = 3;
|
|
79
78
|
const CIRCUIT_BREAKER_THRESHOLD = 3;
|
|
@@ -382,7 +381,6 @@ export class EvolutionReducerImpl implements EvolutionReducer {
|
|
|
382
381
|
}
|
|
383
382
|
|
|
384
383
|
// #204: Write to training store so listEvaluablePrinciples() can find this principle
|
|
385
|
-
// Phase 11: Also write to tree.principles for Rule/Implementation layer support
|
|
386
384
|
if (this.stateDir) {
|
|
387
385
|
try {
|
|
388
386
|
// Determine initial internalization status based on evaluability:
|
|
@@ -406,31 +404,6 @@ export class EvolutionReducerImpl implements EvolutionReducer {
|
|
|
406
404
|
};
|
|
407
405
|
});
|
|
408
406
|
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`);
|
|
434
407
|
} catch (err) {
|
|
435
408
|
SystemLogger.log(this.workspaceDir, 'TRAINING_STORE_UPDATE_FAILED', `Failed to update training store for ${principleId}: ${String(err)}`);
|
|
436
409
|
}
|
|
@@ -690,7 +663,7 @@ export class EvolutionReducerImpl implements EvolutionReducer {
|
|
|
690
663
|
});
|
|
691
664
|
}
|
|
692
665
|
|
|
693
|
-
|
|
666
|
+
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars -- Reason: event timestamp intentionally unused, only data payload matters
|
|
694
667
|
private onPainDetected(data: PainDetectedData, _eventTs: string): void {
|
|
695
668
|
const trigger = String(data.reason ?? data.source ?? 'unknown trigger');
|
|
696
669
|
|
|
@@ -1360,19 +1360,14 @@ 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})`);
|
|
1365
1363
|
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}`);
|
|
1367
1364
|
|
|
1368
1365
|
if (!dreamerOutput.valid || dreamerOutput.candidates.length === 0) {
|
|
1369
|
-
const reason = dreamerOutput.reason ?? 'No valid candidates generated';
|
|
1370
|
-
console.warn(`[Trinity] Dreamer failed: ${reason}`);
|
|
1371
1366
|
failures.push({
|
|
1372
1367
|
stage: 'dreamer',
|
|
1373
|
-
reason,
|
|
1368
|
+
reason: dreamerOutput.reason ?? 'No valid candidates generated',
|
|
1374
1369
|
});
|
|
1375
|
-
telemetry.stageFailures.push(`Dreamer: ${reason}`);
|
|
1370
|
+
telemetry.stageFailures.push(`Dreamer: ${dreamerOutput.reason ?? 'failed'}`);
|
|
1376
1371
|
return { success: false, telemetry, failures, fallbackOccurred: false };
|
|
1377
1372
|
}
|
|
1378
1373
|
|
|
@@ -1380,27 +1375,20 @@ export async function runTrinityAsync(options: RunTrinityOptions): Promise<Trini
|
|
|
1380
1375
|
telemetry.candidateCount = dreamerOutput.candidates.length;
|
|
1381
1376
|
|
|
1382
1377
|
// 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)`);
|
|
1385
1378
|
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}`);
|
|
1387
1379
|
|
|
1388
1380
|
if (!philosopherOutput.valid || philosopherOutput.judgments.length === 0) {
|
|
1389
|
-
const reason = philosopherOutput.reason ?? 'No judgments produced';
|
|
1390
|
-
console.warn(`[Trinity] Philosopher failed: ${reason}`);
|
|
1391
1381
|
failures.push({
|
|
1392
1382
|
stage: 'philosopher',
|
|
1393
|
-
reason,
|
|
1383
|
+
reason: philosopherOutput.reason ?? 'No judgments produced',
|
|
1394
1384
|
});
|
|
1395
|
-
telemetry.stageFailures.push(`Philosopher: ${reason}`);
|
|
1385
|
+
telemetry.stageFailures.push(`Philosopher: ${philosopherOutput.reason ?? 'failed'}`);
|
|
1396
1386
|
return { success: false, telemetry, failures, fallbackOccurred: false };
|
|
1397
1387
|
}
|
|
1398
1388
|
|
|
1399
1389
|
telemetry.philosopherPassed = true;
|
|
1400
1390
|
|
|
1401
1391
|
// 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)`);
|
|
1404
1392
|
const draftArtifact = await adapter.invokeScribe(
|
|
1405
1393
|
dreamerOutput,
|
|
1406
1394
|
philosopherOutput,
|
|
@@ -1409,10 +1397,8 @@ export async function runTrinityAsync(options: RunTrinityOptions): Promise<Trini
|
|
|
1409
1397
|
telemetry,
|
|
1410
1398
|
config
|
|
1411
1399
|
);
|
|
1412
|
-
console.log(`[Trinity] Scribe completed in ${Date.now() - scribeStart}ms, artifact=${!!draftArtifact}`);
|
|
1413
1400
|
|
|
1414
1401
|
if (!draftArtifact) {
|
|
1415
|
-
console.warn(`[Trinity] Scribe failed: Failed to synthesize artifact from candidates`);
|
|
1416
1402
|
failures.push({ stage: 'scribe', reason: 'Failed to synthesize artifact from candidates' });
|
|
1417
1403
|
telemetry.stageFailures.push('Scribe: synthesis failed');
|
|
1418
1404
|
return { success: false, telemetry, failures, fallbackOccurred: false };
|
|
@@ -1420,7 +1406,6 @@ export async function runTrinityAsync(options: RunTrinityOptions): Promise<Trini
|
|
|
1420
1406
|
|
|
1421
1407
|
telemetry.scribePassed = true;
|
|
1422
1408
|
telemetry.selectedCandidateIndex = draftArtifact.selectedCandidateIndex;
|
|
1423
|
-
console.log(`[Trinity] Trinity chain completed successfully: selectedCandidateIndex=${draftArtifact.selectedCandidateIndex}`);
|
|
1424
1409
|
|
|
1425
1410
|
if (draftArtifact.telemetry) {
|
|
1426
1411
|
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
|
+
/* eslint-disable @typescript-eslint/max-params -- Reason: Clamp function requires all parameters for safe numeric conversion */
|
|
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
|
+
/* eslint-disable @typescript-eslint/max-params -- Reason: Clamp function requires all parameters for safe numeric conversion */
|
|
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
|
+
/* eslint-disable no-unused-vars -- Reason: callback parameter used via closure in inner function */
|
|
319
319
|
function mutateLedger<T>(stateDir: string, mutate: (store: HybridLedgerStore) => T): T {
|
|
320
|
-
|
|
320
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later but called in this helper for consistency
|
|
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
|
+
/* eslint-disable no-unused-vars -- Reason: callback parameter used via closure in inner function */
|
|
331
331
|
async function mutateLedgerAsync<T>(stateDir: string, mutate: (store: HybridLedgerStore) => Promise<T>): Promise<T> {
|
|
332
|
-
|
|
332
|
+
// eslint-disable-next-line @typescript-eslint/no-use-before-define -- Reason: function is defined later but called in this helper for consistency
|
|
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
|
+
/* eslint-disable no-unused-vars -- Reason: callback parameter is forwarded to mutateLedger callback */
|
|
367
367
|
mutate: (store: LegacyPrincipleTrainingStore) => void,
|
|
368
368
|
): void {
|
|
369
369
|
mutateLedger(stateDir, (store) => {
|
|
@@ -371,26 +371,6 @@ 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
|
-
|
|
394
374
|
export function createRule(stateDir: string, rule: LedgerRule): LedgerRule {
|
|
395
375
|
return mutateLedger(stateDir, (store) => {
|
|
396
376
|
const principle = store.tree.principles[rule.principleId];
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Parses THINKING_OS.md to extract directive definitions.
|
|
5
5
|
* THINKING_OS.md is the single source of truth for thinking models.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* XML structure:
|
|
8
8
|
* <directive id="T-01" name="MAP_BEFORE_TERRITORY">
|
|
9
9
|
* <trigger>...</trigger>
|
|
10
10
|
* <must>...</must>
|
|
@@ -37,42 +37,47 @@ function extractTag(content: string, tagName: string): string {
|
|
|
37
37
|
|
|
38
38
|
/**
|
|
39
39
|
* Parse THINKING_OS.md content and extract all <directive> blocks.
|
|
40
|
-
* Returns empty array if no
|
|
40
|
+
* Returns empty array if no directives found.
|
|
41
41
|
*/
|
|
42
42
|
export function parseThinkingOsMd(content: string): ThinkingOsDirective[] {
|
|
43
43
|
const directives: ThinkingOsDirective[] = [];
|
|
44
|
+
|
|
45
|
+
// Match all <directive ...> ... </directive> blocks
|
|
44
46
|
const directiveRegex = /<directive\s+([^>]*)>([\s\S]*?)<\/directive>/gi;
|
|
45
|
-
|
|
47
|
+
/* eslint-disable @typescript-eslint/init-declarations, @typescript-eslint/no-use-before-define, @typescript-eslint/prefer-destructuring, no-useless-assignment, @typescript-eslint/no-unused-vars */
|
|
46
48
|
let match: RegExpExecArray | null = null;
|
|
47
49
|
|
|
48
50
|
while ((match = directiveRegex.exec(content)) !== null) {
|
|
49
|
-
const attrs = match
|
|
50
|
-
|
|
51
|
-
const idMatch =
|
|
52
|
-
const nameMatch =
|
|
51
|
+
const [, attrs, body] = match;
|
|
52
|
+
|
|
53
|
+
const idMatch = /id="([^"]+)"/i.exec(attrs);
|
|
54
|
+
const nameMatch = /name="([^"]+)"/i.exec(attrs);
|
|
55
|
+
|
|
53
56
|
if (!idMatch) continue;
|
|
54
57
|
|
|
55
|
-
|
|
58
|
+
const directive: ThinkingOsDirective = {
|
|
56
59
|
id: idMatch[1],
|
|
57
60
|
name: nameMatch ? nameMatch[1] : '',
|
|
58
61
|
trigger: extractTag(body, 'trigger'),
|
|
59
62
|
must: extractTag(body, 'must'),
|
|
60
63
|
forbidden: extractTag(body, 'forbidden'),
|
|
61
|
-
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
directives.push(directive);
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
return directives;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
/**
|
|
68
|
-
* Load THINKING_OS.md from the
|
|
69
|
-
* Falls back to
|
|
73
|
+
* Load THINKING_OS.md from the plugin templates for a given language.
|
|
74
|
+
* Falls back to the workspace THINKING_OS.md if it exists.
|
|
70
75
|
*/
|
|
71
76
|
export function loadThinkingOsFromWorkspace(
|
|
72
77
|
workspaceDir: string,
|
|
73
78
|
language = 'zh',
|
|
74
79
|
): ThinkingOsDirective[] {
|
|
75
|
-
// Priority 1: workspace THINKING_OS.md
|
|
80
|
+
// Priority 1: workspace's own THINKING_OS.md
|
|
76
81
|
const workspacePath = resolvePdPath(workspaceDir, 'THINKING_OS');
|
|
77
82
|
if (fs.existsSync(workspacePath)) {
|
|
78
83
|
try {
|
|
@@ -84,21 +89,39 @@ export function loadThinkingOsFromWorkspace(
|
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
|
|
92
|
+
// ES Module compatible __dirname (must be inside function for bundler)
|
|
93
|
+
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
94
|
+
|
|
87
95
|
// Priority 2: plugin template for the given language
|
|
88
|
-
const templatePath =
|
|
89
|
-
|
|
96
|
+
const templatePath = path.join(
|
|
97
|
+
path.dirname(path.dirname(path.dirname(currentDir))),
|
|
98
|
+
'templates',
|
|
99
|
+
'langs',
|
|
100
|
+
language,
|
|
101
|
+
'principles',
|
|
102
|
+
'THINKING_OS.md',
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
if (fs.existsSync(templatePath)) {
|
|
90
106
|
try {
|
|
91
107
|
const content = fs.readFileSync(templatePath, 'utf-8');
|
|
92
|
-
|
|
93
|
-
if (directives.length > 0) return directives;
|
|
108
|
+
return parseThinkingOsMd(content);
|
|
94
109
|
} catch {
|
|
95
110
|
// Fall through to zh template
|
|
96
111
|
}
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
// Priority 3: zh template as ultimate fallback
|
|
100
|
-
const zhPath =
|
|
101
|
-
|
|
115
|
+
const zhPath = path.join(
|
|
116
|
+
path.dirname(path.dirname(path.dirname(currentDir))),
|
|
117
|
+
'templates',
|
|
118
|
+
'langs',
|
|
119
|
+
'zh',
|
|
120
|
+
'principles',
|
|
121
|
+
'THINKING_OS.md',
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (fs.existsSync(zhPath)) {
|
|
102
125
|
try {
|
|
103
126
|
const content = fs.readFileSync(zhPath, 'utf-8');
|
|
104
127
|
return parseThinkingOsMd(content);
|
|
@@ -110,22 +133,6 @@ export function loadThinkingOsFromWorkspace(
|
|
|
110
133
|
return [];
|
|
111
134
|
}
|
|
112
135
|
|
|
113
|
-
/**
|
|
114
|
-
* Resolve the THINKING_OS.md template path for a given language.
|
|
115
|
-
*/
|
|
116
|
-
function resolveTemplatePath(language: string): string | null {
|
|
117
|
-
const currentDir = path.dirname(fileURLToPath(import.meta.url));
|
|
118
|
-
const templatePath = path.join(
|
|
119
|
-
path.dirname(path.dirname(path.dirname(currentDir))),
|
|
120
|
-
'templates',
|
|
121
|
-
'langs',
|
|
122
|
-
language,
|
|
123
|
-
'principles',
|
|
124
|
-
'THINKING_OS.md',
|
|
125
|
-
);
|
|
126
|
-
return fs.existsSync(templatePath) ? templatePath : null;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
136
|
/**
|
|
130
137
|
* Extract meaningful detection keywords from a trigger string.
|
|
131
138
|
* Returns an array of regex patterns.
|
|
@@ -135,14 +142,14 @@ export function generateDetectionPatterns(trigger: string): RegExp[] {
|
|
|
135
142
|
|
|
136
143
|
const patterns: string[] = [];
|
|
137
144
|
|
|
138
|
-
// Extract Chinese phrases: 3-8 character sequences
|
|
145
|
+
// Extract Chinese phrases: 3-8 character sequences that are meaningful
|
|
139
146
|
const chinesePattern = /[\u4e00-\u9fff]{3,8}/g;
|
|
140
147
|
const chineseMatches = trigger.match(chinesePattern) ?? [];
|
|
141
148
|
for (const phrase of chineseMatches) {
|
|
142
149
|
patterns.push(phrase);
|
|
143
150
|
}
|
|
144
151
|
|
|
145
|
-
// Extract English words/phrases
|
|
152
|
+
// Extract English words/phrases: sequences of letters
|
|
146
153
|
const englishPattern = /[a-zA-Z]{3,20}(?:\s+[a-zA-Z]{3,20}){0,3}/g;
|
|
147
154
|
const englishMatches = trigger.match(englishPattern) ?? [];
|
|
148
155
|
for (const phrase of englishMatches) {
|
|
@@ -152,5 +159,6 @@ export function generateDetectionPatterns(trigger: string): RegExp[] {
|
|
|
152
159
|
}
|
|
153
160
|
}
|
|
154
161
|
|
|
162
|
+
// Convert to case-insensitive regexes
|
|
155
163
|
return patterns.map(p => new RegExp(p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i'));
|
|
156
164
|
}
|