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.
@@ -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.13.0",
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": "ad52c5fcc9c5",
80
- "bundleMd5": "3dc29cb961c7132e1ad419550686df6c",
81
- "builtAt": "2026-04-10T01:19:54.357Z"
79
+ "gitSha": "a320b37a2418",
80
+ "bundleMd5": "6a5838169d98e4e6419e736dd034d93a",
81
+ "builtAt": "2026-04-09T13:57:36.081Z"
82
82
  }
83
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -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, statSync } from 'fs';
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
- delta: { delta, baselineScore, candidateScore },
759
- benchmarkId,
760
- verdict,
761
- } = benchmarkResult);
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, createPrinciple } from './principle-tree-ledger.js';
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
- * Required XML structure:
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 XML directives found.
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[1];
50
- const body = match[2];
51
- const idMatch = attrs.match(/id="([^"]+)"/i);
52
- const nameMatch = attrs.match(/name="([^"]+)"/i);
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
- directives.push({
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 workspace.
69
- * Falls back to plugin templates if workspace file doesn't exist or has no XML directives.
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 = resolveTemplatePath(language);
89
- if (templatePath) {
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
- const directives = parseThinkingOsMd(content);
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 = resolveTemplatePath('zh');
101
- if (zhPath) {
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
  }