@karmaniverous/jeeves 0.5.8 → 0.5.9

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/README.md CHANGED
@@ -220,8 +220,7 @@ Optional `jeeves.config.json` at the workspace root provides shared defaults for
220
220
  },
221
221
  "memory": {
222
222
  "budget": 20000,
223
- "warningThreshold": 0.8,
224
- "staleDays": 90
223
+ "warningThreshold": 0.8
225
224
  }
226
225
  }
227
226
  ```
@@ -240,10 +239,8 @@ Precedence: **CLI flags → environment variables → `jeeves.config.json` → d
240
239
  MEMORY.md has a character budget (default: 20,000 characters). The `analyzeMemory()` function tracks:
241
240
 
242
241
  - **Character count and usage percentage** — warns at 80% of budget (configurable via `warningThreshold`)
243
- - **Stale section detection** — scans ISO dates (`YYYY-MM-DD`) in H2/H3 headings and bullet items; sections whose most recent date exceeds `staleDays` (default: 30) are flagged as stale candidates
244
- - **Evergreen sections** — sections without parseable dates are never flagged
245
242
 
246
- Memory hygiene is reporting-only. Core does not auto-delete content; the assistant or human reviews stale candidates and decides what to prune.
243
+ Memory hygiene is reporting-only. Core does not auto-delete content (Decision 42). Size pressure is the right signal for curation.
247
244
 
248
245
  ## Skill Seeding
249
246
 
@@ -37,7 +37,7 @@ Periodic checks (email, calendar, mentions) belong in jeeves-runner scripts, not
37
37
  ## Platform Surface Conventions
38
38
 
39
39
  **Slack:**
40
- - No threaded replies by default
40
+ - Never initiate a threaded reply. Only reply within a thread started by a human.
41
41
  - Use `<#C…>` for channel references
42
42
 
43
43
  **Table formatting:** On channels that do not support Markdown tables (Slack, Discord, WhatsApp, IRC), use code-block tables with aligned columns. Markdown tables are only safe in contexts that render them (GitHub, jeeves-server, files).
package/content/skill.md CHANGED
@@ -62,7 +62,7 @@ Managed blocks are stationary after initial insertion. Cleanup detection uses Ja
62
62
 
63
63
  `jeeves.config.json` at workspace root provides shared defaults:
64
64
  - Precedence: CLI flags → env vars → file → defaults
65
- - Namespaced: `core.*` (workspace, configRoot, gatewayUrl) and `memory.*` (budget, warningThreshold, staleDays)
65
+ - Namespaced: `core.*` (workspace, configRoot, gatewayUrl, devRepos) and `memory.*` (budget, warningThreshold)
66
66
  - Inspect with `jeeves config [jsonpath]`
67
67
 
68
68
  ## HEARTBEAT Protocol
@@ -44,8 +44,8 @@ Never manually edit `~/.openclaw/extensions/`. Always use the CLI commands above
44
44
 
45
45
  ### Reference Templates
46
46
 
47
- <!-- IF_TEMPLATES -->
48
- Reference templates are available at `__TEMPLATE_PATH__`:
47
+ {{#if templatePath}}
48
+ Reference templates are available at `{{templatePath}}`:
49
49
 
50
50
  | Template | Purpose |
51
51
  |----------|---------|
@@ -53,6 +53,36 @@ Reference templates are available at `__TEMPLATE_PATH__`:
53
53
  | `spec-to-code-guide.md` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
54
54
 
55
55
  Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
56
- <!-- ELSE_TEMPLATES -->
56
+ {{else}}
57
57
  > Reference templates not yet installed. Run `npx @karmaniverous/jeeves install` to seed templates.
58
- <!-- ENDIF_TEMPLATES -->
58
+ {{/if}}
59
+
60
+ ### Post-Upgrade Maintenance
61
+
62
+ After updating OpenClaw (`npm install -g openclaw@latest` or equivalent), reinstall all Jeeves component plugins to repair install state:
63
+
64
+ ```bash
65
+ npx @karmaniverous/jeeves install
66
+ npx @karmaniverous/jeeves-runner-openclaw install
67
+ npx @karmaniverous/jeeves-watcher-openclaw install
68
+ npx @karmaniverous/jeeves-server-openclaw install
69
+ npx @karmaniverous/jeeves-meta-openclaw install
70
+ ```
71
+
72
+ Then restart the gateway. Plugin installers copy dist files and patch config; reinstalling after an OpenClaw update ensures the extensions directory stays consistent.
73
+
74
+ ### Source Code Preference
75
+
76
+ {{#if devRepos}}
77
+ When investigating, debugging, or analyzing Jeeves components, always read TypeScript source from dev repos — never compiled `dist/` from the global npm install. Dev repos:
78
+
79
+ | Component | Dev Repo |
80
+ |-----------|----------|
81
+ {{#each devRepos}}
82
+ | {{@key}} | `{{this}}` |
83
+ {{/each}}
84
+
85
+ Built code is minified, harder to reason about, and wastes context. Always `git pull` before analysis.
86
+ {{else}}
87
+ > Dev repo paths not configured. Add `core.devRepos` to `jeeves.config.json` to enable source code preference guidance.
88
+ {{/if}}
@@ -9,6 +9,7 @@ import { execSync, spawnSync } from 'node:child_process';
9
9
  import { randomUUID } from 'node:crypto';
10
10
  import { lock } from 'proper-lockfile';
11
11
  import { fileURLToPath } from 'node:url';
12
+ import Handlebars from 'handlebars';
12
13
  import { packageDirectorySync } from 'package-directory';
13
14
 
14
15
  function getDefaultExportFromCjs (x) {
@@ -269,14 +270,14 @@ const PLATFORM_COMPONENTS = [
269
270
  * Core library version, inlined at build time.
270
271
  *
271
272
  * @remarks
272
- * The `0.5.7` placeholder is replaced by
273
+ * The `0.5.8` placeholder is replaced by
273
274
  * `@rollup/plugin-replace` during the build with the actual version
274
275
  * from `package.json`. This ensures the correct version survives
275
276
  * when consumers bundle core into their own dist (where runtime
276
277
  * `import.meta.url`-based resolution would find the wrong package.json).
277
278
  */
278
279
  /** The core library version from package.json (inlined at build time). */
279
- const CORE_VERSION = '0.5.7';
280
+ const CORE_VERSION = '0.5.8';
280
281
 
281
282
  /**
282
283
  * Runtime Node.js version floor check.
@@ -333,6 +334,11 @@ const workspaceCoreConfigSchema = z
333
334
  configRoot: z.string().optional().describe('Platform config root path'),
334
335
  /** OpenClaw gateway URL. */
335
336
  gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
337
+ /** Dev repo paths keyed by component name. */
338
+ devRepos: z
339
+ .record(z.string(), z.string())
340
+ .optional()
341
+ .describe('Dev repo paths by component name'),
336
342
  })
337
343
  .partial();
338
344
  /** Memory shared config section. */
@@ -347,13 +353,6 @@ const workspaceMemoryConfigSchema = z
347
353
  .max(1)
348
354
  .optional()
349
355
  .describe('Memory warning threshold'),
350
- /** Staleness threshold in days. */
351
- staleDays: z
352
- .number()
353
- .int()
354
- .positive()
355
- .optional()
356
- .describe('Memory staleness threshold in days'),
357
356
  })
358
357
  .partial();
359
358
  /** Workspace config Zod schema. */
@@ -381,7 +380,6 @@ const WORKSPACE_CONFIG_DEFAULTS = {
381
380
  memory: {
382
381
  budget: 20_000,
383
382
  warningThreshold: 0.8,
384
- staleDays: 30,
385
383
  },
386
384
  };
387
385
  /**
@@ -512,7 +510,6 @@ function resolveCliConfig(opts) {
512
510
  memory: {
513
511
  budget: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_BUDGET'), fileConfig?.memory?.budget, WORKSPACE_CONFIG_DEFAULTS.memory.budget),
514
512
  warningThreshold: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_WARNING_THRESHOLD'), fileConfig?.memory?.warningThreshold, WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold),
515
- staleDays: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_STALE_DAYS'), fileConfig?.memory?.staleDays, WORKSPACE_CONFIG_DEFAULTS.memory.staleDays),
516
513
  },
517
514
  };
518
515
  }
@@ -821,9 +818,8 @@ function getServiceUrl(serviceName, consumerName) {
821
818
  if (coreUrl)
822
819
  return coreUrl;
823
820
  // 3. Fall back to port constants
824
- const port = DEFAULT_PORTS[serviceName];
825
- if (port !== undefined) {
826
- return `http://127.0.0.1:${String(port)}`;
821
+ if (serviceName in DEFAULT_PORTS) {
822
+ return `http://127.0.0.1:${String(DEFAULT_PORTS[serviceName])}`;
827
823
  }
828
824
  throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
829
825
  }
@@ -1178,7 +1174,7 @@ Periodic checks (email, calendar, mentions) belong in jeeves-runner scripts, not
1178
1174
  ## Platform Surface Conventions
1179
1175
 
1180
1176
  **Slack:**
1181
- - No threaded replies by default
1177
+ - Never initiate a threaded reply. Only reply within a thread started by a human.
1182
1178
  - Use \`<#C…>\` for channel references
1183
1179
 
1184
1180
  **Table formatting:** On channels that do not support Markdown tables (Slack, Discord, WhatsApp, IRC), use code-block tables with aligned columns. Markdown tables are only safe in contexts that render them (GitHub, jeeves-server, files).
@@ -1384,8 +1380,8 @@ Never manually edit \`~/.openclaw/extensions/\`. Always use the CLI commands abo
1384
1380
 
1385
1381
  ### Reference Templates
1386
1382
 
1387
- <!-- IF_TEMPLATES -->
1388
- Reference templates are available at \`__TEMPLATE_PATH__\`:
1383
+ {{#if templatePath}}
1384
+ Reference templates are available at \`{{templatePath}}\`:
1389
1385
 
1390
1386
  | Template | Purpose |
1391
1387
  |----------|---------|
@@ -1393,9 +1389,39 @@ Reference templates are available at \`__TEMPLATE_PATH__\`:
1393
1389
  | \`spec-to-code-guide.md\` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
1394
1390
 
1395
1391
  Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
1396
- <!-- ELSE_TEMPLATES -->
1392
+ {{else}}
1397
1393
  > Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
1398
- <!-- ENDIF_TEMPLATES -->
1394
+ {{/if}}
1395
+
1396
+ ### Post-Upgrade Maintenance
1397
+
1398
+ After updating OpenClaw (\`npm install -g openclaw@latest\` or equivalent), reinstall all Jeeves component plugins to repair install state:
1399
+
1400
+ \`\`\`bash
1401
+ npx @karmaniverous/jeeves install
1402
+ npx @karmaniverous/jeeves-runner-openclaw install
1403
+ npx @karmaniverous/jeeves-watcher-openclaw install
1404
+ npx @karmaniverous/jeeves-server-openclaw install
1405
+ npx @karmaniverous/jeeves-meta-openclaw install
1406
+ \`\`\`
1407
+
1408
+ Then restart the gateway. Plugin installers copy dist files and patch config; reinstalling after an OpenClaw update ensures the extensions directory stays consistent.
1409
+
1410
+ ### Source Code Preference
1411
+
1412
+ {{#if devRepos}}
1413
+ When investigating, debugging, or analyzing Jeeves components, always read TypeScript source from dev repos — never compiled \`dist/\` from the global npm install. Dev repos:
1414
+
1415
+ | Component | Dev Repo |
1416
+ |-----------|----------|
1417
+ {{#each devRepos}}
1418
+ | {{@key}} | \`{{this}}\` |
1419
+ {{/each}}
1420
+
1421
+ Built code is minified, harder to reason about, and wastes context. Always \`git pull\` before analysis.
1422
+ {{else}}
1423
+ > Dev repo paths not configured. Add \`core.devRepos\` to \`jeeves.config.json\` to enable source code preference guidance.
1424
+ {{/if}}
1399
1425
  `;
1400
1426
 
1401
1427
  /**
@@ -1844,6 +1870,10 @@ async function updateManagedSection(filePath, content, options = {}) {
1844
1870
  * Platform template with live data, and writes managed sections using
1845
1871
  * `updateManagedSection`.
1846
1872
  */
1873
+ /** Compiled Handlebars template for the Platform section (cached at module level). */
1874
+ const compiledPlatformTemplate = Handlebars.compile(toolsPlatformTemplate, {
1875
+ noEscape: true,
1876
+ });
1847
1877
  /**
1848
1878
  * Resolve the package's content directory for template file copying.
1849
1879
  *
@@ -1887,23 +1917,13 @@ function copyTemplates(coreConfigDir) {
1887
1917
  cpSync(sourceDir, destDir, { recursive: true });
1888
1918
  }
1889
1919
  /**
1890
- * Render the Platform template using simple string replacement.
1920
+ * Render the Platform template using Handlebars.
1891
1921
  *
1892
- * @param templatePath - Path to the templates directory.
1922
+ * @param context - Template context with optional templatePath and devRepos.
1893
1923
  * @returns Rendered platform content string.
1894
1924
  */
1895
- function renderPlatformTemplate(templatePath) {
1896
- const templatesAvailable = existsSync(templatePath);
1897
- let content = toolsPlatformTemplate;
1898
- // Handle <!-- IF_TEMPLATES --> ... <!-- ELSE_TEMPLATES --> ... <!-- ENDIF_TEMPLATES --> block
1899
- const ifRegex = /<!-- IF_TEMPLATES -->([\s\S]*?)<!-- ELSE_TEMPLATES -->([\s\S]*?)<!-- ENDIF_TEMPLATES -->/;
1900
- const match = ifRegex.exec(content);
1901
- if (match) {
1902
- content = content.replace(match[0], templatesAvailable ? match[1] : match[2]);
1903
- }
1904
- // Replace __TEMPLATE_PATH__ with the actual path
1905
- content = content.replace(/__TEMPLATE_PATH__/g, templatePath);
1906
- return content;
1925
+ function renderPlatformTemplate(context) {
1926
+ return compiledPlatformTemplate(context);
1907
1927
  }
1908
1928
  /**
1909
1929
  * Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
@@ -1911,7 +1931,7 @@ function renderPlatformTemplate(templatePath) {
1911
1931
  * @param options - Configuration for the refresh cycle.
1912
1932
  */
1913
1933
  async function refreshPlatformContent(options) {
1914
- const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, } = options;
1934
+ const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, workspaceConfig, } = options;
1915
1935
  const workspacePath = getWorkspacePath();
1916
1936
  const coreConfigDir = getCoreConfigDir();
1917
1937
  // 1. Write calling component's version entry
@@ -1925,7 +1945,11 @@ async function refreshPlatformContent(options) {
1925
1945
  }
1926
1946
  // 2. Render Platform template
1927
1947
  const templatePath = join(coreConfigDir, TEMPLATES_DIR);
1928
- const platformContent = renderPlatformTemplate(templatePath);
1948
+ const wsConfig = workspaceConfig ?? loadWorkspaceConfig(workspacePath);
1949
+ const platformContent = renderPlatformTemplate({
1950
+ templatePath: existsSync(templatePath) ? templatePath : undefined,
1951
+ devRepos: wsConfig?.core?.devRepos,
1952
+ });
1929
1953
  // 3. Write TOOLS.md Platform section
1930
1954
  const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
1931
1955
  await updateManagedSection(toolsPath, platformContent, {
@@ -2019,7 +2043,7 @@ Managed blocks are stationary after initial insertion. Cleanup detection uses Ja
2019
2043
 
2020
2044
  \`jeeves.config.json\` at workspace root provides shared defaults:
2021
2045
  - Precedence: CLI flags → env vars → file → defaults
2022
- - Namespaced: \`core.*\` (workspace, configRoot, gatewayUrl) and \`memory.*\` (budget, warningThreshold, staleDays)
2046
+ - Namespaced: \`core.*\` (workspace, configRoot, gatewayUrl, devRepos) and \`memory.*\` (budget, warningThreshold)
2023
2047
  - Inspect with \`jeeves config [jsonpath]\`
2024
2048
 
2025
2049
  ## HEARTBEAT Protocol
@@ -2203,46 +2227,21 @@ function registerInstallCommand(program) {
2203
2227
  }
2204
2228
 
2205
2229
  /**
2206
- * Memory budget accounting and staleness detection for MEMORY.md.
2230
+ * Memory budget accounting for MEMORY.md.
2207
2231
  *
2208
2232
  * @remarks
2209
- * Scans MEMORY.md for ISO date patterns in H2/H3 headings and bullet items.
2210
- * Reports character count against a configured budget, warning threshold state,
2211
- * and stale section candidates. Does not auto-delete: review remains
2212
- * human- or agent-mediated (Decision 42).
2233
+ * Reports character count against a configured budget and warning threshold
2234
+ * state. Does not auto-delete: review remains human- or agent-mediated
2235
+ * (Decision 42). Staleness detection removed in v0.5.9 (Decision 45).
2213
2236
  */
2214
- /** ISO date pattern: YYYY-MM-DD. */
2215
- const ISO_DATE_RE = /\b(\d{4}-\d{2}-\d{2})\b/g;
2216
- /** H2 heading pattern used to split sections. */
2217
- const H2_RE = /^## /m;
2218
2237
  /**
2219
- * Extract the most recent ISO date from a string.
2220
- *
2221
- * @param text - Text to scan for dates.
2222
- * @returns The most recent date found, or undefined.
2223
- */
2224
- function extractMostRecentDate(text) {
2225
- const matches = text.match(ISO_DATE_RE);
2226
- if (!matches)
2227
- return undefined;
2228
- let latest;
2229
- for (const match of matches) {
2230
- const d = new Date(match + 'T00:00:00Z');
2231
- if (!Number.isNaN(d.getTime())) {
2232
- if (!latest || d > latest)
2233
- latest = d;
2234
- }
2235
- }
2236
- return latest;
2237
- }
2238
- /**
2239
- * Analyze MEMORY.md for budget and staleness.
2238
+ * Analyze MEMORY.md for budget health.
2240
2239
  *
2241
2240
  * @param options - Analysis configuration.
2242
2241
  * @returns Memory hygiene result.
2243
2242
  */
2244
2243
  function analyzeMemory(options) {
2245
- const { workspacePath, budget, warningThreshold, staleDays } = options;
2244
+ const { workspacePath, budget, warningThreshold } = options;
2246
2245
  const memoryPath = join(workspacePath, WORKSPACE_FILES.memory);
2247
2246
  if (!existsSync(memoryPath)) {
2248
2247
  return {
@@ -2252,8 +2251,6 @@ function analyzeMemory(options) {
2252
2251
  usage: 0,
2253
2252
  warning: false,
2254
2253
  overBudget: false,
2255
- staleCandidates: 0,
2256
- staleSectionNames: [],
2257
2254
  };
2258
2255
  }
2259
2256
  const content = readFileSync(memoryPath, 'utf-8');
@@ -2261,21 +2258,6 @@ function analyzeMemory(options) {
2261
2258
  const usage = budget > 0 ? charCount / budget : charCount > 0 ? Infinity : 0;
2262
2259
  const warning = usage >= warningThreshold;
2263
2260
  const overBudget = usage > 1;
2264
- // Split into H2 sections and scan for staleness
2265
- const sections = content.split(H2_RE).slice(1); // skip content before first H2
2266
- const now = Date.now();
2267
- const thresholdMs = staleDays * 24 * 60 * 60 * 1000;
2268
- const staleSectionNames = [];
2269
- for (const section of sections) {
2270
- const sectionName = section.split('\n')[0]?.trim() ?? '';
2271
- const recentDate = extractMostRecentDate(section);
2272
- // Sections without dates are evergreen — never flagged (Decision 47)
2273
- if (!recentDate)
2274
- continue;
2275
- if (now - recentDate.getTime() > thresholdMs) {
2276
- staleSectionNames.push(sectionName);
2277
- }
2278
- }
2279
2261
  return {
2280
2262
  exists: true,
2281
2263
  charCount,
@@ -2283,8 +2265,6 @@ function analyzeMemory(options) {
2283
2265
  usage,
2284
2266
  warning,
2285
2267
  overBudget,
2286
- staleCandidates: staleSectionNames.length,
2287
- staleSectionNames,
2288
2268
  };
2289
2269
  }
2290
2270
 
@@ -2383,7 +2363,6 @@ function registerStatusCommand(program) {
2383
2363
  workspacePath: getWorkspacePath(),
2384
2364
  budget: resolved.memory.budget.value,
2385
2365
  warningThreshold: resolved.memory.warningThreshold.value,
2386
- staleDays: resolved.memory.staleDays.value,
2387
2366
  });
2388
2367
  console.log('Memory hygiene');
2389
2368
  console.log('-'.repeat(60));
@@ -2398,7 +2377,6 @@ function registerStatusCommand(program) {
2398
2377
  ? '⚠ Warning'
2399
2378
  : '✅ OK';
2400
2379
  console.log(`Chars: ${String(memory.charCount)} / ${String(memory.budget)} (${String(usagePct)}%) — ${status}`);
2401
- console.log(`Stale candidates: ${String(memory.staleCandidates)}`);
2402
2380
  }
2403
2381
  console.log();
2404
2382
  if (!allHealthy) {
@@ -130,14 +130,14 @@ const COMPONENT_VERSIONS_FILE = 'component-versions.json';
130
130
  * Core library version, inlined at build time.
131
131
  *
132
132
  * @remarks
133
- * The `0.5.7` placeholder is replaced by
133
+ * The `0.5.8` placeholder is replaced by
134
134
  * `@rollup/plugin-replace` during the build with the actual version
135
135
  * from `package.json`. This ensures the correct version survives
136
136
  * when consumers bundle core into their own dist (where runtime
137
137
  * `import.meta.url`-based resolution would find the wrong package.json).
138
138
  */
139
139
  /** The core library version from package.json (inlined at build time). */
140
- const CORE_VERSION = '0.5.7';
140
+ const CORE_VERSION = '0.5.8';
141
141
 
142
142
  /**
143
143
  * Shared file I/O helpers for managed section operations.
@@ -815,7 +815,7 @@ Managed blocks are stationary after initial insertion. Cleanup detection uses Ja
815
815
 
816
816
  \`jeeves.config.json\` at workspace root provides shared defaults:
817
817
  - Precedence: CLI flags → env vars → file → defaults
818
- - Namespaced: \`core.*\` (workspace, configRoot, gatewayUrl) and \`memory.*\` (budget, warningThreshold, staleDays)
818
+ - Namespaced: \`core.*\` (workspace, configRoot, gatewayUrl, devRepos) and \`memory.*\` (budget, warningThreshold)
819
819
  - Inspect with \`jeeves config [jsonpath]\`
820
820
 
821
821
  ## HEARTBEAT Protocol
@@ -731,6 +731,11 @@ const workspaceCoreConfigSchema = z
731
731
  configRoot: z.string().optional().describe('Platform config root path'),
732
732
  /** OpenClaw gateway URL. */
733
733
  gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
734
+ /** Dev repo paths keyed by component name. */
735
+ devRepos: z
736
+ .record(z.string(), z.string())
737
+ .optional()
738
+ .describe('Dev repo paths by component name'),
734
739
  })
735
740
  .partial();
736
741
  /** Memory shared config section. */
@@ -745,13 +750,6 @@ const workspaceMemoryConfigSchema = z
745
750
  .max(1)
746
751
  .optional()
747
752
  .describe('Memory warning threshold'),
748
- /** Staleness threshold in days. */
749
- staleDays: z
750
- .number()
751
- .int()
752
- .positive()
753
- .optional()
754
- .describe('Memory staleness threshold in days'),
755
753
  })
756
754
  .partial();
757
755
  /** Workspace config Zod schema. */
@@ -1029,11 +1027,6 @@ function createServiceCli(descriptor) {
1029
1027
  });
1030
1028
  // Apply custom CLI commands if provided
1031
1029
  if (descriptor.customCliCommands) {
1032
- // Cast required: @commander-js/extra-typings Command has generic type
1033
- // parameters that don't align with the descriptor's base Command type.
1034
- // The descriptor can't know the parent Command's exact generic parameters
1035
- // at definition time. The cast is safe — customCliCommands only adds
1036
- // subcommands and doesn't depend on the parent's generic state.
1037
1030
  descriptor.customCliCommands(program);
1038
1031
  }
1039
1032
  return program;
package/dist/index.d.ts CHANGED
@@ -295,11 +295,11 @@ declare const workspaceConfigSchema: z.ZodObject<{
295
295
  workspace: z.ZodOptional<z.ZodOptional<z.ZodString>>;
296
296
  configRoot: z.ZodOptional<z.ZodOptional<z.ZodString>>;
297
297
  gatewayUrl: z.ZodOptional<z.ZodOptional<z.ZodString>>;
298
+ devRepos: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>>;
298
299
  }, z.core.$strip>>;
299
300
  memory: z.ZodOptional<z.ZodObject<{
300
301
  budget: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
301
302
  warningThreshold: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
302
- staleDays: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
303
303
  }, z.core.$strip>>;
304
304
  }, z.core.$strip>;
305
305
  /** Workspace config type. */
@@ -327,8 +327,6 @@ declare const WORKSPACE_CONFIG_DEFAULTS: {
327
327
  readonly budget: 20_000;
328
328
  /** Default warning threshold as a fraction of budget (80%). */
329
329
  readonly warningThreshold: 0.8;
330
- /** Default staleness threshold in days. */
331
- readonly staleDays: 30;
332
330
  };
333
331
  };
334
332
  /** Provenance source for a resolved config value. */
@@ -396,8 +394,6 @@ interface ResolvedCliConfig {
396
394
  budget: ResolvedValue<number>;
397
395
  /** Resolved warning threshold as a fraction of budget. */
398
396
  warningThreshold: ResolvedValue<number>;
399
- /** Resolved staleness threshold in days. */
400
- staleDays: ResolvedValue<number>;
401
397
  };
402
398
  }
403
399
 
@@ -576,6 +572,7 @@ declare class ComponentWriter {
576
572
  private readonly gatewayUrl;
577
573
  private readonly pendingCleanups;
578
574
  private cyclePromise;
575
+ private stopped;
579
576
  /** @internal */
580
577
  constructor(component: JeevesComponentDescriptor, options?: ComponentWriterOptions);
581
578
  /** The component's config directory path. */
@@ -1355,13 +1352,12 @@ declare function formatEndMarker(markerText: string): string;
1355
1352
  declare function shouldWrite(myVersion: string, existing: VersionStamp | undefined, stalenessThresholdMs?: number): boolean;
1356
1353
 
1357
1354
  /**
1358
- * Memory budget accounting and staleness detection for MEMORY.md.
1355
+ * Memory budget accounting for MEMORY.md.
1359
1356
  *
1360
1357
  * @remarks
1361
- * Scans MEMORY.md for ISO date patterns in H2/H3 headings and bullet items.
1362
- * Reports character count against a configured budget, warning threshold state,
1363
- * and stale section candidates. Does not auto-delete: review remains
1364
- * human- or agent-mediated (Decision 42).
1358
+ * Reports character count against a configured budget and warning threshold
1359
+ * state. Does not auto-delete: review remains human- or agent-mediated
1360
+ * (Decision 42). Staleness detection removed in v0.5.9 (Decision 45).
1365
1361
  */
1366
1362
  /** Result of memory hygiene analysis. */
1367
1363
  interface MemoryHygieneResult {
@@ -1377,10 +1373,6 @@ interface MemoryHygieneResult {
1377
1373
  warning: boolean;
1378
1374
  /** Whether usage exceeds the budget. */
1379
1375
  overBudget: boolean;
1380
- /** Number of H2 sections flagged as stale candidates. */
1381
- staleCandidates: number;
1382
- /** Names of stale sections. */
1383
- staleSectionNames: string[];
1384
1376
  }
1385
1377
  /** Options for memory hygiene analysis. */
1386
1378
  interface MemoryHygieneOptions {
@@ -1390,18 +1382,9 @@ interface MemoryHygieneOptions {
1390
1382
  budget: number;
1391
1383
  /** Warning threshold as a fraction of budget (0–1). */
1392
1384
  warningThreshold: number;
1393
- /** Staleness threshold in days. */
1394
- staleDays: number;
1395
1385
  }
1396
1386
  /**
1397
- * Extract the most recent ISO date from a string.
1398
- *
1399
- * @param text - Text to scan for dates.
1400
- * @returns The most recent date found, or undefined.
1401
- */
1402
- declare function extractMostRecentDate(text: string): Date | undefined;
1403
- /**
1404
- * Analyze MEMORY.md for budget and staleness.
1387
+ * Analyze MEMORY.md for budget health.
1405
1388
  *
1406
1389
  * @param options - Analysis configuration.
1407
1390
  * @returns Memory hygiene result.
@@ -1439,6 +1422,7 @@ declare function checkMemoryHealth(options: MemoryHygieneOptions): HeartbeatEntr
1439
1422
  * Platform template with live data, and writes managed sections using
1440
1423
  * `updateManagedSection`.
1441
1424
  */
1425
+
1442
1426
  /** Options for refreshPlatformContent. */
1443
1427
  interface RefreshPlatformContentOptions {
1444
1428
  /** Core library version for version-stamp convergence. */
@@ -1453,6 +1437,8 @@ interface RefreshPlatformContentOptions {
1453
1437
  pluginPackage?: string;
1454
1438
  /** Staleness threshold override in ms. */
1455
1439
  stalenessThresholdMs?: number;
1440
+ /** Pre-loaded workspace config (avoids redundant reads when caller already loaded it). */
1441
+ workspaceConfig?: WorkspaceConfig;
1456
1442
  }
1457
1443
  /**
1458
1444
  * Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
@@ -1939,5 +1925,5 @@ declare function getErrorMessage(err: unknown): string;
1939
1925
  */
1940
1926
  declare function isTransientError(err: unknown): boolean;
1941
1927
 
1942
- 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, getComponentConfigPath, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getErrorMessage, getPackageRoot, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, isTransientError, jaccard, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, needsCleanup, nowIso, ok, orchestrateHeartbeat, parseArgs, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, readJson, readJsonl, refreshPlatformContent, registerComponentConfigPath, 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 };
1928
+ 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, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getComponentConfigPath, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getErrorMessage, getPackageRoot, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, isTransientError, jaccard, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, needsCleanup, nowIso, ok, orchestrateHeartbeat, parseArgs, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, readJson, readJsonl, refreshPlatformContent, registerComponentConfigPath, 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 };
1943
1929
  export type { AccountConfig, AsyncContentCacheOptions, ComponentDependencies, ComponentState, ComponentVersionEntry, ComponentVersionsState, ComponentWriterOptions, ConfigApplyHandler, ConfigApplyRequest, ConfigApplyResult, ConfigProvenance, ConfigQueryHandler, ConfigQueryResponse, CoreConfig, CreatePluginCliOptions, CreateStatusHandlerOptions, GoogleAuthOptions, HeartbeatEntry, InitOptions, JeevesComponentDescriptor, ManagedMarkers, ManagedSection, MemoryHygieneOptions, MemoryHygieneResult, OrchestrateHeartbeatOptions, ParseManagedResult, ParsedHeartbeat, PlatformComponent, PluginApi, PluginInstallRecord, RefreshPlatformContentOptions, RemoveManagedSectionOptions, ResolvedCliConfig, ResolvedValue, RetryOptions, RunOptions, SectionId, SeedContentOptions, ServiceAccountFileConfig, ServiceManager, ServiceManagerOptions, ServiceState, SlackWorkspaceOptions, StatusHandler, StatusHandlerResult, StatusResponse, ToolDescriptor, ToolRegistrationOptions, ToolResult, UpdateManagedSectionOptions, VersionStamp, WorkspaceConfig, WorkspaceOptions, WriteComponentVersionOptions };
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import cp, { execSync } from 'node:child_process';
10
10
  import { homedir } from 'node:os';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { packageDirectorySync } from 'package-directory';
13
+ import Handlebars from 'handlebars';
13
14
 
14
15
  /**
15
16
  * Comment markers for managed content blocks.
@@ -183,14 +184,14 @@ const PLATFORM_COMPONENTS = [
183
184
  * Core library version, inlined at build time.
184
185
  *
185
186
  * @remarks
186
- * The `0.5.7` placeholder is replaced by
187
+ * The `0.5.8` placeholder is replaced by
187
188
  * `@rollup/plugin-replace` during the build with the actual version
188
189
  * from `package.json`. This ensures the correct version survives
189
190
  * when consumers bundle core into their own dist (where runtime
190
191
  * `import.meta.url`-based resolution would find the wrong package.json).
191
192
  */
192
193
  /** The core library version from package.json (inlined at build time). */
193
- const CORE_VERSION = '0.5.7';
194
+ const CORE_VERSION = '0.5.8';
194
195
 
195
196
  /**
196
197
  * Workspace and config root initialization.
@@ -724,6 +725,11 @@ const workspaceCoreConfigSchema = z
724
725
  configRoot: z.string().optional().describe('Platform config root path'),
725
726
  /** OpenClaw gateway URL. */
726
727
  gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
728
+ /** Dev repo paths keyed by component name. */
729
+ devRepos: z
730
+ .record(z.string(), z.string())
731
+ .optional()
732
+ .describe('Dev repo paths by component name'),
727
733
  })
728
734
  .partial();
729
735
  /** Memory shared config section. */
@@ -738,13 +744,6 @@ const workspaceMemoryConfigSchema = z
738
744
  .max(1)
739
745
  .optional()
740
746
  .describe('Memory warning threshold'),
741
- /** Staleness threshold in days. */
742
- staleDays: z
743
- .number()
744
- .int()
745
- .positive()
746
- .optional()
747
- .describe('Memory staleness threshold in days'),
748
747
  })
749
748
  .partial();
750
749
  /** Workspace config Zod schema. */
@@ -772,7 +771,6 @@ const WORKSPACE_CONFIG_DEFAULTS = {
772
771
  memory: {
773
772
  budget: 20_000,
774
773
  warningThreshold: 0.8,
775
- staleDays: 30,
776
774
  },
777
775
  };
778
776
  /**
@@ -840,6 +838,11 @@ function generateWorkspaceJsonSchema() {
840
838
  type: 'string',
841
839
  default: WORKSPACE_CONFIG_DEFAULTS.core.gatewayUrl,
842
840
  },
841
+ devRepos: {
842
+ type: 'object',
843
+ additionalProperties: { type: 'string' },
844
+ description: 'Dev repo paths by component name (e.g. { "core": "D:\\\\repos\\\\jeeves" })',
845
+ },
843
846
  },
844
847
  },
845
848
  memory: {
@@ -856,11 +859,6 @@ function generateWorkspaceJsonSchema() {
856
859
  maximum: 1,
857
860
  default: WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold,
858
861
  },
859
- staleDays: {
860
- type: 'integer',
861
- minimum: 1,
862
- default: WORKSPACE_CONFIG_DEFAULTS.memory.staleDays,
863
- },
864
862
  },
865
863
  },
866
864
  },
@@ -904,7 +902,6 @@ function resolveCliConfig(opts) {
904
902
  memory: {
905
903
  budget: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_BUDGET'), fileConfig?.memory?.budget, WORKSPACE_CONFIG_DEFAULTS.memory.budget),
906
904
  warningThreshold: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_WARNING_THRESHOLD'), fileConfig?.memory?.warningThreshold, WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold),
907
- staleDays: resolveConfigValue(undefined, readNumericEnv('JEEVES_MEMORY_STALE_DAYS'), fileConfig?.memory?.staleDays, WORKSPACE_CONFIG_DEFAULTS.memory.staleDays),
908
905
  },
909
906
  };
910
907
  }
@@ -1574,7 +1571,7 @@ Managed blocks are stationary after initial insertion. Cleanup detection uses Ja
1574
1571
 
1575
1572
  \`jeeves.config.json\` at workspace root provides shared defaults:
1576
1573
  - Precedence: CLI flags → env vars → file → defaults
1577
- - Namespaced: \`core.*\` (workspace, configRoot, gatewayUrl) and \`memory.*\` (budget, warningThreshold, staleDays)
1574
+ - Namespaced: \`core.*\` (workspace, configRoot, gatewayUrl, devRepos) and \`memory.*\` (budget, warningThreshold)
1578
1575
  - Inspect with \`jeeves config [jsonpath]\`
1579
1576
 
1580
1577
  ## HEARTBEAT Protocol
@@ -3214,11 +3211,6 @@ function createServiceCli(descriptor) {
3214
3211
  });
3215
3212
  // Apply custom CLI commands if provided
3216
3213
  if (descriptor.customCliCommands) {
3217
- // Cast required: @commander-js/extra-typings Command has generic type
3218
- // parameters that don't align with the descriptor's base Command type.
3219
- // The descriptor can't know the parent Command's exact generic parameters
3220
- // at definition time. The cast is safe — customCliCommands only adds
3221
- // subcommands and doesn't depend on the parent's generic state.
3222
3214
  descriptor.customCliCommands(program);
3223
3215
  }
3224
3216
  return program;
@@ -3503,7 +3495,7 @@ Periodic checks (email, calendar, mentions) belong in jeeves-runner scripts, not
3503
3495
  ## Platform Surface Conventions
3504
3496
 
3505
3497
  **Slack:**
3506
- - No threaded replies by default
3498
+ - Never initiate a threaded reply. Only reply within a thread started by a human.
3507
3499
  - Use \`<#C…>\` for channel references
3508
3500
 
3509
3501
  **Table formatting:** On channels that do not support Markdown tables (Slack, Discord, WhatsApp, IRC), use code-block tables with aligned columns. Markdown tables are only safe in contexts that render them (GitHub, jeeves-server, files).
@@ -3709,8 +3701,8 @@ Never manually edit \`~/.openclaw/extensions/\`. Always use the CLI commands abo
3709
3701
 
3710
3702
  ### Reference Templates
3711
3703
 
3712
- <!-- IF_TEMPLATES -->
3713
- Reference templates are available at \`__TEMPLATE_PATH__\`:
3704
+ {{#if templatePath}}
3705
+ Reference templates are available at \`{{templatePath}}\`:
3714
3706
 
3715
3707
  | Template | Purpose |
3716
3708
  |----------|---------|
@@ -3718,9 +3710,39 @@ Reference templates are available at \`__TEMPLATE_PATH__\`:
3718
3710
  | \`spec-to-code-guide.md\` | The spec-to-code development practice — 7-stage iterative process, convergence loops, release gates |
3719
3711
 
3720
3712
  Read these templates when creating new specs, onboarding to new projects, or when asked about the development process.
3721
- <!-- ELSE_TEMPLATES -->
3713
+ {{else}}
3722
3714
  > Reference templates not yet installed. Run \`npx @karmaniverous/jeeves install\` to seed templates.
3723
- <!-- ENDIF_TEMPLATES -->
3715
+ {{/if}}
3716
+
3717
+ ### Post-Upgrade Maintenance
3718
+
3719
+ After updating OpenClaw (\`npm install -g openclaw@latest\` or equivalent), reinstall all Jeeves component plugins to repair install state:
3720
+
3721
+ \`\`\`bash
3722
+ npx @karmaniverous/jeeves install
3723
+ npx @karmaniverous/jeeves-runner-openclaw install
3724
+ npx @karmaniverous/jeeves-watcher-openclaw install
3725
+ npx @karmaniverous/jeeves-server-openclaw install
3726
+ npx @karmaniverous/jeeves-meta-openclaw install
3727
+ \`\`\`
3728
+
3729
+ Then restart the gateway. Plugin installers copy dist files and patch config; reinstalling after an OpenClaw update ensures the extensions directory stays consistent.
3730
+
3731
+ ### Source Code Preference
3732
+
3733
+ {{#if devRepos}}
3734
+ When investigating, debugging, or analyzing Jeeves components, always read TypeScript source from dev repos — never compiled \`dist/\` from the global npm install. Dev repos:
3735
+
3736
+ | Component | Dev Repo |
3737
+ |-----------|----------|
3738
+ {{#each devRepos}}
3739
+ | {{@key}} | \`{{this}}\` |
3740
+ {{/each}}
3741
+
3742
+ Built code is minified, harder to reason about, and wastes context. Always \`git pull\` before analysis.
3743
+ {{else}}
3744
+ > Dev repo paths not configured. Add \`core.devRepos\` to \`jeeves.config.json\` to enable source code preference guidance.
3745
+ {{/if}}
3724
3746
  `;
3725
3747
 
3726
3748
  /**
@@ -3732,6 +3754,10 @@ Read these templates when creating new specs, onboarding to new projects, or whe
3732
3754
  * Platform template with live data, and writes managed sections using
3733
3755
  * `updateManagedSection`.
3734
3756
  */
3757
+ /** Compiled Handlebars template for the Platform section (cached at module level). */
3758
+ const compiledPlatformTemplate = Handlebars.compile(toolsPlatformTemplate, {
3759
+ noEscape: true,
3760
+ });
3735
3761
  /**
3736
3762
  * Resolve the package's content directory for template file copying.
3737
3763
  *
@@ -3775,23 +3801,13 @@ function copyTemplates(coreConfigDir) {
3775
3801
  cpSync(sourceDir, destDir, { recursive: true });
3776
3802
  }
3777
3803
  /**
3778
- * Render the Platform template using simple string replacement.
3804
+ * Render the Platform template using Handlebars.
3779
3805
  *
3780
- * @param templatePath - Path to the templates directory.
3806
+ * @param context - Template context with optional templatePath and devRepos.
3781
3807
  * @returns Rendered platform content string.
3782
3808
  */
3783
- function renderPlatformTemplate(templatePath) {
3784
- const templatesAvailable = existsSync(templatePath);
3785
- let content = toolsPlatformTemplate;
3786
- // Handle <!-- IF_TEMPLATES --> ... <!-- ELSE_TEMPLATES --> ... <!-- ENDIF_TEMPLATES --> block
3787
- const ifRegex = /<!-- IF_TEMPLATES -->([\s\S]*?)<!-- ELSE_TEMPLATES -->([\s\S]*?)<!-- ENDIF_TEMPLATES -->/;
3788
- const match = ifRegex.exec(content);
3789
- if (match) {
3790
- content = content.replace(match[0], templatesAvailable ? match[1] : match[2]);
3791
- }
3792
- // Replace __TEMPLATE_PATH__ with the actual path
3793
- content = content.replace(/__TEMPLATE_PATH__/g, templatePath);
3794
- return content;
3809
+ function renderPlatformTemplate(context) {
3810
+ return compiledPlatformTemplate(context);
3795
3811
  }
3796
3812
  /**
3797
3813
  * Refresh platform content: SOUL.md, AGENTS.md, and TOOLS.md Platform section.
@@ -3799,7 +3815,7 @@ function renderPlatformTemplate(templatePath) {
3799
3815
  * @param options - Configuration for the refresh cycle.
3800
3816
  */
3801
3817
  async function refreshPlatformContent(options) {
3802
- const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, } = options;
3818
+ const { coreVersion, componentName, componentVersion, servicePackage, pluginPackage, stalenessThresholdMs, workspaceConfig, } = options;
3803
3819
  const workspacePath = getWorkspacePath();
3804
3820
  const coreConfigDir = getCoreConfigDir();
3805
3821
  // 1. Write calling component's version entry
@@ -3813,7 +3829,11 @@ async function refreshPlatformContent(options) {
3813
3829
  }
3814
3830
  // 2. Render Platform template
3815
3831
  const templatePath = join(coreConfigDir, TEMPLATES_DIR);
3816
- const platformContent = renderPlatformTemplate(templatePath);
3832
+ const wsConfig = workspaceConfig ?? loadWorkspaceConfig(workspacePath);
3833
+ const platformContent = renderPlatformTemplate({
3834
+ templatePath: existsSync(templatePath) ? templatePath : undefined,
3835
+ devRepos: wsConfig?.core?.devRepos,
3836
+ });
3817
3837
  // 3. Write TOOLS.md Platform section
3818
3838
  const toolsPath = join(workspacePath, WORKSPACE_FILES.tools);
3819
3839
  await updateManagedSection(toolsPath, platformContent, {
@@ -3941,46 +3961,21 @@ function scanAndEscalateCleanup(targets, gatewayUrl, pendingCleanups) {
3941
3961
  }
3942
3962
 
3943
3963
  /**
3944
- * Memory budget accounting and staleness detection for MEMORY.md.
3964
+ * Memory budget accounting for MEMORY.md.
3945
3965
  *
3946
3966
  * @remarks
3947
- * Scans MEMORY.md for ISO date patterns in H2/H3 headings and bullet items.
3948
- * Reports character count against a configured budget, warning threshold state,
3949
- * and stale section candidates. Does not auto-delete: review remains
3950
- * human- or agent-mediated (Decision 42).
3967
+ * Reports character count against a configured budget and warning threshold
3968
+ * state. Does not auto-delete: review remains human- or agent-mediated
3969
+ * (Decision 42). Staleness detection removed in v0.5.9 (Decision 45).
3951
3970
  */
3952
- /** ISO date pattern: YYYY-MM-DD. */
3953
- const ISO_DATE_RE = /\b(\d{4}-\d{2}-\d{2})\b/g;
3954
- /** H2 heading pattern used to split sections. */
3955
- const H2_RE = /^## /m;
3956
- /**
3957
- * Extract the most recent ISO date from a string.
3958
- *
3959
- * @param text - Text to scan for dates.
3960
- * @returns The most recent date found, or undefined.
3961
- */
3962
- function extractMostRecentDate(text) {
3963
- const matches = text.match(ISO_DATE_RE);
3964
- if (!matches)
3965
- return undefined;
3966
- let latest;
3967
- for (const match of matches) {
3968
- const d = new Date(match + 'T00:00:00Z');
3969
- if (!Number.isNaN(d.getTime())) {
3970
- if (!latest || d > latest)
3971
- latest = d;
3972
- }
3973
- }
3974
- return latest;
3975
- }
3976
3971
  /**
3977
- * Analyze MEMORY.md for budget and staleness.
3972
+ * Analyze MEMORY.md for budget health.
3978
3973
  *
3979
3974
  * @param options - Analysis configuration.
3980
3975
  * @returns Memory hygiene result.
3981
3976
  */
3982
3977
  function analyzeMemory(options) {
3983
- const { workspacePath, budget, warningThreshold, staleDays } = options;
3978
+ const { workspacePath, budget, warningThreshold } = options;
3984
3979
  const memoryPath = join(workspacePath, WORKSPACE_FILES.memory);
3985
3980
  if (!existsSync(memoryPath)) {
3986
3981
  return {
@@ -3990,8 +3985,6 @@ function analyzeMemory(options) {
3990
3985
  usage: 0,
3991
3986
  warning: false,
3992
3987
  overBudget: false,
3993
- staleCandidates: 0,
3994
- staleSectionNames: [],
3995
3988
  };
3996
3989
  }
3997
3990
  const content = readFileSync(memoryPath, 'utf-8');
@@ -3999,21 +3992,6 @@ function analyzeMemory(options) {
3999
3992
  const usage = budget > 0 ? charCount / budget : charCount > 0 ? Infinity : 0;
4000
3993
  const warning = usage >= warningThreshold;
4001
3994
  const overBudget = usage > 1;
4002
- // Split into H2 sections and scan for staleness
4003
- const sections = content.split(H2_RE).slice(1); // skip content before first H2
4004
- const now = Date.now();
4005
- const thresholdMs = staleDays * 24 * 60 * 60 * 1000;
4006
- const staleSectionNames = [];
4007
- for (const section of sections) {
4008
- const sectionName = section.split('\n')[0]?.trim() ?? '';
4009
- const recentDate = extractMostRecentDate(section);
4010
- // Sections without dates are evergreen — never flagged (Decision 47)
4011
- if (!recentDate)
4012
- continue;
4013
- if (now - recentDate.getTime() > thresholdMs) {
4014
- staleSectionNames.push(sectionName);
4015
- }
4016
- }
4017
3995
  return {
4018
3996
  exists: true,
4019
3997
  charCount,
@@ -4021,8 +3999,6 @@ function analyzeMemory(options) {
4021
3999
  usage,
4022
4000
  warning,
4023
4001
  overBudget,
4024
- staleCandidates: staleSectionNames.length,
4025
- staleSectionNames,
4026
4002
  };
4027
4003
  }
4028
4004
 
@@ -4049,20 +4025,14 @@ function checkMemoryHealth(options) {
4049
4025
  const result = analyzeMemory(options);
4050
4026
  if (!result.exists)
4051
4027
  return undefined;
4052
- if (!result.warning && result.staleCandidates === 0)
4028
+ if (!result.warning)
4053
4029
  return undefined;
4054
- const lines = [];
4055
- if (result.warning) {
4056
- const pct = Math.round(result.usage * 100);
4057
- lines.push(`- Budget: ${result.charCount.toLocaleString()} / ${result.budget.toLocaleString()} chars (${String(pct)}%).${result.overBudget ? ' **Over budget.**' : ' Consider reviewing.'}`);
4058
- }
4059
- if (result.staleCandidates > 0) {
4060
- lines.push(`- ${String(result.staleCandidates)} stale section${result.staleCandidates === 1 ? '' : 's'}: ${result.staleSectionNames.join(', ')}`);
4061
- }
4030
+ const pct = Math.round(result.usage * 100);
4031
+ const content = `- Budget: ${result.charCount.toLocaleString()} / ${result.budget.toLocaleString()} chars (${String(pct)}%).${result.overBudget ? ' **Over budget.**' : ' Consider reviewing.'}`;
4062
4032
  return {
4063
4033
  name: MEMORY_HEARTBEAT_NAME,
4064
4034
  declined: false,
4065
- content: lines.join('\n'),
4035
+ content,
4066
4036
  };
4067
4037
  }
4068
4038
 
@@ -4302,9 +4272,8 @@ function getServiceUrl(serviceName, consumerName) {
4302
4272
  if (coreUrl)
4303
4273
  return coreUrl;
4304
4274
  // 3. Fall back to port constants
4305
- const port = DEFAULT_PORTS[serviceName];
4306
- if (port !== undefined) {
4307
- return `http://127.0.0.1:${String(port)}`;
4275
+ if (serviceName in DEFAULT_PORTS) {
4276
+ return `http://127.0.0.1:${String(DEFAULT_PORTS[serviceName])}`;
4308
4277
  }
4309
4278
  throw new Error(`jeeves-core: unknown service "${serviceName}" and no config found`);
4310
4279
  }
@@ -4628,6 +4597,7 @@ function readFileOrEmpty(filePath) {
4628
4597
  */
4629
4598
  async function runHeartbeatCycle(options) {
4630
4599
  const { workspacePath, coreConfigDir, configRoot } = options;
4600
+ const wsConfig = options.workspaceConfig ?? loadWorkspaceConfig(workspacePath);
4631
4601
  const heartbeatPath = join(workspacePath, WORKSPACE_FILES.heartbeat);
4632
4602
  try {
4633
4603
  const existingContent = readFileOrEmpty(heartbeatPath);
@@ -4640,14 +4610,11 @@ async function runHeartbeatCycle(options) {
4640
4610
  });
4641
4611
  // Memory hygiene check (Decision 49)
4642
4612
  if (!declinedNames.has(MEMORY_HEARTBEAT_NAME)) {
4643
- const wsConfig = loadWorkspaceConfig(workspacePath);
4644
4613
  const memoryEntry = checkMemoryHealth({
4645
4614
  workspacePath,
4646
4615
  budget: wsConfig?.memory?.budget ?? WORKSPACE_CONFIG_DEFAULTS.memory.budget,
4647
4616
  warningThreshold: wsConfig?.memory?.warningThreshold ??
4648
4617
  WORKSPACE_CONFIG_DEFAULTS.memory.warningThreshold,
4649
- staleDays: wsConfig?.memory?.staleDays ??
4650
- WORKSPACE_CONFIG_DEFAULTS.memory.staleDays,
4651
4618
  });
4652
4619
  if (memoryEntry)
4653
4620
  entries.push(memoryEntry);
@@ -4701,6 +4668,7 @@ class ComponentWriter {
4701
4668
  gatewayUrl;
4702
4669
  pendingCleanups = new Set();
4703
4670
  cyclePromise;
4671
+ stopped = false;
4704
4672
  /** @internal */
4705
4673
  constructor(component, options) {
4706
4674
  this.component = component;
@@ -4726,6 +4694,7 @@ class ComponentWriter {
4726
4694
  * contention on startup.
4727
4695
  */
4728
4696
  start() {
4697
+ this.stopped = false;
4729
4698
  if (this.isRunning)
4730
4699
  return;
4731
4700
  // Random jitter up to one full interval to spread initial writes
@@ -4738,6 +4707,7 @@ class ComponentWriter {
4738
4707
  }
4739
4708
  /** Stop the writer timer. */
4740
4709
  stop() {
4710
+ this.stopped = true;
4741
4711
  if (this.jitterTimeout) {
4742
4712
  clearTimeout(this.jitterTimeout);
4743
4713
  this.jitterTimeout = undefined;
@@ -4751,7 +4721,7 @@ class ComponentWriter {
4751
4721
  this.timer = setTimeout(() => {
4752
4722
  this.timer = undefined;
4753
4723
  void this.cycle().finally(() => {
4754
- if (this.isRunning)
4724
+ if (!this.stopped)
4755
4725
  this.scheduleNextCycle(intervalMs, intervalMs);
4756
4726
  });
4757
4727
  }, delayMs);
@@ -4786,15 +4756,18 @@ class ComponentWriter {
4786
4756
  markers: TOOLS_MARKERS,
4787
4757
  coreVersion: CORE_VERSION,
4788
4758
  });
4789
- // 2. Platform content maintenance
4759
+ // 2. Load workspace config once for the entire cycle
4760
+ const workspaceConfig = loadWorkspaceConfig(workspacePath);
4761
+ // 3. Platform content maintenance
4790
4762
  await refreshPlatformContent({
4791
4763
  coreVersion: CORE_VERSION,
4792
4764
  componentName: this.component.name,
4793
4765
  componentVersion: this.component.version,
4794
4766
  servicePackage: this.component.servicePackage,
4795
4767
  pluginPackage: this.component.pluginPackage,
4768
+ workspaceConfig,
4796
4769
  });
4797
- // 3. Cleanup escalation
4770
+ // 4. Cleanup escalation
4798
4771
  if (this.gatewayUrl) {
4799
4772
  scanAndEscalateCleanup([
4800
4773
  { filePath: toolsPath, markerIdentity: 'TOOLS' },
@@ -4808,11 +4781,12 @@ class ComponentWriter {
4808
4781
  },
4809
4782
  ], this.gatewayUrl, this.pendingCleanups);
4810
4783
  }
4811
- // 4. HEARTBEAT orchestration
4784
+ // 5. HEARTBEAT orchestration
4812
4785
  await runHeartbeatCycle({
4813
4786
  workspacePath,
4814
4787
  coreConfigDir: getCoreConfigDir(),
4815
4788
  configRoot: getConfigRoot(),
4789
+ workspaceConfig,
4816
4790
  });
4817
4791
  });
4818
4792
  }
@@ -5385,4 +5359,4 @@ async function getChannelWorkspace(channelId, token, options) {
5385
5359
  return teamId;
5386
5360
  }
5387
5361
 
5388
- 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, getComponentConfigPath, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getErrorMessage, getPackageRoot, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, isTransientError, jaccard, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, needsCleanup, nowIso, ok, orchestrateHeartbeat, parseArgs, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, readJson, readJsonl, refreshPlatformContent, registerComponentConfigPath, 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 };
5362
+ 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, fail, fetchJson, fetchWithTimeout, formatBeginMarker, formatEndMarker, generateJsonSchema, generateWorkspaceJsonSchema, getArg, getBindAddress, getChannelWorkspace, getComponentConfigDir, getComponentConfigPath, getConfigRoot, getCoreConfigDir, getCoreConfigFile, getEffectiveServiceName, getErrorMessage, getPackageRoot, getPackageVersion, getServiceState, getServiceUrl, getWorkspacePath, init, isPrime, isTransientError, jaccard, jeevesComponentDescriptorSchema, loadEnvFile, loadWorkspaceConfig, needsCleanup, nowIso, ok, orchestrateHeartbeat, parseArgs, parseHeartbeat, parseManaged, patchConfig, postJson, readComponentVersions, readJson, readJsonl, refreshPlatformContent, registerComponentConfigPath, 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 };
package/package.json CHANGED
@@ -14,16 +14,17 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "commander": "^14.0.3",
17
+ "handlebars": "^4.7.9",
17
18
  "jsonpath-plus": "^10.4.0",
18
19
  "package-directory": "^8.2.0",
19
20
  "proper-lockfile": "^4.1.2",
20
21
  "semver": "^7.7.4",
21
- "zod": "^4.3.6"
22
+ "zod": "^4.4.2"
22
23
  },
23
24
  "description": "Shared library and CLI for the Jeeves AI assistant platform.",
24
25
  "devDependencies": {
25
26
  "@commander-js/extra-typings": "^14.0.0",
26
- "@dotenvx/dotenvx": "^1.59.1",
27
+ "@dotenvx/dotenvx": "^1.64.0",
27
28
  "@eslint/js": "^10.0.1",
28
29
  "@rollup/plugin-alias": "^6.0.0",
29
30
  "@rollup/plugin-commonjs": "^29.0.2",
@@ -32,33 +33,33 @@
32
33
  "@rollup/plugin-replace": "^6.0.3",
33
34
  "@rollup/plugin-typescript": "^12.3.0",
34
35
  "@types/fs-extra": "^11.0.4",
35
- "@types/node": "^25.5.0",
36
+ "@types/node": "^25.6.0",
36
37
  "@types/proper-lockfile": "^4.1.4",
37
38
  "@types/semver": "^7.7.1",
38
- "@vitest/coverage-v8": "^4.1.2",
39
- "@vitest/eslint-plugin": "^1.6.13",
39
+ "@vitest/coverage-v8": "^4.1.5",
40
+ "@vitest/eslint-plugin": "^1.6.16",
40
41
  "auto-changelog": "^2.5.0",
41
42
  "cross-env": "^10.1.0",
42
- "eslint": "^10.1.0",
43
+ "eslint": "^10.3.0",
43
44
  "eslint-config-prettier": "^10.1.8",
44
45
  "eslint-plugin-prettier": "^5.5.5",
45
46
  "eslint-plugin-simple-import-sort": "^12.1.1",
46
47
  "eslint-plugin-tsdoc": "^0.5.2",
47
48
  "fs-extra": "^11.3.4",
48
- "knip": "^6.1.0",
49
- "lefthook": "^2.1.4",
50
- "prettier": "^3.8.1",
49
+ "knip": "^6.11.0",
50
+ "lefthook": "^2.1.6",
51
+ "prettier": "^3.8.3",
51
52
  "release-it": "^19.2.4",
52
53
  "rimraf": "^6.1.3",
53
- "rollup": "^4.60.1",
54
+ "rollup": "^4.60.2",
54
55
  "rollup-plugin-dts": "^6.4.1",
55
56
  "tslib": "^2.8.1",
56
- "typedoc": "^0.28.18",
57
+ "typedoc": "^0.28.19",
57
58
  "typedoc-plugin-mdn-links": "^5.1.1",
58
59
  "typedoc-plugin-replace-text": "^4.2.0",
59
60
  "typescript": "^5.9.3",
60
- "typescript-eslint": "^8.57.2",
61
- "vitest": "^4.1.2"
61
+ "typescript-eslint": "^8.59.1",
62
+ "vitest": "^4.1.5"
62
63
  },
63
64
  "engines": {
64
65
  "node": ">=22"
@@ -139,5 +140,5 @@
139
140
  },
140
141
  "type": "module",
141
142
  "types": "dist/index.d.ts",
142
- "version": "0.5.8"
143
+ "version": "0.5.9"
143
144
  }