@karmaniverous/jeeves-meta 0.12.4 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,13 +9,13 @@ import require$$4 from 'util';
9
9
  import require$$5 from 'assert';
10
10
  import require$$2 from 'events';
11
11
  import vm from 'vm';
12
+ import { z } from 'zod';
12
13
  import * as commander from 'commander';
13
14
  import { homedir, tmpdir } from 'node:os';
14
- import { z } from 'zod';
15
15
  import { execSync } from 'node:child_process';
16
16
  import { fileURLToPath } from 'node:url';
17
- import { readFile, unlink, mkdir, writeFile, copyFile } from 'node:fs/promises';
18
17
  import { randomUUID, createHash } from 'node:crypto';
18
+ import { readFile, unlink, mkdir, writeFile, copyFile } from 'node:fs/promises';
19
19
  import pino from 'pino';
20
20
  import Handlebars from 'handlebars';
21
21
  import { Cron } from 'croner';
@@ -7017,6 +7017,32 @@ const COMPONENT_CONFIG_PREFIX = 'jeeves-';
7017
7017
  /** Core config file name. */
7018
7018
  const CONFIG_FILE = 'config.json';
7019
7019
 
7020
+ /**
7021
+ * Default port assignments for Jeeves platform services.
7022
+ *
7023
+ * @remarks
7024
+ * Each port number is a historical reference:
7025
+ * - 1934: Wodehouse's *Thank You, Jeeves*; Popper's *Logic of Scientific Discovery*
7026
+ * - 1936: Turing's "On Computable Numbers"; Church's lambda calculus
7027
+ * - 1937: Turing's paper in *Proceedings of the London Mathematical Society*
7028
+ * - 1938: Wodehouse's *The Code of the Woosters*; Shannon's relay/switching paper
7029
+ */
7030
+ /** Default port for jeeves-server. */
7031
+ const SERVER_PORT = 1934;
7032
+ /** Default port for jeeves-watcher. */
7033
+ const WATCHER_PORT = 1936;
7034
+ /** Default port for jeeves-runner. */
7035
+ const RUNNER_PORT = 1937;
7036
+ /** Default port for jeeves-meta. */
7037
+ const META_PORT = 1938;
7038
+ /** Map of service names to their default ports. */
7039
+ const DEFAULT_PORTS = {
7040
+ server: SERVER_PORT,
7041
+ watcher: WATCHER_PORT,
7042
+ runner: RUNNER_PORT,
7043
+ meta: META_PORT,
7044
+ };
7045
+
7020
7046
  /**
7021
7047
  * Workspace and config root initialization.
7022
7048
  *
@@ -7305,6 +7331,64 @@ function createStatusHandler(options) {
7305
7331
  };
7306
7332
  };
7307
7333
  }
7334
+ /** Core shared config section. */
7335
+ const workspaceCoreConfigSchema = z
7336
+ .object({
7337
+ /** Workspace root path. */
7338
+ workspace: z.string().optional().describe('Workspace root path'),
7339
+ /** Platform config root path. */
7340
+ configRoot: z.string().optional().describe('Platform config root path'),
7341
+ /** OpenClaw gateway URL. */
7342
+ gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
7343
+ })
7344
+ .partial();
7345
+ /** Memory shared config section. */
7346
+ const workspaceMemoryConfigSchema = z
7347
+ .object({
7348
+ /** MEMORY.md character budget. */
7349
+ budget: z.number().int().positive().optional().describe('Memory budget'),
7350
+ /** Warning threshold as a fraction of budget. */
7351
+ warningThreshold: z
7352
+ .number()
7353
+ .min(0)
7354
+ .max(1)
7355
+ .optional()
7356
+ .describe('Memory warning threshold'),
7357
+ /** Staleness threshold in days. */
7358
+ staleDays: z
7359
+ .number()
7360
+ .int()
7361
+ .positive()
7362
+ .optional()
7363
+ .describe('Memory staleness threshold in days'),
7364
+ })
7365
+ .partial();
7366
+ /** Workspace config Zod schema. */
7367
+ z.object({
7368
+ /** JSON Schema pointer for IDE autocomplete. */
7369
+ $schema: z.string().optional().describe('JSON Schema pointer'),
7370
+ /** Core shared defaults. */
7371
+ core: workspaceCoreConfigSchema.optional(),
7372
+ /** Memory hygiene shared defaults. */
7373
+ memory: workspaceMemoryConfigSchema.optional(),
7374
+ });
7375
+ /** Built-in workspace config defaults. */
7376
+ const WORKSPACE_CONFIG_DEFAULTS = {
7377
+ core: {
7378
+ workspace: '.',
7379
+ configRoot: './config'}};
7380
+
7381
+ /**
7382
+ * Shared CLI defaults and resolution for Jeeves CLI commands.
7383
+ *
7384
+ * @remarks
7385
+ * All root CLI commands share workspace/config-root resolution. Values follow
7386
+ * the shared precedence model: flags → env → jeeves.config.json → defaults.
7387
+ */
7388
+ /** Default workspace path. */
7389
+ const DEFAULT_WORKSPACE = WORKSPACE_CONFIG_DEFAULTS.core.workspace;
7390
+ /** Default config root path. */
7391
+ const DEFAULT_CONFIG_ROOT = WORKSPACE_CONFIG_DEFAULTS.core.configRoot;
7308
7392
 
7309
7393
  function getDefaultExportFromCjs (x) {
7310
7394
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
@@ -7690,7 +7774,7 @@ function isExecError(err) {
7690
7774
  * (Windows), systemd (Linux), or launchd (macOS) based on platform.
7691
7775
  */
7692
7776
  /** Exec helper that returns stdout. */
7693
- function run(cmd) {
7777
+ function run$1(cmd) {
7694
7778
  return execSync(cmd, {
7695
7779
  encoding: 'utf-8',
7696
7780
  timeout: 30_000,
@@ -7743,31 +7827,31 @@ function createWindowsManager(descriptor) {
7743
7827
  const cmdArgs = descriptor.startCommand(cfgPath);
7744
7828
  const appPath = cmdArgs[0];
7745
7829
  const appArgs = cmdArgs.slice(1).join(' ');
7746
- run(`nssm install ${svcName} ${appPath}`);
7830
+ run$1(`nssm install ${svcName} ${appPath}`);
7747
7831
  if (appArgs) {
7748
- run(`nssm set ${svcName} AppParameters ${appArgs}`);
7832
+ run$1(`nssm set ${svcName} AppParameters ${appArgs}`);
7749
7833
  }
7750
- run(`nssm set ${svcName} AppStdout ${join(homedir(), `${svcName}.log`)}`);
7751
- run(`nssm set ${svcName} AppStderr ${join(homedir(), `${svcName}.log`)}`);
7752
- run(`nssm set ${svcName} AppRotateFiles 1`);
7753
- run(`nssm set ${svcName} AppRotateBytes 1048576`);
7834
+ run$1(`nssm set ${svcName} AppStdout ${join(homedir(), `${svcName}.log`)}`);
7835
+ run$1(`nssm set ${svcName} AppStderr ${join(homedir(), `${svcName}.log`)}`);
7836
+ run$1(`nssm set ${svcName} AppRotateFiles 1`);
7837
+ run$1(`nssm set ${svcName} AppRotateBytes 1048576`);
7754
7838
  },
7755
7839
  uninstall(options) {
7756
7840
  const svcName = resolveServiceName(descriptor, options);
7757
7841
  runQuiet(`nssm stop ${svcName}`);
7758
- run(`nssm remove ${svcName} confirm`);
7842
+ run$1(`nssm remove ${svcName} confirm`);
7759
7843
  },
7760
7844
  start(options) {
7761
7845
  const svcName = resolveServiceName(descriptor, options);
7762
- run(`nssm start ${svcName}`);
7846
+ run$1(`nssm start ${svcName}`);
7763
7847
  },
7764
7848
  stop(options) {
7765
7849
  const svcName = resolveServiceName(descriptor, options);
7766
- run(`nssm stop ${svcName}`);
7850
+ run$1(`nssm stop ${svcName}`);
7767
7851
  },
7768
7852
  restart(options) {
7769
7853
  const svcName = resolveServiceName(descriptor, options);
7770
- run(`nssm restart ${svcName}`);
7854
+ run$1(`nssm restart ${svcName}`);
7771
7855
  },
7772
7856
  status(options) {
7773
7857
  const svcName = resolveServiceName(descriptor, options);
@@ -7812,8 +7896,8 @@ function createLinuxManager(descriptor) {
7812
7896
  const cmdArgs = descriptor.startCommand(cfgPath);
7813
7897
  mkdirSync(unitDir, { recursive: true });
7814
7898
  writeFileSync(unitPath(svcName), buildSystemdUnit(svcName, cmdArgs));
7815
- run('systemctl --user daemon-reload');
7816
- run(`systemctl --user enable ${svcName}.service`);
7899
+ run$1('systemctl --user daemon-reload');
7900
+ run$1(`systemctl --user enable ${svcName}.service`);
7817
7901
  },
7818
7902
  uninstall(options) {
7819
7903
  const svcName = resolveServiceName(descriptor, options);
@@ -7826,15 +7910,15 @@ function createLinuxManager(descriptor) {
7826
7910
  },
7827
7911
  start(options) {
7828
7912
  const svcName = resolveServiceName(descriptor, options);
7829
- run(`systemctl --user start ${svcName}.service`);
7913
+ run$1(`systemctl --user start ${svcName}.service`);
7830
7914
  },
7831
7915
  stop(options) {
7832
7916
  const svcName = resolveServiceName(descriptor, options);
7833
- run(`systemctl --user stop ${svcName}.service`);
7917
+ run$1(`systemctl --user stop ${svcName}.service`);
7834
7918
  },
7835
7919
  restart(options) {
7836
7920
  const svcName = resolveServiceName(descriptor, options);
7837
- run(`systemctl --user restart ${svcName}.service`);
7921
+ run$1(`systemctl --user restart ${svcName}.service`);
7838
7922
  },
7839
7923
  status(options) {
7840
7924
  const svcName = resolveServiceName(descriptor, options);
@@ -7898,16 +7982,16 @@ function createMacOSManager(descriptor) {
7898
7982
  },
7899
7983
  start(options) {
7900
7984
  const svcName = resolveServiceName(descriptor, options);
7901
- run(`launchctl load ${plistPath(svcName)}`);
7985
+ run$1(`launchctl load ${plistPath(svcName)}`);
7902
7986
  },
7903
7987
  stop(options) {
7904
7988
  const svcName = resolveServiceName(descriptor, options);
7905
- run(`launchctl unload ${plistPath(svcName)}`);
7989
+ run$1(`launchctl unload ${plistPath(svcName)}`);
7906
7990
  },
7907
7991
  restart(options) {
7908
7992
  const svcName = resolveServiceName(descriptor, options);
7909
7993
  runQuiet(`launchctl unload ${plistPath(svcName)}`);
7910
- run(`launchctl load ${plistPath(svcName)}`);
7994
+ run$1(`launchctl load ${plistPath(svcName)}`);
7911
7995
  },
7912
7996
  status(options) {
7913
7997
  const svcName = resolveServiceName(descriptor, options);
@@ -7936,19 +8020,6 @@ function createServiceManager(descriptor) {
7936
8020
  }
7937
8021
  }
7938
8022
 
7939
- /**
7940
- * Shared CLI defaults and option registration for Jeeves CLI commands.
7941
- *
7942
- * @remarks
7943
- * All three CLI commands (install, uninstall, status) share the same
7944
- * `--workspace` and `--config-root` options with the same defaults.
7945
- * This module centralizes them to eliminate duplication.
7946
- */
7947
- /** Default workspace path (current directory). */
7948
- const DEFAULT_WORKSPACE = '.';
7949
- /** Default config root path. */
7950
- const DEFAULT_CONFIG_ROOT = './config';
7951
-
7952
8023
  /**
7953
8024
  * Factory for the standard Jeeves service CLI.
7954
8025
  *
@@ -8283,6 +8354,38 @@ function loadConfig(configDir) {
8283
8354
  }
8284
8355
  }
8285
8356
 
8357
+ /**
8358
+ * Service URL resolution.
8359
+ *
8360
+ * @remarks
8361
+ * Resolves the URL for a named Jeeves service using the following
8362
+ * resolution order:
8363
+ * 1. Consumer's own component config
8364
+ * 2. Core config (`{configRoot}/jeeves-core/config.json`)
8365
+ * 3. Default port constants
8366
+ */
8367
+ /**
8368
+ * Resolve the URL for a named Jeeves service.
8369
+ *
8370
+ * @param serviceName - The service name (e.g., 'watcher', 'runner').
8371
+ * @param consumerName - Optional consumer component name for config override.
8372
+ * @returns The resolved service URL.
8373
+ * @throws Error if `init()` has not been called or the service is unknown.
8374
+ */
8375
+ function getServiceUrl(serviceName, consumerName) {
8376
+ // 2. Check core config
8377
+ const coreDir = getCoreConfigDir();
8378
+ const coreConfig = loadConfig(coreDir);
8379
+ const coreUrl = coreConfig?.services[serviceName]?.url;
8380
+ if (coreUrl)
8381
+ return coreUrl;
8382
+ // 3. Fall back to port constants
8383
+ const port = DEFAULT_PORTS[serviceName];
8384
+ {
8385
+ return `http://127.0.0.1:${String(port)}`;
8386
+ }
8387
+ }
8388
+
8286
8389
  /**
8287
8390
  * Resolve the bind address for a Jeeves service.
8288
8391
  *
@@ -8320,6 +8423,10 @@ function getBindAddress(componentName) {
8320
8423
  // Tier 4: Default
8321
8424
  return DEFAULT_BIND_ADDRESS;
8322
8425
  }
8426
+ /** Async sleep via setTimeout. */
8427
+ function sleepAsync(ms) {
8428
+ return new Promise((r) => setTimeout(r, ms));
8429
+ }
8323
8430
 
8324
8431
  /**
8325
8432
  * Shared live config hot-reload support.
@@ -8337,7 +8444,6 @@ function getBindAddress(componentName) {
8337
8444
  */
8338
8445
  const RESTART_REQUIRED_FIELDS = [
8339
8446
  'port',
8340
- 'host',
8341
8447
  'watcherUrl',
8342
8448
  'gatewayUrl',
8343
8449
  'gatewayApiKey',
@@ -8449,8 +8555,6 @@ const autoSeedRuleSchema = z.object({
8449
8555
  const serviceConfigSchema = metaConfigSchema.extend({
8450
8556
  /** HTTP port for the service (default: 1938). */
8451
8557
  port: z.number().int().min(1).max(65535).default(1938),
8452
- /** Bind address for the HTTP server (default: 127.0.0.1). */
8453
- host: z.string().default('127.0.0.1'),
8454
8558
  /** Cron schedule for synthesis cycles (default: every 30 min). */
8455
8559
  schedule: z.string().default('*/30 * * * *'),
8456
8560
  /** Optional channel identifier for reporting. */
@@ -8810,6 +8914,174 @@ async function readMetaJson(metaPath) {
8810
8914
  return JSON.parse(raw);
8811
8915
  }
8812
8916
 
8917
+ /**
8918
+ * Escape special glob characters in a path so it can be used as a literal
8919
+ * prefix in glob patterns.
8920
+ *
8921
+ * Glob metacharacters `* ? [ ] { } ( ) !` are escaped with a backslash so
8922
+ * that paths containing parentheses (e.g. Slack channel IDs) or other
8923
+ * special characters are matched literally by the watcher's walk endpoint.
8924
+ *
8925
+ * @module escapeGlob
8926
+ */
8927
+ /**
8928
+ * Escape glob metacharacters in a string using character-class wrapping.
8929
+ *
8930
+ * Backslash escaping (`\(`) does not work reliably on Windows where `\` is
8931
+ * the path separator. Instead, each metacharacter is wrapped in a character
8932
+ * class (e.g. `(` → `[(]`) which is universally supported by glob libraries.
8933
+ *
8934
+ * Square brackets themselves are escaped as `[[]` and `[]]`.
8935
+ *
8936
+ * @param s - Raw path string.
8937
+ * @returns String with glob metacharacters wrapped in character classes.
8938
+ */
8939
+ function escapeGlob(s) {
8940
+ return s.replace(/[*?[\]{}()!]/g, (ch) => `[${ch}]`);
8941
+ }
8942
+
8943
+ /**
8944
+ * Filter file paths by modification time.
8945
+ *
8946
+ * Shared utility for staleness detection and delta file enumeration.
8947
+ * Uses `fs.statSync` for fast local mtime checks on known paths.
8948
+ *
8949
+ * @module mtimeFilter
8950
+ */
8951
+ /**
8952
+ * Check if any file in the list was modified after the given timestamp.
8953
+ *
8954
+ * Short-circuits on first match for efficiency (staleness checks).
8955
+ *
8956
+ * @param files - Array of file paths to check.
8957
+ * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` match.
8958
+ * @returns True if any file was modified after the timestamp.
8959
+ */
8960
+ function hasModifiedAfter(files, afterMs) {
8961
+ for (const filePath of files) {
8962
+ try {
8963
+ if (statSync(filePath).mtimeMs > afterMs)
8964
+ return true;
8965
+ }
8966
+ catch {
8967
+ // Unreadable file — skip
8968
+ }
8969
+ }
8970
+ return false;
8971
+ }
8972
+ /**
8973
+ * Filter files to only those modified after the given timestamp.
8974
+ *
8975
+ * @param files - Array of file paths to filter.
8976
+ * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` are included.
8977
+ * @returns Filtered array of file paths.
8978
+ */
8979
+ function filterModifiedAfter(files, afterMs) {
8980
+ return files.filter((filePath) => {
8981
+ try {
8982
+ return statSync(filePath).mtimeMs > afterMs;
8983
+ }
8984
+ catch {
8985
+ return false;
8986
+ }
8987
+ });
8988
+ }
8989
+
8990
+ /**
8991
+ * Staleness detection via watcher walk.
8992
+ *
8993
+ * A meta is stale when any watched file in its scope was modified after
8994
+ * `_generatedAt`.
8995
+ *
8996
+ * @module scheduling/staleness
8997
+ */
8998
+ /**
8999
+ * Check if a meta is stale.
9000
+ *
9001
+ * Uses watcher `/walk` to enumerate watched files under the scope prefix,
9002
+ * then applies a local mtime check (fast) to detect any modifications since
9003
+ * `_generatedAt`. Short-circuits on first match.
9004
+ *
9005
+ * @param scopePrefix - Path prefix for this meta's scope.
9006
+ * @param meta - Current meta.json content.
9007
+ * @param watcher - WatcherClient instance.
9008
+ * @returns True if any file in scope was modified after `_generatedAt`.
9009
+ */
9010
+ async function isStale(scopePrefix, meta, watcher) {
9011
+ if (!meta._generatedAt)
9012
+ return true; // Never synthesized = stale
9013
+ const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
9014
+ // Exclude .meta/ subtree — synthesis outputs must not trigger staleness.
9015
+ // Handle both forward and back slashes for cross-platform compatibility.
9016
+ const metaSep = /[/\\]\.meta(?:[/\\]|$)/;
9017
+ const filtered = files.filter((f) => !metaSep.test(f));
9018
+ return hasModifiedAfter(filtered, new Date(meta._generatedAt).getTime());
9019
+ }
9020
+ /** Maximum staleness for never-synthesized metas (1 year in seconds). */
9021
+ const MAX_STALENESS_SECONDS = 365 * 86_400;
9022
+ /**
9023
+ * Compute actual staleness in seconds (now minus _generatedAt).
9024
+ *
9025
+ * Never-synthesized metas are capped at {@link MAX_STALENESS_SECONDS}
9026
+ * (1 year) so that depth weighting can differentiate them. Without
9027
+ * bounding, `Infinity * depthFactor` = `Infinity` for all depths.
9028
+ *
9029
+ * @param meta - Current meta.json content.
9030
+ * @returns Staleness in seconds, capped at 1 year for never-synthesized metas.
9031
+ */
9032
+ function actualStaleness(meta) {
9033
+ if (!meta._generatedAt)
9034
+ return MAX_STALENESS_SECONDS;
9035
+ const generatedMs = new Date(meta._generatedAt).getTime();
9036
+ return Math.min((Date.now() - generatedMs) / 1000, MAX_STALENESS_SECONDS);
9037
+ }
9038
+ /**
9039
+ * Check whether the architect step should be triggered.
9040
+ *
9041
+ * @param meta - Current meta.json.
9042
+ * @param structureChanged - Whether the structure hash changed.
9043
+ * @param steerChanged - Whether the steer directive changed.
9044
+ * @param architectEvery - Config: run architect every N cycles.
9045
+ * @returns True if the architect step should run.
9046
+ */
9047
+ function isArchitectTriggered(meta, structureChanged, steerChanged, architectEvery) {
9048
+ return (!meta._builder ||
9049
+ structureChanged ||
9050
+ steerChanged ||
9051
+ (meta._synthesisCount ?? 0) >= architectEvery);
9052
+ }
9053
+ /**
9054
+ * Detect whether the steer directive changed since the last archive.
9055
+ *
9056
+ * @param currentSteer - Current _steer value (or undefined).
9057
+ * @param archiveSteer - Archive _steer value (or undefined).
9058
+ * @param hasArchive - Whether an archive snapshot exists.
9059
+ * @returns True if steer changed.
9060
+ */
9061
+ function hasSteerChanged(currentSteer, archiveSteer, hasArchive) {
9062
+ if (!hasArchive)
9063
+ return Boolean(currentSteer);
9064
+ return currentSteer !== archiveSteer;
9065
+ }
9066
+ /**
9067
+ * Compute a normalized staleness score (0–1) for display purposes.
9068
+ *
9069
+ * Uses the same depth/emphasis weighting as candidate selection,
9070
+ * normalized to a 30-day window.
9071
+ *
9072
+ * @param stalenessSeconds - Raw staleness in seconds (null = never synthesized).
9073
+ * @param depth - Meta tree depth.
9074
+ * @param emphasis - Scheduling emphasis multiplier.
9075
+ * @param depthWeight - Depth weighting exponent from config.
9076
+ * @returns Normalized score between 0 and 1.
9077
+ */
9078
+ function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
9079
+ if (stalenessSeconds === null)
9080
+ return 1;
9081
+ const depthFactor = Math.pow(1 + depthWeight, depth);
9082
+ return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
9083
+ }
9084
+
8813
9085
  /**
8814
9086
  * Build the ownership tree from discovered .meta/ paths.
8815
9087
  *
@@ -8892,8 +9164,6 @@ function findNode(tree, targetPath) {
8892
9164
  *
8893
9165
  * @module discovery/listMetas
8894
9166
  */
8895
- /** Maximum staleness for never-synthesized metas (1 year in seconds). */
8896
- const MAX_STALENESS_SECONDS$1 = 365 * 86_400;
8897
9167
  /**
8898
9168
  * Discover, deduplicate, and enrich all metas.
8899
9169
  *
@@ -8929,7 +9199,7 @@ async function listMetas(config, watcher) {
8929
9199
  // Compute staleness
8930
9200
  let stalenessSeconds;
8931
9201
  if (neverSynth) {
8932
- stalenessSeconds = MAX_STALENESS_SECONDS$1;
9202
+ stalenessSeconds = MAX_STALENESS_SECONDS;
8933
9203
  }
8934
9204
  else {
8935
9205
  const genAt = new Date(meta._generatedAt).getTime();
@@ -8962,79 +9232,6 @@ async function listMetas(config, watcher) {
8962
9232
  };
8963
9233
  }
8964
9234
 
8965
- /**
8966
- * Escape special glob characters in a path so it can be used as a literal
8967
- * prefix in glob patterns.
8968
- *
8969
- * Glob metacharacters `* ? [ ] { } ( ) !` are escaped with a backslash so
8970
- * that paths containing parentheses (e.g. Slack channel IDs) or other
8971
- * special characters are matched literally by the watcher's walk endpoint.
8972
- *
8973
- * @module escapeGlob
8974
- */
8975
- /**
8976
- * Escape glob metacharacters in a string using character-class wrapping.
8977
- *
8978
- * Backslash escaping (`\(`) does not work reliably on Windows where `\` is
8979
- * the path separator. Instead, each metacharacter is wrapped in a character
8980
- * class (e.g. `(` → `[(]`) which is universally supported by glob libraries.
8981
- *
8982
- * Square brackets themselves are escaped as `[[]` and `[]]`.
8983
- *
8984
- * @param s - Raw path string.
8985
- * @returns String with glob metacharacters wrapped in character classes.
8986
- */
8987
- function escapeGlob(s) {
8988
- return s.replace(/[*?[\]{}()!]/g, (ch) => `[${ch}]`);
8989
- }
8990
-
8991
- /**
8992
- * Filter file paths by modification time.
8993
- *
8994
- * Shared utility for staleness detection and delta file enumeration.
8995
- * Uses `fs.statSync` for fast local mtime checks on known paths.
8996
- *
8997
- * @module mtimeFilter
8998
- */
8999
- /**
9000
- * Check if any file in the list was modified after the given timestamp.
9001
- *
9002
- * Short-circuits on first match for efficiency (staleness checks).
9003
- *
9004
- * @param files - Array of file paths to check.
9005
- * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` match.
9006
- * @returns True if any file was modified after the timestamp.
9007
- */
9008
- function hasModifiedAfter(files, afterMs) {
9009
- for (const filePath of files) {
9010
- try {
9011
- if (statSync(filePath).mtimeMs > afterMs)
9012
- return true;
9013
- }
9014
- catch {
9015
- // Unreadable file — skip
9016
- }
9017
- }
9018
- return false;
9019
- }
9020
- /**
9021
- * Filter files to only those modified after the given timestamp.
9022
- *
9023
- * @param files - Array of file paths to filter.
9024
- * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` are included.
9025
- * @returns Filtered array of file paths.
9026
- */
9027
- function filterModifiedAfter(files, afterMs) {
9028
- return files.filter((filePath) => {
9029
- try {
9030
- return statSync(filePath).mtimeMs > afterMs;
9031
- }
9032
- catch {
9033
- return false;
9034
- }
9035
- });
9036
- }
9037
-
9038
9235
  /**
9039
9236
  * Compute the file scope owned by a meta node.
9040
9237
  *
@@ -9111,11 +9308,6 @@ function getDeltaFiles(generatedAt, scopeFiles) {
9111
9308
  return filterModifiedAfter(scopeFiles, new Date(generatedAt).getTime());
9112
9309
  }
9113
9310
 
9114
- /** Sleep for a given number of milliseconds. */
9115
- function sleep(ms) {
9116
- return new Promise((resolve) => setTimeout(resolve, ms));
9117
- }
9118
-
9119
9311
  /**
9120
9312
  * Error thrown when a spawned subprocess is aborted via AbortController.
9121
9313
  *
@@ -9270,7 +9462,7 @@ class GatewayExecutor {
9270
9462
  JSON.stringify(spawnResult));
9271
9463
  }
9272
9464
  // Step 2: Poll for completion via sessions_history
9273
- await sleep(3000);
9465
+ await sleepAsync(3000);
9274
9466
  while (Date.now() < deadline) {
9275
9467
  // Check for abort before each poll iteration
9276
9468
  if (this.controller.signal.aborted) {
@@ -9334,7 +9526,7 @@ class GatewayExecutor {
9334
9526
  catch {
9335
9527
  // Transient poll failure — keep trying
9336
9528
  }
9337
- await sleep(this.pollIntervalMs);
9529
+ await sleepAsync(this.pollIntervalMs);
9338
9530
  }
9339
9531
  throw new SpawnTimeoutError('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms', outputPath);
9340
9532
  }
@@ -10195,101 +10387,6 @@ function discoverStalestPath(candidates, depthWeight) {
10195
10387
  return winner?.node.metaPath ?? null;
10196
10388
  }
10197
10389
 
10198
- /**
10199
- * Staleness detection via watcher walk.
10200
- *
10201
- * A meta is stale when any watched file in its scope was modified after
10202
- * `_generatedAt`.
10203
- *
10204
- * @module scheduling/staleness
10205
- */
10206
- /**
10207
- * Check if a meta is stale.
10208
- *
10209
- * Uses watcher `/walk` to enumerate watched files under the scope prefix,
10210
- * then applies a local mtime check (fast) to detect any modifications since
10211
- * `_generatedAt`. Short-circuits on first match.
10212
- *
10213
- * @param scopePrefix - Path prefix for this meta's scope.
10214
- * @param meta - Current meta.json content.
10215
- * @param watcher - WatcherClient instance.
10216
- * @returns True if any file in scope was modified after `_generatedAt`.
10217
- */
10218
- async function isStale(scopePrefix, meta, watcher) {
10219
- if (!meta._generatedAt)
10220
- return true; // Never synthesized = stale
10221
- const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
10222
- // Exclude .meta/ subtree — synthesis outputs must not trigger staleness.
10223
- // Handle both forward and back slashes for cross-platform compatibility.
10224
- const metaSep = /[/\\]\.meta(?:[/\\]|$)/;
10225
- const filtered = files.filter((f) => !metaSep.test(f));
10226
- return hasModifiedAfter(filtered, new Date(meta._generatedAt).getTime());
10227
- }
10228
- /** Maximum staleness for never-synthesized metas (1 year in seconds). */
10229
- const MAX_STALENESS_SECONDS = 365 * 86_400;
10230
- /**
10231
- * Compute actual staleness in seconds (now minus _generatedAt).
10232
- *
10233
- * Never-synthesized metas are capped at {@link MAX_STALENESS_SECONDS}
10234
- * (1 year) so that depth weighting can differentiate them. Without
10235
- * bounding, `Infinity * depthFactor` = `Infinity` for all depths.
10236
- *
10237
- * @param meta - Current meta.json content.
10238
- * @returns Staleness in seconds, capped at 1 year for never-synthesized metas.
10239
- */
10240
- function actualStaleness(meta) {
10241
- if (!meta._generatedAt)
10242
- return MAX_STALENESS_SECONDS;
10243
- const generatedMs = new Date(meta._generatedAt).getTime();
10244
- return Math.min((Date.now() - generatedMs) / 1000, MAX_STALENESS_SECONDS);
10245
- }
10246
- /**
10247
- * Check whether the architect step should be triggered.
10248
- *
10249
- * @param meta - Current meta.json.
10250
- * @param structureChanged - Whether the structure hash changed.
10251
- * @param steerChanged - Whether the steer directive changed.
10252
- * @param architectEvery - Config: run architect every N cycles.
10253
- * @returns True if the architect step should run.
10254
- */
10255
- function isArchitectTriggered(meta, structureChanged, steerChanged, architectEvery) {
10256
- return (!meta._builder ||
10257
- structureChanged ||
10258
- steerChanged ||
10259
- (meta._synthesisCount ?? 0) >= architectEvery);
10260
- }
10261
- /**
10262
- * Detect whether the steer directive changed since the last archive.
10263
- *
10264
- * @param currentSteer - Current _steer value (or undefined).
10265
- * @param archiveSteer - Archive _steer value (or undefined).
10266
- * @param hasArchive - Whether an archive snapshot exists.
10267
- * @returns True if steer changed.
10268
- */
10269
- function hasSteerChanged(currentSteer, archiveSteer, hasArchive) {
10270
- if (!hasArchive)
10271
- return Boolean(currentSteer);
10272
- return currentSteer !== archiveSteer;
10273
- }
10274
- /**
10275
- * Compute a normalized staleness score (0–1) for display purposes.
10276
- *
10277
- * Uses the same depth/emphasis weighting as candidate selection,
10278
- * normalized to a 30-day window.
10279
- *
10280
- * @param stalenessSeconds - Raw staleness in seconds (null = never synthesized).
10281
- * @param depth - Meta tree depth.
10282
- * @param emphasis - Scheduling emphasis multiplier.
10283
- * @param depthWeight - Depth weighting exponent from config.
10284
- * @returns Normalized score between 0 and 1.
10285
- */
10286
- function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
10287
- if (stalenessSeconds === null)
10288
- return 1;
10289
- const depthFactor = Math.pow(1 + depthWeight, depth);
10290
- return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
10291
- }
10292
-
10293
10390
  /**
10294
10391
  * Shared error utilities.
10295
10392
  *
@@ -11350,7 +11447,10 @@ function buildMetaRules(config) {
11350
11447
  properties: {
11351
11448
  file: {
11352
11449
  properties: {
11353
- path: { type: 'string', glob: '**/jeeves-meta.config.json' },
11450
+ path: {
11451
+ type: 'string',
11452
+ glob: '**/jeeves-meta{.config.json,/config.json}',
11453
+ },
11354
11454
  },
11355
11455
  },
11356
11456
  },
@@ -12511,7 +12611,7 @@ class HttpWatcherClient {
12511
12611
  }
12512
12612
  // Exponential backoff
12513
12613
  const delayMs = this.backoffBaseMs * Math.pow(this.backoffFactor, attempt);
12514
- await sleep(delayMs);
12614
+ await sleepAsync(delayMs);
12515
12615
  }
12516
12616
  // Unreachable, but TypeScript needs it
12517
12617
  throw new Error('Retry exhausted');
@@ -12730,7 +12830,11 @@ async function startService(config, configPath) {
12730
12830
  */
12731
12831
  /** Build the full API URL for a given port string and path. */
12732
12832
  function apiUrl(port, apiPath) {
12733
- return `http://127.0.0.1:${port}${apiPath}`;
12833
+ const url = new URL(apiPath, getServiceUrl('meta'));
12834
+ if (port !== DEFAULT_PORT_STR) {
12835
+ url.port = port;
12836
+ }
12837
+ return url.toString();
12734
12838
  }
12735
12839
  /** Wrap an async CLI action with consistent error handling. */
12736
12840
  function withErrorHandling(fn, label) {