@karmaniverous/jeeves-meta 0.12.4 → 0.13.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,8 +1,9 @@
1
1
  import fs, { readdirSync, readFileSync, existsSync, writeFileSync, renameSync, unlinkSync, mkdirSync, copyFileSync, statSync, watchFile } from 'node:fs';
2
- import path, { join, dirname, resolve, relative, posix } from 'node:path';
2
+ import path, { join, dirname, resolve, basename, relative, posix } from 'node:path';
3
3
  import { unlink, readFile, mkdir, writeFile, copyFile } from 'node:fs/promises';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import process$1 from 'node:process';
6
+ import { randomUUID, createHash } from 'node:crypto';
6
7
  import require$$0$4 from 'path';
7
8
  import require$$0$3 from 'fs';
8
9
  import require$$0$1 from 'constants';
@@ -11,11 +12,10 @@ import require$$4 from 'util';
11
12
  import require$$5 from 'assert';
12
13
  import require$$2 from 'events';
13
14
  import vm from 'vm';
15
+ import { z } from 'zod';
14
16
  import * as commander from 'commander';
15
17
  import { tmpdir } from 'node:os';
16
- import { z } from 'zod';
17
18
  import 'node:child_process';
18
- import { randomUUID, createHash } from 'node:crypto';
19
19
  import pino from 'pino';
20
20
  import Handlebars from 'handlebars';
21
21
  import { Cron } from 'croner';
@@ -7243,6 +7243,32 @@ requireSemver();
7243
7243
 
7244
7244
  /** Core config file name. */
7245
7245
  const CONFIG_FILE = 'config.json';
7246
+
7247
+ /**
7248
+ * Default port assignments for Jeeves platform services.
7249
+ *
7250
+ * @remarks
7251
+ * Each port number is a historical reference:
7252
+ * - 1934: Wodehouse's *Thank You, Jeeves*; Popper's *Logic of Scientific Discovery*
7253
+ * - 1936: Turing's "On Computable Numbers"; Church's lambda calculus
7254
+ * - 1937: Turing's paper in *Proceedings of the London Mathematical Society*
7255
+ * - 1938: Wodehouse's *The Code of the Woosters*; Shannon's relay/switching paper
7256
+ */
7257
+ /** Default port for jeeves-server. */
7258
+ const SERVER_PORT = 1934;
7259
+ /** Default port for jeeves-watcher. */
7260
+ const WATCHER_PORT = 1936;
7261
+ /** Default port for jeeves-runner. */
7262
+ const RUNNER_PORT = 1937;
7263
+ /** Default port for jeeves-meta. */
7264
+ const META_PORT = 1938;
7265
+ /** Map of service names to their default ports. */
7266
+ const DEFAULT_PORTS = {
7267
+ server: SERVER_PORT,
7268
+ watcher: WATCHER_PORT,
7269
+ runner: RUNNER_PORT,
7270
+ meta: META_PORT,
7271
+ };
7246
7272
  /**
7247
7273
  * Get the core config directory path.
7248
7274
  *
@@ -7261,30 +7287,64 @@ function getCoreConfigDir() {
7261
7287
  function getComponentConfigDir(componentName) {
7262
7288
  throw new Error('jeeves-core: init() must be called first');
7263
7289
  }
7290
+ /** Maximum rename retry attempts on EPERM. */
7291
+ const ATOMIC_WRITE_MAX_RETRIES = 3;
7292
+ /** Delay between EPERM retries in milliseconds. */
7293
+ const ATOMIC_WRITE_RETRY_DELAY_MS = 100;
7264
7294
  /**
7265
7295
  * Write content to a file atomically via a temp file + rename.
7266
7296
  *
7297
+ * @remarks
7298
+ * Retries the rename up to three times on EPERM (Windows file-handle
7299
+ * contention) with a 100 ms synchronous delay between attempts.
7300
+ *
7267
7301
  * @param filePath - Absolute path to the target file.
7268
7302
  * @param content - Content to write.
7269
7303
  */
7270
7304
  function atomicWrite(filePath, content) {
7271
7305
  const dir = dirname(filePath);
7272
- const tempPath = join(dir, `.${String(Date.now())}.tmp`);
7306
+ const base = basename(filePath, '.md');
7307
+ const tempPath = join(dir, `.${base}.${String(Date.now())}.${randomUUID().slice(0, 8)}.tmp`);
7273
7308
  writeFileSync(tempPath, content, 'utf-8');
7274
- try {
7275
- renameSync(tempPath, filePath);
7276
- }
7277
- catch (err) {
7309
+ for (let attempt = 0; attempt < ATOMIC_WRITE_MAX_RETRIES; attempt++) {
7278
7310
  try {
7279
- unlinkSync(tempPath);
7311
+ renameSync(tempPath, filePath);
7312
+ return;
7280
7313
  }
7281
- catch {
7282
- /* best-effort cleanup */
7314
+ catch (err) {
7315
+ const isEperm = err instanceof Error &&
7316
+ 'code' in err &&
7317
+ err.code === 'EPERM';
7318
+ if (!isEperm || attempt === ATOMIC_WRITE_MAX_RETRIES - 1) {
7319
+ try {
7320
+ unlinkSync(tempPath);
7321
+ }
7322
+ catch {
7323
+ /* best-effort cleanup */
7324
+ }
7325
+ throw err;
7326
+ }
7327
+ // Synchronous sleep before retry (acceptable in atomic write context)
7328
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ATOMIC_WRITE_RETRY_DELAY_MS);
7283
7329
  }
7284
- throw err;
7285
7330
  }
7286
7331
  }
7287
7332
 
7333
+ /**
7334
+ * Shared internal utility functions.
7335
+ *
7336
+ * @packageDocumentation
7337
+ */
7338
+ /**
7339
+ * Extract a human-readable message from an unknown caught value.
7340
+ *
7341
+ * @param err - The caught value (typically `unknown`).
7342
+ * @returns The error message string.
7343
+ */
7344
+ function getErrorMessage(err) {
7345
+ return err instanceof Error ? err.message : String(err);
7346
+ }
7347
+
7288
7348
  /**
7289
7349
  * Factory for a framework-agnostic config apply HTTP handler.
7290
7350
  *
@@ -7337,8 +7397,7 @@ function readConfigFile(filePath) {
7337
7397
  return JSON.parse(raw);
7338
7398
  }
7339
7399
  catch (err) {
7340
- const msg = err instanceof Error ? err.message : String(err);
7341
- console.warn(`jeeves-core: Could not read config file ${filePath}: ${msg}`);
7400
+ console.warn(`jeeves-core: Could not read config file ${filePath}: ${getErrorMessage(err)}`);
7342
7401
  return {};
7343
7402
  }
7344
7403
  }
@@ -7387,10 +7446,9 @@ function createConfigApplyHandler(descriptor) {
7387
7446
  atomicWrite(configPath, json);
7388
7447
  }
7389
7448
  catch (err) {
7390
- const message = err instanceof Error ? err.message : String(err);
7391
7449
  return {
7392
7450
  status: 500,
7393
- body: { error: `Failed to write config: ${message}` },
7451
+ body: { error: `Failed to write config: ${getErrorMessage(err)}` },
7394
7452
  };
7395
7453
  }
7396
7454
  // Call onConfigApply callback if defined
@@ -7399,12 +7457,11 @@ function createConfigApplyHandler(descriptor) {
7399
7457
  await descriptor.onConfigApply(validatedConfig);
7400
7458
  }
7401
7459
  catch (err) {
7402
- const message = err instanceof Error ? err.message : String(err);
7403
7460
  return {
7404
7461
  status: 200,
7405
7462
  body: {
7406
7463
  applied: true,
7407
- warning: `Config written but callback failed: ${message}`,
7464
+ warning: `Config written but callback failed: ${getErrorMessage(err)}`,
7408
7465
  config: validatedConfig,
7409
7466
  },
7410
7467
  };
@@ -7487,8 +7544,7 @@ function createStatusHandler(options) {
7487
7544
  health = await options.getHealth();
7488
7545
  }
7489
7546
  catch (err) {
7490
- const message = err instanceof Error ? err.message : String(err);
7491
- health = { error: message };
7547
+ health = { error: getErrorMessage(err) };
7492
7548
  overallStatus = 'degraded';
7493
7549
  }
7494
7550
  }
@@ -7504,6 +7560,47 @@ function createStatusHandler(options) {
7504
7560
  };
7505
7561
  };
7506
7562
  }
7563
+ /** Core shared config section. */
7564
+ const workspaceCoreConfigSchema = z
7565
+ .object({
7566
+ /** Workspace root path. */
7567
+ workspace: z.string().optional().describe('Workspace root path'),
7568
+ /** Platform config root path. */
7569
+ configRoot: z.string().optional().describe('Platform config root path'),
7570
+ /** OpenClaw gateway URL. */
7571
+ gatewayUrl: z.string().optional().describe('OpenClaw gateway URL'),
7572
+ })
7573
+ .partial();
7574
+ /** Memory shared config section. */
7575
+ const workspaceMemoryConfigSchema = z
7576
+ .object({
7577
+ /** MEMORY.md character budget. */
7578
+ budget: z.number().int().positive().optional().describe('Memory budget'),
7579
+ /** Warning threshold as a fraction of budget. */
7580
+ warningThreshold: z
7581
+ .number()
7582
+ .min(0)
7583
+ .max(1)
7584
+ .optional()
7585
+ .describe('Memory warning threshold'),
7586
+ /** Staleness threshold in days. */
7587
+ staleDays: z
7588
+ .number()
7589
+ .int()
7590
+ .positive()
7591
+ .optional()
7592
+ .describe('Memory staleness threshold in days'),
7593
+ })
7594
+ .partial();
7595
+ /** Workspace config Zod schema. */
7596
+ z.object({
7597
+ /** JSON Schema pointer for IDE autocomplete. */
7598
+ $schema: z.string().optional().describe('JSON Schema pointer'),
7599
+ /** Core shared defaults. */
7600
+ core: workspaceCoreConfigSchema.optional(),
7601
+ /** Memory hygiene shared defaults. */
7602
+ memory: workspaceMemoryConfigSchema.optional(),
7603
+ });
7507
7604
 
7508
7605
  function getDefaultExportFromCjs (x) {
7509
7606
  return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x;
@@ -7821,6 +7918,38 @@ function loadConfig(configDir) {
7821
7918
  }
7822
7919
  }
7823
7920
 
7921
+ /**
7922
+ * Service URL resolution.
7923
+ *
7924
+ * @remarks
7925
+ * Resolves the URL for a named Jeeves service using the following
7926
+ * resolution order:
7927
+ * 1. Consumer's own component config
7928
+ * 2. Core config (`{configRoot}/jeeves-core/config.json`)
7929
+ * 3. Default port constants
7930
+ */
7931
+ /**
7932
+ * Resolve the URL for a named Jeeves service.
7933
+ *
7934
+ * @param serviceName - The service name (e.g., 'watcher', 'runner').
7935
+ * @param consumerName - Optional consumer component name for config override.
7936
+ * @returns The resolved service URL.
7937
+ * @throws Error if `init()` has not been called or the service is unknown.
7938
+ */
7939
+ function getServiceUrl(serviceName, consumerName) {
7940
+ // 2. Check core config
7941
+ const coreDir = getCoreConfigDir();
7942
+ const coreConfig = loadConfig(coreDir);
7943
+ const coreUrl = coreConfig?.services[serviceName]?.url;
7944
+ if (coreUrl)
7945
+ return coreUrl;
7946
+ // 3. Fall back to port constants
7947
+ const port = DEFAULT_PORTS[serviceName];
7948
+ {
7949
+ return `http://127.0.0.1:${String(port)}`;
7950
+ }
7951
+ }
7952
+
7824
7953
  /**
7825
7954
  * Resolve the bind address for a Jeeves service.
7826
7955
  *
@@ -7858,6 +7987,10 @@ function getBindAddress(componentName) {
7858
7987
  // Tier 4: Default
7859
7988
  return DEFAULT_BIND_ADDRESS;
7860
7989
  }
7990
+ /** Async sleep via setTimeout. */
7991
+ function sleepAsync(ms) {
7992
+ return new Promise((r) => setTimeout(r, ms));
7993
+ }
7861
7994
 
7862
7995
  /**
7863
7996
  * Custom CLI commands for the jeeves-meta service.
@@ -7869,7 +8002,11 @@ function getBindAddress(componentName) {
7869
8002
  */
7870
8003
  /** Build the full API URL for a given port string and path. */
7871
8004
  function apiUrl(port, apiPath) {
7872
- return `http://127.0.0.1:${port}${apiPath}`;
8005
+ const url = new URL(apiPath, getServiceUrl('meta'));
8006
+ if (port !== DEFAULT_PORT_STR) {
8007
+ url.port = port;
8008
+ }
8009
+ return url.toString();
7873
8010
  }
7874
8011
  /** Wrap an async CLI action with consistent error handling. */
7875
8012
  function withErrorHandling(fn, label) {
@@ -8024,7 +8161,6 @@ function registerCustomCliCommands(program) {
8024
8161
  */
8025
8162
  const RESTART_REQUIRED_FIELDS = [
8026
8163
  'port',
8027
- 'host',
8028
8164
  'watcherUrl',
8029
8165
  'gatewayUrl',
8030
8166
  'gatewayApiKey',
@@ -8136,8 +8272,6 @@ const autoSeedRuleSchema = z.object({
8136
8272
  const serviceConfigSchema = metaConfigSchema.extend({
8137
8273
  /** HTTP port for the service (default: 1938). */
8138
8274
  port: z.number().int().min(1).max(65535).default(1938),
8139
- /** Bind address for the HTTP server (default: 127.0.0.1). */
8140
- host: z.string().default('127.0.0.1'),
8141
8275
  /** Cron schedule for synthesis cycles (default: every 30 min). */
8142
8276
  schedule: z.string().default('*/30 * * * *'),
8143
8277
  /** Optional channel identifier for reporting. */
@@ -8523,6 +8657,174 @@ async function readMetaJson(metaPath) {
8523
8657
  return JSON.parse(raw);
8524
8658
  }
8525
8659
 
8660
+ /**
8661
+ * Escape special glob characters in a path so it can be used as a literal
8662
+ * prefix in glob patterns.
8663
+ *
8664
+ * Glob metacharacters `* ? [ ] { } ( ) !` are escaped with a backslash so
8665
+ * that paths containing parentheses (e.g. Slack channel IDs) or other
8666
+ * special characters are matched literally by the watcher's walk endpoint.
8667
+ *
8668
+ * @module escapeGlob
8669
+ */
8670
+ /**
8671
+ * Escape glob metacharacters in a string using character-class wrapping.
8672
+ *
8673
+ * Backslash escaping (`\(`) does not work reliably on Windows where `\` is
8674
+ * the path separator. Instead, each metacharacter is wrapped in a character
8675
+ * class (e.g. `(` → `[(]`) which is universally supported by glob libraries.
8676
+ *
8677
+ * Square brackets themselves are escaped as `[[]` and `[]]`.
8678
+ *
8679
+ * @param s - Raw path string.
8680
+ * @returns String with glob metacharacters wrapped in character classes.
8681
+ */
8682
+ function escapeGlob(s) {
8683
+ return s.replace(/[*?[\]{}()!]/g, (ch) => `[${ch}]`);
8684
+ }
8685
+
8686
+ /**
8687
+ * Filter file paths by modification time.
8688
+ *
8689
+ * Shared utility for staleness detection and delta file enumeration.
8690
+ * Uses `fs.statSync` for fast local mtime checks on known paths.
8691
+ *
8692
+ * @module mtimeFilter
8693
+ */
8694
+ /**
8695
+ * Check if any file in the list was modified after the given timestamp.
8696
+ *
8697
+ * Short-circuits on first match for efficiency (staleness checks).
8698
+ *
8699
+ * @param files - Array of file paths to check.
8700
+ * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` match.
8701
+ * @returns True if any file was modified after the timestamp.
8702
+ */
8703
+ function hasModifiedAfter(files, afterMs) {
8704
+ for (const filePath of files) {
8705
+ try {
8706
+ if (statSync(filePath).mtimeMs > afterMs)
8707
+ return true;
8708
+ }
8709
+ catch {
8710
+ // Unreadable file — skip
8711
+ }
8712
+ }
8713
+ return false;
8714
+ }
8715
+ /**
8716
+ * Filter files to only those modified after the given timestamp.
8717
+ *
8718
+ * @param files - Array of file paths to filter.
8719
+ * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` are included.
8720
+ * @returns Filtered array of file paths.
8721
+ */
8722
+ function filterModifiedAfter(files, afterMs) {
8723
+ return files.filter((filePath) => {
8724
+ try {
8725
+ return statSync(filePath).mtimeMs > afterMs;
8726
+ }
8727
+ catch {
8728
+ return false;
8729
+ }
8730
+ });
8731
+ }
8732
+
8733
+ /**
8734
+ * Staleness detection via watcher walk.
8735
+ *
8736
+ * A meta is stale when any watched file in its scope was modified after
8737
+ * `_generatedAt`.
8738
+ *
8739
+ * @module scheduling/staleness
8740
+ */
8741
+ /**
8742
+ * Check if a meta is stale.
8743
+ *
8744
+ * Uses watcher `/walk` to enumerate watched files under the scope prefix,
8745
+ * then applies a local mtime check (fast) to detect any modifications since
8746
+ * `_generatedAt`. Short-circuits on first match.
8747
+ *
8748
+ * @param scopePrefix - Path prefix for this meta's scope.
8749
+ * @param meta - Current meta.json content.
8750
+ * @param watcher - WatcherClient instance.
8751
+ * @returns True if any file in scope was modified after `_generatedAt`.
8752
+ */
8753
+ async function isStale(scopePrefix, meta, watcher) {
8754
+ if (!meta._generatedAt)
8755
+ return true; // Never synthesized = stale
8756
+ const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
8757
+ // Exclude .meta/ subtree — synthesis outputs must not trigger staleness.
8758
+ // Handle both forward and back slashes for cross-platform compatibility.
8759
+ const metaSep = /[/\\]\.meta(?:[/\\]|$)/;
8760
+ const filtered = files.filter((f) => !metaSep.test(f));
8761
+ return hasModifiedAfter(filtered, new Date(meta._generatedAt).getTime());
8762
+ }
8763
+ /** Maximum staleness for never-synthesized metas (1 year in seconds). */
8764
+ const MAX_STALENESS_SECONDS = 365 * 86_400;
8765
+ /**
8766
+ * Compute actual staleness in seconds (now minus _generatedAt).
8767
+ *
8768
+ * Never-synthesized metas are capped at {@link MAX_STALENESS_SECONDS}
8769
+ * (1 year) so that depth weighting can differentiate them. Without
8770
+ * bounding, `Infinity * depthFactor` = `Infinity` for all depths.
8771
+ *
8772
+ * @param meta - Current meta.json content.
8773
+ * @returns Staleness in seconds, capped at 1 year for never-synthesized metas.
8774
+ */
8775
+ function actualStaleness(meta) {
8776
+ if (!meta._generatedAt)
8777
+ return MAX_STALENESS_SECONDS;
8778
+ const generatedMs = new Date(meta._generatedAt).getTime();
8779
+ return Math.min((Date.now() - generatedMs) / 1000, MAX_STALENESS_SECONDS);
8780
+ }
8781
+ /**
8782
+ * Check whether the architect step should be triggered.
8783
+ *
8784
+ * @param meta - Current meta.json.
8785
+ * @param structureChanged - Whether the structure hash changed.
8786
+ * @param steerChanged - Whether the steer directive changed.
8787
+ * @param architectEvery - Config: run architect every N cycles.
8788
+ * @returns True if the architect step should run.
8789
+ */
8790
+ function isArchitectTriggered(meta, structureChanged, steerChanged, architectEvery) {
8791
+ return (!meta._builder ||
8792
+ structureChanged ||
8793
+ steerChanged ||
8794
+ (meta._synthesisCount ?? 0) >= architectEvery);
8795
+ }
8796
+ /**
8797
+ * Detect whether the steer directive changed since the last archive.
8798
+ *
8799
+ * @param currentSteer - Current _steer value (or undefined).
8800
+ * @param archiveSteer - Archive _steer value (or undefined).
8801
+ * @param hasArchive - Whether an archive snapshot exists.
8802
+ * @returns True if steer changed.
8803
+ */
8804
+ function hasSteerChanged(currentSteer, archiveSteer, hasArchive) {
8805
+ if (!hasArchive)
8806
+ return Boolean(currentSteer);
8807
+ return currentSteer !== archiveSteer;
8808
+ }
8809
+ /**
8810
+ * Compute a normalized staleness score (0–1) for display purposes.
8811
+ *
8812
+ * Uses the same depth/emphasis weighting as candidate selection,
8813
+ * normalized to a 30-day window.
8814
+ *
8815
+ * @param stalenessSeconds - Raw staleness in seconds (null = never synthesized).
8816
+ * @param depth - Meta tree depth.
8817
+ * @param emphasis - Scheduling emphasis multiplier.
8818
+ * @param depthWeight - Depth weighting exponent from config.
8819
+ * @returns Normalized score between 0 and 1.
8820
+ */
8821
+ function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
8822
+ if (stalenessSeconds === null)
8823
+ return 1;
8824
+ const depthFactor = Math.pow(1 + depthWeight, depth);
8825
+ return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
8826
+ }
8827
+
8526
8828
  /**
8527
8829
  * Build the ownership tree from discovered .meta/ paths.
8528
8830
  *
@@ -8605,8 +8907,6 @@ function findNode(tree, targetPath) {
8605
8907
  *
8606
8908
  * @module discovery/listMetas
8607
8909
  */
8608
- /** Maximum staleness for never-synthesized metas (1 year in seconds). */
8609
- const MAX_STALENESS_SECONDS$1 = 365 * 86_400;
8610
8910
  /**
8611
8911
  * Discover, deduplicate, and enrich all metas.
8612
8912
  *
@@ -8642,7 +8942,7 @@ async function listMetas(config, watcher) {
8642
8942
  // Compute staleness
8643
8943
  let stalenessSeconds;
8644
8944
  if (neverSynth) {
8645
- stalenessSeconds = MAX_STALENESS_SECONDS$1;
8945
+ stalenessSeconds = MAX_STALENESS_SECONDS;
8646
8946
  }
8647
8947
  else {
8648
8948
  const genAt = new Date(meta._generatedAt).getTime();
@@ -8675,79 +8975,6 @@ async function listMetas(config, watcher) {
8675
8975
  };
8676
8976
  }
8677
8977
 
8678
- /**
8679
- * Escape special glob characters in a path so it can be used as a literal
8680
- * prefix in glob patterns.
8681
- *
8682
- * Glob metacharacters `* ? [ ] { } ( ) !` are escaped with a backslash so
8683
- * that paths containing parentheses (e.g. Slack channel IDs) or other
8684
- * special characters are matched literally by the watcher's walk endpoint.
8685
- *
8686
- * @module escapeGlob
8687
- */
8688
- /**
8689
- * Escape glob metacharacters in a string using character-class wrapping.
8690
- *
8691
- * Backslash escaping (`\(`) does not work reliably on Windows where `\` is
8692
- * the path separator. Instead, each metacharacter is wrapped in a character
8693
- * class (e.g. `(` → `[(]`) which is universally supported by glob libraries.
8694
- *
8695
- * Square brackets themselves are escaped as `[[]` and `[]]`.
8696
- *
8697
- * @param s - Raw path string.
8698
- * @returns String with glob metacharacters wrapped in character classes.
8699
- */
8700
- function escapeGlob(s) {
8701
- return s.replace(/[*?[\]{}()!]/g, (ch) => `[${ch}]`);
8702
- }
8703
-
8704
- /**
8705
- * Filter file paths by modification time.
8706
- *
8707
- * Shared utility for staleness detection and delta file enumeration.
8708
- * Uses `fs.statSync` for fast local mtime checks on known paths.
8709
- *
8710
- * @module mtimeFilter
8711
- */
8712
- /**
8713
- * Check if any file in the list was modified after the given timestamp.
8714
- *
8715
- * Short-circuits on first match for efficiency (staleness checks).
8716
- *
8717
- * @param files - Array of file paths to check.
8718
- * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` match.
8719
- * @returns True if any file was modified after the timestamp.
8720
- */
8721
- function hasModifiedAfter(files, afterMs) {
8722
- for (const filePath of files) {
8723
- try {
8724
- if (statSync(filePath).mtimeMs > afterMs)
8725
- return true;
8726
- }
8727
- catch {
8728
- // Unreadable file — skip
8729
- }
8730
- }
8731
- return false;
8732
- }
8733
- /**
8734
- * Filter files to only those modified after the given timestamp.
8735
- *
8736
- * @param files - Array of file paths to filter.
8737
- * @param afterMs - Timestamp in milliseconds. Files with `mtimeMs > afterMs` are included.
8738
- * @returns Filtered array of file paths.
8739
- */
8740
- function filterModifiedAfter(files, afterMs) {
8741
- return files.filter((filePath) => {
8742
- try {
8743
- return statSync(filePath).mtimeMs > afterMs;
8744
- }
8745
- catch {
8746
- return false;
8747
- }
8748
- });
8749
- }
8750
-
8751
8978
  /**
8752
8979
  * Compute the file scope owned by a meta node.
8753
8980
  *
@@ -8824,11 +9051,6 @@ function getDeltaFiles(generatedAt, scopeFiles) {
8824
9051
  return filterModifiedAfter(scopeFiles, new Date(generatedAt).getTime());
8825
9052
  }
8826
9053
 
8827
- /** Sleep for a given number of milliseconds. */
8828
- function sleep(ms) {
8829
- return new Promise((resolve) => setTimeout(resolve, ms));
8830
- }
8831
-
8832
9054
  /**
8833
9055
  * Error thrown when a spawned subprocess is aborted via AbortController.
8834
9056
  *
@@ -8983,7 +9205,7 @@ class GatewayExecutor {
8983
9205
  JSON.stringify(spawnResult));
8984
9206
  }
8985
9207
  // Step 2: Poll for completion via sessions_history
8986
- await sleep(3000);
9208
+ await sleepAsync(3000);
8987
9209
  while (Date.now() < deadline) {
8988
9210
  // Check for abort before each poll iteration
8989
9211
  if (this.controller.signal.aborted) {
@@ -9047,7 +9269,7 @@ class GatewayExecutor {
9047
9269
  catch {
9048
9270
  // Transient poll failure — keep trying
9049
9271
  }
9050
- await sleep(this.pollIntervalMs);
9272
+ await sleepAsync(this.pollIntervalMs);
9051
9273
  }
9052
9274
  throw new SpawnTimeoutError('Synthesis subprocess timed out after ' + timeoutMs.toString() + 'ms', outputPath);
9053
9275
  }
@@ -9720,101 +9942,6 @@ function discoverStalestPath(candidates, depthWeight) {
9720
9942
  return winner?.node.metaPath ?? null;
9721
9943
  }
9722
9944
 
9723
- /**
9724
- * Staleness detection via watcher walk.
9725
- *
9726
- * A meta is stale when any watched file in its scope was modified after
9727
- * `_generatedAt`.
9728
- *
9729
- * @module scheduling/staleness
9730
- */
9731
- /**
9732
- * Check if a meta is stale.
9733
- *
9734
- * Uses watcher `/walk` to enumerate watched files under the scope prefix,
9735
- * then applies a local mtime check (fast) to detect any modifications since
9736
- * `_generatedAt`. Short-circuits on first match.
9737
- *
9738
- * @param scopePrefix - Path prefix for this meta's scope.
9739
- * @param meta - Current meta.json content.
9740
- * @param watcher - WatcherClient instance.
9741
- * @returns True if any file in scope was modified after `_generatedAt`.
9742
- */
9743
- async function isStale(scopePrefix, meta, watcher) {
9744
- if (!meta._generatedAt)
9745
- return true; // Never synthesized = stale
9746
- const files = await watcher.walk([`${escapeGlob(scopePrefix)}/**`]);
9747
- // Exclude .meta/ subtree — synthesis outputs must not trigger staleness.
9748
- // Handle both forward and back slashes for cross-platform compatibility.
9749
- const metaSep = /[/\\]\.meta(?:[/\\]|$)/;
9750
- const filtered = files.filter((f) => !metaSep.test(f));
9751
- return hasModifiedAfter(filtered, new Date(meta._generatedAt).getTime());
9752
- }
9753
- /** Maximum staleness for never-synthesized metas (1 year in seconds). */
9754
- const MAX_STALENESS_SECONDS = 365 * 86_400;
9755
- /**
9756
- * Compute actual staleness in seconds (now minus _generatedAt).
9757
- *
9758
- * Never-synthesized metas are capped at {@link MAX_STALENESS_SECONDS}
9759
- * (1 year) so that depth weighting can differentiate them. Without
9760
- * bounding, `Infinity * depthFactor` = `Infinity` for all depths.
9761
- *
9762
- * @param meta - Current meta.json content.
9763
- * @returns Staleness in seconds, capped at 1 year for never-synthesized metas.
9764
- */
9765
- function actualStaleness(meta) {
9766
- if (!meta._generatedAt)
9767
- return MAX_STALENESS_SECONDS;
9768
- const generatedMs = new Date(meta._generatedAt).getTime();
9769
- return Math.min((Date.now() - generatedMs) / 1000, MAX_STALENESS_SECONDS);
9770
- }
9771
- /**
9772
- * Check whether the architect step should be triggered.
9773
- *
9774
- * @param meta - Current meta.json.
9775
- * @param structureChanged - Whether the structure hash changed.
9776
- * @param steerChanged - Whether the steer directive changed.
9777
- * @param architectEvery - Config: run architect every N cycles.
9778
- * @returns True if the architect step should run.
9779
- */
9780
- function isArchitectTriggered(meta, structureChanged, steerChanged, architectEvery) {
9781
- return (!meta._builder ||
9782
- structureChanged ||
9783
- steerChanged ||
9784
- (meta._synthesisCount ?? 0) >= architectEvery);
9785
- }
9786
- /**
9787
- * Detect whether the steer directive changed since the last archive.
9788
- *
9789
- * @param currentSteer - Current _steer value (or undefined).
9790
- * @param archiveSteer - Archive _steer value (or undefined).
9791
- * @param hasArchive - Whether an archive snapshot exists.
9792
- * @returns True if steer changed.
9793
- */
9794
- function hasSteerChanged(currentSteer, archiveSteer, hasArchive) {
9795
- if (!hasArchive)
9796
- return Boolean(currentSteer);
9797
- return currentSteer !== archiveSteer;
9798
- }
9799
- /**
9800
- * Compute a normalized staleness score (0–1) for display purposes.
9801
- *
9802
- * Uses the same depth/emphasis weighting as candidate selection,
9803
- * normalized to a 30-day window.
9804
- *
9805
- * @param stalenessSeconds - Raw staleness in seconds (null = never synthesized).
9806
- * @param depth - Meta tree depth.
9807
- * @param emphasis - Scheduling emphasis multiplier.
9808
- * @param depthWeight - Depth weighting exponent from config.
9809
- * @returns Normalized score between 0 and 1.
9810
- */
9811
- function computeStalenessScore(stalenessSeconds, depth, emphasis, depthWeight) {
9812
- if (stalenessSeconds === null)
9813
- return 1;
9814
- const depthFactor = Math.pow(1 + depthWeight, depth);
9815
- return Math.min(1, (stalenessSeconds * depthFactor * emphasis) / (30 * 86400));
9816
- }
9817
-
9818
9945
  /**
9819
9946
  * Shared error utilities.
9820
9947
  *
@@ -10875,7 +11002,10 @@ function buildMetaRules(config) {
10875
11002
  properties: {
10876
11003
  file: {
10877
11004
  properties: {
10878
- path: { type: 'string', glob: '**/jeeves-meta.config.json' },
11005
+ path: {
11006
+ type: 'string',
11007
+ glob: '**/jeeves-meta{.config.json,/config.json}',
11008
+ },
10879
11009
  },
10880
11010
  },
10881
11011
  },
@@ -12008,7 +12138,7 @@ class HttpWatcherClient {
12008
12138
  }
12009
12139
  // Exponential backoff
12010
12140
  const delayMs = this.backoffBaseMs * Math.pow(this.backoffFactor, attempt);
12011
- await sleep(delayMs);
12141
+ await sleepAsync(delayMs);
12012
12142
  }
12013
12143
  // Unreachable, but TypeScript needs it
12014
12144
  throw new Error('Retry exhausted');
@@ -12264,4 +12394,4 @@ const metaDescriptor = jeevesComponentDescriptorSchema.parse({
12264
12394
  customCliCommands: registerCustomCliCommands,
12265
12395
  });
12266
12396
 
12267
- export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, ProgressReporter, RESTART_REQUIRED_FIELDS, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, mergeAndWrite, metaConfigSchema, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleep, startService, toMetaError, verifyRuleApplication };
12397
+ export { DEFAULT_PORT, DEFAULT_PORT_STR, GatewayExecutor, HttpWatcherClient, MAX_STALENESS_SECONDS, ProgressReporter, RESTART_REQUIRED_FIELDS, RuleRegistrar, SERVICE_NAME, SERVICE_VERSION, Scheduler, SynthesisQueue, acquireLock, actualStaleness, buildArchitectTask, buildBuilderTask, buildContextPackage, buildCriticTask, buildOwnershipTree, cleanupStaleLocks, computeEffectiveStaleness, computeEma, computeStructureHash, createLogger, createServer, createSnapshot, discoverMetas, filterInScope, findNode, formatProgressEvent, getScopePrefix, hasSteerChanged, isArchitectTriggered, isLocked, isStale, listArchiveFiles, listMetas, loadServiceConfig, mergeAndWrite, metaConfigSchema, metaDescriptor, metaErrorSchema, metaJsonSchema, migrateConfigPath, normalizePath, orchestrate, parseArchitectOutput, parseBuilderOutput, parseCriticOutput, pruneArchive, readLatestArchive, readLockState, registerCustomCliCommands, registerRoutes, registerShutdownHandlers, releaseLock, resolveConfigPath, resolveMetaDir, selectCandidate, serviceConfigSchema, sleepAsync as sleep, startService, toMetaError, verifyRuleApplication };