principles-disciple 1.50.0 → 1.52.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.50.0",
5
+ "version": "1.52.0",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.50.0",
3
+ "version": "1.52.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -18,10 +18,20 @@ import { fileURLToPath } from 'url';
18
18
  const __filename = fileURLToPath(import.meta.url);
19
19
  const __dirname = dirname(__filename);
20
20
 
21
+ /**
22
+ * Cross-platform home directory (Linux: HOME, Windows: USERPROFILE/HOMEDRIVE+HOMEPATH)
23
+ */
24
+ function getHomeDir() {
25
+ return process.env.HOME
26
+ || process.env.USERPROFILE
27
+ || (process.env.HOMEDRIVE && process.env.HOMEPATH ? process.env.HOMEDRIVE + process.env.HOMEPATH : null)
28
+ || '.';
29
+ }
30
+
21
31
  // Resolve workspace directory: CLI arg > env var > default
22
32
  const WORKSPACE_DIR = process.argv[2]
23
33
  || process.env.WORKSPACE_DIR
24
- || join(process.env.HOME, '.openclaw', 'workspace-main');
34
+ || join(getHomeDir(), '.openclaw', 'workspace');
25
35
 
26
36
  const STATE_DIR = join(WORKSPACE_DIR, '.state');
27
37
 
@@ -27,8 +27,36 @@ const __filename = fileURLToPath(import.meta.url);
27
27
  const __dirname = dirname(__filename);
28
28
 
29
29
  const SOURCE_DIR = join(__dirname, '..');
30
- const OPENCLAW_DIR = join(process.env.HOME, '.openclaw');
30
+
31
+ /**
32
+ * Cross-platform home directory resolution.
33
+ * Linux/macOS: HOME=/home/user
34
+ * Windows: USERPROFILE=C:\Users\user or HOMEDRIVE/HOMEPATH
35
+ */
36
+ function getHomeDir() {
37
+ return process.env.HOME
38
+ || process.env.USERPROFILE
39
+ || (process.env.HOMEDRIVE && process.env.HOMEPATH ? process.env.HOMEDRIVE + process.env.HOMEPATH : null)
40
+ || '.';
41
+ }
42
+
43
+ const OPENCLAW_DIR = join(getHomeDir(), '.openclaw');
31
44
  const INSTALL_DIR = join(OPENCLAW_DIR, 'extensions', 'principles-disciple');
45
+ function getConfiguredWorkspaceDir() {
46
+ const configPath = join(OPENCLAW_DIR, 'openclaw.json');
47
+ try {
48
+ const raw = readFileSync(configPath, 'utf-8');
49
+ const config = JSON.parse(raw);
50
+ const workspace = config?.agents?.defaults?.workspace;
51
+ if (workspace && existsSync(workspace)) {
52
+ return workspace;
53
+ }
54
+ } catch {
55
+ // Fall through to fallback
56
+ }
57
+ // Fallback: try workspace-main (legacy default)
58
+ return join(OPENCLAW_DIR, 'workspace-main');
59
+ }
32
60
 
33
61
  // Files and directories to sync
34
62
  const SYNC_ITEMS = [
@@ -177,7 +205,11 @@ function checkPrerequisites() {
177
205
  // Check for global package conflicts that cause module resolution traps
178
206
  console.log('šŸ” Checking for global package conflicts...');
179
207
  try {
180
- const globalConflict = execSync('npm list -g principles-disciple --depth=0 2>/dev/null', { encoding: 'utf-8' });
208
+ // Cross-platform: use stdio: 'pipe' to capture stderr, then check output
209
+ const globalConflict = execSync('npm list -g principles-disciple --depth=0', {
210
+ encoding: 'utf-8',
211
+ stdio: ['pipe', 'pipe', 'pipe'] // Capture all streams
212
+ });
181
213
  if (globalConflict.includes('principles-disciple')) {
182
214
  console.error('\nāŒ CONFLICT DETECTED: A version of "principles-disciple" is installed globally via npm.');
183
215
  console.error('This will block OpenClaw from loading the extension version you are trying to install.');
@@ -187,7 +219,17 @@ function checkPrerequisites() {
187
219
  }
188
220
  } catch (e) {
189
221
  // npm list returns non-zero if not found, which is what we want
190
- console.log('āœ… No global package conflicts detected.');
222
+ // Check if the error output contains the package name
223
+ const output = e.stdout || e.stderr || '';
224
+ if (!output.includes('principles-disciple')) {
225
+ console.log('āœ… No global package conflicts detected.');
226
+ } else {
227
+ console.error('\nāŒ CONFLICT DETECTED: A version of "principles-disciple" is installed globally via npm.');
228
+ console.error('This will block OpenClaw from loading the extension version you are trying to install.');
229
+ console.error('\nACTION REQUIRED: Please run the following command first:');
230
+ console.error(' npm uninstall -g principles-disciple\n');
231
+ process.exit(1);
232
+ }
191
233
  }
192
234
  } catch {
193
235
  console.error('āŒ npm not found. Please install Node.js with npm.');
@@ -481,7 +523,7 @@ function verifyInstalledFingerprint() {
481
523
  }
482
524
 
483
525
  /**
484
- * Remove existing installation directory.
526
+ * Remove existing installation directory with Windows-friendly retry logic.
485
527
  */
486
528
  function cleanTargetDir(force) {
487
529
  if (!existsSync(INSTALL_DIR)) return;
@@ -495,7 +537,34 @@ function cleanTargetDir(force) {
495
537
  }
496
538
 
497
539
  console.log('\nšŸ—‘ļø Removing existing installation...');
498
- rmSync(INSTALL_DIR, { recursive: true, force: true });
540
+
541
+ // Windows often returns EPERM due to file locks, add retry logic
542
+ const maxRetries = isWindows() ? 3 : 1;
543
+ let lastError = null;
544
+
545
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
546
+ try {
547
+ rmSync(INSTALL_DIR, { recursive: true, force: true });
548
+ console.log(' āœ… Removed successfully.');
549
+ return;
550
+ } catch (err) {
551
+ lastError = err;
552
+ if (err.code === 'EPERM' && attempt < maxRetries) {
553
+ console.log(` āš ļø Attempt ${attempt}/${maxRetries} failed (EPERM), retrying in 2s...`);
554
+ // Synchronous sleep for retry
555
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2000);
556
+ }
557
+ }
558
+ }
559
+
560
+ // If all retries failed on Windows, try graceful fallback
561
+ if (isWindows() && lastError?.code === 'EPERM') {
562
+ console.log(' āš ļø Windows file lock detected, skipping removal.');
563
+ console.log(' šŸ“ Will overwrite files in place.');
564
+ return; // Continue with overwrite installation
565
+ }
566
+
567
+ throw lastError;
499
568
  }
500
569
 
501
570
  /**
@@ -573,17 +642,115 @@ function cleanStaleBackups() {
573
642
  }
574
643
 
575
644
  /**
576
- * Restart OpenClaw Gateway.
645
+ * Check if running on Windows.
646
+ */
647
+ function isWindows() {
648
+ return process.platform === 'win32';
649
+ }
650
+
651
+ /**
652
+ * Get temporary directory path (cross-platform).
653
+ */
654
+ function getTempDir() {
655
+ if (isWindows()) {
656
+ return process.env.TEMP || process.env.TMP || 'C:\\Windows\\Temp';
657
+ }
658
+ return '/tmp';
659
+ }
660
+
661
+ /**
662
+ * Restart OpenClaw Gateway (cross-platform).
577
663
  */
578
664
  function restartGateway() {
579
665
  console.log('\nšŸ”„ Restarting OpenClaw Gateway...');
666
+
667
+ if (isWindows()) {
668
+ return restartGatewayWindows();
669
+ } else {
670
+ return restartGatewayLinux();
671
+ }
672
+ }
673
+
674
+ /**
675
+ * Restart Gateway on Windows using PowerShell.
676
+ */
677
+ function restartGatewayWindows() {
678
+ const logPath = join(getTempDir(), 'openclaw-auto-restart.log');
679
+
580
680
  try {
681
+ // Step 1: Find and terminate existing gateway processes
682
+ console.log(' Looking for existing gateway processes...');
683
+ try {
684
+ // PowerShell command to find and kill openclaw gateway processes
685
+ // Note: Use single quotes inside -like pattern for proper escaping
686
+ const findCmd = "Get-Process -Name 'node' -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -like '*openclaw*' } | Select-Object -ExpandProperty Id";
687
+ const pids = execSync(`powershell -NoProfile -Command "${findCmd}"`, { encoding: 'utf-8' }).trim();
688
+
689
+ if (pids) {
690
+ console.log(` Terminating existing gateway process(es): ${pids.replace(/\n/g, ', ')}...`);
691
+ // Kill by PID
692
+ const pidList = pids.split('\n').filter(p => p.trim());
693
+ for (const pid of pidList) {
694
+ try {
695
+ execSync(`taskkill /PID ${pid.trim()} /F`, { stdio: 'pipe' });
696
+ } catch { /* ignore if process already gone */ }
697
+ }
698
+ // Wait a moment for process to terminate
699
+ execSync('timeout /t 3 /nobreak > nul', { shell: true, stdio: 'ignore' });
700
+ }
701
+ } catch { /* no existing processes */ }
702
+
703
+ // Step 2: Start new gateway process in background
704
+ console.log(` Starting new gateway (logs: ${logPath})...`);
705
+
706
+ // Use openclaw CLI to start gateway (more reliable than direct node invocation)
707
+ const gatewayCmd = join(getHomeDir(), '.openclaw', 'gateway.cmd');
708
+ const startCmd = `Start-Process -FilePath 'cmd.exe' -ArgumentList '/c ${gatewayCmd}' -WindowStyle Hidden -RedirectStandardOutput '${logPath}' -RedirectStandardError '${join(getTempDir(), 'openclaw-auto-restart.err')}'`;
709
+ execSync(`powershell -NoProfile -Command "${startCmd}"`, { stdio: 'inherit' });
710
+ console.log('āœ… Gateway restart triggered.');
711
+
712
+ // Step 3: Wait and verify
713
+ setTimeout(() => {
714
+ try {
715
+ if (existsSync(logPath)) {
716
+ const logs = readFileSync(logPath, 'utf-8');
717
+ if (logs.includes('Principles Disciple Plugin registered')) {
718
+ console.log('āœ… SUCCESS: Principles Disciple plugin registered successfully!');
719
+ } else if (logs.includes('failed to load') || logs.includes('Error: Cannot find module')) {
720
+ console.error('\nāŒ CRITICAL: Gateway started but PD plugin FAILED to load!');
721
+ console.error(' Check logs at: ' + logPath);
722
+ process.exit(1);
723
+ } else {
724
+ console.warn('āš ļø Gateway started but PD registration not confirmed in recent logs.');
725
+ console.log(' Check logs at: ' + logPath);
726
+ }
727
+ }
728
+ } catch (e) {
729
+ console.warn(`āš ļø Post-restart verification skipped: ${e.message}`);
730
+ }
731
+ }, 8000);
732
+
733
+ } catch (error) {
734
+ console.error(`\nāŒ Failed to restart gateway: ${error.message}`);
735
+ console.error(' You may need to manually restart OpenClaw Gateway.');
736
+ process.exit(1);
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Restart Gateway on Linux using systemctl or process management.
742
+ */
743
+ function restartGatewayLinux() {
744
+ const logPath = '/tmp/openclaw-auto-restart.log';
745
+
746
+ try {
747
+ // Try systemctl first (Linux systemd)
581
748
  try {
582
749
  execSync('systemctl --user is-active openclaw-gateway.service', { stdio: 'pipe' });
583
750
  console.log(' Restarting via systemctl...');
584
751
  execSync('systemctl --user restart openclaw-gateway.service', { stdio: 'inherit' });
585
752
  console.log('āœ… Gateway restarted via systemctl.');
586
-
753
+
587
754
  console.log(' Waiting for Gateway to initialize and load PD plugin (8s)...');
588
755
  setTimeout(() => {
589
756
  try {
@@ -608,8 +775,9 @@ function restartGateway() {
608
775
  }
609
776
  }, 8000);
610
777
  return;
611
- } catch { /* ignore */ }
778
+ } catch { /* systemctl not available, fall through to manual restart */ }
612
779
 
780
+ // Manual process management
613
781
  const pids = execSync('pgrep -f "openclaw-gateway|openclaw gateway"', { encoding: 'utf-8' }).trim();
614
782
  if (pids) {
615
783
  console.log(` Terminating existing gateway process(es)...`);
@@ -617,11 +785,10 @@ function restartGateway() {
617
785
  execSync('sleep 3');
618
786
  }
619
787
 
620
- const logPath = '/tmp/openclaw-auto-restart.log';
621
788
  console.log(` Starting new gateway (logs: ${logPath})...`);
622
789
  execSync(`nohup openclaw gateway --force > ${logPath} 2>&1 &`, { stdio: 'ignore' });
623
790
  console.log('āœ… Gateway restart triggered.');
624
-
791
+
625
792
  setTimeout(() => {
626
793
  if (existsSync(logPath)) {
627
794
  const logs = readFileSync(logPath, 'utf-8');
@@ -699,9 +866,10 @@ function main() {
699
866
  if (existsSync(bootstrapScript)) {
700
867
  console.log('\n🧠 Synchronizing principles to active rules (Bootstrap)...');
701
868
  try {
702
- const targetStateDir = join(process.env.HOME, '.openclaw', 'workspace-main', '.state');
869
+ const workspaceDir = getConfiguredWorkspaceDir();
870
+ const targetStateDir = join(workspaceDir, '.state');
703
871
  if (existsSync(targetStateDir)) {
704
- execSync(`STATE_DIR=${targetStateDir} BOOTSTRAP_LIMIT=100 node scripts/bootstrap-rules.mjs`, { cwd: SOURCE_DIR, stdio: 'inherit' });
872
+ execSync(`node scripts/bootstrap-rules.mjs`, { cwd: SOURCE_DIR, stdio: 'inherit', env: { ...process.env, STATE_DIR: targetStateDir, BOOTSTRAP_LIMIT: '100' } });
705
873
  console.log('āœ… Principles synchronized.');
706
874
  }
707
875
  } catch (e) {
@@ -713,7 +881,8 @@ function main() {
713
881
  if (existsSync(compileScript)) {
714
882
  console.log('\nāš™ļø Compiling pain-derived principles into rules...');
715
883
  try {
716
- const targetWorkspaceDir = join(process.env.HOME, '.openclaw', 'workspace-main');
884
+ const workspaceDir = getConfiguredWorkspaceDir();
885
+ const targetWorkspaceDir = workspaceDir;
717
886
  if (existsSync(targetWorkspaceDir)) {
718
887
  execSync(`node scripts/compile-principles.mjs ${targetWorkspaceDir}`, { cwd: SOURCE_DIR, stdio: 'inherit' });
719
888
  console.log('āœ… Principle compilation complete.');
@@ -732,12 +901,6 @@ function main() {
732
901
  verifyInstalledFingerprint();
733
902
  if (args.dev || args.restart) cleanStaleBackups();
734
903
 
735
- try {
736
- const reloadSignal = join(OPENCLAW_DIR, '.plugin_reload_signal');
737
- writeFileSync(reloadSignal, new Date().toISOString(), 'utf-8');
738
- console.log(`\nšŸ”” Reload signal sent to ${reloadSignal}`);
739
- } catch { /* ignore */ }
740
-
741
904
  console.log('\n╔════════════════════════════════════════════════════════════╗');
742
905
  console.log('ā•‘ āœ… Installation Complete ā•‘');
743
906
  console.log('ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•');
@@ -746,6 +909,7 @@ function main() {
746
909
  restartGateway();
747
910
  } else {
748
911
  console.log('\nšŸ’” Restart OpenClaw Gateway to load the new version.');
912
+ console.log(' (Plugin code changes require a full gateway restart)');
749
913
  }
750
914
  }
751
915
 
package/src/core/init.ts CHANGED
@@ -6,6 +6,7 @@ import { PD_DIRS } from './paths.js';
6
6
  import { defaultContextConfig } from '../types.js';
7
7
  import { loadStore, setPrincipleState, type PrincipleTrainingState } from './principle-training-state.js';
8
8
  import { atomicWriteFileSync } from '../utils/io.js';
9
+ import { createDefaultKeywordStore, saveKeywordStore } from './empathy-keyword-matcher.js';
9
10
 
10
11
  /**
11
12
  * Default PROFILE.json content
@@ -245,6 +246,17 @@ export function ensureStateTemplates(ctx: { logger: PluginLogger }, stateDir: st
245
246
  fs.copyFileSync(dictTemplate, dictDest);
246
247
  ctx.logger.info(`[PD] Initialized pain dictionary in stateDir: ${dictDest} (Lang: ${language})`);
247
248
  }
249
+
250
+ // 3. Initialize empathy keyword store for new users
251
+ // loadKeywordStore() creates the file if missing, but we call it explicitly
252
+ // here so the file exists before any agent/workflow tries to read it
253
+ const empathyFile = path.join(stateDir, 'empathy_keywords.json');
254
+ if (!fs.existsSync(empathyFile)) {
255
+ const lang = language === 'zh' || language === 'en' ? language : 'en';
256
+ const store = createDefaultKeywordStore(lang);
257
+ saveKeywordStore(stateDir, store);
258
+ ctx.logger.info(`[PD] Initialized empathy keyword store in stateDir: ${empathyFile}`);
259
+ }
248
260
  } catch (err) {
249
261
  ctx.logger.error(`[PD] Failed to initialize state templates: ${String(err)}`);
250
262
  }
@@ -144,7 +144,7 @@ function diff(declared: PDTaskSpec[], actual: CronJob[]): DiffAction[] {
144
144
  function buildCronJob(
145
145
  task: PDTaskSpec,
146
146
  nowMs: number,
147
-
147
+ workspaceDir: string,
148
148
  logger?: { info?: (_: string) => void },
149
149
  ): CronJob {
150
150
  logger?.info?.(`[PD:Reconciler] Building cron job: ${task.name} (id=${task.id}, interval=${task.schedule.everyMs}ms)`);
@@ -159,9 +159,7 @@ function buildCronJob(
159
159
  wakeMode: 'now',
160
160
  payload: {
161
161
  kind: 'agentTurn',
162
-
163
-
164
- message: buildTaskPrompt(task, logger),
162
+ message: buildTaskPrompt(task, workspaceDir, logger),
165
163
  lightContext: task.execution.lightContext ?? true,
166
164
  timeoutSeconds: task.execution.timeoutSeconds ?? 120,
167
165
  toolsAllow: task.execution.toolsAllow,
@@ -180,7 +178,12 @@ function buildCronJob(
180
178
  }
181
179
 
182
180
 
183
- function buildTaskPrompt(task: PDTaskSpec, logger?: { info?: (_: string) => void }): string {
181
+ function buildTaskPrompt(task: PDTaskSpec, workspaceDir: string, logger?: { info?: (_: string) => void }): string {
182
+ // Resolve paths dynamically from workspaceDir instead of hardcoding
183
+ const stateDir = path.join(workspaceDir, '.state');
184
+ const empathyKeywordsPath = path.join(stateDir, 'empathy_keywords.json');
185
+ const eventsJsonlPath = path.join(stateDir, 'logs', 'events.jsonl');
186
+
184
187
  if (task.id === 'empathy-optimizer') {
185
188
  logger?.info?.(`[PD:Reconciler] Building empathy optimizer prompt`);
186
189
  return `You are the Principles Disciple Empathy Keyword Optimizer.
@@ -195,7 +198,7 @@ Analyze the current empathy keyword store and recent user message logs to:
195
198
 
196
199
  ### Step 1: Read current keyword store
197
200
  Use read_file to load:
198
- \`~/.openclaw/workspace-main/.state/empathy_keywords.json\`
201
+ \`${empathyKeywordsPath}\`
199
202
 
200
203
  Examine the "terms" object. For each term note:
201
204
  - weight (0.1-0.9): higher = stronger frustration signal
@@ -204,7 +207,7 @@ Examine the "terms" object. For each term note:
204
207
 
205
208
  ### Step 2: Read recent message logs
206
209
  Use search_file_content to scan:
207
- \`~/.openclaw/workspace-main/.state/logs/events.jsonl\`
210
+ \`${eventsJsonlPath}\`
208
211
 
209
212
  Look for user messages containing frustration signals:
210
213
  - Negation: "äøåÆ¹", "错了", "äøč”Œ", "重做"
@@ -214,7 +217,7 @@ Look for user messages containing frustration signals:
214
217
 
215
218
  ### Step 3: Write updated keyword store
216
219
  Use write_file to save the updated store back to:
217
- \`~/.openclaw/workspace-main/.state/empathy_keywords.json\`
220
+ \`${empathyKeywordsPath}\`
218
221
 
219
222
  The file format is:
220
223
  \`\`\`json
@@ -304,7 +307,7 @@ export async function reconcilePDTasks(
304
307
  case 'CREATE':
305
308
  if (action.task) {
306
309
  if (!dryRun) {
307
- const job = buildCronJob(action.task, nowMs, logger);
310
+ const job = buildCronJob(action.task, nowMs, workspaceDir, logger);
308
311
  cronStore.jobs.push(job);
309
312
  logger.info?.(`[PD:Reconciler] Created job: ${action.task.name}`);
310
313
  }
@@ -315,7 +318,7 @@ export async function reconcilePDTasks(
315
318
  if (action.task && action.job) {
316
319
  if (!dryRun) {
317
320
  const idx = cronStore.jobs.indexOf(action.job);
318
- const newJob = buildCronJob(action.task, nowMs, logger);
321
+ const newJob = buildCronJob(action.task, nowMs, workspaceDir, logger);
319
322
  newJob.id = action.job.id;
320
323
  // Preserve original state — only CronService should recalculate nextRunAtMs
321
324
  newJob.state = {
@@ -449,7 +452,7 @@ export async function trigger(
449
452
  existingJob.deleteAfterRun = undefined;
450
453
  } else {
451
454
  log(`Creating new job for manual trigger: ${task.name}`);
452
- const newJob = buildCronJob(task, nowMs, { info: log });
455
+ const newJob = buildCronJob(task, nowMs, workspaceDir, { info: log });
453
456
  newJob.enabled = true;
454
457
  newJob.state.nextRunAtMs = nowMs;
455
458
  cronStore.jobs.push(newJob);
@@ -533,8 +533,11 @@ The empathy observer subagent handles pain detection independently.
533
533
 
534
534
  const empathyEnabled = wctx.config.get('empathy_engine.enabled') !== false;
535
535
  logger?.info?.(`[PD:Empathy] Conditions: enabled=${empathyEnabled}, isUser=${isUserInteraction}, sessionId=${!!sessionId}, api=${!!api}, !agentToAgent=${!isAgentToAgent}, workspaceDir=${!!workspaceDir}, hasMessage=${!!latestUserMessage}`);
536
+
537
+ // Track if we should inject behavioral constraints (will be added to appendSystemContext later)
538
+ let shouldInjectBehavioralConstraints = false;
536
539
  if (empathyEnabled && isUserInteraction && sessionId && api && !isAgentToAgent) {
537
- prependContext = '### BEHAVIORAL_CONSTRAINTS\n' + empathySilenceConstraint + '\n\n' + prependContext;
540
+ shouldInjectBehavioralConstraints = true;
538
541
 
539
542
  // ── Empathy Hybrid Matching (keyword + subagent sampling) ──
540
543
  // Fast keyword scan on every turn, with strategic subagent sampling
@@ -935,9 +938,18 @@ ${taskBlocks}${processingNote}
935
938
  }
936
939
 
937
940
  // Build appendSystemContext with recency effect
938
- // Content order (most important last): project_context -> working_memory -> reflection_log -> thinking_os -> principles
941
+ // Content order (most important last): behavioral_constraints -> project_context -> working_memory -> reflection_log -> thinking_os -> principles
939
942
  const appendParts: string[] = [];
940
943
 
944
+ // 0. Behavioral Constraints (empathy observer coordination)
945
+ // Injected here (appendSystemContext) instead of prependContext to hide from WebUI users.
946
+ // See: https://github.com/csuzngjh/principles/issues/XXX
947
+ if (shouldInjectBehavioralConstraints) {
948
+ appendParts.push(`<behavioral_constraints>
949
+ ${empathySilenceConstraint}
950
+ </behavioral_constraints>`);
951
+ }
952
+
941
953
  // 1. Project Context (lowest priority, goes first)
942
954
  if (projectContextContent) {
943
955
  appendParts.push(`<project_context>\n${projectContextContent}\n</project_context>`);
@@ -1087,6 +1099,7 @@ The sections below are ordered by priority. When conflicts arise, **later sectio
1087
1099
  ---
1088
1100
 
1089
1101
  **怐EXECUTION RULES怑** (Priority: Low → High):
1102
+ - \`<behavioral_constraints>\` - Output format restrictions (hide diagnostic JSON)
1090
1103
  - \`<project_context>\` - Current priorities (can be overridden)
1091
1104
  - \`<reflection_log>\` - Past lessons (inform your approach)
1092
1105
  - \`<thinking_os>\` - Thinking models (guide your reasoning)
@@ -1,9 +1,9 @@
1
1
  /**
2
- * EventLog Auditor — Search and verify events across all .state directories
3
- *
2
+ * EventLog Auditor - Search and verify events across all .state directories
3
+ *
4
4
  * This tool addresses a common debugging issue where hook events may be
5
5
  * written to the wrong .state directory due to workspaceDir resolution bugs.
6
- *
6
+ *
7
7
  * Usage:
8
8
  * const report = await auditEventLogs(openclawDir, ['after_tool_call', 'before_tool_call']);
9
9
  * console.log(report.summary);
@@ -42,7 +42,7 @@ interface AuditReport {
42
42
  */
43
43
  function findEventLogs(baseDir: string, maxDepth = 4): string[] {
44
44
  const results: string[] = [];
45
-
45
+
46
46
  function scan(dir: string, depth: number): void {
47
47
  if (depth > maxDepth) return;
48
48
  try {
@@ -58,37 +58,50 @@ function findEventLogs(baseDir: string, maxDepth = 4): string[] {
58
58
  // Permission denied or directory doesn't exist
59
59
  }
60
60
  }
61
-
61
+
62
62
  scan(baseDir, 0);
63
63
  return results;
64
64
  }
65
65
 
66
66
  /**
67
67
  * Find events.jsonl in well-known locations.
68
+ * Dynamically discovers workspace directories instead of hardcoding names.
68
69
  */
69
70
  function findKnownEventLogPaths(): string[] {
70
71
  const homeDir = os.homedir();
71
72
  const candidates: string[] = [];
72
-
73
- // Common patterns
74
- const patterns = [
73
+
74
+ // Common patterns (legacy, non-workspace paths)
75
+ const legacyPatterns = [
75
76
  path.join(homeDir, '.state', 'logs', 'events.jsonl'),
76
77
  path.join(homeDir, '.openclaw', '.state', 'logs', 'events.jsonl'),
77
- path.join(homeDir, '.openclaw', 'workspace-main', '.state', 'logs', 'events.jsonl'),
78
- path.join(homeDir, '.openclaw', 'workspace-builder', '.state', 'logs', 'events.jsonl'),
79
- path.join(homeDir, '.openclaw', 'workspace-pm', '.state', 'logs', 'events.jsonl'),
80
- path.join(homeDir, '.openclaw', 'workspace-hr', '.state', 'logs', 'events.jsonl'),
81
- path.join(homeDir, '.openclaw', 'workspace-repair', '.state', 'logs', 'events.jsonl'),
82
- path.join(homeDir, '.openclaw', 'workspace-research', '.state', 'logs', 'events.jsonl'),
83
- path.join(homeDir, '.openclaw', 'workspace-scout', '.state', 'logs', 'events.jsonl'),
84
78
  ];
85
-
86
- for (const p of patterns) {
79
+
80
+ for (const p of legacyPatterns) {
87
81
  if (fs.existsSync(p)) {
88
82
  candidates.push(p);
89
83
  }
90
84
  }
91
-
85
+
86
+ // Dynamically discover workspace directories under ~/.openclaw/
87
+ const openclawDir = path.join(homeDir, '.openclaw');
88
+ if (fs.existsSync(openclawDir)) {
89
+ try {
90
+ const entries = fs.readdirSync(openclawDir, { withFileTypes: true });
91
+ for (const entry of entries) {
92
+ if (!entry.isDirectory()) continue;
93
+ // Skip known non-workspace directories
94
+ if (entry.name.startsWith('.') || entry.name === 'extensions' || entry.name === 'memory') continue;
95
+ const eventLogPath = path.join(openclawDir, entry.name, '.state', 'logs', 'events.jsonl');
96
+ if (fs.existsSync(eventLogPath)) {
97
+ candidates.push(eventLogPath);
98
+ }
99
+ }
100
+ } catch {
101
+ // Directory read failed, skip dynamic discovery
102
+ }
103
+ }
104
+
92
105
  return candidates;
93
106
  }
94
107
 
@@ -133,25 +146,25 @@ function countAllHooks(filePath: string): Record<string, number> {
133
146
 
134
147
  /**
135
148
  * Audit all events.jsonl files.
136
- *
149
+ *
137
150
  * @param openclawDir - Base OpenClaw directory (e.g., ~/.openclaw)
138
151
  * @param expectedToolHooks - Hook names that should appear in the primary workspace
139
152
  */
140
-
153
+
141
154
  export async function auditEventLogs(
142
155
  openclawDir: string,
143
156
  expectedToolHooks: string[] = ['before_tool_call', 'after_tool_call'],
144
157
  ): Promise<AuditReport> {
145
158
  const homeDir = os.homedir();
146
-
159
+
147
160
  // Find all event logs
148
161
  const knownPaths = findKnownEventLogPaths();
149
162
  const scannedPaths = findEventLogs(homeDir, 4);
150
163
  const allPaths = [...new Set([...knownPaths, ...scannedPaths])];
151
-
164
+
152
165
  const locations: LocationReport[] = [];
153
166
  let primaryPath: string | null = null;
154
-
167
+
155
168
  for (const filePath of allPaths) {
156
169
  try {
157
170
  const stat = fs.statSync(filePath);
@@ -165,7 +178,7 @@ export async function auditEventLogs(
165
178
  hookCounts: allCounts,
166
179
  recentEntries: recent,
167
180
  });
168
-
181
+
169
182
  // Determine primary path (workspace-main or most recent)
170
183
  if (filePath.includes('workspace-main') || filePath.includes('workspace-main')) {
171
184
  primaryPath = filePath;
@@ -174,7 +187,7 @@ export async function auditEventLogs(
174
187
  // Skip unreadable files
175
188
  }
176
189
  }
177
-
190
+
178
191
  // If no primary found, use most recent
179
192
  if (!primaryPath && locations.length > 0) {
180
193
  locations.sort((a, b) => {
@@ -184,21 +197,21 @@ export async function auditEventLogs(
184
197
  });
185
198
  primaryPath = locations[0].path;
186
199
  }
187
-
200
+
188
201
  // Detect misplaced tool hook events
189
202
  const misplacedEvents: { path: string; entries: EventLogEntry[] }[] = [];
190
203
  for (const loc of locations) {
191
204
  if (loc.path === primaryPath) continue;
192
-
193
- const toolHookEntries = loc.recentEntries.filter(e =>
205
+
206
+ const toolHookEntries = loc.recentEntries.filter(e =>
194
207
  e.type === 'hook_execution' && expectedToolHooks.includes(e.data?.hook as string)
195
208
  );
196
-
209
+
197
210
  if (toolHookEntries.length > 0) {
198
211
  misplacedEvents.push({ path: loc.path, entries: toolHookEntries });
199
212
  }
200
213
  }
201
-
214
+
202
215
  return {
203
216
  searchedPaths: allPaths,
204
217
  locations,
@@ -210,37 +223,37 @@ export async function auditEventLogs(
210
223
  /**
211
224
  * Format audit report for display.
212
225
  */
213
-
226
+
214
227
  export function formatAuditReport(report: AuditReport): string {
215
228
  const lines: string[] = [];
216
-
229
+
217
230
  lines.push('=== Event Log Audit Report ===\n');
218
-
231
+
219
232
  lines.push(`Searched ${report.searchedPaths.length} paths:\n`);
220
233
  for (const p of report.searchedPaths) {
221
234
  lines.push(` ${p}`);
222
235
  }
223
236
  lines.push('');
224
-
237
+
225
238
  lines.push(`Primary: ${report.primaryPath ?? 'NOT FOUND'}\n`);
226
-
239
+
227
240
  for (const loc of report.locations) {
228
241
  const isPrimary = loc.path === report.primaryPath;
229
242
  lines.push(`─── ${isPrimary ? '[PRIMARY]' : '[OTHER] '}${loc.path}`);
230
243
  lines.push(` Last modified: ${loc.lastModified?.toISOString() ?? 'never'}`);
231
244
  lines.push(` Hook counts:`);
232
-
245
+
233
246
  const hooks = Object.entries(loc.hookCounts).sort((a, b) => b[1] - a[1]);
234
247
  for (const [hook, count] of hooks) {
235
248
  lines.push(` ${hook}: ${count}`);
236
249
  }
237
-
250
+
238
251
  if (hooks.length === 0) {
239
252
  lines.push(` (no hooks recorded)`);
240
253
  }
241
254
  lines.push('');
242
255
  }
243
-
256
+
244
257
  if (report.misplacedEvents.length > 0) {
245
258
  lines.push('āš ļø MISPLACED tool hook events detected:');
246
259
  for (const me of report.misplacedEvents) {
@@ -258,6 +271,6 @@ export function formatAuditReport(report: AuditReport): string {
258
271
  } else {
259
272
  lines.push('āœ… No misplaced tool hook events detected.');
260
273
  }
261
-
274
+
262
275
  return lines.join('\n');
263
276
  }