@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.
- package/dist/archive/index.d.ts +10 -0
- package/dist/archive/listArchive.d.ts +12 -0
- package/dist/archive/prune.d.ts +14 -0
- package/dist/archive/readArchive.d.ts +30 -0
- package/dist/archive/readLatest.d.ts +13 -0
- package/dist/archive/snapshot.d.ts +17 -0
- package/dist/bootstrap.d.ts +15 -0
- package/dist/cache.d.ts +22 -0
- package/dist/cli/jeeves-meta/architect.md +17 -0
- package/dist/cli/jeeves-meta/index.js +811 -734
- package/dist/cli.d.ts +10 -0
- package/dist/configHotReload.d.ts +30 -0
- package/dist/configLoader.d.ts +37 -0
- package/dist/constants.d.ts +13 -0
- package/dist/customCliCommands.d.ts +13 -0
- package/dist/descriptor.d.ts +19 -0
- package/dist/discovery/buildMinimalNode.d.ts +22 -0
- package/dist/discovery/computeSummary.d.ts +17 -0
- package/dist/discovery/discoverMetas.d.ts +19 -0
- package/dist/discovery/index.d.ts +11 -0
- package/dist/discovery/listMetas.d.ts +63 -0
- package/dist/discovery/ownershipTree.d.ts +25 -0
- package/dist/discovery/scope.d.ts +47 -0
- package/dist/discovery/types.d.ts +25 -0
- package/dist/ema.d.ts +14 -0
- package/dist/errors.d.ts +15 -0
- package/dist/escapeGlob.d.ts +23 -0
- package/dist/executor/GatewayExecutor.d.ts +48 -0
- package/dist/executor/SpawnAbortedError.d.ts +9 -0
- package/dist/executor/SpawnTimeoutError.d.ts +13 -0
- package/dist/executor/index.d.ts +8 -0
- package/dist/index.d.ts +34 -1660
- package/dist/index.js +1434 -1767
- package/dist/interfaces/MetaContext.d.ts +36 -0
- package/dist/interfaces/MetaExecutor.d.ts +46 -0
- package/dist/interfaces/WatcherClient.d.ts +75 -0
- package/dist/interfaces/index.d.ts +8 -0
- package/dist/lock.d.ts +70 -0
- package/dist/logger/index.d.ts +27 -0
- package/dist/mtimeFilter.d.ts +26 -0
- package/dist/normalizePath.d.ts +6 -0
- package/dist/orchestrator/buildTask.d.ts +38 -0
- package/dist/orchestrator/contextPackage.d.ts +30 -0
- package/dist/orchestrator/index.d.ts +10 -0
- package/dist/orchestrator/orchestratePhase.d.ts +38 -0
- package/dist/orchestrator/parseOutput.d.ts +41 -0
- package/dist/orchestrator/runPhase.d.ts +40 -0
- package/dist/phaseState/derivePhaseState.d.ts +41 -0
- package/dist/phaseState/index.d.ts +9 -0
- package/dist/phaseState/invalidate.d.ts +41 -0
- package/dist/phaseState/phaseScheduler.d.ts +57 -0
- package/dist/phaseState/phaseTransitions.d.ts +83 -0
- package/dist/progress/index.d.ts +38 -0
- package/dist/prompts/architect.md +17 -0
- package/dist/prompts/index.d.ts +15 -0
- package/dist/queue/index.d.ts +131 -0
- package/dist/readMetaJson.d.ts +17 -0
- package/dist/routes/__testUtils.d.ts +37 -0
- package/dist/routes/config.d.ts +11 -0
- package/dist/routes/configApply.d.ts +13 -0
- package/dist/routes/index.d.ts +50 -0
- package/dist/routes/metas.d.ts +9 -0
- package/dist/routes/metasUpdate.d.ts +11 -0
- package/dist/routes/preview.d.ts +8 -0
- package/dist/routes/queue.d.ts +13 -0
- package/dist/routes/seed.d.ts +8 -0
- package/dist/routes/status.d.ts +13 -0
- package/dist/routes/synthesize.d.ts +12 -0
- package/dist/routes/unlock.d.ts +8 -0
- package/dist/rules/healthCheck.d.ts +36 -0
- package/dist/rules/index.d.ts +39 -0
- package/dist/rules/verify.d.ts +22 -0
- package/dist/scheduler/index.d.ts +66 -0
- package/dist/scheduling/index.d.ts +7 -0
- package/dist/scheduling/staleness.d.ts +68 -0
- package/dist/scheduling/weightedFormula.d.ts +38 -0
- package/dist/schema/config.d.ts +54 -0
- package/dist/schema/error.d.ts +6 -0
- package/dist/schema/index.d.ts +8 -0
- package/dist/schema/meta.d.ts +71 -0
- package/dist/seed/autoSeed.d.ts +30 -0
- package/dist/seed/createMeta.d.ts +38 -0
- package/dist/seed/index.d.ts +7 -0
- package/dist/server.d.ts +24 -0
- package/dist/shutdown/index.d.ts +33 -0
- package/dist/structureHash.d.ts +15 -0
- package/dist/watcher-client/HttpWatcherClient.d.ts +38 -0
- package/dist/watcher-client/index.d.ts +6 -0
- 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
|
|
1184
|
+
(function (exports) {
|
|
1185
1185
|
var RetryOperation = requireRetry_operation();
|
|
1186
1186
|
|
|
1187
|
-
exports
|
|
1188
|
-
var timeouts = exports
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4281
|
+
exports = module.exports = {};
|
|
4282
4282
|
|
|
4283
4283
|
// The actual regexps go on exports.re
|
|
4284
|
-
const re = exports
|
|
4285
|
-
const safeRe = exports
|
|
4286
|
-
const src = exports
|
|
4287
|
-
const safeSrc = exports
|
|
4288
|
-
const t = exports
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
7396
|
+
(function (module, exports) {
|
|
7339
7397
|
const commander = require$$0;
|
|
7340
7398
|
|
|
7341
|
-
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
|
|
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
|
|
7352
|
-
exports
|
|
7353
|
-
exports
|
|
7354
|
-
exports
|
|
7355
|
-
exports
|
|
7356
|
-
exports
|
|
7357
|
-
exports
|
|
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
|
|
7360
|
-
exports
|
|
7417
|
+
exports.createCommand = (name) => new commander.Command(name);
|
|
7418
|
+
exports.createOption = (flags, description) =>
|
|
7361
7419
|
new commander.Option(flags, description);
|
|
7362
|
-
exports
|
|
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
|
|
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
|
-
*
|
|
8433
|
-
*
|
|
8482
|
+
* Single source of truth consumed by both the service descriptor and
|
|
8483
|
+
* the OpenClaw plugin registration.
|
|
8434
8484
|
*/
|
|
8435
|
-
|
|
8436
|
-
|
|
8437
|
-
'
|
|
8438
|
-
'
|
|
8439
|
-
'
|
|
8440
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
8578
|
-
*
|
|
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
|
-
*
|
|
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
|
|
8622
|
-
* @returns
|
|
8556
|
+
* @param p - File path (may contain backslashes).
|
|
8557
|
+
* @returns Path with all backslashes replaced by forward slashes.
|
|
8623
8558
|
*/
|
|
8624
|
-
function
|
|
8625
|
-
|
|
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
|
-
*
|
|
9217
|
+
* In-memory cache for listMetas results with TTL and concurrent refresh guard.
|
|
9295
9218
|
*
|
|
9296
|
-
* @module
|
|
9219
|
+
* @module cache
|
|
9297
9220
|
*/
|
|
9298
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
9301
|
-
|
|
9302
|
-
|
|
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
|
|
9392
|
-
async
|
|
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:
|
|
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
|
-
|
|
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
|
|
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 ??
|
|
9448
|
-
|
|
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
|
-
|
|
9479
|
-
|
|
9480
|
-
|
|
9481
|
-
|
|
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
|
-
|
|
9484
|
-
return { output, tokens };
|
|
9647
|
+
unlinkSync(outputPath);
|
|
9485
9648
|
}
|
|
9486
|
-
|
|
9487
|
-
|
|
9488
|
-
unlinkSync(outputPath);
|
|
9489
|
-
}
|
|
9490
|
-
catch {
|
|
9491
|
-
/* cleanup best-effort */
|
|
9492
|
-
}
|
|
9649
|
+
catch {
|
|
9650
|
+
/* cleanup best-effort */
|
|
9493
9651
|
}
|
|
9494
9652
|
}
|
|
9495
|
-
|
|
9496
|
-
|
|
9497
|
-
|
|
9498
|
-
|
|
9499
|
-
|
|
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
|
-
|
|
9502
|
-
|
|
9503
|
-
|
|
9504
|
-
|
|
9505
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
|
|
10707
|
-
|
|
10708
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
12562
|
-
const
|
|
12563
|
-
|
|
12564
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
12581
|
-
const
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
13072
|
-
|
|
13073
|
-
|
|
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:
|
|
13761
|
+
name: META_COMPONENT.name,
|
|
13685
13762
|
version: SERVICE_VERSION,
|
|
13686
|
-
servicePackage:
|
|
13687
|
-
pluginPackage:
|
|
13688
|
-
defaultPort:
|
|
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,
|