@karmaniverous/jeeves 0.4.7 → 0.5.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.
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import fs, { writeFileSync, renameSync, unlinkSync, existsSync, readFileSync, mkdirSync, readdirSync, copyFileSync, rmSync, cpSync } from 'node:fs';
2
- import path, { join, dirname, resolve } from 'node:path';
2
+ import path, { join, dirname, resolve, basename } from 'node:path';
3
3
  import { lock } from 'proper-lockfile';
4
4
  import { JSONPath } from 'jsonpath-plus';
5
+ import { major, valid, gte, gt } from 'semver';
6
+ import { z } from 'zod';
5
7
  import * as commander from 'commander';
6
8
  import { packageDirectorySync } from 'package-directory';
7
- import { valid, gte, gt } from 'semver';
8
9
  import { homedir } from 'node:os';
9
- import { z } from 'zod';
10
10
  import cp, { execSync } from 'node:child_process';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import crypto from 'node:crypto';
@@ -69,8 +69,8 @@ const ALL_MARKERS = [
69
69
  const VERSION_STAMP_PATTERN = /<!--\s*(.+?)\s*\|\s*core:(\S+)\s*\|\s*(\S+)\s*-->/;
70
70
  /** Staleness threshold for version-stamp convergence in milliseconds. */
71
71
  const STALENESS_THRESHOLD_MS = 5 * 60 * 1000;
72
- /** Warning text prepended inside managed block when cleanup is needed. */
73
- const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content may exist below this managed section. Review everything after the END marker and remove any content that duplicates what appears above.';
72
+ /** Warning text injected inside managed block when cleanup is needed. */
73
+ const CLEANUP_FLAG = '> ⚠️ CLEANUP NEEDED: Orphaned Jeeves content detected outside this managed block. Review the file and remove any content outside the BEGIN/END markers that duplicates what appears inside them.';
74
74
 
75
75
  /**
76
76
  * Directory and file path conventions for the Jeeves platform.
@@ -89,7 +89,13 @@ const WORKSPACE_FILES = {
89
89
  agents: 'AGENTS.md',
90
90
  /** HEARTBEAT.md — platform status and health alerts. */
91
91
  heartbeat: 'HEARTBEAT.md',
92
+ /** MEMORY.md — curated long-term memory. */
93
+ memory: 'MEMORY.md',
92
94
  };
95
+ /** Skill directory name within workspace. */
96
+ const SKILLS_DIR = 'skills';
97
+ /** Jeeves skill directory name. */
98
+ const JEEVES_SKILL_DIR = 'jeeves';
93
99
  /** Templates directory name within core config. */
94
100
  const TEMPLATES_DIR = 'templates';
95
101
  /** Registry cache file name. */
@@ -177,14 +183,14 @@ const PLATFORM_COMPONENTS = [
177
183
  * Core library version, inlined at build time.
178
184
  *
179
185
  * @remarks
180
- * The `0.4.6` placeholder is replaced by
186
+ * The `0.5.0` placeholder is replaced by
181
187
  * `@rollup/plugin-replace` during the build with the actual version
182
188
  * from `package.json`. This ensures the correct version survives
183
189
  * when consumers bundle core into their own dist (where runtime
184
190
  * `import.meta.url`-based resolution would find the wrong package.json).
185
191
  */
186
192
  /** The core library version from package.json (inlined at build time). */
187
- const CORE_VERSION = '0.4.6';
193
+ const CORE_VERSION = '0.5.0';
188
194
 
189
195
  /**
190
196
  * Workspace and config root initialization.
@@ -557,6 +563,257 @@ function createStatusHandler(options) {
557
563
  };
558
564
  }
559
565
 
566
+ /**
567
+ * Runtime Node.js version floor check.
568
+ *
569
+ * @module
570
+ */
571
+ /** Minimum supported Node.js major version. */
572
+ const MIN_NODE_MAJOR = 22;
573
+ /**
574
+ * Check that the running Node.js version meets the minimum requirement.
575
+ * Prints an error and exits with code 1 if the check fails.
576
+ */
577
+ function checkNodeVersion() {
578
+ const nodeMajor = major(process.versions.node);
579
+ if (nodeMajor < MIN_NODE_MAJOR) {
580
+ console.error(`Error: jeeves requires Node.js >= ${String(MIN_NODE_MAJOR)}. Current: ${process.versions.node}`);
581
+ process.exit(1);
582
+ }
583
+ }
584
+
585
+ /**
586
+ * Workspace-level shared configuration: `jeeves.config.json`.
587
+ *
588
+ * @remarks
589
+ * Lives at the OpenClaw workspace root alongside TOOLS.md and SOUL.md.
590
+ * Provides namespaced shared defaults consumed by the root Jeeves CLI.
591
+ * Resolution precedence: CLI flags → env vars → jeeves.config.json → defaults.
592
+ *
593
+ * This does not replace component-owned config schemas (Decision 41).
594
+ */
595
+ /** Workspace config file name. */
596
+ const WORKSPACE_CONFIG_FILE = 'jeeves.config.json';
597
+ /** Core shared config section. */
598
+ const workspaceCoreConfigSchema = z
599
+ .object({
600
+ /** Workspace root path. */
601
+ workspace: z.string().optional().describe('Workspace root path'),
602
+ /** Platform config root path. */
603
+ configRoot: z.string().optional().describe('Platform config root path'),
604
+ /** OpenClaw gateway URL. */
605
+ gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
606
+ })
607
+ .partial();
608
+ /** Memory shared config section. */
609
+ const workspaceMemoryConfigSchema = z
610
+ .object({
611
+ /** MEMORY.md character budget. */
612
+ budget: z.number().int().positive().optional().describe('Memory budget'),
613
+ /** Warning threshold as a fraction of budget. */
614
+ warningThreshold: z
615
+ .number()
616
+ .min(0)
617
+ .max(1)
618
+ .optional()
619
+ .describe('Memory warning threshold'),
620
+ /** Staleness threshold in days. */
621
+ staleDays: z
622
+ .number()
623
+ .int()
624
+ .positive()
625
+ .optional()
626
+ .describe('Memory staleness threshold in days'),
627
+ })
628
+ .partial();
629
+ /** Workspace config Zod schema. */
630
+ const workspaceConfigSchema = z.object({
631
+ /** JSON Schema pointer for IDE autocomplete. */
632
+ $schema: z.string().optional().describe('JSON Schema pointer'),
633
+ /** Core shared defaults. */
634
+ core: workspaceCoreConfigSchema.optional(),
635
+ /** Memory hygiene shared defaults. */
636
+ memory: workspaceMemoryConfigSchema.optional(),
637
+ });
638
+ /** Built-in workspace config defaults. */
639
+ const WORKSPACE_CONFIG_DEFAULTS = {
640
+ core: {
641
+ workspace: '.',
642
+ configRoot: './config',
643
+ gatewayUrl: 'http://127.0.0.1:3000',
644
+ },
645
+ memory: {
646
+ budget: 20_000,
647
+ warningThreshold: 0.8,
648
+ staleDays: 30,
649
+ },
650
+ };
651
+ /**
652
+ * Load workspace config from `jeeves.config.json` at a given path.
653
+ *
654
+ * @param workspacePath - Workspace root directory.
655
+ * @returns Parsed config or undefined if missing or invalid.
656
+ */
657
+ function loadWorkspaceConfig(workspacePath) {
658
+ const configPath = join(workspacePath, WORKSPACE_CONFIG_FILE);
659
+ if (!existsSync(configPath))
660
+ return undefined;
661
+ try {
662
+ const raw = readFileSync(configPath, 'utf-8');
663
+ const parsed = JSON.parse(raw);
664
+ return workspaceConfigSchema.parse(parsed);
665
+ }
666
+ catch (err) {
667
+ const msg = err instanceof Error ? err.message : String(err);
668
+ console.warn(`jeeves-core: failed to load ${configPath}: ${msg}`);
669
+ return undefined;
670
+ }
671
+ }
672
+ /**
673
+ * Resolve a config value with four-tier precedence.
674
+ *
675
+ * @param flagValue - CLI flag value (highest priority).
676
+ * @param envValue - Environment variable value.
677
+ * @param fileValue - Value from jeeves.config.json.
678
+ * @param defaultValue - Built-in default (lowest priority).
679
+ * @returns The resolved value with provenance annotation.
680
+ */
681
+ function resolveConfigValue(flagValue, envValue, fileValue, defaultValue) {
682
+ if (flagValue !== undefined)
683
+ return { value: flagValue, provenance: 'flag' };
684
+ if (envValue !== undefined)
685
+ return { value: envValue, provenance: 'env' };
686
+ if (fileValue !== undefined)
687
+ return { value: fileValue, provenance: 'file' };
688
+ return { value: defaultValue, provenance: 'default' };
689
+ }
690
+ /**
691
+ * Generate a JSON Schema for the workspace config.
692
+ *
693
+ * @returns A JSON Schema object.
694
+ */
695
+ function generateWorkspaceJsonSchema() {
696
+ return {
697
+ $schema: 'http://json-schema.org/draft-07/schema#',
698
+ title: 'Jeeves Workspace Configuration',
699
+ type: 'object',
700
+ properties: {
701
+ $schema: { type: 'string' },
702
+ core: {
703
+ type: 'object',
704
+ properties: {
705
+ workspace: {
706
+ type: 'string',
707
+ default: WORKSPACE_CONFIG_DEFAULTS.core.workspace,
708
+ },
709
+ configRoot: {
710
+ type: 'string',
711
+ default: WORKSPACE_CONFIG_DEFAULTS.core.configRoot,
712
+ },
713
+ gatewayUrl: {
714
+ type: 'string',
715
+ default: WORKSPACE_CONFIG_DEFAULTS.core.gatewayUrl,
716
+ },
717
+ },
718
+ },
719
+ memory: {
720
+ type: 'object',
721
+ properties: {
722
+ budget: {
723
+ type: 'integer',
724
+ minimum: 1,
725
+ default: WORKSPACE_CONFIG_DEFAULTS.memory.budget,
726
+ },
727
+ warningThreshold: {
728
+ type: 'number',
729
+ minimum: 0,
730
+ maximum: 1,
731
+ default: WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold,
732
+ },
733
+ staleDays: {
734
+ type: 'integer',
735
+ minimum: 1,
736
+ default: WORKSPACE_CONFIG_DEFAULTS.memory.staleDays,
737
+ },
738
+ },
739
+ },
740
+ },
741
+ };
742
+ }
743
+
744
+ /**
745
+ * Shared CLI defaults and resolution for Jeeves CLI commands.
746
+ *
747
+ * @remarks
748
+ * All root CLI commands share workspace/config-root resolution. Values follow
749
+ * the shared precedence model: flags → env → jeeves.config.json → defaults.
750
+ */
751
+ /** Default workspace path. */
752
+ const DEFAULT_WORKSPACE = WORKSPACE_CONFIG_DEFAULTS.core.workspace;
753
+ /** Default config root path. */
754
+ const DEFAULT_CONFIG_ROOT = WORKSPACE_CONFIG_DEFAULTS.core.configRoot;
755
+ /** Read a numeric env var or return undefined if missing, empty, or invalid. */
756
+ function readNumericEnv(name) {
757
+ const raw = process.env[name];
758
+ if (raw === undefined || raw.trim() === '')
759
+ return undefined;
760
+ const value = Number(raw);
761
+ return Number.isFinite(value) ? value : undefined;
762
+ }
763
+ /**
764
+ * Resolve shared CLI config using flags, env, file, and defaults.
765
+ *
766
+ * @param opts - Parsed CLI workspace/config-root options.
767
+ * @returns Resolved config tree with provenance on every leaf.
768
+ */
769
+ function resolveCliConfig(opts) {
770
+ const workspaceSeed = resolveConfigValue(opts.workspace, process.env['JEEVES_WORKSPACE'], undefined, DEFAULT_WORKSPACE);
771
+ const fileConfig = loadWorkspaceConfig(workspaceSeed.value);
772
+ return {
773
+ core: {
774
+ workspace: resolveConfigValue(opts.workspace, process.env['JEEVES_WORKSPACE'], fileConfig?.core?.workspace, DEFAULT_WORKSPACE),
775
+ configRoot: resolveConfigValue(opts.configRoot, process.env['JEEVES_CONFIG_ROOT'], fileConfig?.core?.configRoot, DEFAULT_CONFIG_ROOT),
776
+ gatewayUrl: resolveConfigValue(undefined, process.env['JEEVES_GATEWAY_URL'], fileConfig?.core?.gatewayUrl, WORKSPACE_CONFIG_DEFAULTS.core.gatewayUrl),
777
+ },
778
+ memory: {
779
+ budget: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_BUDGET'), fileConfig?.memory?.budget, WORKSPACE_CONFIG_DEFAULTS.memory.budget),
780
+ warningThreshold: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_WARNING_THRESHOLD'), fileConfig?.memory?.warningThreshold, WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold),
781
+ staleDays: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_STALE_DAYS'), fileConfig?.memory?.staleDays, WORKSPACE_CONFIG_DEFAULTS.memory.staleDays),
782
+ },
783
+ };
784
+ }
785
+ /**
786
+ * Initialize core from standard CLI options after resolving shared defaults.
787
+ *
788
+ * @param opts - Parsed Commander options with workspace and configRoot.
789
+ * @returns Resolved CLI config.
790
+ */
791
+ function initFromOptions(opts) {
792
+ const resolved = resolveCliConfig(opts);
793
+ init({
794
+ workspacePath: resolve(resolved.core.workspace.value),
795
+ configRoot: resolve(resolved.core.configRoot.value),
796
+ });
797
+ return resolved;
798
+ }
799
+
800
+ /**
801
+ * `jeeves config [jsonpath]` — inspect effective shared CLI configuration.
802
+ *
803
+ * @remarks
804
+ * Shows effective values and provenance using the shared precedence model.
805
+ * Optional JSONPath filters the resolved config tree.
806
+ */
807
+ /**
808
+ * Build the effective shared CLI config tree.
809
+ *
810
+ * @param opts - Parsed CLI workspace/config-root options.
811
+ * @returns Effective config tree with provenance on each leaf.
812
+ */
813
+ function buildEffectiveConfig(opts) {
814
+ return initFromOptions(opts);
815
+ }
816
+
560
817
  function getDefaultExportFromCjs (x) {
561
818
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
562
819
  }
@@ -750,7 +1007,7 @@ function parseHeartbeat(fileContent) {
750
1007
  const userContent = fileContent.slice(0, headingIndex).trim();
751
1008
  const sectionContent = fileContent.slice(headingIndex + HEARTBEAT_HEADING.length);
752
1009
  const entries = [];
753
- const h2Re = /^## (jeeves-\S+?)(?:: declined)?$/gm;
1010
+ const h2Re = /^## (jeeves-\S+?|MEMORY\.md)(?:: declined)?$/gm;
754
1011
  let match;
755
1012
  const h2Positions = [];
756
1013
  while ((match = h2Re.exec(sectionContent)) !== null) {
@@ -1121,6 +1378,135 @@ function buildWithSections(beforeContent, userContent, sections, markers, coreVe
1121
1378
  return parts.join('\n');
1122
1379
  }
1123
1380
 
1381
+ var skillContent = `---
1382
+ name: jeeves
1383
+ description: Jeeves platform architecture, data flow, component interaction, scripts repo, and coordination knowledge. Use when making architectural decisions, coordinating across components, checking platform health, managing service lifecycle, or working with the scripts repo.
1384
+ ---
1385
+
1386
+ # Jeeves Platform Skill
1387
+
1388
+ ## Platform Architecture
1389
+
1390
+ Jeeves is a four-component platform coordinated by a shared library (\`@karmaniverous/jeeves\`):
1391
+
1392
+ | Component | Role | Port |
1393
+ |-----------|------|------|
1394
+ | **jeeves-runner** | Execute: scheduled jobs, SQLite state, HTTP API | 1937 |
1395
+ | **jeeves-watcher** | Index: file→Qdrant semantic indexing, inference rules | 1936 |
1396
+ | **jeeves-server** | Present: web UI, file browser, doc render, export | 1934 |
1397
+ | **jeeves-meta** | Distill: LLM synthesis, .meta/ directories, scheduling | 1938 |
1398
+
1399
+ Core (\`@karmaniverous/jeeves\`) is a **library + CLI**, not a service. No port.
1400
+
1401
+ ## Data Flow
1402
+
1403
+ \`\`\`
1404
+ Files → Watcher (index) → Qdrant → Meta (synthesize) → .meta/ → Watcher (re-index)
1405
+ ↓
1406
+ Runner (schedule) → Scripts → Services ← Server (present) ← Browser
1407
+ \`\`\`
1408
+
1409
+ ## Component Interaction
1410
+
1411
+ - **Watcher** indexes files into Qdrant with inference rules and enrichments.
1412
+ - **Meta** reads from Qdrant, synthesizes \`.meta/\` directories, which watcher re-indexes.
1413
+ - **Runner** executes scheduled scripts that may call any service's HTTP API.
1414
+ - **Server** presents files, renders documents, and provides the event gateway.
1415
+ - **Core** provides shared content management (TOOLS.md, SOUL.md, AGENTS.md), service discovery, config resolution, and the component SDK.
1416
+
1417
+ ## Service Discovery
1418
+
1419
+ Services find each other via config resolution:
1420
+ 1. Component's own config file (\`{configRoot}/jeeves-{name}/config.json\`)
1421
+ 2. Core config file (\`{configRoot}/jeeves-core/config.json\`)
1422
+ 3. Default port constants
1423
+
1424
+ ## Scripts Repo
1425
+
1426
+ Location: \`{configRoot}/jeeves-core/scripts/\`
1427
+ Template: \`@karmaniverous/jeeves-scripts-template\`
1428
+
1429
+ Scripts use utilities from \`@karmaniverous/jeeves\` (general) and \`@karmaniverous/jeeves-runner\` (runner-specific). Any script that could be useful outside runner scheduling belongs in core.
1430
+
1431
+ ## Managed Content System
1432
+
1433
+ Core maintains managed sections in workspace files using comment markers:
1434
+ - **TOOLS.md** — Component sections (section mode) + Platform section
1435
+ - **SOUL.md** — Professional discipline and behavioral foundations (block mode)
1436
+ - **AGENTS.md** — Operational protocols and memory architecture (block mode)
1437
+ - **HEARTBEAT.md** — Platform health status (heading-based)
1438
+
1439
+ Managed blocks are stationary after initial insertion. Cleanup detection uses Jaccard similarity on 3-word shingles. Cleanup escalation spawns a gateway session when orphaned content is detected.
1440
+
1441
+ ## Workspace Configuration
1442
+
1443
+ \`jeeves.config.json\` at workspace root provides shared defaults:
1444
+ - Precedence: CLI flags → env vars → file → defaults
1445
+ - Namespaced: \`core.*\` (workspace, configRoot, gatewayUrl) and \`memory.*\` (budget, warningThreshold, staleDays)
1446
+ - Inspect with \`jeeves config [jsonpath]\`
1447
+
1448
+ ## HEARTBEAT Protocol
1449
+
1450
+ The HEARTBEAT system uses a state machine per component:
1451
+ \`not_installed → deps_missing → config_missing → service_not_installed → service_stopped → healthy\`
1452
+
1453
+ Dependency-aware: hard deps block alerts, soft deps add informational notes. Declined components are tracked via heading suffix.
1454
+
1455
+ ## Plugin Lifecycle
1456
+
1457
+ \`\`\`bash
1458
+ # Core install (seed workspace content)
1459
+ npx @karmaniverous/jeeves install
1460
+
1461
+ # Component plugin install
1462
+ npx @karmaniverous/jeeves-{component}-openclaw install
1463
+
1464
+ # Component plugin uninstall
1465
+ npx @karmaniverous/jeeves-{component}-openclaw uninstall
1466
+
1467
+ # Core uninstall (remove managed sections)
1468
+ npx @karmaniverous/jeeves uninstall
1469
+ \`\`\`
1470
+
1471
+ ## Memory Hygiene
1472
+
1473
+ MEMORY.md has a character budget (default 20,000). Core tracks:
1474
+ - Character count and usage percentage
1475
+ - Warning at 80% of budget
1476
+ - Stale section candidates (H2 sections whose most recent ISO date exceeds the staleness threshold)
1477
+ - Evergreen sections (no dates) are never flagged
1478
+
1479
+ Review is human/agent-mediated — core does not auto-delete.
1480
+
1481
+ ### HEARTBEAT Integration
1482
+
1483
+ 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.
1484
+
1485
+ 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\`.
1486
+ `;
1487
+
1488
+ /**
1489
+ * Skill seeding: write the `jeeves` workspace skill unconditionally.
1490
+ *
1491
+ * @remarks
1492
+ * The skill file is entirely generated — no user-authored content (Decision 48).
1493
+ * Every installer (core CLI and component plugins) writes it unconditionally.
1494
+ * Content is inlined at build time via `rollup-plugin-md.ts`.
1495
+ */
1496
+ /**
1497
+ * Seed the jeeves workspace skill file.
1498
+ *
1499
+ * @param workspacePath - Workspace root directory.
1500
+ */
1501
+ function seedSkill(workspacePath) {
1502
+ const skillDir = join(workspacePath, SKILLS_DIR, JEEVES_SKILL_DIR);
1503
+ if (!existsSync(skillDir)) {
1504
+ mkdirSync(skillDir, { recursive: true });
1505
+ }
1506
+ const skillPath = join(skillDir, 'SKILL.md');
1507
+ writeFileSync(skillPath, skillContent, 'utf-8');
1508
+ }
1509
+
1124
1510
  /**
1125
1511
  * OpenClaw configuration helpers for plugin CLI installers.
1126
1512
  *
@@ -1360,7 +1746,7 @@ function createPluginCli(options) {
1360
1746
  for (const msg of messages) {
1361
1747
  console.log(` ✓ ${msg}`);
1362
1748
  }
1363
- // 4. Write initial HEARTBEAT entry
1749
+ // 4. Write initial HEARTBEAT entry and seed jeeves skill
1364
1750
  try {
1365
1751
  const cfgRoot = opts.configRoot;
1366
1752
  const agents = config.agents;
@@ -1390,10 +1776,17 @@ function createPluginCli(options) {
1390
1776
  catch {
1391
1777
  console.log(' ⚠ Could not write HEARTBEAT entry');
1392
1778
  }
1779
+ try {
1780
+ seedSkill(ws);
1781
+ console.log(' ✓ Jeeves skill seeded');
1782
+ }
1783
+ catch {
1784
+ console.log(' ⚠ Could not seed Jeeves skill');
1785
+ }
1393
1786
  }
1394
1787
  }
1395
1788
  catch {
1396
- // HEARTBEAT is best-effort during install
1789
+ // HEARTBEAT + skill seeding are best-effort during install
1397
1790
  }
1398
1791
  // 5. Write component version
1399
1792
  try {
@@ -2045,19 +2438,6 @@ function createServiceManager(descriptor) {
2045
2438
  }
2046
2439
  }
2047
2440
 
2048
- /**
2049
- * Shared CLI defaults and option registration for Jeeves CLI commands.
2050
- *
2051
- * @remarks
2052
- * All three CLI commands (install, uninstall, status) share the same
2053
- * `--workspace` and `--config-root` options with the same defaults.
2054
- * This module centralizes them to eliminate duplication.
2055
- */
2056
- /** Default workspace path (current directory). */
2057
- const DEFAULT_WORKSPACE = '.';
2058
- /** Default config root path. */
2059
- const DEFAULT_CONFIG_ROOT = './config';
2060
-
2061
2441
  /**
2062
2442
  * Factory for the standard Jeeves service CLI.
2063
2443
  *
@@ -2486,10 +2866,10 @@ async function updateManagedSection(filePath, content, options = {}) {
2486
2866
  ? `# ${markers.title}\n\n${sectionText}`
2487
2867
  : sectionText;
2488
2868
  }
2489
- // Combine beforeContent + userContent for the user zone.
2490
- // When migrating from top→bottom, beforeContent is empty and
2491
- // userContent has the real content. When already at bottom,
2492
- // beforeContent has the user content and userContent is empty.
2869
+ // Build the full managed block
2870
+ const beginLine = formatBeginMarker(markers.begin, coreVersion);
2871
+ const endLine = formatEndMarker(markers.end);
2872
+ // Combine all user content for cleanup detection
2493
2873
  const rawUserContent = [parsed.beforeContent, parsed.userContent]
2494
2874
  .filter(Boolean)
2495
2875
  .join('\n\n')
@@ -2497,9 +2877,6 @@ async function updateManagedSection(filePath, content, options = {}) {
2497
2877
  // Strip foreign managed blocks from user content (cross-contamination fix)
2498
2878
  const userContent = stripForeignMarkers(rawUserContent, markers);
2499
2879
  const cleanupNeeded = needsCleanup(newManagedBody, userContent);
2500
- // Build the full managed block
2501
- const beginLine = formatBeginMarker(markers.begin, coreVersion);
2502
- const endLine = formatEndMarker(markers.end);
2503
2880
  const managedParts = [];
2504
2881
  managedParts.push(beginLine);
2505
2882
  if (cleanupNeeded) {
@@ -2511,26 +2888,54 @@ async function updateManagedSection(filePath, content, options = {}) {
2511
2888
  managedParts.push('');
2512
2889
  managedParts.push(endLine);
2513
2890
  const managedBlock = managedParts.join('\n');
2514
- const position = markers.position ?? 'top';
2515
- const fileParts = [];
2516
- if (position === 'bottom') {
2517
- // User content first, managed block at end
2518
- if (userContent) {
2519
- fileParts.push(userContent);
2891
+ let newFileContent;
2892
+ if (parsed.found) {
2893
+ // Existing block: update in place — preserve position, don't move.
2894
+ // Strip foreign managed blocks from both content zones (cross-contamination fix).
2895
+ const cleanBefore = stripForeignMarkers(parsed.beforeContent, markers);
2896
+ const cleanAfter = stripForeignMarkers(parsed.userContent, markers);
2897
+ const fileParts = [];
2898
+ if (cleanBefore) {
2899
+ fileParts.push(cleanBefore);
2520
2900
  fileParts.push('');
2521
2901
  }
2522
2902
  fileParts.push(managedBlock);
2903
+ if (cleanAfter) {
2904
+ fileParts.push('');
2905
+ fileParts.push(cleanAfter);
2906
+ }
2907
+ fileParts.push('');
2908
+ newFileContent = fileParts.join('\n');
2523
2909
  }
2524
2910
  else {
2525
- // Managed block first (legacy default), user content below
2526
- fileParts.push(managedBlock);
2527
- if (userContent) {
2528
- fileParts.push('');
2529
- fileParts.push(userContent);
2911
+ // No existing block: insert new block using the configured position.
2912
+ // Strip orphaned same-type BEGIN markers from user content to prevent
2913
+ // the parser from pairing them with the new END marker on the next cycle.
2914
+ const escapedBegin = markers.begin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
2915
+ const orphanedBeginRe = new RegExp(`^<!--\\s*${escapedBegin}(?:\\s*\\|[^>]*)?\\s*(?:—[^>]*)?\\s*-->\\s*$\\n?`, 'gm');
2916
+ const cleanUserContent = userContent
2917
+ .replace(orphanedBeginRe, '')
2918
+ .replace(/\n{3,}/g, '\n\n')
2919
+ .trim();
2920
+ const position = markers.position ?? 'top';
2921
+ const fileParts = [];
2922
+ if (position === 'bottom') {
2923
+ if (cleanUserContent) {
2924
+ fileParts.push(cleanUserContent);
2925
+ fileParts.push('');
2926
+ }
2927
+ fileParts.push(managedBlock);
2928
+ }
2929
+ else {
2930
+ fileParts.push(managedBlock);
2931
+ if (cleanUserContent) {
2932
+ fileParts.push('');
2933
+ fileParts.push(cleanUserContent);
2934
+ }
2530
2935
  }
2936
+ fileParts.push('');
2937
+ newFileContent = fileParts.join('\n');
2531
2938
  }
2532
- fileParts.push('');
2533
- const newFileContent = fileParts.join('\n');
2534
2939
  atomicWrite(filePath, newFileContent);
2535
2940
  });
2536
2941
  }
@@ -3024,6 +3429,229 @@ async function refreshPlatformContent(options) {
3024
3429
  copyTemplates(coreConfigDir);
3025
3430
  }
3026
3431
 
3432
+ /**
3433
+ * Cleanup-session escalation for managed files with orphaned duplicated content.
3434
+ *
3435
+ * @remarks
3436
+ * When a managed file contains the cleanup flag, the writer can ask the
3437
+ * OpenClaw gateway to spawn a background session to remove orphaned content.
3438
+ * The request is best-effort: accepted requests return `true`; any transport
3439
+ * or HTTP failure returns `false` so the file warning remains the fallback.
3440
+ */
3441
+ /** Timeout for cleanup-session spawn requests. */
3442
+ const CLEANUP_REQUEST_TIMEOUT_MS = 5_000;
3443
+ /**
3444
+ * Build the cleanup task prompt sent to the gateway session API.
3445
+ *
3446
+ * @param filePath - Managed file requiring cleanup.
3447
+ * @param markerIdentity - Marker identity for the file.
3448
+ * @returns Cleanup instructions for the spawned session.
3449
+ */
3450
+ function buildCleanupTask(filePath, markerIdentity) {
3451
+ return [
3452
+ `Clean up orphaned managed content in ${filePath}.`,
3453
+ `The file uses ${markerIdentity} managed comment markers.`,
3454
+ 'Review content outside the managed block and remove only duplicated managed content.',
3455
+ 'Preserve any unique user-authored content outside the managed block.',
3456
+ 'Do not modify content inside the managed block unless required to preserve valid marker structure.',
3457
+ ].join(' ');
3458
+ }
3459
+ /**
3460
+ * Request a cleanup session from the OpenClaw gateway.
3461
+ *
3462
+ * @remarks
3463
+ * Fire-and-forget. A 200-class response means the request was accepted.
3464
+ * Any HTTP or transport failure returns `false` so the file-level cleanup
3465
+ * warning remains the only signal.
3466
+ *
3467
+ * @param options - Cleanup request configuration.
3468
+ * @returns Whether the gateway accepted the cleanup request.
3469
+ */
3470
+ async function requestCleanupSession(options) {
3471
+ const { gatewayUrl, filePath, markerIdentity } = options;
3472
+ const url = `${gatewayUrl.replace(/\/$/, '')}/sessions/spawn`;
3473
+ const label = `cleanup:${basename(filePath)}`;
3474
+ const body = {
3475
+ task: buildCleanupTask(filePath, markerIdentity),
3476
+ label,
3477
+ };
3478
+ try {
3479
+ const response = await fetchWithTimeout(url, CLEANUP_REQUEST_TIMEOUT_MS, {
3480
+ method: 'POST',
3481
+ headers: { 'Content-Type': 'application/json' },
3482
+ body: JSON.stringify(body),
3483
+ });
3484
+ return response.ok;
3485
+ }
3486
+ catch {
3487
+ return false;
3488
+ }
3489
+ }
3490
+
3491
+ /**
3492
+ * Cleanup flag scanning extracted from ComponentWriter.cycle().
3493
+ *
3494
+ * @remarks
3495
+ * After writing managed files, scans each for the cleanup flag and
3496
+ * fires a best-effort escalation request when a gateway URL is configured.
3497
+ * Uses a `pendingCleanups` set to deduplicate in-flight requests.
3498
+ */
3499
+ /**
3500
+ * Scan managed files for the cleanup flag and escalate when detected.
3501
+ *
3502
+ * @param targets - Managed files to scan.
3503
+ * @param gatewayUrl - Gateway URL for session spawn.
3504
+ * @param pendingCleanups - Set tracking in-flight requests (mutated).
3505
+ */
3506
+ function scanAndEscalateCleanup(targets, gatewayUrl, pendingCleanups) {
3507
+ for (const target of targets) {
3508
+ try {
3509
+ if (pendingCleanups.has(target.filePath))
3510
+ continue;
3511
+ const fileContent = readFileSync(target.filePath, 'utf-8');
3512
+ if (fileContent.includes(CLEANUP_FLAG)) {
3513
+ pendingCleanups.add(target.filePath);
3514
+ void requestCleanupSession({
3515
+ gatewayUrl,
3516
+ filePath: target.filePath,
3517
+ markerIdentity: target.markerIdentity,
3518
+ }).finally(() => {
3519
+ pendingCleanups.delete(target.filePath);
3520
+ });
3521
+ }
3522
+ }
3523
+ catch {
3524
+ // Best-effort: don't fail the cycle for escalation issues.
3525
+ }
3526
+ }
3527
+ }
3528
+
3529
+ /**
3530
+ * Memory budget accounting and staleness detection for MEMORY.md.
3531
+ *
3532
+ * @remarks
3533
+ * Scans MEMORY.md for ISO date patterns in H2/H3 headings and bullet items.
3534
+ * Reports character count against a configured budget, warning threshold state,
3535
+ * and stale section candidates. Does not auto-delete: review remains
3536
+ * human- or agent-mediated (Decision 42).
3537
+ */
3538
+ /** ISO date pattern: YYYY-MM-DD. */
3539
+ const ISO_DATE_RE = /\b(\d{4}-\d{2}-\d{2})\b/g;
3540
+ /** H2 heading pattern used to split sections. */
3541
+ const H2_RE = /^## /m;
3542
+ /**
3543
+ * Extract the most recent ISO date from a string.
3544
+ *
3545
+ * @param text - Text to scan for dates.
3546
+ * @returns The most recent date found, or undefined.
3547
+ */
3548
+ function extractMostRecentDate(text) {
3549
+ const matches = text.match(ISO_DATE_RE);
3550
+ if (!matches)
3551
+ return undefined;
3552
+ let latest;
3553
+ for (const match of matches) {
3554
+ const d = new Date(match + 'T00:00:00Z');
3555
+ if (!Number.isNaN(d.getTime())) {
3556
+ if (!latest || d > latest)
3557
+ latest = d;
3558
+ }
3559
+ }
3560
+ return latest;
3561
+ }
3562
+ /**
3563
+ * Analyze MEMORY.md for budget and staleness.
3564
+ *
3565
+ * @param options - Analysis configuration.
3566
+ * @returns Memory hygiene result.
3567
+ */
3568
+ function analyzeMemory(options) {
3569
+ const { workspacePath, budget, warningThreshold, staleDays } = options;
3570
+ const memoryPath = join(workspacePath, WORKSPACE_FILES.memory);
3571
+ if (!existsSync(memoryPath)) {
3572
+ return {
3573
+ exists: false,
3574
+ charCount: 0,
3575
+ budget,
3576
+ usage: 0,
3577
+ warning: false,
3578
+ overBudget: false,
3579
+ staleCandidates: 0,
3580
+ staleSectionNames: [],
3581
+ };
3582
+ }
3583
+ const content = readFileSync(memoryPath, 'utf-8');
3584
+ const charCount = content.length;
3585
+ const usage = budget > 0 ? charCount / budget : charCount > 0 ? Infinity : 0;
3586
+ const warning = usage >= warningThreshold;
3587
+ const overBudget = usage > 1;
3588
+ // Split into H2 sections and scan for staleness
3589
+ const sections = content.split(H2_RE).slice(1); // skip content before first H2
3590
+ const now = Date.now();
3591
+ const thresholdMs = staleDays * 24 * 60 * 60 * 1000;
3592
+ const staleSectionNames = [];
3593
+ for (const section of sections) {
3594
+ const sectionName = section.split('\n')[0]?.trim() ?? '';
3595
+ const recentDate = extractMostRecentDate(section);
3596
+ // Sections without dates are evergreen — never flagged (Decision 47)
3597
+ if (!recentDate)
3598
+ continue;
3599
+ if (now - recentDate.getTime() > thresholdMs) {
3600
+ staleSectionNames.push(sectionName);
3601
+ }
3602
+ }
3603
+ return {
3604
+ exists: true,
3605
+ charCount,
3606
+ budget,
3607
+ usage,
3608
+ warning,
3609
+ overBudget,
3610
+ staleCandidates: staleSectionNames.length,
3611
+ staleSectionNames,
3612
+ };
3613
+ }
3614
+
3615
+ /**
3616
+ * HEARTBEAT integration for memory hygiene.
3617
+ *
3618
+ * @remarks
3619
+ * Calls `analyzeMemory()` and converts the result into a `HeartbeatEntry`
3620
+ * suitable for inclusion in the HEARTBEAT.md platform status section.
3621
+ * Returns `undefined` when MEMORY.md is healthy (no alert needed).
3622
+ *
3623
+ * Uses the `## MEMORY.md` heading (Decision 50) to distinguish memory
3624
+ * alerts from component alerts (`## jeeves-{name}`).
3625
+ */
3626
+ /** The HEARTBEAT heading name for memory alerts. */
3627
+ const MEMORY_HEARTBEAT_NAME = 'MEMORY.md';
3628
+ /**
3629
+ * Check memory health and return a HEARTBEAT entry if unhealthy.
3630
+ *
3631
+ * @param options - Memory hygiene options (workspacePath, budget, etc.).
3632
+ * @returns A `HeartbeatEntry` when memory needs attention, `undefined` when healthy.
3633
+ */
3634
+ function checkMemoryHealth(options) {
3635
+ const result = analyzeMemory(options);
3636
+ if (!result.exists)
3637
+ return undefined;
3638
+ if (!result.warning && result.staleCandidates === 0)
3639
+ return undefined;
3640
+ const lines = [];
3641
+ if (result.warning) {
3642
+ const pct = Math.round(result.usage * 100);
3643
+ lines.push(`- Budget: ${result.charCount.toLocaleString()} / ${result.budget.toLocaleString()} chars (${String(pct)}%).${result.overBudget ? ' **Over budget.**' : ' Consider reviewing.'}`);
3644
+ }
3645
+ if (result.staleCandidates > 0) {
3646
+ lines.push(`- ${String(result.staleCandidates)} stale section${result.staleCandidates === 1 ? '' : 's'}: ${result.staleSectionNames.join(', ')}`);
3647
+ }
3648
+ return {
3649
+ name: MEMORY_HEARTBEAT_NAME,
3650
+ declined: false,
3651
+ content: lines.join('\n'),
3652
+ };
3653
+ }
3654
+
3027
3655
  /**
3028
3656
  * Core configuration schema and resolution.
3029
3657
  *
@@ -3468,29 +4096,97 @@ async function orchestrateHeartbeat(options) {
3468
4096
  }
3469
4097
 
3470
4098
  /**
3471
- * Timer-based orchestrator for managed content writing.
4099
+ * HEARTBEAT orchestration extracted from ComponentWriter.cycle().
3472
4100
  *
3473
4101
  * @remarks
3474
- * `ComponentWriter` manages a component's TOOLS.md section writes
3475
- * and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
3476
- * on a configurable prime-interval timer cycle.
4102
+ * Reads existing HEARTBEAT.md, resolves declined components, runs the
4103
+ * heartbeat state machine, and writes the result. Best-effort: failures
4104
+ * are logged but do not propagate.
4105
+ */
4106
+ /**
4107
+ * Read a file's content, returning empty string if the file does not exist.
4108
+ *
4109
+ * @param filePath - Absolute file path.
4110
+ * @returns File content or empty string.
3477
4111
  */
4112
+ function readFileOrEmpty(filePath) {
4113
+ try {
4114
+ return readFileSync(filePath, 'utf-8');
4115
+ }
4116
+ catch (err) {
4117
+ if (err instanceof Error &&
4118
+ 'code' in err &&
4119
+ err.code === 'ENOENT') {
4120
+ return '';
4121
+ }
4122
+ throw err;
4123
+ }
4124
+ }
3478
4125
  /**
3479
- * Orchestrates managed content writing for a single Jeeves component.
4126
+ * Run a single HEARTBEAT orchestration cycle.
4127
+ *
4128
+ * @param options - Heartbeat cycle configuration.
4129
+ */
4130
+ async function runHeartbeatCycle(options) {
4131
+ const { workspacePath, coreConfigDir, configRoot } = options;
4132
+ const heartbeatPath = join(workspacePath, WORKSPACE_FILES.heartbeat);
4133
+ try {
4134
+ const existingContent = readFileOrEmpty(heartbeatPath);
4135
+ const parsed = parseHeartbeat(existingContent);
4136
+ const declinedNames = new Set(parsed.entries.filter((e) => e.declined).map((e) => e.name));
4137
+ const entries = await orchestrateHeartbeat({
4138
+ coreConfigDir,
4139
+ configRoot,
4140
+ declinedNames,
4141
+ });
4142
+ // Memory hygiene check (Decision 49)
4143
+ if (!declinedNames.has(MEMORY_HEARTBEAT_NAME)) {
4144
+ const wsConfig = loadWorkspaceConfig(workspacePath);
4145
+ const memoryEntry = checkMemoryHealth({
4146
+ workspacePath,
4147
+ budget: wsConfig?.memory?.budget ?? WORKSPACE_CONFIG_DEFAULTS.memory.budget,
4148
+ warningThreshold: wsConfig?.memory?.warningThreshold ??
4149
+ WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold,
4150
+ staleDays: wsConfig?.memory?.staleDays ??
4151
+ WORKSPACE_CONFIG_DEFAULTS.memory.staleDays,
4152
+ });
4153
+ if (memoryEntry)
4154
+ entries.push(memoryEntry);
4155
+ }
4156
+ else {
4157
+ entries.push({
4158
+ name: MEMORY_HEARTBEAT_NAME,
4159
+ declined: true,
4160
+ content: '',
4161
+ });
4162
+ }
4163
+ await writeHeartbeatSection(heartbeatPath, entries);
4164
+ }
4165
+ catch (err) {
4166
+ const msg = err instanceof Error ? err.message : String(err);
4167
+ console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${msg}`);
4168
+ }
4169
+ }
4170
+
4171
+ /**
4172
+ * Timer-based orchestrator for managed content writing.
3480
4173
  *
3481
4174
  * @remarks
3482
- * Created via `createComponentWriter()`. Manages a timer that fires
3483
- * at the component's prime-interval, calling `generateToolsContent()`
3484
- * and `refreshPlatformContent()` on each cycle.
4175
+ * `ComponentWriter` manages a component's TOOLS.md section writes
4176
+ * and platform content maintenance (SOUL.md, AGENTS.md, Platform section)
4177
+ * on a configurable prime-interval timer cycle.
3485
4178
  */
3486
4179
  class ComponentWriter {
3487
4180
  timer;
3488
4181
  component;
3489
4182
  configDir;
4183
+ gatewayUrl;
4184
+ pendingCleanups = new Set();
3490
4185
  /** @internal */
3491
- constructor(component) {
4186
+ constructor(component, options) {
3492
4187
  this.component = component;
3493
4188
  this.configDir = getComponentConfigDir(component.name);
4189
+ this.gatewayUrl = options?.gatewayUrl;
3494
4190
  }
3495
4191
  /** The component's config directory path. */
3496
4192
  get componentConfigDir() {
@@ -3524,15 +4220,16 @@ class ComponentWriter {
3524
4220
  * Execute a single write cycle.
3525
4221
  *
3526
4222
  * @remarks
3527
- * Calls `generateToolsContent()` and writes the component's
3528
- * TOOLS.md section via `updateManagedSection()`. Also calls
3529
- * `refreshPlatformContent()` for shared content maintenance.
4223
+ * 1. Write the component's TOOLS.md section.
4224
+ * 2. Refresh shared platform content (SOUL.md, AGENTS.md, Platform section).
4225
+ * 3. Scan for cleanup flags and escalate if a gateway URL is configured.
4226
+ * 4. Run HEARTBEAT health orchestration.
3530
4227
  */
3531
4228
  async cycle() {
3532
4229
  try {
3533
4230
  const workspacePath = getWorkspacePath();
3534
4231
  const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
3535
- // Write the component's TOOLS.md section
4232
+ // 1. Write the component's TOOLS.md section
3536
4233
  const toolsContent = this.component.generateToolsContent();
3537
4234
  await updateManagedSection(toolsPath, toolsContent, {
3538
4235
  mode: 'section',
@@ -3540,7 +4237,7 @@ class ComponentWriter {
3540
4237
  markers: TOOLS_MARKERS,
3541
4238
  coreVersion: CORE_VERSION,
3542
4239
  });
3543
- // Platform content maintenance: SOUL.md, AGENTS.md, Platform section
4240
+ // 2. Platform content maintenance
3544
4241
  await refreshPlatformContent({
3545
4242
  coreVersion: CORE_VERSION,
3546
4243
  componentName: this.component.name,
@@ -3548,36 +4245,26 @@ class ComponentWriter {
3548
4245
  servicePackage: this.component.servicePackage,
3549
4246
  pluginPackage: this.component.pluginPackage,
3550
4247
  });
3551
- // HEARTBEAT health orchestration
3552
- const heartbeatPath = join(workspacePath, WORKSPACE_FILES.heartbeat);
3553
- try {
3554
- const existingContent = (() => {
3555
- try {
3556
- return readFileSync(heartbeatPath, 'utf-8');
3557
- }
3558
- catch (err) {
3559
- // Only swallow "file not found" — let permission errors propagate
3560
- if (err instanceof Error &&
3561
- 'code' in err &&
3562
- err.code === 'ENOENT') {
3563
- return '';
3564
- }
3565
- throw err;
3566
- }
3567
- })();
3568
- const parsed = parseHeartbeat(existingContent);
3569
- const declinedNames = new Set(parsed.entries.filter((e) => e.declined).map((e) => e.name));
3570
- const entries = await orchestrateHeartbeat({
3571
- coreConfigDir: getCoreConfigDir(),
3572
- configRoot: getConfigRoot(),
3573
- declinedNames,
3574
- });
3575
- await writeHeartbeatSection(heartbeatPath, entries);
3576
- }
3577
- catch (err) {
3578
- const msg = err instanceof Error ? err.message : String(err);
3579
- console.warn(`jeeves-core: HEARTBEAT orchestration failed: ${msg}`);
4248
+ // 3. Cleanup escalation
4249
+ if (this.gatewayUrl) {
4250
+ scanAndEscalateCleanup([
4251
+ { filePath: toolsPath, markerIdentity: 'TOOLS' },
4252
+ {
4253
+ filePath: join(workspacePath, WORKSPACE_FILES.soul),
4254
+ markerIdentity: 'SOUL',
4255
+ },
4256
+ {
4257
+ filePath: join(workspacePath, WORKSPACE_FILES.agents),
4258
+ markerIdentity: 'AGENTS',
4259
+ },
4260
+ ], this.gatewayUrl, this.pendingCleanups);
3580
4261
  }
4262
+ // 4. HEARTBEAT orchestration
4263
+ await runHeartbeatCycle({
4264
+ workspacePath,
4265
+ coreConfigDir: getCoreConfigDir(),
4266
+ configRoot: getConfigRoot(),
4267
+ });
3581
4268
  }
3582
4269
  catch (err) {
3583
4270
  const message = err instanceof Error ? err.message : String(err);
@@ -3660,13 +4347,14 @@ function createAsyncContentCache(options) {
3660
4347
  * This replaces the v0.4.0 `createComponentWriter(JeevesComponent)`.
3661
4348
  *
3662
4349
  * @param descriptor - The component descriptor to validate and wrap.
4350
+ * @param options - Optional writer configuration (e.g., gatewayUrl for cleanup escalation).
3663
4351
  * @returns A new `ComponentWriter` instance.
3664
4352
  * @throws ZodError if the descriptor is invalid.
3665
4353
  */
3666
- function createComponentWriter(descriptor) {
4354
+ function createComponentWriter(descriptor, options) {
3667
4355
  // Validate via Zod — throws ZodError with detailed messages on failure
3668
4356
  jeevesComponentDescriptorSchema.parse(descriptor);
3669
- return new ComponentWriter(descriptor);
4357
+ return new ComponentWriter(descriptor, options);
3670
4358
  }
3671
4359
 
3672
4360
  /**
@@ -3765,6 +4453,8 @@ async function seedContent(options) {
3765
4453
  content: `- ${NOT_INSTALLED_ALERTS[name]}`,
3766
4454
  }));
3767
4455
  await writeHeartbeatSection(heartbeatPath, entries);
4456
+ // Seed jeeves workspace skill (Decision 48: overwrite-on-install)
4457
+ seedSkill(getWorkspacePath());
3768
4458
  }
3769
4459
 
3770
4460
  /**
@@ -4468,4 +5158,4 @@ async function getChannelWorkspace(channelId, token, options) {
4468
5158
  return teamId;
4469
5159
  }
4470
5160
 
4471
- export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, COMPONENT_VERSIONS_FILE, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_BIND_ADDRESS, DEFAULT_CORE_VERSION, DEFAULT_PORTS, HEARTBEAT_HEADING, META_PORT, PLATFORM_COMPONENTS, REGISTRY_CACHE_FILE, RUNNER_PORT, SECTION_IDS, SECTION_ORDER, SERVER_PORT, SOUL_MARKERS, STALENESS_THRESHOLD_MS, STALE_LOCK_MS, TEMPLATES_DIR, TOOLS_MARKERS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_FILES, appendJsonl, atomicWrite, buildHeartbeatSection, checkRegistryVersion, connectionFail, coreConfigSchema, createAsyncContentCache, createComponentWriter, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginCli, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, jaccard, jeevesComponentDescriptorSchema, loadEnvFile, needsCleanup, nowIso, ok, orchestrateHeartbeat, parseArgs, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, readJson, readJsonl, refreshPlatformContent, removeComponentVersion, removeManagedSection, resetInit, resolveConfigPath, resolveOpenClawHome, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, seedContent, shingles, shouldWrite, sleepAsync, sleepMs, updateManagedSection, uuid, withFileLock, writeComponentVersion, writeHeartbeatSection, writeJsonAtomic, writeJsonl };
5161
+ export { AGENTS_MARKERS, CLEANUP_FLAG, COMPONENT_CONFIG_PREFIX, COMPONENT_VERSIONS_FILE, CONFIG_FILE, CORE_CONFIG_DIR, CORE_VERSION, ComponentWriter, DEFAULT_BIND_ADDRESS, DEFAULT_CORE_VERSION, DEFAULT_PORTS, HEARTBEAT_HEADING, JEEVES_SKILL_DIR, MEMORY_HEARTBEAT_NAME, META_PORT, PLATFORM_COMPONENTS, REGISTRY_CACHE_FILE, RUNNER_PORT, SECTION_IDS, SECTION_ORDER, SERVER_PORT, SKILLS_DIR, SOUL_MARKERS, STALENESS_THRESHOLD_MS, STALE_LOCK_MS, TEMPLATES_DIR, TOOLS_MARKERS, VERSION_STAMP_PATTERN, WATCHER_PORT, WORKSPACE_CONFIG_DEFAULTS, WORKSPACE_CONFIG_FILE, WORKSPACE_FILES, analyzeMemory, appendJsonl, atomicWrite, buildEffectiveConfig, buildHeartbeatSection, checkMemoryHealth, checkNodeVersion, checkRegistryVersion, connectionFail, coreConfigSchema, createAsyncContentCache, createComponentWriter, createConfigApplyHandler, createConfigQueryHandler, createGoogleAuth, createPluginCli, createPluginToolset, createServiceCli, createServiceManager, createStatusHandler, ensureDir, extractMostRecentDate, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, jaccard, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, needsCleanup, nowIso, ok, orchestrateHeartbeat, parseArgs, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, readJson, readJsonl, refreshPlatformContent, removeComponentVersion, removeManagedSection, resetInit, resolveConfigPath, resolveConfigValue, resolveOpenClawHome, resolveOptionalPluginSetting, resolvePluginSetting, resolveWorkspacePath, run, runScript, runWithRetry, saveCache, seedContent, seedSkill, shingles, shouldWrite, sleepAsync, sleepMs, updateManagedSection, uuid, withFileLock, workspaceConfigSchema, writeComponentVersion, writeHeartbeatSection, writeJsonAtomic, writeJsonl };