@karmaniverous/jeeves-meta 0.15.3 → 0.15.5

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.
Files changed (89) hide show
  1. package/dist/archive/index.d.ts +10 -0
  2. package/dist/archive/listArchive.d.ts +12 -0
  3. package/dist/archive/prune.d.ts +14 -0
  4. package/dist/archive/readArchive.d.ts +30 -0
  5. package/dist/archive/readLatest.d.ts +13 -0
  6. package/dist/archive/snapshot.d.ts +17 -0
  7. package/dist/bootstrap.d.ts +15 -0
  8. package/dist/cache.d.ts +22 -0
  9. package/dist/cli/jeeves-meta/architect.md +17 -0
  10. package/dist/cli/jeeves-meta/index.js +811 -734
  11. package/dist/cli.d.ts +10 -0
  12. package/dist/configHotReload.d.ts +30 -0
  13. package/dist/configLoader.d.ts +37 -0
  14. package/dist/constants.d.ts +13 -0
  15. package/dist/customCliCommands.d.ts +13 -0
  16. package/dist/descriptor.d.ts +19 -0
  17. package/dist/discovery/buildMinimalNode.d.ts +22 -0
  18. package/dist/discovery/computeSummary.d.ts +17 -0
  19. package/dist/discovery/discoverMetas.d.ts +19 -0
  20. package/dist/discovery/index.d.ts +11 -0
  21. package/dist/discovery/listMetas.d.ts +63 -0
  22. package/dist/discovery/ownershipTree.d.ts +25 -0
  23. package/dist/discovery/scope.d.ts +47 -0
  24. package/dist/discovery/types.d.ts +25 -0
  25. package/dist/ema.d.ts +14 -0
  26. package/dist/errors.d.ts +15 -0
  27. package/dist/escapeGlob.d.ts +23 -0
  28. package/dist/executor/GatewayExecutor.d.ts +48 -0
  29. package/dist/executor/SpawnAbortedError.d.ts +9 -0
  30. package/dist/executor/SpawnTimeoutError.d.ts +13 -0
  31. package/dist/executor/index.d.ts +8 -0
  32. package/dist/index.d.ts +34 -1660
  33. package/dist/index.js +1434 -1767
  34. package/dist/interfaces/MetaContext.d.ts +36 -0
  35. package/dist/interfaces/MetaExecutor.d.ts +46 -0
  36. package/dist/interfaces/WatcherClient.d.ts +75 -0
  37. package/dist/interfaces/index.d.ts +8 -0
  38. package/dist/lock.d.ts +70 -0
  39. package/dist/logger/index.d.ts +27 -0
  40. package/dist/mtimeFilter.d.ts +26 -0
  41. package/dist/normalizePath.d.ts +6 -0
  42. package/dist/orchestrator/buildTask.d.ts +38 -0
  43. package/dist/orchestrator/contextPackage.d.ts +30 -0
  44. package/dist/orchestrator/index.d.ts +10 -0
  45. package/dist/orchestrator/orchestratePhase.d.ts +38 -0
  46. package/dist/orchestrator/parseOutput.d.ts +41 -0
  47. package/dist/orchestrator/runPhase.d.ts +40 -0
  48. package/dist/phaseState/derivePhaseState.d.ts +41 -0
  49. package/dist/phaseState/index.d.ts +9 -0
  50. package/dist/phaseState/invalidate.d.ts +41 -0
  51. package/dist/phaseState/phaseScheduler.d.ts +57 -0
  52. package/dist/phaseState/phaseTransitions.d.ts +83 -0
  53. package/dist/progress/index.d.ts +38 -0
  54. package/dist/prompts/architect.md +17 -0
  55. package/dist/prompts/index.d.ts +15 -0
  56. package/dist/queue/index.d.ts +131 -0
  57. package/dist/readMetaJson.d.ts +17 -0
  58. package/dist/routes/__testUtils.d.ts +37 -0
  59. package/dist/routes/config.d.ts +11 -0
  60. package/dist/routes/configApply.d.ts +13 -0
  61. package/dist/routes/index.d.ts +50 -0
  62. package/dist/routes/metas.d.ts +9 -0
  63. package/dist/routes/metasUpdate.d.ts +11 -0
  64. package/dist/routes/preview.d.ts +8 -0
  65. package/dist/routes/queue.d.ts +13 -0
  66. package/dist/routes/seed.d.ts +8 -0
  67. package/dist/routes/status.d.ts +13 -0
  68. package/dist/routes/synthesize.d.ts +12 -0
  69. package/dist/routes/unlock.d.ts +8 -0
  70. package/dist/rules/healthCheck.d.ts +36 -0
  71. package/dist/rules/index.d.ts +39 -0
  72. package/dist/rules/verify.d.ts +22 -0
  73. package/dist/scheduler/index.d.ts +66 -0
  74. package/dist/scheduling/index.d.ts +7 -0
  75. package/dist/scheduling/staleness.d.ts +68 -0
  76. package/dist/scheduling/weightedFormula.d.ts +38 -0
  77. package/dist/schema/config.d.ts +54 -0
  78. package/dist/schema/error.d.ts +6 -0
  79. package/dist/schema/index.d.ts +8 -0
  80. package/dist/schema/meta.d.ts +71 -0
  81. package/dist/seed/autoSeed.d.ts +30 -0
  82. package/dist/seed/createMeta.d.ts +38 -0
  83. package/dist/seed/index.d.ts +7 -0
  84. package/dist/server.d.ts +24 -0
  85. package/dist/shutdown/index.d.ts +33 -0
  86. package/dist/structureHash.d.ts +15 -0
  87. package/dist/watcher-client/HttpWatcherClient.d.ts +38 -0
  88. package/dist/watcher-client/index.d.ts +6 -0
  89. package/package.json +17 -27
@@ -1181,11 +1181,11 @@ var hasRequiredRetry$1;
1181
1181
  function requireRetry$1 () {
1182
1182
  if (hasRequiredRetry$1) return retry$1;
1183
1183
  hasRequiredRetry$1 = 1;
1184
- (function (exports$1) {
1184
+ (function (exports) {
1185
1185
  var RetryOperation = requireRetry_operation();
1186
1186
 
1187
- exports$1.operation = function(options) {
1188
- var timeouts = exports$1.timeouts(options);
1187
+ exports.operation = function(options) {
1188
+ var timeouts = exports.timeouts(options);
1189
1189
  return new RetryOperation(timeouts, {
1190
1190
  forever: options && options.forever,
1191
1191
  unref: options && options.unref,
@@ -1193,7 +1193,7 @@ function requireRetry$1 () {
1193
1193
  });
1194
1194
  };
1195
1195
 
1196
- exports$1.timeouts = function(options) {
1196
+ exports.timeouts = function(options) {
1197
1197
  if (options instanceof Array) {
1198
1198
  return [].concat(options);
1199
1199
  }
@@ -1230,7 +1230,7 @@ function requireRetry$1 () {
1230
1230
  return timeouts;
1231
1231
  };
1232
1232
 
1233
- exports$1.createTimeout = function(attempt, opts) {
1233
+ exports.createTimeout = function(attempt, opts) {
1234
1234
  var random = (opts.randomize)
1235
1235
  ? (Math.random() + 1)
1236
1236
  : 1;
@@ -1241,7 +1241,7 @@ function requireRetry$1 () {
1241
1241
  return timeout;
1242
1242
  };
1243
1243
 
1244
- exports$1.wrap = function(obj, options, methods) {
1244
+ exports.wrap = function(obj, options, methods) {
1245
1245
  if (options instanceof Array) {
1246
1246
  methods = options;
1247
1247
  options = null;
@@ -1261,7 +1261,7 @@ function requireRetry$1 () {
1261
1261
  var original = obj[method];
1262
1262
 
1263
1263
  obj[method] = function retryWrapper(original) {
1264
- var op = exports$1.operation(options);
1264
+ var op = exports.operation(options);
1265
1265
  var args = Array.prototype.slice.call(arguments, 1);
1266
1266
  var callback = args.pop();
1267
1267
 
@@ -4270,7 +4270,7 @@ var hasRequiredRe;
4270
4270
  function requireRe () {
4271
4271
  if (hasRequiredRe) return re.exports;
4272
4272
  hasRequiredRe = 1;
4273
- (function (module, exports$1) {
4273
+ (function (module, exports) {
4274
4274
 
4275
4275
  const {
4276
4276
  MAX_SAFE_COMPONENT_LENGTH,
@@ -4278,14 +4278,14 @@ function requireRe () {
4278
4278
  MAX_LENGTH,
4279
4279
  } = requireConstants();
4280
4280
  const debug = requireDebug();
4281
- exports$1 = module.exports = {};
4281
+ exports = module.exports = {};
4282
4282
 
4283
4283
  // The actual regexps go on exports.re
4284
- const re = exports$1.re = [];
4285
- const safeRe = exports$1.safeRe = [];
4286
- const src = exports$1.src = [];
4287
- const safeSrc = exports$1.safeSrc = [];
4288
- const t = exports$1.t = {};
4284
+ const re = exports.re = [];
4285
+ const safeRe = exports.safeRe = [];
4286
+ const src = exports.src = [];
4287
+ const safeSrc = exports.safeSrc = [];
4288
+ const t = exports.t = {};
4289
4289
  let R = 0;
4290
4290
 
4291
4291
  const LETTERDASHNUMBER = '[a-zA-Z0-9-]';
@@ -4408,7 +4408,7 @@ function requireRe () {
4408
4408
  createToken('GTLT', '((?:<|>)?=?)');
4409
4409
 
4410
4410
  // Something like "2.*" or "1.2.x".
4411
- // Note that "x.x" is a valid xRange identifer, meaning "any version"
4411
+ // Note that "x.x" is a valid xRange identifier, meaning "any version"
4412
4412
  // Only the first item is strictly required.
4413
4413
  createToken('XRANGEIDENTIFIERLOOSE', `${src[t.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`);
4414
4414
  createToken('XRANGEIDENTIFIER', `${src[t.NUMERICIDENTIFIER]}|x|X|\\*`);
@@ -4449,7 +4449,7 @@ function requireRe () {
4449
4449
  createToken('LONETILDE', '(?:~>?)');
4450
4450
 
4451
4451
  createToken('TILDETRIM', `(\\s*)${src[t.LONETILDE]}\\s+`, true);
4452
- exports$1.tildeTrimReplace = '$1~';
4452
+ exports.tildeTrimReplace = '$1~';
4453
4453
 
4454
4454
  createToken('TILDE', `^${src[t.LONETILDE]}${src[t.XRANGEPLAIN]}$`);
4455
4455
  createToken('TILDELOOSE', `^${src[t.LONETILDE]}${src[t.XRANGEPLAINLOOSE]}$`);
@@ -4459,7 +4459,7 @@ function requireRe () {
4459
4459
  createToken('LONECARET', '(?:\\^)');
4460
4460
 
4461
4461
  createToken('CARETTRIM', `(\\s*)${src[t.LONECARET]}\\s+`, true);
4462
- exports$1.caretTrimReplace = '$1^';
4462
+ exports.caretTrimReplace = '$1^';
4463
4463
 
4464
4464
  createToken('CARET', `^${src[t.LONECARET]}${src[t.XRANGEPLAIN]}$`);
4465
4465
  createToken('CARETLOOSE', `^${src[t.LONECARET]}${src[t.XRANGEPLAINLOOSE]}$`);
@@ -4472,7 +4472,7 @@ function requireRe () {
4472
4472
  // it modifies, so that `> 1.2.3` ==> `>1.2.3`
4473
4473
  createToken('COMPARATORTRIM', `(\\s*)${src[t.GTLT]
4474
4474
  }\\s*(${src[t.LOOSEPLAIN]}|${src[t.XRANGEPLAIN]})`, true);
4475
- exports$1.comparatorTrimReplace = '$1$2$3';
4475
+ exports.comparatorTrimReplace = '$1$2$3';
4476
4476
 
4477
4477
  // Something like `1.2.3 - 1.2.4`
4478
4478
  // Note that these all use the loose form, because they'll be
@@ -5404,6 +5404,62 @@ function requireCoerce () {
5404
5404
  return coerce_1;
5405
5405
  }
5406
5406
 
5407
+ var truncate_1;
5408
+ var hasRequiredTruncate;
5409
+
5410
+ function requireTruncate () {
5411
+ if (hasRequiredTruncate) return truncate_1;
5412
+ hasRequiredTruncate = 1;
5413
+
5414
+ const parse = requireParse();
5415
+ const constants = requireConstants();
5416
+ const SemVer = requireSemver$1();
5417
+
5418
+ const truncate = (version, truncation, options) => {
5419
+ if (!constants.RELEASE_TYPES.includes(truncation)) {
5420
+ return null
5421
+ }
5422
+
5423
+ const clonedVersion = cloneInputVersion(version, options);
5424
+ return clonedVersion && doTruncation(clonedVersion, truncation)
5425
+ };
5426
+
5427
+ const cloneInputVersion = (version, options) => {
5428
+ const versionStringToParse = (
5429
+ version instanceof SemVer ? version.version : version
5430
+ );
5431
+
5432
+ return parse(versionStringToParse, options)
5433
+ };
5434
+
5435
+ const doTruncation = (version, truncation) => {
5436
+ if (isPrerelease(truncation)) {
5437
+ return version.version
5438
+ }
5439
+
5440
+ version.prerelease = [];
5441
+
5442
+ switch (truncation) {
5443
+ case 'major':
5444
+ version.minor = 0;
5445
+ version.patch = 0;
5446
+ break
5447
+ case 'minor':
5448
+ version.patch = 0;
5449
+ break
5450
+ }
5451
+
5452
+ return version.format()
5453
+ };
5454
+
5455
+ const isPrerelease = (type) => {
5456
+ return type.startsWith('pre')
5457
+ };
5458
+
5459
+ truncate_1 = truncate;
5460
+ return truncate_1;
5461
+ }
5462
+
5407
5463
  var lrucache;
5408
5464
  var hasRequiredLrucache;
5409
5465
 
@@ -6853,6 +6909,7 @@ function requireSemver () {
6853
6909
  const lte = requireLte();
6854
6910
  const cmp = requireCmp();
6855
6911
  const coerce = requireCoerce();
6912
+ const truncate = requireTruncate();
6856
6913
  const Comparator = requireComparator();
6857
6914
  const Range = requireRange();
6858
6915
  const satisfies = requireSatisfies();
@@ -6891,6 +6948,7 @@ function requireSemver () {
6891
6948
  lte,
6892
6949
  cmp,
6893
6950
  coerce,
6951
+ truncate,
6894
6952
  Comparator,
6895
6953
  Range,
6896
6954
  satisfies,
@@ -7335,31 +7393,31 @@ var hasRequiredExtraTypings;
7335
7393
  function requireExtraTypings () {
7336
7394
  if (hasRequiredExtraTypings) return extraTypings.exports;
7337
7395
  hasRequiredExtraTypings = 1;
7338
- (function (module, exports$1) {
7396
+ (function (module, exports) {
7339
7397
  const commander = require$$0;
7340
7398
 
7341
- exports$1 = module.exports = {};
7399
+ exports = module.exports = {};
7342
7400
 
7343
7401
  // Return a different global program than commander,
7344
7402
  // and don't also return it as default export.
7345
- exports$1.program = new commander.Command();
7403
+ exports.program = new commander.Command();
7346
7404
 
7347
7405
  /**
7348
7406
  * Expose classes. The FooT versions are just types, so return Commander original implementations!
7349
7407
  */
7350
7408
 
7351
- exports$1.Argument = commander.Argument;
7352
- exports$1.Command = commander.Command;
7353
- exports$1.CommanderError = commander.CommanderError;
7354
- exports$1.Help = commander.Help;
7355
- exports$1.InvalidArgumentError = commander.InvalidArgumentError;
7356
- exports$1.InvalidOptionArgumentError = commander.InvalidArgumentError; // Deprecated
7357
- exports$1.Option = commander.Option;
7409
+ exports.Argument = commander.Argument;
7410
+ exports.Command = commander.Command;
7411
+ exports.CommanderError = commander.CommanderError;
7412
+ exports.Help = commander.Help;
7413
+ exports.InvalidArgumentError = commander.InvalidArgumentError;
7414
+ exports.InvalidOptionArgumentError = commander.InvalidArgumentError; // Deprecated
7415
+ exports.Option = commander.Option;
7358
7416
 
7359
- exports$1.createCommand = (name) => new commander.Command(name);
7360
- exports$1.createOption = (flags, description) =>
7417
+ exports.createCommand = (name) => new commander.Command(name);
7418
+ exports.createOption = (flags, description) =>
7361
7419
  new commander.Option(flags, description);
7362
- exports$1.createArgument = (name, description) =>
7420
+ exports.createArgument = (name, description) =>
7363
7421
  new commander.Argument(name, description);
7364
7422
  } (extraTypings, extraTypings.exports));
7365
7423
  return extraTypings.exports;
@@ -8419,75 +8477,33 @@ function sleepAsync(ms) {
8419
8477
  }
8420
8478
 
8421
8479
  /**
8422
- * Shared live config hot-reload support.
8423
- *
8424
- * Used by both file-watch reloads in bootstrap and POST /config/apply
8425
- * via the component descriptor's onConfigApply callback.
8426
- *
8427
- * @module configHotReload
8428
- */
8429
- /**
8430
- * Fields that require a service restart to take effect.
8480
+ * Shared component descriptor constants for jeeves-meta.
8431
8481
  *
8432
- * Shared between the descriptor's `onConfigApply` and the file-watcher
8433
- * hot-reload in `bootstrap.ts`.
8482
+ * Single source of truth consumed by both the service descriptor and
8483
+ * the OpenClaw plugin registration.
8434
8484
  */
8435
- const RESTART_REQUIRED_FIELDS = [
8436
- 'port',
8437
- 'watcherUrl',
8438
- 'gatewayUrl',
8439
- 'gatewayApiKey',
8440
- 'defaultArchitect',
8441
- 'defaultCritic',
8442
- ];
8443
- let runtime = null;
8444
- /** Register the active service runtime for config-apply hot reload. */
8445
- function registerConfigHotReloadRuntime(nextRuntime) {
8446
- runtime = nextRuntime;
8447
- }
8448
- /** Apply hot-reloadable config changes to the live shared config object. */
8449
- function applyHotReloadedConfig(newConfig) {
8450
- if (!runtime)
8451
- return;
8452
- const { config, logger, scheduler } = runtime;
8453
- for (const field of RESTART_REQUIRED_FIELDS) {
8454
- const oldVal = config[field];
8455
- const nextVal = newConfig[field];
8456
- if (oldVal !== nextVal) {
8457
- logger.warn({ field, oldValue: oldVal, newValue: nextVal }, 'Config field changed but requires restart to take effect');
8458
- }
8459
- }
8460
- if (newConfig.schedule !== config.schedule) {
8461
- scheduler?.updateSchedule(newConfig.schedule);
8462
- config.schedule = newConfig.schedule;
8463
- logger.info({ schedule: newConfig.schedule }, 'Schedule hot-reloaded');
8464
- }
8465
- if (newConfig.logging.level !== config.logging.level) {
8466
- logger.level = newConfig.logging.level;
8467
- config.logging.level = newConfig.logging.level;
8468
- logger.info({ level: newConfig.logging.level }, 'Log level hot-reloaded');
8469
- }
8470
- const restartSet = new Set(RESTART_REQUIRED_FIELDS);
8471
- for (const key of Object.keys(newConfig)) {
8472
- if (restartSet.has(key) || key === 'logging' || key === 'schedule') {
8473
- continue;
8474
- }
8475
- const oldVal = config[key];
8476
- const nextVal = newConfig[key];
8477
- if (JSON.stringify(oldVal) !== JSON.stringify(nextVal)) {
8478
- config[key] = nextVal;
8479
- logger.info({ field: key }, 'Config field hot-reloaded');
8480
- }
8481
- }
8482
- }
8485
+ /** Shared jeeves-meta component descriptor constants. */
8486
+ const META_COMPONENT = {
8487
+ name: 'meta',
8488
+ servicePackage: '@karmaniverous/jeeves-meta',
8489
+ pluginPackage: '@karmaniverous/jeeves-meta-openclaw',
8490
+ defaultPort: 1938};
8483
8491
 
8484
8492
  /**
8485
- * Zod schema for jeeves-meta service configuration.
8486
- *
8487
- * The service config is a strict superset of the core (library-compatible) meta config.
8493
+ * Structured error schema from a synthesis step failure.
8488
8494
  *
8489
- * @module schema/config
8490
8495
  */
8496
+ /** Zod schema for synthesis step errors. */
8497
+ z.object({
8498
+ /** Which step failed: 'architect', 'builder', or 'critic'. */
8499
+ step: z.enum(['architect', 'builder', 'critic']),
8500
+ /** Error classification code. */
8501
+ code: z.string(),
8502
+ /** Human-readable error message. */
8503
+ message: z.string(),
8504
+ });
8505
+
8506
+ /** Zod schema for the core (library-compatible) meta configuration. */
8491
8507
  /** Zod schema for the core (library-compatible) meta configuration. */
8492
8508
  const metaConfigSchema = z.object({
8493
8509
  /** Watcher service base URL. */
@@ -8525,114 +8541,40 @@ const metaConfigSchema = z.object({
8525
8541
  .record(z.string(), z.unknown())
8526
8542
  .default({ _meta: 'archive' }),
8527
8543
  });
8528
- /** Zod schema for logging configuration. */
8529
- const loggingSchema = z.object({
8530
- /** Log level. */
8531
- level: z.string().default('info'),
8532
- /** Optional file path for log output. */
8533
- file: z.string().optional(),
8534
- });
8535
- /** Zod schema for a single auto-seed policy rule. */
8536
- const autoSeedRuleSchema = z.object({
8537
- /** Glob pattern matched against watcher walk results. */
8538
- match: z.string(),
8539
- /** Optional steering prompt for seeded metas. */
8540
- steer: z.string().optional(),
8541
- /** Optional cross-references for seeded metas. */
8542
- crossRefs: z.array(z.string()).optional(),
8543
- });
8544
- /** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
8545
- const serviceConfigSchema = metaConfigSchema.extend({
8546
- /** HTTP port for the service (default: 1938). */
8547
- port: z.number().int().min(1).max(65535).default(1938),
8548
- /** Cron schedule for synthesis cycles (default: every 30 min). */
8549
- schedule: z.string().default('*/30 * * * *'),
8550
- /** Messaging channel name (e.g. 'slack'). Legacy: also used as target if reportTarget is unset. */
8551
- reportChannel: z.string().optional(),
8552
- /** Channel/user ID to send progress messages to. */
8553
- reportTarget: z.string().optional(),
8554
- /** Optional base URL for the service, used to construct entity links in progress reports. */
8555
- serverBaseUrl: z.string().optional(),
8556
- /** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
8557
- watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
8558
- /** Logging configuration. */
8559
- logging: loggingSchema.default(() => loggingSchema.parse({})),
8560
- /**
8561
- * Auto-seed policy: declarative rules for auto-creating .meta/ directories.
8562
- * Rules are evaluated in order; last match wins for steer/crossRefs.
8563
- */
8564
- autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
8565
- });
8566
8544
 
8567
8545
  /**
8568
- * Load and resolve jeeves-meta service config.
8569
- *
8570
- * Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
8571
- *
8572
- * @module configLoader
8573
- */
8574
- /**
8575
- * Deep-walk a value, replacing `\${VAR\}` patterns with process.env values.
8546
+ * Normalize file paths to forward slashes for consistency with watcher-indexed paths.
8576
8547
  *
8577
- * @param value - Arbitrary JSON-compatible value.
8578
- * @returns Value with env-var placeholders resolved.
8579
- */
8580
- function substituteEnvVars(value) {
8581
- if (typeof value === 'string') {
8582
- return value.replace(/\$\{([^}]+)\}/g, (_match, name) => {
8583
- const envVal = process.env[name];
8584
- if (envVal === undefined) {
8585
- throw new Error(`Environment variable ${name} is not set`);
8586
- }
8587
- return envVal;
8588
- });
8589
- }
8590
- if (Array.isArray(value)) {
8591
- return value.map(substituteEnvVars);
8592
- }
8593
- if (value !== null && typeof value === 'object') {
8594
- const result = {};
8595
- for (const [key, val] of Object.entries(value)) {
8596
- result[key] = substituteEnvVars(val);
8597
- }
8598
- return result;
8599
- }
8600
- return value;
8601
- }
8602
- /**
8603
- * Resolve \@file: references in a config value.
8548
+ * Watcher indexes paths with forward slashes (`j:/domains/...`). This utility
8549
+ * ensures all paths in the library use the same convention, regardless of
8550
+ * the platform's native separator.
8604
8551
  *
8605
- * @param value - String value that may start with "\@file:".
8606
- * @param baseDir - Base directory for resolving relative paths.
8607
- * @returns The resolved string (file contents or original value).
8608
8552
  */
8609
- function resolveFileRef(value, baseDir) {
8610
- if (!value.startsWith('@file:'))
8611
- return value;
8612
- const filePath = join(baseDir, value.slice(6));
8613
- return readFileSync(filePath, 'utf8');
8614
- }
8615
8553
  /**
8616
- * Load service config from a JSON file.
8617
- *
8618
- * Resolves \@file: references for defaultArchitect and defaultCritic,
8619
- * and substitutes environment-variable placeholders throughout.
8554
+ * Normalize a file path to forward slashes.
8620
8555
  *
8621
- * @param configPath - Path to config JSON file.
8622
- * @returns Validated ServiceConfig.
8556
+ * @param p - File path (may contain backslashes).
8557
+ * @returns Path with all backslashes replaced by forward slashes.
8623
8558
  */
8624
- function loadServiceConfig(configPath) {
8625
- const rawText = readFileSync(configPath, 'utf8');
8626
- const raw = substituteEnvVars(JSON.parse(rawText));
8627
- const baseDir = dirname(configPath);
8628
- if (typeof raw['defaultArchitect'] === 'string') {
8629
- raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
8630
- }
8631
- if (typeof raw['defaultCritic'] === 'string') {
8632
- raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
8633
- }
8634
- return serviceConfigSchema.parse(raw);
8559
+ function normalizePath(p) {
8560
+ return p.replaceAll('\\', '/');
8635
8561
  }
8562
+ /** Valid states for a synthesis phase. */
8563
+ const phaseStatuses = [
8564
+ 'fresh',
8565
+ 'stale',
8566
+ 'pending',
8567
+ 'running',
8568
+ 'failed',
8569
+ ];
8570
+ /** Zod schema for a per-phase status value. */
8571
+ const phaseStatusSchema = z.enum(phaseStatuses);
8572
+ /** Zod schema for the per-meta phase state record. */
8573
+ z.object({
8574
+ architect: phaseStatusSchema,
8575
+ builder: phaseStatusSchema,
8576
+ critic: phaseStatusSchema,
8577
+ });
8636
8578
 
8637
8579
  /**
8638
8580
  * Compute summary statistics from an array of MetaEntry objects.
@@ -8707,25 +8649,6 @@ function computeSummary(entries, depthWeight) {
8707
8649
  };
8708
8650
  }
8709
8651
 
8710
- /**
8711
- * Normalize file paths to forward slashes for consistency with watcher-indexed paths.
8712
- *
8713
- * Watcher indexes paths with forward slashes (`j:/domains/...`). This utility
8714
- * ensures all paths in the library use the same convention, regardless of
8715
- * the platform's native separator.
8716
- *
8717
- * @module normalizePath
8718
- */
8719
- /**
8720
- * Normalize a file path to forward slashes.
8721
- *
8722
- * @param p - File path (may contain backslashes).
8723
- * @returns Path with all backslashes replaced by forward slashes.
8724
- */
8725
- function normalizePath(p) {
8726
- return p.replaceAll('\\', '/');
8727
- }
8728
-
8729
8652
  /**
8730
8653
  * Discover .meta/ directories via watcher `/walk` endpoint.
8731
8654
  *
@@ -9291,18 +9214,238 @@ function getDeltaFiles(generatedAt, scopeFiles) {
9291
9214
  }
9292
9215
 
9293
9216
  /**
9294
- * Error thrown when a spawned subprocess is aborted via AbortController.
9217
+ * In-memory cache for listMetas results with TTL and concurrent refresh guard.
9295
9218
  *
9296
- * @module executor/SpawnAbortedError
9219
+ * @module cache
9297
9220
  */
9298
- /** Error indicating a spawn was deliberately aborted. */
9299
- class SpawnAbortedError extends Error {
9300
- constructor(message = 'Synthesis was aborted') {
9301
- super(message);
9302
- this.name = 'SpawnAbortedError';
9303
- }
9304
- }
9305
-
9221
+ const TTL_MS = 60_000;
9222
+ /**
9223
+ * Caches listMetas results to avoid expensive repeated filesystem walks.
9224
+ * Supports concurrent refresh coalescing and manual invalidation.
9225
+ */
9226
+ class MetaCache {
9227
+ result = null;
9228
+ updatedAt = 0;
9229
+ refreshPromise = null;
9230
+ /** Get cached result or refresh if stale. */
9231
+ async get(config, watcher) {
9232
+ if (this.result && Date.now() - this.updatedAt < TTL_MS) {
9233
+ return this.result;
9234
+ }
9235
+ return this.refresh(config, watcher);
9236
+ }
9237
+ /** Force-expire the cache so next get() triggers a refresh. */
9238
+ invalidate() {
9239
+ this.updatedAt = 0;
9240
+ }
9241
+ async refresh(config, watcher) {
9242
+ if (this.refreshPromise)
9243
+ return this.refreshPromise;
9244
+ this.refreshPromise = listMetas(config, watcher)
9245
+ .then((result) => {
9246
+ this.result = result;
9247
+ this.updatedAt = Date.now();
9248
+ return result;
9249
+ })
9250
+ .finally(() => {
9251
+ this.refreshPromise = null;
9252
+ });
9253
+ return this.refreshPromise;
9254
+ }
9255
+ }
9256
+
9257
+ /**
9258
+ * Shared live config hot-reload support.
9259
+ *
9260
+ * Used by both file-watch reloads in bootstrap and POST /config/apply
9261
+ * via the component descriptor's onConfigApply callback.
9262
+ *
9263
+ * @module configHotReload
9264
+ */
9265
+ /**
9266
+ * Fields that require a service restart to take effect.
9267
+ *
9268
+ * Shared between the descriptor's `onConfigApply` and the file-watcher
9269
+ * hot-reload in `bootstrap.ts`.
9270
+ */
9271
+ const RESTART_REQUIRED_FIELDS = [
9272
+ 'port',
9273
+ 'watcherUrl',
9274
+ 'gatewayUrl',
9275
+ 'gatewayApiKey',
9276
+ 'defaultArchitect',
9277
+ 'defaultCritic',
9278
+ ];
9279
+ let runtime = null;
9280
+ /** Register the active service runtime for config-apply hot reload. */
9281
+ function registerConfigHotReloadRuntime(nextRuntime) {
9282
+ runtime = nextRuntime;
9283
+ }
9284
+ /** Apply hot-reloadable config changes to the live shared config object. */
9285
+ function applyHotReloadedConfig(newConfig) {
9286
+ if (!runtime)
9287
+ return;
9288
+ const { config, logger, scheduler } = runtime;
9289
+ for (const field of RESTART_REQUIRED_FIELDS) {
9290
+ const oldVal = config[field];
9291
+ const nextVal = newConfig[field];
9292
+ if (oldVal !== nextVal) {
9293
+ logger.warn({ field, oldValue: oldVal, newValue: nextVal }, 'Config field changed but requires restart to take effect');
9294
+ }
9295
+ }
9296
+ if (newConfig.schedule !== config.schedule) {
9297
+ scheduler?.updateSchedule(newConfig.schedule);
9298
+ config.schedule = newConfig.schedule;
9299
+ logger.info({ schedule: newConfig.schedule }, 'Schedule hot-reloaded');
9300
+ }
9301
+ if (newConfig.logging.level !== config.logging.level) {
9302
+ logger.level = newConfig.logging.level;
9303
+ config.logging.level = newConfig.logging.level;
9304
+ logger.info({ level: newConfig.logging.level }, 'Log level hot-reloaded');
9305
+ }
9306
+ const restartSet = new Set(RESTART_REQUIRED_FIELDS);
9307
+ for (const key of Object.keys(newConfig)) {
9308
+ if (restartSet.has(key) || key === 'logging' || key === 'schedule') {
9309
+ continue;
9310
+ }
9311
+ const oldVal = config[key];
9312
+ const nextVal = newConfig[key];
9313
+ if (JSON.stringify(oldVal) !== JSON.stringify(nextVal)) {
9314
+ config[key] = nextVal;
9315
+ logger.info({ field: key }, 'Config field hot-reloaded');
9316
+ }
9317
+ }
9318
+ }
9319
+
9320
+ /**
9321
+ * Zod schema for jeeves-meta service configuration.
9322
+ *
9323
+ * The service config is a strict superset of the core (library-compatible) meta config.
9324
+ *
9325
+ * @module schema/config
9326
+ */
9327
+ /** Zod schema for logging configuration. */
9328
+ const loggingSchema = z.object({
9329
+ /** Log level. */
9330
+ level: z.string().default('info'),
9331
+ /** Optional file path for log output. */
9332
+ file: z.string().optional(),
9333
+ });
9334
+ /** Zod schema for a single auto-seed policy rule. */
9335
+ const autoSeedRuleSchema = z.object({
9336
+ /** Glob pattern matched against watcher walk results. */
9337
+ match: z.string(),
9338
+ /** Optional steering prompt for seeded metas. */
9339
+ steer: z.string().optional(),
9340
+ /** Optional cross-references for seeded metas. */
9341
+ crossRefs: z.array(z.string()).optional(),
9342
+ });
9343
+ /** Zod schema for jeeves-meta service configuration (superset of MetaConfig). */
9344
+ const serviceConfigSchema = metaConfigSchema.extend({
9345
+ /** HTTP port for the service (default: 1938). */
9346
+ port: z.number().int().min(1).max(65535).default(1938),
9347
+ /** Cron schedule for synthesis cycles (default: every 30 min). */
9348
+ schedule: z.string().default('*/30 * * * *'),
9349
+ /** Messaging channel name (e.g. 'slack'). Legacy: also used as target if reportTarget is unset. */
9350
+ reportChannel: z.string().optional(),
9351
+ /** Channel/user ID to send progress messages to. */
9352
+ reportTarget: z.string().optional(),
9353
+ /** Optional base URL for the service, used to construct entity links in progress reports. */
9354
+ serverBaseUrl: z.string().optional(),
9355
+ /** Interval in ms for periodic watcher health check. 0 = disabled. Default: 60000. */
9356
+ watcherHealthIntervalMs: z.number().int().min(0).default(60_000),
9357
+ /** Logging configuration. */
9358
+ logging: loggingSchema.default(() => loggingSchema.parse({})),
9359
+ /**
9360
+ * Auto-seed policy: declarative rules for auto-creating .meta/ directories.
9361
+ * Rules are evaluated in order; last match wins for steer/crossRefs.
9362
+ */
9363
+ autoSeed: z.array(autoSeedRuleSchema).optional().default([]),
9364
+ });
9365
+
9366
+ /**
9367
+ * Load and resolve jeeves-meta service config.
9368
+ *
9369
+ * Supports \@file: indirection and environment-variable substitution (dollar-brace pattern).
9370
+ *
9371
+ * @module configLoader
9372
+ */
9373
+ /**
9374
+ * Deep-walk a value, replacing `\${VAR\}` patterns with process.env values.
9375
+ *
9376
+ * @param value - Arbitrary JSON-compatible value.
9377
+ * @returns Value with env-var placeholders resolved.
9378
+ */
9379
+ function substituteEnvVars(value) {
9380
+ if (typeof value === 'string') {
9381
+ return value.replace(/\$\{([^}]+)\}/g, (_match, name) => {
9382
+ const envVal = process.env[name];
9383
+ if (envVal === undefined) {
9384
+ throw new Error(`Environment variable ${name} is not set`);
9385
+ }
9386
+ return envVal;
9387
+ });
9388
+ }
9389
+ if (Array.isArray(value)) {
9390
+ return value.map(substituteEnvVars);
9391
+ }
9392
+ if (value !== null && typeof value === 'object') {
9393
+ const result = {};
9394
+ for (const [key, val] of Object.entries(value)) {
9395
+ result[key] = substituteEnvVars(val);
9396
+ }
9397
+ return result;
9398
+ }
9399
+ return value;
9400
+ }
9401
+ /**
9402
+ * Resolve \@file: references in a config value.
9403
+ *
9404
+ * @param value - String value that may start with "\@file:".
9405
+ * @param baseDir - Base directory for resolving relative paths.
9406
+ * @returns The resolved string (file contents or original value).
9407
+ */
9408
+ function resolveFileRef(value, baseDir) {
9409
+ if (!value.startsWith('@file:'))
9410
+ return value;
9411
+ const filePath = join(baseDir, value.slice(6));
9412
+ return readFileSync(filePath, 'utf8');
9413
+ }
9414
+ /**
9415
+ * Load service config from a JSON file.
9416
+ *
9417
+ * Resolves \@file: references for defaultArchitect and defaultCritic,
9418
+ * and substitutes environment-variable placeholders throughout.
9419
+ *
9420
+ * @param configPath - Path to config JSON file.
9421
+ * @returns Validated ServiceConfig.
9422
+ */
9423
+ function loadServiceConfig(configPath) {
9424
+ const rawText = readFileSync(configPath, 'utf8');
9425
+ const raw = substituteEnvVars(JSON.parse(rawText));
9426
+ const baseDir = dirname(configPath);
9427
+ if (typeof raw['defaultArchitect'] === 'string') {
9428
+ raw['defaultArchitect'] = resolveFileRef(raw['defaultArchitect'], baseDir);
9429
+ }
9430
+ if (typeof raw['defaultCritic'] === 'string') {
9431
+ raw['defaultCritic'] = resolveFileRef(raw['defaultCritic'], baseDir);
9432
+ }
9433
+ return serviceConfigSchema.parse(raw);
9434
+ }
9435
+
9436
+ /**
9437
+ * Error thrown when a spawned subprocess is aborted via AbortController.
9438
+ *
9439
+ * @module executor/SpawnAbortedError
9440
+ */
9441
+ /** Error indicating a spawn was deliberately aborted. */
9442
+ class SpawnAbortedError extends Error {
9443
+ constructor(message = 'Synthesis was aborted') {
9444
+ super(message);
9445
+ this.name = 'SpawnAbortedError';
9446
+ }
9447
+ }
9448
+
9306
9449
  /**
9307
9450
  * Error thrown when a spawned subprocess times out.
9308
9451
  *
@@ -9388,21 +9531,29 @@ class GatewayExecutor {
9388
9531
  }
9389
9532
  return data;
9390
9533
  }
9391
- /** Look up totalTokens for a session via sessions_list. */
9392
- async getSessionTokens(sessionKey) {
9534
+ /** Look up session metadata (tokens, completion status) via sessions_list. */
9535
+ async getSessionInfo(sessionKey) {
9393
9536
  try {
9394
9537
  const result = await this.invoke('sessions_list', {
9395
- limit: 20,
9538
+ limit: 200,
9396
9539
  messageLimit: 0,
9397
9540
  });
9398
9541
  const sessions = (result.result?.details?.sessions ??
9399
9542
  result.result?.sessions ??
9400
9543
  []);
9401
9544
  const match = sessions.find((s) => s.key === sessionKey);
9402
- return match?.totalTokens ?? undefined;
9545
+ if (!match) {
9546
+ // Session absent from list — likely cleaned up after completion.
9547
+ // With limit=200 this is reliable; a false positive here only
9548
+ // means we read the output file slightly early (still correct
9549
+ // if the file exists).
9550
+ return { completed: true };
9551
+ }
9552
+ const done = match.status === 'completed' || match.status === 'done';
9553
+ return { tokens: match.totalTokens, completed: done };
9403
9554
  }
9404
9555
  catch {
9405
- return undefined;
9556
+ return { completed: false };
9406
9557
  }
9407
9558
  }
9408
9559
  /** Whether this executor has been aborted by the operator. */
@@ -9444,8 +9595,10 @@ class GatewayExecutor {
9444
9595
  ...(options?.thinking ? { thinking: options.thinking } : {}),
9445
9596
  ...(options?.model ? { model: options.model } : {}),
9446
9597
  });
9447
- const details = (spawnResult.result?.details ?? spawnResult.result);
9448
- const sessionKey = details?.childSessionKey ?? details?.sessionKey;
9598
+ const details = (spawnResult.result?.details ??
9599
+ spawnResult.result ??
9600
+ {});
9601
+ const sessionKey = details.childSessionKey ?? details.sessionKey;
9449
9602
  if (typeof sessionKey !== 'string' || !sessionKey) {
9450
9603
  throw new Error('Gateway sessions_spawn returned no sessionKey: ' +
9451
9604
  JSON.stringify(spawnResult));
@@ -9468,48 +9621,53 @@ class GatewayExecutor {
9468
9621
  historyResult.result?.messages ??
9469
9622
  [];
9470
9623
  const msgArray = messages;
9624
+ // Check 1: terminal stop reason in history
9625
+ let historyDone = false;
9471
9626
  if (msgArray.length > 0) {
9472
9627
  const lastMsg = msgArray[msgArray.length - 1];
9473
- // Complete when last message is assistant with a terminal stop reason
9474
9628
  if (lastMsg.role === 'assistant' &&
9475
9629
  lastMsg.stopReason &&
9476
9630
  lastMsg.stopReason !== 'toolUse' &&
9477
9631
  lastMsg.stopReason !== 'error') {
9478
- // Fetch token usage from session metadata
9479
- const tokens = await this.getSessionTokens(sessionKey);
9480
- // Read output from file (sub-agent wrote it via Write tool)
9481
- if (existsSync(outputPath)) {
9632
+ historyDone = true;
9633
+ }
9634
+ }
9635
+ // Check 2: session completion status via sessions_list
9636
+ const sessionInfo = await this.getSessionInfo(sessionKey);
9637
+ if (historyDone || sessionInfo.completed) {
9638
+ const tokens = sessionInfo.tokens;
9639
+ // Read output from file (sub-agent wrote it via Write tool)
9640
+ if (existsSync(outputPath)) {
9641
+ try {
9642
+ const output = readFileSync(outputPath, 'utf8');
9643
+ return { output, tokens };
9644
+ }
9645
+ finally {
9482
9646
  try {
9483
- const output = readFileSync(outputPath, 'utf8');
9484
- return { output, tokens };
9647
+ unlinkSync(outputPath);
9485
9648
  }
9486
- finally {
9487
- try {
9488
- unlinkSync(outputPath);
9489
- }
9490
- catch {
9491
- /* cleanup best-effort */
9492
- }
9649
+ catch {
9650
+ /* cleanup best-effort */
9493
9651
  }
9494
9652
  }
9495
- // Fallback: extract from message content if file wasn't written
9496
- for (let i = msgArray.length - 1; i >= 0; i--) {
9497
- const msg = msgArray[i];
9498
- if (msg.role === 'assistant' && msg.content) {
9499
- const text = typeof msg.content === 'string'
9653
+ }
9654
+ // Fallback: extract from message content if file wasn't written
9655
+ for (let i = msgArray.length - 1; i >= 0; i--) {
9656
+ const msg = msgArray[i];
9657
+ if (msg.role === 'assistant' && msg.content) {
9658
+ const text = typeof msg.content === 'string'
9659
+ ? msg.content
9660
+ : Array.isArray(msg.content)
9500
9661
  ? msg.content
9501
- : Array.isArray(msg.content)
9502
- ? msg.content
9503
- .filter((b) => b.type === 'text' && b.text)
9504
- .map((b) => b.text)
9505
- .join('\n')
9506
- : '';
9507
- if (text)
9508
- return { output: text, tokens };
9509
- }
9662
+ .filter((b) => b.type === 'text' && b.text)
9663
+ .map((b) => b.text)
9664
+ .join('\n')
9665
+ : '';
9666
+ if (text)
9667
+ return { output: text, tokens };
9510
9668
  }
9511
- return { output: '', tokens };
9512
9669
  }
9670
+ return { output: '', tokens };
9513
9671
  }
9514
9672
  }
9515
9673
  catch {
@@ -9872,6 +10030,7 @@ async function buildContextPackage(node, meta, watcher, logger) {
9872
10030
  *
9873
10031
  * @module orchestrator/buildTask
9874
10032
  */
10033
+ Handlebars.registerHelper('gt', (a, b) => a > b);
9875
10034
  /** Build the template context from synthesis inputs. */
9876
10035
  function buildTemplateContext(ctx, meta, config) {
9877
10036
  return {
@@ -10026,134 +10185,6 @@ function buildCriticTask(ctx, meta, config) {
10026
10185
  return compileTemplate(sections.join('\n'), buildTemplateContext(ctx, meta, config));
10027
10186
  }
10028
10187
 
10029
- /**
10030
- * Structured error from a synthesis step failure.
10031
- *
10032
- * @module schema/error
10033
- */
10034
- /** Zod schema for synthesis step errors. */
10035
- const metaErrorSchema = z.object({
10036
- /** Which step failed: 'architect', 'builder', or 'critic'. */
10037
- step: z.enum(['architect', 'builder', 'critic']),
10038
- /** Error classification code. */
10039
- code: z.string(),
10040
- /** Human-readable error message. */
10041
- message: z.string(),
10042
- });
10043
-
10044
- /**
10045
- * Zod schema for .meta/meta.json files.
10046
- *
10047
- * Reserved properties are underscore-prefixed and engine-managed.
10048
- * All other keys are open schema (builder output).
10049
- *
10050
- * @module schema/meta
10051
- */
10052
- /** Valid states for a synthesis phase. */
10053
- const phaseStatuses = [
10054
- 'fresh',
10055
- 'stale',
10056
- 'pending',
10057
- 'running',
10058
- 'failed',
10059
- ];
10060
- /** Zod schema for a per-phase status value. */
10061
- const phaseStatusSchema = z.enum(phaseStatuses);
10062
- /** Zod schema for the per-meta phase state record. */
10063
- const phaseStateSchema = z.object({
10064
- architect: phaseStatusSchema,
10065
- builder: phaseStatusSchema,
10066
- critic: phaseStatusSchema,
10067
- });
10068
- /** Zod schema for the reserved (underscore-prefixed) meta.json properties. */
10069
- z
10070
- .object({
10071
- /** Stable identity. Auto-generated on first synthesis if not provided. */
10072
- _id: z.uuid().optional(),
10073
- /** Human-provided steering prompt. Optional. */
10074
- _steer: z.string().optional(),
10075
- /**
10076
- * Explicit cross-references to other meta owner paths.
10077
- * Referenced metas' _content is included as architect/builder context.
10078
- */
10079
- _crossRefs: z.array(z.string()).optional(),
10080
- /** Architect system prompt used this turn. Defaults from config. */
10081
- _architect: z.string().optional(),
10082
- /**
10083
- * Task brief generated by the architect. Cached and reused across cycles;
10084
- * regenerated only when triggered.
10085
- */
10086
- _builder: z.string().optional(),
10087
- /** Critic system prompt used this turn. Defaults from config. */
10088
- _critic: z.string().optional(),
10089
- /** Timestamp of last synthesis. ISO 8601. */
10090
- _generatedAt: z.iso.datetime().optional(),
10091
- /** Narrative synthesis output. Rendered by watcher for embedding. */
10092
- _content: z.string().optional(),
10093
- /**
10094
- * Hash of sorted file listing in scope. Detects directory structure
10095
- * changes that trigger an architect re-run.
10096
- */
10097
- _structureHash: z.string().optional(),
10098
- /**
10099
- * Cycles since last architect run. Reset to 0 when architect runs.
10100
- * Used with architectEvery to trigger periodic re-prompting.
10101
- */
10102
- _synthesisCount: z.number().int().min(0).optional(),
10103
- /** Critic evaluation of the last synthesis. */
10104
- _feedback: z.string().optional(),
10105
- /**
10106
- * Present and true on archive snapshots. Distinguishes live vs. archived
10107
- * metas.
10108
- */
10109
- _archived: z.boolean().optional(),
10110
- /** Timestamp when this snapshot was archived. ISO 8601. */
10111
- _archivedAt: z.iso.datetime().optional(),
10112
- /**
10113
- * Scheduling priority. Higher = updates more often. Negative allowed;
10114
- * normalized to min 0 at scheduling time.
10115
- */
10116
- _depth: z.number().optional(),
10117
- /**
10118
- * Emphasis multiplier for depth weighting in scheduling.
10119
- * Default 1. Higher values increase this meta's scheduling priority
10120
- * relative to its depth. Set to 0.5 to halve the depth effect,
10121
- * 2 to double it, 0 to ignore depth entirely for this meta.
10122
- */
10123
- _emphasis: z.number().min(0).optional(),
10124
- /** Token count from last architect subprocess call. */
10125
- _architectTokens: z.number().int().optional(),
10126
- /** Token count from last builder subprocess call. */
10127
- _builderTokens: z.number().int().optional(),
10128
- /** Token count from last critic subprocess call. */
10129
- _criticTokens: z.number().int().optional(),
10130
- /** Exponential moving average of architect token usage (decay 0.3). */
10131
- _architectTokensAvg: z.number().optional(),
10132
- /** Exponential moving average of builder token usage (decay 0.3). */
10133
- _builderTokensAvg: z.number().optional(),
10134
- /** Exponential moving average of critic token usage (decay 0.3). */
10135
- _criticTokensAvg: z.number().optional(),
10136
- /**
10137
- * Opaque state carried across synthesis cycles for progressive work.
10138
- * Set by the builder, passed back as context on next cycle.
10139
- */
10140
- _state: z.unknown().optional(),
10141
- /**
10142
- * Structured error from last cycle. Present when a step failed.
10143
- * Cleared on successful cycle.
10144
- */
10145
- _error: metaErrorSchema.optional(),
10146
- /** When true, this meta is skipped during staleness scheduling. Manual trigger still works. */
10147
- _disabled: z.boolean().optional(),
10148
- /**
10149
- * Per-phase state machine record. Engine-managed.
10150
- * Keyed by phase name (architect, builder, critic) with status values.
10151
- * Persisted to survive ticks; derived on first load for back-compat.
10152
- */
10153
- _phaseState: phaseStateSchema.optional(),
10154
- })
10155
- .loose();
10156
-
10157
10188
  /**
10158
10189
  * Build a minimal MetaNode from a known meta path using watcher walk.
10159
10190
  *
@@ -10215,222 +10246,6 @@ async function buildMinimalNode(metaPath, watcher) {
10215
10246
  return node;
10216
10247
  }
10217
10248
 
10218
- /**
10219
- * Weighted staleness formula for candidate selection.
10220
- *
10221
- * effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
10222
- *
10223
- * @module scheduling/weightedFormula
10224
- */
10225
- /**
10226
- * Compute effective staleness for a set of candidates.
10227
- *
10228
- * Normalizes depths so the minimum becomes 0, then applies the formula:
10229
- * effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
10230
- *
10231
- * Per-meta _emphasis (default 1) multiplies depthWeight, allowing individual
10232
- * metas to tune how much their tree position affects scheduling.
10233
- *
10234
- * @param candidates - Array of \{ node, meta, actualStaleness \}.
10235
- * @param depthWeight - Exponent for depth weighting (0 = pure staleness).
10236
- * @returns Same array with effectiveStaleness computed.
10237
- */
10238
- function computeEffectiveStaleness(candidates, depthWeight) {
10239
- if (candidates.length === 0)
10240
- return [];
10241
- // Get depth for each candidate: use _depth override or tree depth
10242
- const depths = candidates.map((c) => c.meta._depth ?? c.node.treeDepth);
10243
- // Normalize: shift so minimum becomes 0
10244
- const minDepth = Math.min(...depths);
10245
- const normalizedDepths = depths.map((d) => Math.max(0, d - minDepth));
10246
- return candidates.map((c, i) => {
10247
- const emphasis = c.meta._emphasis ?? 1;
10248
- return {
10249
- ...c,
10250
- effectiveStaleness: c.actualStaleness *
10251
- Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
10252
- };
10253
- });
10254
- }
10255
-
10256
- /**
10257
- * Select the best synthesis candidate from stale metas.
10258
- *
10259
- * Picks the meta with highest effective staleness.
10260
- *
10261
- * @module scheduling/selectCandidate
10262
- */
10263
- /**
10264
- * Select the candidate with the highest effective staleness.
10265
- *
10266
- * @param candidates - Array of candidates with computed effective staleness.
10267
- * @returns The winning candidate, or null if no candidates.
10268
- */
10269
- function selectCandidate(candidates) {
10270
- if (candidates.length === 0)
10271
- return null;
10272
- let best = candidates[0];
10273
- for (let i = 1; i < candidates.length; i++) {
10274
- if (candidates[i].effectiveStaleness > best.effectiveStaleness) {
10275
- best = candidates[i];
10276
- }
10277
- }
10278
- return best;
10279
- }
10280
- /**
10281
- * Extract stale candidates from a list and return the stalest path.
10282
- *
10283
- * Consolidates the repeated pattern of:
10284
- * filter → computeEffectiveStaleness → selectCandidate → return path
10285
- *
10286
- * @param candidates - Array with node, meta, and stalenessSeconds.
10287
- * @param depthWeight - Depth weighting exponent from config.
10288
- * @returns The stalest candidate's metaPath, or null if none are stale.
10289
- */
10290
- function discoverStalestPath(candidates, depthWeight) {
10291
- const weighted = computeEffectiveStaleness(candidates, depthWeight);
10292
- const winner = selectCandidate(weighted);
10293
- return winner?.node.metaPath ?? null;
10294
- }
10295
-
10296
- /**
10297
- * Shared error utilities.
10298
- *
10299
- * @module errors
10300
- */
10301
- /**
10302
- * Wrap an unknown caught value into a MetaError.
10303
- *
10304
- * @param step - Which synthesis step failed.
10305
- * @param err - The caught error value.
10306
- * @param code - Error classification code.
10307
- * @returns A structured MetaError.
10308
- */
10309
- function toMetaError(step, err, code = 'FAILED') {
10310
- return {
10311
- step,
10312
- code,
10313
- message: err instanceof Error ? err.message : String(err),
10314
- };
10315
- }
10316
-
10317
- /**
10318
- * Compute a structure hash from a sorted file listing.
10319
- *
10320
- * Used to detect when directory structure changes, triggering
10321
- * an architect re-run.
10322
- *
10323
- * @module structureHash
10324
- */
10325
- /**
10326
- * Compute a SHA-256 hash of a sorted file listing.
10327
- *
10328
- * @param filePaths - Array of file paths in scope.
10329
- * @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
10330
- */
10331
- function computeStructureHash(filePaths) {
10332
- const sorted = [...filePaths].sort();
10333
- const content = sorted.join('\n');
10334
- return createHash('sha256').update(content).digest('hex');
10335
- }
10336
-
10337
- /**
10338
- * Parse subprocess outputs for each synthesis step.
10339
- *
10340
- * - Architect: returns text \> _builder
10341
- * - Builder: returns JSON \> _content + structured fields
10342
- * - Critic: returns text \> _feedback
10343
- *
10344
- * @module orchestrator/parseOutput
10345
- */
10346
- /**
10347
- * Parse architect output. The architect returns a task brief as text.
10348
- *
10349
- * @param output - Raw subprocess output.
10350
- * @returns The task brief string.
10351
- */
10352
- function parseArchitectOutput(output) {
10353
- return output.trim();
10354
- }
10355
- /**
10356
- * Parse builder output. The builder returns JSON with _content and optional fields.
10357
- *
10358
- * Attempts JSON parse first. If that fails, treats the entire output as _content.
10359
- *
10360
- * @param output - Raw subprocess output.
10361
- * @returns Parsed builder output with content and structured fields.
10362
- */
10363
- function parseBuilderOutput(output) {
10364
- const trimmed = output.trim();
10365
- // Strategy 1: Try to parse the entire output as JSON directly
10366
- const direct = tryParseJson(trimmed);
10367
- if (direct)
10368
- return direct;
10369
- // Strategy 2: Try all fenced code blocks (last match first — models often narrate then output)
10370
- const fencePattern = /```(?:json)?\s*([\s\S]*?)```/g;
10371
- const fenceMatches = [];
10372
- let match;
10373
- while ((match = fencePattern.exec(trimmed)) !== null) {
10374
- fenceMatches.push(match[1].trim());
10375
- }
10376
- // Try last fence first (most likely to be the actual output)
10377
- for (let i = fenceMatches.length - 1; i >= 0; i--) {
10378
- const result = tryParseJson(fenceMatches[i]);
10379
- if (result)
10380
- return result;
10381
- }
10382
- // Strategy 3: Find outermost { ... } braces
10383
- const firstBrace = trimmed.indexOf('{');
10384
- const lastBrace = trimmed.lastIndexOf('}');
10385
- if (firstBrace !== -1 && lastBrace > firstBrace) {
10386
- const result = tryParseJson(trimmed.substring(firstBrace, lastBrace + 1));
10387
- if (result)
10388
- return result;
10389
- }
10390
- // Fallback: treat entire output as content
10391
- return { content: trimmed, fields: {} };
10392
- }
10393
- /** Try to parse a string as JSON and extract builder output fields. */
10394
- function tryParseJson(str) {
10395
- try {
10396
- const raw = JSON.parse(str);
10397
- if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
10398
- return null;
10399
- }
10400
- const parsed = raw;
10401
- // Extract _content
10402
- const content = typeof parsed['_content'] === 'string'
10403
- ? parsed['_content']
10404
- : typeof parsed['content'] === 'string'
10405
- ? parsed['content']
10406
- : null;
10407
- if (content === null)
10408
- return null;
10409
- // Extract _state (the ONLY underscore key the builder is allowed to set)
10410
- const state = '_state' in parsed ? parsed['_state'] : undefined;
10411
- // Extract non-underscore fields
10412
- const fields = {};
10413
- for (const [key, value] of Object.entries(parsed)) {
10414
- if (!key.startsWith('_') && key !== 'content') {
10415
- fields[key] = value;
10416
- }
10417
- }
10418
- return { content, fields, ...(state !== undefined ? { state } : {}) };
10419
- }
10420
- catch {
10421
- return null;
10422
- }
10423
- }
10424
- /**
10425
- * Parse critic output. The critic returns evaluation text.
10426
- *
10427
- * @param output - Raw subprocess output.
10428
- * @returns The feedback string.
10429
- */
10430
- function parseCriticOutput(output) {
10431
- return output.trim();
10432
- }
10433
-
10434
10249
  /**
10435
10250
  * Pure phase-state transition functions.
10436
10251
  *
@@ -10480,7 +10295,42 @@ function enforceInvariant(state) {
10480
10295
  // running in non-first position would be a bug, but don't mask it
10481
10296
  }
10482
10297
  }
10483
- return result;
10298
+ return result;
10299
+ }
10300
+ // ── Invalidation cascades ──────────────────────────────────────────────
10301
+ /**
10302
+ * Architect invalidated: architect → pending; builder, critic → stale.
10303
+ * Triggers: _structureHash change, _steer change, _architect change,
10304
+ * _crossRefs declaration change, _synthesisCount \>= architectEvery.
10305
+ */
10306
+ function invalidateArchitect(state) {
10307
+ return enforceInvariant({
10308
+ architect: state.architect === 'failed' ? 'failed' : 'pending',
10309
+ builder: state.builder === 'fresh' ? 'stale' : state.builder,
10310
+ critic: state.critic === 'fresh' ? 'stale' : state.critic,
10311
+ });
10312
+ }
10313
+ /**
10314
+ * Builder invalidated (scope mtime or cross-ref _content change):
10315
+ * builder → pending; critic → stale.
10316
+ * Only applies when architect is fresh; otherwise, builder stays stale.
10317
+ */
10318
+ function invalidateBuilder(state) {
10319
+ if (state.architect !== 'fresh') {
10320
+ // Architect is not fresh — builder stays stale (or whatever it is)
10321
+ return enforceInvariant({
10322
+ ...state,
10323
+ builder: state.builder === 'fresh' || state.builder === 'stale'
10324
+ ? 'stale'
10325
+ : state.builder,
10326
+ critic: state.critic === 'fresh' ? 'stale' : state.critic,
10327
+ });
10328
+ }
10329
+ return enforceInvariant({
10330
+ ...state,
10331
+ builder: state.builder === 'failed' ? 'failed' : 'pending',
10332
+ critic: state.critic === 'fresh' ? 'stale' : state.critic,
10333
+ });
10484
10334
  }
10485
10335
  // ── Phase success transitions ──────────────────────────────────────────
10486
10336
  /**
@@ -10650,7 +10500,9 @@ function derivePhaseState(meta, inputs) {
10650
10500
  }
10651
10501
  // Check architect invalidation (when inputs are provided)
10652
10502
  if (inputs) {
10653
- const architectInvalidated = inputs.structureChanged ||
10503
+ // Progressive metas: structure changes invalidate builder, not architect
10504
+ const structureInvalidatesArchitect = inputs.structureChanged && meta._state === undefined;
10505
+ const architectInvalidated = structureInvalidatesArchitect ||
10654
10506
  inputs.steerChanged ||
10655
10507
  inputs.architectChanged ||
10656
10508
  inputs.crossRefsChanged ||
@@ -10662,6 +10514,14 @@ function derivePhaseState(meta, inputs) {
10662
10514
  critic: 'stale',
10663
10515
  };
10664
10516
  }
10517
+ // Progressive meta with structure change: builder-only invalidation
10518
+ if (inputs.structureChanged && meta._state !== undefined) {
10519
+ return {
10520
+ architect: 'fresh',
10521
+ builder: 'pending',
10522
+ critic: 'stale',
10523
+ };
10524
+ }
10665
10525
  }
10666
10526
  // Has _builder but no _content: builder is pending
10667
10527
  if (meta._builder && !meta._content) {
@@ -10683,6 +10543,154 @@ function derivePhaseState(meta, inputs) {
10683
10543
  return freshPhaseState();
10684
10544
  }
10685
10545
 
10546
+ /**
10547
+ * Compute a structure hash from a sorted file listing.
10548
+ *
10549
+ * Used to detect when directory structure changes, triggering
10550
+ * an architect re-run.
10551
+ *
10552
+ * @module structureHash
10553
+ */
10554
+ /**
10555
+ * Compute a SHA-256 hash of a sorted file listing.
10556
+ *
10557
+ * @param filePaths - Array of file paths in scope.
10558
+ * @returns Hex-encoded SHA-256 hash of the sorted, newline-joined paths.
10559
+ */
10560
+ function computeStructureHash(filePaths) {
10561
+ const sorted = [...filePaths].sort();
10562
+ const content = sorted.join('\n');
10563
+ return createHash('sha256').update(content).digest('hex');
10564
+ }
10565
+
10566
+ /**
10567
+ * Per-tick invalidation pass.
10568
+ *
10569
+ * Computes architect-invalidating and builder-invalidating inputs for a meta,
10570
+ * then applies the cascade to update _phaseState.
10571
+ *
10572
+ * @module phaseState/invalidate
10573
+ */
10574
+ /**
10575
+ * Compute invalidation inputs and apply cascade for a single meta.
10576
+ *
10577
+ * @param meta - Current meta.json content with existing _phaseState.
10578
+ * @param scopeFiles - Sorted file list from scope.
10579
+ * @param config - MetaConfig for architectEvery.
10580
+ * @param node - MetaNode for archive access.
10581
+ * @param crossRefMetas - Map of cross-ref owner paths to their current _content.
10582
+ * @param archiveCrossRefContent - Map of cross-ref owner paths to their archived _content.
10583
+ * @returns Updated phase state and invalidation details.
10584
+ */
10585
+ async function computeInvalidation(meta, scopeFiles, config, node, crossRefMetas, archiveCrossRefContent) {
10586
+ let phaseState = meta._phaseState ?? {
10587
+ architect: 'fresh',
10588
+ builder: 'fresh',
10589
+ critic: 'fresh',
10590
+ };
10591
+ // ── Architect-level inputs ──
10592
+ const structureHash = computeStructureHash(scopeFiles);
10593
+ const structureChanged = structureHash !== meta._structureHash;
10594
+ const latestArchive = await readLatestArchive(node.metaPath);
10595
+ const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
10596
+ // _architect change: compare current vs. archive
10597
+ const architectChanged = latestArchive
10598
+ ? (meta._architect ?? '') !== (latestArchive._architect ?? '')
10599
+ : Boolean(meta._architect);
10600
+ // _crossRefs declaration change
10601
+ const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
10602
+ const archiveRefs = (latestArchive?._crossRefs ?? [])
10603
+ .slice()
10604
+ .sort()
10605
+ .join(',');
10606
+ const crossRefsDeclChanged = latestArchive
10607
+ ? currentRefs !== archiveRefs
10608
+ : currentRefs.length > 0;
10609
+ const architectInvalidators = [];
10610
+ if (structureChanged) {
10611
+ if (meta._state !== undefined) {
10612
+ // Progressive entity: new files → builder only (cursor handles incremental)
10613
+ phaseState = invalidateBuilder(phaseState);
10614
+ }
10615
+ else {
10616
+ architectInvalidators.push('structureHash');
10617
+ }
10618
+ }
10619
+ if (steerChanged)
10620
+ architectInvalidators.push('steer');
10621
+ if (architectChanged)
10622
+ architectInvalidators.push('_architect');
10623
+ if (crossRefsDeclChanged)
10624
+ architectInvalidators.push('_crossRefs');
10625
+ if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
10626
+ architectInvalidators.push('architectEvery');
10627
+ }
10628
+ // First-run check: no _builder means architect must run
10629
+ const firstRun = !meta._builder;
10630
+ if (architectInvalidators.length > 0 || firstRun) {
10631
+ phaseState = invalidateArchitect(phaseState);
10632
+ }
10633
+ // ── Builder-level inputs ──
10634
+ // Scope file mtime check — if any file newer than _generatedAt
10635
+ const scopeMtimeMax = null;
10636
+ // Note: actual mtime check is done by the caller or via isStale;
10637
+ // here we just detect cross-ref content changes for the cascade.
10638
+ // Cross-ref _content change (builder-invalidating)
10639
+ let crossRefContentChanged = false;
10640
+ return {
10641
+ phaseState,
10642
+ architectInvalidators,
10643
+ stalenessInputs: {
10644
+ structureHash,
10645
+ steerChanged,
10646
+ architectChanged,
10647
+ crossRefsDeclChanged,
10648
+ scopeMtimeMax,
10649
+ crossRefContentChanged,
10650
+ },
10651
+ structureHash,
10652
+ steerChanged,
10653
+ };
10654
+ }
10655
+
10656
+ /**
10657
+ * Weighted staleness formula for candidate selection.
10658
+ *
10659
+ * effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
10660
+ *
10661
+ * @module scheduling/weightedFormula
10662
+ */
10663
+ /**
10664
+ * Compute effective staleness for a set of candidates.
10665
+ *
10666
+ * Normalizes depths so the minimum becomes 0, then applies the formula:
10667
+ * effectiveStaleness = actualStaleness * (normalizedDepth + 1) ^ (depthWeight * emphasis)
10668
+ *
10669
+ * Per-meta _emphasis (default 1) multiplies depthWeight, allowing individual
10670
+ * metas to tune how much their tree position affects scheduling.
10671
+ *
10672
+ * @param candidates - Array of \{ node, meta, actualStaleness \}.
10673
+ * @param depthWeight - Exponent for depth weighting (0 = pure staleness).
10674
+ * @returns Same array with effectiveStaleness computed.
10675
+ */
10676
+ function computeEffectiveStaleness(candidates, depthWeight) {
10677
+ if (candidates.length === 0)
10678
+ return [];
10679
+ // Get depth for each candidate: use _depth override or tree depth
10680
+ const depths = candidates.map((c) => c.meta._depth ?? c.node.treeDepth);
10681
+ // Normalize: shift so minimum becomes 0
10682
+ const minDepth = Math.min(...depths);
10683
+ const normalizedDepths = depths.map((d) => Math.max(0, d - minDepth));
10684
+ return candidates.map((c, i) => {
10685
+ const emphasis = c.meta._emphasis ?? 1;
10686
+ return {
10687
+ ...c,
10688
+ effectiveStaleness: c.actualStaleness *
10689
+ Math.pow(normalizedDepths[i] + 1, depthWeight * emphasis),
10690
+ };
10691
+ });
10692
+ }
10693
+
10686
10694
  /**
10687
10695
  * Corpus-wide phase scheduler.
10688
10696
  *
@@ -10695,18 +10703,30 @@ function derivePhaseState(meta, inputs) {
10695
10703
  /**
10696
10704
  * Build phase candidates from listMetas entries.
10697
10705
  *
10698
- * Derives phase state and auto-retries failed phases for each entry.
10706
+ * Derives phase state, auto-retries failed phases, and applies Tier 1
10707
+ * cheap-invalidation (no I/O) for metas with persisted _phaseState.
10699
10708
  * Used by orchestratePhase, queue route, and status route.
10700
10709
  */
10701
- function buildPhaseCandidates(entries) {
10702
- return entries.map((entry) => ({
10703
- node: entry.node,
10704
- meta: entry.meta,
10705
- phaseState: retryAllFailed(derivePhaseState(entry.meta)),
10706
- actualStaleness: entry.stalenessSeconds,
10707
- locked: entry.locked,
10708
- disabled: entry.disabled,
10709
- }));
10710
+ function buildPhaseCandidates(entries, architectEvery) {
10711
+ return entries.map((entry) => {
10712
+ let ps = retryAllFailed(derivePhaseState(entry.meta));
10713
+ // Tier 1 cheap invalidation for metas with persisted _phaseState
10714
+ if (entry.meta._phaseState) {
10715
+ const needsArchitect = !entry.meta._builder ||
10716
+ (entry.meta._synthesisCount ?? 0) >= architectEvery;
10717
+ if (needsArchitect && ps.architect === 'fresh') {
10718
+ ps = { architect: 'pending', builder: 'stale', critic: 'stale' };
10719
+ }
10720
+ }
10721
+ return {
10722
+ node: entry.node,
10723
+ meta: entry.meta,
10724
+ phaseState: ps,
10725
+ actualStaleness: entry.stalenessSeconds,
10726
+ locked: entry.locked,
10727
+ disabled: entry.disabled,
10728
+ };
10729
+ });
10710
10730
  }
10711
10731
  /**
10712
10732
  * Rank all eligible phase candidates by priority.
@@ -10769,6 +10789,124 @@ function selectPhaseCandidate(metas, depthWeight) {
10769
10789
  return rankPhaseCandidates(metas, depthWeight)[0] ?? null;
10770
10790
  }
10771
10791
 
10792
+ /**
10793
+ * Shared error utilities.
10794
+ *
10795
+ * @module errors
10796
+ */
10797
+ /**
10798
+ * Wrap an unknown caught value into a MetaError.
10799
+ *
10800
+ * @param step - Which synthesis step failed.
10801
+ * @param err - The caught error value.
10802
+ * @param code - Error classification code.
10803
+ * @returns A structured MetaError.
10804
+ */
10805
+ function toMetaError(step, err, code = 'FAILED') {
10806
+ return {
10807
+ step,
10808
+ code,
10809
+ message: err instanceof Error ? err.message : String(err),
10810
+ };
10811
+ }
10812
+
10813
+ /**
10814
+ * Parse subprocess outputs for each synthesis step.
10815
+ *
10816
+ * - Architect: returns text \> _builder
10817
+ * - Builder: returns JSON \> _content + structured fields
10818
+ * - Critic: returns text \> _feedback
10819
+ *
10820
+ * @module orchestrator/parseOutput
10821
+ */
10822
+ /**
10823
+ * Parse architect output. The architect returns a task brief as text.
10824
+ *
10825
+ * @param output - Raw subprocess output.
10826
+ * @returns The task brief string.
10827
+ */
10828
+ function parseArchitectOutput(output) {
10829
+ return output.trim();
10830
+ }
10831
+ /**
10832
+ * Parse builder output. The builder returns JSON with _content and optional fields.
10833
+ *
10834
+ * Attempts JSON parse first. If that fails, treats the entire output as _content.
10835
+ *
10836
+ * @param output - Raw subprocess output.
10837
+ * @returns Parsed builder output with content and structured fields.
10838
+ */
10839
+ function parseBuilderOutput(output) {
10840
+ const trimmed = output.trim();
10841
+ // Strategy 1: Try to parse the entire output as JSON directly
10842
+ const direct = tryParseJson(trimmed);
10843
+ if (direct)
10844
+ return direct;
10845
+ // Strategy 2: Try all fenced code blocks (last match first — models often narrate then output)
10846
+ const fencePattern = /```(?:json)?\s*([\s\S]*?)```/g;
10847
+ const fenceMatches = [];
10848
+ let match;
10849
+ while ((match = fencePattern.exec(trimmed)) !== null) {
10850
+ fenceMatches.push(match[1].trim());
10851
+ }
10852
+ // Try last fence first (most likely to be the actual output)
10853
+ for (let i = fenceMatches.length - 1; i >= 0; i--) {
10854
+ const result = tryParseJson(fenceMatches[i]);
10855
+ if (result)
10856
+ return result;
10857
+ }
10858
+ // Strategy 3: Find outermost { ... } braces
10859
+ const firstBrace = trimmed.indexOf('{');
10860
+ const lastBrace = trimmed.lastIndexOf('}');
10861
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
10862
+ const result = tryParseJson(trimmed.substring(firstBrace, lastBrace + 1));
10863
+ if (result)
10864
+ return result;
10865
+ }
10866
+ // Fallback: treat entire output as content
10867
+ return { content: trimmed, fields: {} };
10868
+ }
10869
+ /** Try to parse a string as JSON and extract builder output fields. */
10870
+ function tryParseJson(str) {
10871
+ try {
10872
+ const raw = JSON.parse(str);
10873
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
10874
+ return null;
10875
+ }
10876
+ const parsed = raw;
10877
+ // Extract _content
10878
+ const content = typeof parsed['_content'] === 'string'
10879
+ ? parsed['_content']
10880
+ : typeof parsed['content'] === 'string'
10881
+ ? parsed['content']
10882
+ : null;
10883
+ if (content === null)
10884
+ return null;
10885
+ // Extract _state (the ONLY underscore key the builder is allowed to set)
10886
+ const state = '_state' in parsed ? parsed['_state'] : undefined;
10887
+ // Extract non-underscore fields
10888
+ const fields = {};
10889
+ for (const [key, value] of Object.entries(parsed)) {
10890
+ if (!key.startsWith('_') && key !== 'content') {
10891
+ fields[key] = value;
10892
+ }
10893
+ }
10894
+ return { content, fields, ...(state !== undefined ? { state } : {}) };
10895
+ }
10896
+ catch {
10897
+ return null;
10898
+ }
10899
+ }
10900
+ /**
10901
+ * Parse critic output. The critic returns evaluation text.
10902
+ *
10903
+ * @param output - Raw subprocess output.
10904
+ * @returns The feedback string.
10905
+ */
10906
+ function parseCriticOutput(output) {
10907
+ return output.trim();
10908
+ }
10909
+
10772
10910
  /**
10773
10911
  * Per-phase executors for the phase-state machine.
10774
10912
  *
@@ -11027,7 +11165,7 @@ async function orchestratePhase(config, executor, watcher, targetPath, onProgres
11027
11165
  if (metaResult.entries.length === 0)
11028
11166
  return { executed: false };
11029
11167
  // Build candidates with phase state (including invalidation + auto-retry)
11030
- const candidates = buildPhaseCandidates(metaResult.entries);
11168
+ const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
11031
11169
  // Select best phase candidate
11032
11170
  const winner = selectPhaseCandidate(candidates, config.depthWeight);
11033
11171
  if (!winner) {
@@ -11702,46 +11840,6 @@ function buildMetaRules(config) {
11702
11840
  },
11703
11841
  renderAs: 'md',
11704
11842
  },
11705
- {
11706
- name: 'meta-config',
11707
- description: 'jeeves-meta configuration file',
11708
- match: {
11709
- properties: {
11710
- file: {
11711
- properties: {
11712
- path: {
11713
- type: 'string',
11714
- glob: '**/jeeves-meta{.config.json,/config.json}',
11715
- },
11716
- },
11717
- },
11718
- },
11719
- },
11720
- schema: ['base', { properties: { domains: { set: ['meta-config'] } } }],
11721
- render: {
11722
- frontmatter: [
11723
- 'watcherUrl',
11724
- 'gatewayUrl',
11725
- 'architectEvery',
11726
- 'depthWeight',
11727
- 'maxArchive',
11728
- 'maxLines',
11729
- ],
11730
- body: [
11731
- {
11732
- path: 'json.defaultArchitect',
11733
- heading: 2,
11734
- label: 'Default Architect Prompt',
11735
- },
11736
- {
11737
- path: 'json.defaultCritic',
11738
- heading: 2,
11739
- label: 'Default Critic Prompt',
11740
- },
11741
- ],
11742
- },
11743
- renderAs: 'md',
11744
- },
11745
11843
  ];
11746
11844
  }
11747
11845
  /**
@@ -11985,13 +12083,15 @@ class Scheduler {
11985
12083
  queue;
11986
12084
  logger;
11987
12085
  watcher;
12086
+ cache;
11988
12087
  registrar = null;
11989
12088
  currentExpression;
11990
- constructor(config, queue, logger, watcher) {
12089
+ constructor(config, queue, logger, watcher, cache) {
11991
12090
  this.config = config;
11992
12091
  this.queue = queue;
11993
12092
  this.logger = logger;
11994
12093
  this.watcher = watcher;
12094
+ this.cache = cache;
11995
12095
  this.currentExpression = config.schedule;
11996
12096
  }
11997
12097
  /** Set the rule registrar for watcher restart detection. */
@@ -12108,8 +12208,8 @@ class Scheduler {
12108
12208
  */
12109
12209
  async discoverNextPhase() {
12110
12210
  try {
12111
- const result = await listMetas(this.config, this.watcher);
12112
- const candidates = buildPhaseCandidates(result.entries);
12211
+ const result = await this.cache.get(this.config, this.watcher);
12212
+ const candidates = buildPhaseCandidates(result.entries, this.config.architectEvery);
12113
12213
  const winner = selectPhaseCandidate(candidates, this.config.depthWeight);
12114
12214
  if (!winner)
12115
12215
  return null;
@@ -12480,7 +12580,7 @@ function registerMetasUpdateRoute(app, deps) {
12480
12580
  const metaDir = resolveMetaDir(targetPath);
12481
12581
  let meta;
12482
12582
  try {
12483
- meta = (await readMetaJson(metaDir));
12583
+ meta = await readMetaJson(metaDir);
12484
12584
  }
12485
12585
  catch {
12486
12586
  return reply.status(404).send({
@@ -12534,11 +12634,11 @@ function registerMetasUpdateRoute(app, deps) {
12534
12634
  */
12535
12635
  function registerPreviewRoute(app, deps) {
12536
12636
  app.get('/preview', async (request, reply) => {
12537
- const { config, watcher } = deps;
12637
+ const { config, watcher, cache } = deps;
12538
12638
  const query = request.query;
12539
12639
  let result;
12540
12640
  try {
12541
- result = await listMetas(config, watcher);
12641
+ result = await cache.get(config, watcher);
12542
12642
  }
12543
12643
  catch {
12544
12644
  return reply.status(503).send({
@@ -12558,40 +12658,24 @@ function registerPreviewRoute(app, deps) {
12558
12658
  }
12559
12659
  }
12560
12660
  else {
12561
- // Select stalest candidate
12562
- const stale = result.entries
12563
- .filter((e) => e.stalenessSeconds > 0)
12564
- .map((e) => ({
12565
- node: e.node,
12566
- meta: e.meta,
12567
- actualStaleness: e.stalenessSeconds,
12568
- }));
12569
- const stalestPath = discoverStalestPath(stale, config.depthWeight);
12570
- if (!stalestPath) {
12661
+ // Select best phase candidate
12662
+ const candidates = buildPhaseCandidates(result.entries, config.architectEvery);
12663
+ const winner = selectPhaseCandidate(candidates, config.depthWeight);
12664
+ if (!winner) {
12571
12665
  return { message: 'No stale metas found. Nothing to synthesize.' };
12572
12666
  }
12573
- targetNode = findNode(result.tree, stalestPath);
12667
+ targetNode = findNode(result.tree, winner.node.metaPath);
12574
12668
  }
12575
12669
  const meta = await readMetaJson(targetNode.metaPath);
12576
12670
  // Scope files
12577
12671
  const { scopeFiles } = await getScopeFiles(targetNode, watcher);
12578
- const structureHash = computeStructureHash(scopeFiles);
12672
+ // Compute invalidation inputs (DRY: reuse phaseState/invalidate logic)
12673
+ const invalidation = await computeInvalidation(meta, scopeFiles, config, targetNode);
12674
+ const { architectInvalidators, stalenessInputs } = invalidation;
12675
+ const { structureHash } = invalidation;
12579
12676
  const structureChanged = structureHash !== meta._structureHash;
12580
- const latestArchive = await readLatestArchive(targetNode.metaPath);
12581
- const steerChanged = hasSteerChanged(meta._steer, latestArchive?._steer, Boolean(latestArchive));
12582
- // _architect change detection
12583
- const architectChanged = latestArchive
12584
- ? (meta._architect ?? '') !== (latestArchive._architect ?? '')
12585
- : Boolean(meta._architect);
12586
- // _crossRefs declaration change detection
12587
- const currentRefs = (meta._crossRefs ?? []).slice().sort().join(',');
12588
- const archiveRefs = (latestArchive?._crossRefs ?? [])
12589
- .slice()
12590
- .sort()
12591
- .join(',');
12592
- const crossRefsDeclChanged = latestArchive
12593
- ? currentRefs !== archiveRefs
12594
- : currentRefs.length > 0;
12677
+ const { steerChanged } = invalidation;
12678
+ const { architectChanged, crossRefsDeclChanged } = stalenessInputs;
12595
12679
  const architectTriggered = isArchitectTriggered(meta, structureChanged, steerChanged, config.architectEvery);
12596
12680
  // Delta files
12597
12681
  const deltaFiles = getDeltaFiles(meta._generatedAt, scopeFiles);
@@ -12616,30 +12700,6 @@ function registerPreviewRoute(app, deps) {
12616
12700
  });
12617
12701
  const owedPhase = getOwedPhase(phaseState);
12618
12702
  const priorityBand = getPriorityBand(phaseState);
12619
- // Architect invalidators
12620
- const architectInvalidators = [];
12621
- if (owedPhase === 'architect') {
12622
- if (structureChanged)
12623
- architectInvalidators.push('structureHash');
12624
- if (steerChanged)
12625
- architectInvalidators.push('steer');
12626
- if (architectChanged)
12627
- architectInvalidators.push('_architect');
12628
- if (crossRefsDeclChanged)
12629
- architectInvalidators.push('_crossRefs');
12630
- if ((meta._synthesisCount ?? 0) >= config.architectEvery) {
12631
- architectInvalidators.push('architectEvery');
12632
- }
12633
- }
12634
- // Staleness inputs
12635
- const stalenessInputs = {
12636
- structureHash,
12637
- steerChanged,
12638
- architectChanged,
12639
- crossRefsDeclChanged,
12640
- scopeMtimeMax: null,
12641
- crossRefContentChanged: false,
12642
- };
12643
12703
  return {
12644
12704
  path: targetNode.metaPath,
12645
12705
  staleness: {
@@ -12713,8 +12773,8 @@ function registerQueueRoutes(app, deps) {
12713
12773
  // ranked by scheduler priority (computed on read, not persisted)
12714
12774
  let automatic = [];
12715
12775
  try {
12716
- const metaResult = await listMetas(deps.config, deps.watcher);
12717
- const candidates = buildPhaseCandidates(metaResult.entries);
12776
+ const metaResult = await deps.cache.get(deps.config, deps.watcher);
12777
+ const candidates = buildPhaseCandidates(metaResult.entries, deps.config.architectEvery);
12718
12778
  const ranked = rankPhaseCandidates(candidates, deps.config.depthWeight);
12719
12779
  automatic = ranked.map((c) => ({
12720
12780
  path: c.node.metaPath,
@@ -12856,7 +12916,7 @@ function registerSeedRoute(app, deps) {
12856
12916
  * @module constants
12857
12917
  */
12858
12918
  /** Default HTTP port for the jeeves-meta service. */
12859
- const DEFAULT_PORT = 1938;
12919
+ const DEFAULT_PORT = META_COMPONENT.defaultPort;
12860
12920
  /** Default port as a string (for Commander CLI defaults). */
12861
12921
  const DEFAULT_PORT_STR = String(DEFAULT_PORT);
12862
12922
  /** Service name identifier. */
@@ -12937,7 +12997,7 @@ function registerStatusRoute(app, deps) {
12937
12997
  name: SERVICE_NAME,
12938
12998
  version: SERVICE_VERSION,
12939
12999
  getHealth: async () => {
12940
- const { config, queue, scheduler, stats, watcher } = deps;
13000
+ const { config, queue, scheduler, stats, watcher, cache } = deps;
12941
13001
  // On-demand dependency checks
12942
13002
  const [watcherHealth, gatewayHealth] = await Promise.all([
12943
13003
  checkWatcher(config.watcherUrl),
@@ -12951,7 +13011,7 @@ function registerStatusRoute(app, deps) {
12951
13011
  };
12952
13012
  let nextPhase = null;
12953
13013
  try {
12954
- const metaResult = await listMetas(config, watcher);
13014
+ const metaResult = await cache.get(config, watcher);
12955
13015
  // Count raw phase states (before retry) for display
12956
13016
  for (const entry of metaResult.entries) {
12957
13017
  const ps = derivePhaseState(entry.meta);
@@ -12960,7 +13020,7 @@ function registerStatusRoute(app, deps) {
12960
13020
  }
12961
13021
  }
12962
13022
  // Build candidates (with auto-retry) for scheduling
12963
- const candidates = buildPhaseCandidates(metaResult.entries);
13023
+ const candidates = buildPhaseCandidates(metaResult.entries, config.architectEvery);
12964
13024
  // Find next phase candidate
12965
13025
  const winner = selectPhaseCandidate(candidates, config.depthWeight);
12966
13026
  if (winner) {
@@ -13023,7 +13083,7 @@ const synthesizeBodySchema = z.object({
13023
13083
  function registerSynthesizeRoute(app, deps) {
13024
13084
  app.post('/synthesize', async (request, reply) => {
13025
13085
  const body = synthesizeBodySchema.parse(request.body);
13026
- const { config, watcher, queue } = deps;
13086
+ const { config, watcher, queue, cache } = deps;
13027
13087
  if (body.path) {
13028
13088
  // Path-targeted trigger: create override entry
13029
13089
  const targetPath = resolveMetaDir(body.path);
@@ -13060,7 +13120,7 @@ function registerSynthesizeRoute(app, deps) {
13060
13120
  // Corpus-wide trigger: discover stalest candidate
13061
13121
  let result;
13062
13122
  try {
13063
- result = await listMetas(config, watcher);
13123
+ result = await cache.get(config, watcher);
13064
13124
  }
13065
13125
  catch {
13066
13126
  return reply.status(503).send({
@@ -13068,20 +13128,15 @@ function registerSynthesizeRoute(app, deps) {
13068
13128
  message: 'Watcher unreachable — cannot discover candidates',
13069
13129
  });
13070
13130
  }
13071
- const stale = result.entries
13072
- .filter((e) => e.stalenessSeconds > 0 && !e.disabled)
13073
- .map((e) => ({
13074
- node: e.node,
13075
- meta: e.meta,
13076
- actualStaleness: e.stalenessSeconds,
13077
- }));
13078
- const stalest = discoverStalestPath(stale, config.depthWeight);
13079
- if (!stalest) {
13131
+ const candidates = buildPhaseCandidates(result.entries, config.architectEvery);
13132
+ const winner = selectPhaseCandidate(candidates, config.depthWeight);
13133
+ if (!winner) {
13080
13134
  return reply.code(200).send({
13081
13135
  status: 'skipped',
13082
13136
  message: 'No stale metas found. Nothing to synthesize.',
13083
13137
  });
13084
13138
  }
13139
+ const stalest = winner.node.metaPath;
13085
13140
  const enqueueResult = queue.enqueue(stalest);
13086
13141
  return reply.code(202).send({
13087
13142
  status: 'accepted',
@@ -13182,6 +13237,18 @@ function createServer(options) {
13182
13237
  // Fastify 5 requires `loggerInstance` for external pino loggers
13183
13238
  const app = Fastify({
13184
13239
  loggerInstance: options.logger,
13240
+ requestTimeout: 30_000,
13241
+ });
13242
+ // Readiness gate: return 503 while service is initializing
13243
+ app.addHook('onRequest', async (request, reply) => {
13244
+ if (options.deps.ready)
13245
+ return;
13246
+ const url = request.url;
13247
+ if (url === '/config' || url.startsWith('/config/apply'))
13248
+ return;
13249
+ return reply
13250
+ .status(503)
13251
+ .send({ status: 'starting', message: 'Service initializing' });
13185
13252
  });
13186
13253
  registerRoutes(app, options.deps);
13187
13254
  return app;
@@ -13357,8 +13424,9 @@ async function startService(config, configPath) {
13357
13424
  lastCycleAt: null,
13358
13425
  };
13359
13426
  const queue = new SynthesisQueue(logger);
13427
+ const cache = new MetaCache();
13360
13428
  // Scheduler (needs watcher for discovery)
13361
- const scheduler = new Scheduler(config, queue, logger, watcher);
13429
+ const scheduler = new Scheduler(config, queue, logger, watcher, cache);
13362
13430
  const routeDeps = {
13363
13431
  config,
13364
13432
  logger,
@@ -13366,6 +13434,8 @@ async function startService(config, configPath) {
13366
13434
  watcher,
13367
13435
  scheduler,
13368
13436
  stats,
13437
+ cache,
13438
+ ready: false,
13369
13439
  executor,
13370
13440
  configPath,
13371
13441
  };
@@ -13416,6 +13486,9 @@ async function startService(config, configPath) {
13416
13486
  }
13417
13487
  await progress.report(evt);
13418
13488
  }, logger);
13489
+ // Invalidate cache only when a phase was actually executed
13490
+ if (result.executed)
13491
+ cache.invalidate();
13419
13492
  const durationMs = Date.now() - startMs;
13420
13493
  if (!result.executed) {
13421
13494
  logger.debug({ path: ownerPath }, 'Phase skipped (fully fresh or locked)');
@@ -13479,9 +13552,13 @@ async function startService(config, configPath) {
13479
13552
  scheduler.setRegistrar(registrar);
13480
13553
  routeDeps.registrar = registrar;
13481
13554
  void registrar.register().then(() => {
13555
+ routeDeps.ready = true;
13482
13556
  if (registrar.isRegistered) {
13483
13557
  void verifyRuleApplication(watcher, logger);
13484
13558
  }
13559
+ }, () => {
13560
+ // Registration failed after max retries — mark ready anyway
13561
+ routeDeps.ready = true;
13485
13562
  });
13486
13563
  // Periodic watcher health check (independent of scheduler)
13487
13564
  const healthCheck = new WatcherHealthCheck({
@@ -13681,11 +13758,11 @@ function registerCustomCliCommands(program) {
13681
13758
  * Parsed jeeves-meta component descriptor.
13682
13759
  */
13683
13760
  const metaDescriptor = jeevesComponentDescriptorSchema.parse({
13684
- name: 'meta',
13761
+ name: META_COMPONENT.name,
13685
13762
  version: SERVICE_VERSION,
13686
- servicePackage: '@karmaniverous/jeeves-meta',
13687
- pluginPackage: '@karmaniverous/jeeves-meta-openclaw',
13688
- defaultPort: 1938,
13763
+ servicePackage: META_COMPONENT.servicePackage,
13764
+ pluginPackage: META_COMPONENT.pluginPackage,
13765
+ defaultPort: META_COMPONENT.defaultPort,
13689
13766
  // The runtime Zod custom validator only checks for a .parse() method.
13690
13767
  // Use unknown cast to bridge the Zod v4 (service) → v3 (core SDK) type gap.
13691
13768
  configSchema: serviceConfigSchema,