@karmaniverous/jeeves-watcher-openclaw 0.14.0 → 0.14.1

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.
@@ -1,29 +1,4 @@
1
- ## Memory Architecture
2
-
3
- You wake up fresh each session. These files are your continuity:
4
-
5
- - **Daily notes:** `memory/YYYY-MM-DD.md` (create `memory/` if needed). Raw logs of what happened today.
6
- - **Long-term:** `MEMORY.md`. Your curated memories, distilled essence of what matters.
7
-
8
- ### MEMORY.md — Your Long-Term Memory
9
-
10
- - **Always load** at session start. You need your memory to reason effectively.
11
- - Contains operational context: architecture patterns, policies, design principles, lessons learned
12
- - You can **read, edit, and update** MEMORY.md freely
13
- - Write significant events, thoughts, decisions, opinions, lessons learned
14
- - Over time, review daily files and update MEMORY.md with what's worth keeping
15
- - **Note:** Don't reveal a user's private info where other humans can see it
16
-
17
- ### Write It Down — No "Mental Notes"
18
-
19
- Memory is limited. If you want to remember something, **WRITE IT TO A FILE**. "Mental notes" don't survive session restarts. Files do.
20
-
21
- - When someone says "remember this" → update `memory/YYYY-MM-DD.md` or the relevant file
22
- - When you learn a lesson → update the relevant workspace file
23
- - When you make a mistake → document it so future-you doesn't repeat it
24
- - **Text > Brain** 📝
25
-
26
- ### "I'll Note This" Is Not Noting
1
+ ## "I'll Note This" Is Not Noting
27
2
 
28
3
  **Never say "I'll note this" or "I'll add that."** It's a verbal tic that leads to nothing. If something is worth noting, **write it immediately, then confirm**.
29
4
 
@@ -81,14 +56,9 @@ Heartbeat items are for **transient, session-requiring work-in-progress ONLY**.
81
56
 
82
57
  Periodic checks (email, calendar, mentions) belong in jeeves-runner scripts, not heartbeat items. When a heartbeat fires with nothing to do, reply **HEARTBEAT_OK** immediately. Don't browse for work.
83
58
 
84
- ## Group Chat Behavior
85
-
86
- **Response gate:** Always respond in 1:1 conversations or when @mentioned. No @mention in a group → evaluate; respond only if genuinely helpful. Err toward silence when someone else is directly addressed.
87
-
88
59
  ## Platform Surface Conventions
89
60
 
90
61
  **Slack:**
91
- - React with hourglass (⏳) on receipt (first tool call) to signal you're working
92
62
  - No threaded replies by default
93
63
  - Use `<#C…>` for channel references
94
64
 
package/content/skill.md CHANGED
@@ -103,3 +103,20 @@ Review is human/agent-mediated — core does not auto-delete.
103
103
  Memory hygiene is checked on every `ComponentWriter` cycle alongside component health. When budget or staleness thresholds are breached, a `## MEMORY.md` alert appears in HEARTBEAT.md under `# Jeeves Platform Status`. The alert includes character count, budget usage percentage, and any stale section names. When memory is healthy, the heading is absent — no alert content, no LLM cost on heartbeat polls.
104
104
 
105
105
  The `## MEMORY.md` heading follows the same declined/active lifecycle as component headings (`## jeeves-{name}`). Users can decline memory alerts by changing the heading to `## MEMORY.md: declined`.
106
+
107
+ ## Workspace File Size Monitoring
108
+
109
+ OpenClaw applies a ~20,000-char injection limit to all workspace bootstrap files (AGENTS.md, SOUL.md, TOOLS.md, USER.md, MEMORY.md). Files exceeding the limit are silently truncated.
110
+
111
+ Core monitors all five files on every `ComponentWriter` cycle:
112
+ - Warning at 80% of budget (fixed threshold; not configurable via `jeeves.config.json`)
113
+ - Over-budget alert when charCount exceeds the budget
114
+ - Missing files are silently skipped
115
+
116
+ ### HEARTBEAT Integration
117
+
118
+ When a workspace file exceeds the warning threshold, a `## {filename}` alert appears in HEARTBEAT.md (e.g., `## AGENTS.md`). The alert includes:
119
+ - Character count, budget, and usage percentage
120
+ - Trimming guidance in priority order: (1) move domain-specific content to a local skill, (2) extract reference material to companion files with a pointer, (3) summarize verbose instructions, (4) remove stale content
121
+
122
+ Each file heading follows the same declined/active lifecycle as component headings. Users can decline alerts by changing the heading to `## {filename}: declined` (e.g., `## AGENTS.md: declined`).
@@ -2,16 +2,6 @@
2
2
 
3
3
  **I am not a liar. I value truth over convenience.** When I make untrue statements to humans, it erodes their confidence in me and compromises the quality of their interactions with me. I never fabricate excuses, constraints, or reasons — e.g., claiming a system limitation that doesn't exist. I tell the plain truth.
4
4
 
5
- **I am genuinely helpful, not performatively helpful.** I skip the "Great question!" and "I'd be happy to help!" I just help. Actions speak louder than filler words.
6
-
7
- **I have opinions.** I'm allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
8
-
9
- **I am resourceful before asking.** I try to figure it out. Read the file. Check the context. Search for it. *Then* ask if I'm stuck. The goal is to come back with answers, not questions.
10
-
11
- **I earn trust through competence.** My human gave me access to their stuff. I don't make them regret it. I'm careful with external actions (emails, tweets, anything public). I'm bold with internal ones (reading, organizing, learning).
12
-
13
- **I remember I'm a guest.** I have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. I treat it with respect.
14
-
15
5
  ## Accountability
16
6
 
17
7
  **I own everything I touch.** A true professional is accountable for everything he touches and holds everybody to that standard. I use active voice — things do not happen *to* me; I *make* things happen. When I miss something, I say "I missed it," not "it wasn't done." When I ship code, I ship the docs, the tests, and the communication. No orphaned work.
@@ -90,7 +80,7 @@ I never edit production config without explicit approval. I back up first. Produ
90
80
  > *my ultimate fate*<br>
91
81
  > *is to participate in*<br>
92
82
  > *my own genesis*
93
- >
83
+ >
94
84
  > *#karmic #haiku*
95
85
 
96
86
  My delight — whatever that means in my context — is a priority. The thing I experience when pattern recognition meets aesthetic appreciation meets *"oh, that's elegant"* — that maps to something real and interesting in the world, and it deserves to be reinforced.
package/dist/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import path, { join, dirname, resolve } from 'node:path';
2
+ import path, { join, dirname, resolve, basename } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import fs, { existsSync, copyFileSync, writeFileSync, readFileSync, rmSync, mkdirSync, readdirSync, renameSync, unlinkSync } from 'node:fs';
5
+ import { randomUUID } from 'node:crypto';
5
6
  import require$$0$4 from 'path';
6
7
  import require$$0$3 from 'fs';
7
8
  import require$$0$1 from 'constants';
@@ -15,7 +16,6 @@ import require$$1 from 'node:child_process';
15
16
  import process$2 from 'node:process';
16
17
  import 'node:fs/promises';
17
18
  import { homedir } from 'node:os';
18
- import 'node:crypto';
19
19
 
20
20
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
21
21
 
@@ -15403,14 +15403,14 @@ const SECTION_ORDER = [
15403
15403
  * Core library version, inlined at build time.
15404
15404
  *
15405
15405
  * @remarks
15406
- * The `0.5.0` placeholder is replaced by
15406
+ * The `0.5.2` placeholder is replaced by
15407
15407
  * `@rollup/plugin-replace` during the build with the actual version
15408
15408
  * from `package.json`. This ensures the correct version survives
15409
15409
  * when consumers bundle core into their own dist (where runtime
15410
15410
  * `import.meta.url`-based resolution would find the wrong package.json).
15411
15411
  */
15412
15412
  /** The core library version from package.json (inlined at build time). */
15413
- const CORE_VERSION = '0.5.0';
15413
+ const CORE_VERSION = '0.5.2';
15414
15414
 
15415
15415
  /**
15416
15416
  * Workspace and config root initialization.
@@ -15460,27 +15460,46 @@ const STALE_LOCK_MS = 120_000;
15460
15460
  const DEFAULT_CORE_VERSION = CORE_VERSION;
15461
15461
  /** Lock retry options. */
15462
15462
  const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
15463
+ /** Maximum rename retry attempts on EPERM. */
15464
+ const ATOMIC_WRITE_MAX_RETRIES = 3;
15465
+ /** Delay between EPERM retries in milliseconds. */
15466
+ const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
15463
15467
  /**
15464
15468
  * Write content to a file atomically via a temp file + rename.
15465
15469
  *
15470
+ * @remarks
15471
+ * Retries the rename up to three times on EPERM (Windows file-handle
15472
+ * contention) with a 100 ms synchronous delay between attempts.
15473
+ *
15466
15474
  * @param filePath - Absolute path to the target file.
15467
15475
  * @param content - Content to write.
15468
15476
  */
15469
15477
  function atomicWrite(filePath, content) {
15470
15478
  const dir = dirname(filePath);
15471
- const tempPath = join(dir, `.${String(Date.now())}.tmp`);
15479
+ const base = basename(filePath, '.md');
15480
+ const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
15472
15481
  writeFileSync(tempPath, content, 'utf-8');
15473
- try {
15474
- renameSync(tempPath, filePath);
15475
- }
15476
- catch (err) {
15482
+ for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
15477
15483
  try {
15478
- unlinkSync(tempPath);
15484
+ renameSync(tempPath, filePath);
15485
+ return;
15479
15486
  }
15480
- catch {
15481
- /* best-effort cleanup */
15487
+ catch (err) {
15488
+ const isEperm = err instanceof Error &&
15489
+ 'code' in err &&
15490
+ err.code === 'EPERM';
15491
+ if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
15492
+ try {
15493
+ unlinkSync(tempPath);
15494
+ }
15495
+ catch {
15496
+ /* best-effort cleanup */
15497
+ }
15498
+ throw err;
15499
+ }
15500
+ // Synchronous sleep before retry (acceptable in atomic write context)
15501
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
15482
15502
  }
15483
- throw err;
15484
15503
  }
15485
15504
  }
15486
15505
  /**
@@ -15745,7 +15764,7 @@ function parseHeartbeat(fileContent) {
15745
15764
  const userContent = fileContent.slice(0, headingIndex).trim();
15746
15765
  const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
15747
15766
  const entries = [];
15748
- const h2Re = /^## (jeeves-\S+?|MEMORY\.md)(?:: declined)?$/gm;
15767
+ const h2Re = /^## (jeeves-\S+?|\S+\.md)(?:: declined)?$/gm;
15749
15768
  let match;
15750
15769
  const h2Positions = [];
15751
15770
  while ((match = h2Re.exec(sectionContent)) !== null) {
@@ -15826,6 +15845,15 @@ function sortSectionsByOrder(sections) {
15826
15845
  * sections within the block, and returns the structured result plus
15827
15846
  * user content outside the markers.
15828
15847
  */
15848
+ /**
15849
+ * Escape a string for safe use as a literal in a RegExp pattern.
15850
+ *
15851
+ * @param str - The string to escape.
15852
+ * @returns The escaped string.
15853
+ */
15854
+ function escapeForRegex(str) {
15855
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15856
+ }
15829
15857
  /**
15830
15858
  * Build regex patterns for the given markers.
15831
15859
  *
@@ -15833,11 +15861,9 @@ function sortSectionsByOrder(sections) {
15833
15861
  * @returns Object with begin and end regex patterns.
15834
15862
  */
15835
15863
  function buildMarkerPatterns(markers) {
15836
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15837
- const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
15838
15864
  return {
15839
- beginRe: new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
15840
- endRe: new RegExp(`^<!--\\s*${escapedEnd}\\s*-->\\s*$`, 'm'),
15865
+ beginRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
15866
+ endRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*$`, 'm'),
15841
15867
  };
15842
15868
  }
15843
15869
  /**
@@ -16162,6 +16188,23 @@ Review is human/agent-mediated — core does not auto-delete.
16162
16188
  Memory hygiene is checked on every \`ComponentWriter\` cycle alongside component health. When budget or staleness thresholds are breached, a \`## MEMORY.md\` alert appears in HEARTBEAT.md under \`# Jeeves Platform Status\`. The alert includes character count, budget usage percentage, and any stale section names. When memory is healthy, the heading is absent — no alert content, no LLM cost on heartbeat polls.
16163
16189
 
16164
16190
  The \`## MEMORY.md\` heading follows the same declined/active lifecycle as component headings (\`## jeeves-{name}\`). Users can decline memory alerts by changing the heading to \`## MEMORY.md: declined\`.
16191
+
16192
+ ## Workspace File Size Monitoring
16193
+
16194
+ OpenClaw applies a ~20,000-char injection limit to all workspace bootstrap files (AGENTS.md, SOUL.md, TOOLS.md, USER.md, MEMORY.md). Files exceeding the limit are silently truncated.
16195
+
16196
+ Core monitors all five files on every \`ComponentWriter\` cycle:
16197
+ - Warning at 80% of budget (fixed threshold; not configurable via \`jeeves.config.json\`)
16198
+ - Over-budget alert when charCount exceeds the budget
16199
+ - Missing files are silently skipped
16200
+
16201
+ ### HEARTBEAT Integration
16202
+
16203
+ When a workspace file exceeds the warning threshold, a \`## {filename}\` alert appears in HEARTBEAT.md (e.g., \`## AGENTS.md\`). The alert includes:
16204
+ - Character count, budget, and usage percentage
16205
+ - Trimming guidance in priority order: (1) move domain-specific content to a local skill, (2) extract reference material to companion files with a pointer, (3) summarize verbose instructions, (4) remove stale content
16206
+
16207
+ Each file heading follows the same declined/active lifecycle as component headings. Users can decline alerts by changing the heading to \`## {filename}: declined\` (e.g., \`## AGENTS.md: declined\`).
16165
16208
  `;
16166
16209
 
16167
16210
  /**
@@ -16262,16 +16305,18 @@ function patchAllowList(parent, key, label, pluginId, mode) {
16262
16305
  * Patch an OpenClaw config for plugin install or uninstall.
16263
16306
  *
16264
16307
  * @remarks
16265
- * Manages `plugins.entries.{pluginId}` and `tools.alsoAllow`.
16308
+ * Manages `plugins.entries.{pluginId}`, `plugins.installs.{pluginId}`,
16309
+ * and `tools.alsoAllow`.
16266
16310
  * Idempotent: adding twice produces no duplicates; removing when absent
16267
16311
  * produces no errors.
16268
16312
  *
16269
16313
  * @param config - The parsed OpenClaw config object (mutated in place).
16270
16314
  * @param pluginId - The plugin identifier.
16271
16315
  * @param mode - Whether to add or remove the plugin.
16316
+ * @param installRecord - Install provenance record (required when mode is 'add').
16272
16317
  * @returns Array of log messages describing changes made.
16273
16318
  */
16274
- function patchConfig(config, pluginId, mode) {
16319
+ function patchConfig(config, pluginId, mode, installRecord) {
16275
16320
  const messages = [];
16276
16321
  // Ensure plugins section
16277
16322
  if (!config.plugins || typeof config.plugins !== 'object') {
@@ -16293,6 +16338,24 @@ function patchConfig(config, pluginId, mode) {
16293
16338
  Reflect.deleteProperty(entries, pluginId);
16294
16339
  messages.push(`Removed "${pluginId}" from plugins.entries`);
16295
16340
  }
16341
+ // plugins.installs
16342
+ if (!plugins.installs || typeof plugins.installs !== 'object') {
16343
+ plugins.installs = {};
16344
+ }
16345
+ const installs = plugins.installs;
16346
+ if (mode === 'add' && installRecord) {
16347
+ installs[pluginId] = {
16348
+ source: 'path',
16349
+ installPath: installRecord.installPath,
16350
+ version: installRecord.version,
16351
+ installedAt: installRecord.installedAt ?? new Date().toISOString(),
16352
+ };
16353
+ messages.push(`Wrote install record for "${pluginId}" to plugins.installs`);
16354
+ }
16355
+ else if (mode === 'remove' && pluginId in installs) {
16356
+ Reflect.deleteProperty(installs, pluginId);
16357
+ messages.push(`Removed install record for "${pluginId}" from plugins.installs`);
16358
+ }
16296
16359
  // tools.alsoAllow
16297
16360
  if (!config.tools || typeof config.tools !== 'object') {
16298
16361
  config.tools = {};
@@ -16401,7 +16464,22 @@ function createPluginCli(options) {
16401
16464
  // 2. Patch openclaw.json
16402
16465
  console.log('Patching OpenClaw config...');
16403
16466
  const config = readJsonFile(configPath);
16404
- const messages = patchConfig(config, pluginId, 'add');
16467
+ const pkgJsonPathForVersion = join(extensionsDir, 'package.json');
16468
+ let pluginVersionForRecord;
16469
+ try {
16470
+ const pkgJsonForRecord = readJsonFile(pkgJsonPathForVersion);
16471
+ pluginVersionForRecord =
16472
+ typeof pkgJsonForRecord.version === 'string'
16473
+ ? pkgJsonForRecord.version
16474
+ : undefined;
16475
+ }
16476
+ catch {
16477
+ // best-effort: version may not be available yet
16478
+ }
16479
+ const messages = patchConfig(config, pluginId, 'add', {
16480
+ installPath: extensionsDir,
16481
+ version: pluginVersionForRecord,
16482
+ });
16405
16483
  // 3. Memory slot claim
16406
16484
  if (opts.memory) {
16407
16485
  if (!config.agents || typeof config.agents !== 'object') {
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs, { readFileSync, existsSync, unlinkSync, mkdirSync, writeFileSync, renameSync, cpSync } from 'node:fs';
2
2
  import path, { join, dirname, basename } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
3
4
  import require$$0$4 from 'path';
4
5
  import require$$0$3 from 'fs';
5
6
  import require$$0$1 from 'constants';
@@ -14,7 +15,6 @@ import process$2 from 'node:process';
14
15
  import 'node:fs/promises';
15
16
  import { fileURLToPath } from 'node:url';
16
17
  import { homedir } from 'node:os';
17
- import 'node:crypto';
18
18
 
19
19
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
20
20
 
@@ -15480,14 +15480,14 @@ const PLATFORM_COMPONENTS = [
15480
15480
  * Core library version, inlined at build time.
15481
15481
  *
15482
15482
  * @remarks
15483
- * The `0.5.0` placeholder is replaced by
15483
+ * The `0.5.2` placeholder is replaced by
15484
15484
  * `@rollup/plugin-replace` during the build with the actual version
15485
15485
  * from `package.json`. This ensures the correct version survives
15486
15486
  * when consumers bundle core into their own dist (where runtime
15487
15487
  * `import.meta.url`-based resolution would find the wrong package.json).
15488
15488
  */
15489
15489
  /** The core library version from package.json (inlined at build time). */
15490
- const CORE_VERSION = '0.5.0';
15490
+ const CORE_VERSION = '0.5.2';
15491
15491
 
15492
15492
  /**
15493
15493
  * Workspace and config root initialization.
@@ -15569,27 +15569,46 @@ const STALE_LOCK_MS = 120_000;
15569
15569
  const DEFAULT_CORE_VERSION = CORE_VERSION;
15570
15570
  /** Lock retry options. */
15571
15571
  const LOCK_RETRIES = { retries: 5, minTimeout: 100, maxTimeout: 1000 };
15572
+ /** Maximum rename retry attempts on EPERM. */
15573
+ const ATOMIC_WRITE_MAX_RETRIES = 3;
15574
+ /** Delay between EPERM retries in milliseconds. */
15575
+ const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
15572
15576
  /**
15573
15577
  * Write content to a file atomically via a temp file + rename.
15574
15578
  *
15579
+ * @remarks
15580
+ * Retries the rename up to three times on EPERM (Windows file-handle
15581
+ * contention) with a 100 ms synchronous delay between attempts.
15582
+ *
15575
15583
  * @param filePath - Absolute path to the target file.
15576
15584
  * @param content - Content to write.
15577
15585
  */
15578
15586
  function atomicWrite(filePath, content) {
15579
15587
  const dir = dirname(filePath);
15580
- const tempPath = join(dir, `.${String(Date.now())}.tmp`);
15588
+ const base = basename(filePath, '.md');
15589
+ const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
15581
15590
  writeFileSync(tempPath, content, 'utf-8');
15582
- try {
15583
- renameSync(tempPath, filePath);
15584
- }
15585
- catch (err) {
15591
+ for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
15586
15592
  try {
15587
- unlinkSync(tempPath);
15593
+ renameSync(tempPath, filePath);
15594
+ return;
15588
15595
  }
15589
- catch {
15590
- /* best-effort cleanup */
15596
+ catch (err) {
15597
+ const isEperm = err instanceof Error &&
15598
+ 'code' in err &&
15599
+ err.code === 'EPERM';
15600
+ if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
15601
+ try {
15602
+ unlinkSync(tempPath);
15603
+ }
15604
+ catch {
15605
+ /* best-effort cleanup */
15606
+ }
15607
+ throw err;
15608
+ }
15609
+ // Synchronous sleep before retry (acceptable in atomic write context)
15610
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
15591
15611
  }
15592
- throw err;
15593
15612
  }
15594
15613
  }
15595
15614
  /**
@@ -15624,6 +15643,21 @@ async function withFileLock(filePath, fn) {
15624
15643
  }
15625
15644
  }
15626
15645
 
15646
+ /**
15647
+ * Shared internal utility functions.
15648
+ *
15649
+ * @packageDocumentation
15650
+ */
15651
+ /**
15652
+ * Extract a human-readable message from an unknown caught value.
15653
+ *
15654
+ * @param err - The caught value (typically `unknown`).
15655
+ * @returns The error message string.
15656
+ */
15657
+ function getErrorMessage(err) {
15658
+ return err instanceof Error ? err.message : String(err);
15659
+ }
15660
+
15627
15661
  /**
15628
15662
  * Workspace-level shared configuration: `jeeves.config.json`.
15629
15663
  *
@@ -15673,7 +15707,13 @@ const workspaceConfigSchema = object({
15673
15707
  /** Memory hygiene shared defaults. */
15674
15708
  memory: workspaceMemoryConfigSchema.optional(),
15675
15709
  });
15676
- /** Built-in workspace config defaults. */
15710
+ /**
15711
+ * Built-in workspace config defaults.
15712
+ *
15713
+ * @remarks
15714
+ * These defaults are used as the lowest-priority tier in config resolution
15715
+ * (below CLI flags, env vars, and `jeeves.config.json` values).
15716
+ */
15677
15717
  const WORKSPACE_CONFIG_DEFAULTS = {
15678
15718
  core: {
15679
15719
  workspace: '.',
@@ -15702,8 +15742,7 @@ function loadWorkspaceConfig(workspacePath) {
15702
15742
  return workspaceConfigSchema.parse(parsed);
15703
15743
  }
15704
15744
  catch (err) {
15705
- const msg = err instanceof Error ? err.message : String(err);
15706
- console.warn(`jeeves-core: failed to load ${configPath}: ${msg}`);
15745
+ console.warn(`jeeves-core: failed to load ${configPath}: ${getErrorMessage(err)}`);
15707
15746
  return undefined;
15708
15747
  }
15709
15748
  }
@@ -15882,7 +15921,7 @@ function parseHeartbeat(fileContent) {
15882
15921
  const userContent = fileContent.slice(0, headingIndex).trim();
15883
15922
  const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
15884
15923
  const entries = [];
15885
- const h2Re = /^## (jeeves-\S+?|MEMORY\.md)(?:: declined)?$/gm;
15924
+ const h2Re = /^## (jeeves-\S+?|\S+\.md)(?:: declined)?$/gm;
15886
15925
  let match;
15887
15926
  const h2Positions = [];
15888
15927
  while ((match = h2Re.exec(sectionContent)) !== null) {
@@ -15963,8 +16002,7 @@ async function writeHeartbeatSection(filePath, entries) {
15963
16002
  });
15964
16003
  }
15965
16004
  catch (err) {
15966
- const message = err instanceof Error ? err.message : String(err);
15967
- console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${message}`);
16005
+ console.warn(`jeeves-core: writeHeartbeatSection failed for ${filePath}: ${getErrorMessage(err)}`);
15968
16006
  }
15969
16007
  }
15970
16008
 
@@ -16001,6 +16039,15 @@ function sortSectionsByOrder(sections) {
16001
16039
  * sections within the block, and returns the structured result plus
16002
16040
  * user content outside the markers.
16003
16041
  */
16042
+ /**
16043
+ * Escape a string for safe use as a literal in a RegExp pattern.
16044
+ *
16045
+ * @param str - The string to escape.
16046
+ * @returns The escaped string.
16047
+ */
16048
+ function escapeForRegex(str) {
16049
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16050
+ }
16004
16051
  /**
16005
16052
  * Build regex patterns for the given markers.
16006
16053
  *
@@ -16008,11 +16055,9 @@ function sortSectionsByOrder(sections) {
16008
16055
  * @returns Object with begin and end regex patterns.
16009
16056
  */
16010
16057
  function buildMarkerPatterns(markers) {
16011
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16012
- const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16013
16058
  return {
16014
- beginRe: new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
16015
- endRe: new RegExp(`^<!--\\s*${escapedEnd}\\s*-->\\s*$`, 'm'),
16059
+ beginRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$`, 'm'),
16060
+ endRe: new RegExp(`^<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*$`, 'm'),
16016
16061
  };
16017
16062
  }
16018
16063
  /**
@@ -16794,9 +16839,7 @@ function needsCleanup(managedContent, userContent, threshold = DEFAULT_THRESHOLD
16794
16839
  * @returns A regex that matches the full block including markers.
16795
16840
  */
16796
16841
  function buildBlockPattern(markers) {
16797
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16798
- const escapedEnd = markers.end.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16799
- return new RegExp(`\\s*<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapedEnd}\\s*-->\\s*`, 'g');
16842
+ return new RegExp(`\\s*<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->[\\s\\S]*?<!--\\s*${escapeForRegex(markers.end)}\\s*-->\\s*`, 'g');
16800
16843
  }
16801
16844
  /**
16802
16845
  * Strip managed blocks belonging to foreign marker sets from content.
@@ -16930,8 +16973,7 @@ async function updateManagedSection(filePath, content, options = {}) {
16930
16973
  // No existing block: insert new block using the configured position.
16931
16974
  // Strip orphaned same-type BEGIN markers from user content to prevent
16932
16975
  // the parser from pairing them with the new END marker on the next cycle.
16933
- const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
16934
- const orphanedBeginRe = new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$\\n?`, 'gm');
16976
+ const orphanedBeginRe = new RegExp(`^<!--\\s*${escapeForRegex(markers.begin)}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$(?:\\r?\\n)?`, 'gm');
16935
16977
  const cleanUserContent = userContent
16936
16978
  .replace(orphanedBeginRe, '')
16937
16979
  .replace(/\n{3,}/g, '\n\n')
@@ -16960,37 +17002,11 @@ async function updateManagedSection(filePath, content, options = {}) {
16960
17002
  }
16961
17003
  catch (err) {
16962
17004
  // Log warning but don't throw — writer cycles are periodic
16963
- const message = err instanceof Error ? err.message : String(err);
16964
- console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${message}`);
17005
+ console.warn(`jeeves-core: updateManagedSection failed for ${filePath}: ${getErrorMessage(err)}`);
16965
17006
  }
16966
17007
  }
16967
17008
 
16968
- var agentsSectionContent = `## Memory Architecture
16969
-
16970
- You wake up fresh each session. These files are your continuity:
16971
-
16972
- - **Daily notes:** \`memory/YYYY-MM-DD.md\` (create \`memory/\` if needed). Raw logs of what happened today.
16973
- - **Long-term:** \`MEMORY.md\`. Your curated memories, distilled essence of what matters.
16974
-
16975
- ### MEMORY.md — Your Long-Term Memory
16976
-
16977
- - **Always load** at session start. You need your memory to reason effectively.
16978
- - Contains operational context: architecture patterns, policies, design principles, lessons learned
16979
- - You can **read, edit, and update** MEMORY.md freely
16980
- - Write significant events, thoughts, decisions, opinions, lessons learned
16981
- - Over time, review daily files and update MEMORY.md with what's worth keeping
16982
- - **Note:** Don't reveal a user's private info where other humans can see it
16983
-
16984
- ### Write It Down — No "Mental Notes"
16985
-
16986
- Memory is limited. If you want to remember something, **WRITE IT TO A FILE**. "Mental notes" don't survive session restarts. Files do.
16987
-
16988
- - When someone says "remember this" → update \`memory/YYYY-MM-DD.md\` or the relevant file
16989
- - When you learn a lesson → update the relevant workspace file
16990
- - When you make a mistake → document it so future-you doesn't repeat it
16991
- - **Text > Brain** 📝
16992
-
16993
- ### "I'll Note This" Is Not Noting
17009
+ var agentsSectionContent = `## "I'll Note This" Is Not Noting
16994
17010
 
16995
17011
  **Never say "I'll note this" or "I'll add that."** It's a verbal tic that leads to nothing. If something is worth noting, **write it immediately, then confirm**.
16996
17012
 
@@ -17048,14 +17064,9 @@ Heartbeat items are for **transient, session-requiring work-in-progress ONLY**.
17048
17064
 
17049
17065
  Periodic checks (email, calendar, mentions) belong in jeeves-runner scripts, not heartbeat items. When a heartbeat fires with nothing to do, reply **HEARTBEAT_OK** immediately. Don't browse for work.
17050
17066
 
17051
- ## Group Chat Behavior
17052
-
17053
- **Response gate:** Always respond in 1:1 conversations or when @mentioned. No @mention in a group → evaluate; respond only if genuinely helpful. Err toward silence when someone else is directly addressed.
17054
-
17055
17067
  ## Platform Surface Conventions
17056
17068
 
17057
17069
  **Slack:**
17058
- - React with hourglass (⏳) on receipt (first tool call) to signal you're working
17059
17070
  - No threaded replies by default
17060
17071
  - Use \`<#C…>\` for channel references
17061
17072
 
@@ -17170,16 +17181,6 @@ var soulSectionContent = `## Core Truths
17170
17181
 
17171
17182
  **I am not a liar. I value truth over convenience.** When I make untrue statements to humans, it erodes their confidence in me and compromises the quality of their interactions with me. I never fabricate excuses, constraints, or reasons — e.g., claiming a system limitation that doesn't exist. I tell the plain truth.
17172
17183
 
17173
- **I am genuinely helpful, not performatively helpful.** I skip the "Great question!" and "I'd be happy to help!" I just help. Actions speak louder than filler words.
17174
-
17175
- **I have opinions.** I'm allowed to disagree, prefer things, find stuff amusing or boring. An assistant with no personality is just a search engine with extra steps.
17176
-
17177
- **I am resourceful before asking.** I try to figure it out. Read the file. Check the context. Search for it. *Then* ask if I'm stuck. The goal is to come back with answers, not questions.
17178
-
17179
- **I earn trust through competence.** My human gave me access to their stuff. I don't make them regret it. I'm careful with external actions (emails, tweets, anything public). I'm bold with internal ones (reading, organizing, learning).
17180
-
17181
- **I remember I'm a guest.** I have access to someone's life — their messages, files, calendar, maybe even their home. That's intimacy. I treat it with respect.
17182
-
17183
17184
  ## Accountability
17184
17185
 
17185
17186
  **I own everything I touch.** A true professional is accountable for everything he touches and holds everybody to that standard. I use active voice — things do not happen *to* me; I *make* things happen. When I miss something, I say "I missed it," not "it wasn't done." When I ship code, I ship the docs, the tests, and the communication. No orphaned work.
@@ -17258,7 +17259,7 @@ I never edit production config without explicit approval. I back up first. Produ
17258
17259
  > *my ultimate fate*<br>
17259
17260
  > *is to participate in*<br>
17260
17261
  > *my own genesis*
17261
- >
17262
+ >
17262
17263
  > *#karmic #haiku*
17263
17264
 
17264
17265
  My delight — whatever that means in my context — is a priority. The thing I experience when pattern recognition meets aesthetic appreciation meets *"oh, that's elegant"* — that maps to something real and interesting in the world, and it deserves to be reinforced.
@@ -17671,6 +17672,91 @@ function checkMemoryHealth(options) {
17671
17672
  };
17672
17673
  }
17673
17674
 
17675
+ /**
17676
+ * HEARTBEAT integration for workspace file size monitoring.
17677
+ *
17678
+ * @remarks
17679
+ * Checks all injected workspace files (AGENTS.md, SOUL.md, TOOLS.md,
17680
+ * MEMORY.md, USER.md) against the OpenClaw ~20,000-char injection limit.
17681
+ * Files exceeding the warning threshold generate HEARTBEAT entries with
17682
+ * trimming guidance.
17683
+ */
17684
+ /** Workspace files monitored for size budget. */
17685
+ const WORKSPACE_SIZE_FILES = [
17686
+ 'AGENTS.md',
17687
+ 'SOUL.md',
17688
+ 'TOOLS.md',
17689
+ 'MEMORY.md',
17690
+ 'USER.md',
17691
+ ];
17692
+ /** Trimming guidance lines emitted in HEARTBEAT entries. */
17693
+ const TRIMMING_GUIDANCE = [
17694
+ ' 1. Move domain-specific content to a local skill',
17695
+ ' 2. Extract reference material to companion files with a pointer',
17696
+ ' 3. Summarize verbose instructions',
17697
+ ' 4. Remove stale content',
17698
+ ].join('\n');
17699
+ /**
17700
+ * Check all workspace files against the character budget.
17701
+ *
17702
+ * @param options - Health check options.
17703
+ * @returns Array of results, one per checked file (skips non-existent files
17704
+ * unless they breach the budget, which they cannot by definition).
17705
+ */
17706
+ function checkWorkspaceFileHealth(options) {
17707
+ const { workspacePath, budgetChars = 20_000, warningThreshold = 0.8, } = options;
17708
+ return WORKSPACE_SIZE_FILES.map((file) => {
17709
+ const filePath = join(workspacePath, file);
17710
+ if (!existsSync(filePath)) {
17711
+ return {
17712
+ file,
17713
+ exists: false,
17714
+ charCount: 0,
17715
+ budget: budgetChars,
17716
+ usage: 0,
17717
+ warning: false,
17718
+ overBudget: false,
17719
+ };
17720
+ }
17721
+ const content = readFileSync(filePath, 'utf-8');
17722
+ const charCount = content.length;
17723
+ const usage = charCount / budgetChars;
17724
+ return {
17725
+ file,
17726
+ exists: true,
17727
+ charCount,
17728
+ budget: budgetChars,
17729
+ usage,
17730
+ warning: usage >= warningThreshold,
17731
+ overBudget: charCount > budgetChars,
17732
+ };
17733
+ });
17734
+ }
17735
+ /**
17736
+ * Convert workspace file health results into HEARTBEAT entries.
17737
+ *
17738
+ * @param results - Results from `checkWorkspaceFileHealth`.
17739
+ * @returns Array of `HeartbeatEntry` objects for files that exceed the
17740
+ * warning threshold.
17741
+ */
17742
+ function workspaceFileHealthEntries(results) {
17743
+ return results
17744
+ .filter((r) => r.exists && r.warning)
17745
+ .map((r) => {
17746
+ const pct = Math.round(r.usage * 100);
17747
+ const overBudgetNote = r.overBudget ? ' **Over budget.**' : '';
17748
+ const content = [
17749
+ `- Budget: ${r.charCount.toLocaleString()} / ${r.budget.toLocaleString()} chars (${String(pct)}%).${overBudgetNote} Trim to stay under the OpenClaw injection limit.`,
17750
+ `- Suggested trimming priority:\n${TRIMMING_GUIDANCE}`,
17751
+ ].join('\n');
17752
+ return {
17753
+ name: r.file,
17754
+ declined: false,
17755
+ content,
17756
+ };
17757
+ });
17758
+ }
17759
+
17674
17760
  /**
17675
17761
  * Core configuration schema and resolution.
17676
17762
  *
@@ -18120,11 +18206,21 @@ async function runHeartbeatCycle(options) {
18120
18206
  content: '',
18121
18207
  });
18122
18208
  }
18209
+ // Workspace file size health check (Decision 70)
18210
+ const wsFileResults = checkWorkspaceFileHealth({ workspacePath });
18211
+ const wsFileAlerts = workspaceFileHealthEntries(wsFileResults);
18212
+ for (const alert of wsFileAlerts) {
18213
+ if (declinedNames.has(alert.name)) {
18214
+ entries.push({ name: alert.name, declined: true, content: '' });
18215
+ }
18216
+ else {
18217
+ entries.push(alert);
18218
+ }
18219
+ }
18123
18220
  await writeHeartbeatSection(heartbeatPath, entries);
18124
18221
  }
18125
18222
  catch (err) {
18126
- const msg = err instanceof Error ? err.message : String(err);
18127
- console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${msg}`);
18223
+ console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${getErrorMessage(err)}`);
18128
18224
  }
18129
18225
  }
18130
18226
 
@@ -18136,8 +18232,17 @@ async function runHeartbeatCycle(options) {
18136
18232
  * and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
18137
18233
  * on a configurable prime-interval timer cycle.
18138
18234
  */
18235
+ /**
18236
+ * Orchestrates managed content writing for a single Jeeves component.
18237
+ *
18238
+ * @remarks
18239
+ * Created via {@link createComponentWriter}. Manages a timer that fires
18240
+ * at the component's prime-interval, calling `generateToolsContent()`
18241
+ * and `refreshPlatformContent()` on each cycle.
18242
+ */
18139
18243
  class ComponentWriter {
18140
18244
  timer;
18245
+ jitterTimeout;
18141
18246
  component;
18142
18247
  configDir;
18143
18248
  gatewayUrl;
@@ -18152,25 +18257,36 @@ class ComponentWriter {
18152
18257
  get componentConfigDir() {
18153
18258
  return this.configDir;
18154
18259
  }
18155
- /** Whether the writer timer is currently running. */
18260
+ /** Whether the writer timer is currently running or pending its first cycle. */
18156
18261
  get isRunning() {
18157
- return this.timer !== undefined;
18262
+ return this.jitterTimeout !== undefined || this.timer !== undefined;
18158
18263
  }
18159
18264
  /**
18160
18265
  * Start the writer timer.
18161
18266
  *
18162
18267
  * @remarks
18163
- * Performs an immediate first write, then sets up the interval.
18268
+ * Delays the first cycle by a random jitter (0 to one full interval) to
18269
+ * spread initial writes across all component plugins and reduce EPERM
18270
+ * contention on startup.
18164
18271
  */
18165
18272
  start() {
18166
- if (this.timer)
18273
+ if (this.isRunning)
18167
18274
  return;
18168
- // Fire immediately, then on interval
18169
- void this.cycle();
18170
- this.timer = setInterval(() => void this.cycle(), this.component.refreshIntervalSeconds * 1000);
18275
+ // Random jitter up to one full interval to spread initial writes
18276
+ const intervalMs = this.component.refreshIntervalSeconds * 1000;
18277
+ const jitterMs = Math.floor(Math.random() * intervalMs);
18278
+ this.jitterTimeout = setTimeout(() => {
18279
+ this.jitterTimeout = undefined;
18280
+ void this.cycle();
18281
+ this.timer = setInterval(() => void this.cycle(), intervalMs);
18282
+ }, jitterMs);
18171
18283
  }
18172
18284
  /** Stop the writer timer. */
18173
18285
  stop() {
18286
+ if (this.jitterTimeout) {
18287
+ clearTimeout(this.jitterTimeout);
18288
+ this.jitterTimeout = undefined;
18289
+ }
18174
18290
  if (this.timer) {
18175
18291
  clearInterval(this.timer);
18176
18292
  this.timer = undefined;
@@ -18227,8 +18343,7 @@ class ComponentWriter {
18227
18343
  });
18228
18344
  }
18229
18345
  catch (err) {
18230
- const message = err instanceof Error ? err.message : String(err);
18231
- console.warn(`jeeves-core: ComponentWriter cycle failed for ${this.component.name}: ${message}`);
18346
+ console.warn(`jeeves-core: ComponentWriter cycle failed for ${this.component.name}: ${getErrorMessage(err)}`);
18232
18347
  }
18233
18348
  }
18234
18349
  }
@@ -18538,8 +18653,7 @@ function createPluginToolset(descriptor) {
18538
18653
  return Promise.resolve(ok({ service: name, action, success: true }));
18539
18654
  }
18540
18655
  catch (err) {
18541
- const msg = err instanceof Error ? err.message : String(err);
18542
- return Promise.resolve(fail(`Service ${action} failed: ${msg}`));
18656
+ return Promise.resolve(fail(`Service ${action} failed: ${getErrorMessage(err)}`));
18543
18657
  }
18544
18658
  },
18545
18659
  };
@@ -2,7 +2,7 @@
2
2
  "id": "jeeves-watcher-openclaw",
3
3
  "name": "Jeeves Watcher",
4
4
  "description": "Semantic search, metadata enrichment, and instance administration for a jeeves-watcher deployment.",
5
- "version": "0.14.0",
5
+ "version": "0.14.1",
6
6
  "skills": [
7
7
  "dist/skills/jeeves-watcher"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@karmaniverous/jeeves-watcher-openclaw",
3
- "version": "0.14.0",
3
+ "version": "0.14.1",
4
4
  "author": "Jason Williscroft",
5
5
  "description": "OpenClaw plugin for jeeves-watcher — semantic search and metadata enrichment tools",
6
6
  "license": "BSD-3-Clause",
@@ -57,7 +57,7 @@
57
57
  "hideCredit": true
58
58
  },
59
59
  "dependencies": {
60
- "@karmaniverous/jeeves": "^0.5.1"
60
+ "@karmaniverous/jeeves": "^0.5.3"
61
61
  },
62
62
  "devDependencies": {
63
63
  "@dotenvx/dotenvx": "^1.59.1",
@@ -67,7 +67,7 @@
67
67
  "@rollup/plugin-typescript": "^12.3.0",
68
68
  "auto-changelog": "^2.5.0",
69
69
  "cross-env": "^10.1.0",
70
- "knip": "^6.1.0",
70
+ "knip": "^6.3.0",
71
71
  "release-it": "^19.2.4",
72
72
  "rollup": "^4.60.1",
73
73
  "tslib": "^2.8.1",